lang: generic $R type-arg resolution + receiver-driven ufcs overload (issues 0156, 0157)

0156 Part 1: a single-type generic $R (parsed as comptime_pack_ref)
used as a type-arg in a pack-fn body (Box($R), size_of(Box($R))) hit a
missing arm in resolveTypeWithBindings -> .unresolved -> LLVM panic.
Fix: mirror resolveTypeArg's comptime_pack_ref arm (look up
type_bindings, else a loud diagnostic). Regression: examples/generics/0216.
(Part 2 -- deferred .. spread crashes -- reframed OPEN/non-blocking.)

0157: a user generic ufcs method whose name collides with a stdlib
re-export resolved via last-wins fn_ast_map with no receiver filtering,
so the wrong overload won, $R never bound, and .unresolved reached LLVM.
Fix: selectUfcsGenericByReceiver enumerates all module authors, keeps
the receiver-binding ones, picks the most receiver-specific (concrete >
bare $T), dedups re-exports, and flags a genuine tie as a deterministic
'ambiguous -- qualify' diagnostic. Regression: examples/generics/0217.
This commit is contained in:
agra
2026-06-21 18:43:49 +03:00
parent b1e06f21e3
commit d3944570b9
13 changed files with 443 additions and 2 deletions

View File

@@ -853,6 +853,27 @@ pub const Lowering = struct {
}
return .unresolved;
}
// Bare `$<name>` in a type position. The parser tags EVERY `$name`
// expression as `comptime_pack_ref` — including a single-type generic
// binding (`$R: Type` in `Closure(..$args) -> $R`), which is NOT a
// value pack. Such a binding lives in `type_bindings`; resolve it the
// same way `resolveTypeArg` does (so `Box($R)` / `size_of(Box($R))` /
// a bare `-> $R` return inside a pack-fn mono resolve `$R` to its bound
// TypeId). Without this arm the node fell through to the catch-all
// `else` → `type_bridge` → `.unresolved` → an LLVM-emission panic
// (issue 0156). A name that is genuinely a value PACK (no single-type
// binding) used where one type is required is a real error — diagnose
// it, never silently fabricate a default type.
if (node.data == .comptime_pack_ref) {
const cpr = node.data.comptime_pack_ref;
if (self.type_bindings) |tb| {
if (tb.get(cpr.pack_name)) |ty| return ty;
}
if (self.diagnostics) |diags| {
diags.addFmt(.err, node.span, "pack '{s}' used where a single type is required", .{cpr.pack_name});
}
return .unresolved;
}
// `*Self` substitution inside runtime-class member declarations
// — both runtime and sx-defined — resolves to the class's own
// 0-field stub struct (i.e. the opaque Obj-C pointer type).
@@ -1854,6 +1875,8 @@ pub const Lowering = struct {
// --- moved to lower/call.zig (lower_call) ---
pub const CaptureInfo = lower_closure.CaptureInfo;
pub const lowerCall = lower_call.lowerCall;
pub const ufcsGenericBindsAll = lower_call.ufcsGenericBindsAll;
pub const selectUfcsGenericByReceiver = lower_call.selectUfcsGenericByReceiver;
pub const diagnoseMissingContext = lower_call.diagnoseMissingContext;
pub const allocViaContext = lower_call.allocViaContext;
pub const callExtern = lower_call.callExtern;

View File

@@ -26,6 +26,94 @@ const isPackFn = Lowering.isPackFn;
const headNameOfCallee = Lowering.headNameOfCallee;
const hasComptimeParams = Lowering.hasComptimeParams;
/// True iff every type-parameter of generic ufcs/free-fn `fd` binds to a
/// concrete (present) type given `args_ast` (receiver prepended). A param the
/// argument shapes can't pin is simply absent from the bindings map (e.g. a
/// `*Future($R)` receiver param against a `*Box(i64)` argument never binds `R`).
pub fn ufcsGenericBindsAll(self: *Lowering, fd: *const ast.FnDecl, args_ast: []const *const Node) bool {
var b = self.genericResolver().buildTypeBindings(fd, args_ast);
defer b.deinit();
for (fd.type_params) |tp| {
if (!b.contains(tp.name)) return false;
}
return true;
}
/// True if `fd`'s receiver param (`params[0]`) is a CONCRETE/structured type
/// (`*Task($R)`, `Box($R)`, `*Foo`, `[]T`, …) rather than a BARE type-parameter
/// receiver (`$T` / `T`) that matches ANY receiver. Used to prefer the more
/// receiver-specific overload when several same-named generic ufcs bind.
fn ufcsReceiverConcrete(fd: *const ast.FnDecl) bool {
if (fd.params.len == 0) return false;
const te = fd.params[0].type_expr;
const bare: ?[]const u8 = switch (te.data) {
.comptime_pack_ref => |c| c.pack_name,
.identifier => |id| id.name,
.type_expr => |t| t.name,
else => null, // pointer / parameterized / array / slice → concrete
};
if (bare) |nm| {
for (fd.type_params) |tp| {
if (std.mem.eql(u8, tp.name, nm)) return false; // bare `$T` receiver
}
}
return true;
}
/// issue 0157: a bare-ufcs name resolves through a single last-wins
/// `fn_ast_map` winner, which may be a same-named generic ufcs whose receiver
/// does NOT match the call's receiver (e.g. a user `cancel :: ufcs (t:
/// *Task($R))` shadowed by the stdlib re-export `cancel :: ufcs (f:
/// *Future($R))`). UFCS dispatch is RECEIVER-driven, so the right candidate may
/// live in a namespaced-imported module that is not flat-visible from the
/// caller file — enumerate ALL module authors of `name` (via `module_decls`)
/// and pick the generic ufcs whose receiver binds ALL its type-params for this
/// call. Called for EVERY generic-ufcs dispatch (not only on bind-failure), so
/// a fully-generic `(x: $T)` last-wins winner can't silently shadow a specific
/// `*Task($R)`. To stay DETERMINISTIC despite the hashmap iteration order (two
/// candidates can both bind): prefer the more receiver-SPECIFIC candidate
/// (concrete > bare-`$T`); dedup re-exports by fd identity; and if two DISTINCT
/// equally-specific authors both bind, set `ambiguous.*` (the caller emits a
/// "qualify the call" diagnostic) rather than silently picking one. Returns null
/// when none bind (a genuine "cannot infer", or the author isn't in
/// `module_decls` — the caller then falls back to the last-wins `fd0` if it
/// binds, else diagnoses; never monomorphizes an `.unresolved` into LLVM).
pub fn selectUfcsGenericByReceiver(self: *Lowering, name: []const u8, args_ast: []const *const Node, ambiguous: *bool) ?*const ast.FnDecl {
ambiguous.* = false;
const decls = self.program_index.module_decls orelse return null;
var best: ?*const ast.FnDecl = null;
var best_concrete = false;
var tie = false;
var it = decls.iterator();
while (it.next()) |entry| {
const ref = entry.value_ptr.names.get(name) orelse continue;
const fd = Lowering.fnDeclOfRaw(ref) orelse continue;
if (!(fd.type_params.len > 0 and fd.is_ufcs)) continue;
if (!self.ufcsGenericBindsAll(fd, args_ast)) continue;
const concrete = ufcsReceiverConcrete(fd);
if (best) |b| {
if (b == fd) continue; // same decl reached via a re-export — dedup
if (concrete and !best_concrete) {
best = fd;
best_concrete = true;
tie = false; // a strictly more specific candidate wins outright
} else if (concrete == best_concrete) {
tie = true; // two distinct equally-specific authors → ambiguous
}
// else: fd is strictly less specific than best → ignore
} else {
best = fd;
best_concrete = concrete;
}
}
if (best == null) return null;
if (tie) {
ambiguous.* = true;
return null;
}
return best;
}
pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
var c = c_in;
// A bare reserved-type-name spelling in call position parses as a
@@ -1054,12 +1142,42 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
}
// Generic ufcs target: monomorphize with the receiver's AST
// node prepended so bindings align with fd.params[0].
if (ufcs_fd) |fd| {
if (fd.type_params.len > 0) {
if (ufcs_fd) |fd0| {
if (fd0.type_params.len > 0) {
var eff_args = std.ArrayList(*const Node).empty;
defer eff_args.deinit(self.alloc);
eff_args.append(self.alloc, effective_obj_node) catch unreachable;
for (c.args) |arg| eff_args.append(self.alloc, arg) catch unreachable;
// issue 0157: the last-wins `fn_ast_map` winner may be a
// same-named generic ufcs from another module whose
// receiver doesn't match. Only when it fails to bind all
// its type-params for THIS receiver do we re-select the
// receiver-matching author — so a working call is never
// perturbed; the previously-panicking path either finds
// the right candidate or emits a clean diagnostic
// (never an `.unresolved` reaching codegen).
// Always resolve the receiver-specific author (not just
// on bind-failure): a fully-generic `(x: $T)` last-wins
// winner BINDS for any receiver, so a failure-gated
// re-select would silently keep it over a more specific
// `*Task($R)` — order-dependent dispatch. `selectUfcsGenericByReceiver`
// picks the most specific binder (or flags a genuine
// tie). Fall back to `fd0` only when it isn't enumerable
// in `module_decls` but still binds; diagnose otherwise
// (never monomorphize an `.unresolved` into LLVM).
var fd = fd0;
var amb = false;
if (self.selectUfcsGenericByReceiver(eff_field, eff_args.items, &amb)) |sel| {
fd = sel;
} else if (amb) {
if (self.diagnostics) |d|
d.addFmt(.err, c.callee.span, "ambiguous ufcs call '{s}': multiple overloads' receivers match — qualify the call", .{eff_field});
return Ref.none;
} else if (!self.ufcsGenericBindsAll(fd0, eff_args.items)) {
if (self.diagnostics) |d|
d.addFmt(.err, c.callee.span, "cannot infer generic type parameter for ufcs call '{s}' (no visible overload's receiver matches)", .{eff_field});
return Ref.none;
}
var gbindings = self.genericResolver().buildTypeBindings(fd, eff_args.items);
defer gbindings.deinit();
const gmangled = self.genericResolver().mangleGenericName(eff_field, fd, &gbindings);