Skip to main content

nova_runtime/
task.rs

1//! A single-threaded cooperative executor.
2//!
3//! **Single-threaded is a correctness requirement, not a simplification.**
4//! The collector keeps its entire heap in a `thread_local!` (`gc`'s `HEAP`),
5//! so an object allocated on one thread lives in that thread's heap map and
6//! only that thread's collector can see or free it -- `gc::add_root` and
7//! `gc::remove_root` are `thread_local!` for the identical reason (see their
8//! doc comments in `gc.rs`). A second thread running Nova code would free
9//! objects the first still holds. This executor's own state (`QUEUE`,
10//! `TASKS`, `IN_BLOCK_ON` below) is therefore `thread_local!` too, by
11//! construction rather than by convention: there is no API here for a task
12//! to migrate between threads. See ADR 0009.
13//!
14//! No wakers: a task that returns [`POLL_PENDING`] is re-queued for another
15//! turn unless it staged a park (see [`Wait`]), in which case it waits in the
16//! park set instead until a deadline or another task's completion -- the
17//! executor's only two wake sources -- moves it back. Both are scheduled by
18//! the executor itself, not registered as an arbitrary callback the awaited
19//! resource invokes, which is what "no wakers" still means once parking
20//! exists. That makes interleaving between ready tasks deterministic by
21//! construction, which is what lets [`nova_rt_task_block_on`]'s round-robin
22//! order be pinned by a test rather than merely observed.
23
24use crate::gc;
25use crate::poll::{Interest, RawSocket};
26use std::cell::{Cell, RefCell};
27use std::collections::{HashMap, VecDeque};
28use std::time::{Duration, Instant};
29
30/// The signature every compiled `async fn`'s poll function has -- both the
31/// hand-written ones this module's tests drive it with, and the ones
32/// `async_lower.rs` (Task 5) generates. Reads and writes `state`'s slots (see
33/// [`STATE_SLOT_TAG`] and friends) and reports [`POLL_PENDING`] or
34/// [`POLL_READY`].
35///
36/// `task_ctx` is unused in this phase, and every call site in this module
37/// passes null. The parameter exists now, rather than being added later,
38/// because a future phase (channels, timers) needs a task to be able to park
39/// against an external event instead of being busy-repolled, and adding a
40/// parameter afterward would mean changing every already-compiled poll
41/// function's signature.
42///
43/// `"C-unwind"`, not plain `"C"`: a panic cannot cross a plain `"C"`
44/// function's boundary at all -- it aborts the whole process on the spot,
45/// regardless of the workspace's unwind panic strategy. Some of this module's
46/// diagnostics are panics that a caller is meant to be able to observe --
47/// [`nova_rt_task_take_output`]'s take-once check, and [`poll_one`]'s
48/// rejection of a poll status the ABI does not define -- and each has to leave
49/// the `#[no_mangle]` entry point it starts under. Declaring the ABI
50/// `"C-unwind"` permits that, and every entry point here (this indirect call
51/// included) shares it so this module has one ABI rather than two. It changes
52/// nothing else: `"C-unwind"` was stabilized in Rust 1.71 (under this
53/// workspace's 1.78 MSRV) and marshals parameters and return values
54/// identically to `"C"`, so a generated call site is byte-identical either way.
55///
56/// **Constraint on generated poll functions: a panic must not cross their
57/// boundary.** `"C-unwind"` says an unwind *may* pass through without
58/// aborting; it does not make a frame able to survive one. A poll function
59/// emitted by Cranelift or LLVM has no landing pads and no drop glue, so an
60/// unwind through it would skip whatever cleanup its frame ought to do and
61/// leave the executor's `QUEUE`/`TASKS` describing a task that is neither
62/// running nor finished. Anything in a generated poll function that could
63/// panic must therefore either be provably unable to (`nova_rt_check_bounds`
64/// and `nova_rt_panic_str` both `abort`, not unwind) or catch it before
65/// returning. Every diagnostic in this module that a *well-formed compiled
66/// Nova program* can reach therefore goes through [`abort_with`] instead of
67/// panicking; the panics named above are reachable only from a Rust caller or
68/// from a compiler bug (see each one's own doc comment). The
69/// permission this ABI grants exists for the Rust-side entry points below, not
70/// for compiled Nova frames.
71pub type PollFn = unsafe extern "C-unwind" fn(state: *mut u8, task_ctx: *mut u8) -> i64;
72
73/// Report a violated contract and end the process, in the same style and with
74/// the same `nova: panic:` prefix as `nova_rt_panic_str` and
75/// `nova_rt_check_bounds`.
76///
77/// Aborting rather than panicking is what makes a check callable from compiled
78/// Nova code: a generated frame has no landing pads and no drop glue, and no
79/// unwind table describing it, so an unwind that started under one and passed
80/// through it would have to be resolved by an unwinder that has no description
81/// of that frame (see [`PollFn`]'s doc comment).
82///
83/// `pub(crate)`, not private: `bytes`'s `nova_rt_bytes_at` and
84/// `nova_rt_bytes_from_ints` (Task 3) call this too, for the same reason this
85/// module does -- both are reachable from inside a generated poll frame, so a
86/// rejected caller error must abort rather than unwind. This is still the
87/// *only* place that prints the `nova: panic:` marker and aborts on this path
88/// (ADR 0008's emitter inventory); a second helper doing the same thing would
89/// be a fifth emitter to track there, whereas a second module merely calling
90/// this one is not.
91pub(crate) fn abort_with(msg: &str) -> ! {
92    eprintln!("nova: panic: {msg}");
93    std::process::abort();
94}
95
96/// A [`PollFn`] returns this when it has not yet produced a value; the
97/// executor re-queues the task for another turn.
98pub const POLL_PENDING: i64 = 0;
99/// A [`PollFn`] returns this once it has written the final value to the
100/// state object's output slot ([`STATE_SLOT_OUTPUT`]).
101pub const POLL_READY: i64 = 1;
102
103/// The state object's layout, word-indexed: slot `n` is at byte offset
104/// `8 * n`. `async_lower.rs` (Task 5) must build exactly this layout -- this
105/// executor and every generated poll function address it by raw offset, with
106/// no shared Rust type to keep the two ends in sync if one of them drifts.
107///
108/// - [`STATE_SLOT_TAG`] (offset 0): the resume tag. A poll function's entry
109///   block switches on this to pick up wherever it last suspended.
110/// - [`STATE_SLOT_OUTPUT`] (offset 8): the output value, written exactly
111///   once, by the call that returns [`POLL_READY`].
112/// - [`STATE_SLOT_TEMPS`] (offset 16) and up: one slot per MIR temp in the
113///   `async fn` body, unconditionally -- every local is spilled here whether
114///   or not it is live across a suspend point, so no local is ever live in a
115///   register or on the stack across one.
116pub const STATE_SLOT_TAG: usize = 0;
117pub const STATE_SLOT_OUTPUT: usize = 1;
118pub const STATE_SLOT_TEMPS: usize = 2;
119
120/// The future value's own layout -- the `{ poll_code, state }` two-word fat
121/// pointer every `nova_rt_task_spawn` / `nova_rt_task_block_on` argument
122/// points at, the same shape `MakeClosure` builds for a closure. Named for
123/// the same reason [`STATE_SLOT_TAG`] and friends are: `async_lower.rs`
124/// (Task 5) builds this layout independently and the two ends are kept in
125/// step by nothing but these declared offsets.
126///
127/// - [`FUTURE_SLOT_POLL`] (offset 0): the [`PollFn`]'s address.
128/// - [`FUTURE_SLOT_STATE`] (offset 8): the state object's address.
129pub const FUTURE_SLOT_POLL: usize = 0;
130pub const FUTURE_SLOT_STATE: usize = 1;
131/// The whole future value's size in bytes, for the allocation that holds it.
132pub const FUTURE_SIZE: usize = 16;
133
134/// The smallest state object any future may have: the tag and output slots
135/// are read and written unconditionally, for every future, including one
136/// whose `async fn` returns unit and has no temps at all. A state object
137/// with `n` MIR temps is `(STATE_SLOT_TEMPS + n) * 8` bytes, which is never
138/// smaller than this.
139pub const STATE_MIN_SIZE: usize = (STATE_SLOT_TEMPS) * 8;
140
141/// One task known to this thread's executor.
142struct Task {
143    poll: PollFn,
144    state: *mut u8,
145    done: bool,
146    output: i64,
147    /// Whether this task's GC root has already been released, by either
148    /// [`take_output_internal`] or [`release_internal`]. The latter runs from
149    /// [`poll_one`] at completion, for every task but `block_on`'s root, and
150    /// from [`nova_rt_task_release`]. The root is one
151    /// `gc::add_root`, so it must be cancelled at most once; this flag is what
152    /// both paths check, which is also what makes `release_internal`
153    /// idempotent.
154    taken: bool,
155    /// Whether this task keeps its GC root past completion, until
156    /// [`take_output_internal`] hands its output back. Set only by
157    /// [`spawn_root_internal`], for `block_on`'s root task -- the one task
158    /// whose output is read out of `Task::output` rather than through its
159    /// future. Every other task has its root released in [`poll_one`] the
160    /// moment it completes.
161    keep_root_until_taken: bool,
162}
163
164/// Why a parked task is waiting, and therefore what wakes it.
165///
166/// `Copy`, and staged through a `Cell` rather than a `RefCell`, deliberately:
167/// it is written from inside a poll frame, and a `RefCell` borrow panic there
168/// would unwind through generated code that has no landing pads (see
169/// [`PollFn`]'s doc comment).
170#[derive(Copy, Clone, Debug, PartialEq, Eq)]
171enum Wait {
172    /// Wake once the clock reaches this instant.
173    Deadline(Instant),
174    /// Wake once the task with this id completes, or once `deadline` passes.
175    ///
176    /// The deadline rides inside this variant for the same reason it rides
177    /// inside [`Wait::Io`]: one task must have exactly one `PARKED` entry, or
178    /// every wake path has to remember to remove two.
179    Task { id: i64, deadline: Option<Instant> },
180    /// Wake once `socket` is ready for `interest`, or `deadline` passes.
181    ///
182    /// The deadline rides inside this variant rather than being parked as a
183    /// second entry: one task must have exactly one `PARKED` entry, or every
184    /// wake path has to remember to remove two.
185    Io {
186        socket: RawSocket,
187        interest: Interest,
188        deadline: Option<Instant>,
189    },
190}
191
192thread_local! {
193    /// Ids ready for another turn, FIFO: pushed at the back, popped from the
194    /// front, so a re-queued task's next poll always waits behind every
195    /// other task that was already waiting -- round-robin, not
196    /// last-in-first-out.
197    static QUEUE: RefCell<VecDeque<i64>> = const { RefCell::new(VecDeque::new()) };
198    /// Every task this thread has ever spawned, indexed by id (its index at
199    /// spawn time). Entries are never removed: a finished task stays here,
200    /// `done` and holding its output copy, since `nova_rt_task_is_done` can
201    /// be asked about it an arbitrary time later, [`output_bits`] reads the
202    /// copy, and `run_to_completion` takes it for `block_on`'s root. A
203    /// plain `Vec<Task>`, not `Vec<Option<Task>>`: nothing ever vacates a
204    /// slot, so an `Option` here would add an arm no code path can reach.
205    /// An out-of-range id is still rejected, by `Vec::get` returning `None`.
206    static TASKS: RefCell<Vec<Task>> = const { RefCell::new(Vec::new()) };
207    /// State address to task id, for the two entry points Nova calls.
208    ///
209    /// The executor's own identity is still the `TASKS` index -- `poll_one`
210    /// and `run_to_completion` address tasks by it. This map exists because
211    /// the *Nova-facing* boundary must not be a forgeable integer: a
212    /// `JoinHandle` is constructible (Nova has no field privacy), so an
213    /// `Int` id in it can name a task the caller never spawned. A state
214    /// address cannot be fabricated -- obtaining one requires a real future,
215    /// which only calling an `async fn` produces.
216    ///
217    /// **An entry lasts exactly as long as the object its key names.** An
218    /// address is not a durable identity for a heap object (`gc.rs`'s module
219    /// doc comment states that property and owns it), so a key that outlived
220    /// its state object would go on naming the old task for whatever
221    /// unrelated object the collector next placed at that address, and the
222    /// two reads below would answer about that dead task instead of rejecting
223    /// a future the executor never saw. `spawn_internal` inserts;
224    /// [`prune_freed_states`] removes, at the moment the address stops
225    /// meaning anything.
226    ///
227    /// Two properties follow, and both are load-bearing elsewhere in this
228    /// module. A key is present only for a state object that is still the
229    /// allocation it was spawned with, so the reads need no staleness test of
230    /// their own (see [`task_id_of`]) and `spawn_internal`'s check is about
231    /// something else entirely (see its own comment). And a state a caller
232    /// can still reach is never swept, so it is never pruned -- which is what
233    /// makes a second `join` on a handle that still holds its future resolve
234    /// to the same task as the first.
235    static BY_STATE: RefCell<HashMap<usize, i64>> = RefCell::new(HashMap::new());
236    /// Set for the duration of one `nova_rt_task_block_on` call, so a nested
237    /// call (a poll function calling `block_on` again) can be diagnosed
238    /// instead of running a second executor loop from inside the first
239    /// one's frame and corrupting the shared queue.
240    static IN_BLOCK_ON: Cell<bool> = const { Cell::new(false) };
241    /// Tasks that are waiting on something, and what for. **Disjoint from
242    /// `QUEUE` by construction:** an entry arrives only by `poll_one` declining
243    /// to re-queue, and leaves only by being pushed back onto `QUEUE`. Keyed on
244    /// the task id -- this executor's own private identity -- and deliberately
245    /// not on a state address: `BY_STATE` needs pruning at the GC sweep only
246    /// because its keys are heap addresses a Nova value names, and a second
247    /// address-keyed map would inherit that hazard for nothing.
248    static PARKED: RefCell<Vec<(i64, Wait)>> = const { RefCell::new(Vec::new()) };
249    /// The task `poll_one` is polling right now, so a park staged from inside
250    /// that poll knows whose it is without `task_ctx` having to carry it.
251    static CURRENT: Cell<Option<i64>> = const { Cell::new(None) };
252    /// What the poll in progress has staged so far -- at most one deadline
253    /// and at most one I/O wait (see [`Staged`]), folded by
254    /// [`staged_to_wait`] into the single [`Wait`] `poll_one` reads once the
255    /// status is known: committed on `POLL_PENDING`, discarded on
256    /// `POLL_READY`.
257    static PENDING_PARK: Cell<Staged> = const {
258        Cell::new(Staged {
259            deadline: None,
260            io: None,
261            task: None,
262        })
263    };
264}
265
266/// The task currently being polled, or `None` outside a poll.
267///
268/// `poll_one` sets this to `Some(id)` immediately before calling that task's
269/// poll function, and back to `None` immediately after it returns -- scoped
270/// to exactly the duration of that one `poll` call -- so a runtime intrinsic
271/// reached from generated code can key per-task storage on it. `fs.rs` is the
272/// first consumer.
273pub(crate) fn current_task() -> Option<i64> {
274    CURRENT.with(|c| c.get())
275}
276
277/// Set [`CURRENT`] directly, for tests that need a task context without an
278/// executor.
279///
280/// Test-only because nothing in production may set `CURRENT` outside
281/// `poll_one`. Gated `#[cfg(test)]` only: its callers are `fs.rs`'s tests,
282/// which run on every platform.
283#[cfg(test)]
284pub(crate) fn set_current_for_test(id: Option<i64>) {
285    CURRENT.with(|c| c.set(id));
286}
287
288/// Test-only: the I/O half of whatever this thread's current poll has staged
289/// so far in [`PENDING_PARK`] -- the socket, its interest, and the deadline
290/// (if any) riding alongside it -- without exposing [`Wait`] or [`Staged`]
291/// themselves. `None` if nothing with a socket is staged (nothing at all, or
292/// only a bare `Wait::Task`/`Wait::Deadline`, neither of which has one).
293///
294/// `net.rs`'s own tests need this to assert that a future's would-block
295/// branch genuinely staged a park with the *expected* socket and interest --
296/// not merely that `POLL_PENDING` came back, which a future that stages
297/// nothing at all and busy-spins through `QUEUE` would also return. Reading
298/// [`PENDING_PARK`] here does not widen what `net.rs` can construct or match:
299/// `Wait` and `stage_park` stay private to this module, and this hands back
300/// plain data extracted from `Staged`, which is a different, already-`Copy`
301/// type nothing outside this module can build a fake instance of.
302#[cfg(test)]
303pub(crate) fn staged_io_for_test() -> Option<(RawSocket, Interest, Option<Instant>)> {
304    PENDING_PARK.with(|cell| {
305        let staged = cell.get();
306        staged
307            .io
308            .map(|(socket, interest)| (socket, interest, staged.deadline))
309    })
310}
311
312/// The deadline currently staged by the poll in progress, if any.
313///
314/// The deadline counterpart of [`staged_io_for_test`], and it exists for the
315/// same reason: an outcome test cannot tell a park from a busy spin, because
316/// both complete. Before this, nothing could observe a staged deadline at all
317/// -- so a `sleep` whose unit conversion was wrong by a factor of a million
318/// still passed `tests/runtime/task_sleep_order.nova`, which only pins the
319/// order of a 200 and a 20.
320#[cfg(test)]
321pub(crate) fn staged_deadline_for_test() -> Option<Instant> {
322    PENDING_PARK.with(|cell| cell.get().deadline)
323}
324
325/// The task wait staged by the poll in progress, if any -- the third and last
326/// of [`Staged`]'s fields, completing the set with [`staged_io_for_test`] and
327/// [`staged_deadline_for_test`].
328///
329/// Added because with only two of the three readable, no test could assert
330/// that the slot is **empty**, and "empty" is exactly the postcondition
331/// [`poll_timeout`]'s abandonment path has to establish: an abandoned inner's
332/// leftover [`Wait::Task`] was invisible to every accessor that existed, which
333/// is why a whole class of two-parks-in-one-poll aborts shipped green. An
334/// existence check on the other two fields cannot substitute -- the leftover
335/// this closes is a `task`, and a `Deadline` merges rather than colliding, so
336/// `task` is the one field whose staleness is both invisible and fatal.
337#[cfg(test)]
338pub(crate) fn staged_task_for_test() -> Option<i64> {
339    PENDING_PARK.with(|cell| cell.get().task)
340}
341
342/// Drop every [`BY_STATE`] key whose state object the collector has just
343/// freed. `is_live` holds for exactly the start addresses of objects that
344/// survived the collection.
345///
346/// The removal half of that map's own invariant, and the reason `gc.rs` calls
347/// in here rather than this module watching for it: a freed address can be
348/// reissued for an unrelated object (`gc.rs`'s module doc comment), and a
349/// collection is where that transition happens. It calls this once, after it
350/// has freed and before anything can be allocated again, so a key cannot
351/// start naming the wrong thing.
352///
353/// **Removal only.** Nothing here inserts a key, and nothing here touches
354/// `TASKS`, so this cannot introduce a key whose id `TASKS` does not have --
355/// `spawn_internal` is still the only thing that puts a key in this map or an
356/// entry in `TASKS`, and it takes the id from `TASKS` itself.
357///
358/// Costs one predicate call per key, which is per live task, not per freed
359/// object.
360pub(crate) fn prune_freed_states(is_live: &dyn Fn(usize) -> bool) {
361    BY_STATE.with(|m| m.borrow_mut().retain(|&addr, _| is_live(addr)));
362}
363
364/// Read the `{ poll_code, state }` fat pointer the compiler builds for a
365/// `Future` value (the same two-word shape `MakeClosure` builds for a
366/// closure).
367///
368/// # Safety
369/// `future` must point to a live, readable [`FUTURE_SIZE`]-byte block:
370/// [`FUTURE_SLOT_POLL`] a [`PollFn`] (as a bit pattern), [`FUTURE_SLOT_STATE`]
371/// the address of a state object of at least [`STATE_MIN_SIZE`] bytes (see
372/// [`nova_rt_task_spawn`]'s contract for the full requirement).
373unsafe fn read_future(future: *mut u8) -> (PollFn, *mut u8) {
374    let words = future as *mut usize;
375    // SAFETY: `future` is a valid `FUTURE_SIZE`-byte block per this
376    // function's contract.
377    let poll_code = unsafe { words.add(FUTURE_SLOT_POLL).read() };
378    // SAFETY: same block, second word.
379    let state = unsafe { words.add(FUTURE_SLOT_STATE).read() } as *mut u8;
380    // SAFETY: `poll_code` is a `PollFn` bit pattern by `future`'s own
381    // contract above; a function pointer and a `usize` are both
382    // pointer-width, so the transmute cannot change size. The load-bearing
383    // half of that contract is *non-nullability*: `PollFn` is a bare `fn`
384    // pointer, not `Option<fn>`, so it has a niche at zero and transmuting a
385    // zero word here is instantly undefined behaviour rather than a pointer
386    // that merely faults when called. A future value whose first word is
387    // still zero is exactly what a Task 5/6 bug that allocates the fat
388    // pointer but never writes `poll_code` produces, and `gc::alloc` hands
389    // back zeroed memory -- so this precondition is on the caller, and there
390    // is no check here that could recover from its violation.
391    let poll: PollFn = unsafe { std::mem::transmute::<usize, PollFn>(poll_code) };
392    (poll, state)
393}
394
395/// Register `future` as a new task: read its fat pointer, root its state
396/// object, enqueue it, and return its id.
397///
398/// The `gc::add_root` here is paired with exactly one `gc::remove_root`,
399/// through [`release_internal`] or [`take_output_internal`], whichever runs
400/// first. For an ordinary task that is [`poll_one`] releasing it at
401/// completion; for `block_on`'s root ([`spawn_root_internal`]) it is
402/// `run_to_completion`'s take. This module owns that policy; `gc.rs` states
403/// only the registry's own multiset contract and deliberately not who pairs
404/// it or when.
405///
406/// # Safety
407/// `future` must be a valid future fat pointer (see [`read_future`]).
408unsafe fn spawn_internal(future: *mut u8) -> i64 {
409    // SAFETY: forwarding this function's own contract.
410    let (poll, state) = unsafe { read_future(future) };
411    // Two LIVE tasks sharing one state object would make this map ambiguous,
412    // so the lookup forces a decision here. A key exists only for a state
413    // object that is still the allocation it was spawned with (`BY_STATE`'s
414    // doc comment), so the question this hit asks is not "has this address
415    // been used before" but "is this the same future again".
416    //
417    // Answered on liveness rather than on presence, because re-spawning a
418    // future whose task has been *released* is deliberately allowed. It could
419    // be refused: "released but still held" and "freed and recycled" are
420    // distinguishable here, since the second no longer reaches this branch at
421    // all. But a released future its caller still holds is the
422    // `spawn(h.fut)`-after-`join` shape, and Nova has no move checking, so
423    // `join` cannot consume the handle that makes it expressible. It is
424    // accepted for the same reason the double-await footgun is (ADR 0009):
425    // the second spawn re-polls a completed state machine from its last
426    // suspend point, re-running the body after its final await, which is a
427    // wrong answer confined to the caller that asked for it rather than
428    // corruption of a task that did nothing. What must stay rejected is two
429    // live tasks driving one state object, and `Task::taken` is exactly that
430    // distinction: it is set only where the executor gives up its GC root, so
431    // an unset flag means this state is still a task the executor is driving.
432    if let Some(prior) = BY_STATE.with(|m| m.borrow().get(&(state as usize)).copied()) {
433        let still_live = TASKS.with(|tasks| {
434            !tasks
435                .borrow()
436                .get(prior as usize)
437                .expect("BY_STATE named a task id that TASKS does not have")
438                .taken
439        });
440        if still_live {
441            abort_with(
442                "nova_rt_task_spawn: this future is already a live task; spawn it again only after its task has been released",
443            );
444        }
445    }
446    // The state object sits on no Nova stack and in no register while its
447    // task is queued or parked -- the executor is its only reference, and
448    // those are the collector's only other root sources (see `gc.rs`'s
449    // module doc comment). Paired with exactly one `gc::remove_root`: at
450    // completion in `poll_one`, or at take for `block_on`'s root -- see
451    // `poll_one`'s doc comment.
452    gc::add_root(state);
453    let id = TASKS.with(|tasks| {
454        let mut tasks = tasks.borrow_mut();
455        let id = tasks.len() as i64;
456        tasks.push(Task {
457            poll,
458            state,
459            done: false,
460            output: 0,
461            taken: false,
462            keep_root_until_taken: false,
463        });
464        id
465    });
466    BY_STATE.with(|m| m.borrow_mut().insert(state as usize, id));
467    QUEUE.with(|queue| queue.borrow_mut().push_back(id));
468    id
469}
470
471/// [`spawn_internal`], for `block_on`'s root task: the task keeps its GC root
472/// past completion, until [`take_output_internal`] hands its output back.
473///
474/// `run_to_completion` needs this and nothing else does. It returns the
475/// root's output out of `Task::output`, a copy in a Rust-heap `Vec` that
476/// roots nothing, and it keeps polling other tasks -- which allocate --
477/// between the root's completion and that take. The state object's root is
478/// what keeps a heap-valued output alive across that gap.
479///
480/// # Safety
481/// As [`spawn_internal`].
482unsafe fn spawn_root_internal(future: *mut u8) -> i64 {
483    // SAFETY: forwarding this function's own contract.
484    let id = unsafe { spawn_internal(future) };
485    TASKS.with(|tasks| {
486        tasks
487            .borrow_mut()
488            .get_mut(id as usize)
489            .expect("spawn_internal just registered this id")
490            .keep_root_until_taken = true;
491    });
492    id
493}
494
495/// Test-only: spawn `future` as a task that keeps its GC root, and so its
496/// payload slots, past completion -- [`spawn_root_internal`] exposed to this
497/// crate's other test modules.
498///
499/// For runtime unit tests that spawn a raw I/O future directly and read its
500/// stashed payload back from *outside* the task after it completes. Nova
501/// code never does that: every raw I/O builtin and every payload take is
502/// `std`-only, and `std` awaits the raw future and takes its payload inside
503/// the same task, before that task completes. An ordinary spawned task's
504/// slots are released at completion, so these tests need the kept variant.
505///
506/// # Safety
507/// As [`spawn_internal`].
508#[cfg(test)]
509pub(crate) unsafe fn spawn_kept_for_test(future: *mut u8) -> i64 {
510    // SAFETY: forwarding this function's own contract.
511    unsafe { spawn_root_internal(future) }
512}
513
514/// A poll may stage at most one deadline and at most one I/O wait.
515///
516/// Two of the *same* kind still aborts: that abort is what catches an inner
517/// future's `POLL_PENDING` failing to propagate, and it must keep doing so.
518/// Only the one legitimate combination -- a deadline and an I/O wait, from a
519/// single `read_timeout` -- becomes newly legal, and [`staged_to_wait`] is
520/// where the two are folded into the single [`Wait::Io`] entry [`PARKED`]
521/// actually holds.
522///
523/// `task` is not in the brief's own sketch of this struct, which only names
524/// the two kinds that newly compose. It is here because [`Wait::Task`]
525/// (`poll_join`'s wait) still has to be exclusive with a second task wait or
526/// with an I/O wait -- nothing about waiting on a sibling task composes with
527/// a socket the way a deadline and an I/O wait do, so a `Task` park stacked
528/// against another `Task` or an `Io` one still aborts exactly as any two
529/// same-kind parks do. A `Deadline`, though, composes with a `Task` wait the
530/// same way it composes with an `Io` one: `timeout(d, handle.join())` stages
531/// exactly that pair, and [`try_stage`] merges rather than collides.
532#[derive(Default, Clone, Copy, Debug)]
533struct Staged {
534    deadline: Option<Instant>,
535    io: Option<(RawSocket, Interest)>,
536    task: Option<i64>,
537}
538
539/// Fold everything staged so far back into the single [`Wait`] [`PARKED`]
540/// holds, or `None` if nothing was staged this poll.
541///
542/// One task must have exactly one `PARKED` entry ([`Wait::Io`]'s own doc
543/// comment), so this is where a deadline staged alongside an I/O wait is
544/// folded into that `Wait::Io`'s own `deadline` field instead of becoming a
545/// second entry. `task` still wins first, but it no longer wins by exclusion:
546/// [`try_stage`] now lets a deadline coexist with it, so this folds
547/// `staged.deadline` into the returned [`Wait::Task`]'s own `deadline` field,
548/// exactly as the `io` branch below already does for `Wait::Io`.
549fn staged_to_wait(staged: Staged) -> Option<Wait> {
550    if let Some(id) = staged.task {
551        return Some(Wait::Task {
552            id,
553            deadline: staged.deadline,
554        });
555    }
556    if let Some((socket, interest)) = staged.io {
557        return Some(Wait::Io {
558            socket,
559            interest,
560            deadline: staged.deadline,
561        });
562    }
563    staged.deadline.map(Wait::Deadline)
564}
565
566/// Try to add `wait` to `staged`, or report what it collided with.
567///
568/// A deadline never collides here: staging a second [`Wait::Deadline`], or
569/// one alongside a [`Wait::Task`] or inside a [`Wait::Io`] that already
570/// carries one, merges to the earlier of the two by [`Instant::min`] instead
571/// of erroring -- see [`Staged`]'s own doc comment for why `task` and `io`
572/// still exclude each other and themselves even though a deadline now
573/// composes with either. What still collides is a same-kind clash (two
574/// `Wait::Io`s, two `Wait::Task`s) or a `Wait::Task` crossing a `Wait::Io`.
575///
576/// Pure and non-aborting on purpose, unlike [`stage_park`] itself: a test
577/// exercising the actual collision would have to go through
578/// [`abort_with`]'s `std::process::abort()`, which `#[should_panic]`'s
579/// `catch_unwind` cannot intercept -- an aborting test would take the whole
580/// test binary down with it, not just fail. This is the half a test can call
581/// directly to check the collision is detected, the same way
582/// [`deadlock_report`] is the half a test can call to check
583/// [`report_deadlock`]'s text without ending the process.
584///
585/// Every collision below extracts the specific `Wait` it collided with
586/// directly out of whichever `Staged` field an `if let Some(_)` just proved
587/// is populated, and passes that straight to [`collision_msg`] -- never
588/// through [`staged_to_wait`], which would need to be asked to resolve a
589/// `Staged` that might have nothing in it. `stage_park` is called from
590/// inside a poll function, so nothing in this call chain may panic (see its
591/// own doc comment): an `.expect()` provably unreachable *today* is still a
592/// panic that could cross a poll boundary the day this function's logic
593/// changed under it without that assumption being re-checked, so the
594/// unreachable case is written out of existence here rather than asserted
595/// away.
596///
597/// Exhaustive on `wait` with no wildcard, like every other match on `Wait`
598/// in this module: a fourth variant must be considered here too.
599fn try_stage(staged: Staged, wait: Wait) -> Result<Staged, String> {
600    let mut next = staged;
601    match wait {
602        Wait::Deadline(at) => {
603            next.deadline = Some(match next.deadline {
604                Some(prev) => prev.min(at),
605                None => at,
606            });
607        }
608        Wait::Io {
609            socket,
610            interest,
611            deadline,
612        } => {
613            if let Some((prev_socket, prev_interest)) = next.io {
614                return Err(collision_msg(
615                    Wait::Io {
616                        socket: prev_socket,
617                        interest: prev_interest,
618                        deadline: next.deadline,
619                    },
620                    wait,
621                ));
622            }
623            if let Some(prev_id) = next.task {
624                return Err(collision_msg(
625                    Wait::Task {
626                        id: prev_id,
627                        deadline: next.deadline,
628                    },
629                    wait,
630                ));
631            }
632            if let Some(at) = deadline {
633                next.deadline = Some(match next.deadline {
634                    Some(prev) => prev.min(at),
635                    None => at,
636                });
637            }
638            next.io = Some((socket, interest));
639        }
640        Wait::Task { id, deadline } => {
641            if let Some(prev_id) = next.task {
642                return Err(collision_msg(
643                    Wait::Task {
644                        id: prev_id,
645                        deadline: next.deadline,
646                    },
647                    wait,
648                ));
649            }
650            if let Some((prev_socket, prev_interest)) = next.io {
651                return Err(collision_msg(
652                    Wait::Io {
653                        socket: prev_socket,
654                        interest: prev_interest,
655                        deadline: next.deadline,
656                    },
657                    wait,
658                ));
659            }
660            if let Some(at) = deadline {
661                next.deadline = Some(match next.deadline {
662                    Some(prev) => prev.min(at),
663                    None => at,
664                });
665            }
666            next.task = Some(id);
667        }
668    }
669    Ok(next)
670}
671
672/// The "two parks staged" message [`stage_park`] aborts with.
673///
674/// Takes the colliding `Wait` values directly, not a [`Staged`] that might
675/// resolve to nothing: see [`try_stage`]'s doc comment for why that
676/// distinction is the fix, not a stylistic preference.
677fn collision_msg(previous: Wait, wait: Wait) -> String {
678    format!(
679        "nova_rt: two parks staged in one poll ({previous:?} then {wait:?}); \
680         an inner future's POLL_PENDING did not propagate"
681    )
682}
683
684/// Record that the task currently being polled wants to park on `wait`.
685///
686/// Called from inside a poll function -- [`poll_sleep`], [`poll_join`], and
687/// this module's own tests -- so it must not panic: [`Staged`] is `Copy` and
688/// held in a `Cell`, so neither borrow can fail. The two aborts below are
689/// compiler-or-runtime bugs rather than user error, which is why they end the
690/// process rather than returning a status.
691fn stage_park(wait: Wait) {
692    if CURRENT.with(Cell::get).is_none() {
693        abort_with("nova_rt: a park was staged outside a poll");
694    }
695    PENDING_PARK.with(|cell| match try_stage(cell.get(), wait) {
696        Ok(next) => cell.set(next),
697        Err(msg) => abort_with(&msg),
698    });
699}
700
701/// Stage an I/O park: wake the current task once `socket` is ready for
702/// `interest`, or once `deadline` passes, whichever comes first.
703///
704/// The narrow seam `net.rs` stages an I/O wait through, so it never needs to
705/// construct a [`Wait`] itself. `Wait` and [`stage_park`] stay private --
706/// every exhaustive match on `Wait` lives in this module, so **a new variant**
707/// is a compile error here rather than a silent miss in a sibling module that
708/// matched on it. (This said "a fifth variant" until 2026-08-24; `Wait` has
709/// three -- `Deadline`, `Task`, `Io` -- so the next one added is the fourth.
710/// The property does not depend on the number, so the number is gone rather
711/// than corrected.)
712///
713/// `net.rs` calls this from five production sites: `poll_connect`
714/// (`Interest::Write`), `poll_read` (`Interest::Read`), `poll_write`
715/// (`Interest::Write`), `poll_read_timeout` (`Interest::Read`, and the only
716/// one of the five that passes a `deadline`) and `poll_accept`
717/// (`Interest::Read`, because accept-readiness *is* read-readiness on both
718/// `select` and `WSAPoll`, which is why a listening socket needed no third
719/// `Interest`). An earlier `#[allow(dead_code)]` here recorded that no caller
720/// existed yet; it has been removed with its reason rather than left to mask a
721/// real dead-code finding.
722pub(crate) fn stage_io_park(socket: RawSocket, interest: Interest, deadline: Option<Instant>) {
723    stage_park(Wait::Io {
724        socket,
725        interest,
726        deadline,
727    });
728}
729
730/// Poll task `id` once. On [`POLL_PENDING`], re-queue it for another turn --
731/// or, if the poll staged a park via [`stage_park`], move it to [`PARKED`]
732/// instead. On [`POLL_READY`], copy its output out of the state object into
733/// the `Task` record, mark it done, and wake anything parked on it (see
734/// [`wake_tasks_waiting_on`]).
735///
736/// **A spawned task's GC root is released here, at completion.** The output
737/// value stays in the state object's own [`STATE_SLOT_OUTPUT`]; a caller that
738/// wants it holds the future (`JoinHandle::join` does), and the future
739/// reaches the state object by ordinary tracing, so a heap-valued output
740/// stays alive exactly as long as someone can still read it. A task whose
741/// handle was dropped is then reachable from nothing, and the collector can
742/// free it -- the leak this used to be. The one exception is `block_on`'s
743/// root ([`spawn_root_internal`]): its output is read out of `Task::output`,
744/// a Rust-heap copy that roots nothing, while other tasks keep running and
745/// allocating, so it keeps its root until [`take_output_internal`].
746///
747/// Any status that is neither [`POLL_PENDING`] nor [`POLL_READY`] panics
748/// rather than being treated as completion. This module exists to pin the
749/// poll ABI before codegen depends on it; a generated poll function returning
750/// `2`, or garbage, is a Task 5 codegen bug, and completing the task with
751/// whatever happens to be in the output slot would turn it into a wrong
752/// answer instead of a diagnostic.
753///
754/// A poll function that panics leaves its task `done == false` and its state
755/// permanently rooted -- a leak, on a path that is already unwinding out
756/// through [`nova_rt_task_block_on`]. Not resolved here: cleaning up would
757/// mean a `catch_unwind` per poll, which would swallow exactly the panic
758/// `a_re_entrant_block_on_panics` needs to observe.
759///
760/// # Safety
761/// `id` must be a currently-registered task id.
762unsafe fn poll_one(id: i64) {
763    let (poll, state) = TASKS.with(|tasks| {
764        let tasks = tasks.borrow();
765        let task = tasks
766            .get(id as usize)
767            .expect("poll_one: task id is not registered");
768        (task.poll, task.state)
769    });
770    CURRENT.with(|c| c.set(Some(id)));
771    // SAFETY: unchanged from before -- `poll`/`state` came from a `Task` this
772    // module built in `spawn_internal`; `task_ctx` is still always null.
773    let status = unsafe { poll(state, std::ptr::null_mut()) };
774    CURRENT.with(|c| c.set(None));
775    // Taken unconditionally, which is what discards a park staged by a poll
776    // that then returned `POLL_READY`. Leaving it staged would park the next
777    // task polled; committing it would leave a finished task in `PARKED`,
778    // faking a deadlock for the rest of the process.
779    let staged = staged_to_wait(PENDING_PARK.with(|p| p.take()));
780    if status == POLL_PENDING {
781        match staged {
782            Some(wait) => PARKED.with(|parked| parked.borrow_mut().push((id, wait))),
783            None => QUEUE.with(|queue| queue.borrow_mut().push_back(id)),
784        }
785        return;
786    }
787    // Panics rather than aborting, unlike every other diagnostic here that a
788    // Nova program can reach: this one cannot be reached by a well-formed
789    // compiled program at all. A generated poll function's every exit returns
790    // `POLL_PENDING` or `POLL_READY` (`async_lower.rs` emits them as
791    // `ConstInt`s), so an out-of-range status means the compiler is broken,
792    // and the panic's observability is what lets
793    // `an_out_of_range_poll_status_panics_rather_than_completing_the_task`
794    // assert on the value. The unwind leaves through
795    // `nova_rt_task_block_on`'s `resume_unwind`, so it can cross a generated
796    // frame -- on a path whose precondition is already a compiler bug.
797    if status != POLL_READY {
798        panic!(
799            "poll function for task {id} returned {status}, which is neither \
800             POLL_PENDING ({POLL_PENDING}) nor POLL_READY ({POLL_READY})"
801        );
802    }
803    // SAFETY: `state` is the same live state object `spawn_internal` rooted;
804    // `STATE_SLOT_OUTPUT` is within its bounds regardless of how many temp
805    // slots follow it (see `nova_rt_task_spawn`'s contract, which requires at
806    // least `STATE_MIN_SIZE` bytes for exactly this read).
807    let output = unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).read() };
808    let release_now = TASKS.with(|tasks| {
809        let mut tasks = tasks.borrow_mut();
810        let task = tasks
811            .get_mut(id as usize)
812            .expect("poll_one: task id is not registered");
813        task.output = output;
814        task.done = true;
815        !task.keep_root_until_taken
816    });
817    wake_tasks_waiting_on(id);
818    // Outside the borrow above: `release_internal` borrows `TASKS` itself.
819    // After the wake, which only moves ids between Rust-heap queues and
820    // allocates nothing on the GC heap, so no collection can fall between
821    // completion and release.
822    if release_now {
823        release_internal(id);
824    }
825}
826
827/// Move every task parked on `done_id` back onto the ready queue.
828///
829/// Called from `poll_one` the moment a task is marked done, which is what
830/// makes `Wait::Task` a complete wake source: a task cannot finish by any
831/// other route.
832fn wake_tasks_waiting_on(done_id: i64) {
833    let woken = PARKED.with(|parked| {
834        let mut parked = parked.borrow_mut();
835        let mut woken = Vec::new();
836        parked.retain(|&(id, wait)| match wait {
837            Wait::Task { id: target, .. } if target == done_id => {
838                woken.push(id);
839                false
840            }
841            _ => true,
842        });
843        woken
844    });
845    QUEUE.with(|queue| {
846        let mut queue = queue.borrow_mut();
847        for id in woken {
848            queue.push_back(id);
849        }
850    });
851}
852
853/// Whether task `id` has completed. Aborts on an id no `spawn_internal` ever
854/// handed out.
855///
856/// `id` is this executor's own internal identity, not something a Nova
857/// program can name directly: [`nova_rt_task_is_done`] resolves a future to
858/// one through `BY_STATE` before reaching here, so this abort now guards a
859/// bug in this module rather than a forged caller value.
860fn is_done_internal(id: i64) -> bool {
861    TASKS.with(|tasks| match tasks.borrow().get(id as usize) {
862        Some(task) => task.done,
863        None => abort_with(&format!("nova_rt_task_is_done: unknown task id {id}")),
864    })
865}
866
867/// Release task `id`'s state-object root, handing nothing back. Idempotent.
868///
869/// This is [`take_output_internal`] with the take removed, and the two are not
870/// interchangeable in either direction. That one's second call is diagnosed
871/// precisely because it *would* hand back bits that may name a freed object;
872/// nothing leaves here, so nothing here can go stale, and a second call has
873/// nothing to get wrong. The two share `Task::taken`, so whichever runs first
874/// is the one that cancels the single `gc::add_root`.
875///
876/// The reason a caller can want the root released without the value is that
877/// the value does not only live in `Task::output`: it is also still in
878/// [`STATE_SLOT_OUTPUT`] of the state object, which is reachable from word
879/// [`FUTURE_SLOT_STATE`] of the future -- so a caller that holds the future
880/// holds the value, through the collector's ordinary tracing, and needs nothing
881/// from here but the end of the executor's own claim. [`poll_one`] is the
882/// main caller, at completion. `JoinHandle::join` also calls it (through
883/// `nova_rt_task_release`), and for a completed task that call finds `taken`
884/// already set and does nothing.
885fn release_internal(id: i64) {
886    let state = TASKS.with(|tasks| {
887        let mut tasks = tasks.borrow_mut();
888        let Some(task) = tasks.get_mut(id as usize) else {
889            abort_with(&format!("nova_rt_task_release: unknown task id {id}"));
890        };
891        if task.taken {
892            return None;
893        }
894        task.taken = true;
895        Some(task.state)
896    });
897    // Outside the borrow above, for the same reason `take_output_internal`
898    // orders its own `gc::remove_root` after releasing it.
899    if let Some(state) = state {
900        gc::remove_root(state);
901    }
902    // A task's payload slots die with the task. `fs.rs` owns the storage and the
903    // root discipline; this is the whole of `task.rs`'s knowledge of it. The
904    // matching call is in `take_output_internal` / `release_internal` -- both
905    // release points need it, because either can be the last thing to touch a
906    // task.
907    crate::fs::release_task_slots(id);
908}
909
910/// Hand out task `id`'s output and release the GC root `spawn_internal`
911/// registered for its state object -- the single `gc::remove_root` matching
912/// that single `gc::add_root`, so the registry's multiset stays balanced.
913///
914/// This is the *take*, not a peek, and that is what makes a heap-valued
915/// output sound: until this call the value is kept alive by the state object's
916/// own root (see [`poll_one`]), and after it the value is in the caller's
917/// hands, conservatively rooted by the caller's own frame the same way any
918/// other runtime function's returned pointer is. Both failure modes are
919/// therefore diagnosed rather than answered:
920///
921/// - **Not done.** The `output` field still holds its `0` initializer, which
922///   is indistinguishable from a task that genuinely completed with `0`.
923///   `run_to_completion`, the one production caller, reaches here only once
924///   its queue has drained and the root is done, so reaching here early is a
925///   caller bug.
926/// - **Already taken.** The root is gone, so `output`'s bits may name a freed
927///   object; returning them a second time would be exactly the dangling
928///   pointer this ordering exists to prevent. A caller that needs the value
929///   twice must keep its own copy.
930///
931/// Only a task spawned through [`spawn_root_internal`] -- `block_on`'s root --
932/// still has its root when it completes; every other task's is released in
933/// [`poll_one`] at completion, so taking such a task's output is refused with
934/// a panic that says why. This function or [`release_internal`] ends a task's
935/// claim; both check the same `Task::taken` flag, so a task's single root is
936/// cancelled at most once.
937fn take_output_internal(id: i64) -> i64 {
938    let (output, state) = TASKS.with(|tasks| {
939        let mut tasks = tasks.borrow_mut();
940        let task = tasks
941            .get_mut(id as usize)
942            .expect("nova_rt_task_take_output: unknown task id");
943        assert!(
944            task.done,
945            "nova_rt_task_take_output: task {id} has not completed; \
946             poll nova_rt_task_is_done until it reports true first"
947        );
948        assert!(
949            !task.taken || task.keep_root_until_taken,
950            "nova_rt_task_take_output: task {id}'s GC root has already been \
951             released -- a spawned task's is released when it completes -- so \
952             its output can no longer be taken; read it through its future, as \
953             JoinHandle::join does"
954        );
955        assert!(
956            !task.taken,
957            "nova_rt_task_take_output: task {id}'s output was already taken; \
958             taking released the state object's GC root, so a heap-valued \
959             output may already have been collected"
960        );
961        task.taken = true;
962        (task.output, task.state)
963    });
964    // Strictly after the borrow above is released and after `output` has been
965    // copied out: `gc::remove_root` allocates nothing and so cannot trigger a
966    // collection, but ordering the read first keeps that independent of
967    // `remove_root`'s implementation.
968    gc::remove_root(state);
969    // A task's payload slots die with the task. `fs.rs` owns the storage and the
970    // root discipline; this is the whole of `task.rs`'s knowledge of it. The
971    // matching call is in `take_output_internal` / `release_internal` -- both
972    // release points need it, because either can be the last thing to touch a
973    // task.
974    crate::fs::release_task_slots(id);
975    output
976}
977
978/// Spawn `future` as this thread's newest task, then run this thread's
979/// executor until every task registered so far -- `future`'s own and any
980/// other still-pending task -- has completed, and return `future`'s output.
981///
982/// This drains the whole shared queue rather than stopping as soon as
983/// `future` itself is done. Two consequences, both decisions rather than
984/// accidents:
985///
986/// 1. **A spawned task cannot outlive the root**, so `block_on` implicitly
987///    joins everything queued on this thread. That diverges from tokio's
988///    `block_on`, which returns as soon as its own future resolves and leaves
989///    other tasks to the runtime's background workers. There are no
990///    background workers here: with no waker and no driver thread, a
991///    `block_on` call is the only thing that ever advances any task on this
992///    thread, so a task spawned earlier and left pending would otherwise have
993///    no way to reach the state `nova_rt_task_is_done` and `join` wait for
994///    (`done`, with an output) at all.
995/// 2. **A task that stages a park no longer spins this loop forever, and a
996///    task that instead keeps re-queueing itself can no longer starve
997///    anything else's deadline while it does.** (It still re-queues itself
998///    forever, by design; what changed is only that doing so no longer costs
999///    a sibling its wake-up. A task joining *that* kind of task is a
1000///    livelock, not a deadlock, and this loop still cannot see it -- ADR
1001///    0009 §1's 2026-08-10 amendment records it as accepted, not fixed.)
1002///    Before the park set, a task returning [`POLL_PENDING`] was
1003///    unconditionally re-queued, so one that never became ready re-queued
1004///    forever and the loop hung with no diagnostic. A poll that stages a
1005///    [`Wait`] (see [`stage_park`]) moves its task to [`PARKED`] instead of
1006///    back onto the queue -- but a task that does not park (`yield_now`)
1007///    still re-queues itself every turn by design, and that alone must not
1008///    be able to starve a sibling's deadline. [`wake_due`] is therefore
1009///    called after **every** poll below, not only once this loop's inner
1010///    pass finds the queue empty: if a deadline were checked only there, a
1011///    task that keeps re-queueing itself would keep that pass from ever
1012///    finding the queue empty, and anything parked on a deadline would never
1013///    be examined at all -- not delayed, starved permanently, for as long as
1014///    anything kept re-queueing. Task 2's own end-to-end fixture is what
1015///    surfaced this: `sleep`, awaited underneath what was then a spinning
1016///    `join`, hung indefinitely until this per-poll check was added.
1017///    `poll::wait`, which is where the real sleeping (or, from Task 2 on,
1018///    socket waiting) now happens, is reached only from the
1019///    drained-queue branch below, where blocking this thread is correct
1020///    because there is genuinely nothing left to run; a park set holding
1021///    nothing but *bare* `Wait::Task` entries and no I/O wait there is a
1022///    genuine deadlock, since nothing still running can ever finish and wake
1023///    one, and [`report_deadlock`] ends the process naming each one instead
1024///    of hanging silently. A `Wait::Io` park changes what "nothing left to
1025///    run" means: waiting on a socket that some *other* process must make
1026///    ready is legitimate waiting, not a deadlock. A **timed** `Wait::Task`
1027///    changes it the same way: its own clock, not some other task's
1028///    progress, is what eventually wakes it, so it is legitimate waiting too
1029///    -- [`earliest_deadline`] reports its deadline exactly as it would a
1030///    bare `Wait::Deadline` or a timed `Wait::Io`'s (see that function's own
1031///    doc comment), which is what routes the drained-queue match below to a
1032///    sleep instead of to `report_deadlock`. So `report_deadlock` is
1033///    reachable only when the deadline dimension -- which now includes any
1034///    timed `Wait::Task` -- and the I/O dimension are *both* empty, i.e.
1035///    every remaining park is a bare `Wait::Task` (see the drained-queue
1036///    match below). `sleep` (Task 2) and a parking `join`
1037///    (Task 3) are what let Nova source reach any of this at all: before
1038///    `sleep`, `yield_now` was the only suspension an `async fn` body could
1039///    await, and it never stages a `Wait`, so nothing compiled from Nova
1040///    source could ever populate [`PARKED`] in the first place.
1041///
1042/// # Safety
1043/// `future` must be a valid future fat pointer (see [`read_future`]).
1044unsafe fn run_to_completion(future: *mut u8) -> i64 {
1045    // SAFETY: forwarding this function's own contract.
1046    let root_id = unsafe { spawn_root_internal(future) };
1047    loop {
1048        while let Some(id) = QUEUE.with(|queue| queue.borrow_mut().pop_front()) {
1049            // SAFETY: `id` was just popped from `QUEUE`, so it is a registered
1050            // task id (pushed by `spawn_internal` once, re-pushed only by
1051            // `poll_one` and `wake_tasks_waiting_on` for a live task).
1052            unsafe { poll_one(id) };
1053            // Checked after every poll, not only once this inner loop finds
1054            // the queue empty -- see this function's own doc comment for why
1055            // a self-requeuing task would otherwise starve every deadline in
1056            // `PARKED` forever rather than merely delay it. Guarded on
1057            // `PARKED` being non-empty so the overwhelmingly common case
1058            // (nothing parked) costs one `Vec::is_empty` check and never
1059            // reads the clock.
1060            if !PARKED.with(|parked| parked.borrow().is_empty()) {
1061                wake_due(Instant::now());
1062            }
1063        }
1064        if PARKED.with(|parked| parked.borrow().is_empty()) {
1065            break;
1066        }
1067        // The ready queue is empty and something is still parked. A remaining
1068        // deadline (bare, or riding inside an I/O wait or a task wait) or a
1069        // live I/O wait can each refill the queue on their own; a park set
1070        // holding nothing but *bare* `Wait::Task` entries -- neither
1071        // dimension populated -- cannot, because nothing is running to
1072        // finish. This match is the executor's only remaining wait: sleeping
1073        // *is* waiting on an empty socket set (the `(Some(at), true)` arm),
1074        // so there is exactly one place this thread ever blocks, and
1075        // `task.rs` itself no longer knows how.
1076        let io: Vec<(RawSocket, Interest)> = io_parks();
1077        match (earliest_deadline(), io.is_empty()) {
1078            // Nothing can ever wake anything: the only true deadlock.
1079            (None, true) => report_deadlock(),
1080            // Waiting on a peer is legitimate waiting, not a deadlock.
1081            (None, false) => wake_ready(crate::poll::wait(&io, None)),
1082            // No sockets and a deadline IS a sleep -- there is no second
1083            // timing path; `poll::wait` does the actual blocking.
1084            (Some(at), true) => {
1085                crate::poll::wait(&[], Some(at));
1086            }
1087            (Some(at), false) => wake_ready(crate::poll::wait(&io, Some(at))),
1088        }
1089        wake_due(Instant::now());
1090    }
1091    // The queue is empty, and the root task was in it, so it must have
1092    // finished -- the only way out of the queue is completion. Asserted
1093    // rather than assumed because `take_output_internal` panics on a task
1094    // that is not done, and this call site is the one place that must never
1095    // be able to trigger that panic.
1096    debug_assert!(
1097        is_done_internal(root_id),
1098        "run_to_completion: the queue drained but the root task is not done"
1099    );
1100    take_output_internal(root_id)
1101}
1102
1103/// The soonest instant any parked task is waiting for, if any is waiting on
1104/// the clock at all -- a bare [`Wait::Deadline`], or the deadline riding
1105/// inside a [`Wait::Io`] or a [`Wait::Task`]. A `Wait::Task` with no deadline
1106/// contributes nothing, the same as an untimed `Wait::Io`.
1107fn earliest_deadline() -> Option<Instant> {
1108    PARKED.with(|parked| {
1109        parked
1110            .borrow()
1111            .iter()
1112            .filter_map(|&(_, wait)| match wait {
1113                Wait::Deadline(at) => Some(at),
1114                Wait::Io { deadline, .. } => deadline,
1115                Wait::Task { deadline, .. } => deadline,
1116            })
1117            .min()
1118    })
1119}
1120
1121/// Every socket a parked task is waiting on, paired with the interest it is
1122/// waiting for.
1123///
1124/// This returns a non-empty vector in production now: `net.rs` stages a
1125/// [`Wait::Io`] from each of its five poll functions (see [`stage_io_park`]),
1126/// so an ordinary Nova program awaiting a `connect`, `read`, `write`,
1127/// `read_timeout` or `accept` puts a real socket here, and
1128/// `run_to_completion`'s drained-queue match hands it to `poll::wait`. A
1129/// listening socket goes in through the same `Interest::Read` a stream does,
1130/// because accept-readiness *is* read-readiness on both of `poll::wait`'s
1131/// platform arms. An earlier version of this
1132/// comment said the opposite -- that nothing outside this module's own tests
1133/// ever staged one, so this always returned empty -- which was true only
1134/// before `net.rs` existed.
1135fn io_parks() -> Vec<(RawSocket, Interest)> {
1136    PARKED.with(|parked| {
1137        parked
1138            .borrow()
1139            .iter()
1140            .filter_map(|&(_, wait)| match wait {
1141                Wait::Io {
1142                    socket, interest, ..
1143                } => Some((socket, interest)),
1144                Wait::Deadline(_) | Wait::Task { .. } => None,
1145            })
1146            .collect()
1147    })
1148}
1149
1150/// Move every task parked on a [`Wait::Io`] whose socket is in `ready` back
1151/// onto the ready queue.
1152///
1153/// The I/O counterpart of [`wake_tasks_waiting_on`] and [`wake_due`], and the
1154/// increment's central new wake source: called with whatever `poll::wait`
1155/// reports ready, which is now a genuinely non-empty set whenever a `net.rs`
1156/// operation's socket becomes ready (see [`io_parks`]). An earlier version of
1157/// this comment called it "always a no-op today", true only while nothing
1158/// production ever staged a `Wait::Io`.
1159///
1160/// The `ready.contains(&socket)` guard below is what makes this wake *only*
1161/// the tasks whose own socket is ready; the `retain`'s trailing `_ => true`
1162/// would swallow the loss of that guard silently, so
1163/// `wake_ready_wakes_only_the_task_whose_socket_is_ready` pins it
1164/// behaviourally.
1165fn wake_ready(ready: Vec<RawSocket>) {
1166    if ready.is_empty() {
1167        return;
1168    }
1169    let woken = PARKED.with(|parked| {
1170        let mut parked = parked.borrow_mut();
1171        let mut woken = Vec::new();
1172        parked.retain(|&(id, wait)| match wait {
1173            Wait::Io { socket, .. } if ready.contains(&socket) => {
1174                woken.push(id);
1175                false
1176            }
1177            _ => true,
1178        });
1179        woken
1180    });
1181    QUEUE.with(|queue| {
1182        let mut queue = queue.borrow_mut();
1183        for id in woken {
1184            queue.push_back(id);
1185        }
1186    });
1187}
1188
1189/// Move every parked task whose deadline is `<= now` back onto the ready
1190/// queue, leaving every other entry -- an untimed [`Wait::Io`] or
1191/// [`Wait::Task`], or a [`Wait::Deadline`] (bare, or riding inside a
1192/// [`Wait::Io`] or a [`Wait::Task`]) still in the future -- exactly where it
1193/// was.
1194///
1195/// Once an I/O wait carries a deadline, a passed deadline on that wait *is*
1196/// its timeout firing, so it must wake the task exactly as a bare
1197/// `Wait::Deadline` does -- it just also happens to leave that task's
1198/// socket, if `poll::wait` ever reported one ready at the same moment,
1199/// unconsumed. That is reachable in production now that `net.rs`'s
1200/// `read_timeout` stages a timed `Wait::Io`, and it is harmless for the reason
1201/// `poll_read_timeout`'s own doc comment gives: every poll retries the read
1202/// before consulting the deadline, so a task woken by its timeout still
1203/// observes data that arrived at the same instant and reports success rather
1204/// than a spurious `TimedOut`. An earlier version of this comment said nothing
1205/// here ever populated a `Wait::Io` outside a test, which stopped being true
1206/// when `net.rs` landed.
1207///
1208/// A timed `Wait::Task` wakes on the same rule as a timed `Wait::Io` above:
1209/// its deadline passing is its own timeout firing, which this function is
1210/// what notices -- [`wake_tasks_waiting_on`] is the only other thing that
1211/// ever removes a `Wait::Task` entry, and it reacts to the target completing,
1212/// never to the clock. **This arm is not compiler-forced the way
1213/// [`earliest_deadline`]'s and [`deadlock_report`]'s matches are**: this
1214/// function's `retain` ends in a wildcard, so a timed `Wait::Task` with no
1215/// arm here would fall through it, park forever, and fail nothing at compile
1216/// time -- see `wake_due_wakes_a_task_wait_whose_deadline_elapsed`, the test
1217/// that exists because nothing else would catch it.
1218///
1219/// Called by [`run_to_completion`] after every single poll (cheap: one pass
1220/// over [`PARKED`], no blocking) so a self-requeuing task can never starve a
1221/// deadline -- see its doc comment -- and again after every `poll::wait` call
1222/// in the drained-queue branch, which is the one place this thread actually
1223/// blocks.
1224fn wake_due(now: Instant) {
1225    let woken = PARKED.with(|parked| {
1226        let mut parked = parked.borrow_mut();
1227        let mut woken = Vec::new();
1228        parked.retain(|&(id, wait)| match wait {
1229            Wait::Deadline(deadline) if deadline <= now => {
1230                woken.push(id);
1231                false
1232            }
1233            Wait::Io {
1234                deadline: Some(deadline),
1235                ..
1236            } if deadline <= now => {
1237                woken.push(id);
1238                false
1239            }
1240            Wait::Task {
1241                deadline: Some(deadline),
1242                ..
1243            } if deadline <= now => {
1244                woken.push(id);
1245                false
1246            }
1247            _ => true,
1248        });
1249        woken
1250    });
1251    QUEUE.with(|queue| {
1252        let mut queue = queue.borrow_mut();
1253        for id in woken {
1254            queue.push_back(id);
1255        }
1256    });
1257}
1258
1259/// The deadlock message: a headline plus one line per parked task.
1260///
1261/// Separate from [`report_deadlock`] so a test can assert the text without
1262/// aborting. `run_to_completion` reaches `report_deadlock` -- and so this --
1263/// only from its drive loop's `(None, true)` arm: no deadline anywhere in
1264/// [`PARKED`] *and* no I/O wait anywhere in it either. A bare `Wait::Deadline`
1265/// or a timed `Wait::Io` therefore cannot actually reach here through that
1266/// path, and neither can an untimed `Wait::Io`, since `io.is_empty()` would
1267/// have been `false`. Every arm below is still printed rather than treated as
1268/// unreachable, because a panic on an abort path would replace a clear
1269/// diagnostic with a confusing one, and this function is also called
1270/// directly by this module's own tests with whatever `PARKED` contents they
1271/// choose.
1272fn deadlock_report() -> String {
1273    let entries = PARKED.with(|parked| parked.borrow().clone());
1274    let plural = if entries.len() == 1 {
1275        "task is"
1276    } else {
1277        "tasks are"
1278    };
1279    let mut report = format!(
1280        "nova: deadlock: {} {plural} parked and none can wake\n",
1281        entries.len()
1282    );
1283    for (id, wait) in entries {
1284        match wait {
1285            Wait::Task {
1286                id: target,
1287                deadline: None,
1288            } => {
1289                report.push_str(&format!(
1290                    "  task {id} is waiting for task {target} to finish\n"
1291                ));
1292            }
1293            Wait::Task {
1294                id: target,
1295                deadline: Some(_),
1296            } => {
1297                report.push_str(&format!(
1298                    "  task {id} is waiting for task {target} to finish, with a deadline\n"
1299                ));
1300            }
1301            Wait::Deadline(_) => {
1302                report.push_str(&format!("  task {id} is waiting on a deadline\n"));
1303            }
1304            Wait::Io { deadline: None, .. } => {
1305                report.push_str(&format!("  task {id} is waiting on i/o\n"));
1306            }
1307            Wait::Io {
1308                deadline: Some(_), ..
1309            } => {
1310                report.push_str(&format!("  task {id} is waiting on i/o with a deadline\n"));
1311            }
1312        }
1313    }
1314    report
1315}
1316
1317/// Print the deadlock report and end the process.
1318fn report_deadlock() -> ! {
1319    eprint!("{}", deadlock_report());
1320    std::process::abort();
1321}
1322
1323/// Queue a `{ poll_code, state }` future as a new task and return its id.
1324///
1325/// `"C-unwind"`: see [`PollFn`]'s doc comment for why every entry point in
1326/// this module shares that ABI rather than plain `"C"`.
1327///
1328/// # Safety
1329/// `future` must point to a live [`FUTURE_SIZE`]-byte
1330/// `{ poll_code, state }` block, and:
1331///
1332/// - `poll_code` ([`FUTURE_SLOT_POLL`]) must be a non-null [`PollFn`] address
1333///   (see [`read_future`] for why null is undefined behaviour here, not a
1334///   later fault);
1335/// - `state` ([`FUTURE_SLOT_STATE`]) must point to a live, writable block of
1336///   at least `(STATE_SLOT_TEMPS + n_temps) * 8` bytes, and never fewer than
1337///   [`STATE_MIN_SIZE`]. **[`STATE_SLOT_OUTPUT`] is read unconditionally on
1338///   completion** -- including for a future whose `async fn` returns unit and
1339///   has no temps, where nothing in the poll function itself ever touches
1340///   that slot. A tag-only 8-byte state object satisfies neither this
1341///   contract nor the executor's read, and would be read 8 bytes past its
1342///   allocation;
1343/// - `state` must be allocated **scanned** (`gc::alloc(.., true)`, which is
1344///   what `nova_rt_alloc` does): a heap-valued output written to
1345///   [`STATE_SLOT_OUTPUT`] is kept alive by tracing through the state object,
1346///   so an unscanned state object would let it be collected (see
1347///   [`poll_one`]).
1348#[no_mangle]
1349pub unsafe extern "C-unwind" fn nova_rt_task_spawn(future: *mut u8) -> i64 {
1350    // SAFETY: forwarding this function's own contract.
1351    unsafe { spawn_internal(future) }
1352}
1353
1354/// Drive this thread's executor to quiescence and return `future`'s output.
1355/// See [`run_to_completion`] for exactly what "quiescence" means here.
1356///
1357/// `"C-unwind"`: see [`PollFn`]'s doc comment. [`poll_one`]'s status
1358/// diagnostic leaves through this function's `resume_unwind` below, so this is
1359/// the boundary that permission is needed at.
1360///
1361/// **Re-entrancy ends the process rather than unwinding out of it.** A nested
1362/// call means a poll function called `block_on`, and a poll function's frame is
1363/// generated code with no unwind description, so a panic raised here would have
1364/// to be unwound *through* that frame to reach any handler. `std/task`'s
1365/// `block_on` makes this reachable from ordinary Nova source (`async fn f() {
1366/// block_on(g()) }` compiles), which is exactly why the diagnostic is an
1367/// [`abort_with`] and not a `panic!`.
1368///
1369/// # Safety
1370/// `future` must satisfy [`nova_rt_task_spawn`]'s contract exactly -- same
1371/// [`FUTURE_SIZE`]-byte fat pointer, same non-null `poll_code`, and the same
1372/// minimum of `(STATE_SLOT_TEMPS + n_temps) * 8` scanned, writable bytes
1373/// (never fewer than [`STATE_MIN_SIZE`]) for the state object, because
1374/// [`STATE_SLOT_OUTPUT`] is read unconditionally on completion here too.
1375///
1376/// # Panics
1377/// If any polled task's poll function returns a status that is neither
1378/// [`POLL_PENDING`] nor [`POLL_READY`] (see [`poll_one`]).
1379#[no_mangle]
1380pub unsafe extern "C-unwind" fn nova_rt_task_block_on(future: *mut u8) -> i64 {
1381    if IN_BLOCK_ON.with(|in_block_on| in_block_on.get()) {
1382        abort_with(
1383            "nova_rt_task_block_on called re-entrantly: an async fn must not call block_on, \
1384             which would run a second executor loop from inside the first one's frame",
1385        );
1386    }
1387    IN_BLOCK_ON.with(|in_block_on| in_block_on.set(true));
1388    // `AssertUnwindSafe`: the closure only captures a `*mut u8`, which is
1389    // GC-owned data with no invariant that spans a panic here, so asserting
1390    // unwind-safety is correct rather than papering over a real hazard.
1391    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1392        // SAFETY: forwarding this function's own contract.
1393        unsafe { run_to_completion(future) }
1394    }));
1395    // Cleared on every path out of this call, including `poll_one`'s status
1396    // diagnostic unwinding through the `catch_unwind` above -- otherwise one
1397    // such `block_on` would leave every later call on this thread permanently
1398    // (and incorrectly) diagnosed as re-entrant.
1399    IN_BLOCK_ON.with(|in_block_on| in_block_on.set(false));
1400    match result {
1401        Ok(output) => output,
1402        Err(payload) => std::panic::resume_unwind(payload),
1403    }
1404}
1405
1406/// Read the task id for `future`'s state object, or abort.
1407///
1408/// **A miss means `future` was never spawned, and nothing else can miss.** The
1409/// key is dropped only when the collector frees the state object
1410/// ([`prune_freed_states`]), and that cannot have happened while a live future
1411/// still points at the state: the fat pointer is a scanned heap object holding
1412/// the state address in word [`FUTURE_SLOT_STATE`], so tracing the future marks
1413/// the state. So a hit is this future's own task -- including a released one,
1414/// which is what a second `join` needs -- and there is no staleness test to do
1415/// here.
1416///
1417/// Aborts rather than panics: both callers are reachable from `join`, which
1418/// runs inside a generated poll frame, and a panic must not cross that
1419/// boundary (ADR 0009 section 1).
1420///
1421/// # Safety
1422/// `future` must be a valid future fat pointer (see [`read_future`]).
1423unsafe fn task_id_of(future: *mut u8, who: &str) -> i64 {
1424    // SAFETY: forwarding this function's own contract.
1425    let (_, state) = unsafe { read_future(future) };
1426    match BY_STATE.with(|m| m.borrow().get(&(state as usize)).copied()) {
1427        Some(id) => id,
1428        None => abort_with(&format!(
1429            "{who}: this future was never spawned, so there is no task to ask about"
1430        )),
1431    }
1432}
1433
1434/// Whether the task named by `future`'s state object has completed.
1435///
1436/// `"C-unwind"`: see [`PollFn`]'s doc comment. Sharing one ABI across every
1437/// entry point in this module means a caller never has to know which of
1438/// them can panic; `is_done`/`take_output`'s internal helpers do, on an
1439/// unknown task id, and so does resolving `future` itself (see
1440/// [`task_id_of`]).
1441///
1442/// # Safety
1443/// `future` must be a valid future fat pointer (see [`read_future`]). One
1444/// this thread never spawned ends the process rather than answering.
1445#[no_mangle]
1446pub unsafe extern "C-unwind" fn nova_rt_task_is_done(future: *mut u8) -> i8 {
1447    // SAFETY: forwarding this function's own contract.
1448    let id = unsafe { task_id_of(future, "nova_rt_task_is_done") };
1449    is_done_internal(id) as i8
1450}
1451
1452/// A completed task's output slot, **taken exactly once**.
1453///
1454/// Taking releases the GC root that was keeping the output alive, so this must
1455/// be called once per task and only after [`nova_rt_task_is_done`] reports
1456/// true. **Only `block_on`'s root still has that root when it completes**, and
1457/// `run_to_completion` takes it itself; every other task's root is released at
1458/// completion, so taking its output panics, naming that cause. A second take,
1459/// or a take before completion, panics too -- see [`take_output_internal`].
1460///
1461/// `"C-unwind"`: see [`nova_rt_task_is_done`]'s doc comment.
1462///
1463/// # Safety
1464/// `id` must be an id previously returned by `nova_rt_task_spawn` on this
1465/// same thread.
1466///
1467/// # Panics
1468/// If `id` is unknown, if task `id` has not completed, or if its output has
1469/// already been taken.
1470#[no_mangle]
1471pub unsafe extern "C-unwind" fn nova_rt_task_take_output(id: i64) -> i64 {
1472    take_output_internal(id)
1473}
1474
1475/// The raw bits a completed task's output was copied out as, **without**
1476/// taking it and without touching any GC root.
1477///
1478/// For Rust harnesses that read *scalar* outputs (`Int`, `Float`, `Bool`)
1479/// of a spawned task. After completion a spawned task's GC root is released,
1480/// so if these bits name a heap object, that object may already have been
1481/// freed; this function cannot tell and does not try. Nova code reads a
1482/// task's output through its future instead (`JoinHandle::join`), which
1483/// keeps any heap value reachable by ordinary tracing. Not registered in
1484/// [`crate::symbols`], so compiled Nova code cannot reach it.
1485///
1486/// # Panics
1487/// If `id` is unknown, or if task `id` has not completed.
1488pub fn output_bits(id: i64) -> i64 {
1489    TASKS.with(|tasks| {
1490        let tasks = tasks.borrow();
1491        let task = tasks
1492            .get(id as usize)
1493            .expect("output_bits: unknown task id");
1494        assert!(
1495            task.done,
1496            "output_bits: task {id} has not completed, so its output field \
1497             still holds its 0 initializer"
1498        );
1499        task.output
1500    })
1501}
1502
1503/// End the executor's claim on the task named by `future`'s state object.
1504/// See [`release_internal`] for why this exists next to
1505/// [`nova_rt_task_take_output`] rather than instead of it, and why calling it
1506/// twice is a no-op rather than a diagnostic.
1507///
1508/// `"C-unwind"`: see [`nova_rt_task_is_done`]'s doc comment.
1509///
1510/// # Safety
1511/// `future` must be a valid future fat pointer (see [`read_future`]). One
1512/// this thread never spawned ends the process rather than releasing the
1513/// wrong task (see [`task_id_of`]).
1514#[no_mangle]
1515pub unsafe extern "C-unwind" fn nova_rt_task_release(future: *mut u8) {
1516    // SAFETY: forwarding this function's own contract.
1517    let id = unsafe { task_id_of(future, "nova_rt_task_release") };
1518    release_internal(id);
1519}
1520
1521/// The state object [`nova_rt_task_yield_future`] allocates: the tag and output
1522/// slots, and nothing else.
1523///
1524/// [`poll_yield_once`] holds no value across its one suspension, so it needs no
1525/// temp slots -- and [`STATE_MIN_SIZE`] is the floor regardless, because
1526/// [`STATE_SLOT_OUTPUT`] is read unconditionally on completion.
1527const YIELD_STATE_SIZE: usize = STATE_MIN_SIZE;
1528
1529/// Both slots [`poll_yield_once`] and the executor address must be inside
1530/// [`YIELD_STATE_SIZE`]. A compile-time check rather than a test, because it is
1531/// a relation between two constants in this file and nothing at run time can
1532/// make it hold or fail differently: shrinking [`STATE_MIN_SIZE`], or moving
1533/// [`STATE_SLOT_OUTPUT`] past it, must fail the build here rather than write one
1534/// word past an allocation.
1535const _: () = assert!(YIELD_STATE_SIZE >= (STATE_SLOT_OUTPUT + 1) * 8);
1536
1537/// Report [`POLL_PENDING`] on the first poll of a state object and
1538/// [`POLL_READY`] on every later one.
1539///
1540/// The only [`PollFn`] in the system that `async_lower.rs` did not generate,
1541/// and it exists because a Nova `async fn` body suspends *only* at an
1542/// `.await`: `yield_now` has to await something that is not ready yet, and
1543/// nothing expressible in Nova is. Its resumption is unconditional rather than
1544/// tied to any event -- see this module's own doc comment on the absence of
1545/// wakers.
1546///
1547/// **It must not unwind.** `async_lower.rs`'s argument that no unwind can
1548/// cross a generated poll frame rests on every awaited future's poll code
1549/// having been emitted by that pass; this is the one exception, so it carries
1550/// the obligation directly. Its body is two raw word reads and one raw word
1551/// write, with no allocation, no `TASKS`/`QUEUE` borrow and no fallible
1552/// operation, so there is no panic to suppress.
1553unsafe extern "C-unwind" fn poll_yield_once(state: *mut u8, _task_ctx: *mut u8) -> i64 {
1554    let slots = state as *mut i64;
1555    // SAFETY: `state` is a `YIELD_STATE_SIZE`-byte state object built by
1556    // `nova_rt_task_yield_future`, so both slots below are in bounds.
1557    let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
1558    if tag == 0 {
1559        // SAFETY: same object, tag slot.
1560        unsafe { slots.add(STATE_SLOT_TAG).write(1) };
1561        return POLL_PENDING;
1562    }
1563    // A unit-valued output, written explicitly for the same reason
1564    // `async_lower.rs` writes one: the executor reads this slot on completion
1565    // whether or not the future carries a value.
1566    //
1567    // SAFETY: same object, output slot -- in bounds by `YIELD_STATE_SIZE`.
1568    unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
1569    POLL_READY
1570}
1571
1572/// Build a `{ poll_code, state }` future: a scanned [`FUTURE_SIZE`] fat pointer
1573/// over a freshly allocated scanned state object of `state_size` bytes, with
1574/// `init` handed the state's slots to populate before the future is returned.
1575///
1576/// **The one place this layout is written.** It is the layout
1577/// `async_lower.rs` independently emits, so a second copy that drifts from it
1578/// is a silent miscompile rather than a failure -- this project has already
1579/// shipped one miscompile from two sites drifting apart.
1580///
1581/// `pub(crate)`, not private: `net.rs` (Task 3) builds sockets' futures the
1582/// same way every future in this file is built, so it needs this constructor
1583/// rather than a second copy of the layout it writes -- the same reason
1584/// [`abort_with`] is `pub(crate)` rather than private. `Wait` and
1585/// [`stage_park`] stay private regardless; a wider `build_future` does not
1586/// let `net.rs` construct a `Wait` or match on one, which is the property
1587/// this task exists to protect (see [`stage_io_park`]).
1588///
1589/// `state` is registered as a GC root across the second allocation, which can
1590/// collect while `state` is named only by this frame. That ordering is the
1591/// subtle part every caller previously had to reproduce.
1592///
1593/// `gc::alloc` returns zeroed memory, so a caller whose only state is a resume
1594/// tag starting at `0` passes an `init` that writes nothing.
1595///
1596/// `state_size` must be at least [`STATE_MIN_SIZE`]: [`STATE_SLOT_OUTPUT`] is
1597/// read unconditionally on completion (`poll_one`'s doc comment), regardless
1598/// of what the future's own poll function does with it, so a smaller state
1599/// object is an out-of-bounds read rather than a caught error. Checked with a
1600/// `debug_assert!` rather than an `abort_with`: every caller passes a
1601/// compile-time constant today, so this can only fire on a caller's own
1602/// mistake, not on Nova input, and release builds already pay for the bug via
1603/// the out-of-bounds read itself if the assert is compiled out.
1604pub(crate) fn build_future(
1605    poll: PollFn,
1606    state_size: usize,
1607    init: impl FnOnce(*mut i64),
1608) -> *mut u8 {
1609    debug_assert!(
1610        state_size >= STATE_MIN_SIZE,
1611        "build_future: state_size {state_size} is smaller than STATE_MIN_SIZE \
1612         ({STATE_MIN_SIZE}); STATE_SLOT_OUTPUT is read unconditionally on \
1613         completion and would be read out of bounds"
1614    );
1615    let state = gc::alloc(state_size, true);
1616    gc::add_root(state);
1617    init(state as *mut i64);
1618    let fat = gc::alloc(FUTURE_SIZE, true);
1619    // SAFETY: `fat` is a live, writable `FUTURE_SIZE` block, so both words
1620    // below are in bounds. Written before the root is released, so the state
1621    // object is reachable from the future by the time it is unrooted.
1622    unsafe {
1623        (fat as *mut i64)
1624            .add(FUTURE_SLOT_POLL)
1625            .write(poll as usize as i64);
1626        (fat as *mut i64).add(FUTURE_SLOT_STATE).write(state as i64);
1627    }
1628    gc::remove_root(state);
1629    fat
1630}
1631
1632/// A fresh `Future<unit>` that pends once and then completes -- what
1633/// `std/task`'s `yield_now` awaits.
1634///
1635/// **The state object is fresh on every call**, not a shared static: the whole
1636/// value carried by one of these futures is its resume tag, so two suspensions
1637/// alive at once would otherwise be one suspension, and the second task to
1638/// poll would find the first task's tag already advanced and complete without
1639/// ever having yielded.
1640///
1641/// Builds exactly the layout [`nova_rt_task_spawn`] documents and
1642/// `async_lower.rs` independently emits, via [`build_future`] -- see its doc
1643/// comment for why that layout now has exactly one home. [`poll_yield_once`]
1644/// is the poll function and [`YIELD_STATE_SIZE`] the state size. Reproducing a
1645/// layout the compiler also emits is a silent miscompile when it is wrong,
1646/// which is why `the_yield_futures_layout_is_the_one_the_abi_declares` asserts
1647/// the tracked `(size, scan)` of both allocations rather than only the words
1648/// written into them.
1649#[no_mangle]
1650pub extern "C-unwind" fn nova_rt_task_yield_future() -> *mut u8 {
1651    // Bound as a `PollFn` before being passed, rather than cast from the
1652    // function item: the coercion is what checks that this function's
1653    // signature *is* the poll ABI.
1654    let poll: PollFn = poll_yield_once;
1655    build_future(poll, YIELD_STATE_SIZE, |_slots| {})
1656}
1657
1658/// Report [`POLL_READY`] once the stored deadline has passed, and
1659/// [`POLL_PENDING`] with the deadline re-staged until then.
1660///
1661/// **Level-triggered and tag-free**, which makes this structurally identical
1662/// to [`poll_join`]. It has to be: since a deadline may accompany any wait and
1663/// two deadlines merge to the earlier, this future can be polled because
1664/// *another* wait's deadline fired. An edge-triggered version -- returning
1665/// ready on its second poll regardless of the clock -- would report a
1666/// completion it had not earned.
1667///
1668/// **Must not unwind**, for the reason [`poll_yield_once`] states. Every
1669/// helper it calls is panic-free by construction; see
1670/// [`instant_from_deadline_nanos`] on why the old `Instant + Duration` was not.
1671unsafe extern "C-unwind" fn poll_sleep(state: *mut u8, _task_ctx: *mut u8) -> i64 {
1672    let slots = state as *mut i64;
1673    // SAFETY: `state` is the state object `nova_rt_task_sleep_future_nanos`
1674    // built, of at least `SLEEP_STATE_SIZE` bytes, so both slots are in bounds.
1675    let deadline = unsafe { slots.add(SLEEP_SLOT_DEADLINE_NANOS).read() };
1676    if crate::time::now_nanos() >= deadline {
1677        // SAFETY: same object, output slot.
1678        unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
1679        return POLL_READY;
1680    }
1681    stage_park(Wait::Deadline(instant_from_deadline_nanos(deadline)));
1682    POLL_PENDING
1683}
1684
1685/// A deadline `nanos` nanoseconds from now, as nanoseconds since
1686/// `crate::time::epoch()`, clamping a non-positive argument to "now".
1687///
1688/// Nova's `Int` is signed and nothing stops `sleep(-1)`; treating it as an
1689/// immediate wake keeps the executor's invariants intact without inventing a
1690/// new failure mode for an argument that is merely useless. Saturating rather
1691/// than wrapping, for the same reason the reading itself saturates.
1692fn deadline_nanos_from_now(nanos: i64) -> i64 {
1693    crate::time::now_nanos().saturating_add(nanos.max(0))
1694}
1695
1696/// A deadline in epoch-nanoseconds as an `Instant`, for staging.
1697///
1698/// **`checked_add`, not `+`.** `Instant + Duration` panics on overflow, and
1699/// this is reached from [`poll_sleep`], a hand-written `PollFn` across which
1700/// no panic may pass. The previous `Instant::now() + Duration::from_nanos(..)`
1701/// carried that panic on a path nothing had ruled out.
1702///
1703/// **Delegates the overflow case to [`furthest_representable_instant`]
1704/// rather than falling back to `Instant::now()`.** An earlier version of this
1705/// function did exactly that, and it traded the panic for a livelock: the
1706/// staged `Instant` would read as immediately due to [`wake_due`]'s
1707/// `deadline <= now` check, but [`poll_sleep`] compares the *stored integer*
1708/// deadline (still far in the future) against the clock on every re-poll, so
1709/// the task would wake, find itself not due by its own arithmetic, and
1710/// re-stage the same broken `Instant::now()` -- forever, burning CPU with no
1711/// progress. See [`furthest_representable_instant`] for why its answer does
1712/// not have that problem.
1713fn instant_from_deadline_nanos(deadline: i64) -> Instant {
1714    let ns = u64::try_from(deadline).unwrap_or(0);
1715    furthest_representable_instant(crate::time::epoch(), Duration::from_nanos(ns))
1716}
1717
1718/// `base + duration`, or the furthest instant from `base` this platform's
1719/// `Instant` can represent, halving `duration` until `checked_add` succeeds.
1720///
1721/// **The guarantee is that this does not answer with something [`wake_due`]
1722/// would treat as already due while the caller's own integer deadline is still
1723/// ahead of the clock** -- the property [`instant_from_deadline_nanos`]'s old
1724/// `Instant::now()` fallback broke. It holds **empirically, not structurally**,
1725/// and the distinction matters enough to state precisely, because an earlier
1726/// version of this comment claimed a proof that does not hold.
1727///
1728/// The false proof was: "never earlier than `base`, and `base` is a fixed point
1729/// in the past, so it can never be already due". That is a non-sequitur.
1730/// "result >= base" and "base is in the past" together permit "result < now",
1731/// which is exactly the already-due case. The loop's actual lower bound is
1732/// `base + H/2`, where `H` is this platform's `Instant` headroom from `base`,
1733/// since halving overshoots by at most 2x. With `H >= uptime` that bound is
1734/// `epoch + uptime/2`, which is *before* `now`.
1735///
1736/// What actually holds, on both counts that matter:
1737///
1738/// - **Headroom vastly exceeds uptime on every supported backend.** `H` is
1739///   measured from a process-start `base`, and no supported `Instant`
1740///   representation runs out within ~292 years of it, so the loop halves once
1741///   or twice at most and lands astronomically far out -- not near `now`.
1742///   `furthest_representable_instant_forces_the_halving_fallback_and_is_not_already_due`
1743///   asserts that, with a first assertion that fails (rather than silently
1744///   passing) if a platform ever stops overflowing on `Duration::MAX`.
1745/// - **The fallback is unreachable through this crate's API at all.** The sole
1746///   caller is [`instant_from_deadline_nanos`], fed by
1747///   [`deadline_nanos_from_now`], which saturates at `i64::MAX` nanoseconds
1748///   (~292 years); `checked_add` of that from a process-start `base` succeeds
1749///   on the first attempt everywhere. Only a test forcing `Duration::MAX`
1750///   directly reaches the loop.
1751///
1752/// **Rejected alternative: clamping the result forward** -- returning
1753/// `max(candidate, Instant::now() + Duration::from_nanos(1))` to make the
1754/// property structural instead of empirical. It looks like a strict
1755/// improvement and it is the opposite: **it reintroduces the exact livelock
1756/// this function exists to prevent.** [`poll_sleep`] re-checks the *stored
1757/// integer* deadline, which in the overflow case is astronomically large, so a
1758/// staged `Instant` of `now + 1ns` makes [`wake_due`] wake the task a
1759/// nanosecond later, `poll_sleep` finds its own deadline unreached, re-stages,
1760/// and the process burns a 1ns-per-iteration busy loop making no progress --
1761/// the same defect the `.unwrap_or_else(Instant::now)` fallback had. The clamp
1762/// fires only in the very case it would have to be right about, and there it is
1763/// wrong. (It would also reintroduce a panicking `Instant + Duration` on a path
1764/// no panic may cross.) Do not add it.
1765///
1766/// Halving only ever *shrinks* an unrepresentable `duration` towards one that
1767/// fits, so the loop always terminates (worst case at `Duration::ZERO`,
1768/// which `checked_add` always accepts, since adding nothing cannot overflow)
1769/// and every candidate it tries on the way stays a duration measured
1770/// forward from `base`, never behind it.
1771///
1772/// The halving loop therefore exists for the input this crate cannot actually
1773/// construct through its own API, not the one it can -- one of this module's
1774/// own tests forces it directly with `Duration::MAX`, a value no caller here
1775/// can ever pass in, since `Instant` itself offers no public way to sit near
1776/// its own overflow boundary the way `Duration::MAX` does. Should a second
1777/// caller ever arrive with an unbounded duration, the empirical argument above
1778/// is what has to be re-checked -- and the clamp is still not the answer.
1779fn furthest_representable_instant(base: Instant, mut duration: Duration) -> Instant {
1780    loop {
1781        if let Some(instant) = base.checked_add(duration) {
1782            return instant;
1783        }
1784        duration /= 2;
1785    }
1786}
1787
1788/// Where `nova_rt_task_sleep_future_nanos` stores its **deadline**, as
1789/// nanoseconds since `crate::time::epoch()`.
1790///
1791/// Renamed from `SLEEP_SLOT_NANOS`, which held a *duration*. The slot is an
1792/// `i64` either way, so changing what the integer means while keeping its
1793/// name would be invisible to the compiler -- the same hazard that made the
1794/// previous increment rename this parker rather than only retype it.
1795const SLEEP_SLOT_DEADLINE_NANOS: usize = STATE_SLOT_TEMPS;
1796
1797/// State size for a sleep future: the ABI minimum plus the one temp slot
1798/// holding the deadline.
1799const SLEEP_STATE_SIZE: usize = STATE_MIN_SIZE + 8;
1800
1801const _: () = assert!(SLEEP_STATE_SIZE >= (SLEEP_SLOT_DEADLINE_NANOS + 1) * 8);
1802
1803/// A fresh `Future<unit>` that parks for `nanos` nanoseconds, then completes.
1804///
1805/// Same layout obligation as [`nova_rt_task_yield_future`]: a scanned
1806/// [`FUTURE_SIZE`] fat pointer over a scanned state object, built to the
1807/// layout `async_lower.rs` independently emits. A fresh state object per call,
1808/// because the deadline is per-suspension.
1809#[no_mangle]
1810pub extern "C-unwind" fn nova_rt_task_sleep_future_nanos(nanos: i64) -> *mut u8 {
1811    let poll: PollFn = poll_sleep;
1812    build_future(poll, SLEEP_STATE_SIZE, |slots| {
1813        // SAFETY: `slots` addresses a live `SLEEP_STATE_SIZE` block, and
1814        // `SLEEP_SLOT_DEADLINE_NANOS` is in bounds by the assertion above.
1815        unsafe {
1816            slots
1817                .add(SLEEP_SLOT_DEADLINE_NANOS)
1818                .write(deadline_nanos_from_now(nanos))
1819        };
1820    })
1821}
1822
1823/// Park until the task driving `target` completes, then complete.
1824///
1825/// Polls to `POLL_READY` immediately if the target is already done, so a join
1826/// on a finished task costs no suspension. Otherwise stages a
1827/// [`Wait::Task`] park; the only thing that clears it is
1828/// [`wake_tasks_waiting_on`] at the target's completion, so one suspension is
1829/// always enough and no loop is needed.
1830///
1831/// **Must not unwind**, as [`poll_sleep`] and [`poll_yield_once`] must not.
1832/// `is_done_internal` borrows `TASKS`, and `poll_one` holds no `TASKS` borrow
1833/// across the `poll` call, so that borrow cannot fail.
1834unsafe extern "C-unwind" fn poll_join(state: *mut u8, _task_ctx: *mut u8) -> i64 {
1835    let slots = state as *mut i64;
1836    // SAFETY: `state` is the object `nova_rt_task_join_future` built, at least
1837    // `JOIN_STATE_SIZE` bytes, so every slot below is in bounds.
1838    let target = unsafe { slots.add(JOIN_SLOT_TARGET).read() };
1839    if is_done_internal(target) {
1840        // SAFETY: same object, output slot.
1841        unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
1842        return POLL_READY;
1843    }
1844    stage_park(Wait::Task {
1845        id: target,
1846        deadline: None,
1847    });
1848    POLL_PENDING
1849}
1850
1851/// Where the target task id lives in a join future's state object.
1852const JOIN_SLOT_TARGET: usize = STATE_SLOT_TEMPS;
1853
1854/// State size for a join future: the ABI minimum plus the target-id slot.
1855const JOIN_STATE_SIZE: usize = STATE_MIN_SIZE + 8;
1856
1857const _: () = assert!(JOIN_STATE_SIZE >= (JOIN_SLOT_TARGET + 1) * 8);
1858
1859/// A fresh `Future<unit>` that completes when `future`'s task does.
1860///
1861/// Resolves `future` to a task id through the same `BY_STATE` path
1862/// `nova_rt_task_is_done` uses, so a future no task was spawned for aborts
1863/// there rather than becoming a park nothing can wake. The id is resolved
1864/// *once*, here, rather than on every poll: the target cannot change, and a
1865/// stale-address abort belongs at the call the Nova program made.
1866///
1867/// # Safety
1868/// `future` must point to a live `FUTURE_SIZE` `{ poll_code, state }` block.
1869#[no_mangle]
1870pub unsafe extern "C-unwind" fn nova_rt_task_join_future(future: *mut u8) -> *mut u8 {
1871    // SAFETY: forwarding this function's own contract.
1872    let target = unsafe { task_id_of(future, "nova_rt_task_join_future") };
1873    let poll: PollFn = poll_join;
1874    build_future(poll, JOIN_STATE_SIZE, |slots| {
1875        // SAFETY: `slots` addresses a live `JOIN_STATE_SIZE` block, and
1876        // `JOIN_SLOT_TARGET` is in bounds by the assertion above.
1877        unsafe { slots.add(JOIN_SLOT_TARGET).write(target) };
1878    })
1879}
1880
1881/// Where a timeout future stores the inner future's `{ poll_code, state }`
1882/// fat pointer.
1883///
1884/// The state object is scanned (`build_future` allocates with
1885/// `gc::alloc(size, true)`), so storing this pointer is the whole of the
1886/// rooting: it keeps the inner future and, transitively, its state reachable
1887/// for exactly as long as the timeout future itself.
1888const TIMEOUT_SLOT_INNER: usize = STATE_SLOT_TEMPS;
1889
1890/// Where a timeout future stores its deadline, as nanoseconds since
1891/// `crate::time::epoch()` -- the same encoding `SLEEP_SLOT_DEADLINE_NANOS` uses.
1892const TIMEOUT_SLOT_DEADLINE_NANOS: usize = STATE_SLOT_TEMPS + 1;
1893
1894/// State size for a timeout future: the ABI minimum plus two temp slots.
1895const TIMEOUT_STATE_SIZE: usize = STATE_MIN_SIZE + 16;
1896
1897const _: () = assert!(TIMEOUT_STATE_SIZE >= (TIMEOUT_SLOT_DEADLINE_NANOS + 1) * 8);
1898
1899/// The inner future completed before the deadline.
1900pub const TIMEOUT_STATUS_COMPLETED: i64 = 0;
1901
1902/// The deadline passed with the inner future still pending.
1903pub const TIMEOUT_STATUS_ELAPSED: i64 = 1;
1904
1905/// Poll the inner future, then this timeout's own deadline.
1906///
1907/// **That order is deliberate.** Polling the inner first means work that
1908/// completed is never reported as timed out, and it makes a zero-duration
1909/// timeout over an already-ready future succeed -- the least surprising
1910/// answer. The order is only *available* because `poll_sleep` is
1911/// level-triggered: with an edge-triggered sleep, a woken sleep could report a
1912/// completion it had not earned, forcing a deadline-first check as a defence.
1913///
1914/// **Abandonment takes two lines, not none.** This function was first written
1915/// on the claim that it needed none: when the inner returns [`POLL_PENDING`]
1916/// and this function then returns [`POLL_READY`], the inner has already staged
1917/// a park, and [`poll_one`] takes `PENDING_PARK` unconditionally. That
1918/// mechanism is real, but it fires **once per task poll, not once per
1919/// future**, which is not what abandonment needs. A generated state machine
1920/// advances through as many awaits as complete in one poll, so an abandoned
1921/// inner's park sits in `PENDING_PARK` and the *next* suspension in that same
1922/// task poll stages against it. A [`Wait::Deadline`] merges harmlessly; every
1923/// other pairing reaches [`abort_with`] through [`try_stage`], with a message
1924/// blaming an inner future's `POLL_PENDING` for something it did not do.
1925/// `timeout(d, h.join())` followed by any other parking suspension aborted the
1926/// process for exactly that reason.
1927///
1928/// So `PENDING_PARK` is snapshotted before the inner is polled and
1929/// **restored** -- not cleared to `Staged::default()` -- on both
1930/// [`POLL_READY`] exits. Restoring the snapshot keeps the contract *local*:
1931/// this function hands the slot back exactly as it found it, so it composes
1932/// with a nested timeout and with an earlier abandonment in the same task
1933/// poll, where clearing would silently discard a park this function never
1934/// staged. A local invariant is also the one that survives the next
1935/// combinator: `select`/`race` will have this same shape. `Staged` is `Copy`
1936/// in a `Cell`, so neither the read nor the write can fail -- keep it that
1937/// way, since no panic may cross this boundary.
1938///
1939/// The pending path deliberately leaves the slot alone. There the inner's park
1940/// must survive to merge with this timeout's own deadline, which is the entire
1941/// point of the staging widening.
1942///
1943/// The test-only `poll_once_for_test` drains the slot unconditionally for the
1944/// same hazard seen from the other side; its own doc comment records why.
1945///
1946/// The abandonment *contract* is unchanged by any of this: abandonment is
1947/// still not cancellation, still free for `sleep`/`join`/`read`/`write`, and
1948/// still leaks a socket for `connect` (`std/time`'s own doc comment, and §5 of
1949/// this increment's design). Only the claim that it needed no code was false.
1950///
1951/// **Must not unwind.** Every slot access is a raw read with the SAFETY note
1952/// below; an out-of-range status from the inner poll goes to [`abort_with`],
1953/// the same route a staging collision takes, never `panic!`.
1954unsafe extern "C-unwind" fn poll_timeout(state: *mut u8, _task_ctx: *mut u8) -> i64 {
1955    let slots = state as *mut i64;
1956    // SAFETY: `state` is the object `nova_rt_task_timeout_future` built, at
1957    // least `TIMEOUT_STATE_SIZE` bytes, so every slot below is in bounds.
1958    let inner = unsafe { slots.add(TIMEOUT_SLOT_INNER).read() } as *mut u8;
1959    // SAFETY: same object.
1960    let deadline = unsafe { slots.add(TIMEOUT_SLOT_DEADLINE_NANOS).read() };
1961
1962    // SAFETY: `inner` is the fat pointer written at construction, so word 0 is
1963    // its poll function and word 1 its state -- the layout `build_future`
1964    // guarantees and `async_lower.rs` independently emits.
1965    let inner_poll_addr = unsafe { (inner as *mut usize).add(FUTURE_SLOT_POLL).read() };
1966    // SAFETY: that word is a `PollFn` bit pattern by the inner future's own
1967    // construction; a fn pointer and a `usize` are both pointer-width.
1968    let inner_poll: PollFn = unsafe { std::mem::transmute(inner_poll_addr) };
1969    // SAFETY: same fat pointer, state word.
1970    let inner_state = unsafe { (inner as *mut usize).add(FUTURE_SLOT_STATE).read() } as *mut u8;
1971
1972    // Whatever this task poll had already staged before the inner ran, so both
1973    // completing exits below can hand the slot back exactly as they found it.
1974    // Cannot fail: `Staged` is `Copy` in a `Cell` (see the doc comment above).
1975    let park_before_inner = PENDING_PARK.with(Cell::get);
1976
1977    // SAFETY: `inner_poll`/`inner_state` are the pair inside `inner`;
1978    // `task_ctx` is always null, matching every other call site in this crate.
1979    let inner_status = unsafe { inner_poll(inner_state, std::ptr::null_mut()) };
1980    if inner_status == POLL_READY {
1981        PENDING_PARK.with(|cell| cell.set(park_before_inner));
1982        // SAFETY: same object, output slot.
1983        unsafe { slots.add(STATE_SLOT_OUTPUT).write(TIMEOUT_STATUS_COMPLETED) };
1984        return POLL_READY;
1985    }
1986    if inner_status != POLL_PENDING {
1987        abort_with(&format!(
1988            "nova_rt: a future polled by a timeout returned {inner_status}, which is \
1989             neither POLL_PENDING ({POLL_PENDING}) nor POLL_READY ({POLL_READY})"
1990        ));
1991    }
1992    if crate::time::now_nanos() >= deadline {
1993        // The abandonment path: the inner just parked and is about to be
1994        // dropped on the floor, so its park goes with it.
1995        PENDING_PARK.with(|cell| cell.set(park_before_inner));
1996        // SAFETY: same object, output slot.
1997        unsafe { slots.add(STATE_SLOT_OUTPUT).write(TIMEOUT_STATUS_ELAPSED) };
1998        return POLL_READY;
1999    }
2000    stage_park(Wait::Deadline(instant_from_deadline_nanos(deadline)));
2001    POLL_PENDING
2002}
2003
2004/// A fresh `Future<Int>` that polls `fut` until it completes or `nanos`
2005/// nanoseconds pass, reporting which happened.
2006///
2007/// The value is **not** carried here: the inner future wrote its own output
2008/// slot, and Nova reads it with `task_output` on the inner future itself, so
2009/// nothing moves an `i64` that might be a scalar or a pointer.
2010#[no_mangle]
2011pub extern "C-unwind" fn nova_rt_task_timeout_future(nanos: i64, fut: *mut u8) -> *mut u8 {
2012    let poll: PollFn = poll_timeout;
2013    let deadline = deadline_nanos_from_now(nanos);
2014    build_future(poll, TIMEOUT_STATE_SIZE, |slots| {
2015        // SAFETY: `slots` addresses a live `TIMEOUT_STATE_SIZE` block, and both
2016        // slots are in bounds by the assertion above.
2017        unsafe { slots.add(TIMEOUT_SLOT_INNER).write(fut as i64) };
2018        unsafe { slots.add(TIMEOUT_SLOT_DEADLINE_NANOS).write(deadline) };
2019    })
2020}
2021
2022#[cfg(test)]
2023mod tests {
2024    use super::*;
2025    use std::time::Duration;
2026
2027    /// A hand-written poll function shaped exactly like the ones
2028    /// `async_lower.rs` will generate: it reads and writes the resume tag in
2029    /// slot 0, writes its result to slot 1, and returns PENDING/READY.
2030    ///
2031    /// Suspends once, then completes with 42.
2032    unsafe extern "C-unwind" fn poll_suspend_once(state: *mut u8, _ctx: *mut u8) -> i64 {
2033        let slots = state as *mut i64;
2034        // SAFETY: `state` is a live state object with at least
2035        // `STATE_SLOT_TEMPS` slots (`make_future`'s contract).
2036        let tag = unsafe { *slots.add(STATE_SLOT_TAG) };
2037        if tag == 0 {
2038            unsafe { *slots.add(STATE_SLOT_TAG) = 1 };
2039            POLL_PENDING
2040        } else {
2041            unsafe { *slots.add(STATE_SLOT_OUTPUT) = 42 };
2042            POLL_READY
2043        }
2044    }
2045
2046    unsafe extern "C-unwind" fn poll_ready_now(state: *mut u8, _ctx: *mut u8) -> i64 {
2047        // SAFETY: `state` is a live state object (`make_future`'s contract).
2048        unsafe { *(state as *mut i64).add(STATE_SLOT_OUTPUT) = 7 };
2049        POLL_READY
2050    }
2051
2052    /// Build the `{ poll_code, state_ptr }` fat pointer the compiler emits.
2053    ///
2054    /// `#[inline(never)]`: `root_registration`'s tests need this call's own
2055    /// `state`/`fat` locals confined to a call frame that returns (and can
2056    /// go dead) before the collection they provoke, matching the same
2057    /// requirement `gc.rs`'s `setup_parent_and_child` documents. Inlined,
2058    /// this function's locals would become anonymous temporaries inside the
2059    /// *caller's* frame, under names the caller has no way to null out.
2060    #[inline(never)]
2061    fn make_future(f: PollFn, temps: usize) -> *mut u8 {
2062        // Exactly `nova_rt_task_spawn`'s documented minimum: scanned, and
2063        // `(STATE_SLOT_TEMPS + temps) * 8` bytes so `STATE_SLOT_OUTPUT` is in
2064        // bounds even at `temps == 0`.
2065        let state = gc::alloc((STATE_SLOT_TEMPS + temps) * 8, true);
2066        let fat = gc::alloc(FUTURE_SIZE, true);
2067        unsafe {
2068            (fat as *mut usize).add(FUTURE_SLOT_POLL).write(f as usize);
2069            (fat as *mut usize)
2070                .add(FUTURE_SLOT_STATE)
2071                .write(state as usize);
2072        }
2073        fat
2074    }
2075
2076    /// Read a future's state-object address back out of its fat pointer.
2077    fn state_of(future: *mut u8) -> usize {
2078        // SAFETY: `future` is a `make_future` result, so word
2079        // `FUTURE_SLOT_STATE` is the state object's address.
2080        unsafe { (future as *mut usize).add(FUTURE_SLOT_STATE).read() }
2081    }
2082
2083    /// Poll a future once under a borrowed task context, draining any park it
2084    /// staged.
2085    ///
2086    /// The park must be drained: `stage_park` aborts the process when a second
2087    /// deadline is staged over a first, so a leftover entry kills a later test
2088    /// under `--test-threads=1`.
2089    ///
2090    /// That is the same hazard [`poll_timeout`] has to handle in production,
2091    /// seen from the other side -- and the reason this helper drains
2092    /// *unconditionally*, rather than only when the poll came back
2093    /// `POLL_PENDING`, is precisely that a poll returning `POLL_READY` can
2094    /// still have left something staged. This helper may therefore not be used
2095    /// by any test that needs to read the slot after the poll; those tests
2096    /// write the raw poll idiom out longhand instead.
2097    #[cfg(test)]
2098    fn poll_once_for_test(fut: *mut u8) -> i64 {
2099        let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
2100        // SAFETY: word 0 is a `PollFn` bit pattern by `fut`'s own construction.
2101        let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
2102        let state = state_of(fut) as *mut u8;
2103        let previous = current_task();
2104        set_current_for_test(Some(0));
2105        // SAFETY: `poll`/`state` are the pair inside `fut`; `task_ctx` is null.
2106        let status = unsafe { poll(state, std::ptr::null_mut()) };
2107        PENDING_PARK.with(|cell| {
2108            cell.take();
2109        });
2110        set_current_for_test(previous);
2111        status
2112    }
2113
2114    /// The value a future wrote to its own output slot.
2115    #[cfg(test)]
2116    fn output_of_for_test(fut: *mut u8) -> i64 {
2117        let slots = state_of(fut) as *mut i64;
2118        // SAFETY: every future this module builds is at least
2119        // `STATE_MIN_SIZE`, so the output slot is in bounds.
2120        unsafe { slots.add(STATE_SLOT_OUTPUT).read() }
2121    }
2122
2123    /// Build a future with a `STATE_MIN_SIZE` state and nothing in it but the
2124    /// tag and output slots, which `gc::alloc`'s zeroing already leaves at
2125    /// `0`. A one-liner over [`build_future`] rather than a hand-rolled
2126    /// layout, so the park-set tests below go through the same construction
2127    /// path every other future in the system does.
2128    fn test_future(poll: PollFn) -> *mut u8 {
2129        build_future(poll, STATE_MIN_SIZE, |_| {})
2130    }
2131
2132    /// Run `f` with [`PARKED`] set to exactly `entries` for its duration,
2133    /// restoring whatever was there before once `f` returns.
2134    ///
2135    /// The precedent for splitting a diagnostic this way is
2136    /// [`deadlock_report`] itself: separating the text-only half of a
2137    /// diagnostic from the half that ends the process (`report_deadlock`) is
2138    /// what lets a test assert on it at all. This does the analogous job for
2139    /// the fixture a test needs `PARKED` to hold -- seeding it directly
2140    /// rather than driving a real `spawn_internal`/`poll_one` sequence just
2141    /// to get a specific `Wait` into the park set, and restoring the prior
2142    /// contents afterward rather than assuming `PARKED` started empty (the
2143    /// way this file's other `PARKED.with(|p| p.borrow_mut().clear())`
2144    /// tests do), so a test using this helper composes safely even nested
2145    /// inside one that does not.
2146    fn with_parked<T>(entries: &[(i64, Wait)], f: impl FnOnce() -> T) -> T {
2147        let previous = PARKED.with(|p| p.replace(entries.to_vec()));
2148        let result = f();
2149        PARKED.with(|p| *p.borrow_mut() = previous);
2150        result
2151    }
2152
2153    #[test]
2154    fn block_on_runs_a_ready_future_and_returns_its_output() {
2155        let fut = make_future(poll_ready_now, 0);
2156        assert_eq!(unsafe { nova_rt_task_block_on(fut) }, 7);
2157    }
2158
2159    #[test]
2160    fn block_on_re_polls_a_pending_future_until_it_completes() {
2161        // Discriminates a real re-queue from "poll once and return whatever
2162        // is in the output slot", which would return 0 here.
2163        let fut = make_future(poll_suspend_once, 0);
2164        assert_eq!(unsafe { nova_rt_task_block_on(fut) }, 42);
2165    }
2166
2167    #[test]
2168    fn a_spawned_task_runs_to_completion_and_reports_done() {
2169        let fut = make_future(poll_suspend_once, 0);
2170        let id = unsafe { nova_rt_task_spawn(fut) };
2171        // Not done before the executor has run it at all.
2172        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 0);
2173        let root = make_future(poll_ready_now, 0);
2174        unsafe { nova_rt_task_block_on(root) };
2175        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2176        assert_eq!(output_bits(id), 42);
2177    }
2178
2179    /// `output_bits` refuses a task that has not completed: its `output`
2180    /// field still holds the `0` initializer, which is indistinguishable from
2181    /// a genuine `0`.
2182    #[test]
2183    fn output_bits_of_an_unfinished_task_panics() {
2184        let fut = make_future(poll_suspend_once, 0);
2185        let id = unsafe { nova_rt_task_spawn(fut) };
2186        let r = std::panic::catch_unwind(|| output_bits(id));
2187        assert!(r.is_err(), "output_bits must refuse an unfinished task");
2188    }
2189
2190    /// The lifetime of a *kept* task's GC root -- `block_on`'s root, spawned
2191    /// through `spawn_root_internal` -- asserted on the registry directly
2192    /// rather than through a collection. An ordinary spawned task's root is
2193    /// released at completion instead; that is
2194    /// `a_spawned_tasks_root_is_released_at_completion`, below.
2195    ///
2196    /// A heap-valued output (a `String`, record or sum returned by an
2197    /// `async fn`) is reachable from exactly one place once its task
2198    /// completes: [`STATE_SLOT_OUTPUT`] of the state object. `Task::output`'s
2199    /// copy roots nothing -- `TASKS` is a Rust-heap `Vec`, which is none of
2200    /// the collector's root sources. So unrooting the state at completion
2201    /// (which is what this executor originally did) leaves the output
2202    /// collectable from the moment the task finishes until
2203    /// `nova_rt_task_take_output` is called, and *anything* that allocates in
2204    /// between -- polling the next task in the queue, most obviously -- can
2205    /// trip the threshold and free it. `run_to_completion` returns exactly
2206    /// that value, for `block_on`'s root.
2207    ///
2208    /// Asserted here on `gc::root_count` rather than by allocating until a
2209    /// collection fires and checking the payload survived: the registry is
2210    /// where the invariant actually lives, and a `collect()`-based version
2211    /// would inherit the conservative scan's intermittent over-retention
2212    /// (`docs/adr/0010-conservative-scan-root-test-gating.md`) and could pass
2213    /// on an accidental stack root even with the rooting deleted. The other
2214    /// half of the argument -- that a `PINNED` root is traced *through*, so a
2215    /// pointer in the state object's output slot is marked too -- is
2216    /// `gc.rs`'s `a_registered_root_keeps_its_transitive_children_alive`.
2217    ///
2218    /// The counts are asserted exactly, not merely as non-zero, so this also
2219    /// discriminates a double `add_root` or a missing `remove_root` rather
2220    /// than only a missing root: releasing a kept task's root in `poll_one`
2221    /// on completion fails the middle assertion, never releasing it fails the
2222    /// last, and not registering it in `spawn_internal` at all fails the
2223    /// first.
2224    #[test]
2225    fn a_completed_tasks_state_stays_rooted_until_its_output_is_taken() {
2226        let fut = make_future(poll_ready_now, 0);
2227        let state = state_of(fut);
2228        let id = unsafe { spawn_root_internal(fut) };
2229        assert_eq!(
2230            gc::root_count(state),
2231            1,
2232            "spawn must register the state object exactly once"
2233        );
2234
2235        // Drive the queue to empty, which completes the spawned task.
2236        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2237        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2238        assert_eq!(
2239            gc::root_count(state),
2240            1,
2241            "a completed task's state must stay rooted until its output is \
2242             taken -- the output value lives in the state object's own output \
2243             slot, and Task::output's copy is in a Rust-heap Vec the \
2244             collector never scans"
2245        );
2246
2247        assert_eq!(unsafe { nova_rt_task_take_output(id) }, 7);
2248        assert_eq!(
2249            gc::root_count(state),
2250            0,
2251            "take_output must release the state object's root exactly once, \
2252             so add_root/remove_root stay balanced"
2253        );
2254    }
2255
2256    /// An ordinary spawned task's GC root is released the moment it
2257    /// completes, not when its output is taken -- which, for a `spawn`
2258    /// whose handle is dropped, is never. Asserted on the registry, for the
2259    /// reason `a_completed_tasks_state_stays_rooted_until_its_output_is_taken`
2260    /// documents. The `TASKS` entry survives the release: `is_done` still
2261    /// answers, and the output copy is still there.
2262    #[test]
2263    fn a_spawned_tasks_root_is_released_at_completion() {
2264        let fut = make_future(poll_ready_now, 0);
2265        let state = state_of(fut);
2266        let id = unsafe { nova_rt_task_spawn(fut) };
2267        assert_eq!(
2268            gc::root_count(state),
2269            1,
2270            "spawn must register the state object exactly once"
2271        );
2272
2273        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2274        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2275        assert_eq!(
2276            gc::root_count(state),
2277            0,
2278            "a spawned task's root must be released at completion, so a \
2279             handle nobody joins does not keep its state alive"
2280        );
2281        assert_eq!(output_bits(id), 7, "the TASKS entry keeps its output copy");
2282    }
2283
2284    /// Releasing at completion goes through `release_internal`, so it
2285    /// releases the task's stashed `fs` payloads too -- an undrained error
2286    /// message must not outlive a task nobody joins.
2287    #[test]
2288    fn a_spawned_tasks_stashed_fs_payload_is_released_at_completion() {
2289        let fut = make_future(poll_ready_now, 0);
2290        let id = unsafe { nova_rt_task_spawn(fut) };
2291        let payload = crate::gc_str("release-at-completion-payload");
2292        let addr = payload as usize;
2293        crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, payload);
2294        assert_eq!(
2295            gc::root_count(addr),
2296            1,
2297            "stash_for_test must root its pointer"
2298        );
2299
2300        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2301        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2302        assert_eq!(
2303            gc::root_count(addr),
2304            0,
2305            "completion must release the task's stashed fs payload, not only \
2306             its state root"
2307        );
2308    }
2309
2310    /// A spawned task's output cannot be taken once it has completed: its
2311    /// root is already gone, so the bits could name a freed object. The
2312    /// panic must say so, rather than claim a second take happened.
2313    #[test]
2314    fn taking_a_spawned_tasks_output_after_completion_panics_naming_the_release() {
2315        let fut = make_future(poll_ready_now, 0);
2316        let id = unsafe { nova_rt_task_spawn(fut) };
2317        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2318
2319        let r = std::panic::catch_unwind(|| unsafe { nova_rt_task_take_output(id) });
2320        let msg = match r {
2321            Ok(v) => panic!("take must panic for a released spawned task, got {v}"),
2322            Err(e) => e.downcast_ref::<String>().cloned().unwrap_or_default(),
2323        };
2324        assert!(
2325            msg.contains("released when it completes"),
2326            "the panic must name the cause: {msg}"
2327        );
2328    }
2329
2330    /// The behaviour change the spec records (section 3.4): a completed
2331    /// spawned task has been released, so spawning its future again is
2332    /// allowed, as it already was after `join`. It re-polls the completed
2333    /// state machine -- the footgun ADR 0009 accepts for that shape.
2334    #[test]
2335    fn spawning_a_completed_unjoined_future_again_succeeds() {
2336        let fut = make_future(poll_ready_now, 0);
2337        let first = unsafe { nova_rt_task_spawn(fut) };
2338        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2339        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2340
2341        let second = unsafe { nova_rt_task_spawn(fut) };
2342        assert_ne!(first, second, "a re-spawn registers a new task");
2343        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2344        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2345    }
2346
2347    std::thread_local! {
2348        static OBSERVED_ROOT_STATE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2349        static OBSERVED_ROOT_COUNT: std::cell::Cell<i64> = const { std::cell::Cell::new(-1) };
2350    }
2351
2352    /// Records how many times the `block_on` root's state is registered, at
2353    /// the moment this task is polled -- which is after the root completed.
2354    unsafe extern "C-unwind" fn poll_observe_root(_state: *mut u8, _ctx: *mut u8) -> i64 {
2355        let root_state = OBSERVED_ROOT_STATE.with(|c| c.get());
2356        OBSERVED_ROOT_COUNT.with(|c| c.set(gc::root_count(root_state) as i64));
2357        POLL_READY
2358    }
2359
2360    /// A `block_on` root that spawns an observer and completes on its first
2361    /// poll, with output 7.
2362    unsafe extern "C-unwind" fn poll_spawn_observer_then_ready(
2363        state: *mut u8,
2364        _ctx: *mut u8,
2365    ) -> i64 {
2366        OBSERVED_ROOT_STATE.with(|c| c.set(state as usize));
2367        let observer = make_future(poll_observe_root, 0);
2368        // SAFETY: `observer` is a fresh `make_future` result.
2369        unsafe { nova_rt_task_spawn(observer) };
2370        // SAFETY: `state` is a live state object (`make_future`'s contract).
2371        unsafe { *(state as *mut i64).add(STATE_SLOT_OUTPUT) = 7 };
2372        POLL_READY
2373    }
2374
2375    /// `block_on`'s root keeps its root after completing, until
2376    /// `run_to_completion` takes its output. An observer task, spawned by the
2377    /// root and polled after it (`run_to_completion` drains the whole queue),
2378    /// sees the count while the root is complete but not yet taken. A guard,
2379    /// not a failing-first test: it passes before and after the fix, and
2380    /// fails under the mutation that releases regardless of the flag.
2381    #[test]
2382    fn a_block_on_roots_state_stays_rooted_until_taken() {
2383        let root = make_future(poll_spawn_observer_then_ready, 0);
2384        let root_state = state_of(root);
2385        assert_eq!(unsafe { nova_rt_task_block_on(root) }, 7);
2386        assert_eq!(
2387            OBSERVED_ROOT_COUNT.with(|c| c.get()),
2388            1,
2389            "the block_on root's state must stay rooted between its \
2390             completion and run_to_completion's take"
2391        );
2392        assert_eq!(
2393            gc::root_count(root_state),
2394            0,
2395            "run_to_completion's take must release it exactly once"
2396        );
2397    }
2398
2399    /// `release_internal` must release a task's stashed `fs` payload, not
2400    /// only its own state root.
2401    ///
2402    /// Calls `release_internal` directly -- it is private to this module,
2403    /// and this test lives in it -- rather than through the public
2404    /// `nova_rt_task_release`, and seeds the payload with
2405    /// `crate::fs::stash_for_test` rather than by driving a real poll:
2406    /// `release_internal` has no precondition on `Task::done` (only on
2407    /// `Task::taken`, which a freshly spawned task has not set), so a task
2408    /// that was spawned -- and therefore queued, by `spawn_internal` -- but
2409    /// never actually dequeued and polled already exercises it fully. This
2410    /// is what actually pins `release_internal`'s call into `fs.rs`, unlike
2411    /// `fs::tests::releasing_a_tasks_slots_drops_the_roots_it_held`, which
2412    /// calls `crate::fs::release_task_slots` directly and so cannot see
2413    /// whether `release_internal` still makes that call at all.
2414    #[test]
2415    fn releasing_a_task_also_releases_its_stashed_fs_payload() {
2416        let fut = make_future(poll_ready_now, 0);
2417        let id = unsafe { nova_rt_task_spawn(fut) };
2418
2419        let payload = crate::gc_str("release-internal-payload");
2420        let addr = payload as usize;
2421        crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, payload);
2422        assert_eq!(
2423            gc::root_count(addr),
2424            1,
2425            "stash_for_test must root its pointer"
2426        );
2427
2428        release_internal(id);
2429        assert_eq!(
2430            gc::root_count(addr),
2431            0,
2432            "release_internal must release the task's stashed fs payload, \
2433             not only its own state root"
2434        );
2435    }
2436
2437    /// `take_output_internal` must release a task's stashed `fs` payload,
2438    /// not only its own state root.
2439    ///
2440    /// Unlike `release_internal`'s sibling test above, `take_output_internal`
2441    /// asserts `Task::done`, so this drives one real `nova_rt_task_block_on`
2442    /// pass first to let `poll_one` mark the task done, and only stashes the
2443    /// payload afterward -- the ordering does not matter to `fs.rs`'s slot
2444    /// table, which knows nothing about task completion, but the task must
2445    /// be done before `take_output_internal` will accept it at all. Calls
2446    /// `take_output_internal` directly for the same reason the sibling test
2447    /// calls `release_internal` directly: only that pins the call into
2448    /// `fs.rs`, rather than `crate::fs::release_task_slots` itself, which
2449    /// `fs::tests::releasing_one_tasks_slots_leaves_another_tasks_intact`
2450    /// already covers. Spawned through `spawn_root_internal`, because only a
2451    /// kept task's output can be taken; an ordinary spawned task's root is
2452    /// released at completion.
2453    #[test]
2454    fn taking_a_tasks_output_also_releases_its_stashed_fs_payload() {
2455        let fut = make_future(poll_ready_now, 0);
2456        let id = unsafe { spawn_root_internal(fut) };
2457        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2458        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2459
2460        let payload = crate::gc_str("take-output-internal-payload");
2461        let addr = payload as usize;
2462        crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, payload);
2463        assert_eq!(
2464            gc::root_count(addr),
2465            1,
2466            "stash_for_test must root its pointer"
2467        );
2468
2469        assert_eq!(
2470            take_output_internal(id),
2471            7,
2472            "poll_ready_now's own output, unaffected by the stashed fs payload"
2473        );
2474        assert_eq!(
2475            gc::root_count(addr),
2476            0,
2477            "take_output_internal must release the task's stashed fs \
2478             payload, not only its own state root"
2479        );
2480    }
2481
2482    /// `release_internal` must release all three of a task's stashed `fs`
2483    /// payload kinds, not only the one kind (`Buffer`) the two tests above
2484    /// happen to seed (final review, I2).
2485    ///
2486    /// **Mutation, confirmed to survive before this test existed** (measured
2487    /// directly against `3037908`, in an isolated worktree, apart from just
2488    /// re-running the review's own report): `release_task_slots` collecting
2489    /// `[entry.buffer, 0, 0]` instead of
2490    /// `[entry.buffer, entry.array, entry.message]` passed all 78
2491    /// `nova-runtime` lib tests. Every test that stashed a payload for the
2492    /// *same task* it then released seeded only `Slot::Buffer` -- both tests
2493    /// above do, and `stash_for_test` hardcoded `Buffer` until this test
2494    /// needed otherwise.
2495    /// `fs::tests::releasing_one_tasks_slots_leaves_another_tasks_intact`
2496    /// does stash into `Slot::Message`, but for a *different* task than the
2497    /// one it releases, so it asserts that unrelated root is left alone, not
2498    /// that a same-task `Message`/`Array` root is actually dropped. This is
2499    /// what an undrained OS error message (`fail` stashes one on every
2500    /// failure path) or an undrained `read_dir` array would ride on to keep
2501    /// its root registered for the process lifetime.
2502    #[test]
2503    fn releasing_a_task_releases_all_three_stashed_fs_slot_kinds() {
2504        let fut = make_future(poll_ready_now, 0);
2505        let id = unsafe { nova_rt_task_spawn(fut) };
2506
2507        let buffer = crate::gc_str("all-three-buffer-payload");
2508        let array = crate::gc_str("all-three-array-payload");
2509        let message = crate::gc_str("all-three-message-payload");
2510        let addrs = [buffer as usize, array as usize, message as usize];
2511
2512        crate::fs::stash_for_test(id, crate::fs::Slot::Buffer, buffer);
2513        crate::fs::stash_for_test(id, crate::fs::Slot::Array, array);
2514        crate::fs::stash_for_test(id, crate::fs::Slot::Message, message);
2515        for addr in addrs {
2516            assert_eq!(
2517                gc::root_count(addr),
2518                1,
2519                "stash_for_test must root each of the three payloads"
2520            );
2521        }
2522
2523        release_internal(id);
2524        for addr in addrs {
2525            assert_eq!(
2526                gc::root_count(addr),
2527                0,
2528                "release_internal must release all three of the task's \
2529                 stashed fs payload roots -- buffer, array and message alike \
2530                 -- not only whichever kind a caller happened to seed"
2531            );
2532        }
2533    }
2534
2535    /// `take_output` is a take, not a peek: the second call must be diagnosed,
2536    /// because the first one released the root that was keeping a heap-valued
2537    /// output alive, so the bits it would hand back a second time may name a
2538    /// freed object. Spawned through `spawn_root_internal`, because only a
2539    /// kept task's output can be taken; an ordinary spawned task's root is
2540    /// released at completion.
2541    #[test]
2542    fn taking_an_output_twice_panics_rather_than_returning_stale_bits() {
2543        let fut = make_future(poll_ready_now, 0);
2544        let id = unsafe { spawn_root_internal(fut) };
2545        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2546        assert_eq!(unsafe { nova_rt_task_take_output(id) }, 7);
2547
2548        let r = std::panic::catch_unwind(|| unsafe { nova_rt_task_take_output(id) });
2549        assert!(r.is_err(), "a second take must panic, not succeed");
2550    }
2551
2552    /// Taking the output of a task that has not finished would return the
2553    /// `output: 0` initializer, indistinguishable from a task that genuinely
2554    /// completed with `0`. `JoinHandle::join` is specified to poll
2555    /// `nova_rt_task_is_done` first, so reaching this is a caller bug and is
2556    /// diagnosed as one.
2557    #[test]
2558    fn taking_the_output_of_an_unfinished_task_panics() {
2559        let fut = make_future(poll_suspend_once, 0);
2560        let id = unsafe { nova_rt_task_spawn(fut) };
2561        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 0);
2562
2563        let r = std::panic::catch_unwind(|| unsafe { nova_rt_task_take_output(id) });
2564        assert!(
2565            r.is_err(),
2566            "taking an unfinished task's output must panic, not return 0"
2567        );
2568    }
2569
2570    /// A poll function returning something that is neither [`POLL_PENDING`]
2571    /// nor [`POLL_READY`] is a codegen bug. This module exists to pin the poll
2572    /// ABI *before* codegen depends on it, so the status is checked against
2573    /// both constants rather than "anything not pending is ready" -- otherwise
2574    /// a generated poll returning `2` silently completes the task with
2575    /// whatever happens to be in the output slot.
2576    #[test]
2577    fn an_out_of_range_poll_status_panics_rather_than_completing_the_task() {
2578        unsafe extern "C-unwind" fn poll_bogus_status(_s: *mut u8, _c: *mut u8) -> i64 {
2579            2
2580        }
2581        let fut = make_future(poll_bogus_status, 0);
2582        let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
2583            nova_rt_task_block_on(fut)
2584        }));
2585        let payload = r.expect_err("an unknown poll status must panic");
2586        let msg = payload
2587            .downcast_ref::<String>()
2588            .map(String::as_str)
2589            .unwrap_or("");
2590        assert!(
2591            msg.contains("returned 2"),
2592            "the panic must name the offending status, got {msg:?}"
2593        );
2594    }
2595
2596    #[test]
2597    fn two_pending_tasks_interleave_round_robin() {
2598        // The determinism the sub-phase gate depends on. Records the order
2599        // poll calls actually happen in, and asserts an ALTERNATING order --
2600        // not merely that both ran, which a run-to-completion scheduler would
2601        // also satisfy while producing different output in the gate fixture.
2602        // Thread-local, not a process-global `Mutex`: the executor whose order
2603        // this records is itself thread-local (`QUEUE`/`TASKS` above), so a
2604        // process-global recorder would let any concurrently running test that
2605        // drove these same poll functions interleave into the sequence being
2606        // asserted. Nothing else uses `poll_a`/`poll_b` today, which is why
2607        // the global version passed; the scope of the recorder should still
2608        // match the scope of the thing recorded.
2609        thread_local! {
2610            static ORDER: RefCell<Vec<i64>> = const { RefCell::new(Vec::new()) };
2611        }
2612        ORDER.with(|o| o.borrow_mut().clear());
2613
2614        unsafe extern "C-unwind" fn poll_a(state: *mut u8, _c: *mut u8) -> i64 {
2615            unsafe { record(state, 1) }
2616        }
2617        unsafe extern "C-unwind" fn poll_b(state: *mut u8, _c: *mut u8) -> i64 {
2618            unsafe { record(state, 2) }
2619        }
2620        unsafe fn record(state: *mut u8, who: i64) -> i64 {
2621            let slots = state as *mut i64;
2622            // SAFETY: `state` is a live state object (`make_future`'s
2623            // contract).
2624            let tag = unsafe { *slots.add(STATE_SLOT_TAG) };
2625            ORDER.with(|o| o.borrow_mut().push(who));
2626            if tag < 2 {
2627                unsafe { *slots.add(STATE_SLOT_TAG) = tag + 1 };
2628                POLL_PENDING
2629            } else {
2630                unsafe { *slots.add(STATE_SLOT_OUTPUT) = who };
2631                POLL_READY
2632            }
2633        }
2634
2635        let a = make_future(poll_a, 0);
2636        let b = make_future(poll_b, 0);
2637        unsafe {
2638            nova_rt_task_spawn(a);
2639            nova_rt_task_spawn(b);
2640            nova_rt_task_block_on(make_future(poll_suspend_once, 0));
2641        }
2642        let order = ORDER.with(|o| o.borrow().clone());
2643        let a_positions: Vec<usize> = order
2644            .iter()
2645            .enumerate()
2646            .filter(|(_, &w)| w == 1)
2647            .map(|(i, _)| i)
2648            .collect();
2649        let b_positions: Vec<usize> = order
2650            .iter()
2651            .enumerate()
2652            .filter(|(_, &w)| w == 2)
2653            .map(|(i, _)| i)
2654            .collect();
2655        assert!(
2656            a_positions.len() >= 3 && b_positions.len() >= 3,
2657            "order = {order:?}"
2658        );
2659        assert!(
2660            a_positions[0] < b_positions[0] && b_positions[0] < a_positions[1],
2661            "expected round-robin interleaving, got {order:?}"
2662        );
2663    }
2664
2665    /// The re-entrancy guard is armed for exactly the span of one `block_on`
2666    /// call, and a poll function running inside one can see that it is.
2667    ///
2668    /// The guard's own diagnostic ends the process (`abort_with`, because a
2669    /// nested call means a *generated* frame is on the path an unwind would
2670    /// have to take), so what is observable in-process is the flag it reads
2671    /// rather than the abort. Both halves discriminate a real defect: a guard
2672    /// that never armed would let a nested `block_on` run a second executor
2673    /// loop from inside the first one's frame, and one that never disarmed
2674    /// would end the process on the second, unrelated, `block_on` in a
2675    /// program. The abort itself is asserted end to end, on a real compiled
2676    /// program, by `nova-cli`'s `run_aborts_when_an_async_fn_calls_block_on`.
2677    #[test]
2678    fn the_re_entrancy_guard_is_armed_only_inside_block_on() {
2679        thread_local! {
2680            static SEEN: Cell<bool> = const { Cell::new(false) };
2681        }
2682        unsafe extern "C-unwind" fn poll_observes_guard(state: *mut u8, _c: *mut u8) -> i64 {
2683            SEEN.with(|s| s.set(IN_BLOCK_ON.with(|g| g.get())));
2684            // SAFETY: `state` is a live state object (`make_future`'s contract).
2685            unsafe { *(state as *mut i64).add(STATE_SLOT_OUTPUT) = 3 };
2686            POLL_READY
2687        }
2688        assert!(
2689            !IN_BLOCK_ON.with(|g| g.get()),
2690            "the guard must not be armed before any block_on"
2691        );
2692        assert_eq!(
2693            unsafe { nova_rt_task_block_on(make_future(poll_observes_guard, 0)) },
2694            3
2695        );
2696        assert!(
2697            SEEN.with(|s| s.get()),
2698            "the guard must be armed while a poll function runs"
2699        );
2700        assert!(
2701            !IN_BLOCK_ON.with(|g| g.get()),
2702            "the guard must be disarmed once block_on returns"
2703        );
2704    }
2705
2706    /// `nova_rt_task_release` ends the executor's claim on a task's state
2707    /// object and can be called any number of times.
2708    ///
2709    /// The exact root counts, not merely "eventually zero": releasing twice
2710    /// must not cancel a registration it does not own. Simulated here with a
2711    /// second, independent `gc::add_root` on the same state -- spawning the
2712    /// same future twice no longer reaches this (`spawn_internal` now rejects
2713    /// it), so this stands in for any other root on the object, which release
2714    /// must leave alone. Asserted on the registry rather than through a
2715    /// collection, for the reason
2716    /// `a_completed_tasks_state_stays_rooted_until_its_output_is_taken`
2717    /// documents.
2718    #[test]
2719    fn releasing_a_task_unroots_its_state_exactly_once_however_often_it_is_called() {
2720        let fut = make_future(poll_ready_now, 0);
2721        let state = state_of(fut);
2722        unsafe { nova_rt_task_spawn(fut) };
2723        // A second, independent registration of the same state: it must
2724        // survive the release below (see this test's doc comment).
2725        gc::add_root(state as *mut u8);
2726        assert_eq!(gc::root_count(state), 2);
2727
2728        unsafe { nova_rt_task_release(fut) };
2729        assert_eq!(
2730            gc::root_count(state),
2731            1,
2732            "release must cancel the executor's own registration"
2733        );
2734        unsafe {
2735            nova_rt_task_release(fut);
2736            nova_rt_task_release(fut);
2737        }
2738        assert_eq!(
2739            gc::root_count(state),
2740            1,
2741            "a repeated release must be a no-op, not another remove_root"
2742        );
2743        gc::remove_root(state as *mut u8);
2744    }
2745
2746    /// A released task's output is still readable out of the state object the
2747    /// future points at -- which is what `JoinHandle::join` does, so that a
2748    /// second `join` on one handle neither panics nor reads stale bits.
2749    #[test]
2750    fn a_released_tasks_output_slot_still_holds_its_value() {
2751        let fut = make_future(poll_ready_now, 0);
2752        unsafe { nova_rt_task_spawn(fut) };
2753        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2754        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 1);
2755        unsafe { nova_rt_task_release(fut) };
2756        // SAFETY: `fut` is a live future fat pointer; `state_of` reads its
2757        // state word, and `STATE_SLOT_OUTPUT` is in bounds by `make_future`'s
2758        // minimum size.
2759        let out = unsafe { (state_of(fut) as *mut i64).add(STATE_SLOT_OUTPUT).read() };
2760        assert_eq!(
2761            out, 7,
2762            "the value stays in the slot the poll fn wrote it to"
2763        );
2764    }
2765
2766    #[test]
2767    fn a_handle_on_a_never_spawned_future_is_not_reported_done() {
2768        // The forged-handle case, at the runtime layer. Before this change the
2769        // lookup was by an `Int` index, so a handle could name a DIFFERENT task
2770        // and spin forever. Keyed on the state, a future that was never spawned
2771        // has no entry at all, which is a diagnosable condition rather than a
2772        // silent wrong answer.
2773        //
2774        // Asserted via `catch_unwind` is NOT possible -- the failure aborts, by
2775        // design (a panic must not cross a generated poll frame). So this test
2776        // asserts the *positive* half only: a spawned future IS found. The abort
2777        // is covered from Nova in Task 2, where `nova test`'s per-process runner
2778        // makes it observable.
2779        let fut = make_future(poll_ready_now, 0);
2780        unsafe { nova_rt_task_spawn(fut) };
2781        assert_eq!(unsafe { nova_rt_task_is_done(fut) }, 0, "not polled yet");
2782    }
2783
2784    #[test]
2785    fn is_done_follows_the_future_it_is_given_not_a_positional_id() {
2786        // The discriminating test, and the one that would have caught the
2787        // original defect: two tasks, and each handle must answer about ITS OWN
2788        // future. Under the old id-keyed lookup, passing task 1's index while
2789        // holding task 0's future answered about task 1 -- which is exactly the
2790        // forgery. Here the only thing passed IS the future, so a swap is
2791        // impossible to express; this test pins that the two are distinguished
2792        // at all, so a lookup that always returned the first task fails.
2793        //
2794        // One poll each via `poll_one`, not a full drain through `block_on`:
2795        // `block_on` drains the whole shared queue, and by the time it empties,
2796        // every task queued on this thread -- `a` AND `b` -- has reported
2797        // `POLL_READY`, so both handles would read back `1` even from a lookup
2798        // that resolved every future to the same task (any id in this test is
2799        // done by then). One poll each keeps `a` (`poll_ready_now`) done and
2800        // `b` (`poll_suspend_once`, which needs a second poll) not done, so the
2801        // two answers must disagree for this test to mean anything.
2802        let a = make_future(poll_ready_now, 0);
2803        let b = make_future(poll_suspend_once, 0);
2804        let (id_a, id_b) = unsafe { (nova_rt_task_spawn(a), nova_rt_task_spawn(b)) };
2805        unsafe {
2806            poll_one(id_a);
2807            poll_one(id_b);
2808        }
2809        assert_eq!(unsafe { nova_rt_task_is_done(a) }, 1);
2810        assert_eq!(
2811            unsafe { nova_rt_task_is_done(b) },
2812            0,
2813            "b needs a second poll; reporting it done means the lookup did not \
2814             distinguish the two futures"
2815        );
2816    }
2817
2818    #[test]
2819    fn releasing_by_future_unroots_that_futures_state_and_no_other() {
2820        // Two tasks, release one, and assert the OTHER's root survives. The old
2821        // signature took an index, so an off-by-one released somebody else's
2822        // state -- a premature free. Keyed on the future, the wrong-target case
2823        // cannot be expressed, and this test pins that release still targets
2824        // exactly one.
2825        let a = make_future(poll_ready_now, 0);
2826        let b = make_future(poll_ready_now, 0);
2827        let (sa, sb) = (state_of(a), state_of(b));
2828        unsafe {
2829            nova_rt_task_spawn(a);
2830            nova_rt_task_spawn(b);
2831        }
2832        assert_eq!(gc::root_count(sa), 1);
2833        assert_eq!(gc::root_count(sb), 1);
2834
2835        unsafe { nova_rt_task_release(a) };
2836
2837        assert_eq!(gc::root_count(sa), 0, "release must unroot its own target");
2838        assert_eq!(gc::root_count(sb), 1, "and must not touch another task's");
2839    }
2840
2841    #[test]
2842    fn releasing_the_same_future_twice_unroots_once() {
2843        // Idempotence, preserved from the id-keyed version: `join` releases then
2844        // reads, and Nova has no move checking, so a second `join` on the same
2845        // handle must release again harmlessly.
2846        let fut = make_future(poll_ready_now, 0);
2847        let state = state_of(fut);
2848        unsafe {
2849            nova_rt_task_spawn(fut);
2850            nova_rt_task_release(fut);
2851            nova_rt_task_release(fut);
2852        }
2853        assert_eq!(gc::root_count(state), 0);
2854    }
2855
2856    #[test]
2857    fn spawning_the_same_future_again_after_release_succeeds() {
2858        // Presence in `BY_STATE` is not what must be rejected. What has to
2859        // stay rejected is two LIVE tasks sharing one state object, which is
2860        // what would make the map ambiguous; a released task's entry must not
2861        // block re-spawning the future it belongs to. Re-spawning after
2862        // release is a deliberately accepted footgun rather than a case the
2863        // check cannot see -- see `spawn_internal`'s own comment for why it is
2864        // accepted -- and this test is what pins the acceptance, so a
2865        // "tightening" of the check to bare presence fails here.
2866        let fut = make_future(poll_ready_now, 0);
2867        let id1 = unsafe { nova_rt_task_spawn(fut) };
2868        unsafe { nova_rt_task_release(fut) };
2869        let id2 = unsafe { nova_rt_task_spawn(fut) };
2870        assert_ne!(
2871            id1, id2,
2872            "the second spawn must register a new task, not reuse the first's"
2873        );
2874        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2875        assert_eq!(
2876            unsafe { nova_rt_task_is_done(fut) },
2877            1,
2878            "the re-spawned task must actually run, not merely avoid aborting"
2879        );
2880    }
2881
2882    /// A freed state object's `BY_STATE` key goes with it, so the address
2883    /// cannot resolve a later, unrelated object to the old task.
2884    ///
2885    /// The read path has no staleness test -- a hit is returned as the answer
2886    /// -- so the whole of its correctness is that a key cannot outlive the
2887    /// object it names. Asserted on the map directly, and on the deterministic
2888    /// sweep (`gc::sweep_with_roots_for_test`, an explicit root set and no
2889    /// stack scan) rather than on a real collection: what is being checked is
2890    /// that *sweeping an object* drops its key, and handing the root set in is
2891    /// what makes "this object was swept" a fact about the argument instead of
2892    /// a fact about whatever the conservative scanner happened to retain
2893    /// (`docs/adr/0010-conservative-scan-root-test-gating.md`, and the
2894    /// `#[ignore]`d tests below that pay for it). It also keeps this test on
2895    /// every platform, where a `collect()`-based version would assert nothing
2896    /// wherever `gc.rs`'s `stack_base` has no implementation.
2897    ///
2898    /// The key's presence is asserted *before* the sweep as well, so a
2899    /// `spawn_internal` that stopped inserting could not make this pass by
2900    /// having nothing to remove.
2901    #[test]
2902    fn a_swept_states_key_is_dropped_so_a_recycled_address_cannot_misresolve() {
2903        let fut = make_future(poll_ready_now, 0);
2904        let state = state_of(fut);
2905        unsafe { nova_rt_task_spawn(fut) };
2906        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2907        // Ends the executor's own claim, which is what lets the state be swept
2908        // at all -- while a task holds its root, the collector keeps it.
2909        unsafe { nova_rt_task_release(fut) };
2910        assert!(
2911            BY_STATE.with(|m| m.borrow().contains_key(&state)),
2912            "spawn must have registered this state, or the removal below \
2913             proves nothing"
2914        );
2915
2916        // An empty root set: nothing is reachable, so this state really is
2917        // freed. Every other object this thread allocated is freed with it, so
2918        // nothing below dereferences any of them.
2919        gc::sweep_with_roots_for_test(&[]);
2920
2921        assert!(
2922            !BY_STATE.with(|m| m.borrow().contains_key(&state)),
2923            "a freed state's key must not survive it: the address can be \
2924             handed to an unrelated object, and this map is what the two \
2925             Nova-facing reads resolve, so a surviving key answers about the \
2926             old task instead of rejecting a future that was never spawned"
2927        );
2928    }
2929
2930    /// The other half, and what makes pruning compatible with `join`'s
2931    /// idempotence rather than a trade against it: a state object a caller can
2932    /// still reach keeps its key across a collection.
2933    ///
2934    /// `join` releases and then reads, and a second `join` on the same handle
2935    /// has to resolve the same task again (`std/task`'s `JoinHandle::join`).
2936    /// Released is not swept: the handle still holds the future, the future is
2937    /// a scanned two-word object holding the state address, so tracing the
2938    /// future marks the state. The root set here is the future alone --
2939    /// deliberately *not* the state -- so the state survives only if it is
2940    /// reached *through* the future, which is the actual mechanism.
2941    ///
2942    /// Without this, "prune at release" and "prune when freed" would be
2943    /// indistinguishable, and the first breaks idempotence.
2944    #[test]
2945    fn a_reachable_futures_key_survives_a_collection_so_a_second_read_resolves() {
2946        let fut = make_future(poll_ready_now, 0);
2947        let state = state_of(fut);
2948        unsafe { nova_rt_task_spawn(fut) };
2949        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2950        unsafe { nova_rt_task_release(fut) };
2951
2952        gc::sweep_with_roots_for_test(&[fut as usize]);
2953
2954        assert_eq!(
2955            gc::object_info(state).map(|(_, scan)| scan),
2956            Some(true),
2957            "the state must have been reached through the future's own words, \
2958             or this test is asserting nothing about tracing"
2959        );
2960        assert!(
2961            BY_STATE.with(|m| m.borrow().contains_key(&state)),
2962            "a state a handle can still reach must keep its key, or a second \
2963             join aborts on a handle that did nothing wrong"
2964        );
2965        assert_eq!(
2966            unsafe { nova_rt_task_is_done(fut) },
2967            1,
2968            "and the surviving key must still resolve to that task"
2969        );
2970    }
2971
2972    /// Pruning removes the keys of freed states and nothing else. With every
2973    /// state the map names in the root set, a sweep frees only objects that
2974    /// are not task states, and the map comes out exactly as it went in.
2975    #[test]
2976    fn a_sweep_freeing_only_non_state_objects_removes_no_key() {
2977        let fut = make_future(poll_ready_now, 0);
2978        let state = state_of(fut);
2979        unsafe { nova_rt_task_spawn(fut) };
2980        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
2981        unsafe { nova_rt_task_release(fut) };
2982        let keys: Vec<usize> = BY_STATE.with(|m| m.borrow().keys().copied().collect());
2983        assert!(
2984            keys.contains(&state),
2985            "spawn must have registered this state"
2986        );
2987        for _ in 0..100 {
2988            gc::alloc(24, true);
2989        }
2990        let before = BY_STATE.with(|m| m.borrow().clone());
2991
2992        gc::sweep_with_roots_for_test(&keys);
2993
2994        assert_eq!(
2995            BY_STATE.with(|m| m.borrow().clone()),
2996            before,
2997            "a sweep that freed no state removed a key"
2998        );
2999    }
3000
3001    /// The same two properties for state objects too big for a page, whose
3002    /// liveness the prune predicate finds through `gc`'s large-object list
3003    /// rather than a page bitmap. 300 temps make a `(2 + 300) * 8` = 2416-byte
3004    /// state, over the 2048-byte page limit. Large non-state objects sit
3005    /// between the states, so the predicate's search over that list depends
3006    /// on the list being sorted when it runs.
3007    #[test]
3008    fn a_large_states_key_is_kept_while_reachable_and_dropped_once_freed() {
3009        let mut kept = Vec::new();
3010        let mut dropped = Vec::new();
3011        for i in 0..6 {
3012            let _filler = gc::alloc(3000, true);
3013            let fut = make_future(poll_ready_now, 300);
3014            unsafe { nova_rt_task_spawn(fut) };
3015            if i % 2 == 0 {
3016                kept.push(fut);
3017            } else {
3018                dropped.push(fut);
3019            }
3020        }
3021        unsafe { nova_rt_task_block_on(make_future(poll_ready_now, 0)) };
3022        for &f in kept.iter().chain(&dropped) {
3023            unsafe { nova_rt_task_release(f) };
3024        }
3025        let kept_states: Vec<usize> = kept.iter().map(|&f| state_of(f)).collect();
3026        let dropped_states: Vec<usize> = dropped.iter().map(|&f| state_of(f)).collect();
3027        for s in kept_states.iter().chain(&dropped_states) {
3028            assert!(
3029                BY_STATE.with(|m| m.borrow().contains_key(s)),
3030                "spawn must have registered every state, or the checks below prove nothing"
3031            );
3032            assert!(
3033                gc::object_info(*s).is_some_and(|(size, _)| size > 2048),
3034                "the state must be on the large path"
3035            );
3036        }
3037        let roots: Vec<usize> = kept.iter().map(|&f| f as usize).collect();
3038
3039        gc::sweep_with_roots_for_test(&roots);
3040
3041        for s in &kept_states {
3042            assert!(
3043                BY_STATE.with(|m| m.borrow().contains_key(s)),
3044                "a large state a handle can still reach lost its key"
3045            );
3046        }
3047        for s in &dropped_states {
3048            assert!(
3049                !BY_STATE.with(|m| m.borrow().contains_key(s)),
3050                "a freed large state's key survived it"
3051            );
3052        }
3053    }
3054
3055    /// The exact layout `nova_rt_task_yield_future` builds, read back from the
3056    /// collector's own records.
3057    ///
3058    /// This is the second place in the runtime that constructs a heap value the
3059    /// *compiler* also constructs (`nova_rt_str_chars` was the first), and a
3060    /// layout that disagrees with the compiler's is a silent miscompile rather
3061    /// than a failure. So both allocations are checked against the tracked
3062    /// `(size, scan)` and not only against the words stored in them: reading
3063    /// words back cannot see an allocation that is too small but has slop after
3064    /// it, and cannot see the `scan` flag at all -- and an unscanned state
3065    /// object is marked but never traced (`gc.rs`'s `if !scan { continue; }`).
3066    #[test]
3067    fn the_yield_futures_layout_is_the_one_the_abi_declares() {
3068        let fut = nova_rt_task_yield_future();
3069        assert_eq!(
3070            gc::object_info(fut as usize),
3071            Some((FUTURE_SIZE, true)),
3072            "the fat pointer must be exactly the two-word future, scanned"
3073        );
3074        let state = state_of(fut);
3075        assert_eq!(
3076            gc::object_info(state),
3077            Some((STATE_MIN_SIZE, true)),
3078            "the state object must be at least the tag and output slots, scanned"
3079        );
3080        // That `STATE_MIN_SIZE` really does cover the output slot is checked at
3081        // compile time, next to `YIELD_STATE_SIZE`.
3082        //
3083        // SAFETY: `fut` is this call's own `FUTURE_SIZE`-byte block.
3084        let poll = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3085        let expected: PollFn = poll_yield_once;
3086        assert_eq!(
3087            poll, expected as usize,
3088            "word 0 must be the poll function's address, not the state's"
3089        );
3090    }
3091
3092    /// The exact layout `nova_rt_task_sleep_future_nanos` builds, read back
3093    /// from the collector's own records -- the same discipline as
3094    /// `the_yield_futures_layout_is_the_one_the_abi_declares`, and for the
3095    /// identical reason, but this one is load-bearing in a way that test is
3096    /// not: a review of this function caught a `build_future` call site
3097    /// passing `STATE_MIN_SIZE` where `SLEEP_STATE_SIZE` belonged -- one word
3098    /// short of what `nova_rt_task_sleep_future_nanos`'s own `init` closure
3099    /// writes -- and neither existing guard caught it. The compile-time
3100    /// assertion beside `SLEEP_STATE_SIZE` is a relation between two
3101    /// constants and is blind to what a call site actually passes;
3102    /// `build_future`'s own `debug_assert!(state_size >= STATE_MIN_SIZE)` is
3103    /// a floor, and `STATE_MIN_SIZE` itself satisfies its own floor. Reading
3104    /// the words back out of the state object cannot catch it either, for
3105    /// the same reason the sibling test's doc comment gives: a too-small
3106    /// allocation with slop after it still reads back whatever was written,
3107    /// silently, until something else happens to occupy that word. Only the
3108    /// collector's own recorded size distinguishes "16 bytes, and `nanos`
3109    /// was written one word past the end" from "24 bytes, exactly as
3110    /// declared."
3111    #[test]
3112    fn the_sleep_futures_layout_is_the_one_the_abi_declares() {
3113        let fut = nova_rt_task_sleep_future_nanos(0);
3114        assert_eq!(
3115            gc::object_info(fut as usize),
3116            Some((FUTURE_SIZE, true)),
3117            "the fat pointer must be exactly the two-word future, scanned"
3118        );
3119        let state = state_of(fut);
3120        assert_eq!(
3121            gc::object_info(state),
3122            Some((SLEEP_STATE_SIZE, true)),
3123            "the state object must be the ABI minimum plus the one temp slot \
3124             holding `nanos`, scanned"
3125        );
3126        // SAFETY: `fut` is this call's own `FUTURE_SIZE`-byte block.
3127        let poll = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3128        let expected: PollFn = poll_sleep;
3129        assert_eq!(
3130            poll, expected as usize,
3131            "word 0 must be the poll function's address, not the state's"
3132        );
3133    }
3134
3135    /// A 50ms sleep must stage a deadline roughly 50ms out -- the magnitude,
3136    /// not merely the presence of some deadline.
3137    ///
3138    /// `before` is captured **outside** the poll on purpose. The staged
3139    /// deadline is computed at or after `before`, so by monotonicity the
3140    /// difference is at least the requested 50ms however long this thread is
3141    /// descheduled; measuring from after the poll could drift below any lower
3142    /// bound and flake. The window is loose against jitter and tight against
3143    /// unit errors: a millionfold overshoot stages ~50,000 seconds and fails
3144    /// the upper bound, a millionfold undershoot stages ~50ns and fails the
3145    /// lower one.
3146    #[test]
3147    fn a_sleep_stages_a_deadline_of_the_right_magnitude() {
3148        let before = Instant::now();
3149        let fut = nova_rt_task_sleep_future_nanos(50 * 1_000_000);
3150        // SAFETY: `fut` is this call's own `FUTURE_SIZE`-byte block, so word 0
3151        // is its poll function -- exactly how
3152        // `the_sleep_futures_layout_is_the_one_the_abi_declares` reads it.
3153        let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3154        // SAFETY: that word is a `PollFn` bit pattern by `fut`'s own
3155        // construction; a fn pointer and a `usize` are both pointer-width.
3156        let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
3157        let state = state_of(fut) as *mut u8;
3158
3159        // `stage_park` aborts outside a task context, so borrow one for this
3160        // single manual poll and put back whatever was there.
3161        let previous = current_task();
3162        set_current_for_test(Some(0));
3163        // SAFETY: `poll`/`state` are the pair inside `fut`; `task_ctx` is
3164        // always null, matching every other call site in this crate.
3165        let first = unsafe { poll(state, std::ptr::null_mut()) };
3166        let staged = staged_deadline_for_test();
3167        // This manual poll never goes through `poll_one`'s own cleanup, so
3168        // unlike a real poll, the `Wait::Deadline` it just staged is left
3169        // sitting in `PENDING_PARK` unless something here drains it. Left in
3170        // place, the next test to stage a park on this thread -- if one runs
3171        // before anything else clears it, which `--test-threads=1` makes
3172        // deterministic rather than merely possible -- collides with it, and
3173        // `stage_park` aborts the whole process over two parks it never
3174        // staged itself. Draining with the same `Cell::take` `poll_one` uses
3175        // (line ~674) resets `PENDING_PARK` to empty, exactly as if this
3176        // poll's park had been committed and later cleared.
3177        PENDING_PARK.with(|cell| {
3178            cell.take();
3179        });
3180        set_current_for_test(previous);
3181
3182        assert_eq!(first, POLL_PENDING, "a sleep must park on its first poll");
3183        let staged = staged.expect("a sleep must stage a deadline");
3184        let delta = staged.duration_since(before);
3185        assert!(
3186            delta >= Duration::from_millis(40) && delta <= Duration::from_millis(500),
3187            "a 50ms sleep staged a deadline {delta:?} out -- a unit error, not jitter"
3188        );
3189    }
3190
3191    /// A sleep polled before its deadline must re-stage **the same deadline**
3192    /// and stay pending.
3193    ///
3194    /// Asserted by identity, not existence: a mutant that re-stages
3195    /// `Instant::now()` satisfies "some deadline is staged" and then spins
3196    /// forever, which an existence check cannot distinguish from correct.
3197    #[test]
3198    fn a_sleep_polled_early_re_stages_the_same_deadline() {
3199        let fut = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
3200        let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3201        // SAFETY: word 0 is a `PollFn` bit pattern by `fut`'s own construction.
3202        let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
3203        let state = state_of(fut) as *mut u8;
3204
3205        let previous = current_task();
3206        set_current_for_test(Some(0));
3207        // SAFETY: `poll`/`state` are the pair inside `fut`; `task_ctx` is null.
3208        let first = unsafe { poll(state, std::ptr::null_mut()) };
3209        let staged_first = staged_deadline_for_test();
3210        PENDING_PARK.with(|cell| {
3211            cell.take();
3212        });
3213        // SAFETY: same pair, polled a second time before the deadline.
3214        let second = unsafe { poll(state, std::ptr::null_mut()) };
3215        let staged_second = staged_deadline_for_test();
3216        PENDING_PARK.with(|cell| {
3217            cell.take();
3218        });
3219        set_current_for_test(previous);
3220
3221        assert_eq!(
3222            first, POLL_PENDING,
3223            "a 60s sleep must park on its first poll"
3224        );
3225        assert_eq!(
3226            second, POLL_PENDING,
3227            "and must stay pending when polled again before its deadline"
3228        );
3229        assert_eq!(
3230            staged_second, staged_first,
3231            "the re-staged deadline must be the original, not a fresh one"
3232        );
3233    }
3234
3235    /// A sleep whose deadline has passed completes.
3236    #[test]
3237    fn a_sleep_polled_after_its_deadline_completes() {
3238        let fut = nova_rt_task_sleep_future_nanos(0);
3239        let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3240        // SAFETY: as above.
3241        let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
3242        let state = state_of(fut) as *mut u8;
3243
3244        let previous = current_task();
3245        set_current_for_test(Some(0));
3246        // SAFETY: as above.
3247        let status = unsafe { poll(state, std::ptr::null_mut()) };
3248        PENDING_PARK.with(|cell| {
3249            cell.take();
3250        });
3251        set_current_for_test(previous);
3252
3253        assert_eq!(status, POLL_READY, "a zero-nanosecond sleep is already due");
3254    }
3255
3256    /// `instant_from_deadline_nanos`'s result must never be treated as
3257    /// already due while the deadline it encodes is still in the future --
3258    /// the property the old `Instant::now()` overflow fallback broke (see
3259    /// that function's own doc comment). `i64::MAX` is the largest deadline
3260    /// [`deadline_nanos_from_now`] can ever produce, so it is the input this
3261    /// crate's own API can actually construct that stresses the conversion
3262    /// hardest.
3263    #[test]
3264    fn instant_from_deadline_nanos_of_i64_max_is_not_already_due() {
3265        let before = Instant::now();
3266        let staged = instant_from_deadline_nanos(i64::MAX);
3267        assert!(
3268            staged >= before,
3269            "a deadline this far in the future must not stage an instant \
3270             that is already due"
3271        );
3272    }
3273
3274    /// Forces [`furthest_representable_instant`]'s halving fallback directly,
3275    /// since nothing reachable through this crate's own sleep API can:
3276    /// `deadline_nanos_from_now` bounds every deadline to at most `i64::MAX`
3277    /// nanoseconds (~292 years), and `checked_add` of that many nanoseconds
3278    /// from a process-start `Instant` succeeds on every supported backend --
3279    /// confirmed below, not assumed. `Duration::MAX` (~584 billion years) is
3280    /// the one value guaranteed to overflow it, and `Instant` offers no
3281    /// public way to sit near its own overflow boundary the way `Duration`
3282    /// does, so this is the only lever available to actually exercise the
3283    /// loop rather than merely asserting its result type is right.
3284    #[test]
3285    fn furthest_representable_instant_forces_the_halving_fallback_and_is_not_already_due() {
3286        let base = Instant::now();
3287        assert!(
3288            base.checked_add(Duration::MAX).is_none(),
3289            "this test only exercises the halving loop if Duration::MAX \
3290             overflows Instant::checked_add on this platform; if that ever \
3291             changes, this assertion (not the one below) is what will fail"
3292        );
3293
3294        let before = Instant::now();
3295        let staged = furthest_representable_instant(base, Duration::MAX);
3296        assert!(
3297            staged >= before,
3298            "the furthest representable instant must not be treated as \
3299             already due"
3300        );
3301    }
3302
3303    /// The inner future is polled **before** the deadline is checked, so work
3304    /// that completed is never reported as timed out -- and a zero-duration
3305    /// timeout over an already-ready future succeeds.
3306    ///
3307    /// Reversing the order in `poll_timeout` fails exactly this.
3308    #[test]
3309    fn a_zero_duration_timeout_over_a_ready_future_reports_completed() {
3310        let inner = nova_rt_task_sleep_future_nanos(0);
3311        let fut = nova_rt_task_timeout_future(0, inner);
3312        let status = poll_once_for_test(fut);
3313        assert_eq!(status, POLL_READY);
3314        assert_eq!(
3315            output_of_for_test(fut),
3316            TIMEOUT_STATUS_COMPLETED,
3317            "the inner future was ready, so this must not report elapsed"
3318        );
3319    }
3320
3321    /// A past deadline over a future that will not complete reports elapsed.
3322    #[test]
3323    fn an_expired_timeout_over_a_pending_future_reports_elapsed() {
3324        let inner = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
3325        let fut = nova_rt_task_timeout_future(0, inner);
3326        let status = poll_once_for_test(fut);
3327        assert_eq!(status, POLL_READY);
3328        assert_eq!(output_of_for_test(fut), TIMEOUT_STATUS_ELAPSED);
3329    }
3330
3331    /// A live timeout over a pending inner parks, and the staged park carries
3332    /// **both** the inner's wait and this timeout's deadline, merged.
3333    ///
3334    /// Written with the raw poll idiom rather than `poll_once_for_test`,
3335    /// because that helper drains `PENDING_PARK` before returning and this
3336    /// test must read `staged_deadline_for_test()` **before** the drain.
3337    #[test]
3338    fn a_live_timeout_over_a_pending_future_stages_both_deadlines_merged() {
3339        let inner = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
3340        let fut = nova_rt_task_timeout_future(1_000_000, inner);
3341        let before = Instant::now();
3342        // SAFETY: `fut` is this call's own `FUTURE_SIZE`-byte block, so word 0
3343        // is its poll function.
3344        let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3345        // SAFETY: that word is a `PollFn` bit pattern by `fut`'s own
3346        // construction; a fn pointer and a `usize` are both pointer-width.
3347        let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
3348        let state = state_of(fut) as *mut u8;
3349
3350        let previous = current_task();
3351        set_current_for_test(Some(0));
3352        // SAFETY: `poll`/`state` are the pair inside `fut`; `task_ctx` is
3353        // always null, matching every other call site in this crate.
3354        let status = unsafe { poll(state, std::ptr::null_mut()) };
3355        let staged = staged_deadline_for_test();
3356        PENDING_PARK.with(|cell| {
3357            cell.take();
3358        });
3359        set_current_for_test(previous);
3360
3361        assert_eq!(
3362            status, POLL_PENDING,
3363            "neither the inner nor the deadline is done"
3364        );
3365        let staged = staged.expect("a park must be staged");
3366        let delta = staged.duration_since(before);
3367        assert!(
3368            delta <= Duration::from_secs(1),
3369            "the merged deadline must be the timeout's 1ms, not the inner's \
3370             60s -- got {delta:?}"
3371        );
3372    }
3373
3374    /// Everything one poll left staged in `PENDING_PARK`, read before the
3375    /// drain -- the three [`Staged`] fields as the three `*_for_test`
3376    /// accessors report them, `io` keeping the deadline that rides *inside*
3377    /// the I/O wait rather than folding it away.
3378    #[derive(Debug)]
3379    struct StagedForTest {
3380        deadline: Option<Instant>,
3381        io: Option<(RawSocket, Interest, Option<Instant>)>,
3382        task: Option<i64>,
3383    }
3384
3385    /// Poll `fut` once under a borrowed task context, optionally with
3386    /// `pre_staged` already staged first, and hand back both the status and
3387    /// everything left staged.
3388    ///
3389    /// `poll_once_for_test` cannot serve these tests: it drains
3390    /// `PENDING_PARK` before returning and hands back only the status, so the
3391    /// leftover-park property is precisely what it destroys. The drain still
3392    /// has to happen -- a park left staged aborts the next test to stage one
3393    /// under `--test-threads=1` -- so it happens here, after the read.
3394    ///
3395    /// `pre_staged` is what makes "restores what it found" testable at all:
3396    /// with an empty slot going in, restoring a snapshot and clearing to
3397    /// `Staged::default()` are indistinguishable.
3398    fn poll_once_and_read_staged_for_test(
3399        fut: *mut u8,
3400        pre_staged: Option<Wait>,
3401    ) -> (i64, StagedForTest) {
3402        let poll_addr = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3403        // SAFETY: word 0 is a `PollFn` bit pattern by `fut`'s own construction.
3404        let poll: PollFn = unsafe { std::mem::transmute(poll_addr) };
3405        let state = state_of(fut) as *mut u8;
3406
3407        let previous = current_task();
3408        set_current_for_test(Some(0));
3409        if let Some(wait) = pre_staged {
3410            stage_park(wait);
3411        }
3412        // SAFETY: `poll`/`state` are the pair inside `fut`; `task_ctx` is
3413        // always null, matching every other call site in this crate.
3414        let status = unsafe { poll(state, std::ptr::null_mut()) };
3415        let staged = StagedForTest {
3416            deadline: staged_deadline_for_test(),
3417            io: staged_io_for_test(),
3418            task: staged_task_for_test(),
3419        };
3420        PENDING_PARK.with(|cell| {
3421            cell.take();
3422        });
3423        set_current_for_test(previous);
3424        (status, staged)
3425    }
3426
3427    /// A poll function that parks on a **non-deadline** wait -- a bare
3428    /// `Wait::Task` -- and never completes.
3429    ///
3430    /// Non-deadline on purpose. A `Wait::Deadline` left behind by an abandoned
3431    /// inner merges with the next suspension's deadline and is then corrected
3432    /// by level-triggering, so it cannot expose a leftover park; a
3433    /// `Wait::Task` or a `Wait::Io` collides and aborts the process. Every
3434    /// shipped fixture and every unit test used a `sleep` inner, which stages
3435    /// only a deadline, which is the entire reason the leftover went unseen.
3436    ///
3437    /// `id: 0` names no task that has to exist: `stage_park` does not resolve
3438    /// ids, and nothing here reaches `staged_to_wait` or the wake paths.
3439    unsafe extern "C-unwind" fn poll_parks_on_a_task_forever(
3440        _state: *mut u8,
3441        _task_ctx: *mut u8,
3442    ) -> i64 {
3443        stage_park(Wait::Task {
3444            id: 0,
3445            deadline: None,
3446        });
3447        POLL_PENDING
3448    }
3449
3450    /// A poll function that parks on a `Wait::Io` and never completes -- the
3451    /// other non-deadline wait, and the one `staged_io_for_test` reads.
3452    unsafe extern "C-unwind" fn poll_parks_on_an_io_forever(
3453        _state: *mut u8,
3454        _task_ctx: *mut u8,
3455    ) -> i64 {
3456        stage_park(Wait::Io {
3457            socket: RawSocket(9),
3458            interest: Interest::Read,
3459            deadline: None,
3460        });
3461        POLL_PENDING
3462    }
3463
3464    /// A poll function that stages a park and *then* returns `POLL_READY` --
3465    /// exactly the shape `poll_one`'s "taken unconditionally" comment
3466    /// describes, reproduced here so `poll_timeout`'s completed exit can be
3467    /// held to the same postcondition as its elapsed one.
3468    unsafe extern "C-unwind" fn poll_parks_then_completes(
3469        state: *mut u8,
3470        _task_ctx: *mut u8,
3471    ) -> i64 {
3472        stage_park(Wait::Task {
3473            id: 0,
3474            deadline: None,
3475        });
3476        // SAFETY: `state` is a `STATE_MIN_SIZE` state object from
3477        // `make_future`, so the output slot is in bounds.
3478        unsafe { (state as *mut i64).add(STATE_SLOT_OUTPUT).write(0) };
3479        POLL_READY
3480    }
3481
3482    /// The other half of spec §6.3's structural pending test: a live timeout
3483    /// over an inner that parks on something that is **not** a deadline stages
3484    /// the inner's own wait *and* the timeout's deadline, merged into it.
3485    ///
3486    /// `a_live_timeout_over_a_pending_future_stages_both_deadlines_merged`
3487    /// covers the deadline half with a `sleep` inner, which stages only a
3488    /// deadline, so `staged_io_for_test` went unexercised by any
3489    /// `poll_timeout` test and no unit test polled a timeout over a
3490    /// non-deadline inner at all. This is the assertion that was one line from
3491    /// finding the abandonment bug: it is the same `StagedForTest` read, on
3492    /// the same inner shape, differing only in whether the deadline has
3493    /// already passed.
3494    #[test]
3495    fn a_live_timeout_over_an_io_parking_inner_stages_the_io_wait_and_the_deadline() {
3496        let inner = make_future(poll_parks_on_an_io_forever, 0);
3497        let before = Instant::now();
3498        let fut = nova_rt_task_timeout_future(5 * 1_000_000_000, inner);
3499
3500        let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
3501
3502        assert_eq!(
3503            status, POLL_PENDING,
3504            "neither the inner nor the 5s deadline is done"
3505        );
3506        let (socket, interest, io_deadline) = staged
3507            .io
3508            .expect("the inner's I/O wait must survive into the staged park");
3509        assert_eq!((socket, interest), (RawSocket(9), Interest::Read));
3510        let io_deadline = io_deadline.expect(
3511            "the timeout's own deadline must ride *inside* the inner's I/O \
3512             wait -- one task, one PARKED entry",
3513        );
3514        assert_eq!(
3515            staged.deadline,
3516            Some(io_deadline),
3517            "the deadline `staged_io_for_test` reports and the one \
3518             `staged_deadline_for_test` reports are the same single deadline"
3519        );
3520        // The inner stages no deadline of its own, so any deadline here is the
3521        // timeout's -- and its magnitude is what pins the unit conversion.
3522        let delta = io_deadline.duration_since(before);
3523        assert!(
3524            delta >= Duration::from_secs(4) && delta <= Duration::from_secs(30),
3525            "a 5s timeout staged a deadline {delta:?} out -- a unit error, \
3526             not jitter"
3527        );
3528        assert_eq!(
3529            staged.task, None,
3530            "nothing staged a task wait, so this must be empty"
3531        );
3532    }
3533
3534    /// An **elapsed** timeout must leave nothing staged by the inner it just
3535    /// abandoned.
3536    ///
3537    /// The structural guard for the abandonment bug: `poll_one` takes
3538    /// `PENDING_PARK` once per *task poll*, not once per future, so a leftover
3539    /// `Wait::Task` here collides with the next suspension in the same task
3540    /// poll and aborts the process -- reachable from
3541    /// `timeout(d, h.join())` followed by any other parking suspension. See
3542    /// [`poll_timeout`]'s own doc comment.
3543    ///
3544    /// Deliberately a *unit* test as well as two fixtures: the fixtures cover
3545    /// the two shapes anyone happened to think of, this covers the property.
3546    #[test]
3547    fn an_elapsed_timeout_discards_the_park_its_abandoned_inner_staged() {
3548        let inner = make_future(poll_parks_on_a_task_forever, 0);
3549        let fut = nova_rt_task_timeout_future(0, inner);
3550
3551        let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
3552
3553        assert_eq!(status, POLL_READY, "a zero-duration timeout is already due");
3554        assert_eq!(
3555            output_of_for_test(fut),
3556            TIMEOUT_STATUS_ELAPSED,
3557            "the inner never completes, so this is the elapsed exit"
3558        );
3559        assert_eq!(
3560            staged.task, None,
3561            "the abandoned inner's task wait must not survive this poll: the \
3562             next suspension in this same task poll would collide with it and \
3563             abort the process"
3564        );
3565        assert_eq!(staged.deadline, None, "nothing staged a deadline");
3566        assert!(staged.io.is_none(), "nothing staged an I/O wait");
3567    }
3568
3569    /// The same postcondition over an inner that parked on an **`Io`** wait
3570    /// rather than a `Task` one.
3571    ///
3572    /// The review that found this bug measured the `Task` shape end-to-end and
3573    /// recorded the `Io` shapes as *inferred* -- the leftover mechanism looked
3574    /// kind-agnostic and `try_stage` was probed directly, but no `Io` case was
3575    /// run, because end-to-end it needs a socket fixture. This is that
3576    /// measurement, one layer down where no socket is needed: the abandoned
3577    /// park is discarded regardless of which field of `Staged` it occupies.
3578    /// "Probe one shape, generalise the conclusion" is how the bug got in;
3579    /// asserting the generalisation is cheaper than repeating the mistake.
3580    #[test]
3581    fn an_elapsed_timeout_discards_an_io_park_its_abandoned_inner_staged() {
3582        let inner = make_future(poll_parks_on_an_io_forever, 0);
3583        let fut = nova_rt_task_timeout_future(0, inner);
3584
3585        let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
3586
3587        assert_eq!(status, POLL_READY, "a zero-duration timeout is already due");
3588        assert_eq!(output_of_for_test(fut), TIMEOUT_STATUS_ELAPSED);
3589        assert!(
3590            staged.io.is_none(),
3591            "the abandoned inner's I/O wait must not survive this poll either \
3592             -- got {:?}",
3593            staged.io
3594        );
3595        assert_eq!(staged.task, None, "and nothing staged a task wait");
3596        assert_eq!(staged.deadline, None, "and nothing staged a deadline");
3597    }
3598
3599    /// The **completed** exit is held to the same postcondition, over an inner
3600    /// that stages a park and then returns `POLL_READY` in the same poll.
3601    ///
3602    /// Not a hypothetical shape: it is the one `poll_one`'s "taken
3603    /// unconditionally" comment exists for. Both of `poll_timeout`'s
3604    /// `POLL_READY` exits hand the slot back as they found it, so both are
3605    /// tested; covering only the elapsed one would leave half the contract
3606    /// resting on the belief that no inner ever does this.
3607    #[test]
3608    fn a_completed_timeout_discards_a_park_its_inner_staged_before_completing() {
3609        let inner = make_future(poll_parks_then_completes, 0);
3610        let fut = nova_rt_task_timeout_future(30 * 1_000_000_000, inner);
3611
3612        let (status, staged) = poll_once_and_read_staged_for_test(fut, None);
3613
3614        assert_eq!(status, POLL_READY, "the inner completed");
3615        assert_eq!(
3616            output_of_for_test(fut),
3617            TIMEOUT_STATUS_COMPLETED,
3618            "the inner beat the 30s deadline"
3619        );
3620        assert_eq!(
3621            staged.task, None,
3622            "a park staged by a poll that then completed must not survive \
3623             this poll either"
3624        );
3625    }
3626
3627    /// `poll_timeout` **restores** what it found rather than clearing to
3628    /// `Staged::default()`.
3629    ///
3630    /// The distinction is the whole design of the fix, and it is invisible
3631    /// unless something was already staged: with an empty slot going in, a
3632    /// snapshot restore and a clear produce the same answer. Here a deadline
3633    /// is staged *before* the timeout runs -- an earlier suspension in the
3634    /// same task poll, or an enclosing timeout -- and it must still be there
3635    /// afterwards, while the abandoned inner's task wait must not.
3636    ///
3637    /// A deadline rather than an `Io` or `Task` pre-stage on purpose: those
3638    /// would *collide* with the inner's own `Wait::Task` inside
3639    /// `stage_park`, which aborts the process, and an aborting test takes the
3640    /// whole test binary down instead of failing (see `try_stage`'s doc
3641    /// comment). A deadline merges, so this test can still fail cleanly
3642    /// against every mutant it is aimed at.
3643    #[test]
3644    fn an_elapsed_timeout_restores_the_park_staged_before_it_rather_than_clearing() {
3645        let already_staged = Instant::now() + Duration::from_secs(30);
3646        let inner = make_future(poll_parks_on_a_task_forever, 0);
3647        let fut = nova_rt_task_timeout_future(0, inner);
3648
3649        let (status, staged) =
3650            poll_once_and_read_staged_for_test(fut, Some(Wait::Deadline(already_staged)));
3651
3652        assert_eq!(status, POLL_READY, "a zero-duration timeout is already due");
3653        assert_eq!(output_of_for_test(fut), TIMEOUT_STATUS_ELAPSED);
3654        assert_eq!(
3655            staged.task, None,
3656            "the abandoned inner's task wait must be gone"
3657        );
3658        assert_eq!(
3659            staged.deadline,
3660            Some(already_staged),
3661            "a park staged before this timeout ran must survive it -- \
3662             clearing to Staged::default() would discard a park poll_timeout \
3663             never staged, and this is the assertion that says so"
3664        );
3665    }
3666
3667    /// The inner future's state survives a sweep whose **entire** root set is
3668    /// the timeout's own state, which is what proves the timeout's stored fat
3669    /// pointer -- not some other reachability path -- is what keeps it alive.
3670    ///
3671    /// **Deliberately `gc::sweep_with_roots_for_test`, not
3672    /// `gc::collect_for_test`.** An earlier version of this test used the
3673    /// real, stack-scanning collector and tried to defeat the conservative
3674    /// scan's false positives with the `hide`/`reveal` bit-complement
3675    /// technique `gc.rs`'s own `registry` tests use. That did not work: two
3676    /// throwaway probes (reproducing `gc.rs`'s own
3677    /// `an_unregistered_object_is_swept` verbatim, and stripping this test
3678    /// down to no timeout/combinator code at all) both showed the object
3679    /// surviving a real collection regardless, purely because it ran inside
3680    /// `task.rs`'s test binary rather than `gc.rs`'s -- the identical
3681    /// technique reliably sweeps in `gc.rs`'s own module and reliably does
3682    /// not here. That is a property of the stack-scanning path and this
3683    /// binary, not of anything this combinator does, and it made the
3684    /// stack-scanning version structurally unable to fail against a
3685    /// constructor that never wrote `TIMEOUT_SLOT_INNER` -- passing 20/20
3686    /// runs whether or not the bug was present.
3687    ///
3688    /// `sweep_with_roots_for_test` sidesteps that path entirely: no stack
3689    /// scan, and -- confirmed by reading `collect_with_roots`'s own body,
3690    /// not merely trusting its doc comment -- no consultation of `PINNED`
3691    /// (the `add_root` registry) either. Its marking loop iterates only the
3692    /// `roots` slice a caller passes in; `PINNED` is copied into that set by
3693    /// `collect()` alone, which this path never calls. So the only way
3694    /// `inner_state` can survive this sweep is through `fut_state`'s own
3695    /// scanned contents, which is exactly the property under test.
3696    ///
3697    /// The assertion just above the sweep checks the other way a root could
3698    /// leak: if `build_future`'s `add_root`/`remove_root` pairing ever left
3699    /// either state pinned, `PINNED` would be non-empty for it. Confirmed
3700    /// rather than assumed, per the same standard applied to `PINNED`
3701    /// itself above.
3702    ///
3703    /// No `#[cfg(windows)]`: `sweep_with_roots_for_test` is `#[cfg(test)]`
3704    /// only, so -- unlike the version this replaces -- this test compiles
3705    /// and runs on every platform.
3706    #[test]
3707    fn a_timeouts_inner_future_survives_a_root_set_sweep_of_only_its_own_state() {
3708        let inner = nova_rt_task_sleep_future_nanos(60 * 1_000_000_000);
3709        let inner_state = state_of(inner);
3710        let fut = nova_rt_task_timeout_future(60 * 1_000_000_000, inner);
3711        let fut_state = state_of(fut);
3712
3713        assert_eq!(
3714            gc::root_count(inner_state) + gc::root_count(fut_state),
3715            0,
3716            "build_future's add_root/remove_root pairing left a root pinned \
3717             on one of these states; this test would pass regardless of \
3718             TIMEOUT_SLOT_INNER if PINNED were consulted here (it is not,\
3719             but this state should never arise regardless)"
3720        );
3721
3722        gc::sweep_with_roots_for_test(&[fut_state]);
3723
3724        assert!(
3725            gc::object_info(inner_state).is_some(),
3726            "the inner state must stay live: with only the timeout's own \
3727             state in the root set, the inner state is reachable through \
3728             nothing but the fat pointer TIMEOUT_SLOT_INNER stores"
3729        );
3730    }
3731
3732    /// The exact layout `nova_rt_task_join_future` builds, read back from the
3733    /// collector's own records -- the same discipline as
3734    /// `the_sleep_futures_layout_is_the_one_the_abi_declares`, and for the
3735    /// identical reason: that test's own doc comment records a review that
3736    /// caught a `build_future` call site one word short of what its `init`
3737    /// closure writes, undetected by the const assert beside `JOIN_STATE_SIZE`
3738    /// (a relation between two constants, blind to what a call site actually
3739    /// passes) and by `build_future`'s own `debug_assert!` (a floor, and
3740    /// `STATE_MIN_SIZE` already satisfies its own floor). Only the collector's
3741    /// own recorded size distinguishes "16 bytes, and the target id was
3742    /// written one word past the end" from "24 bytes, exactly as declared."
3743    #[test]
3744    fn the_join_futures_layout_is_the_one_the_abi_declares() {
3745        let target = make_future(poll_ready_now, 0);
3746        // SAFETY: `target` is a well-formed future from `make_future`.
3747        unsafe { nova_rt_task_spawn(target) };
3748        // SAFETY: `target` was just spawned, so `task_id_of` resolves it.
3749        let fut = unsafe { nova_rt_task_join_future(target) };
3750        assert_eq!(
3751            gc::object_info(fut as usize),
3752            Some((FUTURE_SIZE, true)),
3753            "the fat pointer must be exactly the two-word future, scanned"
3754        );
3755        let state = state_of(fut);
3756        assert_eq!(
3757            gc::object_info(state),
3758            Some((JOIN_STATE_SIZE, true)),
3759            "the state object must be the ABI minimum plus the one temp slot \
3760             holding the target task id, scanned"
3761        );
3762        // SAFETY: `fut` is this call's own `FUTURE_SIZE`-byte block.
3763        let poll = unsafe { (fut as *mut usize).add(FUTURE_SLOT_POLL).read() };
3764        let expected: PollFn = poll_join;
3765        assert_eq!(
3766            poll, expected as usize,
3767            "word 0 must be the poll function's address, not the state's"
3768        );
3769    }
3770
3771    /// The layout a bare [`build_future`] call produces, read back from the
3772    /// collector's own records -- the same check as
3773    /// `the_yield_futures_layout_is_the_one_the_abi_declares`, but aimed at
3774    /// the shared constructor directly rather than at one of its callers.
3775    /// After `nova_rt_task_yield_future` was routed through it, `build_future`
3776    /// is where every future's layout in this system actually comes from, so
3777    /// this is the one test that would catch that function's own layout
3778    /// drifting, independent of which caller happens to be tested elsewhere.
3779    #[test]
3780    fn a_build_future_result_has_the_layout_the_abi_declares() {
3781        let fut = test_future(poll_ready_now);
3782        assert_eq!(
3783            gc::object_info(fut as usize),
3784            Some((FUTURE_SIZE, true)),
3785            "the fat pointer must be exactly the two-word future, scanned"
3786        );
3787        let state = state_of(fut);
3788        assert_eq!(
3789            gc::object_info(state),
3790            Some((STATE_MIN_SIZE, true)),
3791            "the state object must be at least the tag and output slots, scanned"
3792        );
3793    }
3794
3795    /// The suspension shape everything else depends on: pending exactly once.
3796    #[test]
3797    fn the_yield_future_pends_once_then_completes() {
3798        let fut = nova_rt_task_yield_future();
3799        // SAFETY: `fut` is a valid future fat pointer, built above.
3800        let (poll, state) = unsafe { read_future(fut) };
3801        // SAFETY: `state` is that future's own state object.
3802        assert_eq!(
3803            unsafe { poll(state, std::ptr::null_mut()) },
3804            POLL_PENDING,
3805            "the first poll must suspend, or `yield_now` never yields"
3806        );
3807        assert_eq!(
3808            unsafe { poll(state, std::ptr::null_mut()) },
3809            POLL_READY,
3810            "the second poll must complete, or `yield_now` never returns"
3811        );
3812    }
3813
3814    /// Each call hands out its own state object, so two suspensions can be
3815    /// alive at once.
3816    ///
3817    /// A shared static state would make the second future start at the first
3818    /// one's advanced tag and complete without ever suspending -- which is the
3819    /// failure asserted here, rather than merely that the addresses differ:
3820    /// distinct addresses alone would also hold for two futures that shared
3821    /// their tag through some other route.
3822    #[test]
3823    fn two_yield_futures_do_not_share_a_resume_tag() {
3824        let a = nova_rt_task_yield_future();
3825        let b = nova_rt_task_yield_future();
3826        assert_ne!(
3827            state_of(a),
3828            state_of(b),
3829            "each call allocates its own state"
3830        );
3831        // SAFETY: both are valid future fat pointers, built above.
3832        let (poll_a, state_a) = unsafe { read_future(a) };
3833        let (poll_b, state_b) = unsafe { read_future(b) };
3834        // SAFETY: each `state` is its own future's state object.
3835        assert_eq!(
3836            unsafe { poll_a(state_a, std::ptr::null_mut()) },
3837            POLL_PENDING
3838        );
3839        assert_eq!(
3840            unsafe { poll_b(state_b, std::ptr::null_mut()) },
3841            POLL_PENDING,
3842            "polling one yield future must not advance another's tag"
3843        );
3844    }
3845
3846    /// The whole `yield_now` shape end to end at the executor level: a task
3847    /// that awaits a yield future gets re-queued and finishes on its next turn.
3848    #[test]
3849    fn a_task_awaiting_a_yield_future_resumes_on_its_next_turn() {
3850        // Shaped like the poll function `async fn f() { yield_now().await }`
3851        // compiles to: the inner future lives in a temp slot, the entry poll
3852        // creates it, and each poll forwards to it.
3853        unsafe extern "C-unwind" fn poll_awaits_yield(state: *mut u8, ctx: *mut u8) -> i64 {
3854            let slots = state as *mut i64;
3855            // SAFETY: a `make_future(_, 1)` state object, so slot
3856            // `STATE_SLOT_TEMPS` is in bounds.
3857            let mut inner = unsafe { slots.add(STATE_SLOT_TEMPS).read() };
3858            if inner == 0 {
3859                inner = nova_rt_task_yield_future() as i64;
3860                // SAFETY: same object, same slot.
3861                unsafe { slots.add(STATE_SLOT_TEMPS).write(inner) };
3862            }
3863            // SAFETY: `inner` is a future this function just built or stored.
3864            let (poll, inner_state) = unsafe { read_future(inner as *mut u8) };
3865            // SAFETY: forwarding the poll ABI, `task_ctx` unchanged.
3866            let status = unsafe { poll(inner_state, ctx) };
3867            if status == POLL_PENDING {
3868                return POLL_PENDING;
3869            }
3870            // SAFETY: same object, output slot.
3871            unsafe { slots.add(STATE_SLOT_OUTPUT).write(11) };
3872            POLL_READY
3873        }
3874        let fut = make_future(poll_awaits_yield, 1);
3875        assert_eq!(
3876            unsafe { nova_rt_task_block_on(fut) },
3877            11,
3878            "the executor must re-poll a task that awaited a yield future"
3879        );
3880    }
3881
3882    /// A poll function that stages a deadline park on its first poll and
3883    /// completes on its second, used to drive the park set without any Nova
3884    /// surface. Mirrors `poll_yield_once`'s shape, including its obligation not
3885    /// to unwind.
3886    unsafe extern "C-unwind" fn poll_park_once(state: *mut u8, _task_ctx: *mut u8) -> i64 {
3887        let slots = state as *mut i64;
3888        // SAFETY: `state` is a `STATE_MIN_SIZE` state object built by the test
3889        // helper below, so both slots are in bounds.
3890        let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
3891        if tag == 0 {
3892            unsafe { slots.add(STATE_SLOT_TAG).write(1) };
3893            stage_park(Wait::Deadline(Instant::now()));
3894            return POLL_PENDING;
3895        }
3896        unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
3897        POLL_READY
3898    }
3899
3900    #[test]
3901    fn a_staged_park_moves_the_task_out_of_the_ready_queue() {
3902        let fut = test_future(poll_park_once);
3903        // SAFETY: `fut` is a well-formed future from `test_future`.
3904        let _id = unsafe { spawn_internal(fut) };
3905        assert_eq!(QUEUE.with(|q| q.borrow().len()), 1, "spawn should queue it");
3906
3907        let id = QUEUE.with(|q| q.borrow_mut().pop_front()).expect("queued");
3908        // SAFETY: `id` was just popped from `QUEUE`.
3909        unsafe { poll_one(id) };
3910
3911        assert_eq!(
3912            QUEUE.with(|q| q.borrow().len()),
3913            0,
3914            "a parked task must NOT be re-queued -- that is the whole change"
3915        );
3916        assert_eq!(
3917            PARKED.with(|p| p.borrow().len()),
3918            1,
3919            "it must be in the park set instead"
3920        );
3921        let _ = id;
3922    }
3923
3924    /// A poll function that stages a bare deadline and then an I/O wait, in
3925    /// two separate `stage_park` calls, on its first poll -- the one
3926    /// legitimate two-parks-in-one-poll combination this task makes legal --
3927    /// and completes on its second.
3928    unsafe extern "C-unwind" fn poll_deadline_then_io(state: *mut u8, _task_ctx: *mut u8) -> i64 {
3929        let slots = state as *mut i64;
3930        // SAFETY: `state` is a `STATE_MIN_SIZE` state object built by
3931        // `test_future`, so both slots are in bounds.
3932        let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
3933        if tag == 0 {
3934            unsafe { slots.add(STATE_SLOT_TAG).write(1) };
3935            stage_park(Wait::Deadline(Instant::now() + Duration::from_secs(30)));
3936            stage_park(Wait::Io {
3937                socket: RawSocket(9),
3938                interest: Interest::Read,
3939                deadline: None,
3940            });
3941            return POLL_PENDING;
3942        }
3943        unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
3944        POLL_READY
3945    }
3946
3947    /// Two separate `stage_park` calls in the same poll -- one bare
3948    /// `Wait::Deadline`, one untimed `Wait::Io` -- must not abort, and must
3949    /// fold into exactly one `PARKED` entry carrying both: `Wait::Io` with
3950    /// its own `deadline` field now populated. Exactly one entry, not two,
3951    /// is the property `Wait::Io`'s own doc comment states: one task has one
3952    /// `PARKED` entry, or every wake path has to remove two.
3953    #[test]
3954    fn a_deadline_and_an_io_wait_stage_together() {
3955        let fut = test_future(poll_deadline_then_io);
3956        // SAFETY: `fut` is a well-formed future from `test_future`.
3957        let id = unsafe { spawn_internal(fut) };
3958        let popped = QUEUE.with(|q| q.borrow_mut().pop_front());
3959        assert_eq!(popped, Some(id), "spawn should have queued it");
3960        // SAFETY: `id` was just popped from `QUEUE`.
3961        unsafe { poll_one(id) };
3962
3963        let entries = PARKED.with(|p| p.borrow().clone());
3964        assert_eq!(
3965            entries.len(),
3966            1,
3967            "a deadline staged alongside an I/O wait must fold into ONE \
3968             PARKED entry, not two -- got: {entries:?}"
3969        );
3970        match entries[0].1 {
3971            Wait::Io {
3972                socket,
3973                interest,
3974                deadline,
3975            } => {
3976                assert_eq!(socket, RawSocket(9));
3977                assert_eq!(interest, Interest::Read);
3978                assert!(
3979                    deadline.is_some(),
3980                    "the separately-staged deadline must ride inside this \
3981                     Io wait"
3982                );
3983            }
3984            other => panic!("expected a single Wait::Io entry, got {other:?}"),
3985        }
3986        PARKED.with(|p| p.borrow_mut().clear());
3987    }
3988
3989    /// The same-kind collision this task keeps: two `Wait::Io` stages in the
3990    /// same poll must still abort -- widening what one poll may stage (a
3991    /// deadline now composes with a task wait or an I/O wait) must not widen
3992    /// which same-kind collisions it tolerates. See `try_stage`'s own doc
3993    /// comment for why this checks `try_stage` directly rather than
3994    /// `stage_park`'s actual abort.
3995    ///
3996    /// `try_stage` has **four** `Err` branches -- the `Wait::Io` arm rejecting
3997    /// `next.io` and `next.task`, and the `Wait::Task` arm rejecting
3998    /// `next.task` and `next.io` -- and for a while this was the only test of
3999    /// any of them, while the widening added a `deadline:` field to all four
4000    /// reconstructed waits that nothing observed. This one covers the
4001    /// `Wait::Io` arm's `next.io` branch; the two tests below cover the other
4002    /// three. All four now check the `deadline:` field as well, because
4003    /// `collision_msg` receives a `Wait` this function *reconstructs* from
4004    /// `Staged` rather than the one that was originally staged, so a mutant
4005    /// hard-coding `deadline: None` there would lose the merged deadline from
4006    /// every collision diagnostic with no test noticing.
4007    #[test]
4008    fn two_io_waits_in_one_poll_still_abort() {
4009        let first = Wait::Io {
4010            socket: RawSocket(1),
4011            interest: Interest::Read,
4012            deadline: None,
4013        };
4014        let second = Wait::Io {
4015            socket: RawSocket(2),
4016            interest: Interest::Write,
4017            deadline: None,
4018        };
4019        let staged =
4020            try_stage(Staged::default(), first).expect("the first I/O wait stages cleanly");
4021        let err =
4022            try_stage(staged, second).expect_err("a second I/O wait in the same poll must collide");
4023        assert!(err.contains("two parks staged in one poll"), "got: {err}");
4024        assert!(
4025            err.contains("deadline: None"),
4026            "with no deadline staged, the reconstructed I/O wait must say so: {err}"
4027        );
4028
4029        // The same collision with a deadline already merged in, so the
4030        // `deadline:` field the reconstruction fills from `next.deadline` is
4031        // actually observed.
4032        let staged = try_stage(
4033            Staged::default(),
4034            Wait::Deadline(Instant::now() + Duration::from_secs(5)),
4035        )
4036        .expect("a bare deadline stages");
4037        let staged = try_stage(staged, first).expect("an I/O wait joins a staged deadline");
4038        let err = try_stage(staged, second)
4039            .expect_err("a second I/O wait must still collide, deadline or not");
4040        assert!(
4041            err.contains("deadline: Some("),
4042            "the merged deadline must appear in the wait the diagnostic \
4043             reconstructs: {err}"
4044        );
4045    }
4046
4047    /// Task+Task in one poll must still abort -- the collision spec §6.1 asked
4048    /// for by name ("Task+Task and Io+Io **still abort**, so the widening did
4049    /// not widen too far") and that no test covered.
4050    ///
4051    /// It is not merely hygiene. `poll_timeout`'s abandonment path makes these
4052    /// branches production-reachable: before it discarded the park its
4053    /// abandoned inner staged, this exact pairing is what aborted the process
4054    /// from `timeout(d, h1.join())` followed by `h2.join().await`. The
4055    /// behaviour under test is the one that *should* abort -- a genuine
4056    /// two-parks-in-one-poll bug -- so it has to keep aborting while the
4057    /// abandonment case stops reaching it.
4058    ///
4059    /// Both ids are named in the assertions, and in order: the diagnostic's
4060    /// value is that it says *which* two waits clashed, so a mutant reporting
4061    /// the incoming wait twice, or reversing the pair, would still contain
4062    /// "two parks staged in one poll".
4063    #[test]
4064    fn two_task_waits_in_one_poll_still_abort() {
4065        let first = Wait::Task {
4066            id: 1,
4067            deadline: None,
4068        };
4069        let second = Wait::Task {
4070            id: 2,
4071            deadline: None,
4072        };
4073        let staged =
4074            try_stage(Staged::default(), first).expect("the first task wait stages cleanly");
4075        let err = try_stage(staged, second)
4076            .expect_err("a second task wait in the same poll must collide");
4077        assert!(err.contains("two parks staged in one poll"), "got: {err}");
4078        assert!(
4079            err.contains("Task { id: 1, deadline: None } then Task { id: 2"),
4080            "the diagnostic must name the already-staged wait first and the \
4081             incoming one second: {err}"
4082        );
4083
4084        // Again with a deadline already merged in -- the `Wait::Task` arm's own
4085        // reconstruction of the previous task wait, whose `deadline:` field is
4086        // otherwise unobserved by any test.
4087        let at = Instant::now() + Duration::from_secs(5);
4088        let staged =
4089            try_stage(Staged::default(), Wait::Deadline(at)).expect("a bare deadline stages");
4090        let staged = try_stage(staged, first).expect("a task wait joins a staged deadline");
4091        let err = try_stage(staged, second)
4092            .expect_err("a second task wait must still collide, deadline or not");
4093        assert!(
4094            err.contains("deadline: Some("),
4095            "the merged deadline must appear in the wait the diagnostic \
4096             reconstructs: {err}"
4097        );
4098    }
4099
4100    /// A `Wait::Task` and a `Wait::Io` in one poll must still abort, **in
4101    /// either order** -- the last two of `try_stage`'s four `Err` branches
4102    /// (`Wait::Io`'s `next.task` rejection and `Wait::Task`'s `next.io` one).
4103    ///
4104    /// Both orders, because they are two *different* branches in two different
4105    /// match arms, not one property observed twice: deleting either rejection
4106    /// leaves the other passing. Nothing about waiting on a sibling task
4107    /// composes with waiting on a socket the way a deadline composes with
4108    /// both, which is the distinction `Staged`'s doc comment draws and this
4109    /// pins.
4110    #[test]
4111    fn a_task_wait_and_an_io_wait_in_one_poll_still_abort_in_either_order() {
4112        let task = Wait::Task {
4113            id: 3,
4114            deadline: None,
4115        };
4116        let io = Wait::Io {
4117            socket: RawSocket(4),
4118            interest: Interest::Write,
4119            deadline: None,
4120        };
4121        let at = Instant::now() + Duration::from_secs(5);
4122
4123        // Task first, then Io -- `try_stage`'s `Wait::Io` arm, `next.task`
4124        // branch, which reconstructs the previous wait as a `Wait::Task`.
4125        let staged = try_stage(Staged::default(), task).expect("the task wait stages cleanly");
4126        let err = try_stage(staged, io).expect_err("an I/O wait must not join a staged task wait");
4127        assert!(err.contains("two parks staged in one poll"), "got: {err}");
4128        assert!(
4129            err.contains("Task { id: 3, deadline: None } then Io {"),
4130            "the diagnostic must reconstruct the staged task wait, then name \
4131             the incoming I/O wait: {err}"
4132        );
4133        let staged =
4134            try_stage(Staged::default(), Wait::Deadline(at)).expect("a bare deadline stages");
4135        let staged = try_stage(staged, task).expect("a task wait joins a staged deadline");
4136        let err = try_stage(staged, io)
4137            .expect_err("an I/O wait must still collide with a timed task wait");
4138        assert!(
4139            err.contains("deadline: Some("),
4140            "the merged deadline must ride in the reconstructed task wait: {err}"
4141        );
4142
4143        // Io first, then Task -- `try_stage`'s `Wait::Task` arm, `next.io`
4144        // branch, which reconstructs the previous wait as a `Wait::Io`.
4145        let staged = try_stage(Staged::default(), io).expect("the I/O wait stages cleanly");
4146        let err = try_stage(staged, task).expect_err("a task wait must not join a staged I/O wait");
4147        assert!(err.contains("two parks staged in one poll"), "got: {err}");
4148        assert!(
4149            err.contains("Io { socket: RawSocket(4)"),
4150            "the diagnostic must reconstruct the staged I/O wait with its own \
4151             socket, not the incoming task wait: {err}"
4152        );
4153        assert!(
4154            err.contains("then Task { id: 3"),
4155            "and name the incoming task wait second: {err}"
4156        );
4157        let staged =
4158            try_stage(Staged::default(), Wait::Deadline(at)).expect("a bare deadline stages");
4159        let staged = try_stage(staged, io).expect("an I/O wait joins a staged deadline");
4160        let err = try_stage(staged, task)
4161            .expect_err("a task wait must still collide with a timed I/O wait");
4162        assert!(
4163            err.contains("deadline: Some("),
4164            "the merged deadline must ride in the reconstructed I/O wait: {err}"
4165        );
4166    }
4167
4168    /// Two deadlines in one poll merge to the earlier, **in either order**.
4169    ///
4170    /// Both orders on purpose: order-independence is the property, and a
4171    /// single-order test passes against a `min` written backwards.
4172    #[test]
4173    fn two_deadlines_in_one_poll_merge_to_the_earlier() {
4174        let base = Instant::now();
4175        let soon = base + Duration::from_secs(1);
4176        let later = base + Duration::from_secs(30);
4177
4178        let staged =
4179            try_stage(Staged::default(), Wait::Deadline(later)).expect("first deadline stages");
4180        let staged = try_stage(staged, Wait::Deadline(soon))
4181            .expect("a second deadline must merge, not collide");
4182        assert_eq!(
4183            staged.deadline,
4184            Some(soon),
4185            "later-then-sooner must keep the earlier"
4186        );
4187
4188        let staged =
4189            try_stage(Staged::default(), Wait::Deadline(soon)).expect("first deadline stages");
4190        let staged = try_stage(staged, Wait::Deadline(later))
4191            .expect("a second deadline must merge, not collide");
4192        assert_eq!(
4193            staged.deadline,
4194            Some(soon),
4195            "sooner-then-later must keep the earlier"
4196        );
4197    }
4198
4199    /// A task wait and a deadline co-stage, in either order -- this is the
4200    /// pair `timeout(d, handle.join())` needs and that aborted the process
4201    /// before this increment.
4202    #[test]
4203    fn a_task_wait_and_a_deadline_co_stage_in_either_order() {
4204        let at = Instant::now() + Duration::from_secs(5);
4205
4206        let staged = try_stage(
4207            Staged::default(),
4208            Wait::Task {
4209                id: 7,
4210                deadline: None,
4211            },
4212        )
4213        .expect("task stages");
4214        let staged = try_stage(staged, Wait::Deadline(at))
4215            .expect("a deadline must join a task wait, not collide");
4216        assert_eq!((staged.task, staged.deadline), (Some(7), Some(at)));
4217
4218        let staged = try_stage(Staged::default(), Wait::Deadline(at)).expect("deadline stages");
4219        let staged = try_stage(
4220            staged,
4221            Wait::Task {
4222                id: 7,
4223                deadline: None,
4224            },
4225        )
4226        .expect("a task wait must join a deadline, not collide");
4227        assert_eq!((staged.task, staged.deadline), (Some(7), Some(at)));
4228    }
4229
4230    /// A **timed** `Wait::Io` and a bare `Wait::Deadline` merge to the
4231    /// earlier, regardless of arrival order **or** which side carries which
4232    /// magnitude.
4233    ///
4234    /// `two_deadlines_in_one_poll_merge_to_the_earlier` only ever stages a
4235    /// bare `Wait::Deadline` on both sides, so it never reaches `try_stage`'s
4236    /// `Wait::Io` arm's own inner `deadline` merge -- that site only fires
4237    /// when `next.deadline` is already `Some` from an earlier stage *and*
4238    /// the incoming `Wait::Io` itself carries `deadline: Some(_)`.
4239    ///
4240    /// That site is `match next.deadline { Some(prev) => prev.min(at), None
4241    /// => at }`, and a two-argument `min` breaks in two independent ways --
4242    /// `Some(prev) => at` (discard the already-staged deadline, keep only
4243    /// the incoming one) and `Some(prev) => prev` (the opposite: freeze on
4244    /// the already-staged one, discard the incoming one) -- each invisible
4245    /// whenever the surviving argument already happens to be the smaller
4246    /// one. Catching both takes both magnitude assignments, the same
4247    /// principle `two_deadlines_in_one_poll_merge_to_the_earlier` already
4248    /// establishes for the standalone arm ("a single-order test passes
4249    /// against a `min` written backwards"):
4250    ///
4251    /// 1. bare `Deadline(soon)` staged first, timed `Io` carrying `later`
4252    ///    second -- `prev` (`soon`) is already the smaller value, so a
4253    ///    `Some(prev) => prev` mutant would accidentally produce the right
4254    ///    answer here; only a `Some(prev) => at` mutant is caught.
4255    /// 2. bare `Deadline(later)` staged first, timed `Io` carrying `soon`
4256    ///    second -- the reverse: `at` (`soon`) is now the smaller value, so
4257    ///    a `Some(prev) => at` mutant would accidentally produce the right
4258    ///    answer here; only a `Some(prev) => prev` mutant is caught.
4259    ///
4260    /// Both are required and neither is redundant with the other -- do not
4261    /// delete one because the other "already covers the merge".
4262    ///
4263    /// A third case (timed `Io` staged first, bare deadline second) instead
4264    /// goes through the standalone `Wait::Deadline` arm, which is covered
4265    /// elsewhere -- it stays here anyway, because what it demonstrates is
4266    /// that the *end state* does not depend on arrival order, not which
4267    /// branch happened to run. Do not delete it as "redundant" either.
4268    #[test]
4269    fn a_timed_io_wait_and_a_bare_deadline_merge_to_the_earlier_in_either_order() {
4270        let base = Instant::now();
4271        let soon = base + Duration::from_secs(1);
4272        let later = base + Duration::from_secs(30);
4273
4274        // Case 1: bare deadline (the earlier value) first, timed Io carrying
4275        // the later value second -- catches a `Some(prev) => at` mutant.
4276        let staged =
4277            try_stage(Staged::default(), Wait::Deadline(soon)).expect("bare deadline stages");
4278        let staged = try_stage(
4279            staged,
4280            Wait::Io {
4281                socket: RawSocket(1),
4282                interest: Interest::Read,
4283                deadline: Some(later),
4284            },
4285        )
4286        .expect("a timed Io wait must merge with a staged deadline, not collide");
4287        assert_eq!(
4288            staged.deadline,
4289            Some(soon),
4290            "deadline(soon)-then-timed-io(later) must keep the earlier"
4291        );
4292
4293        // Case 2: bare deadline (the later value) first, timed Io carrying
4294        // the earlier value second -- catches a `Some(prev) => prev` mutant.
4295        let staged =
4296            try_stage(Staged::default(), Wait::Deadline(later)).expect("bare deadline stages");
4297        let staged = try_stage(
4298            staged,
4299            Wait::Io {
4300                socket: RawSocket(1),
4301                interest: Interest::Read,
4302                deadline: Some(soon),
4303            },
4304        )
4305        .expect("a timed Io wait must merge with a staged deadline, not collide");
4306        assert_eq!(
4307            staged.deadline,
4308            Some(soon),
4309            "deadline(later)-then-timed-io(soon) must keep the earlier"
4310        );
4311
4312        // Case 3: timed Io (carrying the earlier value) first, bare deadline
4313        // (the later value) second -- routes through the standalone
4314        // `Wait::Deadline` arm instead, demonstrating order-independence.
4315        let staged = try_stage(
4316            Staged::default(),
4317            Wait::Io {
4318                socket: RawSocket(1),
4319                interest: Interest::Read,
4320                deadline: Some(soon),
4321            },
4322        )
4323        .expect("timed Io stages");
4324        let staged = try_stage(staged, Wait::Deadline(later))
4325            .expect("a bare deadline must merge with a staged timed Io, not collide");
4326        assert_eq!(
4327            staged.deadline,
4328            Some(soon),
4329            "timed-io-then-deadline must keep the earlier"
4330        );
4331    }
4332
4333    /// A **timed** `Wait::Task` and a bare `Wait::Deadline` merge to the
4334    /// earlier, regardless of arrival order **or** which side carries which
4335    /// magnitude -- the `Wait::Task` counterpart of
4336    /// `a_timed_io_wait_and_a_bare_deadline_merge_to_the_earlier_in_either_order`,
4337    /// for the same reason: `a_task_wait_and_a_deadline_co_stage_in_either_order`
4338    /// only ever stages `Wait::Task { deadline: None, .. }`, so it never
4339    /// reaches the `Wait::Task` arm's own inner `deadline` merge either, and
4340    /// that site breaks in the same two independent ways a two-argument
4341    /// `min` always can: `Some(prev) => at` (discard the already-staged
4342    /// deadline) and `Some(prev) => prev` (discard the incoming one), each
4343    /// invisible whenever the surviving argument already happens to be the
4344    /// smaller one.
4345    ///
4346    /// 1. bare `Deadline(soon)` staged first, timed task carrying `later`
4347    ///    second -- catches a `Some(prev) => at` mutant.
4348    /// 2. bare `Deadline(later)` staged first, timed task carrying `soon`
4349    ///    second -- catches a `Some(prev) => prev` mutant.
4350    ///
4351    /// Both are required and neither is redundant with the other. The third
4352    /// case (timed task first, bare deadline second) goes back through the
4353    /// standalone `Wait::Deadline` arm and stays here anyway, for the same
4354    /// order-independence reason given in the `Io` version -- do not delete
4355    /// any of the three as "redundant".
4356    #[test]
4357    fn a_timed_task_wait_and_a_bare_deadline_merge_to_the_earlier_in_either_order() {
4358        let base = Instant::now();
4359        let soon = base + Duration::from_secs(1);
4360        let later = base + Duration::from_secs(30);
4361
4362        // Case 1: bare deadline (the earlier value) first, timed task
4363        // carrying the later value second -- catches a `Some(prev) => at`
4364        // mutant.
4365        let staged =
4366            try_stage(Staged::default(), Wait::Deadline(soon)).expect("bare deadline stages");
4367        let staged = try_stage(
4368            staged,
4369            Wait::Task {
4370                id: 7,
4371                deadline: Some(later),
4372            },
4373        )
4374        .expect("a timed task wait must merge with a staged deadline, not collide");
4375        assert_eq!(
4376            staged.deadline,
4377            Some(soon),
4378            "deadline(soon)-then-timed-task(later) must keep the earlier"
4379        );
4380
4381        // Case 2: bare deadline (the later value) first, timed task carrying
4382        // the earlier value second -- catches a `Some(prev) => prev`
4383        // mutant.
4384        let staged =
4385            try_stage(Staged::default(), Wait::Deadline(later)).expect("bare deadline stages");
4386        let staged = try_stage(
4387            staged,
4388            Wait::Task {
4389                id: 7,
4390                deadline: Some(soon),
4391            },
4392        )
4393        .expect("a timed task wait must merge with a staged deadline, not collide");
4394        assert_eq!(
4395            staged.deadline,
4396            Some(soon),
4397            "deadline(later)-then-timed-task(soon) must keep the earlier"
4398        );
4399
4400        // Case 3: timed task (carrying the earlier value) first, bare
4401        // deadline (the later value) second -- routes through the
4402        // standalone `Wait::Deadline` arm instead, demonstrating
4403        // order-independence.
4404        let staged = try_stage(
4405            Staged::default(),
4406            Wait::Task {
4407                id: 7,
4408                deadline: Some(soon),
4409            },
4410        )
4411        .expect("timed task stages");
4412        let staged = try_stage(staged, Wait::Deadline(later))
4413            .expect("a bare deadline must merge with a staged timed task, not collide");
4414        assert_eq!(
4415            staged.deadline,
4416            Some(soon),
4417            "timed-task-then-deadline must keep the earlier"
4418        );
4419    }
4420
4421    #[test]
4422    fn a_deadline_park_is_woken_and_the_task_completes() {
4423        let fut = test_future(poll_park_once);
4424        // SAFETY: well-formed future from `test_future`.
4425        let out = unsafe { run_to_completion(fut) };
4426        assert_eq!(
4427            out, 0,
4428            "the parked task must be woken and run to completion"
4429        );
4430        assert!(
4431            PARKED.with(|p| p.borrow().is_empty()),
4432            "the park set must be empty once the loop exits"
4433        );
4434    }
4435
4436    /// Like `poll_park_once`, but the deadline is `STATE_SLOT_TEMPS`
4437    /// milliseconds in the future rather than already due, and the
4438    /// completing poll records -- in its own output slot -- whether the
4439    /// task named by the *second* temp slot was already done at that
4440    /// moment.
4441    ///
4442    /// Final whole-branch review of `park-set`, finding I3: every existing
4443    /// deadline test (`poll_park_once`,
4444    /// `poll_park_then_read_sibling_progress`) stages
4445    /// `Wait::Deadline(Instant::now())` -- already due the instant it is
4446    /// staged -- so `run_to_completion`'s per-poll `wake_due` check (run
4447    /// after every `poll_one`, before the ready queue can ever be observed
4448    /// as drained) always wakes it first. `run_to_completion`'s *other*
4449    /// wake path -- the drained-queue branch's
4450    /// `match earliest_deadline() { Some(at) => wake_due_deadlines(at), ... }`,
4451    /// the one place a real `thread::sleep` runs -- was reachable from no
4452    /// Rust-level test at all. MEASURED: replacing that arm with
4453    /// `Some(_at) => report_deadlock()` left all 59 pre-existing
4454    /// `nova-runtime` tests green; only the end-to-end `nova run` gate
4455    /// (`gate_task_sleep_order_runs`) caught it.
4456    ///
4457    /// **Corrected 2026-08-15 (branch `io-poller-std-net`, Task 1): that
4458    /// quoted match and `wake_due_deadlines` are both gone, and no
4459    /// `thread::sleep` runs in `task.rs` anywhere anymore.** The drained-queue
4460    /// branch now matches `(earliest_deadline(), io_parks().is_empty())`, and
4461    /// the real sleep this finding is about now runs in `poll::wait`'s
4462    /// empty-socket-set branch (`poll.rs`), reached from the `(Some(at),
4463    /// true)` arm of that match. That arm -- unlike this finding's original
4464    /// claim about its predecessor -- **is** covered, by the test just below:
4465    /// `two_not_yet_due_deadlines_drain_the_queue_then_wake_in_deadline_order`
4466    /// stages two not-yet-due deadlines specifically so the ready queue
4467    /// drains with both still parked, forcing exactly this branch to consult
4468    /// `earliest_deadline()` and actually sleep (see that test's own doc
4469    /// comment, which predates this correction and already says so).
4470    unsafe extern "C-unwind" fn poll_park_after_delay(state: *mut u8, _task_ctx: *mut u8) -> i64 {
4471        let slots = state as *mut i64;
4472        // SAFETY: `state` is a `make_future(_, 2)` object: TAG, OUTPUT, a
4473        // delay-in-milliseconds temp, and a sibling-task-id temp.
4474        let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
4475        if tag == 0 {
4476            unsafe { slots.add(STATE_SLOT_TAG).write(1) };
4477            // SAFETY: same object, the delay temp.
4478            let delay_ms = unsafe { slots.add(STATE_SLOT_TEMPS).read() };
4479            stage_park(Wait::Deadline(
4480                Instant::now() + Duration::from_millis(delay_ms as u64),
4481            ));
4482            return POLL_PENDING;
4483        }
4484        // SAFETY: same object, the sibling-id temp, written by the test
4485        // before either task in the pair is ever polled.
4486        let sibling_id = unsafe { slots.add(STATE_SLOT_TEMPS + 1).read() };
4487        let sibling_was_already_done = is_done_internal(sibling_id) as i64;
4488        // SAFETY: same object, output slot.
4489        unsafe { slots.add(STATE_SLOT_OUTPUT).write(sibling_was_already_done) };
4490        POLL_READY
4491    }
4492
4493    /// I3 (final whole-branch review of `park-set`): the one Rust-level gap
4494    /// the review found. Two tasks park on deadlines `SHORT_DELAY_MS` and
4495    /// `LONG_DELAY_MS` in the future -- neither due when staged -- so the
4496    /// ready queue genuinely drains with both still parked, forcing
4497    /// `run_to_completion`'s drained-queue branch to consult
4498    /// `earliest_deadline()` and actually sleep, unlike every other
4499    /// deadline test in this module (see `poll_park_after_delay`'s doc
4500    /// comment). Spawned in *reverse* of deadline order -- the long one
4501    /// first -- so an implementation that woke parked tasks in spawn order
4502    /// rather than deadline order would also be caught here.
4503    ///
4504    /// Asserts completion and ordering only, never elapsed duration, per
4505    /// this project's rule against timing assertions
4506    /// (`docs/adr/0010-conservative-scan-root-test-gating.md` exists
4507    /// because eight tests already flake on real timing). Ordering is read
4508    /// off executor state -- which task's `is_done_internal` the other
4509    /// observes as true at the moment it completes -- never off the clock:
4510    /// nothing here reads elapsed time or makes an assertion about it. The
4511    /// 285ms gap between the two delays is not a timing assertion either;
4512    /// it is margin against ordinary host scheduling delay between staging
4513    /// the two deadlines and the wake that follows. Unlike this module's
4514    /// already-due-deadline tests, where monotonicity alone guarantees the
4515    /// order with no margin needed, this test's ordering claim depends on
4516    /// that margin holding -- an arbitrarily slow host could in principle
4517    /// exceed it. If that ever happens the two assertions on `.output`
4518    /// below fail loudly, which is the safe direction for this kind of
4519    /// dependency to fail in.
4520    #[test]
4521    fn two_not_yet_due_deadlines_drain_the_queue_then_wake_in_deadline_order() {
4522        const SHORT_DELAY_MS: i64 = 15;
4523        const LONG_DELAY_MS: i64 = 300;
4524
4525        let long = make_future(poll_park_after_delay, 2);
4526        let short = make_future(poll_park_after_delay, 2);
4527        // SAFETY: both are `make_future(_, 2)` objects, so slot
4528        // `STATE_SLOT_TEMPS` (the delay) is in bounds.
4529        unsafe {
4530            (state_of(long) as *mut i64)
4531                .add(STATE_SLOT_TEMPS)
4532                .write(LONG_DELAY_MS);
4533            (state_of(short) as *mut i64)
4534                .add(STATE_SLOT_TEMPS)
4535                .write(SHORT_DELAY_MS);
4536        }
4537
4538        // SAFETY: both are well-formed futures from `make_future`.
4539        let long_id = unsafe { spawn_internal(long) };
4540        let short_id = unsafe { spawn_internal(short) };
4541
4542        // Neither id exists until both are spawned, and neither future is
4543        // polled before `run_to_completion` starts below -- `spawn_internal`
4544        // only enqueues -- so stashing each one's sibling id now is still
4545        // strictly before either poll ever reads it.
4546        // SAFETY: both are `make_future(_, 2)` objects, so slot
4547        // `STATE_SLOT_TEMPS + 1` (the sibling id) is in bounds.
4548        unsafe {
4549            (state_of(long) as *mut i64)
4550                .add(STATE_SLOT_TEMPS + 1)
4551                .write(short_id);
4552            (state_of(short) as *mut i64)
4553                .add(STATE_SLOT_TEMPS + 1)
4554                .write(long_id);
4555        }
4556
4557        let root = test_future(poll_ready_now);
4558        // SAFETY: well-formed future from `test_future`.
4559        assert_eq!(
4560            unsafe { run_to_completion(root) },
4561            7,
4562            "the root task must still complete normally"
4563        );
4564
4565        assert!(
4566            PARKED.with(|p| p.borrow().is_empty()),
4567            "both deadline-parked tasks must be woken, not left parked -- an \
4568             empty park set here is exactly what distinguishes this from the \
4569             deadlock this same code path would otherwise report"
4570        );
4571        assert!(
4572            is_done_internal(long_id) && is_done_internal(short_id),
4573            "both tasks must run to completion"
4574        );
4575        assert_eq!(
4576            TASKS.with(|t| t.borrow()[short_id as usize].output),
4577            0,
4578            "the short deadline must wake and complete before the long one, \
4579             so it must not see the long task already done"
4580        );
4581        assert_eq!(
4582            TASKS.with(|t| t.borrow()[long_id as usize].output),
4583            1,
4584            "the long deadline must wake after the short one -- deadline \
4585             order, not spawn order, since long was spawned first -- so it \
4586             must see the short task already done"
4587        );
4588    }
4589
4590    /// A poll function that stages a *timed* task wait -- not yet due -- on
4591    /// its first poll and completes on its second. The target id (`999`)
4592    /// never names a real task, and is not meant to: this exists to drive a
4593    /// park set of nothing but one timed `Wait::Task` through the real
4594    /// drained-queue branch, so only the deadline can wake it, the same way
4595    /// `poll_park_after_delay` does for a bare `Wait::Deadline`.
4596    unsafe extern "C-unwind" fn poll_park_on_timed_task_wait(
4597        state: *mut u8,
4598        _task_ctx: *mut u8,
4599    ) -> i64 {
4600        let slots = state as *mut i64;
4601        // SAFETY: `state` is a `STATE_MIN_SIZE` object from `test_future`.
4602        let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
4603        if tag == 0 {
4604            unsafe { slots.add(STATE_SLOT_TAG).write(1) };
4605            stage_park(Wait::Task {
4606                id: 999,
4607                deadline: Some(Instant::now() + Duration::from_millis(15)),
4608            });
4609            return POLL_PENDING;
4610        }
4611        // SAFETY: same object, output slot.
4612        unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
4613        POLL_READY
4614    }
4615
4616    /// Corrects `run_to_completion`'s own doc comment: a park set holding
4617    /// nothing but a *timed* `Wait::Task`, and no I/O wait, is legitimate
4618    /// waiting, not a deadlock -- only a *bare* one is. Before this test, no
4619    /// test drove that exact park set through `run_to_completion` itself;
4620    /// `earliest_deadline_counts_a_timed_task_wait_and_ignores_a_bare_one`
4621    /// checks `earliest_deadline` in isolation, and
4622    /// `wake_due_wakes_a_task_wait_whose_deadline_elapsed` checks `wake_due`
4623    /// in isolation, but neither exercises the
4624    /// `(earliest_deadline(), io.is_empty())` match in the drained-queue
4625    /// branch, which is the one place `report_deadlock` -- and its
4626    /// `std::process::abort()` -- could fire on this exact park set. If
4627    /// `earliest_deadline` ever again stopped reporting a timed task wait's
4628    /// deadline, this test would not fail cleanly: it would abort the whole
4629    /// test binary, which is the loud failure this class of regression
4630    /// deserves.
4631    ///
4632    /// Uses a not-yet-due deadline, not an already-due one, for the same
4633    /// reason `two_not_yet_due_deadlines_drain_the_queue_then_wake_in_deadline_order`
4634    /// does: an already-due deadline would be caught by the per-poll
4635    /// `wake_due` check before the queue ever drains, never reaching the
4636    /// drained-queue match this test exists to exercise.
4637    #[test]
4638    fn a_lone_timed_task_wait_sleeps_instead_of_deadlocking() {
4639        let fut = test_future(poll_park_on_timed_task_wait);
4640        // SAFETY: well-formed future from `test_future`.
4641        let out = unsafe { run_to_completion(fut) };
4642        assert_eq!(
4643            out, 0,
4644            "a lone timed task wait must be woken by its own deadline and \
4645             complete, not deadlock"
4646        );
4647        assert!(
4648            PARKED.with(|p| p.borrow().is_empty()),
4649            "the park set must be empty once the loop exits"
4650        );
4651    }
4652
4653    #[test]
4654    fn the_earliest_deadline_is_the_one_slept_on() {
4655        let far = Instant::now() + Duration::from_secs(30);
4656        let near = Instant::now();
4657        PARKED.with(|p| {
4658            let mut p = p.borrow_mut();
4659            p.push((7, Wait::Deadline(far)));
4660            p.push((8, Wait::Deadline(near)));
4661        });
4662        assert_eq!(
4663            earliest_deadline(),
4664            Some(near),
4665            "sleeping on the later deadline would strand the earlier task"
4666        );
4667        PARKED.with(|p| p.borrow_mut().clear());
4668    }
4669
4670    /// `wake_due` must wake a task wait whose deadline elapsed.
4671    ///
4672    /// **This is the only test that fails if `wake_due`'s timed-task arm is
4673    /// missing.** Its `retain` ends in `_ => true`, so an unhandled timed
4674    /// `Wait::Task` is not a compile error, not a panic and not a diagnostic
4675    /// -- the task simply stays parked for the rest of the process.
4676    ///
4677    /// Clears `PARKED`/`QUEUE` at both ends, not just the end: the test this
4678    /// task retires (`earliest_deadline_and_wake_due_ignore_task_waits`) only
4679    /// cleared at the end of each phase, on the unstated assumption that
4680    /// whatever ran before it on the same libtest worker thread had already
4681    /// left both empty. `assert!(PARKED...is_empty())` below is exactly the
4682    /// kind of check that assumption can silently falsify, so this test does
4683    /// not carry it forward.
4684    #[test]
4685    fn wake_due_wakes_a_task_wait_whose_deadline_elapsed() {
4686        PARKED.with(|p| p.borrow_mut().clear());
4687        QUEUE.with(|q| q.borrow_mut().clear());
4688        let past = Instant::now();
4689        PARKED.with(|p| {
4690            p.borrow_mut().push((
4691                41,
4692                Wait::Task {
4693                    id: 999,
4694                    deadline: Some(past),
4695                },
4696            ));
4697        });
4698        wake_due(past + Duration::from_millis(1));
4699        let queued = QUEUE.with(|q| q.borrow().contains(&41));
4700        assert!(
4701            queued,
4702            "a timed task wait whose deadline passed must be re-queued"
4703        );
4704        assert!(
4705            PARKED.with(|p| p.borrow().is_empty()),
4706            "and it must be removed from PARKED, not woken twice"
4707        );
4708        PARKED.with(|p| p.borrow_mut().clear());
4709        QUEUE.with(|q| q.borrow_mut().clear());
4710    }
4711
4712    /// A **timed** task wait contributes its deadline; a **bare** one still
4713    /// contributes nothing. This is the split of the old
4714    /// `earliest_deadline_and_wake_due_ignore_task_waits`, whose name asserted
4715    /// the half that this increment reverses.
4716    ///
4717    /// Cleared at the start as well as the end, for the same
4718    /// worker-thread-reuse reason given in
4719    /// `wake_due_wakes_a_task_wait_whose_deadline_elapsed`'s doc comment.
4720    #[test]
4721    fn earliest_deadline_counts_a_timed_task_wait_and_ignores_a_bare_one() {
4722        PARKED.with(|p| p.borrow_mut().clear());
4723        let base = Instant::now();
4724        let soon = base + Duration::from_secs(1);
4725        let later = base + Duration::from_secs(30);
4726        PARKED.with(|p| {
4727            let mut p = p.borrow_mut();
4728            p.push((
4729                1,
4730                Wait::Task {
4731                    id: 999,
4732                    deadline: None,
4733                },
4734            ));
4735            p.push((
4736                2,
4737                Wait::Task {
4738                    id: 998,
4739                    deadline: Some(later),
4740                },
4741            ));
4742            p.push((3, Wait::Deadline(soon)));
4743        });
4744        assert_eq!(earliest_deadline(), Some(soon));
4745
4746        PARKED.with(|p| p.borrow_mut().clear());
4747        PARKED.with(|p| {
4748            let mut p = p.borrow_mut();
4749            p.push((
4750                1,
4751                Wait::Task {
4752                    id: 999,
4753                    deadline: None,
4754                },
4755            ));
4756            p.push((
4757                2,
4758                Wait::Task {
4759                    id: 998,
4760                    deadline: Some(later),
4761                },
4762            ));
4763        });
4764        assert_eq!(
4765            earliest_deadline(),
4766            Some(later),
4767            "a timed task wait is the only deadline here and must be found"
4768        );
4769        PARKED.with(|p| p.borrow_mut().clear());
4770    }
4771
4772    /// `wake_due`'s `Wait::Io { deadline: Some(_), .. }` arm, behaviourally.
4773    ///
4774    /// Reverting that arm still compiles (the trailing `_ => true` swallows
4775    /// it silently), which is exactly what makes it *not* one of the two
4776    /// compiler-forced sites -- unlike `earliest_deadline`/`deadlock_report`,
4777    /// nothing here stops a backwards `<=`/`<` or a dropped `woken.push` from
4778    /// shipping green. Three entries, one of each shape a timed `Wait::Io`
4779    /// can be in relative to `now`, so a mutant that wakes too many or too
4780    /// few is caught either way: a due I/O wait (must wake), a not-yet-due
4781    /// one (must not), and an untimed one (must never be treated as due at
4782    /// all -- the same property a bare `Wait::Task` has, and
4783    /// `wake_due_wakes_a_task_wait_whose_deadline_elapsed` is the analogous
4784    /// check on `wake_due`'s `Wait::Task` arm).
4785    #[test]
4786    fn wake_due_wakes_a_due_io_wait_and_leaves_the_rest_parked() {
4787        let due = Instant::now();
4788        let not_due = due + Duration::from_secs(30);
4789        PARKED.with(|p| {
4790            let mut p = p.borrow_mut();
4791            p.push((
4792                1,
4793                Wait::Io {
4794                    socket: RawSocket(1),
4795                    interest: Interest::Read,
4796                    deadline: Some(due),
4797                },
4798            ));
4799            p.push((
4800                2,
4801                Wait::Io {
4802                    socket: RawSocket(2),
4803                    interest: Interest::Write,
4804                    deadline: Some(not_due),
4805                },
4806            ));
4807            p.push((
4808                3,
4809                Wait::Io {
4810                    socket: RawSocket(3),
4811                    interest: Interest::Read,
4812                    deadline: None,
4813                },
4814            ));
4815        });
4816        wake_due(Instant::now());
4817        assert_eq!(
4818            QUEUE.with(|q| q.borrow().clone()),
4819            std::collections::VecDeque::from([1]),
4820            "only the due I/O wait's task must be woken"
4821        );
4822        assert_eq!(
4823            PARKED.with(|p| p.borrow().clone()),
4824            vec![
4825                (
4826                    2,
4827                    Wait::Io {
4828                        socket: RawSocket(2),
4829                        interest: Interest::Write,
4830                        deadline: Some(not_due),
4831                    }
4832                ),
4833                (
4834                    3,
4835                    Wait::Io {
4836                        socket: RawSocket(3),
4837                        interest: Interest::Read,
4838                        deadline: None,
4839                    }
4840                ),
4841            ],
4842            "the not-yet-due and the untimed I/O waits must stay parked"
4843        );
4844        PARKED.with(|p| p.borrow_mut().clear());
4845        QUEUE.with(|q| q.borrow_mut().clear());
4846    }
4847
4848    /// `wake_ready`'s `ready.contains(&socket)` guard, behaviourally -- the
4849    /// exact counterpart of
4850    /// `wake_due_wakes_a_due_io_wait_and_leaves_the_rest_parked`, for the
4851    /// other of this module's two I/O wake paths, and needed for the same
4852    /// reason it was: the `retain`'s trailing `_ => true` swallows any
4853    /// narrowing mistake silently. Widening that arm to a bare
4854    /// `Wait::Io { .. }` -- waking *every* I/O-parked task no matter which
4855    /// socket `poll::wait` actually reported ready -- compiles, and before
4856    /// this test it also passed the whole suite. This is the increment's
4857    /// central new wake source, and nothing else asserted it consults the
4858    /// ready set at all.
4859    ///
4860    /// Two entries on different sockets, one of them reported ready: a mutant
4861    /// that wakes too many fails the `PARKED` assertion, one that wakes too
4862    /// few fails the `QUEUE` assertion. `wake_ready` reads no clock and does
4863    /// no I/O, so nothing here depends on timing.
4864    #[test]
4865    fn wake_ready_wakes_only_the_task_whose_socket_is_ready() {
4866        let ready_sock = RawSocket(11);
4867        let idle_sock = RawSocket(22);
4868        PARKED.with(|p| {
4869            let mut p = p.borrow_mut();
4870            p.push((
4871                1,
4872                Wait::Io {
4873                    socket: ready_sock,
4874                    interest: Interest::Read,
4875                    deadline: None,
4876                },
4877            ));
4878            p.push((
4879                2,
4880                Wait::Io {
4881                    socket: idle_sock,
4882                    interest: Interest::Read,
4883                    deadline: None,
4884                },
4885            ));
4886        });
4887        wake_ready(vec![ready_sock]);
4888        assert_eq!(
4889            QUEUE.with(|q| q.borrow().clone()),
4890            std::collections::VecDeque::from([1]),
4891            "only the task parked on the socket poll::wait reported ready \
4892             must be woken"
4893        );
4894        assert_eq!(
4895            PARKED.with(|p| p.borrow().clone()),
4896            vec![(
4897                2,
4898                Wait::Io {
4899                    socket: idle_sock,
4900                    interest: Interest::Read,
4901                    deadline: None,
4902                }
4903            )],
4904            "a task parked on a socket that was not reported ready must stay \
4905             parked"
4906        );
4907        PARKED.with(|p| p.borrow_mut().clear());
4908        QUEUE.with(|q| q.borrow_mut().clear());
4909    }
4910
4911    /// How many times `poll_spin_n_times` reports `POLL_PENDING`, without
4912    /// ever staging a park, before it completes. Large enough that "the
4913    /// spinner already finished" and "the spinner still has turns left" are
4914    /// unmistakably different states for
4915    /// `a_self_requeuing_task_does_not_starve_a_sibling_parked_on_a_deadline`
4916    /// to tell apart.
4917    const SPIN_TURNS: i64 = 5;
4918
4919    /// A poll function shaped like `yield_now`, or a spinning `join` before
4920    /// Task 3: it re-queues itself by returning `POLL_PENDING` without ever
4921    /// calling `stage_park`, `SPIN_TURNS` times, then completes.
4922    /// `STATE_SLOT_TAG` doubles as the turn counter.
4923    unsafe extern "C-unwind" fn poll_spin_n_times(state: *mut u8, _task_ctx: *mut u8) -> i64 {
4924        let slots = state as *mut i64;
4925        // SAFETY: `state` is a `STATE_MIN_SIZE` object from `test_future`.
4926        let turns = unsafe { slots.add(STATE_SLOT_TAG).read() };
4927        if turns < SPIN_TURNS {
4928            unsafe { slots.add(STATE_SLOT_TAG).write(turns + 1) };
4929            return POLL_PENDING;
4930        }
4931        unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
4932        POLL_READY
4933    }
4934
4935    /// Like `poll_park_once`, except the completing poll writes -- instead of
4936    /// a fixed `0` -- the current `STATE_SLOT_TAG` of a *different* state
4937    /// object, whose address is stashed in this one's first temp slot. Lets a
4938    /// test read out how far a sibling task had progressed at the exact
4939    /// moment this task was woken and completed, with no shared test-global
4940    /// mutable state and no dependence on wall-clock timing.
4941    unsafe extern "C-unwind" fn poll_park_then_read_sibling_progress(
4942        state: *mut u8,
4943        _task_ctx: *mut u8,
4944    ) -> i64 {
4945        let slots = state as *mut i64;
4946        // SAFETY: `state` is a `make_future(_, 1)` object: TAG, OUTPUT, then
4947        // one temp slot holding the sibling's state address.
4948        let tag = unsafe { slots.add(STATE_SLOT_TAG).read() };
4949        if tag == 0 {
4950            unsafe { slots.add(STATE_SLOT_TAG).write(1) };
4951            stage_park(Wait::Deadline(Instant::now()));
4952            return POLL_PENDING;
4953        }
4954        let sibling = unsafe { slots.add(STATE_SLOT_TEMPS).read() } as *mut i64;
4955        // SAFETY: the sibling is a live state object of at least
4956        // `STATE_MIN_SIZE`, spawned by the test and still registered (hence
4957        // still rooted) for as long as this task is running.
4958        let sibling_progress = unsafe { sibling.add(STATE_SLOT_TAG).read() };
4959        unsafe { slots.add(STATE_SLOT_OUTPUT).write(sibling_progress) };
4960        POLL_READY
4961    }
4962
4963    /// The starvation bug a controller review caught in Task 2: a task that
4964    /// keeps re-queueing itself (`yield_now`, or -- until Task 3 -- a
4965    /// spinning `join`) must not be able to starve a sibling parked on a
4966    /// deadline. Before this test existed, `run_to_completion` only ever
4967    /// checked `PARKED` for due deadlines once its inner queue-draining pass
4968    /// found the ready queue empty; a task that never let the queue go empty
4969    /// (by re-queueing itself every turn) meant that check was never reached
4970    /// at all, so a sibling's deadline -- however short -- was never
4971    /// examined until the spinner happened to finish on its own. Task 2's
4972    /// end-to-end `sleep` fixture hit exactly this, underneath a spinning
4973    /// `join`, and hung indefinitely.
4974    ///
4975    /// Deterministic despite using a real clock: the sleeper's deadline is
4976    /// `Instant::now()` at the moment it first parks, so it is already due
4977    /// the instant anything next checks the clock, on any monotonic clock,
4978    /// regardless of how fast or slow the machine is. Neither poll function
4979    /// ever reaches `thread::sleep`, and nothing here asserts on elapsed
4980    /// time -- only on which of two turn counts is smaller.
4981    #[test]
4982    fn a_self_requeuing_task_does_not_starve_a_sibling_parked_on_a_deadline() {
4983        let spinner = test_future(poll_spin_n_times);
4984        // SAFETY: `spinner` is a well-formed future from `test_future`.
4985        let (_, spinner_state) = unsafe { read_future(spinner) };
4986
4987        let sleeper = make_future(poll_park_then_read_sibling_progress, 1);
4988        // SAFETY: `sleeper` has one temp slot (`STATE_SLOT_TEMPS`), per
4989        // `make_future(_, 1)`, to stash the spinner's state address into.
4990        let (_, sleeper_state) = unsafe { read_future(sleeper) };
4991        unsafe {
4992            (sleeper_state as *mut i64)
4993                .add(STATE_SLOT_TEMPS)
4994                .write(spinner_state as i64);
4995        }
4996
4997        // SAFETY: both are well-formed futures built above.
4998        let spinner_id = unsafe { spawn_internal(spinner) };
4999        let sleeper_id = unsafe { spawn_internal(sleeper) };
5000
5001        // A trivial always-ready root, spawned last so `run_to_completion`'s
5002        // own `spawn_internal` call queues it behind the spinner and the
5003        // sleeper -- matching the shape of Task 2's fixture, where `both()`
5004        // (the root) spawns two tasks and then awaits them.
5005        let root = test_future(poll_ready_now);
5006        // SAFETY: well-formed future from `test_future`.
5007        assert_eq!(
5008            unsafe { run_to_completion(root) },
5009            7,
5010            "the root task must still complete normally"
5011        );
5012
5013        assert!(
5014            is_done_internal(sleeper_id),
5015            "the deadline-parked task must have been woken and completed"
5016        );
5017        let sibling_progress_at_wake =
5018            TASKS.with(|tasks| tasks.borrow()[sleeper_id as usize].output);
5019        assert!(
5020            sibling_progress_at_wake < SPIN_TURNS,
5021            "the parked task must be woken while the spinner still has \
5022             pending turns left ({sibling_progress_at_wake} of {SPIN_TURNS} \
5023             turns spent) -- {sibling_progress_at_wake} >= {SPIN_TURNS} means \
5024             it was only woken once the spinner had already finished on its \
5025             own, which is the starvation bug this test exists to catch"
5026        );
5027        assert!(
5028            is_done_internal(spinner_id),
5029            "run_to_completion must not return until the spinner also \
5030             finishes, regardless of when the sleeper was woken"
5031        );
5032    }
5033
5034    /// The fast path `poll_join`'s own doc comment promises: joining a task
5035    /// that has already finished completes on its first poll, with no park
5036    /// staged at all -- "a join on a finished task costs no suspension".
5037    /// Without this branch, every join would park at least once even when
5038    /// nothing is left to wait for.
5039    #[test]
5040    fn joining_an_already_done_task_completes_without_parking() {
5041        let target = make_future(poll_ready_now, 0);
5042        // SAFETY: well-formed future from `make_future`.
5043        let target_id = unsafe { spawn_internal(target) };
5044        // `poll_one` does not pop from `QUEUE` itself -- `spawn_internal`
5045        // queued `target_id`, and that entry must be drained by hand, or the
5046        // later pop below sees this stale entry instead of the joiner's.
5047        let popped_target = QUEUE.with(|q| q.borrow_mut().pop_front());
5048        assert_eq!(
5049            popped_target,
5050            Some(target_id),
5051            "spawn should have queued the target"
5052        );
5053        // SAFETY: `target_id` is currently registered.
5054        unsafe { poll_one(target_id) };
5055        assert!(
5056            is_done_internal(target_id),
5057            "target must be done before the join below means anything"
5058        );
5059
5060        // SAFETY: `target` was spawned above, so `task_id_of` resolves it.
5061        let join_fut = unsafe { nova_rt_task_join_future(target) };
5062        // SAFETY: `join_fut` is a well-formed future `nova_rt_task_join_future`
5063        // built, so spawning it directly as a task is the same as spawning
5064        // any other future -- `poll_one` needs only a `{ poll_code, state }`
5065        // pair, and that is exactly what it is.
5066        let joiner_id = unsafe { spawn_internal(join_fut) };
5067        let popped = QUEUE.with(|q| q.borrow_mut().pop_front());
5068        assert_eq!(
5069            popped,
5070            Some(joiner_id),
5071            "spawn should have queued the joiner"
5072        );
5073        // SAFETY: `joiner_id` is currently registered.
5074        unsafe { poll_one(joiner_id) };
5075
5076        assert!(
5077            is_done_internal(joiner_id),
5078            "joining an already-finished task must complete on the first \
5079             poll, not park"
5080        );
5081        assert!(
5082            PARKED.with(|p| p.borrow().is_empty()),
5083            "no park may be staged on the immediate-ready path"
5084        );
5085    }
5086
5087    /// The suspend path: joining a task that has not yet finished parks on
5088    /// `Wait::Task`, and is woken -- moved back onto the ready queue -- the
5089    /// moment the target completes, with no re-poll of the joiner in
5090    /// between. `poll_join` always stages its wait with `deadline: None`
5091    /// (only a wrapping combinator like a timeout merges one in), so this
5092    /// bare park has no involvement of `wake_due` either -- only a *timed*
5093    /// `Wait::Task`, like a timed `Wait::Io`, would ever reach it.
5094    ///
5095    /// The join future is spawned directly as the joiner task, the same way
5096    /// `joining_an_already_done_task_completes_without_parking` does: a
5097    /// `{ poll_code, state }` pair is all `poll_one` needs, and
5098    /// `nova_rt_task_join_future` hands back exactly that, so no synthetic
5099    /// wrapper poll function is needed to drive `poll_join` through the real
5100    /// stage/commit park protocol (`CURRENT`/`PENDING_PARK`).
5101    #[test]
5102    fn joining_a_pending_task_parks_and_is_woken_when_the_target_finishes() {
5103        let target = make_future(poll_ready_now, 0);
5104        // SAFETY: well-formed future from `make_future`.
5105        let target_id = unsafe { spawn_internal(target) };
5106        // SAFETY: `target` was just spawned, so `task_id_of` resolves it.
5107        let join_fut = unsafe { nova_rt_task_join_future(target) };
5108        // SAFETY: `join_fut` is a well-formed future `nova_rt_task_join_future`
5109        // built.
5110        let joiner_id = unsafe { spawn_internal(join_fut) };
5111
5112        // Drain the queue `spawn_internal` filled for both, so the rest of
5113        // this test can poll in a deliberately chosen order instead of FIFO
5114        // order -- the joiner first, before its target has ever run.
5115        assert_eq!(
5116            QUEUE.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>()),
5117            vec![target_id, joiner_id],
5118            "spawn should have queued both, in order"
5119        );
5120
5121        // SAFETY: `joiner_id` was just spawned, so it is currently registered.
5122        unsafe { poll_one(joiner_id) };
5123        assert!(
5124            !is_done_internal(joiner_id),
5125            "the joiner must not complete while its target is still pending"
5126        );
5127        assert_eq!(
5128            PARKED.with(|p| p.borrow().clone()),
5129            vec![(
5130                joiner_id,
5131                Wait::Task {
5132                    id: target_id,
5133                    deadline: None
5134                }
5135            )],
5136            "the joiner must be parked on the target's id, not re-queued"
5137        );
5138        assert!(
5139            QUEUE.with(|q| q.borrow().is_empty()),
5140            "a parked task must not also sit in the ready queue"
5141        );
5142
5143        // SAFETY: `target_id` is currently registered.
5144        unsafe { poll_one(target_id) };
5145        assert!(
5146            is_done_internal(target_id),
5147            "poll_ready_now completes on its first poll"
5148        );
5149        assert!(
5150            PARKED.with(|p| p.borrow().is_empty()),
5151            "the target's completion must wake the joiner out of the park set"
5152        );
5153        let woken = QUEUE.with(|q| q.borrow_mut().pop_front());
5154        assert_eq!(
5155            woken,
5156            Some(joiner_id),
5157            "the woken joiner must be the one re-queued"
5158        );
5159
5160        // SAFETY: `joiner_id` is currently registered.
5161        unsafe { poll_one(joiner_id) };
5162        assert!(
5163            is_done_internal(joiner_id),
5164            "once woken, the joiner must complete -- its target is now done"
5165        );
5166    }
5167
5168    #[test]
5169    fn a_deadlock_names_every_parked_task_and_its_reason() {
5170        PARKED.with(|p| {
5171            let mut p = p.borrow_mut();
5172            p.push((
5173                1,
5174                Wait::Task {
5175                    id: 2,
5176                    deadline: None,
5177                },
5178            ));
5179            p.push((
5180                2,
5181                Wait::Task {
5182                    id: 1,
5183                    deadline: None,
5184                },
5185            ));
5186        });
5187        let report = deadlock_report();
5188        assert!(report.contains("2 tasks are parked"), "got: {report}");
5189        assert!(
5190            report.contains("task 1 is waiting for task 2 to finish"),
5191            "every parked task must be named, not just the first -- got: {report}"
5192        );
5193        assert!(
5194            report.contains("task 2 is waiting for task 1 to finish"),
5195            "the second parked task is missing -- got: {report}"
5196        );
5197        PARKED.with(|p| p.borrow_mut().clear());
5198    }
5199
5200    /// `deadlock_report`'s singular-vs-plural headline, exercised with
5201    /// exactly one parked task -- the only other test that calls
5202    /// `deadlock_report` (`a_deadlock_names_every_parked_task_and_its_reason`,
5203    /// directly above) pushes two, so a hardcoded `"tasks are"` would pass
5204    /// unnoticed there. A task parked on its own id is also the smallest
5205    /// possible deadlock: nothing else needs to exist for it to be
5206    /// unwakeable.
5207    #[test]
5208    fn a_task_parked_on_itself_is_reported_as_a_deadlock() {
5209        PARKED.with(|p| {
5210            p.borrow_mut().push((
5211                3,
5212                Wait::Task {
5213                    id: 3,
5214                    deadline: None,
5215                },
5216            ))
5217        });
5218        let report = deadlock_report();
5219        assert!(
5220            report.contains("task 3 is waiting for task 3 to finish"),
5221            "got: {report}"
5222        );
5223        assert!(
5224            report.contains("1 task is"),
5225            "singular headline -- got: {report}"
5226        );
5227        PARKED.with(|p| p.borrow_mut().clear());
5228    }
5229
5230    /// The whole point of this task: an I/O park must describe itself as
5231    /// waiting on i/o, not fall through to a deadline-shaped or task-shaped
5232    /// message -- and, more importantly, `run_to_completion`'s drive loop
5233    /// must never treat it as a deadlock in the first place (see the
5234    /// `(None, false)` arm there). This test covers only the message. The
5235    /// `(None, false)` arm itself still has no dedicated *unit* test, but no
5236    /// longer for the reason an earlier version of this comment gave -- that
5237    /// nothing outside a test could populate a real, unresolved `Wait::Io`
5238    /// through `run_to_completion`. `net.rs` now stages exactly that from
5239    /// `connect`, `read`, `write` and `accept`, each passing `deadline: None`;
5240    /// `read_timeout` stages a wait as well and is the one that carries a
5241    /// deadline. A roster with neither a count nor a quantifier, because this
5242    /// comment has now had one of each go wrong. It said "three of its four",
5243    /// which `poll_accept`'s arrival falsified. The replacement said "every
5244    /// socket operation there but `read_timeout`", which the same increment
5245    /// falsified in the other direction: `net_listen`, `net_local_port` and
5246    /// `net_close` are socket operations in that module which stage no wait at
5247    /// all, so "every socket operation" was never the population being
5248    /// described. Naming the poll functions that park is.
5249    /// The arm is reached in production and end to end by
5250    /// every `std/net` runtime fixture: `tests/runtime/net_interleave.nova`'s
5251    /// `connect` in `main` parks the only live task on an untimed `Wait::Io`
5252    /// and drains the queue behind it, which is that arm exactly.
5253    #[test]
5254    fn a_park_on_io_with_no_deadline_is_not_a_deadlock() {
5255        let report = with_parked(
5256            &[(
5257                7,
5258                Wait::Io {
5259                    socket: RawSocket(-1),
5260                    interest: Interest::Read,
5261                    deadline: None,
5262                },
5263            )],
5264            deadlock_report,
5265        );
5266        assert!(
5267            report.contains("waiting on i/o"),
5268            "an I/O park must describe itself, got: {report}"
5269        );
5270    }
5271
5272    /// A poll function that stages a park and then reports `POLL_READY` anyway --
5273    /// the shape that would strand a finished task in the park set.
5274    unsafe extern "C-unwind" fn poll_stage_then_ready(state: *mut u8, _task_ctx: *mut u8) -> i64 {
5275        let slots = state as *mut i64;
5276        stage_park(Wait::Task {
5277            id: 0,
5278            deadline: None,
5279        });
5280        // SAFETY: `state` is a `STATE_MIN_SIZE` object from `test_future`.
5281        unsafe { slots.add(STATE_SLOT_OUTPUT).write(0) };
5282        POLL_READY
5283    }
5284
5285    #[test]
5286    fn a_park_staged_by_a_poll_that_completes_is_discarded() {
5287        let fut = test_future(poll_stage_then_ready);
5288        // SAFETY: well-formed future from `test_future`.
5289        let out = unsafe { run_to_completion(fut) };
5290        assert_eq!(out, 0);
5291        assert!(
5292            PARKED.with(|p| p.borrow().is_empty()),
5293            "a completed task must not be left parked -- it would fake a deadlock forever"
5294        );
5295    }
5296
5297    /// The discard in `poll_one` (`PENDING_PARK.with(|p| p.take())`) must
5298    /// actually clear the cell, not just read it -- otherwise a park staged
5299    /// by one task's completing poll would still be sitting in `PENDING_PARK`
5300    /// when the *next* task is polled, and that unrelated task would be
5301    /// swept into the park set on the first task's stale wait instead of
5302    /// being re-queued. `a_park_staged_by_a_poll_that_completes_is_discarded`
5303    /// cannot catch this: it only ever polls the one task that staged the
5304    /// park, so it never observes what a `.take()` -> `.get()` regression
5305    /// does to whichever task is polled afterward.
5306    #[test]
5307    fn a_discarded_park_does_not_leak_onto_the_next_task_polled() {
5308        let a = test_future(poll_stage_then_ready);
5309        let b = test_future(poll_suspend_once);
5310        // SAFETY: both are well-formed futures from `test_future`.
5311        let (id_a, id_b) = unsafe { (spawn_internal(a), spawn_internal(b)) };
5312        // `spawn_internal` queues each id as it spawns it; drain that so the
5313        // final QUEUE assertion below reflects only what `poll_one` itself
5314        // does with each task, matching
5315        // `a_staged_park_moves_the_task_out_of_the_ready_queue`'s pattern of
5316        // popping before polling.
5317        assert_eq!(
5318            QUEUE.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>()),
5319            vec![id_a, id_b],
5320            "spawn should have queued both, in order"
5321        );
5322        // SAFETY: `id_a`/`id_b` were just returned by `spawn_internal`, so
5323        // both are currently-registered task ids.
5324        unsafe {
5325            poll_one(id_a);
5326            poll_one(id_b);
5327        }
5328        assert!(
5329            PARKED.with(|p| p.borrow().is_empty()),
5330            "b never called stage_park and returned POLL_PENDING on its own; \
5331             it must be re-queued, not parked on a's leftover wait -- got: {:?}",
5332            PARKED.with(|p| p.borrow().clone())
5333        );
5334        assert_eq!(
5335            QUEUE.with(|q| q.borrow().clone()),
5336            std::collections::VecDeque::from([id_b]),
5337            "b must be the one task left in the ready queue"
5338        );
5339    }
5340
5341    /// Compiled on every platform, like `gc.rs`'s own `mod registry`, and for
5342    /// the reason both were `#[cfg(windows)]` until 2026-10-05: both tests
5343    /// below call `gc::collect_for_test`, which runs the real, stack-scanning
5344    /// `collect()`, and that needs `stack_base()` (`gc.rs`). It has glibc
5345    /// Linux and macOS versions now (`docs/adr/0024-gc-stack-bounds-on-unix.md`).
5346    /// Where it has none, `collect()` returns before marking anything, so an
5347    /// `is_some()` assertion would pass vacuously while an `is_none()`
5348    /// assertion would fail outright.
5349    ///
5350    /// **Both tests below are also unconditionally `#[ignore]`d**, for the
5351    /// identical mechanism as `gc.rs`'s `mod registry` (see that module's doc
5352    /// comment, and `docs/adr/0010-conservative-scan-root-test-gating.md` for
5353    /// the measurements): under default test parallelism, a stale stack word
5354    /// in an already-returned frame is intermittently read as a conservative
5355    /// root, in debug as well as release.
5356    /// `an_unspawned_tasks_state_is_swept` is the one that actually flakes --
5357    /// the ADR's diagnostic capture is of this exact test.
5358    /// `a_spawned_tasks_state_is_registered_as_a_gc_root` is gated alongside
5359    /// it rather than left green alone, for the same negative-control
5360    /// pairing reason `gc.rs`'s doc comment explains: otherwise it would keep
5361    /// passing even against a registry that never registered anything, which
5362    /// the sweep test exists to rule out. Reachable with
5363    /// `cargo test -- --ignored`, which CI runs as an advisory,
5364    /// `continue-on-error` step.
5365    ///
5366    /// Neither test is this module's coverage of the *pairing* of
5367    /// `gc::add_root` with `gc::remove_root`. That invariant is asserted
5368    /// deterministically, on the registry itself, by
5369    /// `a_completed_tasks_state_stays_rooted_until_its_output_is_taken`
5370    /// above -- which needs no collection, so it runs on every platform and
5371    /// in every CI job.
5372    mod root_registration {
5373        use super::*;
5374
5375        /// A local copy of `gc.rs`'s `tests::registry::hide`/`reveal`
5376        /// technique: a bitwise-complement encoding that carries an address
5377        /// across a call boundary with no in-range bit pattern for it
5378        /// anywhere in the frame or a register, so the conservative scanner
5379        /// cannot mistake the carried value itself for a root.
5380        ///
5381        /// Duplicated here rather than reused from `gc.rs`: Task 3's
5382        /// `hide`/`reveal` are private to a module nested two levels inside
5383        /// `gc`'s own `#[cfg(test)]` tests (`tests::registry`), not
5384        /// reachable from a sibling module's tests, and the technique is two
5385        /// lines of pure bit-twiddling -- reimplementing it here is less
5386        /// invasive than restructuring `gc.rs`'s existing, already-reviewed
5387        /// test module.
5388        ///
5389        /// `#[inline(never)]` on both, for the same reason as `gc.rs`'s
5390        /// originals: without it, an optimizing build can inline both calls,
5391        /// prove `reveal(hide(x)) == x`, and keep the original `x` live
5392        /// across a collection instead of ever materializing the hidden
5393        /// form.
5394        #[inline(never)]
5395        fn hide(addr: usize) -> usize {
5396            !addr
5397        }
5398
5399        #[inline(never)]
5400        fn reveal(hidden: usize) -> usize {
5401            !hidden
5402        }
5403
5404        /// Build a `poll_suspend_once` future, optionally spawn it, and hand
5405        /// back its state object's address hidden (see [`hide`]).
5406        ///
5407        /// A dedicated `#[inline(never)]` function, not inlined into the
5408        /// tests themselves, so every raw value this scenario needs -- the
5409        /// future's fat pointer, the state address read back from it -- is
5410        /// local to this one call and provably dead once it returns, rather
5411        /// than potentially left live in the *caller's* frame or in a
5412        /// register the caller happens to still be holding it in. This
5413        /// mirrors `gc.rs`'s `tests::registry::setup_parent_and_child`,
5414        /// which documents the same call-boundary requirement in more
5415        /// detail.
5416        ///
5417        /// Both `fut` (itself a live GC allocation -- `make_future`'s fat
5418        /// pointer is `gc::alloc`-ed and scanned) and `state` are `mut` and
5419        /// overwritten by reassignment, never shadowed: shadowing binds a
5420        /// new name without clearing the original stack slot, which is
5421        /// exactly the hazard the task brief's STOP box warns about. `fut`
5422        /// is cleared for the same reason as `state`: left live, its own
5423        /// scanned contents (word 1 is `state`'s address) would transitively
5424        /// re-root `state` regardless of whether `gc::add_root` does
5425        /// anything at all, defeating the test independently of `state`'s
5426        /// own handling.
5427        #[inline(never)]
5428        fn setup_task_state(spawn: bool) -> usize {
5429            let mut fut = make_future(poll_suspend_once, 0);
5430            let mut state = state_of(fut);
5431            let hidden = hide(state);
5432            if spawn {
5433                unsafe {
5434                    nova_rt_task_spawn(fut);
5435                }
5436            }
5437            fut = std::ptr::null_mut::<u8>();
5438            state = 0;
5439            std::hint::black_box(fut);
5440            std::hint::black_box(state);
5441            hidden
5442        }
5443
5444        #[test]
5445        #[ignore = "not flaky on its own -- never observed to fail by itself. \
5446                    Gated only to stay paired with the sweep-asserting control \
5447                    test that does flake: this one asserts an object SURVIVED, \
5448                    which also passes against a collector that frees nothing at \
5449                    all, so running it while its control is ignored would leave \
5450                    a green assertion proving nothing. See \
5451                    docs/adr/0010-conservative-scan-root-test-gating.md. \
5452                    Reachable with `cargo test -- --ignored`."]
5453        fn a_spawned_tasks_state_is_registered_as_a_gc_root() {
5454            // The positive half: a task's state object, reachable only
5455            // through the executor's root registration (no Nova stack slot,
5456            // no register -- see `setup_task_state`), must survive a real
5457            // collection.
5458            let hidden = setup_task_state(true);
5459
5460            gc::collect_for_test();
5461
5462            let state = reveal(hidden);
5463            assert!(
5464                gc::object_info(state).is_some(),
5465                "a parked task's state object was swept; add_root is not wired"
5466            );
5467        }
5468
5469        #[test]
5470        #[ignore = "flaky: asserts an unreachable object was SWEPT, and the \
5471                    conservative stack scan intermittently retains it instead \
5472                    -- a stale stack word in an already-returned frame is read \
5473                    as a root, in debug as well as release, at default test \
5474                    parallelism. Mechanism identified, not fixed; two remedies \
5475                    were each measured to make it worse. See \
5476                    docs/adr/0010-conservative-scan-root-test-gating.md. \
5477                    Reachable with `cargo test -- --ignored`."]
5478        fn an_unspawned_tasks_state_is_swept() {
5479            // The required negative control (task brief STOP box): the
5480            // positive test above is worthless alone, since it would also
5481            // pass against an executor that registers nothing at all AND
5482            // against a collector that frees nothing at all. Identical
5483            // shape to the positive test except `spawn = false`, so the
5484            // state object is never registered and has no other root.
5485            let hidden = setup_task_state(false);
5486
5487            gc::collect_for_test();
5488
5489            let state = reveal(hidden);
5490            assert!(
5491                gc::object_info(state).is_none(),
5492                "an unregistered, unreachable task state survived a collection; \
5493                 this test cannot discriminate a working registry from a \
5494                 collector that frees nothing"
5495            );
5496        }
5497    }
5498}