Skip to main content

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}