ExZapcode (ExZapcode v0.1.1)

Copy Markdown View Source

Elixir wrapper for zapcode, a minimal secure TypeScript-subset interpreter written in Rust.

ExZapcode is the TypeScript sibling of ExMonty (Python). Both embed a small, sandboxed interpreter as a NIF and share the same interactive start/resume model:

  • Microsecond startup — no Node/V8, just a bytecode VM
  • Interactive execution — guest code pauses at external function calls, hands control to Elixir, and resumes with the result
  • Resource limits — cap wall-clock time, memory, stack depth, allocations
  • Language-level sandbox — no filesystem, network, env, eval, or import; the only way guest code reaches the host is through the external functions you register

Quick start

# Evaluate an expression (the last expression is the result)
{:ok, 7, ""} = ExZapcode.eval("1 + 2 * 3")

# With inputs bound as globals
{:ok, "hi Sam", ""} =
  ExZapcode.eval("`hi ${name}`", inputs: %{"name" => "Sam"})

The tool bridge

Register host functions the guest may await. Execution suspends at each call, you run the tool in ordinary Elixir, and it resumes with the return value:

{:ok, 2, ""} =
  ExZapcode.Sandbox.run(
    "const r = await db({ sql: \"SELECT 1\" }); r.rows[0].n + 1",
    functions: %{"db" => fn [%{"sql" => _}] -> {:ok, %{"rows" => [%{"n" => 1}]}} end}
  )

See ExZapcode.Sandbox for the high-level driver that automates the loop.

Interactive API (low level)

{:function_call, "getWeather", [city], snapshot, _out} =
  ExZapcode.start("await getWeather(city)", inputs: %{"city" => "London"},
    functions: ["getWeather"])

{:complete, value, _out} = ExZapcode.resume(snapshot, %{"temp" => 18})

Summary

Functions

The resource limits applied when :limits is omitted.

Serializes a suspended snapshot to a binary for storage or transport.

Compiles and runs code to completion, with no external functions.

Deserializes a snapshot binary produced by dump_snapshot/1 into a snapshot usable with resume/2.

Resumes a suspended run with the external function's return value.

Begins interactive execution of TypeScript code.

Types

progress()

@type progress() ::
  {:function_call, name :: String.t(), args :: list(), snapshot(),
   output :: String.t()}
  | {:complete, term(), output :: String.t()}
  | {:error, ExZapcode.Exception.t()}

snapshot()

@type snapshot() :: reference()

Functions

default_limits()

@spec default_limits() :: map()

The resource limits applied when :limits is omitted.

dump_snapshot(snapshot)

@spec dump_snapshot(snapshot()) :: {:ok, binary()} | {:error, ExZapcode.Exception.t()}

Serializes a suspended snapshot to a binary for storage or transport.

The pausing use case: a run suspends at an external function (:function_call), you dump_snapshot/1 the snapshot to a binary, persist it (DB, queue, another node), run the tool whenever, then load_snapshot/1 and resume/2 with the result — possibly in a different process or after a restart.

Non-destructive: unlike ExMonty.dump_snapshot/1, this does not consume the snapshot, so the same in-memory snapshot can still be resume/2d afterward.

Trusted input only

Only load_snapshot/1 binaries your application produced and stored in a trusted location. The bytes deserialize directly into native VM state.

eval(code, opts \\ [])

@spec eval(
  String.t(),
  keyword()
) :: {:ok, term(), String.t()} | {:error, ExZapcode.Exception.t()}

Compiles and runs code to completion, with no external functions.

Convenience over ExZapcode.Sandbox.run/2 for pure expressions.

{:ok, 4, ""} = ExZapcode.eval("2 + 2")

load_snapshot(bytes)

@spec load_snapshot(binary()) :: {:ok, snapshot()} | {:error, ExZapcode.Exception.t()}

Deserializes a snapshot binary produced by dump_snapshot/1 into a snapshot usable with resume/2.

resume(snapshot, value)

@spec resume(snapshot(), term()) :: progress()

Resumes a suspended run with the external function's return value.

Returns the next progress/0 — another :function_call if the guest awaits again, or :complete when it finishes.

Examples

{:function_call, "getWeather", ["London"], snap, _} =
  ExZapcode.start("await getWeather(city)",
    functions: ["getWeather"], inputs: %{"city" => "London"})

{:complete, %{"temp" => 18}, ""} = ExZapcode.resume(snap, %{"temp" => 18})

start(code, opts \\ [])

@spec start(
  String.t(),
  keyword()
) :: progress()

Begins interactive execution of TypeScript code.

Options

  • :inputs — map of name => value bound as globals (default: %{})
  • :functions — the names of host functions the guest may await. Either a list of names, or a map whose keys are the names (values ignored here). They must be declared up front: zapcode compiles calls to them as suspension points.
  • :limits — resource limits map, merged over default_limits/0
  • :script_name — accepted for ExMonty parity; currently unused

Returns a progress/0 tuple.