vj_sim/sched/mod.rs
1//! SCHED (`vj-sim::sched`) — cross-track contract only: the event
2//! scheduler/kernel-control surface shared with STATE, A2-PROCESS, DETERMINISM,
3//! RUNTIME and the debugger (scheduler.md). Real event-queue/time-wheel
4//! implementation belongs to the `sched` track, not this wave.
5
6use vj_abi::{Region, SimHandle, SimTime};
7
8use crate::process::{PredicateFn, StmtId};
9use crate::state::{EdgeKind, FourStateValue, StateCellId};
10
11pub mod assert_control;
12pub mod class_event;
13mod control;
14mod dedup;
15mod deferred;
16mod finals;
17mod guard;
18pub mod host;
19mod io_gate;
20mod kernel_adapter;
21pub mod mailbox;
22pub mod maturation;
23mod nba;
24mod notify;
25mod ops;
26mod order;
27pub mod proc_assert;
28pub mod process_rng;
29mod process_table;
30mod queues;
31mod region_callback;
32mod repeat;
33mod reschedule_req_proposal;
34mod resume;
35pub mod semaphore;
36mod sensitivity;
37mod subscribe;
38pub mod thread_hooks;
39mod violation;
40mod wheel;
41pub use control::*;
42pub use dedup::*;
43pub use deferred::{
44 deferred_call_for, deferred_cancel_for, deferred_report, deferred_report_for,
45 set_deferred_writer,
46};
47pub use finals::*;
48pub use guard::*;
49pub use io_gate::*;
50pub use kernel_adapter::*;
51pub use nba::*;
52pub use notify::*;
53pub use ops::*;
54pub use order::*;
55pub use process_table::*;
56pub use queues::*;
57pub use region_callback::*;
58pub use repeat::*;
59pub use resume::*;
60pub use sensitivity::*;
61pub use subscribe::*;
62pub use thread_hooks::{
63 ThreadEvent, ThreadHook, fire_thread_event, set_thread_hook, thread_hook_installed,
64};
65pub use violation::{
66 DEFERRED_MODE_CANCEL, DEFERRED_MODE_FINAL, DEFERRED_MODE_OBSERVED, ViolationType,
67 set_violation_writer, violation_report, violation_report_for,
68};
69pub use wheel::*;
70
71pub type ProcessId = u32;
72pub type ActivationId = u64;
73
74/// Ticks at the compilation's minimum time precision (SC-064).
75pub type SimTick = u64;
76
77/// The scheduler-internal logical-time value (scheduler.md §2; ledger SC-023,
78/// SC-033, SC-129). **Deliberately a different type from the ABI-facing
79/// `vj_abi::SimTime{sim_units, delta}`** (docs/CONTRACTS.md r3 item 1):
80/// `delta` is not a field here and never will be — it is purely a local
81/// iteration count of the inner Active/Inactive/NBA convergence loop
82/// (`reconv_iter` on [`PendingActivation`]/`CanonicalEventKey`), discarded
83/// once that loop converges (SC-023). The only way to get from this type to
84/// the ABI's is [`SchedTime::to_abi_display`], the one documented conversion
85/// point (SC-129) — it never runs the other way, and nothing derives a
86/// `delta` from `ticks` internally.
87#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
88pub struct SchedTime {
89 pub ticks: SimTick,
90}
91
92const _: () = assert!(core::mem::size_of::<SchedTime>() == 8);
93
94impl SchedTime {
95 /// The one documented `SchedTime` -> `vj_abi::SimTime` conversion
96 /// point (SC-129), used only at the ABI/display boundary. `delta` is
97 /// always the caller's own delta-loop iteration counter — never read from
98 /// or derived from `self.ticks` — because the internal type never carries
99 /// one (SC-023).
100 pub fn to_abi_display(self, delta: u32) -> vj_abi::SimTime {
101 vj_abi::SimTime {
102 sim_units: self.ticks as i64,
103 delta,
104 }
105 }
106}
107
108/// The reconciled 5-region ABI discriminant, exhaustively matched (SC-016,
109/// SC-117): pins `region_ordinal` to `Region`'s own `#[repr(u32)]`
110/// discriminant rather than a second, hand-maintained numbering that could
111/// drift from it. No wildcard arm — adding a 6th `Region` variant fails this
112/// match to compile.
113pub fn region_ordinal(region: Region) -> u32 {
114 match region {
115 Region::Active => Region::Active as u32,
116 Region::Inactive => Region::Inactive as u32,
117 Region::Nba => Region::Nba as u32,
118 Region::Reactive => Region::Reactive as u32,
119 Region::Postponed => Region::Postponed as u32,
120 // sva T1 (§1.4, review fix, blocking finding 2): mechanical, like
121 // every other arm above.
122 Region::Preponed => Region::Preponed as u32,
123 // sva T2 (§1.4, review fix, blocking finding 2), Observed's half:
124 // equally mechanical.
125 Region::Observed => Region::Observed as u32,
126 }
127}
128
129/// What woke a `CompiledProcess::resume()` call (scheduler.md §5).
130#[derive(Clone, Copy, Debug)]
131pub enum ResumeArg {
132 Start,
133 TimeElapsed,
134 EventFired {
135 term_index: usize,
136 },
137 ForkChildDone {
138 child: ProcessId,
139 },
140 Disabled,
141 /// **NEW (Task 9, B-sched wave, ledger SC-125).** `Suspend::WaitFork`'s
142 /// resolution: every `ProcessId` live in the resuming process's own
143 /// `fork_children` at the moment it suspended has now reached
144 /// `Suspend::Done` and been reclaimed.
145 AllForkChildrenDone,
146 /// **NEW (Task 9, SC-125).** `Suspend::WaitOrder(order)`'s resolution:
147 /// every `ProcessId` in `order` completed, in exactly that declared
148 /// order.
149 WaitOrderSatisfied,
150}
151
152/// `BlockStatement`'s three join semantics (`StatementBlockKind` mirror; ledger
153/// PR-086 and the fork/join family).
154#[derive(Clone, Copy, PartialEq, Eq, Debug)]
155pub enum JoinKind {
156 All,
157 Any,
158 None,
159}
160
161/// One `@(...)`/`wait(...)` disjunct (scheduler.md §4/§5; ledger SC-048..SC-051,
162/// SC-065).
163#[derive(Clone, Debug)]
164pub enum EventTerm {
165 Edge {
166 cell: StateCellId,
167 edge: EdgeKind,
168 iff: Option<PredicateFn>,
169 },
170 ImplicitAny(Vec<StateCellId>),
171 /// A wait on a bit/part select of one cell (`@(v[3])`, `@(posedge
172 /// v[5:4])`, LRM 9.4.2): it fires on an update that changed bits `[lsb,
173 /// lsb + width)` of `cell` (any change when `edge` is `None`, else the
174 /// transition of bit `lsb` classified as `edge`). The update is judged on
175 /// its own old/new words (`ChangeDelta`), so a bit that is set and cleared
176 /// again by two updates in one time step wakes the waiter once per
177 /// update, exactly as the LRM's update events do (4.9.4).
178 Select {
179 cell: StateCellId,
180 edge: EdgeKind,
181 lsb: u32,
182 width: u32,
183 },
184 /// **Also the resolution for `BlockEventListControl` (`@(block.
185 /// triggered)`), SC-054's own "kept as a hook-only `EventTerm` mapping
186 /// in v1" acceptance, resolved here rather than by adding a 5th
187 /// variant:** `@(block.triggered)` needs no new `EventTerm` shape at
188 /// all -- codegen reserves one synthetic `StateCellId` per named
189 /// statement block and fires it (through the same `TimerAction::
190 /// TriggerEvent`/direct `notify_change` fan-out an ordinary `->`/`->>`
191 /// trigger already uses, `sched::wheel`'s own SC-069 doc comment)
192 /// exactly when that block completes -- i.e. `.triggered` is modeled as
193 /// an ordinary named event whose trigger site is block-exit instead of
194 /// an explicit `->` statement. `NamedEventTrigger`'s own
195 /// scheduler-side consumption (installing an any-change dynamic entry
196 /// keyed by the cell, waking every waiter on a real fire) is already
197 /// real and tested (Task 5, `sched::sensitivity::tests::
198 /// named_event_trigger_term_installs_an_any_change_dynamic_entry`) --
199 /// what remains for `@(block.triggered)`
200 /// specifically is purely whether codegen (B-process, out of this
201 /// track's ownership) actually emits the block-exit trigger call; this
202 /// scheduler-side half never hits a silent mis-simulation regardless
203 /// (SC-054's own negative acceptance clause), since an unfired trigger
204 /// cell simply never wakes anyone -- observably a hang/timeout, never a
205 /// wrong value.
206 NamedEventTrigger(StateCellId),
207 /// `repeat(count)@(...)` (mirrors slang's `RepeatedEventControl{expr,
208 /// event}`; ledger SC-052). `SensitivityTable::install_term` installs
209 /// only `inner`'s own edge at *this* term's index — the count itself is
210 /// resume-orchestration state, tracked live by
211 /// [`RepeatCounters`](crate::sched::RepeatCounters) (`sched/repeat.rs`),
212 /// not by the sensitivity table.
213 ///
214 /// **`##N`/clocking-block cycle delays reduce to this term, with no
215 /// second mechanism (SC-004, SC-005, SC-053):** per ARCHITECTURE §6.5,
216 /// `CycleDelayControl` is resolved to absolute ticks *at runtime*, not
217 /// pre-resolved by codegen/elaboration — `##N` relative to a clocking
218 /// block's bound clock is semantically `Repeat{count: N, inner:
219 /// Edge{cell: <bound clock's cell>, edge: Posedge, iff: None}}`. Because
220 /// `RepeatCounters` counts live firings of `inner`'s real signal rather
221 /// than precomputing an absolute tick offset, "the same compiled `##N`
222 /// delay produces different absolute tick timing when its associated
223 /// clock's period changes dynamically at runtime" (SC-053's own
224 /// differentiating acceptance clause) holds by construction: there is no
225 /// cached tick offset to go stale. See `sched::repeat`'s own module doc
226 /// comment and `sched::control`'s
227 /// `sc053_repeat_based_wait_re_reads_the_clocks_live_period_not_a_cached_tick_offset`
228 /// for the proof against a deliberately irregular (non-periodic) clock.
229 Repeat {
230 count: u64,
231 inner: Box<EventTerm>,
232 },
233}
234
235/// `WaitKind::Any` is SV's only real semantics; kept as an explicit list for
236/// `##N`/`repeat` (scheduler.md).
237#[derive(Clone, Debug, Default)]
238pub struct EventWaitSet {
239 pub terms: Vec<EventTerm>,
240 /// `true` for `wait(expr)`: the resumed process re-checks its condition
241 /// before running on, so a wake delivered while the process was
242 /// suspended (`process::suspend`, LRM 9.7.4) can be parked and replayed
243 /// by `resume()` harmlessly. `false` for an `@(...)` event control,
244 /// where the LRM desensitises a suspended waiter: an event that fires
245 /// during the suspension is missed, not replayed.
246 pub rechecks_on_wake: bool,
247}
248
249/// What a `CompiledProcess::resume()` call returns to the in-process scheduler
250/// orchestration loop. See [`crate::process::WaitSpec`] for the lower-level,
251/// codegen/ABI-facing sibling of this type and the reconciliation note there.
252///
253/// **Fork/join (RESOLVED, docs/CONTRACTS.md "Resolved design decisions" D1).**
254/// A `fork` hands the scheduler *spawn descriptors*, not process identities and
255/// not process objects. The three design docs disagreed three ways
256/// (scheduler.md: `Vec<Box<dyn CompiledProcess>>`; the r2 placeholder:
257/// `Vec<ProcessId>`; process.md §7: an undefined `ProcessHandle`):
258/// - `Vec<ProcessId>` is wrong: the children do not exist yet when the parent
259/// suspends, so nothing could have assigned those ids.
260/// - `Box<dyn CompiledProcess>` contradicts ARCHITECTURE §5's frozen ABI (a
261/// process is an `extern "C"` `ProcessFn` entry in `VJ_PROCESS_TABLE`,
262/// not a Rust trait object) and cannot satisfy `Suspend: Clone`.
263///
264/// Fork branches are statically known code: codegen emits each branch as its
265/// own process-table kernel. So the parent returns one [`SpawnSpec`] per
266/// branch; the scheduler registers each, assigns the child its [`ProcessId`],
267/// records the ids in the parent's `ForkFrame::children`, sets
268/// `ForkFrame::remaining` per [`JoinKind`] (All = n, Any = 1, None = 0 — the
269/// parent resumes immediately), and later reports each completion back via
270/// `ResumeArg::ForkChildDone { child }`. Children reach variables declared in
271/// the parent's enclosing scopes through [`SpawnSpec::parent`] (LRM 9.3.2).
272/// Rolling back a parent that has already spawned live children must also
273/// roll back / kill those children — owned by the determinism rows, not this
274/// type.
275#[derive(Clone, Debug)]
276pub enum Suspend {
277 Delay(u64),
278 WaitEvents(EventWaitSet),
279 ForkJoin(JoinKind, Vec<SpawnSpec>),
280 /// **NEW (Task 9, B-sched wave, ledger SC-125) — scheduler-side half of
281 /// `wait fork;`.** Blocks the calling process until every `ProcessId`
282 /// currently live in its own `ProcessDescriptor::fork_children`
283 /// (populated by an earlier `fork ... join_none`) has reached
284 /// `Suspend::Done` and been reclaimed; resumes via
285 /// `ResumeArg::AllForkChildrenDone`. See `sched::control::Engine::
286 /// apply_wait_fork` and `sched::resume::ResumeDriver::on_child_done`'s
287 /// own doc comments for the mechanism. `disable fork;` is deliberately
288 /// **not** a `Suspend` variant here -- it never suspends the caller, so
289 /// it is instead a synchronous `SchedulerOps::disable_fork` call (see
290 /// that method's own doc comment).
291 WaitFork,
292 /// **NEW (Task 9, SC-125) — scheduler-side half of `wait_order(a, b,
293 /// c);`.** Blocks the calling process until every `ProcessId` in
294 /// `order` has completed, in exactly that declared order; resumes via
295 /// `ResumeArg::WaitOrderSatisfied` on success. An out-of-order
296 /// completion never silently satisfies the wait or panics -- it is
297 /// surfaced as `sched::resume::ResumeError::WaitOrderViolated` (see
298 /// that type's own doc comment).
299 WaitOrder(Vec<ProcessId>),
300 /// Task 8 (AB-085): a barrier-wait suspend -- the process yields until
301 /// every parallel lane has reached the same GVT boundary. Deliberately
302 /// carries no payload and needs no new coroutine/stack mechanism: it
303 /// reuses the exact same suspend/resume dispatch every other `Suspend`
304 /// variant goes through (`apply_suspend` in `sched::control` handles it
305 /// by delegating to the `Delay(0)` arm's existing code path -- see that
306 /// function's own doc comment). The real GVT-barrier semantics (actually
307 /// gating resumption on every parallel lane's progress, not just
308 /// resolving at the next delta step) belong to the determinism/parallel-
309 /// engine track and are not implemented by this wave; this variant only
310 /// wires the ABI-level suspend reason into the existing dispatch so a
311 /// compiled process can report it.
312 WaitForGvt,
313 /// Classes plan Task 13 (LRM 9.7): the process yields because it just
314 /// queued a [`ProcessRequest`] through [`SchedulerOps::process_request`]
315 /// (`p.await()` / `p.suspend()` called as a statement). Unlike every
316 /// other variant it carries no wake condition of its own: the engine
317 /// resumes the process immediately unless the request left it
318 /// self-suspended or awaiting another process (see
319 /// `sched::control::Engine::apply_by_request`).
320 ByRequest,
321 Done,
322}
323
324/// `process::state` (LRM 9.7), in the enum's declaration order -- the
325/// discriminant IS the value `p.status()` returns.
326#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
327pub enum ProcessState {
328 Finished = 0,
329 Running = 1,
330 Waiting = 2,
331 Suspended = 3,
332 Killed = 4,
333}
334
335/// One `process` class method call that mutates another process (or this
336/// one), queued by [`SchedulerOps::process_request`] and applied by the
337/// engine at the next suspend point -- the same drain-at-`apply_suspend`
338/// shape `disable fork` uses, because `SchedulerOps` methods take `&self`.
339#[derive(Clone, Copy, PartialEq, Eq, Debug)]
340pub enum ProcessRequest {
341 Kill(ProcessId),
342 Suspend(ProcessId),
343 Resume(ProcessId),
344 Await(ProcessId),
345 /// Classes plan Task 17 (LRM 15.3): the requester called a blocking
346 /// synchronisation-object method (`semaphore.get`) that must wait; it
347 /// stays yielded after its `Suspend::ByRequest` until a
348 /// [`ProcessRequest::SyncWake`] names it.
349 SyncBlock,
350 /// The named process's blocked `get` has been granted its keys: it runs
351 /// again. Queued by the `put` (or kill) that released it.
352 SyncWake(ProcessId),
353}
354
355/// One fork branch the scheduler must instantiate (see [`Suspend::ForkJoin`]).
356///
357/// Not `Copy` (unlike every other scheduler ABI value): [`Self::disable_name`]
358/// is a real, possibly-nonempty `String`, not FFI/ABI data -- this struct
359/// stays a plain, compile-time-constant Rust value (per its own module's
360/// `decode_reschedule_with_fork_table`/`fork_table` doc comments), never
361/// crosses an `extern "C"` boundary, so losing `Copy` costs nothing a real
362/// caller observes; every construction site this adds a field to is a
363/// struct literal, so a missed one is a compile error, never a silent gap.
364#[derive(Clone, PartialEq, Eq, Debug)]
365pub struct SpawnSpec {
366 /// Index into the compiled kernel's `VJ_PROCESS_TABLE` of the kernel
367 /// codegen emitted for this fork branch.
368 pub kernel: u32,
369 /// Resume point (segment / `resume_pc`) at which the child starts.
370 pub entry_pc: u32,
371 /// The spawning process; the child's activation record links to it for
372 /// access to variables declared in enclosing scopes (LRM 9.3.2).
373 pub parent: ProcessId,
374 /// The name(s) a `disable` elsewhere may target this branch by (LRM
375 /// 9.6.2): the enclosing `fork : NAME` label, if any, and/or this
376 /// branch's own named outermost block, if it has one -- both resolved at
377 /// compile time by `vj_sim::process::cfg`'s fork-branch walk. Empty
378 /// for an unnamed branch of an unnamed fork, which no `disable` can ever
379 /// single out. `Engine::apply_suspend`'s `Suspend::ForkJoin` handling
380 /// tags each newly minted child's `ProcessDescriptor::disable_names`
381 /// with this list the moment `handle_fork` hands back its `ProcessId`.
382 pub disable_name: Vec<String>,
383}
384
385/// One resumable process (scheduler.md §5): the scheduler owns zero per-process
386/// program-counter state — it lives in STATE-arena-allocated, A2-process-owned
387/// `ActivationRecord` storage.
388///
389/// **SC-127 scheduler-side resolution (Task 8, B-sched wave):** the
390/// scheduler places no restriction on *where*, syntactically or
391/// call-stack-wise, a [`Suspend`] value is constructed inside `resume()`.
392/// `resume()` is a plain function returning a plain value — the dispatch
393/// loop (`sched::control::Engine::apply_suspend`) matches only on the
394/// *returned* `Suspend` variant, never on how deep in the call stack it was
395/// built or what code ran to build it. A DPI-imported-task call site that
396/// itself calls back into blocking SV (LRM 35.5.6) is therefore
397/// representable today, with zero scheduler-side changes, exactly like this
398/// module's own `sc127_a_mock_process_may_suspend_from_a_point_that_
399/// stands_in_for_a_dpi_imported_task_callback` test constructs: a `Suspend`
400/// returned from several stack frames deep inside ordinary Rust calls
401/// dispatches and completes identically to one returned directly from
402/// `resume`'s own top-level match arm. **What this does *not* resolve** —
403/// whether `vj-sim::process`'s CFG builder / `analysis::dpi` actually
404/// choose to treat a given DPI-imported-task call site as a suspend point at
405/// all (i.e. which call sites qualify, and whether codegen ever emits one),
406/// per SC-127's own acceptance clause (a) vs. (b) — is that track's decision,
407/// not `sched`'s, and is cross-referenced here rather than silently assumed
408/// resolved.
409pub trait CompiledProcess: Send {
410 fn resume(&mut self, state: SimHandle, arg: ResumeArg, ops: &dyn SchedulerOps) -> Suspend;
411
412 /// Builds the real child process for one branch of a `fork` this process
413 /// just suspended on (`Suspend::ForkJoin`, PR-085). `None` means this
414 /// process has no compiled branch kernels to offer (every hand-written
415 /// test process); `Engine` then falls back to its inert placeholder
416 /// child. A compiled kernel process overrides this so `vj run`
417 /// actually executes each branch body.
418 fn mint_fork_child(&self, _spec: &SpawnSpec) -> Option<Box<dyn CompiledProcess>> {
419 None
420 }
421
422 /// The instance whose block and task names a `disable <label>;` run by
423 /// this process (or a fork branch of it) ends the activities of (LRM
424 /// 9.6.2); `None` for a process that has none (hand-written test
425 /// processes, fork branches).
426 fn disable_instance(&self) -> Option<u32> {
427 None
428 }
429
430 /// Where this process, suspended, resumes if its activity inside the
431 /// named block or task activation `label` is disabled: the block's (or the
432 /// call's) end. `None` when it is not inside `label`, or has finished.
433 fn disable_exit(&self, _label: &str) -> Option<u32> {
434 None
435 }
436
437 /// Whether the process has a disable scope `label` at all, whether or not
438 /// it is inside it now (or has finished): a `disable` of that block ends
439 /// the fork branches it left running (LRM 9.6.2).
440 fn owns_disable_scope(&self, _label: &str) -> bool {
441 false
442 }
443
444 /// Makes the process's next resume continue at `pc`
445 /// ([`Self::disable_exit`]). Returns whether it was blocked joining the
446 /// processes it forked (`join`, `join_any`, `wait fork`, `wait_order`).
447 fn redirect_resume(&mut self, _pc: u32) -> bool {
448 false
449 }
450}
451
452/// A deferred, GVT-gated I/O callback (see [`SchedulerOps::defer_io`]).
453pub type DeferredIoCallback = Box<dyn FnOnce(&mut dyn IoCtx) + Send>;
454
455/// The scheduler-provided operations a running kernel/process calls back into
456/// (scheduler.md's `SchedulerOps`; ledger SC-036, SC-042, SC-044, SC-070, SC-075,
457/// SC-116).
458pub trait SchedulerOps {
459 fn now(&self) -> SchedTime;
460 fn current_region(&self) -> Region;
461 /// Any concrete implementation MUST call [`NbaLog::stage`] and MUST NOT
462 /// provide any other write path for a nonblocking (`<=`) assignment —
463 /// this is the textual pin for SC-036 (no direct-apply path for NBA
464 /// writes anywhere in the scheduler), made enforceable by Task 8's
465 /// mock `SchedulerOps` test.
466 fn stage_nba(&self, cell: StateCellId, value: FourStateValue, source: ProcessId);
467 fn commit_blocking_write(&self, cell: StateCellId, value: FourStateValue);
468 /// The future simulation ticks with something scheduled (timed process
469 /// wake-ups, delayed nonblocking writes), ascending: the time queue a
470 /// VPI application iterates (IEEE 1800-2023 37.81). Empty where the
471 /// implementation has no time wheel.
472 fn pending_ticks(&self) -> Vec<u64> {
473 Vec::new()
474 }
475 /// The processes waiting on `cell` now (a suspended `@`/`wait` or a
476 /// static sensitivity), for VPI's `vpiWaitingProcesses` (IEEE 1800-2023
477 /// 37.27). Empty where the implementation tracks no sensitivity.
478 fn waiting_processes(&self, _cell: StateCellId) -> Vec<ProcessId> {
479 Vec::new()
480 }
481 fn request_finish(&self, code: i32);
482 fn defer_io(&self, at: SchedTime, region: Region, cb: DeferredIoCallback);
483 /// **NEW (Task 4, ledger SC-090/SC-123).** Registers `cb` to run exactly
484 /// once, at the first point `region` is drained with `now >= at` (see
485 /// `region_callback`'s own module doc for the `1step` placement this
486 /// pins and `sched::control::Engine::drain_region_callbacks`'s doc
487 /// comment for exactly when that drain happens relative to
488 /// `dispatch_ready_set`). The one real production implementation is
489 /// `EngineOps` (`sched/control.rs`); every other `SchedulerOps` test
490 /// double in this crate implements this as a no-op, matching this
491 /// trait's own established pattern for methods a given test double's
492 /// own tests don't exercise.
493 fn register_region_callback(&self, region: Region, at: SchedTime, cb: Box<dyn RegionCallback>);
494
495 /// **NEW (Task 9, B-sched wave, ledger SC-125).** `disable fork;`'s
496 /// scheduler-side half: recursively terminates every live descendant
497 /// `proc` has spawned via `fork` (its own `fork_children`, and theirs,
498 /// ...) WITHOUT suspending `proc` itself -- called mid-`resume()`,
499 /// synchronously, the same call shape as the already-real
500 /// [`Self::commit_blocking_write`].
501 ///
502 /// **Deliberately given a default (no-op) body, unlike
503 /// [`Self::register_region_callback`] (Task 4), which this trait's own
504 /// established pattern otherwise favors (every implementor updated
505 /// exhaustively):** several of `SchedulerOps`'s other implementors live
506 /// in sibling tracks' own concurrently-landing files this wave
507 /// (`vj-codegen/tests/*.rs`, `vj-runtime::runtime_ctx`) that
508 /// this task's own file list does not name and must not touch (this
509 /// wave's binding rule 7 -- "keep edits outside sched/ and the files
510 /// your plan names minimal"). The one real production override is
511 /// `EngineOps` (`sched/control.rs`); every test double in this crate
512 /// that does not exercise `disable fork` inherits this default,
513 /// unchanged.
514 fn disable_fork(&self, _proc: ProcessId) {}
515
516 /// Starts `proc` as a process of its own in the Reactive region, with no
517 /// parent: the thread LRM 1800-2023 16.11 schedules for a task a sequence
518 /// match item calls ("scheduled in the Reactive region"). Deferred like
519 /// [`Self::disable_fork`] (the table mutation needs `&mut Engine`); the
520 /// Reactive set drains until the process, and what it wakes, has run.
521 /// Default no-op: a test double that never spawns it inherits it.
522 fn spawn_detached(&self, _proc: Box<dyn CompiledProcess>) {}
523
524 /// `disable f;` executed by `proc`, a branch of the fork block `f`
525 /// (LRM 1800-2023 §9.6.2): every other branch of that fork ends, with
526 /// all descendants (`proc`'s own too), and the parent's join is
527 /// satisfied once `proc` itself finishes -- which it does right after.
528 /// Default no-op for the same reason as [`Self::disable_fork`].
529 fn disable_fork_block(&self, _proc: ProcessId) {}
530
531 /// `disable <name>;` where `<name>` is not an enclosing named block of
532 /// `requester`'s own statement tree, not its own fork
533 /// ([`Self::disable_fork_block`] already covers that case), and not a
534 /// procedural-assertion label: a fork-spawned branch's label, reachable
535 /// from `requester` because `requester` is the branch's parent, a
536 /// sibling branch, or a descendant of an ancestor fork (LRM 9.6.2:
537 /// disabling a named block terminates all its activity wherever it
538 /// executes, including a nested fork and tasks called from it).
539 /// Queues the kill of every live, currently-tagged ([`super::SpawnSpec::
540 /// disable_name`]) match reachable from `requester`'s own top-level fork
541 /// ancestor -- deferred exactly like [`Self::disable_fork`] (the actual
542 /// table mutation cannot happen synchronously inside this `&self` call).
543 /// Returns `true` iff `requester` itself is one of the matches, or
544 /// descends from one (the compiled caller must leave its own kernel at
545 /// once, mirroring [`Self::process_request`]'s identical `Kill` case) --
546 /// `requester` is never actually killed through this call (its own CFG,
547 /// via the abort flag, is what ends it; only OTHER matches/descendants
548 /// are force-killed). Default false/no-op for the same reason as
549 /// [`Self::disable_fork`].
550 fn disable_named(&self, _requester: ProcessId, _label: &str) -> bool {
551 false
552 }
553
554 /// Classes plan Task 13 (LRM 9.7): `requester` called a `process` class
555 /// method that mutates `req`'s target. The request is queued and applied
556 /// by the engine at the requester's next suspend (or, for `Kill`, ends the
557 /// target synchronously enough that [`Self::process_state`] already
558 /// reports it). Returns `true` iff the request terminates `requester`
559 /// itself (it is, or descends from, the kill target) -- the caller then
560 /// leaves its kernel at once. Default no-op for scheduler test doubles.
561 fn process_request(&self, _requester: ProcessId, _req: ProcessRequest) -> bool {
562 false
563 }
564 /// `p.status()` for the process `target` (LRM 9.7). A test double with no
565 /// process table reports every process `Running`.
566 fn process_state(&self, _target: ProcessId) -> ProcessState {
567 ProcessState::Running
568 }
569 /// Marks `proc` as one a `process` handle exists for, so its exit is
570 /// recorded (`FINISHED` vs `KILLED`) after the scheduler reclaims it.
571 /// Default no-op for the same reason as [`Self::disable_fork`].
572 fn process_track(&self, _proc: ProcessId) {}
573
574 /// Resumes `proc`, a process parked with no sensitivity (an empty event
575 /// list), in the current time step with `ResumeArg::EventFired { term }`.
576 /// The `expect` statement's wake (LRM 1800-2023 §16.17): the assertion
577 /// engine, running in the Observed region, reports the outcome and the
578 /// blocked process continues in this same step. Default no-op for the
579 /// same reason as [`Self::disable_fork`].
580 fn wake_process(&self, _proc: ProcessId, _term: usize) {}
581 /// `$monitor` (LRM 21.2.3): the cells of the active monitor's argument
582 /// signals. Any value change to one of them -- even a transient one
583 /// that ends where it started within the timestep -- marks the monitor
584 /// changed; `SchedulerCtx::take_monitor_changed` reads-and-clears that
585 /// at the end of the timestep. Replaces any previous watch set and marks
586 /// changed (a new `$monitor` always prints once). Default no-op, like
587 /// `disable_fork`, for scheduler test doubles.
588 fn set_monitor_watch(&self, _cells: Vec<StateCellId>) {}
589 /// A blocking write to one ROW of a memory (whole-array `cell`), already
590 /// committed to STATE by the caller, changed the stored value: wake
591 /// processes sensitive to the memory (`ops::notify_blocking_change`,
592 /// any-change edge). Default no-op, like `disable_fork`, for scheduler
593 /// test doubles.
594 fn notify_blocking_row_change(&self, _cell: StateCellId) {}
595 /// A windowed blocking commit to a huge signal (`state::huge_commit`),
596 /// already stored in STATE by the caller, changed its value: wake the
597 /// processes sensitive to `cell` with `edge` (the lsb edge). Default
598 /// no-op, like `disable_fork`, for scheduler test doubles. `delta` is the
599 /// changed plane words, present only when [`Self::wants_delta`] said a
600 /// select waiter needs them.
601 fn notify_blocking_wide_change(
602 &self,
603 _cell: StateCellId,
604 _edge: EdgeKind,
605 _delta: Option<crate::state::ChangeDelta>,
606 ) {
607 }
608 /// Whether a process waits on a bit/part select of `cell`, so a windowed
609 /// commit must report its changed words (see
610 /// [`crate::state::ValueChangeSink::wants_delta`]). Default `false`.
611 fn wants_delta(&self, _cell: StateCellId) -> bool {
612 false
613 }
614 /// Stages a partial / memory-row nonblocking write
615 /// (`nba::MaskedNbaUpdate`). The one real implementation is `EngineOps`;
616 /// a test double that never compiles such a write never reaches this.
617 fn stage_nba_masked(
618 &self,
619 cell: StateCellId,
620 row: Option<u64>,
621 value: FourStateValue,
622 mask: Vec<u64>,
623 source: ProcessId,
624 ) {
625 let _ = (row, value, mask, source);
626 unreachable!("stage_nba_masked({cell:?}) on a SchedulerOps without an NBA log")
627 }
628 /// [`Self::stage_nba_masked`] for an intra-assignment-delayed
629 /// nonblocking write (`q <= #d v`, LRM 1800-2023 §10.4.2): staged into
630 /// the NBA log when the time step `delay` ticks from now begins, so it
631 /// commits in that step's NBA region. A zero delay stages now.
632 fn stage_nba_masked_delayed(
633 &self,
634 cell: StateCellId,
635 row: Option<u64>,
636 value: FourStateValue,
637 mask: Vec<u64>,
638 source: ProcessId,
639 delay: u64,
640 ) {
641 let _ = (row, value, mask, source, delay);
642 unreachable!("stage_nba_masked_delayed({cell:?}) on a SchedulerOps without an NBA log")
643 }
644 /// Stages a nonblocking write of one element of the collection whose
645 /// handle `cell` (row `row`) holds (`nba::MaskedNbaUpdate::coll`),
646 /// `delay` ticks from now (zero stages now).
647 fn stage_nba_coll(
648 &self,
649 cell: StateCellId,
650 row: Option<u64>,
651 elem: Vec<u64>,
652 coll: nba::CollWrite,
653 source: ProcessId,
654 delay: u64,
655 ) {
656 let _ = (row, elem, coll, source, delay);
657 unreachable!("stage_nba_coll({cell:?}) on a SchedulerOps without an NBA log")
658 }
659 /// Stages a nonblocking write of class-property bits
660 /// (`nba::MaskedNbaUpdate::obj`), `delay` ticks from now (zero stages now).
661 fn stage_nba_obj(&self, w: nba::ObjWrite, source: ProcessId, delay: u64) {
662 let _ = (w, source, delay);
663 unreachable!("stage_nba_obj on a SchedulerOps without an NBA log")
664 }
665 /// A clocking-block synchronous drive is about to stage writes (LRM
666 /// 14.16: they update in the Re-NBA region): turns the Re-NBA region on
667 /// for a design with no Reactive process. Default: nothing to turn on.
668 fn note_reactive_drive(&self) {}
669 /// A clocking block's event was just triggered (its event counter
670 /// `counter` bumped): a synchronous drive executing later in this time
671 /// step is coincident with it (LRM 14.16). Default: nothing to record.
672 fn note_clocking_event(&self, counter: StateCellId) {
673 let _ = counter;
674 }
675 /// Stages a nonblocking event trigger (`->> e` / `->> #d e`, LRM
676 /// 1800-2023 15.5.2): at the NBA region of the step `delay` ticks from now
677 /// (a zero delay: this step) the event cell (row `row` of an event array)
678 /// is triggered.
679 fn stage_nba_pulse(&self, cell: StateCellId, row: Option<u64>, source: ProcessId, delay: u64) {
680 let _ = (row, source, delay);
681 unreachable!("stage_nba_pulse({cell:?}) on a SchedulerOps without an NBA log")
682 }
683 /// Stages a windowed partial write to a huge vector (`mask`/`value` span
684 /// only the plane words from `window` on; see
685 /// [`nba::MaskedNbaUpdate::window`]), delayed by `delay` ticks (zero
686 /// stages now).
687 fn stage_nba_window(
688 &self,
689 cell: StateCellId,
690 window: u32,
691 value: FourStateValue,
692 mask: Vec<u64>,
693 source: ProcessId,
694 delay: u64,
695 ) {
696 let _ = (window, value, mask, source, delay);
697 unreachable!("stage_nba_window({cell:?}) on a SchedulerOps without an NBA log")
698 }
699}
700
701/// Reserved hook for RUNTIME's `defer_io` callback context (scheduler.md names
702/// `IoCtx` as RUNTIME's concern; the concrete fields are RUNTIME's to define).
703pub trait IoCtx {}
704
705/// Codegen-supplied read-set metadata for `always_comb`/`always_latch`/
706/// `always_ff` (scheduler.md `StaticSensitivity`; ledger CG-079, SC-067,
707/// NL-036..NL-038, NL-045).
708#[derive(Clone, Debug, Default)]
709pub struct StaticSensitivity {
710 pub edges: Vec<(StateCellId, Option<EdgeKind>)>,
711}
712
713/// The canonical, thread-count-independent ordering key entry (scheduler.md §6;
714/// ledger SC-014, SC-029, DT-005, DT-042).
715///
716/// Fixed vs. scheduler.md's literal `logical_order: (SimTime, Region, u64)`
717/// 3-tuple (review fix, DT-042): DT-042 itself records that scheduler.md's
718/// design "describes discarding [the delta/reconvergence-iteration count]
719/// once the delta loop converges, which conflicts with ARCHITECTURE §4's
720/// 5-tuple key requiring it everywhere" — and per this workspace's spec
721/// precedence (ARCHITECTURE.md §4 wins over docs/design/*.md), `reconv_iter`
722/// must be threaded through every `PendingActivation`. Without it, two
723/// activations of the same process in two different delta-loop reconvergence
724/// passes at the same `(SchedTime, Region)` compared equal under the old
725/// 3-tuple, collapsing a real ordering distinction DT-042/SC-013 require.
726/// (The tuple's first field is now `SchedTime`, not `vj_abi::SimTime` —
727/// Task 1, SC-023/SC-033/SC-129: see `SchedTime`'s own doc comment.)
728/// `id` is new for the same review pass: `determinism::DeterminismController
729/// ::on_read`/`on_write` and `ResolvedBatch::completed` all key on
730/// `ActivationId`, but nothing previously let a `PendingActivation`/`WorkItem`
731/// report which `ActivationId` it is — `id` is that missing correlation.
732#[derive(Clone, Copy, Debug)]
733pub struct PendingActivation {
734 pub id: ActivationId,
735 pub proc: ProcessId,
736 pub arg: ResumeArg,
737 /// The delta/reconvergence-loop iteration count within `(SchedTime, Region)`
738 /// — the canonical 5-tuple's third field (ARCHITECTURE §4 item 3; DT-042).
739 pub reconv_iter: u32,
740 pub logical_order: (SchedTime, Region, u64),
741}
742
743#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
744pub struct BreakpointId(pub u32);
745
746#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
747pub struct CheckpointHandle(pub u64);
748
749/// (debugger.md `BreakCondition`; ledger CG-153, SC-096..SC-100.)
750#[derive(Clone, Debug)]
751pub enum BreakCondition {
752 Time(SimTime),
753 StmtHit {
754 stmt: StmtId,
755 cond: Option<PredicateFn>,
756 },
757 Watch {
758 cell: StateCellId,
759 edge: EdgeKind,
760 cond: Option<PredicateFn>,
761 },
762 ProcessSuspend(ProcessId),
763}
764
765#[derive(Clone, Copy, PartialEq, Eq, Debug)]
766pub enum SuspendReason {
767 WaitTime,
768 WaitEvent,
769 WaitFork,
770 Blocked,
771}
772
773#[derive(Clone, Copy, PartialEq, Eq, Debug)]
774pub enum ProcessRunState {
775 Running,
776 Suspended(SuspendReason),
777}
778
779#[derive(Clone, Copy, Debug)]
780pub struct ProcessSnapshot {
781 pub id: ProcessId,
782 pub state: ProcessRunState,
783 pub current: StmtId,
784}
785
786#[derive(Clone, Debug)]
787pub struct KernelSnapshot {
788 pub gvt: SimTime,
789 pub processes: Vec<ProcessSnapshot>,
790}
791
792#[derive(Clone, Copy, Debug)]
793pub struct SpeculationBound {
794 /// `0` == fully conservative; the checked-in v1 default per ARCHITECTURE §4
795 /// item 1 (ledger DB-033).
796 pub max_lookahead_cycles: u32,
797}
798
799#[derive(Clone, Copy, PartialEq, Eq, Debug)]
800pub enum RewindError {
801 PastFossilHorizon,
802 LayoutMismatch,
803}
804
805/// Implemented BY the engine (sched+determinism), called by `vj-debug`
806/// (debugger.md: "Breakpoints/watchpoints are kernel-evaluated... Arming any
807/// breakpoint bounds speculation to conservative for that session"; ledger
808/// SC-094, SC-095, SC-097, SC-100, DB-002, DB-014, CheckpointHandle: SC-105,
809/// DB-029, DB-030, DB-033, DB-046, DB-048).
810pub trait KernelControl: Send {
811 fn arm(&mut self, cond: BreakCondition) -> BreakpointId;
812 fn disarm(&mut self, id: BreakpointId);
813 fn snapshot(&self) -> KernelSnapshot;
814 fn gvt(&self) -> SimTime;
815 fn checkpoint(&mut self) -> CheckpointHandle;
816 fn rewind(&mut self, cp: CheckpointHandle) -> Result<(), RewindError>;
817 fn set_speculation_bound(&mut self, bound: SpeculationBound);
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823
824 #[test]
825 fn pending_activation_logical_order_is_a_plain_copy_tuple() {
826 let a = PendingActivation {
827 id: 42,
828 proc: 3,
829 arg: ResumeArg::Start,
830 reconv_iter: 0,
831 logical_order: (SchedTime { ticks: 10 }, Region::Active, 7),
832 };
833 let b = a;
834 assert_eq!(a.proc, b.proc);
835 assert_eq!(a.id, b.id);
836 assert_eq!(a.logical_order.2, 7);
837 }
838
839 /// Review fix: two activations of the same process at the same
840 /// `(SimTime, Region)` but different delta-loop reconvergence passes must
841 /// be distinguishable — this is the whole point of DT-042's `reconv_iter`.
842 #[test]
843 fn reconv_iter_distinguishes_same_process_same_tick_different_passes() {
844 let base = SchedTime { ticks: 10 };
845 let pass0 = PendingActivation {
846 id: 1,
847 proc: 3,
848 arg: ResumeArg::TimeElapsed,
849 reconv_iter: 0,
850 logical_order: (base, Region::Active, 7),
851 };
852 let pass1 = PendingActivation {
853 reconv_iter: 1,
854 ..pass0
855 };
856 assert_eq!(pass0.logical_order, pass1.logical_order);
857 assert_ne!(
858 pass0.reconv_iter, pass1.reconv_iter,
859 "same 3-tuple logical_order must still be distinguishable via reconv_iter"
860 );
861 }
862
863 // --- Task 1 (SC-016, SC-023, SC-033, SC-064, SC-117, SC-129) ---
864
865 #[test]
866 fn sched_time_has_no_delta_field_and_is_integer_only() {
867 let t = SchedTime { ticks: 10 };
868 let t2 = SchedTime { ticks: t.ticks + 5 };
869 assert_eq!(t2.ticks, 15);
870 assert_eq!(core::mem::size_of::<SchedTime>(), 8);
871 }
872
873 #[test]
874 fn sched_time_to_abi_display_only_populates_delta_from_the_given_counter() {
875 let t = SchedTime { ticks: 7 };
876 let a = t.to_abi_display(3);
877 assert_eq!(
878 a,
879 vj_abi::SimTime {
880 sim_units: 7,
881 delta: 3,
882 }
883 );
884 let b = t.to_abi_display(9);
885 assert_eq!(a.sim_units, b.sim_units);
886 assert_ne!(
887 a.delta, b.delta,
888 "delta must come from the caller's counter, not be derived from ticks"
889 );
890 }
891
892 #[test]
893 fn pending_activation_logical_order_uses_sched_time_not_abi_simtime() {
894 let a = PendingActivation {
895 id: 1,
896 proc: 0,
897 arg: ResumeArg::Start,
898 reconv_iter: 0,
899 logical_order: (SchedTime { ticks: 1 }, Region::Active, 0),
900 };
901 assert_eq!(a.logical_order.0.ticks, 1);
902 }
903
904 /// sva T1 (§1.4, review fix, blocking finding 2): was
905 /// `region_has_exactly_five_variants_no_preponed_observed_re_inactive_re_nba`
906 /// -- `Preponed` is now a real, additive 6th variant (`Observed`/
907 /// `Re-Inactive`/`Re-NBA` stay unimplemented).
908 ///
909 /// sva T2: renamed again -- `Observed` is now a real, additive 7th
910 /// variant too (`Re-Inactive`/`Re-NBA` stay unimplemented per §1.4's own
911 /// documented simplification).
912 #[test]
913 fn region_has_exactly_seven_variants_preponed_and_observed_added_re_inactive_re_nba_still_not()
914 {
915 let all = [
916 Region::Active,
917 Region::Inactive,
918 Region::Nba,
919 Region::Reactive,
920 Region::Postponed,
921 Region::Preponed,
922 Region::Observed,
923 ];
924 for r in all {
925 let _ = region_ordinal(r);
926 }
927 assert_eq!(all.len(), 7);
928 }
929
930 #[test]
931 fn region_ordinal_matches_discriminant_for_every_variant() {
932 for r in [
933 Region::Active,
934 Region::Inactive,
935 Region::Nba,
936 Region::Reactive,
937 Region::Postponed,
938 Region::Preponed,
939 Region::Observed,
940 ] {
941 assert_eq!(region_ordinal(r), r as u32);
942 }
943 }
944
945 #[test]
946 fn sc064_type_aliases_have_exact_underlying_representations() {
947 assert_eq!(core::mem::size_of::<SimTick>(), 8);
948 assert_eq!(core::mem::size_of::<ProcessId>(), 4);
949 assert_eq!(core::mem::size_of::<ActivationId>(), 8);
950 assert_eq!(core::mem::size_of::<crate::state::StateCellId>(), 8);
951 assert_eq!(crate::state::StateCellId(42).0, 42);
952 }
953
954 // --- Task 8 (SC-127: DPI-imported-task suspend-point ABI proof) ---
955
956 /// SC-127's scheduler-side half: proves, constructively, that
957 /// `CompiledProcess::resume`'s ABI imposes no structural restriction on
958 /// *where* a `Suspend` value is constructed — a DPI-imported-task
959 /// callback that blocks by calling back into SV is representable today
960 /// with zero scheduler-side special-casing. `DpiCallbackWaiter::resume`
961 /// stands in for "control has been handed to a foreign DPI-imported
962 /// task, which itself calls back into blocking SV": it returns
963 /// `Suspend::Delay(5)` from three stack frames deep inside ordinary Rust
964 /// helper calls, never directly from `resume`'s own top-level match arm
965 /// (the scheduler ABI is agnostic to *where* in Rust call-stack terms a
966 /// `Suspend` value was constructed — it only ever sees the returned
967 /// value). Drives this through the real `Engine`/`SchedulerControl`/
968 /// `ResumeDriver` dispatch loop, using zero DPI-specific engine API
969 /// (there is none), and asserts it dispatches and completes exactly like
970 /// any other delay-suspending process. Fails if a future change ever
971 /// special-cases suspend-point construction by call-stack shape (e.g.
972 /// panicking, dropping the suspend, or resuming with a different
973 /// `ResumeArg`), which would be exactly the "silently mis-scheduling"
974 /// failure SC-127's own acceptance clause (b) warns against for anything
975 /// that does not take option (a) explicitly.
976 #[test]
977 fn sc127_a_mock_process_may_suspend_from_a_point_that_stands_in_for_a_dpi_imported_task_callback()
978 {
979 struct NoopArena;
980 impl crate::state::SignalAccess for NoopArena {
981 fn read(&self, _cell: crate::state::StateCellId) -> FourStateValue {
982 FourStateValue::Narrow(0)
983 }
984 fn write(&mut self, _cell: crate::state::StateCellId, _value: FourStateValue) -> bool {
985 false
986 }
987 fn subscribe(
988 &mut self,
989 _cell: crate::state::StateCellId,
990 _role: crate::state::ListenerRole,
991 _sink: Box<dyn crate::state::ValueChangeSink + Send>,
992 ) {
993 }
994 }
995
996 struct NoopIoSink;
997 impl IoSink for NoopIoSink {
998 fn enqueue(&self, _at: SchedTime, _region: Region, _seq: u64, _cb: DeferredIoCallback) {
999 }
1000 }
1001
1002 // Three stack frames deep -- the suspend point is nowhere near
1003 // `resume`'s own top-level match arm, standing in for control having
1004 // passed through a foreign (DPI) call stack before a callback into
1005 // blocking SV constructs the `Suspend` value.
1006 fn dpi_frame_3(delay: u64) -> Suspend {
1007 Suspend::Delay(delay)
1008 }
1009 fn dpi_frame_2(delay: u64) -> Suspend {
1010 dpi_frame_3(delay)
1011 }
1012 fn dpi_frame_1(delay: u64) -> Suspend {
1013 dpi_frame_2(delay)
1014 }
1015
1016 struct DpiCallbackWaiter {
1017 log: std::sync::Arc<std::sync::Mutex<Vec<ResumeArg>>>,
1018 }
1019 impl CompiledProcess for DpiCallbackWaiter {
1020 fn resume(
1021 &mut self,
1022 _state: SimHandle,
1023 arg: ResumeArg,
1024 _ops: &dyn SchedulerOps,
1025 ) -> Suspend {
1026 self.log.lock().unwrap().push(arg);
1027 match arg {
1028 ResumeArg::Start => dpi_frame_1(5),
1029 _ => Suspend::Done,
1030 }
1031 }
1032 }
1033
1034 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1035 let mut engine = Engine::new(Box::new(NoopArena), Box::new(NoopIoSink));
1036 engine.register_process(
1037 Box::new(DpiCallbackWaiter { log: log.clone() }),
1038 ProcessKind::Initial,
1039 None,
1040 0,
1041 );
1042
1043 let result = engine.run_until(None);
1044
1045 assert!(
1046 matches!(result, RunResult::Completed),
1047 "a delay suspended from deep inside a DPI-callback-shaped call stack must \
1048 still let the run reach quiescence like any other process, got {:?}",
1049 result
1050 );
1051 assert_eq!(engine.now(), SchedTime { ticks: 5 });
1052 let log = log.lock().unwrap();
1053 assert_eq!(
1054 log.len(),
1055 2,
1056 "must resume exactly twice: Start (which suspends via the DPI-shaped \
1057 callback) and TimeElapsed (which completes)"
1058 );
1059 assert!(matches!(log[0], ResumeArg::Start));
1060 assert!(
1061 matches!(log[1], ResumeArg::TimeElapsed),
1062 "the deeply-nested Suspend::Delay(5) must resume the process via an \
1063 ordinary TimeElapsed wakeup -- no DPI-specific ResumeArg variant exists \
1064 or is needed"
1065 );
1066 }
1067
1068 // --- Task 8 (AB-085) ---
1069
1070 /// Type-level check only, NOT proof of AB-085's dispatch-delegation
1071 /// claim: this asserts that `Suspend::WaitForGvt` needs no more than a
1072 /// label in a plain, single-depth `match` -- i.e. it fits the enum's
1073 /// existing shape rather than requiring a new variant-specific branch
1074 /// structure -- and, because the `label` match below has no wildcard
1075 /// arm, that a new `Suspend` variant added later without updating every
1076 /// such match is a compile error, not a silently-passing gate. It does
1077 /// NOT exercise `sched::control::Engine::apply_suspend` and so proves
1078 /// nothing about whether `WaitForGvt` actually delegates to the
1079 /// `Delay(0)` code path at runtime; that behavioral claim is covered by
1080 /// `apply_suspend_wait_for_gvt_delegates_to_the_same_path_as_delay_zero`
1081 /// in `sched::control`'s test module, which drives a real process
1082 /// through the real `Engine` and compares the observed outcome against
1083 /// `Delay(0)`'s.
1084 #[test]
1085 fn wait_for_gvt_fits_the_suspend_enums_existing_match_shape() {
1086 fn label(s: &Suspend) -> &'static str {
1087 match s {
1088 Suspend::Delay(_) => "delay",
1089 Suspend::WaitEvents(_) => "wait_events",
1090 Suspend::ForkJoin(_, _) => "fork_join",
1091 Suspend::WaitFork => "wait_fork",
1092 Suspend::WaitOrder(_) => "wait_order",
1093 Suspend::WaitForGvt => "wait_for_gvt",
1094 Suspend::ByRequest => "by_request",
1095 Suspend::Done => "done",
1096 }
1097 }
1098
1099 assert_eq!(label(&Suspend::WaitForGvt), "wait_for_gvt");
1100 // The other variants still label correctly through the same match --
1101 // proving WaitForGvt didn't need a different match shape.
1102 assert_eq!(label(&Suspend::Delay(0)), "delay");
1103 assert_eq!(
1104 label(&Suspend::WaitEvents(EventWaitSet::default())),
1105 "wait_events"
1106 );
1107 assert_eq!(
1108 label(&Suspend::ForkJoin(JoinKind::All, Vec::new())),
1109 "fork_join"
1110 );
1111 assert_eq!(label(&Suspend::WaitFork), "wait_fork");
1112 assert_eq!(label(&Suspend::WaitOrder(Vec::new())), "wait_order");
1113 assert_eq!(label(&Suspend::Done), "done");
1114 }
1115}