Map: core / builtins
core/src/builtins/ are the primitives implemented in Rust that run inside the
shell process. builtins.rs holds the builtin_registry! macro: each entry
binds its facets at once — names, type rule (ty),
doc line, and runtime body (call) — into the CORE_BUILTINS static
(&[BuiltinEntry]), so the facets cannot drift apart. Arity is no facet:
BuiltinEntry::fixed_arity derives it from the type rule and caches it, a
usize for every entry in the table. The manifest is authored as two, and
that authoring — not the arity — is the classification: a table entry seeds a
Value::Native in the base scope, a base-frame row seeds a base handler frame
(native_value, seed_natives_and_base in types/shell/host.rs;
a-name-is-a-value-or-it-is-handled,
argv-is-a-list-of-strings).
A body is a BuiltinBody — a Static fn(&[Value], &Mooring, &mut Shell) -> Settled<Value>, or a Captured closure of the same shape for a host frontend
with state to carry — so the run’s mooring arrives beside the shell
(shell-state): it is how a body surfaces an event,
enquires, or starts a nested run parented under the run that called it. The
type-rule facet is a BuiltinTypeRule, which is a scheme factory —
fn(&mut Unifier) -> Scheme — and nothing else. The streaming reducer
fold-lines is an ordinary one whose factory writes its forwarded
payload route directly (typecheck);
there is no separate reducer arm. Beside it sits an optional diagnostic facet,
which is not a typing rule: it carries what a misuse this verb has a name for
earns — a decoder handed an argument, fail handed a literal zero status
(builtins-registry). An entry’s first-class
form is its scheme, so it has one by construction, every entry in the table
having declared its arguments (fixed-arity).
Builtins are shell-scoped: each shell’s session carries a BuiltinTable
(shell-state) seeded from CORE_BUILTINS
(core_builtin_table), and a host’s extra sets ride a HostSurface into
boot::boot_shell (core/src/boot.rs), so the checker’s rule table, the base
scope, and the base frames all come from the manifest the shell was booted with
— there is no process-global registry, and every path that builds or hydrates a
shell must seed through install_builtins or re-link a native by name.
register clones the baked prelude’s bindings into each fresh environment.
Three entries sit outside the macro, implemented in core but installed by a
host. Two are a pair with the hosts swapped: the public WATCH_BUILTIN
(&[BuiltinEntry]) wraps the still-private concurrency::builtin_watch /
scheme::watch so a host with a durable stdout sink (the interactive and
batch ral hosts) installs it while an agent host omits it
(watch-repl-builtin); its mirror
SERVICE_BUILTIN wraps concurrency::builtin_service / scheme::service so
the agent host (exarch), whose lease frame reaps ordinary workers, installs
the durable-birth verb while the ral hosts — which grant no lease, so every
spawn of theirs already lives until cancel or exit — omit it. The third,
DETACH_BUILTIN (cfg(unix), over concurrency::builtin_detach), is the
base-frame manifest’s second row — typed List String -> F Any, no arity to it
— and is carried by a host that arms a detach policy: installing the verb and
arming the budget (Shell::arm_detach) are one act, so absence is an
unknown-name diagnostic rather than a veto, while
whether a given call may spend it is the live grant stack’s question
(GrantStack::permits_detach).
Bodies are grouped by concern, one submodule each:
strings.rs— string and regex primitives.dedentowns the raw-block framing rule: blank lines around a multiline block fall away before the common margin is stripped, while content-line whitespace is preserved;collections.rs,predicates.rs,fs.rs,codecs.rs— the last is also home tobuiltin_echo,to-line’s neighbour by nature: every argument rendered through the totalto-string—Value’sDisplay, mapped over the argv — single-space intercalation, a newline to the byte channel.write_encoded(codecs.rs) writes its bytes to stdout and returnsValue::Unit, soto-csv,to-bytes,ints-to-bytes,to-string,to-lines, andto-jsonare writers: each typesA → F[Bytes] Unitat its own operand type, and its encoded bytes are its sole payload.to-bytestakesBytesandints-to-bytestakes[Int]— two names, no union in the table;shell.rs—cd,alias/unalias;concurrency.rs—spawn/watch/service/detachand the handle verbsawait/poll/race/cancel(builtins under their bare names;parand theis-donepredicate are prelude code over them, not builtins). All but the host-installed three seed throughCORE_BUILTINS; those live here too but reach a session viaWATCH_BUILTIN/SERVICE_BUILTIN/DETACH_BUILTIN, not core.builtin_detachis the surface discipline alone — the birth itself is the ordinary external-command machinery down to the double-fork inruntime/command/detach.rs(runtime), and it yields a{pid, desc}receipt, not aHandle, so none of the eliminators below apply to it. On completion a block’s buffers drain once into a cachedCompletedHandle { stdout, stderr, outcome }(value.rs); the eliminators project that one settle.try_settleis the shared non-blocking sample (cached outcome, else atry_recvcompleted throughcomplete_handle; aDisconnectedreceiver — a panicked worker — settles as the same failureawaitreports, sopoll/racesee a finished block rather than spinning).await/raceproject_completedthe outcome to{value, stdout, stderr}, re-raising`err;pollis total, wrapping it as`settled{stdout, stderr, outcome:ok/err}(the`errpayload built through the sharedevaluator::scope::error_record, the recordtryhands its handler) or`pending{stdout, stderr}(a cumulative, non-destructivepeek_buffersnapshot of the running worker’s output — the buffers are left for the one-shot completiontake_buffer, so a partial poll never steals bytes) — the block’s outcome is data, not a status.awaitandpollgate first onensure_live, the cancelled pre-check (the settle decision, partial-poll-pending-output). A detached worker hangs under the durable session root, not the run’s foreground scope, so a foreground cancel never reaps it;awaitsharesrace’s cancel-aware wait loop (wait_first_settled), so a deadline unwinds the wait while the root-scoped worker survives (concurrency-detached-vs-structured). Under a frame that grants aWorkerLease,spawnarms a self-re-armingprocess::reapercallback — the idle-observation lease chain: a still-running worker unobserved foridleis reaped, where everypolland everyawait/racesweep renews the handle’slast_observedcell, under an absolutebackstopno polling extends; a worker that finished ends the chain silently, its entry lingering as an unclaimed result. A reap removes the registry entry, records aReapNoticethe engine pushes at the next settled run’s ready boundary as a`noticesurface event (emit_ready_boundary_notices; exarch decodes it back viacard::value_to_notice), and cancels the worker’s scope withDeadline— never detaching the handle, so a laterpoll/awaitstill observes the partial output and failure. The class decides the chain at the spawn door:spawn_childtakes aLeaseClass, and only aWorkerbirth arms it —serviceregistersDurableand arms nothing, so no reaper entry ever exists for it; the absent chain is the durable policy, whose only bounds are the handle’s owncancel, the host’s/clear, and process exit. The spawn door also enforces the frame’s admission cap (Mooring::worker_cap): a birth of any class reserves its seat at the door (WorkerRegistry::reserve) — refused whilecapworkers are running or reserved, with an error namingawait/cancelas the remedies, the reservation held across thread spawn and released into the registered entry, so a racing sibling birth never sees a filling seat as free (workersis retired — builtins); settled entries lingering under retention hold no seat. A settled entry’s own lease is retention, armed once at boot (Shell::arm_worker_retention, beside the binding lease): the registry keeps its own clock — onetick_epochper source dispatch — and the engine sweeps at each settled run’s ready boundary (sweep_retention, engine housekeeping), stamping an entry at the first sweep that observes it settled and expiring it — aRetention-causeReapNoticeon the same drain — once its unclaimed result has sat stamped a full retention of ral calls. An unarmed registry (the REPL) retains settled entries indefinitely; the eliminators still remove entries the moment a result is claimed, so the sweep only catches what nobody claimed. A worker runs its thunk on a freshstd::threadviaShell::spawn_thread(shell-state), which inherits a snapshot of the parent’s env;worker_bodycloses the thunk’sCompover that snapshot as aClosureand hands it straight tomachine::evaluate(evaluator), deliberately bypassing theToplevel/Phraseboundary, because the worker’s ownShellis the only one its bindings touch and they die with the thread. The worker carries the parent’s grant stack, so a forced block inside the worker still meets the standard boundary rule and any external child it spawns is confined per-command — aspawnunder agrantcannot escape it. Everyspawn_childalso files the freshly-minted handle onshell.local.workers— a per-shell registry, host-independent and carrying no policy (shell-state);await,race’s winner and its cancelled losers, and a settledpollremove the entry from whichever shell observes it, an explicitcancelremoves it too, and a pendingpollor a bare listing never touches the registry. AHandleis a resident, process-local reference: it cannot cross the pipeline-stage helper wire, so returning one from a helper-evaluated stage raises the wire diagnostic “cannot return a handle from sandboxed evaluation” (core/src/serial.rs) rather than a generic failure (capability-enforcement);modules.rs— the cachelessuse/sourceloader.evaluate_sourceis the shared parse + elaborate + evaluate core —check_sourcecompiles against the live session, peeking theFileIdits own registration will mint so the module’s spans carry its real identity, andevaluate_checkedholds the cycle stack and depth bound;useis a scope-projecting wrapper over it,sourceevaluates into the caller’s scope. Module loads carry no cache, so the guards keep re-evaluation terminating — see cacheless-module-loader;misc.rs— includingsurface, which forwards a tagged variant to the host’s `SurfaceSink` and is the identity under a bare REPL;math.rs— the Float rounding builtins (round,floor,ceil,trunc);help.rs—help(arity-0 command index) andexplain <name>lookup. OneWhereanswers both halves of an entry: the line naming the frame that would run, and the doc ladder that asks that registry — a local owning its name outright rather than inheriting the doc of what it shadows.locate_allreturns the whole resolution chain, soexplainalso names what a name shadows, a PATH binary included, which is the questionwhichanswers wrongly for every name ral provides;print.rs— the value pretty-printer shared by the REPL and exarch’s tool-result rendering (PrintParams; a rendering utility, not a registered builtin). One printer, one policy, per-reader numbers: truncation preserves identity — the depth limit summarises (keys and heads) and only the floor beneath it counts, a string elides unless it is the whole value, and a byte budget is spent inside each container, which closes with…N more. An elision must earn its marker: a string prints whole unless cutting it actually shortens the rendering, so nothing is mutilated to save a character. The two readers differ in window, quote fence, and absorbable bytes — and in whether a nested string is capped at all: the REPL cuts at a terminal row, exarch’sVALUEsection cuts nothing, since a payload’s text is the identity a lateredit-hashmatches;util.rs— shared helpers, JSON coercion.
The capability Value-map decoder is not a builtin: it lives beside the
authority layer in capability/decode.rs (decode_capability_map), consumed by
the grant control operator (evaluator/scope.rs) and the --capabilities
ceiling (capability/load.rs) — see capabilities,
grant.
Why a capability lands in one of these layers rather than another — builtin vs.
coreutil vs. prelude vs. control operator — is design: name-resolution;
what a builtin is and the shape of the set is design: builtins;
the from-X/to-X byte↔value typing in codecs.rs is design: codecs.
Bundled coreutils, diffutils, and ripgrep
core/src/uutils.rs — a top-level module, since every consumer is exec-side and
the manifest module holds manifest things only — declares the bundled tools as
three feature-gated families and the predicate and dispatch that unify them.
- coreutils —
declare_coreutils!takes two parallel lists:cross(always on under thecoreutilsfeature) andunix(additionally undercoreutils-unix-only,cfg(unix)-gated). It emits one mergedCOREUTILS_TOOLSslice, acoreutils_invokearm, and the platform-unconditionalCOREUTILS_UNIX_ONLY_TOOLSlist — the one authoritative spelling of theunixnames, so a caller that must know a bundled name does not exist off-Unix (a profile loader dropping dead exec grants) reads this list rather than keeping a second copy. - diffutils —
DIFFUTILS_TOOLS(["cmp", "diff"],diffutilsfeature), whosecmp_main/diff_mainshims faithfully translate the upstreamdiffutilslibentrypoints (re-audit on a version bump). - ripgrep —
RIPGREP_TOOLS(["rg"],ripgrepfeature), routed throughral-ripgrep-core::run_clibyrg_main(which drops the argv[0] slot).
uutils_invoke is the bare dispatch over all three families (diffutils and
ripgrep matched ahead of the coreutils fall-through, each arm feature-gated);
is_uutils_tool is the membership predicate. These bundled heads share the
capability chokepoint with every other command — part of why ral is a
single-binary. The grep cargo feature separately
backs the re-* regex string builtins.
A bundled head is a resolved command image, not a builtin in CORE_BUILTINS:
it is always an ordinary ral --ral-bundled-tool <tool> child carrying process
semantics (bundled-tools-as-exec-images,
bundled-tools-always-reexec).
That dispatch — the ExecImage::BundledTool placement and the hidden
entrypoint — is the runtime’s; this page owns only the
registry of names, shims, and the in-binary uutils_invoke they converge on.
docs/SPEC.md §14.7 covers the single-binary tool surface.