Skip to main content

sv_sim_sim/sched/
resume.rs

1//! Resume/suspend orchestration (Task 12,
2//! `docs/superpowers/plans/2026-09-23-wave-A-sched.md`).
3//!
4//! **Scope:** ledger `["SC-059","SC-060","SC-061"]`. `ResumeDriver::resume_for`
5//! (SC-061, the multithreaded-stress-test-proof method) shipped in an earlier
6//! commit and is unmodified here — see the fix note below, kept for reference.
7//! This commit adds the rest of Task 12: `start` (SC-059 — `resume(Start)`
8//! exactly once per process, via `ProcessDescriptor::started`'s CAS), the
9//! general one-`Suspend`-variant-to-one-matching-`ResumeArg` dispatch proven by
10//! `each_suspend_variant_gets_the_matching_resume_arg_exactly_once` (SC-060,
11//! hand-orchestrated against `resume_for`/`handle_fork`/`on_child_done` since
12//! Task 13/21's real wiring doesn't exist in this tree yet), and
13//! `handle_fork`/`on_child_done` (fork/join per Resolved design decision D1,
14//! and D1a's v1 sched-internal shadow bookkeeping on `ProcessDescriptor` —
15//! `fork_join_kind`/`fork_remaining`/`fork_any_signaled`/`fork_parent`/
16//! `fork_children`, all shipped unread/unwritten by Task 11).
17//!
18//! **The SC-061 fix itself:** an earlier draft of this plan had `resume_for` take
19//! `table: &mut ProcessTable`, and its test wrapped every thread's call in one
20//! external `Mutex<ProcessTable>` held for the whole call. Combined, those two
21//! choices meant Rust's own `&mut`-exclusivity rules — not any logic inside
22//! `ResumeDriver` — made concurrent entry impossible: a `ResumeDriver` with *no*
23//! reentrancy guard of its own would have passed that test exactly as trivially as
24//! a correct one (a gate that cannot fail). The fix (this file): `resume_for` takes
25//! a **shared** `&ProcessTable` (via `ProcessTable::get`, `process_table.rs`) and
26//! reaches the process's state entirely through `ProcessDescriptor`'s own interior
27//! mutability — `compiled: Mutex<Box<dyn CompiledProcess>>` is locked for exactly
28//! the duration of the `resume()` call (acquired, `.resume(..)` called while held,
29//! released only when the guard drops afterward — never acquired-then-released
30//! before calling `resume`, which would reopen the same race). With no table-wide
31//! lock left anywhere, only `ResumeDriver` correctly locking the right process's
32//! `Mutex` for the whole call is what prevents two threads' `resume()` calls from
33//! genuinely overlapping under real OS thread parallelism — which is what the test
34//! below actually exercises.
35
36use std::sync::atomic::Ordering;
37
38use sv_sim_abi::SimHandle;
39
40use super::{
41    CompiledProcess, JoinKind, ProcessId, ProcessTable, ResumeArg, SchedulerOps, SpawnSpec, Suspend,
42};
43
44/// Errors `ResumeDriver` returns instead of ever panicking or silently dropping a
45/// call — this crate's correctness posture requires a loud, typed error on every
46/// rejected/invalid call, never a silent no-op.
47#[derive(Clone, Copy, PartialEq, Eq, Debug)]
48pub enum ResumeError {
49    UnknownProcess(ProcessId),
50    /// SC-059: `ResumeDriver::start` was called a second time for a
51    /// `ProcessId` whose `ProcessDescriptor::started` CAS was already `true`
52    /// — rejected loudly rather than calling `resume(Start)` twice.
53    AlreadyStarted(ProcessId),
54    /// **NEW (Task 9, B-sched wave, ledger SC-125).** A `wait_order(...)`-
55    /// suspended parent observed `ProcessId` complete out of its own
56    /// declared order — returned by [`ResumeDriver::on_child_done`] instead
57    /// of ever silently accepting the reordering or resuming the parent via
58    /// `ResumeArg::WaitOrderSatisfied` anyway.
59    WaitOrderViolated(ProcessId),
60}
61
62impl std::fmt::Display for ResumeError {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            ResumeError::UnknownProcess(id) => {
66                write!(f, "no process registered for ProcessId {id}")
67            }
68            ResumeError::AlreadyStarted(id) => {
69                write!(
70                    f,
71                    "ProcessId {id} was already started -- resume(Start) may only fire once"
72                )
73            }
74            ResumeError::WaitOrderViolated(id) => {
75                write!(
76                    f,
77                    "ProcessId {id} completed out of its parent's declared wait_order sequence"
78                )
79            }
80        }
81    }
82}
83
84impl std::error::Error for ResumeError {}
85
86/// Drives `CompiledProcess::resume()` calls (scheduler.md §5): `start`
87/// (SC-059), `resume_for` (SC-060/SC-061), and fork/join orchestration
88/// (`handle_fork`/`on_child_done`, Resolved design decision D1). See this
89/// module's doc comment for scope and the SC-061 fix `resume_for` embodies.
90pub struct ResumeDriver;
91
92impl ResumeDriver {
93    /// SC-061: "the scheduler must never call resume() concurrently on the same
94    /// ProcessId; one process activation is the atomicity unit." See this
95    /// module's doc comment for the fix this method embodies.
96    pub fn resume_for(
97        &self,
98        table: &ProcessTable,
99        proc: ProcessId,
100        arg: ResumeArg,
101        state: SimHandle,
102        ops: &dyn SchedulerOps,
103    ) -> Result<Suspend, ResumeError> {
104        let desc = table.get(proc).ok_or(ResumeError::UnknownProcess(proc))?;
105        // The lock is held across the whole `.resume(..)` call below (never
106        // acquired-and-released before calling it) — this is the actual SC-061
107        // guarantee: two threads racing this method for the same `proc` serialize
108        // on this one lock, and nothing else in this method (or in `ProcessTable`)
109        // provides any exclusivity of its own.
110        let mut guard = desc
111            .compiled
112            .lock()
113            .expect("ProcessDescriptor::compiled mutex poisoned by a panicking resume()");
114        // Review fix (A2-part2 r1, binding rule 7): `resume_for` is one of
115        // the two places (`start` is the other) that knows the real `proc`
116        // *before* dispatching into `CompiledProcess::resume` -- exactly the
117        // ambient-state hook `kernel_adapter::with_current_process`/
118        // `real_nba_enqueue` need to attribute an NBA write to its real
119        // `ProcessId` despite the frozen `ProcessFn` ABI carrying none.
120        Ok(super::kernel_adapter::with_current_process(proc, || {
121            guard.resume(state, arg, ops)
122        }))
123    }
124
125    /// SC-059: "the scheduler must call resume(Start) exactly once to begin
126    /// each registered process." The once-only guarantee is
127    /// `ProcessDescriptor::started`'s `AtomicBool` CAS: a losing
128    /// `compare_exchange` (already `true`) is rejected with
129    /// `Err(ResumeError::AlreadyStarted(proc))` *before* `compiled`'s lock is
130    /// even acquired, so a second `start` call never reaches `resume()` at
131    /// all — never a panic, matching `resume_for`'s typed-error style for the
132    /// same reason (misuse rejection, not exhaustive-match fallback, but the
133    /// same no-silent-no-op posture).
134    pub fn start(
135        &self,
136        table: &ProcessTable,
137        proc: ProcessId,
138        state: SimHandle,
139        ops: &dyn SchedulerOps,
140    ) -> Result<Suspend, ResumeError> {
141        let desc = table.get(proc).ok_or(ResumeError::UnknownProcess(proc))?;
142        if desc
143            .started
144            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
145            .is_err()
146        {
147            return Err(ResumeError::AlreadyStarted(proc));
148        }
149        let mut guard = desc
150            .compiled
151            .lock()
152            .expect("ProcessDescriptor::compiled mutex poisoned by a panicking resume()");
153        // Review fix (A2-part2 r1, binding rule 7): see the identical note
154        // on `resume_for` above.
155        Ok(super::kernel_adapter::with_current_process(proc, || {
156            guard.resume(state, ResumeArg::Start, ops)
157        }))
158    }
159
160    /// Fork/join per Resolved design decision D1 (`docs/CONTRACTS.md`), using
161    /// D1a's v1 sched-internal shadow bookkeeping on `ProcessDescriptor`
162    /// (`fork_join_kind`/`fork_remaining`/`fork_any_signaled`, plain fields —
163    /// not `process::ForkFrame`, which `sched` does not own/construct this
164    /// wave). Registers one child per `SpawnSpec` via `ProcessTable::register`
165    /// (`mint_child` stands in for "codegen emits each branch as its own
166    /// process-table kernel"), writes each new child's own
167    /// `ProcessDescriptor::fork_parent = Some(parent)` (LRM 9.3.2 — this is
168    /// `handle_fork`'s one and only writer of `fork_parent`), and sets the
169    /// parent's `fork_children`/`fork_join_kind`/`fork_remaining`/
170    /// `fork_any_signaled` per `kind` (`All => children.len(), Any =>
171    /// 1.min(children.len()), None => 0` — `None` leaves `fork_remaining`
172    /// at 0, the caller-visible "parent proceeds now, no `on_child_done`
173    /// needed" signal). `table: &mut ProcessTable` (not the shared
174    /// `&ProcessTable` `resume_for`/`start` take) is correct here: this
175    /// wave never calls `handle_fork` concurrently with anything else (see
176    /// this module's doc comment).
177    pub fn handle_fork(
178        &self,
179        table: &mut ProcessTable,
180        parent: ProcessId,
181        kind: JoinKind,
182        spawns: &[SpawnSpec],
183        mint_child: &mut dyn FnMut(&SpawnSpec) -> Box<dyn CompiledProcess>,
184    ) -> Vec<ProcessId> {
185        // Join bookkeeping is per FORK, not per parent process: a parent that
186        // runs `fork..join_none; #2; fork..join` has a stray child of the
187        // first fork still running when it blocks on the second. Each fork
188        // gets a fresh generation, stamped on its children, and
189        // `on_child_done` lets only the CURRENT generation's children touch
190        // the parent's join counters (LRM 9.3.2: a join waits for the
191        // processes spawned by its own fork).
192        let group = table.get(parent).map_or(0, |d| d.fork_gen) + 1;
193        let program_thread = table.get(parent).is_some_and(|d| d.program_thread);
194        let mut children = Vec::with_capacity(spawns.len());
195        for spec in spawns {
196            let compiled = mint_child(spec);
197            let child = table.register(compiled);
198            if let Some(child_desc) = table.get_mut(child) {
199                child_desc.fork_parent = Some(parent);
200                child_desc.fork_group = group;
201                child_desc.program_thread = program_thread;
202            }
203            children.push(child);
204        }
205
206        let remaining = match kind {
207            JoinKind::All => children.len() as u32,
208            JoinKind::Any => 1u32.min(children.len() as u32),
209            JoinKind::None => 0,
210        };
211        // Earlier forks' children stay listed (`wait fork`/`disable fork`
212        // cover every immediate child, LRM 9.6.1/9.6.3) except the ones
213        // already reclaimed, pruned here so a `forever` loop of
214        // `fork..join_none` does not grow the list without bound.
215        let mut all_children: Vec<ProcessId> = table
216            .get(parent)
217            .map(|d| d.fork_children.clone())
218            .unwrap_or_default();
219        all_children.retain(|&c| table.get(c).is_some());
220        all_children.extend(children.iter().copied());
221        if let Some(parent_desc) = table.get_mut(parent) {
222            parent_desc.fork_children = all_children;
223            parent_desc.fork_gen = group;
224            parent_desc.fork_join_kind = Some(kind);
225            parent_desc.fork_remaining = remaining;
226            parent_desc.fork_any_signaled = false;
227        }
228
229        children
230    }
231
232    /// D1: reports one fork child's completion to its parent, returning the
233    /// parent's own next `resume()` argument exactly when the join condition
234    /// is satisfied — `None` otherwise (parent not resumed yet, or the
235    /// parent's join was already satisfied and this is a later, detached
236    /// child under `JoinKind::Any`). Looks up `child`'s parent via the
237    /// *child's own* `ProcessDescriptor::fork_parent` (an O(1) read set by
238    /// `handle_fork` above), never a linear scan over every parent's
239    /// `fork_children`.
240    ///
241    /// **Task 9 (B-sched wave, SC-125) additions:** before the ordinary
242    /// `fork_join_kind` (`All`/`Any`/`None`) computation below, this method
243    /// now also checks `child`'s parent for an active `wait_order`
244    /// (`ProcessDescriptor::wait_order`, installed by `Engine::
245    /// apply_wait_order`) or `wait_fork` (`wait_fork_remaining`, installed
246    /// by `Engine::apply_wait_fork`) wait — both genuinely independent of
247    /// `fork_join_kind` in practice (a real `wait fork`/`wait_order` is
248    /// always preceded by a `fork ... join_none`, which leaves
249    /// `fork_join_kind` permanently at `Some(JoinKind::None)`, itself
250    /// always a no-op arm below — see `handle_fork`'s own doc comment).
251    /// Return type changed from `Option<ResumeArg>` to
252    /// `Result<Option<ResumeArg>, ResumeError>` to carry
253    /// `ResumeError::WaitOrderViolated` — every pre-existing call site
254    /// (this file's own tests, `sched::control::Engine::apply_suspend`) is
255    /// updated accordingly; the ordinary `All`/`Any`/`None` paths still
256    /// only ever produce `Ok(..)`, never `Err`.
257    pub fn on_child_done(
258        &self,
259        table: &mut ProcessTable,
260        child: ProcessId,
261    ) -> Result<Option<ResumeArg>, ResumeError> {
262        // Review fix (Task 7 r1, SC-062): the parent lookup below is allowed
263        // to fail -- e.g. a nested/multi-level fork tree where the
264        // intermediate parent is itself a `JoinKind::None` fork child that
265        // completed and was reclaimed synchronously (via `control.rs`'s
266        // `apply_suspend` recursive `Suspend::ForkJoin`->`Suspend::Done`
267        // continuation) *before* this, its own child, ever ran -- so
268        // `table.get_mut(parent)` can legitimately return `None` here. That
269        // must never prevent `child`'s own slot from being reclaimed below:
270        // `child` itself unconditionally returned `Suspend::Done`, and its
271        // slot is `child`'s to reclaim regardless of whether its parent is
272        // still around to be notified. The join-satisfaction computation is
273        // therefore isolated in this inner closure, which is allowed to
274        // short-circuit to `Ok(None)` on its own without early-returning out
275        // of `on_child_done` itself.
276        let result = (|| -> Result<Option<ResumeArg>, ResumeError> {
277            let parent = match table.get(child).and_then(|d| d.fork_parent) {
278                Some(p) => p,
279                None => return Ok(None),
280            };
281
282            // Task 9 (SC-125): `wait_order`, checked first.
283            if let Some(parent_desc) = table.get_mut(parent)
284                && parent_desc.wait_order.is_some()
285            {
286                if parent_desc.wait_order_violated {
287                    // Already-latched violation (see this field's own doc
288                    // comment): this parent's wait can never be satisfied
289                    // again -- every later completion among its declared
290                    // `order` is a no-op here, never a second `Err` and
291                    // never a silently-accepted late match.
292                    return Ok(None);
293                }
294                let (order, next_idx) = parent_desc.wait_order.clone().unwrap();
295                // Review fix (Task 9 r1): `wait_order(...)` only cares about
296                // ordering among the identifiers actually named in `order`.
297                // A sibling fork branch that isn't a member of `order` at
298                // all (e.g. `fork a; b; c; join_none; wait_order(a, c);` --
299                // `b` is not listed) completing early or late is a no-op
300                // here, never a violation: it isn't part of the sequence
301                // being watched, so it can neither advance nor break it.
302                if !order.contains(&child) {
303                    return Ok(None);
304                }
305                if next_idx < order.len() && child == order[next_idx] {
306                    let advanced = next_idx + 1;
307                    if advanced == order.len() {
308                        parent_desc.wait_order = None;
309                        return Ok(Some(ResumeArg::WaitOrderSatisfied));
310                    }
311                    parent_desc.wait_order = Some((order, advanced));
312                    return Ok(None);
313                }
314                // Out-of-order completion -- latch the violation and
315                // surface it loudly now, never silently accept it and never
316                // panic.
317                parent_desc.wait_order_violated = true;
318                return Err(ResumeError::WaitOrderViolated(child));
319            }
320
321            // Task 9 (SC-125): `wait_fork`, independent of `wait_order`
322            // above.
323            if let Some(parent_desc) = table.get_mut(parent)
324                && let Some(remaining) = parent_desc.wait_fork_remaining
325            {
326                let remaining = remaining.saturating_sub(1);
327                return if remaining == 0 {
328                    parent_desc.wait_fork_remaining = None;
329                    Ok(Some(ResumeArg::AllForkChildrenDone))
330                } else {
331                    parent_desc.wait_fork_remaining = Some(remaining);
332                    Ok(None)
333                };
334            }
335
336            // A child of an EARLIER fork (e.g. a `join_none` child still
337            // running) must not count against the join of the fork the
338            // parent is blocked on now -- see `handle_fork`.
339            let child_group = table.get(child).map_or(0, |d| d.fork_group);
340            let parent_desc = match table.get_mut(parent) {
341                Some(d) => d,
342                None => return Ok(None),
343            };
344            if child_group != parent_desc.fork_gen {
345                return Ok(None);
346            }
347            Ok(match parent_desc.fork_join_kind {
348                Some(JoinKind::All) => {
349                    parent_desc.fork_remaining = parent_desc.fork_remaining.saturating_sub(1);
350                    if parent_desc.fork_remaining == 0 {
351                        // Review fix: one-shot latch, symmetric with the `Any` arm's
352                        // `fork_any_signaled` guard below. Without this, `fork_remaining`
353                        // stays saturated at 0 forever (`saturating_sub` never goes
354                        // negative), so a later `on_child_done` call against this same
355                        // parent -- a duplicate/retried completion notification, or a
356                        // future scheduler bug reporting one child twice -- would still
357                        // match `Some(All)` and `fork_remaining == 0` and re-fire
358                        // `Some(ForkChildDone)` a second time, handing the caller a
359                        // second `resume()` to fire against a parent that already
360                        // resumed past its wait (violates SC-060's "exactly one matching
361                        // resume() call fires" guarantee). Clearing `fork_join_kind`
362                        // makes this match fall to the `None` arm on any subsequent
363                        // call for this parent, exactly like `Any`'s `fork_any_signaled`
364                        // check does once it has already signaled.
365                        parent_desc.fork_join_kind = None;
366                        Some(ResumeArg::ForkChildDone { child })
367                    } else {
368                        None
369                    }
370                }
371                Some(JoinKind::Any) => {
372                    if parent_desc.fork_any_signaled {
373                        None
374                    } else {
375                        parent_desc.fork_any_signaled = true;
376                        parent_desc.fork_remaining = parent_desc.fork_remaining.saturating_sub(1);
377                        Some(ResumeArg::ForkChildDone { child })
378                    }
379                }
380                Some(JoinKind::None) | None => None,
381            })
382        })();
383        // Task 7 (ledger SC-062): "Suspend::Done is terminal: the scheduler
384        // drops or recycles the process slot; for fork children, it
385        // notifies the parent per the block's join semantics" -- two
386        // separate actions, not one. `on_child_done` is only ever called
387        // (by `Engine::apply_suspend`'s own `Suspend::Done` arm) once
388        // `child` has itself genuinely returned `Suspend::Done`, so `child`
389        // is unconditionally reclaimed here regardless of whether this
390        // particular completion happens to satisfy/partially-satisfy the
391        // parent's join (`result` being `Ok(Some(..))`) or not -- an early
392        // `All` completion that does not yet zero `fork_remaining`, a later
393        // `Any` straggler arriving after that join already fired, every
394        // `JoinKind::None` detached child, and a `wait_order` violation are
395        // all still real, one-time `Suspend::Done`s for `child` itself, and
396        // each must have its slot reclaimed exactly once. This unconditional
397        // reclaim now also covers the case where the `result` closure above
398        // bailed out early because `child`'s own parent (or the parent's
399        // `fork_join_kind`) was no longer live in the table at all -- e.g.
400        // an intermediate parent in a nested fork tree that was itself
401        // already reclaimed by a `JoinKind::None` synchronous continuation
402        // before `child` ever got a chance to run. Regardless of *why* the
403        // join computation above did or didn't fire, `child`'s own slot
404        // must not leak. A *repeated* notification for an already-removed
405        // `child` (this function's own one-shot-latch tests call
406        // `on_child_done` again for a child already reported done) reaches
407        // `ProcessTable::remove` a second time, which is a harmless `None`
408        // (`slotmap`'s own generational keys), never a panic or a double
409        // free.
410        table.remove(child);
411        result
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::sched::EventWaitSet;
419    use crate::state::{FourStateValue, StateCellId};
420    use std::hash::{Hash, Hasher};
421    use std::sync::{Arc, Mutex};
422    use sv_sim_abi::Region;
423
424    /// A no-op `SchedulerOps` double, `Sync` so `&dyn SchedulerOps` can be shared
425    /// across the SC-061 test's 8 threads. Never touches `state`/`cell`/`value`
426    /// arguments.
427    struct NoopOps;
428    impl SchedulerOps for NoopOps {
429        fn now(&self) -> super::super::SchedTime {
430            super::super::SchedTime { ticks: 0 }
431        }
432        fn current_region(&self) -> Region {
433            Region::Active
434        }
435        fn stage_nba(&self, _cell: StateCellId, _value: FourStateValue, _source: ProcessId) {}
436        fn commit_blocking_write(&self, _cell: StateCellId, _value: FourStateValue) {}
437        fn request_finish(&self, _code: i32) {}
438        fn defer_io(
439            &self,
440            _at: super::super::SchedTime,
441            _region: Region,
442            _cb: super::super::DeferredIoCallback,
443        ) {
444        }
445        fn register_region_callback(
446            &self,
447            _region: Region,
448            _at: super::super::SchedTime,
449            _cb: Box<dyn super::super::RegionCallback>,
450        ) {
451        }
452    }
453
454    /// The in-crate mock standing in for a real CPS-lowered kernel (Task 12's
455    /// `CountingProcess`, this task's slice of it): a reentrancy trap. `inside`
456    /// (a plain, non-atomic `Cell<bool>`) is set `true` on entry and back to
457    /// `false` on exit; a `resume()` call observing it already `true` on entry, or
458    /// finding it flipped back to `false` mid-call, proves a second call
459    /// genuinely overlapped this one under real OS-thread parallelism. A small
460    /// thread-id-derived sleep between the two checks widens the race window so a
461    /// missing/incorrect lock has a real, high-probability chance of tripping the
462    /// trap rather than merely a theoretical one.
463    struct CountingProcess {
464        inside: std::cell::Cell<bool>,
465    }
466
467    impl CountingProcess {
468        fn new() -> Self {
469            CountingProcess {
470                inside: std::cell::Cell::new(false),
471            }
472        }
473    }
474
475    impl CompiledProcess for CountingProcess {
476        fn resume(
477            &mut self,
478            _state: SimHandle,
479            _arg: ResumeArg,
480            _ops: &dyn SchedulerOps,
481        ) -> Suspend {
482            assert!(
483                !self.inside.replace(true),
484                "SC-061 violated: resume() called while another resume() for the \
485                 same ProcessId was already in progress"
486            );
487            let mut hasher = std::collections::hash_map::DefaultHasher::new();
488            std::thread::current().id().hash(&mut hasher);
489            let jitter_micros = hasher.finish() % 500;
490            std::thread::sleep(std::time::Duration::from_micros(jitter_micros));
491            assert!(
492                self.inside.get(),
493                "SC-061 violated: the reentrancy flag was clobbered mid-call by a \
494                 concurrent resume() for the same ProcessId"
495            );
496            self.inside.set(false);
497            Suspend::Done
498        }
499    }
500
501    /// SC-061 — review fix (was a gate that could not fail, see this module's doc
502    /// comment): register 1 `CountingProcess`, share the table via
503    /// `Arc<ProcessTable>` (no external lock of any kind — `resume_for` takes
504    /// `&ProcessTable`, so a cloned `Arc` is all each thread needs), and drive 8
505    /// OS threads' `resume_for` calls at the *same* `ProcessId` concurrently, with
506    /// nothing synchronizing them besides whatever `ResumeDriver`/
507    /// `ProcessDescriptor::compiled`'s own `Mutex` provides internally.
508    ///
509    /// Why a plausible wrong implementation fails this: `resume_for` must route
510    /// every call through `desc.compiled.lock().unwrap().resume(..)`, holding the
511    /// lock across the call. An implementation that (a) never locks `compiled` at
512    /// all, or (b) locks and releases it *before* calling `resume`, lets two
513    /// threads' `resume()` calls genuinely overlap under real OS parallelism; the
514    /// randomized jitter inside `CountingProcess::resume` makes that overlap trip
515    /// the reentrancy trap (an `assert!` panic) with high probability across 8
516    /// threads, which surfaces here as a `JoinHandle::join()` returning `Err`.
517    #[test]
518    fn resume_is_never_called_concurrently_for_the_same_process_id() {
519        let mut table = ProcessTable::new();
520        let proc = table.register(Box::new(CountingProcess::new()));
521        let table = Arc::new(table);
522        let ops = Arc::new(NoopOps);
523
524        let mut handles = Vec::with_capacity(8);
525        for _ in 0..8 {
526            let table = Arc::clone(&table);
527            let ops = Arc::clone(&ops);
528            handles.push(std::thread::spawn(move || {
529                // `sv_sim_abi::SimHandle` wraps a raw `*mut c_void` and derives no
530                // `Send`, so it cannot be captured directly by this `'static`
531                // thread closure. Neither `NoopOps` nor `CountingProcess::resume`
532                // ever dereferences it, so moving its address across as a `usize`
533                // and reconstructing it here (test-local only, no edit to the
534                // `abi`-owned `sv-sim-abi` crate) is sound for this test.
535                let state = SimHandle(std::ptr::null_mut());
536                let driver = ResumeDriver;
537                driver.resume_for(&table, proc, ResumeArg::TimeElapsed, state, ops.as_ref())
538            }));
539        }
540
541        let mut ok_count = 0usize;
542        for h in handles {
543            let result = h
544                .join()
545                .expect("resume_for thread panicked -- SC-061's reentrancy trap tripped");
546            assert!(result.is_ok(), "resume_for returned an error: {result:?}");
547            ok_count += 1;
548        }
549        assert_eq!(
550            ok_count, 8,
551            "all 8 concurrent resume_for calls must complete"
552        );
553    }
554
555    #[test]
556    fn resume_for_an_unregistered_process_id_returns_a_typed_error_not_a_panic() {
557        let table = ProcessTable::new();
558        let ops = NoopOps;
559        let driver = ResumeDriver;
560        let result = driver.resume_for(
561            &table,
562            0,
563            ResumeArg::TimeElapsed,
564            SimHandle(std::ptr::null_mut()),
565            &ops,
566        );
567        assert_eq!(result.unwrap_err(), ResumeError::UnknownProcess(0));
568    }
569
570    /// Mutation-test kill: an implementation that discards `CompiledProcess::
571    /// resume()`'s real return value (e.g. always reporting `Ok(Suspend::Done)`)
572    /// passes every other test in this file, since they only check `is_ok()`/
573    /// the error path. This process's `resume()` return value is keyed off the
574    /// `ResumeArg` it actually receives, so this test also kills a mutant that
575    /// forwards the wrong `ResumeArg` to `resume()`.
576    struct ArgEchoProcess;
577    impl CompiledProcess for ArgEchoProcess {
578        fn resume(
579            &mut self,
580            _state: SimHandle,
581            arg: ResumeArg,
582            _ops: &dyn SchedulerOps,
583        ) -> Suspend {
584            let tag = match arg {
585                ResumeArg::Start => 1,
586                ResumeArg::TimeElapsed => 2,
587                ResumeArg::EventFired { .. } => 3,
588                ResumeArg::ForkChildDone { .. } => 4,
589                ResumeArg::Disabled => 5,
590                ResumeArg::AllForkChildrenDone => 6,
591                ResumeArg::WaitOrderSatisfied => 7,
592            };
593            Suspend::Delay(tag)
594        }
595    }
596
597    #[test]
598    fn resume_for_returns_the_real_suspend_value_for_the_resume_arg_it_was_given() {
599        let mut table = ProcessTable::new();
600        let proc = table.register(Box::new(ArgEchoProcess));
601        let ops = NoopOps;
602        let driver = ResumeDriver;
603
604        let result = driver.resume_for(
605            &table,
606            proc,
607            ResumeArg::Disabled,
608            SimHandle(std::ptr::null_mut()),
609            &ops,
610        );
611        match result {
612            Ok(Suspend::Delay(5)) => {}
613            other => panic!(
614                "resume_for must return the compiled process's actual resume() \
615                 result for the ResumeArg it was actually called with (expected \
616                 Ok(Suspend::Delay(5)) for ResumeArg::Disabled), got {other:?}"
617            ),
618        }
619    }
620
621    /// Mutation-test kill: a `resume_for` that reacts to lock contention with
622    /// `try_lock` + "silently report `Suspend::Done` without calling `resume()`"
623    /// still passes `resume_is_never_called_concurrently_for_the_same_process_id`
624    /// above (that test only asserts no panic and that all calls return `Ok`,
625    /// which a skip trivially satisfies too). This test proves the *other* half
626    /// of SC-061: a contended caller must block on the process's own lock and
627    /// genuinely invoke `resume()`, never skip it.
628    struct RendezvousProcess {
629        call_count: Arc<std::sync::atomic::AtomicUsize>,
630        started_tx: std::sync::mpsc::Sender<()>,
631        release_rx: std::sync::mpsc::Receiver<()>,
632    }
633    impl CompiledProcess for RendezvousProcess {
634        fn resume(
635            &mut self,
636            _state: SimHandle,
637            _arg: ResumeArg,
638            _ops: &dyn SchedulerOps,
639        ) -> Suspend {
640            self.call_count
641                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
642            self.started_tx.send(()).expect("test harness rx dropped");
643            self.release_rx
644                .recv()
645                .expect("test harness did not release this call");
646            Suspend::Done
647        }
648    }
649
650    #[test]
651    fn resume_for_blocks_a_contended_caller_instead_of_silently_skipping_it() {
652        use std::sync::atomic::{AtomicUsize, Ordering};
653        use std::sync::mpsc;
654        use std::time::Duration;
655
656        let call_count = Arc::new(AtomicUsize::new(0));
657        let (started_tx, started_rx) = mpsc::channel();
658        let (release_tx, release_rx) = mpsc::channel();
659
660        let mut table = ProcessTable::new();
661        let proc = table.register(Box::new(RendezvousProcess {
662            call_count: Arc::clone(&call_count),
663            started_tx,
664            release_rx,
665        }));
666        let table = Arc::new(table);
667        let ops = Arc::new(NoopOps);
668
669        // Thread A takes the process's lock and parks inside resume() until we
670        // release it, so it holds the lock for the whole window below.
671        let table_a = Arc::clone(&table);
672        let ops_a = Arc::clone(&ops);
673        let a = std::thread::spawn(move || {
674            let driver = ResumeDriver;
675            driver.resume_for(
676                &table_a,
677                proc,
678                ResumeArg::TimeElapsed,
679                SimHandle(std::ptr::null_mut()),
680                ops_a.as_ref(),
681            )
682        });
683        started_rx
684            .recv_timeout(Duration::from_secs(2))
685            .expect("thread A never entered resume() -- lock never acquired");
686
687        // Thread B races for the SAME ProcessId while A still holds the lock.
688        let table_b = Arc::clone(&table);
689        let ops_b = Arc::clone(&ops);
690        let (b_done_tx, b_done_rx) = mpsc::channel();
691        let b = std::thread::spawn(move || {
692            let driver = ResumeDriver;
693            let result = driver.resume_for(
694                &table_b,
695                proc,
696                ResumeArg::TimeElapsed,
697                SimHandle(std::ptr::null_mut()),
698                ops_b.as_ref(),
699            );
700            let _ = b_done_tx.send(());
701            result
702        });
703
704        // While A still holds the lock, B must NOT have returned yet: a correct
705        // `resume_for` blocks on the contended process's own `Mutex` rather than
706        // `try_lock`-ing and silently reporting `Suspend::Done` on failure.
707        assert!(
708            b_done_rx.recv_timeout(Duration::from_millis(200)).is_err(),
709            "resume_for returned for a contended ProcessId before the holder \
710             released the lock -- it must block on the contended process's own \
711             lock, never silently skip the call"
712        );
713
714        // Release A, then B; both must complete and both must have genuinely
715        // invoked resume() (never overlapping, per SC-061 — that's covered by
716        // the other test above).
717        release_tx.send(()).expect("release channel closed early");
718        let a_result = a.join().expect("thread A panicked");
719        assert!(
720            a_result.is_ok(),
721            "thread A's resume_for errored: {a_result:?}"
722        );
723
724        release_tx.send(()).expect("release channel closed early");
725        let b_result = b.join().expect("thread B panicked");
726        assert!(
727            b_result.is_ok(),
728            "thread B's resume_for errored: {b_result:?}"
729        );
730
731        assert_eq!(
732            call_count.load(Ordering::SeqCst),
733            2,
734            "both contended resume_for calls must have genuinely invoked \
735             resume() -- a try_lock-and-skip implementation would leave this at 1"
736        );
737    }
738
739    /// Mutation-test kill: `ResumeError`'s `Display` impl must report the actual
740    /// unknown `ProcessId`, not a fixed placeholder -- nothing else in this file
741    /// inspects the message text (only `PartialEq`/`Debug` via `unwrap_err`).
742    #[test]
743    fn resume_error_display_reports_the_actual_process_id() {
744        let err = ResumeError::UnknownProcess(42);
745        let msg = err.to_string();
746        assert!(
747            msg.contains("42"),
748            "ResumeError::Display must include the actual unknown ProcessId \
749             (42), got: {msg:?}"
750        );
751    }
752
753    // --- Task 12 remaining work (SC-059, SC-060, fork/join D1) ---
754
755    /// A comparable, owned mirror of `ResumeArg` (which derives neither
756    /// `PartialEq` nor `Hash` — it is a cross-track contract type this task
757    /// does not own, see `docs/CONTRACTS.md`) — lets the tests below assert
758    /// on exactly which `ResumeArg` variant/payload a call log entry was,
759    /// without touching `ResumeArg`'s own derive list.
760    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
761    enum ArgKind {
762        Start,
763        TimeElapsed,
764        EventFired(usize),
765        ForkChildDone(ProcessId),
766        Disabled,
767        AllForkChildrenDone,
768        WaitOrderSatisfied,
769    }
770    impl From<ResumeArg> for ArgKind {
771        fn from(arg: ResumeArg) -> Self {
772            match arg {
773                ResumeArg::Start => ArgKind::Start,
774                ResumeArg::TimeElapsed => ArgKind::TimeElapsed,
775                ResumeArg::EventFired { term_index } => ArgKind::EventFired(term_index),
776                ResumeArg::AllForkChildrenDone => ArgKind::AllForkChildrenDone,
777                ResumeArg::WaitOrderSatisfied => ArgKind::WaitOrderSatisfied,
778                ResumeArg::ForkChildDone { child } => ArgKind::ForkChildDone(child),
779                ResumeArg::Disabled => ArgKind::Disabled,
780            }
781        }
782    }
783
784    /// The in-crate mock standing in for a real CPS-lowered kernel (this
785    /// task's own `CountingProcess` sketch, named `ScriptedProcess` here to
786    /// avoid colliding with the SC-061 reentrancy-trap mock above, which the
787    /// plan's Status note says is the actually-shipped mock under that
788    /// name): records every `(ResumeArg, call_index)` it receives into a
789    /// shared, externally-inspectable log (a plain `Box<dyn CompiledProcess>`
790    /// can't be downcast back out of `ProcessDescriptor::compiled` after
791    /// registration, so the log is an `Arc<Mutex<..>>` the test keeps its
792    /// own clone of), and returns a scripted sequence of `Suspend`s from a
793    /// `VecDeque` pre-loaded at construction (past the end, `Suspend::Done`).
794    struct ScriptedProcess {
795        log: Arc<Mutex<Vec<(ArgKind, usize)>>>,
796        script: std::collections::VecDeque<Suspend>,
797    }
798    impl ScriptedProcess {
799        fn new(log: Arc<Mutex<Vec<(ArgKind, usize)>>>, script: Vec<Suspend>) -> Self {
800            ScriptedProcess {
801                log,
802                script: script.into(),
803            }
804        }
805    }
806    impl CompiledProcess for ScriptedProcess {
807        fn resume(
808            &mut self,
809            _state: SimHandle,
810            arg: ResumeArg,
811            _ops: &dyn SchedulerOps,
812        ) -> Suspend {
813            let mut log = self.log.lock().unwrap();
814            let idx = log.len();
815            log.push((ArgKind::from(arg), idx));
816            drop(log);
817            self.script.pop_front().unwrap_or(Suspend::Done)
818        }
819    }
820
821    fn null_state() -> SimHandle {
822        SimHandle(std::ptr::null_mut())
823    }
824
825    /// A `mint_child` double every `handle_fork` test below shares: mints a
826    /// fresh `ScriptedProcess` (its own, unshared log — these tests only
827    /// inspect the *parent's* log / the `ProcessTable`'s fork-bookkeeping
828    /// fields) that immediately reports `Suspend::Done` when resumed.
829    fn mint_done_child(_: &SpawnSpec) -> Box<dyn CompiledProcess> {
830        Box::new(ScriptedProcess::new(
831            Arc::new(Mutex::new(Vec::new())),
832            vec![Suspend::Done],
833        ))
834    }
835
836    /// SC-059 — "the scheduler must call resume(Start) exactly once to begin
837    /// each registered process."
838    #[test]
839    fn resume_start_is_called_exactly_once_per_process() {
840        let log = Arc::new(Mutex::new(Vec::new()));
841        let mut table = ProcessTable::new();
842        let proc = table.register(Box::new(ScriptedProcess::new(
843            Arc::clone(&log),
844            vec![Suspend::Done],
845        )));
846        let ops = NoopOps;
847        let driver = ResumeDriver;
848
849        let first = driver.start(&table, proc, null_state(), &ops);
850        assert!(first.is_ok(), "the first start() must succeed: {first:?}");
851
852        let second = driver.start(&table, proc, null_state(), &ops);
853        assert_eq!(
854            second.unwrap_err(),
855            ResumeError::AlreadyStarted(proc),
856            "a second start() for the same process must be rejected, never panic"
857        );
858
859        let log = log.lock().unwrap();
860        assert_eq!(
861            log.as_slice(),
862            &[(ArgKind::Start, 0)],
863            "the rejected second start() call must never have reached resume() \
864             at all -- exactly one Start entry, not two"
865        );
866    }
867
868    /// SC-060 — "the scheduler must call resume(arg) exactly once per
869    /// satisfied Suspend the process last returned, with `arg` describing
870    /// why." 4 table-driven sub-cases, one per `Suspend` variant, each with
871    /// its own fresh `ProcessTable` (no cross-contamination between
872    /// sub-cases run in the same test process).
873    #[test]
874    fn each_suspend_variant_gets_the_matching_resume_arg_exactly_once() {
875        let ops = NoopOps;
876        let driver = ResumeDriver;
877
878        // Suspend::Delay -> ResumeArg::TimeElapsed.
879        {
880            let log = Arc::new(Mutex::new(Vec::new()));
881            let mut table = ProcessTable::new();
882            let proc = table.register(Box::new(ScriptedProcess::new(
883                Arc::clone(&log),
884                vec![Suspend::Delay(10), Suspend::Done],
885            )));
886            let started = driver.start(&table, proc, null_state(), &ops).unwrap();
887            assert!(
888                matches!(started, Suspend::Delay(10)),
889                "expected Suspend::Delay(10), got {started:?}"
890            );
891            driver
892                .resume_for(&table, proc, ResumeArg::TimeElapsed, null_state(), &ops)
893                .unwrap();
894            let log = log.lock().unwrap();
895            assert_eq!(
896                log.as_slice(),
897                &[(ArgKind::Start, 0), (ArgKind::TimeElapsed, 1)]
898            );
899        }
900
901        // Suspend::WaitEvents -> ResumeArg::EventFired{term_index}.
902        {
903            let log = Arc::new(Mutex::new(Vec::new()));
904            let mut table = ProcessTable::new();
905            let proc = table.register(Box::new(ScriptedProcess::new(
906                Arc::clone(&log),
907                vec![Suspend::WaitEvents(EventWaitSet::default()), Suspend::Done],
908            )));
909            driver.start(&table, proc, null_state(), &ops).unwrap();
910            driver
911                .resume_for(
912                    &table,
913                    proc,
914                    ResumeArg::EventFired { term_index: 2 },
915                    null_state(),
916                    &ops,
917                )
918                .unwrap();
919            let log = log.lock().unwrap();
920            assert_eq!(
921                log.as_slice(),
922                &[(ArgKind::Start, 0), (ArgKind::EventFired(2), 1)]
923            );
924        }
925
926        // Suspend::ForkJoin -> (via handle_fork/on_child_done) ResumeArg::ForkChildDone.
927        {
928            let log = Arc::new(Mutex::new(Vec::new()));
929            let mut table = ProcessTable::new();
930            let spawns = vec![SpawnSpec {
931                kernel: 0,
932                entry_pc: 0,
933                parent: 0, // the parent below is this table's first registration -> id 0.
934                disable_name: Vec::new(),
935            }];
936            let parent = table.register(Box::new(ScriptedProcess::new(
937                Arc::clone(&log),
938                vec![Suspend::ForkJoin(JoinKind::All, spawns), Suspend::Done],
939            )));
940            assert_eq!(
941                parent, 0,
942                "test assumes the parent is this table's first registration"
943            );
944
945            let started = driver.start(&table, parent, null_state(), &ops).unwrap();
946            let (kind, spawns) = match started {
947                Suspend::ForkJoin(kind, spawns) => (kind, spawns),
948                other => panic!("expected Suspend::ForkJoin, got {other:?}"),
949            };
950            let mut mint = mint_done_child;
951            let children = driver.handle_fork(&mut table, parent, kind, &spawns, &mut mint);
952            assert_eq!(children.len(), 1);
953
954            let arg = driver
955                .on_child_done(&mut table, children[0])
956                .expect("an ordinary All join's on_child_done never errors")
957                .expect("the only child of an All join must signal the parent on completion");
958            driver
959                .resume_for(&table, parent, arg, null_state(), &ops)
960                .unwrap();
961
962            let log = log.lock().unwrap();
963            assert_eq!(log.len(), 2);
964            assert_eq!(log[0], (ArgKind::Start, 0));
965            match log[1].0 {
966                ArgKind::ForkChildDone(child) => assert_eq!(child, children[0]),
967                other => panic!("expected ArgKind::ForkChildDone, got {other:?}"),
968            }
969        }
970
971        // A disabled dynamic wait -> ResumeArg::Disabled.
972        {
973            let log = Arc::new(Mutex::new(Vec::new()));
974            let mut table = ProcessTable::new();
975            let proc = table.register(Box::new(ScriptedProcess::new(
976                Arc::clone(&log),
977                vec![Suspend::WaitEvents(EventWaitSet::default()), Suspend::Done],
978            )));
979            driver.start(&table, proc, null_state(), &ops).unwrap();
980            driver
981                .resume_for(&table, proc, ResumeArg::Disabled, null_state(), &ops)
982                .unwrap();
983            let log = log.lock().unwrap();
984            assert_eq!(
985                log.as_slice(),
986                &[(ArgKind::Start, 0), (ArgKind::Disabled, 1)]
987            );
988        }
989    }
990
991    /// D1 — `JoinKind::All` resumes the parent only after every child
992    /// reports done.
993    #[test]
994    fn join_all_resumes_parent_only_after_every_child_reports_done() {
995        let mut table = ProcessTable::new();
996        let parent = table.register(Box::new(ScriptedProcess::new(
997            Arc::new(Mutex::new(Vec::new())),
998            vec![Suspend::Done],
999        )));
1000        let spawns = vec![
1001            SpawnSpec {
1002                kernel: 0,
1003                entry_pc: 0,
1004                parent,
1005                disable_name: Vec::new(),
1006            },
1007            SpawnSpec {
1008                kernel: 1,
1009                entry_pc: 0,
1010                parent,
1011                disable_name: Vec::new(),
1012            },
1013            SpawnSpec {
1014                kernel: 2,
1015                entry_pc: 0,
1016                parent,
1017                disable_name: Vec::new(),
1018            },
1019        ];
1020        let driver = ResumeDriver;
1021        let mut mint = mint_done_child;
1022        let children = driver.handle_fork(&mut table, parent, JoinKind::All, &spawns, &mut mint);
1023        assert_eq!(children.len(), 3);
1024
1025        assert!(
1026            driver
1027                .on_child_done(&mut table, children[0])
1028                .unwrap()
1029                .is_none(),
1030            "must not resume the parent after only 1 of 3 children"
1031        );
1032        assert!(
1033            driver
1034                .on_child_done(&mut table, children[1])
1035                .unwrap()
1036                .is_none(),
1037            "must not resume the parent after only 2 of 3 children"
1038        );
1039        match driver.on_child_done(&mut table, children[2]) {
1040            Ok(Some(ResumeArg::ForkChildDone { child })) => assert_eq!(child, children[2]),
1041            other => panic!(
1042                "expected Ok(Some(ForkChildDone{{child: children[2]}})) once all 3 \
1043                 children are done, got {other:?}"
1044            ),
1045        }
1046    }
1047
1048    /// D1, review fix — a repeated/extra `on_child_done` call against a
1049    /// parent whose `All` join has already fired must return `None`, never
1050    /// re-signal. Mirrors `join_any_resumes_parent_on_first_child_others_
1051    /// continue_detached`'s double-call proof for `Any`, but for `All`:
1052    /// before the fix, `fork_remaining` stayed saturated at 0 forever
1053    /// (`saturating_sub` never underflows) and `fork_join_kind` was never
1054    /// cleared, so this 4th call would still match `Some(All)` +
1055    /// `fork_remaining == 0` and wrongly return
1056    /// `Some(ForkChildDone { child: children[2] })` a second time.
1057    #[test]
1058    fn join_all_on_child_done_is_a_one_shot_latch_after_the_join_already_fired() {
1059        let mut table = ProcessTable::new();
1060        let parent = table.register(Box::new(ScriptedProcess::new(
1061            Arc::new(Mutex::new(Vec::new())),
1062            vec![Suspend::Done],
1063        )));
1064        let spawns = vec![
1065            SpawnSpec {
1066                kernel: 0,
1067                entry_pc: 0,
1068                parent,
1069                disable_name: Vec::new(),
1070            },
1071            SpawnSpec {
1072                kernel: 1,
1073                entry_pc: 0,
1074                parent,
1075                disable_name: Vec::new(),
1076            },
1077            SpawnSpec {
1078                kernel: 2,
1079                entry_pc: 0,
1080                parent,
1081                disable_name: Vec::new(),
1082            },
1083        ];
1084        let driver = ResumeDriver;
1085        let mut mint = mint_done_child;
1086        let children = driver.handle_fork(&mut table, parent, JoinKind::All, &spawns, &mut mint);
1087        assert_eq!(children.len(), 3);
1088
1089        assert!(
1090            driver
1091                .on_child_done(&mut table, children[0])
1092                .unwrap()
1093                .is_none()
1094        );
1095        assert!(
1096            driver
1097                .on_child_done(&mut table, children[1])
1098                .unwrap()
1099                .is_none()
1100        );
1101        match driver.on_child_done(&mut table, children[2]) {
1102            Ok(Some(ResumeArg::ForkChildDone { child })) => assert_eq!(child, children[2]),
1103            other => panic!(
1104                "expected Ok(Some(ForkChildDone{{child: children[2]}})) once all 3 \
1105                 children are done, got {other:?}"
1106            ),
1107        }
1108
1109        // A repeated/extra completion notification for a child of this same,
1110        // already-satisfied parent (e.g. `children[2]` reported done twice,
1111        // or a duplicate delivery of `children[1]`'s completion) must not
1112        // hand the caller a second `resume()` to fire.
1113        assert!(
1114            driver
1115                .on_child_done(&mut table, children[2])
1116                .unwrap()
1117                .is_none(),
1118            "All must not double-signal the parent once its join has already \
1119             fired -- a repeated on_child_done call must return None"
1120        );
1121        assert!(
1122            driver
1123                .on_child_done(&mut table, children[1])
1124                .unwrap()
1125                .is_none(),
1126            "a duplicate completion report for a different already-done child \
1127             must also return None, not re-fire the parent's resume"
1128        );
1129    }
1130
1131    /// D1 — `JoinKind::Any` resumes the parent on the first child; later
1132    /// children finish detached, with no double-signal.
1133    #[test]
1134    fn join_any_resumes_parent_on_first_child_others_continue_detached() {
1135        let mut table = ProcessTable::new();
1136        let parent = table.register(Box::new(ScriptedProcess::new(
1137            Arc::new(Mutex::new(Vec::new())),
1138            vec![Suspend::Done],
1139        )));
1140        let spawns = vec![
1141            SpawnSpec {
1142                kernel: 0,
1143                entry_pc: 0,
1144                parent,
1145                disable_name: Vec::new(),
1146            },
1147            SpawnSpec {
1148                kernel: 1,
1149                entry_pc: 0,
1150                parent,
1151                disable_name: Vec::new(),
1152            },
1153            SpawnSpec {
1154                kernel: 2,
1155                entry_pc: 0,
1156                parent,
1157                disable_name: Vec::new(),
1158            },
1159        ];
1160        let driver = ResumeDriver;
1161        let mut mint = mint_done_child;
1162        let children = driver.handle_fork(&mut table, parent, JoinKind::Any, &spawns, &mut mint);
1163        assert_eq!(children.len(), 3);
1164
1165        match driver.on_child_done(&mut table, children[0]) {
1166            Ok(Some(ResumeArg::ForkChildDone { child })) => assert_eq!(child, children[0]),
1167            other => panic!(
1168                "expected Ok(Some(ForkChildDone{{child: children[0]}})) on the first \
1169                 Any child, got {other:?}"
1170            ),
1171        }
1172        assert!(
1173            driver
1174                .on_child_done(&mut table, children[1])
1175                .unwrap()
1176                .is_none(),
1177            "Any must not double-signal the parent once it has already resumed"
1178        );
1179
1180        assert_eq!(
1181            table.get(parent).unwrap().fork_children,
1182            children,
1183            "detached children must not be forgotten from fork_children, just \
1184             not blocking"
1185        );
1186    }
1187
1188    /// LRM 9.3.2: a join waits for the processes of ITS OWN fork. A child of
1189    /// an earlier `join_none` fork that is still running when the parent
1190    /// blocks on a later `join`/`join_any` must not decrement that fork's
1191    /// counters (the join bookkeeping is per fork, not per parent), while
1192    /// `fork_children` keeps both forks' children for `wait fork`.
1193    #[test]
1194    fn stray_child_of_an_earlier_fork_never_satisfies_a_later_join() {
1195        for kind in [JoinKind::All, JoinKind::Any] {
1196            let mut table = ProcessTable::new();
1197            let parent = table.register(Box::new(ScriptedProcess::new(
1198                Arc::new(Mutex::new(Vec::new())),
1199                vec![Suspend::Done],
1200            )));
1201            let spec = |kernel| SpawnSpec {
1202                kernel,
1203                entry_pc: 0,
1204                parent,
1205                disable_name: Vec::new(),
1206            };
1207            let driver = ResumeDriver;
1208            let mut mint = mint_done_child;
1209            let stray =
1210                driver.handle_fork(&mut table, parent, JoinKind::None, &[spec(0)], &mut mint);
1211            let own = driver.handle_fork(&mut table, parent, kind, &[spec(1)], &mut mint);
1212            assert_eq!(
1213                table.get(parent).unwrap().fork_children,
1214                [stray[0], own[0]],
1215                "{kind:?}: wait fork / disable fork must still see the earlier fork's child"
1216            );
1217            assert!(
1218                driver
1219                    .on_child_done(&mut table, stray[0])
1220                    .unwrap()
1221                    .is_none(),
1222                "{kind:?}: a stray child of an earlier fork must not release the later join"
1223            );
1224            match driver.on_child_done(&mut table, own[0]) {
1225                Ok(Some(ResumeArg::ForkChildDone { child })) => assert_eq!(child, own[0]),
1226                other => panic!("{kind:?}: the fork's own child must release it, got {other:?}"),
1227            }
1228        }
1229    }
1230
1231    /// Mutation-test kill: `on_child_done`'s `Any` arm gates on
1232    /// `fork_any_signaled`, never on `fork_remaining` reaching a particular
1233    /// value, so a `handle_fork` that set `fork_remaining` to
1234    /// `children.len()` instead of `1.min(children.len())` would pass every
1235    /// other test in this file (`join_any_resumes_parent_on_first_child_
1236    /// others_continue_detached` never reads `fork_remaining` directly).
1237    /// This test asserts `handle_fork`'s own documented field directly,
1238    /// immediately after the call, before any `on_child_done`.
1239    #[test]
1240    fn handle_fork_any_sets_fork_remaining_to_one_regardless_of_child_count() {
1241        let mut table = ProcessTable::new();
1242        let parent = table.register(Box::new(ScriptedProcess::new(
1243            Arc::new(Mutex::new(Vec::new())),
1244            vec![Suspend::Done],
1245        )));
1246        let spawns = vec![
1247            SpawnSpec {
1248                kernel: 0,
1249                entry_pc: 0,
1250                parent,
1251                disable_name: Vec::new(),
1252            },
1253            SpawnSpec {
1254                kernel: 1,
1255                entry_pc: 0,
1256                parent,
1257                disable_name: Vec::new(),
1258            },
1259            SpawnSpec {
1260                kernel: 2,
1261                entry_pc: 0,
1262                parent,
1263                disable_name: Vec::new(),
1264            },
1265        ];
1266        let driver = ResumeDriver;
1267        let mut mint = mint_done_child;
1268        let children = driver.handle_fork(&mut table, parent, JoinKind::Any, &spawns, &mut mint);
1269        assert_eq!(children.len(), 3);
1270
1271        assert_eq!(
1272            table.get(parent).unwrap().fork_remaining,
1273            1,
1274            "JoinKind::Any must set fork_remaining to 1.min(children.len()) == 1, \
1275             never children.len() -- Any only ever waits for the first child"
1276        );
1277    }
1278
1279    /// D1 — `JoinKind::None` never blocks the parent: `handle_fork` returns
1280    /// with `fork_remaining == 0` immediately, with no `on_child_done` call
1281    /// required to unblock it.
1282    #[test]
1283    fn join_none_never_blocks_the_parent() {
1284        let mut table = ProcessTable::new();
1285        let parent = table.register(Box::new(ScriptedProcess::new(
1286            Arc::new(Mutex::new(Vec::new())),
1287            vec![Suspend::Done],
1288        )));
1289        let spawns = vec![SpawnSpec {
1290            kernel: 0,
1291            entry_pc: 0,
1292            parent,
1293            disable_name: Vec::new(),
1294        }];
1295        let driver = ResumeDriver;
1296        let mut mint = mint_done_child;
1297        driver.handle_fork(&mut table, parent, JoinKind::None, &spawns, &mut mint);
1298
1299        let desc = table.get(parent).unwrap();
1300        assert_eq!(desc.fork_join_kind, Some(JoinKind::None));
1301        assert_eq!(
1302            desc.fork_remaining, 0,
1303            "JoinKind::None must leave fork_remaining at 0 -- the caller-visible \
1304             'parent proceeds now' signal, with no on_child_done call needed"
1305        );
1306    }
1307
1308    // --- Task 7 (B-sched wave, ledger SC-062) ---
1309
1310    /// SC-062's own literal acceptance: "A JoinAll fork child returning
1311    /// Suspend::Done correctly satisfies... the parent's join-wait; the
1312    /// child's slot is reclaimed." A `JoinAll` parent with a single child:
1313    /// once that child reports done, both halves must hold -- (a) the
1314    /// existing `ForkChildDone` signal (unchanged behavior), and (b) the
1315    /// child's own `ProcessTable` entry must be gone. Fails if slot reclaim
1316    /// is never wired at all, or if it removes the wrong process (e.g. the
1317    /// parent instead of the child).
1318    #[test]
1319    fn on_child_done_reclaims_the_childs_slot_when_the_join_is_satisfied() {
1320        let mut table = ProcessTable::new();
1321        let parent = table.register(Box::new(ScriptedProcess::new(
1322            Arc::new(Mutex::new(Vec::new())),
1323            vec![Suspend::Done],
1324        )));
1325        let spawns = vec![SpawnSpec {
1326            kernel: 0,
1327            entry_pc: 0,
1328            parent,
1329            disable_name: Vec::new(),
1330        }];
1331        let driver = ResumeDriver;
1332        let mut mint = mint_done_child;
1333        let children = driver.handle_fork(&mut table, parent, JoinKind::All, &spawns, &mut mint);
1334        assert_eq!(children.len(), 1);
1335        let child = children[0];
1336        assert!(
1337            table.get(child).is_some(),
1338            "the freshly minted child must be live before it ever completes"
1339        );
1340
1341        match driver.on_child_done(&mut table, child) {
1342            Ok(Some(ResumeArg::ForkChildDone { child: signaled })) => {
1343                assert_eq!(signaled, child, "the join must signal for the real child")
1344            }
1345            other => panic!("expected Ok(Some(ForkChildDone{{child}})), got {other:?}"),
1346        }
1347        assert!(
1348            table.get(child).is_none(),
1349            "the child's own ProcessTable slot must be reclaimed once its \
1350             Suspend::Done has satisfied the parent's join"
1351        );
1352        assert!(
1353            table.get(parent).is_some(),
1354            "reclaiming the child must never remove the parent"
1355        );
1356    }
1357
1358    /// A `JoinKind::None` child is never awaited (`on_child_done` always
1359    /// returns `None` for it), but it is still a genuine `Suspend::Done`
1360    /// for the child itself and must still be reclaimed -- proving reclaim
1361    /// is unconditional on the *child's* own completion, not gated on
1362    /// whether this particular completion happened to signal the parent.
1363    #[test]
1364    fn on_child_done_reclaims_a_detached_join_none_childs_slot_even_though_it_never_signals() {
1365        let mut table = ProcessTable::new();
1366        let parent = table.register(Box::new(ScriptedProcess::new(
1367            Arc::new(Mutex::new(Vec::new())),
1368            vec![Suspend::Done],
1369        )));
1370        let spawns = vec![SpawnSpec {
1371            kernel: 0,
1372            entry_pc: 0,
1373            parent,
1374            disable_name: Vec::new(),
1375        }];
1376        let driver = ResumeDriver;
1377        let mut mint = mint_done_child;
1378        let children = driver.handle_fork(&mut table, parent, JoinKind::None, &spawns, &mut mint);
1379        let child = children[0];
1380
1381        assert!(
1382            driver.on_child_done(&mut table, child).unwrap().is_none(),
1383            "JoinKind::None never signals the parent"
1384        );
1385        assert!(
1386            table.get(child).is_none(),
1387            "a JoinKind::None child's slot must still be reclaimed on its own \
1388             Suspend::Done, even though it never satisfies any join"
1389        );
1390    }
1391
1392    /// D1, review fix — an earlier draft of this test let an implementation
1393    /// that never writes `fork_parent` pass anyway via an "or the SpawnSpec
1394    /// fed to mint_child" escape hatch that only checked the test's own
1395    /// construction input, trivially true by construction. This version
1396    /// asserts directly on each minted child's *own table entry*, with no
1397    /// fallback to the test's own `SpawnSpec.parent` input.
1398    #[test]
1399    fn spawn_spec_children_reach_parent_scope_via_the_parent_field() {
1400        let mut table = ProcessTable::new();
1401        let parent = table.register(Box::new(ScriptedProcess::new(
1402            Arc::new(Mutex::new(Vec::new())),
1403            vec![Suspend::Done],
1404        )));
1405        let spawns = vec![
1406            SpawnSpec {
1407                kernel: 0,
1408                entry_pc: 0,
1409                parent,
1410                disable_name: Vec::new(),
1411            },
1412            SpawnSpec {
1413                kernel: 1,
1414                entry_pc: 0,
1415                parent,
1416                disable_name: Vec::new(),
1417            },
1418        ];
1419        let driver = ResumeDriver;
1420        let mut mint = mint_done_child;
1421        let children = driver.handle_fork(&mut table, parent, JoinKind::All, &spawns, &mut mint);
1422        assert_eq!(children.len(), 2);
1423
1424        for child in children {
1425            assert_eq!(
1426                table.get(child).unwrap().fork_parent,
1427                Some(parent),
1428                "handle_fork must write each minted child's own fork_parent \
1429                 field -- checked directly on the child's table entry, not via \
1430                 the test's own SpawnSpec.parent input"
1431            );
1432        }
1433    }
1434
1435    /// Review fix (Task 7 r1, SC-062) — regression for the leak found in
1436    /// review: a nested fork tree where the *intermediate* parent is itself
1437    /// reclaimed (its own `on_child_done` call against *its* parent
1438    /// succeeds and removes it from the table) before its own child ever
1439    /// reports done. When that child later completes, `on_child_done` must
1440    /// still reclaim the child's own slot even though `table.get_mut
1441    /// (parent)` now fails -- this is exactly the shape `control.rs`'s
1442    /// `apply_suspend` produces for a `JoinKind::None` fork: the forking
1443    /// process's own next segment is resumed synchronously, in the same
1444    /// call, and can itself return `Suspend::Done` (triggering its own
1445    /// `on_child_done` and reclaim) before any of the children it just
1446    /// spawned have been dispatched at all. Built from `handle_fork`/
1447    /// `on_child_done` calls alone, no codegen/`Engine`, mirroring this
1448    /// module's other fork-tree tests.
1449    ///
1450    /// Before the fix: `on_child_done`'s `table.get(child).and_then(|d|
1451    /// d.fork_parent)?` and `table.get_mut(parent)?` early-returned `None`
1452    /// as soon as the grandchild's parent (`intermediate`) was missing from
1453    /// the table, so the final `table.remove(child)` was never reached and
1454    /// the grandchild's slot leaked forever.
1455    #[test]
1456    fn on_child_done_reclaims_the_childs_slot_even_when_its_own_parent_was_already_reclaimed() {
1457        let mut table = ProcessTable::new();
1458        let driver = ResumeDriver;
1459
1460        // Grandparent `G`, joined `All` on a single child: the intermediate
1461        // process `P`.
1462        let grandparent = table.register(Box::new(ScriptedProcess::new(
1463            Arc::new(Mutex::new(Vec::new())),
1464            vec![Suspend::Done],
1465        )));
1466        let spawns_for_intermediate = vec![SpawnSpec {
1467            kernel: 0,
1468            entry_pc: 0,
1469            parent: grandparent,
1470            disable_name: Vec::new(),
1471        }];
1472        let mut mint = mint_done_child;
1473        let intermediate_children = driver.handle_fork(
1474            &mut table,
1475            grandparent,
1476            JoinKind::All,
1477            &spawns_for_intermediate,
1478            &mut mint,
1479        );
1480        assert_eq!(intermediate_children.len(), 1);
1481        let intermediate = intermediate_children[0];
1482
1483        // `P` (the intermediate) itself forks a `JoinKind::None` child --
1484        // the grandchild whose slot this test is protecting.
1485        let spawns_for_grandchild = vec![SpawnSpec {
1486            kernel: 0,
1487            entry_pc: 0,
1488            parent: intermediate,
1489            disable_name: Vec::new(),
1490        }];
1491        let grandchildren = driver.handle_fork(
1492            &mut table,
1493            intermediate,
1494            JoinKind::None,
1495            &spawns_for_grandchild,
1496            &mut mint,
1497        );
1498        assert_eq!(grandchildren.len(), 1);
1499        let grandchild = grandchildren[0];
1500        assert!(
1501            table.get(grandchild).is_some(),
1502            "the grandchild must be live immediately after being forked"
1503        );
1504
1505        // `P` now completes its own `Suspend::Done` -- exactly what
1506        // `apply_suspend`'s synchronous `JoinKind::None` continuation
1507        // produces when the forking process has nothing left to do right
1508        // after spawning. This satisfies `G`'s `All` join (its only child
1509        // was `P`) and reclaims `P`'s own slot -- *before* the grandchild
1510        // above has been dispatched at all.
1511        match driver.on_child_done(&mut table, intermediate) {
1512            Ok(Some(ResumeArg::ForkChildDone { child })) => assert_eq!(child, intermediate),
1513            other => panic!(
1514                "expected Ok(Some(...)) for P's own completion to satisfy G's \
1515                 single-child All join, got {other:?}"
1516            ),
1517        }
1518        assert!(
1519            table.get(intermediate).is_none(),
1520            "P's own slot must be reclaimed once its Suspend::Done satisfied G's join"
1521        );
1522
1523        // The grandchild now completes. Its own parent (`intermediate`) is
1524        // already gone from the table -- this must not stop its own slot
1525        // from being reclaimed.
1526        assert!(
1527            driver
1528                .on_child_done(&mut table, grandchild)
1529                .unwrap()
1530                .is_none(),
1531            "the grandchild's parent is gone, so there is no join left to \
1532             satisfy -- but that must not stop the grandchild's own slot \
1533             from being reclaimed below"
1534        );
1535        assert!(
1536            table.get(grandchild).is_none(),
1537            "the grandchild's own ProcessTable slot must still be reclaimed \
1538             on its own Suspend::Done even though its parent was already \
1539             removed from the table by the time it completed -- this is the \
1540             leak found in review"
1541        );
1542    }
1543
1544    // --- Task 9 (B-sched wave, ledger SC-125) ---
1545    //
1546    // These exercise `ResumeDriver::on_child_done`'s new `wait_fork`/
1547    // `wait_order` arms directly against a bare `ProcessTable` (this file's
1548    // own established style for `on_child_done`, e.g. the D1 join tests
1549    // above), with `ProcessDescriptor::wait_fork_remaining`/`wait_order` set
1550    // by hand exactly as `sched::control::Engine::apply_wait_fork`/
1551    // `apply_wait_order` would install them -- those two installers are
1552    // separately, directly tested against a real `Engine` in
1553    // `sched::control::tests` (this task's own Engine-integration tests),
1554    // since they need `Engine::apply_suspend`, which this file does not
1555    // have access to.
1556
1557    /// SC-125's own literal `wait fork;` acceptance: resumes the parent only
1558    /// once every live child has completed, never on the first or second.
1559    /// 3 children forked `JoinKind::None` (the only real-world precursor to
1560    /// `wait fork;` -- see `handle_fork`'s own doc comment), with the
1561    /// parent's `wait_fork_remaining` set to `3` (what `apply_wait_fork`
1562    /// would have computed from `fork_children`'s live count at suspend
1563    /// time). Fails if `wait_fork` were wired to the ordinary
1564    /// `fork_join_kind`/`fork_remaining` counter instead of this fresh field
1565    /// -- `fork_join_kind` here is `Some(JoinKind::None)` (handle_fork's own
1566    /// permanent value for that kind), whose arm always returns `None`, so a
1567    /// wrong implementation reusing it would report `None` even after the
1568    /// 3rd completion, never `Some(AllForkChildrenDone)`.
1569    #[test]
1570    fn sc125_wait_fork_resumes_only_once_every_live_child_completes_not_on_the_first() {
1571        let mut table = ProcessTable::new();
1572        let parent = table.register(Box::new(ScriptedProcess::new(
1573            Arc::new(Mutex::new(Vec::new())),
1574            vec![Suspend::Done],
1575        )));
1576        let spawns = vec![
1577            SpawnSpec {
1578                kernel: 0,
1579                entry_pc: 0,
1580                parent,
1581                disable_name: Vec::new(),
1582            },
1583            SpawnSpec {
1584                kernel: 1,
1585                entry_pc: 0,
1586                parent,
1587                disable_name: Vec::new(),
1588            },
1589            SpawnSpec {
1590                kernel: 2,
1591                entry_pc: 0,
1592                parent,
1593                disable_name: Vec::new(),
1594            },
1595        ];
1596        let driver = ResumeDriver;
1597        let mut mint = mint_done_child;
1598        let children = driver.handle_fork(&mut table, parent, JoinKind::None, &spawns, &mut mint);
1599        assert_eq!(children.len(), 3);
1600        table.get_mut(parent).unwrap().wait_fork_remaining = Some(3);
1601
1602        assert!(
1603            driver
1604                .on_child_done(&mut table, children[0])
1605                .unwrap()
1606                .is_none(),
1607            "must not resume the parent after only 1 of 3 live children"
1608        );
1609        assert!(
1610            driver
1611                .on_child_done(&mut table, children[1])
1612                .unwrap()
1613                .is_none(),
1614            "must not resume the parent after only 2 of 3 live children"
1615        );
1616        match driver.on_child_done(&mut table, children[2]) {
1617            Ok(Some(ResumeArg::AllForkChildrenDone)) => {}
1618            other => panic!(
1619                "expected Ok(Some(AllForkChildrenDone)) once all 3 live children have \
1620                 completed, got {other:?}"
1621            ),
1622        }
1623        assert!(
1624            table.get(parent).unwrap().wait_fork_remaining.is_none(),
1625            "wait_fork_remaining must be cleared once the wait is satisfied, matching \
1626             fork_join_kind's own one-shot-latch clearing"
1627        );
1628    }
1629
1630    /// SC-125's `wait_order(...)` acceptance: resumes the parent only once
1631    /// every declared `ProcessId` has completed, in exactly that order.
1632    #[test]
1633    fn sc125_wait_order_resumes_only_when_completions_are_observed_in_the_exact_declared_order() {
1634        let mut table = ProcessTable::new();
1635        let parent = table.register(Box::new(ScriptedProcess::new(
1636            Arc::new(Mutex::new(Vec::new())),
1637            vec![Suspend::Done],
1638        )));
1639        let spawns = vec![
1640            SpawnSpec {
1641                kernel: 0,
1642                entry_pc: 0,
1643                parent,
1644                disable_name: Vec::new(),
1645            },
1646            SpawnSpec {
1647                kernel: 1,
1648                entry_pc: 0,
1649                parent,
1650                disable_name: Vec::new(),
1651            },
1652            SpawnSpec {
1653                kernel: 2,
1654                entry_pc: 0,
1655                parent,
1656                disable_name: Vec::new(),
1657            },
1658        ];
1659        let driver = ResumeDriver;
1660        let mut mint = mint_done_child;
1661        let children = driver.handle_fork(&mut table, parent, JoinKind::None, &spawns, &mut mint);
1662        let (c1, c2, c3) = (children[0], children[1], children[2]);
1663        table.get_mut(parent).unwrap().wait_order = Some((vec![c1, c2, c3], 0));
1664
1665        assert!(
1666            driver.on_child_done(&mut table, c1).unwrap().is_none(),
1667            "must not resume after only the 1st of 3 declared completions"
1668        );
1669        assert!(
1670            driver.on_child_done(&mut table, c2).unwrap().is_none(),
1671            "must not resume after only the 1st and 2nd of 3 declared completions"
1672        );
1673        match driver.on_child_done(&mut table, c3) {
1674            Ok(Some(ResumeArg::WaitOrderSatisfied)) => {}
1675            other => panic!(
1676                "expected Ok(Some(WaitOrderSatisfied)) once all 3 completed in exactly \
1677                 their declared order, got {other:?}"
1678            ),
1679        }
1680    }
1681
1682    /// SC-125's own literal "not a panic, not a silently-accepted
1683    /// reordering" acceptance for `wait_order(...)`: `c2` completes before
1684    /// `c1` (out of the declared `[c1, c2, c3]` order). The parent must
1685    /// never be resumed with `WaitOrderSatisfied` -- not for the misordered
1686    /// completion itself, and not for any later completion either, even one
1687    /// that would otherwise have completed the sequence.
1688    #[test]
1689    fn sc125_wait_order_flags_a_misordered_completion_rather_than_silently_accepting_it() {
1690        let mut table = ProcessTable::new();
1691        let parent = table.register(Box::new(ScriptedProcess::new(
1692            Arc::new(Mutex::new(Vec::new())),
1693            vec![Suspend::Done],
1694        )));
1695        let spawns = vec![
1696            SpawnSpec {
1697                kernel: 0,
1698                entry_pc: 0,
1699                parent,
1700                disable_name: Vec::new(),
1701            },
1702            SpawnSpec {
1703                kernel: 1,
1704                entry_pc: 0,
1705                parent,
1706                disable_name: Vec::new(),
1707            },
1708            SpawnSpec {
1709                kernel: 2,
1710                entry_pc: 0,
1711                parent,
1712                disable_name: Vec::new(),
1713            },
1714        ];
1715        let driver = ResumeDriver;
1716        let mut mint = mint_done_child;
1717        let children = driver.handle_fork(&mut table, parent, JoinKind::None, &spawns, &mut mint);
1718        let (c1, c2, c3) = (children[0], children[1], children[2]);
1719        table.get_mut(parent).unwrap().wait_order = Some((vec![c1, c2, c3], 0));
1720
1721        // c2 completes before c1 -- out of the declared order.
1722        match driver.on_child_done(&mut table, c2) {
1723            Err(ResumeError::WaitOrderViolated(child)) => assert_eq!(child, c2),
1724            other => panic!(
1725                "expected Err(ResumeError::WaitOrderViolated(c2)) for the very completion \
1726                 that broke the declared order, got {other:?}"
1727            ),
1728        }
1729        assert!(
1730            table.get(parent).unwrap().wait_order_violated,
1731            "the violation must be latched on the parent's own descriptor"
1732        );
1733
1734        // c1 and c3 now complete in a way that would otherwise finish the
1735        // sequence -- the parent must never be resumed via
1736        // WaitOrderSatisfied once the order has already been broken.
1737        assert!(
1738            driver.on_child_done(&mut table, c1).unwrap().is_none(),
1739            "a violated wait_order must never be silently satisfied by a later \
1740             completion, even one matching what the sequence originally expected"
1741        );
1742        assert!(
1743            driver.on_child_done(&mut table, c3).unwrap().is_none(),
1744            "a violated wait_order must never be silently satisfied by the final \
1745             completion either"
1746        );
1747    }
1748
1749    /// Review fix (Task 9 r1, SC-125): `wait_order(...)` only watches the
1750    /// identifiers actually named in its `order` list. `fork a; b; c;
1751    /// join_none; wait_order(a, c);` -- `b` is a sibling fork child but is
1752    /// *not* part of `order`. `b` completing before `a` must be a pure
1753    /// no-op: it can neither advance the `a`, `c` sequence nor violate it,
1754    /// and `a` then `c` completing in their declared order must still
1755    /// resume the parent normally. Before this fix, `on_child_done`
1756    /// compared every completing fork child against `order[next_idx]`
1757    /// unconditionally, so `b` (never equal to `order[0] == a`) fell into
1758    /// the "out-of-order" arm and permanently latched
1759    /// `WaitOrderViolated(b)`, deadlocking a fully LRM-legal program.
1760    #[test]
1761    fn sc125_wait_order_ignores_a_sibling_child_not_named_in_order() {
1762        let mut table = ProcessTable::new();
1763        let parent = table.register(Box::new(ScriptedProcess::new(
1764            Arc::new(Mutex::new(Vec::new())),
1765            vec![Suspend::Done],
1766        )));
1767        let spawns = vec![
1768            SpawnSpec {
1769                kernel: 0,
1770                entry_pc: 0,
1771                parent,
1772                disable_name: Vec::new(),
1773            },
1774            SpawnSpec {
1775                kernel: 1,
1776                entry_pc: 0,
1777                parent,
1778                disable_name: Vec::new(),
1779            },
1780            SpawnSpec {
1781                kernel: 2,
1782                entry_pc: 0,
1783                parent,
1784                disable_name: Vec::new(),
1785            },
1786        ];
1787        let driver = ResumeDriver;
1788        let mut mint = mint_done_child;
1789        let children = driver.handle_fork(&mut table, parent, JoinKind::None, &spawns, &mut mint);
1790        let (a, b, c) = (children[0], children[1], children[2]);
1791        // `order` only names `a` and `c` -- `b` is a sibling fork child, not
1792        // part of the declared sequence.
1793        table.get_mut(parent).unwrap().wait_order = Some((vec![a, c], 0));
1794
1795        // `b` completes first, out of turn relative to `a`/`c` -- but `b`
1796        // isn't in `order` at all, so this must be a silent no-op, never a
1797        // violation.
1798        assert!(
1799            driver.on_child_done(&mut table, b).unwrap().is_none(),
1800            "a sibling fork child that isn't named in `order` must never trigger \
1801             WaitOrderViolated, and must not resume the parent either"
1802        );
1803        assert!(
1804            !table.get(parent).unwrap().wait_order_violated,
1805            "an unrelated sibling's completion must never latch wait_order_violated"
1806        );
1807
1808        // `a` then `c` complete in exactly their declared order -- the wait
1809        // must still be satisfiable normally, unaffected by `b`.
1810        assert!(
1811            driver.on_child_done(&mut table, a).unwrap().is_none(),
1812            "must not resume after only the 1st of 2 declared completions"
1813        );
1814        match driver.on_child_done(&mut table, c) {
1815            Ok(Some(ResumeArg::WaitOrderSatisfied)) => {}
1816            other => panic!(
1817                "expected Ok(Some(WaitOrderSatisfied)) once `a` then `c` completed in \
1818                 their declared order, unaffected by `b`'s earlier, unrelated \
1819                 completion, got {other:?}"
1820            ),
1821        }
1822    }
1823}