Lexical scoping for data, dynamic scoping for authority
ral splits scoping by what is being scoped — lexical for data, dynamic for authority:
- Data is lexical.
letbindings capture at definition and are immutable, solet x = 1; { let x = 2 }; echo $xprints1and a closure observes the bindings in force where it was written. - Ambient authority is dynamic. The working directory, environment overlays,
capability restrictions, and effect handlers are inherited from the call site
and scoped by
withinandgrantover the body’s whole dynamic extent.
The duality matches the right model to each:
- data captured at definition is predictable — this is what buys equational
reasoning and safe
spawn; - what files you may touch and which commands exist must be scoped to the call site, so that a function defined in an unrestricted context can be invoked inside a restricted block and respect that restriction without code changes.
An environment variable follows the dynamic side of this split: it is read as
$env[KEY], never as a bare lexical name, and a within [env: …] overlay is
seen by $env, ~, child processes, and PATH resolution alike
(env-is-dynamic-only).
Each dynamic frame nests by its own algebra:
- capability frames by intersection — each layer narrows reachability (grant);
- environment overlays by shadowing — inner
KEY: VALoverrides outer; - handler frames by the deep self-masking discipline (effects-handlers).
A tail-recursive call inside a within or grant block stays under that scope
across every tail landing.
Two layers: lexical scope vs fork inheritance
The lexical scoping above is one mechanism; crossing a shell boundary is a different one, and they should not be conflated.
- Lexical scope within a shell is the
Envtype (core/src/types/env.rs): a stack of name→Bindingscopes, innermost last, with the prelude at index 0 and localspush_scoped /pop_scoped above it. Lookup walks innermost-first, so the inner binding wins. The stack is animbl::Vector<Arc<HashMap<…>>>rather than aVec, so a closure captures its defining environment by cloning theEnv— an O(1) refcount bump on the structurally-shared chain, not an allocation per scope. This is the hot path for recursion. - Fork inheritance is the flow matrix in
inherit.rs: when a genuine runtime fork (aspawnworker, a pipeline stage, a REPL aside, a sub-agent session) needs the parent’s lexical environment, it clones the wholeEnvinto the new shell, alongside the rest of the parent→child manifest (builtin table, dynamic context, cancel root).
A same-thread β-step bridges the two: with_thunk_body sets the body’s
mobile.scope to the closure’s captured Env and push_scopes a fresh frame —
lexical-scope machinery — while sharing the surrounding shell store by identity
rather than forking it (same-thread-body-shares-the-session).
See also syscalls-are-effects (dynamic scope is for the authority over effects), cbpv, control-operators.
Realised in evaluator-machine (the dynamic frame stack).
Cite: RATIONALE
§“Shadowing, not mutation”, §“Scoped execution contexts”; docs/SPEC.md §3,
§3.1.