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.