Skip to content

Erlang: erlangArityAt doesn't skip <<binary>> literals (contrary to its docstring), so binary-arg dispatch sites get an inflated arity and the behaviour-callback edge is dropped or mis-linked — verified #1358

Description

@inth3shadows

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions