Skip to main content

StateEngine

Struct StateEngine 

Source
pub struct StateEngine {
    pub arena: Rc<Arena>,
    pub templates: Rc<TemplateRegistry>,
    /* private fields */
}
Expand description

STATE’s top-level owned state (arena storage + the compiled template set) — review fix: arena/templates are Rc-wrapped, not Box-wrapped or embedded by value. A SimHandle/InstanceHandle minted by Self::allocate_instance must stay valid for the life of the simulation run and cross the compiled-kernel FFI boundary, so the raw pointers it stores must survive StateEngine itself being moved — returned by value from a constructor, pushed into a Vec, or embedded in a future root object are all ordinary things a caller does (this task’s own regression test exercises exactly this). With arena/templates embedded directly, any such move would silently dangle every previously-minted handle — no compiler diagnostic, UB on the next accessor call.

A first review fix boxed them (Box<Arena>/Box<TemplateRegistry>), reasoning that a heap allocation’s address doesn’t change when the (movable) Box pointer value travels with a moved StateEngine. That address argument is correct but insufficient: cargo +nightly miri test -p sv-sim-sim instance:: still fails with Box<Arena>, because moving a Box<T> value — including as a field of a larger struct passed by value into a function, e.g. Box::new(engine) or returning engine from a constructor — retags the Box’s pointee as freshly, exclusively owned under Rust’s Stacked/Tree Borrows model (the same aliasing treatment &mut T gets), independent of the pointee’s own address ever changing. That invalidates every previously-derived raw pointer into it, Arena’s own UnsafeCell-based interior mutability notwithstanding. Rc<T> does not carry this exclusive-ownership retag-on-move semantics — it’s a plain (refcounted) shared-ownership pointer, not a reference-like type Miri’s retagging pass treats specially — so Rc<Arena>/ Rc<TemplateRegistry> keep the same “stable heap address, movable container” property Box gave without the move-retag hazard. This is consistent with (and depends on) Arena’s and TemplateRegistry’s own mutating methods already being &self-based via UnsafeCell: Rc cannot produce &mut access at all (short of Rc::get_mut, unused here), so nothing about this design ever needed &mut Arena/ &mut TemplateRegistry in the first place. self.arena/self.templates field access auto-derefs through Rc exactly as it would through a bare field or a Box.

Task 10 (ST-152): every resource STATE owns — arena, templates, cells (Task 8), vc (the Task 9 retrofit’s VC-tracking state; the VC dispatch table and undo log join this list once Tasks 11/16 land) — is a direct field of this struct, never an Option<*mut OtherGlobal> indirection and never a free-standing process-wide global or lazily-initialized singleton cell of any kind anywhere in this crate (this module’s own test suite enforces the exact forbidden spellings by scanning this crate’s state source). The only pointer any accessor/ABI function accepts to reach any of it is SimHandle/InstanceHandle (itself reachable only via SimHandle, per this plan’s header decision) or, for scheduler-side callers, a &/&mut StateEngine reached however the caller already holds one — never a second, independent “give me the arena” pointer parameter threaded alongside it.

Task 15 (ST-033): every super::FourStateValue in the running simulation lives in exactly one of Arena.{inline,wide_aval,wide_bval} (or the _2s twins), reached only via a StateCellId/SlotLocation STATE itself minted (Task 8’s alloc_cell) — no other crate/module in this workspace holds a second copy. This is the ST-152 discipline above applied specifically to signal values (not merely STATE’s own resources): every read/write path this crate exposes (SignalAccess::read/write, the accessor ABI, driver::resolve_net_step) resolves a StateCellId down to a SlotLocation via self.cells and indexes into self.arena’s planes directly, never into a second, independently-populated store — see region_check:: no_second_authoritative_copy_exists_for_a_cell_read_through_two_apis for the concrete regression proving two different read APIs observe the exact same storage with no manual “sync” step between them.

Fields§

§arena: Rc<Arena>§templates: Rc<TemplateRegistry>

Implementations§

Source§

impl StateEngine

Source

pub fn new(templates: TemplateRegistry) -> Self

Source

pub fn new_with_mode(templates: TemplateRegistry, mode: BuildMode) -> Self

Task 13 (ST-100, ST-101, ST-102): the only constructor that can produce a BuildMode::TwoState StateEngine – mode is threaded straight into Arena::new and never mutated after (see write::StateEngine::build_mode’s own doc comment for why no setter exists anywhere in this crate).

