feat(resolver): route plain bare-call author through Phase B collector via SelectedFunc [stdlib C]

Phase C of the unified resolver (R5 §C, §#3). Re-base the plain bare-name
call author onto the Phase B collector behind one shared SelectedFunc, so
every call-path consumer reads ONE author and they can no longer disagree
(fix-0102 F2). Behavior-preserving: 0722-0735 byte-identical, run_examples
stays at 475.

- SelectedFunc {decl, source, materialized?} replaces ResolvedAuthor in
  BareCallee.func; CallPlan.Target gains a `selected` arm (calls.zig).
- selectPlainCallableAuthor: resolveBareCallee's body verbatim over
  resolver.collectVisibleAuthors (.user_bare_flat) — the ONE graph-walk.
  fnDeclOfRaw mirrors imports.fnDeclOf so the collector's all-domain authors
  reproduce module_fns' fn-only view; every byte of the negative space is
  preserved (own==winner → .none; non-plain-free → .none; filter-before-count;
  ≥2 distinct → .ambiguous). No eager materialization.
- selectedFuncId materializes the FuncId on demand (shadow-only), caching into
  materialized — null until a site needs it (0102d: a shadow taken as a value
  never lowers the winner).
- Six consumers route through the one selector: lowerCall variadic packing,
  free-fn UFCS, fn-value, closure(fn), resolveCallParamTypes, and
  expandCallDefaults (decl-only, no materialization). plan() produces the
  SelectedFunc as `.selected`. Generic/comptime/foreign/builtin stay legacy.
- lower.test.zig: wire module_decls; selectPlainCallableAuthor verdicts
  (own-winner → .none; ≥2 flat → .ambiguous; own-shadow → decl+source, fid
  round-trips, materialized null).

Gate: zig build + zig build test (412 ok) + run_examples (475, byte-identical)
+ m3te ios-sim build exit 0.
This commit is contained in:
agra
2026-06-07 11:02:08 +03:00
parent f2de1a9710
commit 9568f7689f
3 changed files with 199 additions and 86 deletions

View File

@@ -1361,6 +1361,10 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102
var module_fns = imports.ModuleFns.init(alloc);
try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns);
// Phase A raw facts: `selectPlainCallableAuthor` (Phase C) collects authors
// over `module_decls`, not `module_fns`. Wired exactly as `core.zig` does.
var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
const resolved_root = try alloc.create(Node);
resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } };
@@ -1375,6 +1379,7 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102
lowering.program_index.import_graph = &import_graph;
lowering.program_index.flat_import_graph = &flat_import_graph;
lowering.program_index.module_fns = &module_fns;
lowering.program_index.module_decls = &facts.decls;
lowering.lowerRoot(resolved_root);
try std.testing.expect(!diagnostics.hasErrors());
@@ -1428,19 +1433,27 @@ test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102
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.
// fix-0102c / Phase C: THE bare-name selector routes per caller file over the
// Phase A author collector. `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 selects b.sx's
// author — its `*FnDecl` + source, NOT first-wins. The selector does NOT
// eagerly materialize: it returns the decl, and the FuncId still round-trips
// to the shadow slot via the identity map (`fn_decl_fids`).
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 => |resolved| try std.testing.expectEqual(shadow_fid.?, resolved.fid),
try std.testing.expect(lowering.selectPlainCallableAuthor("greet", main_path) == .ambiguous);
try std.testing.expect(lowering.selectPlainCallableAuthor("greet", a_path) == .none);
switch (lowering.selectPlainCallableAuthor("greet", b_path)) {
.func => |sf| {
try std.testing.expectEqual(shadow_fd.?, sf.decl);
try std.testing.expectEqualStrings(b_path, sf.source);
try std.testing.expect(sf.materialized == null);
try std.testing.expectEqual(shadow_fid.?, lowering.fn_decl_fids.get(sf.decl).?);
},
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);
try std.testing.expect(lowering.selectPlainCallableAuthor("nonexistent", b_path) == .none);
}