fix(0123): wrong arg counts to fixed-arity fns error at the call site
checkCallArity compares the supplied count against the declared params (min = params without trailing defaults, max = params.len, unbounded past a variadic) at the five plain dispatch sites in lowerCall — bare selected-author + lazy, namespace alias-gate + qualified, struct method, ufcs. Pack / comptime / generic / #compiler / #builtin callees keep their own dispatch. The method/ufcs sites also gain the appendDefaultArgs fill the generic-instance leg already had, so trailing defaults work on dot-calls instead of emitting under-arity calls. lowerStmt's local fn_decl arm now registers a pointer into the AST node in fn_ast_map, not a stack temporary that aliased every later local fn.
This commit is contained in:
33
examples/0054-basic-dot-call-default-args.sx
Normal file
33
examples/0054-basic-dot-call-default-args.sx
Normal file
@@ -0,0 +1,33 @@
|
||||
// Trailing parameter defaults fill on method and ufcs dot-calls (the
|
||||
// receiver-prepending dispatch paths), matching bare-call expansion (0044);
|
||||
// a `#caller_location` default and a slice variadic keep their flexible
|
||||
// arity under the call-arity check.
|
||||
// Regression (issue 0123).
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
Point :: struct {
|
||||
x: s64;
|
||||
scaled :: (self: Point, k: s64 = 2) -> s64 { return self.x * k; }
|
||||
}
|
||||
|
||||
bump :: ufcs (p: Point, by: s64 = 10) -> s64 { return p.x + by; }
|
||||
|
||||
sum_var :: (..xs: []s64) -> s64 {
|
||||
t := 0;
|
||||
for xs (x) { t = t + x; }
|
||||
return t;
|
||||
}
|
||||
|
||||
here :: (loc: Source_Location = #caller_location) -> s64 { return loc.line; }
|
||||
|
||||
main :: () {
|
||||
p := Point.{ x = 5 };
|
||||
print("{}\n", p.scaled()); // default filled on method dispatch
|
||||
print("{}\n", p.scaled(3)); // explicit overrides
|
||||
print("{}\n", p.bump()); // default filled on ufcs dispatch
|
||||
print("{}\n", p.bump(1));
|
||||
print("{}\n", sum_var()); // variadic: zero args
|
||||
print("{}\n", sum_var(1, 2, 3)); // variadic: many args
|
||||
print("{}\n", here() > 0); // #caller_location default
|
||||
}
|
||||
26
examples/1167-diagnostics-call-arity-mismatch.sx
Normal file
26
examples/1167-diagnostics-call-arity-mismatch.sx
Normal file
@@ -0,0 +1,26 @@
|
||||
// Wrong argument counts to fixed-arity functions are rejected at the call
|
||||
// site — bare calls, flat-imported stdlib fns, method dot-calls, and ufcs
|
||||
// dot-calls — instead of reaching LLVM verification ("Incorrect number of
|
||||
// arguments passed to called function!").
|
||||
// Regression (issue 0123).
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
add2 :: (a: s64, b: s64) -> s64 { return a + b; }
|
||||
|
||||
Point :: struct {
|
||||
x: s64;
|
||||
scaled :: (self: Point, k: s64) -> s64 { return self.x * k; }
|
||||
}
|
||||
|
||||
bump :: ufcs (p: Point, by: s64) -> s64 { return p.x + by; }
|
||||
|
||||
main :: () -> s32 {
|
||||
_ = add2(1, 2, 3); // plain bare call, too many
|
||||
_ = add2(1); // plain bare call, too few
|
||||
_ = concat("a", "b", "c"); // flat-imported stdlib fn, too many
|
||||
p := Point.{ x = 5 };
|
||||
_ = p.scaled(2, 9); // method dot-call, too many
|
||||
_ = p.bump(1, 2); // ufcs dot-call, too many
|
||||
return 0;
|
||||
}
|
||||
1
examples/expected/0054-basic-dot-call-default-args.exit
Normal file
1
examples/expected/0054-basic-dot-call-default-args.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
10
|
||||
15
|
||||
15
|
||||
6
|
||||
0
|
||||
6
|
||||
true
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,29 @@
|
||||
error: 'add2' expects 2 arguments, but 3 were given
|
||||
--> examples/1167-diagnostics-call-arity-mismatch.sx:19:9
|
||||
|
|
||||
19 | _ = add2(1, 2, 3); // plain bare call, too many
|
||||
| ^^^^
|
||||
|
||||
error: 'add2' expects 2 arguments, but 1 was given
|
||||
--> examples/1167-diagnostics-call-arity-mismatch.sx:20:9
|
||||
|
|
||||
20 | _ = add2(1); // plain bare call, too few
|
||||
| ^^^^
|
||||
|
||||
error: 'concat' expects 2 arguments, but 3 were given
|
||||
--> examples/1167-diagnostics-call-arity-mismatch.sx:21:9
|
||||
|
|
||||
21 | _ = concat("a", "b", "c"); // flat-imported stdlib fn, too many
|
||||
| ^^^^^^
|
||||
|
||||
error: 'Point.scaled' expects 1 argument, but 2 were given
|
||||
--> examples/1167-diagnostics-call-arity-mismatch.sx:23:9
|
||||
|
|
||||
23 | _ = p.scaled(2, 9); // method dot-call, too many
|
||||
| ^^^^^^^^
|
||||
|
||||
error: 'bump' expects 1 argument, but 2 were given
|
||||
--> examples/1167-diagnostics-call-arity-mismatch.sx:24:9
|
||||
|
|
||||
24 | _ = p.bump(1, 2); // ufcs dot-call, too many
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# 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
|
||||
|
||||
@@ -1800,6 +1800,7 @@ pub const Lowering = struct {
|
||||
pub const reflectionTypeArgGuard = lower_call.reflectionTypeArgGuard;
|
||||
pub const reflectionErrorSentinel = lower_call.reflectionErrorSentinel;
|
||||
pub const appendDefaultArgs = lower_call.appendDefaultArgs;
|
||||
pub const checkCallArity = lower_call.checkCallArity;
|
||||
pub const expandCallDefaults = lower_call.expandCallDefaults;
|
||||
pub const userParamTypes = lower_call.userParamTypes;
|
||||
pub const resolveCallParamTypes = lower_call.resolveCallParamTypes;
|
||||
|
||||
@@ -461,6 +461,15 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{func_name});
|
||||
return Ref.none;
|
||||
}
|
||||
// `args.items` is the post-expansion count: trailing defaults
|
||||
// were filled by `expandCallDefaults`, comptime-pack spreads
|
||||
// expanded element-wise above.
|
||||
{
|
||||
const arity_fd: ?*const ast.FnDecl = if (sel_author) |sf| sf.decl else self.program_index.fn_ast_map.get(func_name);
|
||||
if (arity_fd) |fd| {
|
||||
if (self.checkCallArity(fd, id.name, args.items.len, false, c.callee.span)) return Ref.none;
|
||||
}
|
||||
}
|
||||
if (sel_author) |sf| {
|
||||
const fid = self.selectedFuncId(sf, func_name);
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
@@ -722,6 +731,7 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
}
|
||||
if (hasComptimeParams(fd)) return self.lowerComptimeCall(fd, c);
|
||||
if (fd.type_params.len > 0) return self.lowerGenericCall(fd, fa.field, c, args.items);
|
||||
if (self.checkCallArity(fd, fa.field, args.items.len, false, c.callee.span)) return Ref.none;
|
||||
var sf = SelectedFunc{ .decl = fd, .source = target.target_module_path };
|
||||
const fid = self.selectedFuncId(&sf, fa.field);
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
@@ -763,6 +773,7 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
const ret_ty = func.ret;
|
||||
const params = func.params;
|
||||
if (self.program_index.fn_ast_map.get(effective_name)) |fd| {
|
||||
if (self.checkCallArity(fd, effective_name, args.items.len, false, c.callee.span)) return Ref.none;
|
||||
self.packVariadicCallArgs(fd, c, &args);
|
||||
}
|
||||
const final_args = self.prependCtxIfNeeded(func, args.items);
|
||||
@@ -973,11 +984,12 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
}
|
||||
|
||||
// Try non-generic qualified method
|
||||
if (self.program_index.fn_ast_map.get(qualified)) |fd| {
|
||||
const plain_method_fd = self.program_index.fn_ast_map.get(qualified);
|
||||
if (plain_method_fd) |fd| {
|
||||
if (self.checkCallArity(fd, qualified, method_args.items.len, true, c.callee.span)) return Ref.none;
|
||||
if (!self.lowered_functions.contains(qualified)) {
|
||||
self.lazyLowerFunction(qualified);
|
||||
}
|
||||
_ = fd;
|
||||
}
|
||||
if (self.resolveFuncByName(qualified)) |fid| {
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
@@ -985,6 +997,7 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
const params = func.params;
|
||||
const has_ctx = func.has_implicit_ctx;
|
||||
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
||||
if (plain_method_fd) |fd| self.appendDefaultArgs(fd, &method_args);
|
||||
// Note: coerceCallArgs can trigger protocol thunk creation
|
||||
// (module.addFunction), invalidating func pointer.
|
||||
// Use pre-extracted params/ret_ty (+ has_ctx) instead of
|
||||
@@ -1073,6 +1086,10 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
return self.emitError(eff_field, c.callee.span);
|
||||
}
|
||||
}
|
||||
const ufcs_arity_fd: ?*const ast.FnDecl = if (sel_author) |sf| sf.decl else ufcs_fd;
|
||||
if (ufcs_arity_fd) |fd| {
|
||||
if (self.checkCallArity(fd, fa.field, method_args.items.len, true, c.callee.span)) return Ref.none;
|
||||
}
|
||||
const ufcs_fid: ?FuncId = blk_uf: {
|
||||
if (sel_author) |sf| {
|
||||
break :blk_uf self.selectedFuncId(sf, eff_field);
|
||||
@@ -1092,6 +1109,7 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
// free function's first param is `*T` and the receiver is a
|
||||
// value `T`, pass its address instead of a by-value copy
|
||||
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
||||
if (ufcs_arity_fd) |fd| self.appendDefaultArgs(fd, &method_args);
|
||||
const final_args = self.prependCtxIfNeeded(func, method_args.items);
|
||||
self.coerceCallArgs(final_args, params);
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
@@ -1969,6 +1987,50 @@ pub fn appendDefaultArgs(self: *Lowering, fd: *const ast.FnDecl, args: *std.Arra
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject a direct call whose argument count cannot bind to the callee's
|
||||
/// declared parameter list. `supplied` counts the args as they bind to
|
||||
/// params — receiver included for dot-dispatch, defaults not yet
|
||||
/// appended. Returns true when a diagnostic was emitted (the call must
|
||||
/// not lower). Pack / comptime / generic / `#compiler` / `#builtin`
|
||||
/// callees bind args through their own dispatch and are exempt.
|
||||
pub fn checkCallArity(self: *Lowering, fd: *const ast.FnDecl, callee_name: []const u8, supplied: usize, has_receiver: bool, span: ast.Span) bool {
|
||||
|
||||
if (fd.type_params.len > 0 or hasComptimeParams(fd) or isPackFn(fd)) return false;
|
||||
switch (fd.body.data) {
|
||||
.compiler_expr, .builtin_expr => return false,
|
||||
else => {},
|
||||
}
|
||||
var min: usize = 0;
|
||||
var max: ?usize = fd.params.len;
|
||||
for (fd.params, 0..) |p, i| {
|
||||
if (p.is_variadic) {
|
||||
max = null;
|
||||
break;
|
||||
}
|
||||
if (p.default_expr == null) min = i + 1;
|
||||
}
|
||||
if (supplied >= min and (max == null or supplied <= max.?)) return false;
|
||||
if (self.diagnostics) |d| {
|
||||
// Dot-dispatch report counts the user-visible args: the receiver
|
||||
// slot is implicit at the call site, so it is elided from both
|
||||
// the expected and the supplied counts.
|
||||
const recv: usize = @intFromBool(has_receiver);
|
||||
const got = supplied -| recv;
|
||||
const lo = min -| recv;
|
||||
const got_verb: []const u8 = if (got == 1) "was" else "were";
|
||||
if (max == null) {
|
||||
const s: []const u8 = if (lo == 1) "" else "s";
|
||||
d.addFmt(.err, span, "'{s}' expects at least {d} argument{s}, but {d} {s} given", .{ callee_name, lo, s, got, got_verb });
|
||||
} else if (max.? -| recv == lo) {
|
||||
const s: []const u8 = if (lo == 1) "" else "s";
|
||||
d.addFmt(.err, span, "'{s}' expects {d} argument{s}, but {d} {s} given", .{ callee_name, lo, s, got, got_verb });
|
||||
} else {
|
||||
d.addFmt(.err, span, "'{s}' expects between {d} and {d} arguments, but {d} {s} given", .{ callee_name, lo, max.? -| recv, got, got_verb });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// When a bare-identifier call omits trailing positional args and the
|
||||
/// callee's signature provides defaults for them, return a fresh Call
|
||||
/// node with the defaults filled in. Returns null when no expansion is
|
||||
|
||||
@@ -179,7 +179,10 @@ pub fn lowerStmt(self: *Lowering, node: *const Node) void {
|
||||
switch (node.data) {
|
||||
.var_decl => |vd| self.lowerVarDecl(&vd),
|
||||
.const_decl => |cd| self.lowerConstDecl(&cd),
|
||||
.fn_decl => |fd| self.lowerLocalFnDecl(&fd),
|
||||
// Pointer capture, not by-value: `lowerLocalFnDecl` registers the
|
||||
// decl pointer in `fn_ast_map`, so it must point into the AST node,
|
||||
// not at a stack temporary that the next statement reuses.
|
||||
.fn_decl => |*fd| self.lowerLocalFnDecl(fd),
|
||||
.return_stmt => |rs| self.lowerReturn(&rs),
|
||||
.raise_stmt => |rs| self.lowerRaise(&rs, node.span),
|
||||
.assignment => |asgn| self.lowerAssignment(&asgn),
|
||||
|
||||
Reference in New Issue
Block a user