Skip to main content

lex_bytecode/
vm.rs

1//! M5: bytecode VM. Stack machine with effect dispatch through a host handler.
2
3use crate::op::*;
4use crate::program::*;
5use crate::value::{ActorCell, Value};
6use std::sync::{Arc, Mutex};
7use indexmap::IndexMap;
8use std::collections::VecDeque;
9
10#[derive(Debug, Clone, thiserror::Error)]
11pub enum VmError {
12    #[error("runtime panic: {0}")]
13    Panic(String),
14    #[error("type mismatch at runtime: {0}")]
15    TypeMismatch(String),
16    #[error("stack underflow")]
17    StackUnderflow,
18    #[error("unknown function: {0}")]
19    UnknownFunction(String),
20    #[error("effect handler error: {0}")]
21    Effect(String),
22    #[error("call stack overflow: recursion depth exceeded ({0})")]
23    CallStackOverflow(u32),
24    /// Refinement predicate failed at a call boundary (#209 slice 3).
25    /// Surfaced when a function declares `param :: Type{x | predicate}`,
26    /// the call-site arg couldn't be discharged statically (slice 2),
27    /// and the runtime evaluator finds the predicate is `false` for
28    /// the actual argument value. The `verdict` mirrors the shape of
29    /// `gate.verdict`-style records in `lex-trace`.
30    #[error("refinement violated: argument {param_index} of `{fn_name}` (binding `{binding}`): {reason}")]
31    RefinementFailed {
32        fn_name: String,
33        param_index: usize,
34        binding: String,
35        reason: String,
36    },
37}
38
39/// Maximum simultaneous call frames. Defends against unbounded
40/// recursion in agent-emitted code: a body that calls itself
41/// without a base case would otherwise blow the host's native
42/// stack and crash the process. Real Lex code rarely exceeds
43/// ~30 frames; 1024 is generous headroom while still well under
44/// the OS stack limit at any per-frame size we use.
45pub const MAX_CALL_DEPTH: u32 = 1024;
46
47/// Host-side effect dispatch. Implementors decide what `kind`/`op` mean
48/// and how arguments map to side effects.
49pub trait EffectHandler {
50    fn dispatch(&mut self, kind: &str, op: &str, args: Vec<Value>) -> Result<Value, String>;
51
52    /// Hook called by the VM at every function call so handlers can
53    /// enforce per-call budget consumption (#225). The argument is
54    /// the sum of `[budget(N)]` declared on the callee's signature;
55    /// the handler returns `Err` to refuse the call (the VM converts
56    /// to `VmError::Effect`). Default impl is a no-op so legacy
57    /// handlers and pure-only runs are unaffected.
58    fn note_call_budget(&mut self, _budget_cost: u64) -> Result<(), String> {
59        Ok(())
60    }
61
62    /// `list.par_map` worker-handler factory (#305 slice 2).
63    ///
64    /// Each parallel worker thread runs its own `Vm` and therefore
65    /// needs its own effect handler. The parent handler may opt in
66    /// to per-worker dispatch by returning `Some(handler)` here;
67    /// returning `None` (the default) keeps slice-1 behavior: the
68    /// worker runs `DenyAllEffects` and any effect call inside the
69    /// closure fails with `VmError::Effect`.
70    ///
71    /// The returned handler must be `Send` so the worker can take
72    /// ownership across a thread boundary. Shared state (budget
73    /// pool, chat registry, etc.) is wired up by the implementer.
74    /// Per-worker independence (MCP client cache, output sink)
75    /// is intentional — the alternative is mutex-serialization of
76    /// the whole effect dispatch, which would defeat the parallelism.
77    fn spawn_for_worker(&self) -> Option<Box<dyn EffectHandler + Send>> {
78        None
79    }
80}
81
82/// `Vm` exposes itself as a `ClosureCaller` so the parser interpreter
83/// can invoke user-supplied closures during a `parser.run` walk
84/// (#221). The Vm is reentrant for closure invocation: pushing a new
85/// frame onto an active call stack is supported, and the handler
86/// stays in place so any effects the closure body fires dispatch
87/// normally.
88impl<'a> crate::parser_runtime::ClosureCaller for Vm<'a> {
89    fn call_closure(&mut self, closure: Value, args: Vec<Value>) -> Result<Value, String> {
90        self.invoke_closure_value(closure, args)
91            .map_err(|e| format!("{e:?}"))
92    }
93}
94
95/// A handler that fails any effect call. Useful as a default for pure-only runs.
96pub struct DenyAllEffects;
97impl EffectHandler for DenyAllEffects {
98    fn dispatch(&mut self, kind: &str, op: &str, _args: Vec<Value>) -> Result<Value, String> {
99        Err(format!("effects not permitted (attempted {kind}.{op})"))
100    }
101}
102
103/// Trace receiver. Implementors record the call/effect tree and may
104/// substitute effect responses (for replay).
105pub trait Tracer {
106    fn enter_call(&mut self, node_id: &str, name: &str, args: &[Value]);
107    fn enter_effect(&mut self, node_id: &str, kind: &str, op: &str, args: &[Value]);
108    fn exit_ok(&mut self, value: &Value);
109    fn exit_err(&mut self, message: &str);
110    /// Tail-call optimization: pop the current frame's open call without
111    /// re-entering the parent (the new call takes its place).
112    fn exit_call_tail(&mut self);
113    /// During replay, return Some(v) to substitute an effect's output.
114    fn override_effect(&mut self, _node_id: &str) -> Option<Value> { None }
115}
116
117/// No-op tracer for normal execution.
118pub struct NullTracer;
119impl Tracer for NullTracer {
120    fn enter_call(&mut self, _: &str, _: &str, _: &[Value]) {}
121    fn enter_effect(&mut self, _: &str, _: &str, _: &str, _: &[Value]) {}
122    fn exit_ok(&mut self, _: &Value) {}
123    fn exit_err(&mut self, _: &str) {}
124    fn exit_call_tail(&mut self) {}
125}
126
127#[derive(Debug, Clone)]
128pub(crate) enum FrameKind {
129    /// Top-level entry frame; doesn't correspond to a Call opcode.
130    Entry,
131    /// Frame opened by Call/TailCall. The `String` is the originating
132    /// `NodeId`; useful for diagnostics even if currently unread.
133    Call(#[allow(dead_code)] String),
134}
135
136pub struct Vm<'a> {
137    program: &'a Program,
138    handler: Box<dyn EffectHandler + 'a>,
139    pub(crate) tracer: Box<dyn Tracer + 'a>,
140    /// Per-call frames. Each frame has its own locals array and pc.
141    frames: Vec<Frame>,
142    stack: Vec<Value>,
143    /// Soft cap to avoid runaway computations in tests.
144    pub step_limit: u64,
145    pub steps: u64,
146    /// Per-Vm memoization cache for pure functions (#229). Keyed by
147    /// `(fn_id, sha256(canonical_json(args))[..16])`. Effectful
148    /// functions never enter this map. The cache lives for the
149    /// lifetime of one `Vm::call` chain — calling `Vm::with_handler`
150    /// again starts a fresh cache.
151    pure_memo: std::collections::HashMap<(u32, [u8; 16]), Value>,
152    /// Diagnostic counters for `--trace` observability (#229).
153    pub pure_memo_hits: u64,
154    pub pure_memo_misses: u64,
155    /// Monomorphic inline caches for `Op::GetField` (#389 slice 2).
156    /// Key: `(fn_id as u64) << 32 | pc as u64` — one entry per
157    /// call site. Value: the IndexMap offset of the field at that
158    /// site. On a hit we verify the field name at the cached offset
159    /// in O(1) and skip the hash-table lookup entirely; on a miss we
160    /// fall back to `IndexMap::get_full` and populate the cache.
161    field_ics: std::collections::HashMap<u64, usize>,
162    /// Stack allocator for function locals (#389 slice 3).
163    ///
164    /// Every function frame claims `locals_count` contiguous slots from
165    /// this Vec on push and releases them on pop.  Because Lex uses
166    /// strictly LIFO frame semantics the most-recently-pushed frame's
167    /// slots always sit at the top of the Vec, so `truncate` is the
168    /// correct (and O(1)) release operation.
169    ///
170    /// The Vec is pre-allocated once at VM construction and then grows
171    /// only if the actual call depth × locals width exceeds the initial
172    /// capacity.  After a top-level `vm.call` returns the Vec is empty
173    /// again but its capacity is retained, so the next request incurs
174    /// zero allocations for locals up to the high-water mark.
175    locals_storage: Vec<Value>,
176}
177
178struct Frame {
179    fn_id: u32,
180    pc: usize,
181    /// Start index of this frame's locals in `Vm::locals_storage` (#389
182    /// slice 3). The frame owns `locals_storage[locals_start..locals_start
183    /// + locals_len]`; `Op::Return` truncates the Vec back to
184    /// `locals_start`, releasing the slots in O(1).
185    locals_start: usize,
186    locals_len: usize,
187    /// Stack base when this frame started (for cleanup on return).
188    stack_base: usize,
189    trace_kind: FrameKind,
190    /// Pure-fn memo key (#229). `Some(key)` if the call was eligible
191    /// for memoization and missed the cache; on Op::Return the key
192    /// is used to write the return value back into the cache.
193    /// `None` means "don't memoize" — either the function isn't pure,
194    /// the call wasn't through Op::Call, or memoization is disabled.
195    memo_key: Option<(u32, [u8; 16])>,
196}
197
198/// Sum of `[budget(N)]` declarations on a function's signature
199/// (#225). Used by Op::Call / Op::TailCall / Op::CallClosure to
200/// notify the EffectHandler of per-call budget cost so the handler
201/// can deduct from a shared pool and refuse calls that would
202/// exceed the policy ceiling. Negative `Int` args are ignored —
203/// the static check (`policy::check_program`) treats budgets as
204/// non-negative.
205fn call_budget_cost(f: &crate::program::Function) -> u64 {
206    let mut total: u64 = 0;
207    for e in &f.effects {
208        if e.kind == "budget" {
209            if let Some(crate::program::EffectArg::Int(n)) = &e.arg {
210                if *n >= 0 {
211                    total = total.saturating_add(*n as u64);
212                }
213            }
214        }
215    }
216    total
217}
218
219/// Hash the argument list for a pure-fn memoization lookup (#229).
220/// Routes through the canonical-JSON path so two semantically-equal
221/// argument lists produce the same hash regardless of how the
222/// containing `Value`s were assembled (Records use IndexMap so field
223/// order is already stable, but canon_json gives the same property
224/// for the inner serde_json shape).
225fn hash_call_args(args: &[Value]) -> [u8; 16] {
226    use sha2::{Digest, Sha256};
227    let json = serde_json::Value::Array(args.iter().map(Value::to_json).collect());
228    let canonical = lex_ast::canon_json::hash_canonical(&json);
229    let mut hasher = Sha256::new();
230    hasher.update(canonical);
231    let full = hasher.finalize();
232    let mut out = [0u8; 16];
233    out.copy_from_slice(&full[..16]);
234    out
235}
236
237/// Evaluate a refinement predicate at runtime against the actual
238/// argument value (#209 slice 3). Mirrors `lex_types::discharge`'s
239/// static evaluator but operates on `Value` directly.
240///
241/// Returns `Ok(true)` / `Ok(false)` for a clean boolean verdict, or
242/// `Err(reason)` if the predicate references something the runtime
243/// can't resolve (free variable beyond the binding, unsupported AST
244/// node). Callers map `Ok(false)` and `Err` to `VmError::RefinementFailed`.
245fn eval_refinement(
246    predicate: &lex_ast::CExpr,
247    binding: &str,
248    arg: &Value,
249) -> Result<bool, String> {
250    match eval_refinement_inner(predicate, binding, arg) {
251        Ok(Value::Bool(b)) => Ok(b),
252        Ok(other) => Err(format!("predicate didn't reduce to a Bool, got {other:?}")),
253        Err(e) => Err(e),
254    }
255}
256
257fn eval_refinement_inner(
258    e: &lex_ast::CExpr,
259    binding: &str,
260    arg: &Value,
261) -> Result<Value, String> {
262    use lex_ast::{CExpr, CLit};
263    match e {
264        CExpr::Literal { value } => Ok(match value {
265            CLit::Int { value } => Value::Int(*value),
266            CLit::Float { value } => Value::Float(value.parse().unwrap_or(0.0)),
267            CLit::Bool { value } => Value::Bool(*value),
268            CLit::Str { value } => Value::Str(value.as_str().into()),
269            CLit::Bytes { value } => Value::Str(value.as_str().into()), // hex; unusual in predicates
270            CLit::Unit => Value::Unit,
271        }),
272        CExpr::Var { name } if name == binding => Ok(arg.clone()),
273        CExpr::Var { name } => Err(format!(
274            "predicate references free var `{name}`; runtime check \
275             only resolves the binding (slice 4 will plumb call-site \
276             context)")),
277        CExpr::UnaryOp { op, expr } => {
278            let v = eval_refinement_inner(expr, binding, arg)?;
279            match (op.as_str(), v) {
280                ("not", Value::Bool(b)) => Ok(Value::Bool(!b)),
281                ("-", Value::Int(n)) => Ok(Value::Int(-n)),
282                ("-", Value::Float(n)) => Ok(Value::Float(-n)),
283                (o, v) => Err(format!("unsupported unary `{o}` on {v:?}")),
284            }
285        }
286        CExpr::BinOp { op, lhs, rhs } => {
287            // Short-circuit `and` / `or` for the same reasons as the
288            // static evaluator.
289            if op == "and" || op == "or" {
290                let l = eval_refinement_inner(lhs, binding, arg)?;
291                let lb = match l {
292                    Value::Bool(b) => b,
293                    other => return Err(format!("`{op}` on non-bool: {other:?}")),
294                };
295                if op == "and" && !lb { return Ok(Value::Bool(false)); }
296                if op == "or"  &&  lb { return Ok(Value::Bool(true));  }
297                let r = eval_refinement_inner(rhs, binding, arg)?;
298                return match r {
299                    Value::Bool(b) => Ok(Value::Bool(b)),
300                    other => Err(format!("`{op}` on non-bool: {other:?}")),
301                };
302            }
303            let l = eval_refinement_inner(lhs, binding, arg)?;
304            let r = eval_refinement_inner(rhs, binding, arg)?;
305            apply_refinement_binop(op, &l, &r)
306        }
307        // Other AST forms (Call, Let, Match, FieldAccess, Lambda,
308        // Block, Constructors, Records, Tuples, Lists, Return) need
309        // a more general evaluator that can call back into the VM.
310        // Out of scope for slice 3; a future slice may unify this
311        // with the spec-checker's gate evaluator.
312        other => Err(format!("unsupported predicate node: {other:?}")),
313    }
314}
315
316fn apply_refinement_binop(op: &str, l: &Value, r: &Value) -> Result<Value, String> {
317    use Value::*;
318    match (op, l, r) {
319        ("+", Int(a), Int(b)) => Ok(Int(a + b)),
320        ("-", Int(a), Int(b)) => Ok(Int(a - b)),
321        ("*", Int(a), Int(b)) => Ok(Int(a * b)),
322        ("/", Int(a), Int(b)) if *b != 0 => Ok(Int(a / b)),
323        ("%", Int(a), Int(b)) if *b != 0 => Ok(Int(a % b)),
324        ("+", Float(a), Float(b)) => Ok(Float(a + b)),
325        ("-", Float(a), Float(b)) => Ok(Float(a - b)),
326        ("*", Float(a), Float(b)) => Ok(Float(a * b)),
327        ("/", Float(a), Float(b)) => Ok(Float(a / b)),
328
329        ("==", a, b) => Ok(Bool(a == b)),
330        ("!=", a, b) => Ok(Bool(a != b)),
331
332        ("<",  Int(a), Int(b)) => Ok(Bool(a < b)),
333        ("<=", Int(a), Int(b)) => Ok(Bool(a <= b)),
334        (">",  Int(a), Int(b)) => Ok(Bool(a > b)),
335        (">=", Int(a), Int(b)) => Ok(Bool(a >= b)),
336
337        ("<",  Float(a), Float(b)) => Ok(Bool(a < b)),
338        ("<=", Float(a), Float(b)) => Ok(Bool(a <= b)),
339        (">",  Float(a), Float(b)) => Ok(Bool(a > b)),
340        (">=", Float(a), Float(b)) => Ok(Bool(a >= b)),
341
342        (op, a, b) => Err(format!(
343            "unsupported binop `{op}` on {a:?} and {b:?}")),
344    }
345}
346
347fn const_str(constants: &[Const], idx: u32) -> String {
348    match constants.get(idx as usize) {
349        Some(Const::NodeId(s)) | Some(Const::Str(s)) => s.clone(),
350        _ => String::new(),
351    }
352}
353
354/// Read `LEX_PAR_MAX_CONCURRENCY` (default = available CPU cores,
355/// fallback 4). Capped at 64 so a malformed env var can't spawn an
356/// unreasonable number of OS threads.
357/// Order-defining comparator for `list.sort_by` keys (#338).
358/// Same-typed Int / Float / Str pairs compare via their native
359/// `Ord` / `PartialOrd`. Mixed-type or other key shapes compare
360/// as Equal; combined with `Vec::sort_by`'s stability that
361/// preserves the original element order — best-effort fallback
362/// that never panics.
363fn compare_sort_keys(a: &Value, b: &Value) -> std::cmp::Ordering {
364    use std::cmp::Ordering;
365    match (a, b) {
366        (Value::Int(x), Value::Int(y)) => x.cmp(y),
367        (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
368        (Value::Str(x), Value::Str(y)) => x.cmp(y),
369        _ => Ordering::Equal,
370    }
371}
372
373fn par_max_concurrency() -> usize {
374    let from_env = std::env::var("LEX_PAR_MAX_CONCURRENCY")
375        .ok()
376        .and_then(|s| s.parse::<usize>().ok())
377        .filter(|n| *n > 0);
378    let default = std::thread::available_parallelism()
379        .map(|n| n.get())
380        .unwrap_or(4);
381    from_env.unwrap_or(default).min(64)
382}
383
384/// `list.par_map`'s runtime: spawn OS threads (capped by
385/// `LEX_PAR_MAX_CONCURRENCY`), apply `closure` to each item, return
386/// results in input order. Each worker runs a fresh `Vm` with
387/// [`DenyAllEffects`] for #305 slice 1 — effectful closures fail
388/// with `VmError::Effect`. Slice 2 will plumb a per-thread effect
389/// handler split.
390fn par_map_run<'a>(
391    program: &'a Program,
392    closure: Value,
393    items: Vec<Value>,
394    worker_handlers: Vec<Box<dyn EffectHandler + Send>>,
395) -> Result<Vec<Value>, VmError> {
396    if items.is_empty() {
397        return Ok(Vec::new());
398    }
399    let n_workers = worker_handlers.len().min(items.len()).max(1);
400    // Carve items into `n_workers` round-robin buckets so each
401    // worker processes (indices, items) pairs and we can reassemble
402    // in input order.
403    let mut buckets: Vec<Vec<(usize, Value)>> = (0..n_workers).map(|_| Vec::new()).collect();
404    for (i, v) in items.into_iter().enumerate() {
405        buckets[i % n_workers].push((i, v));
406    }
407    let n_total: usize = buckets.iter().map(|b| b.len()).sum();
408    let results: std::sync::Mutex<Vec<Option<Result<Value, String>>>> =
409        std::sync::Mutex::new((0..n_total).map(|_| None).collect());
410
411    // Pair each bucket with its pre-built handler so workers own
412    // their handler outright — no shared mutable state across
413    // worker threads.
414    let mut worker_handlers = worker_handlers;
415    worker_handlers.truncate(n_workers);
416    type Pair = (Vec<(usize, Value)>, Box<dyn EffectHandler + Send>);
417    let pairs: Vec<Pair> = buckets.into_iter().zip(worker_handlers).collect();
418
419    std::thread::scope(|s| {
420        let mut handles = Vec::with_capacity(pairs.len());
421        for (bucket, handler) in pairs {
422            let closure = closure.clone();
423            let results = &results;
424            handles.push(s.spawn(move || {
425                // `Box<dyn EffectHandler + Send>` has implicit
426                // `+ 'static`; that coerces to `+ 'a` because
427                // `'static` outlives any `'a`. The `Send` bound is
428                // auto-erased on the unsize coercion.
429                let handler_for_vm: Box<dyn EffectHandler + 'a> = handler;
430                let mut vm = Vm::with_handler(program, handler_for_vm);
431                for (idx, item) in bucket {
432                    let r = vm
433                        .invoke_closure_value(closure.clone(), vec![item])
434                        .map_err(|e| format!("{e:?}"));
435                    results.lock().unwrap()[idx] = Some(r);
436                }
437            }));
438        }
439        for h in handles {
440            h.join().map_err(|_| ()).ok();
441        }
442    });
443
444    let mut out = Vec::with_capacity(n_total);
445    let inner = results.into_inner().unwrap();
446    for r in inner {
447        match r {
448            Some(Ok(v)) => out.push(v),
449            Some(Err(e)) => return Err(VmError::Effect(format!("par_map worker: {e}"))),
450            None => return Err(VmError::Panic("par_map worker did not produce a result".into())),
451        }
452    }
453    Ok(out)
454}
455
456impl<'a> Vm<'a> {
457    pub fn new(program: &'a Program) -> Self {
458        Self::with_handler(program, Box::new(DenyAllEffects))
459    }
460
461    pub fn with_handler(program: &'a Program, handler: Box<dyn EffectHandler + 'a>) -> Self {
462        Self {
463            program,
464            handler,
465            tracer: Box::new(NullTracer),
466            // Pre-allocate enough capacity for a typical request so the first
467            // call incurs no reallocation (#389 slice 3).
468            frames: Vec::with_capacity(32),
469            stack: Vec::with_capacity(128),
470            step_limit: 10_000_000,
471            steps: 0,
472            pure_memo: std::collections::HashMap::new(),
473            pure_memo_hits: 0,
474            pure_memo_misses: 0,
475            field_ics: std::collections::HashMap::new(),
476            // 256 slots handles ~32 frames × 8 locals; grows on demand and
477            // retains capacity across consecutive vm.call() invocations.
478            locals_storage: Vec::with_capacity(256),
479        }
480    }
481
482    pub fn set_tracer(&mut self, tracer: Box<dyn Tracer + 'a>) {
483        self.tracer = tracer;
484    }
485
486    /// Cap the number of opcode dispatches before the VM aborts with
487    /// `step limit exceeded`. Useful as a runtime DoS guard against
488    /// untrusted code (e.g. the `agent-tool` sandbox, where an LLM
489    /// could emit `list.fold(list.range(0, 1_000_000_000), …)` to hang
490    /// the host). Default is 10_000_000.
491    pub fn set_step_limit(&mut self, limit: u64) {
492        self.step_limit = limit;
493    }
494
495    pub fn call(&mut self, name: &str, args: Vec<Value>) -> Result<Value, VmError> {
496        let fn_id = self.program.lookup(name).ok_or_else(|| VmError::Panic(format!("no function `{name}`")))?;
497        self.invoke(fn_id, args)
498    }
499
500    /// Vm-level handler for `parser.run` (#221). Routed here from
501    /// `Op::EffectCall` rather than through the `EffectHandler` so
502    /// the recursive parser interpreter has reentrant Vm access for
503    /// closure invocation. Returns the wrapped `Result[T, ParseErr]`
504    /// value the language sees.
505    fn run_parser_op(&mut self, args: Vec<Value>) -> Result<Value, String> {
506        let parser = args.first().cloned()
507            .ok_or_else(|| "parser.run: missing parser arg".to_string())?;
508        let input = match args.get(1) {
509            Some(Value::Str(s)) => s.clone(),
510            _ => return Err("parser.run: input must be Str".into()),
511        };
512        match crate::parser_runtime::run_parser(&parser, &input, 0, self) {
513            Ok((value, _pos)) => Ok(Value::Variant {
514                name: "Ok".into(),
515                args: vec![value],
516            }),
517            Err((pos, msg)) => {
518                let mut e = indexmap::IndexMap::new();
519                e.insert("pos".into(), Value::Int(pos as i64));
520                e.insert("message".into(), Value::Str(msg.into()));
521                Ok(Value::Variant {
522                    name: "Err".into(),
523                    args: vec![Value::Record(e)],
524                })
525            }
526        }
527    }
528
529    /// VM-level handler for `conc.*` effect ops (#381).
530    ///
531    /// * `conc.spawn(init, handler)` — creates an `Actor` wrapping the
532    ///   initial state and the handler closure. No background thread is
533    ///   started; the actor runs synchronously on the calling thread
534    ///   under a `Mutex` so concurrent callers serialise.
535    ///
536    /// * `conc.ask(actor, msg)` — locks the actor, calls
537    ///   `handler(state, msg)` on *this* VM (reentrant), expects a
538    ///   2-tuple `(new_state, reply)`, updates the actor's state, and
539    ///   returns `reply`.
540    ///
541    /// * `conc.tell(actor, msg)` — same as `ask` but discards the
542    ///   reply and returns `Unit`.
543    fn run_conc_op(&mut self, op: &str, args: Vec<Value>) -> Result<Value, String> {
544        match op {
545            "spawn" => {
546                let mut it = args.into_iter();
547                let init = it.next().unwrap_or(Value::Unit);
548                let handler = it.next().unwrap_or(Value::Unit);
549                if !matches!(handler, Value::Closure { .. }) {
550                    return Err(format!(
551                        "conc.spawn: handler must be a Closure, got {handler:?}"));
552                }
553                Ok(Value::Actor(Arc::new(Mutex::new(ActorCell {
554                    state: init,
555                    handler,
556                }))))
557            }
558            "ask" | "tell" => {
559                let mut it = args.into_iter();
560                let actor_val = it.next().unwrap_or(Value::Unit);
561                let msg = it.next().unwrap_or(Value::Unit);
562                let cell = match actor_val {
563                    Value::Actor(ref arc) => Arc::clone(arc),
564                    other => return Err(format!(
565                        "conc.{op}: first arg must be an Actor, got {other:?}")),
566                };
567                // Lock the actor: guarantees at-most-one-concurrent message.
568                let mut guard = cell.lock().map_err(|e| format!("conc.{op}: actor mutex poisoned: {e}"))?;
569                let handler = guard.handler.clone();
570                let state = guard.state.clone();
571                // Call handler(state, msg) on this VM — full effect access.
572                let result = self.invoke_closure_value(handler, vec![state, msg])
573                    .map_err(|e| format!("conc.{op}: handler error: {e:?}"))?;
574                // Expect (new_state, reply) tuple.
575                match result {
576                    Value::Tuple(mut parts) if parts.len() == 2 => {
577                        let reply = parts.pop().unwrap();
578                        let new_state = parts.pop().unwrap();
579                        guard.state = new_state;
580                        drop(guard);
581                        if op == "ask" { Ok(reply) } else { Ok(Value::Unit) }
582                    }
583                    other => Err(format!(
584                        "conc.{op}: handler must return a 2-tuple (new_state, reply), got {other:?}")),
585                }
586            }
587            other => Err(format!("unknown conc.{other}")),
588        }
589    }
590
591    /// Invoke a `Value::Closure` by combining its captures with the
592    /// supplied call args and dispatching to the underlying function.
593    /// Used by the parser interpreter (#221) to call user-supplied
594    /// `f` arguments inside `parser.map` / `parser.and_then` nodes.
595    pub fn invoke_closure_value(
596        &mut self,
597        closure: Value,
598        args: Vec<Value>,
599    ) -> Result<Value, VmError> {
600        let (fn_id, captures) = match closure {
601            Value::Closure { fn_id, captures, .. } => (fn_id, captures),
602            other => return Err(VmError::TypeMismatch(
603                format!("invoke_closure_value: not a closure: {other:?}"))),
604        };
605        let mut combined = captures;
606        combined.extend(args);
607        self.invoke(fn_id, combined)
608    }
609
610    pub fn invoke(&mut self, fn_id: u32, args: Vec<Value>) -> Result<Value, VmError> {
611        let f = &self.program.functions[fn_id as usize];
612        if args.len() != f.arity as usize {
613            return Err(VmError::Panic(format!("arity mismatch calling {}", f.name)));
614        }
615        // Refinement runtime check at the public entry point too
616        // (#209 slice 3). `Op::Call` checks for in-program calls;
617        // this branch covers `vm.call("entry", ...)` from the host
618        // and the reentrant `invoke_closure_value` path. Same
619        // semantics, same error shape.
620        let f_name = f.name.clone();
621        let refinements = f.refinements.clone();
622        for (i, refinement) in refinements.iter().enumerate() {
623            if let Some(r) = refinement {
624                let arg = args.get(i).cloned().unwrap_or(Value::Unit);
625                match eval_refinement(&r.predicate, &r.binding, &arg) {
626                    Ok(true) => {}
627                    Ok(false) => return Err(VmError::RefinementFailed {
628                        fn_name: f_name,
629                        param_index: i,
630                        binding: r.binding.clone(),
631                        reason: format!("predicate failed for {} = {arg:?}", r.binding),
632                    }),
633                    Err(reason) => return Err(VmError::RefinementFailed {
634                        fn_name: f_name,
635                        param_index: i,
636                        binding: r.binding.clone(),
637                        reason,
638                    }),
639                }
640            }
641        }
642        let f = &self.program.functions[fn_id as usize];
643        // Claim slots from the locals stack allocator (#389 slice 3).
644        let locals_start = self.locals_storage.len();
645        let locals_len = f.locals_count.max(f.arity) as usize;
646        self.locals_storage.resize(locals_start + locals_len, Value::Unit);
647        for (i, v) in args.into_iter().enumerate() {
648            self.locals_storage[locals_start + i] = v;
649        }
650        // Record the depth before pushing — this is what `run` will
651        // exit at, supporting reentrant invocation from inside the
652        // VM (e.g. the parser interpreter calling closures, #221).
653        let base_depth = self.frames.len();
654        self.push_frame(Frame {
655            fn_id, pc: 0, locals_start, locals_len,
656            stack_base: self.stack.len(),
657            trace_kind: FrameKind::Entry,
658            memo_key: None,
659        })?;
660        self.run_to(base_depth)
661    }
662
663    /// All call-frame pushes funnel through here so the depth
664    /// check can't be skipped by a missing branch. Returns
665    /// `CallStackOverflow` instead of letting recursion blow the
666    /// host's native stack.
667    fn push_frame(&mut self, frame: Frame) -> Result<(), VmError> {
668        if self.frames.len() as u32 >= MAX_CALL_DEPTH {
669            return Err(VmError::CallStackOverflow(MAX_CALL_DEPTH));
670        }
671        self.frames.push(frame);
672        Ok(())
673    }
674
675    /// Run until the frame stack drops to `base_depth`. Required for
676    /// reentrant invocation: a `Vm::invoke` call from inside an
677    /// already-running `run()` must return when *its* frame returns,
678    /// not when the entire frame stack empties (#221).
679    fn run_to(&mut self, base_depth: usize) -> Result<Value, VmError> {
680        loop {
681            if self.steps > self.step_limit {
682                let frame_idx = self.frames.len() - 1;
683                let fn_id = self.frames[frame_idx].fn_id;
684                let fn_name = &self.program.functions[fn_id as usize].name;
685                return Err(VmError::Panic(format!(
686                    "step limit exceeded in `{fn_name}` ({} > {})",
687                    self.steps, self.step_limit,
688                )));
689            }
690            self.steps += 1;
691            let frame_idx = self.frames.len() - 1;
692            let pc = self.frames[frame_idx].pc;
693            let fn_id = self.frames[frame_idx].fn_id;
694            let code = &self.program.functions[fn_id as usize].code;
695            if pc >= code.len() {
696                let fn_name = &self.program.functions[fn_id as usize].name;
697                return Err(VmError::Panic(format!("ran past end of code in `{fn_name}`")));
698            }
699            let op = code[pc].clone();
700            self.frames[frame_idx].pc = pc + 1;
701
702            match op {
703                Op::PushConst(i) => {
704                    let c = &self.program.constants[i as usize];
705                    self.stack.push(const_to_value(c));
706                }
707                Op::Pop => { self.pop()?; }
708                Op::Dup => {
709                    let v = self.peek()?.clone();
710                    self.stack.push(v);
711                }
712                Op::LoadLocal(i) => {
713                    let base = self.frames[frame_idx].locals_start;
714                    let v = self.locals_storage[base + i as usize].clone();
715                    self.stack.push(v);
716                }
717                Op::StoreLocal(i) => {
718                    let v = self.pop()?;
719                    let base = self.frames[frame_idx].locals_start;
720                    self.locals_storage[base + i as usize] = v;
721                }
722                Op::MakeRecord { field_name_indices } => {
723                    let n = field_name_indices.len();
724                    let mut values: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
725                    for i in (0..n).rev() {
726                        values[i] = self.pop()?;
727                    }
728                    let mut rec = IndexMap::new();
729                    for (i, val) in values.into_iter().enumerate() {
730                        let name = match &self.program.constants[field_name_indices[i] as usize] {
731                            Const::FieldName(s) => s.clone(),
732                            _ => return Err(VmError::TypeMismatch("expected FieldName const".into())),
733                        };
734                        rec.insert(name, val);
735                    }
736                    self.stack.push(Value::Record(rec));
737                }
738                Op::MakeTuple(n) => {
739                    let mut items: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
740                    for i in (0..n as usize).rev() { items[i] = self.pop()?; }
741                    self.stack.push(Value::Tuple(items));
742                }
743                Op::MakeList(n) => {
744                    let mut items: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
745                    for i in (0..n as usize).rev() { items[i] = self.pop()?; }
746                    self.stack.push(Value::List(items.into()));
747                }
748                Op::MakeVariant { name_idx, arity } => {
749                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
750                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
751                    let name = match &self.program.constants[name_idx as usize] {
752                        Const::VariantName(s) => s.clone(),
753                        _ => return Err(VmError::TypeMismatch("expected VariantName const".into())),
754                    };
755                    self.stack.push(Value::Variant { name, args });
756                }
757                Op::GetField(i) => {
758                    let name_idx = i;
759                    let v = self.pop()?;
760                    match v {
761                        Value::Record(r) => {
762                            // Inline cache keyed by call site: (fn_id << 32 | pc).
763                            // On hit: verify field name at cached offset (O(1), no
764                            // string hash); on miss: walk by name + populate cache.
765                            let site = (fn_id as u64) << 32 | pc as u64;
766                            let value = 'ic: {
767                                if let Some(&off) = self.field_ics.get(&site) {
768                                    if let Some((k, val)) = r.get_index(off) {
769                                        if let Const::FieldName(s) =
770                                            &self.program.constants[name_idx as usize]
771                                        {
772                                            if s == k { break 'ic val.clone(); } // cache hit
773                                        }
774                                    }
775                                }
776                                // Cache miss: resolve by name, populate IC.
777                                let name = match &self.program.constants[name_idx as usize] {
778                                    Const::FieldName(s) => s.as_str(),
779                                    _ => return Err(VmError::TypeMismatch(
780                                        "expected FieldName const".into())),
781                                };
782                                let (off, _, val) = r.get_full(name)
783                                    .ok_or_else(|| VmError::TypeMismatch(
784                                        format!("missing field `{name}`")))?;
785                                self.field_ics.insert(site, off);
786                                val.clone()
787                            };
788                            self.stack.push(value);
789                        }
790                        other => return Err(VmError::TypeMismatch(
791                            format!("GetField on non-record: {other:?}"))),
792                    }
793                }
794                Op::GetElem(i) => {
795                    let v = self.pop()?;
796                    match v {
797                        Value::Tuple(items) => {
798                            let v = items.into_iter().nth(i as usize)
799                                .ok_or_else(|| VmError::TypeMismatch(format!("tuple index {i} out of range")))?;
800                            self.stack.push(v);
801                        }
802                        other => return Err(VmError::TypeMismatch(format!("GetElem on non-tuple: {other:?}"))),
803                    }
804                }
805                Op::TestVariant(i) => {
806                    let name = match &self.program.constants[i as usize] {
807                        Const::VariantName(s) => s.clone(),
808                        _ => return Err(VmError::TypeMismatch("expected VariantName const".into())),
809                    };
810                    let v = self.pop()?;
811                    match &v {
812                        Value::Variant { name: vname, .. } => {
813                            self.stack.push(Value::Bool(vname == &name));
814                        }
815                        // For tag-only enums of primitive type (e.g. ParseError = Empty | NotNumber)
816                        // the value is currently a Variant too, since constructors emit MakeVariant.
817                        other => return Err(VmError::TypeMismatch(format!("TestVariant on non-variant: {other:?}"))),
818                    }
819                }
820                Op::GetVariant(_i) => {
821                    let v = self.pop()?;
822                    match v {
823                        Value::Variant { args, .. } => {
824                            self.stack.push(Value::Tuple(args));
825                        }
826                        other => return Err(VmError::TypeMismatch(format!("GetVariant on non-variant: {other:?}"))),
827                    }
828                }
829                Op::GetVariantArg(i) => {
830                    let v = self.pop()?;
831                    match v {
832                        Value::Variant { mut args, .. } => {
833                            if (i as usize) >= args.len() {
834                                return Err(VmError::TypeMismatch("variant arg index oob".into()));
835                            }
836                            self.stack.push(args.swap_remove(i as usize));
837                        }
838                        other => return Err(VmError::TypeMismatch(format!("GetVariantArg on non-variant: {other:?}"))),
839                    }
840                }
841                Op::GetListLen => {
842                    let v = self.pop()?;
843                    match v {
844                        Value::List(items) => self.stack.push(Value::Int(items.len() as i64)),
845                        other => return Err(VmError::TypeMismatch(format!("GetListLen on non-list: {other:?}"))),
846                    }
847                }
848                Op::GetListElem(i) => {
849                    let v = self.pop()?;
850                    match v {
851                        Value::List(items) => {
852                            let v = items.into_iter().nth(i as usize)
853                                .ok_or_else(|| VmError::TypeMismatch("list index oob".into()))?;
854                            self.stack.push(v);
855                        }
856                        other => return Err(VmError::TypeMismatch(format!("GetListElem on non-list: {other:?}"))),
857                    }
858                }
859                Op::GetListElemDyn => {
860                    // Stack: [list, idx]
861                    let idx = match self.pop()? {
862                        Value::Int(n) => n as usize,
863                        other => return Err(VmError::TypeMismatch(format!("GetListElemDyn idx: {other:?}"))),
864                    };
865                    let v = self.pop()?;
866                    match v {
867                        Value::List(items) => {
868                            let v = items.into_iter().nth(idx)
869                                .ok_or_else(|| VmError::TypeMismatch("list index oob".into()))?;
870                            self.stack.push(v);
871                        }
872                        other => return Err(VmError::TypeMismatch(format!("GetListElemDyn on non-list: {other:?}"))),
873                    }
874                }
875                Op::ListAppend => {
876                    let value = self.pop()?;
877                    let list = self.pop()?;
878                    match list {
879                        Value::List(mut items) => {
880                            items.push_back(value);
881                            self.stack.push(Value::List(items));
882                        }
883                        other => return Err(VmError::TypeMismatch(format!("ListAppend on non-list: {other:?}"))),
884                    }
885                }
886                Op::Jump(off) => {
887                    let new_pc = (self.frames[frame_idx].pc as i32 + off) as usize;
888                    self.frames[frame_idx].pc = new_pc;
889                }
890                Op::JumpIf(off) => {
891                    let v = self.pop()?;
892                    if v.as_bool() {
893                        let new_pc = (self.frames[frame_idx].pc as i32 + off) as usize;
894                        self.frames[frame_idx].pc = new_pc;
895                    }
896                }
897                Op::JumpIfNot(off) => {
898                    let v = self.pop()?;
899                    if !v.as_bool() {
900                        let new_pc = (self.frames[frame_idx].pc as i32 + off) as usize;
901                        self.frames[frame_idx].pc = new_pc;
902                    }
903                }
904                Op::MakeClosure { fn_id, capture_count } => {
905                    let n = capture_count as usize;
906                    let mut captures: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
907                    for i in (0..n).rev() { captures[i] = self.pop()?; }
908                    // Look up the canonical body hash so the resulting
909                    // `Value::Closure` carries it for equality (#222).
910                    let body_hash = self.program.functions[fn_id as usize].body_hash;
911                    self.stack.push(Value::Closure { fn_id, body_hash, captures });
912                }
913                Op::CallClosure { arity, node_id_idx } => {
914                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
915                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
916                    let closure = self.pop()?;
917                    let (fn_id, captures) = match closure {
918                        Value::Closure { fn_id, captures, .. } => (fn_id, captures),
919                        other => return Err(VmError::TypeMismatch(format!("CallClosure on non-closure: {other:?}"))),
920                    };
921                    let node_id = const_str(&self.program.constants, node_id_idx);
922                    let callee_name = self.program.functions[fn_id as usize].name.clone();
923                    let budget_cost = call_budget_cost(&self.program.functions[fn_id as usize]);
924                    if budget_cost > 0 {
925                        self.handler.note_call_budget(budget_cost)
926                            .map_err(VmError::Effect)?;
927                    }
928                    let mut combined = captures;
929                    combined.extend(args);
930                    self.tracer.enter_call(&node_id, &callee_name, &combined);
931                    let f = &self.program.functions[fn_id as usize];
932                    let locals_start = self.locals_storage.len();
933                    let locals_len = f.locals_count.max(f.arity) as usize;
934                    self.locals_storage.resize(locals_start + locals_len, Value::Unit);
935                    for (i, v) in combined.into_iter().enumerate() {
936                        self.locals_storage[locals_start + i] = v;
937                    }
938                    self.push_frame(Frame {
939                        fn_id, pc: 0, locals_start, locals_len,
940                        stack_base: self.stack.len(),
941                        trace_kind: FrameKind::Call(node_id),
942                        // Op::CallClosure intentionally doesn't memoize
943                        // for v1 (#229) — closures over captures need a
944                        // hashing strategy that includes the captures.
945                        // Direct Op::Call is the v1 surface.
946                        memo_key: None,
947                    })?;
948                }
949                Op::SortByKey { node_id_idx: _ } => {
950                    // #338: pop (xs, f). For each x in xs, invoke
951                    // f(x) to derive a sortable key. Stable-sort the
952                    // (key, value) pairs by key. Return the values
953                    // in sorted order. Keys must be Int / Float /
954                    // Str; mixed-type pairs and other types compare
955                    // as equal (preserving original order — stable
956                    // sort).
957                    let f = self.pop()?;
958                    let xs = self.pop()?;
959                    let items = match xs {
960                        Value::List(v) => v,
961                        other => return Err(VmError::TypeMismatch(
962                            format!("SortByKey requires a List, got: {other:?}"))),
963                    };
964                    if !matches!(f, Value::Closure { .. }) {
965                        return Err(VmError::TypeMismatch(
966                            format!("SortByKey requires a closure, got: {f:?}")));
967                    }
968                    let mut keyed: Vec<(Value, Value)> = Vec::with_capacity(items.len());
969                    for item in items {
970                        let key = self.invoke_closure_value(f.clone(), vec![item.clone()])?;
971                        keyed.push((key, item));
972                    }
973                    keyed.sort_by(|(ka, _), (kb, _)| compare_sort_keys(ka, kb));
974                    let sorted: VecDeque<Value> = keyed.into_iter().map(|(_, v)| v).collect();
975                    self.stack.push(Value::List(sorted));
976                }
977                Op::ParallelMap { node_id_idx: _ } => {
978                    // #305 slice 1: pop (xs, f) and apply f to each
979                    // element across OS threads.
980                    //
981                    // #305 slice 2: each worker now asks the parent
982                    // handler for a thread-safe per-worker handler via
983                    // `EffectHandler::spawn_for_worker`. Handlers that
984                    // opt in (e.g. `DefaultHandler`) yield a fresh
985                    // instance sharing the budget pool; handlers that
986                    // don't fall back to the slice-1 behavior of
987                    // `DenyAllEffects` in the worker.
988                    let f = self.pop()?;
989                    let xs = self.pop()?;
990                    let items = match xs {
991                        Value::List(v) => v,
992                        other => return Err(VmError::TypeMismatch(
993                            format!("ParallelMap requires a List, got: {other:?}"))),
994                    };
995                    if !matches!(f, Value::Closure { .. }) {
996                        return Err(VmError::TypeMismatch(
997                            format!("ParallelMap requires a closure, got: {f:?}")));
998                    }
999                    // Pre-build one handler per worker on the main
1000                    // thread so the worker just owns its handler with
1001                    // no shared borrowing. The actual worker count is
1002                    // capped by `LEX_PAR_MAX_CONCURRENCY` (resolved
1003                    // inside par_map_run); cap ≤ items.len() so we
1004                    // never over-allocate handlers.
1005                    let n_workers = par_max_concurrency().max(1).min(items.len().max(1));
1006                    let mut worker_handlers: Vec<Box<dyn EffectHandler + Send>> =
1007                        Vec::with_capacity(n_workers);
1008                    for _ in 0..n_workers {
1009                        worker_handlers.push(
1010                            self.handler
1011                                .spawn_for_worker()
1012                                .unwrap_or_else(|| Box::new(DenyAllEffects)),
1013                        );
1014                    }
1015                    let results = par_map_run(self.program, f, items.into_iter().collect(), worker_handlers)?;
1016                    self.stack.push(Value::List(results.into()));
1017                }
1018                Op::Call { fn_id, arity, node_id_idx } => {
1019                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
1020                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
1021                    let node_id = const_str(&self.program.constants, node_id_idx);
1022                    let callee_name = self.program.functions[fn_id as usize].name.clone();
1023                    let budget_cost = call_budget_cost(&self.program.functions[fn_id as usize]);
1024                    if budget_cost > 0 {
1025                        self.handler.note_call_budget(budget_cost)
1026                            .map_err(VmError::Effect)?;
1027                    }
1028                    // Refinement runtime check (#209 slice 3). Each
1029                    // param's `Option<Refinement>` is evaluated against
1030                    // the actual arg before the frame is pushed. The
1031                    // tracer sees the call enter; failure surfaces as
1032                    // `VmError::RefinementFailed` *before* the body
1033                    // starts, which means an erroring trace shows the
1034                    // call as enter+exit_err with the verdict reason
1035                    // (same shape as `gate.verdict`).
1036                    let refinements = self.program.functions[fn_id as usize]
1037                        .refinements.clone();
1038                    for (i, refinement) in refinements.iter().enumerate() {
1039                        if let Some(r) = refinement {
1040                            let arg = args.get(i).cloned().unwrap_or(Value::Unit);
1041                            match eval_refinement(&r.predicate, &r.binding, &arg) {
1042                                Ok(true) => { /* satisfied, continue */ }
1043                                Ok(false) => {
1044                                    return Err(VmError::RefinementFailed {
1045                                        fn_name: callee_name.clone(),
1046                                        param_index: i,
1047                                        binding: r.binding.clone(),
1048                                        reason: format!(
1049                                            "predicate failed for {} = {arg:?}",
1050                                            r.binding),
1051                                    });
1052                                }
1053                                Err(reason) => {
1054                                    return Err(VmError::RefinementFailed {
1055                                        fn_name: callee_name.clone(),
1056                                        param_index: i,
1057                                        binding: r.binding.clone(),
1058                                        reason,
1059                                    });
1060                                }
1061                            }
1062                        }
1063                    }
1064                    // Pure-fn memoization (#229): if the callee declares
1065                    // no effects, hash the args and consult the cache.
1066                    // On hit, push the cached value, emit synthetic
1067                    // enter+exit trace events (so the trace still shows
1068                    // the call), and skip the frame push entirely.
1069                    let f = &self.program.functions[fn_id as usize];
1070                    let memo_key: Option<(u32, [u8; 16])> = if f.effects.is_empty() {
1071                        Some((fn_id, hash_call_args(&args)))
1072                    } else {
1073                        None
1074                    };
1075                    if let Some(key) = memo_key {
1076                        if let Some(cached) = self.pure_memo.get(&key).cloned() {
1077                            self.pure_memo_hits += 1;
1078                            self.tracer.enter_call(&node_id, &callee_name, &args);
1079                            self.tracer.exit_ok(&cached);
1080                            self.stack.push(cached);
1081                            continue;
1082                        }
1083                        self.pure_memo_misses += 1;
1084                    }
1085                    self.tracer.enter_call(&node_id, &callee_name, &args);
1086                    let locals_start = self.locals_storage.len();
1087                    let locals_len = f.locals_count.max(f.arity) as usize;
1088                    self.locals_storage.resize(locals_start + locals_len, Value::Unit);
1089                    for (i, v) in args.into_iter().enumerate() {
1090                        self.locals_storage[locals_start + i] = v;
1091                    }
1092                    self.push_frame(Frame {
1093                        fn_id, pc: 0, locals_start, locals_len,
1094                        stack_base: self.stack.len(),
1095                        trace_kind: FrameKind::Call(node_id),
1096                        memo_key,
1097                    })?;
1098                }
1099                Op::TailCall { fn_id, arity, node_id_idx } => {
1100                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
1101                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
1102                    let node_id = const_str(&self.program.constants, node_id_idx);
1103                    let callee_name = self.program.functions[fn_id as usize].name.clone();
1104                    let budget_cost = call_budget_cost(&self.program.functions[fn_id as usize]);
1105                    if budget_cost > 0 {
1106                        self.handler.note_call_budget(budget_cost)
1107                            .map_err(VmError::Effect)?;
1108                    }
1109                    // Refinement runtime check on tail calls too
1110                    // (#209 slice 3). Same shape as Op::Call.
1111                    let refinements = self.program.functions[fn_id as usize]
1112                        .refinements.clone();
1113                    for (i, refinement) in refinements.iter().enumerate() {
1114                        if let Some(r) = refinement {
1115                            let arg = args.get(i).cloned().unwrap_or(Value::Unit);
1116                            match eval_refinement(&r.predicate, &r.binding, &arg) {
1117                                Ok(true) => {}
1118                                Ok(false) => return Err(VmError::RefinementFailed {
1119                                    fn_name: callee_name.clone(),
1120                                    param_index: i,
1121                                    binding: r.binding.clone(),
1122                                    reason: format!(
1123                                        "predicate failed for {} = {arg:?}",
1124                                        r.binding),
1125                                }),
1126                                Err(reason) => return Err(VmError::RefinementFailed {
1127                                    fn_name: callee_name.clone(),
1128                                    param_index: i,
1129                                    binding: r.binding.clone(),
1130                                    reason,
1131                                }),
1132                            }
1133                        }
1134                    }
1135                    // A tail call closes the current call's trace frame and
1136                    // opens a new one in its place — preserves the caller's
1137                    // tree depth in the trace.
1138                    self.tracer.exit_call_tail();
1139                    self.tracer.enter_call(&node_id, &callee_name, &args);
1140                    let f = &self.program.functions[fn_id as usize];
1141                    // Reuse the current frame's locals_start position:
1142                    // truncate to release old locals then extend for the
1143                    // new function (#389 slice 3, same as Op::Return but
1144                    // without popping the frame).
1145                    let old_locals_start = self.frames.last().unwrap().locals_start;
1146                    self.locals_storage.truncate(old_locals_start);
1147                    let new_locals_len = f.locals_count.max(f.arity) as usize;
1148                    self.locals_storage.resize(old_locals_start + new_locals_len, Value::Unit);
1149                    for (i, v) in args.into_iter().enumerate() {
1150                        self.locals_storage[old_locals_start + i] = v;
1151                    }
1152                    let frame = self.frames.last_mut().unwrap();
1153                    frame.fn_id = fn_id;
1154                    frame.pc = 0;
1155                    frame.locals_len = new_locals_len;
1156                    frame.trace_kind = FrameKind::Call(node_id);
1157                }
1158                Op::EffectCall { kind_idx, op_idx, arity, node_id_idx } => {
1159                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
1160                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
1161                    let kind = match &self.program.constants[kind_idx as usize] {
1162                        Const::Str(s) => s.clone(),
1163                        _ => return Err(VmError::TypeMismatch("expected Str const for effect kind".into())),
1164                    };
1165                    let op_name = match &self.program.constants[op_idx as usize] {
1166                        Const::Str(s) => s.clone(),
1167                        _ => return Err(VmError::TypeMismatch("expected Str const for effect op".into())),
1168                    };
1169                    let node_id = const_str(&self.program.constants, node_id_idx);
1170                    self.tracer.enter_effect(&node_id, &kind, &op_name, &args);
1171                    let result = match self.tracer.override_effect(&node_id) {
1172                        Some(v) => Ok(v),
1173                        // VM-level intercept for `parser.run` (#221).
1174                        // Routed inline rather than through the handler
1175                        // because the parser interpreter needs reentrant
1176                        // VM access to invoke `Value::Closure` values
1177                        // from `Map` / `AndThen` nodes.
1178                        None if (kind.as_str(), op_name.as_str()) == ("parser", "run")
1179                            => self.run_parser_op(args),
1180                        // VM-level intercept for `conc.*` (#381). The actor
1181                        // handler closure must run on the calling VM so it can
1182                        // dispatch arbitrary effects through the same handler
1183                        // chain (e.g. sql queries inside an actor).
1184                        None if kind.as_str() == "conc"
1185                            => self.run_conc_op(op_name.as_str(), args),
1186                        None => self.handler.dispatch(&kind, &op_name, args),
1187                    };
1188                    match result {
1189                        Ok(v) => {
1190                            self.tracer.exit_ok(&v);
1191                            self.stack.push(v);
1192                        }
1193                        Err(e) => {
1194                            self.tracer.exit_err(&e);
1195                            return Err(VmError::Effect(e));
1196                        }
1197                    }
1198                }
1199                Op::Return => {
1200                    let v = self.pop()?;
1201                    let frame = self.frames.pop().unwrap();
1202                    // Trim any extra stuff that the function pushed but didn't pop.
1203                    self.stack.truncate(frame.stack_base);
1204                    // Release this frame's locals back to the arena (#389 slice 3).
1205                    // LIFO frame ordering guarantees this frame's slots are at the top.
1206                    self.locals_storage.truncate(frame.locals_start);
1207                    if matches!(frame.trace_kind, FrameKind::Call(_)) {
1208                        self.tracer.exit_ok(&v);
1209                    }
1210                    // Pure-fn memoization (#229): if this frame was a
1211                    // memoizable call that missed the cache, write the
1212                    // computed return value back so the next call with
1213                    // the same args returns it without re-executing.
1214                    if let Some(key) = frame.memo_key {
1215                        self.pure_memo.insert(key, v.clone());
1216                    }
1217                    // Exit when we've returned past the depth this
1218                    // `run_to` was entered at — supports reentrancy
1219                    // (a nested `invoke` returns into its caller, not
1220                    // out of the outermost VM run, #221).
1221                    if self.frames.len() <= base_depth {
1222                        return Ok(v);
1223                    }
1224                    self.stack.push(v);
1225                }
1226                Op::Panic(i) => {
1227                    let msg = match &self.program.constants[i as usize] {
1228                        Const::Str(s) => s.clone(),
1229                        _ => "panic".into(),
1230                    };
1231                    return Err(VmError::Panic(msg));
1232                }
1233                // Arithmetic
1234                Op::IntAdd => self.bin_int(|a, b| Value::Int(a + b))?,
1235                Op::IntSub => self.bin_int(|a, b| Value::Int(a - b))?,
1236                Op::IntMul => self.bin_int(|a, b| Value::Int(a * b))?,
1237                Op::IntDiv => self.bin_int(|a, b| Value::Int(a / b))?,
1238                Op::IntMod => self.bin_int(|a, b| Value::Int(a % b))?,
1239                Op::IntNeg => {
1240                    let a = self.pop()?.as_int();
1241                    self.stack.push(Value::Int(-a));
1242                }
1243                Op::IntEq => self.bin_int(|a, b| Value::Bool(a == b))?,
1244                Op::IntLt => self.bin_int(|a, b| Value::Bool(a < b))?,
1245                Op::IntLe => self.bin_int(|a, b| Value::Bool(a <= b))?,
1246                Op::FloatAdd => self.bin_float(|a, b| Value::Float(a + b))?,
1247                Op::FloatSub => self.bin_float(|a, b| Value::Float(a - b))?,
1248                Op::FloatMul => self.bin_float(|a, b| Value::Float(a * b))?,
1249                Op::FloatDiv => self.bin_float(|a, b| Value::Float(a / b))?,
1250                Op::FloatNeg => {
1251                    let a = self.pop()?.as_float();
1252                    self.stack.push(Value::Float(-a));
1253                }
1254                Op::FloatEq => self.bin_float(|a, b| Value::Bool(a == b))?,
1255                Op::FloatLt => self.bin_float(|a, b| Value::Bool(a < b))?,
1256                Op::FloatLe => self.bin_float(|a, b| Value::Bool(a <= b))?,
1257                Op::NumAdd => {
1258                    // #308: `+` is overloaded — Str+Str concatenates,
1259                    // numerics add. Other arithmetic ops (-, *, /, %)
1260                    // still reject Str at the type-checker layer.
1261                    let b = self.pop()?;
1262                    let a = self.pop()?;
1263                    match (a, b) {
1264                        (Value::Int(x), Value::Int(y)) => self.stack.push(Value::Int(x + y)),
1265                        (Value::Float(x), Value::Float(y)) => self.stack.push(Value::Float(x + y)),
1266                        (Value::Str(x), Value::Str(y)) => {
1267                            // SmolStr is immutable; concatenate via a temporary String.
1268                            let mut s = String::with_capacity(x.len() + y.len());
1269                            s.push_str(&x);
1270                            s.push_str(&y);
1271                            self.stack.push(Value::Str(s.into()));
1272                        }
1273                        (a, b) => return Err(VmError::TypeMismatch(format!("Num op: {a:?} {b:?}"))),
1274                    }
1275                }
1276                Op::NumSub => self.bin_num(|a, b| Value::Int(a - b), |a, b| Value::Float(a - b))?,
1277                Op::NumMul => self.bin_num(|a, b| Value::Int(a * b), |a, b| Value::Float(a * b))?,
1278                Op::NumDiv => self.bin_num(|a, b| Value::Int(a / b), |a, b| Value::Float(a / b))?,
1279                Op::NumMod => self.bin_int(|a, b| Value::Int(a % b))?,
1280                Op::NumNeg => {
1281                    let v = self.pop()?;
1282                    match v {
1283                        Value::Int(n) => self.stack.push(Value::Int(-n)),
1284                        Value::Float(f) => self.stack.push(Value::Float(-f)),
1285                        other => return Err(VmError::TypeMismatch(format!("NumNeg on {other:?}"))),
1286                    }
1287                }
1288                Op::NumEq => self.bin_eq()?,
1289                Op::NumLt => self.bin_ord(|a, b| Value::Bool(a < b), |a, b| Value::Bool(a < b), |a, b| Value::Bool(a < b))?,
1290                Op::NumLe => self.bin_ord(|a, b| Value::Bool(a <= b), |a, b| Value::Bool(a <= b), |a, b| Value::Bool(a <= b))?,
1291                Op::BoolAnd => {
1292                    let b = self.pop()?.as_bool();
1293                    let a = self.pop()?.as_bool();
1294                    self.stack.push(Value::Bool(a && b));
1295                }
1296                Op::BoolOr => {
1297                    let b = self.pop()?.as_bool();
1298                    let a = self.pop()?.as_bool();
1299                    self.stack.push(Value::Bool(a || b));
1300                }
1301                Op::BoolNot => {
1302                    let a = self.pop()?.as_bool();
1303                    self.stack.push(Value::Bool(!a));
1304                }
1305                Op::StrConcat => {
1306                    let b = self.pop()?;
1307                    let a = self.pop()?;
1308                    let s = format!("{}{}", a.as_str(), b.as_str());
1309                    self.stack.push(Value::Str(s.into()));
1310                }
1311                Op::StrLen => {
1312                    let v = self.pop()?;
1313                    self.stack.push(Value::Int(v.as_str().len() as i64));
1314                }
1315                Op::StrEq => {
1316                    let b = self.pop()?;
1317                    let a = self.pop()?;
1318                    self.stack.push(Value::Bool(a.as_str() == b.as_str()));
1319                }
1320                Op::BytesLen => {
1321                    let v = self.pop()?;
1322                    match v {
1323                        Value::Bytes(b) => self.stack.push(Value::Int(b.len() as i64)),
1324                        other => return Err(VmError::TypeMismatch(format!("BytesLen on {other:?}"))),
1325                    }
1326                }
1327                Op::BytesEq => {
1328                    let b = self.pop()?;
1329                    let a = self.pop()?;
1330                    let eq = match (a, b) {
1331                        (Value::Bytes(x), Value::Bytes(y)) => x == y,
1332                        _ => return Err(VmError::TypeMismatch("BytesEq operands".into())),
1333                    };
1334                    self.stack.push(Value::Bool(eq));
1335                }
1336            }
1337        }
1338    }
1339
1340    fn pop(&mut self) -> Result<Value, VmError> {
1341        self.stack.pop().ok_or(VmError::StackUnderflow)
1342    }
1343    fn peek(&self) -> Result<&Value, VmError> {
1344        self.stack.last().ok_or(VmError::StackUnderflow)
1345    }
1346
1347    fn bin_int(&mut self, f: impl Fn(i64, i64) -> Value) -> Result<(), VmError> {
1348        let b = self.pop()?.as_int();
1349        let a = self.pop()?.as_int();
1350        self.stack.push(f(a, b));
1351        Ok(())
1352    }
1353    fn bin_float(&mut self, f: impl Fn(f64, f64) -> Value) -> Result<(), VmError> {
1354        let b = self.pop()?.as_float();
1355        let a = self.pop()?.as_float();
1356        self.stack.push(f(a, b));
1357        Ok(())
1358    }
1359    fn bin_num(
1360        &mut self,
1361        i: impl Fn(i64, i64) -> Value,
1362        f: impl Fn(f64, f64) -> Value,
1363    ) -> Result<(), VmError> {
1364        let b = self.pop()?;
1365        let a = self.pop()?;
1366        match (a, b) {
1367            (Value::Int(x), Value::Int(y)) => { self.stack.push(i(x, y)); Ok(()) }
1368            (Value::Float(x), Value::Float(y)) => { self.stack.push(f(x, y)); Ok(()) }
1369            (a, b) => Err(VmError::TypeMismatch(format!("Num op: {a:?} {b:?}"))),
1370        }
1371    }
1372
1373    /// Like `bin_num` but also handles `Str` operands via lexicographic order.
1374    /// Used by `NumLt` / `NumLe` because the type checker admits `Str < Str`
1375    /// and `>` / `>=` compile as swap+NumLt / swap+NumLe (#332).
1376    fn bin_ord(
1377        &mut self,
1378        i: impl Fn(i64, i64) -> Value,
1379        f: impl Fn(f64, f64) -> Value,
1380        s: impl Fn(&str, &str) -> Value,
1381    ) -> Result<(), VmError> {
1382        let b = self.pop()?;
1383        let a = self.pop()?;
1384        match (a, b) {
1385            (Value::Int(x), Value::Int(y)) => { self.stack.push(i(x, y)); Ok(()) }
1386            (Value::Float(x), Value::Float(y)) => { self.stack.push(f(x, y)); Ok(()) }
1387            (Value::Str(x), Value::Str(y)) => { self.stack.push(s(&x, &y)); Ok(()) }
1388            (a, b) => Err(VmError::TypeMismatch(format!("Num op: {a:?} {b:?}"))),
1389        }
1390    }
1391    fn bin_eq(&mut self) -> Result<(), VmError> {
1392        let b = self.pop()?;
1393        let a = self.pop()?;
1394        self.stack.push(Value::Bool(a == b));
1395        Ok(())
1396    }
1397}
1398
1399fn const_to_value(c: &Const) -> Value {
1400    match c {
1401        Const::Int(n) => Value::Int(*n),
1402        Const::Float(f) => Value::Float(*f),
1403        Const::Bool(b) => Value::Bool(*b),
1404        Const::Str(s) => Value::Str(s.as_str().into()),
1405        Const::Bytes(b) => Value::Bytes(b.clone()),
1406        Const::Unit => Value::Unit,
1407        Const::FieldName(s) | Const::VariantName(s) | Const::NodeId(s) => Value::Str(s.as_str().into()),
1408    }
1409}