Skip to main content

sv_sim_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 sv-sim-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}