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}