Builtins: structure and registration
A builtin is a Rust atom the shell keeps in-process. core/src/builtins/ defines
them; the question this page answers is how one declaration stays coherent
across the typechecker and the evaluator.
One macro binds the facets. builtin_registry! (in builtins.rs) declares
each builtin as one row, collapsed into the CORE_BUILTINS static
(&[BuiltinEntry]):
- names;
- doc line;
- type rule — a scheme factory, and nothing else;
- an optional diagnostic facet — what a misuse this verb has a name for earns:
a decoder’s argument,
fail’s literal zero status; - runtime body.
Because the facets are one entry, they cannot drift apart. Arity is not among
them: BuiltinEntry::fixed_arity derives it from the type rule — the curry
depth of the scheme — and answers a usize for every entry in the table, so
the arity the checker enforces is the arity the body assumes with nothing to
keep in agreement (fixed-arity). The derivation runs
a throwaway unifier, so it is cached; it feeds the checker’s diagnostics as
well as the evaluator’s application gate.
The diagnostic facet is deliberately not a typing rule. It says what a wrong
call means for this verb, which no polytype can state — that an argument
written to a decoder is a misreading of where the bytes come from, or that
fail [status: 0] wants return. Everything a type can state is in the
scheme. Bodies are grouped by concern
(strings.rs, collections.rs, predicates.rs, fs.rs, codecs.rs,
concurrency.rs for spawn / watch, modules.rs for use, misc.rs for
the ones with no cluster — fail, exit, surface, warn, ask — …).
The two halves are the two argument conventions, and the choice is visible in
the surface. echo is a base frame because a diagnostic-or-payload line takes
an argv; warn is a table entry because it takes one String. So $warn is a
value and $echo is not, and warn curries where echo spreads
(diagnostics-are-a-builtin).
The manifest is a boot manifest; it is not a resolution layer. It is
authored as two, so installing a set is two installs rather than one sorting
(seed_natives_and_base, with native_value — total over the table — the one
door onto an entry’s value form, shared with wire hydration): a table entry
becomes a Value::Native in the shell’s base scope, reached as an ordinary
binding; a base-frame row becomes a base frame under the handler stack’s run
frames (handler-dispatch,
argv-is-a-list-of-strings).
Dispatch consults no builtin table
(a-name-is-a-value-or-it-is-handled);
the installed table remains as the manifest that the checker’s rule side-table,
help/explain, tab completion, and wire re-linking are read from. The base scope is
reached by lookup after the user scopes and enumerated by no harvest — the
binding harvests walk user scopes only, which is what keeps a pristine native
typed by its Sig rule rather than as a binding, and its head a command rather
than an application. true and false join it as language constants.
Bundled coreutils are not builtins. They are exec images, so they live
outside the manifest module: core/src/uutils.rs declares the vendored tools via
declare_coreutils! as two cfg-gated lists — cross (always on) and unix
(Unix-only) — emitting one COREUTILS_TOOLS slice and a coreutils_invoke
dispatcher; RIPGREP_TOOLS routes rg through ral-ripgrep-core. A bundled
head resolves to an ExecImage::BundledTool and is always born as a
ral --ral-bundled-tool <tool> child, so it carries ordinary process semantics
and the same capability chokepoint as a host external
(bundled-tools-always-reexec) —
which is what lets ral be a single binary with no
sibling helpers.
Host layers register their own, above core. The REPL adds the
_ed-* editor builtins and exarch adds its resident host atoms; both sit above
core and core never inspects them
(repl-builtins-stay-in-repl), and
they seed through the same two installs, so a host contributes natives as well
as base frames: the REPL’s jobs/fg/bg/disown are natives, detach a
base frame on a host that arms the policy. Three core-implemented entries
register this way too — WATCH_BUILTIN, SERVICE_BUILTIN, and
DETACH_BUILTIN, each a public one-entry wrapper over a private body — so a
host whose streams are capture buffers, or whose leases reap ordinary workers,
simply lacks the verb it cannot honour
(watch-repl-builtin,
builtins).
Why these primitives exist and how the set is shaped is
builtins; which layer any given capability lands in is
name-resolution. See also map
builtins. docs/SPEC.md §14, §16.7.