Skip to main content
Stoffel VM is the register-based runtime used by the CLI, StoffelLang compiler, Rust SDK, and lower-level integration surfaces. It is implemented primarily by two crates in the stoffel repository:
CratePurpose
crates/stoffel-vm-typesShared compiler/runtime data model: values, instructions, registers, activations, functions, and .stflb bytecode.
crates/stoffel-vmRuntime execution engine, standard library, MPC builtins, async MPC effect scheduling, storage, networking internals, and FFI.
Application docs should start with the CLI and Rust SDK. This page is for understanding the implementation boundary beneath those surfaces.

Execution model

Stoffel VM execution model showing bytecode loading, instruction dispatch, clear and secret register spaces, runtime stores, builtins, and MPC hooks. The runtime executes lowered/resolved instructions, not raw source text. Symbolic Instruction values are useful at compiler/direct-construction boundaries; ResolvedInstruction values and packed runtime instructions are used for efficient execution.

VirtualMachine and VMState

VirtualMachine is a public wrapper around VMState. VirtualMachine::new() uses the VM builder defaults:
  • registers the standard library;
  • registers MPC builtins;
  • uses the default register layout;
  • uses in-memory table storage;
  • uses stdout as the output sink;
  • starts without a configured MPC engine or local storage backend.
VirtualMachine::without_builtins() creates an empty VM for tests or custom embedding. VMState owns the runtime state:
ComponentRole
Program registryVM and foreign function definitions.
Activation stackCurrent call frames and return flow.
Runtime function cacheLowered instruction data per active frame.
Register layoutMaps absolute register operands to clear or secret banks.
Table memoryObject and array storage backend.
Foreign object storeRust objects exposed to VM code.
Hook managerOptional execution/debug hooks.
MPC runtimeOptional engine metadata and online operation handle.
Output sinkDestination used by print.
Local storageOptional app-provided storage backend for LocalStorage.*.
When the runtime needs to execute multiple functions or MPC tasks concurrently, it can clone an empty execution state that shares immutable program metadata while getting independent activation/table state. Configured local storage remains shared.

Registers and frames

The bytecode ABI uses absolute register indices. The default layout treats r0..r15 as clear registers and r16.. as secret registers. r0 is the return register. Each function has a frame register count. Registration normalizes that count so the frame is wide enough for:
  • at least the return register;
  • all parameters;
  • every referenced clear or secret register.
A call frame stores:
  • local variables;
  • the register file;
  • captured upvalues;
  • a volatile argument stack;
  • a stable spill area;
  • compare flag;
  • instruction pointer;
  • optional closure metadata.
The distinction between argument stack and spill area matters:
  • PUSHARG pushes onto the volatile argument stack for calls;
  • LD reads from that argument stack;
  • STS / LDS access a separate per-frame spill area used by the register allocator.

Clear and secret bank transitions

The register file stores clear and secret banks separately. The layout maps absolute bytecode registers into a bank and bank-local address. A write or move across banks may have MPC meaning:
TransitionRuntime behavior
clear → clearnormal value copy
secret → secretshare/value copy
clear → secretclear value is represented as a share value when needed
secret → clearreveal/open operation; may yield an async MPC effect
Pending reveals are stored as register-slot state until the online MPC operation completes. They are not ordinary Value variants.

Values and table memory

The VM value model includes scalar values, typed table handles, closures, foreign object handles, unit, and share values. Object and array storage is abstracted behind TableMemory. The default backend is an in-memory ObjectStore, but the trait boundary is designed so future backends, including access-tracking or ORAM-like storage, can preserve read/write metadata. For this reason, execution-path table reads go through mutable table-memory APIs even when a read looks logically immutable. Arrays are 0-indexed. The default array implementation uses dense inline storage for small numeric indices plus an extra field map for large or non-numeric keys, with a cached length hint.

Functions and calls

A VMFunction carries:
  • name;
  • parameter names;
  • upvalue names;
  • optional parent function;
  • register count;
  • labels;
  • symbolic instructions, or resolved instructions with constants and call targets.
During registration/loading, the VM validates labels and registers, resolves control-flow targets, interns constants, and resolves call targets. Call-target lookup is cached inside VM state to avoid repeated function-name lookups on hot call paths. VM calls and foreign-function calls share the same call machinery. Method-style calls are resolved against canonical builtin names and receiver types before dispatch.

Hooks and debugging

The VM has an optional hook system for debugging and tooling. When no hooks are enabled, the runtime takes a fast path and does not build hook snapshots. Hook events can cover:
  • before/after instruction execution;
  • register reads/writes with absolute and bank-local register information;
  • variable and upvalue access;
  • object and array field access;
  • function calls;
  • closure creation;
  • stack push/pop.
Use this hook layer for runtime instrumentation instead of adding logging to hot execution paths.

Async MPC effects

The VM can execute ordinary local instructions synchronously. When execution reaches work that needs an MPC engine, async execution yields a typed effect and resumes after the engine returns a result. Examples of operations that can yield effects:
  • client input sharing;
  • secret multiplication;
  • secret boolean bit operations;
  • opening/revealing a share;
  • Share.batch_open;
  • Share.random / Share.random_int;
  • MpcOutput.send_to_client;
  • lower-level RBC, field-open, and exponent-open operations.
The effect scheduler runs local instructions in bounded slices so non-MPC work does not block the async runtime indefinitely.

Builtins and standard library

VirtualMachine::new() registers two broad groups:
GroupExamples
Standard libraryarrays/lists, objects, closures, local storage, print, type, slice, contains, assert, ClientStore.*, MpcOutput.send_to_client
MPC builtinsShare.*, Mpc.*, Bytes.*, Crypto.*, Field.*, Rbc.*, Avss.*
print writes through the configured VM output sink. LocalStorage.* builtins require a local-storage backend to be configured on the VM builder.

Bytecode and manifests

.stflb bytecode is defined by CompiledBinary in stoffel-vm-types. The current format uses magic bytes STFL and format version 9. A compiled binary carries:
  • a scalar constant pool;
  • compiled function records;
  • function type metadata;
  • client input/output schema metadata;
  • MPC backend and curve selections;
  • preprocessing demand estimates.
The preprocessing manifest helps local/network MPC runtimes prepare material such as triples, random shares, PRandBits, and PRandInts. A dynamic flag indicates that runtime demand may exceed the static estimate.

Lower-level embedding

Direct VM construction is useful for VM tests and bytecode tooling, but most application integrations should use stoffel build plus Stoffel::load_file(...) from the Rust SDK.
use std::collections::HashMap;

use stoffel_vm::core_types::Value;
use stoffel_vm::core_vm::VirtualMachine;
use stoffel_vm::functions::VMFunction;
use stoffel_vm::instructions::Instruction;

fn main() -> Result<(), String> {
    let mut vm = VirtualMachine::new();

    let hello = VMFunction::new(
        "hello".to_string(),
        vec![],
        vec![],
        None,
        2,
        vec![
            Instruction::LDI(0, Value::String("Hello from Stoffel VM".to_string())),
            Instruction::PUSHARG(0),
            Instruction::CALL("print".to_string()),
            Instruction::LDI(1, Value::Unit),
            Instruction::RET(1),
        ],
        HashMap::new(),
    );

    vm.try_register_function(hello)?;
    vm.execute("hello")?;
    Ok(())
}

See also