fix(resolver): share plan SelectedFunc across consumers + route UFCS through selector [stdlib C attempt-2]
Address Phase C review (C-1, C-2): make CallResolver.plan's SelectedFunc the single shared call author consumed by the lower-call sites instead of each re-resolving; route free-fn value-receiver UFCS through the selector in plan so plan typing and lowering pick the same author under a flat same-name collision. Adds regression 0740-modules-flat-same-name-ufcs-typing. Salvaged from a worker killed at the wall during its final gate step; manager verified the gate at ground truth (zig build test exit 0; run_examples 476/0 with 0722-0735 + 0740 ok; m3te ios-sim exit 0).
This commit is contained in:
@@ -157,26 +157,19 @@ pub const CallResolver = struct {
|
||||
if (std.mem.eql(u8, bare_name, "type_of")) return refl(bare_name, .any);
|
||||
if (std.mem.eql(u8, bare_name, "field_value")) return refl(bare_name, .any);
|
||||
// Plain bare same-name flat collision (R5 §C): route through the ONE
|
||||
// selector so `plan` reads the SAME author the lowering call-path
|
||||
// binds — they can no longer disagree (fix-0102 F2). The gate mirrors
|
||||
// `lowerCall`'s: a plain top-level identifier with no scope-mangle /
|
||||
// local shadow. A generic / foreign / builtin author is not plain-free
|
||||
// so the selector returns `.none`; `.ambiguous` / `.none` fall through
|
||||
// to the first-wins path below, byte-for-byte.
|
||||
if (std.mem.eql(u8, name, bare_name) and
|
||||
(if (self.l.scope) |scope| scope.lookup(bare_name) == null else true))
|
||||
{
|
||||
if (self.l.current_source_file) |caller_file| {
|
||||
switch (self.l.selectPlainCallableAuthor(bare_name, caller_file)) {
|
||||
.func => |sf| return .{
|
||||
.kind = .direct_fn,
|
||||
.return_type = if (sf.decl.return_type) |rt| self.l.resolveType(rt) else .void,
|
||||
.target = .{ .selected = sf },
|
||||
.expands_defaults = defaultsFor(sf.decl, c.args.len),
|
||||
},
|
||||
.ambiguous, .none => {},
|
||||
}
|
||||
}
|
||||
// author producer `selectedFreeAuthor` so `plan` types the call as the
|
||||
// SAME author the lowering call-path binds — they can no longer
|
||||
// disagree (fix-0102 F2). A generic / foreign / builtin author is not
|
||||
// plain-free so the producer returns `.none`; `.ambiguous` / `.none`
|
||||
// fall through to the first-wins path below, byte-for-byte.
|
||||
switch (self.selectedFreeAuthor(c)) {
|
||||
.func => |sf| return .{
|
||||
.kind = .direct_fn,
|
||||
.return_type = if (sf.decl.return_type) |rt| self.l.resolveType(rt) else .void,
|
||||
.target = .{ .selected = sf },
|
||||
.expands_defaults = defaultsFor(sf.decl, c.args.len),
|
||||
},
|
||||
.ambiguous, .none => {},
|
||||
}
|
||||
// Generic function — infer return type via type bindings.
|
||||
if (self.l.program_index.fn_ast_map.get(name)) |fd| {
|
||||
@@ -322,6 +315,26 @@ pub const CallResolver = struct {
|
||||
// the plan carries `prepends_receiver`, distinct from a true
|
||||
// namespace call (`pkg.fn()`), which must NOT prepend.
|
||||
if (self.objectIsValue(cfa.object)) {
|
||||
// Value-receiver free-fn UFCS (`recv.fn(args)` → `fn(recv, args)`)
|
||||
// routes through the SAME author producer `selectedFreeAuthor` as a
|
||||
// bare call, so the planned target / return type IS the author
|
||||
// lowering dispatches — they can't disagree under a flat same-name
|
||||
// collision (fix-0102 F2 / R5 §C). Without this, plan typed the
|
||||
// first-wins winner while lowering bound the selected shadow,
|
||||
// mis-tagging the call's result (a string-typed winner over an s64
|
||||
// shadow boxes a raw int as a string pointer → segfault).
|
||||
// `.ambiguous` / `.none` fall through to the first-wins path below,
|
||||
// unchanged.
|
||||
switch (self.selectedFreeAuthor(c)) {
|
||||
.func => |sf| return .{
|
||||
.kind = .free_fn_ufcs,
|
||||
.return_type = if (sf.decl.return_type) |rt| self.l.resolveType(rt) else .void,
|
||||
.target = .{ .selected = sf },
|
||||
.prepends_receiver = true,
|
||||
.expands_defaults = defaultsFor(sf.decl, c.args.len + 1),
|
||||
},
|
||||
.ambiguous, .none => {},
|
||||
}
|
||||
if (self.l.resolveFuncByName(cfa.field)) |fid| {
|
||||
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
||||
return .{
|
||||
@@ -434,6 +447,47 @@ pub const CallResolver = struct {
|
||||
return .{ .kind = .unresolved, .return_type = .unresolved };
|
||||
}
|
||||
|
||||
/// THE single producer of the bare / value-UFCS same-name call author
|
||||
/// verdict (R5 §#3). Both `plan` (typing, via its `.selected` arm) and
|
||||
/// `lowerCall` (default expansion / param typing / dispatch) consume THIS one
|
||||
/// result, so they can never pick different same-name authors for the same
|
||||
/// call (fix-0102 F2). Side-effect-free: it consults ONLY the author selector
|
||||
/// (`selectPlainCallableAuthor`) — never return-type inference or type-arg
|
||||
/// resolution — so `lowerCall` can compute it eagerly without emitting a
|
||||
/// premature diagnostic the full `plan` would (e.g. `cast(type)`'s type-arg).
|
||||
///
|
||||
/// - identifier callee: a plain bare call. The gate mirrors `plan`/`lowerCall`
|
||||
/// — a builtin, a scope-mangled / UFCS-aliased name, or a locally-shadowed
|
||||
/// name is never a same-name free-fn collision → `.none`.
|
||||
/// - field-access callee with a VALUE receiver: a free-function UFCS
|
||||
/// (`recv.fn(args)`). A namespace / type prefix receiver → `.none`. The
|
||||
/// verdict over-selects a struct-method / protocol / foreign call whose
|
||||
/// field happens to name a free fn, but those dispatch BEFORE the free-fn
|
||||
/// UFCS path in both `plan` and `lowerCall`, so the verdict is consumed only
|
||||
/// when the call truly is a free-fn UFCS.
|
||||
pub fn selectedFreeAuthor(self: CallResolver, c: *const ast.Call) Lowering.BareCallee {
|
||||
const caller_file = self.l.current_source_file orelse return .none;
|
||||
switch (c.callee.data) {
|
||||
.identifier => |id| {
|
||||
const bare_name = id.name;
|
||||
if (Lowering.resolveBuiltin(bare_name) != null) return .none;
|
||||
const scoped = if (self.l.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
||||
const name = if (self.l.program_index.ufcs_alias_map.get(bare_name)) |target|
|
||||
(if (self.l.scope) |scope| scope.lookupFn(target) orelse target else target)
|
||||
else
|
||||
scoped;
|
||||
if (!std.mem.eql(u8, name, bare_name)) return .none;
|
||||
if (self.l.scope) |scope| if (scope.lookup(bare_name) != null) return .none;
|
||||
return self.l.selectPlainCallableAuthor(bare_name, caller_file);
|
||||
},
|
||||
.field_access => |cfa| {
|
||||
if (!self.objectIsValue(cfa.object)) return .none;
|
||||
return self.l.selectPlainCallableAuthor(cfa.field, caller_file);
|
||||
},
|
||||
else => return .none,
|
||||
}
|
||||
}
|
||||
|
||||
fn refl(name: []const u8, rt: TypeId) CallPlan {
|
||||
return .{ .kind = .reflection, .return_type = rt, .target = .{ .named = name } };
|
||||
}
|
||||
|
||||
197
src/ir/lower.zig
197
src/ir/lower.zig
@@ -7361,10 +7361,27 @@ pub const Lowering = struct {
|
||||
c = rewritten;
|
||||
};
|
||||
}
|
||||
// fix-0102 F2 / R5 §C: select the bare / value-UFCS same-name call author
|
||||
// ONCE, via `CallResolver.selectedFreeAuthor` — the SINGLE producer of
|
||||
// this verdict, the exact same one `CallResolver.plan` consumes for typing.
|
||||
// The call-path consumers (default expansion, param typing, dispatch) all
|
||||
// read THIS one author object, so plan-typing and lowering-dispatch can no
|
||||
// longer disagree about which same-name function the call names, and the
|
||||
// shadow's FuncId is materialized at most once (into `author_verdict`).
|
||||
// `selectedFreeAuthor` is side-effect-free (it only runs the author
|
||||
// selector — no return-type inference / type-arg resolution), so computing
|
||||
// it eagerly here can't emit a premature diagnostic the way the full plan
|
||||
// would.
|
||||
var author_verdict = self.callResolver().selectedFreeAuthor(c);
|
||||
const sel_author: ?*SelectedFunc = switch (author_verdict) {
|
||||
.func => |*sf| sf,
|
||||
else => null,
|
||||
};
|
||||
const author_ambiguous = author_verdict == .ambiguous;
|
||||
// Expand default parameter values for bare identifier callees:
|
||||
// when the caller omits trailing positional args, fill them in
|
||||
// from the callee's `param: T = expr` declarations.
|
||||
if (self.expandCallDefaults(c)) |expanded| c = expanded;
|
||||
if (self.expandCallDefaults(c, sel_author, author_ambiguous)) |expanded| c = expanded;
|
||||
// Check reflection builtins first (before lowering args — some args are type names, not values)
|
||||
if (c.callee.data == .identifier) {
|
||||
if (self.tryLowerReflectionCall(c.callee.data.identifier.name, c)) |ref| return ref;
|
||||
@@ -7515,7 +7532,7 @@ pub const Lowering = struct {
|
||||
var args = std.ArrayList(Ref).empty;
|
||||
defer args.deinit(self.alloc);
|
||||
// Try to resolve param types for target_type context
|
||||
const param_types = self.resolveCallParamTypes(c);
|
||||
const param_types = self.resolveCallParamTypes(c, sel_author);
|
||||
// For enum_literal callees (.Variant(payload)), resolve the payload target type
|
||||
// from the union field type so struct literal fields get proper coercion
|
||||
var enum_payload_ty: ?TypeId = null;
|
||||
@@ -7728,41 +7745,33 @@ 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
|
||||
// (`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.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 => |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(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);
|
||||
return self.builder.call(fid, final_args, ret_ty);
|
||||
},
|
||||
}
|
||||
}
|
||||
// fix-0102c / R5 §C: a genuine flat same-name collision — bind the
|
||||
// author the call resolver selected (own-author-wins, or the single
|
||||
// flat-reachable author), or reject a bare call to a name ≥2
|
||||
// imported modules author. `selectedFreeAuthor` (computed once
|
||||
// above, and the exact verdict `plan` consumes for typing) is the
|
||||
// single producer; lowering CONSUMES it rather than re-resolving
|
||||
// the name, so typing and dispatch read the SAME author and can't
|
||||
// disagree (fix-0102 F2). Reached only for an identifier callee, so
|
||||
// `sel_author` / `author_ambiguous` here are the bare verdict.
|
||||
if (author_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;
|
||||
}
|
||||
if (sel_author) |sf| {
|
||||
const fid = self.selectedFuncId(sf, 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(sf.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);
|
||||
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| {
|
||||
@@ -8247,26 +8256,24 @@ pub const Lowering = struct {
|
||||
// a function reached ONLY via UFCS would otherwise be declared
|
||||
// but never emitted (issue 0063: undefined symbol at link).
|
||||
//
|
||||
// fix-0102d site 3: a free-function UFCS target with a genuine
|
||||
// flat same-name collision must dispatch to the RESOLVED author
|
||||
// for the receiver's source, not the first-wins winner. The
|
||||
// field name is never scope-mangled, so the only gate is a
|
||||
// known source file; `.ambiguous` → loud diagnostic; `.none`
|
||||
// → existing first-wins path.
|
||||
// fix-0102d site 3 / R5 §C: a free-function UFCS target with a
|
||||
// genuine flat same-name collision dispatches to the author the
|
||||
// call PLAN selected for the receiver's source — the SAME author
|
||||
// plan typed the call's result as, so dispatch and typing can't
|
||||
// disagree (fix-0102 F2; without this, a string-typed winner over
|
||||
// an s64 shadow boxes a raw int as a string pointer → segfault).
|
||||
// The plan is the single producer; lowering consumes its verdict
|
||||
// (`sel_author` / `cplan.ambiguous_collision`, computed once above)
|
||||
// rather than re-resolving the field name. `.ambiguous` → loud
|
||||
// diagnostic; otherwise the existing first-wins lazy path.
|
||||
const ufcs_fid: ?FuncId = blk_uf: {
|
||||
if (self.current_source_file) |caller_file| {
|
||||
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});
|
||||
return Ref.none;
|
||||
},
|
||||
.none => {},
|
||||
}
|
||||
if (author_ambiguous) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
|
||||
return Ref.none;
|
||||
}
|
||||
if (sel_author) |sf| {
|
||||
break :blk_uf self.selectedFuncId(sf, fa.field);
|
||||
}
|
||||
if (self.program_index.fn_ast_map.get(fa.field)) |_| {
|
||||
if (!self.lowered_functions.contains(fa.field)) {
|
||||
@@ -12088,7 +12095,7 @@ pub const Lowering = struct {
|
||||
/// callee's signature provides defaults for them, return a fresh Call
|
||||
/// node with the defaults filled in. Returns null when no expansion is
|
||||
/// needed (callee unknown, all args provided, or no defaults available).
|
||||
fn expandCallDefaults(self: *Lowering, c: *const ast.Call) ?*ast.Call {
|
||||
fn expandCallDefaults(self: *Lowering, c: *const ast.Call, sel_author: ?*const SelectedFunc, author_ambiguous: bool) ?*ast.Call {
|
||||
const fd = blk: {
|
||||
switch (c.callee.data) {
|
||||
.identifier => |id| {
|
||||
@@ -12099,30 +12106,19 @@ pub const Lowering = struct {
|
||||
}
|
||||
break :blk2 scoped;
|
||||
};
|
||||
// fix-0102d site 1: for a genuine flat same-name collision the
|
||||
// omitted trailing args must be filled from the RESOLVED
|
||||
// author's defaults, not the first-wins winner's. Only a plain
|
||||
// top-level identifier with no scope-mangle / UFCS alias /
|
||||
// local shadow routes here; `.ambiguous` declines to expand
|
||||
// (the call path emits the single diagnostic); `.none` keeps
|
||||
// the existing first-wins winner, byte-for-byte.
|
||||
if (std.mem.eql(u8, eff_name, id.name) and
|
||||
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
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 => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
// fix-0102d site 1 / R5 §C: for a genuine flat same-name
|
||||
// collision the omitted trailing args are filled from the
|
||||
// author the call resolver selected — its `*FnDecl` defaults —
|
||||
// not the first-wins winner's. lowering consumes the ONE author
|
||||
// verdict (`selectedFreeAuthor`, computed once in `lowerCall`)
|
||||
// rather than re-resolving the name, so default expansion and
|
||||
// dispatch agree on the author. `.ambiguous` declines to expand
|
||||
// (the call path emits the single diagnostic); a non-collision
|
||||
// call keeps the existing first-wins winner, byte-for-byte.
|
||||
// Reading `.decl` only keeps `materialized` null — inspecting
|
||||
// defaults must not lower the author (0102d).
|
||||
if (author_ambiguous) return null;
|
||||
if (sel_author) |sf| break :blk sf.decl;
|
||||
break :blk self.program_index.fn_ast_map.get(eff_name) orelse return null;
|
||||
},
|
||||
// Namespace call `mod.fn(args)` — args map directly to params
|
||||
@@ -12192,7 +12188,7 @@ pub const Lowering = struct {
|
||||
return types_list.items;
|
||||
}
|
||||
|
||||
fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call) []const TypeId {
|
||||
fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call, sel_author: ?*SelectedFunc) []const TypeId {
|
||||
// Method calls: obj.method(args) — resolve param types from the method signature,
|
||||
// skipping the first param (self) since it's prepended later.
|
||||
if (c.callee.data == .field_access) {
|
||||
@@ -12345,29 +12341,20 @@ pub const Lowering = struct {
|
||||
break :blk scoped;
|
||||
};
|
||||
|
||||
// fix-0102c F2: a genuine flat same-name collision must type this
|
||||
// call's args against the RESOLVED author's params, not the first-wins
|
||||
// winner's. Mirror the `lowerCall` routing one layer earlier so arg
|
||||
// lowering (implicit address-of, coercion) matches the author actually
|
||||
// called — otherwise a `*T`-param shadow gets a `T` value arg that is
|
||||
// later bit-cast to a pointer (segfault). Only a plain top-level
|
||||
// identifier with no scope-mangle / UFCS alias / local shadow routes
|
||||
// here; `.ambiguous` / `.none` fall to the existing first-wins path so
|
||||
// single-author / local / std resolution is byte-for-byte unchanged.
|
||||
if (std.mem.eql(u8, name, bare_name) and
|
||||
(if (self.scope) |scope| scope.lookup(bare_name) == null else true))
|
||||
{
|
||||
if (self.current_source_file) |caller_file| {
|
||||
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 => {},
|
||||
}
|
||||
}
|
||||
// fix-0102c F2 / R5 §C: a genuine flat same-name collision must type this
|
||||
// call's args against the author the call resolver selected, not the
|
||||
// first-wins winner's params. lowering consumes the ONE author verdict
|
||||
// (`selectedFreeAuthor`, computed once in `lowerCall`) rather than
|
||||
// re-resolving the name, so arg lowering (implicit address-of, coercion)
|
||||
// matches the author actually dispatched — otherwise a `*T`-param shadow
|
||||
// gets a `T` value arg that is later bit-cast to a pointer (segfault). The
|
||||
// FuncId materializes into the SHARED verdict (once), so dispatch reuses
|
||||
// it. A non-collision call falls to the existing first-wins path below,
|
||||
// byte-for-byte.
|
||||
if (sel_author) |sf| {
|
||||
const fid = self.selectedFuncId(sf, bare_name);
|
||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||
return self.userParamTypes(func);
|
||||
}
|
||||
|
||||
// Check declared functions
|
||||
|
||||
Reference in New Issue
Block a user