Skip to main content

logicaffeine_compile/optimize/egraph/
convert.rs

1//! AST ⇄ e-graph conversion and the statement-level Architect pass.
2//!
3//! Each expression gets its OWN e-graph (mutation between statements can
4//! never leak equalities), seeded with the Oracle's per-site facts. Two
5//! stability rules keep the seeding sound:
6//!
7//! - **Opaque uniqueness**: every unmodeled subexpression (calls, list
8//!   literals, ranges, …) becomes a FRESH opaque leaf per occurrence —
9//!   two identical calls never unify, so effects are never deduplicated.
10//! - **Fact stability**: Oracle facts are seeded only on subtrees whose
11//!   value cannot change WITHIN one evaluation of the enclosing
12//!   expression — literals, variables (no `Set` can run mid-expression),
13//!   and pure arithmetic over those. `Index`/`Length`/opaque subtrees can
14//!   observe a collection mutated by a sibling call, and two occurrences
15//!   share one e-class, so per-site facts must not be intersected there.
16
17use std::collections::HashMap;
18
19use crate::arena::Arena;
20use crate::ast::stmt::{BinaryOpKind, Expr, Literal, Stmt};
21use crate::intern::{Interner, Symbol};
22use crate::optimize::OracleFacts;
23
24use super::extract::{best_tree, best_tree_filtered, ExtractTree};
25use super::{rules, CompilerEGraph, CompilerENode, NodeId};
26
27pub struct Converter<'a> {
28    pub eg: CompilerEGraph,
29    opaques: Vec<&'a Expr<'a>>,
30    symbols: HashMap<u32, Symbol>,
31    /// Length-range facts staged on Len nodes — applied only when the
32    /// WHOLE converted expression is opaque-free (no call can mutate the
33    /// collection mid-evaluation, so the statement-entry snapshot holds
34    /// for every occurrence).
35    pending_len_facts: Vec<(NodeId, (i64, i64))>,
36    saw_opaque: bool,
37    /// CURRENT version per symbol index (cross-statement runs): a `Set`
38    /// or rebinding bumps its target, so equality never leaks across a
39    /// write. Absent = version 0.
40    versions: HashMap<u32, u32>,
41    /// Symbols read AS COLLECTIONS so far (the collection child of
42    /// Len/Index/Slice/Copy/Contains): any collection mutation bumps them
43    /// ALL — aliasing between collections is invisible here, so contents
44    /// equalities die wholesale while scalar equalities survive.
45    coll_read_vars: std::collections::HashSet<u32>,
46    /// Symbol indices whose VALUE facts (scalar value-kind, integer range,
47    /// length range) must NOT be applied: variables mutated inside the
48    /// enclosing loop. The Oracle records facts per OCCURRENCE, and a loop
49    /// guard like `d * d <= i` carries `d`'s FIRST-ITERATION range — sound to
50    /// read point-wise (v1 does), UNSOUND as a universal e-graph rewrite
51    /// (`d * d → 4` collapsed primes). KIND facts (is-collection) stay; they
52    /// are mutation-immune.
53    suppressed: std::collections::HashSet<u32>,
54}
55
56impl<'a> Converter<'a> {
57    pub fn new() -> Self {
58        Converter {
59            eg: CompilerEGraph::new(),
60            opaques: Vec::new(),
61            symbols: HashMap::new(),
62            pending_len_facts: Vec::new(),
63            saw_opaque: false,
64            versions: HashMap::new(),
65            coll_read_vars: std::collections::HashSet::new(),
66            suppressed: std::collections::HashSet::new(),
67        }
68    }
69
70    fn version_of(&self, ix: u32) -> u32 {
71        self.versions.get(&ix).copied().unwrap_or(0)
72    }
73
74    /// Rebinding / mutation: later reads see a FRESH version.
75    fn bump(&mut self, sym: Symbol) {
76        *self.versions.entry(sym.index() as u32).or_insert(0) += 1;
77    }
78
79    /// A collection's CONTENTS changed somewhere: every symbol ever read
80    /// as a collection gets a fresh version (aliases are indistinguishable
81    /// at this level).
82    fn bump_collection_reads(&mut self) {
83        let vars: Vec<u32> = self.coll_read_vars.iter().copied().collect();
84        for ix in vars {
85            *self.versions.entry(ix).or_insert(0) += 1;
86        }
87    }
88
89    fn note_collection_read(&mut self, collection: &Expr) {
90        if let Expr::Identifier(sym) = collection {
91            self.coll_read_vars.insert(sym.index() as u32);
92        }
93    }
94
95    /// True if `expr` references any suppressed (loop-mutated) variable. Such an
96    /// expression's Oracle facts are a single-iteration snapshot, so they must
97    /// not seed a universal rewrite. Recursive; cheap-exits when nothing is
98    /// suppressed (the common, non-loop case).
99    fn mentions_suppressed(&self, expr: &Expr) -> bool {
100        if self.suppressed.is_empty() {
101            return false;
102        }
103        match expr {
104            Expr::Identifier(sym) => self.suppressed.contains(&(sym.index() as u32)),
105            Expr::BinaryOp { left, right, .. } => {
106                self.mentions_suppressed(left) || self.mentions_suppressed(right)
107            }
108            Expr::Not { operand } => self.mentions_suppressed(operand),
109            Expr::Index { collection, index } => {
110                self.mentions_suppressed(collection) || self.mentions_suppressed(index)
111            }
112            Expr::Length { collection } => self.mentions_suppressed(collection),
113            Expr::Copy { expr } => self.mentions_suppressed(expr),
114            Expr::Slice { collection, start, end } => {
115                self.mentions_suppressed(collection)
116                    || self.mentions_suppressed(start)
117                    || self.mentions_suppressed(end)
118            }
119            Expr::Contains { collection, value } => {
120                self.mentions_suppressed(collection) || self.mentions_suppressed(value)
121            }
122            _ => false,
123        }
124    }
125
126    /// Stage opaque-free-gated facts now that conversion is complete.
127    pub fn finish_facts(&mut self) {
128        if !self.saw_opaque {
129            for (id, (lo, hi)) in std::mem::take(&mut self.pending_len_facts) {
130                self.eg.set_int_range(id, lo, hi);
131            }
132        }
133    }
134
135    fn opaque(&mut self, expr: &'a Expr<'a>) -> (NodeId, bool) {
136        self.saw_opaque = true;
137        let ix = self.opaques.len() as u32;
138        self.opaques.push(expr);
139        let id = self.eg.add(CompilerENode::Opaque(ix));
140        // A list literal is opaque (effects in its items stay verbatim),
141        // but its VALUE is unconditionally a list.
142        if matches!(expr, Expr::List(_)) {
143            self.eg.set_list(id);
144        }
145        (id, false)
146    }
147
148    /// Returns (class, stable). `stable` = the subtree's value cannot
149    /// change within one evaluation of the enclosing expression.
150    pub fn to_node(&mut self, expr: &'a Expr<'a>, facts: Option<&OracleFacts>) -> (NodeId, bool) {
151        let (id, stable) = match expr {
152            Expr::Literal(Literal::Number(n)) => (self.eg.add(CompilerENode::Int(*n)), true),
153            Expr::Literal(Literal::Boolean(b)) => (self.eg.add(CompilerENode::Bool(*b)), true),
154            Expr::Literal(Literal::Float(f)) => {
155                (self.eg.add(CompilerENode::Float(f.to_bits())), true)
156            }
157            Expr::Identifier(sym) => {
158                let ix = sym.index() as u32;
159                self.symbols.insert(ix, *sym);
160                let v = self.version_of(ix);
161                let id = self.eg.add(CompilerENode::Var(ix, v));
162                // KIND facts are mutation-immune (contents change, kinds
163                // never do) — seed them unconditionally.
164                if let Some(f) = facts {
165                    if f.expr_is_tracked_collection(expr) {
166                        self.eg.set_collection(id);
167                    }
168                }
169                (id, true)
170            }
171            // ExactDivide (exact Rational division) has no integer e-node model and must
172            // NEVER be floor-folded by the e-graph — treat the whole division as an opaque
173            // leaf so the optimizer leaves it verbatim for the runtime.
174            Expr::BinaryOp { op: BinaryOpKind::ExactDivide, .. } => self.opaque(expr),
175            // Sequence concatenation has no arithmetic e-node model — leave it opaque so the
176            // optimizer never tries to fold it.
177            Expr::BinaryOp { op: BinaryOpKind::SeqConcat, .. } => self.opaque(expr),
178            // Tolerant float comparison: opaque — no algebraic rewrites apply.
179            Expr::BinaryOp { op: BinaryOpKind::ApproxEq, .. } => self.opaque(expr),
180            // `&`/`|` are type-dispatched (Int bitwise / Set ops): opaque to
181            // the numeric e-graph.
182            Expr::BinaryOp { op: BinaryOpKind::BitAnd | BinaryOpKind::BitOr, .. } => self.opaque(expr),
183            // `**` has no e-node model (BigInt-promoting / Float): opaque.
184            Expr::BinaryOp { op: BinaryOpKind::Pow, .. } => self.opaque(expr),
185            // `//` floors toward -inf (distinct from `Div`'s truncation) and can promote
186            // to BigInt — no e-node model, so it stays opaque (never algebraically rewritten).
187            Expr::BinaryOp { op: BinaryOpKind::FloorDivide, .. } => self.opaque(expr),
188            Expr::BinaryOp { op, left, right } => {
189                let (l, ls) = self.to_node(left, facts);
190                let (r, rs) = self.to_node(right, facts);
191                let node = match op {
192                    BinaryOpKind::Add => CompilerENode::Add(l, r),
193                    BinaryOpKind::Subtract => CompilerENode::Sub(l, r),
194                    BinaryOpKind::Multiply => CompilerENode::Mul(l, r),
195                    BinaryOpKind::Divide => CompilerENode::Div(l, r),
196                    BinaryOpKind::ExactDivide => unreachable!("ExactDivide handled by the opaque arm above"),
197                    BinaryOpKind::Pow => unreachable!("Pow handled by the opaque arm above"),
198                    BinaryOpKind::FloorDivide => unreachable!("FloorDivide handled by the opaque arm above"),
199                    BinaryOpKind::ApproxEq => unreachable!("ApproxEq handled by the opaque arm above"),
200                    BinaryOpKind::BitAnd | BinaryOpKind::BitOr => unreachable!("BitAnd/BitOr handled by the opaque arm above"),
201                    BinaryOpKind::Modulo => CompilerENode::Mod(l, r),
202                    BinaryOpKind::Eq => CompilerENode::Eq(l, r),
203                    BinaryOpKind::NotEq => CompilerENode::Ne(l, r),
204                    BinaryOpKind::Lt => CompilerENode::Lt(l, r),
205                    BinaryOpKind::Gt => CompilerENode::Gt(l, r),
206                    BinaryOpKind::LtEq => CompilerENode::Le(l, r),
207                    BinaryOpKind::GtEq => CompilerENode::Ge(l, r),
208                    BinaryOpKind::And => CompilerENode::And(l, r),
209                    BinaryOpKind::Or => CompilerENode::Or(l, r),
210                    BinaryOpKind::Concat => CompilerENode::Concat(l, r),
211                    BinaryOpKind::SeqConcat => unreachable!("SeqConcat handled by the opaque arm above"),
212                    BinaryOpKind::BitXor => CompilerENode::BitXor(l, r),
213                    BinaryOpKind::Shl => CompilerENode::Shl(l, r),
214                    BinaryOpKind::Shr => CompilerENode::Shr(l, r),
215                };
216                (self.eg.add(node), ls && rs)
217            }
218            Expr::Not { operand } => {
219                let (o, os) = self.to_node(operand, facts);
220                (self.eg.add(CompilerENode::Not(o)), os)
221            }
222            Expr::Index { collection, index } => {
223                self.note_collection_read(collection);
224                let (c, _) = self.to_node(collection, facts);
225                let (i, _) = self.to_node(index, facts);
226                // Collection contents can change under a sibling call.
227                (self.eg.add(CompilerENode::Index(c, i)), false)
228            }
229            Expr::Length { collection } => {
230                self.note_collection_read(collection);
231                let (c, _) = self.to_node(collection, facts);
232                let id = self.eg.add(CompilerENode::Len(c));
233                // Length-range facts hold at statement entry; they apply
234                // only if NO opaque (call) can mutate mid-evaluation —
235                // staged here, applied by finish_facts().
236                if let Some(f) = facts {
237                    if !self.mentions_suppressed(collection) {
238                        if let Some((lo, hi)) = f.expr_len_range(collection) {
239                            self.pending_len_facts.push((id, (lo, hi)));
240                        }
241                    }
242                }
243                (id, false)
244            }
245            Expr::Copy { expr: inner } => {
246                self.note_collection_read(inner);
247                let (c, _) = self.to_node(inner, facts);
248                let id = self.eg.add(CompilerENode::Copy(c));
249                // Copy preserves its operand's kind.
250                if self.eg.proven_list(c) {
251                    self.eg.set_list(id);
252                } else if self.eg.proven_collection(c) {
253                    self.eg.set_collection(id);
254                }
255                (id, false)
256            }
257            Expr::Slice { collection, start, end } => {
258                self.note_collection_read(collection);
259                let (c, _) = self.to_node(collection, facts);
260                let (s, _) = self.to_node(start, facts);
261                let (e, _) = self.to_node(end, facts);
262                (self.eg.add(CompilerENode::Slice(c, s, e)), false)
263            }
264            Expr::Contains { collection, value } => {
265                self.note_collection_read(collection);
266                let (c, _) = self.to_node(collection, facts);
267                let (v, _) = self.to_node(value, facts);
268                (self.eg.add(CompilerENode::Contains(c, v)), false)
269            }
270            other => self.opaque(other),
271        };
272        if stable {
273            if let Some(f) = facts {
274                if !self.mentions_suppressed(expr) {
275                    if let Some(k) = f.expr_scalar(expr) {
276                        self.eg.set_scalar(id, k);
277                    }
278                    if let Some((lo, hi)) = f.expr_int_range(expr) {
279                        self.eg.set_int_range(id, lo, hi);
280                    }
281                }
282            }
283        }
284        (id, stable)
285    }
286
287    pub fn tree_to_expr(
288        &self,
289        t: &ExtractTree,
290        arena: &'a Arena<Expr<'a>>,
291    ) -> &'a Expr<'a> {
292        let bin = |op: BinaryOpKind, s: &Self| -> &'a Expr<'a> {
293            arena.alloc(Expr::BinaryOp {
294                op,
295                left: s.tree_to_expr(&t.children[0], arena),
296                right: s.tree_to_expr(&t.children[1], arena),
297            })
298        };
299        match t.node {
300            CompilerENode::Int(n) => arena.alloc(Expr::Literal(Literal::Number(n))),
301            CompilerENode::Bool(b) => arena.alloc(Expr::Literal(Literal::Boolean(b))),
302            CompilerENode::Float(bits) => {
303                arena.alloc(Expr::Literal(Literal::Float(f64::from_bits(bits))))
304            }
305            CompilerENode::Var(ix, _) => arena.alloc(Expr::Identifier(self.symbols[&ix])),
306            CompilerENode::Opaque(ix) => self.opaques[ix as usize],
307            CompilerENode::Not(_) => arena.alloc(Expr::Not {
308                operand: self.tree_to_expr(&t.children[0], arena),
309            }),
310            CompilerENode::Len(_) => arena.alloc(Expr::Length {
311                collection: self.tree_to_expr(&t.children[0], arena),
312            }),
313            CompilerENode::Index(..) => arena.alloc(Expr::Index {
314                collection: self.tree_to_expr(&t.children[0], arena),
315                index: self.tree_to_expr(&t.children[1], arena),
316            }),
317            CompilerENode::Copy(_) => arena.alloc(Expr::Copy {
318                expr: self.tree_to_expr(&t.children[0], arena),
319            }),
320            CompilerENode::Slice(..) => arena.alloc(Expr::Slice {
321                collection: self.tree_to_expr(&t.children[0], arena),
322                start: self.tree_to_expr(&t.children[1], arena),
323                end: self.tree_to_expr(&t.children[2], arena),
324            }),
325            CompilerENode::Contains(..) => arena.alloc(Expr::Contains {
326                collection: self.tree_to_expr(&t.children[0], arena),
327                value: self.tree_to_expr(&t.children[1], arena),
328            }),
329            CompilerENode::Add(..) => bin(BinaryOpKind::Add, self),
330            CompilerENode::Sub(..) => bin(BinaryOpKind::Subtract, self),
331            CompilerENode::Mul(..) => bin(BinaryOpKind::Multiply, self),
332            CompilerENode::Div(..) => bin(BinaryOpKind::Divide, self),
333            CompilerENode::Mod(..) => bin(BinaryOpKind::Modulo, self),
334            CompilerENode::Shl(..) => bin(BinaryOpKind::Shl, self),
335            CompilerENode::Shr(..) => bin(BinaryOpKind::Shr, self),
336            CompilerENode::BitXor(..) => bin(BinaryOpKind::BitXor, self),
337            CompilerENode::BitAnd(..) => bin(BinaryOpKind::BitAnd, self),
338            CompilerENode::BitOr(..) => bin(BinaryOpKind::BitOr, self),
339            CompilerENode::And(..) => bin(BinaryOpKind::And, self),
340            CompilerENode::Or(..) => bin(BinaryOpKind::Or, self),
341            CompilerENode::Eq(..) => bin(BinaryOpKind::Eq, self),
342            CompilerENode::Ne(..) => bin(BinaryOpKind::NotEq, self),
343            CompilerENode::Lt(..) => bin(BinaryOpKind::Lt, self),
344            CompilerENode::Le(..) => bin(BinaryOpKind::LtEq, self),
345            CompilerENode::Gt(..) => bin(BinaryOpKind::Gt, self),
346            CompilerENode::Ge(..) => bin(BinaryOpKind::GtEq, self),
347            CompilerENode::Concat(..) => bin(BinaryOpKind::Concat, self),
348        }
349    }
350}
351
352/// Structural equality over the MODELED fragment (bit-exact floats);
353/// anything else compares by pointer (opaques re-emit the original).
354fn expr_struct_eq(a: &Expr, b: &Expr) -> bool {
355    if std::ptr::eq(a, b) {
356        return true;
357    }
358    match (a, b) {
359        (Expr::Literal(Literal::Number(x)), Expr::Literal(Literal::Number(y))) => x == y,
360        (Expr::Literal(Literal::Boolean(x)), Expr::Literal(Literal::Boolean(y))) => x == y,
361        (Expr::Literal(Literal::Float(x)), Expr::Literal(Literal::Float(y))) => {
362            x.to_bits() == y.to_bits()
363        }
364        (Expr::Identifier(x), Expr::Identifier(y)) => x == y,
365        (
366            Expr::BinaryOp { op: oa, left: la, right: ra },
367            Expr::BinaryOp { op: ob, left: lb, right: rb },
368        ) => oa == ob && expr_struct_eq(la, lb) && expr_struct_eq(ra, rb),
369        (Expr::Not { operand: x }, Expr::Not { operand: y }) => expr_struct_eq(x, y),
370        (Expr::Length { collection: x }, Expr::Length { collection: y }) => expr_struct_eq(x, y),
371        (
372            Expr::Index { collection: ca, index: ia },
373            Expr::Index { collection: cb, index: ib },
374        ) => expr_struct_eq(ca, cb) && expr_struct_eq(ia, ib),
375        (Expr::Copy { expr: a }, Expr::Copy { expr: b }) => expr_struct_eq(a, b),
376        (
377            Expr::Slice { collection: ca, start: sa, end: ea },
378            Expr::Slice { collection: cb, start: sb, end: eb },
379        ) => expr_struct_eq(ca, cb) && expr_struct_eq(sa, sb) && expr_struct_eq(ea, eb),
380        (
381            Expr::Contains { collection: ca, value: va },
382            Expr::Contains { collection: cb, value: vb },
383        ) => expr_struct_eq(ca, cb) && expr_struct_eq(va, vb),
384        _ => false,
385    }
386}
387
388/// Does the expression contain anything the e-graph can act on?
389fn worth_modeling(expr: &Expr) -> bool {
390    match expr {
391        Expr::BinaryOp { .. } | Expr::Not { .. } => true,
392        // A bare `length of xs` / `item i of xs` has nothing to improve,
393        // but the same reads over a COPY or SLICE are fusion's home turf.
394        Expr::Index { collection, index } => {
395            fusable(collection) || worth_modeling(collection) || worth_modeling(index)
396        }
397        Expr::Length { collection } => fusable(collection) || worth_modeling(collection),
398        Expr::Copy { expr } => fusable(expr) || worth_modeling(expr),
399        Expr::Slice { collection, start, end } => {
400            worth_modeling(collection) || worth_modeling(start) || worth_modeling(end)
401        }
402        Expr::Contains { collection, value } => {
403            worth_modeling(collection) || worth_modeling(value)
404        }
405        _ => false,
406    }
407}
408
409/// Operands whose shape the fusion algebra can act on directly.
410fn fusable(expr: &Expr) -> bool {
411    matches!(expr, Expr::Copy { .. } | Expr::Slice { .. })
412}
413
414/// Saturate ONE expression against the rule set and extract the cheapest
415/// equivalent. Returns the original pointer when nothing improved.
416pub fn simplify_expr<'a>(
417    expr: &'a Expr<'a>,
418    facts: &OracleFacts,
419    rule_set: &[rules::Rewrite],
420    arena: &'a Arena<Expr<'a>>,
421) -> &'a Expr<'a> {
422    simplify_expr_in(expr, facts, rule_set, arena, &std::collections::HashSet::new())
423}
424
425/// As [`simplify_expr`], but suppresses value facts for the `suppressed`
426/// (loop-mutated) symbols — their Oracle per-occurrence range is iteration-
427/// specific and may not seed a universal rewrite.
428fn simplify_expr_in<'a>(
429    expr: &'a Expr<'a>,
430    facts: &OracleFacts,
431    rule_set: &[rules::Rewrite],
432    arena: &'a Arena<Expr<'a>>,
433    suppressed: &std::collections::HashSet<u32>,
434) -> &'a Expr<'a> {
435    if !worth_modeling(expr) {
436        return expr;
437    }
438    let mut cv = Converter::new();
439    cv.suppressed = suppressed.clone();
440    let (root, _) = cv.to_node(expr, Some(facts));
441    cv.finish_facts();
442    cv.eg.saturate(rule_set);
443    let tree = best_tree(&mut cv.eg, root);
444    let rebuilt = cv.tree_to_expr(&tree, arena);
445    if expr_struct_eq(rebuilt, expr) {
446        expr
447    } else {
448        rebuilt
449    }
450}
451
452/// The Architect statement pass: STRAIGHT-LINE RUNS of simple statements
453/// share one e-graph (a Let merges its variable with its defining
454/// expression's class, so later statements extract against everything
455/// proven so far — the GVN replacement); everything else falls back to
456/// per-expression graphs and ENDS the run. Versioning makes mutation
457/// kills structural.
458pub fn egraph_stmts<'a>(
459    stmts: Vec<Stmt<'a>>,
460    expr_arena: &'a Arena<Expr<'a>>,
461    stmt_arena: &'a Arena<Stmt<'a>>,
462    interner: &mut Interner,
463) -> Vec<Stmt<'a>> {
464    let facts = crate::optimize::oracle_analyze_with(&stmts, interner);
465    let rule_set = rules::all();
466    walk_stmts(stmts, &facts, &rule_set, expr_arena, stmt_arena, &std::collections::HashSet::new())
467}
468
469/// Statements a cross-statement run can model EXACTLY (effects and kills
470/// understood). Anything else flushes the run.
471fn run_modeled(s: &Stmt) -> bool {
472    matches!(
473        s,
474        Stmt::Let { .. }
475            | Stmt::Set { .. }
476            | Stmt::Show { .. }
477            | Stmt::RuntimeAssert { .. }
478            | Stmt::Return { .. }
479            | Stmt::Push { .. }
480            | Stmt::SetIndex { .. }
481            | Stmt::Call { .. }
482    )
483}
484
485/// One extraction site inside a run: the converted root, the VERSION
486/// SNAPSHOT it may reference, the opaque indices it owns, and the
487/// original expression (the fallback and the no-change comparator).
488struct SiteRec<'a> {
489    root: NodeId,
490    ctx: HashMap<u32, u32>,
491    opq: (usize, usize),
492    original: &'a Expr<'a>,
493}
494
495fn walk_stmts<'a>(
496    stmts: Vec<Stmt<'a>>,
497    facts: &OracleFacts,
498    rule_set: &[rules::Rewrite],
499    expr_arena: &'a Arena<Expr<'a>>,
500    stmt_arena: &'a Arena<Stmt<'a>>,
501    suppressed: &std::collections::HashSet<u32>,
502) -> Vec<Stmt<'a>> {
503    let mut out: Vec<Stmt<'a>> = Vec::new();
504    let mut run: Vec<Stmt<'a>> = Vec::new();
505    for s in stmts {
506        if run_modeled(&s) {
507            run.push(s);
508        } else {
509            flush_run(&mut run, &mut out, facts, rule_set, expr_arena, suppressed);
510            out.push(walk_stmt(s, facts, rule_set, expr_arena, stmt_arena, suppressed));
511        }
512    }
513    flush_run(&mut run, &mut out, facts, rule_set, expr_arena, suppressed);
514    out
515}
516
517/// Convert a run's expression sites into ONE shared graph (with version
518/// bookkeeping per statement), saturate once, then rebuild every
519/// statement from its site's filtered extraction.
520fn flush_run<'a>(
521    run: &mut Vec<Stmt<'a>>,
522    out: &mut Vec<Stmt<'a>>,
523    facts: &OracleFacts,
524    rule_set: &[rules::Rewrite],
525    expr_arena: &'a Arena<Expr<'a>>,
526    suppressed: &std::collections::HashSet<u32>,
527) {
528    if run.is_empty() {
529        return;
530    }
531    let stmts = std::mem::take(run);
532    let mut cv = Converter::new();
533    cv.suppressed = suppressed.clone();
534    let mut sites: Vec<SiteRec<'a>> = Vec::new();
535    // Per statement: the site indices feeding its rebuild, in field order.
536    let mut plans: Vec<Vec<usize>> = Vec::with_capacity(stmts.len());
537
538    let mut convert_site = |cv: &mut Converter<'a>,
539                            sites: &mut Vec<SiteRec<'a>>,
540                            e: &'a Expr<'a>|
541     -> usize {
542        let opq_start = cv.opaques.len();
543        let (root, _) = cv.to_node(e, Some(facts));
544        let opq_end = cv.opaques.len();
545        sites.push(SiteRec {
546            root,
547            ctx: cv.versions.clone(),
548            opq: (opq_start, opq_end),
549            original: e,
550        });
551        sites.len() - 1
552    };
553
554    for s in &stmts {
555        let mut plan: Vec<usize> = Vec::new();
556        match s {
557            Stmt::Let { var, value, .. } => {
558                let site = convert_site(&mut cv, &mut sites, value);
559                plan.push(site);
560                let had_opaques = sites[site].opq.0 != sites[site].opq.1;
561                cv.bump(*var);
562                if !had_opaques {
563                    let ix = var.index() as u32;
564                    cv.symbols.insert(ix, *var);
565                    let v = cv.version_of(ix);
566                    let vnode = cv.eg.add(CompilerENode::Var(ix, v));
567                    let root = sites[site].root;
568                    cv.eg.union(vnode, root);
569                } else {
570                    // The binding's value went through a call — its
571                    // collection arguments may have been resized.
572                    cv.bump_collection_reads();
573                }
574            }
575            Stmt::Set { target, value } => {
576                let site = convert_site(&mut cv, &mut sites, value);
577                plan.push(site);
578                let had_opaques = sites[site].opq.0 != sites[site].opq.1;
579                cv.bump(*target);
580                if !had_opaques {
581                    let ix = target.index() as u32;
582                    cv.symbols.insert(ix, *target);
583                    let v = cv.version_of(ix);
584                    let vnode = cv.eg.add(CompilerENode::Var(ix, v));
585                    let root = sites[site].root;
586                    cv.eg.union(vnode, root);
587                } else {
588                    cv.bump_collection_reads();
589                }
590            }
591            Stmt::Show { object, .. } => {
592                plan.push(convert_site(&mut cv, &mut sites, object));
593                // Show stringifies arbitrary values through an opaque-free
594                // read — no kills.
595            }
596            Stmt::RuntimeAssert { condition, .. } => {
597                plan.push(convert_site(&mut cv, &mut sites, condition));
598            }
599            Stmt::Return { value } => {
600                if let Some(v) = value {
601                    plan.push(convert_site(&mut cv, &mut sites, v));
602                }
603            }
604            Stmt::Push { value, collection } => {
605                plan.push(convert_site(&mut cv, &mut sites, value));
606                // Contents change: every collection read goes stale.
607                if let Expr::Identifier(sym) = collection {
608                    cv.bump(*sym);
609                }
610                cv.bump_collection_reads();
611            }
612            Stmt::SetIndex { collection, index, value } => {
613                plan.push(convert_site(&mut cv, &mut sites, index));
614                plan.push(convert_site(&mut cv, &mut sites, value));
615                if let Expr::Identifier(sym) = collection {
616                    cv.bump(*sym);
617                }
618                cv.bump_collection_reads();
619            }
620            Stmt::Call { args, .. } => {
621                for a in args {
622                    plan.push(convert_site(&mut cv, &mut sites, a));
623                }
624                // The callee may resize any collection it can reach.
625                cv.bump_collection_reads();
626            }
627            _ => unreachable!("run_modeled admitted an unhandled statement"),
628        }
629        plans.push(plan);
630    }
631
632    cv.finish_facts();
633    cv.eg.saturate(rule_set);
634
635    let mut extract = |cv: &mut Converter<'a>, site: usize| -> &'a Expr<'a> {
636        let rec = &sites[site];
637        if !worth_modeling(rec.original) {
638            return rec.original;
639        }
640        let ctx = rec.ctx.clone();
641        let (lo, hi) = rec.opq;
642        let admissible = move |n: &CompilerENode| match *n {
643            CompilerENode::Var(ix, v) => ctx.get(&ix).copied().unwrap_or(0) == v,
644            CompilerENode::Opaque(j) => (lo..hi).contains(&(j as usize)),
645            _ => true,
646        };
647        match best_tree_filtered(&mut cv.eg, rec.root, &admissible) {
648            Some(tree) => {
649                let rebuilt = cv.tree_to_expr(&tree, expr_arena);
650                if expr_struct_eq(rebuilt, rec.original) {
651                    rec.original
652                } else {
653                    rebuilt
654                }
655            }
656            None => rec.original,
657        }
658    };
659
660    for (s, plan) in stmts.into_iter().zip(plans) {
661        let rebuilt = match s {
662            Stmt::Let { var, ty, value: _, mutable } => Stmt::Let {
663                var,
664                ty,
665                value: extract(&mut cv, plan[0]),
666                mutable,
667            },
668            Stmt::Set { target, value: _ } => {
669                Stmt::Set { target, value: extract(&mut cv, plan[0]) }
670            }
671            Stmt::Show { object: _, recipient } => {
672                Stmt::Show { object: extract(&mut cv, plan[0]), recipient }
673            }
674            Stmt::RuntimeAssert { condition: _ , hard } => {
675                Stmt::RuntimeAssert { condition: extract(&mut cv, plan[0]) , hard }
676            }
677            Stmt::Return { value } => Stmt::Return {
678                value: value.map(|_| extract(&mut cv, plan[0])),
679            },
680            Stmt::Push { value: _, collection } => {
681                Stmt::Push { value: extract(&mut cv, plan[0]), collection }
682            }
683            Stmt::SetIndex { collection, index: _, value: _ } => Stmt::SetIndex {
684                collection,
685                index: extract(&mut cv, plan[0]),
686                value: extract(&mut cv, plan[1]),
687            },
688            Stmt::Call { function, args } => Stmt::Call {
689                function,
690                args: (0..args.len()).map(|k| extract(&mut cv, plan[k])).collect(),
691            },
692            other => other,
693        };
694        out.push(rebuilt);
695    }
696}
697
698fn walk_block<'a>(
699    block: &'a [Stmt<'a>],
700    facts: &OracleFacts,
701    rule_set: &[rules::Rewrite],
702    expr_arena: &'a Arena<Expr<'a>>,
703    stmt_arena: &'a Arena<Stmt<'a>>,
704    suppressed: &std::collections::HashSet<u32>,
705) -> &'a [Stmt<'a>] {
706    let walked = walk_stmts(
707        block.to_vec(),
708        facts,
709        rule_set,
710        expr_arena,
711        stmt_arena,
712        suppressed,
713    );
714    stmt_arena.alloc_slice(walked)
715}
716
717/// Accumulate the symbol indices ASSIGNED or BOUND anywhere in `stmts`
718/// (recursively): `Set`/`Let` targets, `Pop`/`Read` bindings, and the
719/// collections that `Push`/`Pop`/`Add`/`Remove`/`SetIndex`/`SetField` mutate.
720/// A variable in this set is loop-variant, so inside the loop its Oracle
721/// per-occurrence facts must not drive a universal rewrite.
722fn collect_mutated(stmts: &[Stmt], out: &mut std::collections::HashSet<u32>) {
723    for s in stmts {
724        match s {
725            Stmt::Set { target, .. } => {
726                out.insert(target.index() as u32);
727            }
728            Stmt::Let { var, .. } => {
729                out.insert(var.index() as u32);
730            }
731            Stmt::If { then_block, else_block, .. } => {
732                collect_mutated(then_block, out);
733                if let Some(e) = else_block {
734                    collect_mutated(e, out);
735                }
736            }
737            Stmt::While { body, .. }
738            | Stmt::Repeat { body, .. }
739            | Stmt::Zone { body, .. } => collect_mutated(body, out),
740            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => collect_mutated(tasks, out),
741            Stmt::Inspect { arms, .. } => {
742                for a in arms {
743                    collect_mutated(a.body, out);
744                }
745            }
746            Stmt::Push { collection, .. }
747            | Stmt::Add { collection, .. }
748            | Stmt::Remove { collection, .. }
749            | Stmt::SetIndex { collection, .. } => {
750                if let Expr::Identifier(sym) = collection {
751                    out.insert(sym.index() as u32);
752                }
753            }
754            Stmt::Pop { collection, into } => {
755                if let Expr::Identifier(sym) = collection {
756                    out.insert(sym.index() as u32);
757                }
758                if let Some(v) = into {
759                    out.insert(v.index() as u32);
760                }
761            }
762            Stmt::SetField { object, .. } => {
763                if let Expr::Identifier(sym) = object {
764                    out.insert(sym.index() as u32);
765                }
766            }
767            Stmt::ReadFrom { var, .. } => {
768                out.insert(var.index() as u32);
769            }
770            _ => {}
771        }
772    }
773}
774
775fn walk_stmt<'a>(
776    stmt: Stmt<'a>,
777    facts: &OracleFacts,
778    rule_set: &[rules::Rewrite],
779    expr_arena: &'a Arena<Expr<'a>>,
780    stmt_arena: &'a Arena<Stmt<'a>>,
781    suppressed: &std::collections::HashSet<u32>,
782) -> Stmt<'a> {
783    let se = |e: &'a Expr<'a>| simplify_expr_in(e, facts, rule_set, expr_arena, suppressed);
784    match stmt {
785        Stmt::Let { var, ty, value, mutable } => {
786            Stmt::Let { var, ty, value: se(value), mutable }
787        }
788        Stmt::Set { target, value } => Stmt::Set { target, value: se(value) },
789        Stmt::If { cond, then_block, else_block } => Stmt::If {
790            cond: se(cond),
791            then_block: walk_block(then_block, facts, rule_set, expr_arena, stmt_arena, suppressed),
792            else_block: else_block
793                .map(|b| walk_block(b, facts, rule_set, expr_arena, stmt_arena, suppressed)),
794        },
795        Stmt::While { cond, body, decreasing } => {
796            // Inside the loop, every variable the body mutates is loop-variant;
797            // its Oracle per-occurrence facts (a single-iteration snapshot) must
798            // not seed a universal rewrite — `cond` is re-evaluated each pass.
799            let mut inner = suppressed.clone();
800            collect_mutated(body, &mut inner);
801            Stmt::While {
802                cond: simplify_expr_in(cond, facts, rule_set, expr_arena, &inner),
803                body: walk_block(body, facts, rule_set, expr_arena, stmt_arena, &inner),
804                decreasing,
805            }
806        }
807        Stmt::Repeat { pattern, iterable, body } => {
808            // `iterable` is evaluated ONCE in the outer context; the body runs
809            // per element, so loop-mutated vars are suppressed within it.
810            let mut inner = suppressed.clone();
811            collect_mutated(body, &mut inner);
812            Stmt::Repeat {
813                pattern,
814                iterable: se(iterable),
815                body: walk_block(body, facts, rule_set, expr_arena, stmt_arena, &inner),
816            }
817        }
818        Stmt::FunctionDef {
819            name,
820            params,
821            generics,
822            body,
823            return_type,
824            is_native,
825            native_path,
826            is_exported,
827            export_target,
828            opt_flags,
829        } => Stmt::FunctionDef {
830            name,
831            params,
832            generics,
833            body: walk_block(body, facts, rule_set, expr_arena, stmt_arena, suppressed),
834            return_type,
835            is_native,
836            native_path,
837            is_exported,
838            export_target,
839            opt_flags,
840        },
841        Stmt::Show { object, recipient } => Stmt::Show { object: se(object), recipient },
842        Stmt::Return { value } => Stmt::Return { value: value.map(se) },
843        Stmt::RuntimeAssert { condition, hard } => Stmt::RuntimeAssert { condition: se(condition) , hard },
844        Stmt::Push { value, collection } => Stmt::Push { value: se(value), collection },
845        Stmt::SetField { object, field, value } => {
846            Stmt::SetField { object, field, value: se(value) }
847        }
848        Stmt::SetIndex { collection, index, value } => {
849            Stmt::SetIndex { collection, index: se(index), value: se(value) }
850        }
851        Stmt::Call { function, args } => Stmt::Call {
852            function,
853            args: args.into_iter().map(se).collect(),
854        },
855        Stmt::Give { object, recipient } => {
856            Stmt::Give { object: se(object), recipient: se(recipient) }
857        }
858        Stmt::Inspect { target, arms, has_otherwise } => Stmt::Inspect {
859            target: se(target),
860            arms: arms
861                .into_iter()
862                .map(|arm| crate::ast::stmt::MatchArm {
863                    enum_name: arm.enum_name,
864                    variant: arm.variant,
865                    bindings: arm.bindings,
866                    body: walk_block(arm.body, facts, rule_set, expr_arena, stmt_arena, suppressed),
867                })
868                .collect(),
869            has_otherwise,
870        },
871        Stmt::Pop { collection, into } => Stmt::Pop { collection: se(collection), into },
872        Stmt::Add { value, collection } => {
873            Stmt::Add { value: se(value), collection: se(collection) }
874        }
875        Stmt::Remove { value, collection } => {
876            Stmt::Remove { value: se(value), collection: se(collection) }
877        }
878        Stmt::Zone { name, capacity, source_file, body } => Stmt::Zone {
879            name,
880            capacity,
881            source_file,
882            body: walk_block(body, facts, rule_set, expr_arena, stmt_arena, suppressed),
883        },
884        Stmt::Concurrent { tasks } => Stmt::Concurrent {
885            tasks: walk_block(tasks, facts, rule_set, expr_arena, stmt_arena, suppressed),
886        },
887        Stmt::Parallel { tasks } => Stmt::Parallel {
888            tasks: walk_block(tasks, facts, rule_set, expr_arena, stmt_arena, suppressed),
889        },
890        Stmt::WriteFile { content, path } => {
891            Stmt::WriteFile { content: se(content), path: se(path) }
892        }
893        Stmt::SendMessage { message, destination, compression, cached, unchecked, layout, shared, computed, indexed, deduped } => {
894            Stmt::SendMessage { message: se(message), destination: se(destination), compression, cached, unchecked, layout, shared, computed, indexed, deduped }
895        }
896        Stmt::StreamMessage { values, destination } => {
897            Stmt::StreamMessage { values: se(values), destination: se(destination) }
898        }
899        Stmt::IncreaseCrdt { object, field, amount } => {
900            Stmt::IncreaseCrdt { object: se(object), field, amount: se(amount) }
901        }
902        Stmt::DecreaseCrdt { object, field, amount } => {
903            Stmt::DecreaseCrdt { object: se(object), field, amount: se(amount) }
904        }
905        Stmt::Sleep { milliseconds } => Stmt::Sleep { milliseconds: se(milliseconds) },
906        Stmt::MergeCrdt { source, target } => {
907            Stmt::MergeCrdt { source: se(source), target: se(target) }
908        }
909        other => other,
910    }
911}