1use 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#[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}