use std::ffi::c_void;
use vj_abi::{SimHandle, SlotId, WideView};
use super::instance::{InstanceHandle, StateEngine};
use super::write::{SlotLocation, commit_write_at, read_slot, resolve_mem_row, write_slot};
use super::{
EdgeKind, FourStateValue, ListenerRole, SignalAccess, SlotClass, StateCellId, ValueChangeSink,
};
impl InstanceHandle {
pub fn into_sim_handle(self: Box<Self>) -> SimHandle {
SimHandle(Box::into_raw(self) as *mut c_void)
}
pub unsafe fn from_sim_handle<'a>(h: SimHandle) -> &'a InstanceHandle {
unsafe { &*(h.0 as *const InstanceHandle) }
}
}
pub(super) fn slot_location_for(handle: &InstanceHandle, slot: SlotId) -> SlotLocation {
let offset = handle.layout().slots[slot.0 as usize].offset;
let word_offset = match offset.class {
SlotClass::Inline => handle.base.inline_base + offset.word_offset,
SlotClass::Wide | SlotClass::Memory => handle.base.wide_base + offset.word_offset,
SlotClass::ForceWide => unreachable!(
"slot_location_for: no SlotDescriptor/SlotOffset the layout builder \
produces is ever SlotClass::ForceWide"
),
};
SlotLocation {
class: offset.class,
word_offset,
bit_width: offset.bit_width,
elem_word_count: offset.elem_word_count,
row_stride_words: offset.row_stride_words,
num_rows: offset.num_rows,
}
}
pub(crate) fn slot_state(
handle: SimHandle,
slot: SlotId,
) -> Result<(StateCellId, u32, bool, FourStateValue, u32), &'static str> {
let this = unsafe { InstanceHandle::from_sim_handle(handle) };
let idx = slot.0 as usize;
let window = if idx >= this.own_slot_count() {
this.foreign_window(idx)
} else {
None
};
if let Some(w) = &window
&& w.row.is_some()
{
return Err("force/release of a ref port bound to an element of an unpacked array");
}
let (handle, slot) = resolve_foreign_slot(handle, slot);
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = slot_location_for(handle, slot);
let cell = handle.cell_for_slot(slot.0 as usize);
let value = super::write::read_slot(handle.arena(), loc);
Ok((
cell,
loc.bit_width,
loc.class == SlotClass::Wide,
value,
window.map_or(0, |w| w.lo),
))
}
pub(crate) fn slot_row_alias(handle: SimHandle, slot: SlotId) -> Option<(u64, u32)> {
let this = unsafe { InstanceHandle::from_sim_handle(handle) };
let idx = slot.0 as usize;
if idx < this.own_slot_count() {
return None;
}
let w = this.foreign_window(idx)?;
w.row.map(|row| (row, w.lo))
}
pub(crate) fn row_state(
handle: SimHandle,
slot: SlotId,
row: u64,
) -> (StateCellId, u32, u32, FourStateValue) {
let (handle, slot) = resolve_foreign_slot(handle, slot);
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = resolve_mem_row(slot_location_for(handle, slot), row);
let cell = handle.cell_for_slot(slot.0 as usize);
let value = super::write::read_slot(handle.arena(), loc);
(cell, loc.word_offset, loc.bit_width, value)
}
pub fn copy_slot_quiet(handle: SimHandle, src: SlotId, dst: SlotId) {
let dst_h = unsafe { InstanceHandle::from_sim_handle(handle) };
let idx = src.0 as usize;
if idx >= dst_h.own_slot_count()
&& let Some(window) = dst_h.foreign_window(idx)
{
let value = read_window(dst_h, idx, window);
write_slot(dst_h.arena(), slot_location_for(dst_h, dst), &value);
return;
}
let (src_handle, src) = resolve_foreign_slot(handle, src);
let src_h = unsafe { InstanceHandle::from_sim_handle(src_handle) };
let value = read_slot(src_h.arena(), slot_location_for(src_h, src));
write_slot(dst_h.arena(), slot_location_for(dst_h, dst), &value);
}
pub(crate) fn with_force_table<R>(
handle: SimHandle,
f: impl FnOnce(&mut super::force::ForceTable) -> R,
) -> R {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
f(&mut handle.vc().force.borrow_mut())
}
pub extern "C" fn sv_get_scalar(handle: SimHandle, slot: SlotId) -> u128 {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = slot_location_for(handle, slot);
handle.arena().inline()[loc.word_offset as usize]
}
pub extern "C" fn sv_set_scalar(handle: SimHandle, slot: SlotId, value: u128) {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = slot_location_for(handle, slot);
let cell = handle.cell_for_slot(slot.0 as usize);
let result = commit_write_at(
handle.arena(),
handle.vc(),
loc,
cell,
FourStateValue::Narrow(value),
&mut (),
);
handle.vc().views.note_change(cell, None, &result);
}
pub extern "C" fn sv_get_wide(handle: SimHandle, slot: SlotId) -> WideView {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = slot_location_for(handle, slot);
let start = loc.word_offset as usize;
handle.arena().seal_wide_plane();
let aval = unsafe { handle.arena().wide_aval_ptr().add(start) };
let bval = unsafe { handle.arena().wide_bval_ptr().add(start) };
WideView {
aval,
bval,
word_count: loc.elem_word_count,
}
}
pub extern "C" fn sv_set_wide(handle: SimHandle, slot: SlotId, value: WideView) {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = slot_location_for(handle, slot);
let cell = handle.cell_for_slot(slot.0 as usize);
let n = loc.elem_word_count as usize;
let aval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.aval, n) }.to_vec();
let bval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.bval, n) }.to_vec();
let result = commit_write_at(
handle.arena(),
handle.vc(),
loc,
cell,
FourStateValue::Wide { aval, bval },
&mut (),
);
handle.vc().views.note_change(cell, None, &result);
}
pub extern "C" fn sv_get_mem_word(handle: SimHandle, slot: SlotId, row_index: u64) -> WideView {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = slot_location_for(handle, slot);
let resolved = resolve_mem_row(loc, row_index);
let start = resolved.word_offset as usize;
handle.arena().seal_wide_plane();
let aval = unsafe { handle.arena().wide_aval_ptr().add(start) };
let bval = unsafe { handle.arena().wide_bval_ptr().add(start) };
WideView {
aval,
bval,
word_count: resolved.elem_word_count,
}
}
pub extern "C" fn sv_set_mem_word(
handle: SimHandle,
slot: SlotId,
row_index: u64,
value: WideView,
) {
let _ = set_mem_word_reporting_change(handle, slot, row_index, value);
}
pub fn set_mem_word_reporting_change(
handle: SimHandle,
slot: SlotId,
row_index: u64,
value: WideView,
) -> (StateCellId, bool) {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
let loc = slot_location_for(handle, slot);
let resolved = resolve_mem_row(loc, row_index);
let cell = handle.cell_for_slot(slot.0 as usize);
let n = resolved.elem_word_count as usize;
let aval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.aval, n) }.to_vec();
let bval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.bval, n) }.to_vec();
let result = commit_write_at(
handle.arena(),
handle.vc(),
resolved,
cell,
FourStateValue::Wide { aval, bval },
&mut (),
);
handle
.vc()
.views
.note_change(cell, Some(row_index), &result);
(cell, result.changed)
}
pub fn set_foreign_slots(handle: SimHandle, targets: &[(SimHandle, SlotId)]) -> u32 {
let this = unsafe { InstanceHandle::from_sim_handle(handle) };
let resolved = targets
.iter()
.map(|&(h, slot)| resolve_target(h, slot))
.collect::<Vec<_>>();
let views = vec![None; resolved.len()];
this.set_foreign(resolved, views);
this.own_slot_count() as u32
}
fn resolve_target(h: SimHandle, slot: SlotId) -> (*const InstanceHandle, usize) {
let target = unsafe { InstanceHandle::from_sim_handle(h) };
let idx = slot.0 as usize;
assert!(
idx < target.own_slot_count() || target.foreign_window(idx).is_none(),
"a foreign slot cannot name a windowed foreign slot; compose the windows first"
);
let (owner, idx) = target.resolve_slot(idx);
(owner as *const InstanceHandle, idx)
}
pub fn set_foreign_slots_windowed(
engine: &mut StateEngine,
handle: SimHandle,
targets: &[(SimHandle, SlotId, Option<super::AliasWindow>)],
) -> u32 {
let this = unsafe { InstanceHandle::from_sim_handle(handle) };
let mut resolved = Vec::with_capacity(targets.len());
let mut views = Vec::with_capacity(targets.len());
for &(h, slot, window) in targets {
let (owner, idx) = resolve_target(h, slot);
views.push(window.map(|w| {
let root = unsafe { &*owner }.cell_for_slot(idx);
(w, engine.alloc_view_cell(root, w))
}));
resolved.push((owner, idx));
}
this.set_foreign(resolved, views);
this.own_slot_count() as u32
}
#[inline]
pub fn resolve_foreign_slot(handle: SimHandle, slot: SlotId) -> (SimHandle, SlotId) {
let this = unsafe { InstanceHandle::from_sim_handle(handle) };
let (owner, idx) = this.resolve_slot(slot.0 as usize);
(
SimHandle(owner as *const InstanceHandle as *mut c_void),
SlotId(idx as u32),
)
}
#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_slot_owner"))]
pub extern "C" fn real_sv_rt_slot_owner(handle: SimHandle, slot: SlotId) -> SimHandle {
resolve_foreign_slot(handle, slot).0
}
#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_get_scalar"))]
pub extern "C" fn real_sv_get_scalar(handle: SimHandle, slot: SlotId) -> u128 {
let this = unsafe { InstanceHandle::from_sim_handle(handle) };
let idx = slot.0 as usize;
if idx >= this.own_slot_count()
&& let Some(window) = this.foreign_window(idx)
{
return match read_window(this, idx, window) {
FourStateValue::Narrow(v) => v,
FourStateValue::Wide { .. } => {
unreachable!("a scalar read of a windowed alias wider than 64 bits")
}
};
}
let (handle, slot) = resolve_foreign_slot(handle, slot);
sv_get_scalar(handle, slot)
}
#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_get_wide"))]
pub extern "C" fn real_sv_get_wide(handle: SimHandle, slot: SlotId) -> WideView {
let this = unsafe { InstanceHandle::from_sim_handle(handle) };
let idx = slot.0 as usize;
if idx >= this.own_slot_count()
&& let Some(window) = this.foreign_window(idx)
{
return scratch_wide(this, read_window(this, idx, window));
}
let (handle, slot) = resolve_foreign_slot(handle, slot);
sv_get_wide(handle, slot)
}
#[cold]
#[inline(never)]
fn read_window(this: &InstanceHandle, idx: usize, window: super::AliasWindow) -> FourStateValue {
let (owner, slot) = this.resolve_slot(idx);
let mut loc = slot_location_for(owner, SlotId(slot as u32));
if let Some(row) = window.row {
loc = resolve_mem_row(loc, row);
}
super::alias::window_value(&read_slot(owner.arena(), loc), window)
}
fn scratch_wide(this: &InstanceHandle, value: FourStateValue) -> WideView {
let FourStateValue::Wide { aval, bval } = value else {
unreachable!("a wide read of a windowed alias no wider than 64 bits")
};
let (aval, bval, word_count) = this.scratch_wide(aval, bval);
WideView {
aval,
bval,
word_count,
}
}
#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_get_mem_word"))]
pub extern "C" fn real_sv_get_mem_word(
handle: SimHandle,
slot: SlotId,
row_index: u64,
) -> WideView {
let (handle, slot) = resolve_foreign_slot(handle, slot);
sv_get_mem_word(handle, slot, row_index)
}
pub fn own_slot_count(handle: SimHandle) -> u32 {
unsafe { InstanceHandle::from_sim_handle(handle) }.own_slot_count() as u32
}
pub fn cell_for_slot(handle: SimHandle, slot: SlotId) -> StateCellId {
let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
handle.cell_for_slot(slot.0 as usize)
}
#[inline(always)]
pub fn sv_get_scalar_inline(handle: SimHandle, slot: SlotId) -> u128 {
sv_get_scalar(handle, slot)
}
#[inline(always)]
pub fn sv_set_scalar_inline(handle: SimHandle, slot: SlotId, value: u128) {
sv_set_scalar(handle, slot, value)
}
#[inline(always)]
pub fn sv_get_wide_inline(handle: SimHandle, slot: SlotId) -> WideView {
sv_get_wide(handle, slot)
}
#[inline(always)]
pub fn sv_set_wide_inline(handle: SimHandle, slot: SlotId, value: WideView) {
sv_set_wide(handle, slot, value)
}
#[inline(always)]
pub fn sv_get_mem_word_inline(handle: SimHandle, slot: SlotId, row_index: u64) -> WideView {
sv_get_mem_word(handle, slot, row_index)
}
#[inline(always)]
pub fn sv_set_mem_word_inline(handle: SimHandle, slot: SlotId, row_index: u64, value: WideView) {
sv_set_mem_word(handle, slot, row_index, value)
}
impl StateEngine {
fn commit_root(
&mut self,
cell: StateCellId,
row: Option<u64>,
value: FourStateValue,
) -> super::write::ChangeResult {
match row {
None => self.commit_write(cell, value, &mut ()),
Some(row) => {
let loc = resolve_mem_row(self.cells[cell.0 as usize], row);
let result = commit_write_at(&self.arena, &self.vc, loc, cell, value, &mut ());
self.vc.views.note_change(cell, Some(row), &result);
result
}
}
}
fn root_location(&self, info: &super::alias::ViewInfo) -> SlotLocation {
let loc = self.cells[info.root.0 as usize];
match info.window.row {
Some(row) => resolve_mem_row(loc, row),
None => loc,
}
}
fn write_view(
&mut self,
view: StateCellId,
info: super::alias::ViewInfo,
value: FourStateValue,
) -> bool {
let root_value = read_slot(&self.arena, self.root_location(&info));
let spliced = super::alias::splice_window(&root_value, info.window, &value);
self.vc.views.table().begin_write(view);
let result = self.commit_root(info.root, info.window.row, spliced);
self.vc.views.table().end_write();
result.changed
&& super::write::four_state_changed(
&super::alias::window_value(&result.old, info.window),
&super::alias::window_value(&result.new, info.window),
)
}
}
impl SignalAccess for StateEngine {
fn read(&self, cell: StateCellId) -> FourStateValue {
if let Some(info) = self.vc.views.info(cell) {
let root = read_slot(&self.arena, self.root_location(&info));
return super::alias::window_value(&root, info.window);
}
let loc = self.cells[cell.0 as usize];
read_slot(&self.arena, loc)
}
fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
if let Some(info) = self.vc.views.info(cell) {
return self.write_view(cell, info, value);
}
self.commit_write(cell, value, &mut ()).changed
}
fn read_row(&self, cell: StateCellId, row: u64) -> FourStateValue {
let loc = resolve_mem_row(self.cells[cell.0 as usize], row);
read_slot(&self.arena, loc)
}
fn write_row(&mut self, cell: StateCellId, row: u64, value: FourStateValue) -> bool {
self.commit_root(cell, Some(row), value).changed
}
fn take_alias_changes(&mut self) -> Vec<super::alias::AliasChange> {
self.vc.views.take_pending()
}
fn read_wide_window(
&self,
cell: StateCellId,
lo_word: usize,
words: usize,
) -> (Vec<u64>, Vec<u64>) {
let loc = self.cells[cell.0 as usize];
debug_assert_eq!(loc.class, SlotClass::Wide);
super::huge_mirror::read_window(&self.arena, loc.word_offset as usize, lo_word, words)
}
fn write_wide_window(
&mut self,
cell: StateCellId,
lo_word: usize,
aval: &[u64],
bval: &[u64],
) -> bool {
let loc = self.cells[cell.0 as usize];
if loc.class != SlotClass::Wide
|| self.vc.force.borrow().contains(cell)
|| self.vc.records_changes()
{
return super::write_wide_window_whole(self, cell, lo_word, aval, bval);
}
let changed = super::huge_mirror::store_window(
&self.arena,
loc.word_offset as usize,
loc.bit_width,
lo_word,
aval,
bval,
);
if changed {
self.dispatch.notify_scheduler(
cell,
EdgeKind::None,
self.vc.current_time(),
self.vc.current_region(),
);
}
changed
}
fn subscribe(
&mut self,
_cell: StateCellId,
role: ListenerRole,
sink: Box<dyn ValueChangeSink + Send>,
) {
self.dispatch.register(role, sink);
}
}
#[cfg(feature = "frontend")]
#[cfg(test)]
mod tests {
use super::*;
use crate::netlist::{SymbolKey, TemplateKey};
use crate::state::instance::TemplateRegistry;
use crate::state::layout::build_template_layout;
use crate::state::value::compile_source;
use crate::state::write::last_vc_record_for;
use crate::state::{SlotDescriptor, SlotOffset, StateLayout};
fn production_code(src: &str) -> String {
let marker = "#[cfg(test)]";
let mut result = String::with_capacity(src.len());
let mut cursor = 0usize;
loop {
let Some(rel_idx) = src[cursor..].find(marker) else {
result.push_str(&src[cursor..]);
break;
};
let idx = cursor + rel_idx;
let after = &src[idx + marker.len()..];
let after_trimmed = after.trim_start();
if after_trimmed.starts_with("mod tests") {
let trim_offset = after.len() - after_trimmed.len();
let mod_tests_abs = idx + marker.len() + trim_offset;
let after_keyword = &src[mod_tests_abs + "mod tests".len()..];
let brace_abs = after_keyword
.find('{')
.map(|rel| mod_tests_abs + "mod tests".len() + rel);
let close_abs = brace_abs.and_then(|b| find_matching_brace(src, b));
match close_abs {
Some(close_abs) => {
result.push_str(&src[cursor..idx]);
cursor = close_abs + 1;
continue;
}
None => {
result.push_str(&src[cursor..idx]);
break;
}
}
}
result.push_str(&src[cursor..idx + marker.len()]);
cursor = idx + marker.len();
}
result
}
fn find_matching_brace(src: &str, open_idx: usize) -> Option<usize> {
let bytes = src.as_bytes();
debug_assert_eq!(bytes.get(open_idx), Some(&b'{'));
let mut depth: i32 = 0;
let mut i = open_idx;
while i < bytes.len() {
match bytes[i] {
b'{' => {
depth += 1;
i += 1;
}
b'}' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
i += 1;
}
b'/' if bytes.get(i + 1) == Some(&b'/') => {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if bytes.get(i + 1) == Some(&b'*') => {
i += 2;
let mut comment_depth = 1u32;
while i < bytes.len() && comment_depth > 0 {
if bytes[i] == b'/' && bytes.get(i + 1) == Some(&b'*') {
comment_depth += 1;
i += 2;
} else if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
comment_depth -= 1;
i += 2;
} else {
i += 1;
}
}
}
b'"' => {
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
if bytes[i] == b'\\' {
i += 1;
}
i += 1;
}
i += 1;
}
b'\'' => {
const MAX_CHAR_LITERAL_LEN: usize = 12; let window_end = (i + 1 + MAX_CHAR_LITERAL_LEN).min(bytes.len());
let mut j = i + 1;
let mut found_close = None;
while j < window_end {
if bytes[j] == b'\n' {
break;
}
if bytes[j] == b'\'' && j > i + 1 {
found_close = Some(j);
break;
}
j += 1;
}
match found_close {
Some(close) => i = close + 1,
None => i += 1,
}
}
_ => {
i += 1;
}
}
}
None
}
#[test]
fn production_code_does_not_truncate_early_at_a_pre_mod_tests_cfg_test_item() {
let write_shaped = concat!(
"/// A `#[cfg(test)]`-visible peek at something.\n",
"#[cfg(test)]\n",
"pub fn last_vc_record_for() {}\n",
"\n",
"pub fn genuinely_production_and_must_stay_scanned() -> u32 { 1 }\n",
"\n",
"#[cfg(test)]\n",
"mod tests {\n",
" // test-only code\n",
"}\n",
);
let scanned = production_code(write_shaped);
assert!(
scanned.contains("genuinely_production_and_must_stay_scanned"),
"a genuine production fn between an early #[cfg(test)] helper and \
the real `mod tests` block must still be included in the scan"
);
assert!(
!scanned.contains("mod tests"),
"the real test module itself must be excluded"
);
let arena_shaped = concat!(
"impl Arena {\n",
" #[cfg(test)]\n",
" pub(crate) fn is_wide_plane_sealed(&self) -> bool { true }\n",
"\n",
" pub fn mode(&self) -> u8 { 0 }\n",
"}\n",
"\n",
"#[cfg(test)]\n",
"mod tests {\n",
" // test-only code\n",
"}\n",
);
let scanned = production_code(arena_shaped);
assert!(
scanned.contains("pub fn mode"),
"a genuine production fn after an early #[cfg(test)]-gated method \
must still be included in the scan"
);
assert!(
!scanned.contains("mod tests"),
"the real test module itself must be excluded"
);
}
#[test]
fn production_code_reaches_the_real_mod_tests_boundary_in_write_rs_and_arena_rs() {
let write_src = production_code(include_str!("write.rs"));
assert!(
write_src.contains("pub(crate) fn commit_write_at"),
"write.rs's real mod tests boundary must not truncate before its production fns"
);
assert!(
!write_src.contains("mod tests"),
"write.rs's scan must still exclude its real test module"
);
let arena_src = production_code(include_str!("arena.rs"));
for needed in [
"pub unsafe fn raw_inline_ptr",
"pub fn mode",
"pub fn ffi_view",
"pub fn wide_aval",
"pub fn wide_bval",
] {
assert!(
arena_src.contains(needed),
"arena.rs's scan must include the genuinely-production `{needed}`, \
which the original first-#[cfg(test)]-match truncation silently dropped"
);
}
assert!(
!arena_src.contains("mod tests"),
"arena.rs's scan must still exclude its real test module"
);
}
#[test]
fn production_code_resumes_scanning_after_the_real_mod_tests_closing_brace() {
let shaped = concat!(
"pub fn before_the_test_module() -> u32 { 1 }\n",
"\n",
"#[cfg(test)]\n",
"mod tests {\n",
" #[test]\n",
" fn uses_brace_shaped_literals() {\n",
" let open = '{';\n",
" let close = '}';\n",
" let s = \"} not a real close {\";\n",
" assert_eq!(open, open);\n",
" assert_eq!(close, close);\n",
" let _ = s;\n",
" }\n",
"}\n",
"\n",
"pub fn genuinely_production_placed_after_mod_tests() -> u32 { 2 }\n",
"\n",
"static mut GLOBAL_LEAK_AFTER_MOD_TESTS: u32 = 0;\n",
);
let scanned = production_code(shaped);
assert!(
scanned.contains("before_the_test_module"),
"content before the real mod tests block must still be scanned"
);
assert!(
!scanned.contains("uses_brace_shaped_literals"),
"the real test module's body must still be excluded, even though it \
contains '{{'/'}}' char literals and a \"}}...{{\" string literal that \
could desync a naive brace counter"
);
assert!(
scanned.contains("genuinely_production_placed_after_mod_tests"),
"a genuine production fn placed AFTER the real mod tests block's \
closing brace must still be scanned -- this is the exact gap the \
review finding reported"
);
assert!(
scanned.contains("GLOBAL_LEAK_AFTER_MOD_TESTS"),
"a `static mut` placed after the real mod tests block must still be \
visible to production_code's output, so the ST-152 scan built on top \
of it can catch it"
);
let two_modules = concat!(
"pub fn between_none_yet() -> u32 { 0 }\n",
"#[cfg(test)]\n",
"mod tests {\n",
" fn first() {}\n",
"}\n",
"pub fn between_the_two_modules() -> u32 { 1 }\n",
"#[cfg(test)]\n",
"mod tests {\n",
" fn second() {}\n",
"}\n",
"pub fn after_both_modules() -> u32 { 2 }\n",
);
let scanned_two = production_code(two_modules);
assert!(!scanned_two.contains("fn first"));
assert!(!scanned_two.contains("fn second"));
assert!(scanned_two.contains("between_the_two_modules"));
assert!(scanned_two.contains("after_both_modules"));
}
#[test]
fn inline_wrappers_produce_identical_results_to_the_extern_c_fns() {
let tmpl = TemplateKey {
definition: 10,
params_hash: 0,
};
let mut engine = engine_with_template(
"module m; logic [7:0] x; logic [127:0] w; endmodule\n",
tmpl,
);
let handle = engine.allocate_instance(tmpl).expect("template registered");
let h = handle.into_sim_handle();
let scalar_slot = SlotId(0);
let wide_slot = SlotId(1);
sv_set_scalar(h, scalar_slot, narrow_word(0x42, 0));
assert_eq!(
sv_get_scalar_inline(h, scalar_slot),
sv_get_scalar(h, scalar_slot),
"sv_get_scalar_inline must return exactly what sv_get_scalar returns"
);
sv_set_scalar_inline(h, scalar_slot, narrow_word(0x99, 0));
assert_eq!(sv_get_scalar(h, scalar_slot), narrow_word(0x99, 0));
let mut aval = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
let mut bval = [0x3333_3333_3333_3333u64, 0x4444_4444_4444_4444u64];
let pattern = WideView {
aval: aval.as_mut_ptr(),
bval: bval.as_mut_ptr(),
word_count: 2,
};
sv_set_wide_inline(h, wide_slot, pattern);
let via_extern_c = sv_get_wide(h, wide_slot);
let via_inline = sv_get_wide_inline(h, wide_slot);
unsafe {
let extern_c_aval = std::slice::from_raw_parts(via_extern_c.aval, 2);
let extern_c_bval = std::slice::from_raw_parts(via_extern_c.bval, 2);
let inline_aval = std::slice::from_raw_parts(via_inline.aval, 2);
let inline_bval = std::slice::from_raw_parts(via_inline.bval, 2);
assert_eq!(extern_c_aval, inline_aval);
assert_eq!(extern_c_bval, inline_bval);
}
unsafe {
drop(Box::from_raw(h.0 as *mut InstanceHandle));
}
}
#[test]
fn no_public_function_returns_a_raw_absolute_pointer_for_caller_arithmetic() {
let forbidden = forbidden_raw_pointer_patterns();
let accessor_impl_src = production_code(include_str!("accessor_impl.rs"));
let write_src = production_code(include_str!("write.rs"));
for (name, src) in [
("accessor_impl.rs", accessor_impl_src.as_str()),
("write.rs", write_src.as_str()),
] {
for pattern in &forbidden {
assert!(
!src.contains(pattern.as_str()),
"{name}'s production code must not return a raw `{pattern}` pointer \
from any function -- this crate's real word types are u128 (inline \
plane) and u64 (wide planes), not just u8"
);
}
}
assert!(
accessor_impl_src.contains("-> WideView"),
"sv_get_wide/sv_get_wide_inline must still return the named WideView type"
);
}
fn forbidden_raw_pointer_patterns() -> Vec<String> {
let widths = ["8", "16", "32", "64", "128"];
widths
.iter()
.flat_map(|w| [["*", "mut u", w].concat(), ["*", "const u", w].concat()])
.collect()
}
#[test]
fn forbidden_raw_pointer_scan_catches_this_crates_real_word_widths() {
let forbidden = forbidden_raw_pointer_patterns();
let u128_reintroduction = concat!(
"pub fn raw_base_ptr(handle: SimHandle) -> *mut u128 {\n",
" unimplemented!()\n",
"}\n",
);
assert!(
forbidden
.iter()
.any(|p| u128_reintroduction.contains(p.as_str())),
"a reintroduced *mut u128 raw-offset accessor must be caught by the \
forbidden-pattern list -- this is the arena's actual inline-plane word type"
);
let u64_reintroduction = concat!(
"pub fn instance_base_words(&self) -> (*mut u128, *mut u64, *mut u64) {\n",
" unimplemented!()\n",
"}\n",
);
assert!(
forbidden
.iter()
.any(|p| u64_reintroduction.contains(p.as_str())),
"a reintroduced *mut u64 raw-offset accessor must be caught by the \
forbidden-pattern list -- this is the arena's actual wide-plane word type"
);
let u8_only: Vec<String> = vec!["*mut u8".to_string(), "*const u8".to_string()];
assert!(
!u8_only
.iter()
.any(|p| u128_reintroduction.contains(p.as_str())),
"sanity check: the u128 shape must not contain the literal u8 substring"
);
assert!(
!u8_only
.iter()
.any(|p| u64_reintroduction.contains(p.as_str())),
"sanity check: the u64 shape must not contain the literal u8 substring"
);
}
#[test]
fn read_and_write_are_callable_without_constructing_any_kernel_or_process_type() {
use crate::state::write::SlotLocation;
use crate::state::{FourStateValue, SignalAccess, StateCellId, StateEngine};
let templates = TemplateRegistry::new();
let mut engine = StateEngine::new(templates);
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: StateCellId = engine.alloc_cell(loc);
let changed =
engine.commit_write(cell, FourStateValue::Narrow(narrow_word(0x2A, 0)), &mut ());
assert!(changed.changed);
match SignalAccess::read(&engine, cell) {
FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(0x2A, 0)),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
}
#[test]
fn state_engine_owns_every_state_owned_resource_directly_no_second_pointer() {
let templates = TemplateRegistry::new();
let mut engine = StateEngine::new(templates);
let _arena: &std::rc::Rc<super::super::arena::Arena> = &engine.arena;
let _templates: &std::rc::Rc<TemplateRegistry> = &engine.templates;
let _cells_len_before = engine.cells.len();
let (cell, _) = {
let word_offset = unsafe { engine.arena.alloc_inline_word() };
let loc = crate::state::write::SlotLocation {
class: SlotClass::Inline,
word_offset,
bit_width: 1,
elem_word_count: 1,
row_stride_words: 0,
num_rows: 0,
};
(engine.alloc_cell(loc), loc)
};
assert_eq!(
engine.cells.len(),
_cells_len_before + 1,
"cells is a direct field"
);
engine.commit_write(cell, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
engine.register_worker_thread();
let forbidden = forbidden_global_patterns();
let state_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/state");
let entries = std::fs::read_dir(&state_dir).expect("state/ directory is readable");
for entry in entries {
let entry = entry.expect("directory entry is readable");
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let contents = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{} must be readable: {e}", path.display()));
let scanned = production_code(&contents);
for forbidden_pattern in &forbidden {
assert!(
!scanned.contains(forbidden_pattern.as_str()),
"{}'s production code must not contain a free-standing global of the \
forbidden kind this test enforces -- every STATE-owned resource must be \
reachable exclusively through a direct StateEngine field",
path.display()
);
}
}
}
fn forbidden_global_patterns() -> Vec<String> {
vec![
["static", " mut"].concat(),
["lazy", "_static"].concat(),
["Once", "Lock"].concat(),
["Once", "Cell"].concat(),
["Mut", "ex<"].concat(),
["Rw", "Lock<"].concat(),
["Atomic", "Ptr<"].concat(),
]
}
#[test]
fn forbidden_global_scan_catches_mutex_rwlock_and_atomicptr_backed_globals() {
let forbidden = forbidden_global_patterns();
let mutex_leak = concat!(
"static GLOBAL_ARENA_REGISTRY: std::sync::Mutex<Option<std::rc::Rc<u8>>> =\n",
" std::sync::Mutex::new(None);\n",
);
assert!(
forbidden.iter().any(|p| mutex_leak.contains(p.as_str())),
"a Mutex-backed free-standing global must be caught by the forbidden-pattern list"
);
let rwlock_leak = "static GLOBAL: std::sync::RwLock<u8> = std::sync::RwLock::new(0);";
assert!(
forbidden.iter().any(|p| rwlock_leak.contains(p.as_str())),
"an RwLock-backed free-standing global must be caught by the forbidden-pattern list"
);
let atomic_ptr_leak = "static GLOBAL: std::sync::atomic::AtomicPtr<u8> = \
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());";
assert!(
forbidden
.iter()
.any(|p| atomic_ptr_leak.contains(p.as_str())),
"an AtomicPtr-backed free-standing global must be caught by the forbidden-pattern list"
);
let r1_patterns: Vec<String> = vec![
["static", " mut"].concat(),
["lazy", "_static"].concat(),
["Once", "Lock"].concat(),
["Once", "Cell"].concat(),
];
for shape in [mutex_leak, rwlock_leak, atomic_ptr_leak] {
assert!(
!r1_patterns.iter().any(|p| shape.contains(p.as_str())),
"sanity check: the r1-era pattern list must not already catch this shape"
);
}
let legitimate_counter = "static NEXT_ENGINE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);";
assert!(
!forbidden
.iter()
.any(|p| legitimate_counter.contains(p.as_str())),
"a plain AtomicU64 counter (no pointer to any STATE-owned resource) must not be flagged"
);
}
fn engine_with_template(src: &str, key: TemplateKey) -> StateEngine {
let design = compile_source(src).expect("valid SV source compiles");
let body = design
.top_instances()
.next()
.expect("one top instance")
.instance_body()
.expect("instance has a body");
let layout: StateLayout = build_template_layout(&body).expect("layout builds");
let templates = TemplateRegistry::new();
unsafe {
templates.register(key, layout);
}
StateEngine::new(templates)
}
fn engine_with_manual_wide_template(key: TemplateKey, elem_word_count: u32) -> StateEngine {
let layout = StateLayout {
slots: vec![SlotDescriptor {
key: SymbolKey(0),
offset: SlotOffset {
class: SlotClass::Wide,
word_offset: 0,
bit_width: elem_word_count * 64,
elem_word_count,
row_stride_words: 0,
num_rows: 0,
},
initial: None,
net_info: None,
}],
inline_words: 0,
wide_words: elem_word_count,
};
let templates = TemplateRegistry::new();
unsafe {
templates.register(key, layout);
}
StateEngine::new(templates)
}
fn narrow_word(val: u64, unk: u64) -> u128 {
(val as u128) | ((unk as u128) << 64)
}
#[test]
fn slot_location_for_threads_memory_row_stride_and_num_rows_through() {
let key = TemplateKey {
definition: 0,
params_hash: 0,
};
let layout = StateLayout {
slots: vec![SlotDescriptor {
key: SymbolKey(0),
offset: SlotOffset {
class: SlotClass::Memory,
word_offset: 0,
bit_width: 32,
elem_word_count: 2,
row_stride_words: 4,
num_rows: 6,
},
initial: None,
net_info: None,
}],
inline_words: 0,
wide_words: 2 * 6,
};
let templates = TemplateRegistry::new();
unsafe {
templates.register(key, layout);
}
let mut engine = StateEngine::new(templates);
let handle = engine.allocate_instance(key).expect("template registered");
let resolved = slot_location_for(&handle, SlotId(0));
assert_eq!(resolved.class, SlotClass::Memory);
assert_eq!(resolved.elem_word_count, 2);
assert_eq!(
resolved.row_stride_words, 4,
"slot_location_for must copy row_stride_words from the SlotOffset, not drop/zero it"
);
assert_eq!(
resolved.num_rows, 6,
"slot_location_for must copy num_rows from the SlotOffset, not drop/zero it"
);
}
#[test]
fn sv_get_set_scalar_round_trips_through_a_real_instance_handle() {
let tmpl = TemplateKey {
definition: 0,
params_hash: 0,
};
let mut engine = engine_with_template("module m; logic [7:0] x; endmodule\n", tmpl);
let handle_a = engine.allocate_instance(tmpl).expect("template registered");
let handle_b = engine.allocate_instance(tmpl).expect("template registered");
let h_a = handle_a.into_sim_handle();
let h_b = handle_b.into_sim_handle();
let slot = SlotId(0);
let pattern_a = narrow_word(0xAA, 0);
let pattern_b = narrow_word(0x55, 0);
assert_ne!(pattern_a, pattern_b);
sv_set_scalar(h_a, slot, pattern_a);
sv_set_scalar(h_b, slot, pattern_b);
assert_eq!(sv_get_scalar(h_a, slot), pattern_a);
assert_eq!(sv_get_scalar(h_b, slot), pattern_b);
unsafe {
drop(Box::from_raw(h_a.0 as *mut InstanceHandle));
drop(Box::from_raw(h_b.0 as *mut InstanceHandle));
}
}
#[test]
fn sv_set_scalar_reports_change_via_the_same_primitive_as_signal_access() {
let tmpl = TemplateKey {
definition: 1,
params_hash: 0,
};
let mut engine = engine_with_template("module m; logic [7:0] x; endmodule\n", tmpl);
let handle = engine.allocate_instance(tmpl).expect("template registered");
let cell = handle.cell_for_slot(0);
let h = handle.into_sim_handle();
let slot = SlotId(0);
sv_set_scalar(h, slot, narrow_word(1, 0));
let after_first = last_vc_record_for(cell);
assert!(
after_first.is_some(),
"the first write (from the zero default) must register a change"
);
sv_set_scalar(h, slot, narrow_word(1, 0));
let after_second = last_vc_record_for(cell).expect("still the first write's record");
match after_second.new {
FourStateValue::Narrow(w) => assert_eq!(
w,
narrow_word(1, 0),
"the identical second write must not append a new record"
),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
unsafe {
drop(Box::from_raw(h.0 as *mut InstanceHandle));
}
}
#[test]
fn sv_get_set_wide_slices_the_correct_word_range_per_instance() {
let tmpl = TemplateKey {
definition: 2,
params_hash: 0,
};
let mut engine = engine_with_template("module m; logic [127:0] w; endmodule\n", tmpl);
let handle_a = engine.allocate_instance(tmpl).expect("template registered");
let handle_b = engine.allocate_instance(tmpl).expect("template registered");
let h_a = handle_a.into_sim_handle();
let h_b = handle_b.into_sim_handle();
let slot = SlotId(0);
let mut aval_a = [0xAAAA_AAAA_AAAA_AAAAu64, 0xBBBB_BBBB_BBBB_BBBBu64];
let mut bval_a = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
let pattern_a = WideView {
aval: aval_a.as_mut_ptr(),
bval: bval_a.as_mut_ptr(),
word_count: 2,
};
sv_set_wide(h_a, slot, pattern_a);
let mut aval_b = [0xCCCC_CCCC_CCCC_CCCCu64, 0xDDDD_DDDD_DDDD_DDDDu64];
let mut bval_b = [0x3333_3333_3333_3333u64, 0x4444_4444_4444_4444u64];
let pattern_b = WideView {
aval: aval_b.as_mut_ptr(),
bval: bval_b.as_mut_ptr(),
word_count: 2,
};
sv_set_wide(h_b, slot, pattern_b);
let view_a = sv_get_wide(h_a, slot);
let read_a_aval = unsafe { std::slice::from_raw_parts(view_a.aval, 2) };
let read_a_bval = unsafe { std::slice::from_raw_parts(view_a.bval, 2) };
assert_eq!(read_a_aval, [aval_a[0], aval_a[1]]);
assert_eq!(read_a_bval, [bval_a[0], bval_a[1]]);
assert_ne!(
read_a_aval,
[aval_b[0], aval_b[1]],
"instance A's aval read must never reflect instance B's pattern"
);
assert_ne!(
read_a_bval,
[bval_b[0], bval_b[1]],
"instance A's bval read must never reflect instance B's pattern"
);
let view_b = sv_get_wide(h_b, slot);
let read_b_aval = unsafe { std::slice::from_raw_parts(view_b.aval, 2) };
let read_b_bval = unsafe { std::slice::from_raw_parts(view_b.bval, 2) };
assert_eq!(read_b_aval, [aval_b[0], aval_b[1]]);
assert_eq!(read_b_bval, [bval_b[0], bval_b[1]]);
unsafe {
drop(Box::from_raw(h_a.0 as *mut InstanceHandle));
drop(Box::from_raw(h_b.0 as *mut InstanceHandle));
}
}
#[test]
fn sv_set_wide_reports_change_via_the_same_primitive_as_signal_access() {
let tmpl = TemplateKey {
definition: 3,
params_hash: 0,
};
let mut engine = engine_with_template("module m; logic [127:0] w; endmodule\n", tmpl);
let handle = engine.allocate_instance(tmpl).expect("template registered");
let cell = handle.cell_for_slot(0);
let h = handle.into_sim_handle();
let slot = SlotId(0);
let mut aval = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
let mut bval = [0x5555_5555_5555_5555u64, 0x6666_6666_6666_6666u64];
let pattern = WideView {
aval: aval.as_mut_ptr(),
bval: bval.as_mut_ptr(),
word_count: 2,
};
sv_set_wide(h, slot, pattern);
let after_first = last_vc_record_for(cell);
assert!(
after_first.is_some(),
"the first wide write (from the zero default) must register a change"
);
match after_first.expect("checked is_some above").new {
FourStateValue::Wide { aval: a, bval: b } => {
assert_eq!(a, vec![aval[0], aval[1]]);
assert_eq!(b, vec![bval[0], bval[1]]);
}
FourStateValue::Narrow(_) => panic!("expected Wide"),
}
let pattern_again = WideView {
aval: aval.as_mut_ptr(),
bval: bval.as_mut_ptr(),
word_count: 2,
};
sv_set_wide(h, slot, pattern_again);
let after_second = last_vc_record_for(cell).expect("still the first write's record");
match after_second.new {
FourStateValue::Wide { aval: a, bval: b } => {
assert_eq!(
a,
vec![aval[0], aval[1]],
"the identical second write must not append a new record"
);
assert_eq!(
b,
vec![bval[0], bval[1]],
"the identical second write must not append a new record"
);
}
FourStateValue::Narrow(_) => panic!("expected Wide"),
}
unsafe {
drop(Box::from_raw(h.0 as *mut InstanceHandle));
}
}
#[test]
fn signal_access_read_write_agree_with_the_accessor_fns_on_the_same_storage() {
let tmpl = TemplateKey {
definition: 4,
params_hash: 0,
};
let mut engine = engine_with_template("module m; logic [7:0] x; endmodule\n", tmpl);
let handle = engine.allocate_instance(tmpl).expect("template registered");
let cell = handle.cell_for_slot(0);
let h = handle.into_sim_handle();
let slot = SlotId(0);
sv_set_scalar(h, slot, narrow_word(0x7B, 0));
match SignalAccess::read(&engine, cell) {
FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(0x7B, 0)),
FourStateValue::Wide { .. } => panic!("expected Narrow"),
}
unsafe {
drop(Box::from_raw(h.0 as *mut InstanceHandle));
}
}
#[test]
#[should_panic(expected = "refusing to grow the wide plane")]
fn sv_get_wide_view_survives_no_further_allocate_instance_call_it_must_panic_not_dangle() {
let tmpl = TemplateKey {
definition: 5,
params_hash: 0,
};
let mut engine = engine_with_template("module m; logic [127:0] w; endmodule\n", tmpl);
let handle = engine.allocate_instance(tmpl).expect("template registered");
let h = handle.into_sim_handle();
let slot = SlotId(0);
let view = sv_get_wide(h, slot);
assert_eq!(view.word_count, 2);
let _ = engine.allocate_instance(tmpl);
unreachable!("allocate_instance after sv_get_wide must panic, not return");
}
#[test]
fn sv_get_wide_view_stays_valid_across_a_later_sv_get_wide_and_a_later_wide_class_write() {
let tmpl = TemplateKey {
definition: 6,
params_hash: 0,
};
let mut engine = engine_with_manual_wide_template(tmpl, 2);
let handle_a = engine.allocate_instance(tmpl).expect("template registered");
let handle_b = engine.allocate_instance(tmpl).expect("template registered");
let h_a = handle_a.into_sim_handle();
let h_b = handle_b.into_sim_handle();
let slot = SlotId(0);
let mut seed_aval = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
let mut seed_bval = [0x3333_3333_3333_3333u64, 0x4444_4444_4444_4444u64];
sv_set_wide(
h_a,
slot,
WideView {
aval: seed_aval.as_mut_ptr(),
bval: seed_bval.as_mut_ptr(),
word_count: 2,
},
);
let view_a = sv_get_wide(h_a, slot);
assert_eq!(view_a.word_count, 2);
let view_b_probe = sv_get_wide(h_b, slot);
assert_eq!(view_b_probe.word_count, 2);
unsafe {
assert_eq!(std::slice::from_raw_parts(view_a.aval, 2), seed_aval);
assert_eq!(std::slice::from_raw_parts(view_a.bval, 2), seed_bval);
}
let mut other_aval = [0x5555_5555_5555_5555u64, 0x6666_6666_6666_6666u64];
let mut other_bval = [0x7777_7777_7777_7777u64, 0x8888_8888_8888_8888u64];
sv_set_wide(
h_b,
slot,
WideView {
aval: other_aval.as_mut_ptr(),
bval: other_bval.as_mut_ptr(),
word_count: 2,
},
);
unsafe {
assert_eq!(std::slice::from_raw_parts(view_a.aval, 2), seed_aval);
assert_eq!(std::slice::from_raw_parts(view_a.bval, 2), seed_bval);
*view_a.aval.add(0) = 0x9999_9999_9999_9999;
}
let view_a_again = sv_get_wide(h_a, slot);
unsafe {
assert_eq!(*view_a_again.aval, 0x9999_9999_9999_9999);
}
unsafe {
drop(Box::from_raw(h_a.0 as *mut InstanceHandle));
drop(Box::from_raw(h_b.0 as *mut InstanceHandle));
}
}
}