Source

pub fn set_current_time(&mut self, at: SimTime)

Task 8’s “how STATE learns now” header decision: called by ENGINE’s region-eval loop once per region, before invoking any kernel/process that might call back into commit_write. commit_write itself never gains a time parameter – it reads this (via self.vc) instead.

Source

pub fn set_current_region(&mut self, region: Region)

The Region counterpart to set_current_time.

Source

pub fn register_worker_thread(&mut self) -> WorkerSlot

Task 8’s “cross-thread visibility of the VC speculative buffer” header decision: called once by each thread before its first commit_write (or self-called by commit_write itself on first use, via state::write’s thread-local cache). Pushes a fresh, individually-boxed RefCell<Vec<VcRecord>> onto self.vc’s worker_buffers and returns its index as an opaque WorkerSlot – each entry’s own heap allocation has a stable address for the life of this StateEngine regardless of how many further times worker_buffers itself grows (the same box-per-entry stability argument TemplateRegistry already relies on for StateLayout).

SAFETY precondition (documented here, exercised for real once a later task’s drain_committed_changes exists): this function – and every commit_write call that appends through the slot it returns – may run only when no other thread concurrently drains any registered buffer; eval and commit/drain are barrier-separated phases in this wave’s WOCB execution model, never concurrent with each other.

Source

pub fn slot_of( &self, template: &TemplateKey, symbol: SymbolKey, ) -> Option<SlotOffset>

Template-relative only (ST-129/ST-135): never adds an instance base. Returns None — never a fabricated default SlotOffset — when symbol is not present in template’s built StateLayout, or when template itself was never registered (ST-037: SlotOffset values are minted exclusively by the Task 4/6 layout builder, never synthesized on demand by this query API).

Source

pub fn template_size(&self, template: &TemplateKey) -> Option<(u32, u32)>

(inline_words, wide_words) straight from the StateLayout (ST-135).

Source

pub fn allocate_instance( &mut self, template: TemplateKey, ) -> Option<Box<InstanceHandle>>

Looks up template_size, bulk-allocates that many words via the two Arena bulk allocators, and returns a heap-boxed InstanceHandle (boxed so its address is stable — Task 8 turns that address directly into a SimHandle) whose base is disjoint from every previously issued base for this arena (ST-067/ST-136). Returns None iff template was never registered — never a fabricated zero-sized allocation.

Source§

impl StateEngine

Source

pub fn allocate_class_static_singletons( &mut self, layouts: Vec<(ClassTemplateId, StateLayout)>, ) -> HashMap<ClassTemplateId, (InstanceId, Box<InstanceHandle>)>

Classes plan Task 4 (docs/superpowers/plans/2026-10-03-classes.md, LRM 8.9): allocates one singleton segment per (ClassTemplateId, StateLayout) pair in layouts — exactly once per class template, regardless of how many objects of it are ever created (object creation itself, Task 5’s new()/the object heap, never allocates one of these; it is minted once here, at elaboration time, sized exactly the way one real module Template’s own single set of slots already is). layouts is expected to already be deduplicated by the caller (e.g. one entry per crate::netlist::class_templates_with_statics result) — this function does not itself re-check for a repeated ClassTemplateId, the same “trust the caller’s own dedup pass” discipline allocate_instance’s own callers already follow for TemplateKey.

For each pair, registers layout under the TemplateKey class_static_instance mints for its ClassTemplateId, then calls Self::allocate_instance on it immediately — the same two-step sv-sim-bin::run already performs for a package’s own singleton instance (the established precedent this reuses, not a new mechanism: allocate_instance’s own base-index/SlotOffset addressing and the SlotId/accessor-ABI read/write path are completely unchanged by this function).

Returns the minted synthetic InstanceId (from class_static_instance) and the singleton’s own InstanceHandle, keyed by ClassTemplateId, for the caller to keep alive for the rest of the run — a future task’s Class::static_prop/ obj.static_prop codegen resolves through the returned handle via a foreign slot, exactly like a package variable access does today (not wired by this function: nothing in this plan’s Task 4 scope yet emits such a reference — see this plan’s own Task 4 deviations).

Source§

impl StateEngine

Source

pub fn alloc_cell(&mut self, loc: SlotLocation) -> StateCellId

StateCellId allocation (ST-109): appends loc to a private Vec<SlotLocation> and returns its dense index as the new StateCellId, so StateCellId(i) and cells[i] are always in lockstep (no reuse/free in v1, matching the arena’s own bump-allocator discipline).

