Skip to main content

logicaffeine_compile/vm/
mod.rs

1//! Register bytecode VM (work/VM_PLAN.md).
2//!
3//! A fast, portable interpreter that runs the same `RuntimeValue` semantics as
4//! the tree-walker. It is the browser/WASM execution engine and the substrate
5//! the native copy-and-patch JIT tiers up from. Built incrementally, RED-first,
6//! differential-tested against the tree-walker.
7
8pub mod compiler;
9// Off-thread native compilation (HOTSWAP §6). Native-only: it needs std::thread and
10// the forge backend, neither of which exists on wasm (which never installs a tier).
11#[cfg(not(target_arch = "wasm32"))]
12mod bg_compile;
13// AOT-native tier loader (HOTSWAP §Axis-3): dlopen a rustc-built cdylib. Native-only.
14#[cfg(not(target_arch = "wasm32"))]
15pub mod aot_tier;
16// Background AOT-native compilation (HOTSWAP §Axis-3 / P18): build+load off-thread.
17#[cfg(not(target_arch = "wasm32"))]
18pub mod bg_aot;
19// Relocatable per-function bytecode units (HOTSWAP §7) — the producer behind the
20// Axis-1 warm side-table and the OPFS tier cache. Pure data; available on all targets.
21pub mod fn_bytecode;
22// LOGOS_TIER_TRACE observability (HOTSWAP §P13): one stderr line per tier hot-swap.
23pub mod tier_trace;
24// Tier cache (HOTSWAP §P12): persist a compiled FnBytecode keyed by source+config+tier.
25pub mod tier_cache;
26#[cfg(test)]
27mod fuzz;
28/// Bytecode disassembler — readable text for the Studio debug drawer and any
29/// future `largo --disasm`. Pure, headless, no VM state.
30pub mod disasm;
31mod instruction;
32mod machine;
33/// WS-F representation overhaul: the narrow (8-byte) NaN-boxed register-file
34/// value (see the module docs). Under `feature = "narrow-value"` it backs
35/// [`value::Value`] and the VM register file; the default build keeps the fat
36/// `RuntimeValue` path, so the scalar accessors/arith here are exercised only by
37/// the in-module tests then. `allow(dead_code)` keeps the default build quiet
38/// about the methods only the narrow path calls.
39#[cfg_attr(not(feature = "narrow-value"), allow(dead_code))]
40mod nanbox;
41mod native_tier;
42mod value;
43
44/// WebAssembly code generation from VM bytecode: the shared byte emitter (`wasm::encode` +
45/// `wasm::func`, always built) plus the WS6 browser WASM-JIT tier (`wasm::region_jit`, behind
46/// the `wasm-jit` feature). The direct AOT backend (`largo build --emit wasm`) consumes the
47/// same emitter.
48pub mod wasm;
49
50/// Back-compat facade: the WS6 browser WASM-JIT tier moved into the shared `wasm` codegen
51/// submodule — `wasm::func` for the byte emitter, `wasm::region_jit` for the tier + host.
52/// `machine.rs` (`super::wasm_jit::WasmTier`) and the `wasm_jit_*` differential/browser test
53/// suites still reach it here, unchanged.
54#[cfg(feature = "wasm-jit")]
55pub mod wasm_jit {
56    pub use super::wasm::func::{compile_function_to_wasm, compile_region_to_wasm};
57    pub use super::wasm::region_jit::WasmTier;
58    #[cfg(target_arch = "wasm32")]
59    pub use super::wasm::region_jit::run_on_host;
60}
61
62pub use compiler::Compiler;
63pub use disasm::{disassemble, format_constant, op_io, DisasmLine, OpIo};
64pub use instruction::{ChanElem, CompiledProgram, Constant, Op};
65pub use machine::Vm;
66pub(crate) use machine::{DebugFrameView, DebugView, DebugVmState, HeapObjView};
67pub(crate) use machine::{VmBlock, VmStep};
68pub use native_tier::{
69    install_native_tier, installed_native_tier, ArrayPin, CalleeSig, FnTable, HoistGuard, NativeCtx,
70    NativeFn,
71    NativeFrame, NativeOutcome, NativeRet, NativeTier, ObservedKind, ParamKind, PinElem,
72    RegBox, RegionFn, RegionOutcome, RegionReturn, RegionReturnKind, SlotKind,
73    NATIVE_TIER_THRESHOLD, REGION_TIER_THRESHOLD,
74};
75pub use value::Value;
76
77use crate::ast::stmt::Stmt;
78use crate::intern::Interner;
79
80/// Hard cap on registers a single frame may claim. Generous for real programs;
81/// prevents a pathological frame from claiming the whole register file.
82pub const MAX_REGISTERS_PER_FRAME: usize = 16_384;
83/// Hard cap on the total register file across all live frames. Deep recursion
84/// hits this and errors instead of consuming unbounded memory.
85pub const MAX_REGISTER_FILE: usize = 1 << 20;
86/// Maximum expression nesting depth the compiler will descend before erroring
87/// (guards the compiler's own recursion against adversarial inputs). Debug
88/// builds allocate every match arm's locals in `compile_expr_into_inner`'s
89/// frame — measured in the tens of KiB per nesting level as the compiler
90/// grows — so 64 keeps the worst case well inside a default 2 MiB
91/// test-thread stack while still exceeding any realistic expression depth.
92/// (An explicit-work-stack compiler would remove the native-stack coupling
93/// entirely; tracked as future work.)
94pub const MAX_EXPR_DEPTH: usize = 64;
95
96/// Shannon entropy of a two-outcome branch profile, in bits (EXODIA 3.3).
97/// 0.0 = perfectly predictable (the hardware branch predictor wins; Tier-1
98/// code is fine), 1.0 = a coin flip (Tier-2 should restructure the branch
99/// away). Zero samples are zero entropy.
100pub fn branch_entropy(taken: u64, not_taken: u64) -> f64 {
101    let total = taken + not_taken;
102    if total == 0 || taken == 0 || not_taken == 0 {
103        return 0.0;
104    }
105    let p = taken as f64 / total as f64;
106    -(p * p.log2() + (1.0 - p) * (1.0 - p).log2())
107}
108
109/// Compile a statement block to bytecode and run it, returning captured output.
110pub fn compile_and_run(stmts: &[Stmt], interner: &Interner) -> Result<String, String> {
111    let program = Compiler::compile(stmts, interner)?;
112    let mut vm = Vm::new(&program);
113    if let Some(tier) = installed_native_tier() {
114        vm = vm.with_native_tier(tier);
115    }
116    vm.run()?;
117    Ok(vm.output().to_string())
118}
119
120/// Compile and run, preserving partial output alongside any error — the shape
121/// the differential harness compares against the tree-walker (which also keeps
122/// the lines emitted before a runtime error). `types` carries the discovery
123/// pass's struct definitions (for default-fill on construction).
124pub fn run_to_outcome(
125    stmts: &[Stmt],
126    interner: &Interner,
127    types: Option<&crate::analysis::TypeRegistry>,
128    policies: Option<&crate::analysis::PolicyRegistry>,
129) -> (String, Option<String>) {
130    let oracle = crate::optimize::oracle_analyze_with(stmts, interner);
131    let program = match Compiler::compile_with_oracle(stmts, interner, types, Some(oracle)) {
132        Ok(p) => p,
133        Err(e) => return (String::new(), Some(e)),
134    };
135    let mut vm = Vm::new(&program);
136    if let Some(tier) = installed_native_tier() {
137        vm = vm.with_native_tier(tier);
138    }
139    if let Some(registry) = policies {
140        vm = vm.with_policy_ctx(registry, interner);
141    }
142    match vm.run() {
143        Ok(()) => (vm.output().to_string(), None),
144        Err(e) => (vm.output().to_string(), Some(e)),
145    }
146}
147
148/// [`run_to_outcome`] with the program argument vector for the `args()`
149/// system native (full argv; index 0 is the program name) and an optional
150/// caller-supplied native tier. A `Some(tier)` overrides the process-wide
151/// installed tier — differential suites use a private tier per program so
152/// its compile counters are isolated from every other test in the binary.
153pub fn run_to_outcome_with_args(
154    stmts: &[Stmt],
155    interner: &Interner,
156    types: Option<&crate::analysis::TypeRegistry>,
157    policies: Option<&crate::analysis::PolicyRegistry>,
158    program_args: &[String],
159    tier: Option<&dyn NativeTier>,
160) -> (String, Option<String>) {
161    let oracle = crate::optimize::oracle_analyze_with(stmts, interner);
162    let program = match Compiler::compile_with_oracle(stmts, interner, types, Some(oracle)) {
163        Ok(p) => p,
164        Err(e) => return (String::new(), Some(e)),
165    };
166    let mut vm = Vm::new(&program).with_program_args(program_args.to_vec());
167    let chosen: Option<&dyn NativeTier> = match tier {
168        Some(t) => Some(t),
169        None => installed_native_tier().map(|t| t as &dyn NativeTier),
170    };
171    if let Some(t) = chosen {
172        vm = vm.with_native_tier(t);
173    }
174    if let Some(registry) = policies {
175        vm = vm.with_policy_ctx(registry, interner);
176    }
177    match vm.run() {
178        Ok(()) => (vm.output().to_string(), None),
179        Err(e) => (vm.output().to_string(), Some(e)),
180    }
181}
182
183/// [`run_to_outcome_with_args`] with BACKGROUND native compilation (HOTSWAP §6): hot
184/// functions compile on a worker thread while the interpreter keeps running bytecode,
185/// then their chains are drained + published. Requires a process-installed tier (the
186/// worker needs a `&'static` backend); with none installed it runs pure bytecode.
187/// Drains outstanding compiles before returning so the native tier engages
188/// deterministically for the differential gates.
189#[cfg(not(target_arch = "wasm32"))]
190pub fn run_to_outcome_bg(
191    stmts: &[Stmt],
192    interner: &Interner,
193    types: Option<&crate::analysis::TypeRegistry>,
194    policies: Option<&crate::analysis::PolicyRegistry>,
195    program_args: &[String],
196) -> (String, Option<String>) {
197    let oracle = crate::optimize::oracle_analyze_with(stmts, interner);
198    let program = match Compiler::compile_with_oracle(stmts, interner, types, Some(oracle)) {
199        Ok(p) => p,
200        Err(e) => return (String::new(), Some(e)),
201    };
202    let mut vm = Vm::new(&program).with_program_args(program_args.to_vec());
203    if let Some(t) = installed_native_tier() {
204        vm = vm.with_bg_native_tier(t);
205    }
206    if let Some(registry) = policies {
207        vm = vm.with_policy_ctx(registry, interner);
208    }
209    let result = match vm.run() {
210        Ok(()) => (vm.output().to_string(), None),
211        Err(e) => (vm.output().to_string(), Some(e)),
212    };
213    vm.drain_pending_compiles();
214    result
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::arena::Arena;
221    use crate::ast::stmt::{BinaryOpKind, Expr, Literal, Stmt};
222    use crate::intern::Interner;
223    use std::cell::RefCell;
224    use std::rc::Rc;
225
226    /// Run a program through the production tree-walker, capturing its output —
227    /// the independent oracle every VM result is checked against.
228    fn run_treewalk(stmts: &[Stmt], interner: &Interner) -> Result<String, String> {
229        use crate::interpreter::{Interpreter, OutputCallback};
230        let buf = Rc::new(RefCell::new(String::new()));
231        let sink = buf.clone();
232        let cb: OutputCallback = Rc::new(RefCell::new(move |s: String| {
233            sink.borrow_mut().push_str(&s);
234            sink.borrow_mut().push('\n');
235        }));
236        let mut interp = Interpreter::new(interner).with_output_callback(cb);
237        interp.run_sync(stmts)?;
238        let out = buf.borrow().clone();
239        Ok(out)
240    }
241
242    fn normalize(s: &str) -> String {
243        s.lines()
244            .map(|l| l.trim_end())
245            .filter(|l| !l.is_empty())
246            .collect::<Vec<_>>()
247            .join("\n")
248    }
249
250    /// The differential gate: the VM's observable output must equal the
251    /// tree-walker's for the same program.
252    fn assert_vm_eq_treewalk(stmts: &[Stmt], interner: &Interner) {
253        let vm_out = compile_and_run(stmts, interner).expect("vm run failed");
254        let tw_out = run_treewalk(stmts, interner).expect("tree-walker run failed");
255        assert_eq!(
256            normalize(&vm_out),
257            normalize(&tw_out),
258            "VM output diverged from the tree-walker oracle"
259        );
260    }
261
262    // ---- AST builder helpers (keep the function tests readable) ---------------
263
264    use crate::ast::stmt::TypeExpr;
265    use crate::intern::Symbol;
266
267    fn num<'a>(ea: &'a Arena<Expr<'a>>, n: i64) -> &'a Expr<'a> {
268        ea.alloc(Expr::Literal(Literal::Number(n)))
269    }
270    fn idref<'a>(ea: &'a Arena<Expr<'a>>, s: Symbol) -> &'a Expr<'a> {
271        ea.alloc(Expr::Identifier(s))
272    }
273    fn bin<'a>(ea: &'a Arena<Expr<'a>>, op: BinaryOpKind, l: &'a Expr<'a>, r: &'a Expr<'a>) -> &'a Expr<'a> {
274        ea.alloc(Expr::BinaryOp { op, left: l, right: r })
275    }
276    fn calle<'a>(ea: &'a Arena<Expr<'a>>, f: Symbol, args: Vec<&'a Expr<'a>>) -> &'a Expr<'a> {
277        ea.alloc(Expr::Call { function: f, args })
278    }
279    fn letb<'a>(var: Symbol, value: &'a Expr<'a>) -> Stmt<'a> {
280        Stmt::Let { var, ty: None, value, mutable: false }
281    }
282    fn ret<'a>(value: &'a Expr<'a>) -> Stmt<'a> {
283        Stmt::Return { value: Some(value) }
284    }
285    fn show<'a>(ea: &'a Arena<Expr<'a>>, show_sym: Symbol, obj: &'a Expr<'a>) -> Stmt<'a> {
286        Stmt::Show { object: obj, recipient: idref(ea, show_sym) }
287    }
288    fn fndef<'a>(
289        name: Symbol,
290        params: Vec<(Symbol, &'a TypeExpr<'a>)>,
291        body: &'a [Stmt<'a>],
292    ) -> Stmt<'a> {
293        Stmt::FunctionDef {
294            name,
295            generics: vec![],
296            params,
297            body,
298            return_type: None,
299            is_native: false,
300            native_path: None,
301            is_exported: false,
302            export_target: None,
303            opt_flags: Default::default(),
304        }
305    }
306
307    #[test]
308    fn vm_function_single_param() {
309        // ## To double (n: Int) -> Int: Return n * 2.
310        // Main: Let r be double(21). Show r.   → 42
311        let ea: Arena<Expr> = Arena::new();
312        let sa: Arena<Stmt> = Arena::new();
313        let ta: Arena<TypeExpr> = Arena::new();
314        let mut it = Interner::new();
315        let double = it.intern("double");
316        let n = it.intern("n");
317        let r = it.intern("r");
318        let show_s = it.intern("show");
319        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
320
321        let body: &[Stmt] = sa.alloc_slice(vec![ret(bin(&ea, BinaryOpKind::Multiply, idref(&ea, n), num(&ea, 2)))]);
322        let main_call = calle(&ea, double, vec![num(&ea, 21)]);
323
324        let stmts = vec![
325            fndef(double, vec![(n, int_ty)], body),
326            letb(r, main_call),
327            show(&ea, show_s, idref(&ea, r)),
328        ];
329        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "42");
330        assert_vm_eq_treewalk(&stmts, &it);
331    }
332
333    #[test]
334    fn vm_function_two_params() {
335        // ## To add (a, b): Return a + b.  Main: Show add(3, 4).   → 7
336        let ea: Arena<Expr> = Arena::new();
337        let sa: Arena<Stmt> = Arena::new();
338        let ta: Arena<TypeExpr> = Arena::new();
339        let mut it = Interner::new();
340        let add = it.intern("add");
341        let a = it.intern("a");
342        let b = it.intern("b");
343        let r = it.intern("r");
344        let show_s = it.intern("show");
345        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
346
347        let body: &[Stmt] = sa.alloc_slice(vec![ret(bin(&ea, BinaryOpKind::Add, idref(&ea, a), idref(&ea, b)))]);
348        let stmts = vec![
349            fndef(add, vec![(a, int_ty), (b, int_ty)], body),
350            letb(r, calle(&ea, add, vec![num(&ea, 3), num(&ea, 4)])),
351            show(&ea, show_s, idref(&ea, r)),
352        ];
353        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "7");
354        assert_vm_eq_treewalk(&stmts, &it);
355    }
356
357    #[test]
358    fn vm_function_recursion_factorial() {
359        // ## To factorial (n: Int): If n <= 1: Return 1. Return n * factorial(n - 1).
360        // Main: Show factorial(5).   → 120
361        let ea: Arena<Expr> = Arena::new();
362        let sa: Arena<Stmt> = Arena::new();
363        let ta: Arena<TypeExpr> = Arena::new();
364        let mut it = Interner::new();
365        let factorial = it.intern("factorial");
366        let n = it.intern("n");
367        let r = it.intern("r");
368        let show_s = it.intern("show");
369        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
370
371        let then_blk: &[Stmt] = sa.alloc_slice(vec![ret(num(&ea, 1))]);
372        let cond = bin(&ea, BinaryOpKind::LtEq, idref(&ea, n), num(&ea, 1));
373        let rec = calle(&ea, factorial, vec![bin(&ea, BinaryOpKind::Subtract, idref(&ea, n), num(&ea, 1))]);
374        let tail = ret(bin(&ea, BinaryOpKind::Multiply, idref(&ea, n), rec));
375        let body: &[Stmt] = sa.alloc_slice(vec![
376            Stmt::If { cond, then_block: then_blk, else_block: None },
377            tail,
378        ]);
379
380        let stmts = vec![
381            fndef(factorial, vec![(n, int_ty)], body),
382            letb(r, calle(&ea, factorial, vec![num(&ea, 5)])),
383            show(&ea, show_s, idref(&ea, r)),
384        ];
385        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "120");
386        assert_vm_eq_treewalk(&stmts, &it);
387    }
388
389    #[test]
390    fn vm_function_nested_calls() {
391        // triple(n)=n*3, double(n)=n*2.  Main: Show double(triple(2)).   → 12
392        let ea: Arena<Expr> = Arena::new();
393        let sa: Arena<Stmt> = Arena::new();
394        let ta: Arena<TypeExpr> = Arena::new();
395        let mut it = Interner::new();
396        let triple = it.intern("triple");
397        let double = it.intern("double");
398        let n = it.intern("n");
399        let r = it.intern("r");
400        let show_s = it.intern("show");
401        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
402
403        let triple_body: &[Stmt] = sa.alloc_slice(vec![ret(bin(&ea, BinaryOpKind::Multiply, idref(&ea, n), num(&ea, 3)))]);
404        let double_body: &[Stmt] = sa.alloc_slice(vec![ret(bin(&ea, BinaryOpKind::Multiply, idref(&ea, n), num(&ea, 2)))]);
405        let inner = calle(&ea, triple, vec![num(&ea, 2)]);
406        let outer = calle(&ea, double, vec![inner]);
407        let stmts = vec![
408            fndef(triple, vec![(n, int_ty)], triple_body),
409            fndef(double, vec![(n, int_ty)], double_body),
410            letb(r, outer),
411            show(&ea, show_s, idref(&ea, r)),
412        ];
413        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "12");
414        assert_vm_eq_treewalk(&stmts, &it);
415    }
416
417    #[test]
418    fn vm_function_with_local_variable() {
419        // ## To compute (x): Let y be x * x. Return y + 1.  Main: Show compute(4).   → 17
420        let ea: Arena<Expr> = Arena::new();
421        let sa: Arena<Stmt> = Arena::new();
422        let ta: Arena<TypeExpr> = Arena::new();
423        let mut it = Interner::new();
424        let compute = it.intern("compute");
425        let x = it.intern("x");
426        let y = it.intern("y");
427        let r = it.intern("r");
428        let show_s = it.intern("show");
429        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
430
431        let body: &[Stmt] = sa.alloc_slice(vec![
432            letb(y, bin(&ea, BinaryOpKind::Multiply, idref(&ea, x), idref(&ea, x))),
433            ret(bin(&ea, BinaryOpKind::Add, idref(&ea, y), num(&ea, 1))),
434        ]);
435        let stmts = vec![
436            fndef(compute, vec![(x, int_ty)], body),
437            letb(r, calle(&ea, compute, vec![num(&ea, 4)])),
438            show(&ea, show_s, idref(&ea, r)),
439        ];
440        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "17");
441        assert_vm_eq_treewalk(&stmts, &it);
442    }
443
444    #[test]
445    fn vm_function_conditional_return_fallthrough() {
446        // ## To sign (n): If n > 0: Return 1. Return 0.
447        // Main: Show sign(5). Show sign(-3).   → 1, 0
448        let ea: Arena<Expr> = Arena::new();
449        let sa: Arena<Stmt> = Arena::new();
450        let ta: Arena<TypeExpr> = Arena::new();
451        let mut it = Interner::new();
452        let sign = it.intern("sign");
453        let n = it.intern("n");
454        let p = it.intern("p");
455        let q = it.intern("q");
456        let show_s = it.intern("show");
457        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
458
459        let then_blk: &[Stmt] = sa.alloc_slice(vec![ret(num(&ea, 1))]);
460        let body: &[Stmt] = sa.alloc_slice(vec![
461            Stmt::If { cond: bin(&ea, BinaryOpKind::Gt, idref(&ea, n), num(&ea, 0)), then_block: then_blk, else_block: None },
462            ret(num(&ea, 0)),
463        ]);
464        let stmts = vec![
465            fndef(sign, vec![(n, int_ty)], body),
466            letb(p, calle(&ea, sign, vec![num(&ea, 5)])),
467            show(&ea, show_s, idref(&ea, p)),
468            letb(q, calle(&ea, sign, vec![num(&ea, -3)])),
469            show(&ea, show_s, idref(&ea, q)),
470        ];
471        let out = compile_and_run(&stmts, &it).unwrap();
472        let lines: Vec<&str> = out.lines().collect();
473        assert_eq!(lines[0], "1");
474        assert_eq!(lines[1], "0");
475        assert_vm_eq_treewalk(&stmts, &it);
476    }
477
478    // ---- Collection builder helpers ----------------------------------------
479
480    fn list_lit<'a>(ea: &'a Arena<Expr<'a>>, items: Vec<&'a Expr<'a>>) -> &'a Expr<'a> {
481        ea.alloc(Expr::List(items))
482    }
483    fn new_coll<'a>(ea: &'a Arena<Expr<'a>>, type_name: Symbol) -> &'a Expr<'a> {
484        ea.alloc(Expr::New { type_name, type_args: vec![], init_fields: vec![] })
485    }
486    fn length_of<'a>(ea: &'a Arena<Expr<'a>>, c: &'a Expr<'a>) -> &'a Expr<'a> {
487        ea.alloc(Expr::Length { collection: c })
488    }
489    fn index_at<'a>(ea: &'a Arena<Expr<'a>>, c: &'a Expr<'a>, i: &'a Expr<'a>) -> &'a Expr<'a> {
490        ea.alloc(Expr::Index { collection: c, index: i })
491    }
492    fn contains_e<'a>(ea: &'a Arena<Expr<'a>>, c: &'a Expr<'a>, v: &'a Expr<'a>) -> &'a Expr<'a> {
493        ea.alloc(Expr::Contains { collection: c, value: v })
494    }
495    fn range_e<'a>(ea: &'a Arena<Expr<'a>>, s: &'a Expr<'a>, e: &'a Expr<'a>) -> &'a Expr<'a> {
496        ea.alloc(Expr::Range { start: s, end: e })
497    }
498    fn push_to<'a>(value: &'a Expr<'a>, collection: &'a Expr<'a>) -> Stmt<'a> {
499        Stmt::Push { value, collection }
500    }
501
502    #[test]
503    fn vm_list_literal_and_length() {
504        // Let xs be [10, 20, 30]. Show length of xs.   → 3
505        let ea: Arena<Expr> = Arena::new();
506        let mut it = Interner::new();
507        let xs = it.intern("xs");
508        let show_s = it.intern("show");
509        let lst = list_lit(&ea, vec![num(&ea, 10), num(&ea, 20), num(&ea, 30)]);
510        let stmts = vec![
511            letb(xs, lst),
512            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
513        ];
514        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "3");
515        assert_vm_eq_treewalk(&stmts, &it);
516    }
517
518    #[test]
519    fn vm_new_seq_push_length() {
520        // Let xs be a new Seq of Int. Push 10 to xs. Push 20 to xs. Show length of xs.   → 2
521        let ea: Arena<Expr> = Arena::new();
522        let mut it = Interner::new();
523        let xs = it.intern("xs");
524        let seq = it.intern("Seq");
525        let show_s = it.intern("show");
526        let stmts = vec![
527            Stmt::Let { var: xs, ty: None, value: new_coll(&ea, seq), mutable: true },
528            push_to(num(&ea, 10), idref(&ea, xs)),
529            push_to(num(&ea, 20), idref(&ea, xs)),
530            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
531        ];
532        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "2");
533        assert_vm_eq_treewalk(&stmts, &it);
534    }
535
536    #[test]
537    fn vm_empty_seq_length_is_zero() {
538        let ea: Arena<Expr> = Arena::new();
539        let mut it = Interner::new();
540        let xs = it.intern("xs");
541        let seq = it.intern("Seq");
542        let show_s = it.intern("show");
543        let stmts = vec![
544            Stmt::Let { var: xs, ty: None, value: new_coll(&ea, seq), mutable: true },
545            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
546        ];
547        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "0");
548        assert_vm_eq_treewalk(&stmts, &it);
549    }
550
551    #[test]
552    fn vm_list_index_one_based() {
553        // Let xs be [5, 6, 7]. Show xs at 2.   → 6
554        let ea: Arena<Expr> = Arena::new();
555        let mut it = Interner::new();
556        let xs = it.intern("xs");
557        let show_s = it.intern("show");
558        let lst = list_lit(&ea, vec![num(&ea, 5), num(&ea, 6), num(&ea, 7)]);
559        let stmts = vec![
560            letb(xs, lst),
561            show(&ea, show_s, index_at(&ea, idref(&ea, xs), num(&ea, 2))),
562        ];
563        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "6");
564        assert_vm_eq_treewalk(&stmts, &it);
565    }
566
567    #[test]
568    fn vm_range_to_list() {
569        // Let xs be 1 to 5. Show length of xs. Show xs at 3.   → 5, 3
570        let ea: Arena<Expr> = Arena::new();
571        let mut it = Interner::new();
572        let xs = it.intern("xs");
573        let show_s = it.intern("show");
574        let stmts = vec![
575            letb(xs, range_e(&ea, num(&ea, 1), num(&ea, 5))),
576            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
577            show(&ea, show_s, index_at(&ea, idref(&ea, xs), num(&ea, 3))),
578        ];
579        let out = compile_and_run(&stmts, &it).unwrap();
580        let lines: Vec<&str> = out.lines().collect();
581        assert_eq!(lines[0], "5");
582        assert_eq!(lines[1], "3");
583        assert_vm_eq_treewalk(&stmts, &it);
584    }
585
586    #[test]
587    fn vm_list_contains() {
588        // Let xs be [1, 2, 3]. Show xs contains 2. Show xs contains 5.   → true, false
589        let ea: Arena<Expr> = Arena::new();
590        let mut it = Interner::new();
591        let xs = it.intern("xs");
592        let show_s = it.intern("show");
593        let lst = list_lit(&ea, vec![num(&ea, 1), num(&ea, 2), num(&ea, 3)]);
594        let stmts = vec![
595            letb(xs, lst),
596            show(&ea, show_s, contains_e(&ea, idref(&ea, xs), num(&ea, 2))),
597            show(&ea, show_s, contains_e(&ea, idref(&ea, xs), num(&ea, 5))),
598        ];
599        let out = compile_and_run(&stmts, &it).unwrap();
600        let lines: Vec<&str> = out.lines().collect();
601        assert_eq!(lines[0], "true");
602        assert_eq!(lines[1], "false");
603        assert_vm_eq_treewalk(&stmts, &it);
604    }
605
606    #[test]
607    fn vm_push_then_index_sum_loop() {
608        // Build [2,4,6] by pushing; sum via 1-based indexing in a While loop.   → 12
609        let ea: Arena<Expr> = Arena::new();
610        let sa: Arena<Stmt> = Arena::new();
611        let mut it = Interner::new();
612        let xs = it.intern("xs");
613        let seq = it.intern("Seq");
614        let total = it.intern("total");
615        let i = it.intern("i");
616        let show_s = it.intern("show");
617
618        let cond = bin(&ea, BinaryOpKind::LtEq, idref(&ea, i), length_of(&ea, idref(&ea, xs)));
619        let elem = index_at(&ea, idref(&ea, xs), idref(&ea, i));
620        let body: &[Stmt] = sa.alloc_slice(vec![
621            Stmt::Set { target: total, value: bin(&ea, BinaryOpKind::Add, idref(&ea, total), elem) },
622            Stmt::Set { target: i, value: bin(&ea, BinaryOpKind::Add, idref(&ea, i), num(&ea, 1)) },
623        ]);
624
625        let stmts = vec![
626            Stmt::Let { var: xs, ty: None, value: new_coll(&ea, seq), mutable: true },
627            push_to(num(&ea, 2), idref(&ea, xs)),
628            push_to(num(&ea, 4), idref(&ea, xs)),
629            push_to(num(&ea, 6), idref(&ea, xs)),
630            Stmt::Let { var: total, ty: None, value: num(&ea, 0), mutable: true },
631            Stmt::Let { var: i, ty: None, value: num(&ea, 1), mutable: true },
632            Stmt::While { cond, body, decreasing: None },
633            show(&ea, show_s, idref(&ea, total)),
634        ];
635        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "12");
636        assert_vm_eq_treewalk(&stmts, &it);
637    }
638
639    #[test]
640    fn vm_index_out_of_bounds_errors_like_treewalk() {
641        // Let xs be [1, 2]. Show xs at 5.   → both VM and tree-walker error.
642        let ea: Arena<Expr> = Arena::new();
643        let mut it = Interner::new();
644        let xs = it.intern("xs");
645        let show_s = it.intern("show");
646        let lst = list_lit(&ea, vec![num(&ea, 1), num(&ea, 2)]);
647        let stmts = vec![
648            letb(xs, lst),
649            show(&ea, show_s, index_at(&ea, idref(&ea, xs), num(&ea, 5))),
650        ];
651        assert!(compile_and_run(&stmts, &it).is_err(), "VM should error on OOB index");
652        assert!(run_treewalk(&stmts, &it).is_err(), "tree-walker should error on OOB index");
653    }
654
655    fn text<'a>(ea: &'a Arena<Expr<'a>>, sym: Symbol) -> &'a Expr<'a> {
656        ea.alloc(Expr::Literal(Literal::Text(sym)))
657    }
658
659    #[test]
660    fn vm_set_add_dedups() {
661        // new Set; Add 1; Add 2; Add 1 (dup). Show length.   → 2
662        let ea: Arena<Expr> = Arena::new();
663        let mut it = Interner::new();
664        let s = it.intern("s");
665        let set_ty = it.intern("Set");
666        let show_s = it.intern("show");
667        let stmts = vec![
668            Stmt::Let { var: s, ty: None, value: new_coll(&ea, set_ty), mutable: true },
669            Stmt::Add { value: num(&ea, 1), collection: idref(&ea, s) },
670            Stmt::Add { value: num(&ea, 2), collection: idref(&ea, s) },
671            Stmt::Add { value: num(&ea, 1), collection: idref(&ea, s) },
672            show(&ea, show_s, length_of(&ea, idref(&ea, s))),
673        ];
674        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "2");
675        assert_vm_eq_treewalk(&stmts, &it);
676    }
677
678    #[test]
679    fn vm_set_contains_and_remove() {
680        // new Set; Add 1,2,3; Remove 2; Show length; Show s contains 2; Show s contains 3.
681        let ea: Arena<Expr> = Arena::new();
682        let mut it = Interner::new();
683        let s = it.intern("s");
684        let set_ty = it.intern("Set");
685        let show_s = it.intern("show");
686        let stmts = vec![
687            Stmt::Let { var: s, ty: None, value: new_coll(&ea, set_ty), mutable: true },
688            Stmt::Add { value: num(&ea, 1), collection: idref(&ea, s) },
689            Stmt::Add { value: num(&ea, 2), collection: idref(&ea, s) },
690            Stmt::Add { value: num(&ea, 3), collection: idref(&ea, s) },
691            Stmt::Remove { value: num(&ea, 2), collection: idref(&ea, s) },
692            show(&ea, show_s, length_of(&ea, idref(&ea, s))),
693            show(&ea, show_s, contains_e(&ea, idref(&ea, s), num(&ea, 2))),
694            show(&ea, show_s, contains_e(&ea, idref(&ea, s), num(&ea, 3))),
695        ];
696        let out = compile_and_run(&stmts, &it).unwrap();
697        let lines: Vec<&str> = out.lines().collect();
698        assert_eq!(lines, vec!["2", "false", "true"]);
699        assert_vm_eq_treewalk(&stmts, &it);
700    }
701
702    #[test]
703    fn vm_map_set_get_length() {
704        // new Map; Set item "a" of m to 10; Set item "b" of m to 20.
705        // Show item "a" of m; Show length of m.   → 10, 2
706        let ea: Arena<Expr> = Arena::new();
707        let mut it = Interner::new();
708        let m = it.intern("m");
709        let map_ty = it.intern("Map");
710        let key_a = it.intern("a");
711        let key_b = it.intern("b");
712        let show_s = it.intern("show");
713        let stmts = vec![
714            Stmt::Let { var: m, ty: None, value: new_coll(&ea, map_ty), mutable: true },
715            Stmt::SetIndex { collection: idref(&ea, m), index: text(&ea, key_a), value: num(&ea, 10) },
716            Stmt::SetIndex { collection: idref(&ea, m), index: text(&ea, key_b), value: num(&ea, 20) },
717            show(&ea, show_s, index_at(&ea, idref(&ea, m), text(&ea, key_a))),
718            show(&ea, show_s, length_of(&ea, idref(&ea, m))),
719        ];
720        let out = compile_and_run(&stmts, &it).unwrap();
721        let lines: Vec<&str> = out.lines().collect();
722        assert_eq!(lines[0], "10");
723        assert_eq!(lines[1], "2");
724        assert_vm_eq_treewalk(&stmts, &it);
725    }
726
727    #[test]
728    fn vm_map_overwrite_and_remove() {
729        // new Map; Set "a"→1; Set "a"→9 (overwrite); Set "b"→2; Remove "a".
730        // Show length; Show item "b" of m.   → 1, 2
731        let ea: Arena<Expr> = Arena::new();
732        let mut it = Interner::new();
733        let m = it.intern("m");
734        let map_ty = it.intern("Map");
735        let key_a = it.intern("a");
736        let key_b = it.intern("b");
737        let show_s = it.intern("show");
738        let stmts = vec![
739            Stmt::Let { var: m, ty: None, value: new_coll(&ea, map_ty), mutable: true },
740            Stmt::SetIndex { collection: idref(&ea, m), index: text(&ea, key_a), value: num(&ea, 1) },
741            Stmt::SetIndex { collection: idref(&ea, m), index: text(&ea, key_a), value: num(&ea, 9) },
742            Stmt::SetIndex { collection: idref(&ea, m), index: text(&ea, key_b), value: num(&ea, 2) },
743            Stmt::Remove { value: text(&ea, key_a), collection: idref(&ea, m) },
744            show(&ea, show_s, length_of(&ea, idref(&ea, m))),
745            show(&ea, show_s, index_at(&ea, idref(&ea, m), text(&ea, key_b))),
746        ];
747        let out = compile_and_run(&stmts, &it).unwrap();
748        let lines: Vec<&str> = out.lines().collect();
749        assert_eq!(lines[0], "1");
750        assert_eq!(lines[1], "2");
751        assert_vm_eq_treewalk(&stmts, &it);
752    }
753
754    #[test]
755    fn vm_list_set_index_one_based() {
756        // Let xs be [1,2,3]. Set item 2 of xs to 99. Show xs at 2.   → 99
757        let ea: Arena<Expr> = Arena::new();
758        let mut it = Interner::new();
759        let xs = it.intern("xs");
760        let show_s = it.intern("show");
761        let lst = list_lit(&ea, vec![num(&ea, 1), num(&ea, 2), num(&ea, 3)]);
762        let stmts = vec![
763            Stmt::Let { var: xs, ty: None, value: lst, mutable: true },
764            Stmt::SetIndex { collection: idref(&ea, xs), index: num(&ea, 2), value: num(&ea, 99) },
765            show(&ea, show_s, index_at(&ea, idref(&ea, xs), num(&ea, 2))),
766        ];
767        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "99");
768        assert_vm_eq_treewalk(&stmts, &it);
769    }
770
771    // ---- Generative differential fuzzer (VM vs tree-walker) ------------------
772    //
773    // Generates random total (overflow-free) programs over Int variables with
774    // arithmetic, comparisons, conditionals, and bounded loops, then asserts the
775    // VM's behavior is identical to the tree-walker's. Any divergence is a real
776    // translation bug. Deterministic (SplitMix64) so failures reproduce by seed.
777
778    struct SplitMix64 {
779        state: u64,
780    }
781    impl SplitMix64 {
782        fn new(seed: u64) -> Self {
783            SplitMix64 { state: seed.wrapping_add(0x9E37_79B9_7F4A_7C15) }
784        }
785        fn next_u64(&mut self) -> u64 {
786            self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
787            let mut z = self.state;
788            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
789            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
790            z ^ (z >> 31)
791        }
792        fn below(&mut self, n: u64) -> u64 {
793            self.next_u64() % n
794        }
795    }
796
797    fn gen_atom<'a>(ea: &'a Arena<Expr<'a>>, rng: &mut SplitMix64, vars: &[Symbol]) -> &'a Expr<'a> {
798        if rng.below(2) == 0 {
799            num(ea, rng.below(6) as i64)
800        } else {
801            idref(ea, vars[rng.below(vars.len() as u64) as usize])
802        }
803    }
804
805    // depth-1 arithmetic over small atoms. +,- keep values tiny; * uses a small
806    // multiplier and / % use a non-zero literal divisor — all provably total and
807    // overflow-free for the bounded value range.
808    fn gen_arith<'a>(ea: &'a Arena<Expr<'a>>, rng: &mut SplitMix64, vars: &[Symbol]) -> &'a Expr<'a> {
809        match rng.below(8) {
810            0 | 1 | 2 => {
811                let l = gen_atom(ea, rng, vars);
812                let r = gen_atom(ea, rng, vars);
813                bin(ea, BinaryOpKind::Add, l, r)
814            }
815            3 | 4 => {
816                let l = gen_atom(ea, rng, vars);
817                let r = gen_atom(ea, rng, vars);
818                bin(ea, BinaryOpKind::Subtract, l, r)
819            }
820            5 => {
821                let l = gen_atom(ea, rng, vars);
822                bin(ea, BinaryOpKind::Multiply, l, num(ea, rng.below(4) as i64))
823            }
824            6 => {
825                let l = gen_atom(ea, rng, vars);
826                bin(ea, BinaryOpKind::Divide, l, num(ea, 1 + rng.below(5) as i64))
827            }
828            _ => {
829                let l = gen_atom(ea, rng, vars);
830                bin(ea, BinaryOpKind::Modulo, l, num(ea, 1 + rng.below(5) as i64))
831            }
832        }
833    }
834
835    fn gen_cmp<'a>(ea: &'a Arena<Expr<'a>>, rng: &mut SplitMix64, vars: &[Symbol]) -> &'a Expr<'a> {
836        let l = idref(ea, vars[rng.below(vars.len() as u64) as usize]);
837        let r = num(ea, rng.below(6) as i64);
838        let op = match rng.below(6) {
839            0 => BinaryOpKind::Lt,
840            1 => BinaryOpKind::Gt,
841            2 => BinaryOpKind::LtEq,
842            3 => BinaryOpKind::GtEq,
843            4 => BinaryOpKind::Eq,
844            _ => BinaryOpKind::NotEq,
845        };
846        bin(ea, op, l, r)
847    }
848
849    fn gen_program<'a>(
850        seed: u64,
851        ea: &'a Arena<Expr<'a>>,
852        sa: &'a Arena<Stmt<'a>>,
853        vars: &[Symbol],
854        show_s: Symbol,
855    ) -> Vec<Stmt<'a>> {
856        let mut rng = SplitMix64::new(seed);
857        let mut stmts: Vec<Stmt> = Vec::new();
858        for &v in vars {
859            stmts.push(Stmt::Let { var: v, ty: None, value: num(ea, rng.below(6) as i64), mutable: true });
860        }
861        let m = 4 + rng.below(8);
862        for _ in 0..m {
863            match rng.below(10) {
864                0..=5 => {
865                    let v = vars[rng.below(vars.len() as u64) as usize];
866                    stmts.push(Stmt::Set { target: v, value: gen_arith(ea, &mut rng, vars) });
867                }
868                6..=7 => {
869                    let cond = gen_cmp(ea, &mut rng, vars);
870                    let v = vars[rng.below(vars.len() as u64) as usize];
871                    let then_blk: &[Stmt] =
872                        sa.alloc_slice(vec![Stmt::Set { target: v, value: gen_arith(ea, &mut rng, vars) }]);
873                    if rng.below(2) == 0 {
874                        let v2 = vars[rng.below(vars.len() as u64) as usize];
875                        let else_blk: &[Stmt] =
876                            sa.alloc_slice(vec![Stmt::Set { target: v2, value: gen_arith(ea, &mut rng, vars) }]);
877                        stmts.push(Stmt::If { cond, then_block: then_blk, else_block: Some(else_blk) });
878                    } else {
879                        stmts.push(Stmt::If { cond, then_block: then_blk, else_block: None });
880                    }
881                }
882                _ => {
883                    // A terminating bounded loop: the loop variable is mutated ONLY
884                    // by its +1 increment, so it strictly increases to the bound.
885                    let li = rng.below(vars.len() as u64) as usize;
886                    let loop_var = vars[li];
887                    let other = vars[(li + 1 + rng.below(vars.len() as u64 - 1) as usize) % vars.len()];
888                    let bound = 2 + rng.below(4);
889                    let cond = bin(ea, BinaryOpKind::Lt, idref(ea, loop_var), num(ea, bound as i64));
890                    let body: &[Stmt] = sa.alloc_slice(vec![
891                        Stmt::Set { target: other, value: gen_arith(ea, &mut rng, vars) },
892                        Stmt::Set {
893                            target: loop_var,
894                            value: bin(ea, BinaryOpKind::Add, idref(ea, loop_var), num(ea, 1)),
895                        },
896                    ]);
897                    stmts.push(Stmt::While { cond, body, decreasing: None });
898                }
899            }
900        }
901        for &v in vars {
902            stmts.push(show(ea, show_s, idref(ea, v)));
903        }
904        stmts
905    }
906
907    #[test]
908    fn vm_differential_fuzz_300_programs() {
909        for seed in 0..300u64 {
910            let ea: Arena<Expr> = Arena::new();
911            let sa: Arena<Stmt> = Arena::new();
912            let mut it = Interner::new();
913            let show_s = it.intern("show");
914            let vars: Vec<Symbol> = (0..4u32).map(|k| it.intern(&format!("v{k}"))).collect();
915            let stmts = gen_program(seed, &ea, &sa, &vars, show_s);
916
917            let vm = compile_and_run(&stmts, &it);
918            let tw = run_treewalk(&stmts, &it);
919            match (vm, tw) {
920                (Ok(a), Ok(b)) => assert_eq!(
921                    normalize(&a),
922                    normalize(&b),
923                    "seed {} diverged:\nVM:\n{}\nTREE-WALKER:\n{}",
924                    seed, a, b
925                ),
926                (Err(_), Err(_)) => {}
927                (a, b) => panic!("seed {} one engine errored: vm={:?} tw={:?}", seed, a, b),
928            }
929        }
930    }
931
932    #[test]
933    fn vm_differential_fuzz_functions() {
934        for seed in 0..150u64 {
935            let ea: Arena<Expr> = Arena::new();
936            let sa: Arena<Stmt> = Arena::new();
937            let ta: Arena<TypeExpr> = Arena::new();
938            let mut it = Interner::new();
939            let show_s = it.intern("show");
940            let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
941            let mut rng = SplitMix64::new(seed ^ 0xF00D);
942
943            let f0 = it.intern("f0");
944            let f1 = it.intern("f1");
945            let pa = it.intern("pa");
946            let pb = it.intern("pb");
947            let params = [pa, pb];
948            // Function bodies are arithmetic over the parameters (divisors are
949            // non-zero literals, so no division-by-zero from arguments).
950            let body0: &[Stmt] = sa.alloc_slice(vec![ret(gen_arith(&ea, &mut rng, &params))]);
951            let body1: &[Stmt] = sa.alloc_slice(vec![ret(gen_arith(&ea, &mut rng, &params))]);
952            let mut stmts = vec![
953                fndef(f0, vec![(pa, int_ty), (pb, int_ty)], body0),
954                fndef(f1, vec![(pa, int_ty), (pb, int_ty)], body1),
955            ];
956            let calls = 3 + rng.below(4);
957            for _ in 0..calls {
958                let f = if rng.below(2) == 0 { f0 } else { f1 };
959                let a = num(&ea, rng.below(6) as i64);
960                let b = num(&ea, 1 + rng.below(5) as i64);
961                stmts.push(show(&ea, show_s, calle(&ea, f, vec![a, b])));
962            }
963
964            let vm = compile_and_run(&stmts, &it);
965            let tw = run_treewalk(&stmts, &it);
966            match (vm, tw) {
967                (Ok(a), Ok(b)) => assert_eq!(normalize(&a), normalize(&b), "fn seed {} diverged", seed),
968                (Err(_), Err(_)) => {}
969                (a, b) => panic!("fn seed {} one engine errored: vm={:?} tw={:?}", seed, a, b),
970            }
971        }
972    }
973
974    fn gen_float_lit<'a>(ea: &'a Arena<Expr<'a>>, rng: &mut SplitMix64) -> &'a Expr<'a> {
975        ea.alloc(Expr::Literal(Literal::Float(rng.below(10) as f64 * 0.5)))
976    }
977    fn gen_float_atom<'a>(ea: &'a Arena<Expr<'a>>, rng: &mut SplitMix64, vars: &[Symbol]) -> &'a Expr<'a> {
978        if rng.below(2) == 0 {
979            gen_float_lit(ea, rng)
980        } else {
981            idref(ea, vars[rng.below(vars.len() as u64) as usize])
982        }
983    }
984    // Float arithmetic uses only +,-,* (no division → no float div-by-zero),
985    // which keeps every program total; any NaN/Inf displays identically in both.
986    fn gen_float_arith<'a>(ea: &'a Arena<Expr<'a>>, rng: &mut SplitMix64, vars: &[Symbol]) -> &'a Expr<'a> {
987        let l = gen_float_atom(ea, rng, vars);
988        let r = gen_float_atom(ea, rng, vars);
989        let op = match rng.below(3) {
990            0 => BinaryOpKind::Add,
991            1 => BinaryOpKind::Subtract,
992            _ => BinaryOpKind::Multiply,
993        };
994        bin(ea, op, l, r)
995    }
996
997    #[test]
998    fn vm_differential_fuzz_floats() {
999        for seed in 0..200u64 {
1000            let ea: Arena<Expr> = Arena::new();
1001            let sa: Arena<Stmt> = Arena::new();
1002            let mut it = Interner::new();
1003            let show_s = it.intern("show");
1004            let vars: Vec<Symbol> = (0..3u32).map(|k| it.intern(&format!("f{k}"))).collect();
1005            let mut rng = SplitMix64::new(seed ^ 0xBEEF);
1006
1007            let mut stmts: Vec<Stmt> = Vec::new();
1008            for &v in &vars {
1009                stmts.push(Stmt::Let { var: v, ty: None, value: gen_float_lit(&ea, &mut rng), mutable: true });
1010            }
1011            let m = 4 + rng.below(6);
1012            for _ in 0..m {
1013                if rng.below(8) < 5 {
1014                    let v = vars[rng.below(vars.len() as u64) as usize];
1015                    stmts.push(Stmt::Set { target: v, value: gen_float_arith(&ea, &mut rng, &vars) });
1016                } else {
1017                    let l = idref(&ea, vars[rng.below(vars.len() as u64) as usize]);
1018                    let r = idref(&ea, vars[rng.below(vars.len() as u64) as usize]);
1019                    let op = match rng.below(4) {
1020                        0 => BinaryOpKind::Lt,
1021                        1 => BinaryOpKind::Gt,
1022                        2 => BinaryOpKind::LtEq,
1023                        _ => BinaryOpKind::GtEq,
1024                    };
1025                    let cond = bin(&ea, op, l, r);
1026                    let v = vars[rng.below(vars.len() as u64) as usize];
1027                    let then_blk: &[Stmt] =
1028                        sa.alloc_slice(vec![Stmt::Set { target: v, value: gen_float_arith(&ea, &mut rng, &vars) }]);
1029                    stmts.push(Stmt::If { cond, then_block: then_blk, else_block: None });
1030                }
1031            }
1032            for &v in &vars {
1033                stmts.push(show(&ea, show_s, idref(&ea, v)));
1034            }
1035
1036            let vm = compile_and_run(&stmts, &it);
1037            let tw = run_treewalk(&stmts, &it);
1038            match (vm, tw) {
1039                (Ok(a), Ok(b)) => assert_eq!(normalize(&a), normalize(&b), "float seed {} diverged", seed),
1040                (Err(_), Err(_)) => {}
1041                (a, b) => panic!("float seed {} one engine errored: vm={:?} tw={:?}", seed, a, b),
1042            }
1043        }
1044    }
1045
1046    #[test]
1047    fn vm_function_mutual_recursion() {
1048        // isEven(n): If n == 0: Return 1. Return isOdd(n - 1).
1049        // isOdd(n):  If n == 0: Return 0. Return isEven(n - 1).
1050        // Main: Show isEven(4).   → 1   (forward reference to isOdd)
1051        let ea: Arena<Expr> = Arena::new();
1052        let sa: Arena<Stmt> = Arena::new();
1053        let ta: Arena<TypeExpr> = Arena::new();
1054        let mut it = Interner::new();
1055        let is_even = it.intern("isEven");
1056        let is_odd = it.intern("isOdd");
1057        let n = it.intern("n");
1058        let r = it.intern("r");
1059        let show_s = it.intern("show");
1060        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
1061
1062        let even_then: &[Stmt] = sa.alloc_slice(vec![ret(num(&ea, 1))]);
1063        let even_body: &[Stmt] = sa.alloc_slice(vec![
1064            Stmt::If { cond: bin(&ea, BinaryOpKind::Eq, idref(&ea, n), num(&ea, 0)), then_block: even_then, else_block: None },
1065            ret(calle(&ea, is_odd, vec![bin(&ea, BinaryOpKind::Subtract, idref(&ea, n), num(&ea, 1))])),
1066        ]);
1067        let odd_then: &[Stmt] = sa.alloc_slice(vec![ret(num(&ea, 0))]);
1068        let odd_body: &[Stmt] = sa.alloc_slice(vec![
1069            Stmt::If { cond: bin(&ea, BinaryOpKind::Eq, idref(&ea, n), num(&ea, 0)), then_block: odd_then, else_block: None },
1070            ret(calle(&ea, is_even, vec![bin(&ea, BinaryOpKind::Subtract, idref(&ea, n), num(&ea, 1))])),
1071        ]);
1072        let stmts = vec![
1073            fndef(is_even, vec![(n, int_ty)], even_body),
1074            fndef(is_odd, vec![(n, int_ty)], odd_body),
1075            letb(r, calle(&ea, is_even, vec![num(&ea, 4)])),
1076            show(&ea, show_s, idref(&ea, r)),
1077        ];
1078        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "1");
1079        assert_vm_eq_treewalk(&stmts, &it);
1080    }
1081
1082    #[test]
1083    fn vm_runs_arithmetic_let_show() {
1084        // Let x be 5. Let y be x + 7. Show y.
1085        let ea: Arena<Expr> = Arena::new();
1086        let mut it = Interner::new();
1087        let x = it.intern("x");
1088        let y = it.intern("y");
1089        let console = it.intern("show");
1090
1091        let five = ea.alloc(Expr::Literal(Literal::Number(5)));
1092        let xref = ea.alloc(Expr::Identifier(x));
1093        let seven = ea.alloc(Expr::Literal(Literal::Number(7)));
1094        let sum = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Add, left: xref, right: seven });
1095        let yref = ea.alloc(Expr::Identifier(y));
1096        let console_ref = ea.alloc(Expr::Identifier(console));
1097
1098        let stmts = vec![
1099            Stmt::Let { var: x, ty: None, value: five, mutable: false },
1100            Stmt::Let { var: y, ty: None, value: sum, mutable: false },
1101            Stmt::Show { object: yref, recipient: console_ref },
1102        ];
1103
1104        let out = compile_and_run(&stmts, &it).unwrap();
1105        assert_eq!(out.trim(), "12");
1106        assert_vm_eq_treewalk(&stmts, &it);
1107    }
1108
1109    #[test]
1110    fn vm_arithmetic_and_comparison_chain() {
1111        // Let a be 6. Let b be a * 4. Let c be b - 2. Show c.   → 22
1112        // Let d be c > 20. Show d.                              → true
1113        let ea: Arena<Expr> = Arena::new();
1114        let mut it = Interner::new();
1115        let a = it.intern("a");
1116        let b = it.intern("b");
1117        let c = it.intern("c");
1118        let d = it.intern("d");
1119        let console = it.intern("show");
1120
1121        let six = ea.alloc(Expr::Literal(Literal::Number(6)));
1122        let aref = ea.alloc(Expr::Identifier(a));
1123        let four = ea.alloc(Expr::Literal(Literal::Number(4)));
1124        let mul = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Multiply, left: aref, right: four });
1125        let bref = ea.alloc(Expr::Identifier(b));
1126        let two = ea.alloc(Expr::Literal(Literal::Number(2)));
1127        let subx = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Subtract, left: bref, right: two });
1128        let cref1 = ea.alloc(Expr::Identifier(c));
1129        let cref2 = ea.alloc(Expr::Identifier(c));
1130        let twenty = ea.alloc(Expr::Literal(Literal::Number(20)));
1131        let gt = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Gt, left: cref2, right: twenty });
1132        let dref = ea.alloc(Expr::Identifier(d));
1133        let console_ref1 = ea.alloc(Expr::Identifier(console));
1134        let console_ref2 = ea.alloc(Expr::Identifier(console));
1135
1136        let stmts = vec![
1137            Stmt::Let { var: a, ty: None, value: six, mutable: false },
1138            Stmt::Let { var: b, ty: None, value: mul, mutable: false },
1139            Stmt::Let { var: c, ty: None, value: subx, mutable: false },
1140            Stmt::Show { object: cref1, recipient: console_ref1 },
1141            Stmt::Let { var: d, ty: None, value: gt, mutable: false },
1142            Stmt::Show { object: dref, recipient: console_ref2 },
1143        ];
1144
1145        let out = compile_and_run(&stmts, &it).unwrap();
1146        let lines: Vec<&str> = out.lines().collect();
1147        assert_eq!(lines[0], "22");
1148        assert_eq!(lines[1], "true");
1149        assert_vm_eq_treewalk(&stmts, &it);
1150    }
1151
1152    #[test]
1153    fn vm_while_loop_sums_to_15() {
1154        // Let total be 0. Let i be 1.
1155        // While i <= 5: Set total to total + i. Set i to i + 1.
1156        // Show total.   → 15
1157        let ea: Arena<Expr> = Arena::new();
1158        let sa: Arena<Stmt> = Arena::new();
1159        let mut it = Interner::new();
1160        let total = it.intern("total");
1161        let i = it.intern("i");
1162        let console = it.intern("show");
1163
1164        let zero = ea.alloc(Expr::Literal(Literal::Number(0)));
1165        let one_init = ea.alloc(Expr::Literal(Literal::Number(1)));
1166
1167        // cond: i <= 5
1168        let i_c = ea.alloc(Expr::Identifier(i));
1169        let five = ea.alloc(Expr::Literal(Literal::Number(5)));
1170        let cond = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::LtEq, left: i_c, right: five });
1171
1172        // body: Set total to total + i.
1173        let total_l = ea.alloc(Expr::Identifier(total));
1174        let i_r = ea.alloc(Expr::Identifier(i));
1175        let tplus = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Add, left: total_l, right: i_r });
1176        // body: Set i to i + 1.
1177        let i_l = ea.alloc(Expr::Identifier(i));
1178        let one = ea.alloc(Expr::Literal(Literal::Number(1)));
1179        let iplus = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Add, left: i_l, right: one });
1180        let body: &[Stmt] = sa.alloc_slice(vec![
1181            Stmt::Set { target: total, value: tplus },
1182            Stmt::Set { target: i, value: iplus },
1183        ]);
1184
1185        let total_show = ea.alloc(Expr::Identifier(total));
1186        let console_ref = ea.alloc(Expr::Identifier(console));
1187
1188        let stmts = vec![
1189            Stmt::Let { var: total, ty: None, value: zero, mutable: true },
1190            Stmt::Let { var: i, ty: None, value: one_init, mutable: true },
1191            Stmt::While { cond, body, decreasing: None },
1192            Stmt::Show { object: total_show, recipient: console_ref },
1193        ];
1194
1195        let out = compile_and_run(&stmts, &it).unwrap();
1196        assert_eq!(out.trim(), "15");
1197        assert_vm_eq_treewalk(&stmts, &it);
1198    }
1199
1200    // ---- Sprint 1: error-string parity with the tree-walker -------------------
1201    //
1202    // The Err string IS part of the spec. Each test runs the same program
1203    // through both engines and asserts the error text is identical.
1204
1205    fn assert_err_parity(stmts: &[Stmt], interner: &Interner) {
1206        let vm_err = compile_and_run(stmts, interner)
1207            .expect_err("VM should error");
1208        let tw_err = run_treewalk(stmts, interner)
1209            .expect_err("tree-walker should error");
1210        assert_eq!(vm_err, tw_err, "error strings diverged");
1211    }
1212
1213    fn boolean<'a>(ea: &'a Arena<Expr<'a>>, b: bool) -> &'a Expr<'a> {
1214        ea.alloc(Expr::Literal(Literal::Boolean(b)))
1215    }
1216    fn nothing_lit<'a>(ea: &'a Arena<Expr<'a>>) -> &'a Expr<'a> {
1217        ea.alloc(Expr::Literal(Literal::Nothing))
1218    }
1219
1220    #[test]
1221    fn vm_add_type_error_matches_treewalk() {
1222        // true + nothing — "Cannot add Bool and Nothing" (capital C).
1223        let ea: Arena<Expr> = Arena::new();
1224        let mut it = Interner::new();
1225        let x = it.intern("x");
1226        let stmts = vec![letb(x, bin(&ea, BinaryOpKind::Add, boolean(&ea, true), nothing_lit(&ea)))];
1227        assert_err_parity(&stmts, &it);
1228    }
1229
1230    #[test]
1231    fn vm_subtract_type_error_matches_treewalk() {
1232        // true - nothing — TW phrases this "Cannot subtract Nothing from Bool".
1233        let ea: Arena<Expr> = Arena::new();
1234        let mut it = Interner::new();
1235        let x = it.intern("x");
1236        let stmts = vec![letb(x, bin(&ea, BinaryOpKind::Subtract, boolean(&ea, true), nothing_lit(&ea)))];
1237        assert_err_parity(&stmts, &it);
1238    }
1239
1240    #[test]
1241    fn vm_multiply_type_error_matches_treewalk() {
1242        let ea: Arena<Expr> = Arena::new();
1243        let mut it = Interner::new();
1244        let x = it.intern("x");
1245        let stmts = vec![letb(x, bin(&ea, BinaryOpKind::Multiply, boolean(&ea, true), nothing_lit(&ea)))];
1246        assert_err_parity(&stmts, &it);
1247    }
1248
1249    #[test]
1250    fn vm_divide_and_modulo_zero_messages_match_treewalk() {
1251        // Division by zero / Modulo by zero — exact text both engines.
1252        let ea: Arena<Expr> = Arena::new();
1253        let mut it = Interner::new();
1254        let x = it.intern("x");
1255        let stmts = vec![letb(x, bin(&ea, BinaryOpKind::Divide, num(&ea, 1), num(&ea, 0)))];
1256        assert_err_parity(&stmts, &it);
1257
1258        let y = it.intern("y");
1259        let stmts = vec![letb(y, bin(&ea, BinaryOpKind::Modulo, num(&ea, 1), num(&ea, 0)))];
1260        assert_err_parity(&stmts, &it);
1261    }
1262
1263    #[test]
1264    fn vm_comparison_type_error_matches_treewalk() {
1265        // true < 1 — TW says "Cannot compare Bool and Int".
1266        let ea: Arena<Expr> = Arena::new();
1267        let mut it = Interner::new();
1268        let x = it.intern("x");
1269        let stmts = vec![letb(x, bin(&ea, BinaryOpKind::Lt, boolean(&ea, true), num(&ea, 1)))];
1270        assert_err_parity(&stmts, &it);
1271    }
1272
1273    #[test]
1274    fn vm_index_and_length_type_errors_match_treewalk() {
1275        // Indexing an Int / length of a Bool — kernel message parity.
1276        let ea: Arena<Expr> = Arena::new();
1277        let mut it = Interner::new();
1278        let x = it.intern("x");
1279        let stmts = vec![letb(x, index_at(&ea, num(&ea, 1), num(&ea, 1)))];
1280        assert_err_parity(&stmts, &it);
1281
1282        let y = it.intern("y");
1283        let stmts = vec![letb(y, length_of(&ea, boolean(&ea, true)))];
1284        assert_err_parity(&stmts, &it);
1285    }
1286
1287    // ---- Sprint 11: the generative differential fuzzer v2 ----------------------
1288
1289    /// Run one generated program through both engines, asserting outcome
1290    /// equality (output AND error). Returns the divergence message if any.
1291    fn fuzz_one(seed: u64, features: super::fuzz::FeatureSet) -> Option<String> {
1292        let ea: Arena<Expr> = Arena::new();
1293        let sa: Arena<Stmt> = Arena::new();
1294        let ta: Arena<TypeExpr> = Arena::new();
1295        let mut it = Interner::new();
1296        let generated = super::fuzz::generate(seed, features, &ea, &sa, &ta, &mut it);
1297
1298        let (vm_out, vm_err) = run_to_outcome(&generated.stmts, &it, None, None);
1299        let (tw_out, tw_err) = run_treewalk_outcome(&generated.stmts, &it);
1300        if normalize(&vm_out) != normalize(&tw_out) || vm_err != tw_err {
1301            return Some(format!(
1302                "SEED={} FEATURES={:#x}\nvm out:\n{}\nvm err: {:?}\ntw out:\n{}\ntw err: {:?}",
1303                seed, features.0, vm_out, vm_err, tw_out, tw_err
1304            ));
1305        }
1306        None
1307    }
1308
1309    #[test]
1310    fn fuzz_generator_is_deterministic() {
1311        // Same seed → identical program → identical outcome both times.
1312        for seed in [0u64, 42, 1234] {
1313            let f = super::fuzz::FeatureSet::all_supported();
1314            let run = |seed| {
1315                let ea: Arena<Expr> = Arena::new();
1316                let sa: Arena<Stmt> = Arena::new();
1317                let ta: Arena<TypeExpr> = Arena::new();
1318                let mut it = Interner::new();
1319                let g = super::fuzz::generate(seed, f, &ea, &sa, &ta, &mut it);
1320                run_to_outcome(&g.stmts, &it, None, None)
1321            };
1322            assert_eq!(run(seed), run(seed), "seed {seed} not deterministic");
1323        }
1324    }
1325
1326    #[test]
1327    fn vm_fuzz_full_feature_differential() {
1328        // The standing CI gate: 1500 seeds over the full supported surface.
1329        let features = super::fuzz::FeatureSet::all_supported();
1330        for seed in 0..1500u64 {
1331            if let Some(divergence) = fuzz_one(seed, features) {
1332                panic!("fuzz divergence:\n{divergence}");
1333            }
1334        }
1335    }
1336
1337    #[test]
1338    fn vm_fuzz_error_injection_differential() {
1339        // 500 seeds with one trap planted per program: the engines must agree
1340        // on partial output AND the exact error string.
1341        let features = super::fuzz::FeatureSet(
1342            super::fuzz::FeatureSet::all_supported().0 | super::fuzz::FeatureSet::ERROR_INJECTION,
1343        );
1344        for seed in 0..500u64 {
1345            if let Some(divergence) = fuzz_one(seed, features) {
1346                panic!("fuzz (error-injection) divergence:\n{divergence}");
1347            }
1348        }
1349    }
1350
1351    /// Overnight soak: `cargo test vm_fuzz_overnight -- --ignored`.
1352    #[test]
1353    #[ignore]
1354    fn vm_fuzz_overnight() {
1355        let features = super::fuzz::FeatureSet::all_supported();
1356        let with_errors =
1357            super::fuzz::FeatureSet(features.0 | super::fuzz::FeatureSet::ERROR_INJECTION);
1358        for seed in 0..500_000u64 {
1359            if let Some(d) = fuzz_one(seed, features) {
1360                panic!("fuzz divergence:\n{d}");
1361            }
1362            if let Some(d) = fuzz_one(seed, with_errors) {
1363                panic!("fuzz divergence:\n{d}");
1364            }
1365        }
1366    }
1367
1368    // ---- Sprint 9: strings, slices, options, temporal --------------------------
1369
1370    #[test]
1371    fn vm_interpolated_string_spec_matrix() {
1372        use crate::ast::stmt::StringPart;
1373        // "{n} {f$} {f.1} {n>5} {n<5} {n^5} {n=}" over n=42, f=2.5.
1374        let ea: Arena<Expr> = Arena::new();
1375        let mut it = Interner::new();
1376        let n = it.intern("n");
1377        let fv = it.intern("fv");
1378        let r = it.intern("r");
1379        let show_s = it.intern("show");
1380        let sep = it.intern(" | ");
1381        let spec_cur = it.intern("$");
1382        let spec_p1 = it.intern(".1");
1383        let spec_r5 = it.intern(">5");
1384        let spec_l5 = it.intern("<5");
1385        let spec_c5 = it.intern("^5");
1386
1387        let parts = vec![
1388            StringPart::Expr { value: idref(&ea, n), format_spec: None, debug: false },
1389            StringPart::Literal(sep),
1390            StringPart::Expr { value: idref(&ea, fv), format_spec: Some(spec_cur), debug: false },
1391            StringPart::Literal(sep),
1392            StringPart::Expr { value: idref(&ea, fv), format_spec: Some(spec_p1), debug: false },
1393            StringPart::Literal(sep),
1394            StringPart::Expr { value: idref(&ea, n), format_spec: Some(spec_r5), debug: false },
1395            StringPart::Literal(sep),
1396            StringPart::Expr { value: idref(&ea, n), format_spec: Some(spec_l5), debug: false },
1397            StringPart::Literal(sep),
1398            StringPart::Expr { value: idref(&ea, n), format_spec: Some(spec_c5), debug: false },
1399            StringPart::Literal(sep),
1400            StringPart::Expr { value: idref(&ea, n), format_spec: None, debug: true },
1401        ];
1402        let interp = ea.alloc(Expr::InterpolatedString(parts));
1403        let f25 = ea.alloc(Expr::Literal(Literal::Float(2.5)));
1404        let stmts = vec![
1405            letb(n, num(&ea, 42)),
1406            letb(fv, f25),
1407            letb(r, interp),
1408            show(&ea, show_s, idref(&ea, r)),
1409        ];
1410        let out = compile_and_run(&stmts, &it).unwrap();
1411        assert_eq!(out.trim_end(), "42 | $2.50 | 2.5 |    42 | 42    |  42   | n=42");
1412        assert_vm_eq_treewalk(&stmts, &it);
1413    }
1414
1415    #[test]
1416    fn vm_slice_copy_tuple_union_intersection() {
1417        let ea: Arena<Expr> = Arena::new();
1418        let mut it = Interner::new();
1419        let xs = it.intern("xs");
1420        let part = it.intern("part");
1421        let cp = it.intern("cp");
1422        let tup = it.intern("tup");
1423        let s1 = it.intern("s1");
1424        let s2 = it.intern("s2");
1425        let u = it.intern("u");
1426        let i_ = it.intern("i_");
1427        let set_ty = it.intern("Set");
1428        let show_s = it.intern("show");
1429
1430        let slice_e = ea.alloc(Expr::Slice {
1431            collection: idref(&ea, xs),
1432            start: num(&ea, 2),
1433            end: num(&ea, 4),
1434        });
1435        let copy_e = ea.alloc(Expr::Copy { expr: idref(&ea, xs) });
1436        let tuple_e = ea.alloc(Expr::Tuple(vec![num(&ea, 7), num(&ea, 8)]));
1437        let union_e = ea.alloc(Expr::Union { left: idref(&ea, s1), right: idref(&ea, s2) });
1438        let inter_e = ea.alloc(Expr::Intersection { left: idref(&ea, s1), right: idref(&ea, s2) });
1439
1440        let stmts = vec![
1441            Stmt::Let { var: xs, ty: None, value: list_lit(&ea, (1..=5).map(|k| num(&ea, k)).collect()), mutable: true },
1442            letb(part, slice_e),
1443            show(&ea, show_s, length_of(&ea, idref(&ea, part))),
1444            show(&ea, show_s, index_at(&ea, idref(&ea, part), num(&ea, 1))),
1445            letb(cp, copy_e),
1446            push_to(num(&ea, 9), idref(&ea, xs)),
1447            show(&ea, show_s, length_of(&ea, idref(&ea, cp))),
1448            letb(tup, tuple_e),
1449            show(&ea, show_s, index_at(&ea, idref(&ea, tup), num(&ea, 2))),
1450            show(&ea, show_s, length_of(&ea, idref(&ea, tup))),
1451            Stmt::Let { var: s1, ty: None, value: new_coll(&ea, set_ty), mutable: true },
1452            Stmt::Let { var: s2, ty: None, value: new_coll(&ea, set_ty), mutable: true },
1453            Stmt::Add { value: num(&ea, 1), collection: idref(&ea, s1) },
1454            Stmt::Add { value: num(&ea, 2), collection: idref(&ea, s1) },
1455            Stmt::Add { value: num(&ea, 2), collection: idref(&ea, s2) },
1456            Stmt::Add { value: num(&ea, 3), collection: idref(&ea, s2) },
1457            letb(u, union_e),
1458            show(&ea, show_s, length_of(&ea, idref(&ea, u))),
1459            letb(i_, inter_e),
1460            show(&ea, show_s, length_of(&ea, idref(&ea, i_))),
1461        ];
1462        let out = compile_and_run(&stmts, &it).unwrap();
1463        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["3", "2", "5", "8", "2", "3", "1"]);
1464        assert_vm_eq_treewalk(&stmts, &it);
1465    }
1466
1467    #[test]
1468    fn vm_option_some_none_match_treewalk() {
1469        let ea: Arena<Expr> = Arena::new();
1470        let mut it = Interner::new();
1471        let a = it.intern("a");
1472        let b = it.intern("b");
1473        let show_s = it.intern("show");
1474        let some_e = ea.alloc(Expr::OptionSome { value: num(&ea, 5) });
1475        let none_e = ea.alloc(Expr::OptionNone);
1476        let stmts = vec![
1477            letb(a, some_e),
1478            show(&ea, show_s, idref(&ea, a)),
1479            letb(b, none_e),
1480            show(&ea, show_s, idref(&ea, b)),
1481        ];
1482        let out = compile_and_run(&stmts, &it).unwrap();
1483        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["5", "nothing"]);
1484        assert_vm_eq_treewalk(&stmts, &it);
1485    }
1486
1487    #[test]
1488    fn vm_today_now_with_fixed_clock_match_treewalk() {
1489        // Pin the clock so both engines see the same instant; also pin the
1490        // shadowing quirk — the NAME `today` resolves to the builtin even
1491        // after `Let today be 5`.
1492        crate::semantics::temporal::set_fixed_clock(19753, 1_700_000_000_000_000_000);
1493        let result = std::panic::catch_unwind(|| {
1494            let ea: Arena<Expr> = Arena::new();
1495            let mut it = Interner::new();
1496            let today_sym = it.intern("today");
1497            let a = it.intern("a");
1498            let b = it.intern("b");
1499            let show_s = it.intern("show");
1500            let stmts = vec![
1501                letb(a, idref(&ea, today_sym)),
1502                show(&ea, show_s, idref(&ea, a)),
1503                letb(b, idref(&ea, it.intern("now"))),
1504                show(&ea, show_s, idref(&ea, b)),
1505                letb(today_sym, num(&ea, 5)),
1506                show(&ea, show_s, idref(&ea, today_sym)),
1507            ];
1508            assert_vm_eq_treewalk(&stmts, &it);
1509        });
1510        crate::semantics::temporal::clear_fixed_clock();
1511        result.unwrap();
1512    }
1513
1514    #[test]
1515    fn vm_escape_expr_errors_like_treewalk() {
1516        let ea: Arena<Expr> = Arena::new();
1517        let mut it = Interner::new();
1518        let x = it.intern("x");
1519        let lang = it.intern("Rust");
1520        let code = it.intern("raw");
1521        let esc = ea.alloc(Expr::Escape { language: lang, code });
1522        let stmts = vec![letb(x, esc)];
1523        assert_err_parity(&stmts, &it);
1524    }
1525
1526    // ---- Sprint 8: closures, first-class calls, globals ------------------------
1527
1528    use crate::ast::stmt::ClosureBody;
1529
1530    fn closure_expr<'a>(
1531        ea: &'a Arena<Expr<'a>>,
1532        params: Vec<(Symbol, &'a TypeExpr<'a>)>,
1533        body: ClosureBody<'a>,
1534    ) -> &'a Expr<'a> {
1535        ea.alloc(Expr::Closure { params, body, return_type: None })
1536    }
1537    fn call_expr<'a>(
1538        ea: &'a Arena<Expr<'a>>,
1539        callee: &'a Expr<'a>,
1540        args: Vec<&'a Expr<'a>>,
1541    ) -> &'a Expr<'a> {
1542        ea.alloc(Expr::CallExpr { callee, args })
1543    }
1544
1545    #[test]
1546    fn vm_closure_capture_is_snapshot() {
1547        // Mirrors the pinned e2e test: `Let x be 10. Let getX be () -> x.
1548        // Set x to 999. Show getX().` → 10.
1549        let ea: Arena<Expr> = Arena::new();
1550        let mut it = Interner::new();
1551        let x = it.intern("x");
1552        let get_x = it.intern("getX");
1553        let show_s = it.intern("show");
1554        let stmts = vec![
1555            Stmt::Let { var: x, ty: None, value: num(&ea, 10), mutable: true },
1556            letb(get_x, closure_expr(&ea, vec![], ClosureBody::Expression(idref(&ea, x)))),
1557            Stmt::Set { target: x, value: num(&ea, 999) },
1558            show(&ea, show_s, call_expr(&ea, idref(&ea, get_x), vec![])),
1559        ];
1560        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "10");
1561        assert_vm_eq_treewalk(&stmts, &it);
1562    }
1563
1564    #[test]
1565    fn vm_closure_capture_recloned_each_call() {
1566        // The closure body mutates its captured list — each call gets a fresh
1567        // deep clone (the tree-walker re-clones per invocation).
1568        let ea: Arena<Expr> = Arena::new();
1569        let sa: Arena<Stmt> = Arena::new();
1570        let mut it = Interner::new();
1571        let xs = it.intern("xs");
1572        let f = it.intern("f");
1573        let show_s = it.intern("show");
1574        let body: &[Stmt] = sa.alloc_slice(vec![
1575            push_to(num(&ea, 9), idref(&ea, xs)),
1576            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
1577            ret(num(&ea, 0)),
1578        ]);
1579        let stmts = vec![
1580            Stmt::Let { var: xs, ty: None, value: list_lit(&ea, vec![num(&ea, 1)]), mutable: true },
1581            letb(f, closure_expr(&ea, vec![], ClosureBody::Block(body))),
1582            Stmt::Call { function: f, args: vec![] },
1583            Stmt::Call { function: f, args: vec![] },
1584            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
1585        ];
1586        let out = compile_and_run(&stmts, &it).unwrap();
1587        // Each call: clone has 1 element, push → 2. The original stays at 1.
1588        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["2", "2", "1"]);
1589        assert_vm_eq_treewalk(&stmts, &it);
1590    }
1591
1592    #[test]
1593    fn vm_closure_sees_live_global_when_created_before_definition() {
1594        // The closure references `g` before `Let g` runs: nothing to capture,
1595        // so the body falls through to the LIVE global.
1596        let ea: Arena<Expr> = Arena::new();
1597        let mut it = Interner::new();
1598        let g = it.intern("g");
1599        let f = it.intern("f");
1600        let show_s = it.intern("show");
1601        let stmts = vec![
1602            letb(f, closure_expr(&ea, vec![], ClosureBody::Expression(idref(&ea, g)))),
1603            Stmt::Let { var: g, ty: None, value: num(&ea, 5), mutable: true },
1604            show(&ea, show_s, call_expr(&ea, idref(&ea, f), vec![])),
1605            Stmt::Set { target: g, value: num(&ea, 7) },
1606            show(&ea, show_s, call_expr(&ea, idref(&ea, f), vec![])),
1607        ];
1608        let out = compile_and_run(&stmts, &it).unwrap();
1609        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["5", "7"]);
1610        assert_vm_eq_treewalk(&stmts, &it);
1611    }
1612
1613    #[test]
1614    fn vm_function_reads_and_writes_global_live() {
1615        // Functions see Main top-level bindings LIVE (lexical globals).
1616        let ea: Arena<Expr> = Arena::new();
1617        let sa: Arena<Stmt> = Arena::new();
1618        let mut it = Interner::new();
1619        let g = it.intern("g");
1620        let reader = it.intern("reader");
1621        let writer = it.intern("writer");
1622        let show_s = it.intern("show");
1623        let reader_body: &[Stmt] = sa.alloc_slice(vec![ret(idref(&ea, g))]);
1624        let writer_body: &[Stmt] = sa.alloc_slice(vec![
1625            Stmt::Set { target: g, value: num(&ea, 42) },
1626            ret(num(&ea, 0)),
1627        ]);
1628        let stmts = vec![
1629            fndef(reader, vec![], reader_body),
1630            fndef(writer, vec![], writer_body),
1631            Stmt::Let { var: g, ty: None, value: num(&ea, 1), mutable: true },
1632            show(&ea, show_s, calle(&ea, reader, vec![])),
1633            Stmt::Set { target: g, value: num(&ea, 2) },
1634            show(&ea, show_s, calle(&ea, reader, vec![])),
1635            Stmt::Call { function: writer, args: vec![] },
1636            show(&ea, show_s, idref(&ea, g)),
1637        ];
1638        let out = compile_and_run(&stmts, &it).unwrap();
1639        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["1", "2", "42"]);
1640        assert_vm_eq_treewalk(&stmts, &it);
1641    }
1642
1643    #[test]
1644    fn vm_main_block_var_invisible_to_function() {
1645        // Lexical scoping: a Let inside a Main If-block is NOT a global; the
1646        // function fails with "Undefined variable" — in both engines, with the
1647        // partial output intact.
1648        let ea: Arena<Expr> = Arena::new();
1649        let sa: Arena<Stmt> = Arena::new();
1650        let mut it = Interner::new();
1651        let t = it.intern("t");
1652        let f = it.intern("f");
1653        let show_s = it.intern("show");
1654        let f_body: &[Stmt] = sa.alloc_slice(vec![ret(idref(&ea, t))]);
1655        let then_blk: &[Stmt] = sa.alloc_slice(vec![
1656            letb(t, num(&ea, 5)),
1657            show(&ea, show_s, calle(&ea, f, vec![])),
1658        ]);
1659        let stmts = vec![
1660            fndef(f, vec![], f_body),
1661            show(&ea, show_s, num(&ea, 1)),
1662            Stmt::If { cond: boolean(&ea, true), then_block: then_blk, else_block: None },
1663        ];
1664        assert_outcome_eq_treewalk(&stmts, &it);
1665    }
1666
1667    #[test]
1668    fn vm_closure_param_shadows_capture() {
1669        let ea: Arena<Expr> = Arena::new();
1670        let ta: Arena<TypeExpr> = Arena::new();
1671        let mut it = Interner::new();
1672        let x = it.intern("x");
1673        let f = it.intern("f");
1674        let show_s = it.intern("show");
1675        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
1676        let stmts = vec![
1677            Stmt::Let { var: x, ty: None, value: num(&ea, 10), mutable: true },
1678            letb(
1679                f,
1680                closure_expr(&ea, vec![(x, int_ty)], ClosureBody::Expression(idref(&ea, x))),
1681            ),
1682            show(&ea, show_s, call_expr(&ea, idref(&ea, f), vec![num(&ea, 77)])),
1683        ];
1684        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "77");
1685        assert_vm_eq_treewalk(&stmts, &it);
1686    }
1687
1688    #[test]
1689    fn vm_callexpr_on_non_function_and_arity_errors_match() {
1690        let ea: Arena<Expr> = Arena::new();
1691        let mut it = Interner::new();
1692        let x = it.intern("x");
1693        let f = it.intern("f");
1694
1695        // Cannot call value of type Int.
1696        let stmts = vec![letb(x, call_expr(&ea, num(&ea, 5), vec![]))];
1697        assert_err_parity(&stmts, &it);
1698
1699        // Closure arity mismatch.
1700        let stmts = vec![
1701            letb(f, closure_expr(&ea, vec![], ClosureBody::Expression(num(&ea, 1)))),
1702            letb(x, call_expr(&ea, idref(&ea, f), vec![num(&ea, 9)])),
1703        ];
1704        assert_err_parity(&stmts, &it);
1705    }
1706
1707    #[test]
1708    fn vm_closure_passed_to_function_and_called_via_param() {
1709        // Higher-order: apply(f, x) = f(x).
1710        let ea: Arena<Expr> = Arena::new();
1711        let sa: Arena<Stmt> = Arena::new();
1712        let ta: Arena<TypeExpr> = Arena::new();
1713        let mut it = Interner::new();
1714        let apply = it.intern("apply");
1715        let fp = it.intern("fp");
1716        let xp = it.intern("xp");
1717        let n = it.intern("n");
1718        let show_s = it.intern("show");
1719        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
1720
1721        let apply_body: &[Stmt] =
1722            sa.alloc_slice(vec![ret(call_expr(&ea, idref(&ea, fp), vec![idref(&ea, xp)]))]);
1723        let doubler = closure_expr(
1724            &ea,
1725            vec![(n, int_ty)],
1726            ClosureBody::Expression(bin(&ea, BinaryOpKind::Multiply, idref(&ea, n), num(&ea, 2))),
1727        );
1728        let stmts = vec![
1729            fndef(apply, vec![(fp, int_ty), (xp, int_ty)], apply_body),
1730            show(&ea, show_s, calle(&ea, apply, vec![doubler, num(&ea, 21)])),
1731        ];
1732        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "42");
1733        assert_vm_eq_treewalk(&stmts, &it);
1734    }
1735
1736    // ---- Sprint 7: structs, enums, Inspect, CRDT -------------------------------
1737
1738    fn struct_def<'a>(name: Symbol, fields: Vec<(Symbol, Symbol, bool)>) -> Stmt<'a> {
1739        Stmt::StructDef { name, fields, is_portable: false }
1740    }
1741    fn new_struct<'a>(
1742        ea: &'a Arena<Expr<'a>>,
1743        type_name: Symbol,
1744        init_fields: Vec<(Symbol, &'a Expr<'a>)>,
1745    ) -> &'a Expr<'a> {
1746        ea.alloc(Expr::New { type_name, type_args: vec![], init_fields })
1747    }
1748    fn field_access<'a>(ea: &'a Arena<Expr<'a>>, object: &'a Expr<'a>, field: Symbol) -> &'a Expr<'a> {
1749        ea.alloc(Expr::FieldAccess { object, field })
1750    }
1751
1752    #[test]
1753    fn vm_struct_new_with_defaults_and_field_access() {
1754        // Point { x: Int, y: Int }; new Point with x 3 — y defaults to 0.
1755        let ea: Arena<Expr> = Arena::new();
1756        let mut it = Interner::new();
1757        let point = it.intern("Point");
1758        let x = it.intern("x");
1759        let y = it.intern("y");
1760        let int_s = it.intern("Int");
1761        let p = it.intern("p");
1762        let show_s = it.intern("show");
1763        let stmts = vec![
1764            struct_def(point, vec![(x, int_s, true), (y, int_s, true)]),
1765            letb(p, new_struct(&ea, point, vec![(x, num(&ea, 3))])),
1766            show(&ea, show_s, field_access(&ea, idref(&ea, p), x)),
1767            show(&ea, show_s, field_access(&ea, idref(&ea, p), y)),
1768        ];
1769        let out = compile_and_run(&stmts, &it).unwrap();
1770        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["3", "0"]);
1771        assert_vm_eq_treewalk(&stmts, &it);
1772    }
1773
1774    #[test]
1775    fn vm_struct_value_semantics_no_aliasing() {
1776        // `Let b be a` copies the struct: SetField through b is invisible via a.
1777        let ea: Arena<Expr> = Arena::new();
1778        let mut it = Interner::new();
1779        let point = it.intern("Point");
1780        let x = it.intern("x");
1781        let int_s = it.intern("Int");
1782        let a = it.intern("a");
1783        let b = it.intern("b");
1784        let show_s = it.intern("show");
1785        let stmts = vec![
1786            struct_def(point, vec![(x, int_s, true)]),
1787            Stmt::Let { var: a, ty: None, value: new_struct(&ea, point, vec![(x, num(&ea, 1))]), mutable: true },
1788            Stmt::Let { var: b, ty: None, value: idref(&ea, a), mutable: true },
1789            Stmt::SetField { object: idref(&ea, b), field: x, value: num(&ea, 99) },
1790            show(&ea, show_s, field_access(&ea, idref(&ea, a), x)),
1791            show(&ea, show_s, field_access(&ea, idref(&ea, b), x)),
1792        ];
1793        let out = compile_and_run(&stmts, &it).unwrap();
1794        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["1", "99"]);
1795        assert_vm_eq_treewalk(&stmts, &it);
1796    }
1797
1798    #[test]
1799    fn vm_field_errors_match_treewalk() {
1800        let ea: Arena<Expr> = Arena::new();
1801        let mut it = Interner::new();
1802        let point = it.intern("Point");
1803        let x = it.intern("x");
1804        let ghost = it.intern("ghost");
1805        let int_s = it.intern("Int");
1806        let p = it.intern("p");
1807        let v = it.intern("v");
1808
1809        // Missing field read.
1810        let stmts = vec![
1811            struct_def(point, vec![(x, int_s, true)]),
1812            letb(p, new_struct(&ea, point, vec![])),
1813            letb(v, field_access(&ea, idref(&ea, p), ghost)),
1814        ];
1815        assert_err_parity(&stmts, &it);
1816
1817        // Field access on a non-struct.
1818        let stmts = vec![letb(v, field_access(&ea, num(&ea, 5), x))];
1819        assert_err_parity(&stmts, &it);
1820
1821        // SetField on a non-struct.
1822        let stmts = vec![
1823            letb(p, num(&ea, 5)),
1824            Stmt::SetField { object: idref(&ea, p), field: x, value: num(&ea, 1) },
1825        ];
1826        assert_err_parity(&stmts, &it);
1827    }
1828
1829    #[test]
1830    fn vm_inspect_struct_arms_and_otherwise() {
1831        use crate::ast::stmt::MatchArm;
1832        // Inspect p (a Point): the Point arm binds x; a Circle arm is skipped;
1833        // then inspect an Int hits Otherwise.
1834        let ea: Arena<Expr> = Arena::new();
1835        let sa: Arena<Stmt> = Arena::new();
1836        let mut it = Interner::new();
1837        let point = it.intern("Point");
1838        let circle = it.intern("Circle");
1839        let x = it.intern("x");
1840        let bx = it.intern("bx");
1841        let int_s = it.intern("Int");
1842        let p = it.intern("p");
1843        let show_s = it.intern("show");
1844
1845        let point_body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, idref(&ea, bx))]);
1846        let circle_body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, num(&ea, 111))]);
1847        let otherwise_body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, num(&ea, 222))]);
1848
1849        let stmts = vec![
1850            struct_def(point, vec![(x, int_s, true)]),
1851            letb(p, new_struct(&ea, point, vec![(x, num(&ea, 7))])),
1852            Stmt::Inspect {
1853                target: idref(&ea, p),
1854                arms: vec![
1855                    MatchArm { enum_name: None, variant: Some(circle), bindings: vec![], body: circle_body },
1856                    MatchArm { enum_name: None, variant: Some(point), bindings: vec![(x, bx)], body: point_body },
1857                    MatchArm { enum_name: None, variant: None, bindings: vec![], body: otherwise_body },
1858                ],
1859                has_otherwise: true,
1860            },
1861            Stmt::Inspect {
1862                target: num(&ea, 5),
1863                arms: vec![
1864                    MatchArm { enum_name: None, variant: Some(point), bindings: vec![], body: circle_body },
1865                    MatchArm { enum_name: None, variant: None, bindings: vec![], body: otherwise_body },
1866                ],
1867                has_otherwise: true,
1868            },
1869        ];
1870        let out = compile_and_run(&stmts, &it).unwrap();
1871        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["7", "222"]);
1872        assert_vm_eq_treewalk(&stmts, &it);
1873    }
1874
1875    #[test]
1876    fn vm_inspect_inductive_positional_bindings() {
1877        use crate::ast::stmt::MatchArm;
1878        let ea: Arena<Expr> = Arena::new();
1879        let sa: Arena<Stmt> = Arena::new();
1880        let mut it = Interner::new();
1881        let shape = it.intern("Shape");
1882        let circle = it.intern("Circle");
1883        let r_field = it.intern("radius");
1884        let r_bind = it.intern("r");
1885        let c = it.intern("c");
1886        let show_s = it.intern("show");
1887
1888        let circle_expr = ea.alloc(Expr::NewVariant {
1889            enum_name: shape,
1890            variant: circle,
1891            fields: vec![(r_field, num(&ea, 10))],
1892        });
1893        let circle_body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, idref(&ea, r_bind))]);
1894
1895        let stmts = vec![
1896            letb(c, circle_expr),
1897            // Matching arm binds positionally.
1898            Stmt::Inspect {
1899                target: idref(&ea, c),
1900                arms: vec![MatchArm {
1901                    enum_name: None,
1902                    variant: Some(circle),
1903                    bindings: vec![(r_field, r_bind)],
1904                    body: circle_body,
1905                }],
1906                has_otherwise: false,
1907            },
1908            show(&ea, show_s, num(&ea, 1)),
1909        ];
1910        let out = compile_and_run(&stmts, &it).unwrap();
1911        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["10", "1"]);
1912        assert_vm_eq_treewalk(&stmts, &it);
1913    }
1914
1915    #[test]
1916    fn vm_inspect_unhandled_variant_without_otherwise_is_loud() {
1917        use crate::ast::stmt::MatchArm;
1918        let ea: Arena<Expr> = Arena::new();
1919        let sa: Arena<Stmt> = Arena::new();
1920        let mut it = Interner::new();
1921        let shape = it.intern("Shape");
1922        let circle = it.intern("Circle");
1923        let square = it.intern("Square");
1924        let r_field = it.intern("radius");
1925        let c = it.intern("c");
1926        let show_s = it.intern("show");
1927
1928        let circle_expr = ea.alloc(Expr::NewVariant {
1929            enum_name: shape,
1930            variant: circle,
1931            fields: vec![(r_field, num(&ea, 10))],
1932        });
1933        let square_body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, num(&ea, 333))]);
1934
1935        // A `Circle` scrutinee against a `Square`-only Inspect with no Otherwise
1936        // is a non-exhaustive match — LOUD, never a silent no-op. tw==VM.
1937        let stmts = vec![
1938            letb(c, circle_expr),
1939            Stmt::Inspect {
1940                target: idref(&ea, c),
1941                arms: vec![MatchArm { enum_name: None, variant: Some(square), bindings: vec![], body: square_body }],
1942                has_otherwise: false,
1943            },
1944        ];
1945        assert_err_parity(&stmts, &it);
1946    }
1947
1948    #[test]
1949    fn vm_inductive_equality_is_structural() {
1950        let ea: Arena<Expr> = Arena::new();
1951        let mut it = Interner::new();
1952        let shape = it.intern("Shape");
1953        let circle = it.intern("Circle");
1954        let r_field = it.intern("radius");
1955        let a = it.intern("a");
1956        let b = it.intern("b");
1957        let show_s = it.intern("show");
1958        let c1 = ea.alloc(Expr::NewVariant {
1959            enum_name: shape,
1960            variant: circle,
1961            fields: vec![(r_field, num(&ea, 5))],
1962        });
1963        let c2 = ea.alloc(Expr::NewVariant {
1964            enum_name: shape,
1965            variant: circle,
1966            fields: vec![(r_field, num(&ea, 5))],
1967        });
1968        let stmts = vec![
1969            letb(a, c1),
1970            letb(b, c2),
1971            show(&ea, show_s, bin(&ea, BinaryOpKind::Eq, idref(&ea, a), idref(&ea, b))),
1972        ];
1973        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "true");
1974        assert_vm_eq_treewalk(&stmts, &it);
1975    }
1976
1977    #[test]
1978    fn vm_crdt_increase_decrease_merge_match_treewalk() {
1979        let ea: Arena<Expr> = Arena::new();
1980        let mut it = Interner::new();
1981        let counter = it.intern("Counter");
1982        let n_field = it.intern("n");
1983        let int_s = it.intern("Int");
1984        let a = it.intern("a");
1985        let b = it.intern("b");
1986        let show_s = it.intern("show");
1987        let stmts = vec![
1988            struct_def(counter, vec![(n_field, int_s, true)]),
1989            Stmt::Let { var: a, ty: None, value: new_struct(&ea, counter, vec![(n_field, num(&ea, 1))]), mutable: true },
1990            Stmt::Let { var: b, ty: None, value: new_struct(&ea, counter, vec![(n_field, num(&ea, 10))]), mutable: true },
1991            Stmt::IncreaseCrdt { object: idref(&ea, a), field: n_field, amount: num(&ea, 5) },
1992            Stmt::DecreaseCrdt { object: idref(&ea, a), field: n_field, amount: num(&ea, 2) },
1993            Stmt::MergeCrdt { source: idref(&ea, b), target: idref(&ea, a) },
1994            show(&ea, show_s, field_access(&ea, idref(&ea, a), n_field)),
1995        ];
1996        let out = compile_and_run(&stmts, &it).unwrap();
1997        assert_eq!(out.trim(), "14");
1998        assert_vm_eq_treewalk(&stmts, &it);
1999    }
2000
2001    // ---- Sprint 6: builtins, call edges, MAX_CALL_DEPTH ------------------------
2002
2003    #[test]
2004    fn vm_builtins_match_treewalk() {
2005        // One Show per builtin, covering Int/Float coercions.
2006        let ea: Arena<Expr> = Arena::new();
2007        let mut it = Interner::new();
2008        let show_s = it.intern("show");
2009        let f25 = ea.alloc(Expr::Literal(Literal::Float(2.5)));
2010        let f29 = ea.alloc(Expr::Literal(Literal::Float(2.9)));
2011        let neg = bin(&ea, BinaryOpKind::Subtract, num(&ea, 0), num(&ea, 7));
2012        let calls: Vec<&Expr> = vec![
2013            calle(&ea, it.intern("abs"), vec![neg]),
2014            calle(&ea, it.intern("sqrt"), vec![num(&ea, 9)]),
2015            calle(&ea, it.intern("min"), vec![num(&ea, 3), f25]),
2016            calle(&ea, it.intern("max"), vec![num(&ea, 3), f25]),
2017            calle(&ea, it.intern("floor"), vec![f29]),
2018            calle(&ea, it.intern("ceil"), vec![f29]),
2019            calle(&ea, it.intern("round"), vec![f29]),
2020            calle(&ea, it.intern("pow"), vec![num(&ea, 2), num(&ea, 10)]),
2021            calle(&ea, it.intern("chr"), vec![num(&ea, 65)]),
2022            calle(&ea, it.intern("length"), vec![text(&ea, it.intern("hello"))]),
2023            calle(&ea, it.intern("format"), vec![num(&ea, 42)]),
2024        ];
2025        let mut stmts = Vec::new();
2026        for (k, c) in calls.into_iter().enumerate() {
2027            let v = it.intern(&format!("b{k}"));
2028            stmts.push(letb(v, c));
2029            stmts.push(show(&ea, show_s, idref(&ea, v)));
2030        }
2031        assert_vm_eq_treewalk(&stmts, &it);
2032    }
2033
2034    #[test]
2035    fn vm_parse_int_and_float_match_treewalk() {
2036        let ea: Arena<Expr> = Arena::new();
2037        let mut it = Interner::new();
2038        let show_s = it.intern("show");
2039        let a = it.intern("a");
2040        let b = it.intern("b");
2041        let s42 = it.intern(" 42 ");
2042        let s25 = it.intern("2.5");
2043        let stmts = vec![
2044            letb(a, calle(&ea, it.intern("parseInt"), vec![text(&ea, s42)])),
2045            show(&ea, show_s, idref(&ea, a)),
2046            letb(b, calle(&ea, it.intern("parseFloat"), vec![text(&ea, s25)])),
2047            show(&ea, show_s, idref(&ea, b)),
2048        ];
2049        assert_vm_eq_treewalk(&stmts, &it);
2050    }
2051
2052    #[test]
2053    fn vm_builtin_error_messages_match_treewalk() {
2054        // parseInt("zz"), chr(-1), sqrt("x") arity/type errors — exact strings.
2055        let ea: Arena<Expr> = Arena::new();
2056        let mut it = Interner::new();
2057        let x = it.intern("x");
2058        let zz = it.intern("zz");
2059
2060        let stmts = vec![letb(x, calle(&ea, it.intern("parseInt"), vec![text(&ea, zz)]))];
2061        assert_err_parity(&stmts, &it);
2062
2063        let neg1 = bin(&ea, BinaryOpKind::Subtract, num(&ea, 0), num(&ea, 1));
2064        let stmts = vec![letb(x, calle(&ea, it.intern("chr"), vec![neg1]))];
2065        assert_err_parity(&stmts, &it);
2066
2067        // Wrong arity: the arity error fires BEFORE evaluating arguments.
2068        let stmts = vec![letb(x, calle(&ea, it.intern("abs"), vec![num(&ea, 1), num(&ea, 2)]))];
2069        assert_err_parity(&stmts, &it);
2070    }
2071
2072    #[test]
2073    fn vm_copy_builtin_is_deep() {
2074        // copy(xs) then mutate xs — the copy must be unaffected (both engines).
2075        let ea: Arena<Expr> = Arena::new();
2076        let mut it = Interner::new();
2077        let xs = it.intern("xs");
2078        let ys = it.intern("ys");
2079        let show_s = it.intern("show");
2080        let stmts = vec![
2081            Stmt::Let { var: xs, ty: None, value: list_lit(&ea, vec![num(&ea, 1)]), mutable: true },
2082            letb(ys, calle(&ea, it.intern("copy"), vec![idref(&ea, xs)])),
2083            push_to(num(&ea, 2), idref(&ea, xs)),
2084            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
2085            show(&ea, show_s, length_of(&ea, idref(&ea, ys))),
2086        ];
2087        let out = compile_and_run(&stmts, &it).unwrap();
2088        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["2", "1"]);
2089        assert_vm_eq_treewalk(&stmts, &it);
2090    }
2091
2092    #[test]
2093    fn vm_let_binding_isolates_value_semantics() {
2094        // Value semantics: `Let a be xs` is an independent value — pushing through
2095        // `a` grows only `a`, leaving `xs` at length 1. VM and tree-walker agree.
2096        let ea: Arena<Expr> = Arena::new();
2097        let mut it = Interner::new();
2098        let xs = it.intern("xs");
2099        let a = it.intern("a");
2100        let show_s = it.intern("show");
2101        let stmts = vec![
2102            Stmt::Let { var: xs, ty: None, value: list_lit(&ea, vec![num(&ea, 1)]), mutable: true },
2103            Stmt::Let { var: a, ty: None, value: idref(&ea, xs), mutable: true },
2104            push_to(num(&ea, 2), idref(&ea, a)),
2105            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
2106        ];
2107        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "1");
2108        assert_vm_eq_treewalk(&stmts, &it);
2109    }
2110
2111    #[test]
2112    fn vm_unknown_function_error_matches_treewalk() {
2113        let ea: Arena<Expr> = Arena::new();
2114        let mut it = Interner::new();
2115        let x = it.intern("x");
2116        let stmts = vec![letb(x, calle(&ea, it.intern("frobnicate"), vec![num(&ea, 1)]))];
2117        assert_err_parity(&stmts, &it);
2118    }
2119
2120    #[test]
2121    fn vm_user_fn_arity_mismatch_matches_treewalk() {
2122        let ea: Arena<Expr> = Arena::new();
2123        let sa: Arena<Stmt> = Arena::new();
2124        let ta: Arena<TypeExpr> = Arena::new();
2125        let mut it = Interner::new();
2126        let f = it.intern("f");
2127        let n = it.intern("n");
2128        let x = it.intern("x");
2129        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
2130        let body: &[Stmt] = sa.alloc_slice(vec![ret(idref(&ea, n))]);
2131        let stmts = vec![
2132            fndef(f, vec![(n, int_ty)], body),
2133            letb(x, calle(&ea, f, vec![num(&ea, 1), num(&ea, 2)])),
2134        ];
2135        assert_err_parity(&stmts, &it);
2136    }
2137
2138    #[test]
2139    fn vm_infinite_recursion_hits_call_depth_limit_like_treewalk() {
2140        // Both engines must report the canonical stack-overflow error instead
2141        // of crashing the host. The tree-walker's SYNC path burns many native
2142        // frames per LOGOS call, so this runs on a big-stack thread (debug
2143        // frames are huge; release frames fit a normal stack at depth 1000).
2144        //
2145        // The recursion is deliberately NON-tail (`spin(n+1) - 1`): the call's
2146        // result feeds a subtraction, so it is not in tail position and is NOT
2147        // tail-call-optimized on any tier. (A direct `return spin(n+1)` is a
2148        // self-tail-call and would run in constant stack forever — TCO is a
2149        // language semantic here, covered by the `tco` differential tests.)
2150        // Subtraction also dodges the AOT accumulator transform, which only
2151        // strength-reduces `+`/`*`, so this stays unbounded recursion everywhere.
2152        std::thread::Builder::new()
2153            .stack_size(256 * 1024 * 1024)
2154            .spawn(|| {
2155                let ea: Arena<Expr> = Arena::new();
2156                let sa: Arena<Stmt> = Arena::new();
2157                let ta: Arena<TypeExpr> = Arena::new();
2158                let mut it = Interner::new();
2159                let spin = it.intern("spin");
2160                let n = it.intern("n");
2161                let x = it.intern("x");
2162                let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
2163                let body: &[Stmt] = sa.alloc_slice(vec![ret(bin(
2164                    &ea,
2165                    BinaryOpKind::Subtract,
2166                    calle(
2167                        &ea,
2168                        spin,
2169                        vec![bin(&ea, BinaryOpKind::Add, idref(&ea, n), num(&ea, 1))],
2170                    ),
2171                    num(&ea, 1),
2172                ))]);
2173                let stmts = vec![
2174                    fndef(spin, vec![(n, int_ty)], body),
2175                    letb(x, calle(&ea, spin, vec![num(&ea, 0)])),
2176                ];
2177                let vm_err = compile_and_run(&stmts, &it).unwrap_err();
2178                assert_eq!(vm_err, "Stack overflow: maximum call depth exceeded");
2179                let (_, tw_err) = run_treewalk_outcome(&stmts, &it);
2180                assert_eq!(
2181                    tw_err.as_deref(),
2182                    Some("Stack overflow: maximum call depth exceeded")
2183                );
2184            })
2185            .unwrap()
2186            .join()
2187            .unwrap();
2188    }
2189
2190    #[test]
2191    fn vm_depth_900_recursion_still_fine() {
2192        // The limit is 1000; 900 must work in both engines.
2193        let ea: Arena<Expr> = Arena::new();
2194        let sa: Arena<Stmt> = Arena::new();
2195        let ta: Arena<TypeExpr> = Arena::new();
2196        let mut it = Interner::new();
2197        let down = it.intern("down");
2198        let n = it.intern("n");
2199        let x = it.intern("x");
2200        let show_s = it.intern("show");
2201        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
2202        let base: &[Stmt] = sa.alloc_slice(vec![ret(num(&ea, 0))]);
2203        let body: &[Stmt] = sa.alloc_slice(vec![
2204            Stmt::If {
2205                cond: bin(&ea, BinaryOpKind::LtEq, idref(&ea, n), num(&ea, 0)),
2206                then_block: base,
2207                else_block: None,
2208            },
2209            ret(calle(&ea, down, vec![bin(&ea, BinaryOpKind::Subtract, idref(&ea, n), num(&ea, 1))])),
2210        ]);
2211        let stmts = vec![
2212            fndef(down, vec![(n, int_ty)], body),
2213            letb(x, calle(&ea, down, vec![num(&ea, 900)])),
2214            show(&ea, show_s, idref(&ea, x)),
2215        ];
2216        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "0");
2217    }
2218
2219    #[test]
2220    fn vm_show_to_function_recipient_calls_it() {
2221        // `Show x to f` calls f(x); `Give x to f` likewise.
2222        let ea: Arena<Expr> = Arena::new();
2223        let sa: Arena<Stmt> = Arena::new();
2224        let ta: Arena<TypeExpr> = Arena::new();
2225        let mut it = Interner::new();
2226        let sink = it.intern("sink");
2227        let v = it.intern("v");
2228        let show_s = it.intern("show");
2229        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
2230        let body: &[Stmt] = sa.alloc_slice(vec![show(
2231            &ea,
2232            show_s,
2233            bin(&ea, BinaryOpKind::Multiply, idref(&ea, v), num(&ea, 2)),
2234        )]);
2235        let stmts = vec![
2236            fndef(sink, vec![(v, int_ty)], body),
2237            Stmt::Show { object: num(&ea, 21), recipient: idref(&ea, sink) },
2238            Stmt::Give { object: num(&ea, 5), recipient: idref(&ea, sink) },
2239        ];
2240        let out = compile_and_run(&stmts, &it).unwrap();
2241        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["42", "10"]);
2242        assert_vm_eq_treewalk(&stmts, &it);
2243    }
2244
2245    // ---- Sprint 5: block scoping, Break, Repeat --------------------------------
2246
2247    /// Full-outcome differential: output AND error must both match the oracle.
2248    fn assert_outcome_eq_treewalk(stmts: &[Stmt], interner: &Interner) {
2249        let (vm_out, vm_err) = run_to_outcome(stmts, interner, None, None);
2250        let (tw_out, tw_err) = match run_treewalk_outcome(stmts, interner) {
2251            (o, e) => (o, e),
2252        };
2253        assert_eq!(
2254            normalize(&vm_out),
2255            normalize(&tw_out),
2256            "partial output diverged (vm err {:?}, tw err {:?})",
2257            vm_err,
2258            tw_err
2259        );
2260        assert_eq!(vm_err, tw_err, "error diverged");
2261    }
2262
2263    /// Tree-walker outcome with partial output preserved.
2264    fn run_treewalk_outcome(stmts: &[Stmt], interner: &Interner) -> (String, Option<String>) {
2265        use crate::interpreter::{Interpreter, OutputCallback};
2266        let buf = Rc::new(RefCell::new(String::new()));
2267        let sink = buf.clone();
2268        let cb: OutputCallback = Rc::new(RefCell::new(move |s: String| {
2269            sink.borrow_mut().push_str(&s);
2270            sink.borrow_mut().push('\n');
2271        }));
2272        let mut interp = Interpreter::new(interner).with_output_callback(cb);
2273        let err = interp.run_sync(stmts).err();
2274        let out = buf.borrow().clone();
2275        (out, err)
2276    }
2277
2278    fn brk<'a>() -> Stmt<'a> {
2279        Stmt::Break
2280    }
2281    fn repeat_ident<'a>(var: Symbol, iterable: &'a Expr<'a>, body: &'a [Stmt<'a>]) -> Stmt<'a> {
2282        use crate::ast::stmt::Pattern;
2283        Stmt::Repeat { pattern: Pattern::Identifier(var), iterable, body }
2284    }
2285
2286    #[test]
2287    fn vm_break_exits_innermost_while() {
2288        // i counts 0..; If i >= 3: Break — output 0,1,2.
2289        let ea: Arena<Expr> = Arena::new();
2290        let sa: Arena<Stmt> = Arena::new();
2291        let mut it = Interner::new();
2292        let i = it.intern("i");
2293        let show_s = it.intern("show");
2294        let break_blk: &[Stmt] = sa.alloc_slice(vec![brk()]);
2295        let body: &[Stmt] = sa.alloc_slice(vec![
2296            Stmt::If {
2297                cond: bin(&ea, BinaryOpKind::GtEq, idref(&ea, i), num(&ea, 3)),
2298                then_block: break_blk,
2299                else_block: None,
2300            },
2301            show(&ea, show_s, idref(&ea, i)),
2302            Stmt::Set { target: i, value: bin(&ea, BinaryOpKind::Add, idref(&ea, i), num(&ea, 1)) },
2303        ]);
2304        let stmts = vec![
2305            Stmt::Let { var: i, ty: None, value: num(&ea, 0), mutable: true },
2306            Stmt::While { cond: bin(&ea, BinaryOpKind::Lt, idref(&ea, i), num(&ea, 100)), body, decreasing: None },
2307            show(&ea, show_s, idref(&ea, i)),
2308        ];
2309        let out = compile_and_run(&stmts, &it).unwrap();
2310        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["0", "1", "2", "3"]);
2311        assert_vm_eq_treewalk(&stmts, &it);
2312    }
2313
2314    #[test]
2315    fn vm_break_only_exits_inner_loop() {
2316        // Outer runs 2 iterations; inner breaks immediately each time.
2317        let ea: Arena<Expr> = Arena::new();
2318        let sa: Arena<Stmt> = Arena::new();
2319        let mut it = Interner::new();
2320        let i = it.intern("i");
2321        let j = it.intern("j");
2322        let show_s = it.intern("show");
2323        let inner_body: &[Stmt] = sa.alloc_slice(vec![brk()]);
2324        let outer_body: &[Stmt] = sa.alloc_slice(vec![
2325            Stmt::Let { var: j, ty: None, value: num(&ea, 0), mutable: true },
2326            Stmt::While { cond: bin(&ea, BinaryOpKind::Lt, idref(&ea, j), num(&ea, 5)), body: inner_body, decreasing: None },
2327            show(&ea, show_s, idref(&ea, i)),
2328            Stmt::Set { target: i, value: bin(&ea, BinaryOpKind::Add, idref(&ea, i), num(&ea, 1)) },
2329        ]);
2330        let stmts = vec![
2331            Stmt::Let { var: i, ty: None, value: num(&ea, 0), mutable: true },
2332            Stmt::While { cond: bin(&ea, BinaryOpKind::Lt, idref(&ea, i), num(&ea, 2)), body: outer_body, decreasing: None },
2333        ];
2334        let out = compile_and_run(&stmts, &it).unwrap();
2335        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["0", "1"]);
2336        assert_vm_eq_treewalk(&stmts, &it);
2337    }
2338
2339    #[test]
2340    fn vm_break_at_top_level_halts_program() {
2341        // The tree-walker's run loop treats Break at Main top level as stop.
2342        let ea: Arena<Expr> = Arena::new();
2343        let mut it = Interner::new();
2344        let show_s = it.intern("show");
2345        let stmts = vec![
2346            show(&ea, show_s, num(&ea, 1)),
2347            brk(),
2348            show(&ea, show_s, num(&ea, 2)),
2349        ];
2350        assert_outcome_eq_treewalk(&stmts, &it);
2351        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "1");
2352    }
2353
2354    #[test]
2355    fn vm_return_at_top_level_halts_program() {
2356        // Return in Main = stop, not "return with no caller".
2357        let ea: Arena<Expr> = Arena::new();
2358        let mut it = Interner::new();
2359        let show_s = it.intern("show");
2360        let stmts = vec![
2361            show(&ea, show_s, num(&ea, 1)),
2362            ret(num(&ea, 99)),
2363            show(&ea, show_s, num(&ea, 2)),
2364        ];
2365        assert_outcome_eq_treewalk(&stmts, &it);
2366        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "1");
2367    }
2368
2369    #[test]
2370    fn vm_block_let_does_not_leak_scope() {
2371        // Let inside an If-block is undone after the block (tree-walker
2372        // execute_block scoping). The use after the block fails AT RUNTIME with
2373        // the partial output preserved.
2374        let ea: Arena<Expr> = Arena::new();
2375        let sa: Arena<Stmt> = Arena::new();
2376        let mut it = Interner::new();
2377        let t = it.intern("t");
2378        let show_s = it.intern("show");
2379        let then_blk: &[Stmt] = sa.alloc_slice(vec![
2380            letb(t, num(&ea, 5)),
2381            show(&ea, show_s, idref(&ea, t)),
2382        ]);
2383        let stmts = vec![
2384            Stmt::If { cond: boolean(&ea, true), then_block: then_blk, else_block: None },
2385            show(&ea, show_s, idref(&ea, t)),
2386        ];
2387        assert_outcome_eq_treewalk(&stmts, &it);
2388    }
2389
2390    #[test]
2391    fn vm_unbound_identifier_in_dead_branch_is_free() {
2392        // `Show ghost` inside `If false:` must not fail in either engine —
2393        // unbound names are a RUNTIME error, not a compile error.
2394        let ea: Arena<Expr> = Arena::new();
2395        let sa: Arena<Stmt> = Arena::new();
2396        let mut it = Interner::new();
2397        let ghost = it.intern("ghost");
2398        let show_s = it.intern("show");
2399        let dead: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, idref(&ea, ghost))]);
2400        let stmts = vec![
2401            Stmt::If { cond: boolean(&ea, false), then_block: dead, else_block: None },
2402            show(&ea, show_s, num(&ea, 7)),
2403        ];
2404        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "7");
2405        assert_outcome_eq_treewalk(&stmts, &it);
2406    }
2407
2408    #[test]
2409    fn vm_repeat_list_snapshot_semantics() {
2410        // Pushing to the list inside the loop must NOT extend the iteration:
2411        // the tree-walker snapshots the collection before looping.
2412        let ea: Arena<Expr> = Arena::new();
2413        let sa: Arena<Stmt> = Arena::new();
2414        let mut it = Interner::new();
2415        let xs = it.intern("xs");
2416        let x = it.intern("x");
2417        let show_s = it.intern("show");
2418        let body: &[Stmt] = sa.alloc_slice(vec![
2419            push_to(num(&ea, 9), idref(&ea, xs)),
2420            show(&ea, show_s, idref(&ea, x)),
2421        ]);
2422        let stmts = vec![
2423            Stmt::Let { var: xs, ty: None, value: list_lit(&ea, vec![num(&ea, 1), num(&ea, 2)]), mutable: true },
2424            repeat_ident(x, idref(&ea, xs), body),
2425            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
2426        ];
2427        let out = compile_and_run(&stmts, &it).unwrap();
2428        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["1", "2", "4"]);
2429        assert_vm_eq_treewalk(&stmts, &it);
2430    }
2431
2432    #[test]
2433    fn vm_repeat_over_text_iterates_chars() {
2434        let ea: Arena<Expr> = Arena::new();
2435        let sa: Arena<Stmt> = Arena::new();
2436        let mut it = Interner::new();
2437        let s = it.intern("s");
2438        let c = it.intern("c");
2439        let show_s = it.intern("show");
2440        let abc = it.intern("abc");
2441        let body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, idref(&ea, c))]);
2442        let stmts = vec![
2443            letb(s, text(&ea, abc)),
2444            repeat_ident(c, idref(&ea, s), body),
2445        ];
2446        let out = compile_and_run(&stmts, &it).unwrap();
2447        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["a", "b", "c"]);
2448        assert_vm_eq_treewalk(&stmts, &it);
2449    }
2450
2451    #[test]
2452    fn vm_repeat_map_single_entry_tuple_pattern() {
2453        use crate::ast::stmt::Pattern;
2454        // One-entry map (iteration order is nondeterministic for larger maps).
2455        let ea: Arena<Expr> = Arena::new();
2456        let sa: Arena<Stmt> = Arena::new();
2457        let mut it = Interner::new();
2458        let m = it.intern("m");
2459        let k = it.intern("k");
2460        let v = it.intern("v");
2461        let map_ty = it.intern("Map");
2462        let key_a = it.intern("a");
2463        let show_s = it.intern("show");
2464        let body: &[Stmt] = sa.alloc_slice(vec![
2465            show(&ea, show_s, idref(&ea, k)),
2466            show(&ea, show_s, idref(&ea, v)),
2467        ]);
2468        let stmts = vec![
2469            Stmt::Let { var: m, ty: None, value: new_coll(&ea, map_ty), mutable: true },
2470            Stmt::SetIndex { collection: idref(&ea, m), index: text(&ea, key_a), value: num(&ea, 10) },
2471            Stmt::Repeat { pattern: Pattern::Tuple(vec![k, v]), iterable: idref(&ea, m), body },
2472        ];
2473        let out = compile_and_run(&stmts, &it).unwrap();
2474        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["a", "10"]);
2475        assert_vm_eq_treewalk(&stmts, &it);
2476    }
2477
2478    #[test]
2479    fn vm_repeat_tuple_pattern_on_non_tuple_errors_like_treewalk() {
2480        use crate::ast::stmt::Pattern;
2481        let ea: Arena<Expr> = Arena::new();
2482        let sa: Arena<Stmt> = Arena::new();
2483        let mut it = Interner::new();
2484        let k = it.intern("k");
2485        let v = it.intern("v");
2486        let show_s = it.intern("show");
2487        let body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, idref(&ea, k))]);
2488        let stmts = vec![Stmt::Repeat {
2489            pattern: Pattern::Tuple(vec![k, v]),
2490            iterable: list_lit(&ea, vec![num(&ea, 1)]),
2491            body,
2492        }];
2493        assert_err_parity(&stmts, &it);
2494    }
2495
2496    #[test]
2497    fn vm_repeat_break_exits_and_loop_var_scoped() {
2498        // Break exits the Repeat; the loop variable is gone after the loop.
2499        let ea: Arena<Expr> = Arena::new();
2500        let sa: Arena<Stmt> = Arena::new();
2501        let mut it = Interner::new();
2502        let xs = it.intern("xs");
2503        let x = it.intern("x");
2504        let show_s = it.intern("show");
2505        let break_blk: &[Stmt] = sa.alloc_slice(vec![brk()]);
2506        let body: &[Stmt] = sa.alloc_slice(vec![
2507            show(&ea, show_s, idref(&ea, x)),
2508            Stmt::If {
2509                cond: bin(&ea, BinaryOpKind::GtEq, idref(&ea, x), num(&ea, 2)),
2510                then_block: break_blk,
2511                else_block: None,
2512            },
2513        ]);
2514        let stmts = vec![
2515            letb(xs, list_lit(&ea, vec![num(&ea, 1), num(&ea, 2), num(&ea, 3)])),
2516            repeat_ident(x, idref(&ea, xs), body),
2517            show(&ea, show_s, idref(&ea, x)),
2518        ];
2519        // Both engines: print 1, 2, then `x` is out of scope after the loop.
2520        assert_outcome_eq_treewalk(&stmts, &it);
2521    }
2522
2523    #[test]
2524    fn vm_repeat_over_int_errors_like_treewalk() {
2525        let ea: Arena<Expr> = Arena::new();
2526        let sa: Arena<Stmt> = Arena::new();
2527        let mut it = Interner::new();
2528        let x = it.intern("x");
2529        let show_s = it.intern("show");
2530        let body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, idref(&ea, x))]);
2531        let stmts = vec![repeat_ident(x, num(&ea, 42), body)];
2532        assert_err_parity(&stmts, &it);
2533    }
2534
2535    // ---- Sprint 5 (statement core): Pop, RuntimeAssert, Zone, Concurrent ------
2536
2537    #[test]
2538    fn vm_pop_into_binds_and_empty_pop_is_nothing() {
2539        let ea: Arena<Expr> = Arena::new();
2540        let mut it = Interner::new();
2541        let xs = it.intern("xs");
2542        let v = it.intern("v");
2543        let w = it.intern("w");
2544        let show_s = it.intern("show");
2545        let stmts = vec![
2546            Stmt::Let { var: xs, ty: None, value: list_lit(&ea, vec![num(&ea, 1), num(&ea, 2)]), mutable: true },
2547            Stmt::Pop { collection: idref(&ea, xs), into: Some(v) },
2548            show(&ea, show_s, idref(&ea, v)),
2549            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
2550            Stmt::Pop { collection: idref(&ea, xs), into: None },
2551            Stmt::Pop { collection: idref(&ea, xs), into: Some(w) },
2552            show(&ea, show_s, idref(&ea, w)),
2553        ];
2554        let out = compile_and_run(&stmts, &it).unwrap();
2555        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["2", "1", "nothing"]);
2556        assert_vm_eq_treewalk(&stmts, &it);
2557    }
2558
2559    #[test]
2560    fn vm_runtime_assert_passes_and_fails_like_treewalk() {
2561        let ea: Arena<Expr> = Arena::new();
2562        let mut it = Interner::new();
2563        let show_s = it.intern("show");
2564        let ok_stmts = vec![
2565            Stmt::RuntimeAssert { condition: boolean(&ea, true) , hard: false },
2566            show(&ea, show_s, num(&ea, 1)),
2567        ];
2568        assert_eq!(compile_and_run(&ok_stmts, &it).unwrap().trim(), "1");
2569        assert_vm_eq_treewalk(&ok_stmts, &it);
2570
2571        let fail_stmts = vec![
2572            show(&ea, show_s, num(&ea, 1)),
2573            Stmt::RuntimeAssert { condition: boolean(&ea, false) , hard: false },
2574            show(&ea, show_s, num(&ea, 2)),
2575        ];
2576        assert_outcome_eq_treewalk(&fail_stmts, &it);
2577    }
2578
2579    #[test]
2580    fn vm_zone_swallows_return_inside_function() {
2581        // The tree-walker DISCARDS a zone body's ControlFlow: a Return inside
2582        // a Zone does not return from the function.
2583        let ea: Arena<Expr> = Arena::new();
2584        let sa: Arena<Stmt> = Arena::new();
2585        let ta: Arena<TypeExpr> = Arena::new();
2586        let mut it = Interner::new();
2587        let f = it.intern("f");
2588        let z = it.intern("z");
2589        let r = it.intern("r");
2590        let show_s = it.intern("show");
2591        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
2592        let _ = int_ty;
2593
2594        let zone_body: &[Stmt] = sa.alloc_slice(vec![ret(num(&ea, 5))]);
2595        let f_body: &[Stmt] = sa.alloc_slice(vec![
2596            Stmt::Zone { name: z, capacity: None, source_file: None, body: zone_body },
2597            show(&ea, show_s, num(&ea, 77)),
2598            ret(num(&ea, 1)),
2599        ]);
2600        let stmts = vec![
2601            fndef(f, vec![], f_body),
2602            letb(r, calle(&ea, f, vec![])),
2603            show(&ea, show_s, idref(&ea, r)),
2604        ];
2605        let out = compile_and_run(&stmts, &it).unwrap();
2606        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["77", "1"]);
2607        assert_vm_eq_treewalk(&stmts, &it);
2608    }
2609
2610    #[test]
2611    fn vm_zone_swallows_break_in_loop() {
2612        // `While …: Zone: Break` — the zone catches the Break before the loop
2613        // sees it, so the loop runs to its own condition.
2614        let ea: Arena<Expr> = Arena::new();
2615        let sa: Arena<Stmt> = Arena::new();
2616        let mut it = Interner::new();
2617        let i = it.intern("i");
2618        let z = it.intern("z");
2619        let show_s = it.intern("show");
2620        let zone_body: &[Stmt] = sa.alloc_slice(vec![brk()]);
2621        let body: &[Stmt] = sa.alloc_slice(vec![
2622            Stmt::Zone { name: z, capacity: None, source_file: None, body: zone_body },
2623            show(&ea, show_s, idref(&ea, i)),
2624            Stmt::Set { target: i, value: bin(&ea, BinaryOpKind::Add, idref(&ea, i), num(&ea, 1)) },
2625        ]);
2626        let stmts = vec![
2627            Stmt::Let { var: i, ty: None, value: num(&ea, 0), mutable: true },
2628            Stmt::While { cond: bin(&ea, BinaryOpKind::Lt, idref(&ea, i), num(&ea, 2)), body, decreasing: None },
2629        ];
2630        let out = compile_and_run(&stmts, &it).unwrap();
2631        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["0", "1"]);
2632        assert_vm_eq_treewalk(&stmts, &it);
2633    }
2634
2635    #[test]
2636    fn vm_return_inside_repeat_inside_zone_unwinds_iterator() {
2637        // The Return jumps out across a live Repeat into the zone end; the
2638        // iterator must be unwound so a LATER Repeat still works.
2639        let ea: Arena<Expr> = Arena::new();
2640        let sa: Arena<Stmt> = Arena::new();
2641        let mut it = Interner::new();
2642        let z = it.intern("z");
2643        let x = it.intern("x");
2644        let y = it.intern("y");
2645        let show_s = it.intern("show");
2646        let rep_body: &[Stmt] = sa.alloc_slice(vec![ret(idref(&ea, x))]);
2647        let zone_body: &[Stmt] = sa.alloc_slice(vec![repeat_ident(
2648            x,
2649            list_lit(&ea, vec![num(&ea, 1), num(&ea, 2), num(&ea, 3)]),
2650            rep_body,
2651        )]);
2652        let second_body: &[Stmt] = sa.alloc_slice(vec![show(&ea, show_s, idref(&ea, y))]);
2653        let stmts = vec![
2654            Stmt::Zone { name: z, capacity: None, source_file: None, body: zone_body },
2655            show(&ea, show_s, num(&ea, 0)),
2656            repeat_ident(y, list_lit(&ea, vec![num(&ea, 7), num(&ea, 8)]), second_body),
2657        ];
2658        let out = compile_and_run(&stmts, &it).unwrap();
2659        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["0", "7", "8"]);
2660        assert_vm_eq_treewalk(&stmts, &it);
2661    }
2662
2663    #[test]
2664    fn vm_concurrent_tasks_run_sequentially_share_scope_and_swallow_flow() {
2665        let ea: Arena<Expr> = Arena::new();
2666        let sa: Arena<Stmt> = Arena::new();
2667        let mut it = Interner::new();
2668        let a = it.intern("a");
2669        let show_s = it.intern("show");
2670        let tasks: &[Stmt] = sa.alloc_slice(vec![
2671            letb(a, num(&ea, 1)),
2672            show(&ea, show_s, idref(&ea, a)),
2673            ret(num(&ea, 9)),
2674            show(&ea, show_s, num(&ea, 3)),
2675        ]);
2676        let stmts = vec![
2677            Stmt::Concurrent { tasks },
2678            // The task's Let persists (tasks execute in the enclosing scope),
2679            // and the Return was swallowed.
2680            show(&ea, show_s, idref(&ea, a)),
2681        ];
2682        let out = compile_and_run(&stmts, &it).unwrap();
2683        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["1", "3", "1"]);
2684        assert_vm_eq_treewalk(&stmts, &it);
2685    }
2686
2687    // ---- Sprint 4: short-circuit And/Or, Concat/BitXor/Shl/Shr, Not, literals --
2688
2689    fn not_e<'a>(ea: &'a Arena<Expr<'a>>, operand: &'a Expr<'a>) -> &'a Expr<'a> {
2690        ea.alloc(Expr::Not { operand })
2691    }
2692
2693    /// `## To noisy: Show 9. Return 1.` — its output is the side-effect probe.
2694    fn noisy_fn<'a>(
2695        ea: &'a Arena<Expr<'a>>,
2696        sa: &'a Arena<Stmt<'a>>,
2697        noisy: Symbol,
2698        show_s: Symbol,
2699    ) -> Stmt<'a> {
2700        let body: &[Stmt] = sa.alloc_slice(vec![
2701            show(ea, show_s, num(ea, 9)),
2702            ret(num(ea, 1)),
2703        ]);
2704        fndef(noisy, vec![], body)
2705    }
2706
2707    #[test]
2708    fn vm_and_short_circuits_side_effects() {
2709        // false and noisy() — noisy must NOT run; output is just "false".
2710        let ea: Arena<Expr> = Arena::new();
2711        let sa: Arena<Stmt> = Arena::new();
2712        let mut it = Interner::new();
2713        let noisy = it.intern("noisy");
2714        let r = it.intern("r");
2715        let show_s = it.intern("show");
2716        let stmts = vec![
2717            noisy_fn(&ea, &sa, noisy, show_s),
2718            letb(r, bin(&ea, BinaryOpKind::And, boolean(&ea, false), calle(&ea, noisy, vec![]))),
2719            show(&ea, show_s, idref(&ea, r)),
2720        ];
2721        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "false");
2722        assert_vm_eq_treewalk(&stmts, &it);
2723    }
2724
2725    #[test]
2726    fn vm_or_short_circuits_side_effects() {
2727        // true or noisy() — noisy must NOT run; output is just "true".
2728        let ea: Arena<Expr> = Arena::new();
2729        let sa: Arena<Stmt> = Arena::new();
2730        let mut it = Interner::new();
2731        let noisy = it.intern("noisy");
2732        let r = it.intern("r");
2733        let show_s = it.intern("show");
2734        let stmts = vec![
2735            noisy_fn(&ea, &sa, noisy, show_s),
2736            letb(r, bin(&ea, BinaryOpKind::Or, boolean(&ea, true), calle(&ea, noisy, vec![]))),
2737            show(&ea, show_s, idref(&ea, r)),
2738        ];
2739        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "true");
2740        assert_vm_eq_treewalk(&stmts, &it);
2741    }
2742
2743    #[test]
2744    fn vm_and_is_logical_truthiness_to_bool() {
2745        // `and` is LOGICAL: `6 and 3` → true. A truthy left still evaluates
2746        // the right (noisy RUNS, prints 9; returns 1 → truthy → true). The
2747        // bitwise spelling is `&` (Op::BitAnd), locked below.
2748        let ea: Arena<Expr> = Arena::new();
2749        let sa: Arena<Stmt> = Arena::new();
2750        let mut it = Interner::new();
2751        let noisy = it.intern("noisy");
2752        let a = it.intern("a");
2753        let b = it.intern("b");
2754        let show_s = it.intern("show");
2755        let stmts = vec![
2756            noisy_fn(&ea, &sa, noisy, show_s),
2757            letb(a, bin(&ea, BinaryOpKind::And, num(&ea, 6), num(&ea, 3))),
2758            show(&ea, show_s, idref(&ea, a)),
2759            letb(b, bin(&ea, BinaryOpKind::And, num(&ea, 6), calle(&ea, noisy, vec![]))),
2760            show(&ea, show_s, idref(&ea, b)),
2761        ];
2762        let out = compile_and_run(&stmts, &it).unwrap();
2763        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["true", "9", "true"]);
2764        assert_vm_eq_treewalk(&stmts, &it);
2765    }
2766
2767    #[test]
2768    fn vm_bitand_bitor_int_are_bitwise() {
2769        // `6 & 3` → 2 and `6 | 3` → 7: the symbols are the bitwise ops.
2770        let ea: Arena<Expr> = Arena::new();
2771        let mut it = Interner::new();
2772        let a = it.intern("a");
2773        let b = it.intern("b");
2774        let show_s = it.intern("show");
2775        let stmts = vec![
2776            letb(a, bin(&ea, BinaryOpKind::BitAnd, num(&ea, 6), num(&ea, 3))),
2777            show(&ea, show_s, idref(&ea, a)),
2778            letb(b, bin(&ea, BinaryOpKind::BitOr, num(&ea, 6), num(&ea, 3))),
2779            show(&ea, show_s, idref(&ea, b)),
2780        ];
2781        let out = compile_and_run(&stmts, &it).unwrap();
2782        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["2", "7"]);
2783        assert_vm_eq_treewalk(&stmts, &it);
2784    }
2785
2786    #[test]
2787    fn vm_and_or_mixed_int_bool_uses_truthiness() {
2788        // Truthiness applies uniformly — Int operands coerce like any other.
2789        let ea: Arena<Expr> = Arena::new();
2790        let mut it = Interner::new();
2791        let a = it.intern("a");
2792        let b = it.intern("b");
2793        let show_s = it.intern("show");
2794        let stmts = vec![
2795            letb(a, bin(&ea, BinaryOpKind::And, num(&ea, 1), boolean(&ea, false))),
2796            show(&ea, show_s, idref(&ea, a)),
2797            letb(b, bin(&ea, BinaryOpKind::Or, num(&ea, 0), boolean(&ea, true))),
2798            show(&ea, show_s, idref(&ea, b)),
2799        ];
2800        let out = compile_and_run(&stmts, &it).unwrap();
2801        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["false", "true"]);
2802        assert_vm_eq_treewalk(&stmts, &it);
2803    }
2804
2805    #[test]
2806    fn vm_not_is_logical_truthiness() {
2807        // `not` negates truthiness — Bool out for every operand (`~` is the
2808        // bitwise complement and lowers to `x ^ -1`, never through Op::Not).
2809        let ea: Arena<Expr> = Arena::new();
2810        let mut it = Interner::new();
2811        let a = it.intern("a");
2812        let b = it.intern("b");
2813        let show_s = it.intern("show");
2814        let stmts = vec![
2815            letb(a, not_e(&ea, num(&ea, 6))),
2816            show(&ea, show_s, idref(&ea, a)),
2817            letb(b, not_e(&ea, boolean(&ea, true))),
2818            show(&ea, show_s, idref(&ea, b)),
2819        ];
2820        let out = compile_and_run(&stmts, &it).unwrap();
2821        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["false", "false"]);
2822        assert_vm_eq_treewalk(&stmts, &it);
2823    }
2824
2825    #[test]
2826    fn vm_not_text_is_emptiness_matches_treewalk() {
2827        // `not "hi"` → false (non-empty Text is truthy); total, never an error.
2828        let ea: Arena<Expr> = Arena::new();
2829        let mut it = Interner::new();
2830        let x = it.intern("x");
2831        let hi = it.intern("hi");
2832        let show_s = it.intern("show");
2833        let stmts = vec![
2834            letb(x, not_e(&ea, text(&ea, hi))),
2835            show(&ea, show_s, idref(&ea, x)),
2836        ];
2837        let out = compile_and_run(&stmts, &it).unwrap();
2838        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["false"]);
2839        assert_vm_eq_treewalk(&stmts, &it);
2840    }
2841
2842    #[test]
2843    fn vm_concat_bitxor_shl_shr_match_treewalk() {
2844        let ea: Arena<Expr> = Arena::new();
2845        let mut it = Interner::new();
2846        let show_s = it.intern("show");
2847        let names: Vec<Symbol> = (0..4).map(|k| it.intern(&format!("o{k}"))).collect();
2848        let stmts = vec![
2849            letb(names[0], bin(&ea, BinaryOpKind::Concat, num(&ea, 1), num(&ea, 2))),
2850            show(&ea, show_s, idref(&ea, names[0])),
2851            letb(names[1], bin(&ea, BinaryOpKind::BitXor, num(&ea, 6), num(&ea, 3))),
2852            show(&ea, show_s, idref(&ea, names[1])),
2853            letb(names[2], bin(&ea, BinaryOpKind::Shl, num(&ea, 1), num(&ea, 3))),
2854            show(&ea, show_s, idref(&ea, names[2])),
2855            letb(names[3], bin(&ea, BinaryOpKind::Shr, num(&ea, 16), num(&ea, 2))),
2856            show(&ea, show_s, idref(&ea, names[3])),
2857        ];
2858        let out = compile_and_run(&stmts, &it).unwrap();
2859        assert_eq!(out.lines().collect::<Vec<_>>(), vec!["12", "5", "8", "4"]);
2860        assert_vm_eq_treewalk(&stmts, &it);
2861    }
2862
2863    #[test]
2864    fn vm_char_nothing_and_temporal_literals_display_like_treewalk() {
2865        let ea: Arena<Expr> = Arena::new();
2866        let mut it = Interner::new();
2867        let show_s = it.intern("show");
2868        let lits: Vec<&Expr> = vec![
2869            ea.alloc(Expr::Literal(Literal::Char('x'))),
2870            ea.alloc(Expr::Literal(Literal::Nothing)),
2871            ea.alloc(Expr::Literal(Literal::Duration(1_500_000_000))),
2872            ea.alloc(Expr::Literal(Literal::Date(19753))),
2873            ea.alloc(Expr::Literal(Literal::Moment(86_400_000_000_000))),
2874            ea.alloc(Expr::Literal(Literal::Span { months: 2, days: 3 })),
2875            ea.alloc(Expr::Literal(Literal::Time(3_600_000_000_000))),
2876        ];
2877        let mut stmts = Vec::new();
2878        for (k, lit) in lits.into_iter().enumerate() {
2879            let v = it.intern(&format!("lit{k}"));
2880            stmts.push(letb(v, lit));
2881            stmts.push(show(&ea, show_s, idref(&ea, v)));
2882        }
2883        assert_vm_eq_treewalk(&stmts, &it);
2884    }
2885
2886    // ---- Sprint 0: encoding widening + compiler hardening ---------------------
2887
2888    #[test]
2889    fn vm_compiles_300_element_list_literal() {
2890        // 300 > u8::MAX — list literals must not be capped at 255 elements.
2891        let ea: Arena<Expr> = Arena::new();
2892        let mut it = Interner::new();
2893        let xs = it.intern("xs");
2894        let show_s = it.intern("show");
2895        let items: Vec<&Expr> = (0..300).map(|k| num(&ea, k)).collect();
2896        let stmts = vec![
2897            letb(xs, list_lit(&ea, items)),
2898            show(&ea, show_s, length_of(&ea, idref(&ea, xs))),
2899            show(&ea, show_s, index_at(&ea, idref(&ea, xs), num(&ea, 300))),
2900        ];
2901        let out = compile_and_run(&stmts, &it).unwrap();
2902        let lines: Vec<&str> = out.lines().collect();
2903        assert_eq!(lines, vec!["300", "299"]);
2904        assert_vm_eq_treewalk(&stmts, &it);
2905    }
2906
2907    #[test]
2908    fn vm_compiles_main_with_400_locals() {
2909        // 400 locals exceed a u8 register index — frames must address more
2910        // than 256 registers.
2911        let ea: Arena<Expr> = Arena::new();
2912        let mut it = Interner::new();
2913        let show_s = it.intern("show");
2914        let mut stmts = Vec::new();
2915        let mut last = None;
2916        for k in 0..400i64 {
2917            let v = it.intern(&format!("v{k}"));
2918            stmts.push(letb(v, num(&ea, k)));
2919            last = Some(v);
2920        }
2921        stmts.push(show(&ea, show_s, idref(&ea, last.unwrap())));
2922        assert_eq!(compile_and_run(&stmts, &it).unwrap().trim(), "399");
2923        assert_vm_eq_treewalk(&stmts, &it);
2924    }
2925
2926    #[test]
2927    fn vm_const_pool_dedups_identical_literals() {
2928        // The same literal used many times must occupy one constant-pool slot.
2929        let ea: Arena<Expr> = Arena::new();
2930        let mut it = Interner::new();
2931        let mut stmts = Vec::new();
2932        for k in 0..50 {
2933            let v = it.intern(&format!("c{k}"));
2934            stmts.push(letb(v, num(&ea, 7)));
2935        }
2936        let program = Compiler::compile(&stmts, &it).unwrap();
2937        assert_eq!(
2938            program.constants.len(),
2939            1,
2940            "identical Int literals must dedup to a single pool entry"
2941        );
2942    }
2943
2944    #[test]
2945    fn vm_patch_jump_is_total() {
2946        // Backpatching a non-jump instruction is a compiler bug that must
2947        // surface as Err, never a panic.
2948        let mut code = vec![Op::Halt];
2949        assert!(super::compiler::patch_jump(&mut code, 0, 5).is_err());
2950
2951        let mut code = vec![Op::Jump { target: usize::MAX }];
2952        assert!(super::compiler::patch_jump(&mut code, 0, 5).is_ok());
2953        assert!(matches!(code[0], Op::Jump { target: 5 }));
2954
2955        // Out-of-bounds instruction index must also be an Err, not a panic.
2956        let mut code = vec![Op::Halt];
2957        assert!(super::compiler::patch_jump(&mut code, 9, 5).is_err());
2958    }
2959
2960    #[test]
2961    fn vm_deeply_nested_expr_errors_not_overflows() {
2962        // A 100k-deep expression must produce a compile error, not blow the
2963        // native stack.
2964        let ea: Arena<Expr> = Arena::new();
2965        let mut it = Interner::new();
2966        let x = it.intern("x");
2967        let mut e = num(&ea, 1);
2968        for _ in 0..100_000 {
2969            e = bin(&ea, BinaryOpKind::Add, e, num(&ea, 1));
2970        }
2971        let stmts = vec![letb(x, e)];
2972        let err = Compiler::compile(&stmts, &it).unwrap_err();
2973        assert!(err.contains("too deeply nested"), "got: {err}");
2974    }
2975
2976    #[test]
2977    fn vm_range_with_float_bound_errors_not_panics() {
2978        // `1 to 2.5` — the tree-walker errors with "Range requires Int bounds";
2979        // the VM must return the same Err, never panic.
2980        let ea: Arena<Expr> = Arena::new();
2981        let mut it = Interner::new();
2982        let xs = it.intern("xs");
2983        let f = ea.alloc(Expr::Literal(Literal::Float(2.5)));
2984        let stmts = vec![letb(xs, range_e(&ea, num(&ea, 1), f))];
2985        let vm_err = compile_and_run(&stmts, &it).unwrap_err();
2986        let tw_err = run_treewalk(&stmts, &it).unwrap_err();
2987        assert_eq!(vm_err, tw_err);
2988    }
2989
2990    #[test]
2991    fn vm_register_file_growth_is_capped() {
2992        // WIDE frames recursing: each frame claims ~3000 registers (a 3000-
2993        // element list literal), so the register file hits MAX_REGISTER_FILE
2994        // (1M) near depth ~350 — well before the 1000 call-depth limit. The
2995        // VM must error, not consume unbounded memory. (VM-only: the
2996        // tree-walker would blow the native stack on this program.)
2997        //
2998        // The recursion is NON-tail (`spin(n+1) - 1`): a direct `return spin(n+1)`
2999        // is a self-tail-call and TCO loops it in constant stack (one frame, no
3000        // register growth), so it would never hit the cap. The `- 1` keeps the
3001        // call out of tail position so real frames accumulate.
3002        let ea: Arena<Expr> = Arena::new();
3003        let sa: Arena<Stmt> = Arena::new();
3004        let ta: Arena<TypeExpr> = Arena::new();
3005        let mut it = Interner::new();
3006        let spin = it.intern("spin");
3007        let n = it.intern("n");
3008        let big = it.intern("big");
3009        let r = it.intern("r");
3010        let show_s = it.intern("show");
3011        let int_ty = ta.alloc(TypeExpr::Primitive(it.intern("Int")));
3012
3013        let then_blk: &[Stmt] = sa.alloc_slice(vec![ret(num(&ea, 0))]);
3014        let cond = bin(&ea, BinaryOpKind::GtEq, idref(&ea, n), num(&ea, 900));
3015        let wide: Vec<&Expr> = (0..3000).map(|_| num(&ea, 0)).collect();
3016        let rec = calle(&ea, spin, vec![bin(&ea, BinaryOpKind::Add, idref(&ea, n), num(&ea, 1))]);
3017        let rec_nontail = bin(&ea, BinaryOpKind::Subtract, rec, num(&ea, 1));
3018        let body: &[Stmt] = sa.alloc_slice(vec![
3019            Stmt::If { cond, then_block: then_blk, else_block: None },
3020            letb(big, list_lit(&ea, wide)),
3021            ret(rec_nontail),
3022        ]);
3023        let stmts = vec![
3024            fndef(spin, vec![(n, int_ty)], body),
3025            letb(r, calle(&ea, spin, vec![num(&ea, 0)])),
3026            show(&ea, show_s, idref(&ea, r)),
3027        ];
3028        let err = compile_and_run(&stmts, &it).unwrap_err();
3029        assert!(err.contains("register file"), "got: {err}");
3030    }
3031
3032    #[test]
3033    fn vm_if_else_takes_correct_branch() {
3034        // Let x be 3. If x > 5: Show 100. Otherwise: Show 200.   → 200
3035        let ea: Arena<Expr> = Arena::new();
3036        let sa: Arena<Stmt> = Arena::new();
3037        let mut it = Interner::new();
3038        let x = it.intern("x");
3039        let console = it.intern("show");
3040
3041        let three = ea.alloc(Expr::Literal(Literal::Number(3)));
3042        let x_c = ea.alloc(Expr::Identifier(x));
3043        let five = ea.alloc(Expr::Literal(Literal::Number(5)));
3044        let cond = ea.alloc(Expr::BinaryOp { op: BinaryOpKind::Gt, left: x_c, right: five });
3045
3046        let hundred = ea.alloc(Expr::Literal(Literal::Number(100)));
3047        let two_hundred = ea.alloc(Expr::Literal(Literal::Number(200)));
3048        let cref1 = ea.alloc(Expr::Identifier(console));
3049        let cref2 = ea.alloc(Expr::Identifier(console));
3050        let then_block: &[Stmt] = sa.alloc_slice(vec![Stmt::Show { object: hundred, recipient: cref1 }]);
3051        let else_block: &[Stmt] = sa.alloc_slice(vec![Stmt::Show { object: two_hundred, recipient: cref2 }]);
3052
3053        let stmts = vec![
3054            Stmt::Let { var: x, ty: None, value: three, mutable: false },
3055            Stmt::If { cond, then_block, else_block: Some(else_block) },
3056        ];
3057
3058        let out = compile_and_run(&stmts, &it).unwrap();
3059        assert_eq!(out.trim(), "200");
3060        assert_vm_eq_treewalk(&stmts, &it);
3061    }
3062}