fix(lower): bare-call resolver binds same-name flat authors per source [0102c]

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.
This commit is contained in:
agra
2026-06-06 14:04:03 +03:00
parent b077e8e29c
commit ea35a05b26
34 changed files with 316 additions and 26 deletions

View File

@@ -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);
}