Source

pub fn commit_write( &mut self, cell: StateCellId, new: FourStateValue, hook: &mut dyn PreWriteHook, ) -> ChangeResult

The one write primitive (ST-109, ST-079, ST-082, ST-091, ST-155): looks up cell’s already-allocated SlotLocation and forwards to [commit_write_at] — see that function’s doc comment for the full algorithm. Task 12 (ST-075): on a real change, also makes exactly one call to Dispatch::notify_scheduler — the only call site notify_scheduler has anywhere in this crate — supplying self.vc’s own current_time/current_region (this plan’s header “how STATE learns now” decision; never a parameter this method’s own signature carries). Dispatch::notify_waveform_and_vpi is deliberately not called here: Waveform/VPI observe only through StateEngine::drain_committed_changes (state::gvt), reading the VcRecord [commit_write_at] already appended above. Scheduler wakeup is never GVT-gated — see Dispatch::notify_scheduler’s own doc comment for why.

Source

pub fn alloc_view_cell( &mut self, root: StateCellId, window: AliasWindow, ) -> StateCellId

The view cell of window of root (state::alias): a cell that owns no storage, whose value is that window of root’s. One cell per distinct (root, window). Never reset (root is).

Source

pub fn build_mode(&self) -> BuildMode

Task 13 (ST-101): read-only accessor so a test (or, later, RUNTIME) can confirm which mode a given StateEngine was constructed in. No set_build_mode method exists anywhere in this crate’s public API – BuildMode is fixed once at Arena::new (via StateEngine::new_with_mode, state::instance) and never mutated after, which is the structural enforcement of “no design or implementation makes 4-state and 2-state arenas coexist at runtime”.

Source§

impl StateEngine

Source

pub fn trireg_charge(&self, cell: StateCellId) -> Option<&TriregCharge>

Task 14 (ST-034): read-only lookup into this engine’s own trireg_charge side table.

Source

pub fn set_trireg_charge(&mut self, cell: StateCellId, charge: TriregCharge)

Task 14 (ST-034): records/replaces cell’s stored charge state. No decay-timing behavior happens here or anywhere in this crate this wave — see TriregCharge’s own doc comment.

Source§

impl StateEngine

Source

pub fn register_force_observed(&mut self, listener: Box<dyn ForceObserved>)

Registers listener to be notified (in registration order) on every future force/release (driver::force_driver/release_driver). No upper bound on how many listeners can be registered (unlike Dispatch’s fixed 3-role table, ST-074) — force/release are rare, external, out-of-band operations, not the per-event hot path ST-074’s no-heap-growth discipline targets.

Source§

impl StateEngine

Source

pub fn snapshot(&self, range: Option<(SlotClass, u32, u32)>) -> ArenaSnapshot

ST-098: takes a snapshot of range (None = the whole arena) — layout_hash is self.templates.signature_hash() at the moment of the call, guarding a later Self::restore against an incompatible StateEngine. Review fix (Task 16 r1, blocking finding 1): build-mode-aware — branches once on self.build_mode() and captures whichever plane-pair ({inline, wide_aval, wide_bval} for FourState, {inline_2s, wide_2s} for TwoState) is actually the engine’s live data at range’s word offsets (Task 13’s alloc_inline_words/alloc_wide_words grow both plane-pairs in lockstep at identical offsets, state::arena’s own module doc comment, so the offsets are shared — only which plane-pair holds the real values differs); the other plane-pair’s fields stay empty Vecs. The captured build_mode is stamped onto the returned snapshot so Self::restore can refuse a cross-mode restore.

§Panics

If range’s SlotClass is ForceWide — force_driver’s synthetic, caller-invisible contribution cells (see SlotClass::ForceWide’s own doc comment) are never part of a real elaborated signal’s storage and are not an intended target of this API (WOCB rollback, $save/ $restart, and VPI/debugger snapshotting all operate over ordinary Inline/Wide/Memory signals); a caller reaching this is a programming error at the call site, the same class of failure write_slot’s own shape-mismatch panic already documents elsewhere in this crate, not a normal case this primitive tolerates silently.

Source

pub fn restore(&mut self, snap: &ArenaSnapshot) -> Result<(), RewindError>

