Skip to main content

logicaffeine_compile/optimize/
abstract_interp.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::arena::Arena;
4use crate::ast::stmt::{BinaryOpKind, Expr, Literal, MatchArm, Pattern, Stmt};
5use crate::intern::Symbol;
6
7/// Per-collection ELEMENT-TYPE tracking: a read `item k of arr` carries `arr`'s
8/// proven element type (the join of every value written into it). A
9/// homogeneously-typed collection's reads gain a concrete scalar kind, which the
10/// magic-reciprocal modulo gate (`(... + ...) % c`) consumes to replace idiv.
11/// PROMOTED 2026-06-21: default ON (kill-switch LOGOS_ELEM_TYPE=0) — coins -11%
12/// on the faithful interleaved A/B, all 33 benchmarks bit-identical, no regression
13/// (a spurious histogram +2.4% on best-of-min was confirmed noise — identical raw
14/// distributions). With it off `eval_type` treats an index read as `Top` exactly
15/// as before, so the byte output is unchanged.
16fn elem_type_enabled() -> bool {
17    crate::optimize::active_config().is_on(crate::optimization::Opt::ElemType)
18}
19
20#[derive(Clone, Debug, PartialEq)]
21enum Bound {
22    NegInf,
23    Finite(i64),
24    PosInf,
25}
26
27impl Bound {
28    fn add(&self, other: &Bound) -> Bound {
29        match (self, other) {
30            (Bound::Finite(a), Bound::Finite(b)) => {
31                match a.checked_add(*b) {
32                    Some(r) => Bound::Finite(r),
33                    None => if *a > 0 { Bound::PosInf } else { Bound::NegInf },
34                }
35            }
36            (Bound::PosInf, Bound::NegInf) | (Bound::NegInf, Bound::PosInf) => Bound::NegInf,
37            (Bound::PosInf, _) | (_, Bound::PosInf) => Bound::PosInf,
38            (Bound::NegInf, _) | (_, Bound::NegInf) => Bound::NegInf,
39        }
40    }
41
42    fn sub(&self, other: &Bound) -> Bound {
43        match (self, other) {
44            (Bound::Finite(a), Bound::Finite(b)) => {
45                match a.checked_sub(*b) {
46                    Some(r) => Bound::Finite(r),
47                    None => if *a > 0 { Bound::PosInf } else { Bound::NegInf },
48                }
49            }
50            (Bound::PosInf, Bound::PosInf) | (Bound::NegInf, Bound::NegInf) => Bound::NegInf,
51            (Bound::PosInf, _) | (_, Bound::NegInf) => Bound::PosInf,
52            (Bound::NegInf, _) | (_, Bound::PosInf) => Bound::NegInf,
53        }
54    }
55
56    fn mul(&self, other: &Bound) -> Bound {
57        // An exact-zero endpoint annihilates: every product with it is 0
58        // (this keeps `0 * [c, +inf]` at 0 rather than collapsing to top).
59        if matches!(self, Bound::Finite(0)) || matches!(other, Bound::Finite(0)) {
60            return Bound::Finite(0);
61        }
62        if let (Bound::Finite(a), Bound::Finite(b)) = (self, other) {
63            return match a.checked_mul(*b) {
64                Some(r) => Bound::Finite(r),
65                None => {
66                    if (*a > 0) == (*b > 0) {
67                        Bound::PosInf
68                    } else {
69                        Bound::NegInf
70                    }
71                }
72            };
73        }
74        // At least one infinite, neither zero — the result's sign is the
75        // product of the operands' signs.
76        let positive = |x: &Bound| match x {
77            Bound::PosInf => true,
78            Bound::NegInf => false,
79            Bound::Finite(v) => *v > 0,
80        };
81        if positive(self) == positive(other) {
82            Bound::PosInf
83        } else {
84            Bound::NegInf
85        }
86    }
87
88    /// Truncating division by a nonzero constant `k` (toward zero, matching
89    /// Rust `i64` `/`). An infinite endpoint divided by a finite divisor stays
90    /// infinite, its sign flipping when `k < 0`.
91    fn div_by(&self, k: i64) -> Bound {
92        match self {
93            Bound::Finite(a) => Bound::Finite(a / k),
94            Bound::NegInf => {
95                if k > 0 {
96                    Bound::NegInf
97                } else {
98                    Bound::PosInf
99                }
100            }
101            Bound::PosInf => {
102                if k > 0 {
103                    Bound::PosInf
104                } else {
105                    Bound::NegInf
106                }
107            }
108        }
109    }
110
111    /// Arithmetic right shift by a constant `k ∈ [0, 63]` — floor-division by
112    /// `2^k`, MONOTONE non-decreasing, so an interval's endpoints map straight
113    /// through. Infinite endpoints stay infinite (their magnitude only shrinks).
114    fn shr_by(&self, k: u32) -> Bound {
115        match self {
116            Bound::Finite(a) => Bound::Finite(a >> k),
117            Bound::NegInf => Bound::NegInf,
118            Bound::PosInf => Bound::PosInf,
119        }
120    }
121
122    fn cmp_bound(&self, other: &Self) -> std::cmp::Ordering {
123        match (self, other) {
124            (Bound::NegInf, Bound::NegInf) => std::cmp::Ordering::Equal,
125            (Bound::NegInf, _) => std::cmp::Ordering::Less,
126            (_, Bound::NegInf) => std::cmp::Ordering::Greater,
127            (Bound::PosInf, Bound::PosInf) => std::cmp::Ordering::Equal,
128            (Bound::PosInf, _) => std::cmp::Ordering::Greater,
129            (_, Bound::PosInf) => std::cmp::Ordering::Less,
130            (Bound::Finite(a), Bound::Finite(b)) => a.cmp(b),
131        }
132    }
133
134    fn min_bound(a: &Bound, b: &Bound) -> Bound {
135        if a.cmp_bound(b) == std::cmp::Ordering::Less { a.clone() } else { b.clone() }
136    }
137
138    fn max_bound(a: &Bound, b: &Bound) -> Bound {
139        if a.cmp_bound(b) == std::cmp::Ordering::Greater { a.clone() } else { b.clone() }
140    }
141}
142
143/// A bounded lattice with the operations abstract interpretation needs.
144///
145/// Each abstract domain of the Oracle (intervals, types, collection shapes,
146/// nullability, aliasing) implements this. The product lattice combines them
147/// componentwise. `widen` accelerates ascending chains to a fixpoint so loop
148/// analysis terminates; `leq` is the lattice order `⊑` (more precise ⊑ less
149/// precise, i.e. `γ`-subset).
150trait AbstractDomain: Clone {
151    /// Greatest element `⊤` — "no information".
152    fn top() -> Self;
153    /// Least element `⊥` — "unreachable / empty".
154    fn bottom() -> Self;
155    /// Least upper bound `⊔` (merge at control-flow joins).
156    fn join(&self, other: &Self) -> Self;
157    /// Greatest lower bound `⊓` (refine under a known fact).
158    fn meet(&self, other: &Self) -> Self;
159    /// Widening `▽` — over-approximates the join to force loop convergence.
160    fn widen(&self, other: &Self) -> Self;
161    /// Lattice order: `self ⊑ other`.
162    fn leq(&self, other: &Self) -> bool;
163}
164
165#[derive(Clone, Debug)]
166struct Interval {
167    lo: Bound,
168    hi: Bound,
169}
170
171impl Interval {
172    fn exact(n: i64) -> Self {
173        Interval { lo: Bound::Finite(n), hi: Bound::Finite(n) }
174    }
175
176    fn top() -> Self {
177        Interval { lo: Bound::NegInf, hi: Bound::PosInf }
178    }
179
180    fn non_negative() -> Self {
181        Interval { lo: Bound::Finite(0), hi: Bound::PosInf }
182    }
183
184    fn is_exact(&self) -> Option<i64> {
185        if let (Bound::Finite(a), Bound::Finite(b)) = (&self.lo, &self.hi) {
186            if a == b { return Some(*a); }
187        }
188        None
189    }
190
191    /// The empty interval `⊥`. Represented as `lo > hi`, which no reachable
192    /// program point ever constructs, so it is unambiguous.
193    fn bottom() -> Self {
194        Interval { lo: Bound::PosInf, hi: Bound::NegInf }
195    }
196
197    fn is_bottom(&self) -> bool {
198        self.lo.cmp_bound(&self.hi) == std::cmp::Ordering::Greater
199    }
200
201    fn join(&self, other: &Interval) -> Interval {
202        if self.is_bottom() { return other.clone(); }
203        if other.is_bottom() { return self.clone(); }
204        Interval {
205            lo: Bound::min_bound(&self.lo, &other.lo),
206            hi: Bound::max_bound(&self.hi, &other.hi),
207        }
208    }
209
210    /// Intersection `⊓`. Disjoint inputs collapse to `⊥`.
211    fn meet(&self, other: &Interval) -> Interval {
212        if self.is_bottom() || other.is_bottom() { return Interval::bottom(); }
213        let r = Interval {
214            lo: Bound::max_bound(&self.lo, &other.lo),
215            hi: Bound::min_bound(&self.hi, &other.hi),
216        };
217        if r.is_bottom() { Interval::bottom() } else { r }
218    }
219
220    /// Widening `self ▽ other` (self = old, other = new). An unstable bound is
221    /// thrown to the corresponding infinity so ascending chains converge.
222    fn widen(&self, other: &Interval) -> Interval {
223        if self.is_bottom() { return other.clone(); }
224        if other.is_bottom() { return self.clone(); }
225        // EXODIA's threshold ladder: an unstable bound snaps to the next
226        // rung before falling off to ±∞ — loop counters converge to finite
227        // bounds (e.g. `While i < 10` keeps i ⊑ [entry, 10ish]) instead of
228        // instantly losing all precision. Still a finite ascent → terminates.
229        const WIDENING_THRESHOLDS: &[i64] = &[-1000, -100, -10, -1, 0, 1, 10, 100, 1000];
230        let lo = if other.lo.cmp_bound(&self.lo) == std::cmp::Ordering::Less {
231            match other.lo {
232                Bound::Finite(v) => WIDENING_THRESHOLDS
233                    .iter()
234                    .rev()
235                    .find(|&&t| t <= v)
236                    .map(|&t| Bound::Finite(t))
237                    .unwrap_or(Bound::NegInf),
238                _ => Bound::NegInf,
239            }
240        } else {
241            self.lo.clone()
242        };
243        let hi = if other.hi.cmp_bound(&self.hi) == std::cmp::Ordering::Greater {
244            match other.hi {
245                Bound::Finite(v) => WIDENING_THRESHOLDS
246                    .iter()
247                    .find(|&&t| t >= v)
248                    .map(|&t| Bound::Finite(t))
249                    .unwrap_or(Bound::PosInf),
250                _ => Bound::PosInf,
251            }
252        } else {
253            self.hi.clone()
254        };
255        Interval { lo, hi }
256    }
257
258    /// Lattice order `self ⊑ other`, i.e. `self ⊆ other` as concrete sets.
259    fn leq(&self, other: &Interval) -> bool {
260        if self.is_bottom() { return true; }
261        if other.is_bottom() { return false; }
262        other.lo.cmp_bound(&self.lo) != std::cmp::Ordering::Greater
263            && self.hi.cmp_bound(&other.hi) != std::cmp::Ordering::Greater
264    }
265
266    fn add(&self, other: &Interval) -> Interval {
267        Interval {
268            lo: self.lo.add(&other.lo),
269            hi: self.hi.add(&other.hi),
270        }
271    }
272
273    fn sub(&self, other: &Interval) -> Interval {
274        Interval {
275            lo: self.lo.sub(&other.hi),
276            hi: self.hi.sub(&other.lo),
277        }
278    }
279
280    fn mul(&self, other: &Interval) -> Interval {
281        // Standard interval product: the extremes lie at the four corner
282        // products. With the `0 * inf = 0` convention in `Bound::mul`, this
283        // stays precise for sign-definite operands (e.g. `i * i` with `i >= 2`
284        // gives `[4, +inf)`, not top).
285        let corners = [
286            self.lo.mul(&other.lo),
287            self.lo.mul(&other.hi),
288            self.hi.mul(&other.lo),
289            self.hi.mul(&other.hi),
290        ];
291        let mut lo = corners[0].clone();
292        let mut hi = corners[0].clone();
293        for c in &corners[1..] {
294            lo = Bound::min_bound(&lo, c);
295            hi = Bound::max_bound(&hi, c);
296        }
297        Interval { lo, hi }
298    }
299
300    fn div(&self, other: &Interval) -> Interval {
301        if let (Some(a), Some(b)) = (self.is_exact(), other.is_exact()) {
302            if b != 0 {
303                // `wrapping_div` matches the runtime (semantics/arith.rs) and
304                // avoids the `i64::MIN / -1` overflow panic of raw `/`.
305                return Interval::exact(a.wrapping_div(b));
306            }
307        }
308        // Constant nonzero divisor, arbitrary dividend range. Truncating
309        // division (toward zero, matching Rust `/`) is monotone in the dividend
310        // for a positive divisor, so both bounds map through; a negative divisor
311        // additionally swaps them. This generalizes the exact/exact case so a
312        // fixpoint can bound `seed / 65536`, `hi / 2`, etc.
313        if let Some(k) = other.is_exact() {
314            if k > 0 {
315                return Interval { lo: self.lo.div_by(k), hi: self.hi.div_by(k) };
316            } else if k < 0 {
317                return Interval { lo: self.hi.div_by(k), hi: self.lo.div_by(k) };
318            }
319        }
320        Interval::top()
321    }
322
323    fn modulo(&self, other: &Interval) -> Interval {
324        if let (Some(a), Some(b)) = (self.is_exact(), other.is_exact()) {
325            if b != 0 {
326                // `wrapping_rem` matches the runtime (semantics/arith.rs) and
327                // avoids the `i64::MIN % -1` overflow panic of raw `%`.
328                return Interval::exact(a.wrapping_rem(b));
329            }
330        }
331        // Constant nonzero divisor, arbitrary dividend range. LOGOS `%` is the
332        // TRUNCATED remainder: `|x % k| <= |k| - 1` and the sign follows the
333        // DIVIDEND. So a provably non-negative dividend gives `[0, |k|-1]`
334        // (the counting_sort / LCG case: `seed % 2147483648 ∈ [0, 2^31-1]`,
335        // `(seed/65536) % 1000 ∈ [0, 999]`), a non-positive one `[-(|k|-1), 0]`,
336        // and a sign-spanning one `[-(|k|-1), |k|-1]`.
337        if let Some(k) = other.is_exact() {
338            if k != 0 {
339                let m = k.checked_abs().unwrap_or(i64::MAX).saturating_sub(1).max(0);
340                let nonneg = matches!(self.lo, Bound::Finite(v) if v >= 0);
341                let nonpos = matches!(self.hi, Bound::Finite(v) if v <= 0);
342                let lo = if nonneg { Bound::Finite(0) } else { Bound::Finite(-m) };
343                let hi = if nonpos { Bound::Finite(0) } else { Bound::Finite(m) };
344                return Interval { lo, hi };
345            }
346        }
347        // VARIABLE (or otherwise non-constant) divisor. The truncated
348        // remainder's SIGN still follows the dividend, so a provably
349        // non-negative dividend yields a non-negative result (magnitude
350        // unbounded without a concrete divisor) — the `x % n >= 0` element
351        // LOWER bound the symbolic prover pairs with the symbolic upper
352        // `x % n <= n - 1` (graph_bfs: `adj` filled with `(...) % n`). A
353        // zero divisor errors at runtime, so the result's range is moot.
354        let nonneg = matches!(self.lo, Bound::Finite(v) if v >= 0);
355        let nonpos = matches!(self.hi, Bound::Finite(v) if v <= 0);
356        if nonneg {
357            return Interval::non_negative();
358        }
359        if nonpos {
360            return Interval { lo: Bound::NegInf, hi: Bound::Finite(0) };
361        }
362        Interval::top()
363    }
364
365    /// Arithmetic right shift `x >> k`. For a constant, in-range amount this is
366    /// floor(x / 2^k): monotone non-decreasing in `x`, so both endpoints map
367    /// straight through (mirrors `div` by a positive power of two). Keeps
368    /// value/element bounds alive across the e-graph's strength reduction
369    /// `x / 2^k → x >> k` — e.g. `(seed >> 16) % 1000 ∈ [0, 999]` survives,
370    /// exactly as `(seed / 65536) % 1000` did.
371    fn shr(&self, other: &Interval) -> Interval {
372        if let Some(k) = other.is_exact() {
373            if (0..64).contains(&k) {
374                let k = k as u32;
375                return Interval { lo: self.lo.shr_by(k), hi: self.hi.shr_by(k) };
376            }
377        }
378        Interval::top()
379    }
380
381    fn definitely_gt(&self, other: &Interval) -> Option<bool> {
382        if self.lo.cmp_bound(&other.hi) == std::cmp::Ordering::Greater {
383            return Some(true);
384        }
385        if self.hi.cmp_bound(&other.lo) != std::cmp::Ordering::Greater {
386            return Some(false);
387        }
388        None
389    }
390
391    fn definitely_lt(&self, other: &Interval) -> Option<bool> {
392        if self.hi.cmp_bound(&other.lo) == std::cmp::Ordering::Less {
393            return Some(true);
394        }
395        if self.lo.cmp_bound(&other.hi) != std::cmp::Ordering::Less {
396            return Some(false);
397        }
398        None
399    }
400
401    fn definitely_gteq(&self, other: &Interval) -> Option<bool> {
402        if self.lo.cmp_bound(&other.hi) != std::cmp::Ordering::Less {
403            return Some(true);
404        }
405        if self.hi.cmp_bound(&other.lo) == std::cmp::Ordering::Less {
406            return Some(false);
407        }
408        None
409    }
410
411    fn definitely_lteq(&self, other: &Interval) -> Option<bool> {
412        if self.hi.cmp_bound(&other.lo) != std::cmp::Ordering::Greater {
413            return Some(true);
414        }
415        if self.lo.cmp_bound(&other.hi) == std::cmp::Ordering::Greater {
416            return Some(false);
417        }
418        None
419    }
420
421    fn definitely_eq(&self, other: &Interval) -> Option<bool> {
422        if let (Some(a), Some(b)) = (self.is_exact(), other.is_exact()) {
423            return Some(a == b);
424        }
425        None
426    }
427
428    fn definitely_neq(&self, other: &Interval) -> Option<bool> {
429        if self.hi.cmp_bound(&other.lo) == std::cmp::Ordering::Less
430            || self.lo.cmp_bound(&other.hi) == std::cmp::Ordering::Greater
431        {
432            return Some(true);
433        }
434        if let (Some(a), Some(b)) = (self.is_exact(), other.is_exact()) {
435            return Some(a != b);
436        }
437        None
438    }
439}
440
441impl AbstractDomain for Interval {
442    fn top() -> Self { Interval::top() }
443    fn bottom() -> Self { Interval::bottom() }
444    fn join(&self, other: &Self) -> Self { Interval::join(self, other) }
445    fn meet(&self, other: &Self) -> Self { Interval::meet(self, other) }
446    fn widen(&self, other: &Self) -> Self { Interval::widen(self, other) }
447    fn leq(&self, other: &Self) -> bool { Interval::leq(self, other) }
448}
449
450/// Concrete runtime type of a value, the atoms of the type lattice. Mirrors the
451/// scalar/temporal arms of `RuntimeValue`; aggregate/struct tags are added as the
452/// transfer functions that produce them land.
453#[derive(Clone, Debug, PartialEq, Eq, Hash)]
454enum TypeTag {
455    Int,
456    Float,
457    Bool,
458    Text,
459    Char,
460    Nothing,
461    Duration,
462    Date,
463    Moment,
464    Span,
465    Time,
466}
467
468/// The type domain: a value is exactly one type (`Concrete`), one of several
469/// (`Union`), unknown (`Top`), or unreachable (`Bottom`). Equality is by the
470/// canonical tag-set, so `Union({Int})` and `Concrete(Int)` compare equal.
471#[derive(Clone, Debug)]
472enum TypeAbstraction {
473    Bottom,
474    Concrete(TypeTag),
475    Union(std::collections::HashSet<TypeTag>),
476    Top,
477}
478
479impl TypeAbstraction {
480    /// Maximum union cardinality before collapsing to `Top` — keeps the lattice
481    /// finite-height so `widen` (= `join` here) always converges.
482    const UNION_CAP: usize = 6;
483
484    /// Canonical view: `None` is `Top`; `Some(set)` is the set of possible tags
485    /// (`Bottom` → empty set, `Concrete(t)` → `{t}`).
486    fn tag_set(&self) -> Option<std::collections::HashSet<TypeTag>> {
487        match self {
488            TypeAbstraction::Top => None,
489            TypeAbstraction::Bottom => Some(std::collections::HashSet::new()),
490            TypeAbstraction::Concrete(t) => {
491                let mut s = std::collections::HashSet::new();
492                s.insert(t.clone());
493                Some(s)
494            }
495            TypeAbstraction::Union(s) => Some(s.clone()),
496        }
497    }
498
499    fn from_tags(s: std::collections::HashSet<TypeTag>) -> Self {
500        if s.is_empty() {
501            TypeAbstraction::Bottom
502        } else if s.len() == 1 {
503            TypeAbstraction::Concrete(s.into_iter().next().unwrap())
504        } else if s.len() > Self::UNION_CAP {
505            TypeAbstraction::Top
506        } else {
507            TypeAbstraction::Union(s)
508        }
509    }
510}
511
512impl PartialEq for TypeAbstraction {
513    fn eq(&self, other: &Self) -> bool {
514        self.tag_set() == other.tag_set()
515    }
516}
517
518impl AbstractDomain for TypeAbstraction {
519    fn top() -> Self { TypeAbstraction::Top }
520    fn bottom() -> Self { TypeAbstraction::Bottom }
521
522    fn join(&self, other: &Self) -> Self {
523        match (self.tag_set(), other.tag_set()) {
524            (None, _) | (_, None) => TypeAbstraction::Top,
525            (Some(a), Some(b)) => TypeAbstraction::from_tags(a.union(&b).cloned().collect()),
526        }
527    }
528
529    fn meet(&self, other: &Self) -> Self {
530        match (self.tag_set(), other.tag_set()) {
531            (None, _) => other.clone(),
532            (_, None) => self.clone(),
533            (Some(a), Some(b)) => TypeAbstraction::from_tags(a.intersection(&b).cloned().collect()),
534        }
535    }
536
537    // Finite-height lattice (union capped at UNION_CAP): widening is just join.
538    fn widen(&self, other: &Self) -> Self { self.join(other) }
539
540    fn leq(&self, other: &Self) -> bool {
541        match (self.tag_set(), other.tag_set()) {
542            (_, None) => true,            // everything ⊑ Top
543            (None, Some(_)) => false,     // Top ⊑ only Top
544            (Some(a), Some(b)) => a.is_subset(&b),
545        }
546    }
547}
548
549fn literal_type(lit: &Literal) -> TypeTag {
550    match lit {
551        Literal::Number(_) => TypeTag::Int,
552        Literal::Float(_) => TypeTag::Float,
553        Literal::Text(_) => TypeTag::Text,
554        Literal::Boolean(_) => TypeTag::Bool,
555        Literal::Nothing => TypeTag::Nothing,
556        Literal::Char(_) => TypeTag::Char,
557        Literal::Duration(_) => TypeTag::Duration,
558        Literal::Date(_) => TypeTag::Date,
559        Literal::Moment(_) => TypeTag::Moment,
560        Literal::Span { .. } => TypeTag::Span,
561        Literal::Time(_) => TypeTag::Time,
562    }
563}
564
565fn binop_type(op: BinaryOpKind, l: &TypeAbstraction, r: &TypeAbstraction) -> TypeAbstraction {
566    use BinaryOpKind::*;
567    let conc = |t: TypeTag| TypeAbstraction::Concrete(t);
568    let is = |a: &TypeAbstraction, t: TypeTag| matches!(a, TypeAbstraction::Concrete(x) if *x == t);
569    match op {
570        // Relational and equality always yield Bool.
571        Lt | Gt | LtEq | GtEq | Eq | NotEq | ApproxEq => conc(TypeTag::Bool),
572        // Bitwise on ints; on Sets these are set ops — Top keeps them out of
573        // numeric reductions.
574        BitAnd | BitOr => TypeAbstraction::Top,
575        // Explicit concatenation is always Text.
576        Concat => conc(TypeTag::Text),
577        // Sequence concatenation yields a sequence — `Top` keeps it out of numeric reductions.
578        SeqConcat => TypeAbstraction::Top,
579        Add => {
580            if is(l, TypeTag::Int) && is(r, TypeTag::Int) { conc(TypeTag::Int) }
581            else if is(l, TypeTag::Float) && is(r, TypeTag::Float) { conc(TypeTag::Float) }
582            else if is(l, TypeTag::Text) || is(r, TypeTag::Text) { conc(TypeTag::Text) }
583            else { TypeAbstraction::Top }
584        }
585        // ExactDivide produces a Rational, never a known Int — `Top` keeps a
586        // Rational-derived value out of the integer-only strength reductions.
587        // Pow can promote to BigInt (or a Float) — `Top` likewise.
588        ExactDivide | Pow => TypeAbstraction::Top,
589        Subtract | Multiply | Divide | FloorDivide | Modulo => {
590            if is(l, TypeTag::Int) && is(r, TypeTag::Int) { conc(TypeTag::Int) }
591            else if is(l, TypeTag::Float) && is(r, TypeTag::Float) { conc(TypeTag::Float) }
592            else { TypeAbstraction::Top }
593        }
594        And | Or => {
595            if is(l, TypeTag::Bool) && is(r, TypeTag::Bool) { conc(TypeTag::Bool) }
596            else if is(l, TypeTag::Int) && is(r, TypeTag::Int) { conc(TypeTag::Int) }
597            else { TypeAbstraction::Top }
598        }
599        BitXor | Shl | Shr => {
600            if is(l, TypeTag::Int) && is(r, TypeTag::Int) { conc(TypeTag::Int) }
601            else { TypeAbstraction::Top }
602        }
603    }
604}
605
606fn eval_type(
607    expr: &Expr,
608    types: &HashMap<Symbol, TypeAbstraction>,
609    fn_returns: &HashMap<Symbol, TypeTag>,
610    elem_type: &HashMap<Symbol, TypeAbstraction>,
611) -> TypeAbstraction {
612    match expr {
613        Expr::Literal(lit) => TypeAbstraction::Concrete(literal_type(lit)),
614        Expr::Identifier(sym) => types.get(sym).cloned().unwrap_or(TypeAbstraction::Top),
615        Expr::BinaryOp { op, left, right } => {
616            let l = eval_type(left, types, fn_returns, elem_type);
617            let r = eval_type(right, types, fn_returns, elem_type);
618            binop_type(*op, &l, &r)
619        }
620        Expr::Not { operand } => match eval_type(operand, types, fn_returns, elem_type) {
621            TypeAbstraction::Concrete(TypeTag::Bool) => TypeAbstraction::Concrete(TypeTag::Bool),
622            TypeAbstraction::Concrete(TypeTag::Int) => TypeAbstraction::Concrete(TypeTag::Int),
623            _ => TypeAbstraction::Top,
624        },
625        Expr::Length { .. } => TypeAbstraction::Concrete(TypeTag::Int),
626        Expr::Contains { .. } => TypeAbstraction::Concrete(TypeTag::Bool),
627        // A call's DECLARED primitive return type is a sound fact: the
628        // kernel enforces it dynamically, native signatures statically.
629        Expr::Call { function, .. } => fn_returns
630            .get(function)
631            .map(|t| TypeAbstraction::Concrete(t.clone()))
632            .unwrap_or(TypeAbstraction::Top),
633        // A read `item k of arr` carries `arr`'s proven ELEMENT TYPE — the join
634        // over every value written into it. Sound because every element IS one
635        // of those written values, so they all satisfy the join. Gated OFF by
636        // default (`Top`, the prior behaviour) for byte-identical A/B.
637        Expr::Index { collection: Expr::Identifier(sym), .. } if elem_type_enabled() => {
638            let t = elem_type.get(sym).cloned().unwrap_or(TypeAbstraction::Top);
639            if !matches!(t, TypeAbstraction::Top) {
640                crate::optimize::mark_fired(crate::optimization::Opt::ElemType);
641            }
642            t
643        }
644        _ => TypeAbstraction::Top,
645    }
646}
647
648/// The collection-size domain — an interval over the (non-negative) length of a
649/// collection, named for the cases that drive guard elimination. Equality and
650/// the lattice ops work on the canonical `(lo, hi)` size bounds, so e.g.
651/// `KnownSize(1)` and `Singleton` are the same element.
652#[derive(Clone, Debug)]
653enum CollectionShape {
654    /// Unreachable — the `⊥` of the lattice.
655    Bottom,
656    /// length == 0
657    Empty,
658    /// length == 1
659    Singleton,
660    /// length == n
661    KnownSize(u64),
662    /// length ∈ [lo, hi]
663    SizeRange(u64, u64),
664    /// length >= 1
665    NonEmpty,
666    /// length >= 0 — the `⊤` of the lattice.
667    Top,
668}
669
670impl CollectionShape {
671    /// A freshly constructed (`a new Seq/Set/Map`) collection is empty.
672    fn empty_collection() -> Self { CollectionShape::Empty }
673
674    /// Canonical size bounds `(lo, hi)`; `hi == None` is unbounded above,
675    /// `None` overall is `Bottom`.
676    fn bounds(&self) -> Option<(u64, Option<u64>)> {
677        match self {
678            CollectionShape::Bottom => None,
679            CollectionShape::Empty => Some((0, Some(0))),
680            CollectionShape::Singleton => Some((1, Some(1))),
681            CollectionShape::KnownSize(n) => Some((*n, Some(*n))),
682            CollectionShape::SizeRange(lo, hi) => Some((*lo, Some(*hi))),
683            CollectionShape::NonEmpty => Some((1, None)),
684            CollectionShape::Top => Some((0, None)),
685        }
686    }
687
688    /// Reconstruct the most specific named shape from size bounds. An unbounded
689    /// lower bound `>= 2` is sound-but-lossily folded to `NonEmpty`.
690    fn from_bounds(lo: u64, hi: Option<u64>) -> Self {
691        match hi {
692            Some(h) if lo > h => CollectionShape::Bottom,
693            Some(0) => CollectionShape::Empty,
694            Some(1) if lo == 1 => CollectionShape::Singleton,
695            Some(h) if lo == h => CollectionShape::KnownSize(h),
696            Some(h) => CollectionShape::SizeRange(lo, h),
697            None if lo == 0 => CollectionShape::Top,
698            None => CollectionShape::NonEmpty,
699        }
700    }
701
702    /// Effect of `Push` — size grows by one.
703    fn pushed(&self) -> Self {
704        match self.bounds() {
705            None => CollectionShape::Bottom,
706            Some((lo, hi)) => CollectionShape::from_bounds(lo + 1, hi.map(|h| h + 1)),
707        }
708    }
709
710    /// Effect of `Pop`/`Remove` — size shrinks by one (saturating at 0).
711    fn popped(&self) -> Self {
712        match self.bounds() {
713            None => CollectionShape::Bottom,
714            Some((lo, hi)) => {
715                CollectionShape::from_bounds(lo.saturating_sub(1), hi.map(|h| h.saturating_sub(1)))
716            }
717        }
718    }
719
720    fn is_definitely_nonempty(&self) -> bool {
721        matches!(self.bounds(), Some((lo, _)) if lo >= 1)
722    }
723
724    fn is_definitely_empty(&self) -> bool {
725        matches!(self.bounds(), Some((_, Some(0))))
726    }
727}
728
729impl PartialEq for CollectionShape {
730    fn eq(&self, other: &Self) -> bool {
731        self.bounds() == other.bounds()
732    }
733}
734
735impl AbstractDomain for CollectionShape {
736    fn top() -> Self { CollectionShape::Top }
737    fn bottom() -> Self { CollectionShape::Bottom }
738
739    fn join(&self, other: &Self) -> Self {
740        match (self.bounds(), other.bounds()) {
741            (None, _) => other.clone(),
742            (_, None) => self.clone(),
743            (Some((l1, h1)), Some((l2, h2))) => {
744                let lo = l1.min(l2);
745                let hi = match (h1, h2) {
746                    (Some(a), Some(b)) => Some(a.max(b)),
747                    _ => None,
748                };
749                CollectionShape::from_bounds(lo, hi)
750            }
751        }
752    }
753
754    fn meet(&self, other: &Self) -> Self {
755        match (self.bounds(), other.bounds()) {
756            (None, _) | (_, None) => CollectionShape::Bottom,
757            (Some((l1, h1)), Some((l2, h2))) => {
758                let lo = l1.max(l2);
759                let hi = match (h1, h2) {
760                    (Some(a), Some(b)) => Some(a.min(b)),
761                    (Some(a), None) => Some(a),
762                    (None, Some(b)) => Some(b),
763                    (None, None) => None,
764                };
765                CollectionShape::from_bounds(lo, hi)
766            }
767        }
768    }
769
770    fn widen(&self, other: &Self) -> Self {
771        match (self.bounds(), other.bounds()) {
772            (None, _) => other.clone(),
773            (_, None) => self.clone(),
774            (Some((l1, h1)), Some((l2, h2))) => {
775                let lo = if l2 < l1 { 0 } else { l1 };
776                let hi = match (h1, h2) {
777                    (Some(a), Some(b)) if b <= a => Some(a),
778                    _ => None,
779                };
780                CollectionShape::from_bounds(lo, hi)
781            }
782        }
783    }
784
785    fn leq(&self, other: &Self) -> bool {
786        match (self.bounds(), other.bounds()) {
787            (None, _) => true,
788            (_, None) => false,
789            (Some((l1, h1)), Some((l2, h2))) => {
790                let lo_ok = l2 <= l1;
791                let hi_ok = match (h1, h2) {
792                    (_, None) => true,
793                    (None, Some(_)) => false,
794                    (Some(a), Some(b)) => a <= b,
795                };
796                lo_ok && hi_ok
797            }
798        }
799    }
800}
801
802/// The nullability domain: is an (optional-typed) value definitely present,
803/// definitely `nothing`, or unknown. A diamond lattice
804/// `Bottom ⊏ {Definite, Null} ⊏ Maybe`. Used to drop `nothing`-checks the Oracle
805/// can prove can never fail.
806#[derive(Clone, Debug, PartialEq, Eq)]
807enum Nullability {
808    /// Unreachable — the `⊥` of the lattice.
809    Bottom,
810    /// Definitely present (not `nothing`).
811    Definite,
812    /// Definitely `nothing`.
813    Null,
814    /// Unknown — the `⊤` of the lattice.
815    Maybe,
816}
817
818impl Nullability {
819    /// The nullability a literal establishes.
820    fn for_literal(lit: &Literal) -> Nullability {
821        match lit {
822            Literal::Nothing => Nullability::Null,
823            _ => Nullability::Definite,
824        }
825    }
826
827    /// Inside a concrete-variant `Inspect` arm (and for its field bindings) the
828    /// matched value is present — the fact that retires the unwrap guard.
829    fn for_matched_variant() -> Nullability {
830        Nullability::Definite
831    }
832}
833
834impl AbstractDomain for Nullability {
835    fn top() -> Self { Nullability::Maybe }
836    fn bottom() -> Self { Nullability::Bottom }
837
838    fn join(&self, other: &Self) -> Self {
839        use Nullability::*;
840        match (self, other) {
841            (Bottom, x) | (x, Bottom) => x.clone(),
842            (Maybe, _) | (_, Maybe) => Maybe,
843            (Definite, Definite) => Definite,
844            (Null, Null) => Null,
845            (Definite, Null) | (Null, Definite) => Maybe,
846        }
847    }
848
849    fn meet(&self, other: &Self) -> Self {
850        use Nullability::*;
851        match (self, other) {
852            (Maybe, x) | (x, Maybe) => x.clone(),
853            (Bottom, _) | (_, Bottom) => Bottom,
854            (Definite, Definite) => Definite,
855            (Null, Null) => Null,
856            (Definite, Null) | (Null, Definite) => Bottom,
857        }
858    }
859
860    // Four-element lattice → widening is join.
861    fn widen(&self, other: &Self) -> Self { self.join(other) }
862
863    fn leq(&self, other: &Self) -> bool {
864        *self == Nullability::Bottom || *other == Nullability::Maybe || self == other
865    }
866}
867
868/// The aliasing domain (a may-alias set). LOGOS collections are
869/// `Rc<RefCell<_>>`, so `Let a be items.` makes `a` and `items` two names for
870/// one allocation: a mutation through either must invalidate facts about both.
871/// `Unique` (aliases nobody) `⊏ MayAlias(set) ⊏ Top` (aliases anything).
872#[derive(Clone, Debug)]
873enum AliasInfo {
874    /// Unreachable — the `⊥` of the lattice.
875    Bottom,
876    /// No other name refers to this allocation — may-alias `∅`.
877    Unique,
878    /// May share its allocation with any of these variables.
879    MayAlias(HashSet<Symbol>),
880    /// May alias anything — the `⊤` of the lattice.
881    Top,
882}
883
884/// Canonical three-state view of an `AliasInfo` (`Bottom` / finite may-set / `⊤`).
885enum AliasReach {
886    Bottom,
887    Finite(HashSet<Symbol>),
888    Anything,
889}
890
891impl AliasInfo {
892    fn reach(&self) -> AliasReach {
893        match self {
894            AliasInfo::Bottom => AliasReach::Bottom,
895            AliasInfo::Unique => AliasReach::Finite(HashSet::new()),
896            AliasInfo::MayAlias(s) => AliasReach::Finite(s.clone()),
897            AliasInfo::Top => AliasReach::Anything,
898        }
899    }
900
901    fn from_reach(r: AliasReach) -> Self {
902        match r {
903            AliasReach::Bottom => AliasInfo::Bottom,
904            AliasReach::Anything => AliasInfo::Top,
905            AliasReach::Finite(s) => {
906                if s.is_empty() { AliasInfo::Unique } else { AliasInfo::MayAlias(s) }
907            }
908        }
909    }
910}
911
912impl PartialEq for AliasInfo {
913    fn eq(&self, other: &Self) -> bool {
914        match (self.reach(), other.reach()) {
915            (AliasReach::Bottom, AliasReach::Bottom) => true,
916            (AliasReach::Anything, AliasReach::Anything) => true,
917            (AliasReach::Finite(a), AliasReach::Finite(b)) => a == b,
918            _ => false,
919        }
920    }
921}
922
923impl AbstractDomain for AliasInfo {
924    fn top() -> Self { AliasInfo::Top }
925    fn bottom() -> Self { AliasInfo::Bottom }
926
927    fn join(&self, other: &Self) -> Self {
928        match (self.reach(), other.reach()) {
929            (AliasReach::Bottom, _) => other.clone(),
930            (_, AliasReach::Bottom) => self.clone(),
931            (AliasReach::Anything, _) | (_, AliasReach::Anything) => AliasInfo::Top,
932            (AliasReach::Finite(a), AliasReach::Finite(b)) => {
933                AliasInfo::from_reach(AliasReach::Finite(a.union(&b).cloned().collect()))
934            }
935        }
936    }
937
938    fn meet(&self, other: &Self) -> Self {
939        match (self.reach(), other.reach()) {
940            (AliasReach::Bottom, _) | (_, AliasReach::Bottom) => AliasInfo::Bottom,
941            (AliasReach::Anything, _) => other.clone(),
942            (_, AliasReach::Anything) => self.clone(),
943            (AliasReach::Finite(a), AliasReach::Finite(b)) => {
944                AliasInfo::from_reach(AliasReach::Finite(a.intersection(&b).cloned().collect()))
945            }
946        }
947    }
948
949    // May-alias sets are bounded by the program's variables → widen is join.
950    fn widen(&self, other: &Self) -> Self { self.join(other) }
951
952    fn leq(&self, other: &Self) -> bool {
953        match (self.reach(), other.reach()) {
954            (AliasReach::Bottom, _) => true,
955            (_, AliasReach::Anything) => true,
956            (AliasReach::Anything, _) => false,
957            (_, AliasReach::Bottom) => false,
958            (AliasReach::Finite(a), AliasReach::Finite(b)) => a.is_subset(&b),
959        }
960    }
961}
962
963/// The may-alias relation over a scope: an undirected graph whose connected
964/// components are sets of variables that share one `Rc<RefCell<_>>`. A mutation
965/// through any vertex invalidates abstract facts for its whole component.
966///
967/// `tainted` marks handles of UNKNOWN PROVENANCE — produced by calls,
968/// extracted from containers (Index/Slice/FieldAccess/Pop/Repeat/Inspect
969/// bindings), or received as function parameters. A tainted handle may alias
970/// anything; only a fresh rebinding clears the mark. The invariant the
971/// distinctness query rests on: an UNtainted handle's component is a
972/// complete account of its possible aliases.
973#[derive(Clone, Default)]
974struct AliasGraph {
975    edges: HashMap<Symbol, HashSet<Symbol>>,
976    tainted: HashSet<Symbol>,
977}
978
979impl AliasGraph {
980    fn new() -> Self {
981        AliasGraph::default()
982    }
983
984    /// `Let a be b.` / `Set a to b.` where `b` is an identifier: `a` and `b`
985    /// alias the same allocation.
986    fn link(&mut self, a: Symbol, b: Symbol) {
987        if a == b {
988            return;
989        }
990        self.edges.entry(a).or_default().insert(b);
991        self.edges.entry(b).or_default().insert(a);
992        // Aliasing a tainted handle spreads its unknown provenance.
993        if self.tainted.contains(&a) {
994            self.tainted.insert(b);
995        }
996        if self.tainted.contains(&b) {
997            self.tainted.insert(a);
998        }
999    }
1000
1001    /// Mark `a` as a handle of unknown provenance.
1002    fn taint(&mut self, a: Symbol) {
1003        self.tainted.insert(a);
1004    }
1005
1006    /// Union the edges and taint of `other` into `self`; true if anything grew.
1007    fn union_from(&mut self, other: &AliasGraph) -> bool {
1008        let mut grew = false;
1009        for (k, ns) in &other.edges {
1010            let e = self.edges.entry(*k).or_default();
1011            for n in ns {
1012                if e.insert(*n) {
1013                    grew = true;
1014                }
1015            }
1016        }
1017        for t in &other.tainted {
1018            if self.tainted.insert(*t) {
1019                grew = true;
1020            }
1021        }
1022        grew
1023    }
1024
1025    /// PROOF of distinctness: both handles have fully tracked provenance and
1026    /// their components do not meet. Anything tainted refuses.
1027    fn definitely_distinct(&self, a: Symbol, b: Symbol) -> bool {
1028        if a == b || self.tainted.contains(&a) || self.tainted.contains(&b) {
1029            return false;
1030        }
1031        !self.may_alias(a).contains(&b)
1032    }
1033
1034    /// Every variable that may alias `v` (its connected component, including `v`).
1035    fn may_alias(&self, v: Symbol) -> HashSet<Symbol> {
1036        let mut seen = HashSet::new();
1037        let mut stack = vec![v];
1038        while let Some(x) = stack.pop() {
1039            if seen.insert(x) {
1040                if let Some(ns) = self.edges.get(&x) {
1041                    for &n in ns {
1042                        if !seen.contains(&n) {
1043                            stack.push(n);
1044                        }
1045                    }
1046                }
1047            }
1048        }
1049        seen
1050    }
1051
1052    /// The facts to invalidate when a mutation flows through `v`.
1053    fn invalidated_by_mutation(&self, v: Symbol) -> HashSet<Symbol> {
1054        self.may_alias(v)
1055    }
1056
1057    /// Rebinding `a` to a fresh allocation severs its old alias edges and
1058    /// clears its taint: the new value's provenance is known again.
1059    fn unlink(&mut self, a: Symbol) {
1060        if let Some(ns) = self.edges.remove(&a) {
1061            for n in ns {
1062                if let Some(s) = self.edges.get_mut(&n) {
1063                    s.remove(&a);
1064                }
1065            }
1066        }
1067        self.tainted.remove(&a);
1068    }
1069}
1070
1071#[derive(Clone)]
1072struct AbstractState {
1073    vars: HashMap<Symbol, Interval>,
1074    lengths: HashMap<Symbol, Interval>,
1075    /// Per-collection ELEMENT interval: a bound satisfied by EVERY element of
1076    /// the collection, joined over the values written into it. Lets a read
1077    /// `item k of arr` carry a value range (`(seed/65536) % 1000 ∈ [0,999]`
1078    /// pushed into `arr` ⟹ `item i of arr ∈ [0,999]`), which in turn proves a
1079    /// scatter index `counts[v+1]` in bounds. `None`/absent = unknown (`top`).
1080    elem: HashMap<Symbol, Interval>,
1081}
1082
1083impl AbstractState {
1084    fn new() -> Self {
1085        AbstractState {
1086            vars: HashMap::new(),
1087            lengths: HashMap::new(),
1088            elem: HashMap::new(),
1089        }
1090    }
1091
1092    fn get_var(&self, sym: &Symbol) -> Interval {
1093        self.vars.get(sym).cloned().unwrap_or(Interval::top())
1094    }
1095
1096    fn set_var(&mut self, sym: Symbol, range: Interval) {
1097        self.vars.insert(sym, range);
1098    }
1099
1100    fn get_length(&self, sym: &Symbol) -> Interval {
1101        self.lengths.get(sym).cloned().unwrap_or(Interval::non_negative())
1102    }
1103
1104    fn set_length(&mut self, sym: Symbol, range: Interval) {
1105        self.lengths.insert(sym, range);
1106    }
1107
1108    /// The proven element interval of a collection, or `top` if unknown.
1109    fn get_elem(&self, sym: &Symbol) -> Interval {
1110        self.elem.get(sym).cloned().unwrap_or(Interval::top())
1111    }
1112
1113    /// Record an element written into `sym`: JOIN with the existing element
1114    /// bound (every element must satisfy the union). Absent means "no element
1115    /// yet" — the first write seeds it exactly.
1116    fn observe_elem(&mut self, sym: Symbol, range: Interval) {
1117        let joined = match self.elem.get(&sym) {
1118            Some(existing) => existing.join(&range),
1119            None => range,
1120        };
1121        self.elem.insert(sym, joined);
1122    }
1123
1124    /// Forget the element bound (collection rebound, aliased, or written with
1125    /// an unknown value).
1126    fn clear_elem(&mut self, sym: &Symbol) {
1127        self.elem.remove(sym);
1128    }
1129}
1130
1131fn eval_expr(expr: &Expr, state: &AbstractState) -> Interval {
1132    match expr {
1133        Expr::Literal(Literal::Number(n)) => Interval::exact(*n),
1134        Expr::Literal(Literal::Boolean(_)) => Interval::top(),
1135        Expr::Literal(Literal::Float(_)) => Interval::top(),
1136        Expr::Identifier(sym) => state.get_var(sym),
1137        Expr::BinaryOp { op, left, right } => {
1138            let l = eval_expr(left, state);
1139            let r = eval_expr(right, state);
1140            match op {
1141                BinaryOpKind::Add => l.add(&r),
1142                BinaryOpKind::Subtract => l.sub(&r),
1143                BinaryOpKind::Multiply => l.mul(&r),
1144                BinaryOpKind::Divide => l.div(&r),
1145                // ExactDivide yields a Rational — it has NO integer interval, so report
1146                // `top`. This is also the defense that keeps a Rational-derived value
1147                // from being "proven Int": the integer-only division strength reductions
1148                // (MagicDivU / DivPow2) require a proven non-negative Int and so never
1149                // misfire on it.
1150                BinaryOpKind::ExactDivide => Interval::top(),
1151                BinaryOpKind::Modulo => l.modulo(&r),
1152                BinaryOpKind::Shr => l.shr(&r),
1153                _ => Interval::top(),
1154            }
1155        }
1156        Expr::Length { collection } => {
1157            if let Expr::Identifier(sym) = collection {
1158                state.get_length(sym)
1159            } else {
1160                Interval::non_negative()
1161            }
1162        }
1163        // A read `item k of arr` carries `arr`'s proven ELEMENT interval — the
1164        // value-range-through-memory step that turns an element into a usable
1165        // scatter index (`item i of arr ∈ [0,999]` ⟹ `counts[that + 1]` in
1166        // bounds). Unknown element bound ⟹ `top`, exactly as before.
1167        Expr::Index { collection, .. } => {
1168            if let Expr::Identifier(sym) = collection {
1169                state.get_elem(sym)
1170            } else {
1171                Interval::top()
1172            }
1173        }
1174        _ => Interval::top(),
1175    }
1176}
1177
1178fn eval_condition(cond: &Expr, state: &AbstractState) -> Option<bool> {
1179    match cond {
1180        Expr::Literal(Literal::Boolean(b)) => Some(*b),
1181        Expr::BinaryOp { op, left, right } => {
1182            let l = eval_expr(left, state);
1183            let r = eval_expr(right, state);
1184            match op {
1185                BinaryOpKind::Gt => l.definitely_gt(&r),
1186                BinaryOpKind::Lt => l.definitely_lt(&r),
1187                BinaryOpKind::GtEq => l.definitely_gteq(&r),
1188                BinaryOpKind::LtEq => l.definitely_lteq(&r),
1189                BinaryOpKind::Eq => l.definitely_eq(&r),
1190                BinaryOpKind::NotEq => l.definitely_neq(&r),
1191                _ => None,
1192            }
1193        }
1194        Expr::Not { operand } => eval_condition(operand, state).map(|b| !b),
1195        _ => None,
1196    }
1197}
1198
1199fn narrow_state(cond: &Expr, state: &mut AbstractState) {
1200    match cond {
1201        Expr::BinaryOp { op: BinaryOpKind::Gt, left, right } => {
1202            if let Expr::Identifier(sym) = left {
1203                let r = eval_expr(right, state);
1204                if let Some(n) = r.is_exact() {
1205                    let cur = state.get_var(sym);
1206                    if let Some(new_lo) = n.checked_add(1) {
1207                        state.set_var(*sym, Interval {
1208                            lo: Bound::max_bound(&cur.lo, &Bound::Finite(new_lo)),
1209                            hi: cur.hi,
1210                        });
1211                    }
1212                }
1213            }
1214        }
1215        Expr::BinaryOp { op: BinaryOpKind::Lt, left, right } => {
1216            if let Expr::Identifier(sym) = left {
1217                let r = eval_expr(right, state);
1218                if let Some(n) = r.is_exact() {
1219                    let cur = state.get_var(sym);
1220                    if let Some(new_hi) = n.checked_sub(1) {
1221                        state.set_var(*sym, Interval {
1222                            lo: cur.lo,
1223                            hi: Bound::min_bound(&cur.hi, &Bound::Finite(new_hi)),
1224                        });
1225                    }
1226                }
1227            }
1228        }
1229        Expr::BinaryOp { op: BinaryOpKind::GtEq, left, right } => {
1230            if let Expr::Identifier(sym) = left {
1231                let r = eval_expr(right, state);
1232                if let Some(n) = r.is_exact() {
1233                    let cur = state.get_var(sym);
1234                    state.set_var(*sym, Interval {
1235                        lo: Bound::max_bound(&cur.lo, &Bound::Finite(n)),
1236                        hi: cur.hi,
1237                    });
1238                }
1239            }
1240        }
1241        Expr::BinaryOp { op: BinaryOpKind::LtEq, left, right } => {
1242            if let Expr::Identifier(sym) = left {
1243                let r = eval_expr(right, state);
1244                if let Some(n) = r.is_exact() {
1245                    let cur = state.get_var(sym);
1246                    state.set_var(*sym, Interval {
1247                        lo: cur.lo,
1248                        hi: Bound::min_bound(&cur.hi, &Bound::Finite(n)),
1249                    });
1250                }
1251            }
1252        }
1253        _ => {}
1254    }
1255}
1256
1257fn narrow_state_negated(cond: &Expr, state: &mut AbstractState) {
1258    match cond {
1259        Expr::BinaryOp { op: BinaryOpKind::Gt, left, right } => {
1260            if let Expr::Identifier(sym) = left {
1261                let r = eval_expr(right, state);
1262                if let Some(n) = r.is_exact() {
1263                    let cur = state.get_var(sym);
1264                    state.set_var(*sym, Interval {
1265                        lo: cur.lo,
1266                        hi: Bound::min_bound(&cur.hi, &Bound::Finite(n)),
1267                    });
1268                }
1269            }
1270        }
1271        Expr::BinaryOp { op: BinaryOpKind::Lt, left, right } => {
1272            if let Expr::Identifier(sym) = left {
1273                let r = eval_expr(right, state);
1274                if let Some(n) = r.is_exact() {
1275                    let cur = state.get_var(sym);
1276                    state.set_var(*sym, Interval {
1277                        lo: Bound::max_bound(&cur.lo, &Bound::Finite(n)),
1278                        hi: cur.hi,
1279                    });
1280                }
1281            }
1282        }
1283        Expr::BinaryOp { op: BinaryOpKind::GtEq, left, right } => {
1284            if let Expr::Identifier(sym) = left {
1285                let r = eval_expr(right, state);
1286                if let Some(n) = r.is_exact() {
1287                    let cur = state.get_var(sym);
1288                    if let Some(new_hi) = n.checked_sub(1) {
1289                        state.set_var(*sym, Interval {
1290                            lo: cur.lo,
1291                            hi: Bound::min_bound(&cur.hi, &Bound::Finite(new_hi)),
1292                        });
1293                    }
1294                }
1295            }
1296        }
1297        Expr::BinaryOp { op: BinaryOpKind::LtEq, left, right } => {
1298            if let Expr::Identifier(sym) = left {
1299                let r = eval_expr(right, state);
1300                if let Some(n) = r.is_exact() {
1301                    let cur = state.get_var(sym);
1302                    if let Some(new_lo) = n.checked_add(1) {
1303                        state.set_var(*sym, Interval {
1304                            lo: Bound::max_bound(&cur.lo, &Bound::Finite(new_lo)),
1305                            hi: cur.hi,
1306                        });
1307                    }
1308                }
1309            }
1310        }
1311        _ => {}
1312    }
1313}
1314
1315pub fn abstract_interp_stmts<'a>(
1316    stmts: Vec<Stmt<'a>>,
1317    expr_arena: &'a Arena<Expr<'a>>,
1318    stmt_arena: &'a Arena<Stmt<'a>>,
1319) -> Vec<Stmt<'a>> {
1320    let mut state = AbstractState::new();
1321    interp_block(stmts, &mut state, expr_arena, stmt_arena)
1322}
1323
1324fn interp_block<'a>(
1325    stmts: Vec<Stmt<'a>>,
1326    state: &mut AbstractState,
1327    expr_arena: &'a Arena<Expr<'a>>,
1328    stmt_arena: &'a Arena<Stmt<'a>>,
1329) -> Vec<Stmt<'a>> {
1330    let mut result = Vec::with_capacity(stmts.len());
1331
1332    for stmt in stmts {
1333        match stmt {
1334            Stmt::Let { var, ty, value, mutable } => {
1335                let range = eval_expr(value, state);
1336                state.set_var(var, range);
1337                if matches!(value, Expr::New { .. }) {
1338                    state.set_length(var, Interval::exact(0));
1339                }
1340                result.push(Stmt::Let { var, ty, value, mutable });
1341            }
1342
1343            Stmt::Set { target, value } => {
1344                let range = eval_expr(value, state);
1345                state.set_var(target, range);
1346                result.push(Stmt::Set { target, value });
1347            }
1348
1349            Stmt::Push { value, collection } => {
1350                if let Expr::Identifier(sym) = collection {
1351                    let cur_len = state.get_length(sym);
1352                    state.set_length(*sym, cur_len.add(&Interval::exact(1)));
1353                }
1354                result.push(Stmt::Push { value, collection });
1355            }
1356
1357            Stmt::If { cond, then_block, else_block } => {
1358                if let Some(val) = eval_condition(cond, state) {
1359                    let new_cond = expr_arena.alloc(Expr::Literal(Literal::Boolean(val)));
1360                    if val {
1361                        let mut then_state = state.clone();
1362                        narrow_state(cond, &mut then_state);
1363                        let new_then = interp_nested_block(then_block, &mut then_state, expr_arena, stmt_arena);
1364                        *state = then_state;
1365                        result.push(Stmt::If {
1366                            cond: new_cond,
1367                            then_block: new_then,
1368                            else_block: None,
1369                        });
1370                    } else {
1371                        if let Some(eb) = else_block {
1372                            let mut else_state = state.clone();
1373                            narrow_state_negated(cond, &mut else_state);
1374                            let new_else = interp_nested_block(eb, &mut else_state, expr_arena, stmt_arena);
1375                            *state = else_state;
1376                            result.push(Stmt::If {
1377                                cond: new_cond,
1378                                then_block: stmt_arena.alloc_slice(vec![]),
1379                                else_block: Some(new_else),
1380                            });
1381                        } else {
1382                            result.push(Stmt::If {
1383                                cond: new_cond,
1384                                then_block: stmt_arena.alloc_slice(vec![]),
1385                                else_block: None,
1386                            });
1387                        }
1388                    }
1389                } else {
1390                    let mut then_state = state.clone();
1391                    narrow_state(cond, &mut then_state);
1392                    let new_then = interp_nested_block(then_block, &mut then_state, expr_arena, stmt_arena);
1393
1394                    let (new_else, else_state) = if let Some(eb) = else_block {
1395                        let mut es = state.clone();
1396                        narrow_state_negated(cond, &mut es);
1397                        let ne = interp_nested_block(eb, &mut es, expr_arena, stmt_arena);
1398                        (Some(ne), Some(es))
1399                    } else {
1400                        (None, None)
1401                    };
1402
1403                    if let Some(es) = else_state {
1404                        join_states(state, &then_state, &es);
1405                    } else {
1406                        let orig = state.clone();
1407                        join_states(state, &then_state, &orig);
1408                    }
1409
1410                    result.push(Stmt::If { cond, then_block: new_then, else_block: new_else });
1411                }
1412            }
1413
1414            Stmt::While { cond, body, decreasing } => {
1415                let mut loop_state = state.clone();
1416
1417                let loop_writes = collect_writes(body);
1418                let bounded_var = extract_bounded_var(cond);
1419
1420                // Widen all loop-written variables (including the counter)
1421                // to their full possible range before analyzing the body.
1422                for w in &loop_writes {
1423                    loop_state.set_var(*w, Interval::top());
1424                }
1425
1426                // Now narrow the loop state based on the condition.
1427                // For the bounded variable (counter), this gives it the range
1428                // from its initial value (widened to top above) narrowed by the
1429                // condition, resulting in [-inf, bound].
1430                narrow_state(cond, &mut loop_state);
1431
1432                let new_body = interp_nested_block(body, &mut loop_state, expr_arena, stmt_arena);
1433
1434                // After loop: condition is false (loop exited)
1435                narrow_state_negated(cond, state);
1436                // Variables written in loop body get widened to top
1437                for w in &loop_writes {
1438                    if Some(*w) != bounded_var {
1439                        state.set_var(*w, Interval::top());
1440                    }
1441                }
1442
1443                result.push(Stmt::While { cond, body: new_body, decreasing });
1444            }
1445
1446            Stmt::Repeat { pattern, iterable, body } => {
1447                let mut loop_state = state.clone();
1448
1449                if let Pattern::Identifier(var) = &pattern {
1450                    loop_state.set_var(*var, Interval::top());
1451                }
1452
1453                let loop_writes = collect_writes(body);
1454                for w in &loop_writes {
1455                    loop_state.set_var(*w, Interval::top());
1456                }
1457
1458                let new_body = interp_nested_block(body, &mut loop_state, expr_arena, stmt_arena);
1459
1460                for w in &loop_writes {
1461                    state.set_var(*w, Interval::top());
1462                }
1463
1464                result.push(Stmt::Repeat { pattern, iterable, body: new_body });
1465            }
1466
1467            Stmt::FunctionDef { name, params, generics, body, return_type, is_native, native_path, is_exported, export_target, opt_flags } => {
1468                let mut func_state = AbstractState::new();
1469                let new_body = interp_nested_block(body, &mut func_state, expr_arena, stmt_arena);
1470                result.push(Stmt::FunctionDef {
1471                    name, params, generics,
1472                    body: new_body,
1473                    return_type, is_native, native_path, is_exported, export_target, opt_flags,
1474                });
1475            }
1476
1477            Stmt::Inspect { target, arms, has_otherwise } => {
1478                let new_arms: Vec<_> = arms.into_iter().map(|arm| {
1479                    let mut arm_state = state.clone();
1480                    let new_body = interp_nested_block(arm.body, &mut arm_state, expr_arena, stmt_arena);
1481                    crate::ast::stmt::MatchArm {
1482                        enum_name: arm.enum_name,
1483                        variant: arm.variant,
1484                        bindings: arm.bindings,
1485                        body: new_body,
1486                    }
1487                }).collect();
1488                result.push(Stmt::Inspect { target, arms: new_arms, has_otherwise });
1489            }
1490
1491            Stmt::Zone { .. } => {
1492                // Don't analyze inside zones — zone-scoped bindings must be
1493                // preserved for escape analysis (same as propagation pass).
1494                result.push(stmt);
1495            }
1496
1497            Stmt::Concurrent { tasks } => {
1498                let mut sub_state = state.clone();
1499                let new_tasks = interp_nested_block(tasks, &mut sub_state, expr_arena, stmt_arena);
1500                result.push(Stmt::Concurrent { tasks: new_tasks });
1501            }
1502
1503            Stmt::Parallel { tasks } => {
1504                let mut sub_state = state.clone();
1505                let new_tasks = interp_nested_block(tasks, &mut sub_state, expr_arena, stmt_arena);
1506                result.push(Stmt::Parallel { tasks: new_tasks });
1507            }
1508
1509            other => {
1510                result.push(other);
1511            }
1512        }
1513    }
1514
1515    result
1516}
1517
1518fn interp_nested_block<'a>(
1519    block: &'a [Stmt<'a>],
1520    state: &mut AbstractState,
1521    expr_arena: &'a Arena<Expr<'a>>,
1522    stmt_arena: &'a Arena<Stmt<'a>>,
1523) -> &'a [Stmt<'a>] {
1524    let stmts: Vec<Stmt<'a>> = block.iter().cloned().collect();
1525    let result = interp_block(stmts, state, expr_arena, stmt_arena);
1526    stmt_arena.alloc_slice(result)
1527}
1528
1529fn join_states(out: &mut AbstractState, a: &AbstractState, b: &AbstractState) {
1530    let mut all_keys: std::collections::HashSet<Symbol> = a.vars.keys().cloned().collect();
1531    all_keys.extend(b.vars.keys().cloned());
1532
1533    for key in all_keys {
1534        let a_range = a.vars.get(&key).cloned().unwrap_or(Interval::top());
1535        let b_range = b.vars.get(&key).cloned().unwrap_or(Interval::top());
1536        out.set_var(key, a_range.join(&b_range));
1537    }
1538
1539    let mut len_keys: std::collections::HashSet<Symbol> = a.lengths.keys().cloned().collect();
1540    len_keys.extend(b.lengths.keys().cloned());
1541
1542    for key in len_keys {
1543        let a_len = a.lengths.get(&key).cloned().unwrap_or(Interval::non_negative());
1544        let b_len = b.lengths.get(&key).cloned().unwrap_or(Interval::non_negative());
1545        out.set_length(key, a_len.join(&b_len));
1546    }
1547
1548    // Element bounds: absent means UNKNOWN (`top`), so a key tracked on only
1549    // one path joins to `top` (sound — the other path could hold anything). An
1550    // EMPTY fresh collection is seeded `⊥` (no elements), so the 0-iteration
1551    // branch of a build loop's exit join preserves the filled branch's bound.
1552    let mut el_keys: std::collections::HashSet<Symbol> = a.elem.keys().cloned().collect();
1553    el_keys.extend(b.elem.keys().cloned());
1554    for key in el_keys {
1555        let a_el = a.get_elem(&key);
1556        let b_el = b.get_elem(&key);
1557        out.elem.insert(key, a_el.join(&b_el));
1558    }
1559}
1560
1561fn collect_writes(block: &[Stmt]) -> Vec<Symbol> {
1562    let mut writes = Vec::new();
1563    for stmt in block {
1564        collect_writes_stmt(stmt, &mut writes);
1565    }
1566    writes
1567}
1568
1569fn collect_writes_stmt(stmt: &Stmt, writes: &mut Vec<Symbol>) {
1570    match stmt {
1571        Stmt::Set { target, .. } => {
1572            if !writes.contains(target) {
1573                writes.push(*target);
1574            }
1575        }
1576        Stmt::If { then_block, else_block, .. } => {
1577            for s in *then_block { collect_writes_stmt(s, writes); }
1578            if let Some(eb) = else_block {
1579                for s in *eb { collect_writes_stmt(s, writes); }
1580            }
1581        }
1582        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
1583            for s in *body { collect_writes_stmt(s, writes); }
1584        }
1585        _ => {}
1586    }
1587}
1588
1589fn extract_bounded_var(cond: &Expr) -> Option<Symbol> {
1590    match cond {
1591        Expr::BinaryOp { op: BinaryOpKind::Lt | BinaryOpKind::LtEq | BinaryOpKind::Gt | BinaryOpKind::GtEq, left, .. } => {
1592            if let Expr::Identifier(sym) = left {
1593                Some(*sym)
1594            } else {
1595                None
1596            }
1597        }
1598        _ => None,
1599    }
1600}
1601
1602// ============================================================================
1603// The Oracle: product lattice + rich, fact-returning analysis (EXODIA Phase 1).
1604//
1605// `abstract_interp_stmts` above transforms the program (interval-driven dead
1606// branch elimination). `rich_abstract_interp_stmts` below is additive: it leaves
1607// the program unchanged and hands back the five-domain abstract facts the VM and
1608// copy-and-patch JIT consult to choose typed stencils and drop guards.
1609// ============================================================================
1610
1611/// The product abstract value — the five domains combined componentwise.
1612#[derive(Clone, Debug)]
1613pub(crate) struct AbstractValue {
1614    interval: Interval,
1615    ty: TypeAbstraction,
1616    shape: CollectionShape,
1617    nullability: Nullability,
1618    alias: AliasInfo,
1619}
1620
1621impl AbstractDomain for AbstractValue {
1622    fn top() -> Self {
1623        AbstractValue {
1624            interval: Interval::top(),
1625            ty: TypeAbstraction::Top,
1626            shape: CollectionShape::Top,
1627            nullability: Nullability::Maybe,
1628            alias: AliasInfo::Top,
1629        }
1630    }
1631
1632    fn bottom() -> Self {
1633        AbstractValue {
1634            interval: Interval::bottom(),
1635            ty: TypeAbstraction::Bottom,
1636            shape: CollectionShape::Bottom,
1637            nullability: Nullability::Bottom,
1638            alias: AliasInfo::Bottom,
1639        }
1640    }
1641
1642    fn join(&self, o: &Self) -> Self {
1643        AbstractValue {
1644            interval: self.interval.join(&o.interval),
1645            ty: self.ty.join(&o.ty),
1646            shape: self.shape.join(&o.shape),
1647            nullability: self.nullability.join(&o.nullability),
1648            alias: self.alias.join(&o.alias),
1649        }
1650    }
1651
1652    fn meet(&self, o: &Self) -> Self {
1653        AbstractValue {
1654            interval: self.interval.meet(&o.interval),
1655            ty: self.ty.meet(&o.ty),
1656            shape: self.shape.meet(&o.shape),
1657            nullability: self.nullability.meet(&o.nullability),
1658            alias: self.alias.meet(&o.alias),
1659        }
1660    }
1661
1662    fn widen(&self, o: &Self) -> Self {
1663        AbstractValue {
1664            interval: self.interval.widen(&o.interval),
1665            ty: self.ty.widen(&o.ty),
1666            shape: self.shape.widen(&o.shape),
1667            nullability: self.nullability.widen(&o.nullability),
1668            alias: self.alias.widen(&o.alias),
1669        }
1670    }
1671
1672    fn leq(&self, o: &Self) -> bool {
1673        self.interval.leq(&o.interval)
1674            && self.ty.leq(&o.ty)
1675            && self.shape.leq(&o.shape)
1676            && self.nullability.leq(&o.nullability)
1677            && self.alias.leq(&o.alias)
1678    }
1679}
1680
1681/// The Oracle's per-program-point state: every domain tracked per variable, plus
1682/// the may-alias relation. `value_of` assembles the product fact on demand.
1683#[derive(Clone)]
1684pub(crate) struct RichAbstractState {
1685    intervals: AbstractState,
1686    types: HashMap<Symbol, TypeAbstraction>,
1687    shapes: HashMap<Symbol, CollectionShape>,
1688    nullability: HashMap<Symbol, Nullability>,
1689    aliases: AliasGraph,
1690    /// Variables PROVEN collection-kinded (created as collections). Kind
1691    /// is binding-level: pushes, pops and callees resize a collection but
1692    /// can never rebind it to a scalar — only reassignment removes a
1693    /// variable here. Size widening (shapes → Top) does NOT erase kind.
1694    coll_vars: std::collections::HashSet<Symbol>,
1695    /// DECLARED primitive return types per function — calls produce facts.
1696    fn_returns: std::rc::Rc<HashMap<Symbol, TypeTag>>,
1697    /// Symbolic length LOWER bound per collection: `arr -> (n, off)` means
1698    /// `length(arr) >= n + off`, where `n` is a program variable. Established
1699    /// by a counted build loop (`while c < n: push to arr` once per iteration
1700    /// from an empty array — the standard allocation-size fact) and consumed
1701    /// by the relational bounds recognizer to prove `item i of arr` reads
1702    /// whose guard bounds `i` by the SAME variable `n`. Invalidated when the
1703    /// array is resized/rebound or when `n` is reassigned.
1704    length_def: HashMap<Symbol, (Symbol, i64)>,
1705    /// Function-PARAMETER collections (`arr: Seq of Int`): their length is
1706    /// fundamentally unknown, so they are the targets of SPECULATIVE
1707    /// region-entry hoisting (a locally-built array, by contrast, has a
1708    /// statically determinable length and is left to the static path).
1709    param_colls: std::collections::HashSet<Symbol>,
1710    /// Affine scalar DEFINITIONS: `X -> linear(E)` for a binding `Let X be E`
1711    /// whose value is affine over other variables (`Let cols be capacity + 1`).
1712    /// Fed to the kernel LIA prover as an EQUALITY `X = E` so a multi-variable
1713    /// bounds proof can relate a length variable to a loop bound (knapsack:
1714    /// `length(prev) = cols = capacity + 1`, guard `w <= capacity`). Invalidated
1715    /// when `X` or any variable in `E` is reassigned.
1716    scalar_def: HashMap<Symbol, super::affine::LinExpr>,
1717    /// SYMBOLIC scalar UPPER bound: `X -> L` means `X <= L` for a linear `L`
1718    /// over program variables. The variable-divisor sibling of the concrete
1719    /// interval upper — `Let neighbor be (...) % n` records `neighbor <= n - 1`,
1720    /// which no concrete `Interval` can hold (`n` is symbolic). Flows into the
1721    /// per-collection element upper through a store and back out through a read,
1722    /// then feeds the Fourier–Motzkin prover so `dist[u]` (u an element of a
1723    /// `% n`-filled array) proves in bounds. Invalidated like `scalar_def`.
1724    scalar_upper: HashMap<Symbol, super::affine::LinExpr>,
1725    /// SYMBOLIC per-collection ELEMENT upper bound: `A -> L` means EVERY element
1726    /// of `A` is `<= L`. Absent means ⊤ (unknown) when the concrete element
1727    /// interval is non-⊥, or ⊥ (fresh, no elements) when it is — the concrete
1728    /// `is_bottom()` disambiguates, exactly as the fixpoint's freshness test
1729    /// does. Seeded by stores/pushes whose value carries a `scalar_upper`, read
1730    /// back as a `scalar_upper` on `Let u be item _ of A`.
1731    elem_upper: HashMap<Symbol, super::affine::LinExpr>,
1732    /// Per-collection ELEMENT TYPE: `A -> T` means EVERY element of `A` is of
1733    /// scalar type `T` (the join over every value written into `A`). The type
1734    /// sibling of the concrete `elem` interval — seeded by the first
1735    /// store/push, joined on every later one, and read back as the proven type
1736    /// of `item k of A`. Lets a read of a homogeneously-typed collection carry
1737    /// a concrete scalar kind (`item k of dp ∈ Int` when `dp` is built only
1738    /// from `Int` writes), which the magic-reciprocal modulo gate needs to fire
1739    /// on `(... + ...) % c`. Absent means ⊤ (unknown) when the concrete `elem`
1740    /// interval is non-⊥, or ⊥ (fresh, no elements) when it is — the same
1741    /// `is_bottom()` freshness test the `elem`/`elem_upper` siblings use.
1742    elem_type: HashMap<Symbol, TypeAbstraction>,
1743    /// SYMBOLIC scalar LOWER bound — the mirror of `scalar_upper`, the relation
1744    /// that makes `dist[u+1]`'s LOWER half (`u >= 0`) provable when `u`'s raw
1745    /// interval is widened to ⊤ (a loop-local read variable's undefined entry
1746    /// value floods the fixpoint join). Captured at bind time from the source's
1747    /// proven lower — an element read of `A` carries `A`'s concrete element
1748    /// interval lower; a `(...) % n` of a non-negative dividend carries `0`.
1749    /// Sound for a MUTATED scalar (re-established by its in-body `Let`).
1750    scalar_lower: HashMap<Symbol, super::affine::LinExpr>,
1751    /// AOT-only: this state belongs to a function body analyzed for the
1752    /// `largo build` codegen, which emits the recursive-1-based-partition entry
1753    /// precondition guard (`assert!(lo >= 1 && hi <= len)`). Only then is it
1754    /// sound to (a) seed that precondition as a fact and (b) propagate a
1755    /// `length_def` across an alias bind — both rest on a runtime check the VM/
1756    /// JIT bytecode path does NOT emit, so they stay OFF for it. Set by
1757    /// [`oracle_analyze_with_entry_guards`]; copied by `clone()` into branch
1758    /// states, so it survives the whole body walk.
1759    aot_entry_guard: bool,
1760}
1761
1762impl RichAbstractState {
1763    fn new() -> Self {
1764        RichAbstractState {
1765            intervals: AbstractState::new(),
1766            types: HashMap::new(),
1767            shapes: HashMap::new(),
1768            nullability: HashMap::new(),
1769            aliases: AliasGraph::new(),
1770            coll_vars: std::collections::HashSet::new(),
1771            fn_returns: std::rc::Rc::new(HashMap::new()),
1772            length_def: HashMap::new(),
1773            param_colls: std::collections::HashSet::new(),
1774            scalar_def: HashMap::new(),
1775            scalar_upper: HashMap::new(),
1776            elem_upper: HashMap::new(),
1777            elem_type: HashMap::new(),
1778            scalar_lower: HashMap::new(),
1779            aot_entry_guard: false,
1780        }
1781    }
1782
1783    /// The product fact known about `sym` at this point.
1784    pub(crate) fn value_of(&self, sym: Symbol) -> AbstractValue {
1785        let mut others = self.aliases.may_alias(sym);
1786        others.remove(&sym);
1787        let alias = if others.is_empty() {
1788            AliasInfo::Unique
1789        } else {
1790            AliasInfo::MayAlias(others)
1791        };
1792        AbstractValue {
1793            interval: self.intervals.get_var(&sym),
1794            ty: self.types.get(&sym).cloned().unwrap_or(TypeAbstraction::Top),
1795            shape: self.shapes.get(&sym).cloned().unwrap_or(CollectionShape::Top),
1796            nullability: self.nullability.get(&sym).cloned().unwrap_or(Nullability::Maybe),
1797            alias,
1798        }
1799    }
1800
1801    /// Forget every scalar fact about `sym` (used when its value is overwritten
1802    /// or produced by an operation we do not model). Rebinding can change
1803    /// KIND too, so the collection proof drops with it.
1804    fn invalidate_var(&mut self, sym: Symbol) {
1805        self.intervals.set_var(sym, Interval::top());
1806        self.types.insert(sym, TypeAbstraction::Top);
1807        self.shapes.insert(sym, CollectionShape::Top);
1808        self.nullability.insert(sym, Nullability::Maybe);
1809        self.coll_vars.remove(&sym);
1810        self.intervals.clear_elem(&sym);
1811        let si = sym.index() as i64;
1812        self.scalar_upper.remove(&sym);
1813        self.scalar_upper.retain(|_, e| !e.coefficients.contains_key(&si));
1814        self.elem_upper.remove(&sym);
1815        self.elem_upper.retain(|_, e| !e.coefficients.contains_key(&si));
1816        self.elem_type.remove(&sym);
1817        self.scalar_lower.remove(&sym);
1818        self.scalar_lower.retain(|_, e| !e.coefficients.contains_key(&si));
1819    }
1820}
1821
1822/// Run the Oracle's rich abstract interpretation, returning the (unchanged)
1823/// statements together with the per-variable abstract facts.
1824/// The scalar kinds downstream consumers (typed bytecode, JIT guard
1825/// elision) act on.
1826#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1827pub enum ScalarKind {
1828    Int,
1829    Float,
1830    Bool,
1831    Text,
1832}
1833
1834/// A variable's program-wide PROVEN facts — the statically-verified summary the
1835/// debugger shows beside its live value (distinct from the dynamic, observed-this-run
1836/// invariants). All fields are sound over-approximations: present ⇒ proven.
1837#[derive(Clone, Debug, Default, PartialEq)]
1838pub struct VarProvenFacts {
1839    /// Proven scalar type, if concrete across the program.
1840    pub scalar: Option<ScalarKind>,
1841    /// Proven finite integer range `[lo, hi]` (the value was in it at every occurrence).
1842    pub int_range: Option<(i64, i64)>,
1843    /// Proven non-negative everywhere (even if unbounded above).
1844    pub nonneg: bool,
1845}
1846
1847/// EXODIA Phase 1's DELIVERY layer: a product fact for every expression
1848/// occurrence at its program point, keyed by ARENA ADDRESS (stable for the
1849/// analyzed snapshot — record facts immediately before consuming them, and
1850/// never across a pass that re-allocates expressions). Shared subtrees that
1851/// appear at several program points JOIN their facts (sound; precision is
1852/// lost exactly where sharing occurs).
1853#[derive(Default)]
1854pub struct OracleFacts {
1855    exprs: HashMap<usize, AbstractValue>,
1856    /// Length interval of the collection an Identifier expression denotes,
1857    /// at that occurrence — the bounds-elision query's other half.
1858    lengths: HashMap<usize, Interval>,
1859    /// Identifier occurrences PROVEN collection-kinded (binding-level —
1860    /// kind survives growth and callees; only rebinding erases it).
1861    collections: std::collections::HashSet<usize>,
1862    /// Converged loop-invariant alias graphs, keyed by the While/Repeat
1863    /// statement's arena address. Borrow hoisting's distinctness queries
1864    /// read these; loops the fixpoint never converged on (or that run under
1865    /// concurrent blocks) have no entry and refuse.
1866    loop_aliases: HashMap<usize, AliasGraph>,
1867    /// When set, per-expression facts (`exprs`/`lengths`/`collections`) are
1868    /// NOT recorded, but state updates and `loop_aliases` snapshots still
1869    /// are. Used to walk Zone interiors for borrow hoisting's alias
1870    /// snapshots WITHOUT feeding new expr facts to the EXODIA VM/JIT region
1871    /// compiler — whose pinned-chain emitter assumes the oracle records
1872    /// nothing inside zones.
1873    suppress_exprs: bool,
1874    /// `index` sub-expression addresses of `item i of arr` reads PROVEN
1875    /// in bounds by RELATIONAL induction-variable reasoning (V8/LLVM SCEV
1876    /// style): a loop guard `i </<= length(arr)` with `i >= 1` and `arr`
1877    /// not resized in the body bounds the index by the array's length — a
1878    /// relation the interval domain cannot represent. `index_provably_in_
1879    /// bounds` consults this in addition to the interval check.
1880    relational_inbounds: std::collections::HashSet<usize>,
1881    /// `index` addresses proven in bounds only SPECULATIVELY — they rely on a
1882    /// region-entry runtime check (`hoist_descs`). Kept SEPARATE from
1883    /// `relational_inbounds` so the compiler elides them ONLY when it actually
1884    /// emits the matching `RegionBoundsGuard` (elision ⟺ guard ⟺ VM check).
1885    speculative_inbounds: std::collections::HashSet<usize>,
1886    /// Per-loop region-entry bounds hoists (V8 loop bound-check elimination),
1887    /// keyed by the `While`/`Repeat` statement's arena address. When a loop's
1888    /// array length cannot be proven statically (e.g. a function parameter),
1889    /// but the induction is monotone, the bound loop-invariant, and the array
1890    /// stable, the covered accesses are recorded in `relational_inbounds`
1891    /// (speculatively) and one descriptor here justifies them with a runtime
1892    /// check the bytecode compiler lowers to `RegionBoundsGuard`.
1893    hoist_descs: HashMap<usize, Vec<HoistDesc>>,
1894    /// Per-access POSITIVITY guards: an `index` (by `*const Expr`) elided only
1895    /// because a `% m` element bound `m - 1` closed the proof carries the
1896    /// divisors `m` whose `m >= 1` precondition the codegen must discharge with
1897    /// a (nonemptiness-guarded, invariant-hoisted) `assert!(m > 0)` at the
1898    /// access. Keeps the lenient symbolic element join sound for ALL inputs:
1899    /// `m <= 0` panics before the would-be out-of-bounds read instead of UB.
1900    positivity_guards: HashMap<usize, Vec<Symbol>>,
1901    /// `Map of Int to Int` locals declared `… with capacity CAP` whose capacity is
1902    /// an affine expression of program-INVARIANT variables — stored as a kernel
1903    /// `LinearExpr` so the dense-key bound proof can relate a key to it. Keyed by
1904    /// the map symbol. The invariance check (no capacity variable is ever
1905    /// reassigned) is what makes the stored expression equal the runtime
1906    /// allocation size at every key site. Populated once, before the loop walks.
1907    map_caps: HashMap<Symbol, super::affine::LinExpr>,
1908    /// `key` sub-expression addresses (insert/get/contains) PROVEN to satisfy
1909    /// `0 <= key <= capacity(map)` under the loop guards, mapped to the map they
1910    /// were proven against. The dense gate lowers a `Map of Int to Int` to a
1911    /// direct-addressed array iff EVERY one of its key sites appears here for it.
1912    dense_map_key_proven: HashMap<usize, Symbol>,
1913    /// `Map of Int to Int` locals whose SINGLE insert loop provably writes EVERY
1914    /// integer in a contiguous range `[A, B]` (unit-stride induction variable,
1915    /// unconditional `Set item iv of m`), keyed to that range as `(A, B)` kernel
1916    /// `LinearExpr`s. The precondition for presence elision: a key proven inside
1917    /// `[A, B]` was definitely inserted, so `get` can skip its presence check.
1918    map_insert_cover: HashMap<Symbol, (super::affine::LinExpr, super::affine::LinExpr)>,
1919    /// `key` addresses additionally PROVEN inside their map's fully-covered insert
1920    /// range `[A, B]` (so the key was definitely written). When every key site of
1921    /// a dense map is here, the map needs no presence bitset (`…NoPresence`).
1922    map_key_covered: HashMap<usize, Symbol>,
1923}
1924
1925/// A region-entry bounds hoist in SYMBOL terms (the compiler maps to
1926/// registers): `length(array) >= bound + add_max` and `iv + add_min >= 1`.
1927#[derive(Clone, Copy, Debug)]
1928pub struct HoistDesc {
1929    pub array: Symbol,
1930    pub bound: Symbol,
1931    pub iv: Symbol,
1932    pub add_max: i32,
1933    pub add_min: i32,
1934}
1935
1936impl OracleFacts {
1937    /// Region-entry bounds hoists for the loop at `loop_ptr` (the `While`/
1938    /// `Repeat` statement's arena address). The compiler lowers each to a
1939    /// `RegionBoundsGuard` at the loop head.
1940    pub fn hoist_descs_for(&self, loop_ptr: usize) -> &[HoistDesc] {
1941        self.hoist_descs.get(&loop_ptr).map_or(&[], |v| v.as_slice())
1942    }
1943
1944    /// Is this `index` sub-expression proven in bounds only SPECULATIVELY
1945    /// (needs a region-entry guard)? The compiler elides it solely when it
1946    /// emitted that guard.
1947    pub fn index_is_speculative(&self, index: &Expr) -> bool {
1948        self.speculative_inbounds.contains(&(index as *const Expr as usize))
1949    }
1950
1951    fn record(&mut self, e: &Expr, av: AbstractValue) {
1952        if self.suppress_exprs {
1953            return;
1954        }
1955        let key = e as *const Expr as usize;
1956        match self.exprs.get_mut(&key) {
1957            None => {
1958                self.exprs.insert(key, av);
1959            }
1960            Some(prev) => *prev = prev.join(&av),
1961        }
1962    }
1963
1964    fn record_length(&mut self, e: &Expr, len: Interval) {
1965        if self.suppress_exprs {
1966            return;
1967        }
1968        let key = e as *const Expr as usize;
1969        match self.lengths.get_mut(&key) {
1970            None => {
1971                self.lengths.insert(key, len);
1972            }
1973            Some(prev) => *prev = prev.join(&len),
1974        }
1975    }
1976
1977    /// The proven scalar kind of this expression occurrence, if concrete.
1978    pub fn expr_scalar(&self, e: &Expr) -> Option<ScalarKind> {
1979        let av = self.exprs.get(&(e as *const Expr as usize))?;
1980        match &av.ty {
1981            TypeAbstraction::Concrete(TypeTag::Int) => Some(ScalarKind::Int),
1982            TypeAbstraction::Concrete(TypeTag::Float) => Some(ScalarKind::Float),
1983            TypeAbstraction::Concrete(TypeTag::Bool) => Some(ScalarKind::Bool),
1984            TypeAbstraction::Concrete(TypeTag::Text) => Some(ScalarKind::Text),
1985            _ => None,
1986        }
1987    }
1988
1989    /// The finite integer range proven for this occurrence, if both bounds
1990    /// are finite.
1991    pub fn expr_int_range(&self, e: &Expr) -> Option<(i64, i64)> {
1992        self.expr_int_range_addr(e as *const Expr as usize)
1993    }
1994
1995    /// Is this occurrence PROVEN non-negative — its abstract interval's LOWER
1996    /// bound is a finite `>= 0`? Unlike [`Self::expr_int_range`] this ignores
1997    /// the upper bound, so it holds even when the value's magnitude is unbounded
1998    /// above (the LCG `seed * 1103515245 + 12345` dividend: both factors
1999    /// non-negative, so `lo = 0`, but the product's `hi` may saturate). This is
2000    /// the soundness gate for `x % 2^k → x & (2^k - 1)`, whose identity needs
2001    /// only `x >= 0`, not a finite ceiling.
2002    pub fn expr_proven_nonneg(&self, e: &Expr) -> bool {
2003        match self.exprs.get(&(e as *const Expr as usize)) {
2004            Some(av) => matches!(av.interval.lo, Bound::Finite(v) if v >= 0),
2005            None => false,
2006        }
2007    }
2008
2009    /// As [`Self::expr_int_range`], keyed by the expression's arena address (the
2010    /// form the i32-narrowing gate collects its key/value sites in).
2011    pub fn expr_int_range_addr(&self, addr: usize) -> Option<(i64, i64)> {
2012        let av = self.exprs.get(&addr)?;
2013        match (&av.interval.lo, &av.interval.hi) {
2014            (Bound::Finite(lo), Bound::Finite(hi)) if lo <= hi => Some((*lo, *hi)),
2015            _ => None,
2016        }
2017    }
2018
2019    /// **Debugger-only** by-name rollup: walk `stmts` and JOIN every `Identifier(sym)`
2020    /// occurrence's already-recorded abstract value into one proven summary per variable.
2021    /// Sound by construction (joining widens), so a returned `int_range` of `[0, 9]` means
2022    /// the variable was within it at *every* use — a program-wide proven invariant.
2023    ///
2024    /// This re-reads the facts that the production walk already computed; it adds NOTHING
2025    /// to `record_expr`/`oracle_analyze`, so it is **zero cost** unless the debugger calls
2026    /// it (no production path does). Reused from the same module so it can read `exprs`.
2027    pub fn summarize_variables(&self, stmts: &[Stmt]) -> Vec<(Symbol, VarProvenFacts)> {
2028        let mut by_sym: HashMap<Symbol, AbstractValue> = HashMap::new();
2029        collect_idents_block(stmts, &mut |sym, addr| {
2030            if let Some(av) = self.exprs.get(&addr) {
2031                match by_sym.get_mut(&sym) {
2032                    None => {
2033                        by_sym.insert(sym, av.clone());
2034                    }
2035                    Some(prev) => *prev = prev.join(av),
2036                }
2037            }
2038        });
2039        by_sym
2040            .into_iter()
2041            .map(|(sym, av)| {
2042                let int_range = match (&av.interval.lo, &av.interval.hi) {
2043                    (Bound::Finite(lo), Bound::Finite(hi)) if lo <= hi => Some((*lo, *hi)),
2044                    _ => None,
2045                };
2046                let scalar = match &av.ty {
2047                    TypeAbstraction::Concrete(TypeTag::Int) => Some(ScalarKind::Int),
2048                    TypeAbstraction::Concrete(TypeTag::Float) => Some(ScalarKind::Float),
2049                    TypeAbstraction::Concrete(TypeTag::Bool) => Some(ScalarKind::Bool),
2050                    TypeAbstraction::Concrete(TypeTag::Text) => Some(ScalarKind::Text),
2051                    _ => None,
2052                };
2053                let nonneg = matches!(av.interval.lo, Bound::Finite(v) if v >= 0);
2054                (sym, VarProvenFacts { scalar, int_range, nonneg })
2055            })
2056            .collect()
2057    }
2058
2059    /// Does this `Map of Int to Int` have a recorded invariant affine capacity
2060    /// (the precondition for any dense-key proof against it)?
2061    pub fn has_dense_map_capacity(&self, map: Symbol) -> bool {
2062        self.map_caps.contains_key(&map)
2063    }
2064
2065    /// The recorded invariant affine capacity for a dense map — the bound every
2066    /// key was proven `<= cap` against. Codegen renders it (see [`lin_to_rust`])
2067    /// to size the direct-addressed array `with_bounds(0, cap + 1)`.
2068    pub fn map_cap_lin(&self, map: Symbol) -> Option<&super::affine::LinExpr> {
2069        self.map_caps.get(&map)
2070    }
2071
2072    /// Was this key occurrence PROVEN within `[0, capacity(map)]` (so a
2073    /// direct-addressed array of `capacity + 1` slots indexes it safely)?
2074    pub fn dense_map_key_proven(&self, key: &Expr, map: Symbol) -> bool {
2075        self.dense_map_key_proven_addr(key as *const Expr as usize, map)
2076    }
2077
2078    /// As [`Self::dense_map_key_proven`], keyed by the expression's arena address
2079    /// directly (the form the dense gate collects its key sites in).
2080    pub fn dense_map_key_proven_addr(&self, key_addr: usize, map: Symbol) -> bool {
2081        self.dense_map_key_proven.get(&key_addr) == Some(&map)
2082    }
2083
2084    /// Does this dense map's single insert loop provably cover a contiguous range
2085    /// fully (the structural precondition for presence elision)?
2086    pub fn dense_map_has_full_coverage(&self, map: Symbol) -> bool {
2087        self.map_insert_cover.contains_key(&map)
2088    }
2089
2090    /// Was this key occurrence PROVEN inside its map's fully-covered insert range
2091    /// (so the key was definitely written, and `get` may skip the presence bit)?
2092    pub fn dense_key_covered_addr(&self, key_addr: usize, map: Symbol) -> bool {
2093        self.map_key_covered.get(&key_addr) == Some(&map)
2094    }
2095
2096    /// Is this occurrence PROVEN collection-kinded? Seeded at collection
2097    /// creations and preserved through growth, shrinkage and calls —
2098    /// kind is a binding-level fact only rebinding can erase.
2099    pub fn expr_is_tracked_collection(&self, e: &Expr) -> bool {
2100        self.collections.contains(&(e as *const Expr as usize))
2101    }
2102
2103    /// PROOF that two handles denote distinct allocations at every iteration
2104    /// of `loop_stmt` (a While/Repeat analyzed to convergence). This is the
2105    /// soundness gate for borrow hoisting: `true` only when both handles
2106    /// have fully tracked provenance and their may-alias components do not
2107    /// meet at the loop invariant — including loop-CARRIED edges. Anything
2108    /// else (tainted handle, non-converged loop, unknown statement,
2109    /// concurrent context) refuses.
2110    pub fn loop_handles_definitely_distinct(
2111        &self,
2112        loop_stmt: &Stmt,
2113        a: Symbol,
2114        b: Symbol,
2115    ) -> bool {
2116        let key = loop_stmt as *const Stmt as usize;
2117        match self.loop_aliases.get(&key) {
2118            Some(g) => g.definitely_distinct(a, b),
2119            None => false,
2120        }
2121    }
2122
2123    /// Finite proven LENGTH bounds for this collection occurrence.
2124    pub fn expr_len_range(&self, e: &Expr) -> Option<(i64, i64)> {
2125        let iv = self.lengths.get(&(e as *const Expr as usize))?;
2126        match (&iv.lo, &iv.hi) {
2127            (Bound::Finite(lo), Bound::Finite(hi)) if lo <= hi => Some((*lo, *hi)),
2128            _ => None,
2129        }
2130    }
2131
2132    /// True when `index` is proven in bounds RELATIONALLY (multi-variable LIA /
2133    /// SCEV / entry-guard), as opposed to a plain interval bound. The swap
2134    /// peephole uses this to keep partition swaps (quicksort's `i`/`j`, related
2135    /// by `i <= j < hi <= len`) as the unchecked indexed form instead of
2136    /// downgrading them to a bounds-checked `.swap()`; literal-index swaps
2137    /// (interval-proven) still become `.swap()`/`__swap_tmp`.
2138    pub fn index_proven_relational(&self, index: &Expr) -> bool {
2139        self.relational_inbounds.contains(&(index as *const Expr as usize))
2140    }
2141
2142    /// The guard-elision query (EXODIA 1.3): is `index` PROVABLY within the
2143    /// 1-based bounds of `collection` at this program point? True only when
2144    /// the index interval sits inside [1, len.lo].
2145    pub fn index_provably_in_bounds(&self, collection: &Expr, index: &Expr) -> bool {
2146        // Relational induction-variable bound (SCEV-style): the loop guard
2147        // already proved `1 <= index <= length(collection)` for this exact
2148        // read. Intervals alone cannot express it.
2149        if self.relational_inbounds.contains(&(index as *const Expr as usize)) {
2150            return true;
2151        }
2152        // Non-relational interval check: a concrete index interval inside
2153        // `[1, proven-minimum-length]`.
2154        let Some(len) = self.lengths.get(&(collection as *const Expr as usize)) else {
2155            return false;
2156        };
2157        let Some((ilo, ihi)) = self.expr_int_range(index) else { return false };
2158        let len_lo = match len.lo {
2159            Bound::Finite(v) => v,
2160            _ => return false,
2161        };
2162        ilo >= 1 && ihi <= len_lo
2163    }
2164
2165    /// The `% m` divisors whose `m >= 1` precondition an elision of this exact
2166    /// `index` relied on (empty for a purely affine/interval elision). Codegen
2167    /// emits `assert!(m > 0)` for each at the access, discharging the symbolic
2168    /// element bound's precondition so the elision is sound for every input.
2169    pub fn index_positivity_guards(&self, index: &Expr) -> &[Symbol] {
2170        self.positivity_guards
2171            .get(&(index as *const Expr as usize))
2172            .map_or(&[], |v| v.as_slice())
2173    }
2174}
2175
2176/// Compute a bottom-up product fact for `e` and every subexpression, at the
2177/// state `st` (the pre-state of the statement that contains it).
2178fn record_expr(e: &Expr, st: &RichAbstractState, facts: &mut OracleFacts) {
2179    use crate::ast::stmt::StringPart;
2180    match e {
2181        Expr::BinaryOp { left, right, .. }
2182        | Expr::Union { left, right }
2183        | Expr::Intersection { left, right }
2184        | Expr::Range { start: left, end: right } => {
2185            record_expr(left, st, facts);
2186            record_expr(right, st, facts);
2187        }
2188        Expr::Not { operand } => record_expr(operand, st, facts),
2189        Expr::Call { args, .. } => {
2190            for a in args {
2191                record_expr(a, st, facts);
2192            }
2193        }
2194        Expr::CallExpr { callee, args } => {
2195            record_expr(callee, st, facts);
2196            for a in args {
2197                record_expr(a, st, facts);
2198            }
2199        }
2200        Expr::Index { collection, index } => {
2201            record_expr(collection, st, facts);
2202            record_expr(index, st, facts);
2203        }
2204        Expr::Slice { collection, start, end } => {
2205            record_expr(collection, st, facts);
2206            record_expr(start, st, facts);
2207            record_expr(end, st, facts);
2208        }
2209        Expr::Copy { expr } => record_expr(expr, st, facts),
2210        Expr::Give { value } => record_expr(value, st, facts),
2211        Expr::Length { collection } => record_expr(collection, st, facts),
2212        Expr::Contains { collection, value } => {
2213            record_expr(collection, st, facts);
2214            record_expr(value, st, facts);
2215        }
2216        Expr::List(items) | Expr::Tuple(items) => {
2217            for i in items {
2218                record_expr(i, st, facts);
2219            }
2220        }
2221        Expr::FieldAccess { object, .. } => record_expr(object, st, facts),
2222        Expr::New { init_fields, .. } => {
2223            for (_, fe) in init_fields {
2224                record_expr(fe, st, facts);
2225            }
2226        }
2227        Expr::NewVariant { fields, .. } => {
2228            for (_, fe) in fields {
2229                record_expr(fe, st, facts);
2230            }
2231        }
2232        Expr::OptionSome { value } => record_expr(value, st, facts),
2233        Expr::WithCapacity { value, capacity } => {
2234            record_expr(value, st, facts);
2235            record_expr(capacity, st, facts);
2236        }
2237        Expr::InterpolatedString(parts) => {
2238            for p in parts {
2239                if let StringPart::Expr { value, .. } = p {
2240                    record_expr(value, st, facts);
2241                }
2242            }
2243        }
2244        _ => {}
2245    }
2246    let av = AbstractValue {
2247        interval: eval_expr(e, &st.intervals),
2248        ty: eval_type(e, &st.types, &st.fn_returns, &st.elem_type),
2249        shape: match e {
2250            Expr::Identifier(sym) => {
2251                st.shapes.get(sym).cloned().unwrap_or(CollectionShape::Top)
2252            }
2253            _ => CollectionShape::Top,
2254        },
2255        nullability: nullability_of_expr(e, st),
2256        alias: AliasInfo::top(),
2257    };
2258    facts.record(e, av);
2259    if let Expr::Identifier(sym) = e {
2260        facts.record_length(e, st.intervals.get_length(sym));
2261        if !facts.suppress_exprs && st.coll_vars.contains(sym) {
2262            facts.collections.insert(e as *const Expr as usize);
2263        }
2264    }
2265}
2266
2267/// **Debugger-only** traversal: invoke `f(sym, addr)` for every `Identifier(sym)`
2268/// occurrence in `stmts`, mirroring the production walk's coverage so the addresses
2269/// line up with the recorded `exprs`. Never called on a production compile path —
2270/// only [`OracleFacts::summarize_variables`] uses it. Incompleteness here would only
2271/// narrow the debugger's summary (still sound), never affect codegen.
2272fn collect_idents_block(stmts: &[Stmt], f: &mut dyn FnMut(Symbol, usize)) {
2273    for s in stmts {
2274        collect_idents_stmt(s, f);
2275    }
2276}
2277
2278fn collect_idents_stmt(stmt: &Stmt, f: &mut dyn FnMut(Symbol, usize)) {
2279    use crate::ast::stmt::ReadSource;
2280    // Direct expressions a statement holds (mirror `record_stmt_exprs`).
2281    match stmt {
2282        Stmt::Let { value, .. } | Stmt::Set { value, .. } => collect_idents_expr(value, f),
2283        Stmt::Return { value: Some(e) } => collect_idents_expr(e, f),
2284        Stmt::Call { args, .. } => {
2285            for a in args {
2286                collect_idents_expr(a, f);
2287            }
2288        }
2289        Stmt::If { cond, .. } | Stmt::While { cond, .. } => collect_idents_expr(cond, f),
2290        Stmt::Repeat { iterable, .. } => collect_idents_expr(iterable, f),
2291        Stmt::Show { object, .. } | Stmt::Give { object, .. } => collect_idents_expr(object, f),
2292        Stmt::Push { value, collection }
2293        | Stmt::Add { value, collection }
2294        | Stmt::Remove { value, collection } => {
2295            collect_idents_expr(value, f);
2296            collect_idents_expr(collection, f);
2297        }
2298        Stmt::SetIndex { collection, index, value } => {
2299            collect_idents_expr(collection, f);
2300            collect_idents_expr(index, f);
2301            collect_idents_expr(value, f);
2302        }
2303        Stmt::SetField { object, value, .. } => {
2304            collect_idents_expr(object, f);
2305            collect_idents_expr(value, f);
2306        }
2307        Stmt::Inspect { target, .. } => collect_idents_expr(target, f),
2308        Stmt::RuntimeAssert { condition, .. } => collect_idents_expr(condition, f),
2309        Stmt::Sleep { milliseconds } => collect_idents_expr(milliseconds, f),
2310        Stmt::ReadFrom { source: ReadSource::File(p), .. } => collect_idents_expr(p, f),
2311        _ => {}
2312    }
2313    // Nested blocks (mirror the block recursion the production walk does).
2314    match stmt {
2315        Stmt::If { then_block, else_block, .. } => {
2316            collect_idents_block(then_block, f);
2317            if let Some(eb) = else_block {
2318                collect_idents_block(eb, f);
2319            }
2320        }
2321        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => collect_idents_block(body, f),
2322        Stmt::FunctionDef { body, .. } => collect_idents_block(body, f),
2323        Stmt::Inspect { arms, .. } => {
2324            for arm in arms {
2325                collect_idents_block(arm.body, f);
2326            }
2327        }
2328        Stmt::Zone { body, .. } => collect_idents_block(body, f),
2329        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => collect_idents_block(tasks, f),
2330        _ => {}
2331    }
2332}
2333
2334fn collect_idents_expr(e: &Expr, f: &mut dyn FnMut(Symbol, usize)) {
2335    use crate::ast::stmt::StringPart;
2336    if let Expr::Identifier(sym) = e {
2337        f(*sym, e as *const Expr as usize);
2338    }
2339    match e {
2340        Expr::BinaryOp { left, right, .. }
2341        | Expr::Union { left, right }
2342        | Expr::Intersection { left, right }
2343        | Expr::Range { start: left, end: right } => {
2344            collect_idents_expr(left, f);
2345            collect_idents_expr(right, f);
2346        }
2347        Expr::Not { operand } => collect_idents_expr(operand, f),
2348        Expr::Call { args, .. } => {
2349            for a in args {
2350                collect_idents_expr(a, f);
2351            }
2352        }
2353        Expr::CallExpr { callee, args } => {
2354            collect_idents_expr(callee, f);
2355            for a in args {
2356                collect_idents_expr(a, f);
2357            }
2358        }
2359        Expr::Index { collection, index } => {
2360            collect_idents_expr(collection, f);
2361            collect_idents_expr(index, f);
2362        }
2363        Expr::Slice { collection, start, end } => {
2364            collect_idents_expr(collection, f);
2365            collect_idents_expr(start, f);
2366            collect_idents_expr(end, f);
2367        }
2368        Expr::Copy { expr } => collect_idents_expr(expr, f),
2369        Expr::Give { value } => collect_idents_expr(value, f),
2370        Expr::Length { collection } => collect_idents_expr(collection, f),
2371        Expr::Contains { collection, value } => {
2372            collect_idents_expr(collection, f);
2373            collect_idents_expr(value, f);
2374        }
2375        Expr::List(items) | Expr::Tuple(items) => {
2376            for i in items {
2377                collect_idents_expr(i, f);
2378            }
2379        }
2380        Expr::FieldAccess { object, .. } => collect_idents_expr(object, f),
2381        Expr::New { init_fields, .. } => {
2382            for (_, fe) in init_fields {
2383                collect_idents_expr(fe, f);
2384            }
2385        }
2386        Expr::NewVariant { fields, .. } => {
2387            for (_, fe) in fields {
2388                collect_idents_expr(fe, f);
2389            }
2390        }
2391        Expr::OptionSome { value } => collect_idents_expr(value, f),
2392        Expr::WithCapacity { value, capacity } => {
2393            collect_idents_expr(value, f);
2394            collect_idents_expr(capacity, f);
2395        }
2396        Expr::InterpolatedString(parts) => {
2397            for p in parts {
2398                if let StringPart::Expr { value, .. } = p {
2399                    collect_idents_expr(value, f);
2400                }
2401            }
2402        }
2403        _ => {}
2404    }
2405}
2406
2407/// What a loop guard bounds an induction variable by.
2408enum IvBound {
2409    /// `length of arr`.
2410    LenOf(Symbol),
2411    /// A program variable `n` (related to an array length by a build-loop fact
2412    /// `length(arr) >= n + off`), optionally minus a provably-nonnegative
2413    /// amount: the guard is `iv <op> n - headroom_or_more`, so `iv` clears the
2414    /// length by at least `headroom` — extra room for a positive affine offset
2415    /// (e.g. `j <= n - 1 - i` lets `item (j + 1) of arr` stay in bounds).
2416    Var { var: Symbol, headroom: i64 },
2417}
2418
2419/// A loop guard `iv </<= bound` bounding the induction variable from above.
2420struct IvGuard {
2421    iv: Symbol,
2422    bound: IvBound,
2423    /// `true` for `<` (so `iv <= bound - 1`), `false` for `<=`.
2424    strict: bool,
2425    /// Proven lower bound of `iv` at the loop body (its 1-based start).
2426    iv_lo: i64,
2427}
2428
2429/// RELATIONAL induction-variable bound elimination (V8 TurboFan / LLVM SCEV).
2430///
2431/// The interval domain proves `iv ∈ [lo, …]` but cannot represent the
2432/// relation `iv <= length(arr)` a loop guard establishes between two
2433/// variables. This recognizer reconstructs it: from a guard `iv </<= B`
2434/// (where `B` is `length of arr` directly, or a variable `n` tied to the
2435/// array's length by a build-loop fact `length(arr) >= n + off`), every
2436/// in-body read `item E of arr` with `E = iv + k` affine in `iv` is proven
2437/// `1 <= E <= length(arr)` — exactly what V8/LLVM hoist. Each proven read's
2438/// `index` address is recorded so the bytecode compiler emits
2439/// `IndexUnchecked` and the JIT drops the bounds branch.
2440fn record_loop_index_bounds(
2441    cond: &Expr,
2442    body: &[Stmt],
2443    state: &RichAbstractState,
2444    facts: &mut OracleFacts,
2445) {
2446    let mut guards: Vec<IvGuard> = Vec::new();
2447    collect_iv_guards(cond, state, &mut guards);
2448    if guards.is_empty() {
2449        return;
2450    }
2451    // A call (a callee may resize) or any unmodeled statement could change a
2452    // length or alias an array — refuse the whole proof. Resizes of SPECIFIC
2453    // arrays are tolerated: those arrays start clobbered, so their own reads
2454    // stay checked, while a stable array read alongside them still elides.
2455    if !body_is_index_proof_safe(body) {
2456        return;
2457    }
2458    let mut clobbered: std::collections::HashSet<Symbol> = std::collections::HashSet::new();
2459    collect_resized_arrays(body, &mut clobbered);
2460    record_proven_reads(body, &guards, &state.length_def, &mut clobbered, facts);
2461}
2462
2463// ===================================================================
2464// GENERAL multi-variable bounds elision via the kernel LIA prover.
2465//
2466// Where the single-variable recognizer above proves `item (iv+k) of arr` and
2467// the interval check proves a constant index into a constant-length array,
2468// this proves ANY affine index `item E of arr` whose `1 <= E <= length(arr)`
2469// follows from the enclosing loop guard, the path guards on the way in, the
2470// loop-invariant scalar ranges (INCLUDING element bounds reached through
2471// `Let v be item k of B` — A2), the affine scalar definitions (`cols =
2472// capacity + 1`), and a symbolic length lower bound. Each `1 <= E` and
2473// `E <= length` obligation is discharged by `logicaffeine_kernel::lia`
2474// (Fourier–Motzkin) and recorded into `relational_inbounds`, shared with the
2475// single-var path and consumed by codegen and the VM. Knapsack's
2476// `prev[w-wi+1]` (guard `w <= capacity`, path guard `w >= wi`, `length(prev) =
2477// cols = capacity + 1`, element bound `wi ∈ [1,50]`) is the canonical case.
2478// ===================================================================
2479
2480/// The affine fact context threaded down a loop nest: the kernel constraints
2481/// in scope (loop and path guards, monotone-IV lower bounds) plus the symbols
2482/// they mention, so a proof can pull in those variables' interval bounds and
2483/// scalar definitions.
2484#[derive(Default, Clone)]
2485struct AffineFacts {
2486    constraints: Vec<super::affine::Constraint>,
2487    syms: Vec<Symbol>,
2488}
2489
2490fn record_affine_index_bounds(
2491    cond: &Expr,
2492    body: &[Stmt],
2493    state: &RichAbstractState,
2494    entry: &RichAbstractState,
2495    facts: &mut OracleFacts,
2496) {
2497    if !body_is_index_proof_safe(body) {
2498        return;
2499    }
2500    let mut clobbered: std::collections::HashSet<Symbol> = std::collections::HashSet::new();
2501    collect_resized_arrays(body, &mut clobbered);
2502    let mutated: std::collections::HashSet<Symbol> =
2503        collect_mutations(body).into_iter().collect();
2504    // The loop fixpoint merges `scalar_upper`/`scalar_lower` across the back-edge
2505    // but NOT `scalar_def`, so a mid-body `Let complement be target - x` is gone
2506    // by the record state. Re-establish the body's TOP-LEVEL affine definitions
2507    // (their RHS is straight-line before every later use, and the bound var is not
2508    // reassigned), so a key proof can follow the chain `complement -> target - x
2509    // -> x`'s element bounds`. Sound: each added equality holds at every use site.
2510    let state = {
2511        let mut st = state.clone();
2512        augment_body_scalar_defs(body, &mut st);
2513        st
2514    };
2515    let inits: HashMap<Symbol, i64> = HashMap::new();
2516    let mut amb = AffineFacts::default();
2517    affine_add_loop_guards(cond, body, &state, Some(entry), &inits, &mut amb);
2518    affine_walk(body, &state, &clobbered, &mutated, &amb, &inits, facts);
2519}
2520
2521/// Re-establish the body's TOP-LEVEL affine scalar definitions into a proof
2522/// state. Only straight-line `Let v be <affine>` where `v` is not reassigned in
2523/// the body (so the equality holds at every later use in the iteration) and the
2524/// RHS is variable-bearing and non-self-referential — exactly the form the LIA
2525/// prover consumes. Conditional (nested-block) defs are skipped: their equality
2526/// is not unconditional. Never overwrites an existing `scalar_def`.
2527fn augment_body_scalar_defs(body: &[Stmt], st: &mut RichAbstractState) {
2528    // Scalars reassigned via `Set` anywhere in the body: a def naming any of them
2529    // (as the bound var OR in its RHS) is not a stable iteration invariant.
2530    let mut reassigned = std::collections::HashSet::new();
2531    collect_set_targets(body, &mut reassigned);
2532    // Only a single, unshadowed top-level `Let` has an unambiguous definition.
2533    let mut let_count: HashMap<Symbol, u32> = HashMap::new();
2534    for s in body {
2535        if let Stmt::Let { var, .. } = s {
2536            *let_count.entry(*var).or_insert(0) += 1;
2537        }
2538    }
2539    for s in body {
2540        if let Stmt::Let { var, value, .. } = s {
2541            if reassigned.contains(var)
2542                || let_count.get(var) != Some(&1)
2543                || st.scalar_def.contains_key(var)
2544            {
2545                continue;
2546            }
2547            if let Some(e) = super::affine::lin_of(value) {
2548                // Every RHS variable must be single-assignment within the body
2549                // (not `Set`-reassigned and not re-`Let`/shadowed) so its value is
2550                // fixed for the iteration — then `var = E` holds at every later use.
2551                // An outer variable (absent from `let_count`) is stable unless it is
2552                // reassigned in the body.
2553                let rhs_stable = e.coefficients.keys().all(|&i| {
2554                    let s = Symbol::from_index(i as usize);
2555                    !reassigned.contains(&s) && let_count.get(&s).map_or(true, |&c| c <= 1)
2556                });
2557                if rhs_stable
2558                    && !e.coefficients.is_empty()
2559                    && !e.coefficients.contains_key(&(var.index() as i64))
2560                {
2561                    st.scalar_def.insert(*var, e);
2562                }
2563            }
2564        }
2565    }
2566}
2567
2568/// Collect every scalar `Set` reassignment target in `stmts` (recursing into
2569/// nested blocks). Distinct from `collect_mutations`, which also counts `Let`
2570/// bindings — here only true reassignments matter.
2571fn collect_set_targets(stmts: &[Stmt], out: &mut std::collections::HashSet<Symbol>) {
2572    for s in stmts {
2573        if let Stmt::Set { target, .. } = s {
2574            out.insert(*target);
2575        }
2576        each_child_block(s, &mut |b| collect_set_targets(b, out));
2577    }
2578}
2579
2580/// `a <op> b` (optionally negated) as a kernel constraint, when both sides are
2581/// affine.
2582fn affine_cmp(
2583    op: BinaryOpKind,
2584    a: &Expr,
2585    b: &Expr,
2586    negate: bool,
2587) -> Option<super::affine::Constraint> {
2588    use super::affine::{ge, gt, le, lin_of, lt};
2589    let (la, lb) = (lin_of(a)?, lin_of(b)?);
2590    Some(match (op, negate) {
2591        (BinaryOpKind::Lt, false) | (BinaryOpKind::GtEq, true) => lt(&la, &lb),
2592        (BinaryOpKind::LtEq, false) | (BinaryOpKind::Gt, true) => le(&la, &lb),
2593        (BinaryOpKind::Gt, false) | (BinaryOpKind::LtEq, true) => gt(&la, &lb),
2594        (BinaryOpKind::GtEq, false) | (BinaryOpKind::Lt, true) => ge(&la, &lb),
2595        _ => return None,
2596    })
2597}
2598
2599/// Add a comparison (and the symbols it mentions) to a fact set, descending
2600/// through `and`. A disjunctive (`or`) guard cannot be soundly negated
2601/// termwise, so a negated `or`/anything non-comparison is dropped.
2602fn affine_add_cmp(cond: &Expr, negate: bool, out: &mut AffineFacts) {
2603    match cond {
2604        Expr::BinaryOp { op: BinaryOpKind::And, left, right } if !negate => {
2605            affine_add_cmp(left, false, out);
2606            affine_add_cmp(right, false, out);
2607        }
2608        Expr::BinaryOp {
2609            op:
2610                op @ (BinaryOpKind::Lt
2611                | BinaryOpKind::LtEq
2612                | BinaryOpKind::Gt
2613                | BinaryOpKind::GtEq),
2614            left,
2615            right,
2616        } => {
2617            if let Some(c) = affine_cmp(*op, left, right, negate) {
2618                out.constraints.push(c);
2619                affine_collect_syms(left, &mut out.syms);
2620                affine_collect_syms(right, &mut out.syms);
2621            }
2622        }
2623        _ => {}
2624    }
2625}
2626
2627/// Collect the identifier symbols of an affine-shaped expression.
2628fn affine_collect_syms(e: &Expr, out: &mut Vec<Symbol>) {
2629    match e {
2630        Expr::Identifier(s) => {
2631            if !out.contains(s) {
2632                out.push(*s);
2633            }
2634        }
2635        Expr::BinaryOp { left, right, .. } => {
2636            affine_collect_syms(left, out);
2637            affine_collect_syms(right, out);
2638        }
2639        _ => {}
2640    }
2641}
2642
2643/// A write `Set iv to value` keeps `iv >= lo` (given the induction hypothesis
2644/// `iv >= lo` before it): an increment by a non-negative literal, a literal
2645/// `>= lo`, or assignment from a LOOP-INVARIANT variable whose interval lower
2646/// is `>= lo`. The loop-invariant requirement makes `state`'s interval for that
2647/// variable a sound bound (a body-mutated source could differ mid-iteration).
2648fn write_keeps_ge(
2649    value: &Expr,
2650    iv: Symbol,
2651    lo: i64,
2652    state: &RichAbstractState,
2653    mutated: &std::collections::HashSet<Symbol>,
2654) -> bool {
2655    match value {
2656        Expr::BinaryOp { op: BinaryOpKind::Add, left, right } => {
2657            let is_iv = |e: &&Expr| matches!(e, Expr::Identifier(s) if *s == iv);
2658            let nonneg = |e: &&Expr| matches!(e, Expr::Literal(Literal::Number(n)) if *n >= 0);
2659            (is_iv(&left) && nonneg(&right)) || (nonneg(&left) && is_iv(&right))
2660        }
2661        Expr::Literal(Literal::Number(n)) => *n >= lo,
2662        Expr::Identifier(s) if !mutated.contains(s) => {
2663            matches!(state.intervals.get_var(s).lo, Bound::Finite(l) if l >= lo)
2664        }
2665        _ => false,
2666    }
2667}
2668
2669/// A SOUND lower bound for a loop variable throughout the body: its entry value
2670/// `lo`, provided every write to it keeps it `>= lo` (induction). The entry
2671/// value comes from an explicit init `Let j be 0` in the ENCLOSING body
2672/// (`inits`) when available — essential for a NESTED loop whose counter is
2673/// reset each outer iteration (so the outer state widens it to top) — else the
2674/// loop-invariant state interval. More general than pure monotonicity: it
2675/// survives the `Set j to needleLen` of a break loop (needleLen loop-invariant,
2676/// `>= 0`), where a strict increment-only check gives up.
2677fn iv_lower_bound(
2678    iv: Symbol,
2679    body: &[Stmt],
2680    state: &RichAbstractState,
2681    mutated: &std::collections::HashSet<Symbol>,
2682    inits: &HashMap<Symbol, i64>,
2683) -> Option<i64> {
2684    let lo = match inits.get(&iv) {
2685        Some(c) => *c,
2686        None => match state.intervals.get_var(&iv).lo {
2687            Bound::Finite(l) => l,
2688            _ => return None,
2689        },
2690    };
2691    fn preserved(
2692        iv: Symbol,
2693        stmts: &[Stmt],
2694        lo: i64,
2695        state: &RichAbstractState,
2696        mutated: &std::collections::HashSet<Symbol>,
2697    ) -> bool {
2698        stmts.iter().all(|s| match s {
2699            Stmt::Set { target, value } if *target == iv => {
2700                write_keeps_ge(value, iv, lo, state, mutated)
2701            }
2702            Stmt::Let { var, value, .. } if *var == iv => {
2703                write_keeps_ge(value, iv, lo, state, mutated)
2704            }
2705            Stmt::If { then_block, else_block, .. } => {
2706                preserved(iv, then_block, lo, state, mutated)
2707                    && else_block.map_or(true, |eb| preserved(iv, eb, lo, state, mutated))
2708            }
2709            Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
2710                preserved(iv, body, lo, state, mutated)
2711            }
2712            _ => true,
2713        })
2714    }
2715    preserved(iv, body, lo, state, mutated).then_some(lo)
2716}
2717
2718/// Add a loop's guard constraints, plus a sound lower bound for each induction
2719/// variable, to the fact set. `inits` carries explicit counter initializations
2720/// from the enclosing body (`Let j be 0`) so a reset nested counter still gets
2721/// its entry lower bound.
2722/// `Set v to v + c` or `Set v to c + v` (literal `c`) → `Some(c)`.
2723pub(crate) fn self_increment(v: Symbol, value: &Expr) -> Option<i64> {
2724    if let Expr::BinaryOp { op: BinaryOpKind::Add, left, right } = value {
2725        if let (Expr::Identifier(s), Expr::Literal(Literal::Number(c))) = (&**left, &**right) {
2726            if *s == v {
2727                return Some(*c);
2728            }
2729        }
2730        if let (Expr::Literal(Literal::Number(c)), Expr::Identifier(s)) = (&**left, &**right) {
2731            if *s == v {
2732                return Some(*c);
2733            }
2734        }
2735    }
2736    None
2737}
2738
2739/// Total number of writes (`Set`/`Let`) to `v` anywhere in `stmts`.
2740pub(crate) fn count_writes_of(stmts: &[Stmt], v: Symbol) -> usize {
2741    let mut n = 0;
2742    for s in stmts {
2743        match s {
2744            Stmt::Set { target, .. } if *target == v => n += 1,
2745            Stmt::Let { var, .. } if *var == v => n += 1,
2746            Stmt::If { then_block, else_block, .. } => {
2747                n += count_writes_of(then_block, v);
2748                if let Some(eb) = else_block {
2749                    n += count_writes_of(eb, v);
2750                }
2751            }
2752            Stmt::While { body, .. } | Stmt::Repeat { body, .. } => n += count_writes_of(body, v),
2753            _ => {}
2754        }
2755    }
2756    n
2757}
2758
2759/// Variables that have a TOP-LEVEL (direct child of `body`, not nested in any
2760/// `If`/`While`) `Set v to v + dj` (`dj > 0`) that is `v`'s ONLY write in the
2761/// whole body — so each iteration increments `v` by exactly `dj`.
2762fn top_level_unconditional_incs(body: &[Stmt]) -> Vec<(Symbol, i64)> {
2763    let mut out = Vec::new();
2764    for s in body {
2765        if let Stmt::Set { target, value } = s {
2766            if let Some(c) = self_increment(*target, value) {
2767                if c > 0 && count_writes_of(body, *target) == 1 {
2768                    out.push((*target, c));
2769                }
2770            }
2771        }
2772    }
2773    out
2774}
2775
2776/// Is every write to `v` in `stmts` a `Set v to v + ci` (`ci >= 0`), none inside
2777/// a nested loop, with the writes summing to `<= limit` and at least one present?
2778/// Such a `v` increases by at most `limit` per outer iteration.
2779fn monotone_inc_within(stmts: &[Stmt], v: Symbol, in_loop: bool, total: &mut i64, count: &mut usize) -> bool {
2780    for s in stmts {
2781        match s {
2782            Stmt::Set { target, value } if *target == v => match self_increment(v, value) {
2783                Some(c) if c >= 0 && !in_loop => {
2784                    *total += c;
2785                    *count += 1;
2786                }
2787                _ => return false,
2788            },
2789            Stmt::Let { var, .. } if *var == v => return false,
2790            Stmt::If { then_block, else_block, .. } => {
2791                if !monotone_inc_within(then_block, v, in_loop, total, count) {
2792                    return false;
2793                }
2794                if let Some(eb) = else_block {
2795                    if !monotone_inc_within(eb, v, in_loop, total, count) {
2796                        return false;
2797                    }
2798                }
2799            }
2800            Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
2801                if !monotone_inc_within(body, v, true, total, count) {
2802                    return false;
2803                }
2804            }
2805            _ => {}
2806        }
2807    }
2808    true
2809}
2810
2811/// Candidate `i` variables for an `i <= j` invariant: monotone increments
2812/// summing to `<= limit`, at least one increment, none inside a nested loop.
2813fn monotone_inc_vars(body: &[Stmt], limit: i64) -> Vec<Symbol> {
2814    let mut cands: Vec<Symbol> = Vec::new();
2815    fn collect(stmts: &[Stmt], out: &mut Vec<Symbol>) {
2816        for s in stmts {
2817            match s {
2818                Stmt::Set { target, value } if self_increment(*target, value).is_some() => {
2819                    if !out.contains(target) {
2820                        out.push(*target);
2821                    }
2822                }
2823                Stmt::If { then_block, else_block, .. } => {
2824                    collect(then_block, out);
2825                    if let Some(eb) = else_block {
2826                        collect(eb, out);
2827                    }
2828                }
2829                Stmt::While { body, .. } | Stmt::Repeat { body, .. } => collect(body, out),
2830                _ => {}
2831            }
2832        }
2833    }
2834    collect(body, &mut cands);
2835    cands
2836        .into_iter()
2837        .filter(|v| {
2838            let (mut total, mut count) = (0, 0);
2839            monotone_inc_within(body, *v, false, &mut total, &mut count)
2840                && count >= 1
2841                && total <= limit
2842        })
2843        .collect()
2844}
2845
2846/// Derive `i <= j` loop invariants (the quicksort/Lomuto partition shape).
2847///
2848/// `j` increments unconditionally by `dj` at the top of the body; `i`'s only
2849/// writes are `Set i to i + ci` (`ci >= 0`) summing to `<= dj`, none inside a
2850/// nested loop; and `i` and `j` start from the SAME value (equal `scalar_def` at
2851/// loop entry — covers `i=1,j=1` and `i=lo,j=lo` alike). Then `i0 = j0` and
2852/// `Δi <= Δj` each iteration, so `i <= j` holds at the loop head. `affine_walk`
2853/// drops the fact when `i` (or `j`) is mutated, so it only proves accesses
2854/// BEFORE the increment — exactly the partition's `item i of arr` read/store.
2855fn derive_iv_le_invariants(
2856    body: &[Stmt],
2857    entry: &RichAbstractState,
2858    state: &RichAbstractState,
2859    mutated: &std::collections::HashSet<Symbol>,
2860    inits: &HashMap<Symbol, i64>,
2861    amb: &mut AffineFacts,
2862) {
2863    use super::affine::{ge, konst, le, var};
2864    for (j, dj) in top_level_unconditional_incs(body) {
2865        for i in monotone_inc_vars(body, dj) {
2866            if i == j {
2867                continue;
2868            }
2869            // i0 == j0: `i` and `j` are defined by the same linear expression at
2870            // loop entry (constant or symbolic `lo`).
2871            let equal_start = match (entry.scalar_def.get(&i), entry.scalar_def.get(&j)) {
2872                (Some(a), Some(b)) => a == b,
2873                _ => false,
2874            };
2875            if !equal_start {
2876                continue;
2877            }
2878            amb.constraints.push(le(&var(i), &var(j)));
2879            // `1 <= i` obligation needs `i`'s sound lower bound (it is not a
2880            // guard variable, so the guard pass would not add it).
2881            if let Some(lo) = iv_lower_bound(i, body, state, mutated, inits) {
2882                amb.constraints.push(ge(&var(i), &konst(lo)));
2883            }
2884            for s in [i, j] {
2885                if !amb.syms.contains(&s) {
2886                    amb.syms.push(s);
2887                }
2888            }
2889        }
2890    }
2891}
2892
2893fn affine_add_loop_guards(
2894    cond: &Expr,
2895    body: &[Stmt],
2896    state: &RichAbstractState,
2897    entry: Option<&RichAbstractState>,
2898    inits: &HashMap<Symbol, i64>,
2899    amb: &mut AffineFacts,
2900) {
2901    affine_add_cmp(cond, false, amb);
2902    let mutated: std::collections::HashSet<Symbol> =
2903        collect_mutations(body).into_iter().collect();
2904    let mut ivs: Vec<Symbol> = Vec::new();
2905    affine_collect_syms(cond, &mut ivs);
2906    for iv in ivs {
2907        if let Some(lo) = iv_lower_bound(iv, body, state, &mutated, inits) {
2908            amb.constraints
2909                .push(super::affine::ge(&super::affine::var(iv), &super::affine::konst(lo)));
2910            if !amb.syms.contains(&iv) {
2911                amb.syms.push(iv);
2912            }
2913        }
2914    }
2915    // Two-induction-variable `i <= j` relations need the true loop-ENTRY state to
2916    // establish `i0 = j0`; only the top-level call threads it (nested loops pass
2917    // `None`, since the widened body state cannot witness the entry equality).
2918    if let Some(entry) = entry {
2919        derive_iv_le_invariants(body, entry, state, &mutated, inits, amb);
2920    }
2921}
2922
2923/// Drop ambient facts mentioning any symbol in `set` (a write reaches earlier
2924/// reads across loop iterations, or a sequential reassignment invalidates a
2925/// guard).
2926fn affine_drop_mentioning(amb: &mut AffineFacts, set: &std::collections::HashSet<Symbol>) {
2927    amb.constraints.retain(|c| {
2928        !set.iter().any(|s| c.expr.coefficients.contains_key(&(s.index() as i64)))
2929    });
2930    amb.syms.retain(|s| !set.contains(s));
2931}
2932
2933fn affine_drop_one(amb: &mut AffineFacts, sym: Symbol) {
2934    let k = sym.index() as i64;
2935    amb.constraints.retain(|c| !c.expr.coefficients.contains_key(&k));
2936    amb.syms.retain(|s| *s != sym);
2937}
2938
2939/// Prove every `Index` node in an expression in place (no ref storage —
2940/// avoids threading the arena lifetime through a collecting `Vec`).
2941fn affine_prove_in_expr(
2942    e: &Expr,
2943    state: &RichAbstractState,
2944    clobbered: &std::collections::HashSet<Symbol>,
2945    mutated: &std::collections::HashSet<Symbol>,
2946    amb: &AffineFacts,
2947    facts: &mut OracleFacts,
2948) {
2949    if let Expr::Index { collection, index } = e {
2950        // Dense `Map of Int to Int` get: `item k of m` → prove `0 <= k <= cap(m)`.
2951        if let Expr::Identifier(m) = collection {
2952            if facts.map_caps.contains_key(m) {
2953                try_prove_dense_key(*m, index, state, mutated, amb, facts);
2954            }
2955        }
2956        try_prove_affine(collection, index, state, clobbered, mutated, amb, facts);
2957    }
2958    // Dense map membership: `m contains k` → prove `0 <= k <= cap(m)`.
2959    if let Expr::Contains { collection, value } = e {
2960        if let Expr::Identifier(m) = collection {
2961            if facts.map_caps.contains_key(m) {
2962                try_prove_dense_key(*m, value, state, mutated, amb, facts);
2963            }
2964        }
2965    }
2966    match e {
2967        Expr::BinaryOp { left, right, .. }
2968        | Expr::Union { left, right }
2969        | Expr::Intersection { left, right }
2970        | Expr::Range { start: left, end: right } => {
2971            affine_prove_in_expr(left, state, clobbered, mutated, amb, facts);
2972            affine_prove_in_expr(right, state, clobbered, mutated, amb, facts);
2973        }
2974        Expr::Not { operand } => affine_prove_in_expr(operand, state, clobbered, mutated, amb, facts),
2975        Expr::Contains { collection, value } => {
2976            affine_prove_in_expr(collection, state, clobbered, mutated, amb, facts);
2977            affine_prove_in_expr(value, state, clobbered, mutated, amb, facts);
2978        }
2979        Expr::Index { collection, index } => {
2980            affine_prove_in_expr(collection, state, clobbered, mutated, amb, facts);
2981            affine_prove_in_expr(index, state, clobbered, mutated, amb, facts);
2982        }
2983        Expr::Call { args, .. } => {
2984            for a in args {
2985                affine_prove_in_expr(a, state, clobbered, mutated, amb, facts);
2986            }
2987        }
2988        Expr::Length { collection } => {
2989            affine_prove_in_expr(collection, state, clobbered, mutated, amb, facts)
2990        }
2991        _ => {}
2992    }
2993}
2994
2995/// A symbolic LOWER bound on `length(arr)` (the `var(n) + off` from a counted
2996/// build loop, else a finite concrete length), with the variables it names.
2997fn affine_length_lb(
2998    arr: Symbol,
2999    state: &RichAbstractState,
3000) -> Option<(super::affine::LinExpr, Vec<Symbol>)> {
3001    use super::affine::{konst, var};
3002    if let Some((n, off)) = state.length_def.get(&arr) {
3003        return Some((var(*n).add(&konst(*off)), vec![*n]));
3004    }
3005    if let Bound::Finite(l) = state.intervals.get_length(&arr).lo {
3006        return Some((konst(l), vec![]));
3007    }
3008    None
3009}
3010
3011/// Walk a loop body in execution order, accumulating path guards and nested
3012/// loop guards, and prove each affine index access in scope.
3013fn affine_walk(
3014    stmts: &[Stmt],
3015    state: &RichAbstractState,
3016    clobbered: &std::collections::HashSet<Symbol>,
3017    mutated: &std::collections::HashSet<Symbol>,
3018    ambient: &AffineFacts,
3019    inits: &HashMap<Symbol, i64>,
3020    facts: &mut OracleFacts,
3021) {
3022    let mut local = ambient.clone();
3023    let mut inits = inits.clone();
3024    for stmt in stmts {
3025        for_each_direct_expr(stmt, &mut |e| {
3026            affine_prove_in_expr(e, state, clobbered, mutated, &local, facts);
3027        });
3028        if let Stmt::SetIndex { collection, index, .. } = stmt {
3029            // Dense `Map of Int to Int` insert: `Set item k of m to v` → prove
3030            // `0 <= k <= cap(m)`.
3031            if let Expr::Identifier(m) = collection {
3032                if facts.map_caps.contains_key(m) {
3033                    try_prove_dense_key(*m, index, state, mutated, &local, facts);
3034                }
3035            }
3036            try_prove_affine(collection, index, state, clobbered, mutated, &local, facts);
3037        }
3038        match stmt {
3039            Stmt::If { cond, then_block, else_block } => {
3040                let mut then_facts = local.clone();
3041                affine_add_cmp(cond, false, &mut then_facts);
3042                affine_walk(then_block, state, clobbered, mutated, &then_facts, &inits, facts);
3043                if let Some(eb) = else_block {
3044                    let mut else_facts = local.clone();
3045                    affine_add_cmp(cond, true, &mut else_facts);
3046                    affine_walk(eb, state, clobbered, mutated, &else_facts, &inits, facts);
3047                }
3048            }
3049            Stmt::While { cond, body, .. } => {
3050                // A nested loop's body may rewrite a variable an ambient fact
3051                // rests on (a later iteration reaching an earlier read), so drop
3052                // facts naming anything it mutates before adding its own guard.
3053                let nested: std::collections::HashSet<Symbol> =
3054                    collect_mutations(body).into_iter().collect();
3055                let mut inner = local.clone();
3056                affine_drop_mentioning(&mut inner, &nested);
3057                affine_add_loop_guards(cond, body, state, None, &inits, &mut inner);
3058                affine_walk(body, state, clobbered, mutated, &inner, &inits, facts);
3059            }
3060            Stmt::Repeat { body, .. } => {
3061                affine_walk(body, state, clobbered, mutated, &local, &inits, facts);
3062            }
3063            _ => {}
3064        }
3065        // Track counter initializations (`Let j be 0`) so a following nested
3066        // loop sees the reset counter's true entry lower bound; a reassignment
3067        // to anything else clears it. Also drop ambient facts naming the target.
3068        match stmt {
3069            Stmt::Set { target, value } => {
3070                if let Expr::Literal(Literal::Number(n)) = value {
3071                    inits.insert(*target, *n);
3072                } else {
3073                    inits.remove(target);
3074                }
3075                affine_drop_one(&mut local, *target);
3076            }
3077            Stmt::Let { var, value, .. } => {
3078                if let Expr::Literal(Literal::Number(n)) = value {
3079                    inits.insert(*var, *n);
3080                } else {
3081                    inits.remove(var);
3082                }
3083                affine_drop_one(&mut local, *var);
3084            }
3085            _ => {}
3086        }
3087    }
3088}
3089
3090/// Discharge `1 <= E <= length(arr)` for a single access via the kernel LIA
3091/// prover, recording the index address on success.
3092fn try_prove_affine(
3093    collection: &Expr,
3094    index: &Expr,
3095    state: &RichAbstractState,
3096    clobbered: &std::collections::HashSet<Symbol>,
3097    mutated: &std::collections::HashSet<Symbol>,
3098    amb: &AffineFacts,
3099    facts: &mut OracleFacts,
3100) {
3101    use super::affine::{konst, le, lin_of, prove, var};
3102    let Expr::Identifier(arr) = collection else { return };
3103    if clobbered.contains(arr) {
3104        return;
3105    }
3106    let key = index as *const Expr as usize;
3107    if facts.relational_inbounds.contains(&key) {
3108        return; // already proven (single-var path or a prior visit)
3109    }
3110    let Some(e_lin) = lin_of(index) else { return };
3111    let Some((len_lb, len_syms)) = affine_length_lb(*arr, state) else {
3112        return;
3113    };
3114
3115    let mut system = amb.constraints.clone();
3116    let mut syms = amb.syms.clone();
3117    affine_collect_syms(index, &mut syms);
3118    for s in len_syms {
3119        if !syms.contains(&s) {
3120            syms.push(s);
3121        }
3122    }
3123    for &s in &syms {
3124        // Affine scalar definition `s = E` (relates a length var to a loop
3125        // bound: `cols = capacity + 1`).
3126        if let Some(def) = state.scalar_def.get(&s) {
3127            let sv = var(s);
3128            system.push(le(&sv, def));
3129            system.push(le(def, &sv));
3130        }
3131        // Loop-invariant scalar's interval bounds (the element bound `wi ∈
3132        // [1,50]` reached through `Let wi be item _ of weights`). A MUTATED
3133        // variable's raw header interval is NOT sound mid-body — those get
3134        // bounds only from guards / monotone-IV lowers.
3135        if !mutated.contains(&s) {
3136            let iv = state.intervals.get_var(&s);
3137            if let Bound::Finite(lo) = iv.lo {
3138                system.push(le(&konst(lo), &var(s)));
3139            }
3140            if let Bound::Finite(hi) = iv.hi {
3141                system.push(le(&var(s), &konst(hi)));
3142            }
3143        }
3144        // Symbolic element-source UPPER bound: a scalar read from a `% n`-filled
3145        // array carries `s <= n - 1` — the variable-divisor relation no concrete
3146        // `Interval` can hold (`n` is symbolic). Unlike the raw header interval
3147        // above this is sound for a MUTATED `s`: the in-body `Let u be item _ of
3148        // adj` re-establishes it every iteration, so EACH value of `u` (not just
3149        // the entry one) satisfies it. The bound's own variables (`n`) reach the
3150        // system through the length lower bound that put them in `syms`.
3151        // The element LOWER bound (`u >= 0` for an element of a `% n`-filled
3152        // array) joins the BASE system: it holds regardless of the divisor's
3153        // sign (`X % n` of a non-negative dividend is `>= 0` for any `n != 0`),
3154        // so it needs NO positivity guard. Sound for a MUTATED `s`: a value
3155        // re-read from a fixed array each iteration lands in its proven range.
3156        if let Some(l) = state.scalar_lower.get(&s) {
3157            system.push(le(l, &var(s)));
3158        }
3159    }
3160
3161    // Defensive: a contradictory fact set proves any goal. Never elide from
3162    // inconsistency — if a stale/false hypothesis poisoned the system, refuse.
3163    if !super::affine::consistent(&system) {
3164        return;
3165    }
3166    // 1-based bounds: lower `E - 1 >= 0` (E >= 1), upper `len_lb - E >= 0`.
3167    let lower = e_lin.add(&konst(-1));
3168    let upper = len_lb.sub(&e_lin);
3169    if !prove(&system, &lower) {
3170        return;
3171    }
3172    // A purely AFFINE upper carries no positivity obligation — try it first.
3173    if prove(&system, &upper) {
3174        facts.relational_inbounds.insert(key);
3175        return;
3176    }
3177    // The upper needs the symbolic element UPPER `u <= m - 1` (the variable-
3178    // divisor modulo bound a concrete interval cannot hold). Adding it closes
3179    // the proof under `m >= 1`; record the `% m` divisors so codegen discharges
3180    // that precondition with a nonemptiness-guarded `assert!(m > 0)`, keeping
3181    // the lenient symbolic element join sound for every input.
3182    let mut mod_divisors: Vec<Symbol> = Vec::new();
3183    for &s in &syms {
3184        if let Some(u) = state.scalar_upper.get(&s) {
3185            system.push(le(&var(s), u));
3186            if let Some(m) = mod_upper_divisor(u) {
3187                mod_divisors.push(m);
3188            }
3189        }
3190    }
3191    if super::affine::consistent(&system) && prove(&system, &upper) {
3192        facts.relational_inbounds.insert(key);
3193        if !mod_divisors.is_empty() {
3194            facts.positivity_guards.entry(key).or_default().extend(mod_divisors);
3195        }
3196    }
3197}
3198
3199/// Discharge `0 <= key <= capacity(map)` for one access on a `Map of Int to Int`
3200/// proven dense-eligible, via the SAME kernel LIA prover as `try_prove_affine`.
3201/// The map's capacity is an invariant affine expression captured at its
3202/// declaration (`facts.map_caps`); proving both bounds means a direct-addressed
3203/// array of `capacity + 1` slots indexes every key safely at offset 0
3204/// (`data[key]`), since `key ∈ [0, capacity]`. Records the key address on success;
3205/// the dense gate fires for a map only when EVERY key site is recorded for it.
3206fn try_prove_dense_key(
3207    map: Symbol,
3208    key: &Expr,
3209    state: &RichAbstractState,
3210    mutated: &std::collections::HashSet<Symbol>,
3211    amb: &AffineFacts,
3212    facts: &mut OracleFacts,
3213) {
3214    use super::affine::{consistent, konst, le, lin_of, prove, var};
3215    let key_addr = key as *const Expr as usize;
3216    if facts.dense_map_key_proven.contains_key(&key_addr) {
3217        return;
3218    }
3219    let Some(cap) = facts.map_caps.get(&map).cloned() else {
3220        return;
3221    };
3222    let Some(k_lin) = lin_of(key) else { return };
3223
3224    // Mirror `try_prove_affine`'s system: ambient loop/path guards plus each
3225    // mentioned symbol's sound scalar facts. Collection is TRANSITIVE over the
3226    // scalar-definition and symbolic-bound chains, so an element-derived key
3227    // (`complement = target - x`, `x = item i of arr`) pulls in `target`'s
3228    // definition (`= n`) and `x`'s symbolic element bounds (`0 <= x <= n-1`) —
3229    // closing `0 <= complement <= n`. Each added fact is individually sound, so
3230    // the (consistency-guarded) system only ever proves MORE, never unsoundly.
3231    let mut system = amb.constraints.clone();
3232    let mut syms = amb.syms.clone();
3233    affine_collect_syms(key, &mut syms);
3234    let mut reached: std::collections::HashSet<Symbol> = syms.iter().copied().collect();
3235    let mut w = 0;
3236    while w < syms.len() {
3237        let s = syms[w];
3238        w += 1;
3239        for chained in [
3240            state.scalar_def.get(&s),
3241            state.scalar_upper.get(&s),
3242            state.scalar_lower.get(&s),
3243        ] {
3244            if let Some(e) = chained {
3245                for &idx in e.coefficients.keys() {
3246                    let t = Symbol::from_index(idx as usize);
3247                    if reached.insert(t) {
3248                        syms.push(t);
3249                    }
3250                }
3251            }
3252        }
3253    }
3254    for &s in &syms {
3255        if let Some(def) = state.scalar_def.get(&s) {
3256            let sv = var(s);
3257            system.push(le(&sv, def));
3258            system.push(le(def, &sv));
3259        }
3260        if !mutated.contains(&s) {
3261            let iv = state.intervals.get_var(&s);
3262            if let Bound::Finite(lo) = iv.lo {
3263                system.push(le(&konst(lo), &var(s)));
3264            }
3265            if let Bound::Finite(hi) = iv.hi {
3266                system.push(le(&var(s), &konst(hi)));
3267            }
3268        }
3269        if let Some(l) = state.scalar_lower.get(&s) {
3270            system.push(le(l, &var(s)));
3271        }
3272        if let Some(u) = state.scalar_upper.get(&s) {
3273            system.push(le(&var(s), u));
3274        }
3275    }
3276
3277    // Never elide from an inconsistent system (a false hypothesis proves anything).
3278    if !consistent(&system) {
3279        return;
3280    }
3281    // Lower `key >= 0` and upper `capacity - key >= 0`.
3282    if prove(&system, &k_lin) && prove(&system, &cap.sub(&k_lin)) {
3283        facts.dense_map_key_proven.insert(key_addr, map);
3284        // Presence elision: if the map's inserts fully cover `[A, B]`, prove this
3285        // key is also inside `[A, B]` — then it was definitely written, so `get`
3286        // needs no presence check. `A <= key` and `key <= B`.
3287        if let Some((a, b)) = facts.map_insert_cover.get(&map).cloned() {
3288            if prove(&system, &k_lin.sub(&a)) && prove(&system, &b.sub(&k_lin)) {
3289                facts.map_key_covered.insert(key_addr, map);
3290            }
3291        }
3292    }
3293}
3294
3295/// Collect upper-bound induction guards from a loop condition, descending
3296/// through `and`: `iv </<= B` and the flipped `B >/>= iv`, where `B` is
3297/// `length of arr` or a plain variable. `iv`'s lower bound must be a known
3298/// constant (its loop-start value).
3299fn collect_iv_guards(cond: &Expr, state: &RichAbstractState, out: &mut Vec<IvGuard>) {
3300    match cond {
3301        Expr::BinaryOp { op: BinaryOpKind::And, left, right } => {
3302            collect_iv_guards(left, state, out);
3303            collect_iv_guards(right, state, out);
3304        }
3305        Expr::BinaryOp { op, left, right } => {
3306            let (iv_e, bnd_e, strict): (&Expr, &Expr, bool) = match op {
3307                BinaryOpKind::Lt => (*left, *right, true),
3308                BinaryOpKind::LtEq => (*left, *right, false),
3309                BinaryOpKind::Gt => (*right, *left, true),
3310                BinaryOpKind::GtEq => (*right, *left, false),
3311                _ => return,
3312            };
3313            let Expr::Identifier(iv) = iv_e else { return };
3314            // A non-constant start (e.g. an induction var seeded from a
3315            // parameter) gives no static lower bound — `i64::MIN` makes the
3316            // STATIC proof fail closed, while the SPECULATIVE hoist (which
3317            // checks the lower bound at runtime) still collects the guard.
3318            let iv_lo = match state.intervals.get_var(iv).lo {
3319                Bound::Finite(lo) => lo,
3320                _ => i64::MIN,
3321            };
3322            let bound = match bnd_e {
3323                Expr::Length { collection } => match &**collection {
3324                    Expr::Identifier(arr) => IvBound::LenOf(*arr),
3325                    _ => return,
3326                },
3327                Expr::Identifier(v) => IvBound::Var { var: *v, headroom: 0 },
3328                // `n - t1 - t2 - …` with each subtracted term provably ≥ 0.
3329                _ => match var_minus_bound(bnd_e, state) {
3330                    Some((v, headroom)) => IvBound::Var { var: v, headroom },
3331                    None => return,
3332                },
3333            };
3334            out.push(IvGuard { iv: *iv, bound, strict, iv_lo });
3335        }
3336        _ => {}
3337    }
3338}
3339
3340/// Recognize a bound expression `n - t1 - t2 - …` rooted at a variable `n`,
3341/// where every subtracted term is provably `>= 0`. Returns `(n, headroom)`
3342/// with `headroom = Σ lower-bounds of the subtracted terms` — a sound lower
3343/// bound on `n - bound`, i.e. how far the guard keeps `iv` below `n`.
3344fn var_minus_bound(b: &Expr, state: &RichAbstractState) -> Option<(Symbol, i64)> {
3345    match b {
3346        Expr::Identifier(v) => Some((*v, 0)),
3347        Expr::BinaryOp { op: BinaryOpKind::Subtract, left, right } => {
3348            let (v, head) = var_minus_bound(left, state)?;
3349            // The subtracted term must be provably non-negative (else `n -
3350            // term` could exceed `n` and the bound would be unsound).
3351            let r_lo = match eval_expr(right, &state.intervals).lo {
3352                Bound::Finite(lo) => lo,
3353                _ => return None,
3354            };
3355            if r_lo < 0 {
3356                return None;
3357            }
3358            Some((v, head.saturating_add(r_lo)))
3359        }
3360        _ => None,
3361    }
3362}
3363
3364/// Extract `(var, offset)` from an affine index expression: `v`, `v + k`,
3365/// `k + v`, or `v - k` (`k` an integer literal).
3366fn affine_of(e: &Expr) -> Option<(Symbol, i64)> {
3367    match e {
3368        Expr::Identifier(s) => Some((*s, 0)),
3369        Expr::BinaryOp { op: BinaryOpKind::Add, left, right } => {
3370            match (&**left, &**right) {
3371                (Expr::Identifier(s), Expr::Literal(Literal::Number(k))) => Some((*s, *k)),
3372                (Expr::Literal(Literal::Number(k)), Expr::Identifier(s)) => Some((*s, *k)),
3373                _ => None,
3374            }
3375        }
3376        Expr::BinaryOp { op: BinaryOpKind::Subtract, left, right } => {
3377            match (&**left, &**right) {
3378                (Expr::Identifier(s), Expr::Literal(Literal::Number(k))) => Some((*s, -*k)),
3379                _ => None,
3380            }
3381        }
3382        _ => None,
3383    }
3384}
3385
3386/// If `collection` is an array and `index` is affine in a live guard's
3387/// induction variable that proves it in bounds, record the index's address
3388/// as proven (consumed by the bytecode compiler for both `IndexUnchecked`
3389/// reads and `SetIndexUnchecked` stores).
3390fn try_record_index(
3391    collection: &Expr,
3392    index: &Expr,
3393    guards: &[IvGuard],
3394    length_def: &HashMap<Symbol, (Symbol, i64)>,
3395    clobbered: &std::collections::HashSet<Symbol>,
3396    facts: &mut OracleFacts,
3397) {
3398    let Expr::Identifier(arr) = collection else { return };
3399    if clobbered.contains(arr) {
3400        return;
3401    }
3402    let Some((iv, k)) = affine_of(index) else { return };
3403    let proven = guards
3404        .iter()
3405        .any(|g| g.iv == iv && !clobbered.contains(&g.iv) && guard_proves(g, *arr, k, length_def));
3406    if proven {
3407        facts.relational_inbounds.insert(index as *const Expr as usize);
3408    }
3409}
3410
3411/// Does guard `g` prove `1 <= (iv + k) <= length(arr)` for an access on
3412/// `arr` with affine offset `k`?
3413fn guard_proves(
3414    g: &IvGuard,
3415    arr: Symbol,
3416    k: i64,
3417    length_def: &HashMap<Symbol, (Symbol, i64)>,
3418) -> bool {
3419    // Lower bound: iv >= iv_lo ⇒ index = iv + k >= iv_lo + k, need >= 1.
3420    if g.iv_lo.saturating_add(k) < 1 {
3421        return false;
3422    }
3423    // Upper bound: index_max = iv_max + k, where iv_max = bound - (strict?1:0).
3424    let slack = if g.strict { 1 } else { 0 };
3425    match &g.bound {
3426        // bound == length(arr): need index_max <= bound ⇒ k - slack <= 0.
3427        IvBound::LenOf(g_arr) => *g_arr == arr && k - slack <= 0,
3428        // bound == n - headroom, length(arr) >= n + off: iv_max = n - headroom
3429        // - slack, so index_max = iv_max + k; need <= n + off
3430        // ⇒ k - slack - headroom <= off.
3431        IvBound::Var { var, headroom } => match length_def.get(&arr) {
3432            Some((n, off)) => *n == *var && k - slack - headroom <= *off,
3433            None => false,
3434        },
3435    }
3436}
3437
3438/// True if `sym` is rebound (target of `Set`/`Let`) anywhere in `body`.
3439fn var_rebound_in(sym: Symbol, body: &[Stmt]) -> bool {
3440    body.iter().any(|s| match s {
3441        Stmt::Set { target, .. } => *target == sym,
3442        Stmt::Let { var, .. } => *var == sym,
3443        Stmt::If { then_block, else_block, .. } => {
3444            var_rebound_in(sym, then_block)
3445                || matches!(else_block, Some(eb) if var_rebound_in(sym, eb))
3446        }
3447        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => var_rebound_in(sym, body),
3448        _ => false,
3449    })
3450}
3451
3452/// `value` is `iv + c` or `c + iv` with `c` a literal `>= 1`.
3453fn is_positive_increment(value: &Expr, iv: Symbol) -> bool {
3454    if let Expr::BinaryOp { op: BinaryOpKind::Add, left, right } = value {
3455        let is_iv = |e: &Expr| matches!(e, Expr::Identifier(s) if *s == iv);
3456        let amt = match (&**left, &**right) {
3457            (l, Expr::Literal(Literal::Number(c))) if is_iv(l) => Some(*c),
3458            (Expr::Literal(Literal::Number(c)), r) if is_iv(r) => Some(*c),
3459            _ => None,
3460        };
3461        return amt.is_some_and(|c| c >= 1);
3462    }
3463    false
3464}
3465
3466/// The loop's LAST top-level statement is `iv = iv + c` (c >= 1) and `iv` is
3467/// written NOWHERE else. Then every access before it sees the un-incremented
3468/// `iv`, so no per-access clobber tracking is needed and the induction is
3469/// monotone — the cleanest sound shape for region-entry hoisting.
3470fn iv_increment_is_last(iv: Symbol, body: &[Stmt]) -> bool {
3471    let Some((last, rest)) = body.split_last() else { return false };
3472    let last_is_inc = matches!(last, Stmt::Set { target, value }
3473        if *target == iv && is_positive_increment(value, iv));
3474    last_is_inc && !var_rebound_in(iv, rest)
3475}
3476
3477/// Record a single covered store/read `item (iv+k) of arr` as speculatively
3478/// in bounds and fold its offset into the per-array `(kmin, kmax)`.
3479fn consider_hoist_access(
3480    collection: &Expr,
3481    index: &Expr,
3482    iv: Symbol,
3483    state: &RichAbstractState,
3484    body: &[Stmt],
3485    facts: &mut OracleFacts,
3486    per_array: &mut HashMap<Symbol, (i64, i64)>,
3487) {
3488    let Expr::Identifier(arr) = collection else { return };
3489    // The array must be a proven collection, stable (never rebound, never
3490    // resized — a realloc would stale the pinned pointer), and the index
3491    // affine in THIS loop's induction variable.
3492    if !state.coll_vars.contains(arr) || var_rebound_in(*arr, body) || array_resized_in(*arr, body) {
3493        return;
3494    }
3495    // Speculate ONLY on a function-PARAMETER collection — its length is
3496    // fundamentally unknown, so a region-entry runtime check is the right
3497    // tool. A locally-built array has a statically determinable length and is
3498    // left to the static path (speculating there would only ever deopt).
3499    if !state.param_colls.contains(arr) {
3500        return;
3501    }
3502    let Some((aiv, k)) = affine_of(index) else { return };
3503    if aiv != iv {
3504        return;
3505    }
3506    let key = index as *const Expr as usize;
3507    if facts.relational_inbounds.contains(&key) {
3508        return; // already proven statically — no runtime guard needed
3509    }
3510    facts.speculative_inbounds.insert(key);
3511    let entry = per_array.entry(*arr).or_insert((k, k));
3512    entry.0 = entry.0.min(k);
3513    entry.1 = entry.1.max(k);
3514}
3515
3516/// Walk statements (and their expressions) collecting covered hoist accesses.
3517fn walk_hoist_accesses(
3518    stmts: &[Stmt],
3519    iv: Symbol,
3520    state: &RichAbstractState,
3521    body: &[Stmt],
3522    facts: &mut OracleFacts,
3523    per_array: &mut HashMap<Symbol, (i64, i64)>,
3524) {
3525    for s in stmts {
3526        for_each_direct_expr(s, &mut |e| {
3527            walk_hoist_in_expr(e, iv, state, body, facts, per_array)
3528        });
3529        if let Stmt::SetIndex { collection, index, .. } = s {
3530            consider_hoist_access(collection, index, iv, state, body, facts, per_array);
3531        }
3532        match s {
3533            Stmt::If { then_block, else_block, .. } => {
3534                walk_hoist_accesses(then_block, iv, state, body, facts, per_array);
3535                if let Some(eb) = else_block {
3536                    walk_hoist_accesses(eb, iv, state, body, facts, per_array);
3537                }
3538            }
3539            Stmt::While { body: b, .. } | Stmt::Repeat { body: b, .. } => {
3540                walk_hoist_accesses(b, iv, state, body, facts, per_array);
3541            }
3542            _ => {}
3543        }
3544    }
3545}
3546
3547/// Find `item (iv+k) of arr` reads anywhere in an expression tree.
3548fn walk_hoist_in_expr(
3549    e: &Expr,
3550    iv: Symbol,
3551    state: &RichAbstractState,
3552    body: &[Stmt],
3553    facts: &mut OracleFacts,
3554    per_array: &mut HashMap<Symbol, (i64, i64)>,
3555) {
3556    use crate::ast::stmt::StringPart;
3557    if let Expr::Index { collection, index } = e {
3558        consider_hoist_access(collection, index, iv, state, body, facts, per_array);
3559    }
3560    let mut go = |x: &Expr| walk_hoist_in_expr(x, iv, state, body, facts, per_array);
3561    match e {
3562        Expr::BinaryOp { left, right, .. }
3563        | Expr::Union { left, right }
3564        | Expr::Intersection { left, right }
3565        | Expr::Range { start: left, end: right } => {
3566            go(left);
3567            go(right);
3568        }
3569        Expr::Not { operand } => go(operand),
3570        Expr::Call { args, .. } => args.iter().for_each(|a| go(a)),
3571        Expr::CallExpr { callee, args } => {
3572            go(callee);
3573            args.iter().for_each(|a| go(a));
3574        }
3575        Expr::Index { collection, index } => {
3576            go(collection);
3577            go(index);
3578        }
3579        Expr::Slice { collection, start, end } => {
3580            go(collection);
3581            go(start);
3582            go(end);
3583        }
3584        Expr::Copy { expr } => go(expr),
3585        Expr::Give { value } => go(value),
3586        Expr::Length { collection } => go(collection),
3587        Expr::Contains { collection, value } => {
3588            go(collection);
3589            go(value);
3590        }
3591        Expr::List(items) | Expr::Tuple(items) => items.iter().for_each(|i| go(i)),
3592        Expr::FieldAccess { object, .. } => go(object),
3593        Expr::New { init_fields, .. } => init_fields.iter().for_each(|(_, fe)| go(fe)),
3594        Expr::NewVariant { fields, .. } => fields.iter().for_each(|(_, fe)| go(fe)),
3595        Expr::OptionSome { value } => go(value),
3596        Expr::WithCapacity { value, capacity } => {
3597            go(value);
3598            go(capacity);
3599        }
3600        Expr::InterpolatedString(parts) => {
3601            for p in parts {
3602                if let StringPart::Expr { value, .. } = p {
3603                    go(value);
3604                }
3605            }
3606        }
3607        _ => {}
3608    }
3609}
3610
3611/// SPECULATIVE region-entry hoist (V8 loop bound-check elimination): when a
3612/// loop's array length is not statically provable (e.g. a function parameter)
3613/// but the induction is monotone, the bound loop-invariant, and the array
3614/// stable, record the covered `item (iv+k) of arr` accesses as in-bounds and
3615/// emit one descriptor per array — justified at runtime by a single
3616/// region-entry check (`RegionBoundsGuard`), which the VM verifies before
3617/// entering native code (declining the region, and so the elision, on miss).
3618fn record_hoist_speculation(
3619    cond: &Expr,
3620    body: &[Stmt],
3621    state: &RichAbstractState,
3622    facts: &mut OracleFacts,
3623    loop_key: usize,
3624) {
3625    // Same body restriction as static elision: no resize, no call, nothing
3626    // unmodeled (any of which could change the array's length).
3627    if !body_is_index_proof_safe(body) {
3628        return;
3629    }
3630    let mut guards: Vec<IvGuard> = Vec::new();
3631    collect_iv_guards(cond, state, &mut guards);
3632    // One plain-variable guard `iv </<= bound`, bound loop-invariant, the
3633    // increment last (so monotone, no clobber tracking needed).
3634    let Some(g) = guards.iter().find(|g| {
3635        matches!(g.bound, IvBound::Var { headroom: 0, var } if !var_rebound_in(var, body))
3636            && iv_increment_is_last(g.iv, body)
3637    }) else {
3638        return;
3639    };
3640    let IvBound::Var { var: bound, .. } = g.bound else {
3641        return;
3642    };
3643    if bound == g.iv {
3644        return;
3645    }
3646    let mut per_array: HashMap<Symbol, (i64, i64)> = HashMap::new();
3647    walk_hoist_accesses(body, g.iv, state, body, facts, &mut per_array);
3648    if per_array.is_empty() {
3649        return;
3650    }
3651    let strict = if g.strict { 1 } else { 0 };
3652    let descs: Vec<HoistDesc> = per_array
3653        .into_iter()
3654        .filter_map(|(array, (kmin, kmax))| {
3655            Some(HoistDesc {
3656                array,
3657                bound,
3658                iv: g.iv,
3659                add_max: i32::try_from(kmax - strict).ok()?,
3660                add_min: i32::try_from(kmin).ok()?,
3661            })
3662        })
3663        .collect();
3664    if !descs.is_empty() {
3665        facts.hoist_descs.entry(loop_key).or_default().extend(descs);
3666    }
3667}
3668
3669/// The length fact a counted build loop establishes for the array it fills.
3670enum BuildLength {
3671    /// Variable bound: `length(arr) >= n + off`.
3672    Symbolic(Symbol, Symbol, i64),
3673    /// Literal bound: `length(arr) == len` exactly (the interval check then
3674    /// proves reads directly).
3675    Concrete(Symbol, i64),
3676}
3677
3678/// Recognize a counted build loop `while c </<= B: <fill arr once per
3679/// iteration>` from an EMPTY array, where `B` is a variable (→ a symbolic
3680/// length lower bound) or an integer literal (→ a concrete exact length) —
3681/// the standard allocation-size fact. Strict by construction: the body is
3682/// flat with exactly one `push to arr`, exactly one `c := c + 1`, `c`
3683/// starting at a known constant, and nothing else that resizes a collection
3684/// or branches (a conditional push would make the length too small).
3685fn infer_build_length(
3686    cond: &Expr,
3687    body: &[Stmt],
3688    entry: &RichAbstractState,
3689) -> Vec<BuildLength> {
3690    let (c_e, b_e, strict) = match cond {
3691        Expr::BinaryOp { op, left, right } => {
3692            let (l, r) = (&**left, &**right);
3693            match op {
3694                BinaryOpKind::Lt => (l, r, true),
3695                BinaryOpKind::LtEq => (l, r, false),
3696                _ => return Vec::new(),
3697            }
3698        }
3699        _ => return Vec::new(),
3700    };
3701    let Expr::Identifier(c) = c_e else { return Vec::new() };
3702    let c = *c;
3703    // The bound is a variable (symbolic) or an integer literal (concrete).
3704    let bound_var = match b_e {
3705        Expr::Identifier(n) => Some(*n),
3706        Expr::Literal(Literal::Number(_)) => None,
3707        _ => return Vec::new(),
3708    };
3709    if bound_var == Some(c) {
3710        return Vec::new();
3711    }
3712    // c starts at a known constant c0 at loop entry.
3713    let iv = entry.intervals.get_var(&c);
3714    let c0 = match (iv.lo, iv.hi) {
3715        (Bound::Finite(lo), Bound::Finite(hi)) if lo == hi => lo,
3716        _ => return Vec::new(),
3717    };
3718    // Pushes PER ARRAY: a MULTI-array build loop (graph_bfs's `Push adjStarts;
3719    // Push adjCounts; Push adj x5`) gives every array pushed EXACTLY once a
3720    // length equal to the trip count. An array pushed `K > 1` times has length
3721    // `K * n`, which `length_def`'s `(var, off)` form cannot hold, so it is
3722    // skipped (no false length fact). The body is still otherwise restricted to
3723    // pushes + the single increment — a branch or call undercounts.
3724    let mut push_counts: HashMap<Symbol, u32> = HashMap::new();
3725    let mut increments = 0;
3726    for s in body {
3727        match s {
3728            Stmt::Push { collection, .. } => {
3729                let Expr::Identifier(a) = &**collection else {
3730                    return Vec::new();
3731                };
3732                *push_counts.entry(*a).or_insert(0) += 1;
3733            }
3734            Stmt::Set { target, value } => {
3735                if *target == c {
3736                    if !is_increment_by_one(value, c) {
3737                        return Vec::new();
3738                    }
3739                    increments += 1;
3740                } else if bound_var == Some(*target) {
3741                    return Vec::new(); // the bound is mutated inside the loop
3742                }
3743            }
3744            Stmt::Let { var, .. } => {
3745                if *var == c || bound_var == Some(*var) {
3746                    return Vec::new();
3747                }
3748            }
3749            // Anything that could resize the array, branch (a conditional
3750            // push undercounts), or call out disqualifies the inference.
3751            Stmt::Show { .. }
3752            | Stmt::RuntimeAssert { .. }
3753            | Stmt::Assert { .. }
3754            | Stmt::Trust { .. } => {}
3755            _ => return Vec::new(),
3756        }
3757    }
3758    if increments != 1 {
3759        return Vec::new();
3760    }
3761    // trip count = B - c0 (for `<`) or B - c0 + 1 (for `<=`).
3762    let off = if strict { -c0 } else { -c0 + 1 };
3763    // Deterministic order (HashMap iteration is not) — sort by dense index.
3764    let mut arrays: Vec<(Symbol, u32)> = push_counts.into_iter().collect();
3765    arrays.sort_by_key(|(a, _)| a.index());
3766    let mut out = Vec::new();
3767    for (arr, count) in arrays {
3768        if count != 1 || arr == c || bound_var == Some(arr) {
3769            continue;
3770        }
3771        // The array must be empty at loop entry — a fresh allocation filled here.
3772        let entry_len = entry.intervals.get_length(&arr);
3773        if !matches!(
3774            (entry_len.lo, entry_len.hi),
3775            (Bound::Finite(0), Bound::Finite(0))
3776        ) {
3777            continue;
3778        }
3779        match (bound_var, b_e) {
3780            (Some(n), _) => out.push(BuildLength::Symbolic(arr, n, off)),
3781            (None, Expr::Literal(Literal::Number(bn))) => {
3782                out.push(BuildLength::Concrete(arr, (*bn + off).max(0)))
3783            }
3784            _ => {}
3785        }
3786    }
3787    out
3788}
3789
3790/// `value` is exactly `c + 1` or `1 + c`.
3791fn is_increment_by_one(value: &Expr, c: Symbol) -> bool {
3792    if let Expr::BinaryOp { op: BinaryOpKind::Add, left, right } = value {
3793        let is_c = |e: &Expr| matches!(e, Expr::Identifier(s) if *s == c);
3794        let is_one = |e: &Expr| matches!(e, Expr::Literal(Literal::Number(1)));
3795        return (is_c(&**left) && is_one(&**right)) || (is_one(&**left) && is_c(&**right));
3796    }
3797    false
3798}
3799
3800/// True when the loop body contains only statements the recognizer can
3801/// reason about. Resizes (`Push`/`Pop`/`Add`/`Remove`) ARE allowed — a read
3802/// on a stable array stays sound while a DIFFERENT array grows (the common
3803/// "build B from A" loop); the per-array `array_resized_in` check keeps the
3804/// resized array's own reads checked. A CALL (which could resize anything) or
3805/// an unmodeled statement still disqualifies.
3806fn body_is_index_proof_safe(body: &[Stmt]) -> bool {
3807    body.iter().all(|s| match s {
3808        Stmt::Let { .. }
3809        | Stmt::Set { .. }
3810        | Stmt::SetIndex { .. }
3811        | Stmt::Push { .. }
3812        | Stmt::Pop { .. }
3813        | Stmt::Add { .. }
3814        | Stmt::Remove { .. }
3815        | Stmt::Show { .. }
3816        | Stmt::Return { .. }
3817        | Stmt::Break
3818        | Stmt::RuntimeAssert { .. }
3819        | Stmt::Assert { .. }
3820        | Stmt::Trust { .. } => true,
3821        Stmt::If { then_block, else_block, .. } => {
3822            body_is_index_proof_safe(then_block)
3823                && match else_block {
3824                    Some(eb) => body_is_index_proof_safe(eb),
3825                    None => true,
3826                }
3827        }
3828        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => body_is_index_proof_safe(body),
3829        _ => false,
3830    })
3831}
3832
3833/// Collect every collection grown/shrunk anywhere in `body`. Reads on these
3834/// stay CHECKED: a resize can realloc the buffer, and the JIT region pins the
3835/// pointer/length once at entry.
3836/// Arrays whose length could DECREASE in the loop (so an `i <= length(arr)`
3837/// guard, checked at the top of each iteration, may no longer hold at a read).
3838/// GROW-only ops (`Push`/`Add`) leave the length monotonically non-decreasing,
3839/// so the guard keeps holding — those do NOT make an array unsafe. Only SHRINK
3840/// (`Pop`/`Remove`) and ALIASING (a second live handle that could itself be
3841/// shrunk) can drop the length below the guard.
3842fn collect_resized_arrays(body: &[Stmt], out: &mut std::collections::HashSet<Symbol>) {
3843    for s in body {
3844        match s {
3845            // Grow-only: length never decreases — the bound survives. NOT
3846            // clobbered (this is the relaxation that proves a growing FIFO's
3847            // own cursor reads, e.g. graph_bfs's queue).
3848            Stmt::Push { .. } | Stmt::Add { .. } => {}
3849            // Shrink: length can drop below the guard.
3850            Stmt::Remove { collection, .. } => {
3851                if let Expr::Identifier(c) = &**collection {
3852                    out.insert(*c);
3853                }
3854            }
3855            Stmt::Pop { collection, into } => {
3856                if let Expr::Identifier(c) = &**collection {
3857                    out.insert(*c);
3858                }
3859                if let Some(v) = into {
3860                    out.insert(*v);
3861                }
3862            }
3863            // Aliasing: `Let b be a` / `Set b to a` makes `b` a second handle
3864            // on `a`'s allocation; a later `Pop b` would shrink `a` without
3865            // naming it, so the source `a` is no longer provably grow-only.
3866            Stmt::Let { value, .. } | Stmt::Set { value, .. } => {
3867                if let Expr::Identifier(src) = &**value {
3868                    out.insert(*src);
3869                }
3870            }
3871            Stmt::If { then_block, else_block, .. } => {
3872                collect_resized_arrays(then_block, out);
3873                if let Some(eb) = else_block {
3874                    collect_resized_arrays(eb, out);
3875                }
3876            }
3877            Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
3878                collect_resized_arrays(body, out);
3879            }
3880            _ => {}
3881        }
3882    }
3883}
3884
3885/// True if `sym` is grown or shrunk (`Push`/`Pop`/`Add`/`Remove`) anywhere in
3886/// `body` — its length is not loop-stable, so its reads stay checked.
3887fn array_resized_in(sym: Symbol, body: &[Stmt]) -> bool {
3888    let hits = |c: &Expr| matches!(c, Expr::Identifier(s) if *s == sym);
3889    body.iter().any(|s| match s {
3890        Stmt::Push { collection, .. }
3891        | Stmt::Add { collection, .. }
3892        | Stmt::Remove { collection, .. } => hits(collection),
3893        Stmt::Pop { collection, into } => {
3894            hits(collection) || matches!(into, Some(v) if *v == sym)
3895        }
3896        Stmt::If { then_block, else_block, .. } => {
3897            array_resized_in(sym, then_block)
3898                || matches!(else_block, Some(eb) if array_resized_in(sym, eb))
3899        }
3900        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => array_resized_in(sym, body),
3901        _ => false,
3902    })
3903}
3904
3905/// Walk the body in execution order, recording each proven `item i of arr`
3906/// read up to the point a guard variable is clobbered. A nested loop
3907/// pre-marks every variable it clobbers (it repeats, so a later-iteration
3908/// write reaches an earlier-source read); a `Repeat`'s reads are not
3909/// recorded (its pattern rebinds the iteration variable).
3910fn record_proven_reads(
3911    stmts: &[Stmt],
3912    guards: &[IvGuard],
3913    length_def: &HashMap<Symbol, (Symbol, i64)>,
3914    clobbered: &mut std::collections::HashSet<Symbol>,
3915    facts: &mut OracleFacts,
3916) {
3917    for stmt in stmts {
3918        for_each_direct_expr(stmt, &mut |e| {
3919            record_index_reads(e, guards, length_def, clobbered, facts)
3920        });
3921        // A store `Set item E of arr to v` — prove the STORE index too (its
3922        // index is a direct expression, not an `Index` node a read walk sees).
3923        if let Stmt::SetIndex { collection, index, .. } = stmt {
3924            try_record_index(collection, index, guards, length_def, clobbered, facts);
3925        }
3926        match stmt {
3927            Stmt::If { then_block, else_block, .. } => {
3928                let mut c_then = clobbered.clone();
3929                record_proven_reads(then_block, guards, length_def, &mut c_then, facts);
3930                let mut c_else = clobbered.clone();
3931                if let Some(eb) = else_block {
3932                    record_proven_reads(eb, guards, length_def, &mut c_else, facts);
3933                }
3934                clobbered.extend(c_then);
3935                clobbered.extend(c_else);
3936            }
3937            Stmt::While { body, .. } => {
3938                let mut inner = clobbered.clone();
3939                for m in collect_mutations(body) {
3940                    inner.insert(m);
3941                }
3942                record_proven_reads(body, guards, length_def, &mut inner.clone(), facts);
3943                *clobbered = inner;
3944            }
3945            Stmt::Repeat { pattern, body, .. } => {
3946                if let Some(v) = pattern_loop_var(pattern) {
3947                    clobbered.insert(v);
3948                }
3949                for m in collect_mutations(body) {
3950                    clobbered.insert(m);
3951                }
3952            }
3953            _ => {}
3954        }
3955        // Only a rebinding of a variable invalidates subsequent proofs — an
3956        // element store (`SetIndex`) leaves the length untouched.
3957        match stmt {
3958            Stmt::Set { target, .. } => {
3959                clobbered.insert(*target);
3960            }
3961            Stmt::Let { var, .. } => {
3962                clobbered.insert(*var);
3963            }
3964            _ => {}
3965        }
3966    }
3967}
3968
3969/// Apply `f` to each expression a statement evaluates directly (not those in
3970/// nested blocks — those are walked separately by `record_proven_reads`).
3971fn for_each_direct_expr(stmt: &Stmt, f: &mut impl FnMut(&Expr)) {
3972    match stmt {
3973        Stmt::Let { value, .. } | Stmt::Set { value, .. } => f(value),
3974        Stmt::SetIndex { collection, index, value } => {
3975            f(collection);
3976            f(index);
3977            f(value);
3978        }
3979        Stmt::Show { object, .. } => f(object),
3980        // The pushed/added value is an expression that may read other arrays
3981        // (e.g. `Push item li of left to result` — the "build B from A" loop).
3982        Stmt::Push { value, .. } | Stmt::Add { value, .. } | Stmt::Remove { value, .. } => f(value),
3983        Stmt::Return { value: Some(v) } => f(v),
3984        Stmt::RuntimeAssert { condition, .. } => f(condition),
3985        Stmt::If { cond, .. } => f(cond),
3986        Stmt::While { cond, decreasing, .. } => {
3987            f(cond);
3988            if let Some(d) = decreasing {
3989                f(d);
3990            }
3991        }
3992        Stmt::Repeat { iterable, .. } => f(iterable),
3993        _ => {}
3994    }
3995}
3996
3997/// Walk `e`, recording the arena address of every `item E of arr` index
3998/// sub-expression a live guard proves in bounds (`E` affine in the guard's
3999/// induction variable, `arr` un-clobbered).
4000fn record_index_reads(
4001    e: &Expr,
4002    guards: &[IvGuard],
4003    length_def: &HashMap<Symbol, (Symbol, i64)>,
4004    clobbered: &std::collections::HashSet<Symbol>,
4005    facts: &mut OracleFacts,
4006) {
4007    use crate::ast::stmt::StringPart;
4008    if let Expr::Index { collection, index } = e {
4009        try_record_index(collection, index, guards, length_def, clobbered, facts);
4010    }
4011    match e {
4012        Expr::BinaryOp { left, right, .. }
4013        | Expr::Union { left, right }
4014        | Expr::Intersection { left, right }
4015        | Expr::Range { start: left, end: right } => {
4016            record_index_reads(left, guards, length_def, clobbered, facts);
4017            record_index_reads(right, guards, length_def, clobbered, facts);
4018        }
4019        Expr::Not { operand } => record_index_reads(operand, guards, length_def, clobbered, facts),
4020        Expr::Call { args, .. } => {
4021            for a in args {
4022                record_index_reads(a, guards, length_def, clobbered, facts);
4023            }
4024        }
4025        Expr::CallExpr { callee, args } => {
4026            record_index_reads(callee, guards, length_def, clobbered, facts);
4027            for a in args {
4028                record_index_reads(a, guards, length_def, clobbered, facts);
4029            }
4030        }
4031        Expr::Index { collection, index } => {
4032            record_index_reads(collection, guards, length_def, clobbered, facts);
4033            record_index_reads(index, guards, length_def, clobbered, facts);
4034        }
4035        Expr::Slice { collection, start, end } => {
4036            record_index_reads(collection, guards, length_def, clobbered, facts);
4037            record_index_reads(start, guards, length_def, clobbered, facts);
4038            record_index_reads(end, guards, length_def, clobbered, facts);
4039        }
4040        Expr::Copy { expr } => record_index_reads(expr, guards, length_def, clobbered, facts),
4041        Expr::Give { value } => record_index_reads(value, guards, length_def, clobbered, facts),
4042        Expr::Length { collection } => record_index_reads(collection, guards, length_def, clobbered, facts),
4043        Expr::Contains { collection, value } => {
4044            record_index_reads(collection, guards, length_def, clobbered, facts);
4045            record_index_reads(value, guards, length_def, clobbered, facts);
4046        }
4047        Expr::List(items) | Expr::Tuple(items) => {
4048            for it in items {
4049                record_index_reads(it, guards, length_def, clobbered, facts);
4050            }
4051        }
4052        Expr::FieldAccess { object, .. } => record_index_reads(object, guards, length_def, clobbered, facts),
4053        Expr::New { init_fields, .. } => {
4054            for (_, fe) in init_fields {
4055                record_index_reads(fe, guards, length_def, clobbered, facts);
4056            }
4057        }
4058        Expr::NewVariant { fields, .. } => {
4059            for (_, fe) in fields {
4060                record_index_reads(fe, guards, length_def, clobbered, facts);
4061            }
4062        }
4063        Expr::OptionSome { value } => record_index_reads(value, guards, length_def, clobbered, facts),
4064        Expr::WithCapacity { value, capacity } => {
4065            record_index_reads(value, guards, length_def, clobbered, facts);
4066            record_index_reads(capacity, guards, length_def, clobbered, facts);
4067        }
4068        Expr::InterpolatedString(parts) => {
4069            for p in parts {
4070                if let StringPart::Expr { value, .. } = p {
4071                    record_index_reads(value, guards, length_def, clobbered, facts);
4072                }
4073            }
4074        }
4075        _ => {}
4076    }
4077}
4078
4079/// EXODIA Phase 1 entry point: analyze a parsed program and return the
4080/// per-expression fact table (Main statements AND every function body,
4081/// seeded from declared parameter types).
4082/// Apply `f` to each child statement block of `s` that shares (or nests under)
4083/// its scope — used by the program-wide scans the dense-map gate needs.
4084fn each_child_block(s: &Stmt, f: &mut impl FnMut(&[Stmt])) {
4085    match s {
4086        Stmt::If { then_block, else_block, .. } => {
4087            f(then_block);
4088            if let Some(eb) = else_block {
4089                f(eb);
4090            }
4091        }
4092        Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => f(body),
4093        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => f(tasks),
4094        Stmt::Inspect { arms, .. } => {
4095            for a in arms {
4096                f(a.body);
4097            }
4098        }
4099        Stmt::FunctionDef { body, .. } => f(body),
4100        _ => {}
4101    }
4102}
4103
4104/// Symbols that are NOT program-invariant: every `Set` target, plus any symbol
4105/// bound by more than one `Let` (a shadow/rebind). A `with capacity` expression
4106/// built only from symbols OUTSIDE this set holds the same value at the map's
4107/// declaration as at every key site, so the dense array's runtime size equals
4108/// the capacity the bound proof reasons about. Conservative across scopes (a
4109/// name reused in two scopes counts as reassigned) — sound, at worst it declines
4110/// to optimize.
4111fn collect_reassigned(stmts: &[Stmt]) -> std::collections::HashSet<Symbol> {
4112    fn walk(
4113        stmts: &[Stmt],
4114        set_targets: &mut std::collections::HashSet<Symbol>,
4115        let_counts: &mut HashMap<Symbol, u32>,
4116    ) {
4117        for s in stmts {
4118            match s {
4119                Stmt::Set { target, .. } => {
4120                    set_targets.insert(*target);
4121                }
4122                Stmt::Let { var, .. } => {
4123                    *let_counts.entry(*var).or_insert(0) += 1;
4124                }
4125                _ => {}
4126            }
4127            each_child_block(s, &mut |b| walk(b, set_targets, let_counts));
4128        }
4129    }
4130    let mut set_targets = std::collections::HashSet::new();
4131    let mut let_counts: HashMap<Symbol, u32> = HashMap::new();
4132    walk(stmts, &mut set_targets, &mut let_counts);
4133    for (sym, count) in let_counts {
4134        if count > 1 {
4135            set_targets.insert(sym);
4136        }
4137    }
4138    set_targets
4139}
4140
4141/// Record, per `Map`/`HashMap` local declared `… with capacity CAP`, the capacity
4142/// as a kernel `LinearExpr` — but only when CAP is affine and every variable it
4143/// names is program-invariant (`reassigned` excludes it). A map symbol defined by
4144/// two such `Let`s has an ambiguous capacity and is poisoned (never recorded).
4145/// Recording for non-`Int` maps is harmless: their keys are not affine integers,
4146/// so `try_prove_dense_key` never proves them and the (Int-only) codegen gate
4147/// never consults them.
4148fn gather_map_caps(
4149    stmts: &[Stmt],
4150    interner: &crate::intern::Interner,
4151    reassigned: &std::collections::HashSet<Symbol>,
4152    out: &mut HashMap<Symbol, super::affine::LinExpr>,
4153    poisoned: &mut std::collections::HashSet<Symbol>,
4154) {
4155    for s in stmts {
4156        if let Stmt::Let { var, value, .. } = s {
4157            if let Expr::WithCapacity { value: inner, capacity } = value {
4158                if let Expr::New { type_name, .. } = inner {
4159                    if matches!(interner.resolve(*type_name), "Map" | "HashMap") {
4160                        if let Some(cap_lin) = super::affine::lin_of(capacity) {
4161                            let mut cap_syms = Vec::new();
4162                            affine_collect_syms(capacity, &mut cap_syms);
4163                            let invariant = cap_syms.iter().all(|s| !reassigned.contains(s));
4164                            if poisoned.contains(var) {
4165                                // already ambiguous — leave unrecorded
4166                            } else if !invariant {
4167                                out.remove(var);
4168                                poisoned.insert(*var);
4169                            } else if out.insert(*var, cap_lin).is_some() {
4170                                // second WithCapacity definition of the same map
4171                                out.remove(var);
4172                                poisoned.insert(*var);
4173                            }
4174                        }
4175                    }
4176                }
4177            }
4178        }
4179        each_child_block(s, &mut |b| gather_map_caps(b, interner, reassigned, out, poisoned));
4180    }
4181}
4182
4183/// The inclusive loop bound `B` of a counted condition `iv </<= B` whose counter
4184/// is a bare identifier — the candidate key-domain capacity for a map filled by
4185/// the loop. `None` for any other condition shape.
4186fn counted_loop_bound<'a>(cond: &'a Expr<'a>) -> Option<&'a Expr<'a>> {
4187    let Expr::BinaryOp { op, left, right } = cond else { return None };
4188    if !matches!(op, BinaryOpKind::Lt | BinaryOpKind::LtEq) {
4189        return None;
4190    }
4191    let Expr::Identifier(_) = *left else { return None };
4192    Some(*right)
4193}
4194
4195/// Collect every `new Map`/`new HashMap` local created WITHOUT an explicit
4196/// capacity (a bare `Expr::New`) — the maps whose capacity, if any, must be
4197/// INFERRED from a fill loop rather than read off the declaration.
4198fn collect_bare_new_maps(
4199    stmts: &[Stmt],
4200    interner: &crate::intern::Interner,
4201    out: &mut std::collections::HashSet<Symbol>,
4202) {
4203    for s in stmts {
4204        if let Stmt::Let { var, value, .. } = s {
4205            if let Expr::New { type_name, .. } = value {
4206                if matches!(interner.resolve(*type_name), "Map" | "HashMap") {
4207                    out.insert(*var);
4208                }
4209            }
4210        }
4211        each_child_block(s, &mut |b| collect_bare_new_maps(b, interner, out));
4212    }
4213}
4214
4215/// Record every bare-`new` map inserted into within `stmts` (a `Set item _ of m`),
4216/// restricted to the `candidates` set.
4217fn collect_loop_inserted_maps(
4218    stmts: &[Stmt],
4219    candidates: &std::collections::HashSet<Symbol>,
4220    out: &mut Vec<Symbol>,
4221) {
4222    for s in stmts {
4223        if let Stmt::SetIndex { collection: Expr::Identifier(m), .. } = s {
4224            if candidates.contains(m) {
4225                out.push(*m);
4226            }
4227        }
4228        each_child_block(s, &mut |b| collect_loop_inserted_maps(b, candidates, out));
4229    }
4230}
4231
4232/// Capacity inference for a `Map of Int to Int` created with a BARE `new Map`
4233/// (no `with capacity`) but filled inside a counted loop: the loop bound `B` of a
4234/// `While iv </<= B` whose body inserts into the map is a candidate key-domain
4235/// capacity. The dense gate still PROVES every key `<= B` before lowering, so a
4236/// loose or unrelated `B` simply fails the proof and the map stays a hash table —
4237/// recording it is never a miscompile, only an opportunity (this is what unlocks
4238/// two_sum's `seen`, whose keys `x = item i of arr` and `complement = n - x` are
4239/// element-derived, not loop counters). `B` must be affine and program-invariant.
4240/// `or_insert` so an explicit `with capacity` already in `out` always wins, and
4241/// `poisoned` (ambiguous explicit caps) are left untouched.
4242fn gather_implicit_map_caps(
4243    stmts: &[Stmt],
4244    interner: &crate::intern::Interner,
4245    reassigned: &std::collections::HashSet<Symbol>,
4246    poisoned: &std::collections::HashSet<Symbol>,
4247    out: &mut HashMap<Symbol, super::affine::LinExpr>,
4248) {
4249    let mut bare = std::collections::HashSet::new();
4250    collect_bare_new_maps(stmts, interner, &mut bare);
4251    if bare.is_empty() {
4252        return;
4253    }
4254    fn walk(
4255        stmts: &[Stmt],
4256        bare: &std::collections::HashSet<Symbol>,
4257        reassigned: &std::collections::HashSet<Symbol>,
4258        poisoned: &std::collections::HashSet<Symbol>,
4259        out: &mut HashMap<Symbol, super::affine::LinExpr>,
4260    ) {
4261        for s in stmts {
4262            if let Stmt::While { cond, body, .. } = s {
4263                if let Some(bound) = counted_loop_bound(cond) {
4264                    if let Some(b_lin) = super::affine::lin_of(bound) {
4265                        let mut syms = Vec::new();
4266                        affine_collect_syms(bound, &mut syms);
4267                        if syms.iter().all(|x| !reassigned.contains(x)) {
4268                            let mut inserted = Vec::new();
4269                            collect_loop_inserted_maps(body, bare, &mut inserted);
4270                            for m in inserted {
4271                                if !poisoned.contains(&m) && !out.contains_key(&m) {
4272                                    out.insert(m, b_lin.clone());
4273                                }
4274                            }
4275                        }
4276                    }
4277                }
4278            }
4279            each_child_block(s, &mut |b| walk(b, bare, reassigned, poisoned, out));
4280        }
4281    }
4282    walk(stmts, &bare, reassigned, poisoned, out);
4283}
4284
4285/// Render an affine capacity `LinExpr` as a Rust `i64` expression string — used to
4286/// size a dense map's direct-addressed array (`with_bounds(0, (cap) + 1)`) at its
4287/// constructor site. `None` if any coefficient or the constant is non-integer (no
4288/// clean rendering), in which case the dense gate declines and the map stays a
4289/// hash table. Terms are emitted in symbol-index order for determinism.
4290pub fn lin_to_rust(e: &super::affine::LinExpr, interner: &crate::intern::Interner) -> Option<String> {
4291    let mut coeffs: Vec<(i64, i64)> = Vec::new();
4292    for (idx, c) in e.coefficients.iter() {
4293        let ci = c.to_i64()?;
4294        if ci != 0 {
4295            coeffs.push((*idx, ci));
4296        }
4297    }
4298    coeffs.sort_by_key(|&(idx, _)| idx);
4299    let mut terms: Vec<String> = Vec::new();
4300    for (idx, ci) in coeffs {
4301        let name = interner.resolve(Symbol::from_index(idx as usize));
4302        terms.push(if ci == 1 {
4303            name.to_string()
4304        } else {
4305            format!("{} * {}", ci, name)
4306        });
4307    }
4308    let k = e.constant.to_i64()?;
4309    if k != 0 || terms.is_empty() {
4310        terms.push(k.to_string());
4311    }
4312    Some(terms.join(" + "))
4313}
4314
4315/// Tally how many `Set item _ of m to _` insert sites each map symbol has across
4316/// the program. A map with exactly one insert loop is eligible for the
4317/// full-coverage recognizer; more than one (or zero) is not.
4318fn count_insert_sites(stmts: &[Stmt], out: &mut HashMap<Symbol, u32>) {
4319    for s in stmts {
4320        if let Stmt::SetIndex { collection: Expr::Identifier(m), .. } = s {
4321            *out.entry(*m).or_insert(0) += 1;
4322        }
4323        each_child_block(s, &mut |b| count_insert_sites(b, out));
4324    }
4325}
4326
4327/// Recognize a CONTIGUOUS, UNIT-STRIDE, UNCONDITIONAL insert loop over a map in
4328/// `map_caps`: `While iv </<= UB:` whose body is EXACTLY `Set item iv of m to _`
4329/// and `Set iv to iv + 1`. Such a loop writes `m[iv]` for every integer `iv` in
4330/// `[entry(iv), B]`, so the inserted key set is that whole contiguous range —
4331/// the gap-free coverage presence elision needs. Returns `(m, iv, B)` with `B`
4332/// the inclusive upper bound (`UB - 1` for `<`, `UB` for `<=`). Deliberately
4333/// strict: a conditional insert, a non-unit stride, a `break`, or any extra
4334/// statement leaves gaps and is rejected (no elision, presence bit kept).
4335fn match_insert_loop(
4336    cond: &Expr,
4337    body: &[Stmt],
4338    map_caps: &HashMap<Symbol, super::affine::LinExpr>,
4339) -> Option<(Symbol, Symbol, super::affine::LinExpr)> {
4340    use super::affine::{konst, lin_of};
4341    // cond: `iv < UB` (B = UB - 1) or `iv <= UB` (B = UB), iv a bare identifier.
4342    let Expr::BinaryOp { op, left, right } = cond else { return None };
4343    let Expr::Identifier(iv) = left else { return None };
4344    let b_lin = match op {
4345        BinaryOpKind::Lt => lin_of(right)?.sub(&konst(1)),
4346        BinaryOpKind::LtEq => lin_of(right)?,
4347        _ => return None,
4348    };
4349    if body.len() != 2 {
4350        return None;
4351    }
4352    let mut found_map: Option<Symbol> = None;
4353    let mut found_incr = false;
4354    for s in body {
4355        match s {
4356            // The unconditional insert `Set item iv of m to _`.
4357            Stmt::SetIndex { collection: Expr::Identifier(m), index: Expr::Identifier(ix), .. }
4358                if ix == iv && map_caps.contains_key(m) =>
4359            {
4360                if found_map.is_some() {
4361                    return None;
4362                }
4363                found_map = Some(*m);
4364            }
4365            // The unit increment `Set iv to iv + 1` (either operand order).
4366            Stmt::Set { target, value: Expr::BinaryOp { op: BinaryOpKind::Add, left: l, right: r } }
4367                if target == iv =>
4368            {
4369                let iv_plus_one = matches!((l, r),
4370                    (Expr::Identifier(a), Expr::Literal(Literal::Number(1))) if a == iv)
4371                    || matches!((l, r),
4372                        (Expr::Literal(Literal::Number(1)), Expr::Identifier(a)) if a == iv);
4373                if !iv_plus_one {
4374                    return None;
4375                }
4376                found_incr = true;
4377            }
4378            _ => return None,
4379        }
4380    }
4381    match (found_map, found_incr) {
4382        (Some(m), true) => Some((m, *iv, b_lin)),
4383        _ => None,
4384    }
4385}
4386
4387/// Record, per map whose single insert loop fully covers a contiguous range, the
4388/// covered interval `[A, B]` — `A` the induction variable's constant entry value
4389/// (the most recent literal assignment in scope before the loop), `B` from the
4390/// loop guard. Only maps with EXACTLY one insert site are considered (so the one
4391/// loop accounts for every inserted key).
4392fn gather_insert_coverage(
4393    stmts: &[Stmt],
4394    map_caps: &HashMap<Symbol, super::affine::LinExpr>,
4395    insert_counts: &HashMap<Symbol, u32>,
4396    inits: &mut HashMap<Symbol, i64>,
4397    out: &mut HashMap<Symbol, (super::affine::LinExpr, super::affine::LinExpr)>,
4398) {
4399    use super::affine::konst;
4400    for s in stmts {
4401        if let Stmt::While { cond, body, .. } = s {
4402            if let Some((m, iv, b_lin)) = match_insert_loop(cond, body, map_caps) {
4403                if insert_counts.get(&m) == Some(&1) {
4404                    if let Some(&a) = inits.get(&iv) {
4405                        out.entry(m).or_insert((konst(a), b_lin));
4406                    }
4407                }
4408            }
4409        }
4410        each_child_block(s, &mut |b| {
4411            gather_insert_coverage(b, map_caps, insert_counts, &mut inits.clone(), out)
4412        });
4413        // Track constant inits in execution order so a loop sees its IV's entry
4414        // value; any non-literal (re)assignment clears the fact.
4415        match s {
4416            Stmt::Let { var, value: Expr::Literal(Literal::Number(n)), .. } => {
4417                inits.insert(*var, *n);
4418            }
4419            Stmt::Let { var, .. } => {
4420                inits.remove(var);
4421            }
4422            Stmt::Set { target, value: Expr::Literal(Literal::Number(n)) } => {
4423                inits.insert(*target, *n);
4424            }
4425            Stmt::Set { target, .. } => {
4426                inits.remove(target);
4427            }
4428            _ => {}
4429        }
4430    }
4431}
4432
4433pub fn oracle_analyze(stmts: &[Stmt]) -> OracleFacts {
4434    let mut facts = OracleFacts::default();
4435    let mut st = RichAbstractState::new();
4436    rich_walk_block(stmts, &mut st, &mut facts);
4437    // Function bodies: parameters carry their declared scalar types.
4438    for s in stmts {
4439        if let Stmt::FunctionDef { params, body, .. } = s {
4440            let mut fst = RichAbstractState::new();
4441            for (psym, ty) in params.iter() {
4442                if let crate::ast::stmt::TypeExpr::Primitive(t) = ty {
4443                    // Declared types arrive as interned names; tag the ones
4444                    // the scalar lattice models.
4445                    fst.types.insert(*psym, type_tag_for_name(*t));
4446                }
4447                // Two parameters may be handed the same allocation by a
4448                // caller — unknown provenance for the alias domain.
4449                fst.aliases.taint(*psym);
4450            }
4451            rich_walk_block(body, &mut fst, &mut facts);
4452        }
4453    }
4454    strip_concurrent_loop_snapshots(stmts, &mut facts, false);
4455    facts
4456}
4457
4458/// Map a declared primitive type NAME to a type-domain fact (Top when the
4459/// name is outside the scalar lattice). Resolved structurally — the interner
4460/// is unavailable here, so compare against the few candidate tags by trying
4461/// each known name through the type registry convention.
4462fn type_tag_for_name(_name: Symbol) -> TypeAbstraction {
4463    // Without the interner, names cannot be resolved to strings here;
4464    // declared-type seeding happens in oracle_analyze_with via the interner.
4465    TypeAbstraction::Top
4466}
4467
4468/// [`oracle_analyze`] with the interner, enabling declared-parameter type
4469/// seeds inside function bodies. Used by every NON-AOT consumer (VM bytecode,
4470/// copy-and-patch JIT, e-graph, UI) — these get NO entry-guard precondition, so
4471/// the partition's accesses stay behind a runtime `RegionBoundsGuard` rather
4472/// than being statically elided on a precondition the bytecode never enforces.
4473pub fn oracle_analyze_with(stmts: &[Stmt], interner: &crate::intern::Interner) -> OracleFacts {
4474    oracle_analyze_with_opts(stmts, interner, false)
4475}
4476
4477/// [`oracle_analyze_with`] for the `largo build` AOT codegen ONLY. Enables the
4478/// recursive-1-based-partition entry-guard precondition (`1 <= lo`,
4479/// `hi <= len`) and alias `length_def` propagation — sound here because the AOT
4480/// codegen emits the matching runtime `assert!` at the function entry. The
4481/// VM/JIT path emits no such assert, so it must use [`oracle_analyze_with`].
4482pub fn oracle_analyze_with_entry_guards(
4483    stmts: &[Stmt],
4484    interner: &crate::intern::Interner,
4485) -> OracleFacts {
4486    oracle_analyze_with_opts(stmts, interner, true)
4487}
4488
4489/// Shared body of the two public entry points. `aot_entry_guard` gates the
4490/// entry-guard-precondition facts to the AOT path that enforces them.
4491fn oracle_analyze_with_opts(
4492    stmts: &[Stmt],
4493    interner: &crate::intern::Interner,
4494    aot_entry_guard: bool,
4495) -> OracleFacts {
4496    let mut facts = OracleFacts::default();
4497    // Dense-map gate (precondition): record each `Map of Int to Int … with
4498    // capacity CAP`'s invariant affine capacity BEFORE the loop walks, so
4499    // `try_prove_dense_key` can relate a key to it while the loop guards are live.
4500    {
4501        let reassigned = collect_reassigned(stmts);
4502        let mut poisoned = std::collections::HashSet::new();
4503        gather_map_caps(stmts, interner, &reassigned, &mut facts.map_caps, &mut poisoned);
4504        // Implicit capacity: a bare `new Map` filled by a counted loop takes that
4505        // loop's bound as a candidate key-domain cap (proof-gated downstream) —
4506        // so a `Map of Int to Int` written without `with capacity` can still go
4507        // dense (two_sum's `seen`). Explicit caps above already win via `or_insert`.
4508        gather_implicit_map_caps(stmts, interner, &reassigned, &poisoned, &mut facts.map_caps);
4509        // Presence elision precondition: which dense maps have a single insert
4510        // loop that fully covers a contiguous key range.
4511        let mut insert_counts = HashMap::new();
4512        count_insert_sites(stmts, &mut insert_counts);
4513        let mut inits = HashMap::new();
4514        gather_insert_coverage(stmts, &facts.map_caps, &insert_counts, &mut inits, &mut facts.map_insert_cover);
4515    }
4516    // DECLARED primitive return types: a call to one of these functions
4517    // (native or user-defined) produces a typed value — a static fact the
4518    // kernel enforces dynamically.
4519    let mut fn_returns: HashMap<Symbol, TypeTag> = HashMap::new();
4520    for s in stmts {
4521        if let Stmt::FunctionDef { name, return_type: Some(ty), .. } = s {
4522            if let crate::ast::stmt::TypeExpr::Primitive(t)
4523            | crate::ast::stmt::TypeExpr::Named(t) = ty
4524            {
4525                let tag = match interner.resolve(*t) {
4526                    "Int" => Some(TypeTag::Int),
4527                    "Float" => Some(TypeTag::Float),
4528                    "Bool" => Some(TypeTag::Bool),
4529                    "Text" => Some(TypeTag::Text),
4530                    _ => None,
4531                };
4532                if let Some(tag) = tag {
4533                    fn_returns.insert(*name, tag);
4534                }
4535            }
4536        }
4537    }
4538    let fn_returns = std::rc::Rc::new(fn_returns);
4539    let mut st = RichAbstractState::new();
4540    st.fn_returns = fn_returns.clone();
4541    rich_walk_block(stmts, &mut st, &mut facts);
4542    for s in stmts {
4543        if let Stmt::FunctionDef { params, body, .. } = s {
4544            let mut fst = RichAbstractState::new();
4545            fst.fn_returns = fn_returns.clone();
4546            fst.aot_entry_guard = aot_entry_guard;
4547            for (psym, ty) in params.iter() {
4548                match ty {
4549                    crate::ast::stmt::TypeExpr::Primitive(t) => {
4550                        let tag = match interner.resolve(*t) {
4551                            "Int" => TypeAbstraction::Concrete(TypeTag::Int),
4552                            "Float" => TypeAbstraction::Concrete(TypeTag::Float),
4553                            "Bool" => TypeAbstraction::Concrete(TypeTag::Bool),
4554                            "Text" => TypeAbstraction::Concrete(TypeTag::Text),
4555                            _ => TypeAbstraction::Top,
4556                        };
4557                        fst.types.insert(*psym, tag);
4558                    }
4559                    // An ordered-collection parameter (`arr: Seq of Int`): a
4560                    // proven collection of unknown length — the speculative
4561                    // region-entry hoist's target.
4562                    crate::ast::stmt::TypeExpr::Generic { base, .. }
4563                        if matches!(interner.resolve(*base), "Seq" | "List" | "Array") =>
4564                    {
4565                        fst.coll_vars.insert(*psym);
4566                        fst.param_colls.insert(*psym);
4567                    }
4568                    _ => {}
4569                }
4570                // Two parameters may be handed the same allocation by a
4571                // caller — unknown provenance for the alias domain.
4572                fst.aliases.taint(*psym);
4573            }
4574            // Recursive 1-based partition precondition (quicksort/Lomuto): the
4575            // function is only ever entered with `1 <= lo` and `hi <= length(arr)`
4576            // — the contract `codegen::entry_guard` asserts at runtime for these
4577            // pure functions. Seeding those facts lets the relational BCE
4578            // discharge the partition's `item j of arr` / `item i of arr` accesses
4579            // (`1 <= i <= j < hi <= len`), a relation the interval domain alone
4580            // cannot express. Sound: every access sits past the `lo < hi` base
4581            // case, exactly where the runtime guard has already enforced these.
4582            // AOT ONLY — the VM/JIT bytecode path emits no entry guard, so it
4583            // must not see these unenforced facts (else it would drop a
4584            // `RegionBoundsGuard` it actually needs).
4585            if aot_entry_guard {
4586                if let Some(g) =
4587                    crate::codegen::entry_guard::detect_entry_guard(params, body, interner)
4588                {
4589                    fst.intervals
4590                        .set_var(g.lo, Interval { lo: Bound::Finite(1), hi: Bound::PosInf });
4591                    fst.intervals
4592                        .set_var(g.hi, Interval { lo: Bound::Finite(1), hi: Bound::PosInf });
4593                    fst.length_def.insert(g.arr, (g.hi, 0));
4594                }
4595            }
4596            rich_walk_block(body, &mut fst, &mut facts);
4597        }
4598    }
4599    strip_concurrent_loop_snapshots(stmts, &mut facts, false);
4600    facts
4601}
4602
4603/// Loops under Concurrent/Parallel blocks run interleaved: the sequential
4604/// alias walk is not a sound model of their entry states, so their
4605/// snapshots are withheld and the distinctness queries refuse.
4606fn strip_concurrent_loop_snapshots(stmts: &[Stmt], facts: &mut OracleFacts, in_concurrent: bool) {
4607    for stmt in stmts {
4608        match stmt {
4609            Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
4610                if in_concurrent {
4611                    facts.loop_aliases.remove(&(stmt as *const Stmt as usize));
4612                }
4613                strip_concurrent_loop_snapshots(body, facts, in_concurrent);
4614            }
4615            Stmt::If { then_block, else_block, .. } => {
4616                strip_concurrent_loop_snapshots(then_block, facts, in_concurrent);
4617                if let Some(eb) = else_block {
4618                    strip_concurrent_loop_snapshots(eb, facts, in_concurrent);
4619                }
4620            }
4621            Stmt::Zone { body, .. } => {
4622                strip_concurrent_loop_snapshots(body, facts, in_concurrent);
4623            }
4624            Stmt::FunctionDef { body, .. } => {
4625                strip_concurrent_loop_snapshots(body, facts, in_concurrent);
4626            }
4627            Stmt::Inspect { arms, .. } => {
4628                for arm in arms {
4629                    strip_concurrent_loop_snapshots(arm.body, facts, in_concurrent);
4630                }
4631            }
4632            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
4633                strip_concurrent_loop_snapshots(tasks, facts, true);
4634            }
4635            _ => {}
4636        }
4637    }
4638}
4639
4640pub(crate) fn rich_abstract_interp_stmts<'a>(
4641    stmts: Vec<Stmt<'a>>,
4642    _expr_arena: &'a Arena<Expr<'a>>,
4643    _stmt_arena: &'a Arena<Stmt<'a>>,
4644) -> (Vec<Stmt<'a>>, RichAbstractState) {
4645    let mut st = RichAbstractState::new();
4646    let mut facts = OracleFacts::default();
4647    rich_walk_block(&stmts, &mut st, &mut facts);
4648    (stmts, st)
4649}
4650
4651fn rich_walk_block(block: &[Stmt], st: &mut RichAbstractState, facts: &mut OracleFacts) {
4652    for stmt in block {
4653        record_stmt_exprs(stmt, st, facts);
4654        rich_walk_stmt(stmt, st, facts);
4655    }
4656}
4657
4658/// Record facts for every expression a statement holds, at the PRE-state
4659/// (the state its expressions evaluate in).
4660fn record_stmt_exprs(stmt: &Stmt, st: &RichAbstractState, facts: &mut OracleFacts) {
4661    use crate::ast::stmt::ReadSource;
4662    match stmt {
4663        Stmt::Let { value, .. } | Stmt::Set { value, .. } => record_expr(value, st, facts),
4664        Stmt::Return { value: Some(e) } => record_expr(e, st, facts),
4665        Stmt::Call { args, .. } => {
4666            for a in args {
4667                record_expr(a, st, facts);
4668            }
4669        }
4670        Stmt::If { cond, .. } | Stmt::While { cond, .. } => record_expr(cond, st, facts),
4671        Stmt::Repeat { iterable, .. } => record_expr(iterable, st, facts),
4672        Stmt::Show { object, .. } | Stmt::Give { object, .. } => record_expr(object, st, facts),
4673        Stmt::Push { value, collection }
4674        | Stmt::Add { value, collection }
4675        | Stmt::Remove { value, collection } => {
4676            record_expr(value, st, facts);
4677            record_expr(collection, st, facts);
4678        }
4679        Stmt::SetIndex { collection, index, value } => {
4680            record_expr(collection, st, facts);
4681            record_expr(index, st, facts);
4682            record_expr(value, st, facts);
4683        }
4684        Stmt::SetField { object, value, .. } => {
4685            record_expr(object, st, facts);
4686            record_expr(value, st, facts);
4687        }
4688        Stmt::Inspect { target, .. } => record_expr(target, st, facts),
4689        Stmt::RuntimeAssert { condition, .. } => record_expr(condition, st, facts),
4690        Stmt::Sleep { milliseconds } => record_expr(milliseconds, st, facts),
4691        Stmt::ReadFrom { source: ReadSource::File(p), .. } => record_expr(p, st, facts),
4692        _ => {}
4693    }
4694}
4695
4696fn rich_walk_stmt(stmt: &Stmt, st: &mut RichAbstractState, facts: &mut OracleFacts) {
4697    match stmt {
4698        Stmt::Let { var, value, .. } => rich_bind(*var, value, st),
4699        Stmt::Set { target, value } => rich_bind(*target, value, st),
4700        Stmt::Push { collection, value } | Stmt::Add { collection, value } => {
4701            rich_grow(collection, st);
4702            observe_written_elem(collection, value, st);
4703        }
4704        Stmt::Pop { collection, into } => {
4705            rich_shrink(collection, st);
4706            if let Some(v) = into {
4707                st.invalidate_var(*v);
4708                st.aliases.unlink(*v);
4709                // A popped element may be any handle ever stored in the
4710                // container — unknown provenance.
4711                st.aliases.taint(*v);
4712            }
4713        }
4714        Stmt::Remove { collection, .. } => rich_shrink(collection, st),
4715        // A store leaves the length unchanged but adds an element value: join
4716        // it into the collection's element bound (the scatter `Set item k of
4717        // arr to V` case).
4718        Stmt::SetIndex { collection, value, .. } => observe_written_elem(collection, value, st),
4719        Stmt::SetField { object, .. } => {
4720            if let Expr::Identifier(s) = *object {
4721                for a in st.aliases.may_alias(*s) {
4722                    st.shapes.insert(a, CollectionShape::Top);
4723                }
4724            }
4725        }
4726        Stmt::If { cond, then_block, else_block } => {
4727            rich_walk_if(cond, then_block, *else_block, st, facts);
4728        }
4729        Stmt::While { cond, body, .. } => {
4730            let key = stmt as *const Stmt as usize;
4731            rich_walk_loop(Some(cond), body, st, None, facts, Some(key));
4732        }
4733        Stmt::Repeat { pattern, body, .. } => {
4734            let key = stmt as *const Stmt as usize;
4735            rich_walk_loop(None, body, st, pattern_loop_var(pattern), facts, Some(key));
4736        }
4737        Stmt::Inspect { target, arms, .. } => rich_walk_inspect(target, arms, st, facts),
4738        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
4739            rich_walk_block(tasks, st, facts)
4740        }
4741        Stmt::Zone { body, .. } => {
4742            // Walk the zone body so aliasing established inside it is visible
4743            // and loops within it get borrow-hoist snapshots — but suppress
4744            // per-expression fact recording: the EXODIA region/JIT compiler
4745            // consumes those and assumes none exist inside zones. State
4746            // updates and loop_aliases snapshots stay live; only the
4747            // exprs/lengths/collections tables are held back.
4748            let prev = facts.suppress_exprs;
4749            facts.suppress_exprs = true;
4750            rich_walk_block(body, st, facts);
4751            facts.suppress_exprs = prev;
4752        }
4753        Stmt::Call { args, .. } => {
4754            // A callee may resize any collection it is handed: forget their sizes.
4755            for arg in args {
4756                if let Expr::Identifier(s) = *arg {
4757                    for a in st.aliases.may_alias(*s) {
4758                        st.shapes.insert(a, CollectionShape::Top);
4759                        st.intervals.set_length(a, Interval::non_negative());
4760                    }
4761                }
4762            }
4763        }
4764        Stmt::Give { object, .. } => {
4765            if let Expr::Identifier(s) = *object {
4766                st.invalidate_var(*s);
4767                st.aliases.unlink(*s);
4768            }
4769        }
4770        Stmt::ReadFrom { var, .. } => {
4771            st.invalidate_var(*var);
4772            st.aliases.unlink(*var);
4773        }
4774        _ => {}
4775    }
4776}
4777
4778/// `Let v be value.` / `Set v to value.` — rebinds `v`, so its old aliasing is
4779/// severed and all five facts are recomputed from `value`.
4780/// Record an element written into a collection (`Push`/`Add`/`SetIndex`):
4781/// join the value's interval into the element bound of every handle that
4782/// aliases the collection (a write through one alias is visible through all).
4783/// The SYMBOLIC upper bound of `value` as a linear expression over program
4784/// variables, or `None` if no useful one is known. This is the variable-divisor
4785/// sibling of `eval_expr`'s concrete interval upper: it exists precisely for the
4786/// bounds a concrete `Interval` cannot hold. A `(...) % n` with a VARIABLE `n`
4787/// is `<= n - 1` (the truncated remainder satisfies this whenever `n >= 1`,
4788/// which the `while i < n` fill loop guarantees and an empty array makes
4789/// vacuous); a scalar carries its `scalar_upper`; an element read carries its
4790/// array's `elem_upper`; a numeric literal is its own bound.
4791fn value_upper(value: &Expr, st: &RichAbstractState) -> Option<super::affine::LinExpr> {
4792    use super::affine::{konst, var};
4793    match value {
4794        Expr::Literal(Literal::Number(k)) => Some(konst(*k)),
4795        Expr::Identifier(s) => st.scalar_upper.get(s).cloned(),
4796        Expr::Index { collection, .. } => match collection {
4797            Expr::Identifier(arr) => st.elem_upper.get(arr).cloned(),
4798            _ => None,
4799        },
4800        Expr::BinaryOp { op: BinaryOpKind::Modulo, right, .. } => match right {
4801            Expr::Identifier(n) => Some(var(*n).sub(&konst(1))),
4802            _ => None,
4803        },
4804        _ => None,
4805    }
4806}
4807
4808/// The symbolic LOWER bound of `value`, the mirror of [`value_upper`]. An
4809/// element read carries its array's proven concrete element interval lower (the
4810/// `% n`-filled array's `0`); a scalar carries its `scalar_lower`; a literal is
4811/// its own bound. Captured here because a loop-local read variable's raw
4812/// interval is flooded to ⊤ by its undefined entry value, losing this lower.
4813fn value_lower(value: &Expr, st: &RichAbstractState) -> Option<super::affine::LinExpr> {
4814    use super::affine::konst;
4815    match value {
4816        Expr::Literal(Literal::Number(k)) => Some(konst(*k)),
4817        Expr::Identifier(s) => st.scalar_lower.get(s).cloned(),
4818        Expr::Index { collection, .. } => match collection {
4819            Expr::Identifier(arr) => match st.intervals.get_elem(arr).lo {
4820                Bound::Finite(lo) => Some(konst(lo)),
4821                _ => None,
4822            },
4823            _ => None,
4824        },
4825        _ => None,
4826    }
4827}
4828
4829/// Merge two symbolic element-upper bounds under the "EVERY element satisfies
4830/// it" reading: the result must dominate BOTH. Equal bounds (the uniform fill —
4831/// every store is the same `% n`) are kept; otherwise the one provably the
4832/// larger in the current interval state wins (so a constant zero-init folds into
4833/// the symbolic `n - 1`), and the bound drops to ⊤ when the two are unordered.
4834fn join_sym_upper(
4835    a: &super::affine::LinExpr,
4836    b: &super::affine::LinExpr,
4837    st: &RichAbstractState,
4838) -> Option<super::affine::LinExpr> {
4839    if a == b {
4840        return Some(a.clone());
4841    }
4842    if lin_ge_zero(&b.sub(a), st) {
4843        return Some(b.clone()); // b - a >= 0 ⟹ b is the weaker (larger) upper
4844    }
4845    if lin_ge_zero(&a.sub(b), st) {
4846        return Some(a.clone());
4847    }
4848    // LENIENT: a non-positive constant element (the zero-init of a build-then-
4849    // scatter array) joins into a `% m` element upper `m - 1`. Sound under
4850    // `m >= 1`: then `c <= 0 <= m - 1`, so every element satisfies `m - 1`
4851    // (vacuous when the array is empty). The `m >= 1` precondition is discharged
4852    // by a nonemptiness-guarded `assert!(m > 0)` at any elision that consumes the
4853    // bound (see `try_prove_affine` / `positivity_guards`). Restricted to the
4854    // `m - 1` shape so the divisor to guard is unambiguous.
4855    if is_nonpos_const(a) && mod_upper_divisor(b).is_some() {
4856        return Some(b.clone());
4857    }
4858    if is_nonpos_const(b) && mod_upper_divisor(a).is_some() {
4859        return Some(a.clone());
4860    }
4861    None
4862}
4863
4864/// `Some(m)` when `e` is the variable-divisor modulo upper `m - 1` (a single
4865/// variable with coefficient `+1` and constant `-1`) — the bound `value_upper`
4866/// emits for `(...) % m`. Its `m >= 1` precondition is what the positivity guard
4867/// discharges.
4868fn mod_upper_divisor(e: &super::affine::LinExpr) -> Option<Symbol> {
4869    if e.coefficients.len() != 1 || e.constant.to_i64() != Some(-1) {
4870        return None;
4871    }
4872    let (&idx, coeff) = e.coefficients.iter().next()?;
4873    (coeff.to_i64() == Some(1)).then(|| Symbol::from_index(idx as usize))
4874}
4875
4876/// Is `e` a constant `<= 0` (no variables, non-positive)?
4877fn is_nonpos_const(e: &super::affine::LinExpr) -> bool {
4878    e.coefficients.is_empty() && !e.constant.is_positive()
4879}
4880
4881/// Is the linear expression `e` provably `>= 0` under the current per-variable
4882/// intervals? Evaluates `const + Σ coeff·interval(var)` and tests the lower
4883/// bound is a finite non-negative. A non-integer rational or a variable whose
4884/// interval is unbounded below (for a positive coefficient) fails closed.
4885fn lin_ge_zero(e: &super::affine::LinExpr, st: &RichAbstractState) -> bool {
4886    let Some(k) = e.constant.to_i64() else { return false };
4887    let mut acc = Interval::exact(k);
4888    for (idx, coeff) in &e.coefficients {
4889        let Some(c) = coeff.to_i64() else { return false };
4890        let sym = Symbol::from_index(*idx as usize);
4891        acc = acc.add(&Interval::exact(c).mul(&st.intervals.get_var(&sym)));
4892    }
4893    matches!(acc.lo, Bound::Finite(v) if v >= 0)
4894}
4895
4896fn observe_written_elem(collection: &Expr, value: &Expr, st: &mut RichAbstractState) {
4897    if let Expr::Identifier(sym) = collection {
4898        let v = eval_expr(value, &st.intervals);
4899        // The symbolic element upper of the written value, computed BEFORE the
4900        // mutations below (it reads other variables' bounds, never `sym`'s).
4901        let sym_up = value_upper(value, st);
4902        // The scalar TYPE of the written value, likewise BEFORE the mutations
4903        // (it reads `types`/`elem_type`, never the array's own future state).
4904        let sym_ty = eval_type(value, &st.types, &st.fn_returns, &st.elem_type);
4905        for a in st.aliases.may_alias(*sym) {
4906            // Freshness BEFORE the concrete update: a fresh array (⊥ element
4907            // interval) is SEEDED by the first write; a non-fresh one JOINS.
4908            let was_fresh = st.intervals.get_elem(&a).is_bottom();
4909            st.intervals.observe_elem(a, v.clone());
4910            // Element TYPE: every element must satisfy the join over all writes.
4911            // A fresh array is seeded; a non-fresh one joins. An unknown write
4912            // type (`Top`) forces the element type to ⊤, exactly as the symbolic
4913            // upper does — once an element's type is unknown, all reads are ⊤.
4914            if was_fresh {
4915                st.elem_type.insert(a, sym_ty.clone());
4916            } else {
4917                let joined = match st.elem_type.get(&a) {
4918                    Some(existing) => existing.join(&sym_ty),
4919                    None => TypeAbstraction::Top,
4920                };
4921                st.elem_type.insert(a, joined);
4922            }
4923            match &sym_up {
4924                // An unbounded written value forces the array's element upper to
4925                // ⊤ — every element must satisfy the bound, and this one doesn't.
4926                None => {
4927                    st.elem_upper.remove(&a);
4928                }
4929                Some(nb) => {
4930                    if was_fresh {
4931                        st.elem_upper.insert(a, nb.clone());
4932                    } else if let Some(existing) = st.elem_upper.get(&a).cloned() {
4933                        match join_sym_upper(&existing, nb, st) {
4934                            Some(m) => {
4935                                st.elem_upper.insert(a, m);
4936                            }
4937                            None => {
4938                                st.elem_upper.remove(&a);
4939                            }
4940                        }
4941                    }
4942                    // existing absent + non-fresh = ⊤ (a prior element was
4943                    // unbounded); a bounded write cannot recover it.
4944                }
4945            }
4946        }
4947    }
4948}
4949
4950fn rich_bind(var: Symbol, value: &Expr, st: &mut RichAbstractState) {
4951    st.aliases.unlink(var);
4952    // Writing `var` invalidates any length fact it participates in: the array
4953    // itself is rebound, or a size variable `n` in `length(arr) >= n + off`
4954    // is reassigned (so the old relation no longer holds).
4955    st.length_def.remove(&var);
4956    st.length_def.retain(|_, (n, _)| *n != var);
4957    // Likewise any affine scalar definition that names `var` (as the defined
4958    // variable or anywhere in another's right-hand side) is now stale.
4959    st.scalar_def.remove(&var);
4960    let vi = var.index() as i64;
4961    st.scalar_def.retain(|_, e| !e.coefficients.contains_key(&vi));
4962    // Symbolic bounds that name `var` (its own scalar upper, the element upper
4963    // of a rebound collection, or any bound whose RHS mentions `var`) are now
4964    // stale — drop them exactly like the affine scalar definitions above.
4965    st.scalar_upper.remove(&var);
4966    st.scalar_upper.retain(|_, e| !e.coefficients.contains_key(&vi));
4967    st.elem_upper.remove(&var);
4968    st.elem_upper.retain(|_, e| !e.coefficients.contains_key(&vi));
4969    // The element TYPE of a rebound collection is stale; its values can't name
4970    // another variable, so only its own entry needs dropping (re-seeded below
4971    // for a fresh/aliased collection by the element-write observers).
4972    st.elem_type.remove(&var);
4973    st.scalar_lower.remove(&var);
4974    st.scalar_lower.retain(|_, e| !e.coefficients.contains_key(&vi));
4975    // Rebinding resets `var`'s extern-length status; the producer arms below
4976    // re-establish it for aliases/copies/slices of an extern-length array.
4977    st.param_colls.remove(&var);
4978
4979    let iv = eval_expr(value, &st.intervals);
4980    st.intervals.set_var(var, iv);
4981    let value_ty = eval_type(value, &st.types, &st.fn_returns, &st.elem_type);
4982    st.types.insert(var, value_ty);
4983    st.nullability.insert(var, nullability_of_expr(value, st));
4984    // Rebinding discards the old binding's element bound; the producer arms
4985    // below re-establish it for fresh/aliased collections.
4986    st.intervals.clear_elem(&var);
4987    // Record an affine scalar definition `var = E` (E linear over ≥1 variable)
4988    // for the LIA bounds prover — a constant value is already in the interval
4989    // domain, so only variable-bearing forms are worth an equality fact. NEVER
4990    // record a SELF-REFERENTIAL update (`Set n to n + 10`): as an equality
4991    // `n = n + 10` it is the contradiction `0 = 10`, which would make the LIA
4992    // system inconsistent and prove every bound vacuously (unsound).
4993    if let Some(e) = super::affine::lin_of(value) {
4994        if !e.coefficients.is_empty() && !e.coefficients.contains_key(&(var.index() as i64)) {
4995            st.scalar_def.insert(var, e);
4996        }
4997    }
4998    // Symbolic UPPER bound for the new binding — `Let u be item _ of adj` carries
4999    // `adj`'s element upper, `Let neighbor be (...) % n` carries `n - 1`. A
5000    // self-referential bound (`Set x to x % n`) would be unsound to keep, so a
5001    // result naming `var` is dropped.
5002    if let Some(u) = value_upper(value, st) {
5003        if !u.coefficients.contains_key(&(var.index() as i64)) {
5004            st.scalar_upper.insert(var, u);
5005        }
5006    }
5007    if let Some(l) = value_lower(value, st) {
5008        if !l.coefficients.contains_key(&(var.index() as i64)) {
5009            st.scalar_lower.insert(var, l);
5010        }
5011    }
5012    // A3 LENGTH BINDING: `Let n be length of A` ⟹ `length(A) = n`, recorded as
5013    // the symbolic lower bound `length(A) >= n + 0` — the symmetric fact to a
5014    // build loop's `length_def`. Lets the bounds prover relate an index bounded
5015    // by `n` back to `A`'s length (string_search: `text[i+j]` with
5016    // `i+j <= textLen = length(text)`). Invalidated when `A` is resized/rebound
5017    // or `n` is reassigned, exactly like the build-loop fact. Holds for a String
5018    // too — its byte length is what the `as_bytes()[..]` access checks.
5019    if let Expr::Length { collection } = value {
5020        if let Expr::Identifier(a) = &**collection {
5021            if *a != var {
5022                st.length_def.insert(*a, (var, 0));
5023            }
5024        }
5025    }
5026
5027    match value {
5028        Expr::New { .. } => {
5029            st.shapes.insert(var, CollectionShape::Empty);
5030            st.intervals.set_length(var, Interval::exact(0));
5031            st.coll_vars.insert(var);
5032            // A fresh empty collection has NO elements: element bound `⊥`, so
5033            // the first `Push` seeds an exact bound and a 0-trip build loop's
5034            // exit join keeps the filled branch's bound.
5035            st.intervals.observe_elem(var, Interval::bottom());
5036        }
5037        Expr::List(items) | Expr::Tuple(items) => {
5038            let n = items.len() as u64;
5039            st.shapes.insert(var, CollectionShape::from_bounds(n, Some(n)));
5040            st.intervals.set_length(var, Interval::exact(items.len() as i64));
5041            st.coll_vars.insert(var);
5042            // Element bound = join of the literal items (`⊥` for an empty list).
5043            let mut el = Interval::bottom();
5044            for it in items.iter() {
5045                el = el.join(&eval_expr(it, &st.intervals));
5046            }
5047            st.intervals.observe_elem(var, el);
5048            // Element TYPE = join of the item types; an empty list stays fresh
5049            // (absent), so its later writes seed it exactly.
5050            let mut item_iter = items.iter();
5051            if let Some(first) = item_iter.next() {
5052                let mut ty = eval_type(first, &st.types, &st.fn_returns, &st.elem_type);
5053                for it in item_iter {
5054                    ty = ty.join(&eval_type(it, &st.types, &st.fn_returns, &st.elem_type));
5055                }
5056                st.elem_type.insert(var, ty);
5057            }
5058        }
5059        Expr::Identifier(src) => {
5060            // `v` and `src` are now two names for one allocation.
5061            st.aliases.link(var, *src);
5062            let shape = st.shapes.get(src).cloned().unwrap_or(CollectionShape::Top);
5063            st.shapes.insert(var, shape);
5064            let len = st.intervals.get_length(src);
5065            st.intervals.set_length(var, len);
5066            // The alias shares the element bound of its source.
5067            if st.intervals.elem.contains_key(src) {
5068                let e = st.intervals.get_elem(src);
5069                st.intervals.observe_elem(var, e);
5070            }
5071            // …and the source's element TYPE (one allocation, same elements).
5072            if let Some(t) = st.elem_type.get(src).cloned() {
5073                st.elem_type.insert(var, t);
5074            }
5075            if st.coll_vars.contains(src) {
5076                st.coll_vars.insert(var);
5077                // An alias of an extern-length array is itself extern-length.
5078                if st.param_colls.contains(src) {
5079                    st.param_colls.insert(var);
5080                }
5081            } else {
5082                st.coll_vars.remove(&var);
5083            }
5084            // An alias shares its source's symbolic length lower bound (one
5085            // allocation): `length(src) >= n` ⟹ `length(var) >= n`. The quicksort
5086            // partition reads `result` after `Let mutable result be arr`, so the
5087            // entry-guard fact on `arr` must flow to `result`. A later resize of
5088            // either name drops it (`rich_grow`/`rich_shrink` walk `may_alias`).
5089            // AOT-only: this rides on the entry-guard precondition, which only the
5090            // AOT codegen enforces — gated so the VM/JIT oracle is unchanged.
5091            if st.aot_entry_guard {
5092                if let Some(&(n, off)) = st.length_def.get(src) {
5093                    if n != var {
5094                        st.length_def.insert(var, (n, off));
5095                    }
5096                }
5097            }
5098        }
5099        Expr::Copy { expr } | Expr::WithCapacity { value: expr, .. } => {
5100            // A fresh unaliased value of the OPERAND's kind.
5101            st.shapes.insert(var, CollectionShape::Top);
5102            let from_coll = matches!(
5103                expr,
5104                Expr::Identifier(s) if st.coll_vars.contains(s)
5105            ) || matches!(expr, Expr::New { .. } | Expr::List(_) | Expr::Slice { .. });
5106            if from_coll {
5107                st.coll_vars.insert(var);
5108            } else {
5109                st.coll_vars.remove(&var);
5110            }
5111        }
5112        Expr::Slice { .. } => {
5113            // A slice that evaluates is always a fresh list.
5114            st.shapes.insert(var, CollectionShape::Top);
5115            st.coll_vars.insert(var);
5116        }
5117        _ => {
5118            // Unmodeled producer — kind unknown.
5119            st.shapes.insert(var, CollectionShape::Top);
5120            st.coll_vars.remove(&var);
5121            // Provenance: scalar and definitely-fresh producers stay
5122            // alias-tracked; anything that can RETURN AN EXISTING HANDLE —
5123            // calls, container extraction (Index/FieldAccess), opaque
5124            // escapes — taints the binding (it may alias anything).
5125            match value {
5126                Expr::Literal(_)
5127                | Expr::BinaryOp { .. }
5128                | Expr::Not { .. }
5129                | Expr::Length { .. }
5130                | Expr::Contains { .. }
5131                | Expr::Range { .. }
5132                | Expr::Union { .. }
5133                | Expr::Intersection { .. }
5134                | Expr::InterpolatedString(_)
5135                | Expr::OptionNone => {}
5136                _ => st.aliases.taint(var),
5137            }
5138        }
5139    }
5140}
5141
5142/// `Push`/`Add` — one element added to a shared allocation: grow the shape and
5143/// length of the collection and every variable that aliases it.
5144fn rich_grow(collection: &Expr, st: &mut RichAbstractState) {
5145    if let Expr::Identifier(sym) = collection {
5146        let new_shape = st.shapes.get(sym).cloned().unwrap_or(CollectionShape::Top).pushed();
5147        let new_len = st.intervals.get_length(sym).add(&Interval::exact(1));
5148        for a in st.aliases.may_alias(*sym) {
5149            st.shapes.insert(a, new_shape.clone());
5150            st.intervals.set_length(a, new_len.clone());
5151            // A resize moves length off its build-time relation — drop it
5152            // (conservative; a grow keeps `>=` valid but we re-derive cleanly).
5153            st.length_def.remove(&a);
5154        }
5155    }
5156}
5157
5158/// `Pop`/`Remove` — one element removed from a shared allocation.
5159fn rich_shrink(collection: &Expr, st: &mut RichAbstractState) {
5160    if let Expr::Identifier(sym) = collection {
5161        let new_shape = st.shapes.get(sym).cloned().unwrap_or(CollectionShape::Top).popped();
5162        let new_len = st.intervals.get_length(sym).sub(&Interval::exact(1));
5163        for a in st.aliases.may_alias(*sym) {
5164            st.shapes.insert(a, new_shape.clone());
5165            st.intervals.set_length(a, new_len.clone());
5166            // A shrink can break the `length(arr) >= n` lower bound — drop it.
5167            st.length_def.remove(&a);
5168        }
5169    }
5170}
5171
5172fn nullability_of_expr(expr: &Expr, st: &RichAbstractState) -> Nullability {
5173    match expr {
5174        Expr::Literal(lit) => Nullability::for_literal(lit),
5175        Expr::Identifier(s) => st.nullability.get(s).cloned().unwrap_or(Nullability::Maybe),
5176        Expr::New { .. }
5177        | Expr::NewVariant { .. }
5178        | Expr::List(_)
5179        | Expr::Tuple(_)
5180        | Expr::Range { .. }
5181        | Expr::BinaryOp { .. }
5182        | Expr::Not { .. }
5183        | Expr::Length { .. }
5184        | Expr::Contains { .. } => Nullability::Definite,
5185        // Index/Call/FieldAccess may yield `nothing`: stay conservative.
5186        _ => Nullability::Maybe,
5187    }
5188}
5189
5190fn pattern_loop_var(p: &Pattern) -> Option<Symbol> {
5191    match p {
5192        Pattern::Identifier(s) => Some(*s),
5193        _ => None,
5194    }
5195}
5196
5197fn rich_walk_if(
5198    cond: &Expr,
5199    then_block: &[Stmt],
5200    else_block: Option<&[Stmt]>,
5201    st: &mut RichAbstractState,
5202    facts: &mut OracleFacts,
5203) {
5204    match eval_condition(cond, &st.intervals) {
5205        Some(true) => {
5206            narrow_state(cond, &mut st.intervals);
5207            rich_walk_block(then_block, st, facts);
5208        }
5209        Some(false) => {
5210            if let Some(eb) = else_block {
5211                narrow_state_negated(cond, &mut st.intervals);
5212                rich_walk_block(eb, st, facts);
5213            }
5214        }
5215        None => {
5216            let mut then_st = st.clone();
5217            narrow_state(cond, &mut then_st.intervals);
5218            rich_walk_block(then_block, &mut then_st, facts);
5219
5220            let mut else_st = st.clone();
5221            narrow_state_negated(cond, &mut else_st.intervals);
5222            if let Some(eb) = else_block {
5223                rich_walk_block(eb, &mut else_st, facts);
5224            }
5225
5226            *st = rich_join(&then_st, &else_st);
5227        }
5228    }
5229}
5230
5231/// Refine a loop-INVARIANT bound variable's interval lower from the guard the
5232/// body entry implies: `i < n` with `i` at its pre-loop value `i0` proves
5233/// `n > i0` (`n >= i0 + 1`); `i <= n` proves `n >= i0`. Sound because `n` is not
5234/// mutated, so the relation established at first entry holds for the whole body.
5235/// Descends through `and` and accepts the flipped `n > i` / `n >= i` forms.
5236fn refine_invariant_bound_from_guard(
5237    cond: &Expr,
5238    pre: &RichAbstractState,
5239    mutated: &[Symbol],
5240    out: &mut AbstractState,
5241) {
5242    if let Expr::BinaryOp { op: BinaryOpKind::And, left, right } = cond {
5243        refine_invariant_bound_from_guard(left, pre, mutated, out);
5244        refine_invariant_bound_from_guard(right, pre, mutated, out);
5245        return;
5246    }
5247    let Expr::BinaryOp { op, left, right } = cond else { return };
5248    let (iv_e, bnd_e, strict) = match op {
5249        BinaryOpKind::Lt => (left, right, true),
5250        BinaryOpKind::LtEq => (left, right, false),
5251        BinaryOpKind::Gt => (right, left, true),
5252        BinaryOpKind::GtEq => (right, left, false),
5253        _ => return,
5254    };
5255    let (Expr::Identifier(iv), Expr::Identifier(bnd)) = (iv_e, bnd_e) else { return };
5256    if mutated.contains(bnd) {
5257        return; // the bound must be loop-invariant for the relation to persist
5258    }
5259    let Bound::Finite(i0) = pre.intervals.get_var(iv).lo else { return };
5260    let Some(new_lo) = (if strict { i0.checked_add(1) } else { Some(i0) }) else { return };
5261    let cur = out.get_var(bnd);
5262    out.set_var(
5263        *bnd,
5264        Interval { lo: Bound::max_bound(&cur.lo, &Bound::Finite(new_lo)), hi: cur.hi },
5265    );
5266}
5267
5268/// Widening-to-fixpoint loop analysis (EXODIA 1.1): iterate the body
5269/// transfer, widening the mutated variables' facts between passes through
5270/// the threshold ladder; on convergence the recorded body facts hold at the
5271/// loop invariant. A non-converging loop (cap hit) falls back to the sound
5272/// invalidate-everything treatment for whatever is still unstable.
5273fn rich_walk_loop(
5274    cond: Option<&Expr>,
5275    body: &[Stmt],
5276    st: &mut RichAbstractState,
5277    loop_var: Option<Symbol>,
5278    facts: &mut OracleFacts,
5279    loop_key: Option<usize>,
5280) {
5281    let mut mutated = collect_mutations(body);
5282    if let Some(lv) = loop_var {
5283        if !mutated.contains(&lv) {
5284            mutated.push(lv);
5285        }
5286    }
5287    // Repeat loop variables hold unknown element values inside the body —
5288    // and the element handle could be ANY handle stored in the iterable.
5289    let mut inside = st.clone();
5290    if let Some(lv) = loop_var {
5291        inside.invalidate_var(lv);
5292        inside.aliases.unlink(lv);
5293        inside.aliases.taint(lv);
5294    }
5295    if let Some(c) = cond {
5296        narrow_state(c, &mut inside.intervals);
5297        // The loop body implies its guard at first entry: `i < n` with `i` at
5298        // its pre-loop value `i0` proves `n > i0`, i.e. `n >= i0 + 1`. Since `n`
5299        // is loop-invariant (not mutated), this lower holds throughout the body
5300        // — it is what lets a `% n` element bound `n - 1` dominate a constant
5301        // zero-init in the symbolic element join (graph_bfs's adjacency array).
5302        refine_invariant_bound_from_guard(c, st, &mutated, &mut inside.intervals);
5303    }
5304
5305    // Fixpoint over the mutated set: facts recorded into a SINK during the
5306    // ascent (intermediate states are not loop invariants).
5307    let mut sink = OracleFacts::default();
5308    let mut converged = false;
5309    for pass in 0..12 {
5310        let mut next = inside.clone();
5311        rich_walk_block(body, &mut next, &mut sink);
5312        if let Some(c) = cond {
5313            narrow_state(c, &mut next.intervals);
5314        }
5315        // Widen the mutated variables; everything else keeps the entry fact
5316        // (sound: un-mutated vars are loop-invariant).
5317        let mut stable = true;
5318        for &m in &mutated {
5319            // Captured BEFORE the concrete element merge below overwrites the
5320            // ⊥-ness: a fresh entry seeds the symbolic element upper from `next`.
5321            let inside_fresh_elem = inside.intervals.get_elem(&m).is_bottom();
5322            let cur_iv = inside.intervals.get_var(&m);
5323            let nxt_iv = next.intervals.get_var(&m);
5324            let wid = cur_iv.widen(&nxt_iv);
5325            if !wid.leq(&cur_iv) || !cur_iv.leq(&wid) {
5326                stable = false;
5327            }
5328            inside.intervals.set_var(m, wid);
5329            let cur_len = inside.intervals.get_length(&m);
5330            let nxt_len = next.intervals.get_length(&m);
5331            let wid_len = cur_len.widen(&nxt_len);
5332            inside.intervals.set_length(m, wid_len);
5333
5334            // Element bound: DELAYED widening. Join for the first few passes so
5335            // a bound fed by an external converging value settles EXACTLY
5336            // (`(seed/65536) % 1000 → [0,999]`, which a threshold widen would
5337            // round up to 1000 and lose by one), then widen so a SELF-feeding
5338            // accumulator (`counts[v] += 1`, growing by 1 each pass) escapes to
5339            // ±∞ and converges instead of pinning the loop to the iteration cap
5340            // (which forces the blunt invalidate-everything fallback, losing the
5341            // length fact). A wider element bound is always sound.
5342            if inside.intervals.elem.contains_key(&m) || next.intervals.elem.contains_key(&m) {
5343                let cur_el = inside.intervals.get_elem(&m);
5344                let nxt_el = next.intervals.get_elem(&m);
5345                let merged = if pass < 3 {
5346                    cur_el.join(&nxt_el)
5347                } else {
5348                    cur_el.widen(&nxt_el)
5349                };
5350                if !(merged.leq(&cur_el) && cur_el.leq(&merged)) {
5351                    stable = false;
5352                }
5353                inside.intervals.elem.insert(m, merged);
5354            }
5355
5356            // Symbolic element upper — the variable-divisor sibling of the
5357            // concrete elem above. Seeded from `next` on a fresh entry, then an
5358            // equality/domination merge (a symbolic LinExpr is its own fixed
5359            // point, so no widening); drops to ⊤ on disagreement.
5360            let merged_sym = if inside_fresh_elem {
5361                next.elem_upper.get(&m).cloned()
5362            } else {
5363                match (inside.elem_upper.get(&m).cloned(), next.elem_upper.get(&m).cloned()) {
5364                    (Some(e), Some(n)) => join_sym_upper(&e, &n, &next),
5365                    _ => None,
5366                }
5367            };
5368            if inside.elem_upper.get(&m) != merged_sym.as_ref() {
5369                stable = false;
5370            }
5371            match merged_sym {
5372                Some(l) => {
5373                    inside.elem_upper.insert(m, l);
5374                }
5375                None => {
5376                    inside.elem_upper.remove(&m);
5377                }
5378            }
5379            // Element TYPE — the type sibling of the concrete elem above. Seeded
5380            // from `next` on a fresh entry, then JOINED across the back-edge. The
5381            // type lattice is finite-height (`widen == join`, capped union), so a
5382            // plain join converges; a divergent type lands at ⊤ (sound: every
5383            // element must satisfy the join, so a wider type only forfeits the
5384            // elision). Stability participates in the fixpoint check.
5385            let merged_et = if inside_fresh_elem {
5386                next.elem_type.get(&m).cloned()
5387            } else {
5388                match (inside.elem_type.get(&m).cloned(), next.elem_type.get(&m).cloned()) {
5389                    (Some(e), Some(n)) => Some(e.join(&n)),
5390                    _ => None,
5391                }
5392            };
5393            if inside.elem_type.get(&m) != merged_et.as_ref() {
5394                stable = false;
5395            }
5396            match merged_et {
5397                Some(t) => {
5398                    inside.elem_type.insert(m, t);
5399                }
5400                None => {
5401                    inside.elem_type.remove(&m);
5402                }
5403            }
5404            // A mutated SCALAR's `scalar_upper` (the element/modulo bound on `u =
5405            // item _ of adj`, re-established by its in-body `Let` every pass) IS
5406            // a loop invariant when stable: build it up from the first defining
5407            // pass, keep it while it holds, drop it the moment it diverges or
5408            // vanishes (so the fixpoint converges instead of oscillating).
5409            let merged_su = match (
5410                inside.scalar_upper.get(&m).cloned(),
5411                next.scalar_upper.get(&m).cloned(),
5412            ) {
5413                (Some(a), Some(b)) if a == b => Some(a),
5414                (None, Some(b)) => Some(b),
5415                _ => None,
5416            };
5417            if inside.scalar_upper.get(&m) != merged_su.as_ref() {
5418                stable = false;
5419            }
5420            match merged_su {
5421                Some(l) => {
5422                    inside.scalar_upper.insert(m, l);
5423                }
5424                None => {
5425                    inside.scalar_upper.remove(&m);
5426                }
5427            }
5428            let merged_sl = match (
5429                inside.scalar_lower.get(&m).cloned(),
5430                next.scalar_lower.get(&m).cloned(),
5431            ) {
5432                (Some(a), Some(b)) if a == b => Some(a),
5433                (None, Some(b)) => Some(b),
5434                _ => None,
5435            };
5436            if inside.scalar_lower.get(&m) != merged_sl.as_ref() {
5437                stable = false;
5438            }
5439            match merged_sl {
5440                Some(l) => {
5441                    inside.scalar_lower.insert(m, l);
5442                }
5443                None => {
5444                    inside.scalar_lower.remove(&m);
5445                }
5446            }
5447
5448            let cur_ty = inside.types.get(&m).cloned().unwrap_or(TypeAbstraction::Top);
5449            let nxt_ty = next.types.get(&m).cloned().unwrap_or(TypeAbstraction::Top);
5450            let j = cur_ty.join(&nxt_ty);
5451            if !(j.leq(&cur_ty) && cur_ty.leq(&j)) {
5452                stable = false;
5453            }
5454            inside.types.insert(m, j);
5455
5456            let cur_sh = inside.shapes.get(&m).cloned().unwrap_or(CollectionShape::Top);
5457            let nxt_sh = next.shapes.get(&m).cloned().unwrap_or(CollectionShape::Top);
5458            inside.shapes.insert(m, cur_sh.widen(&nxt_sh));
5459
5460            // Kind survives growth, but a rebinding inside the body
5461            // (absent from `next`) drops the proof.
5462            if !next.coll_vars.contains(&m) {
5463                inside.coll_vars.remove(&m);
5464            }
5465
5466            let cur_n = inside.nullability.get(&m).cloned().unwrap_or(Nullability::Maybe);
5467            let nxt_n = next.nullability.get(&m).cloned().unwrap_or(Nullability::Maybe);
5468            inside.nullability.insert(m, cur_n.join(&nxt_n));
5469        }
5470        // Loop-CARRIED aliasing: an edge created at the end of one iteration
5471        // (`Set prev to curr`) holds at the top of the next, so the alias
5472        // graph joins by union across the back-edge. Monotone over a finite
5473        // symbol set — converges. Growth participates in the stability check.
5474        if inside.aliases.union_from(&next.aliases) {
5475            stable = false;
5476        }
5477        if stable {
5478            converged = true;
5479            break;
5480        }
5481    }
5482
5483    if converged {
5484        // The converged alias graph IS the loop invariant — snapshot it for
5485        // the distinctness queries (borrow hoisting). Non-converged loops
5486        // record nothing: queries refuse by default.
5487        if let Some(key) = loop_key {
5488            match facts.loop_aliases.entry(key) {
5489                std::collections::hash_map::Entry::Occupied(mut e) => {
5490                    e.get_mut().union_from(&inside.aliases);
5491                }
5492                std::collections::hash_map::Entry::Vacant(v) => {
5493                    v.insert(inside.aliases.clone());
5494                }
5495            }
5496        }
5497        // Record body facts at the invariant UNDER the loop condition (the
5498        // body only runs when it holds), then build the after-state: the
5499        // loop may run zero times (entry state) or exit from the invariant
5500        // — join, then apply the negated condition.
5501        let mut record_state = inside.clone();
5502        if let Some(c) = cond {
5503            narrow_state(c, &mut record_state.intervals);
5504        }
5505        // RELATIONAL induction-variable bound (V8/LLVM SCEV): a guard that
5506        // bounds an index variable by an array's length lets every
5507        // `item i of arr` read in the body skip its bounds check. Then the
5508        // SPECULATIVE hoist: arrays whose length is not statically known get a
5509        // single region-entry runtime check instead.
5510        if let Some(c) = cond {
5511            record_loop_index_bounds(c, body, &record_state, facts);
5512            // GENERAL multi-variable proof (kernel LIA): catches the affine
5513            // indices the single-var recognizer above cannot — knapsack's
5514            // `prev[w-wi+1]` under a path guard, nested-loop windows, scatter
5515            // indices bounded through element ranges. Idempotent with the
5516            // single-var path (both feed the same `relational_inbounds` set).
5517            record_affine_index_bounds(c, body, &record_state, st, facts);
5518            if let Some(key) = loop_key {
5519                record_hoist_speculation(c, body, &record_state, facts, key);
5520            }
5521        }
5522        rich_walk_block(body, &mut record_state, facts);
5523        let mut after = rich_join(st, &inside);
5524        if let Some(c) = cond {
5525            narrow_state_negated(c, &mut after.intervals);
5526            // Allocation-size fact: a counted build loop establishes a length
5527            // bound on the array it fills (`st` is the pre-loop entry state —
5528            // the array's emptiness and the counter's start are read there).
5529            // A variable bound gives a symbolic lower bound (consumed by the
5530            // relational recognizer); a literal bound gives a concrete exact
5531            // length (consumed by the interval bounds check directly).
5532            for bl in infer_build_length(c, body, st) {
5533                match bl {
5534                    BuildLength::Symbolic(arr, n, off) => {
5535                        after.length_def.insert(arr, (n, off));
5536                    }
5537                    BuildLength::Concrete(arr, len) => {
5538                        after.intervals.set_length(arr, Interval::exact(len));
5539                    }
5540                }
5541            }
5542        }
5543        *st = after;
5544    } else {
5545        // Sound fallback: forget everything the body can write.
5546        if let Some(c) = cond {
5547            narrow_state_negated(c, &mut st.intervals);
5548        }
5549        for m in mutated {
5550            st.intervals.set_var(m, Interval::top());
5551            st.intervals.set_length(m, Interval::non_negative());
5552            st.types.insert(m, TypeAbstraction::Top);
5553            st.shapes.insert(m, CollectionShape::Top);
5554            st.nullability.insert(m, Nullability::Maybe);
5555            st.coll_vars.remove(&m);
5556        }
5557    }
5558}
5559
5560fn rich_walk_inspect(
5561    target: &Expr,
5562    arms: &[MatchArm],
5563    st: &mut RichAbstractState,
5564    facts: &mut OracleFacts,
5565) {
5566    if arms.is_empty() {
5567        return;
5568    }
5569    let mut acc: Option<RichAbstractState> = None;
5570    for arm in arms {
5571        let mut arm_st = st.clone();
5572        // A concrete-variant arm proves the scrutinee (and its field bindings)
5573        // present — this is what lets the unwrap/`nothing` guard be removed.
5574        if arm.variant.is_some() {
5575            if let Expr::Identifier(sym) = target {
5576                arm_st.nullability.insert(*sym, Nullability::Definite);
5577            }
5578            for (_field, binding) in &arm.bindings {
5579                arm_st.nullability.insert(*binding, Nullability::Definite);
5580            }
5581        }
5582        // Field bindings extract handles from the scrutinee — unknown
5583        // provenance for the alias domain.
5584        for (_field, binding) in &arm.bindings {
5585            arm_st.aliases.unlink(*binding);
5586            arm_st.aliases.taint(*binding);
5587        }
5588        rich_walk_block(arm.body, &mut arm_st, facts);
5589        acc = Some(match acc {
5590            None => arm_st,
5591            Some(prev) => rich_join(&prev, &arm_st),
5592        });
5593    }
5594    if let Some(joined) = acc {
5595        *st = joined;
5596    }
5597}
5598
5599fn rich_join(a: &RichAbstractState, b: &RichAbstractState) -> RichAbstractState {
5600    let mut intervals = AbstractState::new();
5601    join_states(&mut intervals, &a.intervals, &b.intervals);
5602    RichAbstractState {
5603        intervals,
5604        types: join_maps(&a.types, &b.types),
5605        shapes: join_maps(&a.shapes, &b.shapes),
5606        nullability: join_maps(&a.nullability, &b.nullability),
5607        aliases: alias_union(&a.aliases, &b.aliases),
5608        // Kind proven only where BOTH paths prove it.
5609        coll_vars: a.coll_vars.intersection(&b.coll_vars).cloned().collect(),
5610        fn_returns: a.fn_returns.clone(),
5611        // A length fact survives a join only where both paths agree exactly.
5612        length_def: a
5613            .length_def
5614            .iter()
5615            .filter(|(k, v)| b.length_def.get(k) == Some(v))
5616            .map(|(k, v)| (*k, *v))
5617            .collect(),
5618        // Parameter-collection membership is a function-entry constant.
5619        param_colls: a.param_colls.union(&b.param_colls).copied().collect(),
5620        // A scalar definition survives a join only where both paths define it
5621        // to the SAME affine expression.
5622        scalar_def: a
5623            .scalar_def
5624            .iter()
5625            .filter(|(k, v)| b.scalar_def.get(*k) == Some(*v))
5626            .map(|(k, v)| (*k, v.clone()))
5627            .collect(),
5628        // Symbolic upper bounds survive a join only where both paths carry the
5629        // SAME bound — a divergent (or absent-on-one-side) bound is ⊤. Sound:
5630        // dropping a bound only forfeits an elision, never licenses one.
5631        scalar_upper: a
5632            .scalar_upper
5633            .iter()
5634            .filter(|(k, v)| b.scalar_upper.get(*k) == Some(*v))
5635            .map(|(k, v)| (*k, v.clone()))
5636            .collect(),
5637        elem_upper: join_elem_upper_maps(a, b),
5638        elem_type: join_elem_type_maps(a, b),
5639        scalar_lower: a
5640            .scalar_lower
5641            .iter()
5642            .filter(|(k, v)| b.scalar_lower.get(*k) == Some(*v))
5643            .map(|(k, v)| (*k, v.clone()))
5644            .collect(),
5645        // A function-entry constant (both joined states are the same body), so
5646        // it survives every merge and reaches the post-branch alias binds.
5647        aot_entry_guard: a.aot_entry_guard || b.aot_entry_guard,
5648    }
5649}
5650
5651/// Join the per-collection symbolic element uppers under the "EVERY element
5652/// satisfies it" reading, mirroring the concrete element interval's ⊥-seed: a
5653/// side whose array is ⊥ (a fresh `new Seq`, no elements) contributes nothing,
5654/// so the OTHER side's bound carries across the 0-iteration exit branch of a
5655/// build loop. When both sides hold elements the bounds must agree (or one
5656/// dominate); otherwise the bound is lost to ⊤.
5657fn join_elem_upper_maps(
5658    a: &RichAbstractState,
5659    b: &RichAbstractState,
5660) -> HashMap<Symbol, super::affine::LinExpr> {
5661    let mut out = HashMap::new();
5662    let keys: HashSet<Symbol> = a.elem_upper.keys().chain(b.elem_upper.keys()).cloned().collect();
5663    for key in keys {
5664        let a_bot = a.intervals.get_elem(&key).is_bottom();
5665        let b_bot = b.intervals.get_elem(&key).is_bottom();
5666        let v = if a_bot {
5667            b.elem_upper.get(&key).cloned()
5668        } else if b_bot {
5669            a.elem_upper.get(&key).cloned()
5670        } else {
5671            match (a.elem_upper.get(&key), b.elem_upper.get(&key)) {
5672                (Some(x), Some(y)) => join_sym_upper(x, y, b),
5673                _ => None,
5674            }
5675        };
5676        if let Some(l) = v {
5677            out.insert(key, l);
5678        }
5679    }
5680    out
5681}
5682
5683/// Join the per-collection element TYPES under the "EVERY element satisfies it"
5684/// reading, mirroring [`join_elem_upper_maps`]'s ⊥-seed: a side whose array is ⊥
5685/// (a fresh `new Seq`, no elements) contributes nothing, so the OTHER side's type
5686/// carries across the 0-iteration exit branch of a build loop. When both sides
5687/// hold elements the types JOIN (a divergent pair lands at ⊤, sound).
5688fn join_elem_type_maps(
5689    a: &RichAbstractState,
5690    b: &RichAbstractState,
5691) -> HashMap<Symbol, TypeAbstraction> {
5692    let mut out = HashMap::new();
5693    let keys: HashSet<Symbol> = a.elem_type.keys().chain(b.elem_type.keys()).cloned().collect();
5694    for key in keys {
5695        let a_bot = a.intervals.get_elem(&key).is_bottom();
5696        let b_bot = b.intervals.get_elem(&key).is_bottom();
5697        let v = if a_bot {
5698            b.elem_type.get(&key).cloned()
5699        } else if b_bot {
5700            a.elem_type.get(&key).cloned()
5701        } else {
5702            match (a.elem_type.get(&key), b.elem_type.get(&key)) {
5703                (Some(x), Some(y)) => Some(x.join(y)),
5704                _ => None,
5705            }
5706        };
5707        if let Some(t) = v {
5708            out.insert(key, t);
5709        }
5710    }
5711    out
5712}
5713
5714fn join_maps<V: AbstractDomain>(
5715    a: &HashMap<Symbol, V>,
5716    b: &HashMap<Symbol, V>,
5717) -> HashMap<Symbol, V> {
5718    let mut out = HashMap::new();
5719    let keys: HashSet<Symbol> = a.keys().chain(b.keys()).cloned().collect();
5720    for k in keys {
5721        let top = V::top();
5722        let va = a.get(&k).unwrap_or(&top);
5723        let vb = b.get(&k).unwrap_or(&top);
5724        out.insert(k, va.join(vb));
5725    }
5726    out
5727}
5728
5729fn alias_union(a: &AliasGraph, b: &AliasGraph) -> AliasGraph {
5730    let mut out = a.clone();
5731    out.union_from(b);
5732    out
5733}
5734
5735fn collect_mutations(block: &[Stmt]) -> Vec<Symbol> {
5736    let mut out = Vec::new();
5737    for s in block {
5738        collect_mut_stmt(s, &mut out);
5739    }
5740    out
5741}
5742
5743fn add_unique(sym: Symbol, out: &mut Vec<Symbol>) {
5744    if !out.contains(&sym) {
5745        out.push(sym);
5746    }
5747}
5748
5749fn collect_mut_stmt(s: &Stmt, out: &mut Vec<Symbol>) {
5750    match s {
5751        Stmt::Set { target, .. } => add_unique(*target, out),
5752        Stmt::Let { var, .. } => add_unique(*var, out),
5753        Stmt::Push { collection, .. }
5754        | Stmt::Add { collection, .. }
5755        | Stmt::Remove { collection, .. }
5756        | Stmt::SetIndex { collection, .. } => {
5757            if let Expr::Identifier(sym) = *collection {
5758                add_unique(*sym, out);
5759            }
5760        }
5761        Stmt::Pop { collection, into } => {
5762            if let Expr::Identifier(sym) = *collection {
5763                add_unique(*sym, out);
5764            }
5765            if let Some(v) = into {
5766                add_unique(*v, out);
5767            }
5768        }
5769        Stmt::SetField { object, .. } => {
5770            if let Expr::Identifier(sym) = *object {
5771                add_unique(*sym, out);
5772            }
5773        }
5774        Stmt::ReadFrom { var, .. } => add_unique(*var, out),
5775        Stmt::If { then_block, else_block, .. } => {
5776            for st in *then_block {
5777                collect_mut_stmt(st, out);
5778            }
5779            if let Some(eb) = else_block {
5780                for st in *eb {
5781                    collect_mut_stmt(st, out);
5782                }
5783            }
5784        }
5785        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
5786            for st in *body {
5787                collect_mut_stmt(st, out);
5788            }
5789        }
5790        Stmt::Inspect { arms, .. } => {
5791            for arm in arms {
5792                for st in arm.body {
5793                    collect_mut_stmt(st, out);
5794                }
5795            }
5796        }
5797        _ => {}
5798    }
5799}
5800
5801#[cfg(test)]
5802mod domain_tests {
5803    use super::*;
5804
5805    fn iv(lo: i64, hi: i64) -> Interval {
5806        Interval { lo: Bound::Finite(lo), hi: Bound::Finite(hi) }
5807    }
5808
5809    #[test]
5810    fn interval_top_and_bottom() {
5811        let t = <Interval as AbstractDomain>::top();
5812        assert!(matches!(t.lo, Bound::NegInf));
5813        assert!(matches!(t.hi, Bound::PosInf));
5814        assert!(!t.is_bottom());
5815
5816        let b = <Interval as AbstractDomain>::bottom();
5817        assert!(b.is_bottom());
5818    }
5819
5820    #[test]
5821    fn interval_join_is_least_upper_bound() {
5822        // [1,3] ⊔ [5,7] = [1,7]
5823        let j = iv(1, 3).join(&iv(5, 7));
5824        assert_eq!(j.lo, Bound::Finite(1));
5825        assert_eq!(j.hi, Bound::Finite(7));
5826        // bottom is the identity for join: ⊥ ⊔ x = x
5827        let jb = Interval::bottom().join(&iv(2, 4));
5828        assert_eq!(jb.lo, Bound::Finite(2));
5829        assert_eq!(jb.hi, Bound::Finite(4));
5830        let jb2 = iv(2, 4).join(&Interval::bottom());
5831        assert_eq!(jb2.lo, Bound::Finite(2));
5832        assert_eq!(jb2.hi, Bound::Finite(4));
5833    }
5834
5835    #[test]
5836    fn interval_meet_is_greatest_lower_bound() {
5837        // [1,5] ⊓ [3,7] = [3,5]
5838        let m = iv(1, 5).meet(&iv(3, 7));
5839        assert_eq!(m.lo, Bound::Finite(3));
5840        assert_eq!(m.hi, Bound::Finite(5));
5841        // disjoint intervals meet to ⊥
5842        assert!(iv(1, 2).meet(&iv(5, 6)).is_bottom());
5843        // top is the identity for meet: ⊤ ⊓ x = x
5844        let t = Interval::top().meet(&iv(2, 4));
5845        assert_eq!(t.lo, Bound::Finite(2));
5846        assert_eq!(t.hi, Bound::Finite(4));
5847    }
5848
5849    #[test]
5850    fn interval_widen_climbs_the_threshold_ladder_then_to_infinity() {
5851        // CONTRACT UPDATE (EXODIA M9, plan-approved): widening no longer
5852        // jumps straight to ±∞ — an unstable bound snaps to the next rung
5853        // of the threshold ladder [-1000..1000], and only past the last
5854        // rung falls off to ±∞. Loop counters keep finite bounds.
5855        // rising upper bound snaps to the next rung, stable lower preserved
5856        let w = iv(0, 5).widen(&iv(0, 10));
5857        assert_eq!(w.lo, Bound::Finite(0));
5858        assert_eq!(w.hi, Bound::Finite(10));
5859        let w_mid = iv(0, 5).widen(&iv(0, 11));
5860        assert_eq!(w_mid.hi, Bound::Finite(100));
5861        // beyond the last rung → +∞
5862        let w_inf = iv(0, 5).widen(&iv(0, 1001));
5863        assert!(matches!(w_inf.hi, Bound::PosInf));
5864        // falling lower bound snaps to the next rung down, upper preserved
5865        let w2 = iv(0, 5).widen(&iv(-3, 5));
5866        assert_eq!(w2.lo, Bound::Finite(-10));
5867        assert_eq!(w2.hi, Bound::Finite(5));
5868        // below the last rung → -∞
5869        let w2_inf = iv(0, 5).widen(&iv(-1001, 5));
5870        assert!(matches!(w2_inf.lo, Bound::NegInf));
5871        // stable interval is a fixpoint (no spurious widening)
5872        let w3 = iv(0, 5).widen(&iv(0, 5));
5873        assert_eq!(w3.lo, Bound::Finite(0));
5874        assert_eq!(w3.hi, Bound::Finite(5));
5875        // widening from ⊥ yields the new value
5876        let w4 = Interval::bottom().widen(&iv(2, 4));
5877        assert_eq!(w4.lo, Bound::Finite(2));
5878        assert_eq!(w4.hi, Bound::Finite(4));
5879        // the ascent is FINITE: iterated widening against ever-growing
5880        // inputs stabilizes within ladder-length steps (termination).
5881        let mut cur = iv(0, 1);
5882        let mut steps = 0;
5883        loop {
5884            let grown = Interval {
5885                lo: cur.lo.clone(),
5886                hi: match cur.hi {
5887                    Bound::Finite(v) => Bound::Finite(v.saturating_add(v.abs() + 1)),
5888                    ref b => b.clone(),
5889                },
5890            };
5891            let next = cur.widen(&grown);
5892            steps += 1;
5893            if next.leq(&cur) && cur.leq(&next) {
5894                break;
5895            }
5896            cur = next;
5897            assert!(steps <= 12, "widening must terminate within the ladder");
5898        }
5899        assert!(matches!(cur.hi, Bound::PosInf));
5900    }
5901
5902    #[test]
5903    fn interval_leq_is_the_subset_order() {
5904        assert!(iv(3, 5).leq(&iv(1, 7)));
5905        assert!(!iv(1, 7).leq(&iv(3, 5)));
5906        // ⊥ ⊑ everything; everything ⊑ ⊤
5907        assert!(Interval::bottom().leq(&iv(1, 2)));
5908        assert!(iv(1, 2).leq(&<Interval as AbstractDomain>::top()));
5909        // reflexive
5910        assert!(iv(1, 2).leq(&iv(1, 2)));
5911    }
5912
5913    #[test]
5914    fn interval_lattice_laws_hold() {
5915        let a = iv(1, 5);
5916        let b = iv(3, 9);
5917        // a ⊑ a⊔b and b ⊑ a⊔b (join is an upper bound)
5918        assert!(a.leq(&a.join(&b)));
5919        assert!(b.leq(&a.join(&b)));
5920        // a⊓b ⊑ a and a⊓b ⊑ b (meet is a lower bound)
5921        assert!(a.meet(&b).leq(&a));
5922        assert!(a.meet(&b).leq(&b));
5923        // commutativity of join and meet
5924        assert!(a.join(&b).leq(&b.join(&a)) && b.join(&a).leq(&a.join(&b)));
5925        assert!(a.meet(&b).leq(&b.meet(&a)) && b.meet(&a).leq(&a.meet(&b)));
5926    }
5927}
5928
5929#[cfg(test)]
5930mod type_domain_tests {
5931    use super::*;
5932    use std::collections::HashMap;
5933
5934    fn empty_types() -> HashMap<Symbol, TypeAbstraction> {
5935        HashMap::new()
5936    }
5937
5938    fn union(tags: &[TypeTag]) -> TypeAbstraction {
5939        TypeAbstraction::Union(tags.iter().cloned().collect())
5940    }
5941
5942    #[test]
5943    fn literal_types_are_concrete() {
5944        assert_eq!(eval_type(&Expr::Literal(Literal::Number(5)), &empty_types(), &HashMap::new(), &HashMap::new()),
5945                   TypeAbstraction::Concrete(TypeTag::Int));
5946        assert_eq!(eval_type(&Expr::Literal(Literal::Float(1.5)), &empty_types(), &HashMap::new(), &HashMap::new()),
5947                   TypeAbstraction::Concrete(TypeTag::Float));
5948        assert_eq!(eval_type(&Expr::Literal(Literal::Boolean(true)), &empty_types(), &HashMap::new(), &HashMap::new()),
5949                   TypeAbstraction::Concrete(TypeTag::Bool));
5950        assert_eq!(eval_type(&Expr::Literal(Literal::Nothing), &empty_types(), &HashMap::new(), &HashMap::new()),
5951                   TypeAbstraction::Concrete(TypeTag::Nothing));
5952        assert_eq!(eval_type(&Expr::Literal(Literal::Char('a')), &empty_types(), &HashMap::new(), &HashMap::new()),
5953                   TypeAbstraction::Concrete(TypeTag::Char));
5954        assert_eq!(eval_type(&Expr::Literal(Literal::Duration(60)), &empty_types(), &HashMap::new(), &HashMap::new()),
5955                   TypeAbstraction::Concrete(TypeTag::Duration));
5956    }
5957
5958    #[test]
5959    fn arithmetic_and_comparison_result_types() {
5960        let five = Expr::Literal(Literal::Number(5));
5961        let three = Expr::Literal(Literal::Number(3));
5962        // Int + Int : Int
5963        let add = Expr::BinaryOp { op: BinaryOpKind::Add, left: &five, right: &three };
5964        assert_eq!(eval_type(&add, &empty_types(), &HashMap::new(), &HashMap::new()), TypeAbstraction::Concrete(TypeTag::Int));
5965        // Int < Int : Bool
5966        let lt = Expr::BinaryOp { op: BinaryOpKind::Lt, left: &five, right: &three };
5967        assert_eq!(eval_type(&lt, &empty_types(), &HashMap::new(), &HashMap::new()), TypeAbstraction::Concrete(TypeTag::Bool));
5968        // Float + Float : Float
5969        let f1 = Expr::Literal(Literal::Float(1.0));
5970        let f2 = Expr::Literal(Literal::Float(2.0));
5971        let fadd = Expr::BinaryOp { op: BinaryOpKind::Add, left: &f1, right: &f2 };
5972        assert_eq!(eval_type(&fadd, &empty_types(), &HashMap::new(), &HashMap::new()), TypeAbstraction::Concrete(TypeTag::Float));
5973        // Int + Float : Top (we do not assume a coercion — conservative)
5974        let mixed = Expr::BinaryOp { op: BinaryOpKind::Add, left: &five, right: &f1 };
5975        assert_eq!(eval_type(&mixed, &empty_types(), &HashMap::new(), &HashMap::new()), TypeAbstraction::Top);
5976        // Text + Int : Text (interpolating concat)
5977        let txt = Expr::Literal(Literal::Text(Symbol::EMPTY));
5978        let concat = Expr::BinaryOp { op: BinaryOpKind::Add, left: &txt, right: &three };
5979        assert_eq!(eval_type(&concat, &empty_types(), &HashMap::new(), &HashMap::new()), TypeAbstraction::Concrete(TypeTag::Text));
5980    }
5981
5982    #[test]
5983    fn let_binds_value_type_in_env() {
5984        // `Let x be 5.` ⇒ env[x] = Concrete(Int); a later use of x reads Int.
5985        let x = Symbol::EMPTY;
5986        let mut env = empty_types();
5987        let value = Expr::Literal(Literal::Number(5));
5988        env.insert(x, eval_type(&value, &env, &HashMap::new(), &HashMap::new()));
5989        assert_eq!(env.get(&x), Some(&TypeAbstraction::Concrete(TypeTag::Int)));
5990        // and an identifier reference resolves through the env
5991        assert_eq!(eval_type(&Expr::Identifier(x), &env, &HashMap::new(), &HashMap::new()),
5992                   TypeAbstraction::Concrete(TypeTag::Int));
5993        // an unbound identifier is Top (no information)
5994        let unbound = empty_types();
5995        assert_eq!(eval_type(&Expr::Identifier(x), &unbound, &HashMap::new(), &HashMap::new()), TypeAbstraction::Top);
5996    }
5997
5998    #[test]
5999    fn type_join_merges_distinct_into_union_and_collapses() {
6000        let i = TypeAbstraction::Concrete(TypeTag::Int);
6001        let b = TypeAbstraction::Concrete(TypeTag::Bool);
6002        // distinct concretes join to a union
6003        assert_eq!(i.join(&b), union(&[TypeTag::Int, TypeTag::Bool]));
6004        // identical concretes stay concrete
6005        assert_eq!(i.join(&i), i);
6006        // a singleton union normalizes back to a concrete
6007        assert_eq!(union(&[TypeTag::Int]), i);
6008        // join with Top is Top; join with Bottom is identity
6009        assert_eq!(i.join(&TypeAbstraction::Top), TypeAbstraction::Top);
6010        assert_eq!(i.join(&TypeAbstraction::Bottom), i);
6011        assert_eq!(TypeAbstraction::Bottom.join(&i), i);
6012    }
6013
6014    #[test]
6015    fn type_meet_intersects() {
6016        let i = TypeAbstraction::Concrete(TypeTag::Int);
6017        let b = TypeAbstraction::Concrete(TypeTag::Bool);
6018        let ib = union(&[TypeTag::Int, TypeTag::Bool]);
6019        // Concrete ⊓ matching Union = Concrete
6020        assert_eq!(i.meet(&ib), i);
6021        // disjoint concretes meet to Bottom
6022        assert_eq!(i.meet(&b), TypeAbstraction::Bottom);
6023        // Top is identity for meet
6024        assert_eq!(TypeAbstraction::Top.meet(&i), i);
6025        // Bottom annihilates
6026        assert_eq!(TypeAbstraction::Bottom.meet(&i), TypeAbstraction::Bottom);
6027    }
6028
6029    #[test]
6030    fn type_lattice_order_and_laws() {
6031        let i = TypeAbstraction::Concrete(TypeTag::Int);
6032        let ib = union(&[TypeTag::Int, TypeTag::Bool]);
6033        // Bottom ⊑ everything ⊑ Top
6034        assert!(TypeAbstraction::Bottom.leq(&i));
6035        assert!(i.leq(&TypeAbstraction::Top));
6036        // Concrete ⊑ Union containing it
6037        assert!(i.leq(&ib));
6038        assert!(!ib.leq(&i));
6039        // reflexive
6040        assert!(i.leq(&i));
6041        // join is an upper bound; widen over-approximates join (finite lattice → widen == join)
6042        let b = TypeAbstraction::Concrete(TypeTag::Bool);
6043        assert!(i.leq(&i.join(&b)));
6044        assert!(b.leq(&i.join(&b)));
6045        assert_eq!(i.widen(&b), i.join(&b));
6046    }
6047}
6048
6049#[cfg(test)]
6050mod shape_domain_tests {
6051    use super::*;
6052
6053    #[test]
6054    fn new_collection_is_empty_and_push_pop_track_size() {
6055        assert_eq!(CollectionShape::empty_collection(), CollectionShape::Empty);
6056        // Push grows the size precisely.
6057        assert_eq!(CollectionShape::Empty.pushed(), CollectionShape::Singleton);
6058        assert_eq!(CollectionShape::Singleton.pushed(), CollectionShape::KnownSize(2));
6059        assert_eq!(CollectionShape::KnownSize(2).pushed(), CollectionShape::KnownSize(3));
6060        // Pop shrinks it.
6061        assert_eq!(CollectionShape::KnownSize(3).popped(), CollectionShape::KnownSize(2));
6062        assert_eq!(CollectionShape::Singleton.popped(), CollectionShape::Empty);
6063        // Pop on a definitely-empty collection stays Empty (saturating).
6064        assert_eq!(CollectionShape::Empty.popped(), CollectionShape::Empty);
6065        // Pushing an unknown (Top) collection proves it non-empty.
6066        assert_eq!(CollectionShape::Top.pushed(), CollectionShape::NonEmpty);
6067    }
6068
6069    #[test]
6070    fn nonempty_and_empty_predicates() {
6071        assert!(CollectionShape::Singleton.is_definitely_nonempty());
6072        assert!(CollectionShape::KnownSize(5).is_definitely_nonempty());
6073        assert!(CollectionShape::NonEmpty.is_definitely_nonempty());
6074        assert!(!CollectionShape::Empty.is_definitely_nonempty());
6075        assert!(!CollectionShape::Top.is_definitely_nonempty());
6076        assert!(!CollectionShape::KnownSize(0).is_definitely_nonempty());
6077
6078        assert!(CollectionShape::Empty.is_definitely_empty());
6079        assert!(CollectionShape::KnownSize(0).is_definitely_empty());
6080        assert!(!CollectionShape::Singleton.is_definitely_empty());
6081        assert!(!CollectionShape::Top.is_definitely_empty());
6082    }
6083
6084    #[test]
6085    fn shape_canonical_equality() {
6086        // Named variants that denote the same size set compare equal.
6087        assert_eq!(CollectionShape::KnownSize(0), CollectionShape::Empty);
6088        assert_eq!(CollectionShape::KnownSize(1), CollectionShape::Singleton);
6089        assert_eq!(CollectionShape::SizeRange(1, 1), CollectionShape::Singleton);
6090    }
6091
6092    #[test]
6093    fn shape_lattice_join_meet_leq() {
6094        // join is union of size ranges
6095        assert_eq!(CollectionShape::Empty.join(&CollectionShape::Singleton),
6096                   CollectionShape::SizeRange(0, 1));
6097        assert_eq!(CollectionShape::Singleton.join(&CollectionShape::KnownSize(3)),
6098                   CollectionShape::SizeRange(1, 3));
6099        assert_eq!(CollectionShape::Singleton.join(&CollectionShape::Top),
6100                   CollectionShape::Top);
6101        // meet is intersection; disjoint sizes collapse to Bottom
6102        assert_eq!(CollectionShape::Empty.meet(&CollectionShape::Singleton),
6103                   CollectionShape::Bottom);
6104        assert_eq!(CollectionShape::Top.meet(&CollectionShape::Singleton),
6105                   CollectionShape::Singleton);
6106        // order
6107        assert!(CollectionShape::Singleton.leq(&CollectionShape::NonEmpty));
6108        assert!(CollectionShape::KnownSize(2).leq(&CollectionShape::SizeRange(0, 5)));
6109        assert!(CollectionShape::Empty.leq(&CollectionShape::Top));
6110        assert!(CollectionShape::Bottom.leq(&CollectionShape::Empty));
6111        assert!(!CollectionShape::Top.leq(&CollectionShape::NonEmpty));
6112    }
6113
6114    #[test]
6115    fn shape_widening_converges_growing_loops() {
6116        // A push-only loop keeps growing the upper bound → +∞, stays NonEmpty.
6117        let w = CollectionShape::KnownSize(1).widen(&CollectionShape::KnownSize(2));
6118        assert_eq!(w, CollectionShape::NonEmpty);
6119        // A collection that might shrink widens its lower bound toward 0.
6120        let w2 = CollectionShape::KnownSize(2).widen(&CollectionShape::KnownSize(1));
6121        assert!(w2.leq(&CollectionShape::Top));
6122    }
6123}
6124
6125#[cfg(test)]
6126mod nullability_domain_tests {
6127    use super::*;
6128
6129    #[test]
6130    fn top_and_bottom() {
6131        assert_eq!(<Nullability as AbstractDomain>::top(), Nullability::Maybe);
6132        assert_eq!(<Nullability as AbstractDomain>::bottom(), Nullability::Bottom);
6133    }
6134
6135    #[test]
6136    fn literal_nullability() {
6137        // `nothing` is definitely absent; everything else is definitely present.
6138        assert_eq!(Nullability::for_literal(&Literal::Nothing), Nullability::Null);
6139        assert_eq!(Nullability::for_literal(&Literal::Number(5)), Nullability::Definite);
6140        assert_eq!(Nullability::for_literal(&Literal::Boolean(false)), Nullability::Definite);
6141    }
6142
6143    #[test]
6144    fn matched_variant_binding_is_definite() {
6145        // `Inspect x: When Some(v): ...` ⇒ inside the arm, the match is present.
6146        // This is the fact that lets the unwrap guard be eliminated.
6147        assert_eq!(Nullability::for_matched_variant(), Nullability::Definite);
6148    }
6149
6150    #[test]
6151    fn nullability_join_meet() {
6152        // join merges Definite and Null into Maybe (the diamond top)
6153        assert_eq!(Nullability::Definite.join(&Nullability::Null), Nullability::Maybe);
6154        assert_eq!(Nullability::Definite.join(&Nullability::Definite), Nullability::Definite);
6155        assert_eq!(Nullability::Bottom.join(&Nullability::Null), Nullability::Null);
6156        assert_eq!(Nullability::Maybe.join(&Nullability::Definite), Nullability::Maybe);
6157        // meet refines; Definite ⊓ Null is unreachable
6158        assert_eq!(Nullability::Definite.meet(&Nullability::Null), Nullability::Bottom);
6159        assert_eq!(Nullability::Maybe.meet(&Nullability::Definite), Nullability::Definite);
6160        assert_eq!(Nullability::Definite.meet(&Nullability::Definite), Nullability::Definite);
6161    }
6162
6163    #[test]
6164    fn nullability_order_and_widen() {
6165        assert!(Nullability::Bottom.leq(&Nullability::Definite));
6166        assert!(Nullability::Definite.leq(&Nullability::Maybe));
6167        assert!(Nullability::Null.leq(&Nullability::Maybe));
6168        assert!(Nullability::Definite.leq(&Nullability::Definite));
6169        assert!(!Nullability::Definite.leq(&Nullability::Null));
6170        assert!(!Nullability::Maybe.leq(&Nullability::Definite));
6171        // finite lattice → widen is join
6172        assert_eq!(Nullability::Definite.widen(&Nullability::Null),
6173                   Nullability::Definite.join(&Nullability::Null));
6174    }
6175}
6176
6177#[cfg(test)]
6178mod alias_domain_tests {
6179    use super::*;
6180    use crate::intern::Interner;
6181    use std::collections::HashSet;
6182
6183    fn set(syms: &[Symbol]) -> HashSet<Symbol> {
6184        syms.iter().cloned().collect()
6185    }
6186
6187    #[test]
6188    fn alias_lattice_ops() {
6189        let mut it = Interner::new();
6190        let s = it.intern("s");
6191        let t = it.intern("t");
6192        assert_eq!(<AliasInfo as AbstractDomain>::top(), AliasInfo::Top);
6193        assert_eq!(<AliasInfo as AbstractDomain>::bottom(), AliasInfo::Bottom);
6194        // Unique is "may-alias nobody" = MayAlias(∅)
6195        assert_eq!(AliasInfo::MayAlias(HashSet::new()), AliasInfo::Unique);
6196        // join unions the may-alias sets
6197        assert_eq!(AliasInfo::MayAlias(set(&[s])).join(&AliasInfo::MayAlias(set(&[t]))),
6198                   AliasInfo::MayAlias(set(&[s, t])));
6199        assert_eq!(AliasInfo::Unique.join(&AliasInfo::MayAlias(set(&[s]))),
6200                   AliasInfo::MayAlias(set(&[s])));
6201        assert_eq!(AliasInfo::Top.join(&AliasInfo::Unique), AliasInfo::Top);
6202        // meet intersects
6203        assert_eq!(AliasInfo::MayAlias(set(&[s, t])).meet(&AliasInfo::MayAlias(set(&[s]))),
6204                   AliasInfo::MayAlias(set(&[s])));
6205        assert_eq!(AliasInfo::Top.meet(&AliasInfo::MayAlias(set(&[s]))),
6206                   AliasInfo::MayAlias(set(&[s])));
6207        // order: fewer possible aliases is more precise
6208        assert!(AliasInfo::Unique.leq(&AliasInfo::MayAlias(set(&[s]))));
6209        assert!(AliasInfo::MayAlias(set(&[s])).leq(&AliasInfo::MayAlias(set(&[s, t]))));
6210        assert!(AliasInfo::MayAlias(set(&[s])).leq(&AliasInfo::Top));
6211        assert!(AliasInfo::Bottom.leq(&AliasInfo::Unique));
6212        assert!(!AliasInfo::Top.leq(&AliasInfo::Unique));
6213        // widen = join (alias sets are bounded by the program's variables)
6214        assert_eq!(AliasInfo::MayAlias(set(&[s])).widen(&AliasInfo::MayAlias(set(&[t]))),
6215                   AliasInfo::MayAlias(set(&[s])).join(&AliasInfo::MayAlias(set(&[t]))));
6216    }
6217
6218    #[test]
6219    fn aliasing_mutation_invalidates_shared_allocation() {
6220        let mut it = Interner::new();
6221        let a = it.intern("a");
6222        let items = it.intern("items");
6223        let c = it.intern("c"); // unrelated
6224
6225        let mut g = AliasGraph::new();
6226        g.link(a, items); // `Let a be items.` — they share one Rc<RefCell>
6227
6228        // `Push 1 to a.` mutates the shared allocation → invalidate `items` too.
6229        let inval = g.invalidated_by_mutation(a);
6230        assert!(inval.contains(&items));
6231        assert!(inval.contains(&a));
6232        assert!(!inval.contains(&c));
6233
6234        // symmetric: mutating `items` invalidates `a`
6235        assert!(g.invalidated_by_mutation(items).contains(&a));
6236
6237        // an unrelated variable aliases only itself
6238        assert_eq!(g.invalidated_by_mutation(c), set(&[c]));
6239
6240        // rebinding `a` to a fresh allocation breaks the alias
6241        g.unlink(a);
6242        assert!(!g.invalidated_by_mutation(items).contains(&a));
6243    }
6244
6245    #[test]
6246    fn alias_transitivity() {
6247        let mut it = Interner::new();
6248        let a = it.intern("a");
6249        let b = it.intern("b");
6250        let c = it.intern("c");
6251        let mut g = AliasGraph::new();
6252        g.link(a, b); // Let b be a
6253        g.link(b, c); // Let c be b
6254        // mutation through a reaches c via b
6255        let inval = g.invalidated_by_mutation(a);
6256        assert!(inval.contains(&b) && inval.contains(&c));
6257    }
6258}
6259
6260#[cfg(test)]
6261mod product_lattice_tests {
6262    use super::*;
6263
6264    fn av(iv: (i64, i64), t: TypeTag, sh: CollectionShape, n: Nullability) -> AbstractValue {
6265        AbstractValue {
6266            interval: Interval { lo: Bound::Finite(iv.0), hi: Bound::Finite(iv.1) },
6267            ty: TypeAbstraction::Concrete(t),
6268            shape: sh,
6269            nullability: n,
6270            alias: AliasInfo::Unique,
6271        }
6272    }
6273
6274    #[test]
6275    fn product_top_is_all_top() {
6276        let t = <AbstractValue as AbstractDomain>::top();
6277        assert_eq!(t.ty, TypeAbstraction::Top);
6278        assert_eq!(t.shape, CollectionShape::Top);
6279        assert_eq!(t.nullability, Nullability::Maybe);
6280        assert_eq!(t.alias, AliasInfo::Top);
6281        assert!(t.interval.lo == Bound::NegInf && t.interval.hi == Bound::PosInf);
6282    }
6283
6284    #[test]
6285    fn product_join_is_componentwise() {
6286        let a = av((1, 1), TypeTag::Int, CollectionShape::Singleton, Nullability::Definite);
6287        let b = av((3, 3), TypeTag::Int, CollectionShape::KnownSize(3), Nullability::Definite);
6288        let j = a.join(&b);
6289        // interval joins to [1,3]
6290        assert_eq!(j.interval.lo, Bound::Finite(1));
6291        assert_eq!(j.interval.hi, Bound::Finite(3));
6292        // same type stays Concrete(Int)
6293        assert_eq!(j.ty, TypeAbstraction::Concrete(TypeTag::Int));
6294        // shapes join to a size range
6295        assert_eq!(j.shape, CollectionShape::SizeRange(1, 3));
6296        // both Definite stays Definite
6297        assert_eq!(j.nullability, Nullability::Definite);
6298    }
6299
6300    #[test]
6301    fn product_join_mixed_types_widens_each_component() {
6302        let a = av((0, 0), TypeTag::Int, CollectionShape::Empty, Nullability::Null);
6303        let b = av((5, 5), TypeTag::Bool, CollectionShape::Singleton, Nullability::Definite);
6304        let j = a.join(&b);
6305        assert_eq!(j.interval.lo, Bound::Finite(0));
6306        assert_eq!(j.interval.hi, Bound::Finite(5));
6307        // distinct types → union
6308        assert_eq!(j.ty, TypeAbstraction::Union([TypeTag::Int, TypeTag::Bool].into_iter().collect()));
6309        assert_eq!(j.shape, CollectionShape::SizeRange(0, 1));
6310        // Null ⊔ Definite → Maybe
6311        assert_eq!(j.nullability, Nullability::Maybe);
6312    }
6313
6314    #[test]
6315    fn product_leq_and_widen_componentwise() {
6316        let a = av((1, 1), TypeTag::Int, CollectionShape::Singleton, Nullability::Definite);
6317        let top = <AbstractValue as AbstractDomain>::top();
6318        assert!(a.leq(&top));
6319        assert!(!top.leq(&a));
6320        // widen each component: the rising interval bound snaps to the
6321        // next THRESHOLD rung (EXODIA M9 ladder), not straight to +∞ —
6322        let b = av((1, 9), TypeTag::Int, CollectionShape::KnownSize(9), Nullability::Definite);
6323        let w = a.widen(&b);
6324        assert_eq!(w.interval.hi, Bound::Finite(10));
6325        assert_eq!(w.ty, TypeAbstraction::Concrete(TypeTag::Int));
6326        // — and falls off to +∞ only past the last rung.
6327        let c = av((1, 5000), TypeTag::Int, CollectionShape::KnownSize(9), Nullability::Definite);
6328        let w2 = a.widen(&c);
6329        assert!(matches!(w2.interval.hi, Bound::PosInf));
6330    }
6331}
6332
6333#[cfg(test)]
6334mod rich_walk_tests {
6335    use super::*;
6336    use crate::arena::Arena;
6337    use crate::intern::Interner;
6338
6339    fn lit_num<'a>(ea: &'a Arena<Expr<'a>>, n: i64) -> &'a Expr<'a> {
6340        ea.alloc(Expr::Literal(Literal::Number(n)))
6341    }
6342    fn ident<'a>(ea: &'a Arena<Expr<'a>>, s: Symbol) -> &'a Expr<'a> {
6343        ea.alloc(Expr::Identifier(s))
6344    }
6345    fn add<'a>(ea: &'a Arena<Expr<'a>>, l: &'a Expr<'a>, r: &'a Expr<'a>) -> &'a Expr<'a> {
6346        ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Add, left: l, right: r })
6347    }
6348    fn new_seq<'a>(ea: &'a Arena<Expr<'a>>, name: Symbol) -> &'a Expr<'a> {
6349        ea.alloc(Expr::New { type_name: name, type_args: vec![], init_fields: vec![] })
6350    }
6351    fn let_stmt<'a>(var: Symbol, value: &'a Expr<'a>) -> Stmt<'a> {
6352        Stmt::Let { var, ty: None, value, mutable: true }
6353    }
6354
6355    #[test]
6356    fn let_chain_tracks_type_and_interval() {
6357        let ea: Arena<Expr> = Arena::new();
6358        let sa: Arena<Stmt> = Arena::new();
6359        let mut it = Interner::new();
6360        let x = it.intern("x");
6361        let y = it.intern("y");
6362        // Let x be 5. Let y be x + 7.
6363        let five = lit_num(&ea, 5);
6364        let xref = ident(&ea, x);
6365        let seven = lit_num(&ea, 7);
6366        let sum = add(&ea, xref, seven);
6367        let stmts = vec![let_stmt(x, five), let_stmt(y, sum)];
6368
6369        let (_out, st) = rich_abstract_interp_stmts(stmts, &ea, &sa);
6370        let vx = st.value_of(x);
6371        let vy = st.value_of(y);
6372        assert_eq!(vx.ty, TypeAbstraction::Concrete(TypeTag::Int));
6373        assert_eq!(vx.interval.is_exact(), Some(5));
6374        assert_eq!(vy.ty, TypeAbstraction::Concrete(TypeTag::Int));
6375        assert_eq!(vy.interval.is_exact(), Some(12));
6376        assert_eq!(vx.nullability, Nullability::Definite);
6377    }
6378
6379    #[test]
6380    fn new_and_push_track_shape() {
6381        let ea: Arena<Expr> = Arena::new();
6382        let sa: Arena<Stmt> = Arena::new();
6383        let mut it = Interner::new();
6384        let items = it.intern("items");
6385        let seq_ty = it.intern("Seq");
6386        // Let items be a new Seq. Push 10 to items.
6387        let new_e = new_seq(&ea, seq_ty);
6388        let ten = lit_num(&ea, 10);
6389        let items_ref = ident(&ea, items);
6390        let stmts = vec![
6391            let_stmt(items, new_e),
6392            Stmt::Push { value: ten, collection: items_ref },
6393        ];
6394        let (_out, st) = rich_abstract_interp_stmts(stmts, &ea, &sa);
6395        let v = st.value_of(items);
6396        assert_eq!(v.shape, CollectionShape::Singleton);
6397        assert!(v.shape.is_definitely_nonempty());
6398    }
6399
6400    #[test]
6401    fn alias_push_keeps_aliases_consistent() {
6402        let ea: Arena<Expr> = Arena::new();
6403        let sa: Arena<Stmt> = Arena::new();
6404        let mut it = Interner::new();
6405        let items = it.intern("items");
6406        let a = it.intern("a");
6407        let seq_ty = it.intern("Seq");
6408        // Let items be a new Seq. Push 1 to items. Let a be items. Push 2 to a.
6409        let new_e = new_seq(&ea, seq_ty);
6410        let one = lit_num(&ea, 1);
6411        let two = lit_num(&ea, 2);
6412        let items_ref1 = ident(&ea, items);
6413        let items_ref2 = ident(&ea, items);
6414        let a_ref = ident(&ea, a);
6415        let stmts = vec![
6416            let_stmt(items, new_e),
6417            Stmt::Push { value: one, collection: items_ref1 },
6418            let_stmt(a, items_ref2), // a aliases items
6419            Stmt::Push { value: two, collection: a_ref }, // mutate through alias
6420        ];
6421        let (_out, st) = rich_abstract_interp_stmts(stmts, &ea, &sa);
6422        let vitems = st.value_of(items);
6423        let va = st.value_of(a);
6424        // pushing through `a` updated `items` (shared Rc) — both see size 2.
6425        assert!(vitems.shape.is_definitely_nonempty());
6426        assert_eq!(vitems.shape, va.shape);
6427        assert_eq!(vitems.shape, CollectionShape::KnownSize(2));
6428    }
6429
6430    #[test]
6431    fn if_branches_join() {
6432        let ea: Arena<Expr> = Arena::new();
6433        let sa: Arena<Stmt> = Arena::new();
6434        let mut it = Interner::new();
6435        let x = it.intern("x");
6436        let n = it.intern("n"); // unbound → dynamic condition
6437        // Let mutable x be 0. If n > 5: Set x to 10. Otherwise: Set x to 20.
6438        let zero = lit_num(&ea, 0);
6439        let nref = ident(&ea, n);
6440        let five = lit_num(&ea, 5);
6441        let cond = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Gt, left: nref, right: five });
6442        let ten = lit_num(&ea, 10);
6443        let twenty = lit_num(&ea, 20);
6444        let then_block: &[Stmt] = sa.alloc_slice(vec![Stmt::Set { target: x, value: ten }]);
6445        let else_block: &[Stmt] = sa.alloc_slice(vec![Stmt::Set { target: x, value: twenty }]);
6446        let stmts = vec![
6447            let_stmt(x, zero),
6448            Stmt::If { cond, then_block, else_block: Some(else_block) },
6449        ];
6450        let (_out, st) = rich_abstract_interp_stmts(stmts, &ea, &sa);
6451        let vx = st.value_of(x);
6452        // x is 10 or 20 after the join → interval [10, 20], type Int
6453        assert_eq!(vx.ty, TypeAbstraction::Concrete(TypeTag::Int));
6454        assert_eq!(vx.interval.lo, Bound::Finite(10));
6455        assert_eq!(vx.interval.hi, Bound::Finite(20));
6456    }
6457}