Skip to main content

vj_sim/state/
instance.rs

1//! Task 7 — Concrete-hierarchy base assignment & the compile-time layout
2//! query API (ledger ST-037, ST-067, ST-129, ST-135, ST-136).
3//!
4//! The second layout pass: given a template's already-built
5//! [`super::StateLayout`] (Task 4/6), assign each *concrete instance* of that
6//! template a distinct, non-overlapping base index into the global inline/
7//! wide arrays by bump-allocating `template_size(template)` words per
8//! instance (ST-067). `slot_of`/`template_size` stay template-relative only
9//! (ST-129/ST-135) — no method here ever adds an instance base to a
10//! [`super::SlotOffset`]; that addition is ENGINE's job at kernel-invocation
11//! time, not STATE's.
12//!
13//! **Deviation from the plan's literal `templates: Box<TemplateTable>`
14//! field type:** the plan's prose ("looked up from `templates.layout_of`")
15//! names `dedup::TemplateTable` directly, but that type is `TemplateTable<'d>`
16//! — it borrows `Symbol<'d>` representatives tied to the `sv_lang::Design`
17//! it was built from, and also carries `instance_order`/`definitions`
18//! bookkeeping that only matters during the dedup walk itself. `StateEngine`
19//! must outlive the `Design` used to build it (its `InstanceHandle`s cross
20//! the compiled-kernel FFI boundary, per this task's own header note, and
21//! later tasks construct/move/thread a bare, non-generic `StateEngine` with
22//! no `Design` in scope at all — see e.g. Task 9's "a test that imports only
23//! `state::{StateEngine, StateCellId, ...}`"), so it cannot itself carry the
24//! `'d` lifetime. [`TemplateRegistry`] below is the minimal owned substitute:
25//! it holds only the `(TemplateKey, StateLayout)` pairs `slot_of`/
26//! `template_size`/`allocate_instance` actually need (moved out of a built
27//! `TemplateTable`, which — since `StateLayout` itself owns everything it
28//! contains, see `SlotDescriptor`'s doc comment — never borrows from the
29//! `Design`), with the same `layout_of` query method name/shape the plan's
30//! prose describes. `StateEngine`'s own `templates` field keeps the plan's
31//! `pub`, `Box`-wrapped shape.
32//!
33//! **Deviation:** the plan does not specify a `StateEngine` constructor for
34//! this task (every other task's prose assumes one already exists). Added
35//! [`StateEngine::new`], defaulting to `BuildMode::FourState` (`FourState`
36//! matches every other task's default arena in this plan).
37//!
38//! **Task 13 deviation:** the plan's Files list for Task 13 (2-state
39//! build-mode arena and read/write family) names only `state/write.rs` and
40//! `state/arena.rs`, but neither of those already had a `StateEngine`
41//! constructor that selects a [`BuildMode`] other than the hardcoded
42//! `FourState` [`StateEngine::new`] above -- and every one of this plan's 14
43//! existing `StateEngine::new(templates)` call sites depends on that
44//! single-argument signature staying unchanged. [`StateEngine::new_with_mode`]
45//! is added here (additive only, `new` now forwards to it with
46//! `BuildMode::FourState`) as the minimal way to construct a
47//! `BuildMode::TwoState` engine Task 13's own tests need, without touching
48//! any existing call site.
49
50use std::cell::UnsafeCell;
51use std::collections::{HashMap, HashSet};
52use std::rc::Rc;
53use std::sync::atomic::{AtomicU64, Ordering};
54
55use super::arena::{Arena, BuildMode};
56use super::driver::TriregCharge;
57use super::region_check::ForceObserved;
58use super::write::{SlotLocation, VcState, WorkerSlot};
59use super::{SlotClass, StateCellId, StateLayout};
60use crate::netlist::{ClassTemplateId, InstanceId, InstantiatedSignalId, SymbolKey, TemplateKey};
61
62/// Process-wide, monotonically-increasing source for each `StateEngine`'s own
63/// [`VcState`]'s `engine_id`
64/// (Task 8 deviation 2, see `state::write`'s module doc comment): every
65/// `StateEngine` gets a distinct id, never reused, so the thread-local
66/// self-registration cache in `state::write` can tell two different engines
67/// apart even when they happen to share an OS thread (as this crate's own
68/// test suite routinely does).
69static NEXT_ENGINE_ID: AtomicU64 = AtomicU64::new(0);
70
71/// One concrete instance's base index into the two global arrays, relative
72/// to which every one of its template's template-relative [`super::SlotOffset`]
73/// values must be added (ST-129) to get a real address — that addition is
74/// ENGINE's job, never performed here.
75#[derive(Clone, Copy, PartialEq, Eq, Debug)]
76pub struct InstanceBase {
77    pub inline_base: u32,
78    pub wide_base: u32,
79}
80
81/// The concrete type this plan's header design decision makes `SimHandle`
82/// point at (Task 8 adds the `SimHandle` conversion). `layout`/`arena` are
83/// private raw pointers; they stay valid for the life of this *handle*
84/// (never merely for the life of the `StateEngine` that minted them — see
85/// below) for two *independent* reasons (review fix, Task 7 r1 — the
86/// original comment here conflated them and was wrong about `arena`):
87///
88/// - `arena` points at the [`Arena`] *struct itself*, inside the heap
89///   allocation `arena_owner` below (an owned `Rc<Arena>` clone) keeps alive.
90///   That alone is not sufficient for soundness, though: [`Arena`]'s own
91///   backing storage is `UnsafeCell`-wrapped and every mutating accessor
92///   takes `&self`, specifically so that no code in this module ever needs
93///   to form a fresh `&mut Arena` after this pointer is minted — under
94///   Rust's Stacked/Tree Borrows aliasing model, forming a `&mut Arena` at
95///   all (regardless of whether it is used to write) invalidates every
96///   previously-derived raw pointer's borrow-stack tag on `Arena`'s own
97///   field memory. See `state::arena`'s module doc comment for the full
98///   argument and the `cargo +nightly miri` failure this fixes.
99/// - `layout` points into the heap allocation a `Box<StateLayout>` owns
100///   inside the `TemplateRegistry` heap allocation `templates_owner` below
101///   (an owned `Rc<TemplateRegistry>` clone) keeps alive, at the specific
102///   `Box<StateLayout>` slot for this handle's `template` — see
103///   [`TemplateRegistry`]'s doc comment — so it stays valid even if that
104///   registry's backing `Vec` reallocates from a later `register` call.
105///
106/// **Review fix (Task 7 r2):** `arena_owner`/`templates_owner` are this
107/// handle's *own* `Rc` clones, taken at the moment [`StateEngine::
108/// allocate_instance`] mints the handle — not merely raw pointers derived
109/// from the minting `StateEngine`'s `Rc<Arena>`/`Rc<TemplateRegistry>` with
110/// no owned stake in them. `allocate_instance`'s signature
111/// (`&mut self -> Option<Box<InstanceHandle>>`) returns a fully owned value
112/// with no borrow-checked connection back to `self` — a helper function that
113/// builds a `StateEngine`, calls `allocate_instance`, and returns just the
114/// handle is completely ordinary code, and previously, once that
115/// `StateEngine` (and therefore its `Rc`s) dropped, `arena`/`layout` above
116/// would dangle: `cargo +nightly miri test -p vj-sim instance::` reported
117/// "Undefined Behavior: constructing invalid value of type
118/// &state::arena::Arena: encountered a dangling reference (use-after-free)"
119/// at exactly that read site before this fix. Cloning an `Rc` bumps its
120/// strong count, so as long as *any* `InstanceHandle` referencing a given
121/// `Arena`/`TemplateRegistry` is alive, that allocation's refcount cannot
122/// reach zero — independent of whether the `StateEngine` that minted the
123/// handle is itself still alive. See
124/// `dropping_the_state_engine_after_allocating_an_instance_leaves_the_handle_valid`
125/// below for the Miri-checked regression test.
126///
127/// **Task 10 (ST-027):** `base`/`template` stay `pub` (CODEGEN needs `base` to
128/// mint a `SimHandle` at instantiation time, per this plan's header design
129/// decision), but this type never hands a caller a raw pointer it could add
130/// `base`/a `SlotOffset` to itself — `layout`/`arena` are private, and the only
131/// way to actually reach a signal's storage through this handle is the
132/// accessor fns (`accessor_impl::sv_get_scalar`/`sv_set_scalar`/`_wide`) or
133/// `arena()`/`layout()`'s own `pub(crate)`-scoped, already-safe accessors,
134/// never a `pub fn` returning `*mut`/`*const` for external base+offset
135/// arithmetic. See [`super::SlotOffset`]'s own doc comment for the
136/// superseded-design context this resolves.
137pub struct InstanceHandle {
138    pub base: InstanceBase,
139    pub template: TemplateKey,
140    layout: *const StateLayout,
141    arena: *const Arena,
142    // Review fix (Task 7 r2): owned `Rc` clones whose sole job is to keep
143    // the `Arena`/`TemplateRegistry` heap allocations `arena`/`layout` above
144    // point into alive for exactly as long as this handle exists — see this
145    // struct's doc comment. Never read directly (their `Drop` impl running
146    // when this handle drops is the entire point of holding them), hence
147    // `#[allow(dead_code)]`.
148    #[allow(dead_code)]
149    arena_owner: Rc<Arena>,
150    #[allow(dead_code)]
151    templates_owner: Rc<TemplateRegistry>,
152    /// Task 9 retrofit (ST-013, ST-020..ST-023 — see `state::write`'s module
153    /// doc comment): this handle's own clone of the `StateEngine` that
154    /// minted it own `Rc<VcState>`, letting the accessor ABI
155    /// (`accessor_impl.rs`) call `write::commit_write_at` — the exact same
156    /// primitive `StateEngine::commit_write` calls — with no `&mut
157    /// StateEngine` back-reference, the same way `arena_owner` lets it reach
158    /// the arena directly.
159    vc_owner: Rc<VcState>,
160    /// Task 9 retrofit: one `StateCellId` per slot in this instance's
161    /// template, in the same order `SlotId` indexes `StateLayout::slots`
162    /// (`vj_abi::SlotId`'s own doc comment: "index into its template's
163    /// `StateLayout.slots`") — minted once, here, at `allocate_instance`
164    /// time via Task 8's `StateEngine::alloc_cell`, each already carrying
165    /// this instance's absolute (base-applied) `SlotLocation`. This
166    /// realizes Task 9's own prose ("Task 8 mints `StateCellId`s 1:1 with
167    /// concrete instantiated signals during elaboration via `alloc_cell`")
168    /// — `allocate_instance` is this wave's stand-in for "elaboration"
169    /// (there is no separate elaboration pass yet).
170    ///
171    /// Past the own slots, one entry per foreign slot (see `foreign`): the
172    /// named signal's own cell, so a cell lookup never needs to resolve.
173    cells: std::cell::UnsafeCell<Vec<StateCellId>>,
174    /// Foreign slots: slot `own + k` (`own` = the layout's slot count) names slot `foreign[k].1` of
175    /// the instance `foreign[k].0` (a signal of another instance this one's
176    /// code reaches -- an interface member through an interface port, a
177    /// hierarchical reference, an interface task's body, a package
178    /// variable). Set once every instance exists
179    /// ([`crate::state::accessor_impl::set_foreign_slots`]).
180    foreign: std::cell::RefCell<Vec<(*const InstanceHandle, usize)>>,
181    /// Per foreign slot, the window of the named slot it stands for when it
182    /// aliases a select of it (a ref port bound to `s.m` / `a[2]`,
183    /// `state::alias`); empty when no foreign slot is windowed. A windowed
184    /// slot's `cells` entry is its view cell, not the target's own.
185    windows: std::cell::RefCell<Vec<Option<super::AliasWindow>>>,
186    /// Backing for the `WideView`s of windowed-alias reads, which (unlike a
187    /// whole slot's) have no arena storage to point at: a ring of
188    /// `(aval, bval)` planes plus its cursor, so a view stays valid across
189    /// the next few reads -- more than any one operation holds at once.
190    wide_scratch: std::cell::RefCell<ScratchRing>,
191    /// [`Self::inline_ptr_table`]'s cache: the inline plane's `(base
192    /// pointer, length)` it was built against, then one entry per slot.
193    inline_ptrs: std::cell::UnsafeCell<InlinePtrCache>,
194}
195
196/// See [`InstanceHandle::inline_ptr_table`].
197type InlinePtrCache = ((usize, usize), Vec<*const u128>);
198
199/// The scratch ring's cursor and the `(aval, bval)` planes held so far.
200type ScratchRing = (usize, Vec<(Vec<u64>, Vec<u64>)>);
201
202/// How many windowed wide reads stay valid at once (see
203/// [`InstanceHandle::scratch_wide`]).
204const WIDE_SCRATCH_SLOTS: usize = 32;
205
206impl InstanceHandle {
207    /// Parks one windowed wide read's planes in the scratch ring and returns
208    /// pointers to them; valid until `WIDE_SCRATCH_SLOTS` further such reads.
209    pub(crate) fn scratch_wide(&self, aval: Vec<u64>, bval: Vec<u64>) -> (*mut u64, *mut u64, u32) {
210        let (next, ring) = &mut *self.wide_scratch.borrow_mut();
211        let at = *next % WIDE_SCRATCH_SLOTS;
212        *next += 1;
213        if ring.len() < WIDE_SCRATCH_SLOTS {
214            ring.push((aval, bval));
215        } else {
216            ring[at] = (aval, bval);
217        }
218        let (a, b) = &ring[at.min(ring.len() - 1)];
219        (a.as_ptr().cast_mut(), b.as_ptr().cast_mut(), a.len() as u32)
220    }
221
222    /// SAFETY: `arena` points into the heap allocation this handle's own
223    /// `arena_owner: Rc<Arena>` clone keeps alive for exactly as long as
224    /// `self` does (see this struct's doc comment), and no `&mut Arena` is
225    /// ever formed anywhere in this crate (Task 7 r1), so this shared
226    /// reference cannot be invalidated by aliasing.
227    pub(crate) fn arena(&self) -> &Arena {
228        unsafe { &*self.arena }
229    }
230
231    /// SAFETY: `layout` points into the `Box<StateLayout>` this handle's
232    /// `template` owns inside the heap allocation this handle's own
233    /// `templates_owner: Rc<TemplateRegistry>` clone keeps alive;
234    /// `TemplateRegistry::register` never moves an already-boxed
235    /// `StateLayout` (see that struct's doc comment), so this pointer is
236    /// stable for the handle's lifetime.
237    pub(crate) fn layout(&self) -> &StateLayout {
238        unsafe { &*self.layout }
239    }
240
241    /// The shared VC-tracking state this handle's own `vc_owner: Rc<VcState>`
242    /// clone keeps alive (Task 9 retrofit — see this struct's doc comment).
243    pub(crate) fn vc(&self) -> &VcState {
244        &self.vc_owner
245    }
246
247    /// The `StateCellId` minted for this instance's `idx`-th slot (Task 9
248    /// retrofit — see `cells`' own doc comment above).
249    pub(crate) fn cell_for_slot(&self, idx: usize) -> StateCellId {
250        // SAFETY: `cells` is only written by `set_foreign`, before any
251        // kernel runs; no reference into it outlives this call.
252        let cells = unsafe { &*self.cells.get() };
253        cells[idx]
254    }
255
256    /// The instance whose own slot `idx` is, and its index there: `self`
257    /// for one of its own slots, the target of a foreign slot otherwise
258    /// (see `foreign`). The bare accessors (`sv_get_scalar`, ...) address
259    /// own slots only -- they stay small enough for AOT+LTO to inline --
260    /// and the production entry points resolve first
261    /// ([`crate::state::accessor_impl::resolve_foreign_slot`]).
262    #[inline]
263    pub(crate) fn resolve_slot(&self, idx: usize) -> (&InstanceHandle, usize) {
264        if idx < self.own_slot_count() {
265            (self, idx)
266        } else {
267            self.resolve_foreign_slot(idx)
268        }
269    }
270
271    /// [`Self::resolve_slot`] for a foreign slot -- kept out of line so the
272    /// accessors' own-slot path stays small enough to inline.
273    #[cold]
274    #[inline(never)]
275    fn resolve_foreign_slot(&self, idx: usize) -> (&InstanceHandle, usize) {
276        let own = self.own_slot_count();
277        let (owner, slot) = self
278            .foreign
279            .borrow()
280            .get(idx - own)
281            .copied()
282            .unwrap_or_else(|| panic!("slot {idx} is neither own nor a registered foreign slot"));
283        // SAFETY: `owner` came from a `SimHandle` minted by `into_sim_handle`
284        // (`set_foreign_slots`), which lives for the whole run.
285        (unsafe { &*owner }, slot)
286    }
287
288    /// Registers this instance's foreign slots (see `foreign`); each target
289    /// must be one of its instance's own slots.
290    pub(crate) fn set_foreign(
291        &self,
292        targets: Vec<(*const InstanceHandle, usize)>,
293        views: Vec<Option<(super::AliasWindow, StateCellId)>>,
294    ) {
295        debug_assert_eq!(targets.len(), views.len());
296        // SAFETY: called before any kernel runs, with no reference into
297        // `cells` alive (`cell_for_slot` holds none past its own call).
298        let cells = unsafe { &mut *self.cells.get() };
299        cells.truncate(self.own_slot_count());
300        for (&(owner, slot), view) in targets.iter().zip(&views) {
301            match view {
302                Some((_, cell)) => cells.push(*cell),
303                // SAFETY: see `resolve_foreign_slot`.
304                None => cells.push(unsafe { &*owner }.cell_for_slot(slot)),
305            }
306        }
307        *self.windows.borrow_mut() = if views.iter().any(Option::is_some) {
308            views.iter().map(|v| v.map(|(w, _)| w)).collect()
309        } else {
310            Vec::new()
311        };
312        *self.foreign.borrow_mut() = targets;
313        // SAFETY: see `inline_ptr_table`.
314        unsafe { &mut *self.inline_ptrs.get() }.1.clear();
315    }
316
317    /// The window foreign slot `idx` (`>= own_slot_count`) stands for within
318    /// the slot [`Self::resolve_slot`] names, `None` for a whole-slot alias.
319    #[cold]
320    #[inline(never)]
321    pub(crate) fn foreign_window(&self, idx: usize) -> Option<super::AliasWindow> {
322        let own = self.own_slot_count();
323        self.windows.borrow().get(idx - own).copied().flatten()
324    }
325
326    /// One entry per slot number (own, then foreign): the address of the
327    /// arena `u128` cell `sv_get_scalar` reads for that slot, or null when
328    /// the slot has no such cell (not `Inline`, or a windowed alias, whose
329    /// read goes through [`super::accessor_impl::real_sv_get_scalar`]'s slow
330    /// path). JIT code loads an `Inline` signal straight through it instead
331    /// of calling the accessor (`sv_rt_inline_ptrs`).
332    ///
333    /// The table is rebuilt whenever the inline plane has moved or grown
334    /// since it was built (instances are allocated before any kernel runs,
335    /// but a stale pointer would be a use-after-free, so it is checked on
336    /// every request) and whenever [`Self::set_foreign`] re-points a slot.
337    /// The caller may use the returned pointer until the next arena
338    /// allocation or `set_foreign`, i.e. for the rest of one kernel call.
339    pub(crate) fn inline_ptr_table(&self) -> *const *const u128 {
340        let inline = self.arena().inline();
341        let stamp = (inline.as_ptr() as usize, inline.len());
342        // SAFETY: single logical thread; no reference into the cache is
343        // held across this call (the returned raw pointer is not a borrow).
344        let cache = unsafe { &mut *self.inline_ptrs.get() };
345        if cache.0 != stamp || cache.1.is_empty() {
346            let own = self.own_slot_count();
347            let windows = self.windows.borrow();
348            let total = own + self.foreign.borrow().len();
349            cache.1.clear();
350            cache.1.extend((0..total).map(|idx| {
351                if idx >= own && windows.get(idx - own).is_some_and(Option::is_some) {
352                    return std::ptr::null();
353                }
354                let (owner, oidx) = self.resolve_slot(idx);
355                let offset = owner.layout().slots[oidx].offset;
356                if offset.class != SlotClass::Inline {
357                    return std::ptr::null();
358                }
359                let word = owner.base.inline_base + offset.word_offset;
360                owner
361                    .arena()
362                    .inline()
363                    .get(word as usize)
364                    .map_or(std::ptr::null(), std::ptr::from_ref)
365            }));
366            cache.0 = stamp;
367        }
368        cache.1.as_ptr()
369    }
370
371    /// The number of this instance's own slots.
372    pub(crate) fn own_slot_count(&self) -> usize {
373        self.layout().slots.len()
374    }
375}
376
377/// Task 7's owned, `'static` substitute for `dedup::TemplateTable<'d>` (see
378/// this module's top doc comment) — the minimal `(TemplateKey, StateLayout)`
379/// registry [`StateEngine`] actually needs after elaboration, once the
380/// `sv_lang::Design`-borrowing dedup walk itself is done.
381///
382/// **Review fix (Task 7 r1): each `StateLayout` is individually boxed, and
383/// `templates` is `UnsafeCell`-wrapped so `register` never needs `&mut
384/// self`.** `allocate_instance` hands out a `*const StateLayout` pointing
385/// directly into this registry's backing storage (see
386/// [`InstanceHandle::layout`]). Two independent hazards, both now fixed:
387/// - With `templates: Vec<(TemplateKey, StateLayout)>` (the pre-fix shape),
388///   that pointer targeted the `Vec`'s own tuple element storage — a later
389///   `register` call growing this `pub` (via `StateEngine.templates`), and
390///   therefore externally reachable, `Vec` could reallocate it and silently
391///   dangle every previously-minted `layout` pointer, exactly the hazard
392///   `register`'s old doc comment warned about without preventing it.
393///   `Vec<(TemplateKey, Box<StateLayout>)>` instead gives each `StateLayout`
394///   its own, independent heap allocation: growing the outer `Vec` moves the
395///   `Box` pointers around, never the `StateLayout` values those pointers
396///   point at.
397/// - Separately (see [`StateEngine`]'s doc comment for the full argument):
398///   under Rust's Stacked/Tree Borrows aliasing model, a `Box<T>` value
399///   being moved — including as a field of a larger struct moved by value,
400///   e.g. a `StateEngine` returned from a constructor — recursively
401///   retags (as freshly, exclusively owned) everything reachable through
402///   nested `Box`/`&`/`&mut` fields, which would silently invalidate a
403///   `layout` pointer minted before the move even though `Box<StateLayout>`
404///   itself never reallocates. `templates` therefore uses `UnsafeCell`
405///   (mutated only through `&self`, never `&mut self`, exactly like
406///   [`Arena`]) so that `StateEngine.templates` itself can be an `Rc`, not a
407///   `Box` — `Rc`'s move doesn't carry that same recursive-retag semantics
408///   (it isn't a uniquely-owning reference-like type), so Miri's retag walk
409///   never descends into (and therefore never invalidates) the boxed
410///   `StateLayout`s nested inside.
411#[derive(Default)]
412pub struct TemplateRegistry {
413    templates: UnsafeCell<Vec<(TemplateKey, Box<StateLayout>)>>,
414}
415
416impl std::fmt::Debug for TemplateRegistry {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        // SAFETY: reads only `.len()` through a shared reference for
419        // diagnostic purposes; per this type's single-logical-writer
420        // invariant (see the struct doc comment and `register`), a length
421        // read that doesn't outlive this call and races no concurrent
422        // `register` call is sound.
423        let len = unsafe { (*self.templates.get()).len() };
424        f.debug_struct("TemplateRegistry")
425            .field("template_count", &len)
426            .finish()
427    }
428}
429
430impl TemplateRegistry {
431    pub fn new() -> Self {
432        TemplateRegistry {
433            templates: UnsafeCell::new(Vec::new()),
434        }
435    }
436
437    /// Registers one template's layout. Callable at any time, including
438    /// after `StateEngine::allocate_instance` has already minted one or more
439    /// `InstanceHandle`s against this registry (see this struct's doc
440    /// comment for why growing `templates` no longer moves any
441    /// already-registered `StateLayout`, and never needs to invalidate a
442    /// previously-minted `layout` pointer via a `StateEngine` move either).
443    ///
444    /// # Safety
445    /// The caller must ensure no other live `&`/`&mut` borrow into this
446    /// registry's backing `Vec` (in particular, no live `&StateLayout`
447    /// returned by [`Self::layout_of`]) is held across this call, and that
448    /// at most one logical writer registers templates at a time — the same
449    /// single-logical-writer discipline `Arena`'s mutating accessors
450    /// document. `&self`, not `&mut self`, so this can be called through a
451    /// `Rc<TemplateRegistry>` (see [`StateEngine`]'s doc comment for why
452    /// `templates` is an `Rc`, not a `Box`).
453    pub unsafe fn register(&self, key: TemplateKey, layout: StateLayout) {
454        // SAFETY: forwarded to this function's own `# Safety` section above.
455        unsafe {
456            (*self.templates.get()).push((key, Box::new(layout)));
457        }
458    }
459
460    /// Template-relative layout lookup (ST-066-style: never a per-instance
461    /// storage offset). Safe: any number of live `&StateLayout` reads
462    /// coexist without aliasing; the aliasing obligation is on the `unsafe`
463    /// [`Self::register`] instead (see its doc comment).
464    pub fn layout_of(&self, key: &TemplateKey) -> Option<&StateLayout> {
465        // SAFETY: produces a shared reference from the `UnsafeCell`. Multiple
466        // concurrent reads never conflict with each other; the caller of
467        // `register` is the one obligated not to hold a read like this one
468        // live across their call.
469        unsafe { &*self.templates.get() }
470            .iter()
471            .find(|(k, _)| k == key)
472            .map(|(_, layout)| layout.as_ref())
473    }
474
475    /// Task 16 (ST-098): a hash of the ordered template-signature list this
476    /// registry holds, in registration order — `rollback::ArenaSnapshot`'s
477    /// `layout_hash` guard (`restore` refuses to poke a `StateEngine` whose
478    /// registered template shapes don't match the snapshot's). Not in Task
479    /// 16's own Files list (`state/rollback.rs` (new), `state/mod.rs`) — a
480    /// method on the type that actually owns the template list is required,
481    /// the same "add it directly to the type/struct that owns the resource"
482    /// precedent Task 14/15's own `StateEngine` field additions already
483    /// establish for this crate.
484    ///
485    /// Deliberately hashes only the *shape* every slot's `SlotOffset`
486    /// describes (class/word_offset/bit_width/elem_word_count/
487    /// row_stride_words/num_rows) plus each `TemplateKey` and each
488    /// `StateLayout`'s own word counts — never `SlotDescriptor.initial`
489    /// (`sv_lang::OwnedSVInt` has no stable hash of its own to reuse, and an
490    /// initializer's concrete value never affects arena addressing, which is
491    /// the one thing this guard exists to protect) and never
492    /// `SlotDescriptor.net_info` (net classification also never affects
493    /// where a slot's storage lives). Two registries with the same
494    /// registered shapes in the same order hash identically regardless of
495    /// initializer values; two registries whose shapes genuinely differ
496    /// hash differently with overwhelming probability — exactly the
497    /// distinction `restore`'s layout-mismatch guard needs.
498    pub(crate) fn signature_hash(&self) -> u64 {
499        use std::hash::Hasher;
500        // SAFETY: reads the backing Vec's (key, layout) pairs through a
501        // shared reference for a pure, non-outliving hash computation that
502        // never itself grows or moves anything; per this type's
503        // single-logical-writer invariant (see the struct doc comment and
504        // `register`'s own `# Safety` section), a read like this one that
505        // doesn't outlive this call and races no concurrent `register` call
506        // is sound — identical reasoning to `layout_of` above.
507        let templates: &Vec<(TemplateKey, Box<StateLayout>)> = unsafe { &*self.templates.get() };
508        let mut hasher = std::collections::hash_map::DefaultHasher::new();
509        for (key, layout) in templates.iter() {
510            hasher.write_u32(key.definition);
511            hasher.write_u64(key.params_hash);
512            hasher.write_u32(layout.inline_words);
513            hasher.write_u32(layout.wide_words);
514            hasher.write_usize(layout.slots.len());
515            for slot in &layout.slots {
516                hasher.write_u32(slot.key.0);
517                hasher.write_u8(match slot.offset.class {
518                    SlotClass::Inline => 0,
519                    SlotClass::Wide => 1,
520                    SlotClass::Memory => 2,
521                    SlotClass::ForceWide => 3,
522                });
523                hasher.write_u32(slot.offset.word_offset);
524                hasher.write_u32(slot.offset.bit_width);
525                hasher.write_u32(slot.offset.elem_word_count);
526                hasher.write_u32(slot.offset.row_stride_words);
527                hasher.write_u32(slot.offset.num_rows);
528                hasher.write_u8(slot.net_info.is_some() as u8);
529            }
530        }
531        hasher.finish()
532    }
533}
534
535/// STATE's top-level owned state (arena storage + the compiled template set)
536/// — **review fix: `arena`/`templates` are `Rc`-wrapped, not `Box`-wrapped or
537/// embedded by value.** A `SimHandle`/`InstanceHandle` minted by
538/// [`Self::allocate_instance`] must stay valid for the life of the
539/// simulation run and cross the compiled-kernel FFI boundary, so the raw
540/// pointers it stores must survive `StateEngine` itself being moved —
541/// returned by value from a constructor, pushed into a `Vec`, or embedded in
542/// a future root object are all ordinary things a caller does (this task's
543/// own regression test exercises exactly this). With `arena`/`templates`
544/// embedded directly, any such move would silently dangle every
545/// previously-minted handle — no compiler diagnostic, UB on the next
546/// accessor call.
547///
548/// A first review fix boxed them (`Box<Arena>`/`Box<TemplateRegistry>`),
549/// reasoning that a heap allocation's address doesn't change when the
550/// (movable) `Box` pointer value travels with a moved `StateEngine`. That
551/// address argument is correct but insufficient: `cargo +nightly miri test
552/// -p vj-sim instance::` still fails with `Box<Arena>`, because moving a
553/// `Box<T>` value — including as a field of a larger struct passed by value
554/// into a function, e.g. `Box::new(engine)` or returning `engine` from a
555/// constructor — retags the `Box`'s pointee as freshly, exclusively owned
556/// under Rust's Stacked/Tree Borrows model (the same aliasing treatment
557/// `&mut T` gets), independent of the pointee's own address ever changing.
558/// That invalidates every previously-derived raw pointer into it, `Arena`'s
559/// own `UnsafeCell`-based interior mutability notwithstanding. `Rc<T>` does
560/// not carry this exclusive-ownership retag-on-move semantics — it's a
561/// plain (refcounted) shared-ownership pointer, not a reference-like type
562/// Miri's retagging pass treats specially — so `Rc<Arena>`/
563/// `Rc<TemplateRegistry>` keep the same "stable heap address, movable
564/// container" property `Box` gave without the move-retag hazard. This is
565/// consistent with (and depends on) `Arena`'s and `TemplateRegistry`'s own
566/// mutating methods already being `&self`-based via `UnsafeCell`: `Rc`
567/// cannot produce `&mut` access at all (short of `Rc::get_mut`, unused
568/// here), so nothing about this design ever needed `&mut Arena`/
569/// `&mut TemplateRegistry` in the first place. `self.arena`/`self.templates`
570/// field access auto-derefs through `Rc` exactly as it would through a bare
571/// field or a `Box`.
572///
573/// **Task 10 (ST-152):** every resource STATE owns — `arena`, `templates`,
574/// `cells` (Task 8), `vc` (the Task 9 retrofit's VC-tracking state; the VC
575/// dispatch table and undo log join this list once Tasks 11/16 land) — is a
576/// *direct* field of this struct, never an `Option<*mut OtherGlobal>`
577/// indirection and never a free-standing process-wide global or
578/// lazily-initialized singleton cell of any kind anywhere in this crate (this
579/// module's own test suite enforces the exact forbidden spellings by scanning
580/// this crate's `state` source). The only pointer any accessor/ABI function
581/// accepts to reach any of it is `SimHandle`/`InstanceHandle` (itself
582/// reachable only via `SimHandle`, per this plan's header decision) or, for
583/// scheduler-side callers, a `&`/`&mut StateEngine` reached however the caller
584/// already holds one — never a second, independent "give me the arena"
585/// pointer parameter threaded alongside it.
586///
587/// **Task 15 (ST-033):** every [`super::FourStateValue`] in the running
588/// simulation lives in exactly one of `Arena.{inline,wide_aval,wide_bval}`
589/// (or the `_2s` twins), reached only via a [`StateCellId`]/[`SlotLocation`]
590/// STATE itself minted (Task 8's `alloc_cell`) — no other crate/module in
591/// this workspace holds a second copy. This is the ST-152 discipline above
592/// applied specifically to signal *values* (not merely STATE's own
593/// resources): every read/write path this crate exposes
594/// (`SignalAccess::read`/`write`, the accessor ABI, `driver::resolve_net_step`)
595/// resolves a `StateCellId` down to a `SlotLocation` via `self.cells` and
596/// indexes into `self.arena`'s planes directly, never into a second,
597/// independently-populated store — see `region_check::
598/// no_second_authoritative_copy_exists_for_a_cell_read_through_two_apis` for
599/// the concrete regression proving two different read APIs observe the exact
600/// same storage with no manual "sync" step between them.
601pub struct StateEngine {
602    pub arena: Rc<Arena>,
603    pub templates: Rc<TemplateRegistry>,
604    /// Task 8 (ST-109): dense `StateCellId -> SlotLocation` table;
605    /// `alloc_cell` (`state::write`) pushes, `commit_write` indexes. See
606    /// `state::write`'s module doc comment, deviation 1, for why this is
607    /// `pub(crate)` rather than module-private.
608    pub(crate) cells: Vec<SlotLocation>,
609    /// Task 9 retrofit (see `state::write`'s module doc comment): bundles
610    /// the former `current_time`/`current_region`/`worker_buffers`/
611    /// `engine_id` fields behind one `Rc`-shared, interior-mutable handle so
612    /// `InstanceHandle` (`allocate_instance` below) can clone a reference to
613    /// it and call the exact same `write::commit_write_at` primitive
614    /// `Self::commit_write` calls, without needing a `&mut StateEngine`
615    /// back-reference. `set_current_time`/`set_current_region`/
616    /// `register_worker_thread` below keep their exact pre-Task-9 public
617    /// signatures; only their bodies now forward through this field.
618    pub(crate) vc: Rc<VcState>,
619    /// Task 11 (ST-035, ST-073, ST-074): the fixed 3-role VC listener
620    /// dispatch table. `SignalAccess::subscribe` (`accessor_impl.rs`) is the
621    /// one entry point that populates it; nothing else in this crate reaches
622    /// it except through `&mut self.dispatch` on this direct field (ST-152 —
623    /// no second, free-standing global).
624    pub(crate) dispatch: super::dispatch::Dispatch,
625    /// Task 14 (ST-034, `state::driver`): `trireg` decay-charge side table —
626    /// `trireg`'s own `TriregCharge` is never stored in a normal
627    /// `DriverEntry`/the arena's own planes (see `TriregCharge`'s own doc
628    /// comment); this field only stores the state, the actual decay-timing
629    /// behavior is a TIMING-subsystem hook out of this wave's scope. Not in
630    /// Task 14's own Files list (which names only `state/driver.rs` (new)
631    /// and `state/mod.rs`) — a direct field is required to give this side
632    /// table anywhere to live, per ST-152's "every resource STATE owns is a
633    /// direct field of `StateEngine`" discipline this same struct's own doc
634    /// comment already establishes for every other field here.
635    pub(crate) trireg_charge: HashMap<StateCellId, TriregCharge>,
636    /// Task 15 (ST-122, `state::region_check`): the runtime guard for
637    /// **external** writes — `driver::force_driver`/`release_driver` each
638    /// call every entry here (in registration order) so a future
639    /// partitioning/independence assumption can invalidate itself the
640    /// instant an external `force`/`vpi_put_value` touches a
641    /// region-crossing net, rather than trusting a now-stale static
642    /// classification (ARCHITECTURE §4/§9). Not in Task 15's own Files list
643    /// (which names only `state/region_check.rs` (new) and `state/mod.rs`)
644    /// — a direct field is required to give this registry anywhere to live,
645    /// per ST-152's discipline this same struct's own doc comment already
646    /// establishes for every other field here (the same precedent Task 14's
647    /// `trireg_charge` field sets immediately above).
648    pub(crate) force_observed: Vec<Box<dyn ForceObserved>>,
649    /// Task 16 (ST-098, `state::rollback`): per-cell reset behavior for
650    /// `StateEngine::reset_to_initial`, populated in exact lockstep with
651    /// `cells` — `write::StateEngine::alloc_cell` pushes a conservative
652    /// `rollback::CellReset::AllX` default for *every* cell it allocates
653    /// (regardless of caller), and `allocate_instance`'s per-slot loop below
654    /// (the one call site that actually has a `SlotDescriptor` — with its
655    /// `initial`/`net_info` — in scope) overwrites that default for each of
656    /// a template's real slots. Not in Task 16's own Files list (`state/
657    /// rollback.rs` (new), `state/mod.rs`) — a direct field is required to
658    /// give this bookkeeping anywhere to live, per ST-152's discipline this
659    /// same struct's own doc comment already establishes for every other
660    /// field here (the same precedent Task 14's `trireg_charge` and Task
661    /// 15's `force_observed` fields set immediately above).
662    pub(crate) cell_reset: Vec<super::rollback::CellReset>,
663    /// Task 17 (ST-103, ST-104, ST-110, `state::frame`): the STATE-owned
664    /// backing storage for every `process::ActivationRecord` this engine
665    /// hands out — `new_activation_record` below is the only place that
666    /// grows it. Not in Task 17's own Files list (`state/frame.rs` (new),
667    /// `state/mod.rs`) — a direct field is required to give this arena
668    /// anywhere to live, per ST-152's discipline this same struct's own doc
669    /// comment already establishes for every other field here (the same
670    /// precedent Task 14's `trireg_charge`, Task 15's `force_observed`, and
671    /// Task 16's `cell_reset` fields set immediately above).
672    pub(crate) frame: super::frame::FrameArena,
673    /// Task 18 (ST-118, ST-119, `state::introspect`): each `allocate_instance`
674    /// call mints a fresh [`InstanceId`] from this counter (0, 1, 2, ...,
675    /// never reused/freed — the same bump-allocator discipline `alloc_cell`
676    /// already uses for `StateCellId`) and records it as the instance half
677    /// of every one of that instance's slots' `InstantiatedSignalId` key in
678    /// `hier_cells` below. Not in Task 18's own Files list (`state/
679    /// introspect.rs` (new), `state/mod.rs`) — a direct field/counter is
680    /// required to give per-instance identity anywhere to come from, per
681    /// ST-152's discipline this same struct's own doc comment already
682    /// establishes for every other field here (the same precedent Task
683    /// 14/15/16/17's own `StateEngine` field additions set).
684    pub(crate) next_instance_id: u32,
685    /// Task 18 (ST-118, ST-119): the one `InstantiatedSignalId ->
686    /// StateCellId` map [`introspect::read_by_hier_path`] resolves a
687    /// hierarchical path's already-`NetlistView`-resolved signal identity
688    /// through. Populated exclusively by `allocate_instance`'s per-slot loop
689    /// below, in the same pass that mints each slot's `StateCellId` — never
690    /// a second, independently-populated store (ST-152). Keyed by
691    /// `InstantiatedSignalId` (instance + member), never by `member:
692    /// SymbolKey` alone, which is the concrete data-structure-level fact
693    /// that makes FRONTEND's compiled-body dedup invisible above this
694    /// boundary (ST-119): two instances sharing one template's `SymbolKey`s
695    /// still get two disjoint keys here, because each instance's own
696    /// `InstanceId` (from `next_instance_id` above) differs.
697    pub(crate) hier_cells: HashMap<InstantiatedSignalId, StateCellId>,
698    /// R-run Task 16 (wave-B-state Task 5, ST-111): the set of
699    /// [`StateCellId`]s [`super::event::StateEngine::alloc_event_cell`] has
700    /// minted — an ordinary 1-bit `SlotClass::Inline` cell whose only
701    /// distinguishing property is membership here (see `state::event`'s
702    /// module doc comment for why this is a side-table, not a new
703    /// `SlotClass::Event` variant). Not in this task's own Files list
704    /// (`state/event.rs` (new), `state/mod.rs`) — a direct field is required
705    /// to give this bookkeeping anywhere to live, per ST-152's discipline
706    /// this same struct's own doc comment already establishes for every
707    /// other field here (the same precedent `trireg_charge`/
708    /// `force_observed`/`cell_reset`/`hier_cells` set).
709    pub(crate) event_cells: HashSet<StateCellId>,
710}
711
712impl StateEngine {
713    pub fn new(templates: TemplateRegistry) -> Self {
714        Self::new_with_mode(templates, BuildMode::FourState)
715    }
716
717    /// Task 13 (ST-100, ST-101, ST-102): the only constructor that can
718    /// produce a `BuildMode::TwoState` `StateEngine` -- `mode` is threaded
719    /// straight into `Arena::new` and never mutated after (see
720    /// `write::StateEngine::build_mode`'s own doc comment for why no setter
721    /// exists anywhere in this crate).
722    pub fn new_with_mode(templates: TemplateRegistry, mode: BuildMode) -> Self {
723        let engine_id = NEXT_ENGINE_ID.fetch_add(1, Ordering::Relaxed);
724        StateEngine {
725            arena: Rc::new(Arena::new(mode)),
726            templates: Rc::new(templates),
727            cells: Vec::new(),
728            vc: Rc::new(VcState::new(engine_id)),
729            dispatch: super::dispatch::Dispatch::new(),
730            trireg_charge: HashMap::new(),
731            force_observed: Vec::new(),
732            cell_reset: Vec::new(),
733            frame: super::frame::FrameArena::new(),
734            next_instance_id: 0,
735            hier_cells: HashMap::new(),
736            event_cells: HashSet::new(),
737        }
738    }
739
740    /// Task 8's "how STATE learns now" header decision: called by ENGINE's
741    /// region-eval loop once per region, before invoking any kernel/process
742    /// that might call back into `commit_write`. `commit_write` itself never
743    /// gains a time parameter -- it reads this (via `self.vc`) instead.
744    pub fn set_current_time(&mut self, at: vj_abi::SimTime) {
745        self.vc.set_current_time(at);
746    }
747
748    /// The `Region` counterpart to `set_current_time`.
749    pub fn set_current_region(&mut self, region: vj_abi::Region) {
750        self.vc.set_current_region(region);
751    }
752
753    /// Task 8's "cross-thread visibility of the VC speculative buffer"
754    /// header decision: called once by each thread before its first
755    /// `commit_write` (or self-called by `commit_write` itself on first
756    /// use, via `state::write`'s thread-local cache). Pushes a fresh,
757    /// individually-boxed `RefCell<Vec<VcRecord>>` onto `self.vc`'s
758    /// `worker_buffers` and returns its index as an opaque `WorkerSlot` --
759    /// each entry's own heap allocation has a stable address for the life of
760    /// this `StateEngine` regardless of how many further times
761    /// `worker_buffers` itself grows (the same box-per-entry stability
762    /// argument `TemplateRegistry` already relies on for `StateLayout`).
763    ///
764    /// **SAFETY precondition** (documented here, exercised for real once a
765    /// later task's `drain_committed_changes` exists): this function -- and
766    /// every `commit_write` call that appends through the slot it returns --
767    /// may run only when no other thread concurrently drains any registered
768    /// buffer; eval and commit/drain are barrier-separated phases in this
769    /// wave's WOCB execution model, never concurrent with each other.
770    pub fn register_worker_thread(&mut self) -> WorkerSlot {
771        // SAFETY: `&mut self` guarantees no other call into this
772        // `StateEngine` (and therefore this `VcState`) races this one --
773        // forwards to `VcState`'s own `&self`-based method (see its doc
774        // comment, `state::write`, for why it's `unsafe`/`&self`-based at
775        // all: so it is also callable through `InstanceHandle`'s own
776        // `Rc<VcState>` clone with no `&mut StateEngine` in scope).
777        unsafe { self.vc.register_worker_thread() }
778    }
779
780    /// Template-relative only (ST-129/ST-135): never adds an instance base.
781    /// Returns `None` — never a fabricated default `SlotOffset` — when
782    /// `symbol` is not present in `template`'s built `StateLayout`, or when
783    /// `template` itself was never registered (ST-037: `SlotOffset` values
784    /// are minted exclusively by the Task 4/6 layout builder, never
785    /// synthesized on demand by this query API).
786    pub fn slot_of(&self, template: &TemplateKey, symbol: SymbolKey) -> Option<super::SlotOffset> {
787        self.templates
788            .layout_of(template)?
789            .slots
790            .iter()
791            .find(|slot| slot.key == symbol)
792            .map(|slot| slot.offset)
793    }
794
795    /// `(inline_words, wide_words)` straight from the `StateLayout` (ST-135).
796    pub fn template_size(&self, template: &TemplateKey) -> Option<(u32, u32)> {
797        self.templates
798            .layout_of(template)
799            .map(|layout| (layout.inline_words, layout.wide_words))
800    }
801
802    /// Looks up `template_size`, bulk-allocates that many words via the two
803    /// [`Arena`] bulk allocators, and returns a heap-boxed [`InstanceHandle`]
804    /// (boxed so its address is stable — Task 8 turns that address directly
805    /// into a `SimHandle`) whose base is disjoint from every previously
806    /// issued base for this arena (ST-067/ST-136). Returns `None` iff
807    /// `template` was never registered — never a fabricated zero-sized
808    /// allocation.
809    pub fn allocate_instance(&mut self, template: TemplateKey) -> Option<Box<InstanceHandle>> {
810        let (inline_words, wide_words) = self.template_size(&template)?;
811        // SAFETY: `self.arena` is this `StateEngine`'s own `Rc<Arena>`;
812        // nothing else in this crate holds a live `&[T]`/`&mut [T]` borrow
813        // of either plane across this call (allocate_instance never hands
814        // one out), and `&mut self` above guarantees no other call into
815        // this `StateEngine` (and therefore this `Arena`) races this one —
816        // the single-logical-writer obligation `alloc_inline_words`/
817        // `alloc_wide_words` document. Using `&self.arena` (never
818        // `&mut self.arena`, i.e. never forming `&mut Arena`) here is the
819        // whole point: it's what keeps every previously-minted
820        // `InstanceHandle::arena` raw pointer's Stacked-Borrows tag alive
821        // across this and every future `allocate_instance` call — see
822        // `state::arena`'s module doc comment.
823        let (inline_base, wide_base) = unsafe {
824            (
825                self.arena.alloc_inline_words(inline_words),
826                self.arena.alloc_wide_words(wide_words),
827            )
828        };
829
830        // `template_size` above already proved `template` is registered, so
831        // this second lookup (needed to mint the raw pointer into the
832        // registry's own storage) cannot fail in practice; `?` still keeps
833        // this function total rather than assuming it via `.unwrap()`.
834        // Points into the `Box<StateLayout>` `TemplateRegistry` owns for
835        // this template (see `TemplateRegistry`'s doc comment) — stable
836        // across any later `register` call, not into `templates`' own `Vec`
837        // storage.
838        let layout_ptr: *const StateLayout = self.templates.layout_of(&template)?;
839        // A shared reborrow of `self.arena`'s pointee, never `&mut Arena` —
840        // see the SAFETY comment above and `InstanceHandle`'s doc comment.
841        let arena_ptr: *const Arena = &*self.arena;
842
843        // Task 9 retrofit (ST-013, ST-020..ST-023 — see `InstanceHandle::cells`'
844        // own doc comment): mint one absolute, already-base-applied
845        // `StateCellId` per slot in this template's layout, via Task 8's
846        // `alloc_cell`, in `SlotId`/slot-index order. `layout_ptr` was minted
847        // into `self.templates`' own storage above (never `self`'s), so
848        // borrowing through it here does not conflict with the `&mut self`
849        // `alloc_cell` calls in the loop below.
850        // SAFETY: `layout_ptr` was just returned by `self.templates.layout_of`
851        // above and points into a `Box<StateLayout>` `TemplateRegistry` owns
852        // for `template` — see that struct's doc comment for why this
853        // pointer stays valid for as long as the registry itself does
854        // (unaffected by the `&mut self.cells` pushes in the loop below,
855        // which touch a wholly different field).
856        let layout_ref: &StateLayout = unsafe { &*layout_ptr };
857        // Task 18 retrofit (ST-118, ST-119, `state::introspect`): mint this
858        // instance's own `InstanceId` from `self.next_instance_id`'s
859        // bump-allocator counter, once per `allocate_instance` call — never
860        // reused across instances, so the same template's `SymbolKey`s
861        // combine with a *different* `InstanceId` for every concrete
862        // instance, which is the concrete fact that keeps FRONTEND's
863        // compiled-body dedup invisible above `hier_cells` (ST-119).
864        let instance_id = InstanceId(self.next_instance_id);
865        self.next_instance_id = self
866            .next_instance_id
867            .checked_add(1)
868            .expect("more than u32::MAX instances allocated in one StateEngine");
869        let mut cells = Vec::with_capacity(layout_ref.slots.len());
870        for slot in &layout_ref.slots {
871            let word_offset = match slot.offset.class {
872                SlotClass::Inline => inline_base + slot.offset.word_offset,
873                SlotClass::Wide | SlotClass::Memory => wide_base + slot.offset.word_offset,
874                // Review fix (Task 14 r2, blocking finding 2): a real
875                // elaborated signal's own `SlotOffset` (this loop's `slot`)
876                // comes from the layout builder (Task 4/6), which never
877                // emits `ForceWide` — that class exists only for
878                // `driver::force_driver`'s own synthetic, caller-invisible
879                // contribution cells, allocated directly via `alloc_cell`
880                // outside this template-instantiation path entirely. See
881                // `SlotClass::ForceWide`'s own doc comment.
882                SlotClass::ForceWide => unreachable!(
883                    "allocate_instance: no SlotDescriptor/SlotOffset the layout builder \
884                     produces is ever SlotClass::ForceWide"
885                ),
886            };
887            let loc = SlotLocation {
888                class: slot.offset.class,
889                word_offset,
890                bit_width: slot.offset.bit_width,
891                elem_word_count: slot.offset.elem_word_count,
892                // Task 11 (ST-138): threaded from the real layout, never
893                // zeroed here -- a `SlotClass::Memory` slot's own
894                // `row_stride_words`/`num_rows` must survive onto the cell
895                // this `StateCellId` resolves to, the same way
896                // `accessor_impl::slot_location_for` threads them through
897                // its own, separate `InstanceHandle`-relative resolution
898                // path (see that function's doc comment).
899                row_stride_words: slot.offset.row_stride_words,
900                num_rows: slot.offset.num_rows,
901            };
902            let id = self.alloc_cell(loc);
903            // Task 16 (ST-098): overwrite `alloc_cell`'s conservative
904            // `CellReset::AllX` default with this real slot's actual
905            // reset behavior, derived from its `SlotDescriptor` — the one
906            // place in this crate that still has `slot` (and therefore
907            // `slot.net_info`/`slot.initial`) in scope. A net-backed slot
908            // is never touched by `reset_to_initial` at all (its reset
909            // state is the emergent `Z` `driver::resolve_net_step` folds
910            // to over zero active drivers, not a stored arena default —
911            // see `SlotDescriptor::net_info`'s own doc comment); a slot
912            // with a declared initializer re-blits it (the layout builder
913            // already converted it to `FourStateValue`, CONTRACTS D4; an
914            // initializer too wide for sv-lang's expansion cap arrives as
915            // `None` and gets the same honest all-X default an
916            // uninitialized slot gets); every other slot is all-X.
917            let reset = if slot.net_info.is_some() {
918                super::rollback::CellReset::Net
919            } else if let Some(initial) = &slot.initial {
920                super::rollback::CellReset::Initial(initial.clone())
921            } else {
922                super::rollback::CellReset::AllX
923            };
924            self.cell_reset[id.0 as usize] = reset;
925            // Task 18 retrofit (ST-118, ST-119): record this slot's
926            // `InstantiatedSignalId -> StateCellId` mapping in the same pass
927            // that mints `id` above — never a second, later-populated store
928            // (ST-152). `slot.key` is this member's already-deduped
929            // `SymbolKey`; combined with `instance_id` above, the resulting
930            // key is unique per (instance, member) even when two instances
931            // share this template's `StateLayout`.
932            self.hier_cells.insert(
933                InstantiatedSignalId {
934                    instance: instance_id,
935                    member: slot.key,
936                },
937                id,
938            );
939            cells.push(id);
940        }
941
942        Some(Box::new(InstanceHandle {
943            base: InstanceBase {
944                inline_base,
945                wide_base,
946            },
947            template,
948            layout: layout_ptr,
949            arena: arena_ptr,
950            // Review fix (Task 7 r2): clone (not merely reference) this
951            // StateEngine's own Rc<Arena>/Rc<TemplateRegistry> into the
952            // handle itself, so the handle keeps these allocations alive on
953            // its own — independent of whether `self` (this StateEngine) is
954            // later dropped. See InstanceHandle's doc comment.
955            arena_owner: Rc::clone(&self.arena),
956            templates_owner: Rc::clone(&self.templates),
957            // Task 9 retrofit: same reasoning as arena_owner/templates_owner
958            // above, applied to the shared VC-tracking state.
959            vc_owner: Rc::clone(&self.vc),
960            cells: std::cell::UnsafeCell::new(cells),
961            foreign: std::cell::RefCell::new(Vec::new()),
962            windows: std::cell::RefCell::new(Vec::new()),
963            wide_scratch: std::cell::RefCell::new((0, Vec::new())),
964            inline_ptrs: std::cell::UnsafeCell::new(((0, 0), Vec::new())),
965        }))
966    }
967}
968
969/// Classes plan Task 4 (`docs/superpowers/plans/2026-10-03-classes.md`, LRM
970/// 8.9): the `(InstanceId, TemplateKey)` pair [`StateEngine::
971/// allocate_class_static_singletons`] uses for one `ClassTemplateId`'s
972/// static-property singleton segment — a deterministic, pure function of
973/// `class_template` alone (never re-derived differently by two call sites),
974/// so every caller (that one `allocate_instance` call, and every later
975/// foreign-slot reference a `Class::static_prop`/`obj.static_prop` access
976/// mints) agrees on exactly which synthetic instance a given class
977/// template's statics live in.
978///
979/// **Not defined in `netlist::`** despite otherwise matching
980/// [`crate::netlist::ClassTemplateId`]'s own "frontend-independent, needed
981/// by STATE" placement rationale: `netlist_dedup_invariants.rs`'s NL-018
982/// guard (`codegen_never_needs_a_second_dedup_key_derivation`) source-scans
983/// exactly five `netlist::` files for any function whose signature returns a
984/// `TemplateKey`, asserting the only name that ever does is `dedup_key` —
985/// protecting that module's own monopoly on a REAL elaborated module
986/// instance's dedup key. This function mints a key for a wholly different,
987/// synthetic thing (a class-static singleton has no `Definition`/dedup
988/// identity of its own), the same way `vj::run`'s own package/bank
989/// synthetic `TemplateKey`s are minted inline there, outside `netlist::`
990/// entirely, rather than through a second `netlist::` function — this one
991/// lives next to its one real caller for the identical reason.
992///
993/// **Reserved id-range convention**, matching the one `vj::run`
994/// already established for its own synthetic instances rather than
995/// inventing a second one: a real elaborated `InstanceId` is bump-allocated
996/// from 0; a design's packages each get one counting DOWN from `u32::MAX`;
997/// the net-resolution "bank" reserves the single id `u32::MAX - (1 << 20)`.
998/// Class-static segments reserve the next block down again, counting from
999/// `u32::MAX - (2 << 20)` — disjoint from both of those by the same
1000/// generous-headroom convention (not a mathematically tight partition: no
1001/// real design instantiates anywhere near `2^20` packages, let alone that
1002/// many packages *and* that many distinct class templates with static
1003/// properties at once), so a `TemplateKey.definition` minted here can never
1004/// collide with one a real module `Definition`, a package, or the bank ever
1005/// uses.
1006///
1007/// Panics (rather than silently wrapping into collision) if `class_template`'s
1008/// own dense id would exhaust that reserved block — `expect`-style, never a
1009/// wrapping/saturating fallback, because a wrapped id would silently alias a
1010/// *different* class template's segment: exactly the "gate that can never
1011/// fail" this plan's own correctness posture forbids. `2^20` class templates
1012/// with static properties is not a real design this implementation
1013/// supports; it is a defensive bound stated loudly rather than left to wrap.
1014pub fn class_static_instance(class_template: ClassTemplateId) -> (InstanceId, TemplateKey) {
1015    const RESERVED_BASE: u32 = u32::MAX - (2 << 20);
1016    let n = RESERVED_BASE
1017        .checked_sub(class_template.0)
1018        .unwrap_or_else(|| {
1019            panic!(
1020                "class_static_instance: ClassTemplateId({}) exceeds the reserved \
1021                 synthetic-instance-id block (RESERVED_BASE = {RESERVED_BASE}) -- \
1022                 more class templates with static properties than this \
1023                 implementation's id-space convention supports",
1024                class_template.0
1025            )
1026        });
1027    (
1028        InstanceId(n),
1029        TemplateKey {
1030            definition: n,
1031            // Distinct from the bank's `params_hash: 1` and every package's
1032            // `params_hash: 0` -- defensive only, `definition` alone is
1033            // already disjoint from both by construction above.
1034            params_hash: 2,
1035        },
1036    )
1037}
1038
1039impl StateEngine {
1040    /// Classes plan Task 4 (`docs/superpowers/plans/2026-10-03-classes.md`,
1041    /// LRM 8.9): allocates one singleton segment per `(ClassTemplateId,
1042    /// StateLayout)` pair in `layouts` — exactly once per class template,
1043    /// regardless of how many objects of it are ever created (object
1044    /// creation itself, Task 5's `new()`/the object heap, never allocates
1045    /// one of these; it is minted once here, at elaboration time, sized
1046    /// exactly the way one real module `Template`'s own single set of
1047    /// slots already is). `layouts` is expected to already be deduplicated
1048    /// by the caller (e.g. one entry per
1049    /// [`crate::netlist::class_templates_with_statics`] result) — this
1050    /// function does not itself re-check for a repeated `ClassTemplateId`,
1051    /// the same "trust the caller's own dedup pass" discipline
1052    /// `allocate_instance`'s own callers already follow for `TemplateKey`.
1053    ///
1054    /// For each pair, registers `layout` under the `TemplateKey`
1055    /// [`class_static_instance`] mints for its `ClassTemplateId`, then
1056    /// calls [`Self::allocate_instance`] on it immediately — the same
1057    /// two-step `vj::run` already performs for a package's own
1058    /// singleton instance (the established precedent this reuses, not a
1059    /// new mechanism: `allocate_instance`'s own base-index/`SlotOffset`
1060    /// addressing and the `SlotId`/accessor-ABI read/write path are
1061    /// completely unchanged by this function).
1062    ///
1063    /// Returns the minted synthetic `InstanceId` (from
1064    /// [`class_static_instance`]) and the singleton's own `InstanceHandle`,
1065    /// keyed by `ClassTemplateId`, for the caller to keep alive for the
1066    /// rest of the run — a future task's `Class::static_prop`/
1067    /// `obj.static_prop` codegen resolves through the returned handle via a
1068    /// foreign slot, exactly like a package variable access does today
1069    /// (not wired by this function: nothing in this plan's Task 4 scope yet
1070    /// emits such a reference — see this plan's own Task 4 deviations).
1071    #[must_use]
1072    pub fn allocate_class_static_singletons(
1073        &mut self,
1074        layouts: Vec<(ClassTemplateId, StateLayout)>,
1075    ) -> HashMap<ClassTemplateId, (InstanceId, Box<InstanceHandle>)> {
1076        let mut out = HashMap::with_capacity(layouts.len());
1077        for (class_template, layout) in layouts {
1078            let (instance_id, key) = class_static_instance(class_template);
1079            // SAFETY: `self.templates` is this `StateEngine`'s own registry;
1080            // this function's own loop never calls `layout_of` (so no live
1081            // borrow into it is held across this call), and `&mut self`
1082            // throughout this whole function rules out a concurrent
1083            // `register`/`allocate_instance` call racing this one — the
1084            // same single-logical-writer discipline `allocate_instance`
1085            // above already relies on for this exact `templates` field.
1086            unsafe {
1087                self.templates.register(key, layout);
1088            }
1089            let handle = self.allocate_instance(key).unwrap_or_else(|| {
1090                unreachable!(
1091                    "allocate_class_static_singletons: class_static_instance's own \
1092                     TemplateKey was just registered above, so allocate_instance \
1093                     cannot report it unregistered"
1094                )
1095            });
1096            out.insert(class_template, (instance_id, handle));
1097        }
1098        out
1099    }
1100
1101    /// Task 18 (ST-118, ST-119): the one lookup `introspect::
1102    /// read_by_hier_path` resolves a `NetlistView`-resolved
1103    /// `InstantiatedSignalId` through — `pub(crate)` (not `pub`) because the
1104    /// hierarchical-path-string entry point is `introspect::
1105    /// read_by_hier_path`, not this raw map query; kept visible within the
1106    /// crate so this module's own tests can exercise the map directly (per
1107    /// this task's own "obtained through the hier-path map, not by hand"
1108    /// test requirement) without a second, parallel lookup path.
1109    pub(crate) fn cell_for_instantiated_signal(
1110        &self,
1111        sig: InstantiatedSignalId,
1112    ) -> Option<StateCellId> {
1113        self.hier_cells.get(&sig).copied()
1114    }
1115
1116    /// Task 18 structural test accessor (ST-119): the map's raw entry
1117    /// count, so a test can assert `hier_cells` grows by exactly one entry
1118    /// per signal per instance at the data-structure level, not merely by
1119    /// observing reads. `#[cfg(test)]` — no non-test code needs a bare
1120    /// count.
1121    #[cfg(test)]
1122    pub(crate) fn hier_cells_len(&self) -> usize {
1123        self.hier_cells.len()
1124    }
1125}
1126
1127/// `const u128 *const *sv_rt_inline_ptrs(SimHandle)`: for each slot number of
1128/// `handle`'s instance, the address `sv_get_scalar` would read (null where
1129/// the slot needs the accessor: not `Inline`, or a windowed alias). JIT code
1130/// calls it once per segment and loads an `Inline` signal directly, so a
1131/// process activation costs one call rather than one per signal. Valid for
1132/// the rest of the kernel call ([`InstanceHandle::inline_ptr_table`]). Lives
1133/// here, not in `accessor_impl.rs`, whose source-scan test forbids raw
1134/// pointer returns (this is a read-only table, never base+offset arithmetic).
1135#[cfg_attr(feature = "aot-exports", unsafe(export_name = "sv_rt_inline_ptrs"))]
1136pub extern "C" fn real_sv_rt_inline_ptrs(handle: vj_abi::SimHandle) -> *const *const u128 {
1137    // SAFETY: see `sv_get_scalar`.
1138    unsafe { InstanceHandle::from_sim_handle(handle) }.inline_ptr_table()
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143    use super::*;
1144    use crate::state::{SlotClass, SlotDescriptor, SlotOffset};
1145
1146    fn key(definition: u32, params_hash: u64) -> TemplateKey {
1147        TemplateKey {
1148            definition,
1149            params_hash,
1150        }
1151    }
1152
1153    fn inline_slot(sym: u32, word_offset: u32, bit_width: u32) -> SlotDescriptor {
1154        SlotDescriptor {
1155            key: SymbolKey(sym),
1156            offset: SlotOffset {
1157                class: SlotClass::Inline,
1158                word_offset,
1159                bit_width,
1160                elem_word_count: 1,
1161                row_stride_words: 0,
1162                num_rows: 0,
1163            },
1164            initial: None,
1165            net_info: None,
1166        }
1167    }
1168
1169    /// Task 11 (ST-138) regression: `allocate_instance`'s own `SlotLocation`
1170    /// construction (`self.cells` -- the `StateCellId`-keyed table
1171    /// `StateEngine::commit_write` indexes through) must thread a
1172    /// `SlotClass::Memory` slot's `row_stride_words`/`num_rows` through, not
1173    /// drop/zero them -- a *different* real path from
1174    /// `accessor_impl::slot_location_for`'s own regression test, which only
1175    /// covers the `InstanceHandle`-relative accessor-ABI resolution, not
1176    /// this `StateCellId`-keyed one. `row_stride_words: 5, num_rows: 9`
1177    /// deliberately distinct from `elem_word_count: 3`, so a bug that copied
1178    /// `elem_word_count` instead (or left both zeroed) is caught.
1179    #[test]
1180    fn allocate_instance_threads_memory_row_stride_and_num_rows_into_cells() {
1181        let templates = TemplateRegistry::new();
1182        let tmpl = key(0, 0);
1183        let memory_slot = SlotDescriptor {
1184            key: SymbolKey(0),
1185            offset: SlotOffset {
1186                class: SlotClass::Memory,
1187                word_offset: 0,
1188                bit_width: 32,
1189                elem_word_count: 3,
1190                row_stride_words: 5,
1191                num_rows: 9,
1192            },
1193            initial: None,
1194            net_info: None,
1195        };
1196        // SAFETY: `templates` is a local, single-owned value with no other
1197        // live borrow and no concurrent caller.
1198        unsafe {
1199            templates.register(
1200                tmpl,
1201                StateLayout {
1202                    slots: vec![memory_slot],
1203                    inline_words: 0,
1204                    wide_words: 3 * 9,
1205                },
1206            );
1207        }
1208        let mut engine = StateEngine::new(templates);
1209        engine.allocate_instance(tmpl).expect("template registered");
1210
1211        assert_eq!(engine.cells.len(), 1);
1212        let loc = engine.cells[0];
1213        assert_eq!(loc.class, SlotClass::Memory);
1214        assert_eq!(loc.elem_word_count, 3);
1215        assert_eq!(
1216            loc.row_stride_words, 5,
1217            "allocate_instance must copy row_stride_words from the SlotOffset into self.cells, not drop/zero it"
1218        );
1219        assert_eq!(
1220            loc.num_rows, 9,
1221            "allocate_instance must copy num_rows from the SlotOffset into self.cells, not drop/zero it"
1222        );
1223    }
1224
1225    /// ST-067: a template with `inline_words == 1`; call `allocate_instance`
1226    /// 8x (standing in for a real driver iterating 8 concrete generate-for
1227    /// instances Task 5's dedup walk discovers). The 8 returned
1228    /// `base.inline_base` values must be exactly `0,1,2,...,7` — strictly
1229    /// increasing, no gaps, no repeats.
1230    #[test]
1231    fn second_pass_assigns_distinct_nonoverlapping_bases_for_8_instances() {
1232        let templates = TemplateRegistry::new();
1233        let tmpl = key(0, 0);
1234        // SAFETY: `templates` is a local, single-owned value with no other
1235        // live borrow and no concurrent caller.
1236        unsafe {
1237            templates.register(
1238                tmpl,
1239                StateLayout {
1240                    slots: vec![inline_slot(0, 0, 1)],
1241                    inline_words: 1,
1242                    wide_words: 0,
1243                },
1244            );
1245        }
1246        let mut engine = StateEngine::new(templates);
1247
1248        let bases: Vec<u32> = (0..8)
1249            .map(|_| {
1250                engine
1251                    .allocate_instance(tmpl)
1252                    .expect("template is registered")
1253                    .base
1254                    .inline_base
1255            })
1256            .collect();
1257
1258        assert_eq!(bases, vec![0, 1, 2, 3, 4, 5, 6, 7]);
1259    }
1260
1261    /// ST-129: `slot_of(template, symbol)` called before *and* after calling
1262    /// `allocate_instance` for that template 3 times must return a
1263    /// byte-identical `SlotOffset` every time — the function's own signature
1264    /// takes no `InstanceHandle`/`InstanceId`, so it is structurally
1265    /// incapable of returning an instance-adjusted value.
1266    #[test]
1267    fn slot_of_is_template_relative_and_instance_independent() {
1268        let templates = TemplateRegistry::new();
1269        let tmpl = key(1, 0);
1270        let sym = SymbolKey(7);
1271        // SAFETY: `templates` is a local, single-owned value with no other
1272        // live borrow and no concurrent caller.
1273        unsafe {
1274            templates.register(
1275                tmpl,
1276                StateLayout {
1277                    slots: vec![inline_slot(7, 0, 4)],
1278                    inline_words: 1,
1279                    wide_words: 0,
1280                },
1281            );
1282        }
1283        let mut engine = StateEngine::new(templates);
1284
1285        let before = engine
1286            .slot_of(&tmpl, sym)
1287            .expect("slot exists before allocation");
1288
1289        for _ in 0..3 {
1290            engine
1291                .allocate_instance(tmpl)
1292                .expect("template is registered");
1293        }
1294
1295        let after = engine
1296            .slot_of(&tmpl, sym)
1297            .expect("slot exists after allocation");
1298        assert_eq!(before, after, "slot_of must be instance-independent");
1299    }
1300
1301    /// ST-135: a template with one 8-bit inline member and one 128-bit wide
1302    /// member; `template_size(key) == Some((1, 2))`.
1303    #[test]
1304    fn template_size_matches_the_built_layouts_word_counts() {
1305        let templates = TemplateRegistry::new();
1306        let tmpl = key(2, 0);
1307        // SAFETY: `templates` is a local, single-owned value with no other
1308        // live borrow and no concurrent caller.
1309        unsafe {
1310            templates.register(
1311                tmpl,
1312                StateLayout {
1313                    slots: vec![
1314                        inline_slot(0, 0, 8),
1315                        SlotDescriptor {
1316                            key: SymbolKey(1),
1317                            offset: SlotOffset {
1318                                class: SlotClass::Wide,
1319                                word_offset: 0,
1320                                bit_width: 128,
1321                                elem_word_count: 2,
1322                                row_stride_words: 0,
1323                                num_rows: 0,
1324                            },
1325                            initial: None,
1326                            net_info: None,
1327                        },
1328                    ],
1329                    inline_words: 1,
1330                    wide_words: 2,
1331                },
1332            );
1333        }
1334        let engine = StateEngine::new(templates);
1335
1336        assert_eq!(engine.template_size(&tmpl), Some((1, 2)));
1337    }
1338
1339    /// ST-136: 3 sequential `allocate_instance` calls, interleaved across two
1340    /// *different* templates; all 3 `InstanceHandle`s must have pairwise
1341    /// distinct `(base.inline_base, base.wide_base)` — proving the arena's
1342    /// bump allocator never reuses a freed/previous range (there is no free
1343    /// path at all in v1).
1344    #[test]
1345    fn allocate_instance_n_times_yields_n_handles_that_never_shrink_or_reuse() {
1346        let templates = TemplateRegistry::new();
1347        let tmpl_a = key(3, 0);
1348        let tmpl_b = key(4, 0);
1349        // SAFETY: `templates` is a local, single-owned value with no other
1350        // live borrow and no concurrent caller; both calls are sequential.
1351        unsafe {
1352            templates.register(
1353                tmpl_a,
1354                StateLayout {
1355                    slots: vec![inline_slot(0, 0, 1)],
1356                    inline_words: 1,
1357                    wide_words: 0,
1358                },
1359            );
1360            templates.register(
1361                tmpl_b,
1362                StateLayout {
1363                    slots: vec![inline_slot(0, 0, 1)],
1364                    inline_words: 1,
1365                    wide_words: 0,
1366                },
1367            );
1368        }
1369        let mut engine = StateEngine::new(templates);
1370
1371        let h1 = engine.allocate_instance(tmpl_a).expect("tmpl_a registered");
1372        let h2 = engine.allocate_instance(tmpl_b).expect("tmpl_b registered");
1373        let h3 = engine.allocate_instance(tmpl_a).expect("tmpl_a registered");
1374
1375        let bases: Vec<(u32, u32)> = [&h1, &h2, &h3]
1376            .iter()
1377            .map(|h| (h.base.inline_base, h.base.wide_base))
1378            .collect();
1379
1380        assert_ne!(bases[0], bases[1]);
1381        assert_ne!(bases[1], bases[2]);
1382        assert_ne!(bases[0], bases[2]);
1383    }
1384
1385    /// ST-037: `slot_of` for a `SymbolKey` never present in any built
1386    /// `StateLayout` returns `None`, never a fabricated default
1387    /// `SlotOffset` — the concrete proof that `SlotOffset` values are minted
1388    /// exclusively by the Task 4/6 builder, never synthesized on demand by
1389    /// the query API.
1390    #[test]
1391    fn slot_offset_has_no_external_constructor_outside_the_layout_builder() {
1392        let templates = TemplateRegistry::new();
1393        let tmpl = key(5, 0);
1394        // SAFETY: `templates` is a local, single-owned value with no other
1395        // live borrow and no concurrent caller.
1396        unsafe {
1397            templates.register(
1398                tmpl,
1399                StateLayout {
1400                    slots: vec![inline_slot(0, 0, 1)],
1401                    inline_words: 1,
1402                    wide_words: 0,
1403                },
1404            );
1405        }
1406        let engine = StateEngine::new(templates);
1407
1408        assert!(engine.slot_of(&tmpl, SymbolKey(999)).is_none());
1409        assert!(engine.slot_of(&key(999, 0), SymbolKey(0)).is_none());
1410    }
1411
1412    /// Review fix regression test: the `Box<Arena>`/`Box<TemplateRegistry>`
1413    /// change this module's `StateEngine` doc comment describes exists to
1414    /// make this provable. Build a `StateEngine`, `allocate_instance` once,
1415    /// and (through the handle, before any move) write a known pattern into
1416    /// its one slot directly via the (Task 2) `Arena` accessors — this task
1417    /// does not yet have `commit_write` (Task 8), so the arena's own public
1418    /// `inline_mut`/`inline` methods stand in for it, reached through the
1419    /// still-owned `engine.arena` field. Then force the whole `StateEngine`
1420    /// to relocate in memory (moved out through a second `Box`, exactly the
1421    /// pattern the plan names), and read the same slot again through the
1422    /// *original*, pre-move `InstanceHandle`'s private raw pointers. With
1423    /// `arena`/`templates` embedded by value (not boxed), this would read
1424    /// through a dangling pointer once the move happens; it passes only
1425    /// because `Box`'s heap allocation address is move-invariant.
1426    ///
1427    /// Review fix r1: this test previously failed under
1428    /// `cargo +nightly miri test -p vj-sim instance::` even with the
1429    /// `Box<Arena>` fix in place — the struct-address move-invariance the
1430    /// doc comment above argues for is real, but it isn't what Miri flagged.
1431    /// `engine.arena.inline_mut()` below (standing in for Task 8's
1432    /// not-yet-written `commit_write`) used to require `&mut self.arena`,
1433    /// i.e. a fresh `&mut Arena` reborrow of the *whole* `Arena` struct's
1434    /// memory (not just its inline plane) — under Stacked/Tree Borrows that
1435    /// invalidates `handle.arena`'s already-minted tag on that same memory,
1436    /// independent of the struct's address never moving. `Arena` now backs
1437    /// its planes with `UnsafeCell` and takes `&self` for every mutating
1438    /// accessor (see `state::arena`'s module doc comment) precisely so this
1439    /// call never needs `&mut Arena` at all.
1440    #[test]
1441    fn moving_the_state_engine_after_allocating_an_instance_leaves_the_handle_valid() {
1442        let templates = TemplateRegistry::new();
1443        let tmpl = key(6, 0);
1444        // SAFETY: `templates` is a local, single-owned value with no other
1445        // live borrow and no concurrent caller.
1446        unsafe {
1447            templates.register(
1448                tmpl,
1449                StateLayout {
1450                    slots: vec![inline_slot(0, 0, 1)],
1451                    inline_words: 1,
1452                    wide_words: 0,
1453                },
1454            );
1455        }
1456        let mut engine = StateEngine::new(templates);
1457        let handle = engine
1458            .allocate_instance(tmpl)
1459            .expect("template is registered");
1460
1461        const PATTERN: u128 = 0xDEAD_BEEF_CAFE_F00D_0123_4567_89AB_CDEF;
1462        // SAFETY: single-threaded test; nothing else holds a live borrow of
1463        // the inline plane (no `inline()`/`inline_mut()` call is in flight)
1464        // across this write, satisfying `inline_mut`'s single-logical-writer
1465        // obligation.
1466        unsafe {
1467            engine.arena.inline_mut()[handle.base.inline_base as usize] = PATTERN;
1468        }
1469
1470        // Force `engine` to relocate in memory: move it out through a second
1471        // `Box`, exactly the pattern the plan names.
1472        let relocated = Box::new(engine);
1473        let engine = *relocated;
1474        // Prevent an "unused" warning from suggesting `engine` is dead code —
1475        // its only remaining job is to keep the arena's heap allocation
1476        // alive for the read below.
1477        let _ = &engine;
1478
1479        // SAFETY: `handle.arena` was derived from `engine.arena`'s `Rc`
1480        // heap allocation before the move above; that heap allocation itself
1481        // never moved (only the pointer inside `StateEngine`'s `Rc<Arena>`
1482        // did), `engine` (and therefore its `Rc<Arena>`) is still alive at
1483        // this point (bound in scope on the line above), and `handle.base
1484        // .inline_base` is a valid inline-plane index this same test
1485        // allocated via `allocate_instance`. No concurrent writer exists in
1486        // this single-threaded test, no `&mut Arena` was formed anywhere
1487        // between minting `handle.arena` and this read (the write above went
1488        // through `inline_mut`'s `&self`-based path), and — unlike `Box` —
1489        // moving `engine` (and therefore its `Rc<Arena>` field) through
1490        // `Box::new(engine)` above does not itself retag `Arena`'s memory as
1491        // exclusively (re-)owned, since `Rc` is not a uniquely-owning
1492        // reference-like type (see `StateEngine`'s doc comment); the exact
1493        // failure this comment used to be wrong about.
1494        let read_back = unsafe { (*handle.arena).inline()[handle.base.inline_base as usize] };
1495        assert_eq!(
1496            read_back, PATTERN,
1497            "handle must survive StateEngine relocating in memory"
1498        );
1499    }
1500
1501    /// Review fix regression test (finding 1's own "every later
1502    /// `allocate_instance` call... invalidates every previously-issued
1503    /// `InstanceHandle`'s raw arena pointer" scenario, distinct from the
1504    /// move scenario above): mint a handle, then call `allocate_instance`
1505    /// many more times — enough to force the `Arena`'s backing `Vec`s to
1506    /// reallocate repeatedly — and prove the *first* handle's `arena`
1507    /// pointer, and the value written through it, both survive. Before this
1508    /// fix (`Arena`'s mutating accessors took `&mut self`), every one of
1509    /// these later `allocate_instance` calls formed a fresh `&mut Arena` via
1510    /// `self.arena.alloc_inline_words(...)`, which under Stacked/Tree
1511    /// Borrows invalidates the first handle's already-minted `*const Arena`
1512    /// tag — this test is what `cargo +nightly miri test -p vj-sim
1513    /// instance::` must stay clean against for that scenario, not just the
1514    /// move scenario the previous test covers.
1515    #[test]
1516    fn allocate_instance_called_many_more_times_leaves_the_first_handles_arena_pointer_valid() {
1517        let templates = TemplateRegistry::new();
1518        let tmpl = key(7, 0);
1519        // SAFETY: `templates` is a local, single-owned value with no other
1520        // live borrow and no concurrent caller.
1521        unsafe {
1522            templates.register(
1523                tmpl,
1524                StateLayout {
1525                    slots: vec![inline_slot(0, 0, 1)],
1526                    inline_words: 1,
1527                    wide_words: 0,
1528                },
1529            );
1530        }
1531        let mut engine = StateEngine::new(templates);
1532
1533        let first = engine
1534            .allocate_instance(tmpl)
1535            .expect("template is registered");
1536
1537        const PATTERN: u128 = 0x1357_9BDF_2468_ACE0_FEDC_BA98_7654_3210;
1538        // SAFETY: `first.arena` points at the `Rc`-owned `Arena`; no other live
1539        // borrow of the inline plane is held across this write, and this
1540        // single-threaded test performs no concurrent write.
1541        unsafe {
1542            (*first.arena).inline_mut()[first.base.inline_base as usize] = PATTERN;
1543        }
1544
1545        // Enough further allocations to force multiple `Vec` growth events
1546        // in the inline plane (well past any small fixed initial capacity).
1547        let mut later_handles = Vec::new();
1548        for _ in 0..4096 {
1549            later_handles.push(
1550                engine
1551                    .allocate_instance(tmpl)
1552                    .expect("template is registered"),
1553            );
1554        }
1555
1556        // SAFETY: `first.arena` was minted before the loop above and never
1557        // invalidated by it (every `allocate_instance` call routes its
1558        // `Arena` mutation through `&self`-based accessors, never forming a
1559        // `&mut Arena`); `first.base.inline_base` is still a valid inline
1560        // index into the same, never-shrinking arena.
1561        let read_back = unsafe { (*first.arena).inline()[first.base.inline_base as usize] };
1562        assert_eq!(
1563            read_back, PATTERN,
1564            "first handle's arena pointer must survive many later allocate_instance calls"
1565        );
1566        assert_eq!(later_handles.len(), 4096);
1567    }
1568
1569    /// Review fix regression test (findings 2 and 3): mint a handle for one
1570    /// template, then call the `pub` `TemplateRegistry::register` API
1571    /// directly (exactly as an external caller reachable via
1572    /// `StateEngine.templates` could) many more times — enough to force
1573    /// `TemplateRegistry`'s backing `Vec` to reallocate — and prove the
1574    /// first handle's `layout` pointer still resolves to the correct,
1575    /// original `StateLayout`. Before this fix
1576    /// (`templates: Vec<(TemplateKey, StateLayout)>`), `layout_ptr` pointed
1577    /// directly into the `Vec`'s own tuple storage, so this exact sequence
1578    /// would silently dangle it on reallocation with no compiler diagnostic
1579    /// (the empirical repro the finding describes); with each `StateLayout`
1580    /// individually boxed, the outer `Vec` reallocating never moves the
1581    /// `StateLayout` itself.
1582    #[test]
1583    fn registering_more_templates_after_allocating_an_instance_leaves_the_layout_pointer_valid() {
1584        let templates = TemplateRegistry::new();
1585        let tmpl_a = key(8, 0);
1586        // `StateLayout`/`SlotDescriptor` deliberately cannot derive
1587        // `Clone`/`PartialEq` (see `SlotDescriptor`'s doc comment:
1588        // `sv_lang::OwnedSVInt` has a `Drop` impl), so this test compares
1589        // `handle.layout`'s fields individually against these plain,
1590        // independently-constructed expected values rather than cloning the
1591        // registered `StateLayout` and comparing whole structs.
1592        let expected_offset = inline_slot(0, 0, 1).offset;
1593        // SAFETY: `templates` is a local, single-owned value with no other
1594        // live borrow and no concurrent caller.
1595        unsafe {
1596            templates.register(
1597                tmpl_a,
1598                StateLayout {
1599                    slots: vec![inline_slot(0, 0, 1)],
1600                    inline_words: 1,
1601                    wide_words: 0,
1602                },
1603            );
1604        }
1605        let mut engine = StateEngine::new(templates);
1606
1607        let handle = engine
1608            .allocate_instance(tmpl_a)
1609            .expect("tmpl_a is registered");
1610
1611        // Register enough further templates directly through the `pub`
1612        // `TemplateRegistry` API (`engine.templates`, exactly as an external
1613        // caller could) to force the backing `Vec` to reallocate at least
1614        // once past any small fixed initial capacity.
1615        //
1616        // SAFETY: nothing in this test holds a live `&StateLayout` (from
1617        // `layout_of`) across these calls, and this single-threaded test
1618        // performs no concurrent `register` call — `register`'s
1619        // single-logical-writer obligation holds.
1620        unsafe {
1621            for i in 0..10_000u32 {
1622                engine.templates.register(
1623                    key(100 + i, 0),
1624                    StateLayout {
1625                        slots: vec![inline_slot(0, 0, 1)],
1626                        inline_words: 1,
1627                        wide_words: 0,
1628                    },
1629                );
1630            }
1631        }
1632
1633        // SAFETY: `handle.layout` was minted before the loop above, into the
1634        // `Box<StateLayout>` `TemplateRegistry` owns for `tmpl_a`; per
1635        // `TemplateRegistry`'s doc comment, growing its outer `Vec` never
1636        // moves that boxed `StateLayout`, so this pointer is still valid.
1637        // Review fix (Task 7 r2): `handle` itself now holds its own
1638        // `Rc<TemplateRegistry>` clone (`templates_owner`), which is what
1639        // actually keeps this allocation alive — not `engine` merely still
1640        // being in scope (a property of this test, not of `InstanceHandle`
1641        // as a type); see `dropping_the_state_engine_after_allocating_an_instance_leaves_the_handle_valid`
1642        // below, which proves this by dropping `engine` before this read.
1643        let layout_via_handle = unsafe { &*handle.layout };
1644        assert_eq!(
1645            layout_via_handle.inline_words, 1,
1646            "layout pointer must survive further register() calls after allocate_instance"
1647        );
1648        assert_eq!(layout_via_handle.wide_words, 0);
1649        assert_eq!(layout_via_handle.slots.len(), 1);
1650        assert_eq!(layout_via_handle.slots[0].key, SymbolKey(0));
1651        assert_eq!(layout_via_handle.slots[0].offset, expected_offset);
1652        // Cross-check against the safe query API too, which must agree.
1653        assert_eq!(engine.template_size(&tmpl_a), Some((1, 0)));
1654    }
1655
1656    /// Review fix regression test (Task 7 r2, finding 1): the drop-scenario
1657    /// counterpart to `moving_the_state_engine_after_allocating_an_instance_
1658    /// leaves_the_handle_valid` above (that test moves the `StateEngine`; this
1659    /// one drops it outright — a strictly stronger hazard). Build a
1660    /// `StateEngine`, `allocate_instance` once, write a known pattern through
1661    /// the handle, then explicitly `drop` the `StateEngine` itself — the
1662    /// realistic pattern the finding names: a helper function that builds a
1663    /// `StateEngine`, calls `allocate_instance`, and returns just the
1664    /// (fully owned, detached) `Box<InstanceHandle>`, with nothing in
1665    /// `allocate_instance`'s own signature (`&mut self -> Option<Box
1666    /// <InstanceHandle>>`) tying the handle's lifetime back to `self`. Read
1667    /// the same slot again through the handle *after* the engine has been
1668    /// dropped and assert it still reads the correct pattern.
1669    ///
1670    /// Before `arena_owner`/`templates_owner` existed on `InstanceHandle`
1671    /// (see its doc comment), `handle.arena`/`handle.layout` were raw
1672    /// pointers derived from — but not keeping alive — `engine`'s
1673    /// `Rc<Arena>`/`Rc<TemplateRegistry>`; dropping `engine` here would drop
1674    /// each `Rc`'s last strong reference, free the backing heap allocation,
1675    /// and dangle both pointers. `cargo +nightly miri test -p vj-sim
1676    /// instance::` reported exactly this: "Undefined Behavior: constructing
1677    /// invalid value of type &state::arena::Arena: encountered a dangling
1678    /// reference (use-after-free)" at the read below, before this fix. This
1679    /// test is what that `cargo +nightly miri test` invocation must stay
1680    /// clean against for the drop scenario specifically, the same way the
1681    /// two `allocate_instance`/`register`-reallocation tests above pin it
1682    /// for their own scenarios.
1683    #[test]
1684    fn dropping_the_state_engine_after_allocating_an_instance_leaves_the_handle_valid() {
1685        const PATTERN: u128 = 0x0F1E_2D3C_4B5A_6978_8796_A5B4_C3D2_E1F0;
1686
1687        // Build the engine, allocate an instance, write the pattern, and
1688        // drop the engine, all inside this block — `handle` is the only
1689        // thing that survives it, mirroring "a helper function that returns
1690        // just the handle" from the finding.
1691        let handle = {
1692            let templates = TemplateRegistry::new();
1693            let tmpl = key(9, 0);
1694            // SAFETY: `templates` is a local, single-owned value with no
1695            // other live borrow and no concurrent caller.
1696            unsafe {
1697                templates.register(
1698                    tmpl,
1699                    StateLayout {
1700                        slots: vec![inline_slot(0, 0, 1)],
1701                        inline_words: 1,
1702                        wide_words: 0,
1703                    },
1704                );
1705            }
1706            let mut engine = StateEngine::new(templates);
1707            let handle = engine
1708                .allocate_instance(tmpl)
1709                .expect("template is registered");
1710
1711            // SAFETY: single-threaded test; nothing else holds a live borrow
1712            // of the inline plane across this write, satisfying
1713            // `inline_mut`'s single-logical-writer obligation.
1714            unsafe {
1715                engine.arena.inline_mut()[handle.base.inline_base as usize] = PATTERN;
1716            }
1717
1718            // The exact hazard the finding describes: drop the StateEngine
1719            // while `handle` is still alive and about to escape this block.
1720            drop(engine);
1721
1722            handle
1723        };
1724
1725        // SAFETY: `handle.arena` was cloned into `handle`'s own
1726        // `arena_owner: Rc<Arena>` field at `allocate_instance` time (Task 7
1727        // r2's fix); that `Rc` clone — not `engine`, which is fully dropped
1728        // by this point — is what keeps the `Arena` heap allocation alive
1729        // here, so `handle.arena` is still a valid pointer into it.
1730        // `handle.base.inline_base` is a valid inline-plane index this same
1731        // test allocated via `allocate_instance`, and no `&mut Arena` was
1732        // ever formed between the write above and this read (both go
1733        // through `Arena`'s `&self`-based accessors).
1734        let read_back = unsafe { (*handle.arena).inline()[handle.base.inline_base as usize] };
1735        assert_eq!(
1736            read_back, PATTERN,
1737            "handle must survive the StateEngine that minted it being dropped outright"
1738        );
1739    }
1740
1741    /// Classes plan Task 4: [`class_static_instance`] is a pure, idempotent
1742    /// function of its `ClassTemplateId` argument alone — two independent
1743    /// call sites (e.g. two different `new()` sites' codegen resolving the
1744    /// *same* class's static property) must mint the byte-identical
1745    /// `(InstanceId, TemplateKey)` pair, never two different ones that would
1746    /// silently split one class template's statics across two arena
1747    /// segments.
1748    #[test]
1749    fn class_static_instance_is_deterministic_across_repeat_calls() {
1750        let id = ClassTemplateId(3);
1751        assert_eq!(class_static_instance(id), class_static_instance(id));
1752    }
1753
1754    /// Two distinct `ClassTemplateId`s must never mint the same
1755    /// `(InstanceId, TemplateKey)` — the one property that actually matters
1756    /// for `TemplateRegistry::register`'s correctness (a collision here
1757    /// would silently alias two unrelated classes' static-property storage).
1758    #[test]
1759    fn class_static_instance_is_injective_over_distinct_class_templates() {
1760        let (a_inst, a_key) = class_static_instance(ClassTemplateId(0));
1761        let (b_inst, b_key) = class_static_instance(ClassTemplateId(1));
1762        assert_ne!(a_inst, b_inst);
1763        assert_ne!(a_key, b_key);
1764    }
1765
1766    /// The reserved id block this function mints from must stay strictly
1767    /// below the bank's own reserved id (`u32::MAX - (1 << 20)`,
1768    /// `vj::run`) for every `ClassTemplateId` a real run could ever
1769    /// produce (Task 2's own dedup never emits more class templates than
1770    /// there are `new()` call sites in the design) — proving the two
1771    /// synthetic ranges this function's own doc comment claims are disjoint
1772    /// actually are, rather than merely asserting it in prose.
1773    #[test]
1774    fn class_static_instance_stays_clear_of_the_bank_reserved_id() {
1775        const BANK_RESERVED: u32 = u32::MAX - (1 << 20);
1776        for raw in [0u32, 1, 1000, 1 << 19] {
1777            let (_, key) = class_static_instance(ClassTemplateId(raw));
1778            assert!(
1779                key.definition < BANK_RESERVED,
1780                "ClassTemplateId({raw})'s minted definition {} must stay below \
1781                 the bank's reserved id {BANK_RESERVED}",
1782                key.definition
1783            );
1784        }
1785    }
1786
1787    /// A class template whose id would exhaust the reserved block panics
1788    /// loudly rather than wrapping into a collision with a lower
1789    /// `ClassTemplateId`'s own segment ("no gate that cannot fail" applied
1790    /// to id-space exhaustion).
1791    #[test]
1792    #[should_panic(expected = "exceeds the reserved")]
1793    fn class_static_instance_panics_rather_than_wraps_past_the_reserved_block() {
1794        let _ = class_static_instance(ClassTemplateId(u32::MAX));
1795    }
1796
1797    /// Sanity: the function really does return a `TemplateKey` with a
1798    /// `params_hash` distinguishable from the bank's own/every package's own
1799    /// (defensive only — `definition` alone is already disjoint) — pinned
1800    /// here so a future edit to the bank/package convention in
1801    /// `vj::run` is forced to notice this function's own
1802    /// disjointness claim.
1803    #[test]
1804    fn class_static_instance_params_hash_is_not_the_banks_or_a_packages() {
1805        let (_, key) = class_static_instance(ClassTemplateId(0));
1806        assert_ne!(key.params_hash, 1, "1 is the bank's own params_hash");
1807        assert_ne!(key.params_hash, 0, "0 is every package's own params_hash");
1808    }
1809
1810    /// Classes plan Task 4 (LRM 8.9): `allocate_class_static_singletons`
1811    /// called once with two distinct `ClassTemplateId`s' layouts (each
1812    /// shaped like a `static int count;`) must hand back two handles whose
1813    /// storage never aliases — writing through one class template's handle
1814    /// must leave the other's completely untouched. This is the structural
1815    /// half of "two objects of a class with `static int count` both see
1816    /// the same value after one increments it": since every object of ONE
1817    /// class template shares this single handle (object creation itself is
1818    /// Task 5's job and never mints a second one), the other half — "the
1819    /// same value" — follows for free as long as no *other* class
1820    /// template's segment can ever overlap it, which is what this test
1821    /// pins.
1822    #[test]
1823    fn allocate_class_static_singletons_gives_each_class_template_its_own_storage() {
1824        let templates = TemplateRegistry::new();
1825        let mut engine = StateEngine::new(templates);
1826
1827        let layout_of = || StateLayout {
1828            slots: vec![inline_slot(0, 0, 32)],
1829            inline_words: 1,
1830            wide_words: 0,
1831        };
1832        let a = ClassTemplateId(0);
1833        let b = ClassTemplateId(1);
1834        let mut handles =
1835            engine.allocate_class_static_singletons(vec![(a, layout_of()), (b, layout_of())]);
1836
1837        let (_, handle_a) = handles.remove(&a).expect("class template a allocated");
1838        let (_, handle_b) = handles.remove(&b).expect("class template b allocated");
1839
1840        // SAFETY: single-threaded test; nothing else holds a live borrow of
1841        // the inline plane across these writes.
1842        unsafe {
1843            engine.arena.inline_mut()[handle_a.base.inline_base as usize] = 11;
1844            engine.arena.inline_mut()[handle_b.base.inline_base as usize] = 22;
1845        }
1846        assert_eq!(
1847            engine.arena.inline()[handle_a.base.inline_base as usize],
1848            11
1849        );
1850        assert_eq!(
1851            engine.arena.inline()[handle_b.base.inline_base as usize],
1852            22,
1853            "class template b's static-property storage must be untouched by \
1854             a write through class template a's own singleton handle"
1855        );
1856    }
1857
1858    /// Two reads through the SAME class template's singleton handle (one
1859    /// standing in for object #1's `obj.count` read, the other for object
1860    /// #2's, since Task 5's `new()` does not exist yet to mint two real
1861    /// objects) must observe the write the first one made — the literal
1862    /// "two objects ... both see the same value after one increments it"
1863    /// scenario, exercised at the mechanism level this task owns.
1864    #[test]
1865    fn allocate_class_static_singletons_write_is_visible_to_every_future_reader() {
1866        let templates = TemplateRegistry::new();
1867        let mut engine = StateEngine::new(templates);
1868        let class_template = ClassTemplateId(0);
1869        let layout = StateLayout {
1870            slots: vec![inline_slot(0, 0, 32)],
1871            inline_words: 1,
1872            wide_words: 0,
1873        };
1874        let mut handles = engine.allocate_class_static_singletons(vec![(class_template, layout)]);
1875        let (_, handle) = handles.remove(&class_template).unwrap();
1876        let slot = handle.base.inline_base as usize;
1877
1878        // "object #1" increments the shared static counter.
1879        // SAFETY: single-threaded test, no concurrent borrow of the plane.
1880        unsafe {
1881            engine.arena.inline_mut()[slot] = 0;
1882            engine.arena.inline_mut()[slot] += 1;
1883        }
1884        // "object #2" reads it back through the very same singleton handle
1885        // (there is only ever one -- that is the whole point of this
1886        // mechanism, not an incidental test detail).
1887        assert_eq!(
1888            engine.arena.inline()[slot],
1889            1,
1890            "a second reader of the same class template's static property must \
1891             observe the first reader's write, since both resolve to the same \
1892             singleton handle"
1893        );
1894    }
1895
1896    /// `allocate_class_static_singletons` must mint the exact synthetic
1897    /// `InstanceId` [`class_static_instance`] independently computes for the
1898    /// same `ClassTemplateId` -- never a second, divergent numbering scheme
1899    /// of its own.
1900    #[test]
1901    fn allocate_class_static_singletons_instance_id_matches_class_static_instance() {
1902        let templates = TemplateRegistry::new();
1903        let mut engine = StateEngine::new(templates);
1904        let class_template = ClassTemplateId(7);
1905        let layout = StateLayout {
1906            slots: vec![inline_slot(0, 0, 1)],
1907            inline_words: 1,
1908            wide_words: 0,
1909        };
1910        let mut handles = engine.allocate_class_static_singletons(vec![(class_template, layout)]);
1911        let (instance_id, _) = handles.remove(&class_template).unwrap();
1912
1913        let (expected_instance_id, _) = class_static_instance(class_template);
1914        assert_eq!(instance_id, expected_instance_id);
1915    }
1916
1917    /// Calling `allocate_class_static_singletons` with an empty `layouts`
1918    /// (every class template in the design has no `static` property) must
1919    /// allocate nothing and return an empty map -- never a phantom,
1920    /// zero-width singleton no class actually needs.
1921    #[test]
1922    fn allocate_class_static_singletons_with_no_layouts_allocates_nothing() {
1923        let templates = TemplateRegistry::new();
1924        let mut engine = StateEngine::new(templates);
1925        let handles = engine.allocate_class_static_singletons(Vec::new());
1926        assert!(handles.is_empty());
1927    }
1928}