ST-098: restores snap’s data back into self’s arena at snap’s own recorded range (or the whole arena, for None) — a slice operation for Some(range): words outside that range are left completely untouched. Err(RewindError::LayoutMismatch) — checked before any write — when self’s currently-registered template set doesn’t hash the same as the one snap was taken from, when self’s build_mode doesn’t match snap’s own (review fix, Task 16 r1, blocking finding 1 — restoring a FourState snapshot into a TwoState engine, or vice versa, would otherwise silently target the wrong plane-pair entirely, the identical hazard this fix closes for snapshot/reset_to_initial), and when the target range’s own word count doesn’t match what snap actually holds (a same-hash-different-instance-count edge case the plan’s own four-field ArenaSnapshot shape doesn’t separately name, but which this same typed error class covers rather than panicking on a length-mismatched copy_from_slice); on any mismatch, self’s arena contents are provably unchanged — no partial corruption on the rejected path.

Source

pub fn reset_to_initial(&mut self)

ST-098 (documented alongside snapshot/restore as this same primitive family’s third bulk operation, per the plan’s own prose): re-blits every cell’s [CellReset] (state::instance’s cell_reset, populated by allocate_instance) over the arena — a declared initializer’s exact value for a slot that has one, all-X (4- state) or 0 (2-state) for an uninitialized non-net, non-force slot, and nothing at all for a net-backed slot (its reset state is the emergent Z driver::resolve_net_step folds to over zero active drivers, never a stored arena default) or a force_driver contribution cell (review fix, Task 16 r1, blocking finding 2 — never a real elaborated signal’s storage; see [CellReset::ForceContribution]’s own doc comment for why blitting over one would silently corrupt a live force). Bypasses commit_write/PreWriteHook/VcRecord entirely (a direct write_slot/write_inline_2s/write_wide_2s poke, the same tier of primitive Self::restore uses) — this establishes the simulation’s actual starting state before any process runs, not an ordinary mid-run write a scheduler/waveform/VPI listener should observe as a “change.” Review fix (Task 16 r1, blocking finding 1): build-mode-aware — branches once on self.build_mode() and re-blits whichever plane-pair is this engine’s actual live data (see this module’s top doc comment, deviation 5).

Source

pub fn reset_to_initial_with(&mut self, x_as_zero: bool)

Self::reset_to_initial, optionally starting every uninitialized 4-state variable and memory row at 0 instead of X (x_as_zero): the non-LRM --x-initial zero run option, for benches written against a 2-state simulator (Verilator’s --x-initial 0, VCS’s +vcs+initreg+0). Nets keep their LRM starting values.

Source§

impl StateEngine

Source

pub fn new_activation_record( &mut self, pc: u32, frame_word_count: u32, ) -> ActivationRecord

STATE’s one constructor for a real (non-hand-literal) process::ActivationRecord (ST-103, ST-110): allocates frame_word_count words from this engine’s own FrameArena and returns a fresh record whose pc/frame are both STATE-owned storage. pending_wait/fork_state start at their empty defaults (WaitSpecStorage::None/None) — populating them (via alloc_edge_terms/alloc_fork_children above) is the caller’s job once a suspend point or fork actually occurs, not this constructor’s.

Source§

impl StateEngine

Source

pub fn alloc_event_cell(&mut self) -> StateCellId

ST-111: allocates a fresh event-variable cell — a real, ordinary 1-bit SlotClass::Inline cell (via super::arena::Arena::alloc_inline_word, which zero-initializes its backing word, so the cell decodes to logic 0/“not triggered” immediately after allocation with no further write needed) — and records its StateCellId in self.event_cells, the one thing that distinguishes it from an ordinary 1-bit variable cell (see this module’s own doc comment for why no new SlotClass variant exists for this).

Source

pub fn trigger_event( &mut self, cell: StateCellId, hook: &mut dyn PreWriteHook, ) -> ChangeResult

ST-111: the trigger pulse — an ordinary StateEngine::commit_write (so the real VC/notify_change/Dispatch::notify_scheduler path fires, which is exactly what wakes a process sensitive to sched::EventTerm::NamedEventTrigger(cell) via sched::SensitivityTable’s already-landed “any real change wakes it” handling — see sched::sensitivity::SensitivityTable::install_term’s NamedEventTrigger arm). Writes the logical complement of the cell’s current value (an event cell’s own “value” carries no LRM meaning — only the transition matters — so flipping it, rather than always writing the same constant, guarantees commit_write’s own old-vs-new diff always reports a real change, even across repeated triggers with no intervening clear).

§Panics

