Map: core / evaluator
core/src/evaluator/ runs the CBPV IR as one CEK machine
(machine.rs) — the full narrative is
the evaluator machine. Evaluation is
entered only through framed run doors; the machine’s own verbs are
crate-private. Three reach outside the module — evaluate, apply, and its
argument-free twin force:
machine::evaluate(comp, env, mooring, shell)(pub(crate)) — inject the computation closure ⟨M, ρ⟩ over the empty stack and step until it is empty.run_phrases(evaluator.rs) is the phrase-level verb a tool call, a REPL run, or a script line settles through: it threads aToplevel’sPhrase::{Define, Run}sequence over a localEstarting fromenv, running each phrase as its own closed machine, and — underMode::Sessionalone — writing each landedDefinestraight intoshell.envas it lands, not as a post-run install (unify-turn-evaluation, the-evaluator-steps-closures). Hosts call none of the four directly: they enter through the framedShell::rundoor and the run spine behind it, both incore/src/run.rs, the sole way into evaluation — itsRun(core/src/protocol.rs) carries aProgramof source text or a registered hook. The run door checkpoints and rolls back(env, context)around every run, so a panicking run reports as a failed run instead of corrupting the store.machine::apply(f, args, mooring, shell)(pub(crate)) — the same, from a closed value meeting arguments (Machine::applyingsets the first state);machine::force(v, mooring, shell)is its argument-free twin, the door of a hook’s arity-0 entry and of a worker’s body. A host reaches it only through the run door’s hook-program arm or the in-frame builtin wrapper (a native applying a user function — collection combinators, hook dispatch — runs a nested machine on the host stack, capped byNESTED_MACHINE_LIMIT), so an unframed reduction is unconstructable.
The result surface is Settled<Value>, whose Break is a catchable Error
or an uncatchable Escape.
A tail call binds its argument into the closure’s environment and puts the body
straight in focus,
so it costs no frame and depth is simply stack.len(); reserve — checked
before any pushing rule’s effect — is the cap (session.stack_limit, the
--recursion-limit knob). The escape-propagation guarantees (try does not
swallow exit, grant does not bypass tail calls) are regression-tested
(escape-propagation-bugs).
A same-thread β-step — forcing a block or applying a lambda — evaluates
in place on the caller’s Shell, no snapshot or restore: force puts a
thunk’s computation and environment in focus and beta a λ’s body, directly,
so io, session, and
local state are simply the one Shell’s and the caller’s &Mooring is
passed along. Block and lambda entry are uniform: an unbracketed store write
in either body (cd, alias, a hook registration) persists to the caller,
no snapshot standing between the body and the store
(the-evaluator-steps-closures).
The Value-level force/block split stays intact
(force-eliminates-blocks); only
their shared store-threading is made literal. The Shell lifetime regions
this shares belong to shell-state.
Internals:
machine.rs— the whole machine:Machine { focus: Focus, stack: Vec<Frame> },step_evaland its rule tableeval_rules(one arm perCompKind— the ξ-rules, each raising with?sostep_eval’sstampis their single exit),step_returnandstep_halt(one arm perFrameeach — the two frame-table columns). Every rule,beta,apply_ruleandforceincluded, returnsSettled<Focus>, andstepfolds theErrintoFocus::Haltonce.reserveis the stack cap and hands back theSlotthat is the only way to push, so “infallible after reserve” is a type;Frame::abandonis the one undo forUnmask,Within,Grant,AuditandGather, shared by the rules and the panic walk.Focus::EvalandTerminal::Lambdahold a computation closure{ comp, env }, a type apart from the thunk value’sClosure;Frame::Toalone carries anEnv, andApply,TryandGuardnone.CompKind::Capture(body)pushesFrame::Capture, which holds the sink it replaced, and swaps a fresh buffer in asshell.io.stdout; on return it restores the sink and yields the buffer exactly, asValue::Bytes, ignoring the body’s own value (cap : F^w Unit → F Bytes), and a buffer that overflowedSINK_BUFFER_CAPiscapture_overflowed; on a halt it flushes what the body wrote to the sink it replaced (Shell::write_sink) and propagates the halt. The checker binds that value to a fresh name and composes aDecodenode over it, which — since the kernel’sdecodetakes a value, not a computation — has no frame of its own:step_evalcloses the bound variable, drops the bind’s scope so the buffer is unshared and moves into the string uncopied, and reads it as text after dropping one trailing terminator, strictly, or fails with “captured output is not valid UTF-8 text” (capture-is-decided-by-syntax, a-coercion-is-syntax, types).CompKind::BindpushesFrame::Toand evaluates its right-hand side; a discarded statement writes to whatevershell.io.stdoutis, a capture’s buffer inside a capture.CompKind::Ifandstep_caseforce the arm they choose in place (Machine::force_val: a literal thunk runs in the current environment, closing nothing; any other value is forced as a value);step_casefirst pushes aFrame::Applycarrying the payload (Unitfor a nullary tag), so the arm, a thunk of a function of the payload, receives it, and its store effects outlive thecaseas anifbody’s do (case-is-syntax-try-is-not). The unmatched-tag error is unreachable from source — the checker has proved coverage — and remains for a variant that arrives untyped.CompKind::Rec { group: Arc<GroupNode>, index }unfolds the n-ary recursive group:group.occ()— the union of every member’s mentions, computed once when the elaborator built the node — restricts ρ once, and every sibling’s thunk shares that restricted ρ; the member in focus binds tocompitself,Arc::cloned rather than rebuilt, and the rest bind to a freshRecnode over the samegroup. A recursive reference forces its name, re-enteringRecover the forced closure’s own already-restricted environment, sorestrictanswers the identity again and every later unfold shares that one root; a group of one is Levy’srec f. Mand binds its own member to the node already in focus, allocating nothing.CompKind::Assembleis the one rule that does O(data) work over what looks like value syntax — a list/record/map literal with a spread or a computed key, dispatching toassemble.rs; a plain literal stays aReturnand closes for free.CompKind::Execclassifies the head through the lexical environment and dispatches into runtime’scommand_call.CompKind::Pipelinelaunches and joins aPipeNodein one rule (runtime).scope.rs— dynamic-frame installation implementing scoping and the frame-installing control operators (WithinScope; a failedtry/guard/auditbody becomes anErrorRecordthroughError::record).spawninstalls no frame:machine.rsforms its operands and hands them toconcurrency::spawn_form(builtins).gatherisFrame::Gatherinmachine.rs: it swaps the shell’s cradle for one owning a freshBrood(Cradle::owning), and on exit restores it and settles the brood byconcurrency::exit_broodon return,Brood::endon a halt. Thewithinform installs command handlers: a per-name handler and every alias must be a unary lambda{ |args| ... }, the catch-all a binary lambda{ |name args| ... }; the calling convention is fixed by the surface form and validated at the install boundary byvalidate_handler_arity, never sniffed from the runtime value (handlers-and-aliases-are-lambdas). The handler-stack mechanics live in handler-dispatch.WithinUndoholds the whole cwd cell adir:displaced, so acdin the body is undone on every exit (within-dir-is-local-state).pattern.rs— matching, pure:destructure(pattern, value)returns the(name, value)pairs a compiledIrPattern(wildcard, name, list with optional...rest, map by key) binds in aValue, andbindfolds them into anEnv. Without a...resttail a list pattern must cover the value exactly — a longer list errors rather than silently dropping its extra elements. A mismatch is a located runtime error with anexpected … got …message and a shape hint, propagating like any other failure and so catchable bytry. All-or-nothing:destructurecollects every binding first, so a pattern that fails partway binds nothing.Defineis the one consumer that attaches schemes and observes:Ran::defineinevaluator.rsreaches `Shell::note_define` beside each name’s install, underMode::Sessionalone, so only a session-scope write stamps the binding-lease ledger; a block, lambda, orRecgroup’s fixpoint pre-install binds unobserved (agent-binding-reaping).- Output capture (
with_capture, the audit tee, oneSinkScope) and the redirect-frame open/route/restore lifecycle (RedirectState, the one interpreter of aRedirectslist) live in runtime (runtime/capture.rs,runtime/redirect/scope.rs); the machine enters the latter fromFrame::Redirect, andwith_redirectsserves a base-frame native’s synchronous call and a fused external, whose child then reads the installed sinks incommand/stdio.rs. Targets open stdin, stdout, stderr; every write is observed at settle, carrying its target before and after. val.rsholds the side-effect-freeVallayer:form(val, env, sig)is CBPV’s one-step rule — a constant is itself, a name islookup, a variant forms its payload,thunk Mbuilds⟨M, ρ|occ(M)⟩throughClosure::new(readingoccoff theThunkNoderather than walkingM), and a plain list/record/map literal — no spread, no computed key — isList::literalorMap::literal, the value closure⟨V, ρ|occ(V)⟩, unless one of the names it directly mentions is answered only by Σ, in which caseformbuilds it eagerly instead so that name is never asked of aLiteral’s Σ-free inspection.expr.rsholds the primitive operators the elaborator’s expression desugaring emits (Negate/Not/Binary) and value indexing (Index).assemble.rsis theCompKind::Assemblerule:eval_listsplices...spreadelements (cons/snoc-shaped fast paths reuse the spread’s persistent spine; explicit beats spread, first spread wins),eval_mapis shared by record and map assembly (MapParts/MapPartread either entry enum uniformly) — explicit entries win over spreads, a computed key must close to aString, and a duplicate discovered only here is an error (“key ‘k’ computed twice; which did you mean?”, SPEC §4.5), as a static duplicate is refused at check time.- The command/pipeline machinery — external-command dispatch,
pipeline planning and execution, and the in-process-vs-sandboxed-child
dispatch choice — lives in runtime, which the machine
reaches by dispatching an
Execnode throughcommand_call::classify_command→run_base_frame/run_external, and atPipeNode::launch/join; runtime re-enters the machine only throughmachine::STAGE_EVAL(evaluateandclose_args, passed toPipeNode::launch; a stage’s(comp, env), run frompipeline/thread.rs— a stage never rides a re-exec) — the boundary itself always evaluates its body in process, OS confinement being per-child inbuild_launch(evaluator-runtime-split). call.rs—run_native, the one call site for every native: it runs the body inBuiltinEntry::framed’s frame, the only minter of theFrameproofcall_bodydemands. A builtin application is no observation; the fan-out istypes::audit::door.- A
~-path (CompKind::Tilde, ir) is read inmachine.rsthroughContext::require_homeand shown byTildePath::shown(the one spelling~,homeand a~/…command head share). The six ambient reads (cwd,env,args,user,home,nproc) are natives inbuiltins/ambient.rs(ambient-reads-are-builtins).$SCRIPTis neither — the elaborator bakes it to a literal.
Hot loops poll a cancellation flag cooperatively (hot-path-cancellation).