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