Skip to main content

omena_transform_egg/
lawvere_analysis.rs

1//! Feature-gated Lawvere-style analysis data for optional e-graph execution.
2//!
3//! This module is not part of the default transform path; it documents the
4//! metadata carried when the `lawvere-saturation` experiment is enabled.
5
6use egg::{Analysis, DidMerge, EGraph, Extractor, Id, Language, RecExpr, Runner};
7use omena_lawvere::{
8    AbstractDomainTagV0, LawvereSaturationExecutionV0, summarize_lawvere_saturation_execution_v0,
9};
10
11use crate::{
12    CssRewriteLanguage, EggRewriteCandidateV0, EggRewriteExecutionV0, MdlExtractionCostV0,
13    blocked_execution, decide_egg_rewrite, rewrite_rules_for_pass,
14};
15
16#[derive(Debug, Default, Clone)]
17pub struct LawvereAnalysis;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct LawvereAnalysisDataV0 {
21    pub abstract_domain_tags: Vec<AbstractDomainTagV0>,
22    pub enode_count: usize,
23    pub contains_terminal_projection: bool,
24}
25
26impl LawvereAnalysisDataV0 {
27    fn from_tag(tag: AbstractDomainTagV0) -> Self {
28        Self {
29            abstract_domain_tags: vec![tag],
30            enode_count: 1,
31            contains_terminal_projection: tag == AbstractDomainTagV0::TerminalEmission,
32        }
33    }
34}
35
36impl Analysis<CssRewriteLanguage> for LawvereAnalysis {
37    type Data = LawvereAnalysisDataV0;
38
39    fn make(
40        egraph: &mut EGraph<CssRewriteLanguage, Self>,
41        enode: &CssRewriteLanguage,
42        _id: Id,
43    ) -> Self::Data {
44        let tag = match enode {
45            CssRewriteLanguage::Num(_)
46            | CssRewriteLanguage::Symbol(_)
47            | CssRewriteLanguage::Add(_)
48            | CssRewriteLanguage::Sub(_)
49            | CssRewriteLanguage::Mul(_)
50            | CssRewriteLanguage::Div(_)
51            | CssRewriteLanguage::Calc(_)
52            | CssRewriteLanguage::Unit(_) => AbstractDomainTagV0::TokenValue,
53            CssRewriteLanguage::Is(_)
54            | CssRewriteLanguage::Where(_)
55            | CssRewriteLanguage::List(_) => AbstractDomainTagV0::SelectorShape,
56        };
57        let mut data = LawvereAnalysisDataV0::from_tag(tag);
58        for child in enode.children() {
59            let child_data = &egraph[*child].data;
60            merge_domain_tags(
61                &mut data.abstract_domain_tags,
62                &child_data.abstract_domain_tags,
63            );
64            data.contains_terminal_projection |= child_data.contains_terminal_projection;
65        }
66        data
67    }
68
69    fn merge(&mut self, a: &mut Self::Data, b: Self::Data) -> DidMerge {
70        let before = a.clone();
71        merge_domain_tags(&mut a.abstract_domain_tags, &b.abstract_domain_tags);
72        a.enode_count = a.enode_count.max(b.enode_count);
73        a.contains_terminal_projection |= b.contains_terminal_projection;
74        DidMerge(before != *a, *a != b)
75    }
76
77    fn allow_ematching_cycles(&self) -> bool {
78        false
79    }
80}
81
82fn merge_domain_tags(target: &mut Vec<AbstractDomainTagV0>, source: &[AbstractDomainTagV0]) {
83    for tag in source {
84        if !target.contains(tag) {
85            target.push(*tag);
86        }
87    }
88    target.sort();
89}
90
91pub fn execute_egg_rewrite_with_lawvere_analysis(
92    candidate: EggRewriteCandidateV0,
93) -> (EggRewriteExecutionV0, LawvereSaturationExecutionV0) {
94    let decision = decide_egg_rewrite(candidate.clone());
95    if !decision.accepted {
96        let execution = blocked_execution(candidate.clone(), decision.blocked_reason);
97        let saturation =
98            summarize_lawvere_saturation_execution_v0(candidate.pass_id, 0, 0, 0, 0, false);
99        return (execution, saturation);
100    }
101
102    let expression = match candidate.before.parse::<RecExpr<CssRewriteLanguage>>() {
103        Ok(expression) => expression,
104        Err(_) => {
105            let execution = blocked_execution(
106                candidate.clone(),
107                Some("rewrite expression could not parse"),
108            );
109            let saturation =
110                summarize_lawvere_saturation_execution_v0(candidate.pass_id, 0, 0, 0, 0, false);
111            return (execution, saturation);
112        }
113    };
114    let Some(rules) = rewrite_rules_for_pass::<LawvereAnalysis>(candidate.pass_id) else {
115        let execution = blocked_execution(
116            candidate.clone(),
117            Some("pass is not managed by omena-transform-egg"),
118        );
119        let saturation =
120            summarize_lawvere_saturation_execution_v0(candidate.pass_id, 0, 0, 0, 0, false);
121        return (execution, saturation);
122    };
123
124    let iteration_limit = 8;
125    let runner = Runner::default()
126        .with_expr(&expression)
127        .with_iter_limit(iteration_limit)
128        .run(rules.as_slice());
129    let root = runner.roots[0];
130    let extractor = Extractor::new(&runner.egraph, MdlExtractionCostV0::default_ast_size());
131    let (_, extracted) = extractor.find_best(root);
132    let after = extracted.to_string();
133    let after_matches_candidate = after == candidate.after;
134    let iteration_count = runner.iterations.len();
135    let eclass_count = runner.egraph.number_of_classes();
136    let enode_count = runner.egraph.total_size();
137    let saturation = summarize_lawvere_saturation_execution_v0(
138        candidate.pass_id,
139        iteration_limit,
140        iteration_count,
141        eclass_count,
142        enode_count,
143        after_matches_candidate,
144    );
145
146    let execution = EggRewriteExecutionV0 {
147        schema_version: "0",
148        product: "omena-transform-egg.execution",
149        pass_id: candidate.pass_id,
150        accepted: after_matches_candidate,
151        blocked_reason: (!after_matches_candidate)
152            .then_some("lawvere analysis extraction did not match candidate output"),
153        before: candidate.before,
154        after,
155        expected_after: candidate.after,
156        after_matches_candidate,
157        engine: "egg+lawvere-analysis",
158        iteration_limit,
159        iteration_count,
160        eclass_count,
161        enode_count,
162        mdl_bits: None,
163        mdl_residual_bits: None,
164        mdl_unit: None,
165    };
166    (execution, saturation)
167}
168
169#[cfg(test)]
170mod tests {
171    use omena_transform_cst::TransformPassKind;
172
173    use crate::{EggRewriteCandidateV0, EggRewriteProofV0};
174
175    use super::*;
176
177    #[test]
178    fn lawvere_analysis_fills_parallel_egg_analysis_slot() {
179        let (execution, saturation) =
180            execute_egg_rewrite_with_lawvere_analysis(EggRewriteCandidateV0 {
181                pass_id: TransformPassKind::CalcReduction.id(),
182                before: "(calc (+ (unit 1 px) (unit 2 px)))".to_string(),
183                after: "(unit 3 px)".to_string(),
184                proof: EggRewriteProofV0 {
185                    specificity_preserved: false,
186                    computed_value_preserved: true,
187                    provenance_preserved: true,
188                    cascade_safe_witness: "same-unit calc arithmetic preserves computed value"
189                        .to_string(),
190                },
191            });
192
193        assert!(execution.accepted);
194        assert_eq!(execution.engine, "egg+lawvere-analysis");
195        assert_eq!(saturation.schema_version, "0");
196        assert_eq!(saturation.layer_marker, "enriched-algebraic");
197        assert_eq!(saturation.analysis_slot, "LawvereAnalysis");
198        assert!(saturation.original_unit_analysis_path_preserved);
199        assert_eq!(saturation.differential_fixture_count, 10);
200    }
201}