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};
10use serde::Serialize;
11
12use crate::{
13    CssRewriteLanguage, EggRewriteCandidateV0, EggRewriteExecutionV0, MdlExtractionCostV0,
14    blocked_execution, decide_egg_rewrite, rewrite_rules_for_pass,
15};
16
17#[derive(Debug, Default, Clone)]
18pub struct LawvereAnalysis;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct LawvereAnalysisDataV0 {
23    pub abstract_domain_tags: Vec<AbstractDomainTagV0>,
24    pub enode_count: usize,
25    pub contains_terminal_projection: bool,
26    pub specificity_carrier: LawvereSpecificityCarrierV0,
27    pub computed_value_carrier: LawvereComputedValueCarrierV0,
28    pub var_state_carrier: LawvereVarStateCarrierV0,
29    pub provenance_carrier: LawvereProvenanceCarrierV0,
30}
31
32impl LawvereAnalysisDataV0 {
33    fn from_enode(tag: AbstractDomainTagV0, enode: &CssRewriteLanguage) -> Self {
34        Self {
35            abstract_domain_tags: vec![tag],
36            enode_count: 1,
37            contains_terminal_projection: tag == AbstractDomainTagV0::TerminalEmission,
38            specificity_carrier: LawvereSpecificityCarrierV0::from_enode(enode),
39            computed_value_carrier: LawvereComputedValueCarrierV0::from_enode(enode),
40            var_state_carrier: LawvereVarStateCarrierV0::from_enode(enode),
41            provenance_carrier: LawvereProvenanceCarrierV0::from_enode(enode),
42        }
43    }
44}
45
46#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct LawvereSpecificityCarrierV0 {
49    pub selector_context_count: usize,
50    pub selector_atom_count: usize,
51    pub zero_specificity_context_seen: bool,
52    pub selector_specificity_obligation_ready: bool,
53}
54
55impl LawvereSpecificityCarrierV0 {
56    fn from_enode(enode: &CssRewriteLanguage) -> Self {
57        match enode {
58            CssRewriteLanguage::Is(_) => Self {
59                selector_context_count: 1,
60                selector_atom_count: 0,
61                zero_specificity_context_seen: false,
62                selector_specificity_obligation_ready: true,
63            },
64            CssRewriteLanguage::Where(_) => Self {
65                selector_context_count: 1,
66                selector_atom_count: 0,
67                zero_specificity_context_seen: true,
68                selector_specificity_obligation_ready: true,
69            },
70            CssRewriteLanguage::List(_) => Self {
71                selector_context_count: 1,
72                selector_atom_count: 0,
73                zero_specificity_context_seen: false,
74                selector_specificity_obligation_ready: true,
75            },
76            _ => Self::default(),
77        }
78    }
79
80    fn merge_from(&mut self, other: &Self) {
81        self.selector_context_count = self
82            .selector_context_count
83            .saturating_add(other.selector_context_count);
84        self.selector_atom_count = self
85            .selector_atom_count
86            .saturating_add(other.selector_atom_count);
87        self.zero_specificity_context_seen |= other.zero_specificity_context_seen;
88        self.selector_specificity_obligation_ready |= other.selector_specificity_obligation_ready;
89    }
90}
91
92#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct LawvereComputedValueCarrierV0 {
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub exact_numeric_value: Option<i64>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub exact_unit: Option<String>,
99    pub exact_value_candidates: Vec<String>,
100    pub computed_value_obligation_ready: bool,
101    pub expression_kinds: Vec<&'static str>,
102}
103
104impl LawvereComputedValueCarrierV0 {
105    fn from_enode(enode: &CssRewriteLanguage) -> Self {
106        match enode {
107            CssRewriteLanguage::Num(value) => Self {
108                exact_numeric_value: Some(*value),
109                exact_unit: None,
110                exact_value_candidates: vec![value.to_string()],
111                computed_value_obligation_ready: true,
112                expression_kinds: vec!["numericLiteral"],
113            },
114            CssRewriteLanguage::Symbol(symbol) => Self {
115                exact_numeric_value: None,
116                exact_unit: Some(symbol.to_string()),
117                exact_value_candidates: Vec::new(),
118                computed_value_obligation_ready: false,
119                expression_kinds: vec!["symbolToken"],
120            },
121            CssRewriteLanguage::Calc(_) => Self {
122                exact_numeric_value: None,
123                exact_unit: None,
124                exact_value_candidates: Vec::new(),
125                computed_value_obligation_ready: true,
126                expression_kinds: vec!["calcExpression"],
127            },
128            CssRewriteLanguage::Unit(_) => Self {
129                exact_numeric_value: None,
130                exact_unit: None,
131                exact_value_candidates: Vec::new(),
132                computed_value_obligation_ready: true,
133                expression_kinds: vec!["unitExpression"],
134            },
135            CssRewriteLanguage::Add(_) => Self {
136                exact_numeric_value: None,
137                exact_unit: None,
138                exact_value_candidates: Vec::new(),
139                computed_value_obligation_ready: true,
140                expression_kinds: vec!["addExpression"],
141            },
142            CssRewriteLanguage::Sub(_) => Self {
143                exact_numeric_value: None,
144                exact_unit: None,
145                exact_value_candidates: Vec::new(),
146                computed_value_obligation_ready: true,
147                expression_kinds: vec!["subExpression"],
148            },
149            CssRewriteLanguage::Mul(_) => Self {
150                exact_numeric_value: None,
151                exact_unit: None,
152                exact_value_candidates: Vec::new(),
153                computed_value_obligation_ready: true,
154                expression_kinds: vec!["mulExpression"],
155            },
156            CssRewriteLanguage::Div(_) => Self {
157                exact_numeric_value: None,
158                exact_unit: None,
159                exact_value_candidates: Vec::new(),
160                computed_value_obligation_ready: true,
161                expression_kinds: vec!["divExpression"],
162            },
163            CssRewriteLanguage::Box1(_)
164            | CssRewriteLanguage::Box2(_)
165            | CssRewriteLanguage::Box3(_)
166            | CssRewriteLanguage::Box4(_) => Self {
167                exact_numeric_value: None,
168                exact_unit: None,
169                exact_value_candidates: Vec::new(),
170                computed_value_obligation_ready: true,
171                expression_kinds: vec!["boxShorthandExpression"],
172            },
173            _ => Self::default(),
174        }
175    }
176
177    fn merge_from(&mut self, other: &Self) {
178        self.computed_value_obligation_ready |= other.computed_value_obligation_ready;
179        merge_labels(&mut self.expression_kinds, &other.expression_kinds);
180        merge_strings(
181            &mut self.exact_value_candidates,
182            &other.exact_value_candidates,
183        );
184        self.exact_numeric_value = None;
185        if self.exact_unit != other.exact_unit {
186            self.exact_unit = None;
187        }
188    }
189}
190
191#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct LawvereVarStateCarrierV0 {
194    pub symbolic_reference_count: usize,
195    pub symbol_tokens: Vec<String>,
196    pub unresolved_var_reference_seen: bool,
197}
198
199impl LawvereVarStateCarrierV0 {
200    fn from_enode(enode: &CssRewriteLanguage) -> Self {
201        let CssRewriteLanguage::Symbol(symbol) = enode else {
202            return Self::default();
203        };
204        let symbol = symbol.to_string();
205        let unresolved_var_reference_seen = symbol.contains("--") || symbol.starts_with("var_");
206        Self {
207            symbolic_reference_count: 1,
208            symbol_tokens: vec![symbol],
209            unresolved_var_reference_seen,
210        }
211    }
212
213    fn merge_from(&mut self, other: &Self) {
214        self.symbolic_reference_count = self
215            .symbolic_reference_count
216            .saturating_add(other.symbolic_reference_count);
217        for symbol in &other.symbol_tokens {
218            if !self.symbol_tokens.contains(symbol) {
219                self.symbol_tokens.push(symbol.clone());
220            }
221        }
222        self.symbol_tokens.sort();
223        self.unresolved_var_reference_seen |= other.unresolved_var_reference_seen;
224    }
225}
226
227#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
228#[serde(rename_all = "camelCase")]
229pub struct LawvereProvenanceCarrierV0 {
230    pub enode_kinds: Vec<&'static str>,
231    pub provenance_obligation_ready: bool,
232}
233
234impl LawvereProvenanceCarrierV0 {
235    fn from_enode(enode: &CssRewriteLanguage) -> Self {
236        Self {
237            enode_kinds: vec![lawvere_enode_kind(enode)],
238            provenance_obligation_ready: true,
239        }
240    }
241
242    fn merge_from(&mut self, other: &Self) {
243        merge_labels(&mut self.enode_kinds, &other.enode_kinds);
244        self.provenance_obligation_ready |= other.provenance_obligation_ready;
245    }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
249#[serde(rename_all = "camelCase")]
250pub struct LawvereAnalysisCarrierWitnessV0 {
251    pub schema_version: &'static str,
252    pub product: &'static str,
253    pub feature_gate: &'static str,
254    pub claim_level: &'static str,
255    pub pass_id: &'static str,
256    pub specificity_carrier_ready: bool,
257    pub computed_value_carrier_ready: bool,
258    pub var_state_carrier_ready: bool,
259    pub provenance_carrier_ready: bool,
260    pub theorem_claimed: bool,
261    pub extracted_matches_candidate: bool,
262    pub root_data: LawvereAnalysisDataV0,
263}
264
265impl Analysis<CssRewriteLanguage> for LawvereAnalysis {
266    type Data = LawvereAnalysisDataV0;
267
268    fn make(
269        egraph: &mut EGraph<CssRewriteLanguage, Self>,
270        enode: &CssRewriteLanguage,
271        _id: Id,
272    ) -> Self::Data {
273        let tag = match enode {
274            CssRewriteLanguage::Num(_)
275            | CssRewriteLanguage::Symbol(_)
276            | CssRewriteLanguage::Add(_)
277            | CssRewriteLanguage::Sub(_)
278            | CssRewriteLanguage::Mul(_)
279            | CssRewriteLanguage::Div(_)
280            | CssRewriteLanguage::Calc(_)
281            | CssRewriteLanguage::Unit(_)
282            | CssRewriteLanguage::Box1(_)
283            | CssRewriteLanguage::Box2(_)
284            | CssRewriteLanguage::Box3(_)
285            | CssRewriteLanguage::Box4(_) => AbstractDomainTagV0::TokenValue,
286            CssRewriteLanguage::Is(_)
287            | CssRewriteLanguage::Where(_)
288            | CssRewriteLanguage::List(_) => AbstractDomainTagV0::SelectorShape,
289            CssRewriteLanguage::Declaration(_) | CssRewriteLanguage::StalePrefixDeclaration(_) => {
290                AbstractDomainTagV0::TerminalEmission
291            }
292        };
293        let mut data = LawvereAnalysisDataV0::from_enode(tag, enode);
294        for child in enode.children() {
295            let child_data = &egraph[*child].data;
296            merge_domain_tags(
297                &mut data.abstract_domain_tags,
298                &child_data.abstract_domain_tags,
299            );
300            data.contains_terminal_projection |= child_data.contains_terminal_projection;
301            data.specificity_carrier
302                .merge_from(&child_data.specificity_carrier);
303            data.computed_value_carrier
304                .merge_from(&child_data.computed_value_carrier);
305            data.var_state_carrier
306                .merge_from(&child_data.var_state_carrier);
307            data.provenance_carrier
308                .merge_from(&child_data.provenance_carrier);
309        }
310        refine_computed_value_carrier(enode, &mut data, egraph);
311        data
312    }
313
314    fn merge(&mut self, a: &mut Self::Data, b: Self::Data) -> DidMerge {
315        let before = a.clone();
316        merge_domain_tags(&mut a.abstract_domain_tags, &b.abstract_domain_tags);
317        a.enode_count = a.enode_count.max(b.enode_count);
318        a.contains_terminal_projection |= b.contains_terminal_projection;
319        a.specificity_carrier.merge_from(&b.specificity_carrier);
320        a.computed_value_carrier
321            .merge_from(&b.computed_value_carrier);
322        a.var_state_carrier.merge_from(&b.var_state_carrier);
323        a.provenance_carrier.merge_from(&b.provenance_carrier);
324        DidMerge(before != *a, *a != b)
325    }
326
327    fn allow_ematching_cycles(&self) -> bool {
328        false
329    }
330}
331
332fn merge_domain_tags(target: &mut Vec<AbstractDomainTagV0>, source: &[AbstractDomainTagV0]) {
333    for tag in source {
334        if !target.contains(tag) {
335            target.push(*tag);
336        }
337    }
338    target.sort();
339}
340
341fn merge_labels(target: &mut Vec<&'static str>, source: &[&'static str]) {
342    for label in source {
343        if !target.contains(label) {
344            target.push(*label);
345        }
346    }
347    target.sort();
348}
349
350fn merge_strings(target: &mut Vec<String>, source: &[String]) {
351    for value in source {
352        if !target.contains(value) {
353            target.push(value.clone());
354        }
355    }
356    target.sort();
357}
358
359fn lawvere_enode_kind(enode: &CssRewriteLanguage) -> &'static str {
360    match enode {
361        CssRewriteLanguage::Num(_) => "num",
362        CssRewriteLanguage::Symbol(_) => "symbol",
363        CssRewriteLanguage::Add(_) => "add",
364        CssRewriteLanguage::Sub(_) => "sub",
365        CssRewriteLanguage::Mul(_) => "mul",
366        CssRewriteLanguage::Div(_) => "div",
367        CssRewriteLanguage::Calc(_) => "calc",
368        CssRewriteLanguage::Unit(_) => "unit",
369        CssRewriteLanguage::Box1(_) => "box1",
370        CssRewriteLanguage::Box2(_) => "box2",
371        CssRewriteLanguage::Box3(_) => "box3",
372        CssRewriteLanguage::Box4(_) => "box4",
373        CssRewriteLanguage::Is(_) => "is",
374        CssRewriteLanguage::Where(_) => "where",
375        CssRewriteLanguage::List(_) => "list",
376        CssRewriteLanguage::Declaration(_) => "decl",
377        CssRewriteLanguage::StalePrefixDeclaration(_) => "stale-prefix-decl",
378    }
379}
380
381fn refine_computed_value_carrier(
382    enode: &CssRewriteLanguage,
383    data: &mut LawvereAnalysisDataV0,
384    egraph: &EGraph<CssRewriteLanguage, LawvereAnalysis>,
385) {
386    match enode {
387        CssRewriteLanguage::Calc(child) => {
388            data.computed_value_carrier = egraph[*child].data.computed_value_carrier.clone();
389            data.computed_value_carrier
390                .expression_kinds
391                .push("calcExpression");
392            data.computed_value_carrier.expression_kinds.sort();
393            data.computed_value_carrier.expression_kinds.dedup();
394        }
395        CssRewriteLanguage::Unit([value, unit]) => {
396            let value = &egraph[*value].data.computed_value_carrier;
397            let unit = &egraph[*unit].data.var_state_carrier;
398            data.computed_value_carrier.exact_numeric_value = value.exact_numeric_value;
399            data.computed_value_carrier.exact_unit = unit.symbol_tokens.first().cloned();
400            if let Some(candidate) = exact_computed_candidate_label(
401                data.computed_value_carrier.exact_numeric_value,
402                data.computed_value_carrier.exact_unit.as_deref(),
403            ) {
404                merge_strings(
405                    &mut data.computed_value_carrier.exact_value_candidates,
406                    &[candidate],
407                );
408            }
409        }
410        CssRewriteLanguage::Add([left, right]) => {
411            data.computed_value_carrier =
412                combine_binary_computed_value(&egraph[*left].data, &egraph[*right].data, |a, b| {
413                    a + b
414                });
415            data.computed_value_carrier
416                .expression_kinds
417                .push("addExpression");
418            data.computed_value_carrier.expression_kinds.sort();
419            data.computed_value_carrier.expression_kinds.dedup();
420        }
421        CssRewriteLanguage::Sub([left, right]) => {
422            data.computed_value_carrier =
423                combine_binary_computed_value(&egraph[*left].data, &egraph[*right].data, |a, b| {
424                    a - b
425                });
426            data.computed_value_carrier
427                .expression_kinds
428                .push("subExpression");
429            data.computed_value_carrier.expression_kinds.sort();
430            data.computed_value_carrier.expression_kinds.dedup();
431        }
432        _ => {}
433    }
434}
435
436fn combine_binary_computed_value(
437    left: &LawvereAnalysisDataV0,
438    right: &LawvereAnalysisDataV0,
439    operation: impl FnOnce(i64, i64) -> i64,
440) -> LawvereComputedValueCarrierV0 {
441    let left = &left.computed_value_carrier;
442    let right = &right.computed_value_carrier;
443    let exact_numeric_value = left
444        .exact_numeric_value
445        .zip(right.exact_numeric_value)
446        .and_then(|(left_value, right_value)| {
447            (left.exact_unit == right.exact_unit).then_some(operation(left_value, right_value))
448        });
449    let exact_unit = exact_numeric_value.and_then(|_| left.exact_unit.clone());
450    let exact_value_candidates =
451        exact_computed_candidate_label(exact_numeric_value, exact_unit.as_deref())
452            .into_iter()
453            .collect();
454    let mut expression_kinds = vec!["computedBinaryExpression"];
455    merge_labels(&mut expression_kinds, &left.expression_kinds);
456    merge_labels(&mut expression_kinds, &right.expression_kinds);
457    LawvereComputedValueCarrierV0 {
458        exact_numeric_value,
459        exact_unit,
460        exact_value_candidates,
461        computed_value_obligation_ready: true,
462        expression_kinds,
463    }
464}
465
466fn exact_computed_candidate_label(
467    exact_numeric_value: Option<i64>,
468    exact_unit: Option<&str>,
469) -> Option<String> {
470    let value = exact_numeric_value?;
471    Some(format!("{}{}", value, exact_unit.unwrap_or("")))
472}
473
474pub fn execute_egg_rewrite_with_lawvere_analysis(
475    candidate: EggRewriteCandidateV0,
476) -> (EggRewriteExecutionV0, LawvereSaturationExecutionV0) {
477    let decision = decide_egg_rewrite(candidate.clone());
478    if !decision.accepted {
479        let execution = blocked_execution(candidate.clone(), decision.blocked_reason);
480        let saturation =
481            summarize_lawvere_saturation_execution_v0(candidate.pass_id, 0, 0, 0, 0, false);
482        return (execution, saturation);
483    }
484
485    let expression = match candidate.before.parse::<RecExpr<CssRewriteLanguage>>() {
486        Ok(expression) => expression,
487        Err(_) => {
488            let execution = blocked_execution(
489                candidate.clone(),
490                Some("rewrite expression could not parse"),
491            );
492            let saturation =
493                summarize_lawvere_saturation_execution_v0(candidate.pass_id, 0, 0, 0, 0, false);
494            return (execution, saturation);
495        }
496    };
497    let Some(rules) = rewrite_rules_for_pass::<LawvereAnalysis>(candidate.pass_id) else {
498        let execution = blocked_execution(
499            candidate.clone(),
500            Some("pass is not managed by omena-transform-egg"),
501        );
502        let saturation =
503            summarize_lawvere_saturation_execution_v0(candidate.pass_id, 0, 0, 0, 0, false);
504        return (execution, saturation);
505    };
506
507    let iteration_limit = 8;
508    let runner = Runner::default()
509        .with_expr(&expression)
510        .with_iter_limit(iteration_limit)
511        .run(rules.as_slice());
512    let root = runner.roots[0];
513    let extractor = Extractor::new(&runner.egraph, MdlExtractionCostV0::default_ast_size());
514    let (_, extracted) = extractor.find_best(root);
515    let after = extracted.to_string();
516    let after_matches_candidate = after == candidate.after;
517    let iteration_count = runner.iterations.len();
518    let eclass_count = runner.egraph.number_of_classes();
519    let enode_count = runner.egraph.total_size();
520    let saturation = summarize_lawvere_saturation_execution_v0(
521        candidate.pass_id,
522        iteration_limit,
523        iteration_count,
524        eclass_count,
525        enode_count,
526        after_matches_candidate,
527    );
528
529    let execution = EggRewriteExecutionV0 {
530        schema_version: "0",
531        product: "omena-transform-egg.execution",
532        pass_id: candidate.pass_id,
533        accepted: after_matches_candidate,
534        blocked_reason: (!after_matches_candidate)
535            .then_some("lawvere analysis extraction did not match candidate output"),
536        before: candidate.before,
537        after,
538        expected_after: candidate.after,
539        after_matches_candidate,
540        engine: "egg+lawvere-analysis",
541        iteration_limit,
542        iteration_count,
543        eclass_count,
544        enode_count,
545        mdl_bits: None,
546        mdl_residual_bits: None,
547        mdl_unit: None,
548    };
549    (execution, saturation)
550}
551
552pub fn summarize_lawvere_analysis_carrier_witness_v0(
553    candidate: EggRewriteCandidateV0,
554) -> Option<LawvereAnalysisCarrierWitnessV0> {
555    let decision = decide_egg_rewrite(candidate.clone());
556    if !decision.accepted {
557        return None;
558    }
559
560    let expression = candidate
561        .before
562        .parse::<RecExpr<CssRewriteLanguage>>()
563        .ok()?;
564    let rules = rewrite_rules_for_pass::<LawvereAnalysis>(candidate.pass_id)?;
565    let runner = Runner::default()
566        .with_expr(&expression)
567        .with_iter_limit(8)
568        .run(rules.as_slice());
569    let root = runner.roots[0];
570    let extractor = Extractor::new(&runner.egraph, MdlExtractionCostV0::default_ast_size());
571    let (_, extracted) = extractor.find_best(root);
572    let root_data = runner.egraph[root].data.clone();
573    Some(LawvereAnalysisCarrierWitnessV0 {
574        schema_version: "0",
575        product: "omena-transform-egg.lawvere-analysis-carrier-witness",
576        feature_gate: "lawvere-saturation",
577        claim_level: "fixtureWitnessEclassCarrierWidening",
578        pass_id: candidate.pass_id,
579        specificity_carrier_ready: root_data
580            .specificity_carrier
581            .selector_specificity_obligation_ready,
582        computed_value_carrier_ready: root_data
583            .computed_value_carrier
584            .computed_value_obligation_ready,
585        var_state_carrier_ready: !root_data.var_state_carrier.symbol_tokens.is_empty(),
586        provenance_carrier_ready: root_data.provenance_carrier.provenance_obligation_ready,
587        theorem_claimed: false,
588        extracted_matches_candidate: extracted.to_string() == candidate.after,
589        root_data,
590    })
591}
592
593#[cfg(test)]
594mod tests {
595    use omena_evidence_graph::ObligationFamilyIdV0;
596    use omena_lawvere::{
597        LAWVERE_GLOBAL_TRANSFORM_THEOREM_CLAIMED_V0, LAWVERE_MECHANISM_SCOPE_V0,
598        LAWVERE_PRODUCT_PATH_EVIDENCE_READY_V0,
599    };
600    use omena_transform_cst::TransformPassKind;
601
602    use crate::{EggRewriteCandidateV0, EggRewriteProofV0};
603
604    use super::*;
605
606    #[test]
607    fn lawvere_analysis_fills_parallel_egg_analysis_slot() {
608        let (execution, saturation) =
609            execute_egg_rewrite_with_lawvere_analysis(EggRewriteCandidateV0 {
610                pass_id: TransformPassKind::CalcReduction.id(),
611                before: "(calc (+ (unit 1 px) (unit 2 px)))".to_string(),
612                after: "(unit 3 px)".to_string(),
613                proof: EggRewriteProofV0::new(
614                    false,
615                    ObligationFamilyIdV0::ComputedValuePreservation,
616                    true,
617                    "same-unit calc arithmetic preserves computed value",
618                ),
619            });
620
621        assert!(execution.accepted);
622        assert_eq!(execution.engine, "egg+lawvere-analysis");
623        assert_eq!(saturation.schema_version, "0");
624        assert_eq!(saturation.layer_marker, "enriched-algebraic");
625        assert_eq!(saturation.analysis_slot, "LawvereAnalysis");
626        assert!(saturation.original_unit_analysis_path_preserved);
627        assert_eq!(saturation.differential_fixture_count, 10);
628        assert_eq!(saturation.mechanism_scope, LAWVERE_MECHANISM_SCOPE_V0);
629        assert_eq!(
630            saturation.product_path_evidence_ready,
631            LAWVERE_PRODUCT_PATH_EVIDENCE_READY_V0
632        );
633        assert_eq!(
634            saturation.global_transform_theorem_claimed,
635            LAWVERE_GLOBAL_TRANSFORM_THEOREM_CLAIMED_V0
636        );
637    }
638
639    #[test]
640    fn lawvere_analysis_widens_eclass_carriers_under_fixture_witness() -> Result<(), &'static str> {
641        let witness = summarize_lawvere_analysis_carrier_witness_v0(EggRewriteCandidateV0 {
642            pass_id: TransformPassKind::CalcReduction.id(),
643            before: "(calc (+ (unit 1 px) (unit 2 px)))".to_string(),
644            after: "(unit 3 px)".to_string(),
645            proof: EggRewriteProofV0::new(
646                false,
647                ObligationFamilyIdV0::ComputedValuePreservation,
648                true,
649                "same-unit calc arithmetic preserves computed value",
650            ),
651        })
652        .ok_or("carrier witness should be produced for a managed accepted rewrite")?;
653
654        assert_eq!(witness.claim_level, "fixtureWitnessEclassCarrierWidening");
655        assert!(witness.computed_value_carrier_ready);
656        assert!(witness.var_state_carrier_ready);
657        assert!(witness.provenance_carrier_ready);
658        assert!(!witness.theorem_claimed);
659        assert!(witness.extracted_matches_candidate);
660        assert!(
661            witness
662                .root_data
663                .computed_value_carrier
664                .exact_value_candidates
665                .contains(&"3px".to_string())
666        );
667        assert!(
668            witness
669                .root_data
670                .provenance_carrier
671                .enode_kinds
672                .contains(&"add")
673        );
674        Ok(())
675    }
676
677    #[test]
678    fn lawvere_analysis_carries_selector_specificity_context() -> Result<(), &'static str> {
679        let witness = summarize_lawvere_analysis_carrier_witness_v0(EggRewriteCandidateV0 {
680            pass_id: TransformPassKind::SelectorIsWhereCompression.id(),
681            before: "(where (list ready ready))".to_string(),
682            after: "(where ready)".to_string(),
683            proof: EggRewriteProofV0::new(
684                true,
685                ObligationFamilyIdV0::CascadeSafetyFloor,
686                true,
687                "duplicate :where() argument keeps zero specificity",
688            ),
689        })
690        .ok_or("selector carrier witness should be produced for an accepted rewrite")?;
691
692        assert!(witness.specificity_carrier_ready);
693        assert!(witness.var_state_carrier_ready);
694        assert!(witness.provenance_carrier_ready);
695        assert!(!witness.theorem_claimed);
696        assert!(
697            witness
698                .root_data
699                .specificity_carrier
700                .zero_specificity_context_seen
701        );
702        assert!(
703            witness
704                .root_data
705                .var_state_carrier
706                .symbol_tokens
707                .contains(&"ready".to_string())
708        );
709        Ok(())
710    }
711}