fibers: address adversarial review of the B1 changes (6 findings)
UFCS generic overload resolution (issue 0157 follow-ups): - P1-a: call planning (calls.zig) used the last-wins fn_ast_map winner while lowering reselected by receiver, so the planned result type could disagree with the dispatched function and misbox the result. Both now share selectUfcsGenericByReceiver(.., fd0). - P1-b: selection scanned module_decls globally, flagging a transitively-hidden same-named overload as a false ambiguity. Now two-tier: directly-visible authors first (ambiguity only among those), global fallback for receiver-reachable namespaced methods (e.g. Task.cancel) that defers to fd0 on a hidden tie. - P2-b: boolean specificity tied *$T with *Box($T). Now peels pointer layers so the structurally-narrower receiver wins. Scheduler (sched.sx): - P1-c: a second concurrent Task.wait overwrote the single waiter slot -> silent deadlock. Now one-awaiter-per-task loud abort. - P2-c: sleep(negative) rewound the monotonic virtual clock. Rejected loudly. (P2-a, non-generic-winner-hides-generic, did not reproduce -- the non-generic arm already falls through.) Regressions: examples/generics/0218 (receiver specificity + plan/lowering agreement), examples/concurrency/1818 (negative-sleep abort), 1819 (double-wait abort). Suite green 758/0.
This commit is contained in:
@@ -316,12 +316,27 @@ pub const CallResolver = struct {
|
||||
// Generic ufcs target: infer the return type with the
|
||||
// RECEIVER prepended so binding positions align with
|
||||
// fd.params[0] (mirrors the lowering side's eff_args).
|
||||
if (ufcs_fd) |fd| {
|
||||
if (fd.type_params.len > 0) {
|
||||
if (ufcs_fd) |fd0p| {
|
||||
if (fd0p.type_params.len > 0) {
|
||||
const eff_call_args = self.l.alloc.alloc(*ast.Node, c.args.len + 1) catch
|
||||
return .{ .kind = .unresolved, .return_type = .unresolved };
|
||||
eff_call_args[0] = cfa.object;
|
||||
@memcpy(eff_call_args[1..], c.args);
|
||||
// RESELECT by receiver — the same selector + `fd0`
|
||||
// lowering uses — so the PLANNED return type matches the
|
||||
// function lowering actually dispatches. The last-wins
|
||||
// `fd0p` can be a wrong-receiver overload (e.g. a
|
||||
// `*Other($T)->string` winner over the `*Box($T)->i64`
|
||||
// receiver-match); typing the call by `fd0p` while
|
||||
// lowering calls the other one misboxes the result
|
||||
// (issue 0157 review P1). A `*const Node` view of the
|
||||
// args drives the receiver-aware selection.
|
||||
const sel_args = self.l.alloc.alloc(*const ast.Node, c.args.len + 1) catch
|
||||
return .{ .kind = .unresolved, .return_type = .unresolved };
|
||||
sel_args[0] = cfa.object;
|
||||
for (c.args, 0..) |a, i| sel_args[i + 1] = a;
|
||||
var amb = false;
|
||||
const fd = self.l.selectUfcsGenericByReceiver(eff_field, sel_args, &amb, fd0p) orelse fd0p;
|
||||
var c2 = c.*;
|
||||
c2.args = eff_call_args;
|
||||
return .{
|
||||
|
||||
@@ -41,16 +41,21 @@ pub fn ufcsGenericBindsAll(self: *Lowering, fd: *const ast.FnDecl, args_ast: []c
|
||||
|
||||
/// 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.
|
||||
/// receiver (`$T` / `T` / `*$T`) that matches ANY receiver. Used to prefer the
|
||||
/// more receiver-specific overload when several same-named generic ufcs bind.
|
||||
/// POINTER LAYERS ARE PEELED: a receiver's specificity is its core type, not
|
||||
/// the `*` wrapper — `*Box($T)` (core `Box`, concrete) is strictly more specific
|
||||
/// than `*$T` (core `$T`, bare), so the structurally-narrower overload wins
|
||||
/// instead of tying.
|
||||
fn ufcsReceiverConcrete(fd: *const ast.FnDecl) bool {
|
||||
if (fd.params.len == 0) return false;
|
||||
const te = fd.params[0].type_expr;
|
||||
var te = fd.params[0].type_expr;
|
||||
while (te.data == .pointer_type_expr) te = te.data.pointer_type_expr.pointee_type;
|
||||
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
|
||||
else => null, // parameterized (Box($T)) / array / slice → concrete
|
||||
};
|
||||
if (bare) |nm| {
|
||||
for (fd.type_params) |tp| {
|
||||
@@ -60,26 +65,77 @@ fn ufcsReceiverConcrete(fd: *const ast.FnDecl) bool {
|
||||
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 {
|
||||
/// Rank one candidate `maybe_fd` into the running (best, best_concrete, tie)
|
||||
/// selection state: skip non-(generic ufcs) and non-binding candidates; a
|
||||
/// strictly more receiver-specific candidate wins outright; two distinct
|
||||
/// equally-specific binders set `tie`; a less-specific one is ignored. Re-export
|
||||
/// aliases (same `*FnDecl` reached twice) are deduped by identity.
|
||||
fn rankUfcsCand(self: *Lowering, maybe_fd: ?*const ast.FnDecl, args_ast: []const *const Node, best: *?*const ast.FnDecl, best_concrete: *bool, tie: *bool) void {
|
||||
const fd = maybe_fd orelse return;
|
||||
if (!(fd.type_params.len > 0 and fd.is_ufcs)) return;
|
||||
if (!self.ufcsGenericBindsAll(fd, args_ast)) return;
|
||||
const concrete = ufcsReceiverConcrete(fd);
|
||||
if (best.*) |b| {
|
||||
if (b == fd) return; // same decl via a re-export — dedup
|
||||
if (concrete and !best_concrete.*) {
|
||||
best.* = fd;
|
||||
best_concrete.* = true;
|
||||
tie.* = false; // strictly more specific wins outright
|
||||
} else if (concrete == best_concrete.*) {
|
||||
tie.* = true; // two distinct equally-specific binders
|
||||
}
|
||||
// else: strictly less specific → ignore
|
||||
} else {
|
||||
best.* = fd;
|
||||
best_concrete.* = concrete;
|
||||
}
|
||||
}
|
||||
|
||||
/// issue 0157 + review follow-ups: a bare-ufcs name resolves through a single
|
||||
/// last-wins `fn_ast_map` winner (`fd0`), which may be a same-named generic ufcs
|
||||
/// whose receiver does NOT match the call's receiver (e.g. user `cancel :: ufcs
|
||||
/// (t: *Task($R))` shadowed by the stdlib `cancel :: ufcs (f: *Future($R))`).
|
||||
/// Pick the most receiver-specific BINDING author, in two tiers so visibility is
|
||||
/// respected (the non-transitive import model) without losing receiver-reachable
|
||||
/// namespaced methods:
|
||||
///
|
||||
/// Tier 1 — DIRECTLY-VISIBLE authors (own + one-hop flat, via
|
||||
/// `collectVisibleAuthors`). A genuine user-facing ambiguity (two distinct
|
||||
/// equally-specific VISIBLE binders) sets `ambiguous.*` here.
|
||||
/// Tier 2 (only if no visible author binds) — receiver-reachable methods that
|
||||
/// aren't flat-visible (a `*Task($R)` method reached through a `sched ::
|
||||
/// #import` namespace). Scan all module authors for the unique most-specific
|
||||
/// binder; on a tie among non-visible binders DON'T cry ambiguous — defer to
|
||||
/// `fd0` (the global last-wins) so a transitively-hidden collision never
|
||||
/// surfaces as a false error.
|
||||
///
|
||||
/// MUST be called identically from call planning (`calls.zig`) and lowering so
|
||||
/// the planned result type and the dispatched function never disagree (which
|
||||
/// would misbox the result). Returns null when nothing binds (the caller falls
|
||||
/// back to `fd0` if it binds, else diagnoses — never monomorphizes `.unresolved`).
|
||||
pub fn selectUfcsGenericByReceiver(self: *Lowering, name: []const u8, args_ast: []const *const Node, ambiguous: *bool, fd0: ?*const ast.FnDecl) ?*const ast.FnDecl {
|
||||
ambiguous.* = false;
|
||||
// Tier 1: directly-visible authors. Ambiguity is a user-facing error only here.
|
||||
if (self.current_source_file) |caller_file| {
|
||||
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);
|
||||
var best: ?*const ast.FnDecl = null;
|
||||
var best_concrete = false;
|
||||
var tie = false;
|
||||
if (set.own) |own| rankUfcsCand(self, Lowering.fnDeclOfRaw(own.raw), args_ast, &best, &best_concrete, &tie);
|
||||
for (set.flat) |fa| rankUfcsCand(self, Lowering.fnDeclOfRaw(fa.raw), args_ast, &best, &best_concrete, &tie);
|
||||
if (best) |b| {
|
||||
if (tie) {
|
||||
ambiguous.* = true;
|
||||
return null;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
}
|
||||
// Tier 2: receiver-reachable but not flat-visible (namespaced methods defined
|
||||
// alongside the receiver type). Pick the unique most-specific binder; on a
|
||||
// hidden tie defer to `fd0` rather than reporting a false ambiguity.
|
||||
const decls = self.program_index.module_decls orelse return null;
|
||||
var best: ?*const ast.FnDecl = null;
|
||||
var best_concrete = false;
|
||||
@@ -87,28 +143,13 @@ pub fn selectUfcsGenericByReceiver(self: *Lowering, name: []const u8, args_ast:
|
||||
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;
|
||||
}
|
||||
rankUfcsCand(self, Lowering.fnDeclOfRaw(ref), args_ast, &best, &best_concrete, &tie);
|
||||
}
|
||||
if (best == null) return null;
|
||||
if (tie) {
|
||||
ambiguous.* = true;
|
||||
if (fd0) |w| {
|
||||
if (self.ufcsGenericBindsAll(w, args_ast)) return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return best;
|
||||
@@ -1167,7 +1208,7 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
||||
// (never monomorphize an `.unresolved` into LLVM).
|
||||
var fd = fd0;
|
||||
var amb = false;
|
||||
if (self.selectUfcsGenericByReceiver(eff_field, eff_args.items, &amb)) |sel| {
|
||||
if (self.selectUfcsGenericByReceiver(eff_field, eff_args.items, &amb, fd0)) |sel| {
|
||||
fd = sel;
|
||||
} else if (amb) {
|
||||
if (self.diagnostics) |d|
|
||||
|
||||
Reference in New Issue
Block a user