polydat_core/iteration/comprehension/optimize/finding.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `ReducibilityFinding` and related types — comprehension_forms.md
5//! §10.10.2.
6//!
7//! A finding represents either "no rewrite applies" (empty) or
8//! "this rule rewrites C into the witness AST C'." Findings
9//! also carry a [`ComplexityDelta`] declaring the improvement
10//! the rewrite achieves in compute and/or memory complexity.
11
12use serde::{Deserialize, Serialize};
13
14use crate::iteration::comprehension::ast::Comprehension;
15
16/// Identifier for each R-rule in the optimizer catalog
17/// (§10.2 + §10.10.3).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum RuleId {
20 /// Identity elimination: a singleton combinator or a trivial filter is dropped.
21 R0a,
22 /// Associativity flattening: nested unions or cartesians of one kind become one n-ary node.
23 R0b,
24 /// `order(Lex)` is a counter wrapper: it becomes a truncated lexicographic walk.
25 R1,
26 /// Push-down of a truncated order to an index-addressable child's closed form.
27 R2,
28 /// An untruncated lexicographic order commutes with a filter.
29 R3,
30 /// A filter distributes over a union.
31 R4,
32 /// A per-axis filter pushes down into a cartesian's axes.
33 R5,
34 /// A chain of filters folds into one.
35 R6,
36 /// A chain of orders folds into one when the inner is untruncated
37 /// and the outer strategy selects from its input's shape.
38 R7,
39 // The catalog ends at R7 (comprehension_forms.md §14.1); R8–R10
40 // name rewrites outside it, which no finding carries.
41 /// Range narrowing from a bounded predicate; outside the catalog.
42 R8,
43 /// Discrete-set substitution from an `in` predicate; outside the
44 /// catalog.
45 R9,
46 /// Monotonic-cutoff truncation; outside the catalog.
47 R10,
48}
49
50/// Output of the reducibility analyzer (comprehension_forms.md §10.10.2).
51#[derive(Debug, Clone, PartialEq)]
52pub struct ReducibilityFinding {
53 /// `Some` if a rewrite applies; `None` is the empty
54 /// finding (no rule fires).
55 pub reduction: Option<Reduction>,
56 /// Which rule fired (mirrors `reduction.rule` if it's a
57 /// Rewrite). Useful for logging / diagnostics.
58 pub rule: Option<RuleId>,
59 /// Asymptotic improvement of the witness over the input.
60 pub improvement: ComplexityDelta,
61}
62
63/// The rewrite carried by a non-empty finding.
64#[derive(Debug, Clone, PartialEq)]
65pub enum Reduction {
66 /// Replace the entire AST with `with` (whole-tree swap).
67 Replace {
68 /// The AST that replaces the whole input.
69 with: Comprehension,
70 },
71 /// Rewrite via a tagged R-rule. The `witness` is the new
72 /// AST; `rule` is the catalog identifier.
73 Rewrite {
74 /// The catalog rule that fired.
75 rule: RuleId,
76 /// The rewritten AST.
77 witness: Comprehension,
78 },
79}
80
81/// Strict-improvement vector along the (compute, memory)
82/// dimensions (comprehension_forms.md §10.10.2 and §10.10.3's catalog
83/// table).
84///
85/// A non-empty finding must have at least one dimension
86/// `Less` and the other ≤ `Equal`. The optimizer rejects
87/// findings that are `Equal` on both dimensions.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct ComplexityDelta {
90 /// How the compute cost of the witness compares to the input's.
91 pub compute_order: Ordering,
92 /// How its memory cost compares.
93 pub memory_order: Ordering,
94 /// Why the ordering holds.
95 pub rationale: &'static str,
96}
97
98/// Three-way asymptotic ordering for one complexity dimension.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum Ordering {
101 /// Asymptotically less.
102 Less,
103 /// Asymptotically the same.
104 Equal,
105 /// Asymptotically more.
106 Greater,
107}
108
109impl ComplexityDelta {
110 /// Rule reduces compute, no change to memory.
111 pub fn less_compute() -> Self {
112 Self {
113 compute_order: Ordering::Less,
114 memory_order: Ordering::Equal,
115 rationale: "strictly less compute",
116 }
117 }
118
119 /// Rule reduces memory, no change to compute.
120 pub fn less_memory() -> Self {
121 Self {
122 compute_order: Ordering::Equal,
123 memory_order: Ordering::Less,
124 rationale: "strictly less memory",
125 }
126 }
127
128 /// Rule reduces both compute and memory.
129 pub fn less_both() -> Self {
130 Self {
131 compute_order: Ordering::Less,
132 memory_order: Ordering::Less,
133 rationale: "strictly less compute and memory",
134 }
135 }
136
137 /// No change in either dimension. Used for the empty
138 /// finding; the optimizer never produces a `Reduction`
139 /// with this delta.
140 pub fn equal() -> Self {
141 Self {
142 compute_order: Ordering::Equal,
143 memory_order: Ordering::Equal,
144 rationale: "no asymptotic change",
145 }
146 }
147
148 /// `true` if at least one dimension is strictly Less and
149 /// the other is at most Equal. Per comprehension_forms.md §10.10.2 this is
150 /// the condition for a non-empty finding to be valid.
151 pub fn is_strict_improvement(&self) -> bool {
152 matches!(
153 (self.compute_order, self.memory_order),
154 (Ordering::Less, Ordering::Less)
155 | (Ordering::Less, Ordering::Equal)
156 | (Ordering::Equal, Ordering::Less)
157 )
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn delta_strict_improvement_classifier() {
167 assert!(ComplexityDelta::less_compute().is_strict_improvement());
168 assert!(ComplexityDelta::less_memory().is_strict_improvement());
169 assert!(ComplexityDelta::less_both().is_strict_improvement());
170 assert!(!ComplexityDelta::equal().is_strict_improvement());
171 }
172
173 #[test]
174 fn rule_id_round_trip_serde() {
175 let r = RuleId::R5;
176 let json = serde_json::to_string(&r).unwrap();
177 let back: RuleId = serde_json::from_str(&json).unwrap();
178 assert_eq!(r, back);
179 }
180}