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 } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user