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::Value;
6use indexmap::IndexMap;
7
8#[derive(Debug, Clone, thiserror::Error)]
9pub enum VmError {
10    #[error("runtime panic: {0}")]
11    Panic(String),
12    #[error("type mismatch at runtime: {0}")]
13    TypeMismatch(String),
14    #[error("stack underflow")]
15    StackUnderflow,
16    #[error("unknown function id: {0}")]
17    UnknownFunction(u32),
18    #[error("effect handler error: {0}")]
19    Effect(String),
20    #[error("call stack overflow: recursion depth exceeded ({0})")]
21    CallStackOverflow(u32),
22    /// Refinement predicate failed at a call boundary (#209 slice 3).
23    /// Surfaced when a function declares `param :: Type{x | predicate}`,
24    /// the call-site arg couldn't be discharged statically (slice 2),
25    /// and the runtime evaluator finds the predicate is `false` for
26    /// the actual argument value. The `verdict` mirrors the shape of
27    /// `gate.verdict`-style records in `lex-trace`.
28    #[error("refinement violated: argument {param_index} of `{fn_name}` (binding `{binding}`): {reason}")]
29    RefinementFailed {
30        fn_name: String,
31        param_index: usize,
32        binding: String,
33        reason: String,
34    },
35}
36
37/// Maximum simultaneous call frames. Defends against unbounded
38/// recursion in agent-emitted code: a body that calls itself
39/// without a base case would otherwise blow the host's native
40/// stack and crash the process. Real Lex code rarely exceeds
41/// ~30 frames; 1024 is generous headroom while still well under
42/// the OS stack limit at any per-frame size we use.
43pub const MAX_CALL_DEPTH: u32 = 1024;
44
45/// Host-side effect dispatch. Implementors decide what `kind`/`op` mean
46/// and how arguments map to side effects.
47pub trait EffectHandler {
48    fn dispatch(&mut self, kind: &str, op: &str, args: Vec<Value>) -> Result<Value, String>;
49
50    /// Hook called by the VM at every function call so handlers can
51    /// enforce per-call budget consumption (#225). The argument is
52    /// the sum of `[budget(N)]` declared on the callee's signature;
53    /// the handler returns `Err` to refuse the call (the VM converts
54    /// to `VmError::Effect`). Default impl is a no-op so legacy
55    /// handlers and pure-only runs are unaffected.
56    fn note_call_budget(&mut self, _budget_cost: u64) -> Result<(), String> {
57        Ok(())
58    }
59}
60
61/// `Vm` exposes itself as a `ClosureCaller` so the parser interpreter
62/// can invoke user-supplied closures during a `parser.run` walk
63/// (#221). The Vm is reentrant for closure invocation: pushing a new
64/// frame onto an active call stack is supported, and the handler
65/// stays in place so any effects the closure body fires dispatch
66/// normally.
67impl<'a> crate::parser_runtime::ClosureCaller for Vm<'a> {
68    fn call_closure(&mut self, closure: Value, args: Vec<Value>) -> Result<Value, String> {
69        self.invoke_closure_value(closure, args)
70            .map_err(|e| format!("{e:?}"))
71    }
72}
73
74/// A handler that fails any effect call. Useful as a default for pure-only runs.
75pub struct DenyAllEffects;
76impl EffectHandler for DenyAllEffects {
77    fn dispatch(&mut self, kind: &str, op: &str, _args: Vec<Value>) -> Result<Value, String> {
78        Err(format!("effects not permitted (attempted {kind}.{op})"))
79    }
80}
81
82/// Trace receiver. Implementors record the call/effect tree and may
83/// substitute effect responses (for replay).
84pub trait Tracer {
85    fn enter_call(&mut self, node_id: &str, name: &str, args: &[Value]);
86    fn enter_effect(&mut self, node_id: &str, kind: &str, op: &str, args: &[Value]);
87    fn exit_ok(&mut self, value: &Value);
88    fn exit_err(&mut self, message: &str);
89    /// Tail-call optimization: pop the current frame's open call without
90    /// re-entering the parent (the new call takes its place).
91    fn exit_call_tail(&mut self);
92    /// During replay, return Some(v) to substitute an effect's output.
93    fn override_effect(&mut self, _node_id: &str) -> Option<Value> { None }
94}
95
96/// No-op tracer for normal execution.
97pub struct NullTracer;
98impl Tracer for NullTracer {
99    fn enter_call(&mut self, _: &str, _: &str, _: &[Value]) {}
100    fn enter_effect(&mut self, _: &str, _: &str, _: &str, _: &[Value]) {}
101    fn exit_ok(&mut self, _: &Value) {}
102    fn exit_err(&mut self, _: &str) {}
103    fn exit_call_tail(&mut self) {}
104}
105
106#[derive(Debug, Clone)]
107pub(crate) enum FrameKind {
108    /// Top-level entry frame; doesn't correspond to a Call opcode.
109    Entry,
110    /// Frame opened by Call/TailCall. The `String` is the originating
111    /// `NodeId`; useful for diagnostics even if currently unread.
112    Call(#[allow(dead_code)] String),
113}
114
115pub struct Vm<'a> {
116    program: &'a Program,
117    handler: Box<dyn EffectHandler + 'a>,
118    pub(crate) tracer: Box<dyn Tracer + 'a>,
119    /// Per-call frames. Each frame has its own locals array and pc.
120    frames: Vec<Frame>,
121    stack: Vec<Value>,
122    /// Soft cap to avoid runaway computations in tests.
123    pub step_limit: u64,
124    pub steps: u64,
125    /// Per-Vm memoization cache for pure functions (#229). Keyed by
126    /// `(fn_id, sha256(canonical_json(args))[..16])`. Effectful
127    /// functions never enter this map. The cache lives for the
128    /// lifetime of one `Vm::call` chain — calling `Vm::with_handler`
129    /// again starts a fresh cache.
130    pure_memo: std::collections::HashMap<(u32, [u8; 16]), Value>,
131    /// Diagnostic counters for `--trace` observability (#229).
132    pub pure_memo_hits: u64,
133    pub pure_memo_misses: u64,
134}
135
136struct Frame {
137    fn_id: u32,
138    pc: usize,
139    locals: Vec<Value>,
140    /// Stack base when this frame started (for cleanup on return).
141    stack_base: usize,
142    trace_kind: FrameKind,
143    /// Pure-fn memo key (#229). `Some(key)` if the call was eligible
144    /// for memoization and missed the cache; on Op::Return the key
145    /// is used to write the return value back into the cache.
146    /// `None` means "don't memoize" — either the function isn't pure,
147    /// the call wasn't through Op::Call, or memoization is disabled.
148    memo_key: Option<(u32, [u8; 16])>,
149}
150
151/// Sum of `[budget(N)]` declarations on a function's signature
152/// (#225). Used by Op::Call / Op::TailCall / Op::CallClosure to
153/// notify the EffectHandler of per-call budget cost so the handler
154/// can deduct from a shared pool and refuse calls that would
155/// exceed the policy ceiling. Negative `Int` args are ignored —
156/// the static check (`policy::check_program`) treats budgets as
157/// non-negative.
158fn call_budget_cost(f: &crate::program::Function) -> u64 {
159    let mut total: u64 = 0;
160    for e in &f.effects {
161        if e.kind == "budget" {
162            if let Some(crate::program::EffectArg::Int(n)) = &e.arg {
163                if *n >= 0 {
164                    total = total.saturating_add(*n as u64);
165                }
166            }
167        }
168    }
169    total
170}
171
172/// Hash the argument list for a pure-fn memoization lookup (#229).
173/// Routes through the canonical-JSON path so two semantically-equal
174/// argument lists produce the same hash regardless of how the
175/// containing `Value`s were assembled (Records use IndexMap so field
176/// order is already stable, but canon_json gives the same property
177/// for the inner serde_json shape).
178fn hash_call_args(args: &[Value]) -> [u8; 16] {
179    use sha2::{Digest, Sha256};
180    let json = serde_json::Value::Array(args.iter().map(Value::to_json).collect());
181    let canonical = lex_ast::canon_json::hash_canonical(&json);
182    let mut hasher = Sha256::new();
183    hasher.update(canonical);
184    let full = hasher.finalize();
185    let mut out = [0u8; 16];
186    out.copy_from_slice(&full[..16]);
187    out
188}
189
190/// Evaluate a refinement predicate at runtime against the actual
191/// argument value (#209 slice 3). Mirrors `lex_types::discharge`'s
192/// static evaluator but operates on `Value` directly.
193///
194/// Returns `Ok(true)` / `Ok(false)` for a clean boolean verdict, or
195/// `Err(reason)` if the predicate references something the runtime
196/// can't resolve (free variable beyond the binding, unsupported AST
197/// node). Callers map `Ok(false)` and `Err` to `VmError::RefinementFailed`.
198fn eval_refinement(
199    predicate: &lex_ast::CExpr,
200    binding: &str,
201    arg: &Value,
202) -> Result<bool, String> {
203    match eval_refinement_inner(predicate, binding, arg) {
204        Ok(Value::Bool(b)) => Ok(b),
205        Ok(other) => Err(format!("predicate didn't reduce to a Bool, got {other:?}")),
206        Err(e) => Err(e),
207    }
208}
209
210fn eval_refinement_inner(
211    e: &lex_ast::CExpr,
212    binding: &str,
213    arg: &Value,
214) -> Result<Value, String> {
215    use lex_ast::{CExpr, CLit};
216    match e {
217        CExpr::Literal { value } => Ok(match value {
218            CLit::Int { value } => Value::Int(*value),
219            CLit::Float { value } => Value::Float(value.parse().unwrap_or(0.0)),
220            CLit::Bool { value } => Value::Bool(*value),
221            CLit::Str { value } => Value::Str(value.clone()),
222            CLit::Bytes { value } => Value::Str(value.clone()), // hex; unusual in predicates
223            CLit::Unit => Value::Unit,
224        }),
225        CExpr::Var { name } if name == binding => Ok(arg.clone()),
226        CExpr::Var { name } => Err(format!(
227            "predicate references free var `{name}`; runtime check \
228             only resolves the binding (slice 4 will plumb call-site \
229             context)")),
230        CExpr::UnaryOp { op, expr } => {
231            let v = eval_refinement_inner(expr, binding, arg)?;
232            match (op.as_str(), v) {
233                ("not", Value::Bool(b)) => Ok(Value::Bool(!b)),
234                ("-", Value::Int(n)) => Ok(Value::Int(-n)),
235                ("-", Value::Float(n)) => Ok(Value::Float(-n)),
236                (o, v) => Err(format!("unsupported unary `{o}` on {v:?}")),
237            }
238        }
239        CExpr::BinOp { op, lhs, rhs } => {
240            // Short-circuit `and` / `or` for the same reasons as the
241            // static evaluator.
242            if op == "and" || op == "or" {
243                let l = eval_refinement_inner(lhs, binding, arg)?;
244                let lb = match l {
245                    Value::Bool(b) => b,
246                    other => return Err(format!("`{op}` on non-bool: {other:?}")),
247                };
248                if op == "and" && !lb { return Ok(Value::Bool(false)); }
249                if op == "or"  &&  lb { return Ok(Value::Bool(true));  }
250                let r = eval_refinement_inner(rhs, binding, arg)?;
251                return match r {
252                    Value::Bool(b) => Ok(Value::Bool(b)),
253                    other => Err(format!("`{op}` on non-bool: {other:?}")),
254                };
255            }
256            let l = eval_refinement_inner(lhs, binding, arg)?;
257            let r = eval_refinement_inner(rhs, binding, arg)?;
258            apply_refinement_binop(op, &l, &r)
259        }
260        // Other AST forms (Call, Let, Match, FieldAccess, Lambda,
261        // Block, Constructors, Records, Tuples, Lists, Return) need
262        // a more general evaluator that can call back into the VM.
263        // Out of scope for slice 3; a future slice may unify this
264        // with the spec-checker's gate evaluator.
265        other => Err(format!("unsupported predicate node: {other:?}")),
266    }
267}
268
269fn apply_refinement_binop(op: &str, l: &Value, r: &Value) -> Result<Value, String> {
270    use Value::*;
271    match (op, l, r) {
272        ("+", Int(a), Int(b)) => Ok(Int(a + b)),
273        ("-", Int(a), Int(b)) => Ok(Int(a - b)),
274        ("*", Int(a), Int(b)) => Ok(Int(a * b)),
275        ("/", Int(a), Int(b)) if *b != 0 => Ok(Int(a / b)),
276        ("%", Int(a), Int(b)) if *b != 0 => Ok(Int(a % b)),
277        ("+", Float(a), Float(b)) => Ok(Float(a + b)),
278        ("-", Float(a), Float(b)) => Ok(Float(a - b)),
279        ("*", Float(a), Float(b)) => Ok(Float(a * b)),
280        ("/", Float(a), Float(b)) => Ok(Float(a / b)),
281
282        ("==", a, b) => Ok(Bool(a == b)),
283        ("!=", a, b) => Ok(Bool(a != b)),
284
285        ("<",  Int(a), Int(b)) => Ok(Bool(a < b)),
286        ("<=", Int(a), Int(b)) => Ok(Bool(a <= b)),
287        (">",  Int(a), Int(b)) => Ok(Bool(a > b)),
288        (">=", Int(a), Int(b)) => Ok(Bool(a >= b)),
289
290        ("<",  Float(a), Float(b)) => Ok(Bool(a < b)),
291        ("<=", Float(a), Float(b)) => Ok(Bool(a <= b)),
292        (">",  Float(a), Float(b)) => Ok(Bool(a > b)),
293        (">=", Float(a), Float(b)) => Ok(Bool(a >= b)),
294
295        (op, a, b) => Err(format!(
296            "unsupported binop `{op}` on {a:?} and {b:?}")),
297    }
298}
299
300fn const_str(constants: &[Const], idx: u32) -> String {
301    match constants.get(idx as usize) {
302        Some(Const::NodeId(s)) | Some(Const::Str(s)) => s.clone(),
303        _ => String::new(),
304    }
305}
306
307impl<'a> Vm<'a> {
308    pub fn new(program: &'a Program) -> Self {
309        Self::with_handler(program, Box::new(DenyAllEffects))
310    }
311
312    pub fn with_handler(program: &'a Program, handler: Box<dyn EffectHandler + 'a>) -> Self {
313        Self {
314            program,
315            handler,
316            tracer: Box::new(NullTracer),
317            frames: Vec::new(),
318            stack: Vec::new(),
319            step_limit: 10_000_000,
320            steps: 0,
321            pure_memo: std::collections::HashMap::new(),
322            pure_memo_hits: 0,
323            pure_memo_misses: 0,
324        }
325    }
326
327    pub fn set_tracer(&mut self, tracer: Box<dyn Tracer + 'a>) {
328        self.tracer = tracer;
329    }
330
331    /// Cap the number of opcode dispatches before the VM aborts with
332    /// `step limit exceeded`. Useful as a runtime DoS guard against
333    /// untrusted code (e.g. the `agent-tool` sandbox, where an LLM
334    /// could emit `list.fold(list.range(0, 1_000_000_000), …)` to hang
335    /// the host). Default is 10_000_000.
336    pub fn set_step_limit(&mut self, limit: u64) {
337        self.step_limit = limit;
338    }
339
340    pub fn call(&mut self, name: &str, args: Vec<Value>) -> Result<Value, VmError> {
341        let fn_id = self.program.lookup(name).ok_or_else(|| VmError::Panic(format!("no function `{name}`")))?;
342        self.invoke(fn_id, args)
343    }
344
345    /// Vm-level handler for `parser.run` (#221). Routed here from
346    /// `Op::EffectCall` rather than through the `EffectHandler` so
347    /// the recursive parser interpreter has reentrant Vm access for
348    /// closure invocation. Returns the wrapped `Result[T, ParseErr]`
349    /// value the language sees.
350    fn run_parser_op(&mut self, args: Vec<Value>) -> Result<Value, String> {
351        let parser = args.first().cloned()
352            .ok_or_else(|| "parser.run: missing parser arg".to_string())?;
353        let input = match args.get(1) {
354            Some(Value::Str(s)) => s.clone(),
355            _ => return Err("parser.run: input must be Str".into()),
356        };
357        match crate::parser_runtime::run_parser(&parser, &input, 0, self) {
358            Ok((value, _pos)) => Ok(Value::Variant {
359                name: "Ok".into(),
360                args: vec![value],
361            }),
362            Err((pos, msg)) => {
363                let mut e = indexmap::IndexMap::new();
364                e.insert("pos".into(), Value::Int(pos as i64));
365                e.insert("message".into(), Value::Str(msg));
366                Ok(Value::Variant {
367                    name: "Err".into(),
368                    args: vec![Value::Record(e)],
369                })
370            }
371        }
372    }
373
374    /// Invoke a `Value::Closure` by combining its captures with the
375    /// supplied call args and dispatching to the underlying function.
376    /// Used by the parser interpreter (#221) to call user-supplied
377    /// `f` arguments inside `parser.map` / `parser.and_then` nodes.
378    pub fn invoke_closure_value(
379        &mut self,
380        closure: Value,
381        args: Vec<Value>,
382    ) -> Result<Value, VmError> {
383        let (fn_id, captures) = match closure {
384            Value::Closure { fn_id, captures, .. } => (fn_id, captures),
385            other => return Err(VmError::TypeMismatch(
386                format!("invoke_closure_value: not a closure: {other:?}"))),
387        };
388        let mut combined = captures;
389        combined.extend(args);
390        self.invoke(fn_id, combined)
391    }
392
393    pub fn invoke(&mut self, fn_id: u32, args: Vec<Value>) -> Result<Value, VmError> {
394        let f = &self.program.functions[fn_id as usize];
395        if args.len() != f.arity as usize {
396            return Err(VmError::Panic(format!("arity mismatch calling {}", f.name)));
397        }
398        // Refinement runtime check at the public entry point too
399        // (#209 slice 3). `Op::Call` checks for in-program calls;
400        // this branch covers `vm.call("entry", ...)` from the host
401        // and the reentrant `invoke_closure_value` path. Same
402        // semantics, same error shape.
403        let f_name = f.name.clone();
404        let refinements = f.refinements.clone();
405        for (i, refinement) in refinements.iter().enumerate() {
406            if let Some(r) = refinement {
407                let arg = args.get(i).cloned().unwrap_or(Value::Unit);
408                match eval_refinement(&r.predicate, &r.binding, &arg) {
409                    Ok(true) => {}
410                    Ok(false) => return Err(VmError::RefinementFailed {
411                        fn_name: f_name,
412                        param_index: i,
413                        binding: r.binding.clone(),
414                        reason: format!("predicate failed for {} = {arg:?}", r.binding),
415                    }),
416                    Err(reason) => return Err(VmError::RefinementFailed {
417                        fn_name: f_name,
418                        param_index: i,
419                        binding: r.binding.clone(),
420                        reason,
421                    }),
422                }
423            }
424        }
425        let f = &self.program.functions[fn_id as usize];
426        let mut locals = vec![Value::Unit; f.locals_count.max(f.arity) as usize];
427        for (i, v) in args.into_iter().enumerate() { locals[i] = v; }
428        // Record the depth before pushing — this is what `run` will
429        // exit at, supporting reentrant invocation from inside the
430        // VM (e.g. the parser interpreter calling closures, #221).
431        let base_depth = self.frames.len();
432        self.push_frame(Frame {
433            fn_id, pc: 0, locals, stack_base: self.stack.len(),
434            trace_kind: FrameKind::Entry,
435            memo_key: None,
436        })?;
437        self.run_to(base_depth)
438    }
439
440    /// All call-frame pushes funnel through here so the depth
441    /// check can't be skipped by a missing branch. Returns
442    /// `CallStackOverflow` instead of letting recursion blow the
443    /// host's native stack.
444    fn push_frame(&mut self, frame: Frame) -> Result<(), VmError> {
445        if self.frames.len() as u32 >= MAX_CALL_DEPTH {
446            return Err(VmError::CallStackOverflow(MAX_CALL_DEPTH));
447        }
448        self.frames.push(frame);
449        Ok(())
450    }
451
452    /// Run until the frame stack drops to `base_depth`. Required for
453    /// reentrant invocation: a `Vm::invoke` call from inside an
454    /// already-running `run()` must return when *its* frame returns,
455    /// not when the entire frame stack empties (#221).
456    fn run_to(&mut self, base_depth: usize) -> Result<Value, VmError> {
457        loop {
458            if self.steps > self.step_limit {
459                return Err(VmError::Panic(format!(
460                    "step limit exceeded ({} > {})",
461                    self.steps, self.step_limit,
462                )));
463            }
464            self.steps += 1;
465            let frame_idx = self.frames.len() - 1;
466            let pc = self.frames[frame_idx].pc;
467            let fn_id = self.frames[frame_idx].fn_id;
468            let code = &self.program.functions[fn_id as usize].code;
469            if pc >= code.len() {
470                return Err(VmError::Panic("ran past end of code".into()));
471            }
472            let op = code[pc].clone();
473            self.frames[frame_idx].pc = pc + 1;
474
475            match op {
476                Op::PushConst(i) => {
477                    let c = &self.program.constants[i as usize];
478                    self.stack.push(const_to_value(c));
479                }
480                Op::Pop => { self.pop()?; }
481                Op::Dup => {
482                    let v = self.peek()?.clone();
483                    self.stack.push(v);
484                }
485                Op::LoadLocal(i) => {
486                    let v = self.frames[frame_idx].locals[i as usize].clone();
487                    self.stack.push(v);
488                }
489                Op::StoreLocal(i) => {
490                    let v = self.pop()?;
491                    self.frames[frame_idx].locals[i as usize] = v;
492                }
493                Op::MakeRecord { field_name_indices } => {
494                    let n = field_name_indices.len();
495                    let mut values: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
496                    for i in (0..n).rev() {
497                        values[i] = self.pop()?;
498                    }
499                    let mut rec = IndexMap::new();
500                    for (i, val) in values.into_iter().enumerate() {
501                        let name = match &self.program.constants[field_name_indices[i] as usize] {
502                            Const::FieldName(s) => s.clone(),
503                            _ => return Err(VmError::TypeMismatch("expected FieldName const".into())),
504                        };
505                        rec.insert(name, val);
506                    }
507                    self.stack.push(Value::Record(rec));
508                }
509                Op::MakeTuple(n) => {
510                    let mut items: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
511                    for i in (0..n as usize).rev() { items[i] = self.pop()?; }
512                    self.stack.push(Value::Tuple(items));
513                }
514                Op::MakeList(n) => {
515                    let mut items: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
516                    for i in (0..n as usize).rev() { items[i] = self.pop()?; }
517                    self.stack.push(Value::List(items));
518                }
519                Op::MakeVariant { name_idx, arity } => {
520                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
521                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
522                    let name = match &self.program.constants[name_idx as usize] {
523                        Const::VariantName(s) => s.clone(),
524                        _ => return Err(VmError::TypeMismatch("expected VariantName const".into())),
525                    };
526                    self.stack.push(Value::Variant { name, args });
527                }
528                Op::GetField(i) => {
529                    let name = match &self.program.constants[i as usize] {
530                        Const::FieldName(s) => s.clone(),
531                        _ => return Err(VmError::TypeMismatch("expected FieldName const".into())),
532                    };
533                    let v = self.pop()?;
534                    match v {
535                        Value::Record(r) => {
536                            let v = r.get(&name).cloned()
537                                .ok_or_else(|| VmError::TypeMismatch(format!("missing field `{name}`")))?;
538                            self.stack.push(v);
539                        }
540                        other => return Err(VmError::TypeMismatch(format!("GetField on non-record: {other:?}"))),
541                    }
542                }
543                Op::GetElem(i) => {
544                    let v = self.pop()?;
545                    match v {
546                        Value::Tuple(items) => {
547                            let v = items.into_iter().nth(i as usize)
548                                .ok_or_else(|| VmError::TypeMismatch(format!("tuple index {i} out of range")))?;
549                            self.stack.push(v);
550                        }
551                        other => return Err(VmError::TypeMismatch(format!("GetElem on non-tuple: {other:?}"))),
552                    }
553                }
554                Op::TestVariant(i) => {
555                    let name = match &self.program.constants[i as usize] {
556                        Const::VariantName(s) => s.clone(),
557                        _ => return Err(VmError::TypeMismatch("expected VariantName const".into())),
558                    };
559                    let v = self.pop()?;
560                    match &v {
561                        Value::Variant { name: vname, .. } => {
562                            self.stack.push(Value::Bool(vname == &name));
563                        }
564                        // For tag-only enums of primitive type (e.g. ParseError = Empty | NotNumber)
565                        // the value is currently a Variant too, since constructors emit MakeVariant.
566                        other => return Err(VmError::TypeMismatch(format!("TestVariant on non-variant: {other:?}"))),
567                    }
568                }
569                Op::GetVariant(_i) => {
570                    let v = self.pop()?;
571                    match v {
572                        Value::Variant { args, .. } => {
573                            self.stack.push(Value::Tuple(args));
574                        }
575                        other => return Err(VmError::TypeMismatch(format!("GetVariant on non-variant: {other:?}"))),
576                    }
577                }
578                Op::GetVariantArg(i) => {
579                    let v = self.pop()?;
580                    match v {
581                        Value::Variant { mut args, .. } => {
582                            if (i as usize) >= args.len() {
583                                return Err(VmError::TypeMismatch("variant arg index oob".into()));
584                            }
585                            self.stack.push(args.swap_remove(i as usize));
586                        }
587                        other => return Err(VmError::TypeMismatch(format!("GetVariantArg on non-variant: {other:?}"))),
588                    }
589                }
590                Op::GetListLen => {
591                    let v = self.pop()?;
592                    match v {
593                        Value::List(items) => self.stack.push(Value::Int(items.len() as i64)),
594                        other => return Err(VmError::TypeMismatch(format!("GetListLen on non-list: {other:?}"))),
595                    }
596                }
597                Op::GetListElem(i) => {
598                    let v = self.pop()?;
599                    match v {
600                        Value::List(items) => {
601                            let v = items.into_iter().nth(i as usize)
602                                .ok_or_else(|| VmError::TypeMismatch("list index oob".into()))?;
603                            self.stack.push(v);
604                        }
605                        other => return Err(VmError::TypeMismatch(format!("GetListElem on non-list: {other:?}"))),
606                    }
607                }
608                Op::GetListElemDyn => {
609                    // Stack: [list, idx]
610                    let idx = match self.pop()? {
611                        Value::Int(n) => n as usize,
612                        other => return Err(VmError::TypeMismatch(format!("GetListElemDyn idx: {other:?}"))),
613                    };
614                    let v = self.pop()?;
615                    match v {
616                        Value::List(items) => {
617                            let v = items.into_iter().nth(idx)
618                                .ok_or_else(|| VmError::TypeMismatch("list index oob".into()))?;
619                            self.stack.push(v);
620                        }
621                        other => return Err(VmError::TypeMismatch(format!("GetListElemDyn on non-list: {other:?}"))),
622                    }
623                }
624                Op::ListAppend => {
625                    let value = self.pop()?;
626                    let list = self.pop()?;
627                    match list {
628                        Value::List(mut items) => {
629                            items.push(value);
630                            self.stack.push(Value::List(items));
631                        }
632                        other => return Err(VmError::TypeMismatch(format!("ListAppend on non-list: {other:?}"))),
633                    }
634                }
635                Op::Jump(off) => {
636                    let new_pc = (self.frames[frame_idx].pc as i32 + off) as usize;
637                    self.frames[frame_idx].pc = new_pc;
638                }
639                Op::JumpIf(off) => {
640                    let v = self.pop()?;
641                    if v.as_bool() {
642                        let new_pc = (self.frames[frame_idx].pc as i32 + off) as usize;
643                        self.frames[frame_idx].pc = new_pc;
644                    }
645                }
646                Op::JumpIfNot(off) => {
647                    let v = self.pop()?;
648                    if !v.as_bool() {
649                        let new_pc = (self.frames[frame_idx].pc as i32 + off) as usize;
650                        self.frames[frame_idx].pc = new_pc;
651                    }
652                }
653                Op::MakeClosure { fn_id, capture_count } => {
654                    let n = capture_count as usize;
655                    let mut captures: Vec<Value> = (0..n).map(|_| Value::Unit).collect();
656                    for i in (0..n).rev() { captures[i] = self.pop()?; }
657                    // Look up the canonical body hash so the resulting
658                    // `Value::Closure` carries it for equality (#222).
659                    let body_hash = self.program.functions[fn_id as usize].body_hash;
660                    self.stack.push(Value::Closure { fn_id, body_hash, captures });
661                }
662                Op::CallClosure { arity, node_id_idx } => {
663                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
664                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
665                    let closure = self.pop()?;
666                    let (fn_id, captures) = match closure {
667                        Value::Closure { fn_id, captures, .. } => (fn_id, captures),
668                        other => return Err(VmError::TypeMismatch(format!("CallClosure on non-closure: {other:?}"))),
669                    };
670                    let node_id = const_str(&self.program.constants, node_id_idx);
671                    let callee_name = self.program.functions[fn_id as usize].name.clone();
672                    let budget_cost = call_budget_cost(&self.program.functions[fn_id as usize]);
673                    if budget_cost > 0 {
674                        self.handler.note_call_budget(budget_cost)
675                            .map_err(VmError::Effect)?;
676                    }
677                    let mut combined = captures;
678                    combined.extend(args);
679                    self.tracer.enter_call(&node_id, &callee_name, &combined);
680                    let f = &self.program.functions[fn_id as usize];
681                    let mut locals = vec![Value::Unit; f.locals_count.max(f.arity) as usize];
682                    for (i, v) in combined.into_iter().enumerate() { locals[i] = v; }
683                    self.push_frame(Frame {
684                        fn_id, pc: 0, locals, stack_base: self.stack.len(),
685                        trace_kind: FrameKind::Call(node_id),
686                        // Op::CallClosure intentionally doesn't memoize
687                        // for v1 (#229) — closures over captures need a
688                        // hashing strategy that includes the captures.
689                        // Direct Op::Call is the v1 surface.
690                        memo_key: None,
691                    })?;
692                }
693                Op::Call { fn_id, arity, node_id_idx } => {
694                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
695                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
696                    let node_id = const_str(&self.program.constants, node_id_idx);
697                    let callee_name = self.program.functions[fn_id as usize].name.clone();
698                    let budget_cost = call_budget_cost(&self.program.functions[fn_id as usize]);
699                    if budget_cost > 0 {
700                        self.handler.note_call_budget(budget_cost)
701                            .map_err(VmError::Effect)?;
702                    }
703                    // Refinement runtime check (#209 slice 3). Each
704                    // param's `Option<Refinement>` is evaluated against
705                    // the actual arg before the frame is pushed. The
706                    // tracer sees the call enter; failure surfaces as
707                    // `VmError::RefinementFailed` *before* the body
708                    // starts, which means an erroring trace shows the
709                    // call as enter+exit_err with the verdict reason
710                    // (same shape as `gate.verdict`).
711                    let refinements = self.program.functions[fn_id as usize]
712                        .refinements.clone();
713                    for (i, refinement) in refinements.iter().enumerate() {
714                        if let Some(r) = refinement {
715                            let arg = args.get(i).cloned().unwrap_or(Value::Unit);
716                            match eval_refinement(&r.predicate, &r.binding, &arg) {
717                                Ok(true) => { /* satisfied, continue */ }
718                                Ok(false) => {
719                                    return Err(VmError::RefinementFailed {
720                                        fn_name: callee_name.clone(),
721                                        param_index: i,
722                                        binding: r.binding.clone(),
723                                        reason: format!(
724                                            "predicate failed for {} = {arg:?}",
725                                            r.binding),
726                                    });
727                                }
728                                Err(reason) => {
729                                    return Err(VmError::RefinementFailed {
730                                        fn_name: callee_name.clone(),
731                                        param_index: i,
732                                        binding: r.binding.clone(),
733                                        reason,
734                                    });
735                                }
736                            }
737                        }
738                    }
739                    // Pure-fn memoization (#229): if the callee declares
740                    // no effects, hash the args and consult the cache.
741                    // On hit, push the cached value, emit synthetic
742                    // enter+exit trace events (so the trace still shows
743                    // the call), and skip the frame push entirely.
744                    let f = &self.program.functions[fn_id as usize];
745                    let memo_key: Option<(u32, [u8; 16])> = if f.effects.is_empty() {
746                        Some((fn_id, hash_call_args(&args)))
747                    } else {
748                        None
749                    };
750                    if let Some(key) = memo_key {
751                        if let Some(cached) = self.pure_memo.get(&key).cloned() {
752                            self.pure_memo_hits += 1;
753                            self.tracer.enter_call(&node_id, &callee_name, &args);
754                            self.tracer.exit_ok(&cached);
755                            self.stack.push(cached);
756                            continue;
757                        }
758                        self.pure_memo_misses += 1;
759                    }
760                    self.tracer.enter_call(&node_id, &callee_name, &args);
761                    let mut locals = vec![Value::Unit; f.locals_count.max(f.arity) as usize];
762                    for (i, v) in args.into_iter().enumerate() { locals[i] = v; }
763                    self.push_frame(Frame {
764                        fn_id, pc: 0, locals, stack_base: self.stack.len(),
765                        trace_kind: FrameKind::Call(node_id),
766                        memo_key,
767                    })?;
768                }
769                Op::TailCall { fn_id, arity, node_id_idx } => {
770                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
771                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
772                    let node_id = const_str(&self.program.constants, node_id_idx);
773                    let callee_name = self.program.functions[fn_id as usize].name.clone();
774                    let budget_cost = call_budget_cost(&self.program.functions[fn_id as usize]);
775                    if budget_cost > 0 {
776                        self.handler.note_call_budget(budget_cost)
777                            .map_err(VmError::Effect)?;
778                    }
779                    // Refinement runtime check on tail calls too
780                    // (#209 slice 3). Same shape as Op::Call.
781                    let refinements = self.program.functions[fn_id as usize]
782                        .refinements.clone();
783                    for (i, refinement) in refinements.iter().enumerate() {
784                        if let Some(r) = refinement {
785                            let arg = args.get(i).cloned().unwrap_or(Value::Unit);
786                            match eval_refinement(&r.predicate, &r.binding, &arg) {
787                                Ok(true) => {}
788                                Ok(false) => return Err(VmError::RefinementFailed {
789                                    fn_name: callee_name.clone(),
790                                    param_index: i,
791                                    binding: r.binding.clone(),
792                                    reason: format!(
793                                        "predicate failed for {} = {arg:?}",
794                                        r.binding),
795                                }),
796                                Err(reason) => return Err(VmError::RefinementFailed {
797                                    fn_name: callee_name.clone(),
798                                    param_index: i,
799                                    binding: r.binding.clone(),
800                                    reason,
801                                }),
802                            }
803                        }
804                    }
805                    // A tail call closes the current call's trace frame and
806                    // opens a new one in its place — preserves the caller's
807                    // tree depth in the trace.
808                    self.tracer.exit_call_tail();
809                    self.tracer.enter_call(&node_id, &callee_name, &args);
810                    let f = &self.program.functions[fn_id as usize];
811                    let frame = self.frames.last_mut().unwrap();
812                    frame.fn_id = fn_id;
813                    frame.pc = 0;
814                    frame.locals = vec![Value::Unit; f.locals_count.max(f.arity) as usize];
815                    for (i, v) in args.into_iter().enumerate() { frame.locals[i] = v; }
816                    frame.trace_kind = FrameKind::Call(node_id);
817                }
818                Op::EffectCall { kind_idx, op_idx, arity, node_id_idx } => {
819                    let mut args: Vec<Value> = (0..arity).map(|_| Value::Unit).collect();
820                    for i in (0..arity as usize).rev() { args[i] = self.pop()?; }
821                    let kind = match &self.program.constants[kind_idx as usize] {
822                        Const::Str(s) => s.clone(),
823                        _ => return Err(VmError::TypeMismatch("expected Str const for effect kind".into())),
824                    };
825                    let op_name = match &self.program.constants[op_idx as usize] {
826                        Const::Str(s) => s.clone(),
827                        _ => return Err(VmError::TypeMismatch("expected Str const for effect op".into())),
828                    };
829                    let node_id = const_str(&self.program.constants, node_id_idx);
830                    self.tracer.enter_effect(&node_id, &kind, &op_name, &args);
831                    let result = match self.tracer.override_effect(&node_id) {
832                        Some(v) => Ok(v),
833                        // VM-level intercept for `parser.run` (#221).
834                        // Routed inline rather than through the handler
835                        // because the parser interpreter needs reentrant
836                        // VM access to invoke `Value::Closure` values
837                        // from `Map` / `AndThen` nodes.
838                        None if (kind.as_str(), op_name.as_str()) == ("parser", "run")
839                            => self.run_parser_op(args.clone()),
840                        None => self.handler.dispatch(&kind, &op_name, args.clone()),
841                    };
842                    match result {
843                        Ok(v) => {
844                            self.tracer.exit_ok(&v);
845                            self.stack.push(v);
846                        }
847                        Err(e) => {
848                            self.tracer.exit_err(&e);
849                            return Err(VmError::Effect(e));
850                        }
851                    }
852                }
853                Op::Return => {
854                    let v = self.pop()?;
855                    let frame = self.frames.pop().unwrap();
856                    // Trim any extra stuff that the function pushed but didn't pop.
857                    self.stack.truncate(frame.stack_base);
858                    if matches!(frame.trace_kind, FrameKind::Call(_)) {
859                        self.tracer.exit_ok(&v);
860                    }
861                    // Pure-fn memoization (#229): if this frame was a
862                    // memoizable call that missed the cache, write the
863                    // computed return value back so the next call with
864                    // the same args returns it without re-executing.
865                    if let Some(key) = frame.memo_key {
866                        self.pure_memo.insert(key, v.clone());
867                    }
868                    // Exit when we've returned past the depth this
869                    // `run_to` was entered at — supports reentrancy
870                    // (a nested `invoke` returns into its caller, not
871                    // out of the outermost VM run, #221).
872                    if self.frames.len() <= base_depth {
873                        return Ok(v);
874                    }
875                    self.stack.push(v);
876                }
877                Op::Panic(i) => {
878                    let msg = match &self.program.constants[i as usize] {
879                        Const::Str(s) => s.clone(),
880                        _ => "panic".into(),
881                    };
882                    return Err(VmError::Panic(msg));
883                }
884                // Arithmetic
885                Op::IntAdd => self.bin_int(|a, b| Value::Int(a + b))?,
886                Op::IntSub => self.bin_int(|a, b| Value::Int(a - b))?,
887                Op::IntMul => self.bin_int(|a, b| Value::Int(a * b))?,
888                Op::IntDiv => self.bin_int(|a, b| Value::Int(a / b))?,
889                Op::IntMod => self.bin_int(|a, b| Value::Int(a % b))?,
890                Op::IntNeg => {
891                    let a = self.pop()?.as_int();
892                    self.stack.push(Value::Int(-a));
893                }
894                Op::IntEq => self.bin_int(|a, b| Value::Bool(a == b))?,
895                Op::IntLt => self.bin_int(|a, b| Value::Bool(a < b))?,
896                Op::IntLe => self.bin_int(|a, b| Value::Bool(a <= b))?,
897                Op::FloatAdd => self.bin_float(|a, b| Value::Float(a + b))?,
898                Op::FloatSub => self.bin_float(|a, b| Value::Float(a - b))?,
899                Op::FloatMul => self.bin_float(|a, b| Value::Float(a * b))?,
900                Op::FloatDiv => self.bin_float(|a, b| Value::Float(a / b))?,
901                Op::FloatNeg => {
902                    let a = self.pop()?.as_float();
903                    self.stack.push(Value::Float(-a));
904                }
905                Op::FloatEq => self.bin_float(|a, b| Value::Bool(a == b))?,
906                Op::FloatLt => self.bin_float(|a, b| Value::Bool(a < b))?,
907                Op::FloatLe => self.bin_float(|a, b| Value::Bool(a <= b))?,
908                Op::NumAdd => self.bin_num(|a, b| Value::Int(a + b), |a, b| Value::Float(a + b))?,
909                Op::NumSub => self.bin_num(|a, b| Value::Int(a - b), |a, b| Value::Float(a - b))?,
910                Op::NumMul => self.bin_num(|a, b| Value::Int(a * b), |a, b| Value::Float(a * b))?,
911                Op::NumDiv => self.bin_num(|a, b| Value::Int(a / b), |a, b| Value::Float(a / b))?,
912                Op::NumMod => self.bin_int(|a, b| Value::Int(a % b))?,
913                Op::NumNeg => {
914                    let v = self.pop()?;
915                    match v {
916                        Value::Int(n) => self.stack.push(Value::Int(-n)),
917                        Value::Float(f) => self.stack.push(Value::Float(-f)),
918                        other => return Err(VmError::TypeMismatch(format!("NumNeg on {other:?}"))),
919                    }
920                }
921                Op::NumEq => self.bin_eq()?,
922                Op::NumLt => self.bin_num(|a, b| Value::Bool(a < b), |a, b| Value::Bool(a < b))?,
923                Op::NumLe => self.bin_num(|a, b| Value::Bool(a <= b), |a, b| Value::Bool(a <= b))?,
924                Op::BoolAnd => {
925                    let b = self.pop()?.as_bool();
926                    let a = self.pop()?.as_bool();
927                    self.stack.push(Value::Bool(a && b));
928                }
929                Op::BoolOr => {
930                    let b = self.pop()?.as_bool();
931                    let a = self.pop()?.as_bool();
932                    self.stack.push(Value::Bool(a || b));
933                }
934                Op::BoolNot => {
935                    let a = self.pop()?.as_bool();
936                    self.stack.push(Value::Bool(!a));
937                }
938                Op::StrConcat => {
939                    let b = self.pop()?;
940                    let a = self.pop()?;
941                    let s = format!("{}{}", a.as_str(), b.as_str());
942                    self.stack.push(Value::Str(s));
943                }
944                Op::StrLen => {
945                    let v = self.pop()?;
946                    self.stack.push(Value::Int(v.as_str().len() as i64));
947                }
948                Op::StrEq => {
949                    let b = self.pop()?;
950                    let a = self.pop()?;
951                    self.stack.push(Value::Bool(a.as_str() == b.as_str()));
952                }
953                Op::BytesLen => {
954                    let v = self.pop()?;
955                    match v {
956                        Value::Bytes(b) => self.stack.push(Value::Int(b.len() as i64)),
957                        other => return Err(VmError::TypeMismatch(format!("BytesLen on {other:?}"))),
958                    }
959                }
960                Op::BytesEq => {
961                    let b = self.pop()?;
962                    let a = self.pop()?;
963                    let eq = match (a, b) {
964                        (Value::Bytes(x), Value::Bytes(y)) => x == y,
965                        _ => return Err(VmError::TypeMismatch("BytesEq operands".into())),
966                    };
967                    self.stack.push(Value::Bool(eq));
968                }
969            }
970        }
971    }
972
973    fn pop(&mut self) -> Result<Value, VmError> {
974        self.stack.pop().ok_or(VmError::StackUnderflow)
975    }
976    fn peek(&self) -> Result<&Value, VmError> {
977        self.stack.last().ok_or(VmError::StackUnderflow)
978    }
979
980    fn bin_int(&mut self, f: impl Fn(i64, i64) -> Value) -> Result<(), VmError> {
981        let b = self.pop()?.as_int();
982        let a = self.pop()?.as_int();
983        self.stack.push(f(a, b));
984        Ok(())
985    }
986    fn bin_float(&mut self, f: impl Fn(f64, f64) -> Value) -> Result<(), VmError> {
987        let b = self.pop()?.as_float();
988        let a = self.pop()?.as_float();
989        self.stack.push(f(a, b));
990        Ok(())
991    }
992    fn bin_num(
993        &mut self,
994        i: impl Fn(i64, i64) -> Value,
995        f: impl Fn(f64, f64) -> Value,
996    ) -> Result<(), VmError> {
997        let b = self.pop()?;
998        let a = self.pop()?;
999        match (a, b) {
1000            (Value::Int(x), Value::Int(y)) => { self.stack.push(i(x, y)); Ok(()) }
1001            (Value::Float(x), Value::Float(y)) => { self.stack.push(f(x, y)); Ok(()) }
1002            (a, b) => Err(VmError::TypeMismatch(format!("Num op: {a:?} {b:?}"))),
1003        }
1004    }
1005    fn bin_eq(&mut self) -> Result<(), VmError> {
1006        let b = self.pop()?;
1007        let a = self.pop()?;
1008        self.stack.push(Value::Bool(a == b));
1009        Ok(())
1010    }
1011}
1012
1013fn const_to_value(c: &Const) -> Value {
1014    match c {
1015        Const::Int(n) => Value::Int(*n),
1016        Const::Float(f) => Value::Float(*f),
1017        Const::Bool(b) => Value::Bool(*b),
1018        Const::Str(s) => Value::Str(s.clone()),
1019        Const::Bytes(b) => Value::Bytes(b.clone()),
1020        Const::Unit => Value::Unit,
1021        Const::FieldName(s) | Const::VariantName(s) | Const::NodeId(s) => Value::Str(s.clone()),
1022    }
1023}