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:
@@ -267,6 +267,15 @@ Scheduler :: struct {
|
||||
print("sched: sleep() called outside a fiber (no running fiber)\n");
|
||||
abort();
|
||||
}
|
||||
// The virtual clock is MONOTONIC — it only advances as timers fire. A
|
||||
// negative duration would arm a deadline in the past, rewinding the
|
||||
// clock when it fired and breaking every ordering contract. Reject it
|
||||
// loudly rather than silently corrupting time. (`sleep(0)` is allowed: a
|
||||
// same-tick yield to the timer wheel.)
|
||||
if ms < 0 {
|
||||
print("sched: sleep({}) — negative duration would rewind the virtual clock\n", ms);
|
||||
abort();
|
||||
}
|
||||
t : Timer = .{ deadline_ms = self.clock_ms + ms, fiber = cur };
|
||||
// Long-lived-container rule: a timer outlives this `sleep` call's scope
|
||||
// (it survives in `self.timers` until the scheduler fires it), so grow
|
||||
@@ -731,6 +740,16 @@ go :: ufcs (self: *Scheduler, work: Closure() -> $R) -> *Task($R) {
|
||||
wait :: ufcs (t: *Task($R)) -> ($R, !TaskErr) {
|
||||
if t.canceled != 0 { raise error.Canceled; }
|
||||
if t.state == .pending {
|
||||
// ONE waiter per task (enforced). A `Task` holds a single `waiter` slot;
|
||||
// a second concurrent `wait` on the same pending task would OVERWRITE the
|
||||
// first, and completion would wake only the second — the first fiber
|
||||
// would stay suspended forever (silent deadlock). The M:1 model is
|
||||
// single-await per task; enforce it loudly (mirrors `block_on_fd`'s
|
||||
// one-waiter-per-fd guard). A multi-waiter task would need a waiter list.
|
||||
if t.waiter != null {
|
||||
print("sched: wait() — task already has a waiter (one awaiter per task in the M:1 model)\n");
|
||||
abort();
|
||||
}
|
||||
t.waiter = xx t.sched.current; // register self as the waiter
|
||||
t.sched.suspend_self(); // park until the task's fiber wakes us
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user