Skip to main content

sv_sim_sim/sched/
ops.rs

1//! SCHED::OPS — the blocking-assignment commit path and the `SchedulerOps`
2//! mock surface a compiled process body calls into during `resume()`
3//! (scheduler.md §4/"Public Interfaces"; ARCHITECTURE §6.3; ledger SC-043,
4//! SC-044, SC-070).
5//!
6//! [`commit_blocking_write`] mirrors Task 7's [`crate::sched::commit_nba`]
7//! cell-write path but synchronous/single-cell: applies immediately, and iff
8//! the write actually changed the stored value, calls
9//! [`ValueChangeSink::notify_change`] tagged `Region::Active` — this crate's
10//! second (and last) sanctioned production `.notify_change(` call site
11//! (SC-042; the other is `commit_nba`; Task 22's crate-wide scan is the
12//! proof that exactly these two exist). The function's own signature is the
13//! SC-043 evidence: it takes an opaque [`StateCellId`] and an edge-classified
14//! [`FourStateValue`] transition and nothing else — no parameter, branch, or
15//! call anywhere in this module distinguishes how a cell's value is sourced
16//! or resolved; that distinction belongs entirely to another subsystem.
17
18use crate::sched::{SchedTime, infer_edge};
19use crate::state::{ChangeDelta, FourStateValue, SignalAccess, StateCellId, ValueChangeSink};
20use sv_sim_abi::Region;
21
22/// Commits a blocking (`=`) assignment synchronously, inside the caller's own
23/// `resume()` call: writes `value` to `cell` via `arena.write`, and iff that
24/// write actually changed the stored value, classifies the transition and
25/// calls `sink.notify_change(..)` tagged `Region::Active` before returning.
26/// Any concrete `SchedulerOps::commit_blocking_write` implementation must
27/// call this function (SC-044) rather than applying the write some other
28/// way.
29pub fn commit_blocking_write(
30    cell: StateCellId,
31    value: FourStateValue,
32    arena: &mut dyn SignalAccess,
33    at: SchedTime,
34    sink: &mut dyn ValueChangeSink,
35) -> bool {
36    let old = arena.read(cell);
37    let new_for_edge = value.clone();
38    let changed = arena.write(cell, value);
39    if changed {
40        if sink.wants_delta(cell) {
41            sink.note_delta(cell, ChangeDelta::between(&old, &new_for_edge));
42        }
43        notify_blocking_change(cell, infer_edge(&old, &new_for_edge), at, sink);
44    }
45    notify_alias_changes(arena, at, sink);
46    changed
47}
48
49/// Announces the changes of other names for the storage a write just
50/// touched (window aliases of ref ports, `state::alias`).
51pub fn notify_alias_changes(
52    arena: &mut dyn SignalAccess,
53    at: SchedTime,
54    sink: &mut dyn ValueChangeSink,
55) {
56    for change in arena.take_alias_changes() {
57        let edge = if change.no_edge {
58            crate::state::EdgeKind::None
59        } else {
60            infer_edge(&change.old, &change.new)
61        };
62        notify_blocking_change(change.cell, edge, at, sink);
63    }
64}
65
66/// The notification half of [`commit_blocking_write`], for a blocking write
67/// the caller already committed through another STATE path that
68/// `SignalAccess::write` cannot express -- one row of a memory
69/// (`sched::kernel_adapter::real_sv_set_mem_word`). Call only when that
70/// write actually changed the stored value.
71pub fn notify_blocking_change(
72    cell: StateCellId,
73    edge: crate::state::EdgeKind,
74    at: SchedTime,
75    sink: &mut dyn ValueChangeSink,
76) {
77    sink.notify_change(cell, edge, at.to_abi_display(0), Region::Active);
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::sched::{DeferredIoCallback, NbaLog, ProcessId, SchedulerOps};
84    use crate::state::{EdgeKind, ListenerRole};
85    use std::cell::RefCell;
86    use std::collections::HashMap;
87    use std::rc::Rc;
88    use sv_sim_abi::SimTime;
89
90    // `FakeArena`, independently declared per this plan's established
91    // pattern (see Task 21's own note: `notify.rs`'s `FakeArena` is private
92    // to that module's test block and unreachable here) — same shape as
93    // `notify::tests::FakeArena`: one shared piece of state backing both a
94    // `SignalAccess` view and a `ValueChangeSink` view, split into two thin
95    // handles sharing one `Rc<RefCell<..>>` because `commit_blocking_write`
96    // takes `arena: &mut dyn SignalAccess` and `sink: &mut dyn
97    // ValueChangeSink` as two simultaneous `&mut` parameters.
98    struct FakeArenaState {
99        cells: HashMap<StateCellId, FourStateValue>,
100        notifications: Vec<(StateCellId, EdgeKind, SimTime, Region)>,
101    }
102
103    #[derive(Clone)]
104    struct FakeArena(Rc<RefCell<FakeArenaState>>);
105
106    struct ArenaHandle(Rc<RefCell<FakeArenaState>>);
107    struct SinkHandle(Rc<RefCell<FakeArenaState>>);
108
109    fn four_state_eq(a: &FourStateValue, b: &FourStateValue) -> bool {
110        match (a, b) {
111            (FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => x == y,
112            (
113                FourStateValue::Wide { aval: a1, bval: b1 },
114                FourStateValue::Wide { aval: a2, bval: b2 },
115            ) => a1 == a2 && b1 == b2,
116            _ => false,
117        }
118    }
119
120    impl FakeArena {
121        fn new() -> Self {
122            Self(Rc::new(RefCell::new(FakeArenaState {
123                cells: HashMap::new(),
124                notifications: Vec::new(),
125            })))
126        }
127
128        fn seed(&self, cell: StateCellId, value: FourStateValue) {
129            self.0.borrow_mut().cells.insert(cell, value);
130        }
131
132        fn notifications(&self) -> Vec<(StateCellId, EdgeKind, SimTime, Region)> {
133            self.0.borrow().notifications.clone()
134        }
135
136        fn arena_handle(&self) -> ArenaHandle {
137            ArenaHandle(self.0.clone())
138        }
139
140        fn sink_handle(&self) -> SinkHandle {
141            SinkHandle(self.0.clone())
142        }
143    }
144
145    impl SignalAccess for ArenaHandle {
146        fn read(&self, cell: StateCellId) -> FourStateValue {
147            self.0
148                .borrow()
149                .cells
150                .get(&cell)
151                .cloned()
152                .unwrap_or(FourStateValue::Narrow(0))
153        }
154
155        fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
156            let mut state = self.0.borrow_mut();
157            let changed = match state.cells.get(&cell) {
158                Some(existing) => !four_state_eq(existing, &value),
159                None => true,
160            };
161            state.cells.insert(cell, value);
162            changed
163        }
164
165        fn subscribe(
166            &mut self,
167            _cell: StateCellId,
168            _role: ListenerRole,
169            _sink: Box<dyn ValueChangeSink + Send>,
170        ) {
171            // Not exercised by this task's tests — Task 9 owns
172            // SensitivityTable wiring.
173        }
174    }
175
176    impl ValueChangeSink for SinkHandle {
177        fn notify_change(
178            &mut self,
179            cell: StateCellId,
180            edge: EdgeKind,
181            at: SimTime,
182            region: Region,
183        ) {
184            self.0
185                .borrow_mut()
186                .notifications
187                .push((cell, edge, at, region));
188        }
189    }
190
191    const PROC_A: ProcessId = 1;
192
193    // --- commit_blocking_write (SC-044) ---
194
195    #[test]
196    fn commit_blocking_write_is_visible_immediately_same_activation() {
197        let fake = FakeArena::new();
198        let cell = StateCellId(1);
199        fake.seed(cell, FourStateValue::Narrow(0));
200        let mut arena = fake.arena_handle();
201        let mut sink = fake.sink_handle();
202
203        let changed = commit_blocking_write(
204            cell,
205            FourStateValue::Narrow(1),
206            &mut arena,
207            SchedTime { ticks: 0 },
208            &mut sink,
209        );
210
211        assert!(changed);
212        // Read back through the very same handle, in the same test scope,
213        // before any region/timestep advance — simulating "within one
214        // resume() call". Fails if the write were staged instead of applied
215        // synchronously.
216        assert!(matches!(arena.read(cell), FourStateValue::Narrow(1)));
217    }
218
219    #[test]
220    fn commit_blocking_write_triggers_notify_within_the_same_active_pass() {
221        let fake = FakeArena::new();
222        let cell = StateCellId(2);
223        fake.seed(cell, FourStateValue::Narrow(0));
224        let mut arena = fake.arena_handle();
225        let mut sink = fake.sink_handle();
226
227        commit_blocking_write(
228            cell,
229            FourStateValue::Narrow(1),
230            &mut arena,
231            SchedTime { ticks: 5 },
232            &mut sink,
233        );
234
235        let notes = fake.notifications();
236        assert_eq!(
237            notes.len(),
238            1,
239            "exactly one notify_change call, with no deferral to a later region"
240        );
241        assert_eq!(notes[0].0, cell);
242        assert_eq!(
243            notes[0].3,
244            Region::Active,
245            "commit_blocking_write must tag its notify_change call Region::Active"
246        );
247    }
248
249    /// Mutation-test kill: an implementation that calls `sink.notify_change`
250    /// unconditionally (moving the call outside the `if changed` guard)
251    /// passes every other test in this file, since they only ever write a
252    /// value that actually differs from the seeded one. Writing the cell's
253    /// *already-current* value must report `changed == false` and must not
254    /// call `notify_change` at all (SC-040/SC-041's "never for a write whose
255    /// resolved value equals the cell's current one").
256    #[test]
257    fn commit_blocking_write_does_not_notify_when_the_value_is_unchanged() {
258        let fake = FakeArena::new();
259        let cell = StateCellId(9);
260        fake.seed(cell, FourStateValue::Narrow(5));
261        let mut arena = fake.arena_handle();
262        let mut sink = fake.sink_handle();
263
264        let changed = commit_blocking_write(
265            cell,
266            FourStateValue::Narrow(5),
267            &mut arena,
268            SchedTime { ticks: 0 },
269            &mut sink,
270        );
271
272        assert!(
273            !changed,
274            "writing the cell's own current value must report no change"
275        );
276        assert!(
277            fake.notifications().is_empty(),
278            "a write that does not change the stored value must never call notify_change"
279        );
280    }
281
282    #[test]
283    fn commit_blocking_write_behaves_identically_for_a_net_backed_or_var_backed_cell() {
284        // "net"/"var" label these two cells purely for the human reader —
285        // commit_blocking_write's own signature (above) has no parameter
286        // that could carry such a distinction. That absence, not this
287        // comment, is the actual proof; this test only checks the
288        // consequence: both calls, given equivalent value transitions,
289        // produce structurally identical (bool, notify-log-entry) outcomes.
290        let net_labeled_cell = StateCellId(10);
291        let var_labeled_cell = StateCellId(11);
292
293        let fake = FakeArena::new();
294        fake.seed(net_labeled_cell, FourStateValue::Narrow(0));
295        fake.seed(var_labeled_cell, FourStateValue::Narrow(0));
296        let mut arena = fake.arena_handle();
297        let mut sink = fake.sink_handle();
298
299        let changed_a = commit_blocking_write(
300            net_labeled_cell,
301            FourStateValue::Narrow(1),
302            &mut arena,
303            SchedTime { ticks: 0 },
304            &mut sink,
305        );
306        let changed_b = commit_blocking_write(
307            var_labeled_cell,
308            FourStateValue::Narrow(1),
309            &mut arena,
310            SchedTime { ticks: 0 },
311            &mut sink,
312        );
313
314        assert_eq!(changed_a, changed_b);
315        let notes = fake.notifications();
316        assert_eq!(notes.len(), 2);
317        assert_eq!(
318            notes[0].1, notes[1].1,
319            "both transitions must classify to the same edge"
320        );
321        assert_eq!(
322            notes[0].3, notes[1].3,
323            "both calls must tag the same region"
324        );
325    }
326
327    /// Reviewer-facing negative for SC-043: scans this file's own
328    /// *production* source (via `determinism::scan_support::
329    /// strip_cfg_test_blocks`, the same technique Task 3's DT-006 test
330    /// uses, so this test's own use of the scanned-for words above never
331    /// trips itself) for the case-insensitive substrings a net-vs-variable
332    /// distinction would need, and asserts none appear. A future
333    /// implementation that adds any such branch — even one that still
334    /// passes the behavioral test above by coincidence — fails this test.
335    #[test]
336    fn ops_rs_production_code_has_no_signal_origin_vocabulary() {
337        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/sched/ops.rs");
338        let src = std::fs::read_to_string(&path)
339            .expect("read sched/ops.rs for the SC-043 source-text scan");
340        let production_only = crate::determinism::scan_support::strip_cfg_test_blocks(&src);
341        let lower = production_only.to_lowercase();
342        let banned = ["net", "strength", "driver"];
343        let found: Vec<&str> = banned
344            .iter()
345            .copied()
346            .filter(|needle| lower.contains(needle))
347            .collect();
348        assert!(
349            found.is_empty(),
350            "sched/ops.rs production code must draw no net-vs-variable \
351             distinction (SC-043), but found banned substring(s) {found:?}"
352        );
353    }
354
355    // --- SchedulerOps / MockSchedulerOps (SC-070) ---
356
357    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
358    enum MockOpsCall {
359        Now,
360        CurrentRegion,
361        StageNba,
362        CommitBlockingWrite,
363        RequestFinish,
364        DeferIo,
365        RegisterRegionCallback,
366    }
367
368    /// The in-crate test double standing in for a compiled kernel's view of
369    /// the scheduler (no codegen exists yet). Records one [`MockOpsCall`]
370    /// variant per call, and for `stage_nba`/`commit_blocking_write` also
371    /// actually calls through to [`NbaLog::stage`]/[`commit_blocking_write`]
372    /// so the mock is behaviorally real, not just a spy.
373    struct MockSchedulerOps {
374        arena: RefCell<FakeArena>,
375        // Not in the plan's illustrative field list, but required for
376        // `stage_nba` to genuinely "call through to `NbaLog::stage`" as the
377        // task brief specifies rather than only recording the call —
378        // documented as a deviation (field list is additive, not narrowing).
379        nba: RefCell<NbaLog>,
380        calls: RefCell<Vec<MockOpsCall>>,
381        region: Region,
382        now: SchedTime,
383    }
384
385    impl MockSchedulerOps {
386        fn new(arena: FakeArena, region: Region, now: SchedTime) -> Self {
387            Self {
388                arena: RefCell::new(arena),
389                nba: RefCell::new(NbaLog::new()),
390                calls: RefCell::new(Vec::new()),
391                region,
392                now,
393            }
394        }
395    }
396
397    impl SchedulerOps for MockSchedulerOps {
398        fn now(&self) -> SchedTime {
399            self.calls.borrow_mut().push(MockOpsCall::Now);
400            self.now
401        }
402
403        fn current_region(&self) -> Region {
404            self.calls.borrow_mut().push(MockOpsCall::CurrentRegion);
405            self.region
406        }
407
408        fn stage_nba(&self, cell: StateCellId, value: FourStateValue, source: ProcessId) {
409            self.calls.borrow_mut().push(MockOpsCall::StageNba);
410            self.nba.borrow_mut().stage(cell, value, source);
411        }
412
413        fn commit_blocking_write(&self, cell: StateCellId, value: FourStateValue) {
414            self.calls
415                .borrow_mut()
416                .push(MockOpsCall::CommitBlockingWrite);
417            let fake = self.arena.borrow();
418            let mut arena_handle = fake.arena_handle();
419            let mut sink_handle = fake.sink_handle();
420            super::commit_blocking_write(
421                cell,
422                value,
423                &mut arena_handle,
424                self.now,
425                &mut sink_handle,
426            );
427        }
428
429        fn request_finish(&self, _code: i32) {
430            self.calls.borrow_mut().push(MockOpsCall::RequestFinish);
431        }
432
433        fn defer_io(&self, _at: SchedTime, _region: Region, _cb: DeferredIoCallback) {
434            self.calls.borrow_mut().push(MockOpsCall::DeferIo);
435        }
436
437        fn register_region_callback(
438            &self,
439            _region: Region,
440            _at: SchedTime,
441            _cb: Box<dyn crate::sched::RegionCallback>,
442        ) {
443            self.calls
444                .borrow_mut()
445                .push(MockOpsCall::RegisterRegionCallback);
446        }
447    }
448
449    /// A no-op [`crate::sched::RegionCallback`] double for the exhaustive
450    /// `SchedulerOps` method-count test below — this test never invokes it,
451    /// only proves `MockSchedulerOps::register_region_callback` itself was
452    /// called.
453    struct NoopRegionCallback;
454    impl crate::sched::RegionCallback for NoopRegionCallback {
455        fn invoke(&mut self, _ctx: &dyn crate::sched::SchedulerCtx) {}
456    }
457
458    #[test]
459    fn mock_scheduler_ops_exercises_all_seven_methods() {
460        let fake = FakeArena::new();
461        let cell = StateCellId(20);
462        fake.seed(cell, FourStateValue::Narrow(0));
463        let ops = MockSchedulerOps::new(fake, Region::Active, SchedTime { ticks: 0 });
464
465        // Drives the mock through all seven methods from inside a closure
466        // standing in for a compiled kernel's resume() body, in this order.
467        let resume_body = |ops: &MockSchedulerOps| {
468            let _ = ops.now();
469            let _ = ops.current_region();
470            ops.stage_nba(cell, FourStateValue::Narrow(1), PROC_A);
471            ops.commit_blocking_write(cell, FourStateValue::Narrow(2));
472            ops.request_finish(0);
473            ops.defer_io(SchedTime { ticks: 0 }, Region::Active, Box::new(|_ctx| {}));
474            ops.register_region_callback(
475                Region::Postponed,
476                SchedTime { ticks: 0 },
477                Box::new(NoopRegionCallback),
478            );
479        };
480        resume_body(&ops);
481
482        let calls = ops.calls.borrow();
483        assert_eq!(calls.len(), 7, "the trait has exactly seven methods");
484        assert_eq!(
485            *calls,
486            vec![
487                MockOpsCall::Now,
488                MockOpsCall::CurrentRegion,
489                MockOpsCall::StageNba,
490                MockOpsCall::CommitBlockingWrite,
491                MockOpsCall::RequestFinish,
492                MockOpsCall::DeferIo,
493                MockOpsCall::RegisterRegionCallback,
494            ],
495            "each of the seven methods must appear exactly once, in call order"
496        );
497    }
498}