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 operatorswithin, grant, try, guard, audit (with the syntactic if / 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 (for calls each, lines splits on \n). They curry and shadow like any binding (docs/SPEC.md §17).
  • Bundled coreutilsls, cat, cp, mv, rm, … : Rust, in-process, but not registry entries. The runtime command layer dispatches a bare invocation through coreutils_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 a String (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, or remove-file; cp / mv / rm / mkdir are 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-dir answer with records and lists, replacing a shell-out to stat / ls / dirname and a re-parse of its text. The bytes→text→structured round-trip never arises. (absolute-path is resolve-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 / retry out 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.