Builtins: structure and registration

A builtin is a command implemented in Rust that runs inside the shell 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 six facets. builtin_registry! (in builtins.rs) declares each builtin as six facets at once, collapsed into the CORE_BUILTINS static (&[BuiltinEntry]):

  • names;
  • computation-type hint;
  • fixed arity;
  • doc line;
  • type rule;
  • runtime body.

Because the facets are one entry, they cannot drift apart: the arity the checker enforces is the arity the body assumes (fixed-arity). Bodies are grouped by concern (strings.rs, collections.rs, predicates.rs, fs.rs, codecs.rs, caps.rs, concurrency.rs for spawn / watch, modules.rs for use, …).

Registration seeds environments; it is not dispatch. register clones the builtin bindings into each fresh Env, and is_builtin / builtin_names gate that seeding. A call is dispatched later by the evaluator: a bare head resolved by classification reaches the builtin body only after passing the same capability check as any command (grant).

Bundled coreutils are builtins too. builtins/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. They run in-process and through the same capability chokepoint as the structured primitives, which is what lets ral be a single binary with no sibling helpers. The call-side is runtime/command/uutils.rs: when ral’s sinks are direct-to-fd it dispatches a resolved head straight into uutils_invoke; when a capture buffer, audit tee, or env/cwd override forbids that, it demotes the call to a single anchorless run_pipeline stage so the bundled tool runs in a stage helper (pipeline execution) rather than mutating this process’s env from a thread.

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). One core-implemented builtin registers this way too: WATCH_BUILTIN (the public one-entry wrapper over the private builtin_watch / scheme::watch) is installed by the ral host, not seeded into CORE_BUILTINS, so an agent host whose streams are capture buffers simply lacks it (watch-repl-builtin).

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 §16, §21.