sandscript.run

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:

StatusMeaning
doneExecution complete
pausedFuel exhausted; call run() again to continue
errorRuntime error occurred
external_callFFI call pending; the host must handle it
async_callAsync function called; a new context spawned
awaitThe context awaits a promise
async_completeAsync context finished with a return value
async_rejectedAsync 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 stateCallback behavior
Fuel > 0Executes immediately inline
Fuel = 0Queued 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:

What does not survive:

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.