fix(lower): route early pack/comptime dispatch through SelectedFunc [stdlib C attempt-3]

lowerCall's early pack/comptime/generic dispatch keyed off the first-wins
winner (`fn_ast_map.get(early_name)`) BEFORE the main dispatch consumes the
selected same-name author. Under a genuine flat same-name collision where the
caller's own author is a plain free fn but the first-wins winner is a comptime
pack `(..$args)` (or comptime-param / generic), the early path invoked the
WINNER — so `CallResolver.plan` (which selects the own plain author) and
lowering disagreed about which function a bare call names.

Confirms reviewer finding C-review-1. The earlier manager ground-truth got
`show_b=2` because it used a slice variadic `(..xs: []s64)` — NOT a pack fn
(`isPackParam` false), so it never hit the early dispatch. The reviewer used a
comptime pack `(..$args)` (`isPackFn` true), which does. Both observations are
correct for their respective shapes; the bug is real for the comptime-pack
winner.

Fix: the early dispatch reads the SAME author the selector chose
(`sel_author.decl`) when a collision rerouted the call, else the winner
(common path, byte-identical). The selector only ever returns a plain free fn
(`isPlainFreeFn` excludes type-params / comptime / pack), so a selected author
falls through to the main dispatch that binds it via `SelectedFunc`.

Regression: examples/0741-modules-flat-same-name-bare-pack-winner — a.sx
(imported first) authors `f` as a comptime pack (first-wins winner); b.sx
authors its own plain `f`; b's bare `f()` must return 2 (own author), not 1
(the pack). Fails on 2dd6c3c (b: f() = 1), passes after.

Gate: zig build + zig build test (412/412) + run_examples (477/0) +
m3te ios-sim exit 0.
This commit is contained in:
agra
2026-06-07 12:40:00 +03:00
parent 2dd6c3c13b
commit 82fc71ccbe
7 changed files with 52 additions and 2 deletions

View File

@@ -7494,7 +7494,21 @@ pub const Lowering = struct {
}
break :blk scoped;
};
if (self.program_index.fn_ast_map.get(early_name)) |fd| {
// fix-0102 F2 / R5 §C: the early pack/comptime/generic dispatch reads
// the SAME author the call resolver SELECTED — not the first-wins
// winner — whenever a genuine flat same-name collision rerouted the
// call (`sel_author != null`). The selector only ever returns a plain
// free fn (`isPlainFreeFn` rejects type-params / comptime / pack), so
// `sel_author.decl` matches none of the arms below and the early path
// falls through to the main dispatch, which CONSUMES `sel_author` and
// binds that author. Without this the early path would dispatch the
// first-wins winner (e.g. a pack `(..$args)`) and disagree with the
// main dispatch — the selected plain author's bare call would invoke
// the wrong function. On the common path (`sel_author == null`) this
// reads the winner exactly as before — byte-identical, since the
// selector reroutes nothing there.
const early_fd: ?*const ast.FnDecl = if (sel_author) |sf| sf.decl else self.program_index.fn_ast_map.get(early_name);
if (early_fd) |fd| {
if (isPackFn(fd)) {
// Protocol packs (`..xs: P`) and comptime type-packs
// (`..$args`) both monomorphize per call shape.
@@ -7505,7 +7519,9 @@ pub const Lowering = struct {
}
// Early detection of generic function calls — skip arg lowering for type params
// because lowerGenericCall resolves type params from AST nodes, not lowered refs.
// Only if the name is NOT shadowed by a local variable (closure, fn ptr, etc.)
// Only if the name is NOT shadowed by a local variable (closure, fn ptr, etc.).
// A selected author is never generic (`isPlainFreeFn` excludes
// `type_params > 0`), so this branch fires only on the winner.
const shadowed = if (self.scope) |scope| scope.lookup(c.callee.data.identifier.name) != null else false;
if (fd.type_params.len > 0 and !shadowed) {
// Types are explicit when call args match param count (e.g., are_equal(Point, p1, p2))