Skip to main content

Vm

Struct Vm 

Source
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: u64

Soft cap to avoid runaway computations in tests.

§steps: u64§pure_memo_hits: u64

Diagnostic counters for --trace observability (#229).

§pure_memo_misses: u64§pure_memo_skips: u64

Number of effect-free calls that skipped the cache entirely because adaptive memoization disabled their function (#229 adaptive). Observability only.

§stack_record_allocs: u64

Per-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: u64

Counters 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: u64

Implementations§

Source§

impl<'a> Vm<'a>

Source

pub fn new(program: &'a Program) -> Self

Source

pub fn with_handler( program: &'a Program, handler: Box<dyn EffectHandler + 'a>, ) -> Self

Source

pub fn set_tracer(&mut self, tracer: Box<dyn Tracer + 'a>)

Source

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.

Source

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.

Source

pub fn call(&mut self, name: &str, args: Vec<Value>) -> Result<Value, VmError>

Source

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.

Source

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]).

Source

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.

Source

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.

Source

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.

Source

pub fn exit_request_scope(&mut self, scope_id: u64)

Close the request scope opened by enter_request_scope. Drops the associated arena.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source§

fn call_closure( &mut self, closure: Value, args: Vec<Value>, ) -> Result<Value, String>

Source§

impl Drop for Vm<'_>

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl<'a> !RefUnwindSafe for Vm<'a>

§

impl<'a> !Send for Vm<'a>

§

impl<'a> !Sync for Vm<'a>

§

impl<'a> !UnwindSafe for Vm<'a>

§

impl<'a> Freeze for Vm<'a>

§

impl<'a> Unpin for Vm<'a>

§

impl<'a> UnsafeUnpin for Vm<'a>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.