From ea35a05b267ef66816b44beb49f2596d49431ba5 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 6 Jun 2026 14:04:03 +0300 Subject: [PATCH] fix(lower): bare-call resolver binds same-name flat authors per source [0102c] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third of four fix-0102 sub-steps — the behaviour fix for NORMAL call sites. Adds THE bare-name resolver `resolveBareCallee(name, caller_file)` over fix-0102a's `module_fns` + `flat_import_graph` and routes the primary call path through it: - own-author wins: a file's bare call to a name IT authors binds its OWN author, not the first-wins merge winner. (When the winner already is the caller's own — every single-author and first-importer case — the resolver returns `.none` so the existing path binds it byte-for-byte.) - a bare call to a name two or more FLAT imports both provide is `.ambiguous` and rejected with a loud diagnostic ("declared by multiple imported modules — qualify the call"); a namespaced author never collides. - a single flat-reachable author that differs from the winner binds that author; otherwise `.none`. The resolved shadow author lowers into its OWN FuncId via fix-0102b's identity-addressable `lowerFunctionBodyInto` (shared `bareAuthorFuncId` helper, also used by `lowerRetainedSameNameAuthors`). Only plain free functions route — generic / comptime / foreign / builtin authors and any scope-mangled / UFCS-aliased / locally-shadowed name fall straight to the existing dispatch, so single-author / local / std / qualified resolution is unchanged (full example suite stays green, including bundle.sx and the comptime format/pack examples). Examples 0722 (flat file per-source bind), 0723 (flat vs namespaced, no false ambiguity), 0724 (ambiguous → diagnostic), 0725 (flat directory per-source bind), 0727 (user namespace literally named __m0). Each fails on wt-fix-0102-base (first-wins mis-bind / no diagnostic) and passes here. The fix-0102b unit test now calls a per-module wrapper (main can't bare-call the 2-author name) and asserts the resolver's three variants directly. Gate: zig build, zig build test (400/400), bash tests/run_examples.sh (462 passed) all green. --- examples/0722-modules-flat-same-name-own.sx | 18 ++ examples/0722-modules-flat-same-name-own/a.sx | 5 + examples/0722-modules-flat-same-name-own/b.sx | 4 + examples/0723-modules-flat-vs-namespaced.sx | 17 ++ .../0723-modules-flat-vs-namespaced/flat.sx | 3 + .../0723-modules-flat-vs-namespaced/named.sx | 4 + .../0724-modules-flat-same-name-ambiguous.sx | 12 ++ .../a.sx | 3 + .../b.sx | 2 + examples/0725-modules-flat-dir-same-name.sx | 17 ++ .../0725-modules-flat-dir-same-name/d1/one.sx | 3 + .../0725-modules-flat-dir-same-name/d2/two.sx | 4 + examples/0727-modules-user-ns-m0.sx | 20 +++ examples/0727-modules-user-ns-m0/a.sx | 3 + examples/0727-modules-user-ns-m0/b.sx | 3 + examples/0727-modules-user-ns-m0/m.sx | 3 + .../0722-modules-flat-same-name-own.exit | 1 + .../0722-modules-flat-same-name-own.stderr | 1 + .../0722-modules-flat-same-name-own.stdout | 2 + .../0723-modules-flat-vs-namespaced.exit | 1 + .../0723-modules-flat-vs-namespaced.stderr | 1 + .../0723-modules-flat-vs-namespaced.stdout | 2 + ...0724-modules-flat-same-name-ambiguous.exit | 1 + ...24-modules-flat-same-name-ambiguous.stderr | 5 + ...24-modules-flat-same-name-ambiguous.stdout | 1 + .../0725-modules-flat-dir-same-name.exit | 1 + .../0725-modules-flat-dir-same-name.stderr | 1 + .../0725-modules-flat-dir-same-name.stdout | 2 + .../expected/0727-modules-user-ns-m0.exit | 1 + .../expected/0727-modules-user-ns-m0.stderr | 1 + .../expected/0727-modules-user-ns-m0.stdout | 3 + readme.md | 6 + src/ir/lower.test.zig | 29 +++- src/ir/lower.zig | 162 +++++++++++++++--- 34 files changed, 316 insertions(+), 26 deletions(-) create mode 100644 examples/0722-modules-flat-same-name-own.sx create mode 100644 examples/0722-modules-flat-same-name-own/a.sx create mode 100644 examples/0722-modules-flat-same-name-own/b.sx create mode 100644 examples/0723-modules-flat-vs-namespaced.sx create mode 100644 examples/0723-modules-flat-vs-namespaced/flat.sx create mode 100644 examples/0723-modules-flat-vs-namespaced/named.sx create mode 100644 examples/0724-modules-flat-same-name-ambiguous.sx create mode 100644 examples/0724-modules-flat-same-name-ambiguous/a.sx create mode 100644 examples/0724-modules-flat-same-name-ambiguous/b.sx create mode 100644 examples/0725-modules-flat-dir-same-name.sx create mode 100644 examples/0725-modules-flat-dir-same-name/d1/one.sx create mode 100644 examples/0725-modules-flat-dir-same-name/d2/two.sx create mode 100644 examples/0727-modules-user-ns-m0.sx create mode 100644 examples/0727-modules-user-ns-m0/a.sx create mode 100644 examples/0727-modules-user-ns-m0/b.sx create mode 100644 examples/0727-modules-user-ns-m0/m.sx create mode 100644 examples/expected/0722-modules-flat-same-name-own.exit create mode 100644 examples/expected/0722-modules-flat-same-name-own.stderr create mode 100644 examples/expected/0722-modules-flat-same-name-own.stdout create mode 100644 examples/expected/0723-modules-flat-vs-namespaced.exit create mode 100644 examples/expected/0723-modules-flat-vs-namespaced.stderr create mode 100644 examples/expected/0723-modules-flat-vs-namespaced.stdout create mode 100644 examples/expected/0724-modules-flat-same-name-ambiguous.exit create mode 100644 examples/expected/0724-modules-flat-same-name-ambiguous.stderr create mode 100644 examples/expected/0724-modules-flat-same-name-ambiguous.stdout create mode 100644 examples/expected/0725-modules-flat-dir-same-name.exit create mode 100644 examples/expected/0725-modules-flat-dir-same-name.stderr create mode 100644 examples/expected/0725-modules-flat-dir-same-name.stdout create mode 100644 examples/expected/0727-modules-user-ns-m0.exit create mode 100644 examples/expected/0727-modules-user-ns-m0.stderr create mode 100644 examples/expected/0727-modules-user-ns-m0.stdout diff --git a/examples/0722-modules-flat-same-name-own.sx b/examples/0722-modules-flat-same-name-own.sx new file mode 100644 index 0000000..911e594 --- /dev/null +++ b/examples/0722-modules-flat-same-name-own.sx @@ -0,0 +1,18 @@ +// fix-0102c (issue 0102): two flat FILE imports each author a same-name free +// function `greet`. The first-wins import merge keeps exactly one `greet` in +// the merged scope, but each module's OWN code must bind its OWN author when it +// calls `greet` bare. `from_a` (in a.sx) returns 1; `from_b` (in b.sx) returns +// 2 — per-source binding, resolved by identity, not first-wins. +#import "modules/std.sx"; +#import "0722-modules-flat-same-name-own/a.sx"; +#import "0722-modules-flat-same-name-own/b.sx"; + +report :: (label: string, ok: bool) { + if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); } +} + +main :: () -> s32 { + report("from_a binds a.greet", from_a() == 1); + report("from_b binds b.greet", from_b() == 2); + 0 +} diff --git a/examples/0722-modules-flat-same-name-own/a.sx b/examples/0722-modules-flat-same-name-own/a.sx new file mode 100644 index 0000000..07aa08f --- /dev/null +++ b/examples/0722-modules-flat-same-name-own/a.sx @@ -0,0 +1,5 @@ +// a.sx authors `greet`. Its own `from_a` calls `greet` bare — under fix-0102c +// that binds a.sx's OWN author (own-author wins), even though b.sx also +// authors `greet` and the first-wins merge keeps only one in the merged scope. +greet :: () -> s64 { return 1; } +from_a :: () -> s64 { return greet(); } diff --git a/examples/0722-modules-flat-same-name-own/b.sx b/examples/0722-modules-flat-same-name-own/b.sx new file mode 100644 index 0000000..0c684d2 --- /dev/null +++ b/examples/0722-modules-flat-same-name-own/b.sx @@ -0,0 +1,4 @@ +// b.sx authors its OWN `greet`. `from_b`'s bare `greet` must bind b.sx's +// author (2), not the first-wins winner from a.sx. +greet :: () -> s64 { return 2; } +from_b :: () -> s64 { return greet(); } diff --git a/examples/0723-modules-flat-vs-namespaced.sx b/examples/0723-modules-flat-vs-namespaced.sx new file mode 100644 index 0000000..ac63e62 --- /dev/null +++ b/examples/0723-modules-flat-vs-namespaced.sx @@ -0,0 +1,17 @@ +// fix-0102c (issue 0102): one FLAT and one NAMESPACED author of `value`. The +// bare call `value()` binds the FLAT author (10); the namespaced author is +// reached only through `nm.value()` (20). A namespaced author must NOT make the +// bare call ambiguous — only flat authors collide. +#import "modules/std.sx"; +#import "0723-modules-flat-vs-namespaced/flat.sx"; +nm :: #import "0723-modules-flat-vs-namespaced/named.sx"; + +report :: (label: string, ok: bool) { + if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); } +} + +main :: () -> s32 { + report("bare binds flat", value() == 10); + report("nm.value binds named", nm.value() == 20); + 0 +} diff --git a/examples/0723-modules-flat-vs-namespaced/flat.sx b/examples/0723-modules-flat-vs-namespaced/flat.sx new file mode 100644 index 0000000..7dc0cfd --- /dev/null +++ b/examples/0723-modules-flat-vs-namespaced/flat.sx @@ -0,0 +1,3 @@ +// Flat-imported author of `value`. A bare `value()` in the consumer binds THIS +// one — the only bare (flat) author of the name. +value :: () -> s64 { return 10; } diff --git a/examples/0723-modules-flat-vs-namespaced/named.sx b/examples/0723-modules-flat-vs-namespaced/named.sx new file mode 100644 index 0000000..132a422 --- /dev/null +++ b/examples/0723-modules-flat-vs-namespaced/named.sx @@ -0,0 +1,4 @@ +// Namespaced-imported author of `value`. Reachable only as `nm.value`; it never +// enters the flat merge, so it neither shadows the flat author nor makes the +// bare call ambiguous. +value :: () -> s64 { return 20; } diff --git a/examples/0724-modules-flat-same-name-ambiguous.sx b/examples/0724-modules-flat-same-name-ambiguous.sx new file mode 100644 index 0000000..7a623f0 --- /dev/null +++ b/examples/0724-modules-flat-same-name-ambiguous.sx @@ -0,0 +1,12 @@ +// fix-0102c (issue 0102): a genuinely-ambiguous bare call. `main` flat-imports +// two modules that each author `dup` and neither is `main`'s own — a bare +// `dup()` can't pick one, so the compiler rejects it with a loud diagnostic +// instead of silently first-wins-binding one. Qualify the call to disambiguate. +#import "modules/std.sx"; +#import "0724-modules-flat-same-name-ambiguous/a.sx"; +#import "0724-modules-flat-same-name-ambiguous/b.sx"; + +main :: () -> s32 { + print("{}\n", dup()); + 0 +} diff --git a/examples/0724-modules-flat-same-name-ambiguous/a.sx b/examples/0724-modules-flat-same-name-ambiguous/a.sx new file mode 100644 index 0000000..cb94ede --- /dev/null +++ b/examples/0724-modules-flat-same-name-ambiguous/a.sx @@ -0,0 +1,3 @@ +// One of two flat authors of `dup`. A consumer that flat-imports BOTH and calls +// `dup` bare cannot pick between them. +dup :: () -> s64 { return 1; } diff --git a/examples/0724-modules-flat-same-name-ambiguous/b.sx b/examples/0724-modules-flat-same-name-ambiguous/b.sx new file mode 100644 index 0000000..6bd821b --- /dev/null +++ b/examples/0724-modules-flat-same-name-ambiguous/b.sx @@ -0,0 +1,2 @@ +// The second flat author of `dup`. +dup :: () -> s64 { return 2; } diff --git a/examples/0725-modules-flat-dir-same-name.sx b/examples/0725-modules-flat-dir-same-name.sx new file mode 100644 index 0000000..910716e --- /dev/null +++ b/examples/0725-modules-flat-dir-same-name.sx @@ -0,0 +1,17 @@ +// fix-0102c (issue 0102): two flat DIRECTORY imports each author a same-name +// `tag`. A directory flat-import exposes the directory's authored functions, so +// `caller1`/`caller2` are visible here, and each binds its OWN directory's `tag` +// when it calls bare — per-source binding across directory imports (100 / 200). +#import "modules/std.sx"; +#import "0725-modules-flat-dir-same-name/d1"; +#import "0725-modules-flat-dir-same-name/d2"; + +report :: (label: string, ok: bool) { + if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); } +} + +main :: () -> s32 { + report("caller1 binds d1.tag", caller1() == 100); + report("caller2 binds d2.tag", caller2() == 200); + 0 +} diff --git a/examples/0725-modules-flat-dir-same-name/d1/one.sx b/examples/0725-modules-flat-dir-same-name/d1/one.sx new file mode 100644 index 0000000..47db691 --- /dev/null +++ b/examples/0725-modules-flat-dir-same-name/d1/one.sx @@ -0,0 +1,3 @@ +// d1's author of `tag`. `caller1` (also in d1) binds d1's own `tag` (100). +tag :: () -> s64 { return 100; } +caller1 :: () -> s64 { return tag(); } diff --git a/examples/0725-modules-flat-dir-same-name/d2/two.sx b/examples/0725-modules-flat-dir-same-name/d2/two.sx new file mode 100644 index 0000000..026037d --- /dev/null +++ b/examples/0725-modules-flat-dir-same-name/d2/two.sx @@ -0,0 +1,4 @@ +// d2's author of `tag`. `caller2` (also in d2) binds d2's own `tag` (200), +// even though d1's `tag` is the first-wins merge winner. +tag :: () -> s64 { return 200; } +caller2 :: () -> s64 { return tag(); } diff --git a/examples/0727-modules-user-ns-m0.sx b/examples/0727-modules-user-ns-m0.sx new file mode 100644 index 0000000..5740500 --- /dev/null +++ b/examples/0727-modules-user-ns-m0.sx @@ -0,0 +1,20 @@ +// fix-0102c (issue 0102): a user namespace alias literally named `__m0` +// coexists with flat same-name imports. fix-0102 resolves same-name authors by +// FnDecl IDENTITY — there are no synthetic `__m0`-style names to collide with — +// so a user namespace spelled `__m0` is just an ordinary namespace: `call_a` +// binds a.ping (1), `call_b` binds b.ping (2), and `__m0.ping` reaches m.ping (99). +#import "modules/std.sx"; +#import "0727-modules-user-ns-m0/a.sx"; +#import "0727-modules-user-ns-m0/b.sx"; +__m0 :: #import "0727-modules-user-ns-m0/m.sx"; + +report :: (label: string, ok: bool) { + if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); } +} + +main :: () -> s32 { + report("call_a binds a.ping", call_a() == 1); + report("call_b binds b.ping", call_b() == 2); + report("__m0.ping binds m.ping", __m0.ping() == 99); + 0 +} diff --git a/examples/0727-modules-user-ns-m0/a.sx b/examples/0727-modules-user-ns-m0/a.sx new file mode 100644 index 0000000..f46b7f6 --- /dev/null +++ b/examples/0727-modules-user-ns-m0/a.sx @@ -0,0 +1,3 @@ +// Flat author of `ping`; `call_a` binds a.sx's own `ping` (1). +ping :: () -> s64 { return 1; } +call_a :: () -> s64 { return ping(); } diff --git a/examples/0727-modules-user-ns-m0/b.sx b/examples/0727-modules-user-ns-m0/b.sx new file mode 100644 index 0000000..af3fbfc --- /dev/null +++ b/examples/0727-modules-user-ns-m0/b.sx @@ -0,0 +1,3 @@ +// Second flat author of `ping`; `call_b` binds b.sx's own `ping` (2). +ping :: () -> s64 { return 2; } +call_b :: () -> s64 { return ping(); } diff --git a/examples/0727-modules-user-ns-m0/m.sx b/examples/0727-modules-user-ns-m0/m.sx new file mode 100644 index 0000000..db250e1 --- /dev/null +++ b/examples/0727-modules-user-ns-m0/m.sx @@ -0,0 +1,3 @@ +// Imported under a user namespace literally named `__m0`. Reached as +// `__m0.ping` (99); coexists with the flat `ping` collision. +ping :: () -> s64 { return 99; } diff --git a/examples/expected/0722-modules-flat-same-name-own.exit b/examples/expected/0722-modules-flat-same-name-own.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0722-modules-flat-same-name-own.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0722-modules-flat-same-name-own.stderr b/examples/expected/0722-modules-flat-same-name-own.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0722-modules-flat-same-name-own.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0722-modules-flat-same-name-own.stdout b/examples/expected/0722-modules-flat-same-name-own.stdout new file mode 100644 index 0000000..d9c2011 --- /dev/null +++ b/examples/expected/0722-modules-flat-same-name-own.stdout @@ -0,0 +1,2 @@ +from_a binds a.greet: ok +from_b binds b.greet: ok diff --git a/examples/expected/0723-modules-flat-vs-namespaced.exit b/examples/expected/0723-modules-flat-vs-namespaced.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0723-modules-flat-vs-namespaced.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0723-modules-flat-vs-namespaced.stderr b/examples/expected/0723-modules-flat-vs-namespaced.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0723-modules-flat-vs-namespaced.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0723-modules-flat-vs-namespaced.stdout b/examples/expected/0723-modules-flat-vs-namespaced.stdout new file mode 100644 index 0000000..9e6841a --- /dev/null +++ b/examples/expected/0723-modules-flat-vs-namespaced.stdout @@ -0,0 +1,2 @@ +bare binds flat: ok +nm.value binds named: ok diff --git a/examples/expected/0724-modules-flat-same-name-ambiguous.exit b/examples/expected/0724-modules-flat-same-name-ambiguous.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/0724-modules-flat-same-name-ambiguous.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/0724-modules-flat-same-name-ambiguous.stderr b/examples/expected/0724-modules-flat-same-name-ambiguous.stderr new file mode 100644 index 0000000..46bf580 --- /dev/null +++ b/examples/expected/0724-modules-flat-same-name-ambiguous.stderr @@ -0,0 +1,5 @@ +error: 'dup' is ambiguous; declared by multiple imported modules — qualify the call + --> examples/0724-modules-flat-same-name-ambiguous.sx:10:19 + | +10 | print("{}\n", dup()); + | ^^^ diff --git a/examples/expected/0724-modules-flat-same-name-ambiguous.stdout b/examples/expected/0724-modules-flat-same-name-ambiguous.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0724-modules-flat-same-name-ambiguous.stdout @@ -0,0 +1 @@ + diff --git a/examples/expected/0725-modules-flat-dir-same-name.exit b/examples/expected/0725-modules-flat-dir-same-name.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0725-modules-flat-dir-same-name.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0725-modules-flat-dir-same-name.stderr b/examples/expected/0725-modules-flat-dir-same-name.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0725-modules-flat-dir-same-name.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0725-modules-flat-dir-same-name.stdout b/examples/expected/0725-modules-flat-dir-same-name.stdout new file mode 100644 index 0000000..89953af --- /dev/null +++ b/examples/expected/0725-modules-flat-dir-same-name.stdout @@ -0,0 +1,2 @@ +caller1 binds d1.tag: ok +caller2 binds d2.tag: ok diff --git a/examples/expected/0727-modules-user-ns-m0.exit b/examples/expected/0727-modules-user-ns-m0.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0727-modules-user-ns-m0.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0727-modules-user-ns-m0.stderr b/examples/expected/0727-modules-user-ns-m0.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0727-modules-user-ns-m0.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0727-modules-user-ns-m0.stdout b/examples/expected/0727-modules-user-ns-m0.stdout new file mode 100644 index 0000000..65286e5 --- /dev/null +++ b/examples/expected/0727-modules-user-ns-m0.stdout @@ -0,0 +1,3 @@ +call_a binds a.ping: ok +call_b binds b.ping: ok +__m0.ping binds m.ping: ok diff --git a/readme.md b/readme.md index 9e4a688..7ddb706 100644 --- a/readme.md +++ b/readme.md @@ -393,6 +393,12 @@ Direct C header import: math :: #import "modules/math.sx"; // namespaced import ``` +When two flat-imported modules each define a function of the same name, every +module's own code binds its OWN author — a bare call resolves to the same-name +function in the caller's module (or in its single flat import that provides it). +A bare call to a name that two or more flat imports both provide is ambiguous and +is rejected; qualify it with a namespaced import (`m :: #import …; m.fn()`). + ### Implicit Context Every program gets an implicit `context` with a default allocator: diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index eb682e9..8a853f2 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -1290,14 +1290,15 @@ fn countRealBodies(module: *ir_mod.Module, name: []const u8) usize { } // fix-0102b: two flat-imported modules each author `greet`. The first-wins merge -// keeps a.sx's author in the merged decl list (the WINNER — lowered when `main` -// calls `greet()`) and drops b.sx's, which `module_fns` still retains (0102a). +// keeps a.sx's author in the merged decl list (the WINNER) and drops b.sx's, +// which `module_fns` still retains (0102a). `main` itself can't bare-call `greet` +// — under fix-0102c two flat authors make that ambiguous — so it calls a.sx's +// `use_greet` wrapper, whose own-author call to `greet` binds a.sx's winner. // BEFORE the identity-addressable pass, only the winner has a real body — the // shadowed author has no slot at all (the pre-fix symptom: one `greet`). // `lowerRetainedSameNameAuthors` declares the shadowed author its OWN same-name // FuncId and lowers its body there, so BOTH authors carry distinct, non-extern -// bodies. Call resolution is untouched: `resolveFuncByName` still returns the -// winner, so `main`'s `greet()` binds first-wins (rerouting is fix-0102c). +// bodies, and `resolveFuncByName` still returns the winner (the name-keyed slot). test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102b)" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); @@ -1307,12 +1308,12 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102 var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); - try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "greet :: () -> s64 { 1 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "greet :: () -> s64 { 1 }\nuse_greet :: () -> s64 { greet() }\n" }); try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "greet :: () -> s64 { 2 }\n" }); const main_src = \\#import "a.sx"; \\#import "b.sx"; - \\main :: () -> s64 { greet() } + \\main :: () -> s64 { use_greet() } \\ ; try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = main_src }); @@ -1426,4 +1427,20 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102 const shadow_fid = lowering.fn_decl_fids.get(shadow_fd.?); try std.testing.expect(shadow_fid != null); try std.testing.expect(shadow_fid.? != winner_fid.?); + + // fix-0102c: THE bare-name resolver routes per caller file. `main` flat- + // imports two `greet` authors and is its own author of neither → a bare + // `greet()` from `main` is ambiguous. a.sx authors the WINNER, so its bare + // `greet` resolves through the existing path (`.none`). b.sx authors the + // SHADOW, so own-author-wins binds b.sx's distinct FuncId — not first-wins. + const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir}); + const b_path = try std.fmt.allocPrint(alloc, "{s}/b.sx", .{absdir}); + try std.testing.expect(lowering.resolveBareCallee("greet", main_path) == .ambiguous); + try std.testing.expect(lowering.resolveBareCallee("greet", a_path) == .none); + switch (lowering.resolveBareCallee("greet", b_path)) { + .func => |fid| try std.testing.expectEqual(shadow_fid.?, fid), + else => return error.TestUnexpectedResult, + } + // A name no module authors (and no flat import provides) never routes. + try std.testing.expect(lowering.resolveBareCallee("nonexistent", b_path) == .none); } diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5c7f48b..03209d1 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1512,31 +1512,107 @@ pub const Lowering = struct { // Only plain free functions get an out-of-line slot; generic / // foreign / builtin / #compiler authors keep their existing // dispatch (mirrors lazyLowerFunction / declareFunction guards). - if (fd.type_params.len > 0) continue; - switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => continue, - else => {}, - } + if (!isPlainFreeFn(fd)) continue; - // Already given its own slot + body? (idempotent across reruns.) - if (self.fn_decl_fids.get(fd)) |existing| { - if (self.lowered_fids.contains(existing)) continue; - } - - // Declare a fresh same-name FuncId for this author and lower its - // body in its OWN module's visibility context (the path key IS - // the author's source file, matching `module_scopes`). - const saved_src = self.current_source_file; - self.setCurrentSourceFile(path); - if (!self.fn_decl_fids.contains(fd)) self.declareFunction(fd, name); - self.setCurrentSourceFile(saved_src); - const fid = self.fn_decl_fids.get(fd) orelse continue; - self.lowerFunctionBodyInto(fd, fid, name); - self.lowered_fids.put(fid, {}) catch {}; + _ = self.bareAuthorFuncId(fd, name, path); } } } + /// Result of bare-call disambiguation (fix-0102c). + pub const BareCallee = union(enum) { + /// Bind the call to this specific author's FuncId — the identity- + /// addressable body lowered by `bareAuthorFuncId` (fix-0102b). + func: FuncId, + /// ≥2 distinct flat authors are reachable from the caller and none is + /// the caller's own — the bare call can't pick one; require a qualifier. + ambiguous, + /// 0 or 1 reachable author, or the resolved author IS the existing + /// bare-name winner — defer to the existing path, byte-for-byte. + none, + }; + + /// THE bare-name call resolver (fix-0102c). One canonical traversal over + /// fix-0102a's `module_fns` + `flat_import_graph` that routes a bare + /// identifier call `name` from `caller_file` to the right same-name author + /// when flat imports introduce a genuine collision. Every single-author / + /// local / parameter / std / qualified name resolves through the EXISTING + /// path unchanged: the resolver returns `.none` whenever the outcome would + /// match first-wins, so nothing on the common path is perturbed. + /// + /// - **own-author wins**: if `caller_file` authors `name` and the bare-name + /// first-wins winner is a DIFFERENT author, bind the caller's own author. + /// (When the winner already IS the caller's own — the single-author and + /// first-importer cases — `.none` lets the existing path bind it.) + /// - else collect the authors reachable via `caller_file`'s FLAT import + /// edges (bare `#import` of a file or directory, never a namespaced + /// `ns :: #import`), deduped by `FnDecl` identity (a diamond import of the + /// same module is one author): `≥2 distinct` → `.ambiguous`; exactly one + /// that DIFFERS from the winner → bind it; otherwise `.none`. + /// + /// Generic / comptime / foreign / builtin authors are never rerouted — the + /// existing dispatch owns those shapes — so the resolver returns `.none`. + pub fn resolveBareCallee(self: *Lowering, name: []const u8, caller_file: []const u8) BareCallee { + const module_fns = self.program_index.module_fns orelse return .none; + const winner = self.program_index.fn_ast_map.get(name); + + // own-author wins. + if (module_fns.get(caller_file)) |own_fns| { + if (own_fns.get(name)) |own| { + if (winner != null and winner.? == own) return .none; + if (!isPlainFreeFn(own)) return .none; + return .{ .func = self.bareAuthorFuncId(own, name, caller_file) }; + } + } + + // Caller does not author `name` → collect its flat-reachable authors. + const flat_graph = self.program_index.flat_import_graph orelse return .none; + const edges = flat_graph.get(caller_file) orelse return .none; + var distinct = std.AutoHashMap(*const ast.FnDecl, []const u8).init(self.alloc); + defer distinct.deinit(); + var edge_it = edges.iterator(); + while (edge_it.next()) |e| { + const fns = module_fns.get(e.key_ptr.*) orelse continue; + if (fns.get(name)) |fd| distinct.put(fd, e.key_ptr.*) catch {}; + } + if (distinct.count() == 0) return .none; + if (distinct.count() >= 2) return .ambiguous; + + var one_it = distinct.iterator(); + const entry = one_it.next().?; + const the_one = entry.key_ptr.*; + const the_path = entry.value_ptr.*; + if (winner != null and winner.? == the_one) return .none; + if (!isPlainFreeFn(the_one)) return .none; + return .{ .func = self.bareAuthorFuncId(the_one, name, the_path) }; + } + + /// The FuncId for a resolved bare-call author, ensuring its body is lowered. + /// Only ever called for a SHADOW (an author that is not the name-keyed + /// winner): the winner owns the name-keyed slot and lowers through the + /// normal lazy path, so `resolveBareCallee` returns `.none` for it. A shadow + /// is declared a fresh same-name FuncId in its OWN module's visibility + /// context and its body lowered into that slot via fix-0102b's identity- + /// addressable `lowerFunctionBodyInto`. Idempotent: `lowered_fids` tracks + /// which slots already carry a body. + fn bareAuthorFuncId(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, path: []const u8) FuncId { + if (self.fn_decl_fids.get(fd)) |fid| { + if (!self.lowered_fids.contains(fid)) { + self.lowered_fids.put(fid, {}) catch {}; + self.lowerFunctionBodyInto(fd, fid, name); + } + return fid; + } + const saved_src = self.current_source_file; + self.setCurrentSourceFile(path); + self.declareFunction(fd, name); + self.setCurrentSourceFile(saved_src); + const fid = self.fn_decl_fids.get(fd).?; + self.lowered_fids.put(fid, {}) catch {}; + self.lowerFunctionBodyInto(fd, fid, name); + return fid; + } + /// Declare a function as an extern stub (signature only, no body). pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void { // Skip generic templates — they're monomorphized on demand, not declared as extern @@ -7466,6 +7542,39 @@ pub const Lowering = struct { } } } + // fix-0102c: a genuine flat same-name collision — bind the + // caller file's OWN author (or its single flat-reachable + // author), or reject a bare call to a name ≥2 imported modules + // author. Only a plain top-level identifier call routes here: + // scope-mangled / UFCS-aliased / locally-shadowed names and + // 0/1-author names fall straight to the existing path below + // (`resolveBareCallee` returns `.none`). + if (std.mem.eql(u8, func_name, id.name) and + (if (self.scope) |scope| scope.lookup(id.name) == null else true)) + { + if (self.current_source_file) |caller_file| { + switch (self.resolveBareCallee(func_name, caller_file)) { + .none => {}, + .ambiguous => { + if (self.diagnostics) |d| + d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{func_name}); + return Ref.none; + }, + .func => |fid| { + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + if (self.program_index.fn_ast_map.get(func_name)) |fd| { + self.packVariadicCallArgs(fd, c, &args); + } + const final_args = self.prependCtxIfNeeded(func, args.items); + self.coerceCallArgs(final_args, params); + if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len); + return self.builder.call(fid, final_args, ret_ty); + }, + } + } + } // Check for comptime-expanded or generic functions if (self.program_index.fn_ast_map.get(func_name)) |fd| { if (hasComptimeParams(fd)) { @@ -12018,6 +12127,19 @@ pub const Lowering = struct { return false; } + /// A plain free function: no type params (not generic) and an ordinary sx + /// body (not `#foreign` / `#builtin` / `#compiler`). Only these get an + /// out-of-line identity-addressable slot — the bare-call disambiguation + /// (fix-0102c) and the shadow-author lowering pass leave every other shape + /// to the existing name-keyed dispatch. + fn isPlainFreeFn(fd: *const ast.FnDecl) bool { + if (fd.type_params.len > 0) return false; + return switch (fd.body.data) { + .foreign_expr, .builtin_expr, .compiler_expr => false, + else => true, + }; + } + /// Pack-fn: has a trailing heterogeneous pack param (`is_variadic /// AND is_comptime`). Mixed shapes — non-pack comptime params /// before the pack — are also accepted; the mono folds those