Summary
erlangArityAt (the arity counter for the Erlang behaviour-dispatch synthesizer) does not skip <<binary>> literals, despite its docstring claiming it does. Commas inside a binary literal are counted as top-level argument separators, so a dispatch site like Mod:decode(<<1,2,3>>, Opts) is computed as arity 4 instead of 2. The behaviour-dispatch lookup then keys on the wrong name/arity, so the edge to the real -callback is silently dropped (missed edge) — or collides with an unrelated behaviour that happens to declare that inflated arity (wrong edge). Verified against the real compiled function.
Root cause
// src/resolution/callback-synthesizer.ts (erlangArityAt)
/** ... Skips nested (), [], {}, <<>> content, "strings", 'atoms', and $c char literals ... */
function erlangArityAt(src: string, openIdx: number): number {
let depth = 1; let commas = 0; ...
for (let i = openIdx + 1; i < limit; i++) {
const ch = src[i]!;
if (ch === '"' || ch === "'") { /* skip string/atom */ }
if (ch === '$') { /* skip char literal */ }
if (ch === '(' || ch === '[' || ch === '{') { depth++; ... }
if (ch === ')' || ch === ']' || ch === '}') { depth--; ... }
if (ch === ',' && depth === 1) { commas++; continue; } // <- binary's inner commas land here
...
}
}
The scan special-cases ()/[]/{} but never <</>>, so the , inside <<1,2,3>> is seen at depth === 1 and counted. The docstring's own worked example (init(fun((a, b) -> ok), #{k => v}) → 2) passes only because () and {} balance; the <<>> claim in the same sentence is unimplemented.
Repro (executed against the real compiled function)
| dispatch site |
real arity |
erlangArityAt |
Mod:decode(<<1,2,3>>, Opts) |
2 |
4 |
Mod:encode(<<"pre", Rest/binary>>, Ctx) |
2 |
3 |
Mod:handle(Msg, State) (control, no binary) |
2 |
2 |
init(fun((a, b) -> ok), #{k => v}) (docstring example) |
2 |
2 |
Binary literals with commas are ordinary Erlang (<<1,2,3>>, <<"prefix", Rest/binary>>), so any Var:fn(<<...>>, ...) dispatch site through a behaviour hits this.
Suggested direction
Track <</>> depth alongside the existing bracket depth: on a two-char << increment depth and advance past both chars, on >> decrement (guarding the two-char match so a lone </> comparison isn't misread). Then the docstring's <<>> claim holds and the arity is correct.
Environment
main @ 5955d04, package 1.4.1, Node 22.23.1 (Linux). Function extracted from the built dist/ and exercised directly.
Summary
erlangArityAt(the arity counter for the Erlang behaviour-dispatch synthesizer) does not skip<<binary>>literals, despite its docstring claiming it does. Commas inside a binary literal are counted as top-level argument separators, so a dispatch site likeMod:decode(<<1,2,3>>, Opts)is computed as arity 4 instead of 2. The behaviour-dispatch lookup then keys on the wrongname/arity, so the edge to the real-callbackis silently dropped (missed edge) — or collides with an unrelated behaviour that happens to declare that inflated arity (wrong edge). Verified against the real compiled function.Root cause
The scan special-cases
()/[]/{}but never<</>>, so the,inside<<1,2,3>>is seen atdepth === 1and counted. The docstring's own worked example (init(fun((a, b) -> ok), #{k => v})→ 2) passes only because()and{}balance; the<<>>claim in the same sentence is unimplemented.Repro (executed against the real compiled function)
erlangArityAtMod:decode(<<1,2,3>>, Opts)Mod:encode(<<"pre", Rest/binary>>, Ctx)Mod:handle(Msg, State)(control, no binary)init(fun((a, b) -> ok), #{k => v})(docstring example)Binary literals with commas are ordinary Erlang (
<<1,2,3>>,<<"prefix", Rest/binary>>), so anyVar:fn(<<...>>, ...)dispatch site through a behaviour hits this.Suggested direction
Track
<</>>depth alongside the existing bracket depth: on a two-char<<incrementdepthand advance past both chars, on>>decrement (guarding the two-char match so a lone</>comparison isn't misread). Then the docstring's<<>>claim holds and the arity is correct.Environment
main@5955d04, package1.4.1, Node 22.23.1 (Linux). Function extracted from the builtdist/and exercised directly.