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}