The embedding model

A host compiles a script once into a ScriptState, runs its top level, and then re-enters the same global context as often as it likes: evaluate an expression, call a function by name or by stable slot index, read and write top-level bindings. The VM never recompiles between entries and never exposes its internal Value handles; everything crosses the boundary as an owned HostValue tree.

Cargo.toml
[dependencies]
zipp-vm = { git = "https://github.com/f2i-com/zipp.org", tag = "v0.0.18" }
src/main.rs
use zipp_vm::embed::{compile_script, HostValue};

fn main() -> Result<(), String> {
    let source = std::fs::read_to_string("rules.js").map_err(|e| e.to_string())?;
    let mut st = compile_script(&source)?;

    // Host calls are the only way out of the VM, and they are inert until installed.
    st.set_host_call(Box::new(|kind, args| match kind {
        "log" => { println!("{}", args[0]); Ok(HostValue::Undefined) }
        _ => Err(format!("TypeError: unknown host call {kind}")),
    }));

    st.run_init()?;                                   // top-level code, once
    let total = st.eval_in_context("price(42)")?;     // later, same globals
    println!("{total:?}");
    Ok(())
}
  • call_slot calls a top-level function by index, drains microtasks afterwards, and is the right choice in hot host loops.
  • call_global and has_global_function resolve by name on every call, compile nothing, and do not drain microtasks.
  • compile_script_with_options chooses the grammar goal: the Compat goal accepts top-level return for CommonJS-shaped scripts; Pure is the ECMAScript Script goal.
  • compile_script_with_preamble compiles engine plumbing ahead of a guest while keeping the guest's own "use strict" in force.
  • Arrays and plain objects marshal structurally. Functions, classes, maps, dates, proxies and cycles cross as Opaque, and a structural write-back declines to overwrite an opaque slot.

Embedding Python

The same module compiles Python: compile_source(source, Frontend::Python { .. }) for one file, compile_python_project(entry, &modules) for a folder of modules, and compile_python_program(entry, &modules, &files, &argv, hosted) when the program needs a virtual filesystem and sys.argv. Python and JavaScript states share the runtime, the frame stack and the collector, and the Python page explains the ABI.

In the browser

The WebAssembly Engine class is the same model with a JSON-shaped surface. Capabilities are default-deny: before initScript, grant exactly the synchronous operations the tenant needs, then install bridges.

host.js, inside a dedicated Worker
import init, { Engine } from './zipp_wasm.js'
await init()

const engine = new Engine()
engine.setSyncHostCapabilities(['db.query', 'ls.getItem'])
engine.setDbBridge(tenantScopedDb)
engine.setLocalStorageBridge(tenantScopedStorage)
engine.setInstructionBudget(500_000_000)
engine.initScript(source)

// Later entries, same globals:
const rendered = engine.callFunction('render', [{ user: 'ada' }])
for (const call of engine.drainPendingHostCalls()) {
  engine.resolveHostCallback(call.id, await handle(call))
}
engine.pump()

The Worker is the lifetime boundary: a deadline timer in the page calls worker.terminate() and creates a fresh one. host-sdk/ in the repository is a reference adapter tested in real Chromium, Firefox and WebKit Workers, and the WebAssembly page lists every limit the module enforces.

The CLI as a host

zipp --help, abridged
zipp js  <file.js>            run a script (compat goal; --script-goal for pure Script)
zipp mjs <file.mjs>           run a file as an ES module
zipp py  <file.py | dir>      run a Python program or project
zipp run [--lang=L] <file>    pick the frontend by flag, extension or shebang
zipp sandbox <flags> <file>   hardened run with explicit limits
zipp --version --json         build identity: source commit, dirty flag, rustc, jit

Frontend detection precedence is explicit selection, then a recognized extension (.js, .cjs, .mjs, .py, .pyw), then a recognized shebang, then a leading directive, then an error. Bare source never silently defaults to one language. There is no REPL.

Use cases