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 ®ion_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(®));
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(¤t, 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}