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