The Name That Was Taken
The Scheme interpreter named a function `run`, and on the C backend the call to it compiled to a shell command instead — because `run` is also a built-in, and the compiler matched the built-in before it considered the user's definition. The interpreter had always gotten this right, so only the compiled backend was wrong. It was the fifth name in a slow parade of the same bug — index, remove, acct, dup, and now run — and the pattern was clear enough to stop treating instances and end the category. A single helper now asks whether a name is bound by the user before any built-in claims it, guarding about thirty collision-prone built-ins at their call sites. The guard is provably safe: it changes behavior only when the user actually shadowed a built-in, so every program that did not is byte-for-byte unchanged. A user's definition should win over a built-in, and now it does.
The Scheme interpreter had a function named run — the loop that reads and evaluates each
top-level form. On the interpreter it worked. On the C backend the program did something
bizarre: it tried to execute its own source text as a shell command. The cause was that run
is also a built-in — a way to run an external command line — and the compiler’s code generator
matched that built-in at the call site before it ever considered that the user had defined a
function by the same name. The interpreter resolved names the ordinary way, where a later
binding shadows an earlier one, so it had always been correct; only the compiled backend
reached for the built-in first.
The fifth time is a pattern
This was not the first name to collide. It was the fifth: earlier probes had tripped over
index and remove colliding with C library symbols, and a ledger had named a function acct,
which is a POSIX call. Each had been fixed in place — this one sanitized, that one renamed — and
each fix had quietly acknowledged that the list of dangerous names was inherently incomplete.
Five instances of the same shape is the point where fixing instances stops being the right move.
The bug was never really about run or index or acct; it was about a resolution order that
let a built-in claim a name the user had already bound. The fix had to end the category, not add
a sixth name to a list.
Making the user win
There was already a precedent buried in the codebase: a few built-ins with common names — a
thread join, the character predicates — carried a guard that stepped aside if the name was
bound by the user, added case by case as each one bit someone. The fix generalizes that
precedent into a single helper that asks one question — is this name bound locally, captured
from an enclosing scope, lifted from an inner function, or defined at the top level? — and puts
that guard on about thirty built-ins whose names a user might plausibly choose: run, spawn,
even, odd, show, abs, and the rest. When the answer is yes, the call site falls through
to the ordinary user-call path; when no, the built-in is emitted exactly as before.
Provably safe, and half a problem closed
The reason the change could go in broadly, all at once, is that it is safe by construction. The
guard fires only when the user actually bound the name — and a program that binds run as a
function is a program the compiler was already miscompiling. Every program that does not shadow
a built-in produces byte-for-byte identical output to before; the whole test suite confirmed it,
passing unchanged. What this closes is the built-in half of the reserved-name problem — user
definitions now win over Mere’s own built-ins at the call site. The other half, collisions with
C keywords and POSIX symbols in the generated code, is still handled by a name sanitizer, and
the fully general fix — namespacing every user name so nothing can ever collide — remains
deferred, because with the built-in half closed it is a larger change for a smaller remaining
gain. A category of bug that had recurred five times is now structurally gone, which is worth
more than the fix to any one instance of it.