In debug builds, if cell was never allocated via Self::alloc_event_cell — a caller triggering an ordinary (non-event) cell through this path is a programming error, not a silently-tolerated case, matching this crate’s “loud, never silent” discipline (ARCHITECTURE §7 item 9).

Source

pub fn clear_event_pulses(&mut self)

ST-111: LRM .triggered’s “valid only through the current time step” — clears every tracked event cell back to 0, via StateEngine::commit_write_no_hook (no VcRecord append, no scheduler notification on the clearing edge: clearing is not itself an LRM-observable event — see Self::commit_write_no_hook’s own doc comment for why that primitive is the one that skips both).

Source§

impl StateEngine

Source

pub fn advance_gvt(&mut self, gvt: SimTime)

ST-081: a pure setter (self.vc.set_gvt_watermark(gvt)) — no dependency on any scheduler-internal type, callable with a bare SimTime the caller (ENGINE, later) computed however it likes. See write::VcState’s gvt_watermark field doc comment for why this field exists even though drain_committed_changes below takes its own explicit up_to parameter rather than reading this field back.

Task 20 (ST-124) — open joint sizing risk with ENGINE, not bounded by this crate alone. Every un-drained speculative VC record between two advance_gvt calls stays buffered (write::VcState’s per-thread buffer, Task 8/9): this is only bounded in practice if the caller (ENGINE) advances GVT “cheaply and frequently enough” relative to the write rate. STATE has no way to observe or enforce ENGINE’s actual GVT-advancement cadence from inside this crate — a coarse-grained cadence (e.g. only at major time-step boundaries) could make the buffering cost far larger than assumed here. Tracked as an open risk (docs/design/state.md Risks/Open Questions), not silently assumed safe; restated in docs/CONTRACTS.md’s “Wave A — wA-state notes” section so ENGINE’s own wave can pick it up without re-deriving it from this doc comment.

Source

pub fn drain_committed_changes(&mut self, up_to: SimTime) -> Vec<VcRecord>

ST-078, ST-081: drains every registered worker thread’s speculative buffer (review fix — this plan’s header “cross-thread visibility” decision) of records with at <= up_to, merged by (at, registration index) — see write::VcState::drain_committed_changes’s own doc comment for the full removal/merge algorithm this forwards to. Waveform/VPI/$display-style consumers are meant to be the only callers of this method in a real simulation run.

See StateEngine::advance_gvt’s doc comment (ST-124) for the buffer-growth sizing risk this method’s drain rate must keep bounded.

Source

pub fn set_change_log(&mut self, on: bool)

Whether value changes are logged for Self::drain_committed_changes (on by default). A run whose observers read STATE directly and never drain must turn the log off, or it grows with every change of the run (ST-124: 70-130 GB on a few minutes of a real design).

Trait Implementations§

Source§

impl SignalAccess for StateEngine

Source§

fn read(&self, cell: StateCellId) -> FourStateValue

Source§

fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool

Returns true iff the write actually changed the stored value (the old-vs-new compare state.md §3 requires before any VC record is emitted).
Source§

fn read_row(&self, cell: StateCellId, row: u64) -> FourStateValue

One row of a memory cell (SlotClass::Memory). Only implemented by the real StateEngine; a test double that never stages a memory NBA never reaches this.
Source§

fn write_row( &mut self, cell: StateCellId, row: u64, value: FourStateValue, ) -> bool

Writes one row of a memory cell; true iff the row changed.
Source§

fn take_alias_changes(&mut self) -> Vec<AliasChange>

Changes of other names for storage the last write touched (a window alias of a ref port, state::alias), drained by the scheduler after each commit so it can notify their waiters. Empty for every engine that has no window alias.
Source§

fn read_wide_window( &self, cell: StateCellId, lo_word: usize, words: usize, ) -> (Vec<u64>, Vec<u64>)

The plane words [lo_word, lo_word + words) of a wide cell, as (aval, bval). The default reads the whole value; StateEngine reads just the window (a nonblocking write to a few bits of a huge vector must not cost O(N)).
Source§

fn write_wide_window( &mut self, cell: StateCellId, lo_word: usize, aval: &[u64], bval: &[u64], ) -> bool

Stores the plane words aval/bval at lo_word.. of a wide cell; true iff the value changed. The default patches the whole value; StateEngine stores just the window.
Source§

fn subscribe( &mut self, _cell: StateCellId, role: ListenerRole, sink: Box<dyn ValueChangeSink + Send>, )

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V