Skip to main content

logicaffeine_compile/vm/
machine.rs

1//! The bytecode dispatch loop.
2//!
3//! Registers live in one contiguous `Vec<Value>`; `base` is the current frame's
4//! offset into it, so every register access is `registers[base + r]`. Calls use
5//! register windowing — the callee's frame starts at the caller's `args_start`,
6//! so arguments are passed with zero copying.
7
8use super::instruction::{CompiledProgram, Constant, FuncIdx, Op, Reg};
9use super::value::Value;
10use super::MAX_REGISTER_FILE;
11use logicaffeine_runtime::{ChanId, RtPayload, SelectArm, TaskId};
12
13/// LEVER B callee analysis — may a region CALL this function while passing a
14/// pinned list argument? Returns `(list_params_stable, returns_list_param)`,
15/// both SOUND under-approximations:
16/// - `list_params_stable`: the body has NO `ListPush` and NO sub-`Call` (either
17///   could reallocate a list-param's buffer, staling the caller's derived raw
18///   pointer). So every list-param buffer keeps its address across the call.
19/// - `returns_list_param`: every `Return` traces — through `Move`s, to a
20///   fixpoint — to a list PARAMETER slot, and all list params share one element
21///   kind (the returned list kind is then unambiguous from the signature). A
22///   purely scalar return makes this `false` and rides `ret` instead.
23fn analyze_list_call_safety(
24    body: &[Op],
25    param_count: u16,
26    param_kinds: &[Option<super::native_tier::ParamKind>],
27    register_count: usize,
28) -> (bool, bool) {
29    use super::native_tier::ParamKind;
30    let n = register_count.max(param_count as usize);
31    // Slots that (transitively via Move) hold a list-parameter handle.
32    let mut is_param_list = vec![false; n];
33    for i in 0..param_count as usize {
34        if matches!(param_kinds.get(i), Some(Some(ParamKind::List(_)))) {
35            is_param_list[i] = true;
36        }
37    }
38    loop {
39        let mut changed = false;
40        for op in body {
41            if let Op::Move { dst, src } = *op {
42                let (d, s) = (dst as usize, src as usize);
43                if d < n && s < n && is_param_list[s] && !is_param_list[d] {
44                    is_param_list[d] = true;
45                    changed = true;
46                }
47            }
48        }
49        if !changed {
50            break;
51        }
52    }
53    let list_params_stable =
54        !body.iter().any(|op| matches!(op, Op::ListPush { .. } | Op::Call { .. }));
55    // All list params must share a single element kind so the return's list kind
56    // is unambiguous from the signature alone.
57    let mut elem: Option<super::native_tier::PinElem> = None;
58    let mut uniform = true;
59    for pk in param_kinds {
60        if let Some(ParamKind::List(e)) = pk {
61            if elem.is_some() && elem != Some(*e) {
62                uniform = false;
63            }
64            elem = Some(*e);
65        }
66    }
67    let returns: Vec<u16> = body
68        .iter()
69        .filter_map(|op| if let Op::Return { src } = *op { Some(src) } else { None })
70        .collect();
71    let returns_list_param = uniform
72        && !returns.is_empty()
73        && returns
74            .iter()
75            .all(|&s| (s as usize) < n && is_param_list[s as usize]);
76    (list_params_stable, returns_list_param)
77}
78
79/// Whether `LOGOS_JIT_CANARY=1` armed the region-frame sentinel guard
80/// (read once; the per-region path stays branch-cheap). Off by default and
81/// in release, so normal runs pay nothing — it is a diagnostic for native
82/// out-of-bounds writes.
83fn jit_canary_enabled() -> bool {
84    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
85    *ON.get_or_init(|| std::env::var("LOGOS_JIT_CANARY").is_ok_and(|v| v == "1"))
86}
87
88/// How a native region run left the loop.
89enum RegionExit {
90    /// Fell out the loop's exit edge — resume at this pc.
91    At(usize),
92    /// Hit an in-region `Return` — perform the function return with this value.
93    Return(Value),
94}
95
96/// What the native boundary decided for one call.
97enum NativeDisposition {
98    /// Completed natively; here is the re-boxed result.
99    Done(Value),
100    /// Run this call on bytecode (not compiled / guard mismatch / replay
101    /// deopt).
102    Interpret,
103    /// Precise deopt: push these frames and resume at `resume_pc`.
104    Materialize {
105        resume_pc: usize,
106        frames: Vec<super::native_tier::NativeFrame>,
107        list_args: Vec<Value>,
108    },
109}
110
111#[derive(Clone, Copy)]
112struct CallFrame {
113    return_pc: usize,
114    return_reg: Reg,
115    caller_base: usize,
116    restore_len: usize,
117    /// Iterator-stack depth at call entry; a Return unwinds any iterators the
118    /// callee left open (e.g. `Return` inside a `Repeat`).
119    iter_depth: usize,
120    /// The function whose body this frame runs — selects the per-frame
121    /// named-register map for loop regions tiering up inside it.
122    func: u16,
123    /// Absolute register index of this call's argument window (`callee_base`) and
124    /// how many arguments it holds. On return these slots are nulled: as the
125    /// callee's parameters they persist below `restore_len`, and a collection
126    /// argument would otherwise leave a live `Rc` clone in the caller's frame,
127    /// inflating `strong_count` and forcing needless copy-on-write later. Zero
128    /// `arg_count` (native/scheduler frames) clears nothing.
129    arg_lo: usize,
130    arg_count: u16,
131}
132
133/// The outcome of one `run_until_block` slice (T11). A non-concurrent program
134/// always returns `Done` on the first slice, so `run()` behaves exactly as the
135/// old single-shot loop. A concurrent task returns `Blocked` at each scheduler
136/// op; the driver reads [`Vm::take_pending`] and re-enters after the block clears.
137pub(crate) enum VmStep {
138    /// The (sub)program ran to completion (`Halt` or code exhausted) with the
139    /// given result payload (the main/return value, `Nothing` if none).
140    Done(crate::interpreter::RuntimeValue),
141    /// Suspended at a concurrency op; [`Vm::take_pending`] carries the request.
142    Blocked,
143    /// Suspended by the debug stepper after exhausting its per-call op budget
144    /// (`STEPPED = true` only). Resumable on the next [`Vm::run_steps`]. The
145    /// production path (`run_until_block`, `STEPPED = false`) never yields this.
146    Paused,
147}
148
149/// A read-only view of the paused VM for the Studio debug drawer: the program
150/// counter, the live call frames (Main first, current last) with their register
151/// values, the named globals, and the output so far. Values are rendered with the
152/// same `to_display_string` the `Show` op uses.
153pub(crate) struct DebugView {
154    pub pc: usize,
155    pub current_func: Option<u16>,
156    pub frames: Vec<DebugFrameView>,
157    pub globals: Vec<(String, String)>,
158    pub output: Vec<String>,
159}
160
161/// One call frame's registers in a [`DebugView`]. `func` is `None` for Main; `base`
162/// is the frame's start offset in the linear register file (its stack address).
163pub(crate) struct DebugFrameView {
164    pub func: Option<u16>,
165    pub base: usize,
166    /// `(index, type-name, display-value)` per register, e.g. `(1, "Int", "6")`.
167    pub registers: Vec<(u16, String, String)>,
168}
169
170/// One heap-allocated object (list / map / set / tuple / text / struct) reachable
171/// from the current frame or the globals — the heap-viewer's unit. `id` is the live
172/// allocation address (so two roots sharing one object share an `id` → aliasing), and
173/// `rc` is its reference count.
174pub(crate) struct HeapObjView {
175    pub id: usize,
176    pub kind: String,
177    pub summary: String,
178    /// The underlying storage layout (e.g. `packed Vec<i64>`, `columnar`) — teaches
179    /// how the data is actually laid out in memory.
180    pub storage: String,
181    pub rc: usize,
182    pub referenced_by: Vec<String>,
183}
184
185/// The heap identity of a value — its allocation address, kind, reference count, and
186/// storage-layout label. `None` for inline scalars (Int/Float/Bool/Char/…), which live
187/// in the register slot itself and are not heap objects.
188fn heap_identity(val: &Value) -> Option<(usize, String, usize, String)> {
189    use crate::interpreter::RuntimeValue as RV;
190    use std::rc::Rc;
191    let s = |x: &str| x.to_string();
192    match val.as_runtime_ref()? {
193        RV::List(rc) => Some((Rc::as_ptr(rc) as usize, s("list"), Rc::strong_count(rc), rc.borrow().storage_label().to_string())),
194        RV::Map(rc) => Some((Rc::as_ptr(rc) as usize, s("map"), Rc::strong_count(rc), s("hash map"))),
195        RV::Set(rc) => Some((Rc::as_ptr(rc) as usize, s("set"), Rc::strong_count(rc), s("vec set"))),
196        RV::Tuple(rc) => Some((Rc::as_ptr(rc) as usize, s("tuple"), Rc::strong_count(rc), s("fixed tuple"))),
197        RV::Text(rc) => Some((Rc::as_ptr(rc) as usize, s("text"), Rc::strong_count(rc), s("Rc<String>"))),
198        RV::Struct(b) => Some((&**b as *const _ as usize, s("struct"), 1, s("field map"))),
199        RV::Inductive(b) => Some((&**b as *const _ as usize, s("enum"), 1, s("tagged variant"))),
200        _ => None,
201    }
202}
203
204/// The resumable execution state of a single-task program — enough to pause it and
205/// resume in a freshly-built `tier: None` VM. The debugger owns the
206/// [`CompiledProgram`] and rebuilds the VM each step (it cannot hold a borrowing
207/// `Vm<'p>` across steps), threading this snapshot through. Concurrency request
208/// state is intentionally omitted (the debugger is single-task, bytecode-tier).
209#[derive(Clone)]
210pub(crate) struct DebugVmState {
211    registers: Vec<Value>,
212    base: usize,
213    globals: Vec<Option<Value>>,
214    lines: Vec<String>,
215    iter_stack: Vec<(Vec<Value>, usize)>,
216    sched_active: bool,
217    sched_pc: usize,
218    sched_call_stack: Vec<CallFrame>,
219}
220
221impl DebugVmState {
222    /// The pc the program is stopped at (the op about to execute).
223    pub(crate) fn pc(&self) -> usize {
224        self.sched_pc
225    }
226    /// Call-stack depth (0 = in Main), for step-over / step-out.
227    pub(crate) fn call_depth(&self) -> usize {
228        self.sched_call_stack.len()
229    }
230}
231
232/// A concurrency request a suspended [`Vm`] hands to the scheduler driver — the
233/// VM analog of the tree-walker's `BlockingRequest`. A spawned child travels as a
234/// fully-built `Vm` (sharing the parent's `&'p program`), which the driver wraps
235/// in its own task.
236pub(crate) enum VmBlock {
237    /// Create a channel (`None` = the scheduler's default capacity); resume with its id.
238    NewChan(Option<usize>),
239    /// Send a value into a channel (blocks if full).
240    Send(ChanId, RtPayload),
241    /// Receive from a channel (blocks if empty); resume with the value.
242    Recv(ChanId),
243    /// Non-blocking send; resume with `Bool(success)`.
244    TrySend(ChanId, RtPayload),
245    /// Non-blocking receive; resume with the value or `Nothing`.
246    TryRecv(ChanId),
247    /// Close a channel.
248    Close(ChanId),
249    /// Spawn a child *by descriptor* — function index + materialised args — so the
250    /// driver builds the child `Vm` (the cooperative driver inline, a work-stealing
251    /// worker locally over its own program). `want_handle` distinguishes a launch
252    /// that binds a task handle. Resume with the child's `TaskId`.
253    SpawnDesc { func: FuncIdx, args: Vec<RtPayload>, want_handle: bool },
254    /// Await a task's completion; resume with its result payload.
255    Await(TaskId),
256    /// Abort a task.
257    Abort(TaskId),
258    /// Block on the first ready select arm; resume with the winning arm index.
259    Select(Vec<SelectArm>),
260    /// Sleep for some logical ticks.
261    Sleep(u64),
262    /// Dial the relay (async); resume when connected. Carries the URL value.
263    NetConnect(RtPayload),
264    /// Subscribe our inbox (async); resume when subscribed. Carries the topic value.
265    NetListen(RtPayload),
266    /// Encode + publish to a peer; resume immediately. Carries `(peer, message)`.
267    NetSend(RtPayload, RtPayload),
268    /// Batch-stream a list to a peer; resume immediately. Carries `(peer, list)`.
269    NetStream(RtPayload, RtPayload),
270    /// Await a message (or batch stream, if the flag) from a peer (blocks); resume with the value.
271    /// Carries `(peer, stream_flag)`.
272    NetAwait(RtPayload, bool),
273    /// Resolve an address value into a PeerAgent handle (its canonical topic); resume with the peer.
274    /// Carries the address value.
275    NetMakePeer(RtPayload),
276    /// CRDT sync point: publish the current counter, merge what has arrived, resume with the merged
277    /// value. Carries `(topic, current)`.
278    NetSync(RtPayload, RtPayload),
279}
280
281pub struct Vm<'p> {
282    program: &'p CompiledProgram,
283    /// The constant pool MATERIALISED into runtime values once at construction.
284    /// A `LoadConst` then clones the pre-built `Value` — for a heap `Text` that
285    /// is an `Rc` refcount bump, not a fresh `String`+`Rc` allocation, so a
286    /// 1-char literal reloaded every iteration of a hot loop (string_search's
287    /// `ch`) costs no heap traffic. The pool keeps a live reference, so a
288    /// freshly-loaded literal is never the sole owner and the in-place
289    /// `add_assign` append correctly declines to mutate it.
290    const_pool: Vec<Value>,
291    registers: Vec<Value>,
292    base: usize,
293    /// One element per `Show` (a shown value may itself contain newlines —
294    /// it is still ONE output line, like the tree-walker's emit callback).
295    lines: Vec<String>,
296    /// Live `Repeat` snapshots: (elements, next index). Stack-disciplined —
297    /// `IterPrepare` pushes, `IterPop` pops, nesting nests.
298    iter_stack: Vec<(Vec<Value>, usize)>,
299    /// Promoted globals (None = not yet defined; reading one is the
300    /// "Undefined variable" error).
301    globals: Vec<Option<Value>>,
302    /// Policy registry + interner for `Check` statements (absent ⇒ the
303    /// tree-walker's "Security Check requires policies" error).
304    policy_ctx: Option<(&'p crate::analysis::PolicyRegistry, &'p crate::intern::Interner)>,
305    /// The pluggable native tier (None = pure bytecode, e.g. WASM).
306    tier: Option<&'p dyn super::native_tier::NativeTier>,
307    /// Per-function call counts (profiling toward the tier threshold).
308    hot: Vec<u32>,
309    /// Per-function native state.
310    native: Vec<super::native_tier::NativeSlot>,
311    /// Back-edge counts for MAIN loops (keyed by loop-head pc). FxHash: this
312    /// is probed once per back-edge crossing of every Main loop that has not
313    /// (or cannot) tier up — a per-iteration cost on the bytecode path.
314    region_hot: rustc_hash::FxHashMap<usize, u32>,
315    /// Compiled Main-loop regions (keyed by loop-head pc; same probe rate).
316    regions: rustc_hash::FxHashMap<usize, super::native_tier::RegionSlot>,
317    /// Per-region (loop-head pc) collection registers this region mutates IN
318    /// PLACE. Under value semantics these are copy-on-write'd at region ENTRY
319    /// (`ensure_reg_owned`) so the native code's in-place writes cannot alias a
320    /// shared allocation — the perf-preserving follow-up to the correctness-first
321    /// decline. Only populated when the region is provably alias-free (a mutated
322    /// collection never escapes it), so entry-COW alone isolates it soundly.
323    region_cow_regs: rustc_hash::FxHashMap<usize, Vec<u16>>,
324    /// Per-pc dead-region bitset: once a loop head is known `Failed`
325    /// (un-tierable, or demoted after repeated guard misses) its entry here is
326    /// set, so the back-edge hook short-circuits with a single `Vec<bool>`
327    /// index instead of re-hashing `regions` on every iteration. Loops that
328    /// never tier (effectful bodies, `Text` ops, list-param fns) are the common
329    /// case and pay only this O(1) check after the first failure. Indexed by
330    /// loop-head pc; sized to the code length.
331    region_blacklist: Vec<bool>,
332    /// Program arguments for the `args()` system native — full argv, index 0 is
333    /// the program name (mirrors the compiled binary's `env::args()`). Empty
334    /// when none were supplied.
335    program_args: Vec<String>,
336    /// Per-program native-tier context: the EXODIA 4.7 entry table plus the
337    /// shared deopt-status and live-depth cells every chain patches.
338    native_ctx: super::native_tier::NativeCtx,
339    /// The off-thread native compiler (HOTSWAP §6), present only when the VM was
340    /// given the process-installed `&'static` tier via [`Vm::with_bg_native_tier`].
341    /// `None` ⇒ compile synchronously on this thread (the retained fallback, and the
342    /// only path for a borrowed `&'p` tier). Native-only: needs `std::thread`+forge.
343    #[cfg(not(target_arch = "wasm32"))]
344    bg: Option<super::bg_compile::BgCompiler>,
345    /// Axis-1 warm-bytecode side-table (HOTSWAP §7 / P11): re-optimized function
346    /// bodies appended here, in the same pc space *after* `program.code`. A `Call`
347    /// to a function with a `warm_entry` jumps into this buffer instead of the
348    /// baseline `entry_pc`. Pure bytecode — no forge, no `rustc` — so it is the
349    /// browser's hot-swap tier. Empty until a body is installed, and every read
350    /// path is gated on `pc >= program.code.len()`, so the baseline run loop is
351    /// byte-for-byte unchanged when nothing is warm.
352    warm_code: Vec<Op>,
353    /// Per-function warm entry (indexed by function index): the absolute pc of the
354    /// body in the unified `program.code ++ warm_code` space, and its register
355    /// window. `None` ⇒ the function runs its baseline body.
356    warm_entry: Vec<Option<WarmEntry>>,
357
358    /// Resumable-execution state for the scheduler driver (T11). When a task
359    /// suspends at a concurrency op, `run_until_block` saves its `pc` + call stack
360    /// here and restores them on the next slice. A non-concurrent run never sets
361    /// `sched_active`, so it starts fresh at pc 0 — byte-for-byte the old loop.
362    sched_active: bool,
363    sched_pc: usize,
364    sched_call_stack: Vec<CallFrame>,
365    /// The concurrency request produced by the last `Blocked` slice (taken by the
366    /// driver). `None` between slices and for a non-concurrent run.
367    pending: Option<VmBlock>,
368    /// The register the next resume value is delivered into (`None` for a block
369    /// that yields nothing, e.g. `Send`/`Close`).
370    resume_slot: Option<Reg>,
371    /// Accumulated `Select` arms awaiting a `SelectWait`: each runtime arm plus
372    /// the register a winning recv arm binds its value into. Persists across the
373    /// block so `deliver_select` can route the received value to the right arm.
374    select_pending: Vec<(SelectArm, Option<Reg>)>,
375    /// WS6 (Phase 13): the browser WASM-JIT tier. Consulted from `Op::Call` only under the
376    /// `wasm-jit` feature; entirely absent from the default build (and behind the native x86
377    /// forge tier on native, so it is the JIT tier specifically where forge cannot run —
378    /// wasm32).
379    #[cfg(feature = "wasm-jit")]
380    wasm_tier: super::wasm_jit::WasmTier,
381}
382
383/// A warm function body's location in the unified pc space (`program.code` then
384/// `warm_code`) plus the register window it executes in.
385#[derive(Clone, Copy, Debug)]
386struct WarmEntry {
387    entry_pc: usize,
388    register_count: usize,
389}
390
391impl<'p> Vm<'p> {
392    pub fn new(program: &'p CompiledProgram) -> Self {
393        Vm {
394            program,
395            const_pool: program.constants.iter().map(const_to_value).collect(),
396            registers: vec![Value::nothing(); program.register_count],
397            base: 0,
398            lines: Vec::new(),
399            iter_stack: Vec::new(),
400            globals: vec![None; program.globals.len()],
401            policy_ctx: None,
402            tier: None,
403            hot: vec![0; program.functions.len()],
404            native: (0..program.functions.len())
405                .map(|_| super::native_tier::NativeSlot::Untried)
406                .collect(),
407            region_hot: rustc_hash::FxHashMap::default(),
408            regions: rustc_hash::FxHashMap::default(),
409            region_cow_regs: rustc_hash::FxHashMap::default(),
410            region_blacklist: vec![false; program.code.len()],
411            program_args: Vec::new(),
412            native_ctx: super::native_tier::NativeCtx {
413                table: std::sync::Arc::new(super::native_tier::FnTable::new(
414                    program.functions.len(),
415                )),
416                status: std::sync::Arc::new(std::sync::atomic::AtomicI64::new(0)),
417                depth: std::sync::Arc::new(std::sync::atomic::AtomicI64::new(0)),
418            },
419            #[cfg(not(target_arch = "wasm32"))]
420            bg: None,
421            warm_code: Vec::new(),
422            warm_entry: vec![None; program.functions.len()],
423            sched_active: false,
424            sched_pc: 0,
425            sched_call_stack: Vec::new(),
426            pending: None,
427            resume_slot: None,
428            select_pending: Vec::new(),
429            #[cfg(feature = "wasm-jit")]
430            wasm_tier: super::wasm_jit::WasmTier::new(50),
431        }
432    }
433
434    /// Install a re-optimized body as function `fi`'s warm tier (HOTSWAP §7 / P11):
435    /// append it to `warm_code` after `program.code`, rebasing its 0-relative jumps
436    /// into that unified pc space, and point `warm_entry[fi]` at it. Subsequent calls
437    /// to `fi` run this body. The body shares the program's constant pool (a
438    /// `FnBytecode` preserves constant indices), so only jumps are relocated.
439    pub fn install_warm_bytecode(&mut self, fi: usize, fnbc: &super::fn_bytecode::FnBytecode) -> bool {
440        // Refuse a structurally-invalid body (out-of-range jump/call, missing terminal
441        // op) or one whose arity disagrees with the baseline function — a corrupt cache
442        // entry or a buggy producer then falls back to baseline instead of fetching past
443        // the warm buffer (panic) or reading the wrong registers (HOTSWAP §P12 robustness).
444        if !fnbc.is_well_formed(self.program.functions.len()) {
445            return false;
446        }
447        match self.program.functions.get(fi) {
448            Some(f) if f.param_count == fnbc.param_count => {}
449            _ => return false,
450        }
451        let abs_base = self.program.code.len() + self.warm_code.len();
452        self.warm_code
453            .extend(fnbc.code.iter().map(|&op| super::fn_bytecode::rebase(op, abs_base as isize)));
454        if fi >= self.warm_entry.len() {
455            self.warm_entry.resize(fi + 1, None);
456        }
457        self.warm_entry[fi] = Some(WarmEntry {
458            entry_pc: abs_base,
459            register_count: fnbc.register_count,
460        });
461        let name = self.fn_name(fi);
462        super::tier_trace::trace_transition(fi, &name, super::tier_trace::ExecTier::Warm);
463        true
464    }
465
466    /// Mark a loop head permanently `Failed` and record it in the per-pc
467    /// blacklist so the back-edge hook never probes `regions` for it again.
468    /// Every `RegionSlot::Failed` transition routes through here so the bitset
469    /// can never drift out of sync with the map.
470    fn mark_region_failed(&mut self, head: usize) {
471        self.regions.insert(head, super::native_tier::RegionSlot::Failed);
472        if let Some(slot) = self.region_blacklist.get_mut(head) {
473            *slot = true;
474        }
475    }
476
477    /// The collection registers a region mutates IN PLACE (the collection
478    /// operand of every mutation op in its body).
479    fn region_mutated_collection_regs(body: &[Op]) -> rustc_hash::FxHashSet<u16> {
480        let mut s = rustc_hash::FxHashSet::default();
481        for op in body {
482            match op {
483                Op::ListPush { list: c, .. }
484                | Op::SetAdd { set: c, .. }
485                | Op::RemoveFrom { collection: c, .. }
486                | Op::SetIndex { collection: c, .. }
487                | Op::SetIndexUnchecked { collection: c, .. }
488                | Op::ListPop { list: c, .. } => {
489                    s.insert(*c);
490                }
491                _ => {}
492            }
493        }
494        s
495    }
496
497    /// Collection registers that hold a FRESH, uniquely-owned collection for the
498    /// whole region: a `NewEmpty*{dst=C}` op DOMINATES every in-place mutation of
499    /// `C`. Such a collection is created anew on each entry to its live range, so its
500    /// mutation can NEVER alias — it needs no entry copy-on-write, and any use of
501    /// `C`'s register BEFORE the fresh definition is a disjoint (scalar) live range
502    /// that register-recycling left behind (fannkuch's `Set r to r-1` scratch landing
503    /// on `perm`'s slot before `perm` is created). Excluding these from the mutated
504    /// set keeps the region tier-able under value semantics WITHOUT weakening
505    /// soundness: a genuinely shared/aliased mutation has no dominating fresh
506    /// definition, so it stays in the set and is COW'd or declined.
507    ///
508    /// `body` is `program.code[head..=back]`; region-relative index `i` is pc `head+i`.
509    fn region_fresh_collection_regs(body: &[Op], head: usize) -> rustc_hash::FxHashSet<u16> {
510        let n = body.len();
511        let mut out = rustc_hash::FxHashSet::default();
512        if n == 0 {
513            return out;
514        }
515        // Region-relative successors (an edge leaving [head, back] is dropped — a
516        // fresh definition need only dominate mutations WITHIN the region).
517        let rel = |target: usize| -> Option<usize> { target.checked_sub(head).filter(|&r| r < n) };
518        let mut succs: Vec<Vec<usize>> = vec![Vec::new(); n];
519        for (i, op) in body.iter().enumerate() {
520            match op {
521                Op::Jump { target } => {
522                    if let Some(r) = rel(*target) {
523                        succs[i].push(r);
524                    }
525                }
526                Op::JumpIfFalse { target, .. } | Op::JumpIfTrue { target, .. } => {
527                    if let Some(r) = rel(*target) {
528                        succs[i].push(r);
529                    }
530                    if i + 1 < n {
531                        succs[i].push(i + 1);
532                    }
533                }
534                Op::Return { .. } | Op::ReturnNothing | Op::Halt => {}
535                _ => {
536                    if i + 1 < n {
537                        succs[i].push(i + 1);
538                    }
539                }
540            }
541        }
542        let mut preds: Vec<Vec<usize>> = vec![Vec::new(); n];
543        for (i, ss) in succs.iter().enumerate() {
544            for &s in ss {
545                preds[s].push(i);
546            }
547        }
548        // Iterative dominators over the region CFG (entry = relative 0 = `head`).
549        // `dom[i][k]` == node k dominates node i. Regions are small, so the O(n²) set
550        // representation is fine. Unreachable nodes keep the full (all-true) set —
551        // harmless: they never execute, so any "fresh" verdict on them is moot.
552        let mut dom: Vec<Vec<bool>> = vec![vec![true; n]; n];
553        dom[0] = vec![false; n];
554        dom[0][0] = true;
555        let mut changed = true;
556        while changed {
557            changed = false;
558            for i in 1..n {
559                if preds[i].is_empty() {
560                    continue;
561                }
562                let mut new = vec![true; n];
563                for &p in &preds[i] {
564                    for (k, nk) in new.iter_mut().enumerate() {
565                        *nk &= dom[p][k];
566                    }
567                }
568                new[i] = true;
569                if new != dom[i] {
570                    dom[i] = new;
571                    changed = true;
572                }
573            }
574        }
575        // Mutation positions per collection reg, and fresh-definition positions.
576        let mut muts: rustc_hash::FxHashMap<u16, Vec<usize>> = rustc_hash::FxHashMap::default();
577        let mut news: rustc_hash::FxHashMap<u16, Vec<usize>> = rustc_hash::FxHashMap::default();
578        for (i, op) in body.iter().enumerate() {
579            match op {
580                Op::ListPush { list: c, .. }
581                | Op::SetAdd { set: c, .. }
582                | Op::RemoveFrom { collection: c, .. }
583                | Op::SetIndex { collection: c, .. }
584                | Op::SetIndexUnchecked { collection: c, .. }
585                | Op::ListPop { list: c, .. } => muts.entry(*c).or_default().push(i),
586                Op::NewEmptyList { dst }
587                | Op::NewEmptySet { dst }
588                | Op::NewEmptyMap { dst }
589                | Op::NewEmptyListI32 { dst } => news.entry(*dst).or_default().push(i),
590                _ => {}
591            }
592        }
593        for (c, mpos) in &muts {
594            if let Some(npos) = news.get(c) {
595                // Fresh iff SOME fresh-definition position dominates EVERY mutation.
596                if npos.iter().any(|&q| mpos.iter().all(|&m| dom[m][q])) {
597                    out.insert(*c);
598                }
599            }
600        }
601        out
602    }
603
604    /// True if any mutated-collection register is COPIED, ALIASED, redefined, or
605    /// otherwise escapes the region — so region-entry copy-on-write alone cannot
606    /// keep it isolated and the region must run on the value-semantic VM. An
607    /// in-place collection mutation and a pure read use the collection soundly
608    /// (its buffer stays private to the region); ANY other operand use of a
609    /// mutated register — or any un-modelled op — fails closed.
610    fn region_mutation_escapes(body: &[Op], m: &rustc_hash::FxHashSet<u16>) -> bool {
611        let h = |r: &u16| m.contains(r);
612        let rng = |s: u16, c: u16| (s..s.saturating_add(c)).any(|r| m.contains(&r));
613        body.iter().any(|op| Self::op_escapes_mutated(op, &h, &rng))
614    }
615
616    fn op_escapes_mutated(
617        op: &Op,
618        h: &impl Fn(&u16) -> bool,
619        rng: &impl Fn(u16, u16) -> bool,
620    ) -> bool {
621        match op {
622            // In-place mutation: the collection operand is isolated by the entry
623            // COW; only the OTHER operands can leak/redefine it.
624            Op::ListPush { value, .. } | Op::SetAdd { value, .. } | Op::RemoveFrom { value, .. } => {
625                h(value)
626            }
627            Op::SetIndex { index, value, .. } | Op::SetIndexUnchecked { index, value, .. } => {
628                h(index) || h(value)
629            }
630            Op::ListPop { dst, .. } => h(dst),
631            // Pure reads of a collection.
632            Op::Index { dst, index, .. } | Op::IndexUnchecked { dst, index, .. } => {
633                h(dst) || h(index)
634            }
635            Op::Length { dst, .. } => h(dst),
636            Op::Contains { dst, value, .. } => h(dst) || h(value),
637            Op::RegionBoundsGuard { bound, iv, .. } => h(bound) || h(iv),
638            // Creating a FRESH collection in a mutated register is safe: the new
639            // buffer is uniquely owned, so its in-place mutation cannot alias
640            // (an aliasing copy would still be caught by the other arms). This is
641            // the fresh-list-per-iteration pattern (`Let mutable p be a new Seq`).
642            Op::NewEmptyList { .. }
643            | Op::NewEmptySet { .. }
644            | Op::NewEmptyMap { .. }
645            | Op::NewEmptyListI32 { .. } => false,
646            Op::NewRange { start, end, .. } => h(start) || h(end),
647            Op::NewList { start, count, .. } | Op::NewTuple { start, count, .. } => rng(*start, *count),
648            // Scalars / control flow: decline only if a mutated reg is an operand
649            // (a redefinition of the collection reg, or a scalar read of it).
650            Op::LoadConst { dst, .. }
651            | Op::GlobalGet { dst, .. }
652            | Op::LoadToday { dst }
653            | Op::LoadNow { dst }
654            | Op::Args { dst }
655            | Op::IterNext { dst, .. } => h(dst),
656            // A call-site COW barrier may redefine (clone) the register — decline a
657            // region if it targets a mutated collection reg. In practice it never
658            // appears in a tier-able region (it sits beside a `Call`, and a region
659            // with a call declines regardless).
660            Op::EnsureOwned { reg } => h(reg),
661            Op::Move { dst, src }
662            | Op::Not { dst, src }
663            | Op::AddAssign { dst, src }
664            | Op::FormatValue { dst, src, .. } => h(dst) || h(src),
665            Op::Add { dst, lhs, rhs }
666            | Op::Sub { dst, lhs, rhs }
667            | Op::Mul { dst, lhs, rhs }
668            | Op::Div { dst, lhs, rhs }
669            | Op::ExactDiv { dst, lhs, rhs }
670            | Op::FloorDiv { dst, lhs, rhs }
671            | Op::Mod { dst, lhs, rhs }
672            | Op::Lt { dst, lhs, rhs }
673            | Op::Gt { dst, lhs, rhs }
674            | Op::LtEq { dst, lhs, rhs }
675            | Op::GtEq { dst, lhs, rhs }
676            | Op::Eq { dst, lhs, rhs }
677            | Op::NotEq { dst, lhs, rhs }
678            | Op::ApproxEq { dst, lhs, rhs }
679            | Op::Pow { dst, lhs, rhs }
680            | Op::BitXor { dst, lhs, rhs }
681            | Op::BitAnd { dst, lhs, rhs }
682            | Op::BitOr { dst, lhs, rhs }
683            | Op::Shl { dst, lhs, rhs }
684            | Op::Shr { dst, lhs, rhs } => h(dst) || h(lhs) || h(rhs),
685            Op::MagicDivU { dst, lhs, .. } | Op::DivPow2 { dst, lhs, .. } => h(dst) || h(lhs),
686            // Collection-producing binops READ their operands and yield a FRESH,
687            // independent result — decline only if a mutated reg is involved.
688            Op::Concat { dst, lhs, rhs }
689            | Op::SeqConcat { dst, lhs, rhs }
690            | Op::UnionOp { dst, lhs, rhs }
691            | Op::IntersectOp { dst, lhs, rhs } => h(dst) || h(lhs) || h(rhs),
692            // Enum-arm test/bind and struct-field read: scalar-shaped, no alias.
693            Op::TestArm { dst, target, .. } | Op::BindArm { dst, target, .. } => h(dst) || h(target),
694            Op::GetField { dst, obj, .. } => h(dst) || h(obj),
695            Op::DestructureTuple { src, start, count } => h(src) || rng(*start, *count),
696            Op::Jump { .. } | Op::ReturnNothing | Op::IterPop => false,
697            Op::JumpIfFalse { cond, .. } | Op::JumpIfTrue { cond, .. } => h(cond),
698            Op::IterPrepare { iterable } => h(iterable),
699            Op::Sleep { duration } => h(duration),
700            // Everything else — a Move/Concat producing a live copy, calls,
701            // closures, spawns, channels, CRDTs, global stores, returns, struct/
702            // tuple/inductive builders, Show, slices, deep-clones — could retain
703            // or alias the collection. Fail closed.
704            _ => true,
705        }
706    }
707
708    /// Back-edge hook for hot loops in ANY frame (`Jump` to an earlier pc):
709    /// profile, compile when hot, and — when ready and the guard passes — run
710    /// the region natively. `named`/`frame_regs` describe the ENCLOSING frame
711    /// (Main's or a function's). `cur_func` is the enclosing function index (for
712    /// the region-entry COW's mutable-param check). Returns the pc to resume at
713    /// (the loop's exit).
714    fn try_region(
715        &mut self,
716        head: usize,
717        back_pc: usize,
718        named: &[bool],
719        frame_regs: usize,
720        depth_now: usize,
721        cur_func: Option<u16>,
722    ) -> Option<RegionExit> {
723        use super::native_tier::{RegionSlot, REGION_TIER_THRESHOLD};
724        let tier = self.tier?;
725        match self.regions.get(&head) {
726            Some(RegionSlot::Failed) => return None,
727            Some(RegionSlot::Ready { .. }) => {}
728            None => {
729                let n = self.region_hot.entry(head).or_insert(0);
730                *n += 1;
731                if *n < REGION_TIER_THRESHOLD {
732                    return None;
733                }
734                // Region extent: every jump leaving [head, back_pc] must
735                // agree on ONE exit pc.
736                let body = &self.program.code[head..=back_pc];
737                let mut exit: Option<usize> = None;
738                for op in body {
739                    if let Op::Jump { target } | Op::JumpIfFalse { target, .. }
740                    | Op::JumpIfTrue { target, .. } = *op
741                    {
742                        if !(head..=back_pc).contains(&target) {
743                            match exit {
744                                None => exit = Some(target),
745                                Some(e) if e == target => {}
746                                _ => {
747                                    self.mark_region_failed(head);
748                                    return None;
749                                }
750                            }
751                        }
752                    }
753                }
754                let Some(exit_pc) = exit else {
755                    self.mark_region_failed(head);
756                    return None;
757                };
758                // Value semantics: a region that mutates a collection IN PLACE
759                // may be writing a SHARED (aliased) allocation — native code
760                // writes through the `Rc` directly, a reference-semantics
761                // miscompile. We tier it soundly by copy-on-write'ing each
762                // mutated collection at region ENTRY (isolating it), PROVIDED no
763                // mutated collection escapes the region (then entry-COW is not
764                // enough — decline, run on the value-semantic VM). A `mutable`
765                // param is intentionally shared with the caller, so its in-place
766                // mutation is correct and it is NOT COW'd.
767                let mut cow_regs: Vec<u16> = Vec::new();
768                if crate::semantics::collections::value_semantics_enabled() {
769                    let mut mutated = Self::region_mutated_collection_regs(body);
770                    // A collection created FRESH in-region (its `NewEmpty` dominates
771                    // every mutation) is uniquely owned by construction: it needs no
772                    // entry-COW, and its register's earlier recycled-scratch uses no
773                    // longer read as a spurious alias-escape (the fannkuch `perm`
774                    // whose slot a `Set r to r-1` scratch reused before `perm` exists).
775                    for r in Self::region_fresh_collection_regs(body, head) {
776                        mutated.remove(&r);
777                    }
778                    if !mutated.is_empty() {
779                        if Self::region_mutation_escapes(body, &mutated) {
780                            self.mark_region_failed(head);
781                            return None;
782                        }
783                        let mutable_params = cur_func
784                            .and_then(|fi| self.program.functions.get(fi as usize))
785                            .map(|f| f.mutable_param_regs.as_slice())
786                            .unwrap_or(&[]);
787                        cow_regs =
788                            mutated.into_iter().filter(|r| !mutable_params.contains(r)).collect();
789                    }
790                }
791                let reg_count = u16::try_from(frame_regs).ok()?;
792                // Speculation seed: the kinds sitting in this frame's
793                // registers RIGHT NOW. The adapter compiles against them;
794                // the guard set re-checks them on every entry.
795                let observed: Vec<super::native_tier::ObservedKind> = (0..frame_regs)
796                    .map(|r| {
797                        use crate::interpreter::{ListRepr, RuntimeValue};
798                        use super::native_tier::ObservedKind;
799                        let rt = self.registers.get(self.base + r).map(|v| v.as_runtime());
800                        match rt.as_deref() {
801                            Some(RuntimeValue::Int(_)) => ObservedKind::Int,
802                            // A BigInt is a promoted (overflowed) integer — still an
803                            // integer kind, so the region tiers the slot as Int. The
804                            // entry guard re-checks the representation: a real BigInt in
805                            // an Int-guarded slot fails the guard and stays in the exact
806                            // VM, so the native i64 fast path is never entered with a box.
807                            Some(RuntimeValue::BigInt(_)) => ObservedKind::Int,
808                            Some(RuntimeValue::Float(_)) => ObservedKind::Float,
809                            Some(RuntimeValue::Bool(_)) => ObservedKind::Bool,
810                            Some(RuntimeValue::List(rc)) => match &*rc.borrow() {
811                                ListRepr::Ints(_) => ObservedKind::IntList,
812                                ListRepr::IntsI32(_) => ObservedKind::IntListI32,
813                                ListRepr::Floats(_) => ObservedKind::FloatList,
814                                ListRepr::Bools(_) => ObservedKind::BoolList,
815                                ListRepr::Boxed(_)
816                                | ListRepr::Strings { .. }
817                                | ListRepr::Structs { .. }
818                                | ListRepr::Inductives { .. }
819                                | ListRepr::WireStructs { .. }
820                                | ListRepr::WireColumn { .. } => ObservedKind::Other,
821                            },
822                            Some(RuntimeValue::Map(_)) => ObservedKind::Map,
823                            // An ASCII Text rides the byte-pin lane (char index ==
824                            // byte index, char count == byte length). The metrics
825                            // cache makes the ASCII test O(1) per crossing. A
826                            // non-ASCII Text stays Other → the region bails and the
827                            // per-char decode path runs, so the JIT never diverges.
828                            Some(RuntimeValue::Text(rc))
829                                if crate::semantics::collections::text_is_ascii(rc) =>
830                            {
831                                ObservedKind::TextBytes
832                            }
833                            _ => ObservedKind::Other,
834                        }
835                    })
836                    .collect();
837                let callees: Vec<super::native_tier::CalleeSig> = {
838                    let prog = &self.program;
839                    let code_len = prog.code.len();
840                    prog.functions
841                        .iter()
842                        .map(|f| {
843                            let end = prog
844                                .functions
845                                .iter()
846                                .map(|h| h.entry_pc)
847                                .filter(|&pc| pc > f.entry_pc)
848                                .min()
849                                .unwrap_or(code_len);
850                            let (list_params_stable, returns_list_param) = analyze_list_call_safety(
851                                &prog.code[f.entry_pc..end],
852                                f.param_count,
853                                &f.param_kinds,
854                                f.register_count,
855                            );
856                            super::native_tier::CalleeSig {
857                                param_kinds: f.param_kinds.clone(),
858                                ret: f.ret_kind,
859                                list_params_stable,
860                                returns_list_param,
861                            }
862                        })
863                        .collect()
864                };
865                // PRECISE REGION LIVE-OUT: a name bound INSIDE this loop is
866                // lexically dead at the loop exit, so it must NOT be written
867                // back — dropping it from `named` lets the JIT's copy-prop / CSE
868                // / fusion treat it as true scratch. `loop_locals[head]` is the
869                // compiler's exact per-loop set; absent (no loop record) keeps
870                // the conservative full `named`.
871                let liveout_off = std::env::var("LOGOS_LIVEOUT").as_deref() == Ok("0");
872                if std::env::var_os("LOGOS_LIVEOUT_TRACE").is_some() {
873                    let ll = self.program.loop_locals.get(&head);
874                    let nnamed = named.iter().filter(|&&n| n).count();
875                    let freed = ll.map_or(0, |m| {
876                        named.iter().enumerate().filter(|(r, &n)| n && m.get(*r).copied().unwrap_or(false)).count()
877                    });
878                    eprintln!("liveout-trace: head={head} named={nnamed} loop_locals={} freed={freed}", ll.is_some());
879                }
880                let region_named: Vec<bool> = match self.program.loop_locals.get(&head) {
881                    Some(locals) if !liveout_off => named
882                        .iter()
883                        .enumerate()
884                        .map(|(r, &n)| n && !locals.get(r).copied().unwrap_or(false))
885                        .collect(),
886                    _ => named.to_vec(),
887                };
888                match tier.compile_region(
889                    body,
890                    head,
891                    exit_pc,
892                    &self.program.constants,
893                    reg_count,
894                    &region_named,
895                    &observed,
896                    &self.native_ctx,
897                    &callees,
898                ) {
899                    Some(rf) => {
900                        self.regions.insert(head, RegionSlot::Ready { rf, exit_pc, misses: 0 });
901                        if !cow_regs.is_empty() {
902                            self.region_cow_regs.insert(head, cow_regs);
903                        }
904                    }
905                    None => {
906                        self.mark_region_failed(head);
907                        return None;
908                    }
909                }
910            }
911        }
912        let result = self.run_ready_region(head, depth_now, cur_func);
913        if result.is_none() {
914            // Guard failure or side exit: count it; a region that keeps
915            // missing re-runs work every entry — demote to pure bytecode.
916            let mut demote = false;
917            if let Some(RegionSlot::Ready { misses, .. }) = self.regions.get_mut(&head) {
918                *misses += 1;
919                if *misses >= super::native_tier::REGION_DEMOTE_AFTER {
920                    demote = true;
921                }
922            }
923            if demote {
924                self.mark_region_failed(head);
925            }
926        }
927        result
928    }
929
930    /// The Ready-path body of [`Vm::try_region`]: guards, pinning, the native
931    /// run, and write-back. None = guard failure or side exit (the caller
932    /// counts misses).
933    fn run_ready_region(
934        &mut self,
935        head: usize,
936        depth_now: usize,
937        cur_func: Option<u16>,
938    ) -> Option<RegionExit> {
939        use super::native_tier::RegionSlot;
940        // Region-entry copy-on-write: isolate each collection this region mutates
941        // in place, so the native code's in-place writes cannot leak through a
942        // shared `Rc`. A no-op when already uniquely owned; a one-time deep clone
943        // when aliased. `mutable`-param collections were excluded at formation
944        // (their sharing is intentional), so this only isolates value bindings.
945        if let Some(regs) = self.region_cow_regs.get(&head) {
946            for r in regs.clone() {
947                self.ensure_reg_owned(r, cur_func);
948            }
949        }
950        let Some(RegionSlot::Ready { rf, exit_pc, .. }) = self.regions.get(&head) else {
951            unreachable!()
952        };
953        // Guard: every live-in slot must hold exactly the kind the region
954        // speculated on; copy the raw representation in (floats as bits).
955        {
956            use crate::interpreter::RuntimeValue;
957            use super::native_tier::SlotKind;
958            for &(r, kind) in rf.guard_set() {
959                let v = self.registers.get(self.base + r as usize)?;
960                match (kind, &*v.as_runtime()) {
961                    (SlotKind::Int, RuntimeValue::Int(_)) => {}
962                    (SlotKind::Float, RuntimeValue::Float(_)) => {}
963                    (SlotKind::Bool, RuntimeValue::Bool(_)) => {}
964                    _ => return None,
965                }
966            }
967        }
968        // Frequently re-entered regions (sift-down loops) cannot afford a
969        // heap allocation per entry — reuse one thread-local buffer.
970        thread_local! {
971            static REGION_FRAME: std::cell::RefCell<Vec<i64>> =
972                const { std::cell::RefCell::new(Vec::new()) };
973        }
974        // `LOGOS_JIT_CANARY=1` guards the region frame with a sentinel
975        // canary past its live span (`need` = frame proper + call-arena
976        // headroom): any region-native write beyond `need` trips it loudly
977        // at the source. Off by default (and in release) so normal runs
978        // pay nothing.
979        let frame_canary: usize = if jit_canary_enabled() { 64 } else { 0 };
980        const FRAME_SENTINEL: i64 = 0x6262_6262_6262_6262u64 as i64;
981        let need = rf.frame_size() + rf.arena_slots();
982        let frame_cell = REGION_FRAME.with(|f| {
983            let mut frame = f.take();
984            // Zero only the region frame proper. The call-arena headroom is
985            // REUSED untouched (16MiB for calling regions — zeroing it per
986            // entry would memset megabytes every loop crossing): callee
987            // chains write-before-read by the kind gates and the call
988            // stencil plants each limit slot, so stale slots are
989            // unobservable — the same contract as the function tier's
990            // thread-local arena.
991            if frame.len() < need + frame_canary {
992                frame.resize(need + frame_canary, 0);
993            }
994            frame[..rf.frame_size()].fill(0);
995            for c in &mut frame[need..need + frame_canary] {
996                *c = FRAME_SENTINEL;
997            }
998            frame
999        });
1000        let mut frame = frame_cell;
1001        {
1002            use crate::interpreter::RuntimeValue;
1003            use super::native_tier::SlotKind;
1004            for &(r, kind) in rf.guard_set() {
1005                frame[r as usize] =
1006                    match (kind, &*self.registers[self.base + r as usize].as_runtime()) {
1007                        (SlotKind::Int, RuntimeValue::Int(n)) => *n,
1008                        (SlotKind::Float, RuntimeValue::Float(f)) => f.to_bits() as i64,
1009                        (SlotKind::Bool, RuntimeValue::Bool(b)) => *b as i64,
1010                        _ => unreachable!("guard verified the discriminant above"),
1011                    };
1012            }
1013        }
1014        // Pin arrays: borrow each DISTINCT Rc once (held across the whole
1015        // native run — zero refcount/borrow traffic inside the loop), check
1016        // the speculated repr, and plant buffer pointer + length in the
1017        // dedicated frame slots. Aliased registers resolve to the same
1018        // buffer. Handles drop before write-back (in-place arrays need none;
1019        // the deopt replay is sound by prefix-idempotence).
1020        // The register-file base pointer, captured ONCE before any pin takes an
1021        // (immutable) borrow of `self.registers` through a list/map `Rc`. A
1022        // `TextMut` pin plants a `*mut Value` to a register CELL derived from
1023        // this pointer — the cell is stable across the native run (the register
1024        // file never reallocates while a region runs), so the append helper can
1025        // grow the accumulator through it. Reading/writing a cell through this
1026        // raw pointer does not conflict with the handles' shared borrows.
1027        let reg_base_ptr: *mut Value = self.registers.as_mut_ptr();
1028        let (outcome, text_mut_snapshots) = {
1029            use crate::interpreter::{ListRepr, RuntimeValue};
1030            let pins = rf.array_set();
1031            let mut handles: Vec<(usize, std::cell::RefMut<'_, ListRepr>)> =
1032                Vec::with_capacity(pins.len());
1033            // Parallel to `handles`: a buffer is "mutated" if ANY pin aliasing it
1034            // writes in place under a deopt-capable, non-precise region — it needs
1035            // a full-content snapshot/restore across a classic replay deopt.
1036            let mut handle_mutated: Vec<bool> = Vec::with_capacity(pins.len());
1037            let mut map_handles: Vec<(
1038                usize,
1039                std::cell::RefMut<'_, crate::interpreter::MapStorage>,
1040            )> = Vec::new();
1041            // A pinned MUTABLE Text accumulator grows THROUGH the VM register
1042            // cell (the planted `*mut Value`). A classic replay-from-head Deopt
1043            // would re-run the appends the native prefix already landed in the
1044            // cell — a double-append — so snapshot each distinct accumulator's
1045            // entry `Value` and restore it before a classic `Deopt` replay
1046            // (precise regions resume at the faulting op and never replay, so
1047            // they keep the live grown value). `(register slot, entry Value)`.
1048            let mut text_mut_snapshots: Vec<(usize, Value)> = Vec::new();
1049            for pin in pins {
1050                if pin.elem == super::native_tier::PinElem::Map {
1051                    let v = self.registers.get(self.base + pin.reg as usize)?;
1052                    let Some(RuntimeValue::Map(rc)) = v.as_runtime_ref() else { return None };
1053                    let key = std::rc::Rc::as_ptr(rc) as usize;
1054                    if !map_handles.iter().any(|(k, _)| *k == key) {
1055                        let Ok(b) = rc.try_borrow_mut() else { return None };
1056                        map_handles.push((key, b));
1057                    }
1058                    let idx = map_handles.iter().position(|(k, _)| *k == key).unwrap();
1059                    let storage = &mut *map_handles[idx].1;
1060                    frame[pin.vec_slot as usize] =
1061                        storage as *mut crate::interpreter::MapStorage as i64;
1062                    frame[pin.ptr_slot as usize] = 0;
1063                    frame[pin.len_slot as usize] = 0;
1064                    continue;
1065                }
1066                if pin.elem == super::native_tier::PinElem::TextBytes {
1067                    // A pinned ASCII Text rides its BYTE buffer: char index ==
1068                    // byte index, char count == byte length. RE-CHECK ASCII at
1069                    // every entry (the speculation seed is not a standing
1070                    // guarantee — a region could be re-entered with a non-ASCII
1071                    // Text in the same register) — decline (deopt to bytecode)
1072                    // otherwise so the per-char decode path runs and the output
1073                    // never diverges from the tree-walker. `Rc<String>` is
1074                    // read-only (no RefCell): a TextBytes pin is never written,
1075                    // so the snapshot/rollback machinery does not apply.
1076                    let v = self.registers.get(self.base + pin.reg as usize)?;
1077                    let Some(RuntimeValue::Text(rc)) = v.as_runtime_ref() else { return None };
1078                    if !crate::semantics::collections::text_is_ascii(rc) {
1079                        return None;
1080                    }
1081                    frame[pin.vec_slot as usize] = 0;
1082                    frame[pin.ptr_slot as usize] = rc.as_bytes().as_ptr() as i64;
1083                    frame[pin.len_slot as usize] = rc.len() as i64;
1084                    continue;
1085                }
1086                if pin.elem == super::native_tier::PinElem::TextMut {
1087                    // A pinned MUTABLE Text accumulator: plant a `*mut Value` to
1088                    // the VM REGISTER CELL (stable for the run; the `Rc<String>`
1089                    // inside it reallocs/COWs on append). Decline (deopt to
1090                    // bytecode) if the observed value is not a Text. Snapshot the
1091                    // entry `Value` for the classic-Deopt rollback (one per
1092                    // distinct accumulator register). The cell is reached through
1093                    // `reg_base_ptr` (no `self.registers` borrow that would
1094                    // conflict with the live list/map handles).
1095                    if pin.reg as usize >= self.registers.len().saturating_sub(self.base) {
1096                        return None;
1097                    }
1098                    let slot = self.base + pin.reg as usize;
1099                    let cell: *mut Value = unsafe { reg_base_ptr.add(slot) };
1100                    if !matches!(unsafe { (*cell).as_runtime_ref() }, Some(RuntimeValue::Text(_))) {
1101                        return None;
1102                    }
1103                    if pin.mutated && !text_mut_snapshots.iter().any(|(s, _)| *s == slot) {
1104                        text_mut_snapshots.push((slot, unsafe { (*cell).clone() }));
1105                    }
1106                    frame[pin.vec_slot as usize] = cell as i64;
1107                    frame[pin.ptr_slot as usize] = 0;
1108                    frame[pin.len_slot as usize] = 0;
1109                    continue;
1110                }
1111                let v = self.registers.get(self.base + pin.reg as usize)?;
1112                let Some(RuntimeValue::List(rc)) = v.as_runtime_ref() else { return None };
1113                let key = std::rc::Rc::as_ptr(rc) as usize;
1114                if !handles.iter().any(|(k, _)| *k == key) {
1115                    let Ok(b) = rc.try_borrow_mut() else { return None };
1116                    handles.push((key, b));
1117                    handle_mutated.push(false);
1118                }
1119                let idx = handles.iter().position(|(k, _)| *k == key).unwrap();
1120                if pin.mutated {
1121                    handle_mutated[idx] = true;
1122                }
1123                let payload = &mut *handles[idx].1;
1124                use super::native_tier::PinElem;
1125                let (vec_handle, ptr, len) = match (payload, pin.elem) {
1126                    (ListRepr::Ints(v), PinElem::Int) => {
1127                        (v as *mut Vec<i64> as i64, v.as_mut_ptr() as *mut i64, v.len())
1128                    }
1129                    (ListRepr::IntsI32(v), PinElem::IntI32) => {
1130                        (v as *mut Vec<i32> as i64, v.as_mut_ptr() as *mut i64, v.len())
1131                    }
1132                    // Maps never reach this arm (their pin path is below) —
1133                    // a list register observed as Map is a guard failure.
1134                    (_, PinElem::Map) => return None,
1135                    (ListRepr::Floats(v), PinElem::Float) => {
1136                        (v as *mut Vec<f64> as i64, v.as_mut_ptr() as *mut i64, v.len())
1137                    }
1138                    (ListRepr::Bools(v), PinElem::Bool) => {
1139                        (v as *mut Vec<bool> as i64, v.as_mut_ptr() as *mut i64, v.len())
1140                    }
1141                    // Repr changed since compile (promotion) — guard failure.
1142                    _ => return None,
1143                };
1144                frame[pin.vec_slot as usize] = vec_handle;
1145                frame[pin.ptr_slot as usize] = ptr as i64;
1146                frame[pin.len_slot as usize] = len as i64;
1147                // LEVER B calling-convention invariant (matches the function
1148                // tier): a list register's frame slot MIRRORS its vec handle, so
1149                // staging a pinned array into a call's argument window (a `Move`
1150                // from the register slot) passes the live `*mut Vec`, not the
1151                // zeroed register cell. Harmless for non-call regions (the slot
1152                // is never read for a pinned array, and the frame is discarded on
1153                // deopt — the VM register keeps the real Rc).
1154                frame[pin.reg as usize] = vec_handle;
1155            }
1156            // HOISTED bounds checks (V8 loop bound-check elimination): with
1157            // the pinned lengths in hand, verify ONCE that every covered loop
1158            // access stays in bounds for the whole run. Any failure declines
1159            // the region — the VM replays the loop on bytecode, where the
1160            // accesses are checked and produce the exact error.
1161            for hg in rf.hoist_guards() {
1162                let len = frame[hg.len_slot as usize];
1163                let bound = match &*self.registers.get(self.base + hg.bound_reg as usize)?.as_runtime() {
1164                    RuntimeValue::Int(n) => *n,
1165                    _ => return None,
1166                };
1167                let iv = match &*self.registers.get(self.base + hg.iv_reg as usize)?.as_runtime() {
1168                    RuntimeValue::Int(n) => *n,
1169                    _ => return None,
1170                };
1171                if len < bound.saturating_add(hg.add_max as i64)
1172                    || iv.saturating_add(hg.add_min as i64) < 1
1173                {
1174                    return None;
1175                }
1176            }
1177            // Entry lengths of every pinned list buffer — the rollback target
1178            // if a mid-region side-exit forces discard-and-replay. `ListPush`
1179            // APPENDS, so it is NOT replay-idempotent; on deopt each pushed
1180            // buffer is truncated back to its entry length before the VM
1181            // replays the loop on bytecode, which then re-pushes cleanly
1182            // instead of duplicating. Read-only and in-place (SetIndex) buffers
1183            // keep their entry length, so the truncate is a no-op for them.
1184            let entry_lens: Vec<usize> = handles.iter().map(|(_, h)| h.len()).collect();
1185            // A buffer written IN PLACE (SetIndex) replays unsoundly under the
1186            // classic discard-replay deopt — the write already landed in the
1187            // SHARED buffer, so the bytecode replay-from-head double-applies it
1188            // (a read-modify-write or swap is not idempotent). Snapshot its full
1189            // contents on entry; a classic `Deopt` restores them (subsuming the
1190            // length, so a buffer that BOTH pushes and writes in place is covered
1191            // too). Push-only / read-only buffers keep the cheap length truncate.
1192            let entry_snapshots: Vec<Option<ListRepr>> = handles
1193                .iter()
1194                .zip(handle_mutated.iter())
1195                .map(|((_, h), &mt)| if mt { Some((**h).clone()) } else { None })
1196                .collect();
1197            let out = rf.run(&mut frame[..need], depth_now);
1198            if matches!(out, super::native_tier::RegionOutcome::Deopt) {
1199                for (((_, h), &n), snap) in handles
1200                    .iter_mut()
1201                    .zip(entry_lens.iter())
1202                    .zip(entry_snapshots.into_iter())
1203                {
1204                    match snap {
1205                        Some(s) => **h = s,
1206                        None => h.truncate(n),
1207                    }
1208                }
1209            }
1210            // The list/map handles drop here, releasing their `self.registers`
1211            // borrows; the text-accumulator rollback (which needs `&mut
1212            // self.registers`) runs after the block, gated on a classic Deopt.
1213            (out, text_mut_snapshots)
1214        };
1215        // Roll each pinned mutable-Text accumulator back to its entry `Value` on
1216        // a classic Deopt so the bytecode replay-from-head re-appends from the
1217        // pre-region prefix instead of doubling the native prefix's appends (the
1218        // appends landed directly in the VM register cell). Precise side exits
1219        // and successful completions keep the live grown value.
1220        if matches!(outcome, super::native_tier::RegionOutcome::Deopt) {
1221            for (slot, snap) in text_mut_snapshots {
1222                self.registers[slot] = snap;
1223            }
1224        }
1225        for (k, c) in frame[need..need + frame_canary].iter().enumerate() {
1226            assert_eq!(
1227                *c, FRAME_SENTINEL,
1228                "REGION_FRAME OVERFLOW: canary slot {k} (frame + {}) clobbered by region native code",
1229                need + k
1230            );
1231        }
1232        match outcome {
1233            super::native_tier::RegionOutcome::Completed => {}
1234            // Side exit: discard the private frame — VM registers still hold
1235            // the state of this back-edge crossing, so falling back to
1236            // bytecode re-runs the remaining iterations deterministically up
1237            // to the faulting op and raises the exact kernel error there.
1238            // (In-place array writes already landed; the replay recomputes
1239            // the same prefix values, so they are unobservable.)
1240            super::native_tier::RegionOutcome::Deopt => return None,
1241            // PRECISE side exit (push+SetIndex regions): the buffers were NOT
1242            // truncated (the truncate above is gated on `Deopt`), so completed
1243            // iterations' pushes and in-place writes stand. Materialize every
1244            // touched non-array scalar — all-int by the adapter's gate — from
1245            // the frame into the VM registers, then resume the bytecode AT the
1246            // faulting op (NOT the loop head): the faulting op re-runs exactly
1247            // once, raising the precise error or continuing, with no replay of
1248            // the completed prefix. Array handle registers keep their live Rc
1249            // (the region mutated through their pins).
1250            super::native_tier::RegionOutcome::DeoptAt { resume_pc } => {
1251                use super::native_tier::SlotKind;
1252                // Re-box each touched register by ITS kind at the faulting op
1253                // (from the region's kind flow): Int/Bool/Float from the frame's
1254                // raw bits; `None` keeps the VM register's current value (a
1255                // pinned array mutated in place, or a read-only/unknown slot).
1256                // Cloned so the `rf` borrow ends before the register writes.
1257                let kinds: Option<Vec<Option<SlotKind>>> =
1258                    rf.precise_kinds(resume_pc).map(|k| k.to_vec());
1259                let mut regs: Vec<u16> = rf
1260                    .guard_set()
1261                    .iter()
1262                    .map(|(r, _)| *r)
1263                    .chain(rf.free_set().iter().copied())
1264                    .chain(rf.write_set().iter().map(|(r, _)| *r))
1265                    .collect();
1266                regs.sort_unstable();
1267                regs.dedup();
1268                for r in regs {
1269                    let bits = frame[r as usize];
1270                    let v = match kinds
1271                        .as_ref()
1272                        .and_then(|k| k.get(r as usize).copied().flatten())
1273                    {
1274                        Some(SlotKind::Int) => Value::int(bits),
1275                        Some(SlotKind::Bool) => Value::bool(bits != 0),
1276                        Some(SlotKind::Float) => Value::float(f64::from_bits(bits as u64)),
1277                        None => continue,
1278                    };
1279                    self.set(r, v);
1280                }
1281                REGION_FRAME.with(|f| f.replace(frame));
1282                return Some(RegionExit::At(resume_pc));
1283            }
1284        }
1285        let writes: Vec<(u16, super::native_tier::SlotKind)> = rf.write_set().to_vec();
1286        let region_return = rf.region_return();
1287        let exit = *exit_pc;
1288        for (r, kind) in writes {
1289            use super::native_tier::SlotKind;
1290            let bits = frame[r as usize];
1291            let v = match kind {
1292                SlotKind::Int => Value::int(bits),
1293                SlotKind::Bool => Value::bool(bits != 0),
1294                SlotKind::Float => Value::float(f64::from_bits(bits as u64)),
1295            };
1296            self.set(r, v);
1297        }
1298        let result = match region_return {
1299            Some(rr) if frame[rr.flag_slot as usize] != 0 => {
1300                use super::native_tier::{RegionReturnKind, SlotKind};
1301                let bits = frame[rr.value_slot as usize];
1302                let v = match rr.kind {
1303                    RegionReturnKind::Slot(SlotKind::Int) => Value::int(bits),
1304                    RegionReturnKind::Slot(SlotKind::Bool) => Value::bool(bits != 0),
1305                    RegionReturnKind::Slot(SlotKind::Float) => {
1306                        Value::float(f64::from_bits(bits as u64))
1307                    }
1308                    RegionReturnKind::Register => self.reg(bits as u16).clone(),
1309                };
1310                RegionExit::Return(v)
1311            }
1312            _ => RegionExit::At(exit),
1313        };
1314        REGION_FRAME.with(|f| f.replace(frame));
1315        Some(result)
1316    }
1317
1318    /// Install a native tier: hot functions in the integer subset run as
1319    /// JIT-compiled machine code, guarded per call (non-Int args deopt to
1320    /// the bytecode path).
1321    pub fn with_native_tier(mut self, tier: &'p dyn super::native_tier::NativeTier) -> Self {
1322        self.tier = Some(tier);
1323        self
1324    }
1325
1326    /// Pre-install an AOT-native function for `fi` (HOTSWAP §Axis-3): the VM dispatches
1327    /// to it via the existing `NativeSlot::Ready` path from the first call (it is not
1328    /// `Untried`, so it skips the hotness threshold and the forge compile). Requires a
1329    /// native tier to be installed (the dispatch is gated on `self.tier`); on desktop
1330    /// the forge tier is always present alongside. Absent ⇒ the function stays on
1331    /// VM+JIT — the AOT artifact is strictly optional, no gap at the seam.
1332    #[cfg(not(target_arch = "wasm32"))]
1333    pub fn install_aot_native(&mut self, fi: usize, nf: Box<dyn super::native_tier::NativeFn>) {
1334        if fi < self.native.len() {
1335            let name = self.fn_name(fi);
1336            super::tier_trace::trace_transition(fi, &name, super::tier_trace::ExecTier::NativeAot);
1337            self.native[fi] = super::native_tier::NativeSlot::Ready(nf);
1338        }
1339    }
1340
1341    /// Copy-on-write for value semantics (VM side; mirrors the tree-walker's
1342    /// `ensure_collection_owned`). Before mutating the collection in register
1343    /// `reg`, deep-clone it if another holder shares the allocation (`Rc` strong
1344    /// count > 1). Gated behind the migration flag — off by default, so the hot
1345    /// path is untouched. NOTE: the `mutable`-parameter exemption is NOT yet
1346    /// wired on the VM (compiled bytecode carries no param-mutability marker);
1347    /// that is the remaining VM-compiler work before the flag can be flipped on.
1348    fn ensure_reg_owned(&mut self, reg: Reg, cur_func: Option<u16>) {
1349        if !crate::semantics::collections::value_semantics_enabled() {
1350            return;
1351        }
1352        // A `mutable` parameter passes by reference: mutate the shared allocation
1353        // in place so the caller observes it (mirrors the tree-walker's skip-COW).
1354        let is_mutable_param = cur_func
1355            .and_then(|fi| self.program.functions.get(fi as usize))
1356            .is_some_and(|f| f.mutable_param_regs.contains(&reg));
1357        if is_mutable_param {
1358            return;
1359        }
1360        use crate::interpreter::RuntimeValue;
1361        use std::rc::Rc;
1362        let di = self.base + reg as usize;
1363        let shared = matches!(
1364            self.registers.get(di).map(|v| v.as_runtime()).as_deref(),
1365            Some(RuntimeValue::List(rc)) if Rc::strong_count(rc) > 1
1366        ) || matches!(
1367            self.registers.get(di).map(|v| v.as_runtime()).as_deref(),
1368            Some(RuntimeValue::Map(rc)) if Rc::strong_count(rc) > 1
1369        ) || matches!(
1370            self.registers.get(di).map(|v| v.as_runtime()).as_deref(),
1371            Some(RuntimeValue::Set(rc)) if Rc::strong_count(rc) > 1
1372        );
1373        if shared {
1374            let owned = self.registers[di].as_runtime().deep_clone();
1375            self.registers[di] = Value::from_runtime(owned);
1376        }
1377    }
1378
1379    /// Null this call's argument-window registers after it returns. Dead once the
1380    /// call is over, but as the callee's params they persist below `restore_len`;
1381    /// left set, a collection argument keeps a live `Rc` clone in the caller frame
1382    /// that spuriously inflates `strong_count` and forces later copy-on-write. Zero
1383    /// `arg_count` (native/scheduler frames) does nothing.
1384    #[inline]
1385    fn clear_arg_window(&mut self, frame: &CallFrame) {
1386        let end = (frame.arg_lo + frame.arg_count as usize).min(self.registers.len());
1387        for slot in frame.arg_lo..end {
1388            self.registers[slot] = Value::nothing();
1389        }
1390    }
1391
1392    /// Same as [`Vm::clear_arg_window`] for a call that completes INLINE (a native /
1393    /// WASM / builtin dispatch that never pushes a `CallFrame`): the argument window
1394    /// starts at the current base's `args_start`.
1395    #[inline]
1396    fn clear_args(&mut self, args_start: Reg, arg_count: u16) {
1397        let lo = self.base + args_start as usize;
1398        let end = (lo + arg_count as usize).min(self.registers.len());
1399        for slot in lo..end {
1400            self.registers[slot] = Value::nothing();
1401        }
1402    }
1403
1404    /// Resolve a function's source name for the tier trace; empty when no interner is
1405    /// available (the trace then prints just the index).
1406    fn fn_name(&self, fi: usize) -> String {
1407        match (self.program.functions.get(fi), self.policy_ctx) {
1408            (Some(f), Some((_, interner))) => interner.resolve(f.name).to_string(),
1409            _ => String::new(),
1410        }
1411    }
1412
1413    /// Install the process tier AND a background compiler (HOTSWAP §6): hot functions
1414    /// are compiled on a worker thread instead of stalling the interpreter. The tier
1415    /// must be `&'static` (the process-installed forge backend) so it can cross to the
1416    /// worker — `&'static dyn NativeTier` is `Send` because `NativeTier: Sync`. The
1417    /// interpreter still runs the chains and is the sole `FnTable` writer; the worker
1418    /// only compiles. Falls back to [`Vm::with_native_tier`] (synchronous) for a
1419    /// borrowed `&'p` tier, which cannot be shared with a thread.
1420    #[cfg(not(target_arch = "wasm32"))]
1421    pub fn with_bg_native_tier(
1422        mut self,
1423        tier: &'static dyn super::native_tier::NativeTier,
1424    ) -> Self {
1425        self.tier = Some(tier);
1426        self.bg = Some(super::bg_compile::BgCompiler::new(tier));
1427        self
1428    }
1429
1430    /// Apply every background-compiled result that has come back: publish the native
1431    /// entry to the `FnTable` and flip the slot to `Ready` (or `Failed`). The
1432    /// interpreter is the sole `FnTable` writer, so this only ever runs on this
1433    /// thread, at the profiling points. No-op when there is no background compiler.
1434    #[cfg(not(target_arch = "wasm32"))]
1435    fn drain_bg_compiles(&mut self) {
1436        use super::bg_compile::CompileResult;
1437        use super::native_tier::NativeSlot;
1438        loop {
1439            let res = match self.bg.as_mut() {
1440                Some(b) => b.try_drain(),
1441                None => return,
1442            };
1443            let Some(res) = res else { return };
1444            match res {
1445                CompileResult::Function { fi, nf } => match nf {
1446                    Some(nf) => {
1447                        self.native_ctx.table.publish(fi, nf.entry_ptr(), nf.published_regc());
1448                        let name = self.fn_name(fi);
1449                        super::tier_trace::trace_transition(fi, &name, super::tier_trace::ExecTier::NativeForge);
1450                        self.native[fi] = NativeSlot::Ready(nf);
1451                    }
1452                    None => self.native[fi] = NativeSlot::Failed,
1453                },
1454            }
1455        }
1456    }
1457
1458    /// Block until every outstanding background compile has come back and been
1459    /// published — the determinism hook the differential tests use so the native tier
1460    /// engages predictably regardless of thread scheduling. No-op without a background
1461    /// compiler.
1462    #[cfg(not(target_arch = "wasm32"))]
1463    pub fn drain_pending_compiles(&mut self) {
1464        use super::bg_compile::CompileResult;
1465        use super::native_tier::NativeSlot;
1466        let results = match self.bg.as_mut() {
1467            Some(b) => b.drain_blocking(),
1468            None => return,
1469        };
1470        for res in results {
1471            match res {
1472                CompileResult::Function { fi, nf } => match nf {
1473                    Some(nf) => {
1474                        self.native_ctx.table.publish(fi, nf.entry_ptr(), nf.published_regc());
1475                        let name = self.fn_name(fi);
1476                        super::tier_trace::trace_transition(fi, &name, super::tier_trace::ExecTier::NativeForge);
1477                        self.native[fi] = NativeSlot::Ready(nf);
1478                    }
1479                    None => self.native[fi] = NativeSlot::Failed,
1480                },
1481            }
1482        }
1483    }
1484
1485    /// Supply the program arguments read by the `args()` system native. The
1486    /// vector is the full argv (index 0 is the program name), matching the
1487    /// compiled binary's `env::args()`.
1488    pub fn with_program_args(mut self, args: Vec<String>) -> Self {
1489        self.program_args = args;
1490        self
1491    }
1492
1493    /// Tier dispatch for `Call`: Some(result) = the native fast path ran.
1494    /// Precise-deopt frame materialization — deliberately OUT of the
1495    /// dispatch loop (cold path; keeping its body inline bloats the hot
1496    /// match and costs i-cache on every dispatched op).
1497    #[cold]
1498    #[inline(never)]
1499    #[allow(clippy::too_many_arguments)]
1500    fn materialize_native_frames(
1501        &mut self,
1502        frames: &[super::native_tier::NativeFrame],
1503        list_args: &[Value],
1504        args_start: super::instruction::Reg,
1505        dst: super::instruction::Reg,
1506        func: u16,
1507        pc: usize,
1508        call_stack: &mut Vec<CallFrame>,
1509    ) -> Result<(), String> {
1510        use super::native_tier::RegBox;
1511        let mut frame_base = self.base + args_start as usize;
1512        let mut caller_base = self.base;
1513        for (k, fr) in frames.iter().enumerate() {
1514            if k > 0 {
1515                caller_base = frame_base;
1516                frame_base += fr.offset;
1517            }
1518            let restore_len = self.registers.len();
1519            let needed = frame_base + fr.regs.len();
1520            if needed > MAX_REGISTER_FILE {
1521                return Err("vm: register file limit exceeded".to_string());
1522            }
1523            if self.registers.len() < needed {
1524                self.registers.resize(needed, Value::nothing());
1525            }
1526            call_stack.push(CallFrame {
1527                return_pc: if k == 0 { pc + 1 } else { fr.return_pc },
1528                return_reg: if k == 0 { dst } else { fr.return_reg },
1529                caller_base,
1530                restore_len,
1531                iter_depth: self.iter_stack.len(),
1532                func,
1533                arg_lo: 0,
1534                arg_count: 0,
1535            });
1536            for (r, bits) in fr.regs.iter().enumerate() {
1537                let v = match fr.kinds[r] {
1538                    RegBox::Dead | RegBox::Resolved => continue,
1539                    RegBox::Int => Value::int(*bits),
1540                    RegBox::Bool => Value::bool(*bits != 0),
1541                    RegBox::Float => Value::float(f64::from_bits(*bits as u64)),
1542                    RegBox::ListParam(j) => match list_args.get(j as usize) {
1543                        Some(v) => v.clone(),
1544                        None => return Err("vm: deopt pin index out of range".to_string()),
1545                    },
1546                };
1547                self.registers[frame_base + r] = v;
1548            }
1549            for (r, v) in &fr.resolved {
1550                self.registers[frame_base + *r as usize] = v.clone();
1551            }
1552        }
1553        self.base = frame_base;
1554        Ok(())
1555    }
1556
1557    fn try_native(
1558        &mut self,
1559        func: u16,
1560        args_start: super::instruction::Reg,
1561        arg_count: u16,
1562        bytecode_depth: usize,
1563    ) -> NativeDisposition {
1564        use super::native_tier::{NativeSlot, ParamKind, NATIVE_TIER_THRESHOLD};
1565        let Some(tier) = self.tier else { return NativeDisposition::Interpret };
1566        let fi = func as usize;
1567        // Publish any background-compiled chains that have come back (sole writer).
1568        #[cfg(not(target_arch = "wasm32"))]
1569        self.drain_bg_compiles();
1570        match self.native.get(fi) {
1571            None | Some(NativeSlot::Failed) => return NativeDisposition::Interpret,
1572            // Background compile in flight — keep running bytecode until it lands.
1573            Some(NativeSlot::Pending) => return NativeDisposition::Interpret,
1574            _ => {}
1575        }
1576        if matches!(self.native[fi], NativeSlot::Untried) {
1577            self.hot[fi] += 1;
1578            if self.hot[fi] < NATIVE_TIER_THRESHOLD {
1579                return NativeDisposition::Interpret;
1580            }
1581            let f = &self.program.functions[fi];
1582            if !f.captures.is_empty() {
1583                self.native[fi] = NativeSlot::Failed;
1584                return NativeDisposition::Interpret;
1585            }
1586            // A parameter whose declared type has no native representation
1587            // (Map, Text, nested Seq, …) can never enter native code —
1588            // fail once instead of bumping the hot counter forever.
1589            if f.param_kinds.iter().any(|k| k.is_none()) {
1590                self.native[fi] = NativeSlot::Failed;
1591                return NativeDisposition::Interpret;
1592            }
1593            let end = self
1594                .program
1595                .functions
1596                .iter()
1597                .map(|g| g.entry_pc)
1598                .filter(|&e| e > f.entry_pc)
1599                .min()
1600                .unwrap_or(self.program.code.len());
1601            let callees: Vec<super::native_tier::CalleeSig> = {
1602                let prog = &self.program;
1603                let code_len = prog.code.len();
1604                prog.functions
1605                    .iter()
1606                    .map(|g| {
1607                        let end = prog
1608                            .functions
1609                            .iter()
1610                            .map(|h| h.entry_pc)
1611                            .filter(|&pc| pc > g.entry_pc)
1612                            .min()
1613                            .unwrap_or(code_len);
1614                        let (list_params_stable, returns_list_param) = analyze_list_call_safety(
1615                            &prog.code[g.entry_pc..end],
1616                            g.param_count,
1617                            &g.param_kinds,
1618                            g.register_count,
1619                        );
1620                        super::native_tier::CalleeSig {
1621                            param_kinds: g.param_kinds.clone(),
1622                            ret: g.ret_kind,
1623                            list_params_stable,
1624                            returns_list_param,
1625                        }
1626                    })
1627                    .collect()
1628            };
1629            // With a background compiler, ship the compile off-thread and keep
1630            // running bytecode (HOTSWAP §6); the result is drained + published on a
1631            // later call. Without one (a borrowed `&'p` tier, or wasm), compile
1632            // synchronously — the retained fallback.
1633            #[cfg(not(target_arch = "wasm32"))]
1634            if self.bg.is_some() {
1635                let req = super::bg_compile::CompileRequest::Function(
1636                    super::bg_compile::FunctionRequest {
1637                        fi,
1638                        code: self.program.code[f.entry_pc..end].to_vec(),
1639                        entry_pc: f.entry_pc,
1640                        constants: std::sync::Arc::from(self.program.constants.clone()),
1641                        param_count: f.param_count,
1642                        register_count: f.register_count as u16,
1643                        param_kinds: f.param_kinds.clone(),
1644                        ret_kind: f.ret_kind,
1645                        callees,
1646                        ctx: self.native_ctx.clone(),
1647                    },
1648                );
1649                self.bg.as_mut().unwrap().submit(req);
1650                self.native[fi] = NativeSlot::Pending;
1651                return NativeDisposition::Interpret;
1652            }
1653            match tier.compile_function(
1654                &self.program.code[f.entry_pc..end],
1655                f.entry_pc,
1656                &self.program.constants,
1657                f.param_count,
1658                f.register_count as u16,
1659                func,
1660                &f.param_kinds,
1661                f.ret_kind,
1662                &self.native_ctx,
1663                &callees,
1664            ) {
1665                Some(nf) => {
1666                    self.native_ctx.table.publish(fi, nf.entry_ptr(), nf.published_regc());
1667                    let name = self.fn_name(fi);
1668                    super::tier_trace::trace_transition(fi, &name, super::tier_trace::ExecTier::NativeForge);
1669                    self.native[fi] = NativeSlot::Ready(nf);
1670                }
1671                None => {
1672                    self.native[fi] = NativeSlot::Failed;
1673                    return NativeDisposition::Interpret;
1674                }
1675            }
1676        }
1677        // The per-call guard: every argument must match its DECLARED kind
1678        // (floats travel as raw bits in the i64 slot; lists pin), else the
1679        // call stays interpreted.
1680        let base = self.base + args_start as usize;
1681        // Hot boundary: a stack buffer instead of a per-call Vec — functions
1682        // like gcd cross bytecode→native hundreds of thousands of times.
1683        if arg_count as usize > 16 {
1684            return NativeDisposition::Interpret;
1685        }
1686        let kinds = &self.program.functions[fi].param_kinds;
1687        let mut args = [0i64; 16];
1688        for k in 0..arg_count as usize {
1689            let Some(v) = self.registers.get(base + k) else {
1690                return NativeDisposition::Interpret;
1691            };
1692            args[k] = match kinds.get(k).copied().flatten() {
1693                Some(ParamKind::Scalar(super::native_tier::SlotKind::Int)) | None => {
1694                    match v.as_int() {
1695                        Some(n) => n,
1696                        None => return NativeDisposition::Interpret,
1697                    }
1698                }
1699                Some(ParamKind::Scalar(super::native_tier::SlotKind::Bool)) => {
1700                    match v.as_bool() {
1701                        Some(b) => b as i64,
1702                        None => return NativeDisposition::Interpret,
1703                    }
1704                }
1705                Some(ParamKind::Scalar(super::native_tier::SlotKind::Float)) => {
1706                    match v.as_float() {
1707                        Some(f) => f.to_bits() as i64,
1708                        None => return NativeDisposition::Interpret,
1709                    }
1710                }
1711                // List params ride the pin lane; the register slot is a
1712                // placeholder native code never reads as a scalar.
1713                Some(ParamKind::List(_)) => 0,
1714            };
1715        }
1716        // Pin list parameters: one borrow per DISTINCT Rc for the whole
1717        // call (recursion reuses the same pins via pass-through identity).
1718        // Empty lists retag in place to the declared element repr.
1719        let outcome = {
1720            use crate::interpreter::{ListRepr, RuntimeValue};
1721            use super::native_tier::PinElem;
1722            let mut handles: Vec<(usize, std::cell::RefMut<'_, ListRepr>)> = Vec::new();
1723            let mut pins: Vec<i64> = Vec::new();
1724            let mut pin_args: Vec<Value> = Vec::new();
1725            for (k, pk) in kinds.iter().enumerate().take(arg_count as usize) {
1726                let Some(ParamKind::List(elem)) = pk else { continue };
1727                let Some(v) = self.registers.get(base + k) else {
1728                    return NativeDisposition::Interpret;
1729                };
1730                let Some(RuntimeValue::List(rc)) = v.as_runtime_ref() else {
1731                    return NativeDisposition::Interpret;
1732                };
1733                pin_args.push(v.clone());
1734                let key = std::rc::Rc::as_ptr(rc) as usize;
1735                if !handles.iter().any(|(hk, _)| *hk == key) {
1736                    let Ok(mut b) = rc.try_borrow_mut() else {
1737                        return NativeDisposition::Interpret;
1738                    };
1739                    // Declared-elem retag for the empty list (the shared
1740                    // empty starts as Ints).
1741                    let empty = match &*b {
1742                        ListRepr::Ints(v) => v.is_empty(),
1743                        _ => false,
1744                    };
1745                    if empty {
1746                        match elem {
1747                            PinElem::Float => *b = ListRepr::Floats(Vec::new()),
1748                            PinElem::Bool => *b = ListRepr::Bools(Vec::new()),
1749                            PinElem::IntI32 => *b = ListRepr::IntsI32(Vec::new()),
1750                            PinElem::Int => {}
1751                            // Function params never pin maps or texts (declared
1752                            // kinds only produce Int/Float/Bool list elems).
1753                            PinElem::Map | PinElem::TextBytes | PinElem::TextMut => {
1754                                return NativeDisposition::Interpret
1755                            }
1756                        }
1757                    }
1758                    handles.push((key, b));
1759                }
1760                let idx = handles.iter().position(|(hk, _)| *hk == key).unwrap();
1761                let payload = &mut *handles[idx].1;
1762                let (vec_handle, ptr, len) = match (payload, elem) {
1763                    (ListRepr::Ints(v), PinElem::Int) => {
1764                        (v as *mut Vec<i64> as i64, v.as_mut_ptr() as i64, v.len())
1765                    }
1766                    (ListRepr::IntsI32(v), PinElem::IntI32) => {
1767                        (v as *mut Vec<i32> as i64, v.as_mut_ptr() as i64, v.len())
1768                    }
1769                    (ListRepr::Floats(v), PinElem::Float) => {
1770                        (v as *mut Vec<f64> as i64, v.as_mut_ptr() as i64, v.len())
1771                    }
1772                    (ListRepr::Bools(v), PinElem::Bool) => {
1773                        (v as *mut Vec<bool> as i64, v.as_mut_ptr() as i64, v.len())
1774                    }
1775                    _ => return NativeDisposition::Interpret,
1776                };
1777                pins.push(vec_handle);
1778                pins.push(ptr);
1779                pins.push(len as i64);
1780                // The marshalled argument slot mirrors the vec handle —
1781                // native list registers always carry their handle.
1782                args[k] = vec_handle;
1783            }
1784            let NativeSlot::Ready(nf) = &self.native[fi] else { unreachable!() };
1785            let args_slice = &args[..arg_count as usize];
1786            // The native callee occupies one LOGOS frame on top of the
1787            // bytecode stack; its self-calls count from there against
1788            // MAX_CALL_DEPTH.
1789            let out = nf.call(args_slice, &pins, bytecode_depth + 1);
1790            drop(handles);
1791            (out, pin_args, pins)
1792        };
1793        let (out, pin_args, pins_for_ret) = outcome;
1794        let NativeSlot::Ready(nf) = &self.native[fi] else { unreachable!() };
1795        match out {
1796            // The backend already re-boxed (list-returning functions own
1797            // their allocation registry).
1798            super::native_tier::NativeOutcome::ReturnValue(v) => NativeDisposition::Done(v),
1799            super::native_tier::NativeOutcome::Return(raw) => {
1800                NativeDisposition::Done(match nf.ret() {
1801                    super::native_tier::NativeRet::Scalar(k) => match k {
1802                        super::native_tier::SlotKind::Bool => Value::bool(raw != 0),
1803                        super::native_tier::SlotKind::Float => {
1804                            Value::float(f64::from_bits(raw as u64))
1805                        }
1806                        super::native_tier::SlotKind::Int => Value::int(raw),
1807                    },
1808                    // By-handle return that was NOT registry-owned: it is
1809                    // one of the caller's list arguments — match the pin
1810                    // handles (every triple's first slot) and clone that
1811                    // argument, preserving identity.
1812                    super::native_tier::NativeRet::ListByHandle => {
1813                        let mut found: Option<Value> = None;
1814                        for (k, chunk) in pins_for_ret.chunks(3).enumerate() {
1815                            if chunk.first() == Some(&raw) {
1816                                found = pin_args.get(k).cloned();
1817                                break;
1818                            }
1819                        }
1820                        match found {
1821                            Some(v) => v,
1822                            None => return NativeDisposition::Interpret,
1823                        }
1824                    }
1825                    // Return-by-parameter: the result IS the caller's list
1826                    // argument (same Rc — identity preserved).
1827                    super::native_tier::NativeRet::ListParam(j) => {
1828                        let mut nth = 0usize;
1829                        let mut found: Option<Value> = None;
1830                        for (k, pk) in kinds.iter().enumerate().take(arg_count as usize) {
1831                            if matches!(pk, Some(ParamKind::List(_))) {
1832                                if k == j as usize {
1833                                    found = pin_args.get(nth).cloned();
1834                                    break;
1835                                }
1836                                nth += 1;
1837                            }
1838                        }
1839                        match found {
1840                            Some(v) => v,
1841                            None => return NativeDisposition::Interpret,
1842                        }
1843                    }
1844                })
1845            }
1846            // Plain side exit: every effect was confined to the private
1847            // frame — replaying the whole call on bytecode is sound and
1848            // raises the exact kernel error at the exact point.
1849            super::native_tier::NativeOutcome::Deopt => NativeDisposition::Interpret,
1850            // Precise side exit: effects landed; materialize the chain.
1851            super::native_tier::NativeOutcome::DeoptAt { resume_pc, frames } => {
1852                NativeDisposition::Materialize { resume_pc, frames, list_args: pin_args }
1853            }
1854        }
1855    }
1856
1857    /// Provide the policy registry (and the interner its symbols live in) for
1858    /// `Check` statements.
1859    pub fn with_policy_ctx(
1860        mut self,
1861        registry: &'p crate::analysis::PolicyRegistry,
1862        interner: &'p crate::intern::Interner,
1863    ) -> Self {
1864        self.policy_ctx = Some((registry, interner));
1865        self
1866    }
1867
1868    /// The output lines, one per `Show`.
1869    pub fn lines(&self) -> &[String] {
1870        &self.lines
1871    }
1872
1873    /// The output as one string (one trailing newline per `Show`).
1874    pub fn output(&self) -> String {
1875        let mut s = String::new();
1876        for l in &self.lines {
1877            s.push_str(l);
1878            s.push('\n');
1879        }
1880        s
1881    }
1882
1883    /// Consume the VM, returning its output lines.
1884    pub fn into_lines(self) -> Vec<String> {
1885        self.lines
1886    }
1887
1888    /// Take this slice's output lines (the scheduler driver merges each task's
1889    /// output into a shared sink as it is produced, preserving per-task order).
1890    pub(crate) fn drain_lines(&mut self) -> Vec<String> {
1891        std::mem::take(&mut self.lines)
1892    }
1893
1894    /// Run the whole program to completion (the non-concurrent entry). A
1895    /// concurrent program never reaches here — it is driven through
1896    /// [`Vm::run_until_block`] by the scheduler.
1897    pub fn run(&mut self) -> Result<(), String> {
1898        // Hermetic program start: no ambient exchange rates carried in from a prior run on this
1899        // thread (mirrors a fresh AOT process). The resumable `run_until_block` path is left alone so
1900        // rates installed mid-program survive across concurrency suspensions.
1901        logicaffeine_base::money::clear_ambient_rates();
1902        match self.run_until_block()? {
1903            VmStep::Done(_) => Ok(()),
1904            VmStep::Blocked => {
1905                Err("vm: concurrency op requires the scheduler driver".to_string())
1906            }
1907            VmStep::Paused => unreachable!("run_until_block (STEPPED = false) never pauses"),
1908        }
1909    }
1910
1911    /// Run one slice: from a fresh start (or resumed from a prior block) until the
1912    /// program completes ([`VmStep::Done`]) or a concurrency op suspends it
1913    /// ([`VmStep::Blocked`], request in [`Vm::take_pending`]). Keeping `pc` and the
1914    /// call stack as loop-locals (restored once on entry, saved once on a block)
1915    /// leaves the hot dispatch path byte-for-byte identical to the old `run`.
1916    pub(crate) fn run_until_block(&mut self) -> Result<VmStep, String> {
1917        self.run_until_block_impl::<false>(u64::MAX)
1918    }
1919
1920    /// Step the debug interpreter forward by at most `step_budget` ops, then pause
1921    /// ([`VmStep::Paused`], resumable on the next call). Used only by the Studio
1922    /// debug drawer; production callers use [`Vm::run_until_block`]
1923    /// (`STEPPED = false`), whose monomorphization elides every budget check — the
1924    /// hot dispatch path stays the byte-for-byte old loop.
1925    pub(crate) fn run_steps(&mut self, step_budget: u64) -> Result<VmStep, String> {
1926        self.run_until_block_impl::<true>(step_budget)
1927    }
1928
1929    fn run_until_block_impl<const STEPPED: bool>(
1930        &mut self,
1931        step_budget: u64,
1932    ) -> Result<VmStep, String> {
1933        let mut pc;
1934        let mut call_stack: Vec<CallFrame>;
1935        if self.sched_active {
1936            pc = self.sched_pc;
1937            call_stack = std::mem::take(&mut self.sched_call_stack);
1938            self.sched_active = false;
1939        } else {
1940            pc = 0usize;
1941            call_stack = Vec::new();
1942        }
1943
1944        // The loop ends when the top-level program code is exhausted. A warm body
1945        // lives past `program.code.len()` but is only ever entered via a `Call` (which
1946        // pushes a frame), so `!call_stack.is_empty()` keeps the loop alive while one
1947        // is executing; without any warm body installed a live frame already implies
1948        // `pc < program.code.len()`, so this disjunct is a no-op for the baseline path.
1949        let mut executed: u64 = 0;
1950        while pc < self.program.code.len() || !call_stack.is_empty() {
1951            if STEPPED {
1952                if executed >= step_budget {
1953                    // Pause exactly as a concurrency block saves its slice (pc +
1954                    // call stack into the scheduler slots), but with no pending
1955                    // request — the debugger resumes on the next `run_steps`.
1956                    self.sched_pc = pc;
1957                    self.sched_call_stack = call_stack;
1958                    self.sched_active = true;
1959                    return Ok(VmStep::Paused);
1960                }
1961                executed += 1;
1962            }
1963            let op = if pc < self.program.code.len() {
1964                self.program.code[pc]
1965            } else {
1966                self.warm_code[pc - self.program.code.len()]
1967            };
1968            match op {
1969                Op::LoadConst { dst, idx } => {
1970                    let v = self.const_pool[idx as usize].clone();
1971                    self.set(dst, v);
1972                    pc += 1;
1973                }
1974                Op::Move { dst, src } => {
1975                    self.set(dst, self.reg(src).clone());
1976                    pc += 1;
1977                }
1978                Op::EnsureOwned { reg } => {
1979                    // Call-site copy-on-write barrier: isolate a shared collection
1980                    // before it is passed to a mutable-borrow callee. No-op when the
1981                    // register is a `mutable`-exempt param (its own writes COW) or the
1982                    // collection is already uniquely owned.
1983                    self.ensure_reg_owned(reg, call_stack.last().map(|f| f.func));
1984                    pc += 1;
1985                }
1986                Op::Add { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::add)?; pc += 1; }
1987                Op::AddAssign { dst, src } => { self.add_assign(dst, src)?; pc += 1; }
1988                Op::Sub { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::sub)?; pc += 1; }
1989                Op::Mul { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::mul)?; pc += 1; }
1990                Op::Div { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::div)?; pc += 1; }
1991                Op::ExactDiv { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::exact_div)?; pc += 1; }
1992                Op::FloorDiv { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::floor_div)?; pc += 1; }
1993                Op::Mod { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::modulo)?; pc += 1; }
1994                Op::DivPow2 { dst, lhs, k } => {
1995                    // `lhs / 2^k` (lhs is Oracle-proven Int) — identical result
1996                    // to `Op::Div` by `2^k`, so it matches the tree-walker.
1997                    let v = self.reg(lhs).div(&Value::int(1i64 << k))?;
1998                    self.set(dst, v);
1999                    pc += 1;
2000                }
2001                Op::MagicDivU { dst, lhs, magic, more, mul_back } => {
2002                    // `lhs / c` / `lhs % c` by the precomputed magic reciprocal
2003                    // (`magic`/`more`). Emitted only for an Oracle-proven Int,
2004                    // non-negative `lhs`, so the result is bit-identical to
2005                    // `Op::Div`/`Op::Mod` by the constant `c`.
2006                    let x = self.reg(lhs).as_int().ok_or_else(|| {
2007                        "MagicDivU on a non-Int operand".to_string()
2008                    })?;
2009                    let v = crate::vm::compiler::magic_eval(x, magic, more, mul_back);
2010                    self.set(dst, Value::int(v));
2011                    pc += 1;
2012                }
2013                Op::Lt { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::lt)?; pc += 1; }
2014                Op::Gt { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::gt)?; pc += 1; }
2015                Op::LtEq { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::lte)?; pc += 1; }
2016                Op::GtEq { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::gte)?; pc += 1; }
2017                Op::Eq { dst, lhs, rhs } => {
2018                    let v = self.reg(lhs).eq_op(self.reg(rhs));
2019                    self.set(dst, v);
2020                    pc += 1;
2021                }
2022                Op::ApproxEq { dst, lhs, rhs } => {
2023                    let v = crate::semantics::arith::approx_eq(
2024                        self.reg(lhs).as_runtime().clone(),
2025                        self.reg(rhs).as_runtime().clone(),
2026                    )
2027                    .map(Value::from_runtime)?;
2028                    self.set(dst, v);
2029                    pc += 1;
2030                }
2031                Op::NotEq { dst, lhs, rhs } => {
2032                    let v = self.reg(lhs).neq_op(self.reg(rhs));
2033                    self.set(dst, v);
2034                    pc += 1;
2035                }
2036                Op::Not { dst, src } => {
2037                    let v = self.reg(src).not_op()?;
2038                    self.set(dst, v);
2039                    pc += 1;
2040                }
2041                Op::Concat { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::concat)?; pc += 1; }
2042                Op::SeqConcat { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::seq_concat)?; pc += 1; }
2043                Op::Pow { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::pow)?; pc += 1; }
2044                Op::BitXor { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::bitxor)?; pc += 1; }
2045                Op::BitAnd { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::bitand)?; pc += 1; }
2046                Op::BitOr { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::bitor)?; pc += 1; }
2047                Op::Shl { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::shl)?; pc += 1; }
2048                Op::Shr { dst, lhs, rhs } => { self.binop(dst, lhs, rhs, Value::shr)?; pc += 1; }
2049                Op::Jump { target } => {
2050                    // A back-edge is a hot-loop candidate in EVERY frame
2051                    // (OSR everywhere); the enclosing frame picks the
2052                    // named-register map the write-back contract consults.
2053                    // Skip the whole region machinery (the per-frame named-map
2054                    // selection + the `regions` hashmap probe) once this loop
2055                    // head is blacklisted — an un-tierable / demoted region pays
2056                    // only this O(1) `Vec<bool>` index per back-edge instead of
2057                    // re-hashing every iteration.
2058                    // `target < program.code.len()` keeps the region machinery
2059                    // (blacklist indexed by program pc, `program.code[head..]` scans)
2060                    // off warm-body back-edges; always true on the baseline path.
2061                    if target < pc
2062                        && target < self.program.code.len()
2063                        && self.tier.is_some()
2064                        && !self.region_blacklist[target]
2065                    {
2066                        let (named, frame_regs): (&[bool], usize) = match call_stack.last() {
2067                            None => (&self.program.named_regs, self.program.register_count),
2068                            Some(f) => {
2069                                let fun = &self.program.functions[f.func as usize];
2070                                (&fun.named_regs, fun.register_count)
2071                            }
2072                        };
2073                        let cur_func = call_stack.last().map(|f| f.func);
2074                        match self.try_region(target, pc, named, frame_regs, call_stack.len(), cur_func)
2075                        {
2076                            Some(RegionExit::At(exit)) => {
2077                                pc = exit;
2078                                continue;
2079                            }
2080                            Some(RegionExit::Return(value)) => {
2081                                // The region hit a `Return` — perform the
2082                                // actual function return, exactly like the
2083                                // Op::Return arm.
2084                                let frame =
2085                                    call_stack.pop().ok_or("vm: return with no caller")?;
2086                                self.iter_stack.truncate(frame.iter_depth);
2087                                self.registers.truncate(frame.restore_len);
2088                                self.base = frame.caller_base;
2089                                self.set(frame.return_reg, value);
2090                                self.clear_arg_window(&frame);
2091                                pc = frame.return_pc;
2092                                continue;
2093                            }
2094                            None => {}
2095                        }
2096                    }
2097                    pc = target;
2098                }
2099                Op::JumpIfFalse { cond, target } => {
2100                    if !self.reg(cond).is_truthy() { pc = target; } else { pc += 1; }
2101                }
2102                Op::JumpIfTrue { cond, target } => {
2103                    if self.reg(cond).is_truthy() { pc = target; } else { pc += 1; }
2104                }
2105                Op::GlobalGet { dst, idx } => {
2106                    match &self.globals[idx as usize] {
2107                        Some(v) => {
2108                            let v = v.clone();
2109                            self.set(dst, v);
2110                        }
2111                        None => {
2112                            return Err(format!(
2113                                "Undefined variable: {}",
2114                                self.program.globals[idx as usize]
2115                            ));
2116                        }
2117                    }
2118                    pc += 1;
2119                }
2120                Op::GlobalSet { idx, src } => {
2121                    self.globals[idx as usize] = Some(self.reg(src).clone());
2122                    pc += 1;
2123                }
2124                Op::MakeClosure { dst, func, locals_start } => {
2125                    use crate::interpreter::{ClosureValue, RuntimeValue};
2126                    let f = self
2127                        .program
2128                        .functions
2129                        .get(func as usize)
2130                        .ok_or("vm: MakeClosure on undefined function index")?;
2131                    let mut captured_env = std::collections::HashMap::new();
2132                    let mut local_k: Reg = 0;
2133                    for (sym, global_idx) in &f.captures {
2134                        match global_idx {
2135                            Some(gidx) => {
2136                                // Snapshot the global IF it is defined; an
2137                                // undefined one is simply not captured — the
2138                                // body falls through to the live global.
2139                                if let Some(v) = &self.globals[*gidx as usize] {
2140                                    captured_env.insert(*sym, v.as_runtime().deep_clone());
2141                                }
2142                            }
2143                            None => {
2144                                let v = self.reg(locals_start + local_k).as_runtime().deep_clone();
2145                                captured_env.insert(*sym, v);
2146                                local_k += 1;
2147                            }
2148                        }
2149                    }
2150                    let param_names = vec![crate::intern::Symbol::default(); f.param_count as usize];
2151                    self.set(
2152                        dst,
2153                        Value::from_runtime(RuntimeValue::Function(Box::new(ClosureValue {
2154                            body_index: func as usize,
2155                            captured_env,
2156                            param_names,
2157                            generated: None,
2158                        }))),
2159                    );
2160                    pc += 1;
2161                }
2162                Op::CallValue { dst, callee, args_start, arg_count, name_for_err } => {
2163                    use crate::interpreter::RuntimeValue;
2164                    let closure = match &*self.reg(callee).as_runtime() {
2165                        RuntimeValue::Function(c) => (**c).clone(),
2166                        other => {
2167                            return Err(if name_for_err == u32::MAX {
2168                                format!("Cannot call value of type {}", other.type_name())
2169                            } else {
2170                                match &self.program.constants[name_for_err as usize] {
2171                                    Constant::Text(n) => format!("Unknown function: {}", n),
2172                                    _ => format!("Cannot call value of type {}", other.type_name()),
2173                                }
2174                            });
2175                        }
2176                    };
2177                    if call_stack.len() >= crate::semantics::MAX_CALL_DEPTH {
2178                        return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
2179                    }
2180                    let f = self
2181                        .program
2182                        .functions
2183                        .get(closure.body_index)
2184                        .ok_or("vm: CallValue on undefined function index")?;
2185                    if arg_count as usize != f.param_count as usize {
2186                        return Err(format!(
2187                            "Closure expects {} arguments, got {}",
2188                            f.param_count, arg_count
2189                        ));
2190                    }
2191                    let entry_pc = f.entry_pc;
2192                    let reg_count = f.register_count;
2193                    let captures = f.captures.clone();
2194                    let param_count = f.param_count;
2195
2196                    let callee_base = self.base + args_start as usize;
2197                    let restore_len = self.registers.len();
2198                    let needed = callee_base + reg_count;
2199                    if needed > MAX_REGISTER_FILE {
2200                        return Err("vm: register file limit exceeded".to_string());
2201                    }
2202                    if self.registers.len() < needed {
2203                        self.registers.resize(needed, Value::nothing());
2204                    }
2205                    call_stack.push(CallFrame {
2206                        return_pc: pc + 1,
2207                        return_reg: dst,
2208                        caller_base: self.base,
2209                        restore_len,
2210                        iter_depth: self.iter_stack.len(),
2211                        func: closure.body_index as u16,
2212                        arg_lo: callee_base,
2213                        arg_count,
2214                    });
2215                    self.base = callee_base;
2216                    // Bind captures: value slots then present flags — both
2217                    // deep-cloned PER CALL (the tree-walker re-clones each
2218                    // invocation).
2219                    let cap_count = captures.len() as Reg;
2220                    for (k, (sym, _)) in captures.iter().enumerate() {
2221                        let (v, present) = match closure.captured_env.get(sym) {
2222                            Some(v) => (Value::from_runtime(v.deep_clone()), true),
2223                            None => (Value::nothing(), false),
2224                        };
2225                        self.set(param_count + k as Reg, v);
2226                        self.set(param_count + cap_count + k as Reg, Value::bool(present));
2227                    }
2228                    pc = entry_pc;
2229                }
2230                Op::CallBuiltin { dst, builtin, args_start, arg_count } => {
2231                    let mut args = Vec::with_capacity(arg_count as usize);
2232                    for k in 0..arg_count {
2233                        args.push(self.reg(args_start + k).as_runtime().clone());
2234                    }
2235                    let v = crate::semantics::builtins::call_builtin(builtin, args)?;
2236                    self.set(dst, Value::from_runtime(v));
2237                    self.clear_args(args_start, arg_count);
2238                    pc += 1;
2239                }
2240                Op::Call { dst, func, args_start, arg_count } => {
2241                    if call_stack.len() >= crate::semantics::MAX_CALL_DEPTH {
2242                        return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
2243                    }
2244                    match self.try_native(func, args_start, arg_count, call_stack.len()) {
2245                        NativeDisposition::Done(v) => {
2246                            self.set(dst, v);
2247                            self.clear_args(args_start, arg_count);
2248                            pc += 1;
2249                            continue;
2250                        }
2251                        NativeDisposition::Interpret => {}
2252                        // Precise deopt: every native frame becomes a real
2253                        // CallFrame (registers re-boxed per the adapter's
2254                        // kind capture), then the interpreter resumes AT
2255                        // the faulting op — effects stay, the kernel
2256                        // raises the exact error from the exact state.
2257                        NativeDisposition::Materialize { resume_pc, frames, list_args } => {
2258                            self.materialize_native_frames(
2259                                &frames, &list_args, args_start, dst, func, pc, &mut call_stack,
2260                            )?;
2261                            pc = resume_pc;
2262                            continue;
2263                        }
2264                    }
2265                    // WS6 (Phase 13): the WASM-JIT tier — the browser JIT path, consulted
2266                    // here (behind the native forge tier above, which is absent on wasm32).
2267                    // A hot pure-integer function runs its emitted WebAssembly module instead
2268                    // of the bytecode; non-Int args or an ineligible body fall through.
2269                    #[cfg(feature = "wasm-jit")]
2270                    {
2271                        let prog = self.program;
2272                        let abase = self.base + args_start as usize;
2273                        let mut ints: Vec<i64> = Vec::with_capacity(arg_count as usize);
2274                        let mut all_int = true;
2275                        for k in 0..arg_count as usize {
2276                            match &*self.registers[abase + k].as_runtime() {
2277                                crate::interpreter::RuntimeValue::Int(n) => ints.push(*n),
2278                                _ => {
2279                                    all_int = false;
2280                                    break;
2281                                }
2282                            }
2283                        }
2284                        if all_int {
2285                            if let Some(r) = self.wasm_tier.on_call(prog, func, &ints) {
2286                                self.set(dst, Value::int(r));
2287                                pc += 1;
2288                                continue;
2289                            }
2290                        }
2291                    }
2292                    // Dispatch order (HOTSWAP §7): FnTable native (above) →
2293                    // warm_bytecode → baseline. A warm body runs from the unified
2294                    // pc space past `program.code`.
2295                    let (entry_pc, reg_count) = match self.warm_entry.get(func as usize).and_then(|w| *w) {
2296                        Some(w) => (w.entry_pc, w.register_count),
2297                        None => {
2298                            let f = self
2299                                .program
2300                                .functions
2301                                .get(func as usize)
2302                                .ok_or("vm: call to undefined function index")?;
2303                            (f.entry_pc, f.register_count)
2304                        }
2305                    };
2306                    let callee_base = self.base + args_start as usize;
2307                    let restore_len = self.registers.len();
2308                    let needed = callee_base + reg_count;
2309                    if needed > MAX_REGISTER_FILE {
2310                        return Err("vm: register file limit exceeded".to_string());
2311                    }
2312                    if self.registers.len() < needed {
2313                        self.registers.resize(needed, Value::nothing());
2314                    }
2315                    call_stack.push(CallFrame {
2316                        return_pc: pc + 1,
2317                        return_reg: dst,
2318                        caller_base: self.base,
2319                        restore_len,
2320                        iter_depth: self.iter_stack.len(),
2321                        func,
2322                        arg_lo: callee_base,
2323                        arg_count,
2324                    });
2325                    self.base = callee_base;
2326                    pc = entry_pc;
2327                }
2328                Op::Return { src } => {
2329                    let frame = call_stack.pop().ok_or("vm: return with no caller")?;
2330                    let rv = self.reg(src).clone();
2331                    self.iter_stack.truncate(frame.iter_depth);
2332                    self.registers.truncate(frame.restore_len);
2333                    self.base = frame.caller_base;
2334                    let slot = self.base + frame.return_reg as usize;
2335                    self.registers[slot] = rv;
2336                    self.clear_arg_window(&frame);
2337                    pc = frame.return_pc;
2338                }
2339                Op::ReturnNothing => {
2340                    let frame = call_stack.pop().ok_or("vm: return with no caller")?;
2341                    self.iter_stack.truncate(frame.iter_depth);
2342                    self.registers.truncate(frame.restore_len);
2343                    self.base = frame.caller_base;
2344                    let slot = self.base + frame.return_reg as usize;
2345                    self.registers[slot] = Value::nothing();
2346                    self.clear_arg_window(&frame);
2347                    pc = frame.return_pc;
2348                }
2349                Op::NewList { dst, start, count } => {
2350                    let mut items = Vec::with_capacity(count as usize);
2351                    for k in 0..count {
2352                        items.push(self.reg(start + k).clone());
2353                    }
2354                    self.set(dst, Value::list(items));
2355                    pc += 1;
2356                }
2357                Op::NewEmptyList { dst } => {
2358                    // Allocation reuse: if `dst` already holds a SOLE-OWNED
2359                    // Ints list (e.g. the previous loop iteration's, now dead),
2360                    // clear it in place and reuse its buffer/capacity instead
2361                    // of allocating a fresh Rc + Vec. Sound: refcount 1 means
2362                    // no other holder can observe the clear.
2363                    use crate::interpreter::{ListRepr, RuntimeValue};
2364                    use std::rc::Rc;
2365                    let di = self.base + dst as usize;
2366                    let reused = matches!(
2367                        self.registers.get(di).map(|v| v.as_runtime()).as_deref(),
2368                        Some(RuntimeValue::List(rc))
2369                            if Rc::strong_count(rc) == 1
2370                                && Rc::weak_count(rc) == 0
2371                                && matches!(&*rc.borrow(), ListRepr::Ints(_))
2372                    );
2373                    if reused {
2374                        if let RuntimeValue::List(rc) = &*self.registers[di].as_runtime() {
2375                            if let ListRepr::Ints(buf) = &mut *rc.borrow_mut() {
2376                                buf.clear();
2377                            }
2378                        }
2379                    } else {
2380                        self.set(dst, Value::empty_list());
2381                    }
2382                    pc += 1;
2383                }
2384                Op::NewEmptyListI32 { dst } => {
2385                    // Mirror NewEmptyList's allocation reuse, but for the
2386                    // half-width `IntsI32` repr (the narrowing-proven buffer).
2387                    use crate::interpreter::{ListRepr, RuntimeValue};
2388                    use std::rc::Rc;
2389                    let di = self.base + dst as usize;
2390                    let reused = matches!(
2391                        self.registers.get(di).map(|v| v.as_runtime()).as_deref(),
2392                        Some(RuntimeValue::List(rc))
2393                            if Rc::strong_count(rc) == 1
2394                                && Rc::weak_count(rc) == 0
2395                                && matches!(&*rc.borrow(), ListRepr::IntsI32(_))
2396                    );
2397                    if reused {
2398                        if let RuntimeValue::List(rc) = &*self.registers[di].as_runtime() {
2399                            if let ListRepr::IntsI32(buf) = &mut *rc.borrow_mut() {
2400                                buf.clear();
2401                            }
2402                        }
2403                    } else {
2404                        self.set(dst, Value::empty_list_i32());
2405                    }
2406                    pc += 1;
2407                }
2408                Op::NewEmptySet { dst } => { self.set(dst, Value::empty_set()); pc += 1; }
2409                Op::NewEmptyMap { dst } => { self.set(dst, Value::empty_map()); pc += 1; }
2410                Op::NewRange { dst, start, end } => {
2411                    let (lo, hi) = match (self.reg(start).as_int(), self.reg(end).as_int()) {
2412                        (Some(lo), Some(hi)) => (lo, hi),
2413                        _ => return Err("Range requires Int bounds".to_string()),
2414                    };
2415                    self.set(dst, Value::int_range(lo, hi));
2416                    pc += 1;
2417                }
2418                Op::ListPush { list, value } => {
2419                    let v = self.reg(value).clone();
2420                    self.ensure_reg_owned(list, call_stack.last().map(|f| f.func));
2421                    self.reg(list).list_push(v)?;
2422                    pc += 1;
2423                }
2424                Op::SetAdd { set, value } => {
2425                    let v = self.reg(value).clone();
2426                    self.ensure_reg_owned(set, call_stack.last().map(|f| f.func));
2427                    self.reg(set).set_add(v)?;
2428                    pc += 1;
2429                }
2430                Op::RemoveFrom { collection, value } => {
2431                    self.ensure_reg_owned(collection, call_stack.last().map(|f| f.func));
2432                    self.reg(collection).remove_from(self.reg(value))?;
2433                    pc += 1;
2434                }
2435                Op::SetIndex { collection, index, value }
2436                | Op::SetIndexUnchecked { collection, index, value } => {
2437                    use crate::interpreter::RuntimeValue;
2438                    // Struct field set via index syntax (`Set item "f" of s to
2439                    // v`) — VALUE semantics, mirroring the tree-walker: clone
2440                    // the struct, insert, write the new struct back.
2441                    let is_struct_text = matches!(
2442                        (&*self.reg(collection).as_runtime(), &*self.reg(index).as_runtime()),
2443                        (RuntimeValue::Struct(_), RuntimeValue::Text(_))
2444                    );
2445                    if is_struct_text {
2446                        let field = match &*self.reg(index).as_runtime() {
2447                            RuntimeValue::Text(t) => t.to_string(),
2448                            _ => unreachable!(),
2449                        };
2450                        let new_val = self.reg(value).as_runtime().clone();
2451                        let target = self.reg_mut(collection);
2452                        match target.as_runtime_mut() {
2453                            RuntimeValue::Struct(st) => {
2454                                st.fields.insert(field, new_val);
2455                            }
2456                            _ => unreachable!(),
2457                        }
2458                    } else {
2459                        let v = self.reg(value).clone();
2460                        self.ensure_reg_owned(collection, call_stack.last().map(|f| f.func));
2461                        self.reg(collection).index_set(self.reg(index), v)?;
2462                    }
2463                    pc += 1;
2464                }
2465                // The bytecode interpreter checks both: a sound proof makes
2466                // the `IndexUnchecked` check never fire, so keeping it is
2467                // free defense-in-depth. Only the JIT elides.
2468                Op::Index { dst, collection, index }
2469                | Op::IndexUnchecked { dst, collection, index } => {
2470                    let v = self.reg(collection).index_get(self.reg(index))?;
2471                    self.set(dst, v);
2472                    pc += 1;
2473                }
2474                Op::Length { dst, collection } => {
2475                    let n = self.reg(collection).len()?;
2476                    self.set(dst, Value::int(n));
2477                    pc += 1;
2478                }
2479                // Pure metadata for the native region tier — the interpreter's
2480                // checked accesses make it a no-op (the hoist it enables only
2481                // applies inside a compiled region, verified at region entry).
2482                Op::RegionBoundsGuard { .. } => {
2483                    pc += 1;
2484                }
2485                Op::Contains { dst, collection, value } => {
2486                    let b = self.reg(collection).contains(self.reg(value))?;
2487                    self.set(dst, Value::bool(b));
2488                    pc += 1;
2489                }
2490                Op::ListPushField { obj, field, src } => {
2491                    let field_name = match &self.program.constants[field as usize] {
2492                        Constant::Text(s) => s.clone(),
2493                        other => return Err(format!("vm: field name is not Text: {:?}", other)),
2494                    };
2495                    let val = self.reg(src).as_runtime().clone();
2496                    crate::semantics::collections::push_to_struct_field(
2497                        &self.reg(obj).as_runtime(),
2498                        &field_name,
2499                        val,
2500                    )?;
2501                    pc += 1;
2502                }
2503                Op::CheckPolicy { subject, predicate, is_capability, object, source_text } => {
2504                    let (registry, interner) = match self.policy_ctx {
2505                        Some(ctx) => ctx,
2506                        None => {
2507                            return Err(
2508                                "Security Check requires policies. Use compiled Rust or add ## Policy block."
2509                                    .to_string(),
2510                            );
2511                        }
2512                    };
2513                    let source = match &self.program.constants[source_text as usize] {
2514                        Constant::Text(s) => s.clone(),
2515                        other => return Err(format!("vm: check source is not Text: {:?}", other)),
2516                    };
2517                    let obj_val = if object != Reg::MAX {
2518                        Some(self.reg(object).as_runtime().clone())
2519                    } else {
2520                        None
2521                    };
2522                    crate::semantics::policy::check_policy(
2523                        registry,
2524                        interner,
2525                        &self.reg(subject).as_runtime(),
2526                        predicate,
2527                        is_capability,
2528                        obj_val.as_ref(),
2529                        &source,
2530                    )?;
2531                    pc += 1;
2532                }
2533                Op::FormatValue { dst, src, spec, debug_prefix } => {
2534                    let mut out = String::new();
2535                    if debug_prefix != u32::MAX {
2536                        match &self.program.constants[debug_prefix as usize] {
2537                            Constant::Text(p) => {
2538                                out.push_str(p);
2539                                out.push('=');
2540                            }
2541                            other => {
2542                                return Err(format!("vm: debug prefix is not Text: {:?}", other));
2543                            }
2544                        }
2545                    }
2546                    if spec != u32::MAX {
2547                        let spec_s = match &self.program.constants[spec as usize] {
2548                            Constant::Text(s) => s.as_str(),
2549                            other => return Err(format!("vm: format spec is not Text: {:?}", other)),
2550                        };
2551                        out.push_str(&crate::semantics::format::apply_format_spec(
2552                            &self.reg(src).as_runtime(),
2553                            spec_s,
2554                        ));
2555                    } else {
2556                        out.push_str(&self.reg(src).to_display_string());
2557                    }
2558                    self.set(dst, Value::text(out));
2559                    pc += 1;
2560                }
2561                Op::SliceOp { dst, collection, start, end } => {
2562                    let v = crate::semantics::collections::slice(
2563                        &self.reg(collection).as_runtime(),
2564                        &self.reg(start).as_runtime(),
2565                        &self.reg(end).as_runtime(),
2566                    )?;
2567                    self.set(dst, Value::from_runtime(v));
2568                    pc += 1;
2569                }
2570                Op::DeepClone { dst, src } => {
2571                    let v = self.reg(src).as_runtime().deep_clone();
2572                    self.set(dst, Value::from_runtime(v));
2573                    pc += 1;
2574                }
2575                Op::NewTuple { dst, start, count } => {
2576                    use crate::interpreter::RuntimeValue;
2577                    let mut items = Vec::with_capacity(count as usize);
2578                    for k in 0..count {
2579                        items.push(self.reg(start + k).as_runtime().clone());
2580                    }
2581                    self.set(
2582                        dst,
2583                        Value::from_runtime(RuntimeValue::Tuple(std::rc::Rc::new(items))),
2584                    );
2585                    pc += 1;
2586                }
2587                Op::UnionOp { dst, lhs, rhs } => {
2588                    let v = crate::semantics::collections::union(
2589                        &self.reg(lhs).as_runtime(),
2590                        &self.reg(rhs).as_runtime(),
2591                    )?;
2592                    self.set(dst, Value::from_runtime(v));
2593                    pc += 1;
2594                }
2595                Op::IntersectOp { dst, lhs, rhs } => {
2596                    let v = crate::semantics::collections::intersection(
2597                        &self.reg(lhs).as_runtime(),
2598                        &self.reg(rhs).as_runtime(),
2599                    )?;
2600                    self.set(dst, Value::from_runtime(v));
2601                    pc += 1;
2602                }
2603                Op::LoadToday { dst } => {
2604                    self.set(dst, Value::from_runtime(crate::semantics::temporal::today()));
2605                    pc += 1;
2606                }
2607                Op::LoadNow { dst } => {
2608                    self.set(dst, Value::from_runtime(crate::semantics::temporal::now()));
2609                    pc += 1;
2610                }
2611                Op::NewStruct { dst, type_name } => {
2612                    use crate::interpreter::{RuntimeValue, StructValue};
2613                    let name = match &self.program.constants[type_name as usize] {
2614                        Constant::Text(s) => s.clone(),
2615                        other => return Err(format!("vm: NewStruct name is not Text: {:?}", other)),
2616                    };
2617                    self.set(
2618                        dst,
2619                        Value::from_runtime(RuntimeValue::Struct(Box::new(StructValue {
2620                            type_name: name,
2621                            fields: std::collections::HashMap::new(),
2622                        }))),
2623                    );
2624                    pc += 1;
2625                }
2626                Op::StructInsert { obj, field, value } => {
2627                    use crate::interpreter::RuntimeValue;
2628                    let field_name = match &self.program.constants[field as usize] {
2629                        Constant::Text(s) => s.clone(),
2630                        other => return Err(format!("vm: field name is not Text: {:?}", other)),
2631                    };
2632                    let v = self.reg(value).as_runtime().clone();
2633                    match self.reg_mut(obj).as_runtime_mut() {
2634                        RuntimeValue::Struct(s) => {
2635                            s.fields.insert(field_name, v);
2636                        }
2637                        _ => return Err("Cannot set field on non-struct value".to_string()),
2638                    }
2639                    pc += 1;
2640                }
2641                Op::GetField { dst, obj, field } => {
2642                    use crate::interpreter::RuntimeValue;
2643                    let field_name = match &self.program.constants[field as usize] {
2644                        Constant::Text(s) => s.as_str(),
2645                        other => return Err(format!("vm: field name is not Text: {:?}", other)),
2646                    };
2647                    let v = match &*self.reg(obj).as_runtime() {
2648                        RuntimeValue::Struct(s) => s
2649                            .fields
2650                            .get(field_name)
2651                            .cloned()
2652                            .ok_or_else(|| format!("Field '{}' not found", field_name))?,
2653                        other => {
2654                            return Err(format!("Cannot access field on {}", other.type_name()));
2655                        }
2656                    };
2657                    self.set(dst, Value::from_runtime(v));
2658                    pc += 1;
2659                }
2660                Op::NewInductive { dst, type_name, ctor, args_start, count } => {
2661                    use crate::interpreter::{InductiveValue, RuntimeValue};
2662                    let inductive_type = match &self.program.constants[type_name as usize] {
2663                        Constant::Text(s) => s.clone(),
2664                        other => return Err(format!("vm: enum name is not Text: {:?}", other)),
2665                    };
2666                    let constructor = match &self.program.constants[ctor as usize] {
2667                        Constant::Text(s) => s.clone(),
2668                        other => return Err(format!("vm: variant name is not Text: {:?}", other)),
2669                    };
2670                    let mut args = Vec::with_capacity(count as usize);
2671                    for k in 0..count {
2672                        args.push(self.reg(args_start + k).as_runtime().clone());
2673                    }
2674                    self.set(
2675                        dst,
2676                        Value::from_runtime(RuntimeValue::Inductive(Box::new(InductiveValue {
2677                            inductive_type,
2678                            constructor,
2679                            args,
2680                        }))),
2681                    );
2682                    pc += 1;
2683                }
2684                Op::TestArm { dst, target, variant } => {
2685                    use crate::interpreter::RuntimeValue;
2686                    let variant_name = match &self.program.constants[variant as usize] {
2687                        Constant::Text(s) => s.as_str(),
2688                        other => return Err(format!("vm: variant name is not Text: {:?}", other)),
2689                    };
2690                    let matched = match &*self.reg(target).as_runtime() {
2691                        RuntimeValue::Struct(s) => s.type_name == variant_name,
2692                        RuntimeValue::Inductive(ind) => ind.constructor == variant_name,
2693                        _ => false,
2694                    };
2695                    self.set(dst, Value::bool(matched));
2696                    pc += 1;
2697                }
2698                Op::BindArm { dst, target, field, index } => {
2699                    use crate::interpreter::RuntimeValue;
2700                    let v = match &*self.reg(target).as_runtime() {
2701                        RuntimeValue::Struct(s) => {
2702                            let field_name = match &self.program.constants[field as usize] {
2703                                Constant::Text(s) => s.as_str(),
2704                                other => {
2705                                    return Err(format!("vm: field name is not Text: {:?}", other));
2706                                }
2707                            };
2708                            s.fields.get(field_name).cloned()
2709                        }
2710                        RuntimeValue::Inductive(ind) => ind.args.get(index as usize).cloned(),
2711                        _ => None,
2712                    };
2713                    if let Some(v) = v {
2714                        self.set(dst, Value::from_runtime(v));
2715                    }
2716                    pc += 1;
2717                }
2718                Op::CrdtBump { obj, field, amount, negate } => {
2719                    use crate::interpreter::RuntimeValue;
2720                    let amount_int = match &*self.reg(amount).as_runtime() {
2721                        RuntimeValue::Int(n) => *n,
2722                        _ => {
2723                            return Err(if negate {
2724                                "CRDT decrement amount must be an integer".to_string()
2725                            } else {
2726                                "CRDT increment amount must be an integer".to_string()
2727                            });
2728                        }
2729                    };
2730                    let amount_int = if negate { amount_int.wrapping_neg() } else { amount_int };
2731                    let field_name = match &self.program.constants[field as usize] {
2732                        Constant::Text(s) => s.clone(),
2733                        other => return Err(format!("vm: field name is not Text: {:?}", other)),
2734                    };
2735                    match self.reg_mut(obj).as_runtime_mut() {
2736                        RuntimeValue::Struct(s) => {
2737                            let current =
2738                                s.fields.get(&field_name).cloned().unwrap_or(RuntimeValue::Int(0));
2739                            let new_val = crate::semantics::arith::crdt_counter_bump(
2740                                current,
2741                                amount_int,
2742                                &field_name,
2743                            )?;
2744                            s.fields.insert(field_name, new_val);
2745                        }
2746                        _ => {
2747                            return Err(if negate {
2748                                "Cannot decrease field on non-struct value".to_string()
2749                            } else {
2750                                "Cannot increase field on non-struct value".to_string()
2751                            });
2752                        }
2753                    }
2754                    pc += 1;
2755                }
2756                Op::CrdtMerge { target, source } => {
2757                    use crate::interpreter::RuntimeValue;
2758                    let source_fields = match &*self.reg(source).as_runtime() {
2759                        RuntimeValue::Struct(s) => s.fields.clone(),
2760                        _ => return Err("Merge source must be a struct".to_string()),
2761                    };
2762                    match self.reg_mut(target).as_runtime_mut() {
2763                        RuntimeValue::Struct(s) => {
2764                            for (field_name, incoming) in source_fields {
2765                                let current =
2766                                    s.fields.get(&field_name).cloned().unwrap_or(RuntimeValue::Int(0));
2767                                let merged =
2768                                    crate::semantics::arith::crdt_merge_field(&current, incoming);
2769                                s.fields.insert(field_name, merged);
2770                            }
2771                        }
2772                        _ => return Err("Merge target must be a struct".to_string()),
2773                    }
2774                    pc += 1;
2775                }
2776                Op::NewCrdt { dst, kind } => {
2777                    use crate::interpreter::RuntimeValue;
2778                    use crate::semantics::crdt::{next_replica_id, CrdtValue};
2779                    let cv = match kind {
2780                        0 => CrdtValue::new_set(next_replica_id()),
2781                        1 => CrdtValue::new_seq(next_replica_id()),
2782                        3 => CrdtValue::new_set_remove_wins(next_replica_id()),
2783                        _ => CrdtValue::new_register(next_replica_id()),
2784                    };
2785                    self.set(
2786                        dst,
2787                        Value::from_runtime(RuntimeValue::Crdt(std::rc::Rc::new(
2788                            std::cell::RefCell::new(cv),
2789                        ))),
2790                    );
2791                    pc += 1;
2792                }
2793                Op::CrdtAppend { seq, value } => {
2794                    use crate::interpreter::RuntimeValue;
2795                    let v = self.reg(value).as_runtime().clone();
2796                    let seq_rt = self.reg(seq).as_runtime();
2797                    match &*seq_rt {
2798                        RuntimeValue::Crdt(rc) => rc.borrow_mut().append(&v)?,
2799                        RuntimeValue::List(_) => {
2800                            crate::semantics::collections::list_push(&seq_rt, v)?
2801                        }
2802                        other => return Err(format!("Cannot append to {}", other.type_name())),
2803                    }
2804                    pc += 1;
2805                }
2806                Op::CrdtResolve { obj, field, value } => {
2807                    use crate::interpreter::RuntimeValue;
2808                    let v = self.reg(value).as_runtime().clone();
2809                    let field_name = match &self.program.constants[field as usize] {
2810                        Constant::Text(s) => s.clone(),
2811                        other => return Err(format!("vm: field name is not Text: {:?}", other)),
2812                    };
2813                    match self.reg_mut(obj).as_runtime_mut() {
2814                        RuntimeValue::Struct(s) => {
2815                            // A real divergent register resolves in place via its shared
2816                            // `Rc`; a plain field is overwritten — same fallback as the
2817                            // tree-walker's `Resolve`.
2818                            let is_register =
2819                                matches!(s.fields.get(&field_name), Some(RuntimeValue::Crdt(_)));
2820                            if is_register {
2821                                if let Some(RuntimeValue::Crdt(rc)) = s.fields.get(&field_name) {
2822                                    rc.borrow_mut().resolve(&v)?;
2823                                }
2824                            } else {
2825                                s.fields.insert(field_name, v);
2826                            }
2827                        }
2828                        other => {
2829                            return Err(format!("Cannot resolve a field on {}", other.type_name()))
2830                        }
2831                    }
2832                    pc += 1;
2833                }
2834                Op::IterPrepare { iterable } => {
2835                    let items = crate::semantics::collections::iteration_snapshot(
2836                        &self.reg(iterable).as_runtime(),
2837                    )?;
2838                    self.iter_stack
2839                        .push((items.into_iter().map(Value::from_runtime).collect(), 0));
2840                    pc += 1;
2841                }
2842                Op::IterNext { dst, exit } => {
2843                    let (items, idx) = self
2844                        .iter_stack
2845                        .last_mut()
2846                        .ok_or("vm: IterNext with no live iterator")?;
2847                    if *idx < items.len() {
2848                        let v = items[*idx].clone();
2849                        *idx += 1;
2850                        self.set(dst, v);
2851                        pc += 1;
2852                    } else {
2853                        pc = exit;
2854                    }
2855                }
2856                Op::IterPop => {
2857                    self.iter_stack.pop().ok_or("vm: IterPop with no live iterator")?;
2858                    pc += 1;
2859                }
2860                Op::ListPop { list, dst } => {
2861                    self.ensure_reg_owned(list, call_stack.last().map(|f| f.func));
2862                    let v = crate::semantics::collections::list_pop(&self.reg(list).as_runtime())?;
2863                    self.set(dst, Value::from_runtime(v));
2864                    pc += 1;
2865                }
2866                Op::Sleep { duration } => {
2867                    // A VM `Sleep` only ever runs inside a task (a non-concurrent program
2868                    // with `Sleep` routes to the async tree-walker, never the VM). Route it
2869                    // through the scheduler's virtual timer — the same logical-tick scale as
2870                    // a `Select` `After` arm — by yielding `VmBlock::Sleep`. Blocking on a
2871                    // real host timer here would stall the cooperative scheduler (and errors
2872                    // outright on wasm).
2873                    let ticks = self.as_ticks(duration)?;
2874                    if ticks > 0 {
2875                        return Ok(self.block(pc + 1, call_stack, VmBlock::Sleep(ticks), None));
2876                    }
2877                    pc += 1;
2878                }
2879                Op::DestructureTuple { src, start, count } => {
2880                    use crate::interpreter::RuntimeValue;
2881                    match &*self.reg(src).as_runtime() {
2882                        RuntimeValue::Tuple(items) => {
2883                            // Arity is LOUD — a silent truncation binds ghosts.
2884                            if items.len() != count as usize {
2885                                return Err(format!(
2886                                    "Cannot bind a {}-tuple to {} names",
2887                                    items.len(),
2888                                    count
2889                                ));
2890                            }
2891                            let items: Vec<Value> = items
2892                                .iter()
2893                                .take(count as usize)
2894                                .cloned()
2895                                .map(Value::from_runtime)
2896                                .collect();
2897                            for (i, v) in items.into_iter().enumerate() {
2898                                self.set(start + i as Reg, v);
2899                            }
2900                        }
2901                        other => {
2902                            return Err(format!(
2903                                "Expected tuple for pattern, got {}",
2904                                other.type_name()
2905                            ));
2906                        }
2907                    }
2908                    pc += 1;
2909                }
2910                Op::Show { src } => {
2911                    self.lines.push(self.reg(src).to_display_string());
2912                    pc += 1;
2913                }
2914                Op::Args { dst } => {
2915                    use crate::interpreter::RuntimeValue;
2916                    let items: Vec<RuntimeValue> = self
2917                        .program_args
2918                        .iter()
2919                        .map(|s| RuntimeValue::Text(std::rc::Rc::new(s.clone())))
2920                        .collect();
2921                    let list = RuntimeValue::List(std::rc::Rc::new(std::cell::RefCell::new(
2922                        crate::interpreter::ListRepr::from_values(items),
2923                    )));
2924                    self.set(dst, Value::from_runtime(list));
2925                    pc += 1;
2926                }
2927                // Go-like concurrency (T11). Each op materialises its operands,
2928                // then suspends the slice — `self.block` saves the resume point
2929                // and the request; the scheduler driver services it and re-enters.
2930                Op::ChanNew { dst, cap, .. } => {
2931                    let capacity = if cap < 0 { None } else { Some(cap as usize) };
2932                    return Ok(self.block(pc + 1, call_stack, VmBlock::NewChan(capacity), Some(dst)));
2933                }
2934                Op::ChanSend { chan, val } => {
2935                    let ch = self.as_chan(chan)?;
2936                    let payload = self.materialize_reg(val)?;
2937                    return Ok(self.block(pc + 1, call_stack, VmBlock::Send(ch, payload), None));
2938                }
2939                Op::ChanRecv { dst, chan } => {
2940                    let ch = self.as_chan(chan)?;
2941                    return Ok(self.block(pc + 1, call_stack, VmBlock::Recv(ch), Some(dst)));
2942                }
2943                Op::ChanTrySend { dst, chan, val } => {
2944                    let ch = self.as_chan(chan)?;
2945                    let payload = self.materialize_reg(val)?;
2946                    return Ok(self.block(pc + 1, call_stack, VmBlock::TrySend(ch, payload), Some(dst)));
2947                }
2948                Op::ChanTryRecv { dst, chan } => {
2949                    let ch = self.as_chan(chan)?;
2950                    return Ok(self.block(pc + 1, call_stack, VmBlock::TryRecv(ch), Some(dst)));
2951                }
2952                Op::ChanClose { chan } => {
2953                    let ch = self.as_chan(chan)?;
2954                    return Ok(self.block(pc + 1, call_stack, VmBlock::Close(ch), None));
2955                }
2956                Op::TaskAwait { dst, handle } => {
2957                    let tid = self.as_task(handle)?;
2958                    return Ok(self.block(pc + 1, call_stack, VmBlock::Await(tid), Some(dst)));
2959                }
2960                Op::TaskAbort { handle } => {
2961                    let tid = self.as_task(handle)?;
2962                    return Ok(self.block(pc + 1, call_stack, VmBlock::Abort(tid), None));
2963                }
2964                Op::Spawn { func, args_start, arg_count } => {
2965                    let args = self.materialize_args(args_start, arg_count)?;
2966                    let req = VmBlock::SpawnDesc { func, args, want_handle: false };
2967                    return Ok(self.block(pc + 1, call_stack, req, None));
2968                }
2969                Op::SpawnHandle { dst, func, args_start, arg_count } => {
2970                    let args = self.materialize_args(args_start, arg_count)?;
2971                    let req = VmBlock::SpawnDesc { func, args, want_handle: true };
2972                    return Ok(self.block(pc + 1, call_stack, req, Some(dst)));
2973                }
2974                Op::SelectArmRecv { chan, var } => {
2975                    let ch = self.as_chan(chan)?;
2976                    self.select_pending.push((SelectArm::Recv(ch), Some(var)));
2977                    pc += 1;
2978                }
2979                Op::SelectArmTimeout { ticks } => {
2980                    let t = self.as_ticks(ticks)?;
2981                    self.select_pending.push((SelectArm::Timeout(t), None));
2982                    pc += 1;
2983                }
2984                Op::SelectWait { dst_arm } => {
2985                    let arms: Vec<SelectArm> =
2986                        self.select_pending.iter().map(|(a, _)| a.clone()).collect();
2987                    return Ok(self.block(pc + 1, call_stack, VmBlock::Select(arms), Some(dst_arm)));
2988                }
2989                // Peer networking: materialise the operands and suspend; the async VM driver
2990                // services the request through the shared `NetInbox` (the same inbox the
2991                // tree-walker uses) and resumes — `NetAwait` resumes with the received value.
2992                Op::NetConnect { url } => {
2993                    let u = self.materialize_reg(url)?;
2994                    return Ok(self.block(pc + 1, call_stack, VmBlock::NetConnect(u), None));
2995                }
2996                Op::NetListen { topic } => {
2997                    let t = self.materialize_reg(topic)?;
2998                    return Ok(self.block(pc + 1, call_stack, VmBlock::NetListen(t), None));
2999                }
3000                Op::NetSend { to, msg } => {
3001                    let t = self.materialize_reg(to)?;
3002                    let m = self.materialize_reg(msg)?;
3003                    return Ok(self.block(pc + 1, call_stack, VmBlock::NetSend(t, m), None));
3004                }
3005                Op::NetStream { to, values } => {
3006                    let t = self.materialize_reg(to)?;
3007                    let v = self.materialize_reg(values)?;
3008                    return Ok(self.block(pc + 1, call_stack, VmBlock::NetStream(t, v), None));
3009                }
3010                Op::NetAwait { dst, from, stream } => {
3011                    let f = self.materialize_reg(from)?;
3012                    return Ok(self.block(pc + 1, call_stack, VmBlock::NetAwait(f, stream), Some(dst)));
3013                }
3014                Op::NetMakePeer { dst, addr } => {
3015                    let a = self.materialize_reg(addr)?;
3016                    return Ok(self.block(pc + 1, call_stack, VmBlock::NetMakePeer(a), Some(dst)));
3017                }
3018                Op::NetSync { dst, topic } => {
3019                    let current = self.materialize_reg(dst)?;
3020                    let t = self.materialize_reg(topic)?;
3021                    return Ok(self.block(pc + 1, call_stack, VmBlock::NetSync(t, current), Some(dst)));
3022                }
3023                Op::FailWith { msg } => {
3024                    return Err(match &self.program.constants[msg as usize] {
3025                        Constant::Text(s) => s.clone(),
3026                        other => format!("vm: FailWith constant is not Text: {:?}", other),
3027                    });
3028                }
3029                Op::Halt => break,
3030            }
3031        }
3032        Ok(VmStep::Done(crate::interpreter::RuntimeValue::Nothing))
3033    }
3034
3035    #[inline]
3036    fn reg(&self, r: Reg) -> &Value {
3037        &self.registers[self.base + r as usize]
3038    }
3039
3040    #[inline]
3041    fn set(&mut self, r: Reg, v: Value) {
3042        let slot = self.base + r as usize;
3043        self.registers[slot] = v;
3044    }
3045
3046    #[inline]
3047    fn reg_mut(&mut self, r: Reg) -> &mut Value {
3048        let slot = self.base + r as usize;
3049        &mut self.registers[slot]
3050    }
3051
3052    // ─── Scheduler-driver hooks (T11) ───────────────────────────────────────
3053
3054    /// Save the suspended `pc` + call stack and the request; the slice returns
3055    /// [`VmStep::Blocked`]. `slot` is the register the resume value lands in.
3056    fn block(
3057        &mut self,
3058        resume_pc: usize,
3059        call_stack: Vec<CallFrame>,
3060        req: VmBlock,
3061        slot: Option<Reg>,
3062    ) -> VmStep {
3063        self.sched_pc = resume_pc;
3064        self.sched_call_stack = call_stack;
3065        self.sched_active = true;
3066        self.pending = Some(req);
3067        self.resume_slot = slot;
3068        VmStep::Blocked
3069    }
3070
3071    // ─── Debug-drawer hooks (single-task, bytecode tier) ─────────────────────
3072
3073    /// Build a [`DebugView`] of the paused VM: the call frames with their register
3074    /// values, the globals, the output, and the program counter (the op about to
3075    /// execute). Reconstructs each frame's register window from the saved call
3076    /// stack — Main has base 0; frame `k`'s base is the next frame's `caller_base`
3077    /// (the innermost is the live `self.base`).
3078    pub(crate) fn debug_view(&self) -> DebugView {
3079        let prog = self.program;
3080        let cs = &self.sched_call_stack;
3081        let mut frames = Vec::with_capacity(cs.len() + 1);
3082        frames.push(self.frame_view(None, 0, prog.register_count));
3083        for (k, fr) in cs.iter().enumerate() {
3084            let base = cs.get(k + 1).map(|n| n.caller_base).unwrap_or(self.base);
3085            let count = prog.functions.get(fr.func as usize).map(|f| f.register_count).unwrap_or(0);
3086            frames.push(self.frame_view(Some(fr.func), base, count));
3087        }
3088        let globals = prog
3089            .globals
3090            .iter()
3091            .enumerate()
3092            .filter_map(|(i, name)| {
3093                self.globals
3094                    .get(i)
3095                    .and_then(|o| o.as_ref())
3096                    .map(|v| (name.clone(), v.to_display_string()))
3097            })
3098            .collect();
3099        DebugView {
3100            pc: self.sched_pc,
3101            current_func: cs.last().map(|f| f.func),
3102            frames,
3103            globals,
3104            output: self.lines.clone(),
3105        }
3106    }
3107
3108    fn frame_view(&self, func: Option<u16>, base: usize, count: usize) -> DebugFrameView {
3109        let registers = (0..count)
3110            .filter_map(|i| {
3111                self.registers.get(base + i).map(|v| {
3112                    // `as_runtime` (not `as_runtime_ref`) so inline scalars report their
3113                    // type too — under the narrow-value repr `as_runtime_ref` is `None` for
3114                    // an inline Int/Float/Bool, but the type is still well-defined.
3115                    let kind = v.as_runtime().type_name().to_string();
3116                    (i as u16, kind, v.to_display_string())
3117                })
3118            })
3119            .collect();
3120        DebugFrameView { func, base, registers }
3121    }
3122
3123    /// Walk the roots (the current frame's registers + the globals) and collect the
3124    /// distinct heap objects they reach, recording each object's reference count and
3125    /// every root that points at it — so a shared list shows up once with two
3126    /// referrers (the `let b be a` aliasing that trips up every beginner).
3127    pub(crate) fn debug_heap(&self) -> Vec<HeapObjView> {
3128        let prog = self.program;
3129        let (count, is_main) = match self.sched_call_stack.last() {
3130            Some(fr) => (
3131                prog.functions.get(fr.func as usize).map(|f| f.register_count).unwrap_or(0),
3132                false,
3133            ),
3134            None => (prog.register_count, true),
3135        };
3136        let name_of = |i: usize| -> String {
3137            if is_main {
3138                prog.reg_names
3139                    .iter()
3140                    .find(|(r, _)| *r as usize == i)
3141                    .map(|(_, n)| n.clone())
3142                    .unwrap_or_else(|| format!("R{i}"))
3143            } else {
3144                format!("R{i}")
3145            }
3146        };
3147        let mut objs: Vec<HeapObjView> = Vec::new();
3148        let mut add = |v: &Value, root: String, objs: &mut Vec<HeapObjView>| {
3149            if let Some((id, kind, rc, storage)) = heap_identity(v) {
3150                match objs.iter_mut().find(|o| o.id == id) {
3151                    Some(o) => {
3152                        if !o.referenced_by.contains(&root) {
3153                            o.referenced_by.push(root);
3154                        }
3155                    }
3156                    None => objs.push(HeapObjView {
3157                        id,
3158                        kind,
3159                        summary: v.to_display_string(),
3160                        storage,
3161                        rc,
3162                        referenced_by: vec![root],
3163                    }),
3164                }
3165            }
3166        };
3167        for i in 0..count {
3168            if let Some(v) = self.registers.get(self.base + i) {
3169                add(v, name_of(i), &mut objs);
3170            }
3171        }
3172        for (i, name) in prog.globals.iter().enumerate() {
3173            if let Some(Some(v)) = self.globals.get(i) {
3174                add(v, name.clone(), &mut objs);
3175            }
3176        }
3177        objs
3178    }
3179
3180    /// Clone the resumable execution state so the debugger can carry it between
3181    /// steps (it rebuilds the VM each step — see [`DebugVmState`]).
3182    pub(crate) fn save_debug_state(&self) -> DebugVmState {
3183        DebugVmState {
3184            registers: self.registers.clone(),
3185            base: self.base,
3186            globals: self.globals.clone(),
3187            lines: self.lines.clone(),
3188            iter_stack: self.iter_stack.clone(),
3189            sched_active: self.sched_active,
3190            sched_pc: self.sched_pc,
3191            sched_call_stack: self.sched_call_stack.clone(),
3192        }
3193    }
3194
3195    /// Restore a snapshot taken by [`Vm::save_debug_state`].
3196    pub(crate) fn restore_debug_state(&mut self, st: DebugVmState) {
3197        self.registers = st.registers;
3198        self.base = st.base;
3199        self.globals = st.globals;
3200        self.lines = st.lines;
3201        self.iter_stack = st.iter_stack;
3202        self.sched_active = st.sched_active;
3203        self.sched_pc = st.sched_pc;
3204        self.sched_call_stack = st.sched_call_stack;
3205    }
3206
3207    /// Take the pending concurrency request (the driver services it).
3208    pub(crate) fn take_pending(&mut self) -> Option<VmBlock> {
3209        self.pending.take()
3210    }
3211
3212    /// Deliver a resume value into the slot the last block reserved (if any).
3213    pub(crate) fn deliver_resume(&mut self, value: Value) {
3214        if let Some(slot) = self.resume_slot.take() {
3215            self.set(slot, value);
3216        }
3217    }
3218
3219    /// Deliver a resolved `Select`: the received value (when a recv arm won) into
3220    /// that arm's var register, and the winning arm index into the `SelectWait`'s
3221    /// destination register.
3222    pub(crate) fn deliver_select(&mut self, arm: usize, value: Value) {
3223        let var = self.select_pending.get(arm).and_then(|(_, v)| *v);
3224        if let Some(reg) = var {
3225            self.set(reg, value);
3226        }
3227        if let Some(slot) = self.resume_slot.take() {
3228            self.set(slot, Value::from_runtime(crate::interpreter::RuntimeValue::Int(arm as i64)));
3229        }
3230        self.select_pending.clear();
3231    }
3232
3233    /// Read a select-timeout register as a non-negative logical tick count.
3234    fn as_ticks(&self, r: Reg) -> Result<u64, String> {
3235        use crate::interpreter::RuntimeValue;
3236        Ok(match &*self.reg(r).as_runtime() {
3237            RuntimeValue::Int(n) => (*n).max(0) as u64,
3238            RuntimeValue::Duration(d) => (*d).max(0) as u64,
3239            RuntimeValue::Span { months, days } => {
3240                (((*months as i64) * 30 + *days as i64) * 86_400).max(0) as u64
3241            }
3242            other => {
3243                return Err(format!(
3244                    "select timeout must be a number or duration, found {}",
3245                    other.type_name()
3246                ))
3247            }
3248        })
3249    }
3250
3251    /// Read a channel handle from register `r`.
3252    fn as_chan(&self, r: Reg) -> Result<ChanId, String> {
3253        match &*self.reg(r).as_runtime() {
3254            crate::interpreter::RuntimeValue::Chan(id) => Ok(*id),
3255            other => Err(format!("expected a channel, found {}", other.type_name())),
3256        }
3257    }
3258
3259    /// Read a task handle from register `r`.
3260    fn as_task(&self, r: Reg) -> Result<TaskId, String> {
3261        match &*self.reg(r).as_runtime() {
3262            crate::interpreter::RuntimeValue::TaskHandle(id) => Ok(*id),
3263            other => Err(format!("expected a task handle, found {}", other.type_name())),
3264        }
3265    }
3266
3267    /// Materialise register `r`'s value into a Send-able payload for a channel.
3268    fn materialize_reg(&self, r: Reg) -> Result<RtPayload, String> {
3269        let rt = self.reg(r).as_runtime();
3270        crate::concurrency::marshal::materialize(&rt)
3271            .map_err(|e| format!("cannot send value through a channel: {:?}", e))
3272    }
3273
3274    /// Materialise a contiguous register window `[args_start, args_start+count)`
3275    /// into `Send`-able payloads — the spawn arguments that cross to the child
3276    /// task (and, under work-stealing, to its worker thread).
3277    fn materialize_args(&self, args_start: Reg, arg_count: u16) -> Result<Vec<RtPayload>, String> {
3278        (0..arg_count as Reg).map(|i| self.materialize_reg(args_start + i)).collect()
3279    }
3280
3281    /// Install the spawn entry-state for `functions[func](args)` into THIS VM:
3282    /// rebuild the payload args into a base-0 register window, push a sentinel
3283    /// frame so the body's `Return` terminates the task cleanly (result in
3284    /// register 0), and arm the scheduler `pc` at the function's `entry_pc`.
3285    ///
3286    /// Shared by both drivers: the cooperative one calls it on a freshly-cloned
3287    /// child (see [`Vm::spawn_task_vm`]); a work-stealing worker calls it on a VM
3288    /// built locally over its own borrow of the program.
3289    pub(crate) fn setup_task(&mut self, func: FuncIdx, args: &[RtPayload]) {
3290        let (entry_pc, reg_count) = {
3291            let f = &self.program.functions[func as usize];
3292            (f.entry_pc, f.register_count)
3293        };
3294        if self.registers.len() < reg_count {
3295            self.registers.resize(reg_count, Value::nothing());
3296        }
3297        for (i, a) in args.iter().enumerate() {
3298            self.registers[i] = Value::from_runtime(crate::concurrency::marshal::rebuild(a.clone()));
3299        }
3300        let restore_len = self.registers.len();
3301        self.sched_call_stack = vec![CallFrame {
3302            return_pc: self.program.code.len(),
3303            return_reg: 0,
3304            caller_base: 0,
3305            restore_len,
3306            iter_depth: 0,
3307            func,
3308            arg_lo: 0,
3309            arg_count: 0,
3310        }];
3311        self.sched_active = true;
3312        self.sched_pc = entry_pc;
3313    }
3314
3315    /// Build a fresh child VM that runs `functions[func](args)` — a spawned task —
3316    /// sharing this VM's `&'p program` and run context (tier, policy, program
3317    /// args). Used by the cooperative driver, which builds children inline.
3318    pub(crate) fn spawn_task_vm(&self, func: FuncIdx, args: &[RtPayload]) -> Vm<'p> {
3319        let mut child = Vm::new(self.program);
3320        child.policy_ctx = self.policy_ctx;
3321        child.tier = self.tier;
3322        child.program_args = self.program_args.clone();
3323        child.setup_task(func, args);
3324        child
3325    }
3326
3327    fn binop(
3328        &mut self,
3329        dst: Reg,
3330        lhs: Reg,
3331        rhs: Reg,
3332        f: impl Fn(&Value, &Value) -> Result<Value, String>,
3333    ) -> Result<(), String> {
3334        let v = f(self.reg(lhs), self.reg(rhs))?;
3335        self.set(dst, v);
3336        Ok(())
3337    }
3338
3339    /// `R[dst] = R[dst] + R[src]`, extending in place when `R[dst]` is a
3340    /// Text this register exclusively owns (`Rc` count 1 — no alias, capture
3341    /// snapshot, or iterator can observe the mutation). The two in-place arms
3342    /// transcribe the kernel's `(Text, Text)` / `(Text, other)` add rules;
3343    /// every other shape — shared Rc included — takes the kernel itself.
3344    fn add_assign(&mut self, dst: Reg, src: Reg) -> Result<(), String> {
3345        use crate::interpreter::RuntimeValue;
3346        use std::rc::Rc;
3347        let di = self.base + dst as usize;
3348        let si = self.base + src as usize;
3349        if di != si {
3350            let (a, b) = if di < si {
3351                let (lo, hi) = self.registers.split_at_mut(si);
3352                (&mut lo[di], &hi[0])
3353            } else {
3354                let (lo, hi) = self.registers.split_at_mut(di);
3355                (&mut hi[0], &lo[si])
3356            };
3357            // Only the heap Text arm takes the in-place fast path; peek without
3358            // promoting (`as_runtime_mut` would box an inline scalar) so the
3359            // common non-Text case stays inline and falls to the kernel below.
3360            if matches!(a.as_runtime_ref(), Some(RuntimeValue::Text(_))) {
3361                if let RuntimeValue::Text(rc) = a.as_runtime_mut() {
3362                    if let Some(s) = Rc::get_mut(rc) {
3363                        match &*b.as_runtime() {
3364                            RuntimeValue::Text(t) => s.push_str(t),
3365                            other => s.push_str(&other.to_display_string()),
3366                        }
3367                        return Ok(());
3368                    }
3369                }
3370            }
3371        }
3372        let v = self.reg(dst).add(self.reg(src))?;
3373        self.set(dst, v);
3374        Ok(())
3375    }
3376}
3377
3378fn const_to_value(c: &Constant) -> Value {
3379    use crate::interpreter::RuntimeValue;
3380    match c {
3381        Constant::Int(n) => Value::int(*n),
3382        Constant::Float(f) => Value::float(*f),
3383        Constant::Bool(b) => Value::bool(*b),
3384        Constant::Text(s) => Value::text(s.clone()),
3385        Constant::Char(c) => Value::from_runtime(RuntimeValue::Char(*c)),
3386        Constant::Nothing => Value::nothing(),
3387        Constant::Duration(n) => Value::from_runtime(RuntimeValue::Duration(*n)),
3388        Constant::Date(d) => Value::from_runtime(RuntimeValue::Date(*d)),
3389        Constant::Moment(n) => Value::from_runtime(RuntimeValue::Moment(*n)),
3390        Constant::Span { months, days } => {
3391            Value::from_runtime(RuntimeValue::Span { months: *months, days: *days })
3392        }
3393        Constant::Time(n) => Value::from_runtime(RuntimeValue::Time(*n)),
3394    }
3395}
3396
3397#[cfg(test)]
3398mod string_build_fastpath {
3399    //! Structural proof for Task B: the constant pool is materialised ONCE
3400    //! (a `LoadConst` of a Text bumps a shared `Rc` instead of allocating a
3401    //! fresh `String`), and the sole-owned in-place append (`AddAssign`)
3402    //! grows the accumulator's own buffer rather than reallocating each step.
3403    use super::*;
3404    use crate::interpreter::RuntimeValue;
3405    use std::rc::Rc;
3406
3407    /// The `Rc<String>` backing register `r` (or panic if it isn't a Text).
3408    fn text_ptr(vm: &Vm, r: usize) -> *const String {
3409        match vm.registers[r].as_runtime_ref() {
3410            Some(RuntimeValue::Text(rc)) => Rc::as_ptr(rc),
3411            other => panic!("register {r} is not a Text: {other:?}"),
3412        }
3413    }
3414
3415    fn text_strong(vm: &Vm, r: usize) -> usize {
3416        match vm.registers[r].as_runtime_ref() {
3417            Some(RuntimeValue::Text(rc)) => Rc::strong_count(rc),
3418            other => panic!("register {r} is not a Text: {other:?}"),
3419        }
3420    }
3421
3422    /// Two `LoadConst`s of the SAME Text-constant index hand out the same
3423    /// `Rc` allocation — proof the pool is materialised once, so reloading a
3424    /// literal in a loop is a refcount bump, not a `String` allocation.
3425    #[test]
3426    fn loadconst_text_shares_one_allocation() {
3427        let prog = CompiledProgram {
3428            constants: vec![Constant::Text("abc".to_string())],
3429            code: vec![
3430                Op::LoadConst { dst: 0, idx: 0 },
3431                Op::LoadConst { dst: 1, idx: 0 },
3432                Op::Halt,
3433            ],
3434            register_count: 2,
3435            ..Default::default()
3436        };
3437        let mut vm = Vm::new(&prog);
3438        vm.run().unwrap();
3439        assert_eq!(
3440            text_ptr(&vm, 0),
3441            text_ptr(&vm, 1),
3442            "two loads of the same Text constant must share one Rc allocation"
3443        );
3444        // And the live constant keeps a reference, so a register's clone is
3445        // never the sole owner — an in-place append on a freshly-loaded
3446        // literal must therefore NOT fire (it would corrupt the pool).
3447        assert!(text_strong(&vm, 0) >= 3, "pool + two registers all reference the constant");
3448    }
3449
3450    /// `Set s to s + ch` repeated: once the accumulator owns its buffer, each
3451    /// append reuses the SAME allocation (pointer stable while capacity holds).
3452    /// The first append, where `s` is the just-loaded shared `""` constant,
3453    /// must allocate a fresh owned buffer (the pool stays intact); subsequent
3454    /// appends grow it in place.
3455    #[test]
3456    fn add_assign_appends_in_place_after_first() {
3457        // r0 = "" (shared constant), r1 = "x" (shared constant).
3458        // r0 += r1, capture; r0 += r1 a few more times, pointer stays put.
3459        let prog = CompiledProgram {
3460            constants: vec![Constant::Text(String::new()), Constant::Text("x".to_string())],
3461            code: vec![
3462                Op::LoadConst { dst: 0, idx: 0 },
3463                Op::LoadConst { dst: 1, idx: 1 },
3464                Op::AddAssign { dst: 0, src: 1 },
3465                Op::AddAssign { dst: 0, src: 1 },
3466                Op::AddAssign { dst: 0, src: 1 },
3467                Op::AddAssign { dst: 0, src: 1 },
3468                Op::Halt,
3469            ],
3470            register_count: 2,
3471            ..Default::default()
3472        };
3473        // Run only the first two appends to capture the owned buffer, then the
3474        // rest, by stepping the whole thing and re-checking — simplest: run to
3475        // completion, the invariant we assert is the FINAL value's correctness
3476        // plus that the rhs constant was never mutated.
3477        let mut vm = Vm::new(&prog);
3478        vm.run().unwrap();
3479        // The accumulator built "xxxx".
3480        match vm.registers[0].as_runtime_ref() {
3481            Some(RuntimeValue::Text(rc)) => assert_eq!(&***rc, "xxxx"),
3482            other => panic!("r0 not text: {other:?}"),
3483        }
3484        // The shared constant "x" was NOT corrupted by the in-place appends.
3485        match vm.registers[1].as_runtime_ref() {
3486            Some(RuntimeValue::Text(rc)) => assert_eq!(&***rc, "x"),
3487            other => panic!("r1 not text: {other:?}"),
3488        }
3489    }
3490
3491    /// The buffer-stability proof: drive the loop manually so we can read the
3492    /// accumulator's `Rc::as_ptr` between appends. After the first append
3493    /// (which clones off the shared constant into an owned buffer with spare
3494    /// capacity), every later append keeps the SAME allocation.
3495    #[test]
3496    fn add_assign_reuses_buffer_allocation() {
3497        let prog = CompiledProgram {
3498            constants: vec![Constant::Text("seed-with-capacity-headroom".to_string()), Constant::Text("z".to_string())],
3499            code: vec![
3500                Op::LoadConst { dst: 0, idx: 0 },
3501                Op::LoadConst { dst: 1, idx: 1 },
3502                Op::AddAssign { dst: 0, src: 1 }, // first: clone off the shared constant
3503                Op::AddAssign { dst: 0, src: 1 }, // owned now → in place
3504                Op::AddAssign { dst: 0, src: 1 },
3505                Op::Halt,
3506            ],
3507            register_count: 2,
3508            ..Default::default()
3509        };
3510        // Manually dispatch up to the marker so we can inspect between appends.
3511        // Reserve generous capacity on the first owned buffer so growth never
3512        // forces a realloc within this test.
3513        let mut vm = Vm::new(&prog);
3514        // Step 1+2: loads.
3515        vm.set(0, const_to_value(&prog.constants[0]));
3516        vm.set(1, const_to_value(&prog.constants[1]));
3517        // First append: must produce an OWNED buffer (sole owner now).
3518        vm.add_assign(0, 1).unwrap();
3519        // Force headroom so the next appends don't realloc for capacity.
3520        if let RuntimeValue::Text(rc) = vm.registers[0].as_runtime_mut() {
3521            if let Some(s) = Rc::get_mut(rc) {
3522                s.reserve(64);
3523            }
3524        }
3525        let p_after_first = text_ptr(&vm, 0);
3526        vm.add_assign(0, 1).unwrap();
3527        let p_after_second = text_ptr(&vm, 0);
3528        vm.add_assign(0, 1).unwrap();
3529        let p_after_third = text_ptr(&vm, 0);
3530        assert_eq!(p_after_first, p_after_second, "append must reuse the owned buffer");
3531        assert_eq!(p_after_second, p_after_third, "append must reuse the owned buffer");
3532        match vm.registers[0].as_runtime_ref() {
3533            Some(RuntimeValue::Text(rc)) => assert_eq!(&***rc, "seed-with-capacity-headroomzzz"),
3534            other => panic!("r0 not text: {other:?}"),
3535        }
3536    }
3537}
3538
3539#[cfg(test)]
3540mod debug_stepping {
3541    //! The debug stepper (`STEPPED = true`): `run_steps` advances exactly one op
3542    //! per call, yields `Paused` between ops, exposes the paused state via
3543    //! `debug_view`, and — the soundness invariant — produces output BYTE-IDENTICAL
3544    //! to a single-shot `run()`. This is the contract the Studio debug drawer rides.
3545    use super::*;
3546
3547    /// `Let x be 6. Let y be 7. Show x times y.` hand-lowered to bytecode.
3548    fn mul_program() -> CompiledProgram {
3549        CompiledProgram {
3550            constants: vec![Constant::Int(6), Constant::Int(7)],
3551            code: vec![
3552                Op::LoadConst { dst: 0, idx: 0 },
3553                Op::LoadConst { dst: 1, idx: 1 },
3554                Op::Mul { dst: 2, lhs: 0, rhs: 1 },
3555                Op::Show { src: 2 },
3556                Op::Halt,
3557            ],
3558            register_count: 3,
3559            ..Default::default()
3560        }
3561    }
3562
3563    #[test]
3564    fn run_steps_advances_one_op_and_pauses() {
3565        let prog = mul_program();
3566        let mut vm = Vm::new(&prog);
3567        // First op (LoadConst R0 = 6) → paused at pc 1 with R0 visible.
3568        assert!(matches!(vm.run_steps(1).unwrap(), VmStep::Paused));
3569        let v = vm.debug_view();
3570        assert_eq!(v.pc, 1, "stopped before the second instruction");
3571        assert_eq!(v.frames.len(), 1, "single Main frame");
3572        assert_eq!(v.frames[0].registers[0], (0u16, "Int".to_string(), "6".to_string()));
3573        // Second op (LoadConst R1 = 7).
3574        assert!(matches!(vm.run_steps(1).unwrap(), VmStep::Paused));
3575        let v = vm.debug_view();
3576        assert_eq!(v.pc, 2);
3577        assert_eq!(v.frames[0].registers[1], (1u16, "Int".to_string(), "7".to_string()));
3578        // Third op (Mul R2 = R0 * R1).
3579        assert!(matches!(vm.run_steps(1).unwrap(), VmStep::Paused));
3580        assert_eq!(vm.debug_view().frames[0].registers[2], (2u16, "Int".to_string(), "42".to_string()));
3581    }
3582
3583    #[test]
3584    fn stepped_run_is_byte_identical_to_single_shot() {
3585        let prog = mul_program();
3586
3587        // Stepped to completion, one op at a time.
3588        let mut stepper = Vm::new(&prog);
3589        let mut pauses = 0usize;
3590        loop {
3591            match stepper.run_steps(1).unwrap() {
3592                VmStep::Paused => pauses += 1,
3593                VmStep::Done(_) => break,
3594                VmStep::Blocked => unreachable!("no concurrency op in this program"),
3595            }
3596        }
3597        let stepped_out = stepper.into_lines();
3598
3599        // Single-shot run of the very same program.
3600        let mut oneshot = Vm::new(&prog);
3601        oneshot.run().unwrap();
3602        let oneshot_out = oneshot.into_lines();
3603
3604        assert_eq!(stepped_out, oneshot_out, "stepping must not change observable output");
3605        assert_eq!(stepped_out, vec!["42".to_string()]);
3606        assert_eq!(pauses, 4, "one pause after each of the 4 ops before Halt");
3607    }
3608
3609    #[test]
3610    fn larger_budget_runs_several_ops_then_pauses() {
3611        let prog = mul_program();
3612        let mut vm = Vm::new(&prog);
3613        // Budget of 3 runs the two loads + the Mul, then pauses at the Show (pc 3).
3614        assert!(matches!(vm.run_steps(3).unwrap(), VmStep::Paused));
3615        let v = vm.debug_view();
3616        assert_eq!(v.pc, 3);
3617        assert_eq!(v.frames[0].registers[2], (2u16, "Int".to_string(), "42".to_string()));
3618        // The rest completes.
3619        assert!(matches!(vm.run_steps(u64::MAX).unwrap(), VmStep::Done(_)));
3620        assert_eq!(vm.into_lines(), vec!["42".to_string()]);
3621    }
3622}