Found during the #405 review of the call-compilation site (src/compiler.c, AST_RELATION case). The dispatch of [table, key, arg] → OP_DISPATCH superinstruction keys on the IDENT NAME dispatch with a 3-element AST_LIST argument and nothing else. Three facets, worst first:
1. Silent hijack of a user rebinding (real bug). dispatch is not reserved, but the superinstruction ignores any user binding:
fns is [(x) => x + 1, (x) => x * 2]
define dispatch(a, b, c) as:
return 999
print of (dispatch of [fns, 1, 21]) # prints 42 (OP_DISPATCH), expected 999
The call never sees the user function — silent wrong answer, the top bug class. report/report_value/observe name-keyed forms behave the same way (define report(v) as: return \"mine\" then report of x still prints the observer classification) — those are arguably observer special forms, but the position should be DECIDED: either document them as reserved special forms (like the predicates) or honor rebinding uniformly. dispatch has no such excuse — it's a plain builtin optimization.
2. The shadow lint misses it. W013 (function definition shadows a builtin) does NOT fire on define dispatch — so the hijack ships without a whisper. Whatever W013's builtin set is, it doesn't include dispatch (the runtime clearly binds it: dispatch of xs works and errors as dispatch: — worth checking whether other registered names are missing from the lint's set too, given #404's lesson about hand-copied lists drifting).
3. The #355 contract miss (currently unobservable). The superinstruction doesn't check !arg_node->parenthesized, so dispatch of ([fns, 1, 21]) — one argument by the #355/#405 rule — also compiles to the 3-arg OP_DISPATCH. Today the builtin fallback given a single 3-element list happens to agree, so behavior is identical; it's a landmine only if either side drifts. Fixing facet 1 properly should make the paren form take the normal-call path anyway.
Fix direction. Apply the superinstruction only when dispatch cannot be user-bound at the call site — e.g. skip the optimization if the name resolves to any local/param/module binding at compile time (the E003 pass now has exactly this binding knowledge), or cheapest: skip if the compilation unit defines/assigns the name anywhere (fail open to the normal call path; the builtin fallback is semantically identical). Add dispatch (and any other missing registered names) to W013's set via the register_builtins()-derived list rather than a hand list.
Severity. No in-repo or consumer code rebinds dispatch (or the observer names), so nothing is currently miscompiled — found by review, not by a field failure. Pre-existing; unaffected by #405 (verified same behavior before and after).
Repro binary: main @ 9810a99.
Found during the #405 review of the call-compilation site (src/compiler.c, AST_RELATION case). The
dispatch of [table, key, arg]→OP_DISPATCHsuperinstruction keys on the IDENT NAMEdispatchwith a 3-elementAST_LISTargument and nothing else. Three facets, worst first:1. Silent hijack of a user rebinding (real bug).
dispatchis not reserved, but the superinstruction ignores any user binding:The call never sees the user function — silent wrong answer, the top bug class.
report/report_value/observename-keyed forms behave the same way (define report(v) as: return \"mine\"thenreport of xstill prints the observer classification) — those are arguably observer special forms, but the position should be DECIDED: either document them as reserved special forms (like the predicates) or honor rebinding uniformly.dispatchhas no such excuse — it's a plain builtin optimization.2. The shadow lint misses it. W013 (function definition shadows a builtin) does NOT fire on
define dispatch— so the hijack ships without a whisper. Whatever W013's builtin set is, it doesn't includedispatch(the runtime clearly binds it:dispatch of xsworks and errors asdispatch:— worth checking whether other registered names are missing from the lint's set too, given #404's lesson about hand-copied lists drifting).3. The #355 contract miss (currently unobservable). The superinstruction doesn't check
!arg_node->parenthesized, sodispatch of ([fns, 1, 21])— one argument by the #355/#405 rule — also compiles to the 3-arg OP_DISPATCH. Today the builtin fallback given a single 3-element list happens to agree, so behavior is identical; it's a landmine only if either side drifts. Fixing facet 1 properly should make the paren form take the normal-call path anyway.Fix direction. Apply the superinstruction only when
dispatchcannot be user-bound at the call site — e.g. skip the optimization if the name resolves to any local/param/module binding at compile time (the E003 pass now has exactly this binding knowledge), or cheapest: skip if the compilation unit defines/assigns the name anywhere (fail open to the normal call path; the builtin fallback is semantically identical). Adddispatch(and any other missing registered names) to W013's set via the register_builtins()-derived list rather than a hand list.Severity. No in-repo or consumer code rebinds
dispatch(or the observer names), so nothing is currently miscompiled — found by review, not by a field failure. Pre-existing; unaffected by #405 (verified same behavior before and after).Repro binary: main @ 9810a99.