pub struct Vm<'a> {
pub step_limit: u64,
pub steps: u64,
pub pure_memo_hits: u64,
pub pure_memo_misses: u64,
pub pure_memo_skips: u64,
pub stack_record_allocs: u64,
pub stack_record_heap_fallbacks: u64,
pub heap_record_allocs: u64,
pub arena_record_allocs: u64,
pub arena_record_heap_fallbacks: u64,
/* private fields */
}Fields§
§step_limit: u64Soft cap to avoid runaway computations in tests.
steps: u64§pure_memo_hits: u64Diagnostic counters for --trace observability (#229).
pure_memo_misses: u64§pure_memo_skips: u64Number of effect-free calls that skipped the cache entirely because adaptive memoization disabled their function (#229 adaptive). Observability only.
stack_record_allocs: u64Per-Vm counters for #464 acceptance measurement. Incremented
on every Op::MakeRecord / Op::AllocStackRecord dispatch.
The bench reads these to compute the stack-allocation rate
(≥ 60% of records on the stack is the acceptance bar). Cheap
in the hot path — two unconditional u64 increments per record.
stack_record_heap_fallbacks: u64§heap_record_allocs: u64§arena_record_allocs: u64Counters for #463 slice-2b acceptance (will be the arena-allocation-rate gate, paralleling the #464 stack-rate counters above). Incremented in the op handlers; harmless in slice 2a since codegen doesn’t emit the ops yet.
arena_record_heap_fallbacks: u64Implementations§
Source§impl<'a> Vm<'a>
impl<'a> Vm<'a>
pub fn new(program: &'a Program) -> Self
pub fn with_handler( program: &'a Program, handler: Box<dyn EffectHandler + 'a>, ) -> Self
pub fn set_tracer(&mut self, tracer: Box<dyn Tracer + 'a>)
Sourcepub fn set_jit_hook(&mut self, hook: Option<Box<dyn JitHook + 'a>>)
pub fn set_jit_hook(&mut self, hook: Option<Box<dyn JitHook + 'a>>)
Install (or replace) the JIT hook consulted by Op::Call’s
dispatch arm. With None, dispatch behaves exactly as before
— the hook check is a single null-option branch the optimizer
can hoist. See the crate::jit_hook module for the
contract callers must uphold.
Sourcepub fn set_step_limit(&mut self, limit: u64)
pub fn set_step_limit(&mut self, limit: u64)
Cap the number of opcode dispatches before the VM aborts with
step limit exceeded. Useful as a runtime DoS guard against
untrusted code (e.g. the agent-tool sandbox, where an LLM
could emit list.fold(list.range(0, 1_000_000_000), …) to hang
the host). Default is 10_000_000.
pub fn call(&mut self, name: &str, args: Vec<Value>) -> Result<Value, VmError>
Sourcepub fn invoke_closure_value(
&mut self,
closure: Value,
args: Vec<Value>,
) -> Result<Value, VmError>
pub fn invoke_closure_value( &mut self, closure: Value, args: Vec<Value>, ) -> Result<Value, VmError>
Invoke a Value::Closure by combining its captures with the
supplied call args and dispatching to the underlying function.
Used by the parser interpreter (#221) to call user-supplied
f arguments inside parser.map / parser.and_then nodes.
Sourcepub fn invoke_closure_1(
&mut self,
closure: Value,
arg: Value,
) -> Result<Value, VmError>
pub fn invoke_closure_1( &mut self, closure: Value, arg: Value, ) -> Result<Value, VmError>
Invoke a 1-arg closure without allocating a separate args
Vec (#464 call-overhead). The closure’s own captures Vec
is reused as the combined captures ++ [arg] argument buffer,
so the per-element call in ListMap/ListFilter/SortByKey
allocates at most once (the push) instead of twice (a fresh
vec![arg] plus the extend). Semantically identical to
invoke_closure_value(closure, vec![arg]).
Sourcepub fn invoke_closure_2(
&mut self,
closure: Value,
a: Value,
b: Value,
) -> Result<Value, VmError>
pub fn invoke_closure_2( &mut self, closure: Value, a: Value, b: Value, ) -> Result<Value, VmError>
Invoke a 2-arg closure without a separate args Vec — the
ListFold combiner path. See invoke_closure_1.
Sourcepub fn enter_request_scope(&mut self) -> u64
pub fn enter_request_scope(&mut self) -> u64
Open a request-scoped arena via the underlying
EffectHandler::enter_request_scope (#463 scaffolding).
Runtime layers — net.serve_fn, net.serve_ws,
net.serve_quic — call this immediately before invoking the
user handler closure for a single request. Pair with
exit_request_scope once the response has been built and
any lazy iterators in it have been drained (#477).
Returns the scope id the runtime should pass back to
exit_request_scope. The handler’s default impl returns 0
and the matching exit is a no-op; DefaultHandler’s
implementation actually allocates an arena.
Sourcepub fn arena_scope_active(&self) -> bool
pub fn arena_scope_active(&self) -> bool
True iff there is at least one active request scope — i.e. an
enter_request_scope not yet matched by exit_request_scope.
Runtime layers use this to skip materialize_arena_handles on
paths where no scope was entered (e.g. tiny-http worker
dispatch), keeping the no-arena path zero-cost. Slice 2b-i.
Sourcepub fn exit_request_scope(&mut self, scope_id: u64)
pub fn exit_request_scope(&mut self, scope_id: u64)
Close the request scope opened by enter_request_scope.
Drops the associated arena.
Sourcepub fn materialize_arena_handles(&self, value: Value) -> Value
pub fn materialize_arena_handles(&self, value: Value) -> Value
Deep-walk value and resolve every Value::ArenaRecord /
Value::ArenaTuple handle into its heap-owned equivalent
(Value::Record / Value::Tuple), reading field contents
out of Vm::arena_slab along the way. Primitives, closures,
maps/sets, and the host-managed handles (Actor / Ticker /
ArrowTable) are returned unchanged.
The boundary helper flagged in
docs/design/arena-plumbing.md § “Arena handles MUST be
readable at serialization”. Callers — the response
serialization path in lex-runtime, the trace recorder when
it records a Call/EffectCall arg, anywhere a value crosses
out of the VM into host-managed storage — call this
while the producing scope is still active, before
exit_request_scope. After exit the slab is truncated, so a
handle materialized after-the-fact would read garbage (or
panic on the bounds check).
Value::StackRecord / Value::StackTuple would similarly
need slab resolution, but the #464 escape analysis prevents
them from reaching boundary-crossing ops in the first place
(they’re frame-local by construction). Reaching here means a
hand-built or analysis-buggy program; we panic with the same
loud-not-silent contract the other inspection paths use.
Idempotent on already-materialized values (no arena handles in the tree → only the recursive walk’s clones, no slab lookups). Cost per call is one walk + clone of the tree — amortized over the per-node mallocs avoided during request handling, the net stays strongly positive.
Sourcepub fn get_record_field(&self, value: &Value, name: &str) -> Option<Value>
pub fn get_record_field(&self, value: &Value, name: &str) -> Option<Value>
Read a named field out of a record without materializing its
parent. Works uniformly on Value::Record (heap) and
Value::ArenaRecord (slab handle), so a runtime layer can
consume the response record structurally — straight out of
the arena slab — instead of paying for a tree-wide
materialize_arena_handles walk just to read three top-level
fields.
Returns None if the value isn’t a record or the field
doesn’t exist. The returned Value is a clone of the slot
contents (records’ field values can themselves be records,
variants, etc.; cloning at the boundary is unavoidable
without lifetime trickery on the public API).
Performance: on the heap path it’s a IndexMap::get + clone.
On the arena path it’s a linear walk of the shape’s
field-name vec (field_count long, typically ≤ 10) +
an O(1) slab index + clone. The polymorphic-IC equivalent
inside the VM is faster, but this API is for host
consumers, not hot-loop dispatch.
Value::StackRecord is deliberately not handled — those
handles are frame-local by construction (#464 escape pass)
and shouldn’t reach host boundaries; reaching them here is
a soundness bug surfaced as a panic, matching the existing
inspection-path contract.
Sourcepub fn get_tuple_elem(&self, value: &Value, idx: u16) -> Option<Value>
pub fn get_tuple_elem(&self, value: &Value, idx: u16) -> Option<Value>
Positional read out of a tuple without materializing its
parent. Works uniformly on Value::Tuple and
Value::ArenaTuple. See get_record_field for the lifetime
rationale.
Sourcepub fn value_to_json(&self, value: &Value) -> Value
pub fn value_to_json(&self, value: &Value) -> Value
Arena-aware to_json — produces a serde_json::Value from
a Value whose tree may contain ArenaRecord / ArenaTuple
handles, reading them straight out of Vm::arena_slab
instead of materializing into a heap Value::Record mirror
first.
Equivalent output to value.to_json() on a fully-materialized
tree (idempotent in that sense). Use this when serializing a
handler return value to JSON for the response — saves the
per-node IndexMap allocations the materialize-then-to_json
pattern pays.
pub fn invoke(&mut self, fn_id: u32, args: Vec<Value>) -> Result<Value, VmError>
Trait Implementations§
Source§impl<'a> ClosureCaller for Vm<'a>
Vm exposes itself as a ClosureCaller so the parser interpreter
can invoke user-supplied closures during a parser.run walk
(#221). The Vm is reentrant for closure invocation: pushing a new
frame onto an active call stack is supported, and the handler
stays in place so any effects the closure body fires dispatch
normally.
impl<'a> ClosureCaller for Vm<'a>
Vm exposes itself as a ClosureCaller so the parser interpreter
can invoke user-supplied closures during a parser.run walk
(#221). The Vm is reentrant for closure invocation: pushing a new
frame onto an active call stack is supported, and the handler
stays in place so any effects the closure body fires dispatch
normally.