Skip to content

Name-keyed dispatch superinstruction hijacks a user-rebound 'dispatch' (silent wrong result); also fires on the parenthesized #355 form #459

Description

@InauguralPhysicist

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions