Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Resource limits and fuel

Two knobs make rut safe for third-party code: a heap budget and fuel. Both are enforced as resumable traps, both are set at construction, and both bind on the VM’s own accounting (the VM heap) — not on the OS.

#![allow(unused)]
fn main() {
pub struct Limits {
    pub fuel: Option<u64>,             // None = unbounded ops
    pub heap_limit_bytes: Option<u64>, // None = host-enforced only
    pub interrupt_every: u32,          // check period; default 1024
}
}

The heap budget

What countseverything the VM heap tracks: cell headers + payloads, buffer blocks, string blocks, frames and register blocks, opaque store entries
What does notmodule binaries, the shared type table, host-side Rust state
Check pointsevery Heap::alloc route: cell mint, Vec growth (push reallocation), string concat, frame-pool growth, host crossing mints
On failureTrap::OutOfMemory — the check runs before any write, so the heap is byte-identical to its pre-op state; nothing is half-initialized
Resumptionthe frame is parked: raise the limit (Heap::set_limit), drop references and retry, or drop the VM
Observabilityvm.heap_usage() (live), vm.heap_peak() (high-water)

Vec growth charges the budget before the write; shrinking is never refunded (v1 overcounts rather than undercounts).

Fuel

  • One unit per executed op; fuel counts down. The counter is checked every interrupt_every ops (default 1024) and at loop back-edges.
  • Trap::OutOfFuel parks the frame exactly like any resumable stop: nothing is unwound. Resumption is vm.add_fuel(n) then vm.resume() — the frame is the loop state.
  • Fuel is the deterministic budget: the same program with the same fuel dies at the same op, every run — reproducible reports and hang proofs in tests.
  • Native fns run outside fuel. A hanging native is a host bug: keep native bodies non-blocking, or push blocking work to a thread through the host-futures lane (the host futures bridge).

The trap contract

TrapRaised byFrame stateResumption
OutOfMemoryheap budget exceeded at an allocation checkparked before the writeraise limit / free refs → resume()
OutOfFuelfuel reached 0 at a check pointparked at the exact pcadd_fuel(n) → resume()
#![allow(unused)]
fn main() {
let limits = Limits { fuel: Some(1_000_000),
                      heap_limit_bytes: Some(64 * 1024 * 1024),
                      interrupt_every: 1024 };
let mut vm = Vm::new(prog, limits, HostHooks::default(), hosts)?;

match vm.call::<_, ()>("main", ()) {
    Err(t) if t.name() == "OutOfFuel" => {
        vm.add_fuel(1_000_000);
        vm.resume::<()>()?;           // re-enters at the parked pc
    }
    r => { r?; }
}
}

The async layer’s resume granularity is the checkpoint, not the op: a drive that runs out of fuel parks the future’s frame, and a re-drive re-enters at the frame’s checkpoint state (async and await).

Hang detection

The host owns time; the recipes differ by embedder:

HostRecipe
UI / gamedrain the ready queue inside the frame loop; grant fuel per frame so a runaway script starves at the next check
wasm pagegrant finite fuel; a watchdog stops refueling — the parked frame dies at its next check
desktop servicea watchdog thread flips a flag the host honors between resume() calls; no joins, no signals
testsfinite fuel + the virtual clock (vm.set_now) → fully deterministic hang proofs

Anything blocking that cannot be bounded by fuel (IO, locks) belongs on a worker thread answering a Completer — the VM thread never blocks on it (the host futures bridge).

Workers

A worker VM is constructed with its own Limits; argument transfers are counted against the child budget before the worker starts, so a worker cannot OOM its parent (workers and channels).

Within one VM, the ready ring is round-robin: one greedy launched future cannot starve the rest of a queue drain indefinitely — each frame runs to its next park or completion, and fuel bounds each of those runs.