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

@@ -1461,7 +1461,7 @@ pub const Lowering = struct {
// A `#run` body lowers in its OWN module's source context (fix-0102d
// site 4): `NAME :: #run f()` written in an imported module must
// resolve a bare `f` from that module's flat imports, not the main
// file's. Without this, `resolveBareCallee` runs with the main
// file's. Without this, `selectPlainCallableAuthor` runs with the main
// file's perspective and reports a genuine per-source author as
// ambiguous. Mirrors `scanDecls` / `lowerDecls`, which already set
// the source file per decl.
@@ -1552,14 +1552,16 @@ pub const Lowering = struct {
}
}
/// Result of bare-call disambiguation (fix-0102c).
/// Result of bare-call disambiguation (fix-0102c, now over the Phase B
/// author collector).
pub const BareCallee = union(enum) {
/// Bind the call to this specific author — its identity-addressable
/// FuncId (fix-0102b's `bareAuthorFuncId`) AND its `*FnDecl`. The decl
/// travels with the FuncId so every callee-signature decision in the
/// call path (variadic packing, …) reads the RESOLVED author, never a
/// first-wins re-lookup by name (fix-0102c F1).
func: ResolvedAuthor,
/// Bind the call to this specific author, carried as the shared
/// `SelectedFunc` (R5 §#3): its `*FnDecl` + authoring source, FuncId
/// materialized on demand. Every callee-signature decision in the call
/// path (variadic packing, param typing, default expansion) reads the
/// RESOLVED author from this one object — never a first-wins re-lookup
/// by name (fix-0102c F1).
func: SelectedFunc,
/// ≥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,
@@ -1568,78 +1570,122 @@ pub const Lowering = struct {
none,
};
/// A resolved bare-call author: its FuncId and the `*FnDecl` that defined
/// it, kept together so the call path has ONE source of truth for the
/// callee (no re-fetch by name after resolution).
pub const ResolvedAuthor = struct { fid: FuncId, decl: *const ast.FnDecl };
/// The single bare-call author object (R5 §#3): the `*FnDecl` that defines
/// the call and the SOURCE file that authors it, kept together so the call
/// path has ONE source of truth for the callee. `materialized` holds the
/// author's FuncId once a site needs it; it is filled on demand by
/// `selectedFuncId` (→ `bareAuthorFuncId`), NOT during selection — so a
/// selection that only needs the decl (default-arg expansion), or a shadow
/// taken purely as a value, never lowers the first-wins winner (0102d).
pub const SelectedFunc = struct {
decl: *const ast.FnDecl,
source: []const u8,
materialized: ?FuncId = null,
};
/// 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.
/// THE plain bare-name call selector (fix-0102c, R5 §C). `resolveBareCallee`'s
/// body verbatim, now over the Phase B author collector
/// (`resolver.collectVisibleAuthors` — the ONE graph-walk) instead of a direct
/// `module_fns` + `flat_import_graph` traversal. 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
/// selector 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
/// The collector returns RAW authors across ALL decl domains; this selector
/// reproduces `module_fns`' fn-only view by filtering each author through
/// `fnDeclOfRaw` (a `const`-wrapped fn unwraps to its inner fn — the exact
/// `*FnDecl` `module_fns` stored; every other domain drops out), preserving
/// resolveBareCallee's negative space byte-for-byte.
///
/// - **own-author wins**: if `caller_file` authors `name` as a fn and the
/// bare-name first-wins winner is a DIFFERENT author, select 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 select among 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
/// `ns :: #import`), deduped by author 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`.
/// that DIFFERS from the winner → select 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;
/// existing dispatch owns those shapes; `isPlainFreeFn` filters them out
/// BEFORE the count gate (so a same-name collision of non-plain authors is
/// NOT ambiguous), and the selector returns `.none`. No eager
/// materialization: the returned `SelectedFunc` carries decl + source and
/// `materialized = null`; a consumer fills the FuncId via `selectedFuncId`
/// only when it truly needs it (0102d).
pub fn selectPlainCallableAuthor(self: *Lowering, name: []const u8, caller_file: []const u8) BareCallee {
const winner = self.program_index.fn_ast_map.get(name);
var res = self.resolver();
const set = res.collectVisibleAuthors(name, caller_file, .user_bare_flat);
defer if (set.flat.len > 0) self.alloc.free(set.flat);
// own-author wins.
if (module_fns.get(caller_file)) |own_fns| {
if (own_fns.get(name)) |own| {
// own-author wins. The collector's `own` spans all domains; a non-fn
// (or a const not bound to a function) means `caller_file` has no fn
// `name` — fall through to the flat authors, exactly as the fn-only
// `module_fns` walk did.
if (set.own) |own_author| {
if (fnDeclOfRaw(own_author.raw)) |own| {
if (winner != null and winner.? == own) return .none;
if (!isPlainFreeFn(own)) return .none;
return .{ .func = .{ .fid = self.bareAuthorFuncId(own, name, caller_file), .decl = own } };
return .{ .func = .{ .decl = own, .source = own_author.source } };
}
}
// 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;
// Only plain free functions are eligible for rerouting; generic /
// foreign / builtin / #compiler authors keep their existing
// dispatch. Filtering BEFORE the count gate means a same-name
// collision of non-plain authors (e.g. two flat-imported modules
// each `#foreign`ing the same symbol) is NOT counted as ambiguous
// it falls through to `.none` and the existing first-wins path.
if (fns.get(name)) |fd| {
if (!isPlainFreeFn(fd)) continue;
distinct.put(fd, e.key_ptr.*) catch {};
}
// Caller does not author `name` as a fn → its flat-reachable authors.
// Filter to plain free functions BEFORE counting: a same-name collision
// of non-plain authors (e.g. two flat-imported modules each `#foreign`ing
// the same symbol) is NOT counted as ambiguous — it falls through to
// `.none` and the existing first-wins path.
var the_one: ?*const ast.FnDecl = null;
var the_source: []const u8 = &.{};
var count: usize = 0;
for (set.flat) |fa| {
const fd = fnDeclOfRaw(fa.raw) orelse continue;
if (!isPlainFreeFn(fd)) continue;
count += 1;
if (count >= 2) return .ambiguous;
the_one = fd;
the_source = fa.source;
}
if (distinct.count() == 0) return .none;
if (distinct.count() >= 2) return .ambiguous;
if (count == 0) return .none;
if (winner != null and winner.? == the_one.?) return .none;
return .{ .func = .{ .decl = the_one.?, .source = the_source } };
}
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;
return .{ .func = .{ .fid = self.bareAuthorFuncId(the_one, name, the_path), .decl = the_one } };
/// The `*FnDecl` a raw author wraps, or null when the author is not a
/// function — `imports.fnDeclOf` over a `RawDeclRef` so the collector's
/// all-domain authors reproduce `module_fns`' fn-only view (a `const`-wrapped
/// fn unwraps to its inner fn, the same pointer `module_fns` holds; every
/// other domain → null).
fn fnDeclOfRaw(ref: resolver_mod.RawDeclRef) ?*const ast.FnDecl {
return switch (ref) {
.fn_decl => |fd| fd,
.const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null,
else => null,
};
}
/// Materialize (lower-on-demand) the FuncId for a selected bare-call author,
/// caching into `sf.materialized`. Shadow-only: the winner owns the
/// name-keyed slot and lowers through the lazy path, so
/// `selectPlainCallableAuthor` returns `.none` for it and this is never asked
/// to lower the winner (0102d). `name` is the call name (== the author's
/// registered name); `sf.source` pins the author's own visibility context.
fn selectedFuncId(self: *Lowering, sf: *SelectedFunc, name: []const u8) FuncId {
if (sf.materialized) |fid| return fid;
const fid = self.bareAuthorFuncId(sf.decl, name, sf.source);
sf.materialized = fid;
return fid;
}
/// 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
/// normal lazy path, so `selectPlainCallableAuthor` 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
@@ -3325,8 +3371,11 @@ pub const Lowering = struct {
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
{
if (self.current_source_file) |caller_file| {
switch (self.resolveBareCallee(id.name, caller_file)) {
.func => |resolved| break :blk_fv resolved.fid,
switch (self.selectPlainCallableAuthor(id.name, caller_file)) {
.func => |sf| {
var selected = sf;
break :blk_fv self.selectedFuncId(&selected, id.name);
},
.ambiguous => {
if (self.diagnostics) |d|
d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name});
@@ -7377,8 +7426,11 @@ pub const Lowering = struct {
(if (self.scope) |scope| scope.lookup(fn_name) == null else true))
{
if (self.current_source_file) |caller_file| {
switch (self.resolveBareCallee(fn_name, caller_file)) {
.func => |resolved| break :blk_cl resolved.fid,
switch (self.selectPlainCallableAuthor(fn_name, caller_file)) {
.func => |sf| {
var selected = sf;
break :blk_cl self.selectedFuncId(&selected, fn_name);
},
.ambiguous => {
if (self.diagnostics) |d|
d.addFmt(.err, arg.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fn_name});
@@ -7682,27 +7734,28 @@ pub const Lowering = struct {
// 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`).
// (`selectPlainCallableAuthor` 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)) {
switch (self.selectPlainCallableAuthor(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 => |resolved| {
const fid = resolved.fid;
.func => |sf| {
var selected = sf;
const fid = self.selectedFuncId(&selected, func_name);
const func = &self.module.functions.items[@intFromEnum(fid)];
const ret_ty = func.ret;
const params = func.params;
// The RESOLVED author's decl drives variadic
// packing — not a first-wins re-lookup by name,
// whose variadic shape may differ (fix-0102c F1).
self.packVariadicCallArgs(resolved.decl, c, &args);
self.packVariadicCallArgs(selected.decl, 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);
@@ -8202,8 +8255,11 @@ pub const Lowering = struct {
// → existing first-wins path.
const ufcs_fid: ?FuncId = blk_uf: {
if (self.current_source_file) |caller_file| {
switch (self.resolveBareCallee(fa.field, caller_file)) {
.func => |resolved| break :blk_uf resolved.fid,
switch (self.selectPlainCallableAuthor(fa.field, caller_file)) {
.func => |sf| {
var selected = sf;
break :blk_uf self.selectedFuncId(&selected, fa.field);
},
.ambiguous => {
if (self.diagnostics) |d|
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
@@ -12054,8 +12110,14 @@ pub const Lowering = struct {
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
{
if (self.current_source_file) |caller_file| {
switch (self.resolveBareCallee(id.name, caller_file)) {
.func => |resolved| break :blk resolved.decl,
switch (self.selectPlainCallableAuthor(id.name, caller_file)) {
// Default expansion needs only the author's decl
// (its param defaults) — never the FuncId. Reading
// `sf.decl` here keeps `materialized` null, so a
// bare call whose body is never emitted (e.g. only
// its defaults are inspected) does not lower the
// author (0102d).
.func => |sf| break :blk sf.decl,
.ambiguous => return null,
.none => {},
}
@@ -12296,9 +12358,11 @@ pub const Lowering = struct {
(if (self.scope) |scope| scope.lookup(bare_name) == null else true))
{
if (self.current_source_file) |caller_file| {
switch (self.resolveBareCallee(bare_name, caller_file)) {
.func => |resolved| {
const func = &self.module.functions.items[@intFromEnum(resolved.fid)];
switch (self.selectPlainCallableAuthor(bare_name, caller_file)) {
.func => |sf| {
var selected = sf;
const fid = self.selectedFuncId(&selected, bare_name);
const func = &self.module.functions.items[@intFromEnum(fid)];
return self.userParamTypes(func);
},
.ambiguous, .none => {},
@@ -14548,6 +14612,13 @@ pub const Lowering = struct {
return .{ .l = self };
}
/// A `Resolver` facade over the borrowed Phase A import facts (Phase B). Cheap
/// by-value; `collectVisibleAuthors`'s `AuthorSet.flat` slice is backed by
/// `self.alloc` and owned by the caller (`selectPlainCallableAuthor` frees it).
fn resolver(self: *Lowering) resolver_mod.Resolver {
return resolver_mod.Resolver.init(&self.program_index, self.alloc);
}
pub fn genericResolver(self: *Lowering) GenericResolver {
return .{ .l = self };
}