use crate::sched::SchedTime;
use crate::sched::nba::NbaLog;
use crate::state::{
ChangeDelta, EdgeKind, FourStateValue, SignalAccess, StateCellId, ValueChangeSink,
};
use sv_sim_abi::{Bit4, Region};
fn lsb_bit(value: &FourStateValue) -> Bit4 {
match value {
FourStateValue::Narrow(word) => {
let val = (word & 1) != 0;
let unk = ((word >> 64) & 1) != 0;
Bit4::from_val_unk(val, unk)
}
FourStateValue::Wide { aval, bval } => {
let a = aval.first().copied().unwrap_or(0) & 1 != 0;
let b = bval.first().copied().unwrap_or(0) & 1 != 0;
Bit4::from_aval_bval(a, b)
}
}
}
pub fn infer_edge(old: &FourStateValue, new: &FourStateValue) -> EdgeKind {
edge_between(lsb_bit(old), lsb_bit(new))
}
pub fn edge_between(old: Bit4, new: Bit4) -> EdgeKind {
use Bit4::*;
match (old, new) {
(Zero, Zero) => EdgeKind::None,
(Zero, One) => EdgeKind::Posedge,
(Zero, X) => EdgeKind::Posedge,
(Zero, Z) => EdgeKind::Posedge,
(One, Zero) => EdgeKind::Negedge,
(One, One) => EdgeKind::None,
(One, X) => EdgeKind::Negedge,
(One, Z) => EdgeKind::Negedge,
(X, Zero) => EdgeKind::Negedge,
(X, One) => EdgeKind::Posedge,
(X, X) => EdgeKind::None,
(X, Z) => EdgeKind::None,
(Z, Zero) => EdgeKind::Negedge,
(Z, One) => EdgeKind::Posedge,
(Z, X) => EdgeKind::None,
(Z, Z) => EdgeKind::None,
}
}
pub fn commit_nba(
log: &mut NbaLog,
arena: &mut dyn SignalAccess,
at: SchedTime,
sink: &mut dyn ValueChangeSink,
) -> usize {
let entries = log.take();
let mut masked = log.take_masked();
if masked.iter().any(|m| m.obj.is_some()) {
let (mut objs, rest): (Vec<_>, Vec<_>) = masked.into_iter().partition(|m| m.obj.is_some());
masked = rest;
objs.sort_unstable_by_key(|m| m.order);
for m in objs {
if let Some(w) = m.obj {
if w.class_event {
super::class_event::trigger(w.h, at.ticks, &mut |req| {
super::class_event::defer_wake(req);
});
} else {
w.commit();
}
}
}
}
if !masked.is_empty() {
return commit_nba_with_partial_writes(entries, masked, arena, at, sink);
}
let mut entries = entries;
entries.sort_by_key(|e| (e.cell, e.order));
let mut changed_count = 0usize;
let mut last_changed: Option<StateCellId> = None;
for entry in entries {
let cell = entry.cell;
let old = arena.read(cell);
let new_for_edge = entry.value.clone();
if arena.write(cell, entry.value) {
if sink.wants_delta(cell) {
sink.note_delta(cell, ChangeDelta::between(&old, &new_for_edge));
}
notify_nba_change(sink, cell, infer_edge(&old, &new_for_edge), at);
if last_changed != Some(cell) {
changed_count += 1;
last_changed = Some(cell);
}
}
for (cell, edge) in alias_edges(arena) {
notify_nba_change(sink, cell, edge, at);
}
}
changed_count
}
fn alias_edges(arena: &mut dyn SignalAccess) -> Vec<(StateCellId, EdgeKind)> {
arena
.take_alias_changes()
.into_iter()
.map(|c| {
let edge = if c.no_edge {
EdgeKind::None
} else {
infer_edge(&c.old, &c.new)
};
(c.cell, edge)
})
.collect()
}
fn notify_nba_change(
sink: &mut dyn ValueChangeSink,
cell: StateCellId,
edge: EdgeKind,
at: SchedTime,
) {
sink.notify_change(cell, edge, at.to_abi_display(0), Region::Nba);
}
fn merge_masked(cur: &FourStateValue, new: &FourStateValue, mask: &[u64]) -> FourStateValue {
let pick = |c: u64, n: u64, m: u64| (c & !m) | (n & m);
match (cur, new) {
(FourStateValue::Narrow(c), FourStateValue::Narrow(n)) => {
let m = mask.first().copied().unwrap_or(0);
let val = pick(*c as u64, *n as u64, m);
let unk = pick((*c >> 64) as u64, (*n >> 64) as u64, m);
FourStateValue::Narrow(u128::from(val) | (u128::from(unk) << 64))
}
(
FourStateValue::Wide { aval: ca, bval: cb },
FourStateValue::Wide { aval: na, bval: nb },
) => {
let words = |c: &[u64], n: &[u64]| -> Vec<u64> {
c.iter()
.enumerate()
.map(|(i, &cw)| {
let nw = n.get(i).copied().unwrap_or(0);
pick(cw, nw, mask.get(i).copied().unwrap_or(0))
})
.collect()
};
FourStateValue::Wide {
aval: words(ca, na),
bval: words(cb, nb),
}
}
(FourStateValue::Wide { aval, .. }, FourStateValue::Narrow(n)) => {
let (val, unk) = (*n as u64, (*n >> 64) as u64);
let mut na = vec![0u64; aval.len()];
let mut nb = vec![0u64; aval.len()];
if let (Some(a), Some(b)) = (na.first_mut(), nb.first_mut()) {
(*a, *b) = sv_sim_abi::bit4::narrow_word_to_wide(val, unk);
}
merge_masked(cur, &FourStateValue::Wide { aval: na, bval: nb }, mask)
}
(FourStateValue::Narrow(_), FourStateValue::Wide { aval, bval }) => {
let a = aval.first().copied().unwrap_or(0);
let b = bval.first().copied().unwrap_or(0);
let narrow = FourStateValue::Narrow(u128::from(a ^ b) | (u128::from(b) << 64));
merge_masked(cur, &narrow, mask)
}
}
}
fn commit_nba_with_partial_writes(
entries: Vec<super::nba::NbaUpdate>,
masked: Vec<super::nba::MaskedNbaUpdate>,
arena: &mut dyn SignalAccess,
at: SchedTime,
sink: &mut dyn ValueChangeSink,
) -> usize {
enum Op {
Whole(FourStateValue),
Part(FourStateValue, Vec<u64>, Option<u32>),
Pulse,
Coll(Vec<u64>, super::nba::CollWrite),
}
type Target = (StateCellId, Option<u64>);
let mut items: Vec<(Target, u32, Op)> = Vec::with_capacity(entries.len() + masked.len());
for e in entries {
items.push(((e.cell, None), e.order, Op::Whole(e.value)));
}
for m in masked {
let op = if m.pulse {
Op::Pulse
} else if let Some(c) = m.coll {
Op::Coll(m.mask, c)
} else {
Op::Part(m.value, m.mask, m.window)
};
items.push(((m.cell, m.row), m.order, op));
}
items.sort_unstable_by_key(|(target, order, _)| (*target, *order));
struct Update {
cell: StateCellId,
edge: EdgeKind,
delta: Option<ChangeDelta>,
}
let mut updates: Vec<Update> = Vec::new();
let mut changed_cells = 0usize;
let mut alias_notes: Vec<(StateCellId, EdgeKind)> = Vec::new();
let mut items = items.into_iter().peekable();
while let Some(((cell, row), _, first)) = items.next() {
let mut group = vec![first];
while let Some((_, _, op)) = items.next_if(|(t, _, _)| *t == (cell, row)) {
group.push(op);
}
let mark = updates.len();
let wants = sink.wants_delta(cell);
if row.is_none() && group.iter().all(|op| matches!(op, Op::Part(_, _, Some(_)))) {
let mut spans: Vec<(usize, usize)> = group
.iter()
.filter_map(|op| match op {
Op::Part(FourStateValue::Wide { aval, .. }, _, Some(w)) => {
Some((*w as usize, *w as usize + aval.len()))
}
_ => None,
})
.collect();
spans.sort_unstable();
let mut segments: Vec<(usize, usize)> = Vec::new();
for (l, h) in spans {
match segments.last_mut() {
Some(seg) if l <= seg.1 => seg.1 = seg.1.max(h),
_ => segments.push((l, h)),
}
}
let mut segs: Vec<(usize, usize, Vec<u64>, Vec<u64>)> = segments
.iter()
.map(|&(lo, hi)| {
let (a, b) = arena.read_wide_window(cell, lo, hi - lo);
(lo, hi, a, b)
})
.collect();
for op in &group {
let Op::Part(FourStateValue::Wide { aval, bval }, mask, Some(w)) = op else {
continue;
};
let w = *w as usize;
let Some((lo, _, a, b)) = segs
.iter_mut()
.find(|(lo, hi, _, _)| w >= *lo && w + mask.len() <= *hi)
else {
continue;
};
let at = w - *lo;
let n = mask.len();
let (old_a, old_b) = (a[at..at + n].to_vec(), b[at..at + n].to_vec());
for (k, &m) in mask.iter().enumerate() {
a[at + k] = (a[at + k] & !m) | (aval[k] & m);
b[at + k] = (b[at + k] & !m) | (bval[k] & m);
}
if old_a[..] == a[at..at + n] && old_b[..] == b[at..at + n] {
continue;
}
let edge = if w == 0 {
edge_between(
Bit4::from_aval_bval(old_a[0] & 1 != 0, old_b[0] & 1 != 0),
Bit4::from_aval_bval(a[0] & 1 != 0, b[0] & 1 != 0),
)
} else {
EdgeKind::None
};
let delta = wants.then(|| ChangeDelta {
lo_word: w,
old_a,
old_b,
new_a: a[at..at + n].to_vec(),
new_b: b[at..at + n].to_vec(),
});
updates.push(Update { cell, edge, delta });
}
let mut changed = false;
let mut forced = false;
for (lo, hi, a, b) in &segs {
if arena.write_wide_window(cell, *lo, a, b) {
changed = true;
} else if updates.len() > mark {
let (ra, rb) = arena.read_wide_window(cell, *lo, hi - lo);
forced |= ra != *a || rb != *b;
}
}
if forced {
updates.truncate(mark);
}
if changed || updates.len() > mark {
changed_cells += 1;
}
continue;
}
let old = match row {
Some(r) => arena.read_row(cell, r),
None => arena.read(cell),
};
let apply = |cur: &FourStateValue, op: Op| match op {
Op::Whole(v) => v,
Op::Pulse => triggered_event_value(cur, at),
Op::Coll(elem, c) => {
let h = match cur {
FourStateValue::Narrow(v) => *v as u64,
FourStateValue::Wide { aval, .. } => aval.first().copied().unwrap_or(0),
};
let new = match c.key {
Some(k) => crate::collections::store(&c.shape, h, k, &elem),
None => crate::collections::set(&c.shape, h, c.index, &elem, c.queue),
};
match cur {
FourStateValue::Narrow(_) => FourStateValue::Narrow(u128::from(new)),
FourStateValue::Wide { aval, bval } => {
let (mut a, b) = (aval.clone(), vec![0; bval.len()]);
a[0] = new;
FourStateValue::Wide { aval: a, bval: b }
}
}
}
Op::Part(v, mask, None) => merge_masked(cur, &v, &mask),
Op::Part(v, mask, Some(w)) => {
let (FourStateValue::Wide { aval, bval }, FourStateValue::Wide { aval: ca, .. }) =
(&v, cur)
else {
unreachable!("a windowed NBA targets a wide cell")
};
let n = ca.len();
let at = w as usize;
let (mut fa, mut fb, mut fm) = (vec![0u64; n], vec![0u64; n], vec![0u64; n]);
fa[at..at + aval.len()].copy_from_slice(aval);
fb[at..at + bval.len()].copy_from_slice(bval);
fm[at..at + mask.len()].copy_from_slice(&mask);
merge_masked(cur, &FourStateValue::Wide { aval: fa, bval: fb }, &fm)
}
};
let mut cur = old.clone();
for op in group {
let next = apply(&cur, op);
if !same_value(&cur, &next) {
let (edge, delta) = if row.is_some() {
(EdgeKind::None, None)
} else {
(
infer_edge(&cur, &next),
wants.then(|| ChangeDelta::between(&cur, &next)),
)
};
updates.push(Update { cell, edge, delta });
}
cur = next;
}
let stored = match row {
Some(r) => arena.write_row(cell, r, cur.clone()),
None => arena.write(cell, cur.clone()),
};
if updates.len() > mark {
if !stored {
let now = match row {
Some(r) => arena.read_row(cell, r),
None => arena.read(cell),
};
if !same_value(&now, &cur) {
updates.truncate(mark);
}
}
if updates.len() > mark {
changed_cells += 1;
}
}
alias_notes.extend(alias_edges(arena));
}
for u in updates {
if let Some(d) = u.delta {
sink.note_delta(u.cell, d);
}
notify_nba_change(sink, u.cell, u.edge, at);
}
for (cell, edge) in alias_notes {
notify_nba_change(sink, cell, edge, at);
}
changed_cells
}
fn triggered_event_value(cur: &FourStateValue, at: SchedTime) -> FourStateValue {
let count = match cur {
FourStateValue::Wide { aval, .. } => aval.first().copied().unwrap_or(0),
FourStateValue::Narrow(n) => *n as u64,
};
FourStateValue::Wide {
aval: vec![count.wrapping_add(1), at.ticks.wrapping_add(1)],
bval: vec![0, 0],
}
}
fn same_value(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,
_ => a.planes() == b.planes(),
}
}
#[cfg(test)]
mod tests {
use super::super::nba::NbaLog;
use crate::sched::{ProcessId, SchedTime};
use crate::state::{
EdgeKind, FourStateValue, ListenerRole, SignalAccess, StateCellId, ValueChangeSink,
};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use sv_sim_abi::{Region, SimTime};
struct FakeArenaState {
cells: HashMap<StateCellId, FourStateValue>,
notifications: Vec<(StateCellId, EdgeKind, SimTime, Region)>,
deltas: Vec<super::ChangeDelta>,
}
#[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(),
deltas: Vec::new(),
})))
}
fn seed(&self, cell: StateCellId, value: FourStateValue) {
self.0.borrow_mut().cells.insert(cell, value);
}
fn read_cell(&self, cell: StateCellId) -> FourStateValue {
self.0
.borrow()
.cells
.get(&cell)
.cloned()
.unwrap_or(FourStateValue::Narrow(0))
}
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>,
) {
}
}
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));
}
fn wants_delta(&self, _cell: StateCellId) -> bool {
true
}
fn note_delta(&mut self, _cell: StateCellId, delta: super::ChangeDelta) {
self.0.borrow_mut().deltas.push(delta);
}
}
const PROC_A: ProcessId = 10;
const PROC_B: ProcessId = 5;
#[test]
fn two_nba_writes_same_cell_deterministically_commit_the_last_logged_one() {
let cell = StateCellId(1);
let fake1 = FakeArena::new();
let mut arena1 = fake1.arena_handle();
let mut sink1 = fake1.sink_handle();
let mut log1 = NbaLog::new();
log1.stage(cell, FourStateValue::Narrow(1), PROC_A);
log1.stage(cell, FourStateValue::Narrow(2), PROC_B);
super::commit_nba(&mut log1, &mut arena1, SchedTime { ticks: 0 }, &mut sink1);
match fake1.read_cell(cell) {
FourStateValue::Narrow(v) => assert_eq!(v, 2, "last-pushed write (B) must win"),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
let fake2 = FakeArena::new();
let mut arena2 = fake2.arena_handle();
let mut sink2 = fake2.sink_handle();
let mut log2 = NbaLog::new();
log2.stage(cell, FourStateValue::Narrow(2), PROC_B);
log2.stage(cell, FourStateValue::Narrow(1), PROC_A);
super::commit_nba(&mut log2, &mut arena2, SchedTime { ticks: 0 }, &mut sink2);
match fake2.read_cell(cell) {
FourStateValue::Narrow(v) => assert_eq!(v, 1, "last-pushed write (A) must win"),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
}
#[test]
fn cancelling_nbas_are_two_update_events_in_staging_order() {
let cell = StateCellId(3);
let fake = FakeArena::new();
fake.seed(cell, FourStateValue::Narrow(0));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let mut log = NbaLog::new();
log.stage(cell, FourStateValue::Narrow(1), PROC_A);
log.stage(cell, FourStateValue::Narrow(0), PROC_A);
super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
let edges: Vec<EdgeKind> = fake.notifications().into_iter().map(|n| n.1).collect();
assert_eq!(edges, vec![EdgeKind::Posedge, EdgeKind::Negedge]);
match fake.read_cell(cell) {
FourStateValue::Narrow(v) => assert_eq!(v, 0),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
}
#[test]
fn cancelling_partial_nbas_are_two_update_events_each_with_its_bits() {
let cell = StateCellId(4);
let fake = FakeArena::new();
fake.seed(
cell,
FourStateValue::Wide {
aval: vec![0, 0],
bval: vec![0, 0],
},
);
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let mut log = NbaLog::new();
for bit in [1u64, 0] {
log.stage_masked(
cell,
None,
FourStateValue::Wide {
aval: vec![bit << 9, 0],
bval: vec![0, 0],
},
vec![1 << 9, 0],
None,
PROC_A,
);
}
super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
let n = fake.notifications();
assert_eq!(n.len(), 2, "one update event per value-changing write");
let deltas = fake.0.borrow().deltas.clone();
assert_eq!(deltas.len(), 2);
assert_eq!(deltas[0].bit(9), Some(((false, false), (true, false))));
assert_eq!(deltas[1].bit(9), Some(((true, false), (false, false))));
}
#[test]
fn a_staged_pulse_triggers_the_event_cell_in_the_nba_region() {
let ev = StateCellId(6);
let data = StateCellId(7);
let fake = FakeArena::new();
fake.seed(
ev,
FourStateValue::Wide {
aval: vec![3, 4],
bval: vec![0, 0],
},
);
fake.seed(data, FourStateValue::Narrow(0));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let mut log = NbaLog::new();
log.stage(data, FourStateValue::Narrow(1), PROC_A);
log.stage_pulse(ev, None, PROC_A);
log.stage_pulse(ev, None, PROC_B);
super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 9 }, &mut sink);
match fake.read_cell(ev) {
FourStateValue::Wide { aval, bval } => {
assert_eq!(aval, vec![5, 10], "count 3 -> 5, stamped tick 9 + 1");
assert_eq!(bval, vec![0, 0]);
}
FourStateValue::Narrow(_) => panic!("an event cell is wide"),
}
let n = fake.notifications();
assert_eq!(n.iter().filter(|x| x.0 == ev).count(), 2);
assert!(n.iter().all(|x| x.3 == Region::Nba));
assert_eq!(n.iter().filter(|x| x.0 == data).count(), 1);
}
#[test]
fn unchanged_commit_produces_no_notify_change_call() {
let cell = StateCellId(2);
let fake = FakeArena::new();
fake.seed(cell, FourStateValue::Narrow(5));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let mut log = NbaLog::new();
log.stage(cell, FourStateValue::Narrow(5), PROC_A);
let changed = super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 3 }, &mut sink);
assert_eq!(changed, 0);
assert!(
fake.notifications().is_empty(),
"an NBA write equal to the current value must not call notify_change"
);
}
#[test]
fn changed_commit_calls_notify_change_exactly_once_per_changed_cell() {
let cell_changed_1 = StateCellId(10);
let cell_changed_2 = StateCellId(11);
let cell_unchanged = StateCellId(12);
let fake = FakeArena::new();
fake.seed(cell_changed_1, FourStateValue::Narrow(0));
fake.seed(cell_changed_2, FourStateValue::Narrow(0));
fake.seed(cell_unchanged, FourStateValue::Narrow(7));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let mut log = NbaLog::new();
log.stage(cell_changed_1, FourStateValue::Narrow(1), PROC_A);
log.stage(cell_changed_2, FourStateValue::Narrow(1), PROC_A);
log.stage(cell_unchanged, FourStateValue::Narrow(7), PROC_A);
let changed = super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
assert_eq!(changed, 2);
let notified: Vec<StateCellId> = fake.notifications().into_iter().map(|n| n.0).collect();
assert_eq!(notified.len(), 2, "no duplicate notify_change calls");
let notified_set: HashSet<StateCellId> = notified.into_iter().collect();
assert!(notified_set.contains(&cell_changed_1));
assert!(notified_set.contains(&cell_changed_2));
assert!(!notified_set.contains(&cell_unchanged));
}
#[test]
fn changed_cell_diffing_is_the_sole_wakeup_source() {
let cell_a = StateCellId(20);
let cell_b = StateCellId(21);
let cell_c = StateCellId(22);
let cell_d = StateCellId(23);
let fake = FakeArena::new();
fake.seed(cell_a, FourStateValue::Narrow(0));
fake.seed(cell_b, FourStateValue::Narrow(9));
fake.seed(cell_c, FourStateValue::Narrow(0));
fake.seed(cell_d, FourStateValue::Narrow(4));
let mut arena = fake.arena_handle();
let mut sink = fake.sink_handle();
let mut log = NbaLog::new();
log.stage(cell_a, FourStateValue::Narrow(1), PROC_A);
log.stage(cell_b, FourStateValue::Narrow(9), PROC_A);
log.stage(cell_c, FourStateValue::Narrow(2), PROC_A);
log.stage(cell_d, FourStateValue::Narrow(4), PROC_A);
super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
let notified_set: HashSet<StateCellId> =
fake.notifications().into_iter().map(|n| n.0).collect();
let expected: HashSet<StateCellId> = [cell_a, cell_c].into_iter().collect();
assert_eq!(notified_set, expected);
}
#[test]
fn multi_bit_change_with_unchanged_bit0_still_wakes_an_any_change_static_entry() {
use crate::sched::StaticSensitivity;
use crate::sched::sensitivity::{SensitivityLookupSink, SensitivityTable};
let cell = StateCellId(30);
let fake = FakeArena::new();
fake.seed(cell, FourStateValue::Narrow(0b10));
let mut arena = fake.arena_handle();
let mut table = SensitivityTable::new();
const PROC_COMB: ProcessId = 99;
table.install_static(
&StaticSensitivity {
edges: vec![(cell, None)],
},
PROC_COMB,
);
let mut woken = Vec::new();
let mut sink = SensitivityLookupSink::new(&table, &mut woken);
let mut log = NbaLog::new();
log.stage(cell, FourStateValue::Narrow(0b110), PROC_A);
let changed = super::commit_nba(&mut log, &mut arena, SchedTime { ticks: 0 }, &mut sink);
assert_eq!(changed, 1, "Narrow(0b10) -> Narrow(0b110) is a real change");
assert_eq!(
woken,
vec![PROC_COMB],
"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"
);
}
#[test]
fn infer_edge_covers_the_four_state_transition_table() {
use sv_sim_abi::Bit4;
fn bit_value(b: Bit4) -> FourStateValue {
let (val, unk) = b.to_val_unk();
FourStateValue::Narrow((val as u128) | ((unk as u128) << 64))
}
let cases: &[(Bit4, Bit4, EdgeKind)] = &[
(Bit4::Zero, Bit4::Zero, EdgeKind::None),
(Bit4::One, Bit4::One, EdgeKind::None),
(Bit4::X, Bit4::X, EdgeKind::None),
(Bit4::Z, Bit4::Z, EdgeKind::None),
(Bit4::Zero, Bit4::One, EdgeKind::Posedge),
(Bit4::One, Bit4::Zero, EdgeKind::Negedge),
(Bit4::Zero, Bit4::X, EdgeKind::Posedge),
(Bit4::X, Bit4::One, EdgeKind::Posedge),
(Bit4::Z, Bit4::One, EdgeKind::Posedge),
(Bit4::Zero, Bit4::Z, EdgeKind::Posedge),
(Bit4::One, Bit4::X, EdgeKind::Negedge),
(Bit4::One, Bit4::Z, EdgeKind::Negedge),
(Bit4::X, Bit4::Zero, EdgeKind::Negedge),
(Bit4::Z, Bit4::Zero, EdgeKind::Negedge),
(Bit4::X, Bit4::Z, EdgeKind::None),
(Bit4::Z, Bit4::X, EdgeKind::None),
];
for (old, new, expected) in cases {
let old_v = bit_value(*old);
let new_v = bit_value(*new);
let got = super::infer_edge(&old_v, &new_v);
assert_eq!(
got, *expected,
"infer_edge({:?} -> {:?}) should be {:?}, got {:?}",
old, new, expected, got
);
}
}
#[test]
fn nba_write_lowering_shapes_both_append_to_the_log() {
let mut log_a = NbaLog::new();
log_a.stage(StateCellId(9), FourStateValue::Narrow(42), 3);
let from_procedural_assign = log_a.take();
let mut log_b = NbaLog::new();
log_b.stage(StateCellId(9), FourStateValue::Narrow(42), 3);
let from_expression_statement = log_b.take();
assert_eq!(from_procedural_assign.len(), 1);
assert_eq!(from_expression_statement.len(), 1);
let a = &from_procedural_assign[0];
let b = &from_expression_statement[0];
assert_eq!(a.cell, b.cell);
assert_eq!(a.source, b.source);
assert_eq!(a.order, b.order);
match (&a.value, &b.value) {
(FourStateValue::Narrow(x), FourStateValue::Narrow(y)) => assert_eq!(x, y),
_ => panic!("expected Narrow"),
}
}
}