Map: core / runtime values & shell state
core/src/types/ defines what the evaluator manipulates
at runtime. types.rs is a re-export façade so the rest of the tree spells
everything crate::types::*.
Values
value.rs—Value(the runtime value category); beside ithandler.rs(the user handler stack:HandlerFrame,HandlerStack,FrameHandle),builtin.rs(BuiltinEntry/BuiltinTable, kept separate from the handler stack), andhandle.rs(the concurrency substrate behindValue::Handle:HandleInner,CompletedHandle,SurfaceBuffer). Handlers are deep and self-masking, with noresume(effects-handlers, handlers-deep-self-masking). A handler or alias arm is always a lambda:HandlerEntrycarries itsHandlerArity(Unaryfor a per-name arm or alias,CatchAllforwithin [handler: …]) fixed by the surface form, andvalidate_handler_arityrejects any non-lambda or wrong-arity value at every install boundary — the calling convention is never inferred from the runtime shape (handlers-and-aliases-are-lambdas).list.rs/map.rs—ListandMap, opaque newtypes over persistentimbl::Vector/imbl::OrdMap.flow.rs— the control-flow surface:Settled,Escape,Break, and the crate-internalControl/Raw/Tail(completion-escape-refactor). NoOption/null appears; optionality is open variants (optionality-via-variants).error.rs—Error,Status, and theBodyResultsplit.audit.rs— theAuditcollector andExecNodeexecution tree.env.rs— lexicalEnvandEnvVarsprocess-env overrides; a scope entry isBinding { value, scheme }, so the checker’s verdict rides next to the value (session-scheme-continuity).coerce.rs— thesig/sig_hintruntime-error constructors and theas_mapfamily ofValue→Mapcoercions, sitting below both the builtin and capability layers so each reaches them throughcrate::types::*.
Capabilities
capability.rs holds the capability types the grant
decision layer interprets: Capabilities, ExecPolicy, FsPolicy,
EditorPolicy, ShellPolicy, GrantStack, SandboxProjection, and the Meet
/ Join lattice operations (tested in capability/lattice_tests.rs).
Shell
shell/ partitions the interpreter state into four fields by lifetime — the
field name is the invariant — joined by Shell
(turn-local-state):
-
Mobile— the persistable computation state (lexicalscope+ControlState+ dynamicContext) that crosses evaluation boundaries and thread spawns.mobileis the public embedding seam. -
TurnState— the dynamic frame a top-level turn installs and restores on teardown: the pipeline-stageIo, thesurfacesink, thedeferredsink (a detached worker’s completion delivery —Noneoutside an agent host), thedeskanswering the turn’s enquiries (enquiry-channel), thenurseryholding engine-side session forks a desk handler adopts (Noneoutside a host that installs one; likedesk, never given to a deferred worker), the foregroundcancelscope, the source-positionloccursor, the deferred-workerWorkerLease(deferred_lease— the idle bound and absolute backstop travel as one value;Nonenever reaps), theworker_capadmission bound (Some(cap)refuses a spawn of any class whilecapworkers still run;Noneadmits freely, and both flow into same-thread bodies and spawned workers alike), and the turn’sTerminalAccess. -
SessionState— what survives every turn’s teardown: the durable cancelrootthat detached workers parent under (withpublishes_signal_slots, true only for the one signal-facing session per process), thesourcesregistry rendered against after a turn returns (reset and reseeded at each turn start), theexit_hintstable, the host-installedbuiltinswith the session’slibrary_docs, and the session’sterminal_lease. -
LocalState— host-local scratch carrying its own flow rules (audit trail, REPL scratch, theworkersregistry, thebindingsledger); the residue once turn and session state are named. The worker registry (shell/workers.rs) is a per-Shelldirectory of everyspawn/watchedHandleInner, one per agent;Shell::spawn_threadshares it byArcinto a spawned worker’s own shell (so a nestedspawnregisters alongside its parent), but a sub-agent fork or pipeline stage starts with a fresh, empty one. Beside the entries it keeps theReapNoticeledger the reap policies write — one compact record per entry removed by policy, atomic with the removal under the registry’s one lock — drained by the host throughShell::take_worker_reap_notices. A settled entry carries asettled_epochstamp:Shell::advance_worker_epoch, the host’s per-call sweep, stamps it at the first advance that observes the entry settled and expires it (aRetentionnotice) once its unclaimed result has sat a full retention of ral calls — a host that never sweeps (the REPL) retains settled entries indefinitely.Shell::cancel_workersis the other worker operation onhost.rs’s surface: the host’s/cleararm, it fires every entry’s cancel scope and resets both ledgers wholesale — entries and pending notices alike — since explicit destruction outranks every lease, the durable class included.WorkerEntryalso implements the smallResidentsignature (types/resident.rs, residency) — designator, population, capability kind, lease row, state label, cancel — so the REPL’sjobslisting and its exit-time survivor warning read a worker’s facets through it instead of hand-formatting per population.The binding-lease ledger (
shell/bindings.rs, agent-binding-reaping) sits beside the worker registry but needs no lock: it has exactly one writer, the thread that owns&mut Shellfor every turn, install, and prune (verified in the module’s own doc comment). Inert (BindingLedger:: default()) untilShell::arm_binding_leaseseals every name then visible in the scope chain as permanently-exempt baseline and starts the committed-turn clock. Every persistent top-level scope write funnels through one fused chokepoint,Shell::install_scope_binding(scope.rs, besidebind_value/set_var): it classifies the write byEnv::at_session_scope()and stamps the ledger only when true, so “write a scope entry” and “stamp the ledger” can never be pulled apart at a call site — the evaluator’s four writers (assign_pattern’sName/...restarms,eval_letrec’s two installs) all route here. Host verbs (bind_value,set_var) stay on the rawEnvprimitive, since every host call to them precedes arming. Idleness is use-observation, not re-installation:Shell::run_turn’s source arm ticks the committed-turn clock, and a lease is renewed by reference — the compiled program’sir::referenced_namesat each successful compile (ir), plus the resolved name at anEnv-arm command dispatch. The same chokepoint runs a second, orthogonal check:BindingLeasealso carrieslarge_binding_bytes, and an install whose value’sValue::shallow_size(a structural estimate —String/Bytesbyte lengths,List/Map/Variantrecursing into elements,Lambda/Block/Handlea small fixed constant, never descended) meets it queues aLargeBindingNoticeregardless of baseline status or idle age — residency and lifetime are independent axes.Shell::leased_binding_countandShell::take_large_binding_noticesround out the accessor surface, the first a probe figure, the second a boundary drain.
turn / session / local are pub(crate): the fields that encode turn
safety are not a public API. Hosts drive a session through the narrow accessors
gathered in host.rs, which a host crate reaches while only mobile stays the
public embedding seam. Shell::binding_count sits there too — the lexical
scope’s probe figure for a host’s /resources fold
(probe-convention): a count, never the
values, and enumeration renews nothing.
Surface
The surface sink (TurnState::surface, Option<SurfaceSink> where
SurfaceSink = Arc<dyn EventSink>) is the value-typed dual of the byte
Io sinks. EventSink is a synchronous trait taking a
borrowed Value; Shell::surface forwards onto the installed sink and is inert
when none is present (a bare REPL). Turn-scoped, not a persistent capability — a
turn door installs it, so a clone of it has no liveness role and can never decide
a turn is over. A detached worker does not receive the live sink: its events
buffer into a SurfaceBuffer and are delivered exactly once — replayed through
the awaiting turn’s surface on the first await / race, or handed to the
session-lived deferred sink at the worker’s own completion, whichever renders
first (a shared joined latch decides).
Terminal handoff
The authority to hand the controlling terminal to a child is an unforgeable
TerminalLease, not an inferred predicate
(terminal-lease). It splits across two
lifetimes:
- The lease itself lives on
SessionState::terminal_lease, minted once at startup from thetcgetpgrp == getpgrpwitness —Somewhen ral owns the foreground,Noneotherwise. It is lent, never moved or cloned. - A turn’s authority to borrow it is the per-turn
TerminalAccessonTurnState:Denied(the safe default — an exarch tool turn, the boot frame),Leased(an interactive turn), orExplicitLoan(a within-turn elevation raised only by the host loan token).Shell::terminal_leaseyields&TerminalLeaseonly when access permits and the session owns a lease, so aDeniedturn cannot construct a foreground handoff.
The host-facing TerminalLoan (host.rs) raises a Leased turn to
ExplicitLoan for _ed-tui and restores the prior access on surrender; it
leaves a Denied turn untouched, closing the Denied → ExplicitLoan door so a
loan can only raise an authorised turn, never mint authority.
Method modules
Methods on Shell live by concern, one submodule each:
init.rs— construction, the startup env-var seeding pass intocontext.env_overrides(env-is-dynamic-only), and the machine facts (OS_NAME/OS_ARCH/OS_FAMILY) seeded into$env;host.rs— the host-embedding accessor surface, plusTerminalLoan;context.rs— theContextdynamic-context verbs;scope.rs—within/grantguards realising scoping;checks.rs— forwarders to the `capability::check_*(&Context, …)` decisions, splitting the disjoint context/audit borrow for the audit-bearing checks;cwd.rs(Cwd),inherit.rs(the flow matrix, below),modules.rs,control.rs,hooks.rs(the session-lived hook table of named turn-entry points — prompt render, startup, plugin hooks — resolved by the turn door’s hook-program arm),repl.rs(ReplScratch, owned by the REPL layer).
The flow matrix
inherit.rs centralises what state crosses a parent→child shell boundary —
one file rather than a decision scattered across call sites, so no inheritable
datum (the host builtin table among them) can be silently severed by a call site
copying only the fields it happened to remember. There are two regimes.
A same-thread β-step — forcing a block or applying a lambda — does not fork:
Shell::with_thunk_body runs the body in the caller’s Shell. Only the
Mobile is swapped, rescoped to the closure’s captured Env plus a fresh frame;
the turn, session, and local state are shared by identity, so the body
observes the caller’s audit trail, byte sinks, builtin table, cancel root, and
terminal lease without any of them being copied. There is no second store to
drift from the first (same-thread-body-shares-the-session).
The ThunkBody kind fixes the only two places a block and a lambda differ: a
block enters with the caller’s $? and folds only last_status back; a lambda
enters with a fresh $? and folds {last_status, cwd} back, so a cd inside a
function, alias, or handler persists like every other shell.
The owned-Shell modes are genuine runtime forks — a different store — and so
copy state explicitly. Each starts from a freshly-defaulted SessionState and so
holds no terminal authority — TerminalAccess::Denied, no lease — the safe
default for a store that is not the session’s:
spawn_thread— a spawned worker (spawn,par, the detached-worker helper) on a fresh OS thread that owns its own IO; nothing flows back. Runs under a child of the durable root, not the foreground scope, so a turn timeout or Esc does not reach it.inherit_from/return_to— the per-substate manifests a cross-process pipeline stage (child_of, child-eval) leans on. Their asymmetry is the flow matrix: the source cursor (turn.loc) and thewithin-attenuable bits do not flow back, butcontext.cwddoes.child_from— a REPL aside (the prompt/hook shell, one call site in the REPL plugin runtime): an independent sibling that clones the parent’scontext, source cursor, and builtin table without touching its IO / audit / REPL scratch; no flow-back.fork_session— the host session fork (the sub-agent case), the session-scoped specialisation ofchild_from. See agent.
Every genuine fork copies session.builtins (the dispatch table), so dispatch
reaches the child; the same-thread β-step shares it by identity.