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. Two reach outside the module:
machine::evaluate(closure, mooring, shell)(pub(crate)) — injectClosure { comp, env }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, Source, 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 never call either 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/transport.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). 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, pattern defaults — runs a nested machine on the host stack, capped byNESTED_MACHINE_LIMIT), so an unframed reduction is unconstructable.
The result surface is Settled<Value> carrying Escape / BodyResult.
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/beta in
machine.rs step the body’s Closure 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_eval(one arm perCompKind— the ξ-rules),step_returnandstep_halt(one arm perFrameeach — the two frame-table columns).CompKind::Capture(body)installs a buffer throughevaluator::capture’swith_captureand returns the collected bytes exactly, asValue::Bytes, underFrame::Capture; 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 and reads it as the text a value boundary wants inline, no name and no frame a session can intercept (a-coercion-is-syntax, types).CompKind::Bindswapsshell.io.stdoutto the ambient sink before its left computation runs (Frame::Tocarries the prior sink to restore), so aCapturenode one level in only ever drains its own tail’s bytes — the literal continuation of what a separateeval_seqused to flush.CompKind::Rec { group, index }unfolds the n-ary recursive group: every member’s name binds to the thunk of its own projection: a recursive reference forces its name, re-enteringRecand re-extending from the outer environment; a group of one is Levy’srec f. M.CompKind::Execclassifies the head through the lexical environment and dispatches into runtime’scommand_call.CompKind::Pipelinelaunches and joins aPipeNodein one rule (runtime).step_caseselects the arm carrying the scrutinee’s tag, binds the payload (Unitfor a nullary tag) to that arm’s pattern in a fresh environment, and evaluates the arm’s body there — a branch, not a function applied to the payload, so 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.scope.rs— dynamic-frame installation implementing scoping and the five control operators (WithinScope,error_record, thetry/guardoutcome classifierOutcome). 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.pattern.rs— matching:bind_pattern/bind_pattern_stageddestructure aValueagainst a compiledIrPattern(wildcard, name, list with optional...rest, map with pre-elaborated defaults) and fold the result straight 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:stage_patterncollects every binding first, so a pattern that fails partway leaves theEnvit was given untouched.bind_pattern_staged’sobservecallback is howevaluator.rs’srun_phrase_definereaches `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).capture.rs—with_capturefor output capture;redirect.rs— the redirect-frame open/route/restore lifecycle (RedirectState), entered directly bymachine.rs’sFrame::Redirectand bywith_redirectsfor a base-frame native’s synchronous call, distinct from the external-command fd machinery in runtime’scommand/redirect.rs.val.rsholds the side-effect-freeVallayer (close);expr.rsholds the primitive operators the elaborator’s expression desugaring emits (Negate/Not/Binary) and value indexing (Index).- 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::apply_handler(detach’s one-shot handler call) andmachine::evaluate(a re-exec’d stage’s closure) — the boundary itself always evaluates its body in process, OS confinement being per-child inbuild_command(evaluator-runtime-split). audit.rs— execution-tree recording (run_native, the one audited call site for every native).observe.rs—observe, the one reader ofir::Register(ir): the five pseudo-variables ($ENV,$ARGS,$NPROC,$CWD,$USER) and a~-path awaitingHOME, as a total match rather than a string dispatch.$SCRIPTis not among them — the elaborator bakes it to a literal, so no runtime reader exists.
Hot loops poll a cancellation flag cooperatively (hot-path-cancellation).