Embedding
SandScript is host-owned. The host allocates the memory, drives the execution loop, and supplies every capability. This chapter summarizes the embedding surface. The SandScript repository owns the full reference, including the airlock protocol and the vat layout.
Sessions
createSession attaches to a
WebAssembly.Memory and a membrane buffer that the
caller has already populated. freshSession wraps the
allocate-and-attach boilerplate for tests and one-liners.
import { freshSession } from
'https://urania.blue/urania/libs/sandscript/src/host-owned-session.js';
const session = freshSession();
const parseResult = session.parse('let x = 1 + 2');
session.mem.setContextInstructionIndex(0, parseResult.startIndex);
session.mem.setContextStatus(0, STATUS_RUNNING);
const result = session.run(0, 1000); // slot 0, 1000 fuel units
result.status; // 'done'
session.state(0).scope.x; // 3
session.run(slot, fuel)
Execution continues until the fuel is exhausted, the program completes, or a yield point requires host action. The result status is one of:
| Status | Meaning |
|---|---|
done | Execution complete |
paused | Fuel exhausted; call run() again to continue |
error | Runtime error occurred |
external_call | FFI call pending; the host must handle it |
async_call | Async function called; a new context spawned |
await | The context awaits a promise |
async_complete | Async context finished with a return value |
async_rejected | Async context finished with a thrown error |
Exports and invocation
session.exports() returns the exported names from the
last parse. session.invokeExport(name, args) prepares
a context for one exported function and returns a context slot
ready to run. The host runs it, extracts the result, and frees the
context.
FFI and the airlock
External calls pause execution and return control to the host. The airlock handles external calls automatically when handlers are registered:
const { airlock } = session;
const consoleId = airlock.register({});
airlock.setHandlers(consoleId, {
log: ({ args }) => console.log(...args),
});
airlock.declare('Console', consoleId);
session.run(0, 1000); // external calls are handled automatically
Host-owned async execution
SandScript does not schedule async work automatically. The host
owns the execution loop, finds a runnable context, resumes an
awaiting context when its promise settles, and handles
async_complete and async_rejected
through the airlock. This gives the host complete control over
scheduling, debugging, resource limits, and custom policies. The
interpreter never runs code without explicit host action.
Fuel and callbacks
Fuel controls all execution, including callbacks. When a SandScript closure is passed to an external handler and JavaScript invokes it later, the current fuel level decides what happens:
| Fuel state | Callback behavior |
|---|---|
| Fuel > 0 | Executes immediately inline |
| Fuel = 0 | Queued for later execution |
This is a deliberate design, not a special mode. With high fuel, timers and events behave naturally. With fuel at zero, callbacks become visible and steppable: the interpreter signals that a callback is pending, and the host decides when to run it and with how much fuel. A callback queued during an external call freezes that call in an awaiting-value state; the host executes the callback, captures its return value, and resumes the external call with it.
Snapshot and restore
A session can be captured as bytes and restored later, in a
different process or on a different machine. The host owns the
buffers: it calls quiesce(), slices its own memory
and membrane buffer, and calls resume(). Restoration
copies the bytes into fresh buffers and calls
createSession.
What survives the round-trip:
- The vat state: variables, objects, scopes, in-flight call frames, suspended async contexts.
- Handles and grants, with stable slot indices. Revoked grants stay revoked.
- Declarations and drone-registered callbacks, which execute under their original captured grants.
- Host-supplied metadata on handles, grants, and closure handles.
What does not survive:
- JavaScript-side handler functions. The host must re-register every handler, getter, and setter after restore. A forgotten handler fails loudly: a drone call to it throws a catchable TypeError.
- Linked promises in flight. The host either rejects them with
airlock.rejectOrphanedLinkedPromises()or settles them from its own persisted correlation state.
After restore, the host walks
airlock.membrane.enumerateHandles(),
airlock.membrane.enumerateGrants(), and
airlock.enumerateClosureHandles(), re-binds
implementations and handlers, and only then calls
session.run().
Long-running hosts
Call airlock.compactMembrane() between dispatch
cycles to reclaim dead handles, dead grants, and orphaned arena
bytes. Compaction never moves live slots.
airlock.membraneStats() reports occupancy so the host
can decide when to compact.