Name resolution: where a capability lives
A name in head position resolves through a fixed layering, and which layer a
capability belongs to is a design decision — driven by whether it returns a
structured value or performs an effect, and by who owns it. No single
mechanism is “the builtins”; this page answers where a capability lands and
why, leaving what a builtin is to builtins, the how of
registration to builtins-registry, and the
formal catalog to docs/SPEC.md §16.
The layers a head name can be
From most reserved to most peripheral:
- Control operators —
within,grant,try,guard,audit(with the syntacticif/case/?). Grammar arms carried as dedicated IR nodes, not builtins, because each manipulates dynamic frames or audit ownership that no value can observe (control-operators). - Core builtins — Rust atoms in
CORE_BUILTINS, each a six-facet registry entry binding names, type rule, fixed arity (fixed-arity), doc, and body together. What makes a capability one of these, and the shape of the set, is builtins. - Underscore primitives —
_type,_ansi-ok, and the host_ed-*/_plugin: internals the prelude wraps, never called directly by user scripts. - Prelude functions — ordinary ral bindings in scope before user code,
wrapping the layers below for convenience (
forcallseach,linessplits on\n). They curry and shadow like any binding (docs/SPEC.md§17). - Bundled coreutils —
ls,cat,cp,mv,rm, … : Rust, in-process, but not registry entries. The runtime command layer dispatches a bare invocation throughcoreutils_invoke, riding the same capability chokepoint and wait/signal/exit boundary as a system binary. - Host builtins — registered above core by the embedder (the REPL’s
_ed-*, exarch’s edit/search atoms); core never inspects them (repl-builtins-stay-in-repl). - External commands — resolved on
PATH; their captured stdout decodes as aString(one trailing newline stripped), never re-lexed.
Coreutils running in-process beside the structured primitives is what lets ral be a single binary with no sibling helpers.
The principle: which layer a capability belongs to
Three rules decide the placement, and they are not interchangeable:
- Effects go through coreutils, never structured primitives. There is no
copy-file,make-dir, orremove-file;cp/mv/rm/mkdirare canonical. An effect returns no structured value, so wrapping it in a builtin buys nothing but a second spelling — and a second spelling is a second thing to keep capability-checked. - Structured queries are Rust builtins — a syscall bridge, not text parsing.
list-dir,file-info,is-file/is-dir/exists,glob,resolve-path,temp-diranswer with records and lists, replacing a shell-out tostat/ls/dirnameand a re-parse of its text. The bytes→text→structured round-trip never arises. (absolute-pathisresolve-path’s pure lexical sibling — it earns builtin status by needing host state, the logical cwd, not a syscall.) - Conveniences are prelude. If a capability is expressible as a function over
values and thunks under ordinary Hindley–Milner typing, it is a prelude binding,
not a builtin — the same derivability test that keeps
for/retryout of the grammar (control-operators). A builtin earns Rust only by needing host state, a type rule HM cannot derive, or a syscall.
Ambient scope is the fourth placement, already settled: it is a control operator, because it scopes frames over a body’s whole dynamic extent rather than computing a value.
The one boundary this layering does not draw is between bytes and values: that
crossing is named by the from-X / to-X codecs, which are core builtins like
any other (codecs).
See also syscalls-are-effects (the layering is the pure-fragment/effect boundary),
builtins, codecs,
pipelines, control-operators,
grant; builtins-registry,
map: builtins.
Cite: RATIONALE §“External commands return strings”,
§“Bundled coreutils are mandatory in exarch, optional in ral”,
§“Syscall bridge, not text parsing”, §“Control flow is library”;
docs/SPEC.md §4.2, §16, §17, §21.