vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
//! SCHED::OPS — the blocking-assignment commit path and the `SchedulerOps`
//! mock surface a compiled process body calls into during `resume()`
//! (scheduler.md §4/"Public Interfaces"; ARCHITECTURE §6.3; ledger SC-043,
//! SC-044, SC-070).
//!
//! [`commit_blocking_write`] mirrors Task 7's [`crate::sched::commit_nba`]
//! cell-write path but synchronous/single-cell: applies immediately, and iff
//! the write actually changed the stored value, calls
//! [`ValueChangeSink::notify_change`] tagged `Region::Active` — this crate's
//! second (and last) sanctioned production `.notify_change(` call site
//! (SC-042; the other is `commit_nba`; Task 22's crate-wide scan is the
//! proof that exactly these two exist). The function's own signature is the
//! SC-043 evidence: it takes an opaque [`StateCellId`] and an edge-classified
//! [`FourStateValue`] transition and nothing else — no parameter, branch, or
//! call anywhere in this module distinguishes how a cell's value is sourced
//! or resolved; that distinction belongs entirely to another subsystem.

use crate::sched::{SchedTime, infer_edge};
use crate::state::{ChangeDelta, FourStateValue, SignalAccess, StateCellId, ValueChangeSink};
use vj_abi::Region;

/// Commits a blocking (`=`) assignment synchronously, inside the caller's own
/// `resume()` call: writes `value` to `cell` via `arena.write`, and iff that
/// write actually changed the stored value, classifies the transition and
/// calls `sink.notify_change(..)` tagged `Region::Active` before returning.
/// Any concrete `SchedulerOps::commit_blocking_write` implementation must
/// call this function (SC-044) rather than applying the write some other
/// way.
pub fn commit_blocking_write(
    cell: StateCellId,
    value: FourStateValue,
    arena: &mut dyn SignalAccess,
    at: SchedTime,
    sink: &mut dyn ValueChangeSink,
) -> bool {
    let old = arena.read(cell);
    let new_for_edge = value.clone();
    let changed = arena.write(cell, value);
    if changed {
        if sink.wants_delta(cell) {
            sink.note_delta(cell, ChangeDelta::between(&old, &new_for_edge));
        }
        notify_blocking_change(cell, infer_edge(&old, &new_for_edge), at, sink);
    }
    notify_alias_changes(arena, at, sink);
    changed
}

/// Announces the changes of other names for the storage a write just
/// touched (window aliases of ref ports, `state::alias`).
pub fn notify_alias_changes(
    arena: &mut dyn SignalAccess,
    at: SchedTime,
    sink: &mut dyn ValueChangeSink,
) {
    for change in arena.take_alias_changes() {
        let edge = if change.no_edge {
            crate::state::EdgeKind::None
        } else {
            infer_edge(&change.old, &change.new)
        };
        notify_blocking_change(change.cell, edge, at, sink);
    }
}

