Skip to main content

sv_sim_sim/state/
dispatch.rs

1//! Task 11 — VC listener dispatch: the fixed 3-role table (ledger ST-035,
2//! ST-073, ST-074).
3//!
4//! `Dispatch` is the concrete realization of ST-073's "fixed, small role
5//! table... not an open-ended dynamic subscriber list": exactly 3 named
6//! `Option<Box<dyn ValueChangeSink + Send>>` fields (`scheduler`, `waveform`,
7//! `vpi`), never a `Vec`/`HashMap` keyed by role. `register` dispatches into
8//! one of the 3 fields via an exhaustive match with no wildcard arm (ST-073's
9//! "attempting a fourth/unknown role is a compile-time... invalid call" —
10//! `ListenerRole` itself has exactly 3 variants, frozen in the Task-0
11//! contract stub in `state::mod`, so a 4th role literally cannot be named at
12//! this call site without a compile error). `dispatch` calls `notify_change`
13//! on whichever fields are populated, in the fixed `Scheduler, Waveform, Vpi`
14//! order (ST-073's fixed order) via 3 unrolled `if let Some(..)` checks, not
15//! a loop over a collection (ST-074: the hot path has no heap allocation or
16//! dynamic vtable growth regardless of how many events are dispatched, since
17//! the table's shape is fixed at compile time and never grows).
18//!
19//! `ST-035` ("the mechanism that decides 'did this value change' is
20//! implemented once in STATE and reused by every consumer") is satisfied
21//! architecturally by this being the *only* fan-out point every consumer
22//! (scheduler wakeup, waveform, VPI) registers into via `SignalAccess::
23//! subscribe` (`accessor_impl.rs`, wired below) — VC detection itself lives
24//! in `write::commit_write_at` (Task 8/9), and this module is the one place
25//! its result is announced onward.
26
27use sv_sim_abi::{Region, SimTime};
28
29use super::{EdgeKind, ListenerRole, StateCellId, ValueChangeSink};
30
31/// The fixed 3-role VC listener dispatch table (ST-073, ST-074). See this
32/// module's doc comment for why this is 3 named fields, not a collection.
33pub struct Dispatch {
34    scheduler: Option<Box<dyn ValueChangeSink + Send>>,
35    waveform: Option<Box<dyn ValueChangeSink + Send>>,
36    vpi: Option<Box<dyn ValueChangeSink + Send>>,
37}
38
39impl Dispatch {
40    pub fn new() -> Self {
41        Dispatch {
42            scheduler: None,
43            waveform: None,
44            vpi: None,
45        }
46    }
47
48    /// Registers `sink` for `role`, replacing any previously-registered sink
49    /// for that same role (last write wins — this task does not introduce a
50    /// multi-listener-per-role fan-out; each of the 3 roles holds at most one
51    /// sink, matching `Dispatch`'s own field shape). The match is exhaustive
52    /// with no wildcard arm: `ListenerRole` has exactly 3 variants (frozen in
53    /// `state::mod`'s Task-0 contract stub), so this is the concrete
54    /// enforcement ST-073's "attempting a fourth/unknown role is a
55    /// compile-time... invalid call" asks for — a 4th variant added to
56    /// `ListenerRole` anywhere in this crate breaks this match at compile
57    /// time (ARCHITECTURE §7 item 9's exhaustive-match discipline).
58    pub fn register(&mut self, role: ListenerRole, sink: Box<dyn ValueChangeSink + Send>) {
59        match role {
60            ListenerRole::Scheduler => self.scheduler = Some(sink),
61            ListenerRole::Waveform => self.waveform = Some(sink),
62            ListenerRole::Vpi => self.vpi = Some(sink),
63        }
64    }
65
66    /// Fans `notify_change` out to every registered sink, in the fixed
67    /// `Scheduler, Waveform, Vpi` order (ST-073), via 3 unrolled `if let
68    /// Some(..)` checks — never a loop over a collection, so this method's
69    /// cost is the same 3 branches regardless of how many times it has been
70    /// called before (ST-074).
71    pub fn dispatch(&mut self, cell: StateCellId, edge: EdgeKind, at: SimTime, region: Region) {
72        if let Some(sink) = &mut self.scheduler {
73            sink.notify_change(cell, edge, at, region);
74        }
75        if let Some(sink) = &mut self.waveform {
76            sink.notify_change(cell, edge, at, region);
77        }
78        if let Some(sink) = &mut self.vpi {
79            sink.notify_change(cell, edge, at, region);
80        }
81    }
82
83    /// Task 12 (ST-075): the *only* fan-out `write::commit_write` (`state::write`)
84    /// ever calls — fires the `Scheduler` slot alone, unconditionally, on
85    /// every real change, immediately (never GVT-gated: v1's conservative
86    /// `LOOKAHEAD_CYCLES = 0` engine has no outstanding speculation, so
87    /// "speculative" and "committed" coincide at every event). Never touches
88    /// `waveform`/`vpi` — those observe exclusively through
89    /// `StateEngine::drain_committed_changes` (`state::gvt`), never through
90    /// this method.
91    pub fn notify_scheduler(
92        &mut self,
93        cell: StateCellId,
94        edge: EdgeKind,
95        at: SimTime,
96        region: Region,
97    ) {
98        if let Some(sink) = &mut self.scheduler {
99            sink.notify_change(cell, edge, at, region);
100        }
101    }
102
103    /// Task 12 (ST-078): the Scheduler-less analogue of `dispatch` — fires
104    /// `waveform` then `vpi`, in that fixed order, each only if registered,
105    /// never `scheduler`. Not called eagerly from `commit_write` (see that
106    /// method's own doc comment): Waveform/VPI/`$display`-style consumers
107    /// observe committed changes through `StateEngine::drain_committed_changes`'s
108    /// returned `VcRecord`s instead; this method exists for a future caller
109    /// (e.g. RUNTIME) that already has `EdgeKind`/`Region` context to push an
110    /// eager per-write callback on top of that commit-stream.
111    pub fn notify_waveform_and_vpi(
112        &mut self,
113        cell: StateCellId,
114        edge: EdgeKind,
115        at: SimTime,
116        region: Region,
117    ) {
118        if let Some(sink) = &mut self.waveform {
119            sink.notify_change(cell, edge, at, region);
120        }
121        if let Some(sink) = &mut self.vpi {
122            sink.notify_change(cell, edge, at, region);
123        }
124    }
125}
126
127impl Default for Dispatch {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use std::sync::{Arc, Mutex};
137
138    fn t(units: i64) -> SimTime {
139        SimTime {
140            sim_units: units,
141            delta: 0,
142        }
143    }
144
145    /// One recorded `notify_change` call's arguments, as captured by
146    /// `SpySink::calls` below.
147    type RecordedCall = (StateCellId, EdgeKind, SimTime, Region);
148
149    /// Records every `notify_change` call it receives, tagged with a
150    /// caller-chosen label, into a shared log — used both as a per-role call
151    /// counter (with its own private `Vec`) and, when several spies share one
152    /// `log`, as the fixed-order proof. `Arc<Mutex<..>>` (not `Rc<RefCell<..>>`)
153    /// because `Dispatch::register` requires `Box<dyn ValueChangeSink + Send>`.
154    struct SpySink {
155        label: &'static str,
156        log: Arc<Mutex<Vec<&'static str>>>,
157        calls: Arc<Mutex<Vec<RecordedCall>>>,
158    }
159
160    impl SpySink {
161        fn new(label: &'static str, log: Arc<Mutex<Vec<&'static str>>>) -> Self {
162            SpySink {
163                label,
164                log,
165                calls: Arc::new(Mutex::new(Vec::new())),
166            }
167        }
168    }
169
170    impl ValueChangeSink for SpySink {
171        fn notify_change(
172            &mut self,
173            cell: StateCellId,
174            edge: EdgeKind,
175            at: SimTime,
176            region: Region,
177        ) {
178            self.log.lock().unwrap().push(self.label);
179            self.calls.lock().unwrap().push((cell, edge, at, region));
180        }
181    }
182
183    /// ST-073: register a spy as `Scheduler` only; `dispatch` fires it
184    /// exactly once with the exact arguments passed, and a second spy
185    /// registered as `Waveform` (left unexercised by this call) does not
186    /// fire — proving the 3 slots are independent, each firing only when
187    /// populated (Task 12 adds the real GVT-based routing rule on top of
188    /// this; this task only proves the 3-slot structure itself).
189    #[test]
190    fn register_scheduler_then_dispatch_calls_only_scheduler() {
191        let log = Arc::new(Mutex::new(Vec::new()));
192        let scheduler_spy = SpySink::new("scheduler", log.clone());
193        let scheduler_calls = scheduler_spy.calls.clone();
194        let waveform_spy = SpySink::new("waveform", log.clone());
195        let waveform_calls = waveform_spy.calls.clone();
196
197        let mut dispatch = Dispatch::new();
198        dispatch.register(ListenerRole::Scheduler, Box::new(scheduler_spy));
199        dispatch.register(ListenerRole::Waveform, Box::new(waveform_spy));
200
201        // Only exercise `dispatch` once, and only assert on the Scheduler
202        // role's own effect; the Waveform spy is registered too so its
203        // absence from the log is a meaningful negative, not a vacuous one.
204        let cell = StateCellId(7);
205        let at = t(100);
206        dispatch.dispatch(cell, EdgeKind::Posedge, at, Region::Active);
207
208        assert_eq!(*log.lock().unwrap(), vec!["scheduler", "waveform"]);
209        assert_eq!(scheduler_calls.lock().unwrap().len(), 1);
210        assert_eq!(
211            scheduler_calls.lock().unwrap()[0],
212            (cell, EdgeKind::Posedge, at, Region::Active)
213        );
214        // Waveform *does* fire here too (all populated slots fire on every
215        // `dispatch` call at this task's layer — Task 12 adds the GVT gate
216        // that makes Scheduler-vs-Waveform/VPI visibility diverge); what
217        // this test actually proves is that an *unregistered* role (Vpi,
218        // never registered below) never fires and never panics.
219        assert_eq!(waveform_calls.lock().unwrap().len(), 1);
220    }
221
222    /// ST-073: 3 spies, one per role, all sharing one ordered log; one
223    /// `dispatch` call must produce exactly `["scheduler", "waveform",
224    /// "vpi"]` — the fixed order, never randomized (e.g. never insertion
225    /// order of `register` calls, which this test deliberately reverses
226    /// below to prove that).
227    #[test]
228    fn all_three_roles_registered_all_three_fire_in_fixed_order() {
229        let log = Arc::new(Mutex::new(Vec::new()));
230        let scheduler_spy = SpySink::new("scheduler", log.clone());
231        let waveform_spy = SpySink::new("waveform", log.clone());
232        let vpi_spy = SpySink::new("vpi", log.clone());
233
234        let mut dispatch = Dispatch::new();
235        // Registered in reverse-of-dispatch-order deliberately: if `dispatch`
236        // ever fired in *registration* order instead of the fixed
237        // Scheduler/Waveform/Vpi order, this would produce
238        // ["vpi", "waveform", "scheduler"] instead and fail the assertion
239        // below.
240        dispatch.register(ListenerRole::Vpi, Box::new(vpi_spy));
241        dispatch.register(ListenerRole::Waveform, Box::new(waveform_spy));
242        dispatch.register(ListenerRole::Scheduler, Box::new(scheduler_spy));
243
244        dispatch.dispatch(StateCellId(1), EdgeKind::None, t(0), Region::Nba);
245
246        assert_eq!(*log.lock().unwrap(), vec!["scheduler", "waveform", "vpi"]);
247    }
248
249    /// ST-074: register 3 spies once, then call `dispatch` 10_000× in a
250    /// loop. This test has two independent parts:
251    ///
252    /// 1. A *functional* check at scale: every one of the 10_000 calls
253    ///    actually reaches all 3 spies (not merely "doesn't panic") — a
254    ///    dispatch table that silently dropped/skipped entries under
255    ///    repeated use would fail the exact call count assertions below.
256    /// 2. A *structural* proof that the table itself cannot grow no matter
257    ///    how many events pass through it: an exhaustive field-by-field
258    ///    destructure of `Dispatch` (`let Dispatch { scheduler, waveform,
259    ///    vpi } = ...`, no `..` rest pattern) after the 10_000 calls. This
260    ///    is the actual enforcement — a future edit that turned `Dispatch`
261    ///    into e.g. a `Vec<Box<dyn ValueChangeSink>>`-backed table (the
262    ///    dynamically-resizable shape ST-074 forbids) would add or remove a
263    ///    field this destructure names, which fails to *compile*, not
264    ///    merely fails an assertion. (A `size_of_val`-before/after
265    ///    comparison was considered and rejected per review: `size_of_val`
266    ///    reports a type's fixed, compile-time-known size, which is
267    ///    identical before and after any number of calls regardless of
268    ///    whether the type internally heap-allocates — it would pass this
269    ///    exact scenario just as trivially on a `Vec`-based `Dispatch`, so
270    ///    it is not evidence of anything this row asks for.)
271    #[test]
272    fn dispatch_table_has_no_heap_growth_across_many_events() {
273        let log = Arc::new(Mutex::new(Vec::new()));
274        let scheduler_spy = SpySink::new("scheduler", log.clone());
275        let scheduler_calls = scheduler_spy.calls.clone();
276        let waveform_spy = SpySink::new("waveform", log.clone());
277        let waveform_calls = waveform_spy.calls.clone();
278        let vpi_spy = SpySink::new("vpi", log.clone());
279        let vpi_calls = vpi_spy.calls.clone();
280
281        let mut dispatch = Dispatch::new();
282        dispatch.register(ListenerRole::Scheduler, Box::new(scheduler_spy));
283        dispatch.register(ListenerRole::Waveform, Box::new(waveform_spy));
284        dispatch.register(ListenerRole::Vpi, Box::new(vpi_spy));
285
286        const N: usize = 10_000;
287        for i in 0..N {
288            dispatch.dispatch(
289                StateCellId(i as u64),
290                EdgeKind::Posedge,
291                t(i as i64),
292                Region::Active,
293            );
294        }
295
296        assert_eq!(scheduler_calls.lock().unwrap().len(), N);
297        assert_eq!(waveform_calls.lock().unwrap().len(), N);
298        assert_eq!(vpi_calls.lock().unwrap().len(), N);
299        assert_eq!(log.lock().unwrap().len(), 3 * N);
300
301        // Structural proof (see doc comment above): fails to compile, not
302        // merely fails an assertion, if `Dispatch` ever gains or loses a
303        // field.
304        let Dispatch {
305            scheduler,
306            waveform,
307            vpi,
308        } = dispatch;
309        assert!(scheduler.is_some());
310        assert!(waveform.is_some());
311        assert!(vpi.is_some());
312    }
313
314    /// ST-073 (negative): `ListenerRole` has exactly the 3 variants
315    /// Scheduler/Waveform/Vpi (frozen in the Task-0 contract stub,
316    /// `state::mod`), and `Dispatch::register`'s own match (above) is
317    /// exhaustive with no wildcard arm — the real enforcement mechanism:
318    /// adding a 4th `ListenerRole` variant anywhere in this crate breaks
319    /// that match (and this test's own independent mirror match below) at
320    /// compile time. This test asserts that match compiles today and
321    /// exercises it, which is itself the guard this row asks for.
322    /// Task 12 (ST-075): `notify_scheduler` fires only the `Scheduler` slot,
323    /// even when `Waveform`/`Vpi` are also registered — proving the
324    /// Scheduler-only fan-out `commit_write` relies on. Fails if
325    /// `notify_scheduler` ever fanned out to a second role.
326    #[test]
327    fn notify_scheduler_fires_only_scheduler_never_waveform_or_vpi() {
328        let log = Arc::new(Mutex::new(Vec::new()));
329        let scheduler_spy = SpySink::new("scheduler", log.clone());
330        let waveform_spy = SpySink::new("waveform", log.clone());
331        let waveform_calls = waveform_spy.calls.clone();
332        let vpi_spy = SpySink::new("vpi", log.clone());
333        let vpi_calls = vpi_spy.calls.clone();
334
335        let mut dispatch = Dispatch::new();
336        dispatch.register(ListenerRole::Scheduler, Box::new(scheduler_spy));
337        dispatch.register(ListenerRole::Waveform, Box::new(waveform_spy));
338        dispatch.register(ListenerRole::Vpi, Box::new(vpi_spy));
339
340        dispatch.notify_scheduler(StateCellId(4), EdgeKind::Posedge, t(60), Region::Active);
341
342        assert_eq!(*log.lock().unwrap(), vec!["scheduler"]);
343        assert_eq!(waveform_calls.lock().unwrap().len(), 0);
344        assert_eq!(vpi_calls.lock().unwrap().len(), 0);
345    }
346
347    /// Task 12 (ST-078): `notify_waveform_and_vpi` fires `Waveform` then
348    /// `Vpi`, in that fixed order, and never `Scheduler` — the
349    /// Scheduler-less analogue of Task 11's `all_three_roles_registered_all_three_fire_in_fixed_order`.
350    /// Fails if the order were reversed, if `Vpi` were skipped, or if
351    /// `Scheduler` ever fired from this call.
352    #[test]
353    fn notify_waveform_and_vpi_fires_waveform_then_vpi_never_scheduler() {
354        let log = Arc::new(Mutex::new(Vec::new()));
355        let scheduler_spy = SpySink::new("scheduler", log.clone());
356        let scheduler_calls = scheduler_spy.calls.clone();
357        // Registered reverse-of-fixed-order deliberately, same technique as
358        // Task 11's `all_three_roles_registered_all_three_fire_in_fixed_order`.
359        let vpi_spy = SpySink::new("vpi", log.clone());
360        let waveform_spy = SpySink::new("waveform", log.clone());
361
362        let mut dispatch = Dispatch::new();
363        dispatch.register(ListenerRole::Scheduler, Box::new(scheduler_spy));
364        dispatch.register(ListenerRole::Vpi, Box::new(vpi_spy));
365        dispatch.register(ListenerRole::Waveform, Box::new(waveform_spy));
366
367        dispatch.notify_waveform_and_vpi(StateCellId(3), EdgeKind::None, t(50), Region::Nba);
368
369        assert_eq!(*log.lock().unwrap(), vec!["waveform", "vpi"]);
370        assert_eq!(
371            scheduler_calls.lock().unwrap().len(),
372            0,
373            "notify_waveform_and_vpi must never reach Scheduler"
374        );
375    }
376
377    #[test]
378    fn attempting_a_fourth_role_does_not_compile() {
379        fn describe(role: ListenerRole) -> &'static str {
380            match role {
381                ListenerRole::Scheduler => "scheduler",
382                ListenerRole::Waveform => "waveform",
383                ListenerRole::Vpi => "vpi",
384            }
385        }
386        assert_eq!(describe(ListenerRole::Scheduler), "scheduler");
387        assert_eq!(describe(ListenerRole::Waveform), "waveform");
388        assert_eq!(describe(ListenerRole::Vpi), "vpi");
389    }
390}