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