/// The notification half of [`commit_blocking_write`], for a blocking write
/// the caller already committed through another STATE path that
/// `SignalAccess::write` cannot express -- one row of a memory
/// (`sched::kernel_adapter::real_sv_set_mem_word`). Call only when that
/// write actually changed the stored value.
pub fn notify_blocking_change(
    cell: StateCellId,
    edge: crate::state::EdgeKind,
    at: SchedTime,
    sink: &mut dyn ValueChangeSink,
) {
    sink.notify_change(cell, edge, at.to_abi_display(0), Region::Active);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sched::{DeferredIoCallback, NbaLog, ProcessId, SchedulerOps};
    use crate::state::{EdgeKind, ListenerRole};
    use std::cell::RefCell;
    use std::collections::HashMap;
    use std::rc::Rc;
    use vj_abi::SimTime;

    // `FakeArena`, independently declared per this plan's established
    // pattern (see Task 21's own note: `notify.rs`'s `FakeArena` is private
    // to that module's test block and unreachable here) — same shape as
    // `notify::tests::FakeArena`: one shared piece of state backing both a
    // `SignalAccess` view and a `ValueChangeSink` view, split into two thin
    // handles sharing one `Rc<RefCell<..>>` because `commit_blocking_write`
    // takes `arena: &mut dyn SignalAccess` and `sink: &mut dyn
    // ValueChangeSink` as two simultaneous `&mut` parameters.
    struct FakeArenaState {
        cells: HashMap<StateCellId, FourStateValue>,
        notifications: Vec<(StateCellId, EdgeKind, SimTime, Region)>,
    }

    #[derive(Clone)]
    struct FakeArena(Rc<RefCell<FakeArenaState>>);

    struct ArenaHandle(Rc<RefCell<FakeArenaState>>);
    struct SinkHandle(Rc<RefCell<FakeArenaState>>);

    fn four_state_eq(a: &FourStateValue, b: &FourStateValue) -> bool {
        match (a, b) {
            (FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => x == y,
            (
                FourStateValue::Wide { aval: a1, bval: b1 },
                FourStateValue::Wide { aval: a2, bval: b2 },
            ) => a1 == a2 && b1 == b2,
            _ => false,
        }
    }

    impl FakeArena {
        fn new() -> Self {
            Self(Rc::new(RefCell::new(FakeArenaState {
                cells: HashMap::new(),
                notifications: Vec::new(),
            })))
        }

        fn seed(&self, cell: StateCellId, value: FourStateValue) {
            self.0.borrow_mut().cells.insert(cell, value);
        }

        fn notifications(&self) -> Vec<(StateCellId, EdgeKind, SimTime, Region)> {
            self.0.borrow().notifications.clone()
        }

        fn arena_handle(&self) -> ArenaHandle {
            ArenaHandle(self.0.clone())
        }

        fn sink_handle(&self) -> SinkHandle {
            SinkHandle(self.0.clone())
        }
    }

    impl SignalAccess for ArenaHandle {
        fn read(&self, cell: StateCellId) -> FourStateValue {
            self.0
                .borrow()
                .cells
                .get(&cell)
                .cloned()
                .unwrap_or(FourStateValue::Narrow(0))
        }

        fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
            let mut state = self.0.borrow_mut();
            let changed = match state.cells.get(&cell) {
                Some(existing) => !four_state_eq(existing, &value),
                None => true,
            };
            state.cells.insert(cell, value);
            changed
        }

        fn subscribe(
            &mut self,
            _cell: StateCellId,
            _role: ListenerRole,
            _sink: Box<dyn ValueChangeSink + Send>,
        ) {
            // Not exercised by this task's tests — Task 9 owns
            // SensitivityTable wiring.
        }
    }

    impl ValueChangeSink for SinkHandle {
        fn notify_change(
            &mut self,
            cell: StateCellId,
            edge: EdgeKind,
            at: SimTime,
            region: Region,
        ) {
            self.0
                .borrow_mut()
                .notifications
                .push((cell, edge, at, region));
        }
    }

    const PROC_A: ProcessId = 1;

    // --- commit_blocking_write (SC-044) ---

    #[test]
    fn commit_blocking_write_is_visible_immediately_same_activation() {
        let fake = FakeArena::new();
        let cell = StateCellId(1);
        fake.seed(cell, FourStateValue::Narrow(0));
        let mut arena = fake.arena_handle();
        let mut sink = fake.sink_handle();

        let changed = commit_blocking_write(
            cell,
            FourStateValue::Narrow(1),
            &mut arena,
            SchedTime { ticks: 0 },
            &mut sink,
        );

        assert!(changed);
        // Read back through the very same handle, in the same test scope,
        // before any region/timestep advance — simulating "within one
        // resume() call". Fails if the write were staged instead of applied
        // synchronously.
        assert!(matches!(arena.read(cell), FourStateValue::Narrow(1)));
    }

    #[test]
    fn commit_blocking_write_triggers_notify_within_the_same_active_pass() {
        let fake = FakeArena::new();
        let cell = StateCellId(2);
        fake.seed(cell, FourStateValue::Narrow(0));
        let mut arena = fake.arena_handle();
        let mut sink = fake.sink_handle();

        commit_blocking_write(
            cell,
            FourStateValue::Narrow(1),
            &mut arena,
            SchedTime { ticks: 5 },
            &mut sink,
        );

        let notes = fake.notifications();
        assert_eq!(
            notes.len(),
            1,
            "exactly one notify_change call, with no deferral to a later region"
        );
        assert_eq!(notes[0].0, cell);
        assert_eq!(
            notes[0].3,
            Region::Active,
            "commit_blocking_write must tag its notify_change call Region::Active"
        );
    }

    /// Mutation-test kill: an implementation that calls `sink.notify_change`
    /// unconditionally (moving the call outside the `if changed` guard)
    /// passes every other test in this file, since they only ever write a
    /// value that actually differs from the seeded one. Writing the cell's
    /// *already-current* value must report `changed == false` and must not
    /// call `notify_change` at all (SC-040/SC-041's "never for a write whose
    /// resolved value equals the cell's current one").
    #[test]
    fn commit_blocking_write_does_not_notify_when_the_value_is_unchanged() {
        let fake = FakeArena::new();
        let cell = StateCellId(9);
        fake.seed(cell, FourStateValue::Narrow(5));
        let mut arena = fake.arena_handle();
        let mut sink = fake.sink_handle();

        let changed = commit_blocking_write(
            cell,
            FourStateValue::Narrow(5),
            &mut arena,
            SchedTime { ticks: 0 },
            &mut sink,
        );

        assert!(
            !changed,
            "writing the cell's own current value must report no change"
        );
        assert!(
            fake.notifications().is_empty(),
            "a write that does not change the stored value must never call notify_change"
        );
    }

    #[test]
    fn commit_blocking_write_behaves_identically_for_a_net_backed_or_var_backed_cell() {
        // "net"/"var" label these two cells purely for the human reader —
        // commit_blocking_write's own signature (above) has no parameter
        // that could carry such a distinction. That absence, not this
        // comment, is the actual proof; this test only checks the
        // consequence: both calls, given equivalent value transitions,
        // produce structurally identical (bool, notify-log-entry) outcomes.
        let net_labeled_cell = StateCellId(10);
        let var_labeled_cell = StateCellId(11);

        let fake = FakeArena::new();
        fake.seed(net_labeled_cell, FourStateValue::Narrow(0));
        fake.seed(var_labeled_cell, FourStateValue::Narrow(0));
        let mut arena = fake.arena_handle();
        let mut sink = fake.sink_handle();

        let changed_a = commit_blocking_write(
            net_labeled_cell,
            FourStateValue::Narrow(1),
            &mut arena,
            SchedTime { ticks: 0 },
            &mut sink,
        );
        let changed_b = commit_blocking_write(
            var_labeled_cell,
            FourStateValue::Narrow(1),
            &mut arena,
            SchedTime { ticks: 0 },
            &mut sink,
        );

        assert_eq!(changed_a, changed_b);
        let notes = fake.notifications();
        assert_eq!(notes.len(), 2);
        assert_eq!(
            notes[0].1, notes[1].1,
            "both transitions must classify to the same edge"
        );
        assert_eq!(
            notes[0].3, notes[1].3,
            "both calls must tag the same region"
        );
    }

    /// Reviewer-facing negative for SC-043: scans this file's own
    /// *production* source (via `determinism::scan_support::
    /// strip_cfg_test_blocks`, the same technique Task 3's DT-006 test
    /// uses, so this test's own use of the scanned-for words above never
    /// trips itself) for the case-insensitive substrings a net-vs-variable
    /// distinction would need, and asserts none appear. A future
    /// implementation that adds any such branch — even one that still
    /// passes the behavioral test above by coincidence — fails this test.
    #[test]
    fn ops_rs_production_code_has_no_signal_origin_vocabulary() {
        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/sched/ops.rs");
        let src = std::fs::read_to_string(&path)
            .expect("read sched/ops.rs for the SC-043 source-text scan");
        let production_only = crate::determinism::scan_support::strip_cfg_test_blocks(&src);
        let lower = production_only.to_lowercase();
        let banned = ["net", "strength", "driver"];
        let found: Vec<&str> = banned
            .iter()
            .copied()
            .filter(|needle| lower.contains(needle))
            .collect();
        assert!(
            found.is_empty(),
            "sched/ops.rs production code must draw no net-vs-variable \
             distinction (SC-043), but found banned substring(s) {found:?}"
        );
    }

    // --- SchedulerOps / MockSchedulerOps (SC-070) ---

    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
    enum MockOpsCall {
        Now,
        CurrentRegion,
        StageNba,
        CommitBlockingWrite,
        RequestFinish,
        DeferIo,
        RegisterRegionCallback,
    }

    /// The in-crate test double standing in for a compiled kernel's view of
    /// the scheduler (no codegen exists yet). Records one [`MockOpsCall`]
    /// variant per call, and for `stage_nba`/`commit_blocking_write` also
    /// actually calls through to [`NbaLog::stage`]/[`commit_blocking_write`]
    /// so the mock is behaviorally real, not just a spy.
    struct MockSchedulerOps {
        arena: RefCell<FakeArena>,
        // Not in the plan's illustrative field list, but required for
        // `stage_nba` to genuinely "call through to `NbaLog::stage`" as the
        // task brief specifies rather than only recording the call —
        // documented as a deviation (field list is additive, not narrowing).
        nba: RefCell<NbaLog>,
        calls: RefCell<Vec<MockOpsCall>>,
        region: Region,
        now: SchedTime,
    }

    impl MockSchedulerOps {
        fn new(arena: FakeArena, region: Region, now: SchedTime) -> Self {
            Self {
                arena: RefCell::new(arena),
                nba: RefCell::new(NbaLog::new()),
                calls: RefCell::new(Vec::new()),
                region,
                now,
            }
        }
    }

    impl SchedulerOps for MockSchedulerOps {
        fn now(&self) -> SchedTime {
            self.calls.borrow_mut().push(MockOpsCall::Now);
            self.now
        }

        fn current_region(&self) -> Region {
            self.calls.borrow_mut().push(MockOpsCall::CurrentRegion);
            self.region
        }

        fn stage_nba(&self, cell: StateCellId, value: FourStateValue, source: ProcessId) {
            self.calls.borrow_mut().push(MockOpsCall::StageNba);
            self.nba.borrow_mut().stage(cell, value, source);
        }

        fn commit_blocking_write(&self, cell: StateCellId, value: FourStateValue) {
            self.calls
                .borrow_mut()
                .push(MockOpsCall::CommitBlockingWrite);
            let fake = self.arena.borrow();
            let mut arena_handle = fake.arena_handle();
            let mut sink_handle = fake.sink_handle();
            super::commit_blocking_write(
                cell,
                value,
                &mut arena_handle,
                self.now,
                &mut sink_handle,
            );
        }

        fn request_finish(&self, _code: i32) {
            self.calls.borrow_mut().push(MockOpsCall::RequestFinish);
        }

        fn defer_io(&self, _at: SchedTime, _region: Region, _cb: DeferredIoCallback) {
            self.calls.borrow_mut().push(MockOpsCall::DeferIo);
        }

        fn register_region_callback(
            &self,
            _region: Region,
            _at: SchedTime,
            _cb: Box<dyn crate::sched::RegionCallback>,
        ) {
            self.calls
                .borrow_mut()
                .push(MockOpsCall::RegisterRegionCallback);
        }
    }

    /// A no-op [`crate::sched::RegionCallback`] double for the exhaustive
    /// `SchedulerOps` method-count test below — this test never invokes it,
    /// only proves `MockSchedulerOps::register_region_callback` itself was
    /// called.
    struct NoopRegionCallback;
    impl crate::sched::RegionCallback for NoopRegionCallback {
        fn invoke(&mut self, _ctx: &dyn crate::sched::SchedulerCtx) {}
    }

    #[test]
    fn mock_scheduler_ops_exercises_all_seven_methods() {
        let fake = FakeArena::new();
        let cell = StateCellId(20);
        fake.seed(cell, FourStateValue::Narrow(0));
        let ops = MockSchedulerOps::new(fake, Region::Active, SchedTime { ticks: 0 });

        // Drives the mock through all seven methods from inside a closure
        // standing in for a compiled kernel's resume() body, in this order.
        let resume_body = |ops: &MockSchedulerOps| {
            let _ = ops.now();
            let _ = ops.current_region();
            ops.stage_nba(cell, FourStateValue::Narrow(1), PROC_A);
            ops.commit_blocking_write(cell, FourStateValue::Narrow(2));
            ops.request_finish(0);
            ops.defer_io(SchedTime { ticks: 0 }, Region::Active, Box::new(|_ctx| {}));
            ops.register_region_callback(
                Region::Postponed,
                SchedTime { ticks: 0 },
                Box::new(NoopRegionCallback),
            );
        };
        resume_body(&ops);

        let calls = ops.calls.borrow();
        assert_eq!(calls.len(), 7, "the trait has exactly seven methods");
        assert_eq!(
            *calls,
            vec![
                MockOpsCall::Now,
                MockOpsCall::CurrentRegion,
                MockOpsCall::StageNba,
                MockOpsCall::CommitBlockingWrite,
                MockOpsCall::RequestFinish,
                MockOpsCall::DeferIo,
                MockOpsCall::RegisterRegionCallback,
            ],
            "each of the seven methods must appear exactly once, in call order"
        );
    }
}