From 82fc71ccbecdf42f4578ef16fe71e366760a8b3d Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 7 Jun 2026 12:40:00 +0300 Subject: [PATCH] fix(lower): route early pack/comptime dispatch through SelectedFunc [stdlib C attempt-3] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...modules-flat-same-name-bare-pack-winner.sx | 18 +++++++++++++++++ .../a.sx | 6 ++++++ .../b.sx | 6 ++++++ ...dules-flat-same-name-bare-pack-winner.exit | 1 + ...les-flat-same-name-bare-pack-winner.stderr | 1 + ...les-flat-same-name-bare-pack-winner.stdout | 2 ++ src/ir/lower.zig | 20 +++++++++++++++++-- 7 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 examples/0741-modules-flat-same-name-bare-pack-winner.sx create mode 100644 examples/0741-modules-flat-same-name-bare-pack-winner/a.sx create mode 100644 examples/0741-modules-flat-same-name-bare-pack-winner/b.sx create mode 100644 examples/expected/0741-modules-flat-same-name-bare-pack-winner.exit create mode 100644 examples/expected/0741-modules-flat-same-name-bare-pack-winner.stderr create mode 100644 examples/expected/0741-modules-flat-same-name-bare-pack-winner.stdout diff --git a/examples/0741-modules-flat-same-name-bare-pack-winner.sx b/examples/0741-modules-flat-same-name-bare-pack-winner.sx new file mode 100644 index 0000000..11f1e3a --- /dev/null +++ b/examples/0741-modules-flat-same-name-bare-pack-winner.sx @@ -0,0 +1,18 @@ +// Regression (issue 0102, Phase C): a BARE call whose own author is a plain free +// fn must DISPATCH to that author, not the first-wins winner — even when the +// winner is a comptime PACK (`..$args`) of the same name. a.sx (imported first) +// authors `f` as a pack → first-wins winner; b.sx authors its OWN plain `f`. In +// b.sx, `f()` must reach b.f (returns 2). Before the fix, lowerCall's early +// pack/comptime/generic dispatch keyed off the first-wins winner (a's pack) and +// invoked it (returns 1) BEFORE consuming the selected author — so plan-selected +// author and lowered+dispatched author disagreed. The early dispatch now reads +// the SAME `SelectedFunc` the main dispatch binds (fix-0102 F2). +#import "modules/std.sx"; +#import "0741-modules-flat-same-name-bare-pack-winner/a.sx"; +#import "0741-modules-flat-same-name-bare-pack-winner/b.sx"; + +main :: () -> s32 { + show_a(); // a-side: own == winner (the pack) → returns 1, byte-for-byte unchanged + show_b(); // b-side: selected own plain author → returns 2, not the pack winner + 0 +} diff --git a/examples/0741-modules-flat-same-name-bare-pack-winner/a.sx b/examples/0741-modules-flat-same-name-bare-pack-winner/a.sx new file mode 100644 index 0000000..50fb7a7 --- /dev/null +++ b/examples/0741-modules-flat-same-name-bare-pack-winner/a.sx @@ -0,0 +1,6 @@ +#import "modules/std.sx"; +// a.sx authors `f` as a comptime pack; imported first → first-wins winner. +// `show_a`'s bare `f()` is the caller's OWN author (own == winner → existing +// pack path, byte-for-byte unchanged): dispatched as a.f (the pack → 1). +f :: (..$args) -> s64 { return 1; } +show_a :: () { print("a: f() = {}\n", f()); } diff --git a/examples/0741-modules-flat-same-name-bare-pack-winner/b.sx b/examples/0741-modules-flat-same-name-bare-pack-winner/b.sx new file mode 100644 index 0000000..45f3a70 --- /dev/null +++ b/examples/0741-modules-flat-same-name-bare-pack-winner/b.sx @@ -0,0 +1,6 @@ +#import "modules/std.sx"; +// b.sx authors its OWN plain `f`. `show_b`'s bare `f()` must dispatch b.f (2), +// not the first-wins pack winner from a.sx (1). The selector picks b.f; the +// early pack/comptime dispatch must NOT hijack it with the winner. +f :: () -> s64 { return 2; } +show_b :: () { print("b: f() = {}\n", f()); } diff --git a/examples/expected/0741-modules-flat-same-name-bare-pack-winner.exit b/examples/expected/0741-modules-flat-same-name-bare-pack-winner.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0741-modules-flat-same-name-bare-pack-winner.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0741-modules-flat-same-name-bare-pack-winner.stderr b/examples/expected/0741-modules-flat-same-name-bare-pack-winner.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0741-modules-flat-same-name-bare-pack-winner.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0741-modules-flat-same-name-bare-pack-winner.stdout b/examples/expected/0741-modules-flat-same-name-bare-pack-winner.stdout new file mode 100644 index 0000000..c8a3f30 --- /dev/null +++ b/examples/expected/0741-modules-flat-same-name-bare-pack-winner.stdout @@ -0,0 +1,2 @@ +a: f() = 1 +b: f() = 2 diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 775a321..5f4dafe 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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))