use std::cell::{Cell, RefCell, UnsafeCell};
use vj_abi::{Region, SimTime};
use super::arena::{Arena, BuildMode};
use super::instance::StateEngine;
use super::{AliasWindow, EdgeKind, FourStateValue, SlotClass, StateCellId};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SlotLocation {
pub class: SlotClass,
pub word_offset: u32,
pub bit_width: u32,
pub elem_word_count: u32,
pub row_stride_words: u32,
pub num_rows: u32,
}
pub fn resolve_mem_row(loc: SlotLocation, row_index: u64) -> SlotLocation {
assert!(
loc.class == SlotClass::Memory,
"resolve_mem_row: SlotLocation class {:?} is not SlotClass::Memory -- \
row addressing only ever applies to a memory slot",
loc.class
);
assert!(
row_index < loc.num_rows as u64,
"resolve_mem_row: row index {row_index} is out of bounds for a {}-row memory",
loc.num_rows
);
let word_offset = loc.word_offset as u64 + loc.row_stride_words as u64 * row_index;
SlotLocation {
word_offset: u32::try_from(word_offset)
.expect("resolve_mem_row: resolved word_offset overflows u32"),
..loc
}
}
#[derive(Debug)]
pub struct ChangeResult {
pub changed: bool,
pub old: FourStateValue,
pub new: FourStateValue,
}
#[derive(Debug, Clone)]
pub struct VcRecord {
pub cell: StateCellId,
pub old: FourStateValue,
pub new: FourStateValue,
pub at: SimTime,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct WorkerSlot(pub(crate) usize);
pub(crate) struct VcState {
current_time: Cell<SimTime>,
current_region: Cell<Region>,
#[allow(clippy::vec_box)]
worker_buffers: UnsafeCell<Vec<Box<RefCell<Vec<VcRecord>>>>>,
engine_id: u64,
gvt_watermark: Cell<SimTime>,
pub(crate) force: RefCell<super::force::ForceTable>,
record_changes: Cell<bool>,
pub(crate) views: super::alias::Views,
}
impl VcState {
pub(crate) fn new(engine_id: u64) -> Self {
VcState {
current_time: Cell::new(SimTime {
sim_units: 0,
delta: 0,
}),
current_region: Cell::new(Region::Active),
worker_buffers: UnsafeCell::new(Vec::new()),
engine_id,
gvt_watermark: Cell::new(SimTime {
sim_units: 0,
delta: 0,
}),
force: RefCell::new(super::force::ForceTable::default()),
record_changes: Cell::new(true),
views: super::alias::Views::default(),
}
}
pub(crate) fn records_changes(&self) -> bool {
self.record_changes.get()
}
pub(crate) fn set_record_changes(&self, on: bool) {
self.record_changes.set(on);
}
pub(crate) fn set_current_time(&self, at: SimTime) {
self.current_time.set(at);
}
pub(crate) fn set_current_region(&self, region: Region) {
self.current_region.set(region);
}
pub(crate) fn current_time(&self) -> SimTime {
self.current_time.get()
}
pub(crate) fn current_region(&self) -> Region {
self.current_region.get()
}
pub(crate) fn set_gvt_watermark(&self, gvt: SimTime) {
self.gvt_watermark.set(gvt);
}
#[cfg(test)]
pub(crate) fn gvt_watermark(&self) -> SimTime {
self.gvt_watermark.get()
}
pub(crate) unsafe fn register_worker_thread(&self) -> WorkerSlot {
unsafe {
let buffers = &mut *self.worker_buffers.get();
buffers.push(Box::new(RefCell::new(Vec::new())));
WorkerSlot(buffers.len() - 1)
}
}
unsafe fn buffer_ptr(&self, slot: WorkerSlot) -> *const RefCell<Vec<VcRecord>> {
let buffers: &Vec<Box<RefCell<Vec<VcRecord>>>> = unsafe { &*self.worker_buffers.get() };
&*buffers[slot.0]
}
pub(crate) fn drain_committed_changes(&self, up_to: SimTime) -> Vec<VcRecord> {
let buffers: &Vec<Box<RefCell<Vec<VcRecord>>>> = unsafe { &*self.worker_buffers.get() };
let mut merged: Vec<VcRecord> = Vec::new();
for buf in buffers.iter() {
let mut records = buf.borrow_mut();
let mut i = 0;
while i < records.len() {
if sim_time_le(records[i].at, up_to) {
merged.push(records.remove(i));
} else {
i += 1;
}
}
}
merged.sort_by_key(|a| (a.at.sim_units, a.at.delta));
merged
}
}
pub(crate) fn sim_time_le(a: SimTime, b: SimTime) -> bool {
(a.sim_units, a.delta) <= (b.sim_units, b.delta)
}
pub trait PreWriteHook {
fn before_write(
&mut self,
cell: StateCellId,
loc: SlotLocation,
old: &FourStateValue,
new_incoming: &FourStateValue,
at: SimTime,
);
}
impl PreWriteHook for () {
fn before_write(
&mut self,
_cell: StateCellId,
_loc: SlotLocation,
_old: &FourStateValue,
_new_incoming: &FourStateValue,
_at: SimTime,
) {
}
}
struct WorkerCacheEntry {
engine_id: u64,
#[allow(dead_code)]
slot: WorkerSlot,
buffer: *const RefCell<Vec<VcRecord>>,
}
thread_local! {
static WORKER_CACHE: RefCell<Option<WorkerCacheEntry>> = const { RefCell::new(None) };
}
pub(crate) fn four_state_changed(old: &FourStateValue, new: &FourStateValue) -> bool {
match (old, new) {
(FourStateValue::Narrow(o), FourStateValue::Narrow(n)) => {
let o_val = *o as u64;
let o_unk = (*o >> 64) as u64;
let n_val = *n as u64;
let n_unk = (*n >> 64) as u64;
o_val != n_val || o_unk != n_unk
}
(
FourStateValue::Wide { aval: oa, bval: ob },
FourStateValue::Wide { aval: na, bval: nb },
) => oa != na || ob != nb,
(FourStateValue::Narrow(_), FourStateValue::Wide { .. })
| (FourStateValue::Wide { .. }, FourStateValue::Narrow(_)) => {
panic!(
"commit_write: old/new FourStateValue shape mismatch (Narrow vs Wide) for the \
same cell -- the caller's `new` value does not match this cell's storage class"
);
}
}
}
pub fn read_slot(arena: &Arena, loc: SlotLocation) -> FourStateValue {
match loc.class {
SlotClass::Inline => FourStateValue::Narrow(arena.inline()[loc.word_offset as usize]),
SlotClass::Wide | SlotClass::Memory => {
let start = loc.word_offset as usize;
let end = start + loc.elem_word_count as usize;
FourStateValue::Wide {
aval: arena.wide_aval()[start..end].to_vec(),
bval: arena.wide_bval()[start..end].to_vec(),
}
}
SlotClass::ForceWide => {
let start = loc.word_offset as usize;
let end = start + loc.elem_word_count as usize;
FourStateValue::Wide {
aval: arena.force_wide_aval()[start..end].to_vec(),
bval: arena.force_wide_bval()[start..end].to_vec(),
}
}
}
}
pub fn write_slot(arena: &Arena, loc: SlotLocation, new: &FourStateValue) {
match (loc.class, new) {
(SlotClass::Inline, FourStateValue::Narrow(word)) => {
unsafe {
arena.inline_mut()[loc.word_offset as usize] = *word;
}
}
(SlotClass::Wide | SlotClass::Memory, FourStateValue::Wide { aval, bval }) => {
let start = loc.word_offset as usize;
arena.huge.invalidate_range(start, aval.len());
unsafe {
arena.wide_aval_mut()[start..start + aval.len()].copy_from_slice(aval);
arena.wide_bval_mut()[start..start + bval.len()].copy_from_slice(bval);
}
}
(SlotClass::ForceWide, FourStateValue::Wide { aval, bval }) => {
let start = loc.word_offset as usize;
unsafe {
arena.force_wide_aval_mut()[start..start + aval.len()].copy_from_slice(aval);
arena.force_wide_bval_mut()[start..start + bval.len()].copy_from_slice(bval);
}
}
(class, value) => panic!(
"commit_write: SlotClass {class:?} does not match the FourStateValue shape \
being written ({value:?}) -- a caller passed a value shape that doesn't match \
this cell's own storage class"
),
}
}
fn thread_local_buffer(vc: &VcState) -> *const RefCell<Vec<VcRecord>> {
let cached = WORKER_CACHE.with(|cache| {
cache
.borrow()
.as_ref()
.filter(|entry| entry.engine_id == vc.engine_id)
.map(|entry| entry.buffer)
});
if let Some(ptr) = cached {
return ptr;
}
let slot = unsafe { vc.register_worker_thread() };
let ptr = unsafe { vc.buffer_ptr(slot) };
WORKER_CACHE.with(|cache| {
*cache.borrow_mut() = Some(WorkerCacheEntry {
engine_id: vc.engine_id,
slot,
buffer: ptr,
});
});
ptr
}
pub(crate) fn commit_write_at(
arena: &Arena,
vc: &VcState,
loc: SlotLocation,
cell: StateCellId,
new: FourStateValue,
hook: &mut dyn PreWriteHook,
) -> ChangeResult {
let new = {
let mut forced = vc.force.borrow_mut();
if forced.is_empty() {
new
} else {
if loc.class == SlotClass::Memory {
forced.filter_row(cell, loc.word_offset, new)
} else {
forced.filter(cell, new)
}
}
};
let old = read_slot(arena, loc);
let at = vc.current_time.get();
hook.before_write(cell, loc, &old, &new, at);
let changed = four_state_changed(&old, &new);
if changed {
write_slot(arena, loc, &new);
}
if changed && vc.record_changes.get() {
let record = VcRecord {
cell,
old: old.clone(),
new: new.clone(),
at,
};
let buf_ptr = thread_local_buffer(vc);
unsafe {
(*buf_ptr).borrow_mut().push(record);
}
}
ChangeResult { changed, old, new }
}
pub fn read_inline_2s(arena: &Arena, word_offset: u32) -> u64 {
arena.inline_2s()[word_offset as usize]
}
pub fn write_inline_2s(arena: &Arena, word_offset: u32, value: u64) -> bool {
let idx = word_offset as usize;
let old = arena.inline_2s()[idx];
let changed = old != value;
if changed {
unsafe {
arena.inline_2s_mut()[idx] = value;
}
}
changed
}
pub fn read_wide_2s(arena: &Arena, word_offset: u32, elem_word_count: u32) -> &[u64] {
let start = word_offset as usize;
let end = start + elem_word_count as usize;
&arena.wide_2s()[start..end]
}
pub fn write_wide_2s(arena: &Arena, word_offset: u32, values: &[u64]) -> bool {
let start = word_offset as usize;
let end = start + values.len();
let changed = arena.wide_2s()[start..end] != *values;
if changed {
unsafe {
arena.wide_2s_mut()[start..end].copy_from_slice(values);
}
}
changed
}
impl StateEngine {
pub fn alloc_cell(&mut self, loc: SlotLocation) -> StateCellId {
let id = self.cells.len() as u64;
self.cells.push(loc);
self.cell_reset.push(super::rollback::CellReset::AllX);
StateCellId(id)
}
pub fn commit_write(
&mut self,
cell: StateCellId,
new: FourStateValue,
hook: &mut dyn PreWriteHook,
) -> ChangeResult {
let loc = self.cells[cell.0 as usize];
let result = commit_write_at(&self.arena, &self.vc, loc, cell, new, hook);
self.vc.views.note_change(cell, None, &result);
if result.changed {
self.dispatch.notify_scheduler(
cell,
EdgeKind::None,
self.vc.current_time(),
self.vc.current_region(),
);
}
result
}
pub fn alloc_view_cell(&mut self, root: StateCellId, window: AliasWindow) -> StateCellId {
debug_assert!(
self.vc.views.info(root).is_none(),
"a window alias's root is a real cell, never another view"
);
let existing = self.vc.views.table().existing(root, window);
if let Some(view) = existing {
return view;
}
let loc = self.cells[root.0 as usize];
let view = self.alloc_cell(loc);
self.cell_reset[view.0 as usize] = super::rollback::CellReset::ForceContribution;
self.vc.views.table().register(view, root, window);
self.vc.views.mark_any();
view
}
pub fn build_mode(&self) -> BuildMode {
self.arena.mode()
}
}
#[cfg(test)]
pub fn last_vc_record_for(cell: StateCellId) -> Option<VcRecord> {
WORKER_CACHE.with(|cache| {
let buffer_ptr = cache.borrow().as_ref().map(|entry| entry.buffer)?;
let buffer = unsafe { &*buffer_ptr };
buffer
.borrow()
.iter()
.rev()
.find(|record| record.cell == cell)
.cloned()
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::instance::TemplateRegistry;
use crate::state::{ListenerRole, SignalAccess, ValueChangeSink};
use std::sync::{Arc, Mutex};
use vj_abi::Region;
fn fresh_engine() -> StateEngine {
StateEngine::new(TemplateRegistry::new())
}
struct SpySink {
log: Arc<Mutex<Vec<StateCellId>>>,
}
impl ValueChangeSink for SpySink {
fn notify_change(
&mut self,
cell: StateCellId,
_edge: EdgeKind,
_at: SimTime,
_region: Region,
) {
self.log.lock().unwrap().push(cell);
}
}
fn inline_cell(engine: &mut StateEngine) -> (StateCellId, SlotLocation) {
let word_offset = unsafe { engine.arena.alloc_inline_word() };
let loc = SlotLocation {
class: SlotClass::Inline,
word_offset,
bit_width: 8,
elem_word_count: 1,
row_stride_words: 0,
num_rows: 0,
};
let cell = engine.alloc_cell(loc);
(cell, loc)
}
fn wide_cell(engine: &mut StateEngine, elem_word_count: u32) -> (StateCellId, SlotLocation) {
let word_offset = unsafe { engine.arena.alloc_wide_words(elem_word_count) };
let loc = SlotLocation {
class: SlotClass::Wide,
word_offset,
bit_width: elem_word_count * 64,
elem_word_count,
row_stride_words: 0,
num_rows: 0,
};
let cell = engine.alloc_cell(loc);
(cell, loc)
}
fn narrow_word(val: u64, unk: u64) -> u128 {
(val as u128) | ((unk as u128) << 64)
}
#[test]
fn alloc_cell_returns_dense_lockstep_indices() {
let mut engine = fresh_engine();
let (c0, _) = inline_cell(&mut engine);
let (c1, _) = inline_cell(&mut engine);
let (c2, _) = inline_cell(&mut engine);
assert_eq!(c0, StateCellId(0));
assert_eq!(c1, StateCellId(1));
assert_eq!(c2, StateCellId(2));
}
#[test]
fn commit_write_on_unchanged_value_produces_zero_vc_records() {
let mut engine = fresh_engine();
let (cell, _) = inline_cell(&mut engine);
let seed = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
assert!(
seed.changed,
"seeding from the zero default to 5 is a real change"
);
let repeat = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
assert!(
!repeat.changed,
"writing the already-held value must not report a change"
);
let last = last_vc_record_for(cell).expect("the seed write appended a record");
match last.old {
FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(0, 0)),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
match last.new {
FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(5, 0)),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
}
#[test]
fn commit_write_notifies_the_scheduler_only_on_a_real_change_never_on_a_no_op_write() {
let mut engine = fresh_engine();
let (cell, _) = inline_cell(&mut engine);
let log = Arc::new(Mutex::new(Vec::new()));
engine.subscribe(
cell,
ListenerRole::Scheduler,
Box::new(SpySink { log: log.clone() }),
);
let seed = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
assert!(
seed.changed,
"seeding from the zero default to 5 is a real change"
);
assert_eq!(
log.lock().unwrap().len(),
1,
"the seed write is a real change and must notify the scheduler exactly once"
);
let repeat = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
assert!(
!repeat.changed,
"writing the already-held value must not report a change"
);
assert_eq!(
log.lock().unwrap().len(),
1,
"a no-op write (no real value change) must never notify the scheduler again"
);
}
#[test]
fn commit_write_on_changed_value_produces_exactly_one_vc_record() {
let mut engine = fresh_engine();
let (cell, loc) = inline_cell(&mut engine);
unsafe {
engine.arena.inline_mut()[loc.word_offset as usize] = narrow_word(5, 0);
}
let result = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(6, 0)), &mut ());
assert!(result.changed);
match result.old {
FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(5, 0)),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
match result.new {
FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(6, 0)),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
let count = WORKER_CACHE.with(|cache| {
let buffer_ptr = cache.borrow().as_ref().unwrap().buffer;
let buffer = unsafe { &*buffer_ptr };
buffer.borrow().iter().filter(|r| r.cell == cell).count()
});
assert_eq!(count, 1, "the bypassed seed write must not be counted");
}
#[test]
fn commit_write_diffs_the_mask_half_not_just_the_value_half() {
let mut engine = fresh_engine();
let (cell, loc) = inline_cell(&mut engine);
unsafe {
engine.arena.inline_mut()[loc.word_offset as usize] = narrow_word(1, 0);
}
let result = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(1, 1)), &mut ());
assert!(
result.changed,
"a driven-1 -> driven-Z transition (value bit constant, mask bit flips) must register as changed"
);
}
#[test]
fn commit_write_diffs_the_mask_half_not_just_the_value_half_on_a_wide_cell() {
let mut engine = fresh_engine();
let (cell, loc) = wide_cell(&mut engine, 1);
unsafe {
engine.arena.wide_aval_mut()[loc.word_offset as usize] = 1;
engine.arena.wide_bval_mut()[loc.word_offset as usize] = 0;
}
let result = engine.commit_write(
cell,
FourStateValue::Wide {
aval: vec![1],
bval: vec![1],
},
&mut (),
);
assert!(
result.changed,
"aval unchanged / bval flips 0->1 must still register as changed on the wide tier"
);
}
#[test]
fn before_write_hook_is_called_exactly_once_per_commit_write_before_the_store() {
struct RecordingHook {
arena: std::rc::Rc<super::super::arena::Arena>,
calls: Vec<(StateCellId, SlotLocation, u128, u128)>,
}
impl PreWriteHook for RecordingHook {
fn before_write(
&mut self,
cell: StateCellId,
loc: SlotLocation,
old: &FourStateValue,
new_incoming: &FourStateValue,
_at: SimTime,
) {
let current = self.arena.inline()[loc.word_offset as usize];
let old_word = match old {
FourStateValue::Narrow(w) => *w,
FourStateValue::Wide { .. } => panic!("expected Narrow"),
};
assert_eq!(
current, old_word,
"the stored value must still equal `old` inside the hook -- it fires before the mutation"
);
let new_word = match new_incoming {
FourStateValue::Narrow(w) => *w,
FourStateValue::Wide { .. } => panic!("expected Narrow"),
};
self.calls.push((cell, loc, old_word, new_word));
}
}
let mut engine = fresh_engine();
let (cell, loc) = inline_cell(&mut engine);
let mut hook = RecordingHook {
arena: std::rc::Rc::clone(&engine.arena),
calls: Vec::new(),
};
engine.commit_write(cell, FourStateValue::Narrow(narrow_word(9, 0)), &mut hook);
assert_eq!(hook.calls.len(), 1);
let (called_cell, called_loc, old_word, new_word) = hook.calls[0];
assert_eq!(called_cell, cell);
assert_eq!(called_loc, loc);
assert_eq!(old_word, narrow_word(0, 0));
assert_eq!(new_word, narrow_word(9, 0));
}
#[test]
fn register_worker_thread_returns_distinct_increasing_slots_and_commit_write_self_registers() {
let mut engine = fresh_engine();
let slot0 = engine.register_worker_thread();
let slot1 = engine.register_worker_thread();
assert_eq!(slot0, WorkerSlot(0));
assert_eq!(slot1, WorkerSlot(1));
let mut fresh = fresh_engine();
let (cell, _) = inline_cell(&mut fresh);
fresh.commit_write(cell, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
let record = last_vc_record_for(cell);
assert!(
record.is_some(),
"commit_write must self-register this thread against the fresh engine, not require an explicit register_worker_thread call first"
);
}
#[test]
fn two_state_write_changes_are_detected_without_any_mask_comparison() {
let arena = Arena::new(BuildMode::TwoState);
let off = unsafe { arena.alloc_inline_2s_words(1) };
assert!(
write_inline_2s(&arena, off, 5),
"uninitialized zero -> 5 is a real change"
);
assert!(
!write_inline_2s(&arena, off, 5),
"writing the already-held value must not report a change"
);
assert!(write_inline_2s(&arena, off, 6), "5 -> 6 is a real change");
assert_eq!(read_inline_2s(&arena, off), 6);
}
#[test]
fn two_state_binary_never_allocates_a_wide_bval_word() {
use crate::netlist::{SymbolKey, TemplateKey};
use crate::state::{SlotDescriptor, SlotOffset, StateLayout};
use std::rc::Rc;
let templates = TemplateRegistry::new();
let tmpl = TemplateKey {
definition: 0,
params_hash: 0,
};
unsafe {
templates.register(
tmpl,
StateLayout {
slots: vec![SlotDescriptor {
key: SymbolKey(0),
offset: SlotOffset {
class: SlotClass::Wide,
word_offset: 0,
bit_width: 128,
elem_word_count: 2,
row_stride_words: 0,
num_rows: 0,
},
initial: None,
net_info: None,
}],
inline_words: 0,
wide_words: 2,
},
);
}
let mut engine = StateEngine::new_with_mode(templates, BuildMode::TwoState);
assert_eq!(engine.build_mode(), BuildMode::TwoState);
let handle = engine
.allocate_instance(tmpl)
.expect("template is registered");
let changed = write_wide_2s(
&engine.arena,
handle.base.wide_base,
&[0xAAAA_AAAA_AAAA_AAAA, 0xBBBB_BBBB_BBBB_BBBB],
);
assert!(
changed,
"writing a nonzero pattern to a fresh zero-initialized cell must report changed"
);
assert_eq!(
read_wide_2s(&engine.arena, handle.base.wide_base, 2),
&[0xAAAA_AAAA_AAAA_AAAA, 0xBBBB_BBBB_BBBB_BBBB]
);
drop(handle);
let arena = Rc::get_mut(&mut engine.arena)
.expect("no other Rc<Arena> clone outstanding after dropping the handle");
let view = arena.ffi_view();
assert!(view.wide_aval.is_null());
assert!(view.wide_bval.is_null());
}
#[test]
fn two_state_functions_never_touch_the_4state_planes() {
let arena = Arena::new(BuildMode::TwoState);
let off = unsafe { arena.alloc_inline_2s_words(1) };
write_inline_2s(&arena, off, 0xDEAD_BEEF);
assert!(
arena.inline().iter().all(|&w| w == 0),
"a 2-state-only write must never touch the 4-state inline plane"
);
}
#[test]
fn build_mode_is_fixed_at_construction_no_setter_exists() {
let engine = fresh_engine();
assert_eq!(engine.build_mode(), BuildMode::FourState);
let two_state = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
assert_eq!(two_state.build_mode(), BuildMode::TwoState);
}
#[test]
fn resolve_mem_row_computes_word_offset_plus_row_stride_times_row_index() {
let loc = SlotLocation {
class: SlotClass::Memory,
word_offset: 100,
bit_width: 32,
elem_word_count: 2,
row_stride_words: 3,
num_rows: 8,
};
assert_eq!(resolve_mem_row(loc, 0).word_offset, 100);
assert_eq!(resolve_mem_row(loc, 3).word_offset, 109);
assert_eq!(resolve_mem_row(loc, 7).word_offset, 121);
}
#[test]
#[should_panic(expected = "row index")]
fn resolve_mem_row_panics_on_an_out_of_bounds_row_index() {
let loc = SlotLocation {
class: SlotClass::Memory,
word_offset: 100,
bit_width: 32,
elem_word_count: 2,
row_stride_words: 3,
num_rows: 8,
};
resolve_mem_row(loc, 8);
}
#[test]
#[should_panic]
fn resolve_mem_row_rejects_a_non_memory_slot_location() {
let loc = SlotLocation {
class: SlotClass::Wide,
word_offset: 0,
bit_width: 64,
elem_word_count: 1,
row_stride_words: 0,
num_rows: 0,
};
resolve_mem_row(loc, 0);
}
#[test]
fn wide_write_detects_a_change_confined_to_only_the_last_of_four_words() {
let mut engine = fresh_engine();
let (cell, _) = wide_cell(&mut engine, 4);
let first = engine.commit_write(
cell,
FourStateValue::Wide {
aval: vec![1, 2, 3, 4],
bval: vec![0, 0, 0, 0],
},
&mut (),
);
assert!(
first.changed,
"seeding from the zero default is a real change"
);
let second = engine.commit_write(
cell,
FourStateValue::Wide {
aval: vec![1, 2, 3, 999],
bval: vec![0, 0, 0, 0],
},
&mut (),
);
assert!(
second.changed,
"a difference confined to the last of 4 words must still register as changed \
-- a truncated/prefix-only word compare would miss this"
);
let third = engine.commit_write(
cell,
FourStateValue::Wide {
aval: vec![1, 2, 3, 999],
bval: vec![0, 0, 0, 0],
},
&mut (),
);
assert!(
!third.changed,
"writing the already-held 4-word value must not report a change"
);
}
}