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. A BuiltinEntry is a
Decl (everything the checker may know: name, Convention, doc, type rule,
diagnostic, a boundary flag) plus the body only the runtime runs; its
constructors set boundary from the body they are handed, so the flag and the
body cannot disagree. Arity is no facet: Decl::fixed_arity derives it from the
type rule and caches it with the settle-at-Unit fact in one Spine, a usize
for every entry in the table. What a row does with stdout is
in its scheme: a writing row (the encoders, echo, help, explain, clear,
reset, ints-to-bytes) ends in Command, F^w Unit, and there is no
separate declaration
(graded-f). Settling at Unit is enforced at the
same door as arity:
Decl::settles_at_unit reads the declared result off that curry spine,
and a row whose scheme says Unit answers unit from call_body
whatever its Rust body computed, so no builtin
can hand a value of another type to a name the checker believes is Unit.
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, Runtime::install_set in types/shell/construct.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 (scheme::fold_lines) is a
plain scheme (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_SETS
(the core rows, the boundaries and the base frames, one list), and a host’s
extra sets ride a HostSurface into HostSurface::shell and 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 be built over a Runtime that carries them or re-link a native by name.
register clones the baked prelude’s bindings into each fresh environment.
BOUNDARY_BUILTINS (from-json, from-jsonl, from-json-at, use, worker) also sits
outside it, for a different reason: each row is a BuiltinEntry::boundary, whose
body takes the Site the checker solved at the call and admits the value it
lets in against it (a-boundary-is-checked-against-its-type).
Three entries sit outside the macro, implemented in core but installed by a
host. The public WATCH_BUILTIN
(&[BuiltinEntry]) wraps the still-private concurrency::builtin_watch /
scheme::watch — a watched worker’s lines leave as `watch [label, line]
surfaces through the session’s deferred sink (Sink::Watch), which outlives
the run — so only a host with a deferred sink installs it, and elsewhere
watch is an unknown-name diagnostic rather than a runtime refusal
(watch-repl-builtin). The second,
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(Flag::Detach)). The third, SURFACE_BUILTIN, wraps
misc::builtin_surface / scheme::surface_op under the bare name surface;
exarch declares its own entry over the same body under exarch-surface, with
its own doc, so no host’s name enters core’s vocabulary. builtin_surface is
pub for exactly this — the one core body two hosts each name their own way.
Bodies are grouped by concern, one submodule each:
strings.rs— string and regex primitives, includinglines(String -> [String]), the same split asfrom-linesrun over a string’s bytes rather than the channel, sharingutil::read_lines/lossy_line_list.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(keys,has: ordering and equality areValue::compare/Value::equals, andlt,gt,is-empty,sort-list,identityare prelude one-liners over<,>,lengthandsort-list-by),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 stdout.write_encoded(codecs.rs) writes its bytes to stdout and returnsValue::Unit, soto-csv,to-bytes,ints-to-bytes,to-string,to-lines,to-json, andto-jsonlare writers: each typesA → Commandat its own operand type, and its encoded bytes are what it writes.to-bytestakesBytesandints-to-bytestakes[Int]— two names, no union in the table.builtin_from_jsonlreads throughutil::stdin_linesand parses each line alone, so an error names the input’s line. Each JSON direction has one typed refusal, worded by each codec in its own terms:json_to_value’sOutOfRange,value_to_json’sUnrepresentable, whichto-jsonlprefixes with the element’s index;shell.rs—cd,alias/unalias;concurrency.rs— thespawnform’s door (spawn_form),watch/detach, the handle verbsawait/poll/first-settled/race/cancel/worker, and theworkerslisting (builtin_workers), split by concern: the root keeps the verbs of birth,concurrency/birth.rstheBirthdoor (spawn_child) and the one exit rule a gather and a worker’s base share (exit_brood),concurrency/eliminate.rsthe eliminators,worker, and the one cancel-aware sweep (first_ready) underawait,race, a gather’s join andfirst-settled(builtin_first_settled;raceends its losers throughend_unclaimed), andconcurrency/tests.rsthe tests; the worker’s deferred surface lives intypes/handle/surface.rs, its cradle and brood intypes/handle/brood.rs(builtins under their bare names;is-done,par,bothandfirst-ofare prelude code over them, prelude).spawnandgatherare control operators, not rows: the machine formsspawn’s operands and calls its door, and runsgatheras a frame (Frame::Gather) that settles its brood byexit_brood(evaluator). All butworkerand the host-installed two seed throughCORE_BUILTINS;workerseeds throughBOUNDARY_BUILTINS, and the other two live here too but reach a session viaWATCH_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. A handle is one private lock over aHandleState:Running,Settled(CompletedHandle { stdout, stderr, end }), orClaimed.HandleInner::newreturns the handle with its oneSettler, which the worker thread owns:Settler::settleflushes the worker’s deferred surface, thencompletedrains the buffers intoSettled(or drops the end of a handle alreadyClaimed) and sets the endStamp; aSettlerdropped unsettled — a panic — settles the handle as a panic naming the worker, sopoll/racesee a finished block rather than spinning. Observers read aSample—Pending,Settled(R)orClaimed— and the only transitions arecomplete,observe(a read that lends the result to a closure under the lock),claim_settled(moves a settled result out and leavesClaimed, in one step) andclaim_and_stop(the only way fromRunningtoClaimed: it fires the worker’s node, and discards a settled result). So a result is claimed exactly once, by construction. The registry lock may take a handle’s lock, never the reverse.awaitis exact:joinwaits (claim_first) until it claims the handle’s settled result or finds it claimed by another;exactreplays the stdout bytes, then the stderr bytes, into the awaiter’s own sinks andconcludes as the block ended — the value (admitted byadmit_handled), the failure as it stands, anexitor a stop re-raised, a panic resumed — soawaitof a block is that block run there. A handle another claimer has failsawait_handlewith<verb>: this worker's result was already claimed, an ordinary error, andjoinreturnsNonefor it, which a worker’s return rule skips.raceclaims the first handle it finds settled, in list order, skipping any another caller claimed, and fails asawaitdoes only once every handle is claimed, at entry or mid-wait; it then claims, stops and drains its losers before it returns. An empty list is its own error.cancelclaims the handle and stops it, and is nothing on one already claimed. The handle carries its block’s grade (Ty::Handle(Grade, Ty), printedHandle (Returns α)/Handle Command), soawaitreturns the grade the block had.pollnever writes and never claims, reporting the settle as`settled{stdout, stderr, elapsed, outcome:ok/err}(the`errpayload built throughError::record, the recordtryhands its handler) or`pending{stdout, stderr, elapsed}(a cumulative, non-destructiveCapturedBytes::peeksnapshot of the running worker’s output, peeked before the handle is observed — the buffers are left for the worker’s one-shotCapturedBytes::takeas it settles, so a partial poll never steals bytes) — the block’s outcome is data, not a status. A worker stopped from elsewhere settles with its ownWorkerStoppedrecord, anexit, a foreign stop or a panic is re-raised, asawaitre-raises them:pollis total on an unclaimed handle, and a handlecancelorawaitclaimed fails as it does forawait(delimiters-are-handlers-for-poll, the settle decision, partial-poll-pending-output). Aspawnorwatchworker hangs under its shell’s cradle, not the run’s foreground scope: under the durable session root in a run outside any gather, so a foreground cancel never reaps it, and inside a gather — a worker’s body or agather { … }— under the gather’s entry scope, enrolled in itsBrood(Cradle::owning): each child awaited on return, cancelled and drained on a halt, a child another claimer has skipped, and every cancelled child waited for within the grace (Brood::wait_cancelled), socancelof a worker reaches its tree.awaitsharesrace’s cancel-aware wait loop (claim_first, overfirst_ready), so a deadline unwinds the wait while a session worker survives (await-is-the-block-run-here, gather). The spawn door isspawn_child, which takes aBirth—SpawnorWatch— and reads the owner off the cradle: with a brood in scope the child is the brood’s (Owner::GatherorOwner::Worker, from itsBroodOwner), enrolled, writing to the run’s live sink as a pipeline stage does, unannounced and flushing no`done; with none it is the session’s, announced, its surface deferred. It mints the worker’sWorkerIdfirst and builds the handle around it:HandleInnercarries theid, thetitle, and the worker’s clock, a birthStampand an endStampthat itsSettlersets on every exit path of the worker thread, from whichelapsedandsettled_atare derived, each in one place. The registry entry adds only theOwner. A birth’stitle— the name every eliminator’s error, the trail, and the completion event use — comes from the birth itself: aspawn’s title (non-empty and one line,legible_title), elseshell.call_site()—block at <script>, line N— which the run’sTitlePolicymay refuse before any birth; a watch’s label. The completion event (DoneEvent) carries the handle’s id, title and elapsed time beside its outcome, including on failure and panic. No policy reaps a running worker: it ends by completing, bycancel, or with its owner.worker <id>(builtin_worker, a boundary row) takes back the handle of any worker still in the registry, running or settled, by that id; the value it settles with is admitted at theawaitsite (admit_handled). The spawn door also enforces the frame’s admission cap (Mooring::worker_cap): a birth reserves its seat at the door (WorkerRegistry::reserve) — refused whilecapworkers are running or reserved, with an error naming the verb the caller actually wrote andawait/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; settled entries lingering under retention hold no seat. A settled entry’s one bound 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 once its unclaimed result has sat stamped a full retention of ral calls. The expiry records aRetention-causeReapNotice, which the 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). An unarmed registry (the REPL) retains settled entries indefinitely; a claimer still removes an entry the moment it claims the result, so the sweep only catches what nobody claimed. A worker runs its thunk on a freshstd::threadviaShell::spawn_thread(shell-state), seeded from the parent’s session;worker_bodyhands the thunk, its capture riding in its closure, straight tomachine::force(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,cancel, and a brood’s join or end claim the handle (so no other brood answers for it) and remove the entry from whichever shell claims it, andpoll, on either arm, or aworkerslisting never touches the registry. A listing is data;worker <id>is the door.workersmaps every entry, running or retained, ids ascending, throughworker_row(types/shell/host.rs, shared with the`workersprobe) to a seven-field record —id,title,owner(`session,`worker id,`gather),state(`runningor`donewith theDoneending),started(Unix seconds),elapsed(seconds, off the handle’sStamps),settled(`someUnix seconds or`none) — reading the state withsettled, which caches but claims nothing, and omitting a handle claimed since the entry was read (WorkerRow::ral_value; the probe’s eighth field,retention-left, is the host’s alone). It holds no handle, so a top-levelworkerscrosses the engine protocol. AHandleis process-local:Shell::bequeathscrubs it from every fork, and a run whose result is or holds one endsUnreturnable(core/src/run/report.rs);modules.rs— the cachelessuseloader (a boundary: it builds the export record and admits it withSite::admit_module, holding each function to the scheme the module was checked at). It runs its phrases throughload::module_phrases, the cycle stack and depth bound it shares with the host loading doorload::evaluate_source(rank 10,core/src/load.rs, which also holdscheck_source, the compile door both share: it checks against the live session, registering the source first so the module’s spans carry its real identity, and hands back a failure as anErrorcarrying itsRejection(diagnostics));useis a scope-projecting caller of that door, running under the session environment rather than the caller’s own block-local scope. Module loads carry no cache, so the guards keep re-evaluation terminating — see cacheless-module-loader;misc.rs— includingbuiltin_surface, the bodySURFACE_BUILTIN(above) wraps: it 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 language and command index) andexplain <name>lookup, both reflecting on the session — its scope and the shell’s registries, never the caller’s block.LANGUAGE_DOCSgives every parser keyword and Boolean literal a standalone guide; a completeness test pairs that table withCONTROL_OPERATORS,FLOW_KEYWORDS, andLANGUAGE_CONSTANTS, so new syntax cannot land undocumented.LANGUAGE_GUIDESadds reference topics to the same lookup and index, includingredirects.withinowns handler and option restrictions, andreturncovers host configuration records;trydetails the error record’s optional source location. Thegrantguide owns the capability schema and examples of exclusions and computed paths; the agent prompt points here for those details. OneWhereanswers both halves of a resolved command entry: the line naming the frame that would run, and the doc ladder that asks that registry — a session binding owning its name outright rather than inheriting the doc of what it shadows. The shared renderer separates a doc’s conventionalusage — proseopening and labelsUsage:,Type:,Source:, andShadows:; absent facts are omitted.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. One layout rule belongs to the printer, not to any value: a list of two or more non-empty records sharing one key sequence prints one record per line, eachlabel: valuecell padded to its column’s widest, still pasteable surface syntax (rows); a cell that would break a line, or a list past the depth limit, keeps the ordinary layout. So aworkersresult reads as a table in the REPL and in exarch’sVALUEalike, and no worker-specific code renders anything;util.rs— shared helpers.read_linesis the one line reader (codecs’ line rule), generic over its byte source and leaving each line undecoded, since decoding is each caller’s policy;stdin_linesruns it overstdin_reader, andlossy_line_listdecodes its lines into the[String]thatfrom-linesandlinesreturn.
The capability Value-map decoder is not a builtin: it lives beside the
authority layer in guard/decode.rs (decode_capability_map), consumed by
the grant control operator (evaluator/scope.rs) and the --capabilities
ceiling (load/profile.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 Program::Tool 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.