Files
sx/issues/0123-call-arity-unchecked.md
agra d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00

105 lines
4.9 KiB
Markdown

# 0123 — wrong arg counts to fixed-arity fns reach LLVM emission
> **RESOLVED** (2026-06-12). Root cause: no dispatch path in `lowerCall`
> ever compared the supplied arg count against the callee's declared
> params (`coerceCallArgs` iterates `@min(args.len, params.len)`, so a
> mismatch sailed through to the LLVM verifier). Fix: a shared
> `checkCallArity` (src/ir/lower/call.zig) computes min (params without
> trailing defaults) / max (`params.len`, unbounded past a variadic)
> from the AST decl and rejects with a source-located diagnostic at the
> five plain dispatch sites — bare selected-author + lazy, namespace
> alias-gate + qualified, struct-method, ufcs. Pack / comptime / generic
> / `#compiler` / `#builtin` callees are exempt (own dispatch). The
> method/ufcs sites also gained the `appendDefaultArgs` fill the
> generic-instance leg already had — trailing defaults on dot-calls
> previously emitted under-arity calls (same verifier failure). Flushed
> out en route: `lowerStmt`'s `.fn_decl => |fd| ... (&fd)` registered a
> STACK address in `fn_ast_map`, so every local fn's map entry aliased
> the most recently lowered one — pointer capture (`|*fd|`) fixes it.
> Regressions: `examples/1167-diagnostics-call-arity-mismatch.sx`
> (too many / too few, bare + stdlib + method + ufcs) and
> `examples/0054-basic-dot-call-default-args.sx` (dot-call defaults,
> variadic, `#caller_location`). Gates: zig build test 426/426, suite
> 590/590 (fix in isolation), distribution repo 14/14.
## Symptom
Calling a fixed-arity function with the wrong number of arguments is
not rejected by the frontend — the mismatched argument list flows all
the way to LLVM emission, which fails verification instead of a
source-located diagnostic.
- **Observed**: `LLVM verification failed: Incorrect number of
arguments passed to called function!` plus the raw IR call line —
no file/line/snippet, no callee name in user terms.
- **Expected**: a compile error at the call site naming the callee
and its declared arity (matching the style of existing
diagnostics, e.g. the "unresolved ..." errors with source
snippets).
Both directions are broken, on every plain dispatch path probed:
- too MANY args, bare call: `concat("a", "b", "c")` (std's `concat`
takes 2 strings) → LLVM verifier failure.
- too FEW args, bare call: `add2(1)` with `add2 :: (a: i64, b: i64)`
→ same.
- methods / ufcs dot-calls: same shape, receiver included. Worse:
a trailing-default param on a plain struct method or a ufcs fn is
never filled on the dot-call path (`p.scaled()` with
`scaled :: (self: Point, k: i64 = 2)` emits a 1-arg call to a
2-param fn — bare calls fill defaults via `expandCallDefaults`,
the method/ufcs sites never run `appendDefaultArgs`).
Legitimate flexible shapes must keep working: slice variadics
(`..xs: []T` — no upper bound), comptime/protocol packs (`..$args` /
`..xs: P` — own dispatch), default-valued params (incl.
`loc: Source_Location = #caller_location`), generic `$T` fns
(explicit vs inferred type args make the count flexible), `#foreign`
C variadics, `#compiler` / `#builtin` bodies.
## Reproduction
```sx
#import "modules/std.sx";
main :: () -> i32 {
s := concat("a", "b", "c"); // concat takes (a: string, b: string)
out(s);
return 0;
}
```
Observed at master b3b78e2: compiles past resolution, dies at LLVM
verification with the verifier message above.
## Investigation prompt
Call argument binding never compares the supplied arg count against
the callee's declared parameter list. Suspected area: the plain
direct-dispatch sites in `src/ir/lower/call.zig` (`lowerCall`) — the
bare-identifier selected-author and lazy-lower legs, the
namespace-qualified legs, the qualified struct-method leg, and the
ufcs leg. All of them run `packVariadicCallArgs` /
`coerceCallArgs` and emit `builder.call` without an arity check;
`coerceCallArgs` iterates `@min(args.len, params.len)` so a mismatch
sails through to the emitter.
The fix likely needs a shared `checkCallArity(fd, name, supplied,
has_receiver, span)` helper consulted at each plain dispatch site,
computing min (params without trailing defaults) / max
(`fd.params.len`, unbounded when a variadic param exists) from the
AST decl and emitting via `self.diagnostics.addFmt(.err, span, ...)`
on violation. Pack (`isPackFn`), comptime (`hasComptimeParams`),
generic (`type_params.len > 0`), `#compiler`, and `#builtin` callees
bind args through their own dispatch and must be exempt. The
method/ufcs sites also need `appendDefaultArgs` (the generic
instance-method leg already runs it) so trailing defaults fill
before the count is meaningful.
Verification: the repro errors with a source-located diagnostic
naming `concat` and its 2-arg signature; too-few errors likewise;
variadic / pack / default / ufcs / generic calls keep compiling
(`print`, `format`, `List.append`, `#caller_location` defaults).
`zig build && zig build test`, `bash tests/run_examples.sh` all
green; pin the repro as a diagnostics example per CLAUDE.md.