Skip to main content

sv_sim_sim/sched/
notify.rs

1//! SCHED::NOTIFY — the NBA commit pass, `notify_change`, and changed-cell
2//! diffing (scheduler.md §3/§4; ARCHITECTURE §6.3; ledger SC-038, SC-039,
3//! SC-040, SC-041, SC-042).
4//!
5//! [`commit_nba`] is the SC-039 algorithm: drain the [`NbaLog`] once, apply
6//! the staged updates cell by cell in the log's own insertion `order` (so the
7//! committed value tracks push order, SC-039), write each update through
8//! [`SignalAccess`], and call [`ValueChangeSink::notify_change`] for every
9//! update that actually changed the stored value (SC-040/SC-041; LRM 4.9.4,
10//! 10.4.2: every value-changing update is an update event, so `q <= 1;
11//! q <= 0;` wakes an `@(q)` waiter even though `q` ends where it started) —
12//! never for a write whose value equals the cell's current one. A
13//! [`ChangeDelta`] is recorded for the cells whose select waiters need it
14//! (`ValueChangeSink::wants_delta`). This is one of this wave's two sanctioned production
15//! `.notify_change(` call sites (SC-042; the other is Task 8's
16//! `commit_blocking_write`; the crate-wide "exactly two call sites" proof is
17//! Task 22's, this task's own contribution is that this file has exactly
18//! one).
19
20use crate::sched::SchedTime;
21use crate::sched::nba::NbaLog;
22use crate::state::{
23    ChangeDelta, EdgeKind, FourStateValue, SignalAccess, StateCellId, ValueChangeSink,
24};
25use sv_sim_abi::{Bit4, Region};
26
27/// Extracts the single reference bit `infer_edge` classifies transitions on:
28/// bit 0 of the narrow word's `val`/`unk` planes (ARCHITECTURE §6.2's FIXED
29/// layout — `unk` always at bit 64 regardless of width), or bit 0 of the
30/// wide planes' LSB word ([`FourStateValue::Wide`]'s `aval`/`bval` word
31/// planes). Both decode through [`Bit4`]'s own documented conversions
32/// (`sv_sim_abi::bit4`), never a hand-rolled second decode.
33fn lsb_bit(value: &FourStateValue) -> Bit4 {
34    match value {
35        FourStateValue::Narrow(word) => {
36            let val = (word & 1) != 0;
37            let unk = ((word >> 64) & 1) != 0;
38            Bit4::from_val_unk(val, unk)
39        }
40        FourStateValue::Wide { aval, bval } => {
41            let a = aval.first().copied().unwrap_or(0) & 1 != 0;
42            let b = bval.first().copied().unwrap_or(0) & 1 != 0;
43            Bit4::from_aval_bval(a, b)
44        }
45    }
46}
47
48/// Classifies an old->new transition per IEEE 1800 edge-event semantics
49/// (LRM Table 9-1, §9.4.2). The rule is precise and asymmetric, *not* a
50/// "conservative union" of Posedge+Negedge for any transition touching X/Z:
51/// each X/Z-involving transition is assigned to exactly one specific edge
52/// (or none). `0->{1,X,Z}` is `Posedge` (X/Z read as "possibly became 1");
53/// `1->{0,X,Z}` is `Negedge` (X/Z read as "possibly became 0"); `X->0` and
54/// `Z->0` are `Negedge`; `X->1` and `Z->1` are `Posedge`; and the four
55/// "stayed unknown" transitions `X->X`, `X->Z`, `Z->X`, `Z->Z` are `None` —
56/// verified directly against iverilog 12 (`x->1` fires only `@(posedge)`,
57/// `1->x` only `@(negedge)`, `x->z`/`z->x` fire neither edge at all).
58/// `EdgeKind::BothEdges` is never produced by this table; that variant
59/// exists in the enum for the unrelated `edge` keyword / any-change case,
60/// not for X/Z transitions. Exhaustive over all 16 `(Bit4, Bit4)` pairs,
61/// written out rather than collapsed with a wildcard, so the truth table is
62/// legible inline as the task brief asks:
63///
64/// | old \ new | 0        | 1        | X         | Z         |
65/// |-----------|----------|----------|-----------|-----------|
66/// | 0         | None     | Posedge  | Posedge   | Posedge   |
67/// | 1         | Negedge  | None     | Negedge   | Negedge   |
68/// | X         | Negedge  | Posedge  | None      | None      |
69/// | Z         | Negedge  | Posedge  | None      | None      |
70pub fn infer_edge(old: &FourStateValue, new: &FourStateValue) -> EdgeKind {
71    edge_between(lsb_bit(old), lsb_bit(new))
72}
73
74/// [`infer_edge`]'s table on the two lsb bits themselves (the windowed
75/// huge-signal commit knows only bit 0, not the whole old value).
76pub fn edge_between(old: Bit4, new: Bit4) -> EdgeKind {
77    use Bit4::*;
78    match (old, new) {
79        (Zero, Zero) => EdgeKind::None,
80        (Zero, One) => EdgeKind::Posedge,
81        (Zero, X) => EdgeKind::Posedge,
82        (Zero, Z) => EdgeKind::Posedge,
83        (One, Zero) => EdgeKind::Negedge,
84        (One, One) => EdgeKind::None,
85        (One, X) => EdgeKind::Negedge,
86        (One, Z) => EdgeKind::Negedge,
87        (X, Zero) => EdgeKind::Negedge,
88        (X, One) => EdgeKind::Posedge,
89        (X, X) => EdgeKind::None,
90        (X, Z) => EdgeKind::None,
91        (Z, Zero) => EdgeKind::Negedge,
92        (Z, One) => EdgeKind::Posedge,
93        (Z, X) => EdgeKind::None,
94        (Z, Z) => EdgeKind::None,
95    }
96}
97
98/// The SC-039 NBA commit pass: `take()`s `log`, applies each cell's updates
99/// in staging order, writes each through `arena`, and calls
100/// `sink.notify_change(..)` for every update that actually changed the stored
101/// value (SC-040/SC-041). Returns the count of cells that changed.
102pub fn commit_nba(
103    log: &mut NbaLog,
104    arena: &mut dyn SignalAccess,
105    at: SchedTime,
106    sink: &mut dyn ValueChangeSink,
107) -> usize {
108    let entries = log.take();
109    let mut masked = log.take_masked();
110    // Class-property writes name no cell: land them in staging order, then
111    // commit the cell writes.
112    if masked.iter().any(|m| m.obj.is_some()) {
113        let (mut objs, rest): (Vec<_>, Vec<_>) = masked.into_iter().partition(|m| m.obj.is_some());
114        masked = rest;
115        objs.sort_unstable_by_key(|m| m.order);
116        for m in objs {
117            if let Some(w) = m.obj {
118                if w.class_event {
119                    // Wakes land in the engine's pending list, which it
120                    // applies right after this commit pass.
121                    super::class_event::trigger(w.h, at.ticks, &mut |req| {
122                        super::class_event::defer_wake(req);
123                    });
124                } else {
125                    w.commit();
126                }
127            }
128        }
129    }
130    if !masked.is_empty() {
131        return commit_nba_with_partial_writes(entries, masked, arena, at, sink);
132    }
133
134    // The log's insertion-order `order` is one strictly increasing counter
135    // over the whole timestep, whichever process staged an update (SC-039:
136    // "the committed value tracks push order, not `ProcessId` order"), so
137    // sorting by `(cell, order)` applies every cell's updates in the order
138    // they were staged.
139    //
140    // Each update is applied and notified on its own (LRM 4.9.4, 10.4.2:
141    // nonblocking updates take effect in order, and every value-changing
142    // update is an update event): `q <= 1; q <= 0;` from `q == 0` wakes an
143    // `@(q)` / `@(posedge q)` waiter even though `q` ends where it started.
144    // Wake-ups of one process are still deduplicated by the engine's queued
145    // gate, so a process wakes once per time step however many updates hit it.
146    let mut entries = entries;
147    entries.sort_by_key(|e| (e.cell, e.order));
148
149    let mut changed_count = 0usize;
150    let mut last_changed: Option<StateCellId> = None;
151    for entry in entries {
152        let cell = entry.cell;
153        let old = arena.read(cell);
154        let new_for_edge = entry.value.clone();
155        if arena.write(cell, entry.value) {
156            if sink.wants_delta(cell) {
157                sink.note_delta(cell, ChangeDelta::between(&old, &new_for_edge));
158            }
159            notify_nba_change(sink, cell, infer_edge(&old, &new_for_edge), at);
160            if last_changed != Some(cell) {
161                changed_count += 1;
162                last_changed = Some(cell);
163            }
164        }
165        for (cell, edge) in alias_edges(arena) {
166            notify_nba_change(sink, cell, edge, at);
167        }
168    }
169    changed_count
170}
171
172/// The changes of other names for the storage the last write touched
173/// (window aliases of ref ports, `state::alias`), with their edges.
174fn alias_edges(arena: &mut dyn SignalAccess) -> Vec<(StateCellId, EdgeKind)> {
175    arena
176        .take_alias_changes()
177        .into_iter()
178        .map(|c| {
179            let edge = if c.no_edge {
180                EdgeKind::None
181            } else {
182                infer_edge(&c.old, &c.new)
183            };
184            (c.cell, edge)
185        })
186        .collect()
187}
188
189/// The one NBA-region change notification (SC-042's sanctioned call site).
190fn notify_nba_change(
191    sink: &mut dyn ValueChangeSink,
192    cell: StateCellId,
193    edge: EdgeKind,
194    at: SchedTime,
195) {
196    sink.notify_change(cell, edge, at.to_abi_display(0), Region::Nba);
197}
198
199/// Merges `new` into `cur` on the bits set in `mask` (both value planes;
200/// the two encodings -- `Narrow`'s val/unk and `Wide`'s aval/bval -- are
201/// both per-bit planes, so the same mask applies to each).
202fn merge_masked(cur: &FourStateValue, new: &FourStateValue, mask: &[u64]) -> FourStateValue {
203    let pick = |c: u64, n: u64, m: u64| (c & !m) | (n & m);
204    match (cur, new) {
205        (FourStateValue::Narrow(c), FourStateValue::Narrow(n)) => {
206            let m = mask.first().copied().unwrap_or(0);
207            let val = pick(*c as u64, *n as u64, m);
208            let unk = pick((*c >> 64) as u64, (*n >> 64) as u64, m);
209            FourStateValue::Narrow(u128::from(val) | (u128::from(unk) << 64))
210        }
211        (
212            FourStateValue::Wide { aval: ca, bval: cb },
213            FourStateValue::Wide { aval: na, bval: nb },
214        ) => {
215            let words = |c: &[u64], n: &[u64]| -> Vec<u64> {
216                c.iter()
217                    .enumerate()
218                    .map(|(i, &cw)| {
219                        let nw = n.get(i).copied().unwrap_or(0);
220                        pick(cw, nw, mask.get(i).copied().unwrap_or(0))
221                    })
222                    .collect()
223            };
224            FourStateValue::Wide {
225                aval: words(ca, na),
226                bval: words(cb, nb),
227            }
228        }
229        // A memory row is stored in the wide planes whatever its width, so a
230        // <=64-bit staged value meets a `Wide` row: convert it (and the
231        // symmetric case) to the stored shape first.
232        (FourStateValue::Wide { aval, .. }, FourStateValue::Narrow(n)) => {
233            let (val, unk) = (*n as u64, (*n >> 64) as u64);
234            let mut na = vec![0u64; aval.len()];
235            let mut nb = vec![0u64; aval.len()];
236            if let (Some(a), Some(b)) = (na.first_mut(), nb.first_mut()) {
237                (*a, *b) = sv_sim_abi::bit4::narrow_word_to_wide(val, unk);
238            }
239            merge_masked(cur, &FourStateValue::Wide { aval: na, bval: nb }, mask)
240        }
241        (FourStateValue::Narrow(_), FourStateValue::Wide { aval, bval }) => {
242            let a = aval.first().copied().unwrap_or(0);
243            let b = bval.first().copied().unwrap_or(0);
244            let narrow = FourStateValue::Narrow(u128::from(a ^ b) | (u128::from(b) << 64));
245            merge_masked(cur, &narrow, mask)
246        }
247    }
248}
249
250/// `commit_nba` for a batch that includes partial (`q[3:0] <= v`) or
251/// memory-row (`mem[i] <= v`) writes: every write to one cell (or one
252/// memory row) is applied in staging order -- so `q <= 0; q[3] <= 1;`
253/// yields 8 -- then the cell is written once and notified once if it
254/// changed. Cells are processed in `StateCellId` order, as `commit_nba`'s
255/// last-write-wins path does.
256fn commit_nba_with_partial_writes(
257    entries: Vec<super::nba::NbaUpdate>,
258    masked: Vec<super::nba::MaskedNbaUpdate>,
259    arena: &mut dyn SignalAccess,
260    at: SchedTime,
261    sink: &mut dyn ValueChangeSink,
262) -> usize {
263    enum Op {
264        Whole(FourStateValue),
265        /// `(value, mask, window)`: a windowed op's `value` and `mask` span
266        /// only the plane words from `window` on.
267        Part(FourStateValue, Vec<u64>, Option<u32>),
268        /// A nonblocking event trigger (`->> e`): bump the event cell's
269        /// trigger count and stamp it with this commit's time.
270        Pulse,
271        /// A write of one element of the collection the cell holds: the
272        /// element's words and where it goes (`MaskedNbaUpdate::coll`).
273        Coll(Vec<u64>, super::nba::CollWrite),
274    }
275    /// A write target -- a whole cell, or one row of a memory cell.
276    type Target = (StateCellId, Option<u64>);
277    // Every staged operation, sorted by target and then staging order: the
278    // targets come out in the same `(cell, row)` order a sorted map would
279    // give, without a node allocation per operation.
280    let mut items: Vec<(Target, u32, Op)> = Vec::with_capacity(entries.len() + masked.len());
281    for e in entries {
282        items.push(((e.cell, None), e.order, Op::Whole(e.value)));
283    }
284    for m in masked {
285        let op = if m.pulse {
286            Op::Pulse
287        } else if let Some(c) = m.coll {
288            Op::Coll(m.mask, c)
289        } else {
290            Op::Part(m.value, m.mask, m.window)
291        };
292        items.push(((m.cell, m.row), m.order, op));
293    }
294    items.sort_unstable_by_key(|(target, order, _)| (*target, *order));
295
296    // One update event per value-changing operation, in cell order and then
297    // staging order (LRM 4.9.4: `v[9] <= 1; v[9] <= 0;` is two updates, each
298    // waking an `@(v[9])` waiter, though `v` ends unchanged). A memory row's
299    // update has no single edge, so it reports `EdgeKind::None`.
300    struct Update {
301        cell: StateCellId,
302        edge: EdgeKind,
303        delta: Option<ChangeDelta>,
304    }
305    let mut updates: Vec<Update> = Vec::new();
306    let mut changed_cells = 0usize;
307    let mut alias_notes: Vec<(StateCellId, EdgeKind)> = Vec::new();
308    let mut items = items.into_iter().peekable();
309    while let Some(((cell, row), _, first)) = items.next() {
310        let mut group = vec![first];
311        while let Some((_, _, op)) = items.next_if(|(t, _, _)| *t == (cell, row)) {
312            group.push(op);
313        }
314        let mark = updates.len();
315        let wants = sink.wants_delta(cell);
316        // Writes to a few bits of a huge vector: when every write to the
317        // cell is windowed, only the words they touch are read, merged,
318        // stored and compared (`O(window)`, not `O(N)`).
319        if row.is_none() && group.iter().all(|op| matches!(op, Op::Part(_, _, Some(_)))) {
320            // The words each write covers, merged into disjoint segments: a
321            // few writes far apart cost their own words, not the distance
322            // between them.
323            let mut spans: Vec<(usize, usize)> = group
324                .iter()
325                .filter_map(|op| match op {
326                    Op::Part(FourStateValue::Wide { aval, .. }, _, Some(w)) => {
327                        Some((*w as usize, *w as usize + aval.len()))
328                    }
329                    _ => None,
330                })
331                .collect();
332            spans.sort_unstable();
333            let mut segments: Vec<(usize, usize)> = Vec::new();
334            for (l, h) in spans {
335                match segments.last_mut() {
336                    Some(seg) if l <= seg.1 => seg.1 = seg.1.max(h),
337                    _ => segments.push((l, h)),
338                }
339            }
340            // Read every segment, apply the writes in staging order (each one
341            // is its own update event, with the words it covered), then store
342            // the segments back.
343            let mut segs: Vec<(usize, usize, Vec<u64>, Vec<u64>)> = segments
344                .iter()
345                .map(|&(lo, hi)| {
346                    let (a, b) = arena.read_wide_window(cell, lo, hi - lo);
347                    (lo, hi, a, b)
348                })
349                .collect();
350            for op in &group {
351                let Op::Part(FourStateValue::Wide { aval, bval }, mask, Some(w)) = op else {
352                    continue;
353                };
354                let w = *w as usize;
355                let Some((lo, _, a, b)) = segs
356                    .iter_mut()
357                    .find(|(lo, hi, _, _)| w >= *lo && w + mask.len() <= *hi)
358                else {
359                    continue;
360                };
361                let at = w - *lo;
362                let n = mask.len();
363                let (old_a, old_b) = (a[at..at + n].to_vec(), b[at..at + n].to_vec());
364                for (k, &m) in mask.iter().enumerate() {
365                    a[at + k] = (a[at + k] & !m) | (aval[k] & m);
366                    b[at + k] = (b[at + k] & !m) | (bval[k] & m);
367                }
368                if old_a[..] == a[at..at + n] && old_b[..] == b[at..at + n] {
369                    continue;
370                }
371                // Only the lsb decides an edge, and only an update covering
372                // word 0 can have changed it.
373                let edge = if w == 0 {
374                    edge_between(
375                        Bit4::from_aval_bval(old_a[0] & 1 != 0, old_b[0] & 1 != 0),
376                        Bit4::from_aval_bval(a[0] & 1 != 0, b[0] & 1 != 0),
377                    )
378                } else {
379                    EdgeKind::None
380                };
381                let delta = wants.then(|| ChangeDelta {
382                    lo_word: w,
383                    old_a,
384                    old_b,
385                    new_a: a[at..at + n].to_vec(),
386                    new_b: b[at..at + n].to_vec(),
387                });
388                updates.push(Update { cell, edge, delta });
389            }
390            let mut changed = false;
391            let mut forced = false;
392            for (lo, hi, a, b) in &segs {
393                if arena.write_wide_window(cell, *lo, a, b) {
394                    changed = true;
395                } else if updates.len() > mark {
396                    // Nothing stored: either the writes cancelled, or the cell
397                    // is forced and kept its value.
398                    let (ra, rb) = arena.read_wide_window(cell, *lo, hi - lo);
399                    forced |= ra != *a || rb != *b;
400                }
401            }
402            if forced {
403                updates.truncate(mark);
404            }
405            if changed || updates.len() > mark {
406                changed_cells += 1;
407            }
408            continue;
409        }
410        let old = match row {
411            Some(r) => arena.read_row(cell, r),
412            None => arena.read(cell),
413        };
414        let apply = |cur: &FourStateValue, op: Op| match op {
415            Op::Whole(v) => v,
416            Op::Pulse => triggered_event_value(cur, at),
417            // The handle is read now, so the write lands in the collection
418            // the variable holds at update time, and an out-of-range index
419            // is ignored there (`collections::set`); a copy-on-write set may
420            // hand back a new handle, which becomes the variable's value.
421            Op::Coll(elem, c) => {
422                let h = match cur {
423                    FourStateValue::Narrow(v) => *v as u64,
424                    FourStateValue::Wide { aval, .. } => aval.first().copied().unwrap_or(0),
425                };
426                let new = match c.key {
427                    Some(k) => crate::collections::store(&c.shape, h, k, &elem),
428                    None => crate::collections::set(&c.shape, h, c.index, &elem, c.queue),
429                };
430                match cur {
431                    FourStateValue::Narrow(_) => FourStateValue::Narrow(u128::from(new)),
432                    FourStateValue::Wide { aval, bval } => {
433                        let (mut a, b) = (aval.clone(), vec![0; bval.len()]);
434                        a[0] = new;
435                        FourStateValue::Wide { aval: a, bval: b }
436                    }
437                }
438            }
439            Op::Part(v, mask, None) => merge_masked(cur, &v, &mask),
440            Op::Part(v, mask, Some(w)) => {
441                // A windowed write meeting a whole-value one: widen it to the
442                // full value.
443                let (FourStateValue::Wide { aval, bval }, FourStateValue::Wide { aval: ca, .. }) =
444                    (&v, cur)
445                else {
446                    unreachable!("a windowed NBA targets a wide cell")
447                };
448                let n = ca.len();
449                let at = w as usize;
450                let (mut fa, mut fb, mut fm) = (vec![0u64; n], vec![0u64; n], vec![0u64; n]);
451                fa[at..at + aval.len()].copy_from_slice(aval);
452                fb[at..at + bval.len()].copy_from_slice(bval);
453                fm[at..at + mask.len()].copy_from_slice(&mask);
454                merge_masked(cur, &FourStateValue::Wide { aval: fa, bval: fb }, &fm)
455            }
456        };
457        let mut cur = old.clone();
458        for op in group {
459            let next = apply(&cur, op);
460            if !same_value(&cur, &next) {
461                let (edge, delta) = if row.is_some() {
462                    (EdgeKind::None, None)
463                } else {
464                    (
465                        infer_edge(&cur, &next),
466                        wants.then(|| ChangeDelta::between(&cur, &next)),
467                    )
468                };
469                updates.push(Update { cell, edge, delta });
470            }
471            cur = next;
472        }
473        let stored = match row {
474            Some(r) => arena.write_row(cell, r, cur.clone()),
475            None => arena.write(cell, cur.clone()),
476        };
477        if updates.len() > mark {
478            if !stored {
479                // Nothing stored: the writes cancelled, or the cell is forced.
480                let now = match row {
481                    Some(r) => arena.read_row(cell, r),
482                    None => arena.read(cell),
483                };
484                if !same_value(&now, &cur) {
485                    updates.truncate(mark);
486                }
487            }
488            if updates.len() > mark {
489                changed_cells += 1;
490            }
491        }
492        alias_notes.extend(alias_edges(arena));
493    }
494    for u in updates {
495        if let Some(d) = u.delta {
496            sink.note_delta(u.cell, d);
497        }
498        notify_nba_change(sink, u.cell, u.edge, at);
499    }
500    for (cell, edge) in alias_notes {
501        notify_nba_change(sink, cell, edge, at);
502    }
503    changed_cells
504}
505
506/// An event cell's value (`state::EVENT_CELL_WIDTH` bits, 2-state: the trigger
507/// count in the low 64 bits, `tick + 1` of the latest trigger in the high 64)
508/// after one more trigger at `at` -- what `-> e` computes in compiled code
509/// (`sv_sim_codegen::expr::lower_event_trigger`).
510fn triggered_event_value(cur: &FourStateValue, at: SchedTime) -> FourStateValue {
511    let count = match cur {
512        FourStateValue::Wide { aval, .. } => aval.first().copied().unwrap_or(0),
513        FourStateValue::Narrow(n) => *n as u64,
514    };
515    FourStateValue::Wide {
516        aval: vec![count.wrapping_add(1), at.ticks.wrapping_add(1)],
517        bval: vec![0, 0],
518    }
519}
520
521/// Whether two stored values are bit-identical in both planes.
522fn same_value(a: &FourStateValue, b: &FourStateValue) -> bool {
523    match (a, b) {
524        (FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => x == y,
525        (
526            FourStateValue::Wide { aval: a1, bval: b1 },
527            FourStateValue::Wide { aval: a2, bval: b2 },
528        ) => a1 == a2 && b1 == b2,
529        _ => a.planes() == b.planes(),
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::super::nba::NbaLog;
536    use crate::sched::{ProcessId, SchedTime};
537    use crate::state::{
538        EdgeKind, FourStateValue, ListenerRole, SignalAccess, StateCellId, ValueChangeSink,
539    };
540    use std::cell::RefCell;
541    use std::collections::{HashMap, HashSet};
542    use std::rc::Rc;
543    use sv_sim_abi::{Region, SimTime};
544
545    // `FakeArena` per the task brief: one shared piece of state (a
546    // `HashMap<StateCellId, FourStateValue>` plus a notify-call log) that
547    // backs both a `SignalAccess` view and a `ValueChangeSink` view. Split
548    // into two thin handle types sharing one `Rc<RefCell<..>>` rather than one
549    // type implementing both traits directly, because `commit_nba` takes
550    // `arena: &mut dyn SignalAccess` and `sink: &mut dyn ValueChangeSink` as
551    // two *simultaneous* `&mut` parameters — two `&mut` borrows of the exact
552    // same value in one call is rejected by the borrow checker regardless of
553    // which fields each trait impl touches, so the shared state has to live
554    // behind interior mutability and be reached through two distinct handle
555    // values instead. This is a mechanical adaptation only: both handles see
556    // and mutate the same underlying cells/notifications, so the test's
557    // observable behavior (arena state + notify log) is exactly what a single
558    // dual-trait `FakeArena` would produce.
559    struct FakeArenaState {
560        cells: HashMap<StateCellId, FourStateValue>,
561        notifications: Vec<(StateCellId, EdgeKind, SimTime, Region)>,
562        deltas: Vec<super::ChangeDelta>,
563    }
564
565    #[derive(Clone)]
566    struct FakeArena(Rc<RefCell<FakeArenaState>>);
567
568    struct ArenaHandle(Rc<RefCell<FakeArenaState>>);
569    struct SinkHandle(Rc<RefCell<FakeArenaState>>);
570
571    fn four_state_eq(a: &FourStateValue, b: &FourStateValue) -> bool {
572        match (a, b) {
573            (FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => x == y,
574            (
575                FourStateValue::Wide { aval: a1, bval: b1 },
576                FourStateValue::Wide { aval: a2, bval: b2 },
577            ) => a1 == a2 && b1 == b2,
578            _ => false,
579        }
580    }
581
582    impl FakeArena {
583        fn new() -> Self {
584            Self(Rc::new(RefCell::new(FakeArenaState {
585                cells: HashMap::new(),
586                notifications: Vec::new(),
587                deltas: Vec::new(),
588            })))
589        }
590
591        fn seed(&self, cell: StateCellId, value: FourStateValue) {
592            self.0.borrow_mut().cells.insert(cell, value);
593        }
594
595        fn read_cell(&self, cell: StateCellId) -> FourStateValue {
596            self.0
597                .borrow()
598                .cells
599                .get(&cell)
600                .cloned()
601                .unwrap_or(FourStateValue::Narrow(0))
602        }
603
604        fn notifications(&self) -> Vec<(StateCellId, EdgeKind, SimTime, Region)> {
605            self.0.borrow().notifications.clone()
606        }
607
608        fn arena_handle(&self) -> ArenaHandle {
609            ArenaHandle(self.0.clone())
610        }
611
612        fn sink_handle(&self) -> SinkHandle {
613            SinkHandle(self.0.clone())
614        }
615    }
616
617    impl SignalAccess for ArenaHandle {
618        fn read(&self, cell: StateCellId) -> FourStateValue {
619            self.0
620                .borrow()
621                .cells
622                .get(&cell)
623                .cloned()
624                .unwrap_or(FourStateValue::Narrow(0))
625        }
626
627        fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
628            let mut state = self.0.borrow_mut();
629            let changed = match state.cells.get(&cell) {
630                Some(existing) => !four_state_eq(existing, &value),
631                None => true,
632            };
633            state.cells.insert(cell, value);
634            changed
635        }
636
637        fn subscribe(
638            &mut self,
639            _cell: StateCellId,
640            _role: ListenerRole,
641            _sink: Box<dyn ValueChangeSink + Send>,
642        ) {
643            // Not exercised by Task 7's tests — Task 9 owns SensitivityTable
644            // wiring.
645        }
646    }
647
648    impl ValueChangeSink for SinkHandle {
649        fn notify_change(
650            &mut self,
651            cell: StateCellId,
652            edge: EdgeKind,
653            at: SimTime,
654            region: Region,
655        ) {
656            self.0
657                .borrow_mut()
658                .notifications
659                .push((cell, edge, at, region));
660        }
661
662        fn wants_delta(&self, _cell: StateCellId) -> bool {
663            true
664        }
665
666        fn note_delta(&mut self, _cell: StateCellId, delta: super::ChangeDelta) {
667            self.0.borrow_mut().deltas.push(delta);
668        }
669    }
670
671    const PROC_A: ProcessId = 10;
672    const PROC_B: ProcessId = 5;
673
674    #[test]
675    fn two_nba_writes_same_cell_deterministically_commit_the_last_logged_one() {
676        let cell = StateCellId(1);
677
678        // A (ProcessId 10, numerically larger) pushes first (order 0), B
679        // (ProcessId 5, numerically smaller) pushes second (order 1). If the
680        // tiebreak were numeric ProcessId order rather than push order, A
681        // (the larger id) would win; the spec says push order wins, so B
682        // (pushed last) must win here.
683        let fake1 = FakeArena::new();
684        let mut arena1 = fake1.arena_handle();
685        let mut sink1 = fake1.sink_handle();
686        let mut log1 = NbaLog::new();
687        log1.stage(cell, FourStateValue::Narrow(1), PROC_A);
688        log1.stage(cell, FourStateValue::Narrow(2), PROC_B);
689        super::commit_nba(&mut log1, &mut arena1, SchedTime { ticks: 0 }, &mut sink1);
690        match fake1.read_cell(cell) {
691            FourStateValue::Narrow(v) => assert_eq!(v, 2, "last-pushed write (B) must win"),
692            FourStateValue::Wide { .. } => panic!("expected Narrow"),
693        }
694
695        // Swap the push order: B first, A second. The committed value must
696        // track the new push order (A wins now), proving the tiebreak is
697        // push/insertion order, not ProcessId numeric order (which is
698        // unchanged between the two sub-tests).
699        let fake2 = FakeArena::new();
700        let mut arena2 = fake2.arena_handle();
701        let mut sink2 = fake2.sink_handle();
702        let mut log2 = NbaLog::new();
703        log2.stage(cell, FourStateValue::Narrow(2), PROC_B);
704        log2.stage(cell, FourStateValue::Narrow(1), PROC_A);
705        super::commit_nba(&mut log2, &mut arena2, SchedTime { ticks: 0 }, &mut sink2);
706        match fake2.read_cell(cell) {
707            FourStateValue::Narrow(v) => assert_eq!(v, 1, "last-pushed write (A) must win"),
708            FourStateValue::Wide { .. } => panic!("expected Narrow"),
709        }
710    }
711
712    #[test]
713    fn cancelling_nbas_are_two_update_events_in_staging_order() {
714        // LRM 4.9.4 / 10.4.2: `q <= 1; q <= 0;` applies in order and each
715        // value-changing update is an update event, though `q` ends where it
716        // began.
717        let cell = StateCellId(3);
718        let fake = FakeArena::new();
719        fake.seed(cell, FourStateValue::Narrow(0));
720        let mut arena = fake.arena_handle();
721        let mut sink = fake.sink_handle();
722        let mut log = NbaLog::new();
723        log.stage(cell, FourStateValue::Narrow(1), PROC_A);
724        log.stage(cell, FourStateValue::Narrow(0), PROC_A);
725        super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
726        let edges: Vec<EdgeKind> = fake.notifications().into_iter().map(|n| n.1).collect();
727        assert_eq!(edges, vec![EdgeKind::Posedge, EdgeKind::Negedge]);
728        match fake.read_cell(cell) {
729            FourStateValue::Narrow(v) => assert_eq!(v, 0),
730            FourStateValue::Wide { .. } => panic!("expected Narrow"),
731        }
732    }
733
734    #[test]
735    fn cancelling_partial_nbas_are_two_update_events_each_with_its_bits() {
736        // `v[9] <= 1; v[9] <= 0;` on a 128-bit vector: two events, whose
737        // deltas show bit 9 rising and then falling.
738        let cell = StateCellId(4);
739        let fake = FakeArena::new();
740        fake.seed(
741            cell,
742            FourStateValue::Wide {
743                aval: vec![0, 0],
744                bval: vec![0, 0],
745            },
746        );
747        let mut arena = fake.arena_handle();
748        let mut sink = fake.sink_handle();
749        let mut log = NbaLog::new();
750        for bit in [1u64, 0] {
751            log.stage_masked(
752                cell,
753                None,
754                FourStateValue::Wide {
755                    aval: vec![bit << 9, 0],
756                    bval: vec![0, 0],
757                },
758                vec![1 << 9, 0],
759                None,
760                PROC_A,
761            );
762        }
763        super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
764        let n = fake.notifications();
765        assert_eq!(n.len(), 2, "one update event per value-changing write");
766        let deltas = fake.0.borrow().deltas.clone();
767        assert_eq!(deltas.len(), 2);
768        assert_eq!(deltas[0].bit(9), Some(((false, false), (true, false))));
769        assert_eq!(deltas[1].bit(9), Some(((true, false), (false, false))));
770    }
771
772    #[test]
773    fn a_staged_pulse_triggers_the_event_cell_in_the_nba_region() {
774        // LRM 15.5.2: `->> e` (twice) bumps the event's trigger count and
775        // stamps it with the commit's `tick + 1`, notifying once per trigger
776        // from the NBA region, in order with a data write staged before it.
777        let ev = StateCellId(6);
778        let data = StateCellId(7);
779        let fake = FakeArena::new();
780        fake.seed(
781            ev,
782            FourStateValue::Wide {
783                aval: vec![3, 4],
784                bval: vec![0, 0],
785            },
786        );
787        fake.seed(data, FourStateValue::Narrow(0));
788        let mut arena = fake.arena_handle();
789        let mut sink = fake.sink_handle();
790        let mut log = NbaLog::new();
791        log.stage(data, FourStateValue::Narrow(1), PROC_A);
792        log.stage_pulse(ev, None, PROC_A);
793        log.stage_pulse(ev, None, PROC_B);
794        super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 9 }, &mut sink);
795        match fake.read_cell(ev) {
796            FourStateValue::Wide { aval, bval } => {
797                assert_eq!(aval, vec![5, 10], "count 3 -> 5, stamped tick 9 + 1");
798                assert_eq!(bval, vec![0, 0]);
799            }
800            FourStateValue::Narrow(_) => panic!("an event cell is wide"),
801        }
802        let n = fake.notifications();
803        assert_eq!(n.iter().filter(|x| x.0 == ev).count(), 2);
804        assert!(n.iter().all(|x| x.3 == Region::Nba));
805        assert_eq!(n.iter().filter(|x| x.0 == data).count(), 1);
806    }
807
808    #[test]
809    fn unchanged_commit_produces_no_notify_change_call() {
810        let cell = StateCellId(2);
811        let fake = FakeArena::new();
812        fake.seed(cell, FourStateValue::Narrow(5));
813        let mut arena = fake.arena_handle();
814        let mut sink = fake.sink_handle();
815
816        let mut log = NbaLog::new();
817        log.stage(cell, FourStateValue::Narrow(5), PROC_A);
818
819        let changed = super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 3 }, &mut sink);
820
821        assert_eq!(changed, 0);
822        assert!(
823            fake.notifications().is_empty(),
824            "an NBA write equal to the current value must not call notify_change"
825        );
826    }
827
828    #[test]
829    fn changed_commit_calls_notify_change_exactly_once_per_changed_cell() {
830        let cell_changed_1 = StateCellId(10);
831        let cell_changed_2 = StateCellId(11);
832        let cell_unchanged = StateCellId(12);
833
834        let fake = FakeArena::new();
835        fake.seed(cell_changed_1, FourStateValue::Narrow(0));
836        fake.seed(cell_changed_2, FourStateValue::Narrow(0));
837        fake.seed(cell_unchanged, FourStateValue::Narrow(7));
838        let mut arena = fake.arena_handle();
839        let mut sink = fake.sink_handle();
840
841        let mut log = NbaLog::new();
842        log.stage(cell_changed_1, FourStateValue::Narrow(1), PROC_A);
843        log.stage(cell_changed_2, FourStateValue::Narrow(1), PROC_A);
844        log.stage(cell_unchanged, FourStateValue::Narrow(7), PROC_A);
845
846        let changed = super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
847
848        assert_eq!(changed, 2);
849        let notified: Vec<StateCellId> = fake.notifications().into_iter().map(|n| n.0).collect();
850        assert_eq!(notified.len(), 2, "no duplicate notify_change calls");
851        let notified_set: HashSet<StateCellId> = notified.into_iter().collect();
852        assert!(notified_set.contains(&cell_changed_1));
853        assert!(notified_set.contains(&cell_changed_2));
854        assert!(!notified_set.contains(&cell_unchanged));
855    }
856
857    #[test]
858    fn changed_cell_diffing_is_the_sole_wakeup_source() {
859        let cell_a = StateCellId(20);
860        let cell_b = StateCellId(21);
861        let cell_c = StateCellId(22);
862        let cell_d = StateCellId(23);
863
864        let fake = FakeArena::new();
865        fake.seed(cell_a, FourStateValue::Narrow(0));
866        fake.seed(cell_b, FourStateValue::Narrow(9));
867        fake.seed(cell_c, FourStateValue::Narrow(0));
868        fake.seed(cell_d, FourStateValue::Narrow(4));
869        let mut arena = fake.arena_handle();
870        let mut sink = fake.sink_handle();
871
872        let mut log = NbaLog::new();
873        // a and c actually change; b and d are staged with their current
874        // value and must not change.
875        log.stage(cell_a, FourStateValue::Narrow(1), PROC_A);
876        log.stage(cell_b, FourStateValue::Narrow(9), PROC_A);
877        log.stage(cell_c, FourStateValue::Narrow(2), PROC_A);
878        log.stage(cell_d, FourStateValue::Narrow(4), PROC_A);
879
880        super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
881
882        let notified_set: HashSet<StateCellId> =
883            fake.notifications().into_iter().map(|n| n.0).collect();
884        let expected: HashSet<StateCellId> = [cell_a, cell_c].into_iter().collect();
885        // Exact-equality (not a subset check): a buggy "notify everything
886        // staged" implementation would produce {a, b, c, d} here, which
887        // fails this assertion.
888        assert_eq!(notified_set, expected);
889    }
890
891    #[test]
892    fn multi_bit_change_with_unchanged_bit0_still_wakes_an_any_change_static_entry() {
893        // Regression for the review finding on SensitivityTable::matches:
894        // infer_edge only classifies bit 0 of the old/new value (Table 9-1's
895        // scalar posedge/negedge rule), so a real, multi-bit change that
896        // happens to leave bit 0 fixed (Narrow(0b10) -> Narrow(0b110): bit 0
897        // is 0 in both) is a genuine `SignalAccess::write` change but
898        // `infer_edge` classifies it as `EdgeKind::None`. An always_comb-style
899        // "any change" Static entry (SC-046) must still be woken by this,
900        // through the real `commit_nba` -> `notify_change` -> `matches`
901        // pipeline, not a hand-picked call into `matches` directly.
902        use crate::sched::StaticSensitivity;
903        use crate::sched::sensitivity::{SensitivityLookupSink, SensitivityTable};
904
905        let cell = StateCellId(30);
906        let fake = FakeArena::new();
907        fake.seed(cell, FourStateValue::Narrow(0b10));
908        let mut arena = fake.arena_handle();
909
910        let mut table = SensitivityTable::new();
911        const PROC_COMB: ProcessId = 99;
912        table.install_static(
913            &StaticSensitivity {
914                edges: vec![(cell, None)],
915            },
916            PROC_COMB,
917        );
918        let mut woken = Vec::new();
919        let mut sink = SensitivityLookupSink::new(&table, &mut woken);
920
921        let mut log = NbaLog::new();
922        log.stage(cell, FourStateValue::Narrow(0b110), PROC_A);
923
924        let changed = super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
925
926        assert_eq!(changed, 1, "Narrow(0b10) -> Narrow(0b110) is a real change");
927        assert_eq!(
928            woken,
929            vec![PROC_COMB],
930            "an any-change Static entry must fire on a real multi-bit change even when bit 0 (infer_edge's only input) doesn't toggle"
931        );
932    }
933
934    #[test]
935    fn infer_edge_covers_the_four_state_transition_table() {
936        use sv_sim_abi::Bit4;
937
938        fn bit_value(b: Bit4) -> FourStateValue {
939            let (val, unk) = b.to_val_unk();
940            FourStateValue::Narrow((val as u128) | ((unk as u128) << 64))
941        }
942
943        let cases: &[(Bit4, Bit4, EdgeKind)] = &[
944            (Bit4::Zero, Bit4::Zero, EdgeKind::None),
945            (Bit4::One, Bit4::One, EdgeKind::None),
946            (Bit4::X, Bit4::X, EdgeKind::None),
947            (Bit4::Z, Bit4::Z, EdgeKind::None),
948            (Bit4::Zero, Bit4::One, EdgeKind::Posedge),
949            (Bit4::One, Bit4::Zero, EdgeKind::Negedge),
950            // LRM Table 9-1 (§9.4.2): X/Z-involving transitions are
951            // asymmetric, each landing on exactly one specific edge (or
952            // none) — never `BothEdges`. Verified against iverilog 12.
953            (Bit4::Zero, Bit4::X, EdgeKind::Posedge),
954            (Bit4::X, Bit4::One, EdgeKind::Posedge),
955            (Bit4::Z, Bit4::One, EdgeKind::Posedge),
956            (Bit4::Zero, Bit4::Z, EdgeKind::Posedge),
957            (Bit4::One, Bit4::X, EdgeKind::Negedge),
958            (Bit4::One, Bit4::Z, EdgeKind::Negedge),
959            (Bit4::X, Bit4::Zero, EdgeKind::Negedge),
960            (Bit4::Z, Bit4::Zero, EdgeKind::Negedge),
961            // The four "stayed unknown" transitions produce no edge event
962            // at all per the LRM — a buggy "any X/Z touch = BothEdges"
963            // implementation (the bug this regression guards against)
964            // would report `BothEdges` here instead of `None`.
965            (Bit4::X, Bit4::Z, EdgeKind::None),
966            (Bit4::Z, Bit4::X, EdgeKind::None),
967        ];
968
969        for (old, new, expected) in cases {
970            let old_v = bit_value(*old);
971            let new_v = bit_value(*new);
972            let got = super::infer_edge(&old_v, &new_v);
973            assert_eq!(
974                got, *expected,
975                "infer_edge({:?} -> {:?}) should be {:?}, got {:?}",
976                old, new, expected, got
977            );
978        }
979    }
980
981    #[test]
982    fn nba_write_lowering_shapes_both_append_to_the_log() {
983        // (a) simulates lowering a `ProceduralAssignStatement`-style
984        // nonblocking write (`x <= expr;`).
985        let mut log_a = NbaLog::new();
986        log_a.stage(StateCellId(9), FourStateValue::Narrow(42), 3);
987        let from_procedural_assign = log_a.take();
988
989        // (b) simulates lowering an `Assignment` expression with
990        // `isNonBlocking()==true` nested inside an `ExpressionStatement`.
991        let mut log_b = NbaLog::new();
992        log_b.stage(StateCellId(9), FourStateValue::Narrow(42), 3);
993        let from_expression_statement = log_b.take();
994
995        assert_eq!(from_procedural_assign.len(), 1);
996        assert_eq!(from_expression_statement.len(), 1);
997
998        let a = &from_procedural_assign[0];
999        let b = &from_expression_statement[0];
1000        // Both AST shapes must lower to representationally identical
1001        // NbaUpdate values — the log format does not (and must not)
1002        // distinguish which AST shape produced an entry.
1003        assert_eq!(a.cell, b.cell);
1004        assert_eq!(a.source, b.source);
1005        assert_eq!(a.order, b.order);
1006        match (&a.value, &b.value) {
1007            (FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => assert_eq!(x, y),
1008            _ => panic!("expected Narrow"),
1009        }
1010    }
1011}