# 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 |