use vj_abi::SimTime;
use super::arena::{Arena, BuildMode};
use super::instance::StateEngine;
use super::write::{PreWriteHook, SlotLocation, write_inline_2s, write_slot, write_wide_2s};
use super::{FourStateValue, SlotClass, StateCellId};
use crate::sched::RewindError;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct RollbackMark(usize);
#[derive(Debug)]
pub(crate) enum CellReset {
AllX,
Initial(FourStateValue),
Net,
ForceContribution,
}
#[derive(Default)]
pub struct UndoLog {
entries: Vec<(StateCellId, FourStateValue, SimTime)>,
collected: usize,
}
impl UndoLog {
pub fn new() -> Self {
UndoLog::default()
}
pub fn checkpoint_marker(&self) -> RollbackMark {
RollbackMark(self.collected + self.entries.len())
}
pub fn rollback_to(
&mut self,
engine: &mut StateEngine,
mark: RollbackMark,
) -> Result<(), RewindError> {
if mark.0 < self.collected {
return Err(RewindError::PastFossilHorizon);
}
let target_local = mark.0 - self.collected;
while self.entries.len() > target_local {
let (cell, old, _at) = self
.entries
.pop()
.expect("entries.len() > target_local, checked by the loop condition, guarantees pop succeeds");
engine.commit_write_no_hook(cell, old);
}
Ok(())
}
pub fn fossil_collect(&mut self, gvt: SimTime) {
let mut cut = 0;
while cut < self.entries.len() && super::write::sim_time_le(self.entries[cut].2, gvt) {
cut += 1;
}
if cut > 0 {
self.entries.drain(0..cut);
self.collected += cut;
}
}
#[cfg(test)]
fn retained_len(&self) -> usize {
self.entries.len()
}
}
impl PreWriteHook for UndoLog {
fn before_write(
&mut self,
cell: StateCellId,
_loc: SlotLocation,
old: &FourStateValue,
_new_incoming: &FourStateValue,
at: SimTime,
) {
self.entries.push((cell, old.clone(), at));
}
}
pub struct ArenaSnapshot {
layout_hash: u64,
range: Option<(SlotClass, u32, u32)>,
build_mode: BuildMode,
inline: Vec<u128>,
wide_aval: Vec<u64>,
wide_bval: Vec<u64>,
inline_2s: Vec<u64>,
wide_2s: Vec<u64>,
}
fn all_x_value_for(loc: SlotLocation) -> FourStateValue {
match loc.class {
SlotClass::Inline => {
let (x_val, x_unk) = vj_abi::Bit4::X.to_val_unk();
let mask: u64 = if loc.bit_width >= 64 {
u64::MAX
} else {
(1u64 << loc.bit_width) - 1
};
let val = if x_val { mask } else { 0 };
let unk = if x_unk { mask } else { 0 };
FourStateValue::Narrow((val as u128) | ((unk as u128) << 64))
}
SlotClass::Wide | SlotClass::Memory | SlotClass::ForceWide => {
let n = loc.elem_word_count as usize;
FourStateValue::Wide {
aval: vec![u64::MAX; n],
bval: vec![u64::MAX; n],
}
}
}
}
fn reset_2s_cell_to_zero(arena: &Arena, loc: SlotLocation) {
match loc.class {
SlotClass::Inline => {
write_inline_2s(arena, loc.word_offset, 0);
}
SlotClass::Wide | SlotClass::Memory => {
let zeros = vec![0u64; loc.elem_word_count as usize];
write_wide_2s(arena, loc.word_offset, &zeros);
}
SlotClass::ForceWide => unreachable!(
"reset_2s_cell_to_zero: force_driver's contribution cells are tagged \
CellReset::ForceContribution and are filtered out by reset_to_initial \
before this helper is ever reached for one"
),
}
}
fn write_2s_cell_from_initial(arena: &Arena, loc: SlotLocation, fv: &FourStateValue) {
match (loc.class, fv) {
(SlotClass::Inline, FourStateValue::Narrow(word)) => {
write_inline_2s(arena, loc.word_offset, *word as u64);
}
(SlotClass::Wide | SlotClass::Memory, FourStateValue::Wide { aval, .. }) => {
write_wide_2s(arena, loc.word_offset, aval);
}
(class, value) => panic!(
"write_2s_cell_from_initial: SlotClass {class:?} does not match the \
FourStateValue shape of this cell's own CellReset::Initial ({value:?}) -- \
a CellReset::Initial is only ever built by allocate_instance from the same \
slot's own SlotLocation, so this indicates a lockstep-invariant violation \
elsewhere in this crate, not a normal case this helper tolerates silently"
),
}
}
impl StateEngine {
pub(crate) fn commit_write_no_hook(&mut self, cell: StateCellId, value: FourStateValue) {
let loc = self.cells[cell.0 as usize];
write_slot(&self.arena, loc, &value);
}
pub fn snapshot(&self, range: Option<(SlotClass, u32, u32)>) -> ArenaSnapshot {
let layout_hash = self.templates.signature_hash();
let build_mode = self.build_mode();
let (inline, wide_aval, wide_bval, inline_2s, wide_2s) = match (build_mode, range) {
(BuildMode::FourState, None) => (
self.arena.inline().to_vec(),
self.arena.wide_aval().to_vec(),
self.arena.wide_bval().to_vec(),
Vec::new(),
Vec::new(),
),
(BuildMode::FourState, Some((SlotClass::Inline, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
(
self.arena.inline()[s..e].to_vec(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
)
}
(BuildMode::FourState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
(
Vec::new(),
self.arena.wide_aval()[s..e].to_vec(),
self.arena.wide_bval()[s..e].to_vec(),
Vec::new(),
Vec::new(),
)
}
(BuildMode::TwoState, None) => (
Vec::new(),
Vec::new(),
Vec::new(),
self.arena.inline_2s().to_vec(),
self.arena.wide_2s().to_vec(),
),
(BuildMode::TwoState, Some((SlotClass::Inline, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
(
Vec::new(),
Vec::new(),
Vec::new(),
self.arena.inline_2s()[s..e].to_vec(),
Vec::new(),
)
}
(BuildMode::TwoState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
(
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
self.arena.wide_2s()[s..e].to_vec(),
)
}
(_, Some((SlotClass::ForceWide, _, _))) => panic!(
"StateEngine::snapshot: SlotClass::ForceWide is never a valid snapshot/restore \
range -- force_driver's synthetic contribution cells are never part of a real \
elaborated signal's storage and are not observable through this API"
),
};
ArenaSnapshot {
layout_hash,
range,
build_mode,
inline,
wide_aval,
wide_bval,
inline_2s,
wide_2s,
}
}
pub fn restore(&mut self, snap: &ArenaSnapshot) -> Result<(), RewindError> {
if self.templates.signature_hash() != snap.layout_hash {
return Err(RewindError::LayoutMismatch);
}
if self.build_mode() != snap.build_mode {
return Err(RewindError::LayoutMismatch);
}
self.arena.huge.invalidate_all();
match (snap.build_mode, snap.range) {
(BuildMode::FourState, None) => {
if snap.inline.len() != self.arena.inline().len()
|| snap.wide_aval.len() != self.arena.wide_aval().len()
|| snap.wide_bval.len() != self.arena.wide_bval().len()
{
return Err(RewindError::LayoutMismatch);
}
unsafe {
self.arena.inline_mut()[..snap.inline.len()].copy_from_slice(&snap.inline);
self.arena.wide_aval_mut()[..snap.wide_aval.len()]
.copy_from_slice(&snap.wide_aval);
self.arena.wide_bval_mut()[..snap.wide_bval.len()]
.copy_from_slice(&snap.wide_bval);
}
}
(BuildMode::FourState, Some((SlotClass::Inline, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
if e > self.arena.inline().len() || snap.inline.len() != count as usize {
return Err(RewindError::LayoutMismatch);
}
unsafe {
self.arena.inline_mut()[s..e].copy_from_slice(&snap.inline);
}
}
(BuildMode::FourState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
if e > self.arena.wide_aval().len()
|| e > self.arena.wide_bval().len()
|| snap.wide_aval.len() != count as usize
|| snap.wide_bval.len() != count as usize
{
return Err(RewindError::LayoutMismatch);
}
unsafe {
self.arena.wide_aval_mut()[s..e].copy_from_slice(&snap.wide_aval);
self.arena.wide_bval_mut()[s..e].copy_from_slice(&snap.wide_bval);
}
}
(BuildMode::TwoState, None) => {
if snap.inline_2s.len() != self.arena.inline_2s().len()
|| snap.wide_2s.len() != self.arena.wide_2s().len()
{
return Err(RewindError::LayoutMismatch);
}
unsafe {
self.arena.inline_2s_mut()[..snap.inline_2s.len()]
.copy_from_slice(&snap.inline_2s);
self.arena.wide_2s_mut()[..snap.wide_2s.len()].copy_from_slice(&snap.wide_2s);
}
}
(BuildMode::TwoState, Some((SlotClass::Inline, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
if e > self.arena.inline_2s().len() || snap.inline_2s.len() != count as usize {
return Err(RewindError::LayoutMismatch);
}
unsafe {
self.arena.inline_2s_mut()[s..e].copy_from_slice(&snap.inline_2s);
}
}
(BuildMode::TwoState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
let (s, e) = (start as usize, start as usize + count as usize);
if e > self.arena.wide_2s().len() || snap.wide_2s.len() != count as usize {
return Err(RewindError::LayoutMismatch);
}
unsafe {
self.arena.wide_2s_mut()[s..e].copy_from_slice(&snap.wide_2s);
}
}
(_, Some((SlotClass::ForceWide, _, _))) => unreachable!(
"StateEngine::restore: an ArenaSnapshot's own `range` is only ever set by \
Self::snapshot, which already rejects SlotClass::ForceWide before constructing \
one -- no ArenaSnapshot with a ForceWide range can exist to reach this arm"
),
}
Ok(())
}
pub fn reset_to_initial(&mut self) {
self.reset_to_initial_with(false);
}
pub fn reset_to_initial_with(&mut self, x_as_zero: bool) {
let build_mode = self.build_mode();
for idx in 0..self.cell_reset.len() {
let loc = self.cells[idx];
match &self.cell_reset[idx] {
CellReset::Net => {}
CellReset::ForceContribution => {}
CellReset::AllX => {
let rows = if loc.class == SlotClass::Memory {
loc.num_rows as u64
} else {
1
};
for row in 0..rows {
let row_loc = if loc.class == SlotClass::Memory {
super::write::resolve_mem_row(loc, row)
} else {
loc
};
match build_mode {
BuildMode::FourState => {
let value = if x_as_zero {
match row_loc.class {
SlotClass::Inline => FourStateValue::Narrow(0),
_ => FourStateValue::Wide {
aval: vec![0; row_loc.elem_word_count as usize],
bval: vec![0; row_loc.elem_word_count as usize],
},
}
} else {
all_x_value_for(row_loc)
};
write_slot(&self.arena, row_loc, &value);
}
BuildMode::TwoState => reset_2s_cell_to_zero(&self.arena, row_loc),
}
}
}
CellReset::Initial(fv) if loc.class == SlotClass::Memory => {
let words = loc.elem_word_count as usize;
for row in 0..loc.num_rows as usize {
let row_loc = super::write::resolve_mem_row(loc, row as u64);
let (aval, bval) = match fv {
FourStateValue::Wide { aval, bval } => (
aval[row * words..(row + 1) * words].to_vec(),
bval[row * words..(row + 1) * words].to_vec(),
),
FourStateValue::Narrow(_) => (vec![0; words], vec![0; words]),
};
match build_mode {
BuildMode::FourState => write_slot(
&self.arena,
row_loc,
&FourStateValue::Wide { aval, bval },
),
BuildMode::TwoState => {
write_wide_2s(&self.arena, row_loc.word_offset, &aval);
}
}
}
}
CellReset::Initial(fv) => match build_mode {
BuildMode::FourState => {
let value = fv.clone();
write_slot(&self.arena, loc, &value);
}
BuildMode::TwoState => write_2s_cell_from_initial(&self.arena, loc, fv),
},
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::write::{read_inline_2s, read_wide_2s};
use super::*;
use crate::netlist::{SymbolKey, TemplateKey};
use crate::state::instance::TemplateRegistry;
use crate::state::write::SlotLocation;
use crate::state::{SignalAccess, SlotDescriptor, SlotOffset, StateLayout};
fn fresh_engine() -> StateEngine {
StateEngine::new(TemplateRegistry::new())
}
fn inline_cell(engine: &mut StateEngine) -> StateCellId {
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,
};
engine.alloc_cell(loc)
}
fn narrow_word(val: u64, unk: u64) -> u128 {
(val as u128) | ((unk as u128) << 64)
}
fn narrow_at(engine: &StateEngine, cell: StateCellId) -> u128 {
match engine.read(cell) {
FourStateValue::Narrow(w) => w,
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
}
fn t(units: i64) -> SimTime {
SimTime {
sim_units: units,
delta: 0,
}
}
#[test]
fn rollback_to_a_marker_undoes_writes_in_reverse_order() {
let mut engine = fresh_engine();
let cell_a = inline_cell(&mut engine);
let cell_b = inline_cell(&mut engine);
let cell_c = inline_cell(&mut engine);
let mut log = UndoLog::new();
let mark = log.checkpoint_marker();
engine.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut log);
engine.commit_write(cell_b, FourStateValue::Narrow(narrow_word(2, 0)), &mut log);
engine.commit_write(cell_c, FourStateValue::Narrow(narrow_word(3, 0)), &mut log);
log.rollback_to(&mut engine, mark)
.expect("mark is well within the retained log");
for cell in [cell_a, cell_b, cell_c] {
assert_eq!(
narrow_at(&engine, cell),
narrow_word(0, 0),
"every cell written since `mark` must be back to its pre-write value"
);
}
}
#[test]
fn rollback_partial_marker_only_undoes_writes_after_it() {
let mut engine = fresh_engine();
let cell_a = inline_cell(&mut engine);
let cell_b = inline_cell(&mut engine);
let cell_c = inline_cell(&mut engine);
let mut log = UndoLog::new();
engine.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut log);
let mark = log.checkpoint_marker();
engine.commit_write(cell_b, FourStateValue::Narrow(narrow_word(2, 0)), &mut log);
engine.commit_write(cell_c, FourStateValue::Narrow(narrow_word(3, 0)), &mut log);
log.rollback_to(&mut engine, mark)
.expect("mark is well within the retained log");
assert_eq!(
narrow_at(&engine, cell_a),
narrow_word(1, 0),
"A's write predates `mark` and must survive"
);
assert_eq!(
narrow_at(&engine, cell_b),
narrow_word(0, 0),
"B's write postdates `mark` and must be undone"
);
assert_eq!(
narrow_at(&engine, cell_c),
narrow_word(0, 0),
"C's write postdates `mark` and must be undone"
);
}
#[test]
fn fossil_collect_trims_entries_at_or_before_gvt_never_past_it() {
let mut engine = fresh_engine();
let cell_a = inline_cell(&mut engine);
let cell_b = inline_cell(&mut engine);
let cell_c = inline_cell(&mut engine);
let mut log = UndoLog::new();
let mark_before_any = log.checkpoint_marker();
engine.set_current_time(t(1));
engine.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut log);
engine.set_current_time(t(2));
engine.commit_write(cell_b, FourStateValue::Narrow(narrow_word(2, 0)), &mut log);
engine.set_current_time(t(3));
engine.commit_write(cell_c, FourStateValue::Narrow(narrow_word(3, 0)), &mut log);
log.fossil_collect(t(2));
assert_eq!(
log.retained_len(),
1,
"only the t=3 entry (strictly after gvt=2) must remain"
);
let result = log.rollback_to(&mut engine, mark_before_any);
assert_eq!(
result,
Err(RewindError::PastFossilHorizon),
"a marker before the fossil-collected horizon must fail loudly, never silently under-roll-back"
);
}
#[test]
fn snapshot_restore_round_trips_a_partial_range_not_just_the_whole_arena() {
let mut engine = fresh_engine();
let cells: Vec<StateCellId> = (0..4).map(|_| inline_cell(&mut engine)).collect();
for (i, &cell) in cells.iter().enumerate() {
engine.commit_write(
cell,
FourStateValue::Narrow(narrow_word(i as u64 + 1, 0)),
&mut (),
);
}
let snap = engine.snapshot(Some((SlotClass::Inline, 1, 2)));
engine.commit_write(
cells[1],
FourStateValue::Narrow(narrow_word(99, 0)),
&mut (),
);
engine
.restore(&snap)
.expect("same engine, same layout hash, in-bounds range");
assert_eq!(
narrow_at(&engine, cells[1]),
narrow_word(2, 0),
"word 1 must be back to its snapshotted value"
);
assert_eq!(
narrow_at(&engine, cells[0]),
narrow_word(1, 0),
"word 0 is outside the snapshotted range and must be unaffected"
);
assert_eq!(
narrow_at(&engine, cells[3]),
narrow_word(4, 0),
"word 3 is outside the snapshotted range and must be unaffected"
);
}
#[test]
fn restore_with_a_layout_hash_mismatch_is_a_typed_error_never_silent_corruption() {
let templates_a = TemplateRegistry::new();
let tmpl_a = TemplateKey {
definition: 100,
params_hash: 0,
};
unsafe {
templates_a.register(
tmpl_a,
StateLayout {
slots: vec![],
inline_words: 1,
wide_words: 0,
},
);
}
let mut engine_a = StateEngine::new(templates_a);
let handle_a = engine_a
.allocate_instance(tmpl_a)
.expect("tmpl_a is registered");
drop(handle_a);
let snap_a = engine_a.snapshot(None);
let templates_b = TemplateRegistry::new();
let tmpl_b = TemplateKey {
definition: 200,
params_hash: 0,
};
unsafe {
templates_b.register(
tmpl_b,
StateLayout {
slots: vec![],
inline_words: 1,
wide_words: 0,
},
);
}
let mut engine_b = StateEngine::new(templates_b);
let handle_b = engine_b
.allocate_instance(tmpl_b)
.expect("tmpl_b is registered");
let cell_b = engine_b.alloc_cell(SlotLocation {
class: SlotClass::Inline,
word_offset: handle_b.base.inline_base,
bit_width: 8,
elem_word_count: 1,
row_stride_words: 0,
num_rows: 0,
});
engine_b.commit_write(
cell_b,
FourStateValue::Narrow(narrow_word(0x2A, 0)),
&mut (),
);
drop(handle_b);
let before: Vec<u128> = engine_b.arena.inline().to_vec();
let result = engine_b.restore(&snap_a);
assert_eq!(
result,
Err(RewindError::LayoutMismatch),
"restoring across two different template sets must be rejected"
);
assert_eq!(
engine_b.arena.inline().to_vec(),
before,
"a rejected restore must never partially corrupt the target arena"
);
}
#[test]
fn reset_to_initial_reblits_declared_initializers_and_x_fills_the_rest() {
let templates = TemplateRegistry::new();
let tmpl = TemplateKey {
definition: 42,
params_hash: 0,
};
let initial_value = FourStateValue::Narrow(0xAA);
unsafe {
templates.register(
tmpl,
StateLayout {
slots: vec![
SlotDescriptor {
key: SymbolKey(0),
offset: SlotOffset {
class: SlotClass::Inline,
word_offset: 0,
bit_width: 8,
elem_word_count: 1,
row_stride_words: 0,
num_rows: 0,
},
initial: Some(initial_value),
net_info: None,
},
SlotDescriptor {
key: SymbolKey(1),
offset: SlotOffset {
class: SlotClass::Inline,
word_offset: 1,
bit_width: 8,
elem_word_count: 1,
row_stride_words: 0,
num_rows: 0,
},
initial: None,
net_info: None,
},
],
inline_words: 2,
wide_words: 0,
},
);
}
let mut engine = StateEngine::new(templates);
let handle = engine.allocate_instance(tmpl).expect("tmpl is registered");
let cell_initialized = handle.cell_for_slot(0);
let cell_uninitialized = handle.cell_for_slot(1);
drop(handle);
engine.commit_write(
cell_initialized,
FourStateValue::Narrow(narrow_word(0x11, 0)),
&mut (),
);
engine.commit_write(
cell_uninitialized,
FourStateValue::Narrow(narrow_word(0x22, 0)),
&mut (),
);
engine.reset_to_initial();
let initialized = narrow_at(&engine, cell_initialized);
assert_eq!(
initialized as u64 & 0xFF,
0xAA,
"the declared initializer must be re-blitted exactly"
);
assert_eq!(
(initialized >> 64) as u64 & 0xFF,
0,
"a fully-known initializer has no unknown bits"
);
let uninitialized = narrow_at(&engine, cell_uninitialized);
assert_eq!(
uninitialized as u64 & 0xFF,
0,
"an uninitialized slot's value half must reset to 0"
);
assert_eq!(
(uninitialized >> 64) as u64 & 0xFF,
0xFF,
"an uninitialized slot must reset to all-X (mask=all-1) within its width"
);
}
#[test]
fn reset_to_initial_is_build_mode_aware_for_two_state_engines() {
let mut engine = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
assert_eq!(engine.build_mode(), BuildMode::TwoState);
let cell_initialized = inline_cell(&mut engine);
let cell_uninitialized = inline_cell(&mut engine);
engine.cell_reset[cell_initialized.0 as usize] =
CellReset::Initial(FourStateValue::Narrow(0xAA));
let loc_initialized = engine.cells[cell_initialized.0 as usize];
let loc_uninitialized = engine.cells[cell_uninitialized.0 as usize];
write_inline_2s(&engine.arena, loc_initialized.word_offset, 0x11);
write_inline_2s(&engine.arena, loc_uninitialized.word_offset, 0x22);
engine.reset_to_initial();
assert_eq!(
read_inline_2s(&engine.arena, loc_initialized.word_offset),
0xAA,
"a declared initializer must be re-blitted into the live 2-state plane"
);
assert_eq!(
read_inline_2s(&engine.arena, loc_uninitialized.word_offset),
0,
"an uninitialized 2-state slot must reset to 0, not all-X (there is no mask half in this plane)"
);
}
#[test]
fn reset_to_initial_two_state_wide_all_x_resets_every_word_to_zero() {
let mut engine = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
let word_offset = unsafe { engine.arena.alloc_wide_words(2) };
let loc = SlotLocation {
class: SlotClass::Wide,
word_offset,
bit_width: 128,
elem_word_count: 2,
row_stride_words: 0,
num_rows: 0,
};
engine.alloc_cell(loc);
write_wide_2s(&engine.arena, word_offset, &[0x1111_1111, 0x2222_2222]);
assert_eq!(
read_wide_2s(&engine.arena, word_offset, 2),
&[0x1111_1111, 0x2222_2222]
);
engine.reset_to_initial();
assert_eq!(
read_wide_2s(&engine.arena, word_offset, 2),
&[0, 0],
"an uninitialized wide 2-state slot must reset to all-zero words"
);
}
#[test]
fn snapshot_restore_two_state_round_trips_the_live_plane_not_the_inert_four_state_one() {
let mut engine = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
let cells: Vec<StateCellId> = (0..4).map(|_| inline_cell(&mut engine)).collect();
let offsets: Vec<u32> = cells
.iter()
.map(|&c| engine.cells[c.0 as usize].word_offset)
.collect();
for (i, &off) in offsets.iter().enumerate() {
write_inline_2s(&engine.arena, off, i as u64 + 1);
}
let snap = engine.snapshot(Some((SlotClass::Inline, offsets[1], 2)));
write_inline_2s(&engine.arena, offsets[1], 99);
engine
.restore(&snap)
.expect("same engine, same layout hash, same build mode, in-bounds range");
assert_eq!(
read_inline_2s(&engine.arena, offsets[1]),
2,
"word 1 must be back to its snapshotted 2-state value"
);
assert_eq!(
read_inline_2s(&engine.arena, offsets[0]),
1,
"word 0 is outside the snapshotted range and must be unaffected"
);
assert_eq!(
read_inline_2s(&engine.arena, offsets[3]),
4,
"word 3 is outside the snapshotted range and must be unaffected"
);
}
#[test]
fn restore_rejects_a_build_mode_mismatch_even_when_layout_hash_matches() {
let mut four_state_engine =
StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::FourState);
let cell = inline_cell(&mut four_state_engine);
four_state_engine.commit_write(cell, FourStateValue::Narrow(narrow_word(0x2A, 0)), &mut ());
let snap = four_state_engine.snapshot(None);
let mut two_state_engine =
StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
let two_cell = inline_cell(&mut two_state_engine);
let loc = two_state_engine.cells[two_cell.0 as usize];
write_inline_2s(&two_state_engine.arena, loc.word_offset, 0x77);
let before = two_state_engine.arena.inline_2s().to_vec();
let result = two_state_engine.restore(&snap);
assert_eq!(
result,
Err(RewindError::LayoutMismatch),
"restoring a FourState snapshot into a TwoState engine must be rejected"
);
assert_eq!(
two_state_engine.arena.inline_2s().to_vec(),
before,
"a rejected cross-build-mode restore must never partially corrupt the target arena"
);
}
#[test]
fn reset_to_initial_never_corrupts_a_live_force_contribution_cell() {
let mut engine = fresh_engine();
let net_cell = inline_cell(&mut engine);
let mut set = crate::state::driver::DriverSet {
resolved: net_cell,
drivers: Vec::new(),
net_kind: crate::state::NetKind::Wire,
};
let forced_cell = crate::state::driver::force_driver(
&mut engine,
&mut set,
net_cell,
FourStateValue::Narrow(narrow_word(0x2A, 0)),
);
assert_eq!(
narrow_at(&engine, forced_cell),
narrow_word(0x2A, 0),
"force_driver must seed its own contribution cell with the forced value"
);
engine.reset_to_initial();
assert_eq!(
narrow_at(&engine, forced_cell),
narrow_word(0x2A, 0),
"reset_to_initial must never blit a reset value over a live force_driver contribution cell"
);
}
}