Skip to main content

omena_transform_egg/
lib.rs

1//! Optional e-graph rewrite boundary for Omena CSS transforms.
2//!
3//! Selector and computed-value rewrites are the current e-graph candidates.
4//! This crate keeps their proof requirements explicit without forcing an
5//! e-graph dependency into the core transform path.
6
7use omena_transform_cst::TransformPassKind;
8use omena_transform_passes::{TransformPassPlanV0, plan_transform_passes};
9use serde::Serialize;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct EggRewriteProofV0 {
14    pub specificity_preserved: bool,
15    pub computed_value_preserved: bool,
16    pub provenance_preserved: bool,
17    pub cascade_safe_witness: String,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct EggRewriteCandidateV0 {
23    pub pass_id: &'static str,
24    pub before: String,
25    pub after: String,
26    pub proof: EggRewriteProofV0,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct EggRewriteDecisionV0 {
32    pub schema_version: &'static str,
33    pub product: &'static str,
34    pub pass_id: &'static str,
35    pub accepted: bool,
36    pub blocked_reason: Option<&'static str>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct TransformEggBoundarySummaryV0 {
42    pub schema_version: &'static str,
43    pub product: &'static str,
44    pub managed_pass_ids: Vec<&'static str>,
45    pub optional_engine: &'static str,
46    pub proof_obligations: Vec<&'static str>,
47    pub planner_surface: &'static str,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub struct TransformEggPlanV0 {
53    pub schema_version: &'static str,
54    pub product: &'static str,
55    pub requested_pass_ids: Vec<&'static str>,
56    pub planned_pass_ids: Vec<&'static str>,
57    pub pass_plan: TransformPassPlanV0,
58}
59
60pub fn summarize_omena_transform_egg_boundary() -> TransformEggBoundarySummaryV0 {
61    TransformEggBoundarySummaryV0 {
62        schema_version: "0",
63        product: "omena-transform-egg.boundary",
64        managed_pass_ids: managed_egg_passes().iter().map(|pass| pass.id()).collect(),
65        optional_engine: "egg-compatible equality saturation engine",
66        proof_obligations: vec![
67            "selector rewrites preserve specificity",
68            "calc rewrites preserve computed value",
69            "all rewrites preserve provenance",
70            "all accepted rewrites carry a cascade-safe witness",
71        ],
72        planner_surface: "omena-transform-passes.plan",
73    }
74}
75
76pub fn plan_egg_rewrite_passes(include_selector: bool, include_calc: bool) -> TransformEggPlanV0 {
77    let mut requested_passes = Vec::new();
78    if include_selector {
79        requested_passes.push(TransformPassKind::SelectorIsWhereCompression);
80    }
81    if include_calc {
82        requested_passes.push(TransformPassKind::CalcReduction);
83    }
84    let pass_plan = plan_transform_passes(&requested_passes);
85
86    TransformEggPlanV0 {
87        schema_version: "0",
88        product: "omena-transform-egg.plan",
89        requested_pass_ids: requested_passes.iter().map(|pass| pass.id()).collect(),
90        planned_pass_ids: pass_plan.ordered_pass_ids.clone(),
91        pass_plan,
92    }
93}
94
95pub fn decide_egg_rewrite(candidate: EggRewriteCandidateV0) -> EggRewriteDecisionV0 {
96    let blocked_reason = if !is_managed_egg_pass_id(candidate.pass_id) {
97        Some("pass is not managed by omena-transform-egg")
98    } else if candidate.proof.cascade_safe_witness.is_empty() {
99        Some("missing cascade-safe witness")
100    } else if !candidate.proof.provenance_preserved {
101        Some("rewrite does not preserve provenance")
102    } else if candidate.pass_id == TransformPassKind::SelectorIsWhereCompression.id()
103        && !candidate.proof.specificity_preserved
104    {
105        Some("selector rewrite does not preserve specificity")
106    } else if candidate.pass_id == TransformPassKind::CalcReduction.id()
107        && !candidate.proof.computed_value_preserved
108    {
109        Some("calc rewrite does not preserve computed value")
110    } else {
111        None
112    };
113
114    EggRewriteDecisionV0 {
115        schema_version: "0",
116        product: "omena-transform-egg.decision",
117        pass_id: candidate.pass_id,
118        accepted: blocked_reason.is_none(),
119        blocked_reason,
120    }
121}
122
123fn managed_egg_passes() -> [TransformPassKind; 2] {
124    [
125        TransformPassKind::SelectorIsWhereCompression,
126        TransformPassKind::CalcReduction,
127    ]
128}
129
130fn is_managed_egg_pass_id(pass_id: &str) -> bool {
131    managed_egg_passes().iter().any(|pass| pass.id() == pass_id)
132}
133
134#[cfg(test)]
135mod tests {
136    use super::{
137        EggRewriteCandidateV0, EggRewriteProofV0, decide_egg_rewrite, plan_egg_rewrite_passes,
138        summarize_omena_transform_egg_boundary,
139    };
140    use omena_transform_cst::TransformPassKind;
141
142    #[test]
143    fn exposes_selector_and_calc_optional_egg_boundary() {
144        let boundary = summarize_omena_transform_egg_boundary();
145
146        assert_eq!(boundary.product, "omena-transform-egg.boundary");
147        assert_eq!(
148            boundary.managed_pass_ids,
149            vec!["selector-is-where-compression", "calc-reduction"]
150        );
151        assert_eq!(boundary.proof_obligations.len(), 4);
152    }
153
154    #[test]
155    fn plans_requested_egg_passes_through_transform_pass_planner() {
156        let plan = plan_egg_rewrite_passes(true, true);
157
158        assert_eq!(
159            plan.planned_pass_ids,
160            vec!["selector-is-where-compression", "calc-reduction"]
161        );
162        assert_eq!(plan.pass_plan.violated_dag_edge_count, 0);
163    }
164
165    #[test]
166    fn accepts_selector_rewrite_only_with_specificity_and_provenance_witnesses() {
167        let decision = decide_egg_rewrite(EggRewriteCandidateV0 {
168            pass_id: TransformPassKind::SelectorIsWhereCompression.id(),
169            before: ":is(.a, .b)".to_string(),
170            after: ".a,.b".to_string(),
171            proof: EggRewriteProofV0 {
172                specificity_preserved: true,
173                computed_value_preserved: false,
174                provenance_preserved: true,
175                cascade_safe_witness: "specificity tuple preserved".to_string(),
176            },
177        });
178
179        assert!(decision.accepted);
180        assert_eq!(decision.blocked_reason, None);
181    }
182
183    #[test]
184    fn rejects_calc_rewrite_without_computed_value_witness() {
185        let decision = decide_egg_rewrite(EggRewriteCandidateV0 {
186            pass_id: TransformPassKind::CalcReduction.id(),
187            before: "calc(1rem + 2px)".to_string(),
188            after: "1rem".to_string(),
189            proof: EggRewriteProofV0 {
190                specificity_preserved: false,
191                computed_value_preserved: false,
192                provenance_preserved: true,
193                cascade_safe_witness: "candidate generated".to_string(),
194            },
195        });
196
197        assert!(!decision.accepted);
198        assert_eq!(
199            decision.blocked_reason,
200            Some("calc rewrite does not preserve computed value")
201        );
202    }
203}