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