sv_sim_sim/state/accessor_impl.rs
1//! Task 9 — the accessor ABI implementation (`sv_get`/`sv_set` `_scalar`/`_wide`)
2//! and `SignalAccess` (ledger ST-013, ST-020, ST-021, ST-022, ST-023).
3//!
4//! Realizes the plan header's "what `SimHandle` points to" design decision:
5//! `SimHandle.0` *is* a `Box<InstanceHandle>::into_raw`-leaked pointer for
6//! this wave (`InstanceHandle::into_sim_handle`/`from_sim_handle` below).
7//! `sv_get_scalar`/`sv_set_scalar` resolve `absolute_word = handle.base
8//! .inline_base + layout.slots[slot.0].offset.word_offset` and read/write
9//! `arena.inline[absolute_word]` directly; `sv_get_wide`/`sv_set_wide` do the
10//! wide-plane analogue against `handle.base.wide_base`.
11//!
12//! `sv_set_scalar`/`sv_set_wide` route through `write::commit_write_at` — the
13//! exact primitive `StateEngine::commit_write` also calls (see
14//! `state::write`'s module doc comment for the Task 9 retrofit that makes
15//! this reachable with no `&mut StateEngine` in scope) — so VC detection is
16//! never bypassed by the ABI entry point and there is exactly one write path,
17//! never a second, ad hoc one.
18//!
19//! Task 10 — accessor discipline, the reference API as real non-JIT impl,
20//! single-`SimHandle` reachability (ledger ST-026, ST-027, ST-029, ST-152).
21//!
22//! **ST-029:** every fn in this module (`sv_get_scalar`/`sv_set_scalar`/
23//! `sv_get_wide`/`sv_set_wide` above, `SignalAccess::read`/`write`/`subscribe`
24//! below) is a plain Rust/`extern "C"` function, callable with nothing more
25//! than an `InstanceHandle`/`StateCellId` in hand — no `ProcessFn`/
26//! `CompiledKernel` invocation is required to reach any of them. This is the
27//! structural property that makes them usable by `$display`/VPI/the debugger
28//! once those crates exist: this crate's reference read/write API *is* the
29//! actual implementation those non-JIT consumers call into, never a second,
30//! parallel one only the compiled kernel path can reach.
31//!
32//! **ST-152:** the whole of STATE's owned state — the arena, the template
33//! registry, the `cells` table, the VC-tracking state (and, once Tasks 11/16
34//! land, the VC dispatch table and the undo log) — lives inside one
35//! `StateEngine` (`state::instance`), reached from the ABI side exclusively
36//! through the `InstanceHandle` a `SimHandle` points at (which itself, per
37//! this plan's header decision, is reachable only via `SimHandle`); no
38//! function in this crate accepts a second, independent "give me the arena"
39//! pointer parameter. See `StateEngine`'s own doc comment (`state::instance`)
40//! for the field-level half of this guarantee.
41
42use std::ffi::c_void;
43
44use sv_sim_abi::{SimHandle, SlotId, WideView};
45
46use super::instance::{InstanceHandle, StateEngine};
47use super::write::{SlotLocation, commit_write_at, read_slot, resolve_mem_row, write_slot};
48use super::{
49 EdgeKind, FourStateValue, ListenerRole, SignalAccess, SlotClass, StateCellId, ValueChangeSink,
50};
51
52impl InstanceHandle {
53 /// `Box::into_raw` leak — the handle's lifetime is the simulation run's,
54 /// matching how `sim_init`/`sim_final` bracket it per ARCHITECTURE §5.
55 pub fn into_sim_handle(self: Box<Self>) -> SimHandle {
56 SimHandle(Box::into_raw(self) as *mut c_void)
57 }
58
59 /// # Safety
60 /// `h` must be a `SimHandle` previously produced by [`Self::into_sim_handle`]
61 /// on an `InstanceHandle` that has not since been passed to a reclaiming
62 /// function (no such function exists this wave — every handle this crate
63 /// mints lives for the simulation run's duration, per ARCHITECTURE §5, so
64 /// this precondition is trivially satisfied by every caller in this
65 /// wave), and no other thread is concurrently reclaiming/freeing it.
66 pub unsafe fn from_sim_handle<'a>(h: SimHandle) -> &'a InstanceHandle {
67 // SAFETY: forwarded to this function's own `# Safety` section above.
68 unsafe { &*(h.0 as *const InstanceHandle) }
69 }
70}
71
72/// Resolves `slot` against `handle`'s template-relative `StateLayout` and
73/// this instance's own base, producing the already-*absolute* `SlotLocation`
74/// every accessor fn below reads/writes through — the one place this
75/// resolution happens, shared by every `sv_get_*`/`sv_set_*` fn (never
76/// re-derived ad hoc at each call site).
77pub(super) fn slot_location_for(handle: &InstanceHandle, slot: SlotId) -> SlotLocation {
78 let offset = handle.layout().slots[slot.0 as usize].offset;
79 let word_offset = match offset.class {
80 SlotClass::Inline => handle.base.inline_base + offset.word_offset,
81 SlotClass::Wide | SlotClass::Memory => handle.base.wide_base + offset.word_offset,
82 // Review fix (Task 14 r2, blocking finding 2): a real elaborated
83 // signal's `SlotOffset` (from `handle.layout()`) is never
84 // `ForceWide` — see `SlotClass::ForceWide`'s own doc comment.
85 SlotClass::ForceWide => unreachable!(
86 "slot_location_for: no SlotDescriptor/SlotOffset the layout builder \
87 produces is ever SlotClass::ForceWide"
88 ),
89 };
90 SlotLocation {
91 class: offset.class,
92 word_offset,
93 bit_width: offset.bit_width,
94 elem_word_count: offset.elem_word_count,
95 row_stride_words: offset.row_stride_words,
96 num_rows: offset.num_rows,
97 }
98}
99
100/// `slot` of `handle`: its cell, bit width, whether it is wide, and current
101/// value (`force`/`release`, `state::force`).
102///
103/// For a windowed alias (a ref port bound to a select) the state is the
104/// *target's*: its cell, width and value, plus the window's offset in it (a
105/// force of the alias is a force of those bits of the target). `Err` when
106/// the target is an element of a memory, which `force` does not reach.
107pub(crate) fn slot_state(
108 handle: SimHandle,
109 slot: SlotId,
110) -> Result<(StateCellId, u32, bool, FourStateValue, u32), &'static str> {
111 // SAFETY: see `sv_get_scalar`.
112 let this = unsafe { InstanceHandle::from_sim_handle(handle) };
113 let idx = slot.0 as usize;
114 let window = if idx >= this.own_slot_count() {
115 this.foreign_window(idx)
116 } else {
117 None
118 };
119 if let Some(w) = &window
120 && w.row.is_some()
121 {
122 // `real_sv_rt_force`/`release` route these to the row path first.
123 return Err("force/release of a ref port bound to an element of an unpacked array");
124 }
125 let (handle, slot) = resolve_foreign_slot(handle, slot);
126 // SAFETY: see `sv_get_scalar`.
127 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
128 let loc = slot_location_for(handle, slot);
129 let cell = handle.cell_for_slot(slot.0 as usize);
130 let value = super::write::read_slot(handle.arena(), loc);
131 Ok((
132 cell,
133 loc.bit_width,
134 loc.class == SlotClass::Wide,
135 value,
136 window.map_or(0, |w| w.lo),
137 ))
138}
139
140/// The row (and the bit offset in it) of the unpacked-array element a foreign
141/// `slot` of `handle` -- a ref port bound to `arr[i]` (LRM 23.3.3.2) --
142/// stands for; `None` for any other slot. A `force`/`release` of such a slot
143/// is one of that row ([`row_state`] follows the slot to its owner).
144pub(crate) fn slot_row_alias(handle: SimHandle, slot: SlotId) -> Option<(u64, u32)> {
145 // SAFETY: see `sv_get_scalar`.
146 let this = unsafe { InstanceHandle::from_sim_handle(handle) };
147 let idx = slot.0 as usize;
148 if idx < this.own_slot_count() {
149 return None;
150 }
151 let w = this.foreign_window(idx)?;
152 w.row.map(|row| (row, w.lo))
153}
154
155/// The state a `force`/`release` of row `row` of memory `slot` needs: the
156/// memory's cell, the row's word offset (its key in the force table), its
157/// bit width and its value now. A foreign slot is followed to its owner.
158pub(crate) fn row_state(
159 handle: SimHandle,
160 slot: SlotId,
161 row: u64,
162) -> (StateCellId, u32, u32, FourStateValue) {
163 let (handle, slot) = resolve_foreign_slot(handle, slot);
164 // SAFETY: see `sv_get_scalar`.
165 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
166 let loc = resolve_mem_row(slot_location_for(handle, slot), row);
167 let cell = handle.cell_for_slot(slot.0 as usize);
168 let value = super::write::read_slot(handle.arena(), loc);
169 (cell, loc.word_offset, loc.bit_width, value)
170}
171
172/// Copies `src`'s current value into `dst` of the same instance without
173/// reporting a value change: the sampled value functions' hidden P/S cells
174/// (`crate::sva::sampled`) are history, not signals -- no listener waits on
175/// them, no waveform shows them, and their updates must not wake a process.
176/// `src` may be a foreign slot (a hierarchically connected signal); `dst`
177/// is always one of the instance's own cells, of the same class and width
178/// as `src` by construction (`hidden_slots` clones the source's shape).
179pub fn copy_slot_quiet(handle: SimHandle, src: SlotId, dst: SlotId) {
180 // SAFETY: see `sv_get_scalar`.
181 let dst_h = unsafe { InstanceHandle::from_sim_handle(handle) };
182 let idx = src.0 as usize;
183 if idx >= dst_h.own_slot_count()
184 && let Some(window) = dst_h.foreign_window(idx)
185 {
186 let value = read_window(dst_h, idx, window);
187 write_slot(dst_h.arena(), slot_location_for(dst_h, dst), &value);
188 return;
189 }
190 let (src_handle, src) = resolve_foreign_slot(handle, src);
191 let src_h = unsafe { InstanceHandle::from_sim_handle(src_handle) };
192 let value = read_slot(src_h.arena(), slot_location_for(src_h, src));
193 write_slot(dst_h.arena(), slot_location_for(dst_h, dst), &value);
194}
195
196/// Runs `f` on the force table of `handle`'s engine (`state::force`).
197pub(crate) fn with_force_table<R>(
198 handle: SimHandle,
199 f: impl FnOnce(&mut super::force::ForceTable) -> R,
200) -> R {
201 // SAFETY: see `sv_get_scalar`.
202 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
203 f(&mut handle.vc().force.borrow_mut())
204}
205
206/// Narrow (`SlotClass::Inline`) scalar read (ST-020): `sv_sim_abi::accessor`'s
207/// FIXED `val` low 64 bits / `unk` high 64 bits layout, read straight out of
208/// the arena's inline plane at `handle`'s own base-applied word offset.
209pub extern "C" fn sv_get_scalar(handle: SimHandle, slot: SlotId) -> u128 {
210 // SAFETY: see `InstanceHandle::from_sim_handle`'s own `# Safety` section
211 // — this fn's only documented callers (compiled-kernel code, or this
212 // crate's own tests) pass back a `SimHandle` this crate itself minted
213 // via `into_sim_handle`.
214 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
215 let loc = slot_location_for(handle, slot);
216 handle.arena().inline()[loc.word_offset as usize]
217}
218
219/// Narrow scalar write (ST-021): writes `value` and reports whether it
220/// changed by routing through [`commit_write_at`] — the same VC-detection
221/// primitive `StateEngine::commit_write` uses — never a second, parallel
222/// write path. The `extern "C"` signature itself returns `()` (the frozen
223/// ABI type alias, `sv_sim_abi::accessor::SvSetScalarFn`); the change report
224/// lands in the same speculative VC buffer every other `commit_write_at`
225/// caller's changes do, observable via `write::last_vc_record_for` in tests.
226pub extern "C" fn sv_set_scalar(handle: SimHandle, slot: SlotId, value: u128) {
227 // SAFETY: see `sv_get_scalar`.
228 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
229 let loc = slot_location_for(handle, slot);
230 let cell = handle.cell_for_slot(slot.0 as usize);
231 let result = commit_write_at(
232 handle.arena(),
233 handle.vc(),
234 loc,
235 cell,
236 FourStateValue::Narrow(value),
237 &mut (),
238 );
239 handle.vc().views.note_change(cell, None, &result);
240}
241
242/// Wide (`SlotClass::Wide`) non-memory read (ST-022): a non-owning
243/// [`WideView`] over `handle`'s own `aval`/`bval` plane words at `slot`'s
244/// base-applied offset — `word_count` words starting at that offset in each
245/// plane, per `sv_sim_abi::accessor`'s documented shape.
246pub extern "C" fn sv_get_wide(handle: SimHandle, slot: SlotId) -> WideView {
247 // SAFETY: see `sv_get_scalar`.
248 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
249 let loc = slot_location_for(handle, slot);
250 let start = loc.word_offset as usize;
251 // Review fix (Task 9 r3, blocking finding 1): seal the arena's wide
252 // planes *before* handing out pointers into their live storage below.
253 // The returned `WideView`'s `aval`/`bval` pointers are retained by the
254 // caller past this call's return (a non-owning *view*, per `WideView`'s
255 // own doc comment, deliberately aliasing the arena's own live storage,
256 // not a copy) -- unlike `sv_get_scalar`/`sv_set_scalar`/`sv_set_wide`,
257 // none of which ever caches a pointer across calls. Without this seal, a
258 // later `StateEngine::allocate_instance` growing `wide_aval`/`wide_bval`
259 // via `Arena::alloc_wide_words` (a plain growable `Vec<u64>`, not
260 // individually boxed per allocation the way `TemplateRegistry` boxes
261 // each `StateLayout`) could reallocate their backing buffer and dangle
262 // this exact pointer pair -- a genuine use-after-free. `seal_wide_plane`
263 // makes every later nonzero-word `alloc_wide_words` call panic instead
264 // (see `Arena::wide_sealed`'s doc comment), which is what actually makes
265 // the *reallocation* half of the SAFETY argument below true.
266 handle.arena().seal_wide_plane();
267 // Review fix (Task 9 r4, blocking finding): this used to read
268 // `handle.arena().wide_aval_mut()[start..].as_mut_ptr()` -- forming a
269 // fresh `&mut [u64]` reborrow of the *whole* wide plane on every call.
270 // Under Stacked Borrows that reborrow is retagged from scratch each
271 // time, which invalidates any pointer a *previous* call like this one
272 // returned the instant *any* later `wide_aval_mut`/`wide_bval_mut` call
273 // happens anywhere against this `Arena` -- the very next `sv_get_wide`
274 // for a different instance/slot (no write required), or the next
275 // wide-class `commit_write_at` write via `write::write_slot`. Confirmed
276 // by an isolated Miri repro of exactly this pattern (both a later
277 // unrelated wide-class write and a second, read-only-shaped
278 // `sv_get_wide` call with zero intervening writes reproduce Miri's
279 // "tag does not exist in the borrow stack" error against the first
280 // pointer). `seal_wide_plane` above only prevents the Vec from
281 // *reallocating*; it does nothing about this separate, far-more-easily
282 // triggered reborrow-invalidation hazard.
283 //
284 // SAFETY: `start..start+elem_word_count` was reserved for this
285 // instance's slot by `allocate_instance`'s bulk wide-plane allocation
286 // and is not touched by any other instance's own bases (Task 7's
287 // ST-067/ST-136 disjoint-base guarantee), so the pointer arithmetic
288 // below stays within the plane's allocated length. `Arena::wide_aval_ptr`/
289 // `wide_bval_ptr` never form a `&mut [u64]`/`&[u64]` over the plane's
290 // buffer (see their own doc comments for the full argument) -- the
291 // pointers they return keep the buffer's original allocation tag for
292 // the Arena's whole life once sealed, so they stay valid to dereference
293 // no matter how many later `wide_aval_mut`/`wide_bval_mut`-based reads
294 // or writes (from `write::write_slot`, `read_slot`, `rollback::restore`,
295 // or another `sv_get_wide` call) happen against this same plane
296 // afterward: those all reborrow a *child* tag on top of the buffer's
297 // root tag, never invalidating the root tag itself (Miri-verified; see
298 // `sv_get_wide_view_stays_valid_across_a_later_sv_get_wide_and_a_later_
299 // wide_class_write` below, the Miri-reachable regression test for this
300 // exact fix). `seal_wide_plane` just above is what keeps the buffer's
301 // *address* (not merely its tag) stable for the rest of this Arena's
302 // life, which this argument also depends on.
303 let aval = unsafe { handle.arena().wide_aval_ptr().add(start) };
304 let bval = unsafe { handle.arena().wide_bval_ptr().add(start) };
305 WideView {
306 aval,
307 bval,
308 word_count: loc.elem_word_count,
309 }
310}
311
312/// Wide non-memory write (ST-023): copies `value`'s `elem_word_count` words
313/// out of the caller-supplied [`WideView`] into an owned [`FourStateValue::Wide`]
314/// and routes through [`commit_write_at`] — the wide-tier sibling of
315/// `sv_set_scalar`, through the identical shared primitive.
316pub extern "C" fn sv_set_wide(handle: SimHandle, slot: SlotId, value: WideView) {
317 // SAFETY: see `sv_get_scalar`.
318 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
319 let loc = slot_location_for(handle, slot);
320 let cell = handle.cell_for_slot(slot.0 as usize);
321 let n = loc.elem_word_count as usize;
322 // SAFETY: per the accessor-ABI contract (`sv_sim_abi::accessor`), a
323 // legitimate `SvSetWideFn` caller supplies a `WideView` whose `aval`/
324 // `bval` point at (at least) `word_count` contiguous, initialized `u64`s
325 // it owns for at least the duration of this call; this fn's own callers
326 // (this crate's tests) uphold that by construction (`n` here comes from
327 // `handle`'s own slot shape, never from `value.word_count`, so a
328 // mismatched `value` would read past/short of its own buffer -- the
329 // caller's obligation, not something this fn can check from a raw
330 // pointer pair alone).
331 let aval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.aval, n) }.to_vec();
332 let bval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.bval, n) }.to_vec();
333 let result = commit_write_at(
334 handle.arena(),
335 handle.vc(),
336 loc,
337 cell,
338 FourStateValue::Wide { aval, bval },
339 &mut (),
340 );
341 handle.vc().views.note_change(cell, None, &result);
342}
343
344/// Task 12 (ST-024, ST-025, ST-114): memory-word accessors. `slot` addresses
345/// the whole `SlotClass::Memory` array the same way `sv_get_wide`/
346/// `sv_set_wide` address a whole `SlotClass::Wide` signal
347/// ([`slot_location_for`] is shared, unchanged); the one extra step is
348/// [`resolve_mem_row`], applied to that base location with the caller's
349/// *runtime* `row_index`, per state.md §3's pinned `word_offset' =
350/// word_offset + row_stride_words * row_index` formula. Both fns panic
351/// (never silently read/write past the array) if `row_index` is out of
352/// bounds or `slot` does not resolve to a `SlotClass::Memory` location — see
353/// [`resolve_mem_row`]'s own doc comment for the exact panic conditions.
354///
355/// This is the shape a future `sv_rt_readmemh` (RUNTIME's own, not built this
356/// wave) will call once per parsed word: `sv_set_mem_word(handle, slot,
357/// row_index, parsed_word_view)` in a loop, never touching `Arena` directly
358/// (ST-114's literal acceptance text).
359///
360/// **ST-024:** the `sv_get_wide`-shaped read counterpart, through the same
361/// `resolve_mem_row` step and the same live-arena-aliasing `WideView` shape
362/// `sv_get_wide` returns — see that function's own doc comment for the full
363/// seal/pointer-stability argument, which applies identically here (the same
364/// `handle.arena().seal_wide_plane()` call, the same wide planes, just at a
365/// row-resolved offset instead of the slot's own base offset).
366pub extern "C" fn sv_get_mem_word(handle: SimHandle, slot: SlotId, row_index: u64) -> WideView {
367 // SAFETY: see `sv_get_scalar`.
368 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
369 let loc = slot_location_for(handle, slot);
370 let resolved = resolve_mem_row(loc, row_index);
371 let start = resolved.word_offset as usize;
372 // See `sv_get_wide`'s own doc comment for why sealing before handing out
373 // pointers into the live wide-plane storage is required here too.
374 handle.arena().seal_wide_plane();
375 // SAFETY: `start..start+elem_word_count` was reserved for this row within
376 // this instance's memory slot by `allocate_instance`'s bulk wide-plane
377 // allocation (the whole memory's `num_rows * row_stride_words` words are
378 // disjoint from every other instance's own base, and `resolve_mem_row`
379 // asserted `row_index < num_rows` above, so this row's own sub-range
380 // stays within that allocation) — the identical pointer-stability
381 // argument `sv_get_wide` documents applies unchanged, since both read the
382 // same sealed wide planes via the same non-`&mut`-forming raw-pointer
383 // accessors.
384 let aval = unsafe { handle.arena().wide_aval_ptr().add(start) };
385 let bval = unsafe { handle.arena().wide_bval_ptr().add(start) };
386 WideView {
387 aval,
388 bval,
389 word_count: resolved.elem_word_count,
390 }
391}
392
393/// **ST-025:** the `sv_set_wide`-shaped write counterpart, through the same
394/// `resolve_mem_row` step and the same `commit_write_at` primitive every
395/// other write in this crate goes through — never a second, ad hoc write
396/// path. `cell` is this slot's one whole-memory `StateCellId` (the same
397/// `handle.cell_for_slot(slot.0)` `sv_set_wide` uses; a `SlotClass::Memory`
398/// slot mints exactly one cell for the whole array, not one per row — see
399/// `StateEngine::allocate_instance`'s own per-slot loop), so a VC record from
400/// this fn reports "this memory changed", identifying the row via `at`/the
401/// caller's own `row_index`, exactly as `sv_set_wide` already does for a
402/// whole non-memory wide signal.
403pub extern "C" fn sv_set_mem_word(
404 handle: SimHandle,
405 slot: SlotId,
406 row_index: u64,
407 value: WideView,
408) {
409 let _ = set_mem_word_reporting_change(handle, slot, row_index, value);
410}
411
412/// [`sv_set_mem_word`]'s body, returning the memory's whole-array cell and
413/// whether the row's stored value actually changed -- what a
414/// scheduler-aware caller (`sched::kernel_adapter::real_sv_set_mem_word`)
415/// needs to wake processes sensitive to the memory.
416pub fn set_mem_word_reporting_change(
417 handle: SimHandle,
418 slot: SlotId,
419 row_index: u64,
420 value: WideView,
421) -> (StateCellId, bool) {
422 // SAFETY: see `sv_get_scalar`.
423 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
424 let loc = slot_location_for(handle, slot);
425 let resolved = resolve_mem_row(loc, row_index);
426 let cell = handle.cell_for_slot(slot.0 as usize);
427 let n = resolved.elem_word_count as usize;
428 // SAFETY: per the accessor-ABI contract (`sv_sim_abi::accessor`), a
429 // legitimate `SvSetMemWordFn` caller supplies a `WideView` whose `aval`/
430 // `bval` point at (at least) one row's own `elem_word_count` contiguous,
431 // initialized `u64`s it owns for at least the duration of this call --
432 // the same obligation `sv_set_wide` documents, `n` here likewise coming
433 // from `resolved`'s own row shape, never from `value.word_count`.
434 let aval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.aval, n) }.to_vec();
435 let bval: Vec<u64> = unsafe { std::slice::from_raw_parts(value.bval, n) }.to_vec();
436 let result = commit_write_at(
437 handle.arena(),
438 handle.vc(),
439 resolved,
440 cell,
441 FourStateValue::Wide { aval, bval },
442 &mut (),
443 );
444 handle
445 .vc()
446 .views
447 .note_change(cell, Some(row_index), &result);
448 (cell, result.changed)
449}
450
451/// Task F3 (`docs/superpowers/plans/2026-09-27-wave-A2-slice-finish.md`,
452/// CG-125/NL-035): resolves `slot` (the same `SlotId` `sv_get_scalar`/
453/// `sv_set_scalar` already address by) to the absolute [`StateCellId`] a
454/// synthetic inter-instance port-connection process needs for
455/// [`crate::sched::SchedulerOps::commit_blocking_write`]/
456/// [`crate::sched::StaticSensitivity`] (registering a wake on the source
457/// side, writing through the canonical commit path on the destination side).
458/// Every other `StateCellId` in this crate is minted directly by
459/// `StateEngine::alloc_cell`/`allocate_instance`'s own per-slot loop
460/// (`InstanceHandle::cell_for_slot`, `pub(crate)`); this is the one place a
461/// caller *outside* this crate (`sv-sim-bin`'s hierarchy pipeline, which
462/// holds a `SimHandle`+`SlotId` pair from its own signal-slot bookkeeping but
463/// never runs `allocate_instance`'s loop itself) needs to look one back up.
464/// Gives `handle`'s instance its foreign slots: slot `own + k` (where `own`
465/// is the instance's own slot count, returned) names `targets[k]`, another
466/// instance's own slot. A target that is itself a foreign slot is followed
467/// to its owner.
468pub fn set_foreign_slots(handle: SimHandle, targets: &[(SimHandle, SlotId)]) -> u32 {
469 // SAFETY: see `sv_get_scalar`.
470 let this = unsafe { InstanceHandle::from_sim_handle(handle) };
471 let resolved = targets
472 .iter()
473 .map(|&(h, slot)| resolve_target(h, slot))
474 .collect::<Vec<_>>();
475 let views = vec![None; resolved.len()];
476 this.set_foreign(resolved, views);
477 this.own_slot_count() as u32
478}
479
480/// The own slot a foreign-slot target names (a target that is itself a
481/// foreign slot is followed to its owner). A target that is a *windowed*
482/// foreign slot cannot be followed -- the caller composes the windows first.
483fn resolve_target(h: SimHandle, slot: SlotId) -> (*const InstanceHandle, usize) {
484 // SAFETY: see `sv_get_scalar`.
485 let target = unsafe { InstanceHandle::from_sim_handle(h) };
486 let idx = slot.0 as usize;
487 assert!(
488 idx < target.own_slot_count() || target.foreign_window(idx).is_none(),
489 "a foreign slot cannot name a windowed foreign slot; compose the windows first"
490 );
491 let (owner, idx) = target.resolve_slot(idx);
492 (owner as *const InstanceHandle, idx)
493}
494
495/// [`set_foreign_slots`] where a target may be a window of its slot -- a ref
496/// port bound to a select (IEEE 1800-2023 23.3.3.2, 13.5.2): such a foreign
497/// slot is a *view* of the target (`state::alias`) with its own cell, so a
498/// wait on it wakes only when the window changes.
499pub fn set_foreign_slots_windowed(
500 engine: &mut StateEngine,
501 handle: SimHandle,
502 targets: &[(SimHandle, SlotId, Option<super::AliasWindow>)],
503) -> u32 {
504 // SAFETY: see `sv_get_scalar`.
505 let this = unsafe { InstanceHandle::from_sim_handle(handle) };
506 let mut resolved = Vec::with_capacity(targets.len());
507 let mut views = Vec::with_capacity(targets.len());
508 for &(h, slot, window) in targets {
509 let (owner, idx) = resolve_target(h, slot);
510 views.push(window.map(|w| {
511 // SAFETY: `owner` is a live handle (`resolve_target`).
512 let root = unsafe { &*owner }.cell_for_slot(idx);
513 (w, engine.alloc_view_cell(root, w))
514 }));
515 resolved.push((owner, idx));
516 }
517 this.set_foreign(resolved, views);
518 this.own_slot_count() as u32
519}
520
521/// `(handle, slot)` itself for one of the instance's own slots; for a
522/// foreign slot (see [`set_foreign_slots`]), the instance and slot it
523/// names. Every production entry point that reads or writes a slot's
524/// storage for a compiled kernel resolves through this first
525/// (`real_sv_get_*`, `real_sv_set_mem_word`, force); a cell lookup needs no
526/// resolving (a foreign slot's cell is in the instance's own cell table).
527#[inline]
528pub fn resolve_foreign_slot(handle: SimHandle, slot: SlotId) -> (SimHandle, SlotId) {
529 // SAFETY: see `sv_get_scalar`.
530 let this = unsafe { InstanceHandle::from_sim_handle(handle) };
531 let (owner, idx) = this.resolve_slot(slot.0 as usize);
532 (
533 SimHandle(owner as *const InstanceHandle as *mut c_void),
534 SlotId(idx as u32),
535 )
536}
537
538/// `SimHandle sv_rt_slot_owner(SimHandle, SlotId)`: the instance whose storage
539/// `slot` of `handle` is -- `handle` itself for one of its own slots, the
540/// instance a foreign slot names otherwise (see [`set_foreign_slots`]). A
541/// hierarchical call into another instance (`u1.f()`, LRM 23.6) passes this
542/// as the callee's instance handle, so the callee's memories and block
543/// events are `u1`'s own.
544#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_slot_owner"))]
545pub extern "C" fn real_sv_rt_slot_owner(handle: SimHandle, slot: SlotId) -> SimHandle {
546 resolve_foreign_slot(handle, slot).0
547}
548
549/// The production binding of the `sv_get_scalar` ABI symbol:
550/// [`sv_get_scalar`] after [`resolve_foreign_slot`].
551#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_get_scalar"))]
552pub extern "C" fn real_sv_get_scalar(handle: SimHandle, slot: SlotId) -> u128 {
553 // SAFETY: see `sv_get_scalar`.
554 let this = unsafe { InstanceHandle::from_sim_handle(handle) };
555 let idx = slot.0 as usize;
556 if idx >= this.own_slot_count()
557 && let Some(window) = this.foreign_window(idx)
558 {
559 return match read_window(this, idx, window) {
560 FourStateValue::Narrow(v) => v,
561 FourStateValue::Wide { .. } => {
562 unreachable!("a scalar read of a windowed alias wider than 64 bits")
563 }
564 };
565 }
566 let (handle, slot) = resolve_foreign_slot(handle, slot);
567 sv_get_scalar(handle, slot)
568}
569
570/// The production binding of `sv_get_wide` (see [`real_sv_get_scalar`]).
571#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_get_wide"))]
572pub extern "C" fn real_sv_get_wide(handle: SimHandle, slot: SlotId) -> WideView {
573 // SAFETY: see `sv_get_scalar`.
574 let this = unsafe { InstanceHandle::from_sim_handle(handle) };
575 let idx = slot.0 as usize;
576 if idx >= this.own_slot_count()
577 && let Some(window) = this.foreign_window(idx)
578 {
579 return scratch_wide(this, read_window(this, idx, window));
580 }
581 let (handle, slot) = resolve_foreign_slot(handle, slot);
582 sv_get_wide(handle, slot)
583}
584
585/// The current value of the window foreign slot `idx` of `this` stands for.
586#[cold]
587#[inline(never)]
588fn read_window(this: &InstanceHandle, idx: usize, window: super::AliasWindow) -> FourStateValue {
589 let (owner, slot) = this.resolve_slot(idx);
590 let mut loc = slot_location_for(owner, SlotId(slot as u32));
591 if let Some(row) = window.row {
592 loc = resolve_mem_row(loc, row);
593 }
594 super::alias::window_value(&read_slot(owner.arena(), loc), window)
595}
596
597fn scratch_wide(this: &InstanceHandle, value: FourStateValue) -> WideView {
598 let FourStateValue::Wide { aval, bval } = value else {
599 unreachable!("a wide read of a windowed alias no wider than 64 bits")
600 };
601 let (aval, bval, word_count) = this.scratch_wide(aval, bval);
602 WideView {
603 aval,
604 bval,
605 word_count,
606 }
607}
608
609/// The production binding of `sv_get_mem_word` (see [`real_sv_get_scalar`]).
610#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_get_mem_word"))]
611pub extern "C" fn real_sv_get_mem_word(
612 handle: SimHandle,
613 slot: SlotId,
614 row_index: u64,
615) -> WideView {
616 let (handle, slot) = resolve_foreign_slot(handle, slot);
617 sv_get_mem_word(handle, slot, row_index)
618}
619
620/// The number of slots of `handle`'s instance's own layout -- the first
621/// foreign slot's number.
622pub fn own_slot_count(handle: SimHandle) -> u32 {
623 // SAFETY: see `sv_get_scalar`.
624 unsafe { InstanceHandle::from_sim_handle(handle) }.own_slot_count() as u32
625}
626
627pub fn cell_for_slot(handle: SimHandle, slot: SlotId) -> StateCellId {
628 // SAFETY: see `sv_get_scalar`.
629 let handle = unsafe { InstanceHandle::from_sim_handle(handle) };
630 handle.cell_for_slot(slot.0 as usize)
631}
632
633/// Task 10 (ST-026): the concrete artifact ARCHITECTURE §7 item 1's
634/// "accessor-ABI as the contract with `alwaysinline` wrappers" resolves to on
635/// STATE's side — a thin, `#[inline(always)]` Rust-level wrapper around the
636/// `extern "C"` fn above, a pure pass-through never a second, divergent
637/// implementation. CODEGEN is expected to call *these* (or emit its own
638/// `alwaysinline`-attributed LLVM declaration of the `extern "C"` symbol
639/// directly — either way, STATE's obligation is only that an inlinable entry
640/// point exists and is documented as the one sanctioned call shape).
641#[inline(always)]
642pub fn sv_get_scalar_inline(handle: SimHandle, slot: SlotId) -> u128 {
643 sv_get_scalar(handle, slot)
644}
645
646/// The `sv_set_scalar` twin of [`sv_get_scalar_inline`] (ST-026).
647#[inline(always)]
648pub fn sv_set_scalar_inline(handle: SimHandle, slot: SlotId, value: u128) {
649 sv_set_scalar(handle, slot, value)
650}
651
652/// The `sv_get_wide` twin of [`sv_get_scalar_inline`] (ST-026).
653#[inline(always)]
654pub fn sv_get_wide_inline(handle: SimHandle, slot: SlotId) -> WideView {
655 sv_get_wide(handle, slot)
656}
657
658/// The `sv_set_wide` twin of [`sv_get_scalar_inline`] (ST-026).
659#[inline(always)]
660pub fn sv_set_wide_inline(handle: SimHandle, slot: SlotId, value: WideView) {
661 sv_set_wide(handle, slot, value)
662}
663
664/// The `sv_get_mem_word` twin of [`sv_get_scalar_inline`] (ST-026, Task 12).
665#[inline(always)]
666pub fn sv_get_mem_word_inline(handle: SimHandle, slot: SlotId, row_index: u64) -> WideView {
667 sv_get_mem_word(handle, slot, row_index)
668}
669
670/// The `sv_set_mem_word` twin of [`sv_get_scalar_inline`] (ST-026, Task 12).
671#[inline(always)]
672pub fn sv_set_mem_word_inline(handle: SimHandle, slot: SlotId, row_index: u64, value: WideView) {
673 sv_set_mem_word(handle, slot, row_index, value)
674}
675
676/// ST-013: fills in the Task-0 contract stub — `read`/`write` operate on the
677/// *absolute*, already-instance-resolved [`StateCellId`] scheduler-side code
678/// uses (distinct from the ABI's template-relative `SlotId` above), by
679/// delegating to the exact same [`read_slot`]/[`commit_write_at`] primitives
680/// the accessor fns use, so both paths are provably the same storage.
681impl StateEngine {
682 /// Writes `row` (a memory's, when `Some`) of the real cell `cell`,
683 /// recording the effect on its window aliases.
684 fn commit_root(
685 &mut self,
686 cell: StateCellId,
687 row: Option<u64>,
688 value: FourStateValue,
689 ) -> super::write::ChangeResult {
690 match row {
691 None => self.commit_write(cell, value, &mut ()),
692 Some(row) => {
693 let loc = resolve_mem_row(self.cells[cell.0 as usize], row);
694 let result = commit_write_at(&self.arena, &self.vc, loc, cell, value, &mut ());
695 self.vc.views.note_change(cell, Some(row), &result);
696 result
697 }
698 }
699 }
700
701 /// The root's own storage of `info` (its row of a memory).
702 fn root_location(&self, info: &super::alias::ViewInfo) -> SlotLocation {
703 let loc = self.cells[info.root.0 as usize];
704 match info.window.row {
705 Some(row) => resolve_mem_row(loc, row),
706 None => loc,
707 }
708 }
709
710 /// A write through the view cell `view`: splices into the root, commits
711 /// the root, and returns whether the *window* changed.
712 fn write_view(
713 &mut self,
714 view: StateCellId,
715 info: super::alias::ViewInfo,
716 value: FourStateValue,
717 ) -> bool {
718 let root_value = read_slot(&self.arena, self.root_location(&info));
719 let spliced = super::alias::splice_window(&root_value, info.window, &value);
720 self.vc.views.table().begin_write(view);
721 let result = self.commit_root(info.root, info.window.row, spliced);
722 self.vc.views.table().end_write();
723 result.changed
724 && super::write::four_state_changed(
725 &super::alias::window_value(&result.old, info.window),
726 &super::alias::window_value(&result.new, info.window),
727 )
728 }
729}
730
731impl SignalAccess for StateEngine {
732 fn read(&self, cell: StateCellId) -> FourStateValue {
733 if let Some(info) = self.vc.views.info(cell) {
734 let root = read_slot(&self.arena, self.root_location(&info));
735 return super::alias::window_value(&root, info.window);
736 }
737 let loc = self.cells[cell.0 as usize];
738 read_slot(&self.arena, loc)
739 }
740
741 fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool {
742 if let Some(info) = self.vc.views.info(cell) {
743 return self.write_view(cell, info, value);
744 }
745 self.commit_write(cell, value, &mut ()).changed
746 }
747
748 fn read_row(&self, cell: StateCellId, row: u64) -> FourStateValue {
749 let loc = resolve_mem_row(self.cells[cell.0 as usize], row);
750 read_slot(&self.arena, loc)
751 }
752
753 fn write_row(&mut self, cell: StateCellId, row: u64, value: FourStateValue) -> bool {
754 self.commit_root(cell, Some(row), value).changed
755 }
756
757 fn take_alias_changes(&mut self) -> Vec<super::alias::AliasChange> {
758 self.vc.views.take_pending()
759 }
760
761 fn read_wide_window(
762 &self,
763 cell: StateCellId,
764 lo_word: usize,
765 words: usize,
766 ) -> (Vec<u64>, Vec<u64>) {
767 let loc = self.cells[cell.0 as usize];
768 debug_assert_eq!(loc.class, SlotClass::Wide);
769 super::huge_mirror::read_window(&self.arena, loc.word_offset as usize, lo_word, words)
770 }
771
772 fn write_wide_window(
773 &mut self,
774 cell: StateCellId,
775 lo_word: usize,
776 aval: &[u64],
777 bval: &[u64],
778 ) -> bool {
779 let loc = self.cells[cell.0 as usize];
780 // The windowed store replaces `commit_write_at` only where that does
781 // nothing the window skips (see `huge_mirror`'s module doc).
782 if loc.class != SlotClass::Wide
783 || self.vc.force.borrow().contains(cell)
784 || self.vc.records_changes()
785 {
786 return super::write_wide_window_whole(self, cell, lo_word, aval, bval);
787 }
788 let changed = super::huge_mirror::store_window(
789 &self.arena,
790 loc.word_offset as usize,
791 loc.bit_width,
792 lo_word,
793 aval,
794 bval,
795 );
796 if changed {
797 self.dispatch.notify_scheduler(
798 cell,
799 EdgeKind::None,
800 self.vc.current_time(),
801 self.vc.current_region(),
802 );
803 }
804 changed
805 }
806
807 fn subscribe(
808 &mut self,
809 _cell: StateCellId,
810 role: ListenerRole,
811 sink: Box<dyn ValueChangeSink + Send>,
812 ) {
813 // Task 11 (ST-035, ST-073, ST-074): fills in the placeholder left by
814 // Task 9 — this is now the *one* entry point every consumer
815 // (scheduler wakeup, waveform, VPI) registers into, and it forwards
816 // directly to `Dispatch::register`, the fixed 3-role table
817 // (`state::dispatch`), never a second ad hoc `Vec`/`HashMap`-backed
818 // table.
819 //
820 // `_cell` stays unused this task: this wave's `Dispatch` is one
821 // shared 3-role table per `StateEngine` (not per-cell), matching
822 // `commit_write`'s own current fan-out shape (it does not yet route
823 // per-cell either — see Task 12's GVT-gating task for where
824 // per-write routing through this table is actually exercised end to
825 // end). Per-cell subscription filtering, if ever needed, is future
826 // scope, not part of ST-035/ST-073/ST-074's acceptance.
827 self.dispatch.register(role, sink);
828 }
829}
830
831// These tests build real layouts from SV source, which needs the frontend.
832// The feature gate goes first: this file's own source scans find the test
833// module as the test attribute immediately followed by `mod tests`.
834#[cfg(feature = "frontend")]
835#[cfg(test)]
836mod tests {
837 use super::*;
838 use crate::netlist::{SymbolKey, TemplateKey};
839 use crate::state::instance::TemplateRegistry;
840 use crate::state::layout::build_template_layout;
841 use crate::state::value::compile_source;
842 use crate::state::write::last_vc_record_for;
843 use crate::state::{SlotDescriptor, SlotOffset, StateLayout};
844
845 /// Review fix (Task 10 r1, both blocking findings): the shared boundary
846 /// finder both `no_public_function_returns_a_raw_absolute_pointer_for_
847 /// caller_arithmetic` (ST-027) and `state_engine_owns_every_state_owned_
848 /// resource_directly_no_second_pointer` (ST-152) use to isolate a
849 /// scanned file's *production* code from its trailing `mod tests` block.
850 ///
851 /// The original implementation (in both tests, duplicated) truncated at
852 /// the FIRST literal occurrence of the substring `"#[cfg(test)]"`
853 /// anywhere in the file -- including inside doc-comment prose that
854 /// merely *mentions* the attribute in backticks (`write.rs`'s
855 /// `last_vc_record_for` doc comment) and on real, individually
856 /// `#[cfg(test)]`-gated helper items that sit well before the real test
857 /// module (`write.rs`'s own `last_vc_record_for` fn; `arena.rs`'s
858 /// `is_wide_plane_sealed` method, ~220 lines before its real `mod
859 /// tests`). Either shape silently truncates the "production code" this
860 /// function returns far too early, excluding genuine, always-compiled
861 /// code from both structural scans -- a "gate that cannot fail" for any
862 /// file shaped this way (verified by fault injection: a `pub fn`
863 /// returning `*mut u8`/a `static mut` placed in that excluded region
864 /// passed both scans green before this fix).
865 ///
866 /// The fix (r1): don't stop at *any* `#[cfg(test)]`, only at the one
867 /// immediately (modulo whitespace) followed by `mod tests` -- the actual
868 /// test-module boundary every file in this crate uses. A `#[cfg(test)]`
869 /// on some other item, or the bare text of the attribute inside a doc
870 /// comment, is skipped and scanning continues past it.
871 ///
872 /// The fix (r2, review finding "gate that cannot fail for content placed
873 /// *after* `mod tests`"): r1 still `return`ed at the real boundary,
874 /// which means it never scanned anything textually *after* that
875 /// module's closing `}` -- nothing in the language forces `mod tests` to
876 /// be the last item in a file, so genuinely-production code placed after
877 /// it was silently invisible to both scans. Fixed by excising each real
878 /// `#[cfg(test)] mod tests { ... }` span (located via brace-depth
879 /// matching, not truncation) and concatenating everything else --
880 /// before, between, and after every such block -- so scanning resumes
881 /// past the excluded region for the rest of the file instead of
882 /// stopping there. On every file in this crate today `mod tests` is
883 /// still the last item, so the excised suffix is just trailing
884 /// whitespace and this is a no-op change in practice; the fix is that
885 /// it's no longer an *assumed* invariant the scan quietly depends on.
886 fn production_code(src: &str) -> String {
887 let marker = "#[cfg(test)]";
888 let mut result = String::with_capacity(src.len());
889 let mut cursor = 0usize;
890 loop {
891 let Some(rel_idx) = src[cursor..].find(marker) else {
892 result.push_str(&src[cursor..]);
893 break;
894 };
895 let idx = cursor + rel_idx;
896 let after = &src[idx + marker.len()..];
897 let after_trimmed = after.trim_start();
898 if after_trimmed.starts_with("mod tests") {
899 let trim_offset = after.len() - after_trimmed.len();
900 let mod_tests_abs = idx + marker.len() + trim_offset;
901 let after_keyword = &src[mod_tests_abs + "mod tests".len()..];
902 let brace_abs = after_keyword
903 .find('{')
904 .map(|rel| mod_tests_abs + "mod tests".len() + rel);
905 let close_abs = brace_abs.and_then(|b| find_matching_brace(src, b));
906 match close_abs {
907 Some(close_abs) => {
908 // Keep everything up to (not including) the
909 // `#[cfg(test)]` marker, then jump past the whole
910 // module and keep scanning the remainder.
911 result.push_str(&src[cursor..idx]);
912 cursor = close_abs + 1;
913 continue;
914 }
915 None => {
916 // Malformed input (no matching brace found) --
917 // never silently include unparseable content past
918 // this point; fall back to the old truncate-here
919 // behavior for safety rather than guessing.
920 result.push_str(&src[cursor..idx]);
921 break;
922 }
923 }
924 }
925 // Not the real boundary (either doc-comment prose mentioning the
926 // attribute, or a `#[cfg(test)]`-gated item that isn't `mod
927 // tests` itself) -- keep scanning past this occurrence.
928 result.push_str(&src[cursor..idx + marker.len()]);
929 cursor = idx + marker.len();
930 }
931 result
932 }
933
934 /// Depth-aware `{`/`}` matcher for [`production_code`]: given the byte
935 /// index of an opening `{` in `src`, returns the byte index of its
936 /// matching closing `}`, skipping braces that appear inside line/block
937 /// comments, string literals and char literals (so a `"}"` or `'}'` in
938 /// a test body's assertion text doesn't desynchronize the count). This
939 /// is a lightweight tokenizer, not a full Rust parser -- sufficient for
940 /// well-formed source files in this crate, and returns `None` (never
941 /// panics or guesses) rather than silently mis-locating the boundary on
942 /// anything it can't confidently classify.
943 fn find_matching_brace(src: &str, open_idx: usize) -> Option<usize> {
944 let bytes = src.as_bytes();
945 debug_assert_eq!(bytes.get(open_idx), Some(&b'{'));
946 let mut depth: i32 = 0;
947 let mut i = open_idx;
948 while i < bytes.len() {
949 match bytes[i] {
950 b'{' => {
951 depth += 1;
952 i += 1;
953 }
954 b'}' => {
955 depth -= 1;
956 if depth == 0 {
957 return Some(i);
958 }
959 i += 1;
960 }
961 b'/' if bytes.get(i + 1) == Some(&b'/') => {
962 while i < bytes.len() && bytes[i] != b'\n' {
963 i += 1;
964 }
965 }
966 b'/' if bytes.get(i + 1) == Some(&b'*') => {
967 i += 2;
968 let mut comment_depth = 1u32;
969 while i < bytes.len() && comment_depth > 0 {
970 if bytes[i] == b'/' && bytes.get(i + 1) == Some(&b'*') {
971 comment_depth += 1;
972 i += 2;
973 } else if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
974 comment_depth -= 1;
975 i += 2;
976 } else {
977 i += 1;
978 }
979 }
980 }
981 b'"' => {
982 i += 1;
983 while i < bytes.len() && bytes[i] != b'"' {
984 if bytes[i] == b'\\' {
985 i += 1;
986 }
987 i += 1;
988 }
989 i += 1;
990 }
991 b'\'' => {
992 // Ambiguous between a char literal (`'x'`, `'\n'`,
993 // `'\u{7B}'`) and a lifetime tick (`'static`, `'a`).
994 // Look ahead a bounded window for a closing `'` with no
995 // intervening newline; if found, treat the whole span
996 // as a char literal and skip it (so a `'{'`/`'}'` char
997 // literal can never desync the brace count). Otherwise
998 // it's a lifetime -- just consume the tick.
999 const MAX_CHAR_LITERAL_LEN: usize = 12; // covers '\u{10FFFF}'
1000 let window_end = (i + 1 + MAX_CHAR_LITERAL_LEN).min(bytes.len());
1001 let mut j = i + 1;
1002 let mut found_close = None;
1003 while j < window_end {
1004 if bytes[j] == b'\n' {
1005 break;
1006 }
1007 if bytes[j] == b'\'' && j > i + 1 {
1008 found_close = Some(j);
1009 break;
1010 }
1011 j += 1;
1012 }
1013 match found_close {
1014 Some(close) => i = close + 1,
1015 None => i += 1,
1016 }
1017 }
1018 _ => {
1019 i += 1;
1020 }
1021 }
1022 }
1023 None
1024 }
1025
1026 /// Review fix (Task 10 r1): the fault-injection cases the two blocking
1027 /// findings describe, run against the fixed [`production_code`] above,
1028 /// locking the fix in. Both cases previously passed (i.e. the injected
1029 /// violation went undetected) against the old first-`#[cfg(test)]`
1030 /// truncation; both must now be caught.
1031 #[test]
1032 fn production_code_does_not_truncate_early_at_a_pre_mod_tests_cfg_test_item() {
1033 // Shape 1 (write.rs): a `#[cfg(test)]`-visible peek helper, gated
1034 // individually, sitting before the real `mod tests` block -- plus a
1035 // doc comment that merely *mentions* the attribute in prose first.
1036 let write_shaped = concat!(
1037 "/// A `#[cfg(test)]`-visible peek at something.\n",
1038 "#[cfg(test)]\n",
1039 "pub fn last_vc_record_for() {}\n",
1040 "\n",
1041 "pub fn genuinely_production_and_must_stay_scanned() -> u32 { 1 }\n",
1042 "\n",
1043 "#[cfg(test)]\n",
1044 "mod tests {\n",
1045 " // test-only code\n",
1046 "}\n",
1047 );
1048 let scanned = production_code(write_shaped);
1049 assert!(
1050 scanned.contains("genuinely_production_and_must_stay_scanned"),
1051 "a genuine production fn between an early #[cfg(test)] helper and \
1052 the real `mod tests` block must still be included in the scan"
1053 );
1054 assert!(
1055 !scanned.contains("mod tests"),
1056 "the real test module itself must be excluded"
1057 );
1058
1059 // Shape 2 (arena.rs): a small `#[cfg(test)]`-gated method embedded
1060 // deep inside a real `impl` block, well before the real `mod tests`.
1061 let arena_shaped = concat!(
1062 "impl Arena {\n",
1063 " #[cfg(test)]\n",
1064 " pub(crate) fn is_wide_plane_sealed(&self) -> bool { true }\n",
1065 "\n",
1066 " pub fn mode(&self) -> u8 { 0 }\n",
1067 "}\n",
1068 "\n",
1069 "#[cfg(test)]\n",
1070 "mod tests {\n",
1071 " // test-only code\n",
1072 "}\n",
1073 );
1074 let scanned = production_code(arena_shaped);
1075 assert!(
1076 scanned.contains("pub fn mode"),
1077 "a genuine production fn after an early #[cfg(test)]-gated method \
1078 must still be included in the scan"
1079 );
1080 assert!(
1081 !scanned.contains("mod tests"),
1082 "the real test module itself must be excluded"
1083 );
1084 }
1085
1086 /// Review fix (Task 10 r1): the same [`production_code`] boundary finder
1087 /// applied to the two *real* files the original findings named, proving
1088 /// the fix closes their exact reported blind spots (not just the
1089 /// synthetic shapes above). `write.rs`'s scan must reach past its early
1090 /// doc-comment mention of `#[cfg(test)]` (line 465-ish) and its
1091 /// individually-gated `last_vc_record_for` helper, and must still
1092 /// exclude the real `mod tests` block. `arena.rs`'s scan must reach past
1093 /// its early `#[cfg(test)]`-gated `is_wide_plane_sealed` method and
1094 /// include the genuinely-production `raw_inline_ptr`/`mode`/`ffi_view`
1095 /// functions that sit between it and the real `mod tests` block --
1096 /// exactly the ~500 lines the original first-match truncation dropped.
1097 #[test]
1098 fn production_code_reaches_the_real_mod_tests_boundary_in_write_rs_and_arena_rs() {
1099 let write_src = production_code(include_str!("write.rs"));
1100 assert!(
1101 write_src.contains("pub(crate) fn commit_write_at"),
1102 "write.rs's real mod tests boundary must not truncate before its production fns"
1103 );
1104 assert!(
1105 !write_src.contains("mod tests"),
1106 "write.rs's scan must still exclude its real test module"
1107 );
1108
1109 let arena_src = production_code(include_str!("arena.rs"));
1110 for needed in [
1111 "pub unsafe fn raw_inline_ptr",
1112 "pub fn mode",
1113 "pub fn ffi_view",
1114 "pub fn wide_aval",
1115 "pub fn wide_bval",
1116 ] {
1117 assert!(
1118 arena_src.contains(needed),
1119 "arena.rs's scan must include the genuinely-production `{needed}`, \
1120 which the original first-#[cfg(test)]-match truncation silently dropped"
1121 );
1122 }
1123 assert!(
1124 !arena_src.contains("mod tests"),
1125 "arena.rs's scan must still exclude its real test module"
1126 );
1127 }
1128
1129 /// Review fix (Task 10 r2, both blocking findings on the boundary
1130 /// finder): r1's `production_code` stopped dead at the real `mod tests`
1131 /// boundary and never scanned anything past it -- nothing in the
1132 /// language forces `mod tests` to be the last item in a file, so a
1133 /// genuinely-production, un-gated item placed textually *after* the
1134 /// real `mod tests` block's closing brace was silently invisible to
1135 /// both structural scans. Mirrors the exact fault-injection shape the
1136 /// finding used (`static mut GLOBAL_LEAK: u32 = 0;` appended after a
1137 /// file's closing `mod tests` brace): the item must still show up in
1138 /// `production_code`'s output, and the brace matcher must not be
1139 /// confused by a `'{'`/`"}"` literal inside the excluded test body, nor
1140 /// stop at only the first of two `mod tests`-shaped blocks.
1141 #[test]
1142 fn production_code_resumes_scanning_after_the_real_mod_tests_closing_brace() {
1143 let shaped = concat!(
1144 "pub fn before_the_test_module() -> u32 { 1 }\n",
1145 "\n",
1146 "#[cfg(test)]\n",
1147 "mod tests {\n",
1148 " #[test]\n",
1149 " fn uses_brace_shaped_literals() {\n",
1150 " let open = '{';\n",
1151 " let close = '}';\n",
1152 " let s = \"} not a real close {\";\n",
1153 " assert_eq!(open, open);\n",
1154 " assert_eq!(close, close);\n",
1155 " let _ = s;\n",
1156 " }\n",
1157 "}\n",
1158 "\n",
1159 "pub fn genuinely_production_placed_after_mod_tests() -> u32 { 2 }\n",
1160 "\n",
1161 "static mut GLOBAL_LEAK_AFTER_MOD_TESTS: u32 = 0;\n",
1162 );
1163 let scanned = production_code(shaped);
1164 assert!(
1165 scanned.contains("before_the_test_module"),
1166 "content before the real mod tests block must still be scanned"
1167 );
1168 assert!(
1169 !scanned.contains("uses_brace_shaped_literals"),
1170 "the real test module's body must still be excluded, even though it \
1171 contains '{{'/'}}' char literals and a \"}}...{{\" string literal that \
1172 could desync a naive brace counter"
1173 );
1174 assert!(
1175 scanned.contains("genuinely_production_placed_after_mod_tests"),
1176 "a genuine production fn placed AFTER the real mod tests block's \
1177 closing brace must still be scanned -- this is the exact gap the \
1178 review finding reported"
1179 );
1180 assert!(
1181 scanned.contains("GLOBAL_LEAK_AFTER_MOD_TESTS"),
1182 "a `static mut` placed after the real mod tests block must still be \
1183 visible to production_code's output, so the ST-152 scan built on top \
1184 of it can catch it"
1185 );
1186
1187 // Two real test modules in one file: both must be excluded, and the
1188 // content between and after them must both survive.
1189 let two_modules = concat!(
1190 "pub fn between_none_yet() -> u32 { 0 }\n",
1191 "#[cfg(test)]\n",
1192 "mod tests {\n",
1193 " fn first() {}\n",
1194 "}\n",
1195 "pub fn between_the_two_modules() -> u32 { 1 }\n",
1196 "#[cfg(test)]\n",
1197 "mod tests {\n",
1198 " fn second() {}\n",
1199 "}\n",
1200 "pub fn after_both_modules() -> u32 { 2 }\n",
1201 );
1202 let scanned_two = production_code(two_modules);
1203 assert!(!scanned_two.contains("fn first"));
1204 assert!(!scanned_two.contains("fn second"));
1205 assert!(scanned_two.contains("between_the_two_modules"));
1206 assert!(scanned_two.contains("after_both_modules"));
1207 }
1208
1209 /// Task 10 (ST-026): for a built instance/slot, the `#[inline(always)]`
1210 /// wrappers must produce results identical to the `extern "C"` fns they
1211 /// wrap, on both the scalar and wide tiers — proving each wrapper is a
1212 /// pure pass-through, never a second, divergent implementation. Fails if
1213 /// a wrapper re-derives its own resolution/storage path instead of
1214 /// simply forwarding to the existing `extern "C"` fn.
1215 #[test]
1216 fn inline_wrappers_produce_identical_results_to_the_extern_c_fns() {
1217 let tmpl = TemplateKey {
1218 definition: 10,
1219 params_hash: 0,
1220 };
1221 let mut engine = engine_with_template(
1222 "module m; logic [7:0] x; logic [127:0] w; endmodule\n",
1223 tmpl,
1224 );
1225 let handle = engine.allocate_instance(tmpl).expect("template registered");
1226 let h = handle.into_sim_handle();
1227 let scalar_slot = SlotId(0);
1228 let wide_slot = SlotId(1);
1229
1230 // Scalar: write through the extern "C" fn, read back through both
1231 // the extern "C" fn and the inline wrapper -- they must agree.
1232 sv_set_scalar(h, scalar_slot, narrow_word(0x42, 0));
1233 assert_eq!(
1234 sv_get_scalar_inline(h, scalar_slot),
1235 sv_get_scalar(h, scalar_slot),
1236 "sv_get_scalar_inline must return exactly what sv_get_scalar returns"
1237 );
1238
1239 // Scalar write path: write through the inline wrapper, read back
1240 // through the extern "C" fn -- both directions must agree.
1241 sv_set_scalar_inline(h, scalar_slot, narrow_word(0x99, 0));
1242 assert_eq!(sv_get_scalar(h, scalar_slot), narrow_word(0x99, 0));
1243
1244 // Wide: same round trip, through both wrappers.
1245 let mut aval = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
1246 let mut bval = [0x3333_3333_3333_3333u64, 0x4444_4444_4444_4444u64];
1247 let pattern = WideView {
1248 aval: aval.as_mut_ptr(),
1249 bval: bval.as_mut_ptr(),
1250 word_count: 2,
1251 };
1252 sv_set_wide_inline(h, wide_slot, pattern);
1253 let via_extern_c = sv_get_wide(h, wide_slot);
1254 let via_inline = sv_get_wide_inline(h, wide_slot);
1255 // SAFETY: both views alias the same, still-live arena storage this
1256 // same test's `engine`/`handle` own; `word_count` words are valid to
1257 // read on both planes per `sv_get_wide`'s own contract.
1258 unsafe {
1259 let extern_c_aval = std::slice::from_raw_parts(via_extern_c.aval, 2);
1260 let extern_c_bval = std::slice::from_raw_parts(via_extern_c.bval, 2);
1261 let inline_aval = std::slice::from_raw_parts(via_inline.aval, 2);
1262 let inline_bval = std::slice::from_raw_parts(via_inline.bval, 2);
1263 assert_eq!(extern_c_aval, inline_aval);
1264 assert_eq!(extern_c_bval, inline_bval);
1265 }
1266
1267 // SAFETY: see the module's other tests' identical reclaim comment.
1268 unsafe {
1269 drop(Box::from_raw(h.0 as *mut InstanceHandle));
1270 }
1271 }
1272
1273 /// Task 10 (ST-027): a source-text structural test proving no `pub fn` in
1274 /// either `accessor_impl.rs` or `write.rs` returns a raw pointer at any
1275 /// of this crate's actual signal-storage word widths -- the shape a
1276 /// raw-offset-exposing API (the superseded state.md design) would need
1277 /// to hand a caller a pointer it could do its own base+offset
1278 /// arithmetic against. Fails if a future edit reintroduces that
1279 /// superseded design.
1280 ///
1281 /// Review fix (Task 10 r2, both blocking findings on this test): r1 only
1282 /// scanned for `*mut u8`/`*const u8`, but no storage in this crate is
1283 /// ever addressed as `u8` -- the arena's inline plane is `Vec<u128>` and
1284 /// its wide planes are `Vec<u64>` (`arena.rs`'s own `raw_inline_ptr`
1285 /// already returns `*mut u128`, and the accepted `WideView` accessor
1286 /// already carries `*mut u64` fields), so a realistic reintroduction of
1287 /// the superseded design would use one of *those* widths and sail
1288 /// through undetected. Broadened to the widths this crate actually
1289 /// uses. `sv_get_wide`/`sv_get_wide_inline`/`sv_set_wide`/
1290 /// `sv_set_wide_inline` stay clean under the broadened scan because
1291 /// their signatures name the `WideView` struct, never a bare pointer
1292 /// type, in production code -- asserted explicitly below rather than
1293 /// just assumed.
1294 #[test]
1295 fn no_public_function_returns_a_raw_absolute_pointer_for_caller_arithmetic() {
1296 let forbidden = forbidden_raw_pointer_patterns();
1297
1298 let accessor_impl_src = production_code(include_str!("accessor_impl.rs"));
1299 let write_src = production_code(include_str!("write.rs"));
1300 for (name, src) in [
1301 ("accessor_impl.rs", accessor_impl_src.as_str()),
1302 ("write.rs", write_src.as_str()),
1303 ] {
1304 for pattern in &forbidden {
1305 assert!(
1306 !src.contains(pattern.as_str()),
1307 "{name}'s production code must not return a raw `{pattern}` pointer \
1308 from any function -- this crate's real word types are u128 (inline \
1309 plane) and u64 (wide planes), not just u8"
1310 );
1311 }
1312 }
1313
1314 // Confirm the broadened scan still doesn't false-positive on the
1315 // legitimate wide-plane accessors: their signatures name the
1316 // `WideView` struct, not a bare pointer type, so exactly the two
1317 // sanctioned `pub fn`s below are allowed to have `WideView` in their
1318 // signature and none of them may have a bare pointer return type.
1319 assert!(
1320 accessor_impl_src.contains("-> WideView"),
1321 "sv_get_wide/sv_get_wide_inline must still return the named WideView type"
1322 );
1323 }
1324
1325 /// The forbidden raw-pointer shapes [`no_public_function_returns_a_raw_
1326 /// absolute_pointer_for_caller_arithmetic`] scans for -- factored out so
1327 /// the fault-injection test below exercises the exact same list the real
1328 /// scan uses, rather than a hand-copied duplicate that could silently
1329 /// drift out of sync with it.
1330 ///
1331 /// Built via concatenation, not as one contiguous literal per pattern,
1332 /// so this file's own source text never contains any forbidden
1333 /// substring contiguously either (this exact test function would
1334 /// otherwise match itself).
1335 fn forbidden_raw_pointer_patterns() -> Vec<String> {
1336 let widths = ["8", "16", "32", "64", "128"];
1337 widths
1338 .iter()
1339 .flat_map(|w| [["*", "mut u", w].concat(), ["*", "const u", w].concat()])
1340 .collect()
1341 }
1342
1343 /// Review fix (Task 10 r2, both blocking findings on ST-027's width
1344 /// scan): fault injection proving the broadened scan actually catches
1345 /// this crate's *real* pointer shapes (`*mut u128`, `*mut u64`), not
1346 /// just the `*mut u8` shape the r1 scan was limited to and which has no
1347 /// natural occurrence anywhere in this codebase. Mirrors the exact
1348 /// shapes the finding named: a `raw_base_ptr`/`instance_base_words`-like
1349 /// signature exposing the arena's real word types for caller-side
1350 /// base+offset arithmetic.
1351 #[test]
1352 fn forbidden_raw_pointer_scan_catches_this_crates_real_word_widths() {
1353 let forbidden = forbidden_raw_pointer_patterns();
1354
1355 let u128_reintroduction = concat!(
1356 "pub fn raw_base_ptr(handle: SimHandle) -> *mut u128 {\n",
1357 " unimplemented!()\n",
1358 "}\n",
1359 );
1360 assert!(
1361 forbidden
1362 .iter()
1363 .any(|p| u128_reintroduction.contains(p.as_str())),
1364 "a reintroduced *mut u128 raw-offset accessor must be caught by the \
1365 forbidden-pattern list -- this is the arena's actual inline-plane word type"
1366 );
1367
1368 let u64_reintroduction = concat!(
1369 "pub fn instance_base_words(&self) -> (*mut u128, *mut u64, *mut u64) {\n",
1370 " unimplemented!()\n",
1371 "}\n",
1372 );
1373 assert!(
1374 forbidden
1375 .iter()
1376 .any(|p| u64_reintroduction.contains(p.as_str())),
1377 "a reintroduced *mut u64 raw-offset accessor must be caught by the \
1378 forbidden-pattern list -- this is the arena's actual wide-plane word type"
1379 );
1380
1381 // The r1-era u8-only list must NOT have caught either shape (sanity
1382 // check that this test actually exercises new coverage, not
1383 // coverage the r1 scan already had).
1384 let u8_only: Vec<String> = vec!["*mut u8".to_string(), "*const u8".to_string()];
1385 assert!(
1386 !u8_only
1387 .iter()
1388 .any(|p| u128_reintroduction.contains(p.as_str())),
1389 "sanity check: the u128 shape must not contain the literal u8 substring"
1390 );
1391 assert!(
1392 !u8_only
1393 .iter()
1394 .any(|p| u64_reintroduction.contains(p.as_str())),
1395 "sanity check: the u64 shape must not contain the literal u8 substring"
1396 );
1397 }
1398
1399 /// Task 10 (ST-029): `state::{StateEngine, StateCellId, FourStateValue}`
1400 /// alone (no `crate::process::*`, no `crate::sched::*`) must be enough to
1401 /// perform an `alloc_cell` + `commit_write` + `SignalAccess::read`
1402 /// sequence -- the import list itself is the proof this reference API
1403 /// needs no `ProcessFn`/`CompiledKernel` type to be reachable.
1404 #[test]
1405 fn read_and_write_are_callable_without_constructing_any_kernel_or_process_type() {
1406 use crate::state::write::SlotLocation;
1407 use crate::state::{FourStateValue, SignalAccess, StateCellId, StateEngine};
1408
1409 let templates = TemplateRegistry::new();
1410 let mut engine = StateEngine::new(templates);
1411 // SAFETY: local, single-owned engine; no other live borrow of the
1412 // inline plane, single-threaded test.
1413 let word_offset = unsafe { engine.arena.alloc_inline_word() };
1414 let loc = SlotLocation {
1415 class: SlotClass::Inline,
1416 word_offset,
1417 bit_width: 8,
1418 elem_word_count: 1,
1419 row_stride_words: 0,
1420 num_rows: 0,
1421 };
1422 let cell: StateCellId = engine.alloc_cell(loc);
1423
1424 let changed =
1425 engine.commit_write(cell, FourStateValue::Narrow(narrow_word(0x2A, 0)), &mut ());
1426 assert!(changed.changed);
1427
1428 match SignalAccess::read(&engine, cell) {
1429 FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(0x2A, 0)),
1430 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1431 }
1432 }
1433
1434 /// Task 10 (ST-152): every resource `StateEngine` owns (`arena`,
1435 /// `templates`, `cells`, `vc` -- `dispatch`/the undo log are added by
1436 /// Tasks 11/16, later than this task) is a *direct* field, reachable
1437 /// purely through `&engine.<field>`/`&mut engine.<field>`, never through
1438 /// a second, free-standing global -- and no forbidden-kind global (a
1439 /// mutable process-wide static, an external once-per-process-init macro,
1440 /// or a lazily-initialized single-cell singleton type) exists anywhere in
1441 /// this crate's `state` module (the concrete negative half of the proof;
1442 /// see this test's own body for the exact forbidden spellings it scans
1443 /// for, listed there rather than here so this doc comment's own prose
1444 /// cannot trip the very scan it describes).
1445 #[test]
1446 fn state_engine_owns_every_state_owned_resource_directly_no_second_pointer() {
1447 let templates = TemplateRegistry::new();
1448 let mut engine = StateEngine::new(templates);
1449
1450 // Reach every currently-existing STATE-owned resource purely through
1451 // a direct field access on `&mut engine`/`&engine` -- no second,
1452 // free-standing global exists to reach any of them through instead.
1453 let _arena: &std::rc::Rc<super::super::arena::Arena> = &engine.arena;
1454 let _templates: &std::rc::Rc<TemplateRegistry> = &engine.templates;
1455 let _cells_len_before = engine.cells.len();
1456 let (cell, _) = {
1457 // SAFETY: local, single-owned engine; no other live borrow.
1458 let word_offset = unsafe { engine.arena.alloc_inline_word() };
1459 let loc = crate::state::write::SlotLocation {
1460 class: SlotClass::Inline,
1461 word_offset,
1462 bit_width: 1,
1463 elem_word_count: 1,
1464 row_stride_words: 0,
1465 num_rows: 0,
1466 };
1467 (engine.alloc_cell(loc), loc)
1468 };
1469 assert_eq!(
1470 engine.cells.len(),
1471 _cells_len_before + 1,
1472 "cells is a direct field"
1473 );
1474 engine.commit_write(cell, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
1475 engine.register_worker_thread(); // reaches `vc` purely via `&mut engine`
1476
1477 // Negative half: no forbidden-kind global anywhere in this crate's
1478 // `state` module's *production* code. See [`forbidden_global_patterns`]
1479 // for the exact forbidden spellings and why each one is there.
1480 let forbidden = forbidden_global_patterns();
1481 let state_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/state");
1482 let entries = std::fs::read_dir(&state_dir).expect("state/ directory is readable");
1483 for entry in entries {
1484 let entry = entry.expect("directory entry is readable");
1485 let path = entry.path();
1486 if path.extension().and_then(|e| e.to_str()) != Some("rs") {
1487 continue;
1488 }
1489 let contents = std::fs::read_to_string(&path)
1490 .unwrap_or_else(|e| panic!("{} must be readable: {e}", path.display()));
1491 let scanned = production_code(&contents);
1492 for forbidden_pattern in &forbidden {
1493 assert!(
1494 !scanned.contains(forbidden_pattern.as_str()),
1495 "{}'s production code must not contain a free-standing global of the \
1496 forbidden kind this test enforces -- every STATE-owned resource must be \
1497 reachable exclusively through a direct StateEngine field",
1498 path.display()
1499 );
1500 }
1501 }
1502 }
1503
1504 /// The forbidden free-standing-global shapes [`state_engine_owns_every_
1505 /// state_owned_resource_directly_no_second_pointer`] scans for --
1506 /// factored out so the fault-injection test below exercises the exact
1507 /// same list the real scan uses, rather than a hand-copied duplicate
1508 /// that could silently drift out of sync with it.
1509 ///
1510 /// Review fix (Task 10 r2): r1's list (`static mut`, `lazy_static`,
1511 /// `OnceLock`, `OnceCell`) only catches globals that need the `mut`
1512 /// keyword or one of those three specific types. A free-standing,
1513 /// process-wide, mutably-shared global does not need any of them -- a
1514 /// bare `static` whose type is `Mutex<T>`/`RwLock<T>`/`AtomicPtr<T>` is
1515 /// an ordinary, no-`unsafe`-required way to build exactly the "second,
1516 /// free-standing global" pointer to STATE-owned data ST-152 forbids,
1517 /// using none of the four original spellings.
1518 ///
1519 /// Added: `Mutex<`/`RwLock<` (the two `Sync`-safe interior-mutability
1520 /// wrapper types that can hold an arbitrary `T`, including an
1521 /// `Rc`/`Arc`/raw pointer at STATE's owned data) and `AtomicPtr<` (the
1522 /// pointer-typed atomic -- deliberately narrower than a bare `Atomic`
1523 /// substring, which would also match this crate's existing, legitimate
1524 /// `instance.rs` monotonic-id counter: a plain integer atomic holds no
1525 /// pointer to any STATE-owned resource and is not the shape ST-152
1526 /// forbids, so it must not be flagged; see the fault-injection test
1527 /// below for the check that it stays clean).
1528 ///
1529 /// Every spelling here is built by concatenation (never as one
1530 /// contiguous literal, and never named in a doc/prose comment either)
1531 /// for the same self-matching reason r1's list already documents.
1532 fn forbidden_global_patterns() -> Vec<String> {
1533 vec![
1534 ["static", " mut"].concat(),
1535 ["lazy", "_static"].concat(),
1536 ["Once", "Lock"].concat(),
1537 ["Once", "Cell"].concat(),
1538 ["Mut", "ex<"].concat(),
1539 ["Rw", "Lock<"].concat(),
1540 ["Atomic", "Ptr<"].concat(),
1541 ]
1542 }
1543
1544 /// Review fix (Task 10 r2): fault injection proving the broadened
1545 /// forbidden-global list actually catches a Mutex/RwLock/AtomicPtr-
1546 /// backed static -- the exact evasion shape the finding demonstrated
1547 /// (a `Mutex<Option<Rc<...>>>`-typed free-standing global) that passed
1548 /// all four r1 patterns clean. Also proves the broadened list does NOT
1549 /// false-positive on this crate's one existing, legitimate
1550 /// free-standing static (`instance.rs`'s `NEXT_ENGINE_ID: AtomicU64`
1551 /// monotonic-id counter), which holds no pointer to any STATE-owned
1552 /// resource.
1553 #[test]
1554 fn forbidden_global_scan_catches_mutex_rwlock_and_atomicptr_backed_globals() {
1555 let forbidden = forbidden_global_patterns();
1556
1557 let mutex_leak = concat!(
1558 "static GLOBAL_ARENA_REGISTRY: std::sync::Mutex<Option<std::rc::Rc<u8>>> =\n",
1559 " std::sync::Mutex::new(None);\n",
1560 );
1561 assert!(
1562 forbidden.iter().any(|p| mutex_leak.contains(p.as_str())),
1563 "a Mutex-backed free-standing global must be caught by the forbidden-pattern list"
1564 );
1565
1566 let rwlock_leak = "static GLOBAL: std::sync::RwLock<u8> = std::sync::RwLock::new(0);";
1567 assert!(
1568 forbidden.iter().any(|p| rwlock_leak.contains(p.as_str())),
1569 "an RwLock-backed free-standing global must be caught by the forbidden-pattern list"
1570 );
1571
1572 let atomic_ptr_leak = "static GLOBAL: std::sync::atomic::AtomicPtr<u8> = \
1573 std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());";
1574 assert!(
1575 forbidden
1576 .iter()
1577 .any(|p| atomic_ptr_leak.contains(p.as_str())),
1578 "an AtomicPtr-backed free-standing global must be caught by the forbidden-pattern list"
1579 );
1580
1581 // None of the r1-era four patterns caught any of the above (sanity
1582 // check that this test actually exercises new coverage, not
1583 // coverage the r1 scan already had).
1584 let r1_patterns: Vec<String> = vec![
1585 ["static", " mut"].concat(),
1586 ["lazy", "_static"].concat(),
1587 ["Once", "Lock"].concat(),
1588 ["Once", "Cell"].concat(),
1589 ];
1590 for shape in [mutex_leak, rwlock_leak, atomic_ptr_leak] {
1591 assert!(
1592 !r1_patterns.iter().any(|p| shape.contains(p.as_str())),
1593 "sanity check: the r1-era pattern list must not already catch this shape"
1594 );
1595 }
1596
1597 // Must NOT false-positive on this crate's real, legitimate plain
1598 // integer atomic counter -- it is not a pointer to STATE-owned data.
1599 let legitimate_counter = "static NEXT_ENGINE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);";
1600 assert!(
1601 !forbidden
1602 .iter()
1603 .any(|p| legitimate_counter.contains(p.as_str())),
1604 "a plain AtomicU64 counter (no pointer to any STATE-owned resource) must not be flagged"
1605 );
1606 }
1607
1608 fn engine_with_template(src: &str, key: TemplateKey) -> StateEngine {
1609 let design = compile_source(src).expect("valid SV source compiles");
1610 let body = design
1611 .top_instances()
1612 .next()
1613 .expect("one top instance")
1614 .instance_body()
1615 .expect("instance has a body");
1616 let layout: StateLayout = build_template_layout(&body).expect("layout builds");
1617
1618 let templates = TemplateRegistry::new();
1619 // SAFETY: `templates` is a local, single-owned value with no other
1620 // live borrow and no concurrent caller.
1621 unsafe {
1622 templates.register(key, layout);
1623 }
1624 StateEngine::new(templates)
1625 }
1626
1627 /// Review fix (Task 9 r4, blocking finding): builds a `StateEngine` with
1628 /// one registered template holding a single `SlotClass::Wide` slot of
1629 /// `elem_word_count` words, entirely by hand -- no `compile_source`/
1630 /// `build_template_layout` (hence no `sv_lang` C++ FFI call) anywhere in
1631 /// this path, unlike [`engine_with_template`] above. `sv_lang`'s FFI
1632 /// cannot run under Miri, so this is the route the Miri-reachable
1633 /// regression test below uses to build a real `sv_get_wide`-capable
1634 /// instance. Mirrors [`engine_with_template`]'s own registration
1635 /// sequence exactly, just with a hand-built [`StateLayout`] in place of
1636 /// one `build_template_layout` produces from real elaborated SV.
1637 fn engine_with_manual_wide_template(key: TemplateKey, elem_word_count: u32) -> StateEngine {
1638 let layout = StateLayout {
1639 slots: vec![SlotDescriptor {
1640 key: SymbolKey(0),
1641 offset: SlotOffset {
1642 class: SlotClass::Wide,
1643 word_offset: 0,
1644 bit_width: elem_word_count * 64,
1645 elem_word_count,
1646 row_stride_words: 0,
1647 num_rows: 0,
1648 },
1649 initial: None,
1650 net_info: None,
1651 }],
1652 inline_words: 0,
1653 wide_words: elem_word_count,
1654 };
1655
1656 let templates = TemplateRegistry::new();
1657 // SAFETY: `templates` is a local, single-owned value with no other
1658 // live borrow and no concurrent caller.
1659 unsafe {
1660 templates.register(key, layout);
1661 }
1662 StateEngine::new(templates)
1663 }
1664
1665 fn narrow_word(val: u64, unk: u64) -> u128 {
1666 (val as u128) | ((unk as u128) << 64)
1667 }
1668
1669 /// Task 11 (ST-138) regression: `slot_location_for` (this module's own
1670 /// private resolver, exercised directly here since this test lives
1671 /// inside the same module) must thread a `SlotClass::Memory` slot's
1672 /// `row_stride_words`/`num_rows` through into the returned
1673 /// `SlotLocation`, not drop/zero them -- the concrete case Task 1's own
1674 /// hand-built-`SlotLocation` unit tests (`state::write::tests`) cannot
1675 /// catch, since they never go through `slot_location_for` at all. A
1676 /// hand-built `SlotOffset` (no `sv_lang` FFI, mirrors
1677 /// `engine_with_manual_wide_template`'s own reasoning) with
1678 /// `row_stride_words: 4, num_rows: 6` deliberately distinct from
1679 /// `elem_word_count: 2`, so a bug that copied `elem_word_count` into
1680 /// those fields instead (or left them zeroed) is caught, not silently
1681 /// matched.
1682 #[test]
1683 fn slot_location_for_threads_memory_row_stride_and_num_rows_through() {
1684 let key = TemplateKey {
1685 definition: 0,
1686 params_hash: 0,
1687 };
1688 let layout = StateLayout {
1689 slots: vec![SlotDescriptor {
1690 key: SymbolKey(0),
1691 offset: SlotOffset {
1692 class: SlotClass::Memory,
1693 word_offset: 0,
1694 bit_width: 32,
1695 elem_word_count: 2,
1696 row_stride_words: 4,
1697 num_rows: 6,
1698 },
1699 initial: None,
1700 net_info: None,
1701 }],
1702 inline_words: 0,
1703 wide_words: 2 * 6,
1704 };
1705 let templates = TemplateRegistry::new();
1706 // SAFETY: `templates` is a local, single-owned value with no other
1707 // live borrow and no concurrent caller.
1708 unsafe {
1709 templates.register(key, layout);
1710 }
1711 let mut engine = StateEngine::new(templates);
1712 let handle = engine.allocate_instance(key).expect("template registered");
1713
1714 let resolved = slot_location_for(&handle, SlotId(0));
1715 assert_eq!(resolved.class, SlotClass::Memory);
1716 assert_eq!(resolved.elem_word_count, 2);
1717 assert_eq!(
1718 resolved.row_stride_words, 4,
1719 "slot_location_for must copy row_stride_words from the SlotOffset, not drop/zero it"
1720 );
1721 assert_eq!(
1722 resolved.num_rows, 6,
1723 "slot_location_for must copy num_rows from the SlotOffset, not drop/zero it"
1724 );
1725 }
1726
1727 /// ST-020, ST-021: build a 1-slot template (8-bit inline var), allocate 2
1728 /// instances, convert each to a `SimHandle`; `sv_set_scalar` distinct
1729 /// patterns through each, and `sv_get_scalar` must read back exactly the
1730 /// pattern written through *that* handle — proving the same `SlotId`
1731 /// resolves to two different absolute words because the two
1732 /// `InstanceHandle`s carry different bases. Fails if `handle.base` is
1733 /// ignored (both instances would alias).
1734 #[test]
1735 fn sv_get_set_scalar_round_trips_through_a_real_instance_handle() {
1736 let tmpl = TemplateKey {
1737 definition: 0,
1738 params_hash: 0,
1739 };
1740 let mut engine = engine_with_template("module m; logic [7:0] x; endmodule\n", tmpl);
1741
1742 let handle_a = engine.allocate_instance(tmpl).expect("template registered");
1743 let handle_b = engine.allocate_instance(tmpl).expect("template registered");
1744 let h_a = handle_a.into_sim_handle();
1745 let h_b = handle_b.into_sim_handle();
1746
1747 let slot = SlotId(0);
1748 let pattern_a = narrow_word(0xAA, 0);
1749 let pattern_b = narrow_word(0x55, 0);
1750 assert_ne!(pattern_a, pattern_b);
1751
1752 sv_set_scalar(h_a, slot, pattern_a);
1753 sv_set_scalar(h_b, slot, pattern_b);
1754
1755 assert_eq!(sv_get_scalar(h_a, slot), pattern_a);
1756 assert_eq!(sv_get_scalar(h_b, slot), pattern_b);
1757
1758 // SAFETY: reclaim the two handles this test leaked via
1759 // `into_sim_handle`, exactly as `from_sim_handle`'s own `# Safety`
1760 // section documents no wave-A caller does for a live simulation run
1761 // -- safe here only because this test performs no further access to
1762 // either handle afterward.
1763 unsafe {
1764 drop(Box::from_raw(h_a.0 as *mut InstanceHandle));
1765 drop(Box::from_raw(h_b.0 as *mut InstanceHandle));
1766 }
1767 }
1768
1769 /// ST-021: write the same value twice via `sv_set_scalar`; the first
1770 /// write (different from the slot's zero-initial value) must register a
1771 /// change in the same VC buffer `commit_write` appends to, and the
1772 /// second (identical value) must not. Fails if `sv_set_scalar` wrote
1773 /// through a path that never touches `commit_write_at`.
1774 #[test]
1775 fn sv_set_scalar_reports_change_via_the_same_primitive_as_signal_access() {
1776 let tmpl = TemplateKey {
1777 definition: 1,
1778 params_hash: 0,
1779 };
1780 let mut engine = engine_with_template("module m; logic [7:0] x; endmodule\n", tmpl);
1781 let handle = engine.allocate_instance(tmpl).expect("template registered");
1782 let cell = handle.cell_for_slot(0);
1783 let h = handle.into_sim_handle();
1784 let slot = SlotId(0);
1785
1786 sv_set_scalar(h, slot, narrow_word(1, 0));
1787 let after_first = last_vc_record_for(cell);
1788 assert!(
1789 after_first.is_some(),
1790 "the first write (from the zero default) must register a change"
1791 );
1792
1793 sv_set_scalar(h, slot, narrow_word(1, 0));
1794 let after_second = last_vc_record_for(cell).expect("still the first write's record");
1795 match after_second.new {
1796 FourStateValue::Narrow(w) => assert_eq!(
1797 w,
1798 narrow_word(1, 0),
1799 "the identical second write must not append a new record"
1800 ),
1801 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1802 }
1803
1804 // SAFETY: see the previous test's identical reclaim comment.
1805 unsafe {
1806 drop(Box::from_raw(h.0 as *mut InstanceHandle));
1807 }
1808 }
1809
1810 /// ST-022, ST-023: a template with one 128-bit wide member
1811 /// (`elem_word_count == 2`); 2 instances; `sv_set_wide` a distinct
1812 /// pattern per instance -- on *both* the `aval` and `bval` (unknown-bit)
1813 /// planes, since ST-022/ST-023's own requirement text names "the
1814 /// aval/bval limb planes" explicitly -- and `sv_get_wide` on the other
1815 /// instance must never reflect the first instance's pattern on either
1816 /// plane (reads its own, independently-based words) -- the wide-plane
1817 /// analogue of the scalar round-trip test. Review fix (r2): the
1818 /// original version of this test used `bval: [0, 0]` for every instance
1819 /// and never read `view.bval` back at all, so a `sv_set_wide` that
1820 /// silently dropped/zeroed the caller's `bval` (e.g. a copy-paste bug
1821 /// writing `aval` into both planes) would still pass; asserting on
1822 /// `bval` here as rigorously as `aval` closes that gap.
1823 #[test]
1824 fn sv_get_set_wide_slices_the_correct_word_range_per_instance() {
1825 let tmpl = TemplateKey {
1826 definition: 2,
1827 params_hash: 0,
1828 };
1829 let mut engine = engine_with_template("module m; logic [127:0] w; endmodule\n", tmpl);
1830
1831 let handle_a = engine.allocate_instance(tmpl).expect("template registered");
1832 let handle_b = engine.allocate_instance(tmpl).expect("template registered");
1833 let h_a = handle_a.into_sim_handle();
1834 let h_b = handle_b.into_sim_handle();
1835 let slot = SlotId(0);
1836
1837 let mut aval_a = [0xAAAA_AAAA_AAAA_AAAAu64, 0xBBBB_BBBB_BBBB_BBBBu64];
1838 let mut bval_a = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
1839 let pattern_a = WideView {
1840 aval: aval_a.as_mut_ptr(),
1841 bval: bval_a.as_mut_ptr(),
1842 word_count: 2,
1843 };
1844 sv_set_wide(h_a, slot, pattern_a);
1845
1846 let mut aval_b = [0xCCCC_CCCC_CCCC_CCCCu64, 0xDDDD_DDDD_DDDD_DDDDu64];
1847 let mut bval_b = [0x3333_3333_3333_3333u64, 0x4444_4444_4444_4444u64];
1848 let pattern_b = WideView {
1849 aval: aval_b.as_mut_ptr(),
1850 bval: bval_b.as_mut_ptr(),
1851 word_count: 2,
1852 };
1853 sv_set_wide(h_b, slot, pattern_b);
1854
1855 let view_a = sv_get_wide(h_a, slot);
1856 // SAFETY: `view_a` was just returned by `sv_get_wide` above, backed
1857 // by this same test's still-live `engine`/`handle_a`; `word_count`
1858 // words are valid to read per `sv_get_wide`'s own contract, on both
1859 // the `aval` and `bval` planes.
1860 let read_a_aval = unsafe { std::slice::from_raw_parts(view_a.aval, 2) };
1861 let read_a_bval = unsafe { std::slice::from_raw_parts(view_a.bval, 2) };
1862 assert_eq!(read_a_aval, [aval_a[0], aval_a[1]]);
1863 assert_eq!(read_a_bval, [bval_a[0], bval_a[1]]);
1864 assert_ne!(
1865 read_a_aval,
1866 [aval_b[0], aval_b[1]],
1867 "instance A's aval read must never reflect instance B's pattern"
1868 );
1869 assert_ne!(
1870 read_a_bval,
1871 [bval_b[0], bval_b[1]],
1872 "instance A's bval read must never reflect instance B's pattern"
1873 );
1874
1875 let view_b = sv_get_wide(h_b, slot);
1876 // SAFETY: same reasoning as above, applied to `handle_b`.
1877 let read_b_aval = unsafe { std::slice::from_raw_parts(view_b.aval, 2) };
1878 let read_b_bval = unsafe { std::slice::from_raw_parts(view_b.bval, 2) };
1879 assert_eq!(read_b_aval, [aval_b[0], aval_b[1]]);
1880 assert_eq!(read_b_bval, [bval_b[0], bval_b[1]]);
1881
1882 // SAFETY: see the first test's identical reclaim comment.
1883 unsafe {
1884 drop(Box::from_raw(h_a.0 as *mut InstanceHandle));
1885 drop(Box::from_raw(h_b.0 as *mut InstanceHandle));
1886 }
1887 }
1888
1889 /// ST-023 (review fix: the wide-tier sibling of
1890 /// `sv_set_scalar_reports_change_via_the_same_primitive_as_signal_access`,
1891 /// since the slicing test above only checks value round-trip, never that
1892 /// a change was actually detected/recorded). `sv_set_wide` with a
1893 /// pattern different from the slot's zero-initial value must register a
1894 /// change; the identical pattern written again must not append a new
1895 /// record. Fails if `sv_set_wide` were a bare write into
1896 /// `arena.wide_aval`/`wide_bval` that never calls into
1897 /// `commit_write_at`'s diff/append logic at all.
1898 ///
1899 /// Review fix (r2): the pattern's `bval` is now nonzero (was `[0, 0]`,
1900 /// indistinguishable from a dropped/zeroed `bval`), and the read-back
1901 /// `VcRecord::new` is asserted on its `bval` plane exactly as its `aval`
1902 /// plane already was (was destructured with `bval: ..`, discarding it)
1903 /// -- so a `sv_set_wide` that silently drops or zeroes the caller's
1904 /// `bval` before it ever reaches `commit_write_at` now fails this test.
1905 #[test]
1906 fn sv_set_wide_reports_change_via_the_same_primitive_as_signal_access() {
1907 let tmpl = TemplateKey {
1908 definition: 3,
1909 params_hash: 0,
1910 };
1911 let mut engine = engine_with_template("module m; logic [127:0] w; endmodule\n", tmpl);
1912 let handle = engine.allocate_instance(tmpl).expect("template registered");
1913 let cell = handle.cell_for_slot(0);
1914 let h = handle.into_sim_handle();
1915 let slot = SlotId(0);
1916
1917 let mut aval = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
1918 let mut bval = [0x5555_5555_5555_5555u64, 0x6666_6666_6666_6666u64];
1919 let pattern = WideView {
1920 aval: aval.as_mut_ptr(),
1921 bval: bval.as_mut_ptr(),
1922 word_count: 2,
1923 };
1924
1925 sv_set_wide(h, slot, pattern);
1926 let after_first = last_vc_record_for(cell);
1927 assert!(
1928 after_first.is_some(),
1929 "the first wide write (from the zero default) must register a change"
1930 );
1931 match after_first.expect("checked is_some above").new {
1932 FourStateValue::Wide { aval: a, bval: b } => {
1933 assert_eq!(a, vec![aval[0], aval[1]]);
1934 assert_eq!(b, vec![bval[0], bval[1]]);
1935 }
1936 FourStateValue::Narrow(_) => panic!("expected Wide"),
1937 }
1938
1939 // Reissue the identical pattern (same backing arrays, unchanged).
1940 let pattern_again = WideView {
1941 aval: aval.as_mut_ptr(),
1942 bval: bval.as_mut_ptr(),
1943 word_count: 2,
1944 };
1945 sv_set_wide(h, slot, pattern_again);
1946 let after_second = last_vc_record_for(cell).expect("still the first write's record");
1947 match after_second.new {
1948 FourStateValue::Wide { aval: a, bval: b } => {
1949 assert_eq!(
1950 a,
1951 vec![aval[0], aval[1]],
1952 "the identical second write must not append a new record"
1953 );
1954 assert_eq!(
1955 b,
1956 vec![bval[0], bval[1]],
1957 "the identical second write must not append a new record"
1958 );
1959 }
1960 FourStateValue::Narrow(_) => panic!("expected Wide"),
1961 }
1962
1963 // SAFETY: see the first test's identical reclaim comment.
1964 unsafe {
1965 drop(Box::from_raw(h.0 as *mut InstanceHandle));
1966 }
1967 }
1968
1969 /// ST-013: for one instance, write a value via `sv_set_scalar` (the ABI
1970 /// path) then read it back via `StateEngine`'s `SignalAccess::read` on
1971 /// the `StateCellId` Task 8 mints for that same concrete signal --
1972 /// proving STATE has exactly one canonical storage location reachable
1973 /// both ways, never two.
1974 #[test]
1975 fn signal_access_read_write_agree_with_the_accessor_fns_on_the_same_storage() {
1976 let tmpl = TemplateKey {
1977 definition: 4,
1978 params_hash: 0,
1979 };
1980 let mut engine = engine_with_template("module m; logic [7:0] x; endmodule\n", tmpl);
1981 let handle = engine.allocate_instance(tmpl).expect("template registered");
1982 let cell = handle.cell_for_slot(0);
1983 let h = handle.into_sim_handle();
1984 let slot = SlotId(0);
1985
1986 sv_set_scalar(h, slot, narrow_word(0x7B, 0));
1987
1988 match SignalAccess::read(&engine, cell) {
1989 FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(0x7B, 0)),
1990 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1991 }
1992
1993 // SAFETY: see the first test's identical reclaim comment.
1994 unsafe {
1995 drop(Box::from_raw(h.0 as *mut InstanceHandle));
1996 }
1997 }
1998
1999 /// Review fix (Task 9 r3, blocking finding 1): `sv_get_wide`'s returned
2000 /// `WideView` aliases `Arena::wide_aval`/`wide_bval`'s own live storage
2001 /// and is retained by the caller past the call -- before this fix, a
2002 /// later `allocate_instance` call for the same (or any) wide-slot
2003 /// template could grow those planes via `Arena::alloc_wide_words` enough
2004 /// to reallocate their backing `Vec<u64>`s, silently dangling every
2005 /// previously issued `WideView`'s `aval`/`bval` pointers (a genuine
2006 /// use-after-free with no compiler diagnostic -- confirmed via an
2007 /// isolated Miri repro of this exact pattern, per the finding). Allocate
2008 /// one wide-slot instance, obtain a `WideView` via `sv_get_wide`, then
2009 /// call `allocate_instance` again for the same template -- this must now
2010 /// panic (`Arena::alloc_wide_words`'s new seal check firing) rather than
2011 /// silently succeeding and leaving `view`'s pointers dangling. Mirrors
2012 /// `instance.rs`'s own `allocate_instance_called_many_more_times_leaves_
2013 /// the_first_handles_arena_pointer_valid` test's "force further growth,
2014 /// then read back through the first handle" shape, except here the
2015 /// correct outcome is a loud panic, not a successful read: no
2016 /// stable-address storage exists for the wide planes (see
2017 /// `Arena::wide_sealed`'s doc comment for why the `TemplateRegistry`
2018 /// per-entry-boxing fix doesn't apply to them), so the chosen fix is an
2019 /// enforced phase boundary instead.
2020 #[test]
2021 #[should_panic(expected = "refusing to grow the wide plane")]
2022 fn sv_get_wide_view_survives_no_further_allocate_instance_call_it_must_panic_not_dangle() {
2023 let tmpl = TemplateKey {
2024 definition: 5,
2025 params_hash: 0,
2026 };
2027 let mut engine = engine_with_template("module m; logic [127:0] w; endmodule\n", tmpl);
2028
2029 let handle = engine.allocate_instance(tmpl).expect("template registered");
2030 let h = handle.into_sim_handle();
2031 let slot = SlotId(0);
2032
2033 // Obtain a WideView that (per sv_get_wide's own contract, and this
2034 // module's own doc comment above) aliases the arena's live
2035 // wide_aval/wide_bval storage.
2036 let view = sv_get_wide(h, slot);
2037 assert_eq!(view.word_count, 2);
2038
2039 // Any further wide-slot allocation must now be refused -- it would
2040 // otherwise be free to reallocate the very buffer `view` points
2041 // into. This call is expected to panic before returning.
2042 let _ = engine.allocate_instance(tmpl);
2043
2044 // Unreachable: the panic above must fire first. Left here (rather
2045 // than omitted) so that if the seal is ever accidentally weakened to
2046 // a silent no-op instead of a panic, this test still fails loudly
2047 // (via the leaked, un-reclaimed `h`/`view` triggering nothing
2048 // useful) rather than passing by accident through an early return.
2049 unreachable!("allocate_instance after sv_get_wide must panic, not return");
2050 }
2051
2052 /// Review fix (Task 9 r4, blocking finding): `sv_get_wide`'s returned
2053 /// `WideView` pointers are Undefined Behavior to dereference under
2054 /// Stacked Borrows as soon as *any* subsequent `Arena::wide_aval_mut`/
2055 /// `wide_bval_mut` reborrow happens anywhere in the same `Arena` --
2056 /// which the finding showed fires on the very next `sv_get_wide` call
2057 /// for a *different* instance/slot (no write required at all), or on
2058 /// the next wide-class `commit_write_at` write to any wide signal. The
2059 /// Task 9 r3 fix (`Arena::wide_sealed`/`seal_wide_plane`) only prevents
2060 /// the backing `Vec` from *reallocating* -- a real but different hazard
2061 /// -- and does nothing about this separate, far more easily triggered
2062 /// reborrow-invalidation hazard.
2063 ///
2064 /// This test exercises both of the finding's exact trigger shapes
2065 /// against a real `sv_get_wide`-returned `WideView`, then reads back
2066 /// through the *first* view -- the shape `sv_get_set_wide_slices_the_
2067 /// correct_word_range_per_instance` never covered, since it always
2068 /// fully consumed `view_a` before `view_b` ever came into existence.
2069 /// Built via [`engine_with_manual_wide_template`] (no `compile_source`/
2070 /// `sv_lang` FFI) specifically so it can run under Miri:
2071 ///
2072 /// ```text
2073 /// CARGO_TARGET_DIR=/mnt/bulk/sv-sim-target-wA-state \
2074 /// rustup run nightly cargo miri test -p sv-sim-sim \
2075 /// sv_get_wide_view_stays_valid_across_a_later_sv_get_wide_and_a_later_wide_class_write
2076 /// ```
2077 ///
2078 /// Before the Task 9 r4 fix (raw-base-pointer construction in
2079 /// `Arena::wide_aval_ptr`/`wide_bval_ptr`, used by `sv_get_wide` instead
2080 /// of `wide_aval_mut()[start..].as_mut_ptr()`), the above Miri
2081 /// invocation reproduced exactly the "tag does not exist in the borrow
2082 /// stack for this location" error the finding described, at the
2083 /// `view_a` read-backs below. Under `cargo test` (no Miri) this test
2084 /// also passes before the fix -- the UB is invisible without Miri,
2085 /// which is exactly why the fix needs Miri to validate, not just this
2086 /// test's ordinary pass/fail.
2087 #[test]
2088 fn sv_get_wide_view_stays_valid_across_a_later_sv_get_wide_and_a_later_wide_class_write() {
2089 let tmpl = TemplateKey {
2090 definition: 6,
2091 params_hash: 0,
2092 };
2093 let mut engine = engine_with_manual_wide_template(tmpl, 2);
2094
2095 // Both instances are allocated up front, before any sv_get_wide
2096 // call -- exactly like sv_get_set_wide_slices_the_correct_word_
2097 // range_per_instance above -- so `seal_wide_plane` (fired by the
2098 // first sv_get_wide call below) never blocks either allocation.
2099 let handle_a = engine.allocate_instance(tmpl).expect("template registered");
2100 let handle_b = engine.allocate_instance(tmpl).expect("template registered");
2101 let h_a = handle_a.into_sim_handle();
2102 let h_b = handle_b.into_sim_handle();
2103 let slot = SlotId(0);
2104
2105 // Seed instance A's wide slot with a known pattern via the ordinary
2106 // write path, then retain a WideView onto it via sv_get_wide -- the
2107 // exact "aliasing view retained past the call" shape the finding
2108 // describes.
2109 let mut seed_aval = [0x1111_1111_1111_1111u64, 0x2222_2222_2222_2222u64];
2110 let mut seed_bval = [0x3333_3333_3333_3333u64, 0x4444_4444_4444_4444u64];
2111 sv_set_wide(
2112 h_a,
2113 slot,
2114 WideView {
2115 aval: seed_aval.as_mut_ptr(),
2116 bval: seed_bval.as_mut_ptr(),
2117 word_count: 2,
2118 },
2119 );
2120 let view_a = sv_get_wide(h_a, slot);
2121 assert_eq!(view_a.word_count, 2);
2122
2123 // Finding scenario (b): a second, read-only-shaped `sv_get_wide`
2124 // call for a *different* instance's slot, with zero intervening
2125 // writes. Before the fix, this alone invalidated `view_a`'s
2126 // pointers under Miri.
2127 let view_b_probe = sv_get_wide(h_b, slot);
2128 assert_eq!(view_b_probe.word_count, 2);
2129
2130 // `view_a` must still be dereferenceable and correct afterward.
2131 // SAFETY: `view_a` aliases instance A's own wide-plane words,
2132 // reserved for it (Task 7's disjoint-base guarantee) and still live
2133 // for this same test's `engine`/`handle_a`.
2134 unsafe {
2135 assert_eq!(std::slice::from_raw_parts(view_a.aval, 2), seed_aval);
2136 assert_eq!(std::slice::from_raw_parts(view_a.bval, 2), seed_bval);
2137 }
2138
2139 // Finding scenario (a): a later wide-class write to a *different*
2140 // wide signal (instance B's slot) via `sv_set_wide` -- reaches
2141 // `write::write_slot`'s `wide_aval_mut`/`wide_bval_mut`-based arm.
2142 // Before the fix, this also invalidated `view_a`'s pointers under
2143 // Miri.
2144 let mut other_aval = [0x5555_5555_5555_5555u64, 0x6666_6666_6666_6666u64];
2145 let mut other_bval = [0x7777_7777_7777_7777u64, 0x8888_8888_8888_8888u64];
2146 sv_set_wide(
2147 h_b,
2148 slot,
2149 WideView {
2150 aval: other_aval.as_mut_ptr(),
2151 bval: other_bval.as_mut_ptr(),
2152 word_count: 2,
2153 },
2154 );
2155
2156 // `view_a` must STILL be valid, readable, and writable through its
2157 // own raw pointers after that unrelated write -- and a fresh
2158 // `sv_get_wide` call must observe a write made directly through the
2159 // retained view, proving it still addresses the arena's real live
2160 // storage.
2161 // SAFETY: same reasoning as above; instance B's own disjoint base
2162 // means its write never touches instance A's words.
2163 unsafe {
2164 assert_eq!(std::slice::from_raw_parts(view_a.aval, 2), seed_aval);
2165 assert_eq!(std::slice::from_raw_parts(view_a.bval, 2), seed_bval);
2166 *view_a.aval.add(0) = 0x9999_9999_9999_9999;
2167 }
2168 let view_a_again = sv_get_wide(h_a, slot);
2169 // SAFETY: a fresh view over the same still-live slot.
2170 unsafe {
2171 assert_eq!(*view_a_again.aval, 0x9999_9999_9999_9999);
2172 }
2173
2174 // SAFETY: reclaim the two handles this test leaked via
2175 // `into_sim_handle`, exactly as this module's other tests do.
2176 unsafe {
2177 drop(Box::from_raw(h_a.0 as *mut InstanceHandle));
2178 drop(Box::from_raw(h_b.0 as *mut InstanceHandle));
2179 }
2180 }
2181}