Fixed arity
Fixed arity binds application: every invocable with a first-class $name
form — builtin, prelude function, user binding — takes a fixed number of value
arguments. There is no variadic application and no defaulted parameter.
Arity is part of a computation’s type. Under call-by-push-value a function consumes a fixed sequence of value arguments; a variable count would make application irregular, and would leave a reified primitive’s type undefined (builtins).
An entry that takes an argv is not applied, so the rule does not reach it.
An external command, an intercepted operation, and a command-shaped builtin
take an argv, not a curried argument sequence: an intercepted operation takes
as many arguments as the call site writes, and echo and detach are variadic
over a list of strings, typed List String -> Return(Bytes, Unit) and
List String -> F Any
(survives-exit-is-its-own-verb).
The manifest is authored as two, and arity is the consequence. A native
table entry declares its arguments, so it has an arity — the curry-spine depth
of its scheme, which is the one place any arity is read from
(BuiltinEntry::fixed_arity, a usize for every entry there) — and it is a
first-class native value. A base-frame manifest row takes an argv instead, so
the argv half has no arity at all: the row is a base frame on the handler
stack, whose arguments arrive as an argv like any command’s
(a-name-is-a-value-or-it-is-handled,
argv-is-a-list-of-strings). So
$name exists exactly where an arity does, by construction rather than by
agreement between a declaration and a check.
Arity binds application; it does not bind writing a name. A builtin is a
function and functions curry, so supplying fewer arguments than the arity is
not an error — it is the residual function, and let f = length,
let f = to-json and let f = range 1 all bind one. A bare name and $name
agree about this; they differ only in that the first is a command and the
second a value.
What is refused is a discarded value that is still waiting for an
argument. A computation whose value is thrown away must be ready to run, not
a function: nothing will ever supply the rest of it, so it cannot have run, and
the statement did nothing. A value is discarded in exactly two places — a
non-tail part of a sequence, and the program itself, whose only surviving trace
is its status. A sequence’s tail is not discarded: it is the block’s value,
and { |x| $x } is a block whose value is a function.
That is not a rule about builtins, and not a new one. It is the demand a
pipeline stage already met — a stage must be a computation
ready to run, not an arrow still waiting — generalised from a stage to any
discarded value. Both readings name the verb when they can: bare cd is
cd expected 1 argument, got 0, rather than an anonymous shape mismatch.
So the two rules are about different things, which is why they do not conflict:
arity binds application, and readiness binds discarding. Over-application is
the arity rule’s business (upper "a" "b" is one T0050, the surplus
inferred but unified against nothing, so a single slip earns a single
diagnostic); under-application is nobody’s until the value is dropped.
That split is the interception rule too. A table entry is a native seeded
into the base env scope, which resolution reaches before the handler stack, so
no installed handler intercepts it under its bare name — only ^name, which
skips the env by definition, reaches one. A base-frame row is a frame at the
bottom of that stack, so a user frame stacks above it and every bare call
arrives there first. Handleability is therefore nothing an entry states about
itself; it is which half of the manifest holds it.
A spread is the notation of an argv. ...$xs splices a list into an argv,
so it may be written only where an argv exists: a command, an external, a base
frame, or a handler or alias arm. A value takes its arguments by application at
a declared arity, so it has no argv and ... has nothing to spread into — a
spread in a value’s argument position is T0056
(SpreadIntoApplication). A list literal’s spread ([1, ...$xs, 2]) and a
rest-pattern ([first, ...rest]) are a different construct, building and
taking apart a list rather than an argument sequence, and this rule leaves them
alone.
The discipline survives at that boundary by packing, not by banning.
render_handler_args (core/src/evaluator/machine.rs) hands a handler its arguments
as one list value — [argv] for a per-name entry, [name, argv] for a
catch-all — every element rendered as it is packed, so a variadic operation is
consumed by a fixed-arity lambda typed Fun(List String, B)
(handler-dispatch,
argv-is-a-list-of-strings).
Variable arity is a surface phenomenon; at the value level it is always a list
of strings.
Optionality is data too, never a hole in an argument list: an
open variant (`some v /
`none), passed as an ordinary value. The caller always supplies every
argument; the value it supplies may carry presence or absence, and no
signature spells an argument the call site may leave out
(no-value-has-an-optional-argument).
This is a hard rule, not a stylistic preference. Do not introduce variadic
application, a splat parameter, a defaulted argument on a function or prelude
binding, a spread in a value’s argument position, or a first-class $name for
an entry that takes an argv.