Skip to main content

omena_transform_egg/
transform_catalog_analysis.rs

1//! Feature-gated TransformCatalog-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 `transform-catalog-saturation` experiment is enabled.
5
6use egg::{Analysis, DidMerge, EGraph, Extractor, Id, Language, RecExpr, Runner};
7#[allow(deprecated)]
8use omena_lawvere::LawvereSaturationExecutionV0;
9use omena_lawvere::{
10    AbstractDomainTagV0, TransformCatalogSaturationExecutionV0,
11    summarize_transform_catalog_saturation_execution_v0,
12};
13use serde::Serialize;
14
15use crate::{
16    CssRewriteLanguage, EggRewriteCandidateV0, EggRewriteExecutionV0, MdlExtractionCostV0,
17    blocked_execution, decide_egg_rewrite, rewrite_rules_for_pass,
18};
19
20#[derive(Debug, Default, Clone)]
21pub struct TransformCatalogAnalysis;
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct TransformCatalogAnalysisDataV0 {
26    pub abstract_domain_tags: Vec<AbstractDomainTagV0>,
27    pub enode_count: usize,
28    pub contains_terminal_projection: bool,
29    pub specificity_carrier: TransformCatalogSpecificityCarrierV0,
30    pub computed_value_carrier: TransformCatalogComputedValueCarrierV0,
31    pub var_state_carrier: TransformCatalogVarStateCarrierV0,
32    pub provenance_carrier: TransformCatalogProvenanceCarrierV0,
33}
34
35impl TransformCatalogAnalysisDataV0 {
36    fn from_enode(tag: AbstractDomainTagV0, enode: &CssRewriteLanguage) -> Self {
37        Self {
38            abstract_domain_tags: vec![tag],
39            enode_count: 1,
40            contains_terminal_projection: tag == AbstractDomainTagV0::TerminalEmission,
41            specificity_carrier: TransformCatalogSpecificityCarrierV0::from_enode(enode),
42            computed_value_carrier: TransformCatalogComputedValueCarrierV0::from_enode(enode),
43            var_state_carrier: TransformCatalogVarStateCarrierV0::from_enode(enode),
44            provenance_carrier: TransformCatalogProvenanceCarrierV0::from_enode(enode),
45        }
46    }
47}
48
49#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct TransformCatalogSpecificityCarrierV0 {
52    pub selector_context_count: usize,
53    pub selector_atom_count: usize,
54    pub zero_specificity_context_seen: bool,
55    pub selector_specificity_obligation_ready: bool,
56}
57
58impl TransformCatalogSpecificityCarrierV0 {
59    fn from_enode(enode: &CssRewriteLanguage) -> Self {
60        match enode {
61            CssRewriteLanguage::Is(_) => Self {
62                selector_context_count: 1,
63                selector_atom_count: 0,
64                zero_specificity_context_seen: false,
65                selector_specificity_obligation_ready: true,
66            },
67            CssRewriteLanguage::Where(_) => Self {
68                selector_context_count: 1,
69                selector_atom_count: 0,
70                zero_specificity_context_seen: true,
71                selector_specificity_obligation_ready: true,
72            },
73            CssRewriteLanguage::List(_) => Self {
74                selector_context_count: 1,
75                selector_atom_count: 0,
76                zero_specificity_context_seen: false,
77                selector_specificity_obligation_ready: true,
78            },
79            _ => Self::default(),
80        }
81    }
82
83    fn merge_from(&mut self, other: &Self) {
84        self.selector_context_count = self
85            .selector_context_count
86            .saturating_add(other.selector_context_count);
87        self.selector_atom_count = self
88            .selector_atom_count
89            .saturating_add(other.selector_atom_count);
90        self.zero_specificity_context_seen |= other.zero_specificity_context_seen;
91        self.selector_specificity_obligation_ready |= other.selector_specificity_obligation_ready;
92    }
93}
94
95#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct TransformCatalogComputedValueCarrierV0 {
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub exact_numeric_value: Option<i64>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub exact_unit: Option<String>,
102    pub exact_value_candidates: Vec<String>,
103    pub computed_value_obligation_ready: bool,
104    pub expression_kinds: Vec<&'static str>,
105}
106
107impl TransformCatalogComputedValueCarrierV0 {
108    fn from_enode(enode: &CssRewriteLanguage) -> Self {
109        match enode {
110            CssRewriteLanguage::Num(value) => Self {
111                exact_numeric_value: Some(*value),
112                exact_unit: None,
113                exact_value_candidates: vec![value.to_string()],
114                computed_value_obligation_ready: true,
115                expression_kinds: vec!["numericLiteral"],
116            },
117            CssRewriteLanguage::Symbol(symbol) => Self {
118                exact_numeric_value: None,
119                exact_unit: Some(symbol.to_string()),
120                exact_value_candidates: Vec::new(),
121                computed_value_obligation_ready: false,
122                expression_kinds: vec!["symbolToken"],
123            },
124            CssRewriteLanguage::Calc(_) => Self {
125                exact_numeric_value: None,
126                exact_unit: None,
127                exact_value_candidates: Vec::new(),
128                computed_value_obligation_ready: true,
129                expression_kinds: vec!["calcExpression"],
130            },
131            CssRewriteLanguage::Unit(_) => Self {
132                exact_numeric_value: None,
133                exact_unit: None,
134                exact_value_candidates: Vec::new(),
135                computed_value_obligation_ready: true,
136                expression_kinds: vec!["unitExpression"],
137            },
138            CssRewriteLanguage::Add(_) => Self {
139                exact_numeric_value: None,
140                exact_unit: None,
141                exact_value_candidates: Vec::new(),
142                computed_value_obligation_ready: true,
143                expression_kinds: vec!["addExpression"],
144            },
145            CssRewriteLanguage::Sub(_) => Self {
146                exact_numeric_value: None,
147                exact_unit: None,
148                exact_value_candidates: Vec::new(),
149                computed_value_obligation_ready: true,
150                expression_kinds: vec!["subExpression"],
151            },
152            CssRewriteLanguage::Mul(_) => Self {
153                exact_numeric_value: None,
154                exact_unit: None,
155                exact_value_candidates: Vec::new(),
156                computed_value_obligation_ready: true,
157                expression_kinds: vec!["mulExpression"],
158            },
159            CssRewriteLanguage::Div(_) => Self {
160                exact_numeric_value: None,
161                exact_unit: None,
162                exact_value_candidates: Vec::new(),
163                computed_value_obligation_ready: true,
164                expression_kinds: vec!["divExpression"],
165            },
166            CssRewriteLanguage::Box1(_)
167            | CssRewriteLanguage::Box2(_)
168            | CssRewriteLanguage::Box3(_)
169            | CssRewriteLanguage::Box4(_) => Self {
170                exact_numeric_value: None,
171                exact_unit: None,
172                exact_value_candidates: Vec::new(),
173                computed_value_obligation_ready: true,
174                expression_kinds: vec!["boxShorthandExpression"],
175            },
176            _ => Self::default(),
177        }
178    }
179
180    fn merge_from(&mut self, other: &Self) {
181        self.computed_value_obligation_ready |= other.computed_value_obligation_ready;
182        merge_labels(&mut self.expression_kinds, &other.expression_kinds);
183        merge_strings(
184            &mut self.exact_value_candidates,
185            &other.exact_value_candidates,
186        );
187        self.exact_numeric_value = None;
188        if self.exact_unit != other.exact_unit {
189            self.exact_unit = None;
190        }
191    }
192}
193
194#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
195#[serde(rename_all = "camelCase")]
196pub struct TransformCatalogVarStateCarrierV0 {
197    pub symbolic_reference_count: usize,
198    pub symbol_tokens: Vec<String>,
199    pub unresolved_var_reference_seen: bool,
200}
201
202impl TransformCatalogVarStateCarrierV0 {
203    fn from_enode(enode: &CssRewriteLanguage) -> Self {
204        let CssRewriteLanguage::Symbol(symbol) = enode else {
205            return Self::default();
206        };
207        let symbol = symbol.to_string();
208        let unresolved_var_reference_seen = symbol.contains("--") || symbol.starts_with("var_");
209        Self {
210            symbolic_reference_count: 1,
211            symbol_tokens: vec![symbol],
212            unresolved_var_reference_seen,
213        }
214    }
215
216    fn merge_from(&mut self, other: &Self) {
217        self.symbolic_reference_count = self
218            .symbolic_reference_count
219            .saturating_add(other.symbolic_reference_count);
220        for symbol in &other.symbol_tokens {
221            if !self.symbol_tokens.contains(symbol) {
222                self.symbol_tokens.push(symbol.clone());
223            }
224        }
225        self.symbol_tokens.sort();
226        self.unresolved_var_reference_seen |= other.unresolved_var_reference_seen;
227    }
228}
229
230#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
231#[serde(rename_all = "camelCase")]
232pub struct TransformCatalogProvenanceCarrierV0 {
233    pub enode_kinds: Vec<&'static str>,
234    pub provenance_obligation_ready: bool,
235}
236
237impl TransformCatalogProvenanceCarrierV0 {
238    fn from_enode(enode: &CssRewriteLanguage) -> Self {
239        Self {
240            enode_kinds: vec![transform_catalog_enode_kind(enode)],
241            provenance_obligation_ready: true,
242        }
243    }
244
245    fn merge_from(&mut self, other: &Self) {
246        merge_labels(&mut self.enode_kinds, &other.enode_kinds);
247        self.provenance_obligation_ready |= other.provenance_obligation_ready;
248    }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
252#[serde(rename_all = "camelCase")]
253pub struct TransformCatalogAnalysisCarrierWitnessV0 {
254    pub schema_version: &'static str,
255    pub product: &'static str,
256    pub feature_gate: &'static str,
257    pub claim_level: &'static str,
258    pub pass_id: &'static str,
259    pub specificity_carrier_ready: bool,
260    pub computed_value_carrier_ready: bool,
261    pub var_state_carrier_ready: bool,
262    pub provenance_carrier_ready: bool,
263    pub theorem_claimed: bool,
264    pub extracted_matches_candidate: bool,
265    pub root_data: TransformCatalogAnalysisDataV0,
266}
267
268impl Analysis<CssRewriteLanguage> for TransformCatalogAnalysis {
269    type Data = TransformCatalogAnalysisDataV0;
270
271    fn make(
272        egraph: &mut EGraph<CssRewriteLanguage, Self>,
273        enode: &CssRewriteLanguage,
274        _id: Id,
275    ) -> Self::Data {
276        let tag = match enode {
277            CssRewriteLanguage::Num(_)
278            | CssRewriteLanguage::Symbol(_)
279            | CssRewriteLanguage::Add(_)
280            | CssRewriteLanguage::Sub(_)
281            | CssRewriteLanguage::Mul(_)
282            | CssRewriteLanguage::Div(_)
283            | CssRewriteLanguage::Calc(_)
284            | CssRewriteLanguage::Unit(_)
285            | CssRewriteLanguage::Box1(_)
286            | CssRewriteLanguage::Box2(_)
287            | CssRewriteLanguage::Box3(_)
288            | CssRewriteLanguage::Box4(_) => AbstractDomainTagV0::TokenValue,
289            CssRewriteLanguage::Is(_)
290            | CssRewriteLanguage::Where(_)
291            | CssRewriteLanguage::List(_) => AbstractDomainTagV0::SelectorShape,
292            CssRewriteLanguage::Declaration(_) | CssRewriteLanguage::StalePrefixDeclaration(_) => {
293                AbstractDomainTagV0::TerminalEmission
294            }
295        };
296        let mut data = TransformCatalogAnalysisDataV0::from_enode(tag, enode);
297        for child in enode.children() {
298            let child_data = &egraph[*child].data;
299            merge_domain_tags(
300                &mut data.abstract_domain_tags,
301                &child_data.abstract_domain_tags,
302            );
303            data.contains_terminal_projection |= child_data.contains_terminal_projection;
304            data.specificity_carrier
305                .merge_from(&child_data.specificity_carrier);
306            data.computed_value_carrier
307                .merge_from(&child_data.computed_value_carrier);
308            data.var_state_carrier
309                .merge_from(&child_data.var_state_carrier);
310            data.provenance_carrier
311                .merge_from(&child_data.provenance_carrier);
312        }
313        refine_computed_value_carrier(enode, &mut data, egraph);
314        data
315    }
316
317    fn merge(&mut self, a: &mut Self::Data, b: Self::Data) -> DidMerge {
318        let before = a.clone();
319        merge_domain_tags(&mut a.abstract_domain_tags, &b.abstract_domain_tags);
320        a.enode_count = a.enode_count.max(b.enode_count);
321        a.contains_terminal_projection |= b.contains_terminal_projection;
322        a.specificity_carrier.merge_from(&b.specificity_carrier);
323        a.computed_value_carrier
324            .merge_from(&b.computed_value_carrier);
325        a.var_state_carrier.merge_from(&b.var_state_carrier);
326        a.provenance_carrier.merge_from(&b.provenance_carrier);
327        DidMerge(before != *a, *a != b)
328    }
329
330    fn allow_ematching_cycles(&self) -> bool {
331        false
332    }
333}
334
335fn merge_domain_tags(target: &mut Vec<AbstractDomainTagV0>, source: &[AbstractDomainTagV0]) {
336    for tag in source {
337        if !target.contains(tag) {
338            target.push(*tag);
339        }
340    }
341    target.sort();
342}
343
344fn merge_labels(target: &mut Vec<&'static str>, source: &[&'static str]) {
345    for label in source {
346        if !target.contains(label) {
347            target.push(*label);
348        }
349    }
350    target.sort();
351}
352
353fn merge_strings(target: &mut Vec<String>, source: &[String]) {
354    for value in source {
355        if !target.contains(value) {
356            target.push(value.clone());
357        }
358    }
359    target.sort();
360}
361
362fn transform_catalog_enode_kind(enode: &CssRewriteLanguage) -> &'static str {
363    match enode {
364        CssRewriteLanguage::Num(_) => "num",
365        CssRewriteLanguage::Symbol(_) => "symbol",
366        CssRewriteLanguage::Add(_) => "add",
367        CssRewriteLanguage::Sub(_) => "sub",
368        CssRewriteLanguage::Mul(_) => "mul",
369        CssRewriteLanguage::Div(_) => "div",
370        CssRewriteLanguage::Calc(_) => "calc",
371        CssRewriteLanguage::Unit(_) => "unit",
372        CssRewriteLanguage::Box1(_) => "box1",
373        CssRewriteLanguage::Box2(_) => "box2",
374        CssRewriteLanguage::Box3(_) => "box3",
375        CssRewriteLanguage::Box4(_) => "box4",
376        CssRewriteLanguage::Is(_) => "is",
377        CssRewriteLanguage::Where(_) => "where",
378        CssRewriteLanguage::List(_) => "list",
379        CssRewriteLanguage::Declaration(_) => "decl",
380        CssRewriteLanguage::StalePrefixDeclaration(_) => "stale-prefix-decl",
381    }
382}
383
384fn refine_computed_value_carrier(
385    enode: &CssRewriteLanguage,
386    data: &mut TransformCatalogAnalysisDataV0,
387    egraph: &EGraph<CssRewriteLanguage, TransformCatalogAnalysis>,
388) {
389    match enode {
390        CssRewriteLanguage::Calc(child) => {
391            data.computed_value_carrier = egraph[*child].data.computed_value_carrier.clone();
392            data.computed_value_carrier
393                .expression_kinds
394                .push("calcExpression");
395            data.computed_value_carrier.expression_kinds.sort();
396            data.computed_value_carrier.expression_kinds.dedup();
397        }
398        CssRewriteLanguage::Unit([value, unit]) => {
399            let value = &egraph[*value].data.computed_value_carrier;
400            let unit = &egraph[*unit].data.var_state_carrier;
401            data.computed_value_carrier.exact_numeric_value = value.exact_numeric_value;
402            data.computed_value_carrier.exact_unit = unit.symbol_tokens.first().cloned();
403            if let Some(candidate) = exact_computed_candidate_label(
404                data.computed_value_carrier.exact_numeric_value,
405                data.computed_value_carrier.exact_unit.as_deref(),
406            ) {
407                merge_strings(
408                    &mut data.computed_value_carrier.exact_value_candidates,
409                    &[candidate],
410                );
411            }
412        }
413        CssRewriteLanguage::Add([left, right]) => {
414            data.computed_value_carrier =
415                combine_binary_computed_value(&egraph[*left].data, &egraph[*right].data, |a, b| {
416                    a + b
417                });
418            data.computed_value_carrier
419                .expression_kinds
420                .push("addExpression");
421            data.computed_value_carrier.expression_kinds.sort();
422            data.computed_value_carrier.expression_kinds.dedup();
423        }
424        CssRewriteLanguage::Sub([left, right]) => {
425            data.computed_value_carrier =
426                combine_binary_computed_value(&egraph[*left].data, &egraph[*right].data, |a, b| {
427                    a - b
428                });
429            data.computed_value_carrier
430                .expression_kinds
431                .push("subExpression");
432            data.computed_value_carrier.expression_kinds.sort();
433            data.computed_value_carrier.expression_kinds.dedup();
434        }
435        _ => {}
436    }
437}
438
439fn combine_binary_computed_value(
440    left: &TransformCatalogAnalysisDataV0,
441    right: &TransformCatalogAnalysisDataV0,
442    operation: impl FnOnce(i64, i64) -> i64,
443) -> TransformCatalogComputedValueCarrierV0 {
444    let left = &left.computed_value_carrier;
445    let right = &right.computed_value_carrier;
446    let exact_numeric_value = left
447        .exact_numeric_value
448        .zip(right.exact_numeric_value)
449        .and_then(|(left_value, right_value)| {
450            (left.exact_unit == right.exact_unit).then_some(operation(left_value, right_value))
451        });
452    let exact_unit = exact_numeric_value.and_then(|_| left.exact_unit.clone());
453    let exact_value_candidates =
454        exact_computed_candidate_label(exact_numeric_value, exact_unit.as_deref())
455            .into_iter()
456            .collect();
457    let mut expression_kinds = vec!["computedBinaryExpression"];
458    merge_labels(&mut expression_kinds, &left.expression_kinds);
459    merge_labels(&mut expression_kinds, &right.expression_kinds);
460    TransformCatalogComputedValueCarrierV0 {
461        exact_numeric_value,
462        exact_unit,
463        exact_value_candidates,
464        computed_value_obligation_ready: true,
465        expression_kinds,
466    }
467}
468
469fn exact_computed_candidate_label(
470    exact_numeric_value: Option<i64>,
471    exact_unit: Option<&str>,
472) -> Option<String> {
473    let value = exact_numeric_value?;
474    Some(format!("{}{}", value, exact_unit.unwrap_or("")))
475}
476
477pub fn execute_egg_rewrite_with_transform_catalog_analysis(
478    candidate: EggRewriteCandidateV0,
479) -> (EggRewriteExecutionV0, TransformCatalogSaturationExecutionV0) {
480    let decision = decide_egg_rewrite(candidate.clone());
481    if !decision.accepted {
482        let execution = blocked_execution(candidate.clone(), decision.blocked_reason);
483        let saturation = summarize_transform_catalog_saturation_execution_v0(
484            candidate.pass_id,
485            0,
486            0,
487            0,
488            0,
489            false,
490        );
491        return (execution, saturation);
492    }
493
494    let expression = match candidate.before.parse::<RecExpr<CssRewriteLanguage>>() {
495        Ok(expression) => expression,
496        Err(_) => {
497            let execution = blocked_execution(
498                candidate.clone(),
499                Some("rewrite expression could not parse"),
500            );
501            let saturation = summarize_transform_catalog_saturation_execution_v0(
502                candidate.pass_id,
503                0,
504                0,
505                0,
506                0,
507                false,
508            );
509            return (execution, saturation);
510        }
511    };
512    let Some(rules) = rewrite_rules_for_pass::<TransformCatalogAnalysis>(candidate.pass_id) else {
513        let execution = blocked_execution(
514            candidate.clone(),
515            Some("pass is not managed by omena-transform-egg"),
516        );
517        let saturation = summarize_transform_catalog_saturation_execution_v0(
518            candidate.pass_id,
519            0,
520            0,
521            0,
522            0,
523            false,
524        );
525        return (execution, saturation);
526    };
527
528    let iteration_limit = 8;
529    let runner = Runner::default()
530        .with_expr(&expression)
531        .with_iter_limit(iteration_limit)
532        .run(rules.as_slice());
533    let root = runner.roots[0];
534    let extractor = Extractor::new(&runner.egraph, MdlExtractionCostV0::default_ast_size());
535    let (_, extracted) = extractor.find_best(root);
536    let after = extracted.to_string();
537    let after_matches_candidate = after == candidate.after;
538    let iteration_count = runner.iterations.len();
539    let eclass_count = runner.egraph.number_of_classes();
540    let enode_count = runner.egraph.total_size();
541    let saturation = summarize_transform_catalog_saturation_execution_v0(
542        candidate.pass_id,
543        iteration_limit,
544        iteration_count,
545        eclass_count,
546        enode_count,
547        after_matches_candidate,
548    );
549
550    let execution = EggRewriteExecutionV0 {
551        schema_version: "0",
552        product: "omena-transform-egg.execution",
553        pass_id: candidate.pass_id,
554        accepted: after_matches_candidate,
555        blocked_reason: (!after_matches_candidate)
556            .then_some("transform-catalog analysis extraction did not match candidate output"),
557        before: candidate.before,
558        after,
559        expected_after: candidate.after,
560        after_matches_candidate,
561        engine: "egg+transform-catalog-analysis",
562        iteration_limit,
563        iteration_count,
564        eclass_count,
565        enode_count,
566        mdl_bits: None,
567        mdl_residual_bits: None,
568        mdl_unit: None,
569    };
570    (execution, saturation)
571}
572
573pub fn summarize_transform_catalog_analysis_carrier_witness_v0(
574    candidate: EggRewriteCandidateV0,
575) -> Option<TransformCatalogAnalysisCarrierWitnessV0> {
576    let decision = decide_egg_rewrite(candidate.clone());
577    if !decision.accepted {
578        return None;
579    }
580
581    let expression = candidate
582        .before
583        .parse::<RecExpr<CssRewriteLanguage>>()
584        .ok()?;
585    let rules = rewrite_rules_for_pass::<TransformCatalogAnalysis>(candidate.pass_id)?;
586    let runner = Runner::default()
587        .with_expr(&expression)
588        .with_iter_limit(8)
589        .run(rules.as_slice());
590    let root = runner.roots[0];
591    let extractor = Extractor::new(&runner.egraph, MdlExtractionCostV0::default_ast_size());
592    let (_, extracted) = extractor.find_best(root);
593    let root_data = runner.egraph[root].data.clone();
594    Some(TransformCatalogAnalysisCarrierWitnessV0 {
595        schema_version: "0",
596        product: "omena-transform-egg.transform-catalog-analysis-carrier-witness",
597        feature_gate: "transform-catalog-saturation",
598        claim_level: "fixtureWitnessEclassCarrierWidening",
599        pass_id: candidate.pass_id,
600        specificity_carrier_ready: root_data
601            .specificity_carrier
602            .selector_specificity_obligation_ready,
603        computed_value_carrier_ready: root_data
604            .computed_value_carrier
605            .computed_value_obligation_ready,
606        var_state_carrier_ready: !root_data.var_state_carrier.symbol_tokens.is_empty(),
607        provenance_carrier_ready: root_data.provenance_carrier.provenance_obligation_ready,
608        theorem_claimed: false,
609        extracted_matches_candidate: extracted.to_string() == candidate.after,
610        root_data,
611    })
612}
613
614/// Published 0.3 compatibility analysis marker.
615/// Owner: `omena-transform-egg` maintainers. Removal condition: not before 1.0,
616/// after downstream migration and zero audited non-compatibility uses.
617#[deprecated(
618    since = "0.4.0",
619    note = "use TransformCatalogAnalysis; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
620)]
621#[derive(Debug, Default, Clone)]
622pub struct LawvereAnalysis;
623
624#[deprecated(
625    since = "0.4.0",
626    note = "use TransformCatalogSpecificityCarrierV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
627)]
628#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
629#[serde(rename_all = "camelCase")]
630pub struct LawvereSpecificityCarrierV0 {
631    pub selector_context_count: usize,
632    pub selector_atom_count: usize,
633    pub zero_specificity_context_seen: bool,
634    pub selector_specificity_obligation_ready: bool,
635}
636
637#[deprecated(
638    since = "0.4.0",
639    note = "use TransformCatalogComputedValueCarrierV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
640)]
641#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
642#[serde(rename_all = "camelCase")]
643pub struct LawvereComputedValueCarrierV0 {
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub exact_numeric_value: Option<i64>,
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub exact_unit: Option<String>,
648    pub exact_value_candidates: Vec<String>,
649    pub computed_value_obligation_ready: bool,
650    pub expression_kinds: Vec<&'static str>,
651}
652
653#[deprecated(
654    since = "0.4.0",
655    note = "use TransformCatalogVarStateCarrierV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
656)]
657#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
658#[serde(rename_all = "camelCase")]
659pub struct LawvereVarStateCarrierV0 {
660    pub symbolic_reference_count: usize,
661    pub symbol_tokens: Vec<String>,
662    pub unresolved_var_reference_seen: bool,
663}
664
665#[deprecated(
666    since = "0.4.0",
667    note = "use TransformCatalogProvenanceCarrierV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
668)]
669#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
670#[serde(rename_all = "camelCase")]
671pub struct LawvereProvenanceCarrierV0 {
672    pub enode_kinds: Vec<&'static str>,
673    pub provenance_obligation_ready: bool,
674}
675
676#[allow(deprecated)]
677#[deprecated(
678    since = "0.4.0",
679    note = "use TransformCatalogAnalysisDataV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
680)]
681#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
682#[serde(rename_all = "camelCase")]
683pub struct LawvereAnalysisDataV0 {
684    pub abstract_domain_tags: Vec<AbstractDomainTagV0>,
685    pub enode_count: usize,
686    pub contains_terminal_projection: bool,
687    pub specificity_carrier: LawvereSpecificityCarrierV0,
688    pub computed_value_carrier: LawvereComputedValueCarrierV0,
689    pub var_state_carrier: LawvereVarStateCarrierV0,
690    pub provenance_carrier: LawvereProvenanceCarrierV0,
691}
692
693#[allow(deprecated)]
694#[deprecated(
695    since = "0.4.0",
696    note = "use TransformCatalogAnalysisCarrierWitnessV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
697)]
698#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
699#[serde(rename_all = "camelCase")]
700pub struct LawvereAnalysisCarrierWitnessV0 {
701    pub schema_version: &'static str,
702    pub product: &'static str,
703    pub feature_gate: &'static str,
704    pub claim_level: &'static str,
705    pub pass_id: &'static str,
706    pub specificity_carrier_ready: bool,
707    pub computed_value_carrier_ready: bool,
708    pub var_state_carrier_ready: bool,
709    pub provenance_carrier_ready: bool,
710    pub theorem_claimed: bool,
711    pub extracted_matches_candidate: bool,
712    pub root_data: LawvereAnalysisDataV0,
713}
714
715#[allow(deprecated)]
716#[deprecated(
717    since = "0.4.0",
718    note = "compatibility conversion owned by omena-transform-egg maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
719)]
720fn analysis_witness_into_compatibility_wire_v0(
721    witness: TransformCatalogAnalysisCarrierWitnessV0,
722) -> LawvereAnalysisCarrierWitnessV0 {
723    LawvereAnalysisCarrierWitnessV0 {
724        schema_version: witness.schema_version,
725        product: "omena-transform-egg.lawvere-analysis-carrier-witness",
726        feature_gate: "lawvere-saturation",
727        claim_level: witness.claim_level,
728        pass_id: witness.pass_id,
729        specificity_carrier_ready: witness.specificity_carrier_ready,
730        computed_value_carrier_ready: witness.computed_value_carrier_ready,
731        var_state_carrier_ready: witness.var_state_carrier_ready,
732        provenance_carrier_ready: witness.provenance_carrier_ready,
733        theorem_claimed: witness.theorem_claimed,
734        extracted_matches_candidate: witness.extracted_matches_candidate,
735        root_data: LawvereAnalysisDataV0 {
736            abstract_domain_tags: witness.root_data.abstract_domain_tags,
737            enode_count: witness.root_data.enode_count,
738            contains_terminal_projection: witness.root_data.contains_terminal_projection,
739            specificity_carrier: LawvereSpecificityCarrierV0 {
740                selector_context_count: witness
741                    .root_data
742                    .specificity_carrier
743                    .selector_context_count,
744                selector_atom_count: witness.root_data.specificity_carrier.selector_atom_count,
745                zero_specificity_context_seen: witness
746                    .root_data
747                    .specificity_carrier
748                    .zero_specificity_context_seen,
749                selector_specificity_obligation_ready: witness
750                    .root_data
751                    .specificity_carrier
752                    .selector_specificity_obligation_ready,
753            },
754            computed_value_carrier: LawvereComputedValueCarrierV0 {
755                exact_numeric_value: witness.root_data.computed_value_carrier.exact_numeric_value,
756                exact_unit: witness.root_data.computed_value_carrier.exact_unit,
757                exact_value_candidates: witness
758                    .root_data
759                    .computed_value_carrier
760                    .exact_value_candidates,
761                computed_value_obligation_ready: witness
762                    .root_data
763                    .computed_value_carrier
764                    .computed_value_obligation_ready,
765                expression_kinds: witness.root_data.computed_value_carrier.expression_kinds,
766            },
767            var_state_carrier: LawvereVarStateCarrierV0 {
768                symbolic_reference_count: witness
769                    .root_data
770                    .var_state_carrier
771                    .symbolic_reference_count,
772                symbol_tokens: witness.root_data.var_state_carrier.symbol_tokens,
773                unresolved_var_reference_seen: witness
774                    .root_data
775                    .var_state_carrier
776                    .unresolved_var_reference_seen,
777            },
778            provenance_carrier: LawvereProvenanceCarrierV0 {
779                enode_kinds: witness.root_data.provenance_carrier.enode_kinds,
780                provenance_obligation_ready: witness
781                    .root_data
782                    .provenance_carrier
783                    .provenance_obligation_ready,
784            },
785        },
786    }
787}
788
789#[allow(deprecated)]
790#[deprecated(
791    since = "0.4.0",
792    note = "compatibility conversion owned by omena-transform-egg maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
793)]
794fn saturation_into_compatibility_wire_v0(
795    saturation: TransformCatalogSaturationExecutionV0,
796) -> LawvereSaturationExecutionV0 {
797    LawvereSaturationExecutionV0 {
798        schema_version: saturation.schema_version,
799        product: saturation.product,
800        layer_marker: saturation.layer_marker,
801        feature_gate: "lawvere-saturation",
802        mechanism_scope: saturation.mechanism_scope,
803        product_path_evidence_ready: saturation.product_path_evidence_ready,
804        global_transform_theorem_claimed: saturation.global_transform_theorem_claimed,
805        theory_version: "lawvere-css-transform-catalog-v0",
806        pass_id: saturation.pass_id,
807        analysis_slot: "LawvereAnalysis",
808        original_unit_analysis_path_preserved: saturation.original_unit_analysis_path_preserved,
809        differential_tier: saturation.differential_tier,
810        differential_fixture_count: saturation.differential_fixture_count,
811        iteration_limit: saturation.iteration_limit,
812        iteration_count: saturation.iteration_count,
813        eclass_count: saturation.eclass_count,
814        enode_count: saturation.enode_count,
815        accepted: saturation.accepted,
816        extracted_matches_candidate: saturation.extracted_matches_candidate,
817    }
818}
819
820#[allow(deprecated)]
821#[deprecated(
822    since = "0.4.0",
823    note = "use execute_egg_rewrite_with_transform_catalog_analysis; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
824)]
825pub fn execute_egg_rewrite_with_lawvere_analysis(
826    candidate: EggRewriteCandidateV0,
827) -> (EggRewriteExecutionV0, LawvereSaturationExecutionV0) {
828    let (mut execution, saturation) =
829        execute_egg_rewrite_with_transform_catalog_analysis(candidate);
830    if execution.blocked_reason
831        == Some("transform-catalog analysis extraction did not match candidate output")
832    {
833        execution.blocked_reason =
834            Some("lawvere analysis extraction did not match candidate output");
835    }
836    execution.engine = "egg+lawvere-analysis";
837    (execution, saturation_into_compatibility_wire_v0(saturation))
838}
839
840#[allow(deprecated)]
841#[deprecated(
842    since = "0.4.0",
843    note = "use summarize_transform_catalog_analysis_carrier_witness_v0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
844)]
845pub fn summarize_lawvere_analysis_carrier_witness_v0(
846    candidate: EggRewriteCandidateV0,
847) -> Option<LawvereAnalysisCarrierWitnessV0> {
848    summarize_transform_catalog_analysis_carrier_witness_v0(candidate)
849        .map(analysis_witness_into_compatibility_wire_v0)
850}
851
852#[cfg(test)]
853mod tests {
854    use omena_evidence_graph::ObligationFamilyIdV0;
855    use omena_lawvere::{
856        TRANSFORM_CATALOG_GLOBAL_TRANSFORM_THEOREM_CLAIMED_V0,
857        TRANSFORM_CATALOG_MECHANISM_SCOPE_V0, TRANSFORM_CATALOG_PRODUCT_PATH_EVIDENCE_READY_V0,
858    };
859    use omena_transform_cst::TransformPassKind;
860    use sha2::{Digest, Sha256};
861
862    use crate::{EggRewriteCandidateV0, EggRewriteProofV0};
863
864    use super::*;
865
866    #[allow(deprecated)]
867    #[deprecated(
868        since = "0.4.0",
869        note = "compatibility test adapter owned by omena-transform-egg maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
870    )]
871    fn compatibility_analysis_serialized_v0(
872        candidate: EggRewriteCandidateV0,
873    ) -> Result<String, serde_json::Error> {
874        let execution = execute_egg_rewrite_with_lawvere_analysis(candidate.clone());
875        let witness = summarize_lawvere_analysis_carrier_witness_v0(candidate);
876        serde_json::to_string(&(execution, witness))
877    }
878
879    #[test]
880    #[allow(deprecated)]
881    fn compatibility_and_canonical_analysis_surfaces_keep_distinct_exact_wire_bytes()
882    -> Result<(), serde_json::Error> {
883        let candidate = EggRewriteCandidateV0 {
884            pass_id: TransformPassKind::CalcReduction.id(),
885            before: "(calc (+ (unit 1 px) (unit 2 px)))".to_string(),
886            after: "(unit 3 px)".to_string(),
887            proof: EggRewriteProofV0::new(
888                false,
889                ObligationFamilyIdV0::ComputedValuePreservation,
890                true,
891                "same-unit calc arithmetic preserves computed value",
892            ),
893        };
894        let compatibility_json = compatibility_analysis_serialized_v0(candidate.clone())?;
895        let canonical_execution =
896            execute_egg_rewrite_with_transform_catalog_analysis(candidate.clone());
897        let canonical_witness = summarize_transform_catalog_analysis_carrier_witness_v0(candidate);
898        let canonical_json = serde_json::to_string(&(canonical_execution, canonical_witness))?;
899        let digest = |bytes: &[u8]| {
900            Sha256::digest(bytes)
901                .iter()
902                .map(|byte| format!("{byte:02x}"))
903                .collect::<String>()
904        };
905        assert_eq!(compatibility_json.len(), 2_050);
906        assert_eq!(
907            digest(compatibility_json.as_bytes()),
908            "eed58058f36a6f0903b3eda4c3d739eb69321b22ba3742ba11c8edc6771a2d82"
909        );
910        assert_eq!(canonical_json.len(), 2_091);
911        assert_eq!(
912            digest(canonical_json.as_bytes()),
913            "9b3144364d808c6052b29ebbfd1fd3d8b7bbc153cb757a62ce4d964104b3adfa"
914        );
915        assert_ne!(compatibility_json, canonical_json);
916        Ok(())
917    }
918
919    #[test]
920    fn transform_catalog_analysis_fills_parallel_egg_analysis_slot() {
921        let (execution, saturation) =
922            execute_egg_rewrite_with_transform_catalog_analysis(EggRewriteCandidateV0 {
923                pass_id: TransformPassKind::CalcReduction.id(),
924                before: "(calc (+ (unit 1 px) (unit 2 px)))".to_string(),
925                after: "(unit 3 px)".to_string(),
926                proof: EggRewriteProofV0::new(
927                    false,
928                    ObligationFamilyIdV0::ComputedValuePreservation,
929                    true,
930                    "same-unit calc arithmetic preserves computed value",
931                ),
932            });
933
934        assert!(execution.accepted);
935        assert_eq!(execution.engine, "egg+transform-catalog-analysis");
936        assert_eq!(saturation.schema_version, "0");
937        assert_eq!(saturation.layer_marker, "enriched-algebraic");
938        assert_eq!(saturation.analysis_slot, "TransformCatalogAnalysis");
939        assert!(saturation.original_unit_analysis_path_preserved);
940        assert_eq!(saturation.differential_fixture_count, 10);
941        assert_eq!(
942            saturation.mechanism_scope,
943            TRANSFORM_CATALOG_MECHANISM_SCOPE_V0
944        );
945        assert_eq!(
946            saturation.product_path_evidence_ready,
947            TRANSFORM_CATALOG_PRODUCT_PATH_EVIDENCE_READY_V0
948        );
949        assert_eq!(
950            saturation.global_transform_theorem_claimed,
951            TRANSFORM_CATALOG_GLOBAL_TRANSFORM_THEOREM_CLAIMED_V0
952        );
953    }
954
955    #[test]
956    fn transform_catalog_analysis_widens_eclass_carriers_under_fixture_witness()
957    -> Result<(), &'static str> {
958        let witness =
959            summarize_transform_catalog_analysis_carrier_witness_v0(EggRewriteCandidateV0 {
960                pass_id: TransformPassKind::CalcReduction.id(),
961                before: "(calc (+ (unit 1 px) (unit 2 px)))".to_string(),
962                after: "(unit 3 px)".to_string(),
963                proof: EggRewriteProofV0::new(
964                    false,
965                    ObligationFamilyIdV0::ComputedValuePreservation,
966                    true,
967                    "same-unit calc arithmetic preserves computed value",
968                ),
969            })
970            .ok_or("carrier witness should be produced for a managed accepted rewrite")?;
971
972        assert_eq!(witness.claim_level, "fixtureWitnessEclassCarrierWidening");
973        assert!(witness.computed_value_carrier_ready);
974        assert!(witness.var_state_carrier_ready);
975        assert!(witness.provenance_carrier_ready);
976        assert!(!witness.theorem_claimed);
977        assert!(witness.extracted_matches_candidate);
978        assert!(
979            witness
980                .root_data
981                .computed_value_carrier
982                .exact_value_candidates
983                .contains(&"3px".to_string())
984        );
985        assert!(
986            witness
987                .root_data
988                .provenance_carrier
989                .enode_kinds
990                .contains(&"add")
991        );
992        Ok(())
993    }
994
995    #[test]
996    fn transform_catalog_analysis_carries_selector_specificity_context() -> Result<(), &'static str>
997    {
998        let witness =
999            summarize_transform_catalog_analysis_carrier_witness_v0(EggRewriteCandidateV0 {
1000                pass_id: TransformPassKind::SelectorIsWhereCompression.id(),
1001                before: "(where (list ready ready))".to_string(),
1002                after: "(where ready)".to_string(),
1003                proof: EggRewriteProofV0::new(
1004                    true,
1005                    ObligationFamilyIdV0::CascadeSafetyFloor,
1006                    true,
1007                    "duplicate :where() argument keeps zero specificity",
1008                ),
1009            })
1010            .ok_or("selector carrier witness should be produced for an accepted rewrite")?;
1011
1012        assert!(witness.specificity_carrier_ready);
1013        assert!(witness.var_state_carrier_ready);
1014        assert!(witness.provenance_carrier_ready);
1015        assert!(!witness.theorem_claimed);
1016        assert!(
1017            witness
1018                .root_data
1019                .specificity_carrier
1020                .zero_specificity_context_seen
1021        );
1022        assert!(
1023            witness
1024                .root_data
1025                .var_state_carrier
1026                .symbol_tokens
1027                .contains(&"ready".to_string())
1028        );
1029        Ok(())
1030    }
1031}