Builtins: the structured primitive set
A builtin is a Rust atom the language keeps in-process precisely because the value language cannot reduce it to anything simpler — it reaches a syscall or the shell’s own state, performs a base computation the prelude has no smaller pieces for, or carries a type no ordinary binding could be given. The prelude is written over the builtins, so anything expressible as a value-function is a prelude binding, not a builtin ([[design/name-resolution|where a capability lives]]). The set is small and in-process; it computes structured values or touches the shell’s own state, never the filesystem effects the bundled coreutils own.
Why a capability earns a Rust body
The prelude bottoms out in the builtins, so a capability is one exactly when it cannot be written as a value-function over simpler ones. Three kinds of irreducibility:
- It reaches outside the value language — a syscall or structured OS query
(the filesystem family), or the shell’s own runtime state a spawned process
could never touch (
cd, thespawnfamily,source/use,surface,ask). The query case is a syscall bridge, not text parsing: records and lists in place of a shell-out tostat/ls/dirnameand a re-parse, so the bytes→text→structured round-trip never arises. - It is a base computation — an operation the prelude has no smaller pieces to build from: a regex engine, the string transforms, structural comparison dispatched on the runtime value, scalar coercion.
- Its type cannot be given to an ordinary binding — the codec byte-modes
F[Bytes, ∅]/F[∅, Bytes](codecs) andfail’s divergent, open-row result.
Filesystem effects are deliberately none of these: there is no copy-file or
make-dir, because cp / mv / rm / mkdir already own that and a second
spelling would be a second thing to keep capability-checked
(name-resolution).
The families
The core entries group by what they compute:
- List & higher-order —
eachmapfilterfoldsort-listsort-list-byrange. Each takes a thunk, and the callback’s pipeline modes are universally quantified:map { echo $x }typechecks because the callback may itself write bytes while the list operation still returns a value (equality-constrained modes). - String & regex —
upperlowerdedentsliceintercalatere-matchre-splitre-find-matchre-find-matchesre-replacere-replace-allstring-replaceshell-quoteshell-split. - Parsing —
intfloatstr: value→scalar coercions. - Structure & comparison —
lengthis-emptykeyshasequalltgt: ad-hoc-polymorphic, dispatched on the runtime value’s shape. - Structured filesystem queries —
list-dirfile-infois-file/is-dir/is-link/is-readable/is-writableexistsglobresolve-pathabsolute-pathtemp-dir/temp-file. - Codecs — the
from-X/to-Xpairs and the streamingfold-lines: the typed byte↔value crossing, given its own page (codecs). - Concurrency —
spawnwatchawaitpollracecancel: a worker scheduler and theHandle αvalues only the host runtime can mint.pollis the non-blocking probe of a handle — total over a finished block, reporting completion or failure as one settle variant rather than blocking or re-raising (the settle decision). - Session & terminal —
cdcwdalias/unaliassource/useexit/quitaskechoclearresetsurfacehelp, with the underscore probes_type/_ansi-ok.
fail sits outside these: it diverges rather than computing, and its role in
fallback chains is failure.
Two ways a builtin is typed
The registry’s six-facet entry carries a typing rule whose shape follows from how command-like the builtin is:
Scheme— an ordinary first-class polytype, allocated fresh per call. The default: a curried function usable in command position and reifiable as a value ($map). A builtin is aSchemewhenever its surface is an ordinary curried function.fold-linesis aSchemelike any other; its byte-boundary modes — bytes in, output following the callback — are invisible to a structural projection of the type, so its scheme constructor writes them in viareducer_spec(codecs).Sig— a command signature: argv shape and result computation read directly, without falling through to command-name classification. This is for builtins whose surface is not a curried value — variadic (echo,help), optional-argument (thefrom-Xcodecs, which read stdin when given none), divergent (fail, resultNever, carrying the nonzero-status diagnostic — failure), or carrying a compile-time probe (_type). ASigstill exposes a first-class form when it declares a value scheme ($range,$int).
Each codec being its own Sig rather than one polymorphic decode / encode is
what lets from-json < file dispatch straight through the command arm with the
concrete return type in view, and a misspelled codec fail at command lookup
rather than as a runtime “unknown codec” string.
Reification routes through the name
A builtin used in value position ($upper) is η-expanded into a curried lambda
whose body is a name-dispatched command, not a direct call into the Rust body.
So a reified primitive is an ordinary closure that composes like a user function,
and a later within [handlers: …] frame or alias still intercepts it, because
dispatch happens by name at application time (resolution).
Fixed arity (fixed-arity) is what makes the
η-expansion well-defined; variadic and command-only entries (echo, help) have
no first-class form.
See also syscalls-are-effects (builtins are the pure fragment — not every kernel call is an effect),
name-resolution, codecs,
failure, pipelines;
builtins-registry, map: builtins,
map: typecheck.
Cite: RATIONALE §“Syscall bridge, not text parsing”,
§“Control flow is library”; docs/SPEC.md §16, §17, §21.