Skip to main content

logicaffeine_compile/vm/
compiler.rs

1//! AST → bytecode compiler.
2//!
3//! Layout: Main's bytecode is emitted first (entry pc 0) and ends with `Halt`;
4//! each function body is appended after it and reached via `Call` (an absolute
5//! jump to its `entry_pc`). Function names are registered in a first pass so
6//! forward references and (mutual) recursion resolve.
7//!
8//! Locals are assigned registers as they are first bound; expression temporaries
9//! get fresh registers above them, so a call's argument block never overlaps a
10//! live caller local.
11
12use std::collections::HashMap;
13
14use crate::ast::stmt::{BinaryOpKind, Expr, Literal, Stmt};
15use crate::intern::{Interner, Symbol};
16
17use super::instruction::{
18    BoundaryType, CompiledFunction, CompiledProgram, ConstIdx, Constant, FuncIdx, Op, Reg, StructTypeDef,
19};
20use super::{MAX_EXPR_DEPTH, MAX_REGISTERS_PER_FRAME};
21
22/// Whether the W24 constant-divisor magic-reciprocal lowering is enabled.
23/// Default ON (it is bit-identical to `Op::Div`/`Op::Mod` for the proven
24/// non-negative dividend it gates on, validated by the corpus differential).
25/// `LOGOS_MAGIC_DIV=0` is the kill-switch — also the A/B handle for measuring
26/// the lever's effect on a quiet box. Read once.
27pub fn magic_div_enabled() -> bool {
28    crate::optimize::active_config().is_on(crate::optimization::Opt::FastDiv)
29}
30
31/// Narrow a `new Seq of Int` whose elements provably fit `i32` into half-width
32/// (`Vec<i32>`) VM storage — the interpreter analogue of the AOT `Vec<i64>`→
33/// `Vec<i32>` narrowing, halving the array footprint and cache pressure. The JIT
34/// region path is i32-array aware (the hot region still tiers — 0 bails). PROMOTED
35/// 2026-06-21: default ON (kill-switch LOGOS_NARROW_VM=0) — graph_bfs -27.6% and
36/// array_fill -16.7% on the faithful interleaved A/B, all 33 benchmarks
37/// bit-identical, full suite 10341/10341 green, no regression.
38///
39/// Read fresh on each body-compile (not memoized) so a process can compile both
40/// regimes — the A/B differential harness and the in-process tests rely on this.
41/// The cost is one env lookup per function body, never per op.
42pub fn narrow_vm_enabled() -> bool {
43    crate::optimize::active_config().is_on(crate::optimization::Opt::NarrowVm)
44}
45
46/// Precompute the unsigned magic-reciprocal constants `(magic, more)` for a
47/// constant divisor `c` (`c != 0`), reusing the single canonical
48/// [`logicaffeine_data::LogosDivU64`] generator — the same algorithm the AOT
49/// `compile_to_rust` path emits, exhaustively proven against hardware `/`/`%`.
50/// The `more` byte encoding (low 6 bits = shift, `0x40` = the 65-bit add-marker
51/// path, `0x80` = the pure-shift power-of-two path) is exactly what
52/// [`magic_eval`] consumes.
53pub fn magic_u64_gen(c: u64) -> (u64, u8) {
54    logicaffeine_data::LogosDivU64::new(c).parts()
55}
56
57/// Evaluate the magic reciprocal: `x / c` when `mul_back == 0`, else `x % c`
58/// (`mul_back == c`). `x` is the dividend reinterpreted as `u64` — sound because
59/// the op is emitted ONLY for a proven non-negative `x`, where the i64 bit
60/// pattern equals the mathematical value and the unsigned quotient/remainder
61/// equal the signed truncating ones. The remainder is `x - q*c` computed in
62/// wrapping i64 (the low 64 bits agree with the u64 difference), bit-exact with
63/// the kernel's `wrapping_rem` for non-negative `x`.
64pub fn magic_eval(x: i64, magic: u64, more: u8, mul_back: i64) -> i64 {
65    const SHIFT_MASK: u8 = 0x3F;
66    const ADD_MARKER: u8 = 0x40;
67    const SHIFT_PATH: u8 = 0x80;
68    let n = x as u64;
69    let q = if more & SHIFT_PATH != 0 {
70        n >> (more & SHIFT_MASK)
71    } else {
72        let hi = (((magic as u128) * (n as u128)) >> 64) as u64;
73        if more & ADD_MARKER != 0 {
74            let t = (n.wrapping_sub(hi) >> 1).wrapping_add(hi);
75            t >> (more & SHIFT_MASK)
76        } else {
77            hi >> (more & SHIFT_MASK)
78        }
79    };
80    if mul_back == 0 {
81        q as i64
82    } else {
83        x.wrapping_sub((q as i64).wrapping_mul(mul_back))
84    }
85}
86
87/// Constant-pool dedup key. Floats are keyed by bit pattern so that distinct
88/// NaN payloads and -0.0/0.0 stay distinct pool entries.
89#[derive(Clone, PartialEq, Eq, Hash)]
90enum ConstKey {
91    Int(i64),
92    FloatBits(u64),
93    Bool(bool),
94    Text(String),
95    Char(char),
96    Nothing,
97    Duration(i64),
98    Date(i32),
99    Moment(i64),
100    Span { months: i32, days: i32 },
101    Time(i64),
102}
103
104fn const_key(c: &Constant) -> ConstKey {
105    match c {
106        Constant::Int(n) => ConstKey::Int(*n),
107        Constant::Float(f) => ConstKey::FloatBits(f.to_bits()),
108        Constant::Bool(b) => ConstKey::Bool(*b),
109        Constant::Text(s) => ConstKey::Text(s.clone()),
110        Constant::Char(c) => ConstKey::Char(*c),
111        Constant::Nothing => ConstKey::Nothing,
112        Constant::Duration(n) => ConstKey::Duration(*n),
113        Constant::Date(d) => ConstKey::Date(*d),
114        Constant::Moment(n) => ConstKey::Moment(*n),
115        Constant::Span { months, days } => ConstKey::Span { months: *months, days: *days },
116        Constant::Time(n) => ConstKey::Time(*n),
117    }
118}
119
120/// Which i64-slot kind a DECLARED scalar type compiles to, if any.
121fn slot_kind_of_type(
122    ty: &crate::ast::stmt::TypeExpr,
123    interner: &Interner,
124) -> Option<super::native_tier::SlotKind> {
125    use super::native_tier::SlotKind;
126    use crate::ast::stmt::TypeExpr;
127    match ty {
128        // Float reaches the AST as Named (the Primitive set is Int, Nat,
129        // Text, Bool) — accept the scalar names through either variant.
130        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => match interner.resolve(*sym) {
131            "Int" | "Nat" => Some(SlotKind::Int),
132            "Float" => Some(SlotKind::Float),
133            "Bool" => Some(SlotKind::Bool),
134            _ => None,
135        },
136        TypeExpr::Refinement { base, .. } => slot_kind_of_type(base, interner),
137        _ => None,
138    }
139}
140
141/// A DECLARED parameter's native representation: scalar slot or pinned
142/// `Seq of <scalar>`.
143fn param_kind_of_type(
144    ty: &crate::ast::stmt::TypeExpr,
145    interner: &Interner,
146) -> Option<super::native_tier::ParamKind> {
147    use super::native_tier::{ParamKind, PinElem, SlotKind};
148    use crate::ast::stmt::TypeExpr;
149    if let TypeExpr::Mutable { inner } = ty {
150        return param_kind_of_type(inner, interner);
151    }
152    if let Some(k) = slot_kind_of_type(ty, interner) {
153        return Some(ParamKind::Scalar(k));
154    }
155    if let TypeExpr::Generic { base, params } = ty {
156        if interner.resolve(*base) == "Seq" && params.len() == 1 {
157            return match slot_kind_of_type(&params[0], interner) {
158                Some(SlotKind::Int) => Some(ParamKind::List(PinElem::Int)),
159                Some(SlotKind::Float) => Some(ParamKind::List(PinElem::Float)),
160                Some(SlotKind::Bool) => Some(ParamKind::List(PinElem::Bool)),
161                None => None,
162            };
163        }
164    }
165    if let TypeExpr::Refinement { base, .. } = ty {
166        return param_kind_of_type(base, interner);
167    }
168    None
169}
170
171/// Resolve a type NAME to its full [`BoundaryType`] — a scalar/temporal primitive, or (if the name
172/// is a user type) a `Struct`/`Enum` reference (`user_types[name]` = true for struct, false for
173/// enum). `None` for an unmodeled type (Map, generic param…).
174fn boundary_of_name(
175    s: Symbol,
176    user_types: &std::collections::HashMap<Symbol, bool>,
177    interner: &Interner,
178) -> Option<BoundaryType> {
179    Some(match interner.resolve(s) {
180        "Int" | "Nat" => BoundaryType::Int,
181        "Float" => BoundaryType::Float,
182        "Bool" => BoundaryType::Bool,
183        "Text" => BoundaryType::Text,
184        "Date" => BoundaryType::Date,
185        "Moment" => BoundaryType::Moment,
186        "Word32" => BoundaryType::Word32,
187        "Word64" => BoundaryType::Word64,
188        name @ ("Duration" | "Time" | "Span" | "Rational" | "Complex" | "Decimal" | "Modular"
189        | "Money" | "Quantity" | "Uuid") => BoundaryType::Builtin(name.to_string()),
190        other => match user_types.get(&s) {
191            Some(true) => BoundaryType::Struct(other.to_string()),
192            Some(false) => BoundaryType::Enum(other.to_string()),
193            None => return None,
194        },
195    })
196}
197
198/// Resolve a parameter's AST [`TypeExpr`] to its [`BoundaryType`]. `None` for an unmodeled type.
199fn boundary_of_type_expr(
200    ty: &crate::ast::stmt::TypeExpr,
201    user_types: &std::collections::HashMap<Symbol, bool>,
202    interner: &Interner,
203) -> Option<BoundaryType> {
204    use crate::ast::stmt::TypeExpr;
205    match ty {
206        TypeExpr::Primitive(s) | TypeExpr::Named(s) => boundary_of_name(*s, user_types, interner),
207        TypeExpr::Generic { base, params }
208            if matches!(interner.resolve(*base), "Seq" | "List") && params.len() == 1 =>
209        {
210            boundary_of_type_expr(&params[0], user_types, interner).map(|e| BoundaryType::Seq(Box::new(e)))
211        }
212        // `Map of K to V` — resolve both the key and value element types (carried so a parameter
213        // map's `item k of m` resolves its result kind); rejected if either is unresolvable.
214        TypeExpr::Generic { base, params }
215            if matches!(interner.resolve(*base), "Map" | "HashMap") && params.len() == 2 =>
216        {
217            let k = boundary_of_type_expr(&params[0], user_types, interner)?;
218            let v = boundary_of_type_expr(&params[1], user_types, interner)?;
219            Some(BoundaryType::Map(Box::new(k), Box::new(v)))
220        }
221        // A HOMOGENEOUS tuple (`Pair of Int and Int`, `Triple of Int and Int and Int`) lays out
222        // identically to a `Seq` of the shared element kind, so it resolves through the same seq path
223        // (`item N of t` = seq index). A heterogeneous tuple has no single AOT element kind and stays
224        // rejected — exactly as a heterogeneous tuple LITERAL is rejected at lowering.
225        TypeExpr::Generic { base, params } if matches!(interner.resolve(*base), "Pair" | "Triple") => {
226            let elems: Vec<BoundaryType> =
227                params.iter().filter_map(|p| boundary_of_type_expr(p, user_types, interner)).collect();
228            if elems.len() != params.len() {
229                None
230            } else if elems.windows(2).all(|w| w[0] == w[1]) {
231                Some(BoundaryType::Seq(Box::new(elems[0].clone())))
232            } else {
233                Some(BoundaryType::Tuple(elems))
234            }
235        }
236        TypeExpr::Refinement { base, .. } => boundary_of_type_expr(base, user_types, interner),
237        TypeExpr::Mutable { inner } => boundary_of_type_expr(inner, user_types, interner),
238        _ => None,
239    }
240}
241
242/// Resolve a struct FIELD's registry [`FieldType`] to its [`BoundaryType`] (the registry, unlike the
243/// lossy `struct_defs` map, preserves a `Seq of <elem>` element).
244fn boundary_of_field_type(
245    ft: &crate::analysis::registry::FieldType,
246    user_types: &std::collections::HashMap<Symbol, bool>,
247    interner: &Interner,
248) -> Option<BoundaryType> {
249    use crate::analysis::registry::FieldType;
250    match ft {
251        FieldType::Primitive(s) | FieldType::Named(s) => boundary_of_name(*s, user_types, interner),
252        FieldType::Generic { base, params }
253            if matches!(interner.resolve(*base), "Seq" | "List") && params.len() == 1 =>
254        {
255            boundary_of_field_type(&params[0], user_types, interner).map(|e| BoundaryType::Seq(Box::new(e)))
256        }
257        // A `Map of K to V` field — resolve both element types, so a struct carrying a map can be a
258        // parameter and `item k of s's m` resolves the value kind cross-region. A `SharedMap from K to V`
259        // (single-replica ORMap CRDT) lays out as its underlying Map, so it resolves the same way.
260        FieldType::Generic { base, params }
261            if matches!(interner.resolve(*base), "Map" | "HashMap" | "SharedMap") && params.len() == 2 =>
262        {
263            let k = boundary_of_field_type(&params[0], user_types, interner)?;
264            let v = boundary_of_field_type(&params[1], user_types, interner)?;
265            Some(BoundaryType::Map(Box::new(k), Box::new(v)))
266        }
267        // A HOMOGENEOUS tuple field (`Pair of Int and Int`, `Triple of …`) lays out identically to a
268        // `Seq` of the shared element kind, so it resolves through the seq path (`item N of s's t` =
269        // seq index). A heterogeneous tuple field has no single AOT element kind and stays rejected.
270        FieldType::Generic { base, params } if matches!(interner.resolve(*base), "Pair" | "Triple") => {
271            let elems: Vec<BoundaryType> =
272                params.iter().filter_map(|p| boundary_of_field_type(p, user_types, interner)).collect();
273            if elems.len() != params.len() {
274                None
275            } else if elems.windows(2).all(|w| w[0] == w[1]) {
276                Some(BoundaryType::Seq(Box::new(elems[0].clone())))
277            } else {
278                Some(BoundaryType::Tuple(elems))
279            }
280        }
281        FieldType::TypeParam(_) | FieldType::Generic { .. } => None,
282    }
283}
284
285/// Resolve the static [`BoundaryType`] of a `Let`-binding's VALUE expression — for a Main top-level
286/// binding promoted to a global, so a closure capturing it can be typed with the composite's shape
287/// (the capture analog of a parameter's declared type). Handles the constructor forms whose type is
288/// statically evident (`new Struct`, `new Map of K to V`, `a new EnumVariant`); `None` otherwise (the
289/// global stays Kind-only — fine for self-describing values like a `Seq` and for scalars).
290fn boundary_of_value_expr(
291    e: &crate::ast::stmt::Expr,
292    user_types: &std::collections::HashMap<Symbol, bool>,
293    interner: &Interner,
294) -> Option<BoundaryType> {
295    use crate::ast::stmt::{Expr, Literal};
296    match e {
297        Expr::New { type_name, type_args, .. } => {
298            if matches!(interner.resolve(*type_name), "Map" | "HashMap") && type_args.len() == 2 {
299                let k = boundary_of_type_expr(&type_args[0], user_types, interner)?;
300                let v = boundary_of_type_expr(&type_args[1], user_types, interner)?;
301                Some(BoundaryType::Map(Box::new(k), Box::new(v)))
302            } else {
303                boundary_of_name(*type_name, user_types, interner)
304            }
305        }
306        Expr::NewVariant { enum_name, .. } => boundary_of_name(*enum_name, user_types, interner),
307        // A scalar literal — also resolves a tuple-literal's elements (below).
308        Expr::Literal(Literal::Number(_)) => Some(BoundaryType::Int),
309        Expr::Literal(Literal::Float(_)) => Some(BoundaryType::Float),
310        Expr::Literal(Literal::Boolean(_)) => Some(BoundaryType::Bool),
311        Expr::Literal(Literal::Text(_)) => Some(BoundaryType::Text),
312        // A tuple literal — every element's type must resolve. A homogeneous tuple lays out like a
313        // `Seq` (self-describing, no shape needed); a heterogeneous one carries its per-position kinds.
314        Expr::Tuple(elems) if !elems.is_empty() => {
315            let bts: Vec<BoundaryType> =
316                elems.iter().filter_map(|el| boundary_of_value_expr(el, user_types, interner)).collect();
317            if bts.len() != elems.len() {
318                None
319            } else if bts.windows(2).all(|w| w[0] == w[1]) {
320                Some(BoundaryType::Seq(Box::new(bts[0].clone())))
321            } else {
322                Some(BoundaryType::Tuple(bts))
323            }
324        }
325        _ => None,
326    }
327}
328
329pub struct Compiler<'i> {
330    interner: &'i Interner,
331    code: Vec<Op>,
332    constants: Vec<Constant>,
333    const_map: HashMap<ConstKey, ConstIdx>,
334    functions: Vec<CompiledFunction>,
335    fn_index: HashMap<Symbol, FuncIdx>,
336    // Struct definitions: field (name, declared-type, is_public) per struct —
337    // from `Stmt::StructDef` and the discovery-pass type registry. Drives
338    // default-fill on construction, like the tree-walker's struct_defs.
339    struct_defs: HashMap<Symbol, Vec<(Symbol, Symbol, bool)>>,
340    // Every user type's name → is-it-a-struct (true) / enum (false), built once before statement
341    // compilation. Lets a CLOSURE resolve a struct/enum PARAMETER's declared type (the closure path
342    // has no other access to the type registry the top-level function path uses).
343    user_types: HashMap<Symbol, bool>,
344    // Lexical block scopes for the frame currently being compiled (Main or one
345    // function body). scopes[0] is the frame root; If/While/Repeat bodies push
346    // a child scope whose registers are recycled on exit — mirroring the
347    // tree-walker's execute_block undo-scope.
348    scopes: Vec<HashMap<Symbol, Reg>>,
349    next_reg: Reg,
350    /// DEBUG-ONLY: when set (the debugger's compile path), capture source variable
351    /// names so the debug drawer can show `x` rather than `R0`. Off in production.
352    debug_names: bool,
353    /// DEBUG-ONLY: (register, name-symbol) bound in the CURRENT frame; cleared per
354    /// frame in `begin_scope`, resolved into `CompiledProgram::reg_names` for Main.
355    dbg_names: Vec<(Reg, Symbol)>,
356    /// Registers bound to a NAME (Let targets, params, loop variables,
357    /// captures) in any frame compiled so far. Everything else is a
358    /// statement-local scratch — dead at every statement boundary by the
359    /// allocator's recycling discipline. Snapshotted for Main into
360    /// `CompiledProgram::named_regs`, where the region JIT uses it to skip
361    /// write-back (and pre-state preservation) for scratch slots.
362    named: Vec<bool>,
363    /// Loop heads (back-edge target pcs) currently being compiled, outermost
364    /// first. Every `mark_named` while this is non-empty records the register as
365    /// a LOOP-LOCAL of each enclosing loop — bound inside a loop body, so it is
366    /// lexically dead the instant the loop exits (the scope is popped).
367    loop_stack: Vec<usize>,
368    /// `loop_locals[head]` = the registers bound inside the loop whose region
369    /// head is `head` (absolute pc). The region JIT subtracts these from the
370    /// write-back set: a slot dead at the loop exit must not be written back,
371    /// which is what lets copy-prop/CSE/fusion treat it as true scratch. Keyed
372    /// by absolute pc (Main and every function share one code array), valued by
373    /// the OWNING frame's register indices.
374    loop_locals: std::collections::HashMap<usize, Vec<bool>>,
375    /// High-water mark of `next_reg` for the current frame — block scopes
376    /// recycle registers, so the frame's true size is the maximum ever
377    /// reached, not the final `next_reg`.
378    max_reg: Reg,
379    expr_depth: usize,
380    // Enclosing control-flow contexts (innermost last). Loops collect Break
381    // jumps; Zones (and Concurrent/Parallel tasks) SWALLOW Break/Return — the
382    // tree-walker's execute path discards their ControlFlow.
383    flow_stack: Vec<FlowCtx>,
384    // Whether we are compiling a function body (false ⇒ Main, where a
385    // top-level Break/Return halts the program).
386    in_function: bool,
387    /// Self-tail-call optimization context for the function currently being
388    /// compiled: `(name, entry_pc, param_count)`. A self-call in TAIL position
389    /// (`Return f(args)`, or a `Set/Let x = f(args)` immediately followed by
390    /// `Return x`) is lowered to "reassign params 0..param_count, jump to
391    /// entry_pc" instead of Call+Return — turning self-tail-recursion into a
392    /// loop (constant stack, no per-call frame round-trip). `None` outside any
393    /// function and for non-self / non-arity-matching calls.
394    tail_fn: Option<(Symbol, usize, u16)>,
395    // Promoted globals: Main TOP-LEVEL Let names referenced from function or
396    // closure bodies, with their slot in the runtime globals table.
397    promoted: HashMap<Symbol, u16>,
398    // When compiling a closure body: per promoted capture, its (value, flag)
399    // frame registers — a captured-global access reads the snapshot when the
400    // flag is set and the LIVE global otherwise (tree-walker fall-through).
401    closure_ctx: Option<HashMap<Symbol, (Reg, Reg)>>,
402    /// Oracle range-analysis facts (M9) for the program being compiled.
403    /// Present only on the live run path; when set, an `Index` whose
404    /// `index_provably_in_bounds` holds is emitted as `IndexUnchecked`
405    /// (bounds-check elimination, V8/LLVM-style). Keyed on the AST `Expr`
406    /// pointers of THESE stmts, so the facts must be analyzed on the exact
407    /// snapshot being compiled.
408    oracle: Option<crate::optimize::OracleFacts>,
409    /// Per-enclosing-loop sets of arrays whose region-entry `RegionBoundsGuard`
410    /// was successfully emitted. A speculative access elides ONLY when its
411    /// array is in some enclosing frame — the compiler's half of the
412    /// elision ⟺ guard ⟺ VM-check invariant.
413    hoist_enabled: Vec<std::collections::HashSet<Symbol>>,
414    /// `new Seq of Int` declarations in the body CURRENTLY being compiled whose
415    /// every element provably fits `i32` (the `codegen::narrow` proof). Their
416    /// empty-constructor lowers to [`Op::NewEmptyListI32`] (half-width storage),
417    /// observably identical to the i64 form. Populated per-body only when
418    /// `LOGOS_NARROW_VM=1`; empty otherwise (default OFF).
419    narrowable_seqs: std::collections::HashSet<Symbol>,
420    /// Inferred MUTABLE-BORROW params per function: `fn → set of param indices` that
421    /// are mutated in place and returned (the AOT's `&mut [T]` set, computed by the
422    /// SAME eligibility). Their frame registers are copy-on-write-exempt so the
423    /// callee's element writes stay in place; each call site gets an `EnsureOwned`
424    /// barrier so an aliasing caller still observes value semantics. Only populated
425    /// when the type registry is available (`compile_with_types`).
426    mut_borrow_params: HashMap<Symbol, std::collections::HashSet<usize>>,
427    /// Per function, the consume-alias LOCAL of each mutable-borrow param (`Let
428    /// result be arr`): its register must ALSO be COW-exempt (the in-place writes
429    /// land on the alias, not the param). Recorded during body compile.
430    mut_borrow_alias_syms: HashMap<Symbol, std::collections::HashSet<Symbol>>,
431    /// The alias symbols of the function CURRENTLY being compiled (from
432    /// `mut_borrow_alias_syms`); a `let_reg` for one of these appends its register to
433    /// `current_exempt_regs`.
434    current_exempt_syms: std::collections::HashSet<Symbol>,
435    /// Registers bound to `current_exempt_syms` in the current function body — merged
436    /// into `CompiledFunction::mutable_param_regs` after the body compiles.
437    current_exempt_regs: Vec<Reg>,
438}
439
440/// How a name resolves at a point in compilation.
441enum NameRef {
442    Local(Reg),
443    /// A closure capture whose name is also a promoted global: snapshot when
444    /// present, live global otherwise.
445    CaptureOrGlobal { value: Reg, flag: Reg, global: u16 },
446    Global(u16),
447    Unbound,
448}
449
450enum FlowCtx {
451    Loop {
452        breaks: Vec<usize>,
453        /// Repeat loops own a live iterator that must be IterPop'd when a
454        /// Return jumps out across them into a Zone.
455        is_repeat: bool,
456    },
457    Zone {
458        exits: Vec<usize>,
459    },
460}
461
462impl<'i> Compiler<'i> {
463    /// Compile a statement block to a runnable program.
464    pub fn compile(stmts: &[Stmt], interner: &'i Interner) -> Result<CompiledProgram, String> {
465        Self::compile_with_types(stmts, interner, None)
466    }
467
468    /// Compile with the discovery-pass type registry (struct definitions that
469    /// never appear as `Stmt::StructDef`).
470    pub fn compile_with_types(
471        stmts: &[Stmt],
472        interner: &'i Interner,
473        types: Option<&crate::analysis::TypeRegistry>,
474    ) -> Result<CompiledProgram, String> {
475        Self::compile_with_oracle(stmts, interner, types, None)
476    }
477
478    /// Like `compile_with_types`, plus the Oracle's range-analysis facts
479    /// (M9) for bounds-check elimination. The live run path computes these
480    /// on the exact (optimized) snapshot it then compiles, so an `Index`
481    /// proven in-bounds becomes `IndexUnchecked`. `None` ⇒ every index
482    /// stays checked (the codegen / test paths).
483    pub fn compile_with_oracle(
484        stmts: &[Stmt],
485        interner: &'i Interner,
486        types: Option<&crate::analysis::TypeRegistry>,
487        oracle: Option<crate::optimize::OracleFacts>,
488    ) -> Result<CompiledProgram, String> {
489        Self::compile_inner(stmts, interner, types, oracle, false)
490    }
491
492    /// Like `compile_with_types` but also captures source variable names into
493    /// `CompiledProgram::reg_names` for the Studio debugger. Used only by the
494    /// debugger's compile path; production callers pay nothing for it.
495    pub fn compile_for_debug(
496        stmts: &[Stmt],
497        interner: &'i Interner,
498        types: Option<&crate::analysis::TypeRegistry>,
499    ) -> Result<CompiledProgram, String> {
500        Self::compile_inner(stmts, interner, types, None, true)
501    }
502
503    fn compile_inner(
504        stmts: &[Stmt],
505        interner: &'i Interner,
506        types: Option<&crate::analysis::TypeRegistry>,
507        oracle: Option<crate::optimize::OracleFacts>,
508        debug_names: bool,
509    ) -> Result<CompiledProgram, String> {
510        let mut c = Compiler {
511            interner,
512            debug_names,
513            dbg_names: Vec::new(),
514            code: Vec::new(),
515            constants: Vec::new(),
516            const_map: HashMap::new(),
517            functions: Vec::new(),
518            fn_index: HashMap::new(),
519            struct_defs: HashMap::new(),
520            user_types: HashMap::new(),
521            scopes: vec![HashMap::new()],
522            next_reg: 0,
523            named: Vec::new(),
524            loop_stack: Vec::new(),
525            loop_locals: std::collections::HashMap::new(),
526            max_reg: 0,
527            expr_depth: 0,
528            flow_stack: Vec::new(),
529            tail_fn: None,
530            hoist_enabled: Vec::new(),
531            in_function: false,
532            promoted: HashMap::new(),
533            closure_ctx: None,
534            oracle,
535            narrowable_seqs: std::collections::HashSet::new(),
536            mut_borrow_params: HashMap::new(),
537            mut_borrow_alias_syms: HashMap::new(),
538            current_exempt_syms: std::collections::HashSet::new(),
539            current_exempt_regs: Vec::new(),
540        };
541
542        // Every user type's name → is-it-a-struct (true) or an enum (false) — so a field or
543        // parameter referencing a user type resolves to `BoundaryType::Struct`/`Enum`. Collected
544        // from both the registry and the pass-1 statements (a type can be defined either way).
545        let mut user_types: std::collections::HashMap<Symbol, bool> = std::collections::HashMap::new();
546        for s in stmts {
547            if let Stmt::StructDef { name, .. } = s {
548                user_types.insert(*name, true);
549            }
550        }
551        // Enum types are not a top-level `Stmt` — they come from the type registry (below).
552        // The bytecode's static struct registry (name → resolved field layout). Built from the type
553        // registry, which (unlike the lossy `struct_defs` map below) preserves `Seq` field elements.
554        let mut struct_types: Vec<StructTypeDef> = Vec::new();
555        // The sum-type companion: name → per-variant payload layout, for typing a `When V (binds)`
556        // extraction on an enum PARAMETER (whose construction isn't in scope).
557        let mut enum_types: Vec<crate::vm::instruction::EnumTypeDef> = Vec::new();
558
559        // Struct definitions from the type registry (mirrors the tree-walker's
560        // with_type_registry) and from StructDef statements (pass 1 below).
561        if let Some(registry) = types {
562            use crate::analysis::registry::{FieldType, TypeDef};
563            for (name_sym, type_def) in registry.iter_types() {
564                match type_def {
565                    TypeDef::Struct { .. } => {
566                        user_types.insert(*name_sym, true);
567                    }
568                    TypeDef::Enum { .. } => {
569                        user_types.insert(*name_sym, false);
570                    }
571                    _ => {}
572                }
573            }
574            for (name_sym, type_def) in registry.iter_types() {
575                if let TypeDef::Struct { fields, .. } = type_def {
576                    let field_defs: Vec<(Symbol, Symbol, bool)> = fields
577                        .iter()
578                        .map(|f| {
579                            let type_sym = match &f.ty {
580                                FieldType::Primitive(s)
581                                | FieldType::Named(s)
582                                | FieldType::TypeParam(s) => *s,
583                                FieldType::Generic { base, .. } => *base,
584                            };
585                            (f.name, type_sym, f.is_public)
586                        })
587                        .collect();
588                    c.struct_defs.insert(*name_sym, field_defs);
589                    // Resolve the FULL field layout; carry the struct only if EVERY field resolves
590                    // (a partial layout would mis-place slots), so an unmodeled field type (Map, …)
591                    // simply leaves a parameter of that struct type rejected — never miscompiled.
592                    let resolved: Option<Vec<(String, BoundaryType)>> = fields
593                        .iter()
594                        .map(|f| {
595                            boundary_of_field_type(&f.ty, &user_types, interner)
596                                .map(|t| (interner.resolve(f.name).to_string(), t))
597                        })
598                        .collect();
599                    if let Some(field_layout) = resolved {
600                        struct_types.push(StructTypeDef {
601                            name: interner.resolve(*name_sym).to_string(),
602                            fields: field_layout,
603                        });
604                    }
605                }
606            }
607            // Enum variant payload layouts. Carry the enum only if EVERY variant's EVERY field
608            // resolves (a partial layout could mis-type a `BindArm`), so an unmodeled payload type
609            // simply leaves a parameter of that enum's payload extraction rejected — never miscompiled.
610            for (name_sym, type_def) in registry.iter_types() {
611                if let TypeDef::Enum { variants, .. } = type_def {
612                    let resolved: Option<Vec<crate::vm::instruction::EnumVariantDef>> = variants
613                        .iter()
614                        .map(|v| {
615                            let field_types: Option<Vec<BoundaryType>> = v
616                                .fields
617                                .iter()
618                                .map(|f| boundary_of_field_type(&f.ty, &user_types, interner))
619                                .collect();
620                            field_types.map(|ft| crate::vm::instruction::EnumVariantDef {
621                                name: interner.resolve(v.name).to_string(),
622                                field_types: ft,
623                            })
624                        })
625                        .collect();
626                    if let Some(variant_layouts) = resolved {
627                        enum_types.push(crate::vm::instruction::EnumTypeDef {
628                            name: interner.resolve(*name_sym).to_string(),
629                            variants: variant_layouts,
630                        });
631                    }
632                }
633            }
634        }
635        // Expose the fully-built user-type map so `compile_closure` can resolve a struct/enum
636        // closure PARAMETER's declared type (it runs deep in statement compilation, below).
637        c.user_types = user_types.clone();
638
639        // Inferred mutable-borrow params (value-semantics COW exemption): compute the
640        // SAME set the AOT uses (`codegen/program.rs`) so the eager VM mutates a
641        // consumed-and-returned Seq param in place instead of deep-cloning on every
642        // `SetIndex` (the heap_sort/quicksort O(n²) collapse). Gated on the type
643        // registry being available AND value semantics being on (else the exemption
644        // is irrelevant). See [`Op::EnsureOwned`] for the call-site soundness barrier.
645        if let Some(registry) = types {
646            if crate::semantics::collections::value_semantics_enabled() {
647                use crate::analysis::readonly::{detect_consume_alias, MutableBorrowParams};
648                use crate::codegen::detection::is_vec_type_expr;
649                use crate::codegen::program::body_contains_escape;
650                use crate::codegen::tce::is_tail_recursive;
651                use crate::optimization::Opt;
652                use crate::optimize::active_config;
653                use crate::tail_call::detect_accumulator_pattern;
654                let cg = crate::analysis::callgraph::CallGraph::build(stmts, interner);
655                let type_env = crate::analysis::TypeEnv::infer_program(stmts, interner, registry);
656                let mb = MutableBorrowParams::analyze(stmts, &cg, &type_env);
657                for s in stmts {
658                    if let Stmt::FunctionDef {
659                        name, params, body, is_native, is_exported, opt_flags, ..
660                    } = s
661                    {
662                        if *is_native || *is_exported {
663                            continue;
664                        }
665                        if !active_config().merged(opt_flags).is_on(Opt::Borrow) {
666                            continue;
667                        }
668                        if is_tail_recursive(*name, body)
669                            || detect_accumulator_pattern(*name, body).is_some()
670                            || body_contains_escape(body)
671                        {
672                            continue;
673                        }
674                        let mut idxs = std::collections::HashSet::new();
675                        let mut aliases = std::collections::HashSet::new();
676                        for (i, (psym, pty)) in params.iter().enumerate() {
677                            if mb.is_mutable_borrow(*name, *psym) && is_vec_type_expr(pty, interner) {
678                                idxs.insert(i);
679                                if let Some(alias) = detect_consume_alias(body, *psym) {
680                                    aliases.insert(alias);
681                                }
682                            }
683                        }
684                        if !idxs.is_empty() {
685                            c.mut_borrow_params.insert(*name, idxs);
686                            c.mut_borrow_alias_syms.insert(*name, aliases);
687                        }
688                    }
689                }
690            }
691        }
692
693        // Pass 1: register every function name → index (entry_pc filled later)
694        // and every struct definition.
695        for s in stmts {
696            if let Stmt::StructDef { name, fields, .. } = s {
697                c.struct_defs.insert(*name, fields.clone());
698            }
699            if let Stmt::FunctionDef { name, params, return_type, .. } = s {
700                if c.fn_index.contains_key(name) {
701                    return Err(format!("vm: function '{}' defined twice", interner.resolve(*name)));
702                }
703                let idx = c.functions.len() as FuncIdx;
704                c.fn_index.insert(*name, idx);
705                c.functions.push(CompiledFunction {
706                    name: *name,
707                    entry_pc: 0,
708                    param_count: u16::try_from(params.len())
709                        .map_err(|_| "vm: too many parameters".to_string())?,
710                    register_count: 0,
711                    captures: Vec::new(),
712                    named_regs: Vec::new(),
713                    param_kinds: params
714                        .iter()
715                        .map(|(_, ty)| param_kind_of_type(ty, interner))
716                        .collect(),
717                    ret_kind: return_type.and_then(|ty| slot_kind_of_type(ty, interner)),
718                    param_types: params
719                        .iter()
720                        .map(|(_, ty)| boundary_of_type_expr(ty, &user_types, interner))
721                        .collect(),
722                    return_type: return_type.and_then(|ty| boundary_of_type_expr(ty, &user_types, interner)),
723                    mutable_param_regs: {
724                        // `mutable` param at position `i` → frame register `i`; plus
725                        // every INFERRED mutable-borrow param (same reg == index).
726                        let inferred = c.mut_borrow_params.get(name);
727                        let mut mp: Vec<Reg> = Vec::new();
728                        for (i, (_psym, ty)) in params.iter().enumerate() {
729                            if matches!(ty, crate::ast::stmt::TypeExpr::Mutable { .. })
730                                || inferred.is_some_and(|s| s.contains(&i))
731                            {
732                                mp.push(i as Reg);
733                            }
734                        }
735                        mp
736                    },
737                });
738            }
739        }
740
741        // Pass 1.5: promote Main TOP-LEVEL Let names referenced from any
742        // function or closure body to globals (lexically visible everywhere).
743        let mut nonlocal_idents: std::collections::HashSet<Symbol> = std::collections::HashSet::new();
744        for s in stmts {
745            collect_nonlocal_idents_stmt(s, true, &mut nonlocal_idents);
746        }
747        let mut global_names: Vec<String> = Vec::new();
748        // Parallel to `global_names`: each promoted global's resolved composite type (for typing a
749        // closure capture of it); `None` for a scalar / self-describing / unmodeled value.
750        let mut global_types: Vec<Option<BoundaryType>> = Vec::new();
751        for s in stmts {
752            if let Stmt::Let { var, value, .. } = s {
753                if nonlocal_idents.contains(var) && !c.promoted.contains_key(var) {
754                    let idx = u16::try_from(c.promoted.len())
755                        .map_err(|_| "vm: too many globals".to_string())?;
756                    c.promoted.insert(*var, idx);
757                    global_names.push(interner.resolve(*var).to_string());
758                    global_types.push(boundary_of_value_expr(value, &user_types, interner));
759                }
760            }
761        }
762
763        // Pass 2a: compile Main (every non-FunctionDef top-level statement).
764        // Behind `LOGOS_NARROW_VM`: certify which top-level `new Seq of Int`
765        // declarations fit `i32` (the AOT narrowing proof), so their empty
766        // constructor lowers to the half-width `NewEmptyListI32`. The proof's
767        // `walk`/`escapes` ignore `FunctionDef` statements (none of the write
768        // forms it tracks), and the runtime widen-on-overflow is the ultimate
769        // safety net, so feeding the full statement list is sound.
770        if narrow_vm_enabled() {
771            c.narrowable_seqs = crate::codegen::narrow::narrowable_seqs_for_body(stmts, interner);
772            if !c.narrowable_seqs.is_empty() {
773                crate::optimize::mark_fired(crate::optimization::Opt::NarrowVm);
774            }
775        }
776        c.begin_scope();
777        for s in stmts {
778            if !matches!(s, Stmt::FunctionDef { .. }) {
779                c.compile_stmt(s)?;
780            }
781        }
782        c.narrowable_seqs.clear();
783        c.emit(Op::Halt);
784        let main_regs = c.max_reg as usize;
785        let mut named_regs = c.named.clone();
786        named_regs.resize(main_regs, false);
787        // Snapshot Main's variable names NOW — the per-function `begin_scope` below
788        // clears `dbg_names`. Empty unless the debugger enabled name capture.
789        let reg_names: Vec<(u16, String)> = c
790            .dbg_names
791            .iter()
792            .map(|(r, sym)| (*r, c.interner.resolve(*sym).to_string()))
793            .collect();
794
795        // Pass 2b: compile each function body, recording its entry point.
796        for s in stmts {
797            if let Stmt::FunctionDef { name, params, body, .. } = s {
798                let idx = c.fn_index[name];
799                let entry_pc = c.code.len();
800                c.begin_scope();
801                c.in_function = true;
802                c.named.clear();
803                for (i, (psym, _ty)) in params.iter().enumerate() {
804                    c.scopes.last_mut().unwrap().insert(*psym, i as Reg);
805                    c.mark_named(i as Reg);
806                }
807                c.next_reg = params.len() as Reg;
808                // Parameters occupy registers 0..n even when the body
809                // allocates no temps — register_count must cover them or
810                // the native frame's limit slot aliases a parameter.
811                c.max_reg = c.max_reg.max(c.next_reg);
812                debug_assert!(params.len() <= MAX_REGISTERS_PER_FRAME);
813                c.tail_fn = Some((*name, entry_pc, params.len() as u16));
814                // COW-exempt the consume-alias locals of this function's mutable-borrow
815                // params: `let_reg` appends each one's register as the body compiles.
816                c.current_exempt_syms = c.mut_borrow_alias_syms.get(name).cloned().unwrap_or_default();
817                c.current_exempt_regs.clear();
818                let body_slice: &[Stmt] = body;
819                let mut bk = 0;
820                while bk < body_slice.len() {
821                    // TAIL-CALL PAIR: `Set/Let x = self(args)` immediately
822                    // followed by `Return x` is a self-tail-call (x is returned,
823                    // never read after) — lower it to the loop-back instead of
824                    // call+return. A direct `Return self(args)` is handled in the
825                    // Return arm of compile_stmt (a Return is inherently tail).
826                    if bk + 1 < body_slice.len() {
827                        if let Some(args) = crate::tail_call::tail_pair_args(
828                            &body_slice[bk],
829                            &body_slice[bk + 1],
830                            *name,
831                            params.len(),
832                        ) {
833                            c.emit_tail_call(args, entry_pc, params.len() as u16)?;
834                            bk += 2;
835                            continue;
836                        }
837                    }
838                    c.compile_stmt(&body_slice[bk])?;
839                    bk += 1;
840                }
841                c.tail_fn = None;
842                // Fall off the end → return nothing.
843                c.emit(Op::ReturnNothing);
844                // Merge the consume-alias exempt registers discovered while compiling
845                // the body into this function's COW-exempt set.
846                let exempt = std::mem::take(&mut c.current_exempt_regs);
847                c.current_exempt_syms.clear();
848                let f = &mut c.functions[idx as usize];
849                for r in exempt {
850                    if !f.mutable_param_regs.contains(&r) {
851                        f.mutable_param_regs.push(r);
852                    }
853                }
854                f.entry_pc = entry_pc;
855                f.register_count = c.max_reg as usize;
856                let mut fnamed = c.named.clone();
857                fnamed.resize(f.register_count, false);
858                f.named_regs = fnamed;
859            }
860        }
861
862        Ok(CompiledProgram {
863            constants: c.constants,
864            code: c.code,
865            register_count: main_regs,
866            functions: c.functions,
867            fn_index: c.fn_index,
868            globals: global_names,
869            named_regs,
870            loop_locals: c.loop_locals,
871            reg_names,
872            struct_types,
873            enum_types,
874            global_types,
875        })
876    }
877
878    fn begin_scope(&mut self) {
879        self.scopes = vec![HashMap::new()];
880        self.next_reg = 0;
881        self.max_reg = 0;
882        self.in_function = false;
883        self.dbg_names.clear();
884    }
885
886    /// Enter a child block scope (If/While/Repeat body).
887    fn enter_block(&mut self) -> Reg {
888        self.scopes.push(HashMap::new());
889        self.next_reg
890    }
891
892    /// Leave a block scope, recycling every register it allocated (its values
893    /// are dead — the tree-walker's pop_scope undoes the bindings too).
894    fn exit_block(&mut self, mark: Reg) {
895        self.scopes.pop();
896        self.next_reg = mark;
897    }
898
899    /// Resolve a name through the enclosing block scopes (innermost first).
900    fn lookup_local(&self, sym: Symbol) -> Option<Reg> {
901        self.scopes.iter().rev().find_map(|s| s.get(&sym).copied())
902    }
903
904    /// Is `sym`'s region-entry bounds guard active in some enclosing loop?
905    fn is_hoist_enabled(&self, sym: Symbol) -> bool {
906        self.hoist_enabled.iter().any(|s| s.contains(&sym))
907    }
908
909    /// Should `item index of collection` be emitted UNCHECKED? Either the
910    /// Oracle proved it statically, or it is speculatively in bounds AND its
911    /// array's region-entry guard was emitted for an enclosing loop (so the VM
912    /// verifies it before any unchecked native access runs).
913    fn index_in_bounds(&self, collection: &Expr, index: &Expr) -> bool {
914        let Some(o) = self.oracle.as_ref() else { return false };
915        if o.index_provably_in_bounds(collection, index) {
916            // A symbolic `% m` element-bound elision carries a `m >= 1`
917            // precondition the AOT discharges with a preheader `assert!`. The VM
918            // emits no such guard, so it KEEPS the runtime check (it is the safe
919            // reference, and `largo run` is not the benchmark path) rather than
920            // risk an unchecked native access when `m <= 0`.
921            return o.index_positivity_guards(index).is_empty();
922        }
923        o.index_is_speculative(index)
924            && matches!(collection, Expr::Identifier(s) if self.is_hoist_enabled(*s))
925    }
926
927    /// `x / 2^k` where the divisor is a literal power of two and the Oracle
928    /// proves the dividend is `Int` → the shift amount `k` (`1..=62`), else
929    /// `None`. Type-safety is the crux: `Op::Div` dispatches int/float at
930    /// runtime on the dividend, so lowering to a shift is sound ONLY for an
931    /// integer dividend. The literal divisor is visible here pre-hoist, so this
932    /// fires for loop-invariant divisors the JIT's in-region detector misses.
933    fn divpow2_shift(&self, left: &Expr, right: &Expr) -> Option<u32> {
934        let Expr::Literal(Literal::Number(d)) = right else { return None };
935        let d = *d;
936        if d < 2 || (d & (d - 1)) != 0 {
937            return None;
938        }
939        let k = d.trailing_zeros();
940        if !(1..=62).contains(&k) {
941            return None;
942        }
943        if self.oracle.as_ref()?.expr_scalar(left)? != crate::optimize::ScalarKind::Int {
944            return None;
945        }
946        Some(k)
947    }
948
949    /// `x % 2^k` where the divisor is a literal power of two AND the Oracle
950    /// proves the dividend is `Int` and NON-NEGATIVE → the low-bit mask
951    /// `2^k - 1`, else `None`. On a two's-complement i64 the truncated
952    /// remainder agrees with `x & (2^k - 1)` only when `x >= 0` (the sign of
953    /// `x % m` follows the dividend, so `(-1) % 8 == -1 != 7 == (-1) & 7`); the
954    /// non-negativity gate is the SOUNDNESS condition — exactly the gate the AOT
955    /// e-graph's `mod-pow2-and` rule applies. The literal divisor is visible
956    /// here pre-hoist, so this fires for loop-invariant moduli the JIT's
957    /// in-region detector misses (histogram's `% 2^31` LCG feedback). The win:
958    /// a register-form `BitAnd` (1-cycle AND, which the JIT lowers to a
959    /// `BitAnd` stencil) instead of `Op::Mod`'s idiv on BOTH the VM and JIT.
960    fn modpow2_mask(&self, left: &Expr, right: &Expr) -> Option<i64> {
961        let Expr::Literal(Literal::Number(d)) = right else { return None };
962        let d = *d;
963        if d < 2 || (d & (d - 1)) != 0 {
964            return None;
965        }
966        let k = d.trailing_zeros();
967        if !(1..=62).contains(&k) {
968            return None;
969        }
970        let oracle = self.oracle.as_ref()?;
971        if oracle.expr_scalar(left)? != crate::optimize::ScalarKind::Int {
972            return None;
973        }
974        if !oracle.expr_proven_nonneg(left) {
975            return None;
976        }
977        Some(d - 1)
978    }
979
980    /// `x / c` or `x % c` where the divisor is a literal constant `c > 0` that is
981    /// NOT a power of two (W5's `divpow2_shift`/`modpow2_mask` own powers of two)
982    /// AND the Oracle proves `x` is `Int` and NON-NEGATIVE → the precomputed
983    /// `(magic, more)` for the unsigned magic-reciprocal sequence, else `None`.
984    ///
985    /// SOUNDNESS — the unsigned magic equals the signed truncating `/`/`%` only
986    /// for a non-negative dividend: `x.wrapping_div(c)` / `x.wrapping_rem(c)`
987    /// equal `(x as u64) / c` / `(x as u64) % c` exactly when `x >= 0` and
988    /// `c > 0` (both operands non-negative, no overflow edge — `c` is a literal
989    /// `> 0` so `MIN/-1` is impossible). For `x < 0` the signs disagree, so the
990    /// non-negativity gate is the soundness condition — exactly the gate
991    /// `modpow2_mask` and the AOT `fast_div` lever apply. The literal divisor is
992    /// visible here pre-hoist, so this fires for loop-invariant moduli the JIT's
993    /// in-region detector misses (matrix's `% 1000000007`, histogram's `% 1000`).
994    fn magic_div_const(&self, left: &Expr, right: &Expr) -> Option<(u64, u8)> {
995        if !magic_div_enabled() {
996            return None;
997        }
998        let Expr::Literal(Literal::Number(d)) = right else { return None };
999        let d = *d;
1000        // `c > 0` and NOT a power of two (pow2 is W5's AND/shift territory).
1001        if d < 2 || (d & (d - 1)) == 0 {
1002            return None;
1003        }
1004        let oracle = self.oracle.as_ref()?;
1005        if oracle.expr_scalar(left)? != crate::optimize::ScalarKind::Int {
1006            return None;
1007        }
1008        if !oracle.expr_proven_nonneg(left) {
1009            return None;
1010        }
1011        crate::optimize::mark_fired(crate::optimization::Opt::FastDiv);
1012        Some(magic_u64_gen(d as u64))
1013    }
1014
1015    /// Emit `RegionBoundsGuard` for each hoist descriptor the Oracle recorded
1016    /// for the loop `stmt`, returning the set of arrays whose guard was
1017    /// emitted (only those are then eligible for speculative elision). All
1018    /// three symbols must resolve to local registers, and the loop must read
1019    /// the array's length nowhere stale — guaranteed by the Oracle's analysis.
1020    fn emit_hoist_guards(&mut self, stmt: &Stmt) -> std::collections::HashSet<Symbol> {
1021        let mut enabled = std::collections::HashSet::new();
1022        let Some(oracle) = self.oracle.as_ref() else { return enabled };
1023        let descs = oracle
1024            .hoist_descs_for(stmt as *const Stmt as usize)
1025            .to_vec();
1026        for d in descs {
1027            let (Some(array), Some(bound), Some(iv)) = (
1028                self.lookup_local(d.array),
1029                self.lookup_local(d.bound),
1030                self.lookup_local(d.iv),
1031            ) else {
1032                continue;
1033            };
1034            self.emit(Op::RegionBoundsGuard {
1035                array,
1036                bound,
1037                iv,
1038                add_max: d.add_max,
1039                add_min: d.add_min,
1040            });
1041            enabled.insert(d.array);
1042        }
1043        enabled
1044    }
1045
1046    /// Emit the runtime "Undefined variable" failure — unbound names are a
1047    /// RUNTIME error in the tree-walker, so dead branches stay free.
1048    fn emit_unbound(&mut self, sym: Symbol) -> Result<(), String> {
1049        let msg = format!("Undefined variable: {}", self.interner.resolve(sym));
1050        let idx = self.add_const(Constant::Text(msg))?;
1051        self.emit(Op::FailWith { msg: idx });
1052        Ok(())
1053    }
1054
1055    /// Emit an unconditional runtime failure with a fixed message — the
1056    /// pattern for statements the interpreter spec rejects WHEN EXECUTED.
1057    fn emit_fail(&mut self, msg: &str) -> Result<(), String> {
1058        let idx = self.add_const(Constant::Text(msg.to_string()))?;
1059        self.emit(Op::FailWith { msg: idx });
1060        Ok(())
1061    }
1062
1063    /// Resolve a name: block locals (and closure captures) shadow promoted
1064    /// globals; a promoted capture in a closure body needs the
1065    /// snapshot-or-live-global split.
1066    fn resolve_name(&self, sym: Symbol) -> NameRef {
1067        if let Some(r) = self.lookup_local(sym) {
1068            if let Some(ctx) = &self.closure_ctx {
1069                if let Some(&(value, flag)) = ctx.get(&sym) {
1070                    // Only when the local resolution IS the capture slot (a
1071                    // body-local `Let` shadows the capture entirely).
1072                    if r == value {
1073                        if let Some(&global) = self.promoted.get(&sym) {
1074                            return NameRef::CaptureOrGlobal { value, flag, global };
1075                        }
1076                    }
1077                }
1078            }
1079            NameRef::Local(r)
1080        } else if let Some(&g) = self.promoted.get(&sym) {
1081            NameRef::Global(g)
1082        } else {
1083            NameRef::Unbound
1084        }
1085    }
1086
1087    /// Emit a read of `sym` into `dst`.
1088    fn emit_read(&mut self, sym: Symbol, dst: Reg) -> Result<(), String> {
1089        match self.resolve_name(sym) {
1090            NameRef::Local(src) => {
1091                if src != dst {
1092                    self.emit(Op::Move { dst, src });
1093                }
1094                Ok(())
1095            }
1096            NameRef::CaptureOrGlobal { value, flag, global } => {
1097                let jg = self.emit_placeholder_jump_if_false(flag);
1098                self.emit(Op::Move { dst, src: value });
1099                let jend = self.emit_placeholder_jump();
1100                self.patch_jump_target(jg, self.current_pc())?;
1101                self.emit(Op::GlobalGet { dst, idx: global });
1102                self.patch_jump_target(jend, self.current_pc())?;
1103                Ok(())
1104            }
1105            NameRef::Global(idx) => {
1106                self.emit(Op::GlobalGet { dst, idx });
1107                Ok(())
1108            }
1109            NameRef::Unbound => self.emit_unbound(sym),
1110        }
1111    }
1112
1113    /// Emit a write of `R[src]` to `sym` (Set semantics: the binding must
1114    /// already exist somewhere).
1115    fn emit_write(&mut self, sym: Symbol, src: Reg) -> Result<(), String> {
1116        match self.resolve_name(sym) {
1117            NameRef::Local(dst) => {
1118                if src != dst {
1119                    self.emit(Op::Move { dst, src });
1120                }
1121                Ok(())
1122            }
1123            NameRef::CaptureOrGlobal { value, flag, global } => {
1124                let jg = self.emit_placeholder_jump_if_false(flag);
1125                self.emit(Op::Move { dst: value, src });
1126                let jend = self.emit_placeholder_jump();
1127                self.patch_jump_target(jg, self.current_pc())?;
1128                self.emit(Op::GlobalSet { idx: global, src });
1129                self.patch_jump_target(jend, self.current_pc())?;
1130                Ok(())
1131            }
1132            NameRef::Global(idx) => {
1133                self.emit(Op::GlobalSet { idx, src });
1134                Ok(())
1135            }
1136            NameRef::Unbound => self.emit_unbound(sym),
1137        }
1138    }
1139
1140    fn alloc_reg(&mut self) -> Result<Reg, String> {
1141        self.reserve_regs(1)
1142    }
1143
1144    /// Emit a self-tail-call as a LOOP-BACK (TCO): evaluate every argument into a
1145    /// FRESH temp first — so a parameter read by a later argument is never
1146    /// clobbered by an earlier parameter store — then move the temps into the
1147    /// parameter slots `0..param_count` and jump to the function entry. This
1148    /// turns self-tail-recursion into constant-stack iteration (no per-call frame
1149    /// round-trip, no `MAX_CALL_DEPTH` ceiling for tail calls).
1150    fn emit_tail_call(
1151        &mut self,
1152        args: &[&Expr],
1153        entry_pc: usize,
1154        param_count: u16,
1155    ) -> Result<(), String> {
1156        let mut temps = Vec::with_capacity(args.len());
1157        for a in args {
1158            let t = self.alloc_reg()?;
1159            self.compile_expr_into(a, t)?;
1160            temps.push(t);
1161        }
1162        for (i, t) in temps.into_iter().enumerate() {
1163            if t != i as Reg {
1164                self.emit(Op::Move { dst: i as Reg, src: t });
1165            }
1166        }
1167        self.emit(Op::Jump { target: entry_pc });
1168        Ok(())
1169    }
1170
1171    /// Reserve `n` consecutive registers, returning the first.
1172    fn reserve_regs(&mut self, n: u16) -> Result<Reg, String> {
1173        let start = self.next_reg;
1174        let next = (self.next_reg as usize) + n as usize;
1175        if next > MAX_REGISTERS_PER_FRAME {
1176            return Err(format!(
1177                "vm: out of registers (frame limit is {})",
1178                MAX_REGISTERS_PER_FRAME
1179            ));
1180        }
1181        self.next_reg = next as Reg;
1182        if self.next_reg > self.max_reg {
1183            self.max_reg = self.next_reg;
1184        }
1185        // Scratch reserved inside a loop is a per-iteration temp, dead at the
1186        // loop exit — record it as a loop-local so the region JIT treats it as
1187        // true scratch even when it recycled a (now-dead) named variable's slot
1188        // (the fannkuch `perm[hi]` transfer temp that blocked swap fusion).
1189        if !self.loop_stack.is_empty() {
1190            for r in start..(next as Reg) {
1191                self.record_loop_local(r);
1192            }
1193        }
1194        Ok(start)
1195    }
1196
1197    /// Resolve a mutation-target collection (`Push … to xs`) to its name
1198    /// reference. The caller materializes it (Rc-backed collections mutate
1199    /// through any clone) or emits the unbound failure AFTER the value's side
1200    /// effects, matching the tree-walker.
1201    fn resolve_collection(&mut self, e: &Expr) -> Result<(Symbol, NameRef), String> {
1202        match e {
1203            Expr::Identifier(sym) => Ok((*sym, self.resolve_name(*sym))),
1204            _ => Err("vm: expected an identifier collection".to_string()),
1205        }
1206    }
1207
1208    /// Materialize a resolved collection into a register (None = unbound).
1209    fn collection_reg(&mut self, sym: Symbol, nr: &NameRef) -> Result<Option<Reg>, String> {
1210        match nr {
1211            NameRef::Local(r) => Ok(Some(*r)),
1212            NameRef::Unbound => Ok(None),
1213            _ => {
1214                let scratch = self.alloc_reg()?;
1215                self.emit_read(sym, scratch)?;
1216                Ok(Some(scratch))
1217            }
1218        }
1219    }
1220
1221    /// Bind `sym` for a `Let`: reuse its register when the innermost scope
1222    /// already has it (re-Let overwrites), otherwise allocate a fresh one in
1223    /// the innermost scope (shadowing any outer binding).
1224    fn let_reg(&mut self, sym: Symbol) -> Result<Reg, String> {
1225        let r = if let Some(&r) = self.scopes.last().unwrap().get(&sym) {
1226            self.mark_named(r);
1227            r
1228        } else {
1229            let r = self.alloc_reg()?;
1230            self.scopes.last_mut().unwrap().insert(sym, r);
1231            self.mark_named(r);
1232            r
1233        };
1234        self.record_dbg_name(sym, r);
1235        // A consume-alias of a mutable-borrow param (`Let result be arr`): its
1236        // register is COW-exempt, since the in-place writes land on it, not the param.
1237        if self.current_exempt_syms.contains(&sym) && !self.current_exempt_regs.contains(&r) {
1238            self.current_exempt_regs.push(r);
1239        }
1240        Ok(r)
1241    }
1242
1243    /// DEBUG-ONLY: remember that register `r` holds source variable `sym` in the
1244    /// current frame (last name wins after block-scope register reuse). A no-op
1245    /// unless the debugger's compile path enabled name capture.
1246    fn record_dbg_name(&mut self, sym: Symbol, r: Reg) {
1247        if !self.debug_names {
1248            return;
1249        }
1250        match self.dbg_names.iter_mut().find(|(rr, _)| *rr == r) {
1251            Some(slot) => slot.1 = sym,
1252            None => self.dbg_names.push((r, sym)),
1253        }
1254    }
1255
1256    /// Record that `r` carries a user-visible name in the current frame.
1257    fn mark_named(&mut self, r: Reg) {
1258        let i = r as usize;
1259        if self.named.len() <= i {
1260            self.named.resize(i + 1, false);
1261        }
1262        self.named[i] = true;
1263        self.record_loop_local(r);
1264    }
1265
1266    /// Record register `r` as LOCAL to every enclosing loop — lexically dead at
1267    /// each loop's exit — so the region JIT drops it from the write-back set.
1268    /// Called for named bindings (`mark_named`) AND for anonymous scratch
1269    /// reserved inside a loop: a per-iteration temp is dead at the loop exit too,
1270    /// and may have recycled a now-dead named variable's slot (block-scope reuse).
1271    /// Without this, such a temp keeps that slot's stale `named` flag and the
1272    /// region JIT treats it as live — blocking fusions like the swap idiom
1273    /// (fannkuch's `perm[hi]` transfer temp).
1274    fn record_loop_local(&mut self, r: Reg) {
1275        let i = r as usize;
1276        for &head in &self.loop_stack {
1277            let m = self.loop_locals.entry(head).or_default();
1278            if m.len() <= i {
1279                m.resize(i + 1, false);
1280            }
1281            m[i] = true;
1282        }
1283    }
1284
1285    fn add_const(&mut self, c: Constant) -> Result<ConstIdx, String> {
1286        let key = const_key(&c);
1287        if let Some(&idx) = self.const_map.get(&key) {
1288            return Ok(idx);
1289        }
1290        let idx = ConstIdx::try_from(self.constants.len())
1291            .map_err(|_| "vm: constant pool overflow".to_string())?;
1292        self.constants.push(c);
1293        self.const_map.insert(key, idx);
1294        Ok(idx)
1295    }
1296
1297    fn emit(&mut self, op: Op) {
1298        self.code.push(op);
1299    }
1300
1301    fn compile_stmt(&mut self, s: &Stmt) -> Result<(), String> {
1302        match s {
1303            Stmt::Splice { body } => {
1304                // Scope-transparent desugar output: compile the statements in
1305                // order, no scope of their own (temporaries are gensym'd).
1306                for inner in body.iter() {
1307                    self.compile_stmt(inner)?;
1308                }
1309                Ok(())
1310            }
1311            Stmt::Let { var, value, .. } => {
1312                // A promoted Main TOP-LEVEL Let defines the global; everywhere
1313                // else (function bodies, Main blocks — which shadow) a
1314                // register binding.
1315                if !self.in_function && self.scopes.len() == 1 && self.promoted.contains_key(var) {
1316                    let scratch = self.alloc_reg()?;
1317                    self.compile_expr_into(value, scratch)?;
1318                    let idx = self.promoted[var];
1319                    self.emit(Op::GlobalSet { idx, src: scratch });
1320                    return Ok(());
1321                }
1322                // `Let var be a new Seq/Set/Map` (a fresh EMPTY collection):
1323                // bind first and construct directly into the var's register.
1324                // An empty constructor reads nothing, so the
1325                // evaluate-in-old-environment rule below is moot — and writing
1326                // into the var's stable register lets the loop-carried reuse
1327                // path (NewEmptyList over a sole-owned buffer) fire across
1328                // iterations instead of allocating fresh every pass.
1329                if let Expr::New { type_name, init_fields, .. } = value {
1330                    if init_fields.is_empty()
1331                        && matches!(
1332                            self.interner.resolve(*type_name),
1333                            "Seq" | "List" | "Set" | "HashSet" | "Map" | "HashMap"
1334                        )
1335                    {
1336                        let dst = self.let_reg(*var)?;
1337                        // A narrowing-proven `new Seq of Int` allocates the
1338                        // half-width (`Vec<i32>`) buffer directly into the var's
1339                        // stable register (so the loop-carried reuse path fires).
1340                        if self.narrowable_seqs.contains(var)
1341                            && matches!(self.interner.resolve(*type_name), "Seq" | "List")
1342                        {
1343                            self.emit(Op::NewEmptyListI32 { dst });
1344                        } else {
1345                            self.compile_expr_into(value, dst)?;
1346                        }
1347                        return Ok(());
1348                    }
1349                }
1350                // The value is evaluated BEFORE the new binding exists (the
1351                // tree-walker evaluates in the old environment): `Let x be
1352                // x + 1` in a block reads the OUTER x. Compile into a scratch
1353                // register first, then bind.
1354                let scratch = self.alloc_reg()?;
1355                self.compile_expr_into(value, scratch)?;
1356                let dst = self.let_reg(*var)?;
1357                self.emit(Op::Move { dst, src: scratch });
1358                Ok(())
1359            }
1360            Stmt::Set { target, value } => {
1361                match self.resolve_name(*target) {
1362                    NameRef::Local(dst) => {
1363                        // `Set x to x + e1 + … + ek`: lower to an AddAssign
1364                        // chain (in-place Text append; identical fold for
1365                        // every other type). Only when each later term
1366                        // provably never observes x — see add_assign_chain.
1367                        if let Some(terms) = add_assign_chain(*target, value) {
1368                            for term in terms {
1369                                let scratch = self.alloc_reg()?;
1370                                self.compile_expr_into(term, scratch)?;
1371                                self.emit(Op::AddAssign { dst, src: scratch });
1372                            }
1373                            return Ok(());
1374                        }
1375                        // `Set x to f(…)`: compile the call straight into `x`. A call
1376                        // writes its destination LAST (after its arguments are read into
1377                        // the arg window), so `x` is never clobbered before use — even
1378                        // in the self-consuming `Set x to f(x, …)` shape. This avoids a
1379                        // scratch whose lingering `Rc` clone would leave `x` spuriously
1380                        // shared (`strong == 2`) and force the next mutable-borrow call's
1381                        // `EnsureOwned` barrier to deep-clone every iteration (the
1382                        // heap_sort O(n²) collapse under value semantics). Skipped when
1383                        // `x` is a COW-exempt local (its writes never clone anyway) so a
1384                        // self-recursive function's tier-able body is left untouched.
1385                        if matches!(value, Expr::Call { .. }) && !self.current_exempt_syms.contains(target) {
1386                            self.compile_expr_into(value, dst)?;
1387                            return Ok(());
1388                        }
1389                        // Never compile directly into the live target: a
1390                        // multi-op value (interpolation accumulation, branch
1391                        // joins) would clobber the register before reading it
1392                        // — `Set result to "{result}{s}"` must accumulate.
1393                        let scratch = self.alloc_reg()?;
1394                        self.compile_expr_into(value, scratch)?;
1395                        self.emit(Op::Move { dst, src: scratch });
1396                        Ok(())
1397                    }
1398                    NameRef::Unbound => {
1399                        // The tree-walker evaluates the value FIRST (its side
1400                        // effects happen), then fails the assignment — and only
1401                        // when the statement actually executes.
1402                        let scratch = self.alloc_reg()?;
1403                        self.compile_expr_into(value, scratch)?;
1404                        self.emit_unbound(*target)
1405                    }
1406                    _ => {
1407                        let scratch = self.alloc_reg()?;
1408                        self.compile_expr_into(value, scratch)?;
1409                        self.emit_write(*target, scratch)
1410                    }
1411                }
1412            }
1413            Stmt::Return { value } => {
1414                // A Zone swallows Return (the tree-walker discards the zone
1415                // body's ControlFlow): jump to the zone's end instead of
1416                // returning, popping the iterator of every Repeat crossed.
1417                let zone_pos = self
1418                    .flow_stack
1419                    .iter()
1420                    .rposition(|c| matches!(c, FlowCtx::Zone { .. }));
1421                if let Some(pos) = zone_pos {
1422                    if let Some(e) = value {
1423                        let scratch = self.alloc_reg()?;
1424                        self.compile_expr_into(e, scratch)?;
1425                    }
1426                    let crossed_repeats = self.flow_stack[pos + 1..]
1427                        .iter()
1428                        .filter(|c| matches!(c, FlowCtx::Loop { is_repeat: true, .. }))
1429                        .count();
1430                    for _ in 0..crossed_repeats {
1431                        self.emit(Op::IterPop);
1432                    }
1433                    let j = self.code.len();
1434                    self.emit(Op::Jump { target: usize::MAX });
1435                    if let FlowCtx::Zone { exits } = &mut self.flow_stack[pos] {
1436                        exits.push(j);
1437                    }
1438                } else if self.in_function {
1439                    match value {
1440                        Some(e) => {
1441                            // TCO: a direct self-tail-call `Return self(args)` is
1442                            // inherently in tail position — lower it to the
1443                            // loop-back (reassign params + jump to entry) instead
1444                            // of a real call + return.
1445                            if let Some((tf, entry_pc, pc)) = self.tail_fn {
1446                                // Do NOT TCO across a `Repeat` (for-in): the
1447                                // loop-back jump would skip its `IterPop`,
1448                                // stacking abandoned iterators. (A `While`
1449                                // holds no runtime state, so jumping out of it
1450                                // is fine; `Zone`s are handled above.)
1451                                let in_repeat = self.flow_stack.iter().any(|c| {
1452                                    matches!(c, FlowCtx::Loop { is_repeat: true, .. })
1453                                });
1454                                if !in_repeat {
1455                                    if let Some(args) =
1456                                        crate::tail_call::direct_self_tail_args(*e, tf, pc as usize)
1457                                    {
1458                                        self.emit_tail_call(args, entry_pc, pc)?;
1459                                        return Ok(());
1460                                    }
1461                                }
1462                            }
1463                            let src = self.compile_expr(e)?;
1464                            self.emit(Op::Return { src });
1465                        }
1466                        None => self.emit(Op::ReturnNothing),
1467                    }
1468                } else {
1469                    // Return at Main top level = stop the program (the value's
1470                    // side effects still happen first).
1471                    if let Some(e) = value {
1472                        let scratch = self.alloc_reg()?;
1473                        self.compile_expr_into(e, scratch)?;
1474                    }
1475                    self.emit(Op::Halt);
1476                }
1477                Ok(())
1478            }
1479            Stmt::Break => {
1480                // The innermost Loop or Zone catches it, whichever is nearer.
1481                match self.flow_stack.last_mut() {
1482                    Some(FlowCtx::Loop { breaks, .. }) => {
1483                        let j = self.code.len();
1484                        breaks.push(j);
1485                        self.emit(Op::Jump { target: usize::MAX });
1486                    }
1487                    Some(FlowCtx::Zone { .. }) => {
1488                        let j = self.code.len();
1489                        self.emit(Op::Jump { target: usize::MAX });
1490                        if let Some(FlowCtx::Zone { exits }) = self.flow_stack.last_mut() {
1491                            exits.push(j);
1492                        }
1493                    }
1494                    None => {
1495                        if self.in_function {
1496                            // A Break with no enclosing loop ends the function
1497                            // body (the tree-walker's call loop treats Break
1498                            // as "stop executing the body").
1499                            self.emit(Op::ReturnNothing);
1500                        } else {
1501                            // At Main top level it stops the program.
1502                            self.emit(Op::Halt);
1503                        }
1504                    }
1505                }
1506                Ok(())
1507            }
1508            Stmt::Call { function, args } => {
1509                // A call used for its effect; the result register is discarded.
1510                let dst = self.alloc_reg()?;
1511                self.compile_call(*function, args, dst)
1512            }
1513            Stmt::Push { value, collection } => match collection {
1514                Expr::Identifier(_) => {
1515                    let (sym, nr) = self.resolve_collection(collection)?;
1516                    match self.collection_reg(sym, &nr)? {
1517                        Some(list) => {
1518                            let val = self.compile_expr(value)?;
1519                            self.emit(Op::ListPush { list, value: val });
1520                            // A non-local name reads into a scratch register, so
1521                            // the (copy-on-write) mutation must be written back
1522                            // to the capture/global slot — a no-op for Rc-shared
1523                            // collections (same allocation).
1524                            if !matches!(nr, NameRef::Local(_)) {
1525                                self.emit_write(sym, list)?;
1526                            }
1527                            Ok(())
1528                        }
1529                        None => {
1530                            let scratch = self.alloc_reg()?;
1531                            self.compile_expr_into(value, scratch)?;
1532                            self.emit_unbound(sym)
1533                        }
1534                    }
1535                }
1536                Expr::FieldAccess { object: Expr::Identifier(obj_sym), field } => {
1537                    // The value is evaluated BEFORE the object lookup.
1538                    let val = self.compile_expr(value)?;
1539                    let obj = self.alloc_reg()?;
1540                    self.emit_read(*obj_sym, obj)?;
1541                    let name = self.interner.resolve(*field).to_string();
1542                    let fidx = self.add_const(Constant::Text(name))?;
1543                    self.emit(Op::ListPushField { obj, field: fidx, src: val });
1544                    Ok(())
1545                }
1546                Expr::FieldAccess { .. } => {
1547                    let scratch = self.alloc_reg()?;
1548                    self.compile_expr_into(value, scratch)?;
1549                    self.emit_fail("Push to nested field access not supported")
1550                }
1551                _ => {
1552                    // Any place expression is an l-value: `Push 5 to item i of
1553                    // grid`. The value is evaluated BEFORE the collection,
1554                    // matching the tree-walker; the evaluated handle is
1555                    // Rc-shared, so `ListPush` mutates the collection in place
1556                    // (same model as the generalized `Add` below).
1557                    let val = self.compile_expr(value)?;
1558                    let list = self.compile_expr(collection)?;
1559                    self.emit(Op::ListPush { list, value: val });
1560                    Ok(())
1561                }
1562            },
1563            Stmt::Add { value, collection } => {
1564                if !matches!(collection, Expr::Identifier(_)) {
1565                    // A field-access (or other) collection — `Add "Alice" to p's guests`.
1566                    // Compile it to a register: a collection/CRDT field reads as a shallow
1567                    // `Rc`, so the in-place `SetAdd` mutates the value stored in the struct.
1568                    // Mirrors the tree-walker's generalized `Add` (value evaluated first).
1569                    let val = self.compile_expr(value)?;
1570                    let set = self.compile_expr(collection)?;
1571                    self.emit(Op::SetAdd { set, value: val });
1572                    return Ok(());
1573                }
1574                let (sym, nr) = self.resolve_collection(collection)?;
1575                match self.collection_reg(sym, &nr)? {
1576                    Some(set) => {
1577                        let val = self.compile_expr(value)?;
1578                        self.emit(Op::SetAdd { set, value: val });
1579                        if !matches!(nr, NameRef::Local(_)) {
1580                            self.emit_write(sym, set)?;
1581                        }
1582                        Ok(())
1583                    }
1584                    None => {
1585                        let scratch = self.alloc_reg()?;
1586                        self.compile_expr_into(value, scratch)?;
1587                        self.emit_unbound(sym)
1588                    }
1589                }
1590            }
1591            Stmt::Remove { value, collection } => {
1592                if !matches!(collection, Expr::Identifier(_)) {
1593                    // Field-access collection — mirrors the generalized `Add` above.
1594                    let val = self.compile_expr(value)?;
1595                    let coll = self.compile_expr(collection)?;
1596                    self.emit(Op::RemoveFrom { collection: coll, value: val });
1597                    return Ok(());
1598                }
1599                let (sym, nr) = self.resolve_collection(collection)?;
1600                match self.collection_reg(sym, &nr)? {
1601                    Some(coll) => {
1602                        let val = self.compile_expr(value)?;
1603                        self.emit(Op::RemoveFrom { collection: coll, value: val });
1604                        if !matches!(nr, NameRef::Local(_)) {
1605                            self.emit_write(sym, coll)?;
1606                        }
1607                        Ok(())
1608                    }
1609                    None => {
1610                        let scratch = self.alloc_reg()?;
1611                        self.compile_expr_into(value, scratch)?;
1612                        self.emit_unbound(sym)
1613                    }
1614                }
1615            }
1616            Stmt::SetIndex { collection, index, value } => {
1617                if !matches!(collection, Expr::Identifier(_)) {
1618                    // Any place expression is an l-value: `Set item j of
1619                    // (item i of grid) to v`. Tree-walker order — index,
1620                    // value, then the collection handle; the handle is
1621                    // Rc-shared, so `SetIndex` writes the collection in place.
1622                    let idx = self.compile_expr(index)?;
1623                    let val = self.compile_expr(value)?;
1624                    let coll = self.compile_expr(collection)?;
1625                    self.emit(Op::SetIndex { collection: coll, index: idx, value: val });
1626                    return Ok(());
1627                }
1628                let proven = self.index_in_bounds(collection, index);
1629                let (sym, nr) = self.resolve_collection(collection)?;
1630                match self.collection_reg(sym, &nr)? {
1631                    Some(coll) => {
1632                        let idx = self.compile_expr(index)?;
1633                        let val = self.compile_expr(value)?;
1634                        if proven {
1635                            self.emit(Op::SetIndexUnchecked { collection: coll, index: idx, value: val });
1636                        } else {
1637                            self.emit(Op::SetIndex { collection: coll, index: idx, value: val });
1638                        }
1639                        // Structs are VALUE types: the struct-field form of
1640                        // SetIndex rewrites the register, so a promoted/global
1641                        // or captured name needs the write-back (a no-op for
1642                        // Rc-shared collections — same allocation).
1643                        if !matches!(nr, NameRef::Local(_)) {
1644                            self.emit_write(sym, coll)?;
1645                        }
1646                        Ok(())
1647                    }
1648                    None => {
1649                        // Tree-walker order: index, then value, then lookup.
1650                        let s1 = self.alloc_reg()?;
1651                        self.compile_expr_into(index, s1)?;
1652                        let s2 = self.alloc_reg()?;
1653                        self.compile_expr_into(value, s2)?;
1654                        self.emit_unbound(sym)
1655                    }
1656                }
1657            }
1658            Stmt::Show { object, recipient } => {
1659                if let Expr::Identifier(sym) = recipient {
1660                    self.compile_recipient_call(*sym, object)
1661                } else {
1662                    // Non-identifier recipient: the tree-walker still evaluates
1663                    // the object (side effects), then silently moves on.
1664                    let scratch = self.alloc_reg()?;
1665                    self.compile_expr_into(object, scratch)?;
1666                    Ok(())
1667                }
1668            }
1669            Stmt::Give { object, recipient } => {
1670                if let Expr::Identifier(sym) = recipient {
1671                    self.compile_recipient_call(*sym, object)
1672                } else {
1673                    // Non-identifier recipient: the tree-walker evaluates the
1674                    // object (side effects) and silently moves on.
1675                    let scratch = self.alloc_reg()?;
1676                    self.compile_expr_into(object, scratch)?;
1677                    Ok(())
1678                }
1679            }
1680            Stmt::If { cond, then_block, else_block } => {
1681                let c = self.compile_expr(cond)?;
1682                let jif = self.emit_placeholder_jump_if_false(c);
1683                let mark = self.enter_block();
1684                for st in *then_block {
1685                    self.compile_stmt(st)?;
1686                }
1687                self.exit_block(mark);
1688                match else_block {
1689                    Some(eb) => {
1690                        let jend = self.emit_placeholder_jump();
1691                        self.patch_jump_target(jif, self.current_pc())?;
1692                        let mark = self.enter_block();
1693                        for st in *eb {
1694                            self.compile_stmt(st)?;
1695                        }
1696                        self.exit_block(mark);
1697                        self.patch_jump_target(jend, self.current_pc())?;
1698                    }
1699                    None => {
1700                        self.patch_jump_target(jif, self.current_pc())?;
1701                    }
1702                }
1703                Ok(())
1704            }
1705            Stmt::While { cond, body, .. } => {
1706                // Region-entry bounds hoists go at the loop head (the back-edge
1707                // target), so they fall inside any region formed for the loop.
1708                let loop_start = self.current_pc();
1709                // Track names bound inside this loop body (the region head is
1710                // `loop_start`, the back-edge target) so the JIT can treat them
1711                // as dead-at-exit scratch. Pushed before the cond (re-evaluated
1712                // each iteration, so cond-bound names are loop-local too).
1713                self.loop_stack.push(loop_start);
1714                let enabled = self.emit_hoist_guards(s);
1715                self.hoist_enabled.push(enabled);
1716                let c = self.compile_expr(cond)?;
1717                let jexit = self.emit_placeholder_jump_if_false(c);
1718                self.flow_stack.push(FlowCtx::Loop { breaks: Vec::new(), is_repeat: false });
1719                let mark = self.enter_block();
1720                for st in *body {
1721                    self.compile_stmt(st)?;
1722                }
1723                self.exit_block(mark);
1724                self.hoist_enabled.pop();
1725                self.loop_stack.pop();
1726                self.emit(Op::Jump { target: loop_start });
1727                let exit_pc = self.current_pc();
1728                self.patch_jump_target(jexit, exit_pc)?;
1729                if let Some(FlowCtx::Loop { breaks, .. }) = self.flow_stack.pop() {
1730                    for j in breaks {
1731                        self.patch_jump_target(j, exit_pc)?;
1732                    }
1733                }
1734                Ok(())
1735            }
1736            Stmt::Repeat { pattern, iterable, body } => {
1737                self.compile_repeat(pattern, iterable, body)
1738            }
1739            Stmt::Pop { collection, into } => {
1740                if !matches!(collection, Expr::Identifier(_)) {
1741                    return self.emit_fail("Pop collection must be an identifier");
1742                }
1743                let (sym, nr) = self.resolve_collection(collection)?;
1744                match self.collection_reg(sym, &nr)? {
1745                    Some(list) => {
1746                        let dst = match into {
1747                            Some(var) => self.let_reg(*var)?,
1748                            None => self.alloc_reg()?,
1749                        };
1750                        self.emit(Op::ListPop { list, dst });
1751                        if !matches!(nr, NameRef::Local(_)) {
1752                            self.emit_write(sym, list)?;
1753                        }
1754                        Ok(())
1755                    }
1756                    None => self.emit_unbound(sym),
1757                }
1758            }
1759            Stmt::RuntimeAssert { condition, .. } => {
1760                let c = self.compile_expr(condition)?;
1761                let jok = self.code.len();
1762                self.emit(Op::JumpIfTrue { cond: c, target: usize::MAX });
1763                let idx = self.add_const(Constant::Text("Assertion failed".to_string()))?;
1764                self.emit(Op::FailWith { msg: idx });
1765                self.patch_jump_target(jok, self.current_pc())?;
1766                Ok(())
1767            }
1768            Stmt::Zone { name, body, .. } => self.compile_zone(*name, body),
1769            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
1770                // Sequential in the interpreter spec. Each task runs WITHOUT a
1771                // block scope (its Lets persist), and its Break/Return is
1772                // swallowed — the tree-walker discards each task's ControlFlow.
1773                for task in tasks.iter() {
1774                    self.flow_stack.push(FlowCtx::Zone { exits: Vec::new() });
1775                    self.compile_stmt(task)?;
1776                    let end_pc = self.current_pc();
1777                    if let Some(FlowCtx::Zone { exits }) = self.flow_stack.pop() {
1778                        for j in exits {
1779                            self.patch_jump_target(j, end_pc)?;
1780                        }
1781                    }
1782                }
1783                Ok(())
1784            }
1785            Stmt::Sleep { milliseconds } => {
1786                let duration = self.compile_expr(milliseconds)?;
1787                self.emit(Op::Sleep { duration });
1788                Ok(())
1789            }
1790            // Verification-only / declaration statements: no runtime effect.
1791            Stmt::Assert { .. } | Stmt::Trust { .. } | Stmt::Require { .. }
1792            | Stmt::Theorem(_) | Stmt::Definition(_) | Stmt::Axiom(_) | Stmt::Theory(_) => {
1793                Ok(())
1794            }
1795            // Definitions were registered in pass 1.
1796            Stmt::StructDef { .. } => Ok(()),
1797            Stmt::SetField { object, field, value } => {
1798                let field_name = self.interner.resolve(*field).to_string();
1799                let fidx = self.add_const(Constant::Text(field_name))?;
1800                if let Expr::Identifier(obj_sym) = object {
1801                    // Tree-walker order: value first, then the target lookup.
1802                    match self.resolve_name(*obj_sym) {
1803                        NameRef::Local(obj) => {
1804                            let v = self.compile_expr(value)?;
1805                            self.emit(Op::StructInsert { obj, field: fidx, value: v });
1806                            Ok(())
1807                        }
1808                        NameRef::Unbound => {
1809                            let scratch = self.alloc_reg()?;
1810                            self.compile_expr_into(value, scratch)?;
1811                            self.emit_unbound(*obj_sym)
1812                        }
1813                        _ => {
1814                            // Structs are VALUES: load the global's copy,
1815                            // mutate it, store it back.
1816                            let v = self.compile_expr(value)?;
1817                            let obj = self.alloc_reg()?;
1818                            self.emit_read(*obj_sym, obj)?;
1819                            self.emit(Op::StructInsert { obj, field: fidx, value: v });
1820                            self.emit_write(*obj_sym, obj)
1821                        }
1822                    }
1823                } else {
1824                    let scratch = self.alloc_reg()?;
1825                    self.compile_expr_into(value, scratch)?;
1826                    let idx = self
1827                        .add_const(Constant::Text("SetField target must be an identifier".to_string()))?;
1828                    self.emit(Op::FailWith { msg: idx });
1829                    Ok(())
1830                }
1831            }
1832            Stmt::Inspect { target, arms, .. } => self.compile_inspect(target, arms),
1833            Stmt::IncreaseCrdt { object, field, amount }
1834            | Stmt::DecreaseCrdt { object, field, amount } => {
1835                let negate = matches!(s, Stmt::DecreaseCrdt { .. });
1836                let amt = self.compile_expr(amount)?;
1837                let fname = self.interner.resolve(*field).to_string();
1838                let fidx = self.add_const(Constant::Text(fname))?;
1839                if let Expr::Identifier(obj_sym) = object {
1840                    match self.resolve_name(*obj_sym) {
1841                        NameRef::Local(obj) => {
1842                            self.emit(Op::CrdtBump { obj, field: fidx, amount: amt, negate });
1843                            Ok(())
1844                        }
1845                        NameRef::Unbound => self.emit_unbound(*obj_sym),
1846                        _ => {
1847                            let obj = self.alloc_reg()?;
1848                            self.emit_read(*obj_sym, obj)?;
1849                            self.emit(Op::CrdtBump { obj, field: fidx, amount: amt, negate });
1850                            self.emit_write(*obj_sym, obj)
1851                        }
1852                    }
1853                } else {
1854                    let msg = if negate {
1855                        "DecreaseCrdt target must be an identifier"
1856                    } else {
1857                        "IncreaseCrdt target must be an identifier"
1858                    };
1859                    let idx = self.add_const(Constant::Text(msg.to_string()))?;
1860                    self.emit(Op::FailWith { msg: idx });
1861                    Ok(())
1862                }
1863            }
1864            Stmt::MergeCrdt { source, target } => {
1865                let src = self.compile_expr(source)?;
1866                if let Expr::Identifier(target_sym) = target {
1867                    match self.resolve_name(*target_sym) {
1868                        NameRef::Local(tgt) => {
1869                            self.emit(Op::CrdtMerge { target: tgt, source: src });
1870                            Ok(())
1871                        }
1872                        NameRef::Unbound => self.emit_unbound(*target_sym),
1873                        _ => {
1874                            let tgt = self.alloc_reg()?;
1875                            self.emit_read(*target_sym, tgt)?;
1876                            self.emit(Op::CrdtMerge { target: tgt, source: src });
1877                            self.emit_write(*target_sym, tgt)
1878                        }
1879                    }
1880                } else {
1881                    let idx = self
1882                        .add_const(Constant::Text("Merge target must be an identifier".to_string()))?;
1883                    self.emit(Op::FailWith { msg: idx });
1884                    Ok(())
1885                }
1886            }
1887            Stmt::ReadFrom { var, source } => {
1888                use crate::ast::stmt::ReadSource;
1889                match source {
1890                    ReadSource::Console => {
1891                        // Console reads yield empty Text in the interpreter.
1892                        let dst = self.let_reg(*var)?;
1893                        let idx = self.add_const(Constant::Text(String::new()))?;
1894                        self.emit(Op::LoadConst { dst, idx });
1895                        Ok(())
1896                    }
1897                    ReadSource::File(path_expr) => {
1898                        // The VM has no VFS (yet): the path's side effects run,
1899                        // then the tree-walker's exact no-VFS error.
1900                        let scratch = self.alloc_reg()?;
1901                        self.compile_expr_into(path_expr, scratch)?;
1902                        let idx = self.add_const(Constant::Text(
1903                            "VFS not initialized. Use Interpreter::with_vfs()".to_string(),
1904                        ))?;
1905                        self.emit(Op::FailWith { msg: idx });
1906                        Ok(())
1907                    }
1908                }
1909            }
1910            Stmt::WriteFile { content, path } => {
1911                let s1 = self.alloc_reg()?;
1912                self.compile_expr_into(content, s1)?;
1913                let s2 = self.alloc_reg()?;
1914                self.compile_expr_into(path, s2)?;
1915                let idx = self.add_const(Constant::Text(
1916                    "VFS not initialized. Use Interpreter::with_vfs()".to_string(),
1917                ))?;
1918                self.emit(Op::FailWith { msg: idx });
1919                Ok(())
1920            }
1921            Stmt::Mount { path, .. } => {
1922                let scratch = self.alloc_reg()?;
1923                self.compile_expr_into(path, scratch)?;
1924                let idx = self.add_const(Constant::Text(
1925                    "VFS not initialized. Use Interpreter::with_vfs()".to_string(),
1926                ))?;
1927                self.emit(Op::FailWith { msg: idx });
1928                Ok(())
1929            }
1930            Stmt::Spawn { name, .. } => {
1931                // Agents do not run in the interpreter: the handle is Nothing.
1932                let dst = self.let_reg(*name)?;
1933                let idx = self.add_const(Constant::Nothing)?;
1934                self.emit(Op::LoadConst { dst, idx });
1935                Ok(())
1936            }
1937            // Peer messaging: emit the networking opcodes the async VM net driver
1938            // (`run_vm_net_async`) services through the shared `NetInbox` — the same inbox the
1939            // tree-walker uses, so a `Send`/`Await` is byte-identical on both tiers. (The dial
1940            // knobs — compression/cached/redundant/view — are not yet threaded here; the default
1941            // self-describing encode still decodes identically, so output parity holds.)
1942            Stmt::SendMessage { message, destination, .. } => {
1943                let to = self.compile_expr(destination)?;
1944                let msg = self.compile_expr(message)?;
1945                self.emit(Op::NetSend { to, msg });
1946                Ok(())
1947            }
1948            Stmt::StreamMessage { values, destination } => {
1949                let to = self.compile_expr(destination)?;
1950                let vals = self.compile_expr(values)?;
1951                self.emit(Op::NetStream { to, values: vals });
1952                Ok(())
1953            }
1954            Stmt::AwaitMessage { source, into, stream, .. } => {
1955                let from = self.compile_expr(source)?;
1956                let dst = self.let_reg(*into)?;
1957                self.emit(Op::NetAwait { dst, from, stream: *stream });
1958                Ok(())
1959            }
1960            Stmt::Check { subject, predicate, is_capability, object, source_text, .. } => {
1961                let subj = self.alloc_reg()?;
1962                self.emit_read(*subject, subj)?;
1963                // The object is only looked up for capability checks.
1964                let obj = if *is_capability {
1965                    match object {
1966                        Some(obj_sym) => {
1967                            let r = self.alloc_reg()?;
1968                            self.emit_read(*obj_sym, r)?;
1969                            r
1970                        }
1971                        None => Reg::MAX,
1972                    }
1973                } else {
1974                    Reg::MAX
1975                };
1976                let st = self.add_const(Constant::Text(source_text.clone()))?;
1977                self.emit(Op::CheckPolicy {
1978                    subject: subj,
1979                    predicate: *predicate,
1980                    is_capability: *is_capability,
1981                    object: obj,
1982                    source_text: st,
1983                });
1984                Ok(())
1985            }
1986            Stmt::AppendToSequence { sequence, value } => {
1987                // The RGA is always behind a shared `Rc` (whether a bare variable or a
1988                // struct field), so appending in place propagates — no write-back needed.
1989                // Value compiled first, matching the tree-walker's evaluation order.
1990                let val = self.compile_expr(value)?;
1991                let seq = self.compile_expr(sequence)?;
1992                self.emit(Op::CrdtAppend { seq, value: val });
1993                Ok(())
1994            }
1995            Stmt::ResolveConflict { object, field, value } => {
1996                let val = self.compile_expr(value)?;
1997                let fname = self.interner.resolve(*field).to_string();
1998                let fidx = self.add_const(Constant::Text(fname))?;
1999                // Mirrors `IncreaseCrdt`: a register field resolves in place via its shared
2000                // `Rc`; a plain field is overwritten, so it needs the read/write-back path
2001                // (structs have value semantics).
2002                if let Expr::Identifier(obj_sym) = object {
2003                    match self.resolve_name(*obj_sym) {
2004                        NameRef::Local(obj) => {
2005                            self.emit(Op::CrdtResolve { obj, field: fidx, value: val });
2006                            Ok(())
2007                        }
2008                        NameRef::Unbound => self.emit_unbound(*obj_sym),
2009                        _ => {
2010                            let obj = self.alloc_reg()?;
2011                            self.emit_read(*obj_sym, obj)?;
2012                            self.emit(Op::CrdtResolve { obj, field: fidx, value: val });
2013                            self.emit_write(*obj_sym, obj)
2014                        }
2015                    }
2016                } else {
2017                    let idx = self.add_const(Constant::Text(
2018                        "Resolve target must be a struct field".to_string(),
2019                    ))?;
2020                    self.emit(Op::FailWith { msg: idx });
2021                    Ok(())
2022                }
2023            }
2024            // Relay networking opcodes — serviced by the async VM net driver (`run_vm_net_async`)
2025            // through the shared `NetInbox`, byte-identically to the tree-walker. `LetPeerAgent`
2026            // and `Sync` are not yet driven on the VM net runner (they stay async-tier below).
2027            Stmt::Listen { address, secure } => {
2028                if secure.is_some() {
2029                    return Err("the PNP one-time pad (`with pad`) is only supported by the interpreter tier, not the bytecode VM".to_string());
2030                }
2031                let topic = self.compile_expr(address)?;
2032                self.emit(Op::NetListen { topic });
2033                Ok(())
2034            }
2035            Stmt::ConnectTo { address, secure } => {
2036                if secure.is_some() {
2037                    return Err("the PNP one-time pad (`with pad`) is only supported by the interpreter tier, not the bytecode VM".to_string());
2038                }
2039                let url = self.compile_expr(address)?;
2040                self.emit(Op::NetConnect { url });
2041                Ok(())
2042            }
2043            Stmt::LetPeerAgent { var, address } => {
2044                let addr = self.compile_expr(address)?;
2045                let dst = self.let_reg(*var)?;
2046                self.emit(Op::NetMakePeer { dst, addr });
2047                Ok(())
2048            }
2049            Stmt::Sync { var, topic } => {
2050                // Read the local counter, sync+merge over the relay, write the merged value back —
2051                // a read-modify-write on `var` through `Op::NetSync`.
2052                let topic_reg = self.compile_expr(topic)?;
2053                let tmp = self.alloc_reg()?;
2054                self.emit_read(*var, tmp)?;
2055                self.emit(Op::NetSync { dst: tmp, topic: topic_reg });
2056                self.emit_write(*var, tmp)
2057            }
2058            // Go-like concurrency (T11d). The VM emits the scheduler opcodes; a
2059            // concurrent program is then driven through `run_until_block` by a
2060            // `VmTask`. (Concurrent programs currently route to the tree-walker
2061            // before the VM, so these are exercised via the dedicated VM-scheduler
2062            // entry; the default routing is unchanged.)
2063            Stmt::CreatePipe { var, element_type, capacity } => {
2064                let dst = self.let_reg(*var)?;
2065                let cap = capacity.map(|c| c as i32).unwrap_or(-1);
2066                let elem = crate::vm::instruction::ChanElem::from_type_name(self.interner.resolve(*element_type));
2067                self.emit(Op::ChanNew { dst, cap, elem });
2068                Ok(())
2069            }
2070            Stmt::SendPipe { value, pipe } => {
2071                let val = self.compile_expr(value)?;
2072                let chan = self.compile_expr(pipe)?;
2073                self.emit(Op::ChanSend { chan, val });
2074                Ok(())
2075            }
2076            Stmt::ReceivePipe { var, pipe } => {
2077                let chan = self.compile_expr(pipe)?;
2078                let dst = self.let_reg(*var)?;
2079                self.emit(Op::ChanRecv { dst, chan });
2080                Ok(())
2081            }
2082            Stmt::TrySendPipe { value, pipe, result } => {
2083                let val = self.compile_expr(value)?;
2084                let chan = self.compile_expr(pipe)?;
2085                let dst = match result {
2086                    Some(sym) => self.let_reg(*sym)?,
2087                    None => self.alloc_reg()?,
2088                };
2089                self.emit(Op::ChanTrySend { dst, chan, val });
2090                Ok(())
2091            }
2092            Stmt::TryReceivePipe { var, pipe } => {
2093                let chan = self.compile_expr(pipe)?;
2094                let dst = self.let_reg(*var)?;
2095                self.emit(Op::ChanTryRecv { dst, chan });
2096                Ok(())
2097            }
2098            Stmt::LaunchTask { function, args } => self.compile_spawn(*function, args, None),
2099            Stmt::LaunchTaskWithHandle { handle, function, args } => {
2100                self.compile_spawn(*function, args, Some(*handle))
2101            }
2102            Stmt::StopTask { handle } => {
2103                let h = self.compile_expr(handle)?;
2104                self.emit(Op::TaskAbort { handle: h });
2105                Ok(())
2106            }
2107            Stmt::Select { branches } => {
2108                use crate::ast::stmt::SelectBranch;
2109                // Register each arm. A recv arm allocates and binds its var
2110                // register up front, so the winning branch body reads the value
2111                // `deliver_select` writes there.
2112                for branch in branches.iter() {
2113                    match branch {
2114                        SelectBranch::Receive { var, pipe, .. } => {
2115                            let chan = self.compile_expr(pipe)?;
2116                            let var_reg = self.let_reg(*var)?;
2117                            self.emit(Op::SelectArmRecv { chan, var: var_reg });
2118                        }
2119                        SelectBranch::Timeout { milliseconds, .. } => {
2120                            let ticks = self.compile_expr(milliseconds)?;
2121                            self.emit(Op::SelectArmTimeout { ticks });
2122                        }
2123                    }
2124                }
2125                let dst_arm = self.alloc_reg()?;
2126                self.emit(Op::SelectWait { dst_arm });
2127                // Dispatch on the winning arm index: run that branch's body.
2128                let mut end_jumps = Vec::new();
2129                for (i, branch) in branches.iter().enumerate() {
2130                    let lit = self.alloc_reg()?;
2131                    let idx = self.add_const(Constant::Int(i as i64))?;
2132                    self.emit(Op::LoadConst { dst: lit, idx });
2133                    let cmp = self.alloc_reg()?;
2134                    self.emit(Op::Eq { dst: cmp, lhs: dst_arm, rhs: lit });
2135                    let jskip = self.emit_placeholder_jump_if_false(cmp);
2136                    let body = match branch {
2137                        SelectBranch::Receive { body, .. } | SelectBranch::Timeout { body, .. } => {
2138                            *body
2139                        }
2140                    };
2141                    let mark = self.enter_block();
2142                    for st in body {
2143                        self.compile_stmt(st)?;
2144                    }
2145                    self.exit_block(mark);
2146                    let jend = self.emit_placeholder_jump();
2147                    end_jumps.push(jend);
2148                    self.patch_jump_target(jskip, self.current_pc())?;
2149                }
2150                for j in end_jumps {
2151                    self.patch_jump_target(j, self.current_pc())?;
2152                }
2153                Ok(())
2154            }
2155            Stmt::Escape { .. } => self.emit_fail(
2156                "Escape blocks contain raw Rust code and cannot be interpreted. \
2157                     Use `largo build` or `largo run` to compile and run this program.",
2158            ),
2159            Stmt::FunctionDef { .. } => {
2160                // Nested function definitions are unreachable from the parser
2161                // (## To headers are top level); statically registering one
2162                // here cannot model the tree-walker's conditional runtime
2163                // definition, so it is a compile error.
2164                Err("vm: nested function definitions are not supported".to_string())
2165            }
2166        }
2167    }
2168
2169    fn current_pc(&self) -> usize {
2170        self.code.len()
2171    }
2172
2173    fn emit_placeholder_jump(&mut self) -> usize {
2174        let idx = self.code.len();
2175        self.emit(Op::Jump { target: usize::MAX });
2176        idx
2177    }
2178
2179    fn emit_placeholder_jump_if_false(&mut self, cond: Reg) -> usize {
2180        let idx = self.code.len();
2181        self.emit(Op::JumpIfFalse { cond, target: usize::MAX });
2182        idx
2183    }
2184
2185    fn patch_jump_target(&mut self, idx: usize, target: usize) -> Result<(), String> {
2186        patch_jump(&mut self.code, idx, target)
2187    }
2188
2189    /// Compile `e`, returning the register that holds its value. An unbound
2190    /// identifier becomes a runtime failure at this point in the instruction
2191    /// stream (never a compile error — dead branches stay free).
2192    fn compile_expr(&mut self, e: &Expr) -> Result<Reg, String> {
2193        match e {
2194            Expr::Identifier(sym) => match self.interner.resolve(*sym) {
2195                "today" | "now" => {
2196                    let scratch = self.alloc_reg()?;
2197                    self.compile_expr_into(e, scratch)?;
2198                    Ok(scratch)
2199                }
2200                _ => match self.resolve_name(*sym) {
2201                    NameRef::Local(r) => Ok(r),
2202                    _ => {
2203                        let scratch = self.alloc_reg()?;
2204                        self.emit_read(*sym, scratch)?;
2205                        Ok(scratch)
2206                    }
2207                },
2208            },
2209            _ => {
2210                let dst = self.alloc_reg()?;
2211                self.compile_expr_into(e, dst)?;
2212                Ok(dst)
2213            }
2214        }
2215    }
2216
2217    /// Compile `e`, placing its value into `dst`. Depth-guarded so adversarially
2218    /// nested expressions fail with an error instead of exhausting the native
2219    /// stack.
2220    fn compile_expr_into(&mut self, e: &Expr, dst: Reg) -> Result<(), String> {
2221        self.expr_depth += 1;
2222        if self.expr_depth > MAX_EXPR_DEPTH {
2223            self.expr_depth -= 1;
2224            return Err("vm: expression too deeply nested".to_string());
2225        }
2226        let result = self.compile_expr_into_inner(e, dst);
2227        self.expr_depth -= 1;
2228        result
2229    }
2230
2231    fn compile_expr_into_inner(&mut self, e: &Expr, dst: Reg) -> Result<(), String> {
2232        match e {
2233            Expr::Literal(Literal::Text(sym)) => {
2234                let idx = self.add_const(Constant::Text(self.interner.resolve(*sym).to_string()))?;
2235                self.emit(Op::LoadConst { dst, idx });
2236                Ok(())
2237            }
2238            Expr::Literal(lit) => {
2239                let idx = self.add_const(literal_const(lit)?)?;
2240                self.emit(Op::LoadConst { dst, idx });
2241                Ok(())
2242            }
2243            Expr::Identifier(sym) => {
2244                // Temporal builtins: the NAME wins even when shadowed
2245                // (tree-walker checks before the env lookup).
2246                match self.interner.resolve(*sym) {
2247                    "today" => {
2248                        self.emit(Op::LoadToday { dst });
2249                        Ok(())
2250                    }
2251                    "now" => {
2252                        self.emit(Op::LoadNow { dst });
2253                        Ok(())
2254                    }
2255                    _ => self.emit_read(*sym, dst),
2256                }
2257            }
2258            Expr::BinaryOp { op: BinaryOpKind::And, left, right } => {
2259                self.compile_short_circuit(true, left, right, dst)
2260            }
2261            Expr::BinaryOp { op: BinaryOpKind::Or, left, right } => {
2262                self.compile_short_circuit(false, left, right, dst)
2263            }
2264            Expr::BinaryOp { op, left, right } => {
2265                // Int `x / 2^k` → single `DivPow2` op (the JIT lowers it to the
2266                // 1-op shift stencil): replaces idiv, fires for loop-invariant
2267                // divisors, no scratch-register pressure.
2268                if matches!(op, BinaryOpKind::Divide) {
2269                    if let Some(k) = self.divpow2_shift(left, right) {
2270                        let lhs = self.compile_expr(left)?;
2271                        self.emit(Op::DivPow2 { dst, lhs, k: k as u8 });
2272                        return Ok(());
2273                    }
2274                    // Proven-non-negative Int `x / c` (c a constant non-pow2) →
2275                    // the magic reciprocal (mul-high + shift) instead of idiv.
2276                    if let Some((magic, more)) = self.magic_div_const(left, right) {
2277                        let lhs = self.compile_expr(left)?;
2278                        self.emit(Op::MagicDivU { dst, lhs, magic, more, mul_back: 0 });
2279                        return Ok(());
2280                    }
2281                }
2282                // Proven-non-negative Int `x % 2^k` → `x & (2^k - 1)`, emitted
2283                // as the register-form `BitAnd`: replaces idiv with a 1-cycle
2284                // AND, which the JIT lowers to a `BitAnd` stencil. Gated on
2285                // Oracle-proven non-negativity — unsound for negative dividends.
2286                if matches!(op, BinaryOpKind::Modulo) {
2287                    if let Some(mask) = self.modpow2_mask(left, right) {
2288                        let lhs = self.compile_expr(left)?;
2289                        let rhs = self.alloc_reg()?;
2290                        let idx = self.add_const(Constant::Int(mask))?;
2291                        self.emit(Op::LoadConst { dst: rhs, idx });
2292                        self.emit(Op::BitAnd { dst, lhs, rhs });
2293                        return Ok(());
2294                    }
2295                    // Proven-non-negative Int `x % c` (c a constant non-pow2) →
2296                    // the magic reciprocal: `x - (x/c)*c` via mul-high + shift,
2297                    // instead of idiv. `mul_back == c` selects the remainder.
2298                    if let Expr::Literal(Literal::Number(c)) = right {
2299                        if let Some((magic, more)) = self.magic_div_const(left, right) {
2300                            let lhs = self.compile_expr(left)?;
2301                            self.emit(Op::MagicDivU { dst, lhs, magic, more, mul_back: *c });
2302                            return Ok(());
2303                        }
2304                    }
2305                }
2306                let lhs = self.compile_expr(left)?;
2307                let rhs = self.compile_expr(right)?;
2308                self.emit(binop_op(*op, dst, lhs, rhs)?);
2309                Ok(())
2310            }
2311            Expr::Not { operand } => {
2312                let src = self.compile_expr(operand)?;
2313                self.emit(Op::Not { dst, src });
2314                Ok(())
2315            }
2316            Expr::Call { function, args } => self.compile_call(*function, args, dst),
2317            Expr::List(items) => {
2318                let count = u16::try_from(items.len())
2319                    .map_err(|_| "vm: list literal too long (max 65535 elements)".to_string())?;
2320                let start = self.reserve_regs(count)?;
2321                for (i, item) in items.iter().enumerate() {
2322                    self.compile_expr_into(item, start + i as Reg)?;
2323                }
2324                self.emit(Op::NewList { dst, start, count });
2325                Ok(())
2326            }
2327            Expr::New { type_name, init_fields, .. } => {
2328                self.compile_new(*type_name, init_fields, dst)
2329            }
2330            Expr::FieldAccess { object, field } => {
2331                let obj = self.compile_expr(object)?;
2332                let fname = self.interner.resolve(*field).to_string();
2333                let fidx = self.add_const(Constant::Text(fname))?;
2334                self.emit(Op::GetField { dst, obj, field: fidx });
2335                Ok(())
2336            }
2337            Expr::NewVariant { enum_name, variant, fields } => {
2338                let tname = self.interner.resolve(*enum_name).to_string();
2339                let cname = self.interner.resolve(*variant).to_string();
2340                let tidx = self.add_const(Constant::Text(tname))?;
2341                let cidx = self.add_const(Constant::Text(cname))?;
2342                let count = u16::try_from(fields.len())
2343                    .map_err(|_| "vm: too many variant fields".to_string())?;
2344                let args_start = self.reserve_regs(count)?;
2345                for (i, (_, field_expr)) in fields.iter().enumerate() {
2346                    self.compile_expr_into(field_expr, args_start + i as Reg)?;
2347                }
2348                self.emit(Op::NewInductive { dst, type_name: tidx, ctor: cidx, args_start, count });
2349                Ok(())
2350            }
2351            Expr::Range { start, end } => {
2352                let s = self.compile_expr(start)?;
2353                let e = self.compile_expr(end)?;
2354                self.emit(Op::NewRange { dst, start: s, end: e });
2355                Ok(())
2356            }
2357            Expr::Length { collection } => {
2358                let c = self.compile_expr(collection)?;
2359                self.emit(Op::Length { dst, collection: c });
2360                Ok(())
2361            }
2362            Expr::Index { collection, index } => {
2363                // Bounds-check elimination: if the Oracle proved this index
2364                // in `[1, length]` (range analysis, M9), emit the unchecked
2365                // form — the JIT drops the bounds branch. Checked otherwise.
2366                let proven = self.index_in_bounds(collection, index);
2367                let c = self.compile_expr(collection)?;
2368                let i = self.compile_expr(index)?;
2369                if proven {
2370                    self.emit(Op::IndexUnchecked { dst, collection: c, index: i });
2371                } else {
2372                    self.emit(Op::Index { dst, collection: c, index: i });
2373                }
2374                Ok(())
2375            }
2376            Expr::Contains { collection, value } => {
2377                let c = self.compile_expr(collection)?;
2378                let v = self.compile_expr(value)?;
2379                self.emit(Op::Contains { dst, collection: c, value: v });
2380                Ok(())
2381            }
2382            Expr::InterpolatedString(parts) => self.compile_interpolation(parts, dst),
2383            Expr::Slice { collection, start, end } => {
2384                let c = self.compile_expr(collection)?;
2385                let st = self.compile_expr(start)?;
2386                let en = self.compile_expr(end)?;
2387                self.emit(Op::SliceOp { dst, collection: c, start: st, end: en });
2388                Ok(())
2389            }
2390            Expr::Copy { expr } => {
2391                let src = self.compile_expr(expr)?;
2392                self.emit(Op::DeepClone { dst, src });
2393                Ok(())
2394            }
2395            Expr::Give { value } => self.compile_expr_into(value, dst),
2396            Expr::Tuple(items) => {
2397                let count = u16::try_from(items.len())
2398                    .map_err(|_| "vm: tuple literal too long".to_string())?;
2399                let start = self.reserve_regs(count)?;
2400                for (i, item) in items.iter().enumerate() {
2401                    self.compile_expr_into(item, start + i as Reg)?;
2402                }
2403                self.emit(Op::NewTuple { dst, start, count });
2404                Ok(())
2405            }
2406            Expr::Union { left, right } => {
2407                let l = self.compile_expr(left)?;
2408                let r = self.compile_expr(right)?;
2409                self.emit(Op::UnionOp { dst, lhs: l, rhs: r });
2410                Ok(())
2411            }
2412            Expr::Intersection { left, right } => {
2413                let l = self.compile_expr(left)?;
2414                let r = self.compile_expr(right)?;
2415                self.emit(Op::IntersectOp { dst, lhs: l, rhs: r });
2416                Ok(())
2417            }
2418            Expr::OptionSome { value } => self.compile_expr_into(value, dst),
2419            Expr::OptionNone => {
2420                let idx = self.add_const(Constant::Nothing)?;
2421                self.emit(Op::LoadConst { dst, idx });
2422                Ok(())
2423            }
2424            Expr::WithCapacity { value, .. } => self.compile_expr_into(value, dst),
2425            Expr::ManifestOf { .. } => {
2426                self.emit(Op::NewEmptyList { dst });
2427                Ok(())
2428            }
2429            Expr::ChunkAt { .. } => {
2430                let idx = self.add_const(Constant::Nothing)?;
2431                self.emit(Op::LoadConst { dst, idx });
2432                Ok(())
2433            }
2434            Expr::Escape { .. } => {
2435                let idx = self.add_const(Constant::Text(
2436                    "Escape expressions contain raw Rust code and cannot be interpreted. \
2437                     Use `largo build` or `largo run` to compile and run this program."
2438                        .to_string(),
2439                ))?;
2440                self.emit(Op::FailWith { msg: idx });
2441                Ok(())
2442            }
2443            Expr::Closure { params, body, .. } => self.compile_closure(params, body, dst),
2444            Expr::CallExpr { callee, args } => {
2445                let c = self.compile_expr(callee)?;
2446                let arg_count =
2447                    u16::try_from(args.len()).map_err(|_| "vm: too many arguments".to_string())?;
2448                let args_start = self.reserve_regs(arg_count)?;
2449                for (i, arg) in args.iter().enumerate() {
2450                    self.compile_expr_into(arg, args_start + i as Reg)?;
2451                }
2452                self.emit(Op::CallValue {
2453                    dst,
2454                    callee: c,
2455                    args_start,
2456                    arg_count,
2457                    name_for_err: u32::MAX,
2458                });
2459                Ok(())
2460            }
2461            _ => Err("vm: unsupported expression".to_string()),
2462        }
2463    }
2464
2465    /// Compile a `Repeat` loop (extracted: keeps `compile_stmt`'s recursion
2466    /// frame small in debug builds).
2467    fn compile_repeat(
2468        &mut self,
2469        pattern: &crate::ast::stmt::Pattern,
2470        iterable: &Expr,
2471        body: &[Stmt],
2472    ) -> Result<(), String> {
2473        use crate::ast::stmt::Pattern;
2474
2475        let it_reg = self.compile_expr(iterable)?;
2476        self.emit(Op::IterPrepare { iterable: it_reg });
2477
2478        // The loop variable lives in a scope spanning the whole loop
2479        // (the tree-walker pushes ONE scope outside the iteration).
2480        let outer_mark = self.enter_block();
2481        let loop_start;
2482        let next_idx;
2483        match pattern {
2484            Pattern::Identifier(sym) => {
2485                let dst = self.let_reg(*sym)?;
2486                loop_start = self.current_pc();
2487                next_idx = self.code.len();
2488                self.emit(Op::IterNext { dst, exit: usize::MAX });
2489            }
2490            Pattern::Tuple(syms) => {
2491                let tmp = self.alloc_reg()?;
2492                let count = u16::try_from(syms.len())
2493                    .map_err(|_| "vm: tuple pattern too long".to_string())?;
2494                let start = self.reserve_regs(count)?;
2495                for (i, sym) in syms.iter().enumerate() {
2496                    self.scopes.last_mut().unwrap().insert(*sym, start + i as Reg);
2497                    self.mark_named(start + i as Reg);
2498                }
2499                loop_start = self.current_pc();
2500                next_idx = self.code.len();
2501                self.emit(Op::IterNext { dst: tmp, exit: usize::MAX });
2502                self.emit(Op::DestructureTuple { src: tmp, start, count });
2503            }
2504        }
2505
2506        self.flow_stack.push(FlowCtx::Loop { breaks: Vec::new(), is_repeat: true });
2507        let body_mark = self.enter_block();
2508        for st in body {
2509            self.compile_stmt(st)?;
2510        }
2511        self.exit_block(body_mark);
2512        self.emit(Op::Jump { target: loop_start });
2513
2514        let exit_pc = self.current_pc();
2515        self.patch_jump_target(next_idx, exit_pc)?;
2516        if let Some(FlowCtx::Loop { breaks, .. }) = self.flow_stack.pop() {
2517            for j in breaks {
2518                self.patch_jump_target(j, exit_pc)?;
2519            }
2520        }
2521        self.emit(Op::IterPop);
2522        self.exit_block(outer_mark);
2523        Ok(())
2524    }
2525
2526    /// Compile a `Zone`: the name is bound to Nothing for the body's duration,
2527    /// and the zone SWALLOWS Break/Return escaping the body.
2528    fn compile_zone(&mut self, name: Symbol, body: &[Stmt]) -> Result<(), String> {
2529        let outer_mark = self.enter_block();
2530        let name_reg = self.let_reg(name)?;
2531        let idx = self.add_const(Constant::Nothing)?;
2532        self.emit(Op::LoadConst { dst: name_reg, idx });
2533        self.flow_stack.push(FlowCtx::Zone { exits: Vec::new() });
2534        let body_mark = self.enter_block();
2535        for st in body {
2536            self.compile_stmt(st)?;
2537        }
2538        self.exit_block(body_mark);
2539        let end_pc = self.current_pc();
2540        if let Some(FlowCtx::Zone { exits }) = self.flow_stack.pop() {
2541            for j in exits {
2542                self.patch_jump_target(j, end_pc)?;
2543            }
2544        }
2545        self.exit_block(outer_mark);
2546        Ok(())
2547    }
2548
2549    /// Compile `Inspect` as a chain of TestArm/JumpIfFalse arms (the
2550    /// tree-walker's linear arm scan).
2551    fn compile_inspect(
2552        &mut self,
2553        target: &Expr,
2554        arms: &[crate::ast::stmt::MatchArm],
2555    ) -> Result<(), String> {
2556        let t = self.compile_expr(target)?;
2557        let mut end_jumps: Vec<usize> = Vec::new();
2558        let mut has_otherwise = false;
2559        for arm in arms.iter() {
2560            match arm.variant {
2561                None => {
2562                    // Otherwise: unconditional, and no arm after it runs.
2563                    has_otherwise = true;
2564                    let mark = self.enter_block();
2565                    for st in arm.body {
2566                        self.compile_stmt(st)?;
2567                    }
2568                    self.exit_block(mark);
2569                    break;
2570                }
2571                Some(variant) => {
2572                    let vname = self.interner.resolve(variant).to_string();
2573                    let vidx = self.add_const(Constant::Text(vname))?;
2574                    let flag = self.alloc_reg()?;
2575                    self.emit(Op::TestArm { dst: flag, target: t, variant: vidx });
2576                    let jnext = self.emit_placeholder_jump_if_false(flag);
2577
2578                    let mark = self.enter_block();
2579                    // Struct arms bind by field NAME, inductive arms by
2580                    // POSITION; the flavor is only known at runtime, so
2581                    // BindArm carries both and dispatches there.
2582                    for (i, (field_name, binding_name)) in arm.bindings.iter().enumerate() {
2583                        let dst = self.let_reg(*binding_name)?;
2584                        let fname = self.interner.resolve(*field_name).to_string();
2585                        let fidx = self.add_const(Constant::Text(fname))?;
2586                        self.emit(Op::BindArm {
2587                            dst,
2588                            target: t,
2589                            field: fidx,
2590                            index: u16::try_from(i)
2591                                .map_err(|_| "vm: too many arm bindings".to_string())?,
2592                        });
2593                    }
2594                    for st in arm.body {
2595                        self.compile_stmt(st)?;
2596                    }
2597                    self.exit_block(mark);
2598                    let j = self.emit_placeholder_jump();
2599                    end_jumps.push(j);
2600                    self.patch_jump_target(jnext, self.current_pc())?;
2601                }
2602            }
2603        }
2604        // An unmatched scrutinee (no arm's TestArm fired) falls to HERE. Without
2605        // an Otherwise it is a non-exhaustive match — LOUD, never a silent
2606        // no-op. Matched arms jump PAST this via `end_jumps`.
2607        if !has_otherwise {
2608            self.emit_fail(
2609                "Inspect has no arm for the value and no Otherwise (matches must be exhaustive)",
2610            )?;
2611        }
2612        let end_pc = self.current_pc();
2613        for j in end_jumps {
2614            self.patch_jump_target(j, end_pc)?;
2615        }
2616        Ok(())
2617    }
2618
2619    /// Compile `a new T …` — collections, or a struct with default-fill.
2620    fn compile_new(
2621        &mut self,
2622        type_name: Symbol,
2623        init_fields: &[(Symbol, &Expr)],
2624        dst: Reg,
2625    ) -> Result<(), String> {
2626        // Collection names win unconditionally (tree-walker order).
2627        match self.interner.resolve(type_name) {
2628            "Seq" | "List" => { self.emit(Op::NewEmptyList { dst }); return Ok(()); }
2629            "Set" | "HashSet" => { self.emit(Op::NewEmptySet { dst }); return Ok(()); }
2630            "Map" | "HashMap" => { self.emit(Op::NewEmptyMap { dst }); return Ok(()); }
2631            _ => {}
2632        }
2633        let name = self.interner.resolve(type_name).to_string();
2634        let name_idx = self.add_const(Constant::Text(name))?;
2635        self.emit(Op::NewStruct { dst, type_name: name_idx });
2636
2637        let mut provided: std::collections::HashSet<Symbol> = std::collections::HashSet::new();
2638        for (field_sym, field_expr) in init_fields {
2639            provided.insert(*field_sym);
2640            let fname = self.interner.resolve(*field_sym).to_string();
2641            let fidx = self.add_const(Constant::Text(fname))?;
2642            let v = self.compile_expr(field_expr)?;
2643            self.emit(Op::StructInsert { obj: dst, field: fidx, value: v });
2644        }
2645
2646        // Default-fill the declared fields not provided (tree-walker defaults
2647        // by declared type name).
2648        if let Some(def) = self.struct_defs.get(&type_name).cloned() {
2649            for (field_sym, type_sym, _) in def {
2650                if provided.contains(&field_sym) {
2651                    continue;
2652                }
2653                let fname = self.interner.resolve(field_sym).to_string();
2654                let fidx = self.add_const(Constant::Text(fname))?;
2655                let v = self.alloc_reg()?;
2656                match self.interner.resolve(type_sym) {
2657                    "Int" | "Byte" => {
2658                        let i = self.add_const(Constant::Int(0))?;
2659                        self.emit(Op::LoadConst { dst: v, idx: i });
2660                    }
2661                    "Float" => {
2662                        let i = self.add_const(Constant::Float(0.0))?;
2663                        self.emit(Op::LoadConst { dst: v, idx: i });
2664                    }
2665                    "Bool" => {
2666                        let i = self.add_const(Constant::Bool(false))?;
2667                        self.emit(Op::LoadConst { dst: v, idx: i });
2668                    }
2669                    "Text" | "String" => {
2670                        let i = self.add_const(Constant::Text(String::new()))?;
2671                        self.emit(Op::LoadConst { dst: v, idx: i });
2672                    }
2673                    "Char" => {
2674                        let i = self.add_const(Constant::Char('\0'))?;
2675                        self.emit(Op::LoadConst { dst: v, idx: i });
2676                    }
2677                    "Seq" | "List" => self.emit(Op::NewEmptyList { dst: v }),
2678                    "Set" | "HashSet" => self.emit(Op::NewEmptySet { dst: v }),
2679                    "Map" | "HashMap" => self.emit(Op::NewEmptyMap { dst: v }),
2680                    // A `Shared` struct's CRDT collection fields default to an empty live
2681                    // CRDT, mirroring the tree-walker's `new`-struct init.
2682                    "SharedSet" | "ORSet" | "SharedSet_AddWins" => {
2683                        self.emit(Op::NewCrdt { dst: v, kind: 0 })
2684                    }
2685                    "SharedSet_RemoveWins" => self.emit(Op::NewCrdt { dst: v, kind: 3 }),
2686                    "SharedSequence" | "RGA" | "SharedSequence_YATA" | "CollaborativeSequence" => {
2687                        self.emit(Op::NewCrdt { dst: v, kind: 1 })
2688                    }
2689                    _ => {
2690                        let i = self.add_const(Constant::Nothing)?;
2691                        self.emit(Op::LoadConst { dst: v, idx: i });
2692                    }
2693                }
2694                self.emit(Op::StructInsert { obj: dst, field: fidx, value: v });
2695            }
2696        }
2697        Ok(())
2698    }
2699
2700    /// Compile `and`/`or` — logical truthiness with short-circuit, Bool result,
2701    /// pure control flow (JumpIfTrue/False are `is_truthy`-based, so containers,
2702    /// floats and Text all follow the one truthiness definition). The right
2703    /// operand is compiled ONCE and only evaluated when the left doesn't decide.
2704    ///
2705    /// ```text
2706    ///   rL = eval(left)
2707    ///   JumpIfTrue  rL → eval  (and)  ; or: JumpIfFalse rL → eval
2708    ///   dst = false            (and)  ; or: dst = true   — short-circuit
2709    ///   Jump → end
2710    /// eval:
2711    ///   rR = eval(right)
2712    ///   JumpIfTrue  rR → t
2713    ///   dst = false
2714    ///   Jump → end
2715    /// t:
2716    ///   dst = true
2717    /// end:
2718    /// ```
2719    fn compile_short_circuit(
2720        &mut self,
2721        is_and: bool,
2722        left: &Expr,
2723        right: &Expr,
2724        dst: Reg,
2725    ) -> Result<(), String> {
2726        let l = self.compile_expr(left)?;
2727
2728        let j_eval = self.current_pc();
2729        if is_and {
2730            self.emit(Op::JumpIfTrue { cond: l, target: usize::MAX });
2731        } else {
2732            self.emit(Op::JumpIfFalse { cond: l, target: usize::MAX });
2733        }
2734
2735        // Short-circuit result: `and` → false, `or` → true.
2736        let idx_false = self.add_const(Constant::Bool(false))?;
2737        let idx_true = self.add_const(Constant::Bool(true))?;
2738        self.emit(Op::LoadConst { dst, idx: if is_and { idx_false } else { idx_true } });
2739        let j_end = self.emit_placeholder_jump();
2740
2741        let eval_pc = self.current_pc();
2742        self.patch_jump_target(j_eval, eval_pc)?;
2743        let r = self.compile_expr(right)?;
2744        let j_true = self.current_pc();
2745        self.emit(Op::JumpIfTrue { cond: r, target: usize::MAX });
2746        self.emit(Op::LoadConst { dst, idx: idx_false });
2747        let j_end2 = self.emit_placeholder_jump();
2748        let t_pc = self.current_pc();
2749        self.patch_jump_target(j_true, t_pc)?;
2750        self.emit(Op::LoadConst { dst, idx: idx_true });
2751        self.patch_jump_target(j_end, self.current_pc())?;
2752        self.patch_jump_target(j_end2, self.current_pc())?;
2753        Ok(())
2754    }
2755
2756    /// Compile a call, writing the result to `dst`. Dispatch order mirrors the
2757    /// tree-walker exactly: `show` → kernel builtins → user functions →
2758    /// runtime "Unknown function". Arity errors fire at RUNTIME, BEFORE the
2759    /// arguments are evaluated.
2760    /// Compile `Launch a task to f with args` (and the handle-binding variant).
2761    /// Mirrors [`Self::compile_call`]'s function resolution + contiguous arg
2762    /// window, then emits `Spawn`/`SpawnHandle`.
2763    fn compile_spawn(
2764        &mut self,
2765        function: Symbol,
2766        args: &[&Expr],
2767        handle: Option<Symbol>,
2768    ) -> Result<(), String> {
2769        let func = match self.fn_index.get(&function) {
2770            Some(&f) => f,
2771            None => {
2772                return self.emit_fail(&format!(
2773                    "Unknown task function: {}",
2774                    self.interner.resolve(function)
2775                ))
2776            }
2777        };
2778        let arg_count =
2779            u16::try_from(args.len()).map_err(|_| "vm: too many task arguments".to_string())?;
2780        let args_start = self.reserve_regs(arg_count)?;
2781        for (i, arg) in args.iter().enumerate() {
2782            self.compile_expr_into(arg, args_start + i as Reg)?;
2783        }
2784        match handle {
2785            Some(h) => {
2786                let dst = self.let_reg(h)?;
2787                self.emit(Op::SpawnHandle { dst, func, args_start, arg_count });
2788            }
2789            None => self.emit(Op::Spawn { func, args_start, arg_count }),
2790        }
2791        Ok(())
2792    }
2793
2794    fn compile_call(&mut self, function: Symbol, args: &[&Expr], dst: Reg) -> Result<(), String> {
2795        use crate::semantics::builtins::{builtin_from_name, check_arity, BuiltinId};
2796
2797        let name = self.interner.resolve(function);
2798        if name == "show" {
2799            // show(a, b, …) emits each argument; result is Nothing.
2800            for arg in args {
2801                let src = self.compile_expr(arg)?;
2802                self.emit(Op::Show { src });
2803            }
2804            let idx = self.add_const(Constant::Nothing)?;
2805            self.emit(Op::LoadConst { dst, idx });
2806            return Ok(());
2807        }
2808
2809        // `args()` is the program-argument system native (declared `## To
2810        // native args ()`); it resolves to the VM's stored argv, like the
2811        // compiled binary's `env::args()`. Intercepted by name BEFORE the
2812        // empty native-decl body would be reached, mirroring `show`.
2813        if name == "args" {
2814            self.emit(Op::Args { dst });
2815            return Ok(());
2816        }
2817
2818        // `{k: v, …}` / `{a, b, …}` literals lower to `mapOf`/`setOf`; the VM
2819        // expands them into ops it already has (NewEmptyMap + SetIndex /
2820        // NewEmptySet + SetAdd) so every downstream tier — JIT, direct-WASM,
2821        // the PE dialects — sees only cataloged ops, no new CallBuiltin arm.
2822        if matches!(name, "mapOf" | "setOf") {
2823            let id = builtin_from_name(name).expect("mapOf/setOf are registered builtins");
2824            if let Err(msg) = check_arity(id, args.len()) {
2825                let idx = self.add_const(Constant::Text(msg))?;
2826                self.emit(Op::FailWith { msg: idx });
2827                return Ok(());
2828            }
2829            if id == BuiltinId::MapOf {
2830                self.emit(Op::NewEmptyMap { dst });
2831                for pair in args.chunks(2) {
2832                    let k = self.compile_expr(pair[0])?;
2833                    let v = self.compile_expr(pair[1])?;
2834                    self.emit(Op::SetIndex { collection: dst, index: k, value: v });
2835                }
2836            } else {
2837                self.emit(Op::NewEmptySet { dst });
2838                for arg in args {
2839                    let v = self.compile_expr(arg)?;
2840                    self.emit(Op::SetAdd { set: dst, value: v });
2841                }
2842            }
2843            return Ok(());
2844        }
2845
2846        if let Some(id) = builtin_from_name(name) {
2847            if let Err(msg) = check_arity(id, args.len()) {
2848                let idx = self.add_const(Constant::Text(msg))?;
2849                self.emit(Op::FailWith { msg: idx });
2850                return Ok(());
2851            }
2852            // `format` evaluates only its first argument (tree-walker laziness).
2853            let used: &[&Expr] = if id == BuiltinId::Format && !args.is_empty() {
2854                &args[..1]
2855            } else {
2856                args
2857            };
2858            let arg_count =
2859                u16::try_from(used.len()).map_err(|_| "vm: too many arguments".to_string())?;
2860            let args_start = self.reserve_regs(arg_count)?;
2861            for (i, arg) in used.iter().enumerate() {
2862                self.compile_expr_into(arg, args_start + i as Reg)?;
2863            }
2864            self.emit(Op::CallBuiltin { dst, builtin: id, args_start, arg_count });
2865            return Ok(());
2866        }
2867
2868        if let Some(&func) = self.fn_index.get(&function) {
2869            let param_count = self.functions[func as usize].param_count as usize;
2870            if args.len() != param_count {
2871                let msg = format!(
2872                    "Function {} expects {} arguments, got {}",
2873                    name,
2874                    param_count,
2875                    args.len()
2876                );
2877                let idx = self.add_const(Constant::Text(msg))?;
2878                self.emit(Op::FailWith { msg: idx });
2879                return Ok(());
2880            }
2881            let arg_count =
2882                u16::try_from(args.len()).map_err(|_| "vm: too many arguments".to_string())?;
2883            let args_start = self.reserve_regs(arg_count)?;
2884            // Call-site COW barrier: before an identifier argument is passed to a
2885            // mutable-borrow parameter, isolate its buffer if shared (see
2886            // [`Op::EnsureOwned`]) so the callee's in-place writes stay value-semantic
2887            // for any aliasing caller. Emitted BEFORE materialization, while the
2888            // source register still holds the caller's handle. A no-op for the common
2889            // uniquely-owned `Set x to f(x, …)` consume-reassign.
2890            if let Some(mb_idxs) = self.mut_borrow_params.get(&function).cloned() {
2891                for (i, arg) in args.iter().enumerate() {
2892                    if mb_idxs.contains(&i) {
2893                        if let Expr::Identifier(x) = arg {
2894                            // Skip an argument that is itself a COW-exempt local of the
2895                            // CURRENT function (a self-recursive `f(result, …)` where
2896                            // `result` aliases our own mutable-borrow param). It has no
2897                            // external alias — the mutable-borrow analysis proved that —
2898                            // so the barrier is a runtime no-op; omitting it keeps the
2899                            // op out of a hot, tier-able function body.
2900                            if self.current_exempt_syms.contains(x) {
2901                                continue;
2902                            }
2903                            if let NameRef::Local(x_reg) = self.resolve_name(*x) {
2904                                self.emit(Op::EnsureOwned { reg: x_reg });
2905                            }
2906                        }
2907                    }
2908                }
2909            }
2910            for (i, arg) in args.iter().enumerate() {
2911                self.compile_expr_into(arg, args_start + i as Reg)?;
2912            }
2913            self.emit(Op::Call { dst, func, args_start, arg_count });
2914            return Ok(());
2915        }
2916
2917        // A variable holding a closure: call it by name. A bound non-Function
2918        // value errors "Unknown function: {name}" at runtime — the
2919        // tree-walker's by-name fallback.
2920        if !matches!(self.resolve_name(function), NameRef::Unbound) {
2921            let callee = self.compile_expr(&Expr::Identifier(function))?;
2922            let arg_count =
2923                u16::try_from(args.len()).map_err(|_| "vm: too many arguments".to_string())?;
2924            let args_start = self.reserve_regs(arg_count)?;
2925            for (i, arg) in args.iter().enumerate() {
2926                self.compile_expr_into(arg, args_start + i as Reg)?;
2927            }
2928            let name_idx = self.add_const(Constant::Text(name.to_string()))?;
2929            self.emit(Op::CallValue { dst, callee, args_start, arg_count, name_for_err: name_idx });
2930            return Ok(());
2931        }
2932
2933        let msg = format!("Unknown function: {}", name);
2934        let idx = self.add_const(Constant::Text(msg))?;
2935        self.emit(Op::FailWith { msg: idx });
2936        Ok(())
2937    }
2938
2939    /// Compile `Show x to f` / `Give x to f` — the value goes to a FUNCTION
2940    /// recipient via the tree-walker's `call_function_with_values` dispatch,
2941    /// which knows only `show`, user functions, and closures (NOT the other
2942    /// builtins).
2943    fn compile_recipient_call(&mut self, recipient: Symbol, object: &Expr) -> Result<(), String> {
2944        let name = self.interner.resolve(recipient);
2945        if name == "show" {
2946            let src = self.compile_expr(object)?;
2947            self.emit(Op::Show { src });
2948            return Ok(());
2949        }
2950        if let Some(&func) = self.fn_index.get(&recipient) {
2951            let param_count = self.functions[func as usize].param_count as usize;
2952            let args_start = self.reserve_regs(1)?;
2953            self.compile_expr_into(object, args_start)?;
2954            if param_count != 1 {
2955                let msg = format!(
2956                    "Function {} expects {} arguments, got 1",
2957                    name, param_count
2958                );
2959                let idx = self.add_const(Constant::Text(msg))?;
2960                self.emit(Op::FailWith { msg: idx });
2961                return Ok(());
2962            }
2963            let dst = self.alloc_reg()?;
2964            self.emit(Op::Call { dst, func, args_start, arg_count: 1 });
2965            return Ok(());
2966        }
2967        // A variable holding a closure (the with_values closure fallback).
2968        if !matches!(self.resolve_name(recipient), NameRef::Unbound) {
2969            let args_start = self.reserve_regs(1)?;
2970            self.compile_expr_into(object, args_start)?;
2971            let callee = self.compile_expr(&Expr::Identifier(recipient))?;
2972            let dst = self.alloc_reg()?;
2973            let name_idx = self.add_const(Constant::Text(name.to_string()))?;
2974            self.emit(Op::CallValue { dst, callee, args_start, arg_count: 1, name_for_err: name_idx });
2975            return Ok(());
2976        }
2977        // The object's side effects happen before the dispatch failure.
2978        let scratch = self.alloc_reg()?;
2979        self.compile_expr_into(object, scratch)?;
2980        let msg = format!("Unknown function: {}", name);
2981        let idx = self.add_const(Constant::Text(msg))?;
2982        self.emit(Op::FailWith { msg: idx });
2983        Ok(())
2984    }
2985
2986    /// Compile an interpolated string: parts accumulate into a Text register
2987    /// via Concat (Text+Text concatenation — identical to the tree-walker's
2988    /// push_str building).
2989    fn compile_interpolation(
2990        &mut self,
2991        parts: &[crate::ast::stmt::StringPart],
2992        dst: Reg,
2993    ) -> Result<(), String> {
2994        use crate::ast::stmt::StringPart;
2995
2996        let empty = self.add_const(Constant::Text(String::new()))?;
2997        self.emit(Op::LoadConst { dst, idx: empty });
2998        for part in parts {
2999            match part {
3000                StringPart::Literal(sym) => {
3001                    let idx =
3002                        self.add_const(Constant::Text(self.interner.resolve(*sym).to_string()))?;
3003                    let lit = self.alloc_reg()?;
3004                    self.emit(Op::LoadConst { dst: lit, idx });
3005                    self.emit(Op::Concat { dst, lhs: dst, rhs: lit });
3006                }
3007                StringPart::Expr { value, format_spec, debug } => {
3008                    let v = self.compile_expr(value)?;
3009                    let needs_format = format_spec.is_some() || *debug;
3010                    let piece = if needs_format {
3011                        let spec = match format_spec {
3012                            Some(sym) => self.add_const(Constant::Text(
3013                                self.interner.resolve(*sym).to_string(),
3014                            ))?,
3015                            None => u32::MAX,
3016                        };
3017                        let debug_prefix = if *debug {
3018                            let prefix = match value {
3019                                Expr::Identifier(sym) => {
3020                                    self.interner.resolve(*sym).to_string()
3021                                }
3022                                _ => "expr".to_string(),
3023                            };
3024                            self.add_const(Constant::Text(prefix))?
3025                        } else {
3026                            u32::MAX
3027                        };
3028                        let formatted = self.alloc_reg()?;
3029                        self.emit(Op::FormatValue { dst: formatted, src: v, spec, debug_prefix });
3030                        formatted
3031                    } else {
3032                        v
3033                    };
3034                    self.emit(Op::Concat { dst, lhs: dst, rhs: piece });
3035                }
3036            }
3037        }
3038        Ok(())
3039    }
3040
3041    /// Compile a closure literal: the body becomes an anonymous function
3042    /// emitted INLINE (jumped over at the creation site); local captures are
3043    /// snapshotted from a register window, global captures from the globals
3044    /// table at creation time. Frame layout:
3045    /// `[params…, capture values…, capture-present flags…]`.
3046    fn compile_closure(
3047        &mut self,
3048        params: &[(Symbol, &crate::ast::stmt::TypeExpr)],
3049        body: &crate::ast::stmt::ClosureBody,
3050        dst: Reg,
3051    ) -> Result<(), String> {
3052        use crate::ast::stmt::ClosureBody;
3053        use crate::interpreter::Interpreter;
3054
3055        let free = Interpreter::free_vars_in_closure(params, body);
3056        let mut captures: Vec<(Symbol, Option<u16>)> = Vec::new();
3057        let mut local_sources: Vec<Reg> = Vec::new();
3058        for sym in free {
3059            match self.resolve_name(sym) {
3060                NameRef::Local(r) => {
3061                    captures.push((sym, None));
3062                    local_sources.push(r);
3063                }
3064                NameRef::CaptureOrGlobal { value, global, .. } => {
3065                    // Closure-in-closure over a promoted capture: snapshot the
3066                    // capture slot (the live-global fall-through nests with
3067                    // the outer flag at creation time only).
3068                    let _ = global;
3069                    captures.push((sym, None));
3070                    local_sources.push(value);
3071                }
3072                NameRef::Global(g) => captures.push((sym, Some(g))),
3073                NameRef::Unbound => {}
3074            }
3075        }
3076
3077        // Move local capture sources into a contiguous window for MakeClosure.
3078        let local_count =
3079            u16::try_from(local_sources.len()).map_err(|_| "vm: too many captures".to_string())?;
3080        let locals_start = self.reserve_regs(local_count)?;
3081        for (i, src) in local_sources.iter().enumerate() {
3082            let w = locals_start + i as Reg;
3083            if *src != w {
3084                self.emit(Op::Move { dst: w, src: *src });
3085            }
3086        }
3087
3088        // Register the body as a function and compile it inline, jumped over.
3089        let func_idx = u16::try_from(self.functions.len())
3090            .map_err(|_| "vm: too many functions".to_string())?;
3091        let param_count =
3092            u16::try_from(params.len()).map_err(|_| "vm: too many parameters".to_string())?;
3093        let jover = self.emit_placeholder_jump();
3094        self.functions.push(CompiledFunction {
3095            name: Symbol::default(),
3096            entry_pc: self.code.len(),
3097            param_count,
3098            register_count: 0,
3099            captures: captures.clone(),
3100            named_regs: Vec::new(),
3101            // Closures carry no declarations — all-Int is the historical
3102            // entry-guard contract (kept for the VM/JIT tiering path).
3103            param_kinds: vec![
3104                Some(super::native_tier::ParamKind::Scalar(
3105                    super::native_tier::SlotKind::Int
3106                ));
3107                param_count as usize
3108            ],
3109            ret_kind: None,
3110            // The closure's DECLARED parameter types — a `(n: Float)` closure must type its param as
3111            // Float, not the all-Int entry-guard default, or a static backend (the WASM AOT) mis-sizes
3112            // its signature (f64 arg vs i64 param). Additive: only the AOT reads `param_types` (the VM/
3113            // JIT use `param_kinds`, left untouched). Scalars resolve with an empty user-type map; a
3114            // struct/enum closure parameter resolves via the compiler's `user_types` map.
3115            param_types: params
3116                .iter()
3117                .map(|(_, ty)| boundary_of_type_expr(ty, &self.user_types, self.interner))
3118                .collect(),
3119            return_type: None,
3120            // Closures carry no `mutable` declarations.
3121            mutable_param_regs: Vec::new(),
3122        });
3123
3124        // Shelve the enclosing frame's compilation state.
3125        let saved_scopes = std::mem::replace(&mut self.scopes, vec![HashMap::new()]);
3126        let saved_next = self.next_reg;
3127        let saved_max = self.max_reg;
3128        let saved_flow = std::mem::take(&mut self.flow_stack);
3129        let saved_in_fn = self.in_function;
3130        let saved_ctx = self.closure_ctx.take();
3131        let saved_named = std::mem::take(&mut self.named);
3132
3133        self.in_function = true;
3134        let p = param_count;
3135        let cap_n = captures.len() as Reg;
3136        for (i, (psym, _)) in params.iter().enumerate() {
3137            self.scopes.last_mut().unwrap().insert(*psym, i as Reg);
3138            self.mark_named(i as Reg);
3139        }
3140        let mut ctx: HashMap<Symbol, (Reg, Reg)> = HashMap::new();
3141        for (k, (sym, global)) in captures.iter().enumerate() {
3142            let value = p + k as Reg;
3143            self.scopes.last_mut().unwrap().insert(*sym, value);
3144            self.mark_named(value);
3145            if global.is_some() {
3146                ctx.insert(*sym, (value, p + cap_n + k as Reg));
3147            }
3148        }
3149        self.next_reg = p + 2 * cap_n;
3150        self.max_reg = self.next_reg;
3151        self.closure_ctx = Some(ctx);
3152
3153        let body_result = (|| -> Result<(), String> {
3154            match body {
3155                ClosureBody::Expression(e) => {
3156                    let r = self.compile_expr(e)?;
3157                    self.emit(Op::Return { src: r });
3158                }
3159                ClosureBody::Block(block) => {
3160                    for st in block.iter() {
3161                        self.compile_stmt(st)?;
3162                    }
3163                    self.emit(Op::ReturnNothing);
3164                }
3165            }
3166            Ok(())
3167        })();
3168        self.functions[func_idx as usize].register_count = self.max_reg as usize;
3169        let mut cnamed = std::mem::replace(&mut self.named, saved_named);
3170        cnamed.resize(self.functions[func_idx as usize].register_count, false);
3171        self.functions[func_idx as usize].named_regs = cnamed;
3172
3173        // Restore the enclosing frame's state (even when the body failed).
3174        self.scopes = saved_scopes;
3175        self.next_reg = saved_next;
3176        self.max_reg = saved_max.max(self.next_reg);
3177        self.flow_stack = saved_flow;
3178        self.in_function = saved_in_fn;
3179        self.closure_ctx = saved_ctx;
3180        body_result?;
3181
3182        self.patch_jump_target(jover, self.current_pc())?;
3183        self.emit(Op::MakeClosure { dst, func: func_idx, locals_start });
3184        Ok(())
3185    }
3186}
3187
3188/// Collect identifiers that appear inside function or closure bodies — the
3189/// names that, when bound at Main top level, must live in the globals table.
3190/// `at_main` is true while walking Main-level statements (whose own
3191/// identifiers are frame-local); inside a FunctionDef body or a Closure body
3192/// every identifier counts.
3193fn collect_nonlocal_idents_stmt(
3194    s: &Stmt,
3195    at_main: bool,
3196    out: &mut std::collections::HashSet<Symbol>,
3197) {
3198    use crate::ast::stmt::ClosureBody;
3199    use crate::interpreter::Interpreter;
3200
3201    if let Stmt::FunctionDef { params, body, .. } = s {
3202        // Everything free in a function body is a nonlocal reference.
3203        let free = Interpreter::free_vars_in_closure(params, &ClosureBody::Block(body));
3204        out.extend(free);
3205        return;
3206    }
3207    // Walk this statement's expressions for Closure literals (whose free vars
3208    // are nonlocal) and recurse into nested statements.
3209    visit_stmt_exprs(s, &mut |e| {
3210        if let Expr::Closure { params, body, .. } = e {
3211            let free = Interpreter::free_vars_in_closure(params, body);
3212            out.extend(free);
3213        }
3214    });
3215    for_each_child_block(s, &mut |block| {
3216        for st in block {
3217            collect_nonlocal_idents_stmt(st, at_main, out);
3218        }
3219    });
3220}
3221
3222/// Apply `f` to every expression directly held by `s` (and, via the walker in
3223/// `f` itself when needed, their subexpressions are reached by the free-vars
3224/// collector — Closure bodies included).
3225fn visit_stmt_exprs(s: &Stmt, f: &mut dyn FnMut(&Expr)) {
3226    // Iterative walk: expressions can be arbitrarily deep (degenerate inputs),
3227    // so no native recursion here.
3228    fn walk_expr<'a>(root: &'a Expr<'a>, f: &mut dyn FnMut(&Expr)) {
3229        let mut stack: Vec<&Expr> = vec![root];
3230        while let Some(e) = stack.pop() {
3231            f(e);
3232            match e {
3233                Expr::BinaryOp { left, right, .. } => {
3234                    stack.push(left);
3235                    stack.push(right);
3236                }
3237                Expr::Not { operand } => stack.push(operand),
3238                Expr::Call { args, .. } => stack.extend(args.iter().copied()),
3239                Expr::CallExpr { callee, args } => {
3240                    stack.push(callee);
3241                    stack.extend(args.iter().copied());
3242                }
3243                Expr::Index { collection, index } => {
3244                    stack.push(collection);
3245                    stack.push(index);
3246                }
3247                Expr::Slice { collection, start, end } => {
3248                    stack.push(collection);
3249                    stack.push(start);
3250                    stack.push(end);
3251                }
3252                Expr::Copy { expr } => stack.push(expr),
3253                Expr::Give { value } => stack.push(value),
3254                Expr::Length { collection } => stack.push(collection),
3255                Expr::Contains { collection, value } => {
3256                    stack.push(collection);
3257                    stack.push(value);
3258                }
3259                Expr::Union { left, right } | Expr::Intersection { left, right } => {
3260                    stack.push(left);
3261                    stack.push(right);
3262                }
3263                Expr::List(items) | Expr::Tuple(items) => stack.extend(items.iter().copied()),
3264                Expr::Range { start, end } => {
3265                    stack.push(start);
3266                    stack.push(end);
3267                }
3268                Expr::FieldAccess { object, .. } => stack.push(object),
3269                Expr::New { init_fields, .. } => {
3270                    stack.extend(init_fields.iter().map(|(_, fe)| *fe));
3271                }
3272                Expr::NewVariant { fields, .. } => {
3273                    stack.extend(fields.iter().map(|(_, fe)| *fe));
3274                }
3275                Expr::OptionSome { value } => stack.push(value),
3276                Expr::WithCapacity { value, .. } => stack.push(value),
3277                _ => {}
3278            }
3279        }
3280    }
3281
3282    use crate::ast::stmt::ReadSource;
3283    match s {
3284        Stmt::Let { value, .. } | Stmt::Set { value, .. } => walk_expr(value, f),
3285        Stmt::Return { value: Some(e) } => walk_expr(e, f),
3286        Stmt::Call { args, .. } => {
3287            for a in args {
3288                walk_expr(a, f);
3289            }
3290        }
3291        Stmt::If { cond, .. } | Stmt::While { cond, .. } => walk_expr(cond, f),
3292        Stmt::Repeat { iterable, .. } => walk_expr(iterable, f),
3293        Stmt::Show { object, .. } | Stmt::Give { object, .. } => walk_expr(object, f),
3294        Stmt::Push { value, collection }
3295        | Stmt::Add { value, collection }
3296        | Stmt::Remove { value, collection } => {
3297            walk_expr(value, f);
3298            walk_expr(collection, f);
3299        }
3300        Stmt::SetIndex { collection, index, value } => {
3301            walk_expr(collection, f);
3302            walk_expr(index, f);
3303            walk_expr(value, f);
3304        }
3305        Stmt::SetField { object, value, .. } => {
3306            walk_expr(object, f);
3307            walk_expr(value, f);
3308        }
3309        Stmt::Inspect { target, .. } => walk_expr(target, f),
3310        Stmt::RuntimeAssert { condition, .. } => walk_expr(condition, f),
3311        Stmt::Sleep { milliseconds } => walk_expr(milliseconds, f),
3312        Stmt::IncreaseCrdt { amount, .. } | Stmt::DecreaseCrdt { amount, .. } => {
3313            walk_expr(amount, f)
3314        }
3315        Stmt::MergeCrdt { source, .. } => walk_expr(source, f),
3316        Stmt::WriteFile { content, path } => {
3317            walk_expr(content, f);
3318            walk_expr(path, f);
3319        }
3320        Stmt::ReadFrom { source: ReadSource::File(p), .. } => walk_expr(p, f),
3321        _ => {}
3322    }
3323}
3324
3325/// Apply `f` to every nested statement block of `s`.
3326fn for_each_child_block<'a>(s: &Stmt<'a>, f: &mut dyn FnMut(&[Stmt<'a>])) {
3327    match s {
3328        Stmt::If { then_block, else_block, .. } => {
3329            f(then_block);
3330            if let Some(eb) = else_block {
3331                f(eb);
3332            }
3333        }
3334        Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => f(body),
3335        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => f(tasks),
3336        Stmt::Inspect { arms, .. } => {
3337            for arm in arms {
3338                f(arm.body);
3339            }
3340        }
3341        _ => {}
3342    }
3343}
3344
3345/// Rewrite the placeholder target of the jump at `idx`. Total: a non-jump op or
3346/// an out-of-bounds index is a compiler-internal bug surfaced as `Err`.
3347pub(super) fn patch_jump(code: &mut [Op], idx: usize, target: usize) -> Result<(), String> {
3348    match code.get_mut(idx) {
3349        Some(Op::Jump { target: t })
3350        | Some(Op::JumpIfFalse { target: t, .. })
3351        | Some(Op::JumpIfTrue { target: t, .. })
3352        | Some(Op::IterNext { exit: t, .. }) => {
3353            *t = target;
3354            Ok(())
3355        }
3356        Some(other) => Err(format!("vm: patch_jump on non-jump op {:?}", other)),
3357        None => Err(format!("vm: patch_jump index {} out of bounds", idx)),
3358    }
3359}
3360
3361fn literal_const(lit: &Literal) -> Result<Constant, String> {
3362    match lit {
3363        Literal::Number(n) => Ok(Constant::Int(*n)),
3364        Literal::Float(f) => Ok(Constant::Float(*f)),
3365        Literal::Boolean(b) => Ok(Constant::Bool(*b)),
3366        Literal::Nothing => Ok(Constant::Nothing),
3367        Literal::Char(c) => Ok(Constant::Char(*c)),
3368        Literal::Duration(nanos) => Ok(Constant::Duration(*nanos)),
3369        Literal::Date(days) => Ok(Constant::Date(*days)),
3370        Literal::Moment(nanos) => Ok(Constant::Moment(*nanos)),
3371        Literal::Span { months, days } => Ok(Constant::Span { months: *months, days: *days }),
3372        Literal::Time(nanos) => Ok(Constant::Time(*nanos)),
3373        Literal::Text(_) => unreachable!("Text literals are interned and handled by the caller"),
3374    }
3375}
3376
3377/// Match `value` as the left-spine `target + e1 + … + ek` (k ≥ 1), the
3378/// accumulate shape `Set x to x + …` lowers to an [`Op::AddAssign`] chain —
3379/// the exact left-fold the expression denotes: same kernel calls, same
3380/// evaluation order, same error points, but folding into the target register
3381/// at each step so a sole-owner Text can extend in place.
3382///
3383/// Soundness: the first term evaluates before any fold touches the register,
3384/// so it may mention the target freely. Every LATER term runs after a fold —
3385/// it must provably never observe the target ([`expr_avoids`]), or the chain
3386/// would read a partially-folded value the original expression never exposes.
3387fn add_assign_chain<'a>(target: Symbol, value: &'a Expr<'a>) -> Option<Vec<&'a Expr<'a>>> {
3388    let mut terms: Vec<&Expr> = Vec::new();
3389    let mut cur = value;
3390    loop {
3391        match cur {
3392            Expr::BinaryOp { op: BinaryOpKind::Add, left, right } => {
3393                terms.push(right);
3394                cur = left;
3395            }
3396            Expr::Identifier(sym) if *sym == target => break,
3397            _ => return None,
3398        }
3399    }
3400    if terms.is_empty() {
3401        return None;
3402    }
3403    terms.reverse();
3404    if terms[1..].iter().all(|t| expr_avoids(t, target)) {
3405        Some(terms)
3406    } else {
3407        None
3408    }
3409}
3410
3411/// True when `e` provably never observes `sym`: every node comes from a
3412/// closed set of value-only forms with no `Identifier(sym)` anywhere.
3413/// Closures are conservatively "may observe" — they snapshot captures at
3414/// evaluation, and under the AddAssign rewrite that snapshot would see the
3415/// partially-folded register. Named calls cannot read the caller's locals
3416/// (and the rewrite only fires for locals), so only their argument
3417/// expressions need scanning. Anything unrecognized fails closed.
3418fn expr_avoids(e: &Expr, sym: Symbol) -> bool {
3419    use crate::ast::stmt::StringPart;
3420    let mut stack: Vec<&Expr> = vec![e];
3421    while let Some(e) = stack.pop() {
3422        match e {
3423            Expr::Identifier(s) => {
3424                if *s == sym {
3425                    return false;
3426                }
3427            }
3428            Expr::Literal(_) | Expr::OptionNone => {}
3429            Expr::BinaryOp { left, right, .. }
3430            | Expr::Union { left, right }
3431            | Expr::Intersection { left, right }
3432            | Expr::Range { start: left, end: right } => {
3433                stack.push(left);
3434                stack.push(right);
3435            }
3436            Expr::Not { operand } => stack.push(operand),
3437            Expr::Call { args, .. } => stack.extend(args.iter().copied()),
3438            Expr::CallExpr { callee, args } => {
3439                stack.push(callee);
3440                stack.extend(args.iter().copied());
3441            }
3442            Expr::Index { collection, index } => {
3443                stack.push(collection);
3444                stack.push(index);
3445            }
3446            Expr::Slice { collection, start, end } => {
3447                stack.push(collection);
3448                stack.push(start);
3449                stack.push(end);
3450            }
3451            Expr::Copy { expr } => stack.push(expr),
3452            Expr::Length { collection } => stack.push(collection),
3453            Expr::Contains { collection, value } => {
3454                stack.push(collection);
3455                stack.push(value);
3456            }
3457            Expr::List(items) | Expr::Tuple(items) => stack.extend(items.iter().copied()),
3458            Expr::FieldAccess { object, .. } => stack.push(object),
3459            Expr::New { init_fields, .. } => {
3460                stack.extend(init_fields.iter().map(|(_, fe)| *fe));
3461            }
3462            Expr::NewVariant { fields, .. } => {
3463                stack.extend(fields.iter().map(|(_, fe)| *fe));
3464            }
3465            Expr::OptionSome { value } => stack.push(value),
3466            Expr::WithCapacity { value, capacity } => {
3467                stack.push(value);
3468                stack.push(capacity);
3469            }
3470            Expr::InterpolatedString(parts) => {
3471                for p in parts {
3472                    if let StringPart::Expr { value, .. } = p {
3473                        stack.push(value);
3474                    }
3475                }
3476            }
3477            _ => return false,
3478        }
3479    }
3480    true
3481}
3482
3483fn binop_op(op: BinaryOpKind, dst: Reg, lhs: Reg, rhs: Reg) -> Result<Op, String> {
3484    use BinaryOpKind::*;
3485    Ok(match op {
3486        Add => Op::Add { dst, lhs, rhs },
3487        Subtract => Op::Sub { dst, lhs, rhs },
3488        Multiply => Op::Mul { dst, lhs, rhs },
3489        Divide => Op::Div { dst, lhs, rhs },
3490        ExactDivide => Op::ExactDiv { dst, lhs, rhs },
3491        FloorDivide => Op::FloorDiv { dst, lhs, rhs },
3492        Modulo => Op::Mod { dst, lhs, rhs },
3493        Lt => Op::Lt { dst, lhs, rhs },
3494        Gt => Op::Gt { dst, lhs, rhs },
3495        LtEq => Op::LtEq { dst, lhs, rhs },
3496        GtEq => Op::GtEq { dst, lhs, rhs },
3497        Eq => Op::Eq { dst, lhs, rhs },
3498        ApproxEq => Op::ApproxEq { dst, lhs, rhs },
3499        NotEq => Op::NotEq { dst, lhs, rhs },
3500        Concat => Op::Concat { dst, lhs, rhs },
3501        SeqConcat => Op::SeqConcat { dst, lhs, rhs },
3502        Pow => Op::Pow { dst, lhs, rhs },
3503        BitXor => Op::BitXor { dst, lhs, rhs },
3504        BitAnd => Op::BitAnd { dst, lhs, rhs },
3505        BitOr => Op::BitOr { dst, lhs, rhs },
3506        Shl => Op::Shl { dst, lhs, rhs },
3507        Shr => Op::Shr { dst, lhs, rhs },
3508        // And/Or are compiled by `compile_short_circuit`, never through here.
3509        And | Or => return Err("vm: internal — And/Or must use compile_short_circuit".to_string()),
3510    })
3511}