# Runtimes
A **runtime** is Deft's unit of isolation: it owns one VM, one heap,
one coroutine scheduler, and a set of top-level bindings. Runtimes
are isolated by value — they cannot see each other's variables or
heaps — but share the platform layer (`pub/*` broker, `config/*`
registry, defaults snapshot) so they can coordinate.
The `deft` scripting binary drives a single runtime. Each TUI app
panel (in `retro` / `retra`) runs its own runtime in its own OS
thread. You can also spawn and interact with child runtimes
explicitly from any host.
## The Runtime Hierarchy
```
+------------------+
| Root runtime | owns VM, heap, scheduler
| (script or TUI | shares pubsub / config / registry with children
| workspace root) |
+------------------+
| createChild / registerChild
v
+------------------+ +------------------+
| Child runtime A | ... | Child runtime B |
| own VM + heap | | own VM + heap |
+------------------+ +------------------+
```
Children share:
- The `pub/*` broker — see [Pubsub](pubsub).
- The `config/*` registry — see [Config](config).
- The defaults snapshot for `config/save-overrides`.
Children do **not** share:
- Top-level bindings, `def`s, or globals.
- The heap — values must be serialised across the boundary.
- The coroutine scheduler.
## `runtime/*` Stdlib functions
| Function | Purpose |
|---|---|
| `[runtime/current]` | The current runtime's name (e.g. `"main"`) |
| `[runtime/create "name"]` | Create a named child runtime |
| `[runtime/exists? "name"]` | Bool — does a child exist? |
| `[runtime/list]` | List of child runtime names |
| `[runtime/destroy "name"]` | Destroy a child runtime |
| `[runtime/eval "name" "code"]` | Sync eval in child — returns `%{:value :output :error}` |
| `[runtime/eval-async "name" "code"]` | Async eval — returns id, result on `"runtime/eval-result"` topic |
| `[runtime/list-defs "name"]` | Defs in the child's current namespace |
| `[runtime/run "name" "path"]` | Run a file in the child (blocking) |
| `[runtime/spawn "name" "path" ?params-map]` | Spawn a file in the child on a dedicated OS thread |
| `[runtime/mem-stats]` | Process + VM memory map |
| `[runtime/on-shutdown fn]` | Register a callable invoked at VM teardown |
## Creating and Using a Child Runtime
# Create a named child runtime runtime/create "sandbox" # Evaluate code in it (synchronous) set r1 [runtime/eval "sandbox" "set secret 999"] # => %{ :value 999 :output "" :error nil } set r2 [runtime/eval "sandbox" "$secret"] # => %{ :value 999 :output "" :error nil } # variables persist across calls within the same sandbox # Inspect contents echo [runtime/list-defs "sandbox"] # Async eval — returns an id immediately set id [runtime/eval-async "worker" "[do-expensive-thing]"] # Result delivered later via pubsub on topic "runtime/eval-result" # Tear down runtime/destroy "sandbox"
## Isolation Semantics
Variables do not cross runtime boundaries:
set parent-var "hello" runtime/create "sandbox" set r [runtime/eval "sandbox" "$parent-var"] # => %{ :value nil :error "unbound variable: parent-var" } runtime/eval "sandbox" "set child-var 42" echo $child-var # Error: unbound variable: child-var (in the parent runtime)
Values that travel between runtimes (via `pub/*`, `runtime/spawn`
params, or `runtime/eval` return values) are **serialised** through
`deft/stringify` / `deft/parse` to keep heaps disjoint. Scalars,
strings, lists, maps, tagged maps, and keywords round-trip cleanly.
Closures and native resources don't.
## Spawning a File as a Process
`runtime/spawn` runs a `.dft` file in the named child runtime on a
dedicated OS thread. The optional params map is injected as globals
into the child before the file runs:
runtime/create "worker" runtime/spawn "worker" "src/worker.dft" %{ :queue "high-priority" :retries 3 }
Inside `worker.dft`:
# Reads injected globals as ordinary variables def queue $queue def retries $retries
This is the pattern the `stdext/repl` module uses to launch
isolated REPL processes (see `packages/stdext/src/repl.dft`).
## Async Eval Via Pubsub
`runtime/eval-async` is non-blocking. The result is published on the
`"runtime/eval-result"` pubsub topic with the eval id as a correlator:
def on-eval-result {|ev| @doc "Handle runtime/eval-async results." if ($ev~topic == "runtime/eval-result") { echo "got result for id $ev~data~id: $ev~data~value" | } } pub/subscribe "runtime/eval-result" "on-eval-result" runtime/eval-async "worker" "[compute-something]"
The event map shape is documented in [Pubsub](pubsub).
## Memory Stats and GC
[gc/collect] # force GC + trim glibc arenas [gc/stats] # => %{ :live-objects ... :heap ... } [runtime/mem-stats] # => %{ :rss ... :vm-size ... :live-objects ... :heap-bytes ... }
Use these in long-running services or TUI apps to track memory
growth and trigger explicit collections.
## Shutdown Hooks
`runtime/on-shutdown` registers a callable that runs when the VM
tears down. This is the right place to close database handles,
unmount packages, or signal external processes:
def db [sqlite/open "data.db"] runtime/on-shutdown { try { sqlite/close $db } catch _e { nil } }
The hook fires on normal exit, on `os/exit`, and on most fatal
errors. It does **not** fire on SIGKILL or sudden crashes.
## Cross-Runtime Communication
| Mechanism | Direction | Use |
|---|---|---|
| `pub/publish` / `pub/subscribe` | One-to-many broadcast | Loose coupling, events |
| `rpc` against `@rpc` functions | One-to-one direct call | Type-safe RPC, all hosts |
| `config/set` (with reactivity) | Indirect via shared state | Cross-runtime shared preferences |
| `runtime/eval` / `runtime/eval-async` | Caller → child | Direct code injection |
See [Pubsub](pubsub) and [RPC](rpc) for the details.
## When To Spawn a Child Runtime
| Need | Approach |
|---|---|
| Run untrusted code | `runtime/create` + `runtime/eval` (separate heap prevents leakage) |
| Background long-running process | `runtime/spawn` with a file |
| Parallel compute on multiple cores | `runtime/spawn` per worker + `pub/publish` to collect |
| In-process REPL or debugger | `runtime/create` + `runtime/spawn` of a REPL host |
| Hostile code with capability restriction | Deft does **not** yet ship a capability-restricted runtime; isolation is heap-only |
## Sandboxing Caveats
Deft's runtime isolation is **memory isolation**, not a security
sandbox. Children share the OS process, the filesystem, the network,
and the `os/shell` / `os/run` stdlib functions. Don't treat
`runtime/create` as a security boundary against hostile code.
For OS-level filesystem restriction, the Landlock sandbox
([Sandbox](sandbox), opt-in via `DEFT_SANDBOX=1`) bounds the whole
process to the prelude's `:sandbox` path lists. Child runtimes share
that one set of grants — it is not a per-runtime boundary.
A real capability-restricted runtime (no `fs/*`, no `os/run`,
limited `net/*`) is on the roadmap but not yet shipped.
## Quick Reference
| Form | Purpose |
|---|---|
| `[runtime/current]` | Current runtime's name |
| `[runtime/create "name"]` | Make a child |
| `[runtime/list]` | List children |
| `[runtime/eval "name" "code"]` | Sync eval |
| `[runtime/eval-async "name" "code"]` | Async eval |
| `[runtime/spawn "name" "path" ?params]` | Spawn a file in a child |
| `[runtime/destroy "name"]` | Tear down |
| `[runtime/mem-stats]` | Memory map |
| `[runtime/on-shutdown fn]` | Cleanup hook |
| `[gc/collect]`, `[gc/stats]` | Garbage collection control |
| `[deft/stringify value]`, `[deft/parse str]` | Cross-runtime serialisation |
| `[satisfies? value "Protocol"]` | Protocol membership check |