vj_sim/state/arena.rs
1//! Task 2 — SoA arena storage, word-alignment, and the FFI arena view (ledger
2//! ST-007, ST-008, ST-009, ST-038, ST-039, ST-145).
3//!
4//! This is the one design-wide flat Structure-of-Arrays backing store
5//! ARCHITECTURE §6.2/state.md's "two tiers, hard 64-bit boundary" describes:
6//! signals with `bit_width <= 64` live inline as a single packed `u128` word
7//! (val low 64 bits / xzmask high 64 bits, `sv_lang::Bit`/`Bit4` convention —
8//! see `state::value`), signals wider than 64 bits (and all memories) live in
9//! two parallel `Vec<u64>` planes (`wide_aval`/`wide_bval`), word-aligned, no
10//! cross-signal bit-packing (ST-038: even a 1-bit wire gets a full `u128`).
11//!
12//! No signal is ever represented as a per-signal heap object (ST-007): the
13//! only fields on [`Arena`] are the five flat backing `Vec`s themselves — no
14//! `HashMap`/`BTreeMap` anywhere in this file.
15//!
16//! **Review fix (Task 7 r1): interior mutability, not `&mut self`, for every
17//! accessor that grows or writes through the backing `Vec`s.** Task 7's
18//! `InstanceHandle::arena` mints a `*const Arena` once and reuses it for the
19//! life of the simulation run — including across every *later*
20//! `StateEngine::allocate_instance` call and (from Task 8 on) `commit_write`.
21//! Under Rust's Stacked/Tree Borrows aliasing model, forming a fresh
22//! `&mut Arena` *at all* (even one that never writes) invalidates every
23//! previously-derived raw pointer's borrow-stack tag on `Arena`'s own field
24//! memory — confirmed by `cargo +nightly miri test -p vj-sim instance::`
25//! failing on exactly that pattern before this fix. The five backing `Vec`s
26//! are therefore each wrapped in [`std::cell::UnsafeCell`] and every method
27//! that grows or writes through one takes `&self`: growing/writing methods
28//! are `unsafe fn` (the caller must uphold the single-logical-writer
29//! discipline [`Arena::raw_inline_ptr`] already documented, and must not
30//! hold a live `&[T]`/`&mut [T]` from any other accessor across the call);
31//! read-only methods stay safe, since two live `&[T]` reads never alias.
32//! `self`'s own type therefore never again needs a `&mut Arena` reborrow
33//! after a `Box<Arena>` is constructed, so a `*const Arena` minted once
34//! stays valid. (This still assumes no code takes `&mut *engine.arena`
35//! directly through `StateEngine`'s `pub arena: Box<Arena>` field, bypassing
36//! this module's methods entirely — that remains a documented trust
37//! boundary of the `pub` field, not something this module can prevent.)
38
39use std::cell::{Cell, UnsafeCell};
40use std::ptr;
41
42/// Whether an [`Arena`] backs a 4-state (`aval`/`bval`, X/Z-capable) or
43/// 2-state (`inline_2s`/`wide_2s`, val-only) compiled binary. Feeds Task 10's
44/// 2-state build path; this task only allocates/exposes the storage, it does
45/// not yet implement 2-state read/write semantics.
46#[derive(Clone, Copy, PartialEq, Eq, Debug)]
47pub enum BuildMode {
48 FourState,
49 TwoState,
50}
51
52/// The one design-wide flat SoA backing store (ST-007: grouped by storage
53/// tier, not a per-signal heap object model). `inline` holds every
54/// `bit_width <= 64` signal as a packed `u128` (val low 64 / xzmask high 64);
55/// `wide_aval`/`wide_bval` hold every wider signal and every memory element,
56/// word-aligned, at identical relative offsets in both planes (ST-009);
57/// `inline_2s`/`wide_2s` are the 2-state-build counterparts (Task 10 exercises
58/// them; this task only reserves the fields).
59///
60/// Each backing `Vec` is wrapped in [`UnsafeCell`] (review fix, Task 7 r1):
61/// see the module-level doc comment for why growing/writing must never
62/// require a fresh `&mut Arena`.
63///
64/// **Task 20 (ST-040) — cross-signal bit-packing is out of scope for v1, by
65/// deliberate decision, not a silently-dropped optimization.** Every signal
66/// gets a full `u128` inline word or full word-aligned wide-plane slot (see
67/// the module doc comment's "no cross-signal bit-packing... even a 1-bit
68/// wire gets a full `u128`"); nothing in this file ever packs two or more
69/// narrow signals into one word's spare bits. This is a **tracked v2 scaling
70/// risk** for huge port-heavy designs (a design with millions of 1-bit
71/// signals pays a full 16 bytes each rather than the theoretical 1 bit),
72/// cross-referenced at `docs/design/state.md` §1/Risks and restated in
73/// `docs/CONTRACTS.md`'s "Wave A — wA-state notes" section so it is
74/// discoverable without reading this file.
75pub struct Arena {
76 mode: BuildMode,
77 inline: UnsafeCell<Vec<u128>>,
78 wide_aval: UnsafeCell<Vec<u64>>,
79 wide_bval: UnsafeCell<Vec<u64>>,
80 inline_2s: UnsafeCell<Vec<u64>>,
81 wide_2s: UnsafeCell<Vec<u64>>,
82 /// **Review fix (Task 14 r2, blocking finding 2):** a wide-word plane
83 /// pair dedicated exclusively to `driver::force_driver`'s dynamically
84 /// -allocated Wide/Memory-class contribution cells (ST-055). Unlike
85 /// `wide_aval`/`wide_bval`, nothing in this crate ever hands out a raw
86 /// pointer aliasing this Vec's backing storage (`ffi_view`/`sv_get_wide`
87 /// never touch it — only [`SlotClass::ForceWide`]-class `SlotLocation`s
88 /// address it, and those are only ever minted by `force_driver`, never by
89 /// the layout builder or the accessor ABI), so growing/reallocating it
90 /// can never dangle an already-issued `WideView`. It is therefore never
91 /// gated by `wide_sealed` — `force`/`release` (ST-055) must stay real,
92 /// always-available runtime operations even after `sv_get_wide` has
93 /// sealed the *ordinary* wide plane (see `alloc_force_wide_words`'s doc
94 /// comment for the full argument this fixes).
95 force_wide_aval: UnsafeCell<Vec<u64>>,
96 force_wide_bval: UnsafeCell<Vec<u64>>,
97 /// **Review fix (Task 9 r3): closes a wide-plane use-after-free.**
98 /// `accessor_impl.rs::sv_get_wide` returns a `WideView` whose `aval`/
99 /// `bval` raw pointers deliberately alias `wide_aval`/`wide_bval`'s own
100 /// live storage and are retained by the caller past the call (unlike
101 /// every other accessor, which never retains a pointer). Unlike
102 /// `inline`, the wide planes are one flat, growable `Vec<u64>` each (not
103 /// individually boxed per allocation — the FFI arena view, ST-145,
104 /// needs one truly contiguous raw-pointer-addressable buffer per plane,
105 /// so `TemplateRegistry`'s per-entry-boxing fix doesn't apply here
106 /// without breaking that contract): a later `allocate_instance` call
107 /// growing them via `alloc_wide_words` could reallocate the backing
108 /// buffer and dangle every previously issued `WideView`. `sv_get_wide`
109 /// sets this the moment it hands out such a view (`seal_wide_plane`
110 /// below); `alloc_wide_words` then refuses (panics on) any further
111 /// nonzero-word growth, turning the hazard into an immediate, loud
112 /// failure at the first offending call instead of a silent dangling
113 /// pointer. A `words == 0` request stays a legal no-op even once
114 /// sealed — it never touches either `Vec`'s capacity, so it can't
115 /// reallocate anything (see `alloc_inline_words`'s identical
116 /// zero-is-a-no-op contract).
117 wide_sealed: Cell<bool>,
118 /// Huge-signal mirrors (`state::huge_mirror`): the registry itself lives
119 /// there, this is only where an arena keeps it. Every plane writer
120 /// invalidates the mirrors it overlaps.
121 pub(crate) huge: super::huge_mirror::HugeMirrors,
122}
123
124// `UnsafeCell` makes `Arena` `!Sync` by default, which is correct: nothing
125// in this module claims Arena is safe to share across threads without
126// external synchronization. `raw_inline_ptr`'s own SAFETY comment already
127// documents the single-logical-writer-per-offset discipline multi-threaded
128// callers (e.g. this file's `concurrent_writes_to_disjoint_words_produce_no_torn_reads`
129// test) must uphold by hand.
130impl std::fmt::Debug for Arena {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 // SAFETY: reads only each Vec's `.len()` through a shared reference
133 // for diagnostic purposes; per this Arena's single-logical-writer
134 // invariant (see the module doc comment and `raw_inline_ptr`), a
135 // length read that doesn't outlive this call and races no other
136 // access is sound the same way `inline()`/`wide_aval()`/`wide_bval()`
137 // below are.
138 let (inline_len, wide_len, inline_2s_len, wide_2s_len, force_wide_len) = unsafe {
139 (
140 (*self.inline.get()).len(),
141 (*self.wide_aval.get()).len(),
142 (*self.inline_2s.get()).len(),
143 (*self.wide_2s.get()).len(),
144 (*self.force_wide_aval.get()).len(),
145 )
146 };
147 f.debug_struct("Arena")
148 .field("mode", &self.mode)
149 .field("inline_len", &inline_len)
150 .field("wide_len", &wide_len)
151 .field("inline_2s_len", &inline_2s_len)
152 .field("wide_2s_len", &wide_2s_len)
153 .field("force_wide_len", &force_wide_len)
154 .finish()
155 }
156}
157
158impl Arena {
159 pub fn new(mode: BuildMode) -> Self {
160 Arena {
161 mode,
162 inline: UnsafeCell::new(Vec::new()),
163 wide_aval: UnsafeCell::new(Vec::new()),
164 wide_bval: UnsafeCell::new(Vec::new()),
165 inline_2s: UnsafeCell::new(Vec::new()),
166 wide_2s: UnsafeCell::new(Vec::new()),
167 force_wide_aval: UnsafeCell::new(Vec::new()),
168 force_wide_bval: UnsafeCell::new(Vec::new()),
169 wide_sealed: Cell::new(false),
170 huge: super::huge_mirror::HugeMirrors::default(),
171 }
172 }
173
174 /// **Review fix (Task 9 r3):** called by `accessor_impl.rs::sv_get_wide`
175 /// the instant it hands out a `WideView` aliasing the wide planes' live
176 /// storage — see [`Self::wide_sealed`]'s doc comment. Idempotent and
177 /// safe (`Cell::set`, no aliasing concern): any number of `sv_get_wide`
178 /// calls, from any number of instances, just keep it set.
179 pub(crate) fn seal_wide_plane(&self) {
180 self.wide_sealed.set(true);
181 }
182
183 /// Whether [`Self::seal_wide_plane`] has fired — exposed so tests can
184 /// assert the seal state directly rather than only through
185 /// `alloc_wide_words`'s panic message.
186 #[cfg(test)]
187 pub(crate) fn is_wide_plane_sealed(&self) -> bool {
188 self.wide_sealed.get()
189 }
190
191 /// Appends `n` new inline `u128` words in one call, returning the shared
192 /// start offset (Task 7, ST-067/ST-136's bulk variant of
193 /// [`Self::alloc_inline_word`] — reserves a whole template instance's
194 /// inline words in one bump, the same way [`Self::alloc_wide_words`]
195 /// already bulk-allocates the wide planes). `n == 0` is a legal no-op
196 /// that still returns the current length (a template with zero inline
197 /// words gets a well-defined, if unused, base).
198 ///
199 /// # Safety
200 /// The caller must ensure no other live `&`/`&mut` borrow of the inline
201 /// plane (from [`Self::inline`], [`Self::inline_mut`], or
202 /// [`Self::raw_inline_ptr`]) is held across this call, and that at most
203 /// one logical writer grows this Arena's planes at a time (the same
204 /// single-logical-writer discipline [`Self::raw_inline_ptr`] documents).
205 /// This method takes `&self`, not `&mut self`, specifically so it can be
206 /// called through a `*const Arena` minted once (e.g.
207 /// `InstanceHandle::arena`) without ever forming a fresh `&mut Arena` —
208 /// see the module-level doc comment.
209 pub unsafe fn alloc_inline_words(&self, n: u32) -> u32 {
210 // SAFETY: forwarded to this function's own `# Safety` section above.
211 unsafe {
212 let inline = &mut *self.inline.get();
213 let inline_2s = &mut *self.inline_2s.get();
214 let off = inline.len() as u32;
215 for _ in 0..n {
216 inline.push(0);
217 inline_2s.push(0);
218 }
219 off
220 }
221 }
222
223 /// Appends one new inline `u128` word, returning its `word_offset`
224 /// (always `len`-before-push — dense, order-preserving, never a reused or
225 /// coalesced gap: ST-038's "even a single-bit wire gets a full word").
226 /// Task 7: a thin wrapper over [`Self::alloc_inline_words`]`(1)`.
227 ///
228 /// # Safety
229 /// Same obligations as [`Self::alloc_inline_words`].
230 pub unsafe fn alloc_inline_word(&self) -> u32 {
231 // SAFETY: forwarded to `alloc_inline_words`'s `# Safety` section.
232 unsafe { self.alloc_inline_words(1) }
233 }
234
235 /// Appends `words` new `u64` slots to both wide planes in lockstep,
236 /// returning the shared start offset (ST-009: the same relative
237 /// addressing scheme in both planes).
238 ///
239 /// # Panics
240 /// **Review fix (Task 9 r3):** if `words > 0` and [`Self::seal_wide_plane`]
241 /// has already been called on this `Arena` (i.e. `sv_get_wide` has
242 /// already handed some caller a `WideView` aliasing these planes' live
243 /// storage) — growing either `Vec` now could reallocate its backing
244 /// buffer and dangle that already-issued `WideView`, so this refuses
245 /// rather than silently doing it. See [`Self::wide_sealed`]'s doc
246 /// comment for the full hazard. `words == 0` is exempt (it never
247 /// touches either `Vec`'s capacity, so it can never reallocate
248 /// anything) and stays the legal, always-safe no-op it already was.
249 ///
250 /// # Safety
251 /// The caller must ensure no other live `&`/`&mut` borrow of either wide
252 /// plane (from [`Self::wide_aval`]/[`Self::wide_bval`]/
253 /// [`Self::wide_aval_mut`]/[`Self::wide_bval_mut`]) is held across this
254 /// call, and that at most one logical writer grows this Arena's planes
255 /// at a time. `&self`, not `&mut self`, for the same reason as
256 /// [`Self::alloc_inline_words`].
257 pub unsafe fn alloc_wide_words(&self, words: u32) -> u32 {
258 assert!(
259 words == 0 || !self.wide_sealed.get(),
260 "Arena::alloc_wide_words: refusing to grow the wide plane -- \
261 sv_get_wide has already handed out a WideView aliasing this \
262 Arena's wide_aval/wide_bval live storage, and growing them now \
263 would reallocate that storage and dangle it (a use-after-free); \
264 no further wide-word allocation (e.g. via allocate_instance) is \
265 legal against this Arena once any sv_get_wide call has been made"
266 );
267 // SAFETY: forwarded to this function's own `# Safety` section above.
268 unsafe {
269 let wide_aval = &mut *self.wide_aval.get();
270 let wide_bval = &mut *self.wide_bval.get();
271 let wide_2s = &mut *self.wide_2s.get();
272 let off = wide_aval.len() as u32;
273 for _ in 0..words {
274 wide_aval.push(0);
275 wide_bval.push(0);
276 wide_2s.push(0);
277 }
278 off
279 }
280 }
281
282 /// **Review fix (Task 14 r2, blocking finding 2):** the `force_wide_aval`/
283 /// `force_wide_bval` counterpart of [`Self::alloc_wide_words`] —
284 /// `driver::force_driver`'s Wide/Memory-class contribution-cell
285 /// allocator. Deliberately **never** consults [`Self::wide_sealed`]: that
286 /// flag exists only to protect `WideView`s `sv_get_wide` has handed out
287 /// aliasing `wide_aval`/`wide_bval`'s storage, and this method never
288 /// grows those planes at all — it grows a wholly separate pair that no
289 /// accessor-ABI function or `ffi_view` call ever exposes a pointer into
290 /// (see this field pair's own doc comment on [`Arena`]). Growing/
291 /// reallocating it is therefore unconditionally safe, `wide_sealed` or
292 /// not, which is exactly what makes `force`/`release` real, always
293 /// -available runtime operations (ST-055) rather than ones that panic the
294 /// instant any wide signal has been read through `sv_get_wide` first —
295 /// the bug this fixes (Task 14 review finding: `force_driver` on a net
296 /// wider than 64 bits panicked via `alloc_wide_words` once the ordinary
297 /// wide plane had been sealed by an unrelated prior `sv_get_wide` call).
298 ///
299 /// # Safety
300 /// Same obligations as [`Self::alloc_wide_words`], applied to this
301 /// separate plane pair.
302 pub unsafe fn alloc_force_wide_words(&self, words: u32) -> u32 {
303 // SAFETY: forwarded to this function's own `# Safety` section above.
304 unsafe {
305 let force_wide_aval = &mut *self.force_wide_aval.get();
306 let force_wide_bval = &mut *self.force_wide_bval.get();
307 let off = force_wide_aval.len() as u32;
308 for _ in 0..words {
309 force_wide_aval.push(0);
310 force_wide_bval.push(0);
311 }
312 off
313 }
314 }
315
316 /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
317 /// to the force-only wide `aval` plane ([`Self::alloc_force_wide_words`]'s
318 /// doc comment).
319 pub fn force_wide_aval(&self) -> &[u64] {
320 // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
321 unsafe { &*self.force_wide_aval.get() }
322 }
323
324 /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
325 /// to the force-only wide `bval` plane ([`Self::alloc_force_wide_words`]'s
326 /// doc comment).
327 pub fn force_wide_bval(&self) -> &[u64] {
328 // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
329 unsafe { &*self.force_wide_bval.get() }
330 }
331
332 /// # Safety
333 /// Same obligations as [`Self::inline_mut`], applied to the force-only
334 /// wide `aval` plane.
335 #[allow(clippy::mut_from_ref)]
336 pub unsafe fn force_wide_aval_mut(&self) -> &mut [u64] {
337 // SAFETY: forwarded to this function's own `# Safety` section above.
338 unsafe { &mut *self.force_wide_aval.get() }
339 }
340
341 /// # Safety
342 /// Same obligations as [`Self::inline_mut`], applied to the force-only
343 /// wide `bval` plane.
344 #[allow(clippy::mut_from_ref)]
345 pub unsafe fn force_wide_bval_mut(&self) -> &mut [u64] {
346 // SAFETY: forwarded to this function's own `# Safety` section above.
347 unsafe { &mut *self.force_wide_bval.get() }
348 }
349
350 /// Task 13 (ST-100, ST-139): the 2-state-family name for the same bulk
351 /// growth [`Self::alloc_inline_words`] already performs on the
352 /// `inline_2s` plane. **Deviation from the plan's literal prose** ("this
353 /// task adds their allocators", implying a growth operation independent
354 /// of the 4-state family's own bulk allocators): `alloc_inline_words`
355 /// already grows `inline`/`inline_2s` in lockstep (see its own body
356 /// above), which is load-bearing, not incidental -- a `SlotOffset`'s
357 /// `word_offset` is computed once by the mode-agnostic Task 4/6 layout
358 /// builder and must address the *same* position regardless of which
359 /// build mode the compiled binary ends up using, so the two planes'
360 /// offset spaces cannot be allowed to diverge. A genuinely independent
361 /// `inline_2s`-only counter would break that invariant the first time a
362 /// caller allocated through one family without the other. This method is
363 /// therefore a thin, same-offset forwarding wrapper -- a 2-state-family
364 /// name for API discoverability/completeness, not a second allocator.
365 ///
366 /// # Safety
367 /// Same obligations as [`Self::alloc_inline_words`].
368 pub unsafe fn alloc_inline_2s_words(&self, n: u32) -> u32 {
369 // SAFETY: forwarded to `alloc_inline_words`'s `# Safety` section.
370 unsafe { self.alloc_inline_words(n) }
371 }
372
373 /// Task 13 (ST-100, ST-139): the wide-plane counterpart of
374 /// [`Self::alloc_inline_2s_words`] -- see that method's doc comment for
375 /// why this forwards to [`Self::alloc_wide_words`] rather than growing
376 /// `wide_2s` independently.
377 ///
378 /// # Safety
379 /// Same obligations as [`Self::alloc_wide_words`].
380 pub unsafe fn alloc_wide_2s_words(&self, n: u32) -> u32 {
381 // SAFETY: forwarded to `alloc_wide_words`'s `# Safety` section.
382 unsafe { self.alloc_wide_words(n) }
383 }
384
385 /// Read-only access to the inline plane (test/read helper — the hot path
386 /// is the FFI raw-pointer view below). Safe: any number of live `&[T]`
387 /// reads coexist without aliasing; the aliasing obligation is on the
388 /// `unsafe` mutating accessors instead (see their doc comments).
389 pub fn inline(&self) -> &[u128] {
390 // SAFETY: produces a shared slice from the `UnsafeCell`. Multiple
391 // concurrent `&[u128]` reads never conflict with each other; the
392 // caller of any mutating accessor (`alloc_inline_words`,
393 // `inline_mut`, `raw_inline_ptr`) is the one obligated not to hold a
394 // read like this one live across their call (documented on each).
395 unsafe { &*self.inline.get() }
396 }
397
398 /// Mutable access to the inline plane.
399 ///
400 /// # Safety
401 /// The caller must ensure no other live `&`/`&mut` borrow of the inline
402 /// plane exists for as long as the returned slice is used, and that no
403 /// other writer touches it concurrently (single-logical-writer, same as
404 /// [`Self::raw_inline_ptr`]). `&self`, not `&mut self`, for the same
405 /// reason as [`Self::alloc_inline_words`] — see the module-level doc
406 /// comment.
407 // `clippy::mut_from_ref` flags `fn(&self) -> &mut _` on sight as a
408 // near-universal UB source; here it's the deliberate `UnsafeCell`
409 // interior-mutability pattern this whole module documents (the
410 // aliasing obligation is pushed onto the caller via `# Safety` above,
411 // not silently ignored), so it's a known false positive for this shape.
412 #[allow(clippy::mut_from_ref)]
413 pub unsafe fn inline_mut(&self) -> &mut [u128] {
414 // SAFETY: forwarded to this function's own `# Safety` section above.
415 unsafe { &mut *self.inline.get() }
416 }
417
418 /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
419 /// to the wide `aval` plane.
420 pub fn wide_aval(&self) -> &[u64] {
421 // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
422 unsafe { &*self.wide_aval.get() }
423 }
424
425 /// Safe: see [`Self::inline`]'s doc comment — the same reasoning applies
426 /// to the wide `bval` plane.
427 pub fn wide_bval(&self) -> &[u64] {
428 // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
429 unsafe { &*self.wide_bval.get() }
430 }
431
432 /// # Safety
433 /// Same obligations as [`Self::inline_mut`], applied to the wide `aval`
434 /// plane.
435 // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
436 #[allow(clippy::mut_from_ref)]
437 pub unsafe fn wide_aval_mut(&self) -> &mut [u64] {
438 // SAFETY: forwarded to this function's own `# Safety` section above.
439 unsafe { &mut *self.wide_aval.get() }
440 }
441
442 /// # Safety
443 /// Same obligations as [`Self::inline_mut`], applied to the wide `bval`
444 /// plane.
445 // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
446 #[allow(clippy::mut_from_ref)]
447 pub unsafe fn wide_bval_mut(&self) -> &mut [u64] {
448 // SAFETY: forwarded to this function's own `# Safety` section above.
449 unsafe { &mut *self.wide_bval.get() }
450 }
451
452 /// **Review fix (Task 9 r4, blocking finding): the Stacked-Borrows-safe
453 /// way to obtain a pointer into the wide `aval` plane that the caller
454 /// retains and dereferences *after* this call returns** (unlike every
455 /// other accessor on this type, which is used and discarded within a
456 /// single call). [`Self::wide_aval_mut`] is unsound for this purpose:
457 /// under Rust's Stacked Borrows aliasing model, `&mut *self.wide_aval
458 /// .get()` (plus the `Vec<u64> -> &mut [u64]` deref coercion
459 /// `wide_aval_mut`'s return type triggers) retags the plane's backing
460 /// buffer with a *fresh* `Unique` tag on **every call** — a raw pointer
461 /// formed from *that* reborrow (e.g. via `wide_aval_mut()[start..]
462 /// .as_mut_ptr()`, the pattern `sv_get_wide` used before this fix) is
463 /// invalidated the very next time *any* code calls `wide_aval_mut()`
464 /// again for this `Arena` (a later `sv_get_wide` for a different slot,
465 /// or a wide-class `commit_write_at` write via [`Self::wide_aval_mut`]
466 /// in `write::write_slot`) — confirmed via an isolated Miri repro of
467 /// exactly this pattern (see this crate's Task 9 r4 fix commit).
468 ///
469 /// This method instead reads `Vec::as_mut_ptr()` directly off the
470 /// `UnsafeCell`-derived place (`(*self.wide_aval.get()).as_mut_ptr()`)
471 /// **without ever forming a `&mut [u64]`/`&[u64]` reference over the
472 /// buffer** — `Vec::as_mut_ptr()`'s only effect is reading the `Vec`'s
473 /// own `ptr` field (a plain pointer *value* copy, not a reborrow of the
474 /// heap buffer it points to), so the returned pointer keeps the
475 /// buffer's original allocation tag every time this is called, for the
476 /// life of the `Arena` (as long as [`Self::seal_wide_plane`] has fixed
477 /// the buffer's address — see [`Self::alloc_wide_words`]'s panic
478 /// guard). Because that tag sits at the *bottom* of the buffer's borrow
479 /// stack, it survives every later child reborrow pushed on top of it by
480 /// `wide_aval_mut`/`wide_aval`/`wide_aval_mut()`-based writes and
481 /// reads elsewhere (verified: Miri accepts a pointer obtained this way
482 /// being read/written both before *and* after such an intervening
483 /// slice-based access to the same plane, disjoint range or not).
484 /// Pointer arithmetic (`.add(offset)`) against the returned base
485 /// pointer, never slicing, is the caller's obligation for the same
486 /// reason.
487 ///
488 /// # Safety
489 /// The Arena's wide planes must already be sealed ([`Self::seal_wide_plane`])
490 /// or the caller must otherwise guarantee no later [`Self::alloc_wide_words`]
491 /// call reallocates the buffer while this pointer is retained (same
492 /// reallocation hazard [`Self::wide_sealed`]'s doc comment describes);
493 /// `offset` plus however many words the caller reads/writes through the
494 /// result must stay within the plane's allocated length.
495 pub(crate) unsafe fn wide_aval_ptr(&self) -> *mut u64 {
496 // SAFETY: forwarded to this function's own `# Safety` section above;
497 // reads only the `Vec`'s own `ptr` field (via the auto-ref
498 // `as_mut_ptr()`'s `&mut self` receiver requires, scoped to this
499 // call and touching only the `Vec` struct's own memory, never the
500 // heap buffer) and returns it by value — no `&mut [u64]`/`&[u64]`
501 // over the buffer is ever formed here.
502 unsafe { (*self.wide_aval.get()).as_mut_ptr() }
503 }
504
505 /// The `wide_bval` twin of [`Self::wide_aval_ptr`] — see its doc comment
506 /// for the full Stacked-Borrows argument, identical here.
507 ///
508 /// # Safety
509 /// Same obligations as [`Self::wide_aval_ptr`], applied to the `wide_bval`
510 /// plane.
511 pub(crate) unsafe fn wide_bval_ptr(&self) -> *mut u64 {
512 // SAFETY: see `wide_aval_ptr`'s SAFETY comment; identical reasoning.
513 unsafe { (*self.wide_bval.get()).as_mut_ptr() }
514 }
515
516 /// Task 13 (ST-100, ST-102, ST-139): read-only access to the 2-state
517 /// inline plane -- see [`Self::inline`]'s doc comment, identical
518 /// reasoning.
519 pub fn inline_2s(&self) -> &[u64] {
520 // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
521 unsafe { &*self.inline_2s.get() }
522 }
523
524 /// # Safety
525 /// Same obligations as [`Self::inline_mut`], applied to the `inline_2s`
526 /// plane.
527 // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
528 #[allow(clippy::mut_from_ref)]
529 pub unsafe fn inline_2s_mut(&self) -> &mut [u64] {
530 // SAFETY: forwarded to this function's own `# Safety` section above.
531 unsafe { &mut *self.inline_2s.get() }
532 }
533
534 /// Task 13 (ST-100, ST-102, ST-139): read-only access to the 2-state
535 /// wide plane -- see [`Self::inline`]'s doc comment, identical
536 /// reasoning.
537 pub fn wide_2s(&self) -> &[u64] {
538 // SAFETY: see `inline()`'s SAFETY comment; identical reasoning.
539 unsafe { &*self.wide_2s.get() }
540 }
541
542 /// # Safety
543 /// Same obligations as [`Self::inline_mut`], applied to the `wide_2s`
544 /// plane.
545 // See `inline_mut`'s `clippy::mut_from_ref` comment — same reasoning.
546 #[allow(clippy::mut_from_ref)]
547 pub unsafe fn wide_2s_mut(&self) -> &mut [u64] {
548 // SAFETY: forwarded to this function's own `# Safety` section above.
549 unsafe { &mut *self.wide_2s.get() }
550 }
551
552 pub fn mode(&self) -> BuildMode {
553 self.mode
554 }
555
556 /// The FFI arena view (ST-145): raw pointers into the three planes a
557 /// compiled kernel indexes through the accessor ABI. `wide_aval`/
558 /// `wide_bval` are null when `mode == TwoState` — the 4-state planes
559 /// simply aren't touched in a 2-state build. Takes `&mut self`: this is
560 /// Task 2's original, not-yet-consumed-anywhere entry point (no Task 7
561 /// code path calls it against an `Arena` that already has live
562 /// `InstanceHandle`s pointing at it), so it keeps requiring genuine
563 /// exclusive access rather than adopting Task 7's `&self`/`UnsafeCell`
564 /// pattern; `UnsafeCell::get_mut()` is a safe way to reach the backing
565 /// `Vec`s given that exclusive access.
566 pub fn ffi_view(&mut self) -> StateArena {
567 StateArena {
568 inline: self.inline.get_mut().as_mut_ptr(),
569 wide_aval: if self.mode == BuildMode::TwoState {
570 ptr::null_mut()
571 } else {
572 self.wide_aval.get_mut().as_mut_ptr()
573 },
574 wide_bval: if self.mode == BuildMode::TwoState {
575 ptr::null_mut()
576 } else {
577 self.wide_bval.get_mut().as_mut_ptr()
578 },
579 }
580 }
581
582 /// Raw pointer to the start of the inline plane, for callers that need to
583 /// hand out disjoint per-word pointers to multiple worker threads
584 /// (ST-039: word-aligned, non-packed slots enable lock-free writes across
585 /// worker threads with no per-write locking). Takes `&mut self` for the
586 /// same reason [`Self::ffi_view`] does — see its doc comment.
587 ///
588 /// # Safety
589 /// The caller writes only through offsets it obtained from this Arena's
590 /// own `alloc_*` calls, each offset written by at most one thread for the
591 /// duration of the unsafe scope, and must not alias with any `&Arena`/
592 /// `&mut Arena` use until the writer threads have joined.
593 pub unsafe fn raw_inline_ptr(&mut self) -> *mut u128 {
594 self.inline.get_mut().as_mut_ptr()
595 }
596}
597
598/// `#[repr(C)]` FFI view of the arena's storage planes (ST-145) — the shape a
599/// compiled kernel's accessor-ABI functions index through. `wide_aval`/
600/// `wide_bval` are `null` in a 2-state-compiled binary.
601#[repr(C)]
602pub struct StateArena {
603 pub inline: *mut u128,
604 pub wide_aval: *mut u64,
605 pub wide_bval: *mut u64,
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 /// ST-007: a `HashMap<SignalId, Box<Signal>>`-shaped implementation has
613 /// no way to produce this dense, order-preserving sequence from a bare
614 /// counter — this is the structural proof of "SoA arena, not a
615 /// per-signal heap object model."
616 #[test]
617 fn alloc_inline_word_is_contiguous_no_per_signal_heap_object() {
618 let arena = Arena::new(BuildMode::FourState);
619 // SAFETY: `arena` is a local, single-owned value with no other live
620 // borrow and no concurrent caller; each call is sequential.
621 let offsets: Vec<u32> = (0..5)
622 .map(|_| unsafe { arena.alloc_inline_word() })
623 .collect();
624 assert_eq!(offsets, vec![0, 1, 2, 3, 4]);
625 }
626
627 /// ST-008: writing a `bits_to_four_state`-derived word to the inline
628 /// plane and reading it back through the fixed low64/high64 split must
629 /// match the same val/unk pair the value-layer test computed. Fails if
630 /// `alloc_inline_word`'s slot were narrower than 128 bits or misaligned.
631 #[cfg(feature = "frontend")] // uses the sv-lang `bits_to_four_state` converter
632 #[test]
633 fn inline_word_low64_high64_matches_bits_to_four_state() {
634 use super::super::{FourStateValue, bits_to_four_state};
635 use sv_lang::Bit;
636
637 let fv = bits_to_four_state(&[Bit::One, Bit::X, Bit::Zero, Bit::Z]);
638 let (expected_val, expected_unk) = match fv {
639 FourStateValue::Narrow(w) => (w as u64, (w >> 64) as u64),
640 FourStateValue::Wide { .. } => panic!("expected Narrow for 4 bits"),
641 };
642
643 let arena = Arena::new(BuildMode::FourState);
644 // SAFETY: `arena` is a local, single-owned value; no other borrow of
645 // the inline plane is live across these sequential calls.
646 let off = unsafe { arena.alloc_inline_word() };
647 unsafe {
648 arena.inline_mut()[off as usize] =
649 (expected_val as u128) | ((expected_unk as u128) << 64);
650 }
651
652 let word = arena.inline()[off as usize];
653 let (val, unk) = (word as u64, (word >> 64) as u64);
654 assert_eq!(val, expected_val);
655 assert_eq!(unk, expected_unk);
656 }
657
658 /// ST-009: both wide planes must use the exact same relative addressing
659 /// scheme — allocating for a second signal never overlaps the first
660 /// signal's region in either plane.
661 #[test]
662 fn wide_planes_share_identical_relative_offsets() {
663 let arena = Arena::new(BuildMode::FourState);
664 // SAFETY: `arena` is a local, single-owned value; no other borrow of
665 // either wide plane is live across these sequential calls.
666 let o1 = unsafe { arena.alloc_wide_words(2) };
667 unsafe {
668 arena.wide_aval_mut()[o1 as usize] = 0xAAAA_AAAA_AAAA_AAAA;
669 arena.wide_bval_mut()[o1 as usize] = 0xBBBB_BBBB_BBBB_BBBB;
670 arena.wide_aval_mut()[(o1 + 1) as usize] = 0xCCCC_CCCC_CCCC_CCCC;
671 arena.wide_bval_mut()[(o1 + 1) as usize] = 0xDDDD_DDDD_DDDD_DDDD;
672 }
673
674 let o2 = unsafe { arena.alloc_wide_words(2) };
675 assert_eq!(o2, o1 + 2);
676
677 // The second signal's allocation must not have touched the first
678 // signal's already-written region in either plane.
679 assert_eq!(arena.wide_aval()[o1 as usize], 0xAAAA_AAAA_AAAA_AAAA);
680 assert_eq!(arena.wide_bval()[o1 as usize], 0xBBBB_BBBB_BBBB_BBBB);
681 assert_eq!(arena.wide_aval()[(o1 + 1) as usize], 0xCCCC_CCCC_CCCC_CCCC);
682 assert_eq!(arena.wide_bval()[(o1 + 1) as usize], 0xDDDD_DDDD_DDDD_DDDD);
683
684 // The second signal's own region starts fresh (zeroed), proving no
685 // overlap the other direction either.
686 assert_eq!(arena.wide_aval()[o2 as usize], 0);
687 assert_eq!(arena.wide_bval()[o2 as usize], 0);
688 }
689
690 /// ST-038: two consecutive 1-bit-wire allocations must land in two
691 /// distinct words — a bit-packing implementation trying to co-locate two
692 /// 1-bit wires in one word would return the same offset for both,
693 /// failing this.
694 #[test]
695 fn no_two_signals_ever_share_a_64bit_word_even_two_1bit_wires() {
696 let arena = Arena::new(BuildMode::FourState);
697 // SAFETY: `arena` is a local, single-owned value; no other borrow is
698 // live across these sequential calls.
699 let a = unsafe { arena.alloc_inline_word() };
700 let b = unsafe { arena.alloc_inline_word() };
701 assert_eq!(b - a, 1);
702 assert_ne!(a, b);
703 }
704
705 /// ST-039: word-aligned, non-packed slots must allow multiple threads to
706 /// write disjoint words with no locking and no torn reads. Each thread
707 /// derives its own raw pointer from one `raw_inline_ptr()` call before
708 /// spawning (so the unsafe scope's precondition — one writer per offset,
709 /// no aliasing `&Arena`/`&mut Arena` use until join — holds), writes a
710 /// distinct pattern, and after `join` every word is read back through the
711 /// safe `Vec` API and must match exactly.
712 #[test]
713 fn concurrent_writes_to_disjoint_words_produce_no_torn_reads() {
714 const N: usize = 8;
715 let mut arena = Arena::new(BuildMode::FourState);
716 for _ in 0..N {
717 // SAFETY: sequential, single-owned, no other live borrow.
718 unsafe {
719 arena.alloc_inline_word();
720 }
721 }
722
723 let patterns: [u128; N] = [
724 0x1111_1111_1111_1111_1111_1111_1111_1111,
725 0x2222_2222_2222_2222_2222_2222_2222_2222,
726 0x3333_3333_3333_3333_3333_3333_3333_3333,
727 0x4444_4444_4444_4444_4444_4444_4444_4444,
728 0x5555_5555_5555_5555_5555_5555_5555_5555,
729 0x6666_6666_6666_6666_6666_6666_6666_6666,
730 0x7777_7777_7777_7777_7777_7777_7777_7777,
731 0x8888_8888_8888_8888_8888_8888_8888_8888,
732 ];
733
734 // SAFETY: 8 words were pre-allocated above via alloc_inline_word, so
735 // offsets 0..8 are all valid. Each spawned thread below receives
736 // exactly one derived pointer (ptr.add(i)) and writes only through
737 // it; std::thread::scope guarantees the pointers cannot outlive
738 // `arena`, and no other &Arena/&mut Arena use happens until after
739 // join, so the disjoint-offset, single-writer, non-aliasing
740 // precondition documented on raw_inline_ptr holds.
741 // Raw pointers aren't `Send` by default; each thread below owns
742 // exactly one disjoint derived pointer for the duration of the
743 // scope, matching `raw_inline_ptr`'s documented precondition, so
744 // wrapping it to cross the thread boundary is sound.
745 struct SendPtr(*mut u128);
746 // SAFETY: each `SendPtr` is used by exactly one thread to write
747 // through a word offset no other thread touches, for the scope's
748 // duration only (see the surrounding SAFETY comment).
749 unsafe impl Send for SendPtr {}
750
751 let base = unsafe { arena.raw_inline_ptr() };
752 std::thread::scope(|s| {
753 for (i, &pattern) in patterns.iter().enumerate() {
754 let p = SendPtr(unsafe { base.add(i) });
755 s.spawn(move || {
756 // Force capture of the whole `SendPtr` (not just its
757 // `.0` field via Rust 2021 disjoint-field capture),
758 // which is what actually makes the closure `Send`.
759 let p = p;
760 // SAFETY: `p` is this thread's own disjoint word offset,
761 // derived from `base` before any thread spawned; see the
762 // scope-level SAFETY comment above.
763 unsafe { ptr::write(p.0, pattern) };
764 });
765 }
766 });
767
768 for (i, &pattern) in patterns.iter().enumerate() {
769 assert_eq!(arena.inline()[i], pattern, "torn/wrong write at word {i}");
770 }
771 }
772
773 /// Task 7 (ST-067/ST-136): `alloc_inline_words(n)` must reserve `n`
774 /// contiguous words in one call and return the shared start offset —
775 /// the bulk primitive `StateEngine::allocate_instance` needs to give an
776 /// 8-instance generate-for loop 8 distinct, non-overlapping bases without
777 /// looping `alloc_inline_word()` itself. Also proves the thin-wrapper
778 /// relationship: `alloc_inline_word()` followed by `alloc_inline_words(3)`
779 /// must land immediately after the single word, with no gap.
780 #[test]
781 fn alloc_inline_words_reserves_n_contiguous_words_in_one_call() {
782 let arena = Arena::new(BuildMode::FourState);
783 // SAFETY: `arena` is a local, single-owned value; no other borrow is
784 // live across these sequential calls.
785 let single = unsafe { arena.alloc_inline_word() };
786 assert_eq!(single, 0);
787
788 let bulk_start = unsafe { arena.alloc_inline_words(3) };
789 assert_eq!(
790 bulk_start, 1,
791 "must start immediately after the single word, no gap"
792 );
793 assert_eq!(arena.inline().len(), 4, "1 single word + 3 bulk words");
794
795 let next = unsafe { arena.alloc_inline_word() };
796 assert_eq!(
797 next, 4,
798 "a later single-word alloc must not overlap the bulk region"
799 );
800 }
801
802 /// Task 7: a zero-word bulk request is a legal no-op that still returns a
803 /// well-defined offset (the current length), never panics and never
804 /// perturbs a later allocation's offset.
805 #[test]
806 fn alloc_inline_words_of_zero_is_a_no_op_returning_current_length() {
807 let arena = Arena::new(BuildMode::FourState);
808 // SAFETY: `arena` is a local, single-owned value; no other borrow is
809 // live across these sequential calls.
810 unsafe {
811 arena.alloc_inline_word();
812 }
813 let off = unsafe { arena.alloc_inline_words(0) };
814 assert_eq!(off, 1);
815 assert_eq!(
816 arena.inline().len(),
817 1,
818 "zero-word request must not grow the arena"
819 );
820
821 let next = unsafe { arena.alloc_inline_word() };
822 assert_eq!(
823 next, 1,
824 "the zero-word request must not have consumed an offset"
825 );
826 }
827
828 /// ST-145: the FFI view's wide planes must be null exactly in 2-state
829 /// mode, non-null in 4-state mode (when at least one wide word exists) —
830 /// fails if the FFI view always returns the (possibly-empty-but-non-null)
831 /// `Vec`'s pointer regardless of mode.
832 #[test]
833 fn ffi_view_wide_planes_null_only_in_two_state_mode() {
834 let mut two_state = Arena::new(BuildMode::TwoState);
835 // SAFETY: `two_state` is a local, single-owned value; no other
836 // borrow is live across this call.
837 unsafe {
838 two_state.alloc_wide_words(1);
839 }
840 let view = two_state.ffi_view();
841 assert!(view.wide_aval.is_null());
842 assert!(view.wide_bval.is_null());
843
844 let mut four_state = Arena::new(BuildMode::FourState);
845 // SAFETY: same as above.
846 unsafe {
847 four_state.alloc_wide_words(1);
848 }
849 let view = four_state.ffi_view();
850 assert!(!view.wide_aval.is_null());
851 assert!(!view.wide_bval.is_null());
852 }
853
854 /// Review fix (Task 9 r3, blocking finding 1): before `seal_wide_plane`
855 /// is called, `alloc_wide_words` must keep growing normally (the seal
856 /// defaults to unset) -- proves the fix doesn't regress the ordinary,
857 /// unsealed case any of this module's other tests already rely on.
858 #[test]
859 fn alloc_wide_words_is_unaffected_before_the_plane_is_sealed() {
860 let arena = Arena::new(BuildMode::FourState);
861 assert!(!arena.is_wide_plane_sealed());
862 // SAFETY: `arena` is a local, single-owned value; no other live
863 // borrow of either wide plane across these sequential calls.
864 let a = unsafe { arena.alloc_wide_words(3) };
865 let b = unsafe { arena.alloc_wide_words(5) };
866 assert_eq!(a, 0);
867 assert_eq!(b, 3);
868 }
869
870 /// Review fix (Task 9 r3, blocking finding 1): once `seal_wide_plane` has
871 /// fired, a nonzero-word `alloc_wide_words` call must panic rather than
872 /// silently reallocating the backing `Vec<u64>`s -- this is the Arena
873 /// -level unit proof of the enforcement `sv_get_wide` now relies on
874 /// (`accessor_impl.rs`'s own regression test proves the same thing at the
875 /// accessor-ABI level, through a real `WideView`).
876 #[test]
877 #[should_panic(expected = "refusing to grow the wide plane")]
878 fn alloc_wide_words_panics_on_nonzero_growth_once_sealed() {
879 let arena = Arena::new(BuildMode::FourState);
880 // SAFETY: `arena` is a local, single-owned value; no other live
881 // borrow of either wide plane across this call.
882 unsafe {
883 arena.alloc_wide_words(2);
884 }
885 arena.seal_wide_plane();
886 assert!(arena.is_wide_plane_sealed());
887 // SAFETY: this call is expected to panic before touching either
888 // `Vec`; no live borrow of either wide plane is held here regardless.
889 unsafe {
890 arena.alloc_wide_words(1);
891 }
892 }
893
894 /// Review fix (Task 9 r3): a `words == 0` request must stay a legal
895 /// no-op even once sealed -- it never touches either `Vec`'s capacity
896 /// (mirrors `alloc_inline_words_of_zero_is_a_no_op_returning_current_length`),
897 /// so it can never be the source of a reallocation and must not be
898 /// refused just because some `WideView` is live elsewhere.
899 #[test]
900 fn alloc_wide_words_of_zero_is_still_a_no_op_once_sealed() {
901 let arena = Arena::new(BuildMode::FourState);
902 // SAFETY: `arena` is a local, single-owned value; no other live
903 // borrow of either wide plane across these sequential calls.
904 let off = unsafe { arena.alloc_wide_words(4) };
905 arena.seal_wide_plane();
906 let after_seal = unsafe { arena.alloc_wide_words(0) };
907 assert_eq!(
908 after_seal,
909 off + 4,
910 "zero-word request must still return the current length"
911 );
912 assert_eq!(
913 arena.wide_aval().len(),
914 4,
915 "zero-word request must not grow the arena even when sealed"
916 );
917 }
918}