Skip to main content

sv_sim_sim/state/
mod.rs

1//! STATE (`sv-sim-sim::state`) — cross-track contract only: the Rust-owned 4-state
2//! arena's SoA layout query shape, its value-change fan-out primitive, and the one
3//! canonical signal read/write/subscribe interface (ARCHITECTURE §2: "Signal
4//! read/write/subscribe: STATE owns the one canonical primitive; VPI/FST/debugger
5//! are pure consumers"). The arena implementation itself belongs to the `state`
6//! track, not this contracts wave.
7//!
8//! Reconciliation: state.md calls the value-change listener trait `ChangeListener`;
9//! scheduler.md independently sketches the same contract under the name
10//! `ValueChangeSink`. Per the task's naming and because scheduler.md's signature is
11//! the more concrete of the two, this crate uses `ValueChangeSink` (ledger SC-071,
12//! RT-005) and does not separately define a `ChangeListener`.
13//!
14//! **Task 20 (ST-130) — dependency direction is an invariant, not an accident.**
15//! RUNTIME consumes STATE's reference read/write API
16//! ([`SignalAccess`]/[`ValueChangeSink`]) and its `Bit`/`OwnedSVInt`-adjacent
17//! [`FourStateValue`] type, never the reverse: FST/VPI/`$display` are consumers,
18//! not producers, of these interfaces, and no code anywhere in `sv-sim-sim::state`
19//! calls into `sv-sim-runtime`'s FST/VPI/`$display` formatting. This crate's own
20//! `Cargo.toml` (`crates/sv-sim-sim/Cargo.toml`) lists no `sv-sim-runtime`
21//! dependency, which is the mechanically-checkable half of this invariant.
22//!
23//! This is not a hypothetical edge: `sv-sim-runtime`'s `Cargo.toml` already
24//! depends on `sv-sim-sim` (since the very first crate-skeleton commit), and
25//! `sv-sim-runtime::signal_consumer::RuntimeSignalConsumer` already consumes
26//! [`SignalAccess`] directly. Both halves of the invariant — the real, present
27//! edge in one direction and the absent edge in the other — are guarded by an
28//! automated test: `crates/sv-sim-sim/tests/dependency_direction.rs`. Confirm
29//! live with `cargo tree -p sv-sim-sim -i sv-sim-runtime` (expected to report no
30//! path) or `cargo test -p sv-sim-sim --test dependency_direction`; see
31//! `docs/CONTRACTS.md`'s "Wave A — wA-state notes" section for the fuller
32//! writeup, including for RUNTIME's own wave to keep honoring as it grows more
33//! dependencies.
34
35use sv_sim_abi::{Bit4, Region, SimTime};
36
37// sv-lang value conversions, the layout builder and template dedup walk the
38// frontend `Design`, so they exist only with this crate's `frontend` feature.
39#[cfg(feature = "frontend")]
40pub(crate) mod value;
41#[cfg(feature = "frontend")]
42pub use value::*;
43
44mod arena;
45pub use arena::*;
46
47#[cfg(feature = "frontend")]
48pub mod storage;
49
50#[cfg(feature = "frontend")]
51pub mod event_pool;
52
53#[cfg(feature = "frontend")]
54mod layout;
55#[cfg(feature = "frontend")]
56pub use layout::*;
57
58#[cfg(feature = "frontend")]
59mod dedup;
60#[cfg(feature = "frontend")]
61pub use dedup::*;
62
63mod instance;
64pub use instance::*;
65
66mod write;
67
68mod huge_mirror;
69pub use huge_mirror::{
70    HugeCommit, NbaWindow, huge_commit, huge_commit_full_done, huge_mark, huge_nba_window,
71    huge_prime,
72};
73
74pub mod force;
75pub use write::*;
76
77mod driver;
78pub use driver::*;
79
80pub use crate::driver_counts::*;
81
82mod region_check;
83pub use region_check::*;
84
85mod accessor_impl;
86pub use accessor_impl::*;
87
88mod alias;
89pub use alias::{AliasChange, AliasWindow, splice_window, window_value};
90
91mod dispatch;
92pub use dispatch::*;
93
94mod rollback;
95pub use rollback::*;
96
97mod snapshot;
98pub use snapshot::*;
99
100mod frame;
101pub use frame::*;
102
103mod introspect;
104pub use introspect::*;
105
106mod kernel_ctx;
107pub use kernel_ctx::*;
108
109mod event;
110// `event`'s only top-level item is `EVENT_CELL_WIDTH`; R-run Task 16's
111// `alloc_event_cell`/`trigger_event`/`clear_event_pulses` are `impl
112// StateEngine` methods on the already-`pub use`d `instance::StateEngine`.
113pub use event::*;
114
115mod gvt;
116// `gvt` adds no new `pub` top-level items of its own -- Task 12's
117// `advance_gvt`/`drain_committed_changes` are `impl StateEngine` methods on
118// the already-`pub use`d `instance::StateEngine` type, so this re-export
119// currently has nothing to bring in; kept for symmetry with every other
120// submodule here and so a future `pub` item added to `gvt.rs` is exported
121// automatically.
122#[allow(unused_imports)]
123pub use gvt::*;
124
125/// Opaque per-signal handle, STATE-assigned (scheduler.md: "STATE-owned opaque
126/// handle"; ledger ST-109, ST-111, SC-037). Distinct from [`sv_sim_abi::SlotId`]:
127/// `SlotId` is the ABI-facing, compile-time-constant index CODEGEN bakes into IR;
128/// `StateCellId` is the in-process (non-FFI) handle the scheduler/determinism/
129/// runtime Rust code passes around at simulation time. STATE's own implementation
130/// is free to make them numerically related, but the two names stay distinct
131/// contract types because their callers (CODEGEN vs. the scheduler) are on
132/// opposite sides of the ABI boundary.
133#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
134pub struct StateCellId(pub u64);
135
136/// The storage tiers state.md §1 splits every slot into (ledger ST-020..ST-024,
137/// CG-107, CG-163).
138///
139/// **Review fix (Task 14 r2, blocking finding 2) adds `ForceWide`:** the
140/// Wide/Memory-class counterpart of `driver::force_driver`'s dynamically
141/// -allocated contribution cell, addressing `Arena`'s separate, never-sealed
142/// `force_wide_aval`/`force_wide_bval` planes (`state::arena`'s
143/// `alloc_force_wide_words` doc comment has the full argument) instead of the
144/// ordinary `wide_aval`/`wide_bval` planes `sv_get_wide` can seal. No
145/// `SlotDescriptor`/`SlotOffset` the layout builder (Task 4/6) ever produces
146/// for a real elaborated signal is `ForceWide` — it is minted exclusively by
147/// `force_driver` for its own synthetic, caller-invisible contribution cell,
148/// never observed by CODEGEN/the accessor ABI at all.
149#[derive(Clone, Copy, PartialEq, Eq, Debug)]
150pub enum SlotClass {
151    Inline,
152    Wide,
153    Memory,
154    ForceWide,
155}
156
157/// Compile-time layout query result STATE hands to CODEGEN (state.md §3: "baked
158/// into compiled kernels as LLVM constants"; ledger ST-027, ST-037, ST-129,
159/// ST-134, ST-135). Pure Rust (not `#[repr(C)]`) — this crosses the CODEGEN/STATE
160/// boundary as ordinary Rust values at *compile* time, not as bytes across the
161/// compiled-kernel ABI, so it has no FFI-layout obligation.
162/// Task 7 adds `PartialEq` (previously `Clone, Copy, Debug` only): `slot_of`
163/// (ST-129) is required to return a byte-identical `SlotOffset` across
164/// repeated calls regardless of intervening `allocate_instance` calls, and
165/// the direct field-by-field way to prove that in a test is a structural
166/// equality check, not a `Debug`-string comparison. Every field is itself
167/// `PartialEq` (`SlotClass` already derives it; the rest are plain integers),
168/// so this is a safe, non-lossy derive addition.
169///
170/// **Task 10 (ST-027):** state.md's original state-pointer ABI design — CODEGEN
171/// inlining a raw compile-time `base + word_offset` pointer directly, bypassing
172/// the accessor calls above — is **superseded** by ARCHITECTURE §7 item 1's
173/// accessor-ABI (ST-026). This struct's fields stay `pub` and readable (CODEGEN
174/// needs them to size/typecheck a slot at compile time — width, class, element
175/// count), but no function anywhere in `sv-sim-sim::state` hands a caller a raw
176/// `*mut`/`*const` pointer it could add one of these fields to itself: the
177/// actual address computation (`InstanceBase` + `SlotOffset`) happens only
178/// inside `accessor_impl::slot_location_for`/`write::commit_write_at`, never
179/// re-derived by a caller from these fields against a raw base pointer it also
180/// holds.
181#[derive(Clone, Copy, PartialEq, Debug)]
182pub struct SlotOffset {
183    pub class: SlotClass,
184    pub word_offset: u32,
185    pub bit_width: u32,
186    pub elem_word_count: u32,
187    pub row_stride_words: u32,
188    pub num_rows: u32,
189}
190
191/// Mirrors `sv_lang::EdgeKind`'s four variants (ledger SC-045, SC-067, SC-071,
192/// NL-047, NL-083). Kept as this crate's own copy (rather than a re-export)
193/// because `sv-sim-sim`'s cross-track contract layer does not depend on `sv-lang`
194/// in this wave (see `docs/CONTRACTS.md` open questions); the `netlist` track owns
195/// the real conversion once it wires the `sv-lang` dependency for its own
196/// elaboration work.
197#[derive(Clone, Copy, PartialEq, Eq, Debug)]
198pub enum EdgeKind {
199    None,
200    Posedge,
201    Negedge,
202    BothEdges,
203}
204
205/// Lives here, not in the frontend-only `value` module (CONTRACTS D4): the
206/// driver resolver needs it with or without the `frontend` feature.
207///
208/// Reads bit `i` of a `FourStateValue` back out as a `Bit4` — the inverse
209/// per-bit step of [`bits_to_four_state`], used by
210/// [`four_state_to_owned_svint`] to thread known/unknown bits into an
211/// `sv_lang::OwnedSVInt` one bit (or run) at a time.
212///
213/// `pub(crate)` (Task 14 deviation, not in that task's own Files list):
214/// `state::driver::resolve_net_step`'s bit-by-bit fold needs this exact same
215/// per-bit extraction (ST-148's "one table" discipline — reusing this
216/// function, not a second hand-rolled copy of `Bit4::from_val_unk`/
217/// `from_aval_bval`'s polarity). Widening from module-private to
218/// `pub(crate)` only exposes it to this crate's own `state` submodules,
219/// never outside `sv-sim-sim`.
220pub(crate) fn four_state_bit_at(v: &FourStateValue, i: u32) -> Bit4 {
221    match v {
222        FourStateValue::Narrow(word) => {
223            let val = *word as u64;
224            let unk = (*word >> 64) as u64;
225            Bit4::from_val_unk((val >> i) & 1 != 0, (unk >> i) & 1 != 0)
226        }
227        FourStateValue::Wide { aval, bval } => {
228            let word = (i / 64) as usize;
229            let bit = i % 64;
230            let a = aval.get(word).copied().unwrap_or(0);
231            let b = bval.get(word).copied().unwrap_or(0);
232            Bit4::from_aval_bval((a >> bit) & 1 != 0, (b >> bit) & 1 != 0)
233        }
234    }
235}
236
237/// STATE's cross-subsystem four-state value representation (state.md §3/§5,
238/// ledger ST-008, ST-009, ST-099, ST-120): `Narrow` mirrors the accessor ABI's
239/// FIXED inline `u128` word (ARCHITECTURE §6.2 / ST-008: `val` in the low 64
240/// bits, `unk`/xzmask in the high 64 bits, regardless of `bit_width` —
241/// [`sv_sim_abi::bit4`]-compatible; **not** `sv-sim-codegen::value`'s internal,
242/// width-tight `i(2N)` LLVM packing, which uses a different bit offset for any
243/// width < 64 — see `sv_sim_abi::accessor`'s doc comment); `Wide` mirrors the
244/// `aval`/`bval` word planes ([`sv_sim_abi::WideView`]-compatible).
245/// Reference/introspection shape only (VPI/`$display`/debugger/harness) —
246/// never the codegen-emitted hot path, which calls the accessor ABI directly.
247#[derive(Clone, Debug, PartialEq)]
248pub enum FourStateValue {
249    Narrow(u128),
250    Wide { aval: Vec<u64>, bval: Vec<u64> },
251}
252
253impl FourStateValue {
254    /// The value as `(aval, bval)` plane words (`aval = val ^ unk`, `bval =
255    /// unk`); a narrow value is one word.
256    pub fn planes(&self) -> (Vec<u64>, Vec<u64>) {
257        match self {
258            FourStateValue::Narrow(w) => {
259                let (val, unk) = (*w as u64, (*w >> 64) as u64);
260                let (a, b) = sv_sim_abi::bit4::narrow_word_to_wide(val, unk);
261                (vec![a], vec![b])
262            }
263            FourStateValue::Wide { aval, bval } => (aval.clone(), bval.clone()),
264        }
265    }
266}
267
268/// The fixed, small listener-role table state.md §5 dispatches value changes to.
269#[derive(Clone, Copy, PartialEq, Eq, Debug)]
270pub enum ListenerRole {
271    Scheduler,
272    Waveform,
273    Vpi,
274}
275
276/// Called by STATE/NETS after any committed write or net resolution
277/// (scheduler.md §4: "the single, uniform fan-out point for variable writes AND
278/// net resolution"; ledger SC-071, RT-005).
279pub trait ValueChangeSink {
280    fn notify_change(&mut self, cell: StateCellId, edge: EdgeKind, at: SimTime, region: Region);
281
282    /// Whether the sink would use a [`ChangeDelta`] for `cell` (a process
283    /// waits on a bit/part select of it, LRM 9.4.2): a caller builds the delta
284    /// only when this is true, so a write to an unwatched signal stays free of
285    /// the per-write copy.
286    fn wants_delta(&self, cell: StateCellId) -> bool {
287        let _ = cell;
288        false
289    }
290
291    /// The old and new plane words of the update the NEXT
292    /// [`Self::notify_change`] on `cell` reports. Offered only when
293    /// [`Self::wants_delta`] said so.
294    fn note_delta(&mut self, cell: StateCellId, delta: ChangeDelta) {
295        let _ = (cell, delta);
296    }
297}
298
299/// The bits one committed update changed, as plane words (`aval`/`bval`, the
300/// `Wide` encoding): `lo_word` is the plane-word index of `old_a[0]`, so a
301/// windowed update of a huge vector carries only the words it touched. A
302/// bit/part-select event control (LRM 9.4.2) compares the selected bits of
303/// `old` and `new` to decide whether *its* operand changed, and takes an edge
304/// on the select's least-significant bit.
305#[derive(Clone, Debug, Default)]
306pub struct ChangeDelta {
307    pub lo_word: usize,
308    pub old_a: Vec<u64>,
309    pub old_b: Vec<u64>,
310    pub new_a: Vec<u64>,
311    pub new_b: Vec<u64>,
312}
313
314impl ChangeDelta {
315    /// The delta between two whole values (the narrow inline word and the
316    /// wide planes both decode to `aval`/`bval`).
317    pub fn between(old: &FourStateValue, new: &FourStateValue) -> Self {
318        let (old_a, old_b) = old.planes();
319        let (new_a, new_b) = new.planes();
320        Self {
321            lo_word: 0,
322            old_a,
323            old_b,
324            new_a,
325            new_b,
326        }
327    }
328
329    fn word(plane: &[u64], lo_word: usize, word: usize) -> Option<u64> {
330        word.checked_sub(lo_word)
331            .and_then(|i| plane.get(i))
332            .copied()
333    }
334
335    /// Bit `index` of the value before and after, as `(aval, bval)` pairs; a
336    /// bit outside the carried words did not change (`None`).
337    pub fn bit(&self, index: u32) -> Option<((bool, bool), (bool, bool))> {
338        let (w, b) = ((index / 64) as usize, index % 64);
339        let pick = |p: &[u64]| Self::word(p, self.lo_word, w).map(|x| (x >> b) & 1 != 0);
340        Some((
341            (pick(&self.old_a)?, pick(&self.old_b)?),
342            (pick(&self.new_a)?, pick(&self.new_b)?),
343        ))
344    }
345
346    /// Whether any bit of `[lsb, lsb + width)` differs, in either plane.
347    pub fn range_changed(&self, lsb: u32, width: u32) -> bool {
348        let (lo, hi) = (lsb as usize, lsb as usize + width as usize);
349        let first = lo / 64;
350        let last = hi.saturating_sub(1) / 64;
351        for w in first..=last {
352            let (Some(oa), Some(ob), Some(na), Some(nb)) = (
353                Self::word(&self.old_a, self.lo_word, w),
354                Self::word(&self.old_b, self.lo_word, w),
355                Self::word(&self.new_a, self.lo_word, w),
356                Self::word(&self.new_b, self.lo_word, w),
357            ) else {
358                continue;
359            };
360            let lo_bit = lo.saturating_sub(w * 64);
361            let hi_bit = (hi - w * 64).min(64);
362            let mask = if hi_bit - lo_bit >= 64 {
363                u64::MAX
364            } else {
365                ((1u64 << (hi_bit - lo_bit)) - 1) << lo_bit
366            };
367            if ((oa ^ na) | (ob ^ nb)) & mask != 0 {
368                return true;
369            }
370        }
371        false
372    }
373}
374
375/// STATE owns the one canonical signal read/write/subscribe primitive
376/// (ARCHITECTURE §2); RUNTIME's VPI/FST/`$display`/debugger hosts are pure
377/// consumers of this trait, never a second implementation of signal access
378/// (`sv-sim-runtime::signal_consumer` demonstrates this by depending on it).
379pub trait SignalAccess {
380    fn read(&self, cell: StateCellId) -> FourStateValue;
381    /// Returns `true` iff the write actually changed the stored value (the
382    /// old-vs-new compare state.md §3 requires before any VC record is emitted).
383    fn write(&mut self, cell: StateCellId, value: FourStateValue) -> bool;
384    /// One row of a memory cell (`SlotClass::Memory`). Only implemented by
385    /// the real `StateEngine`; a test double that never stages a memory NBA
386    /// never reaches this.
387    fn read_row(&self, cell: StateCellId, row: u64) -> FourStateValue {
388        let _ = row;
389        unreachable!("read_row({cell:?}) on a SignalAccess without memory rows")
390    }
391    /// Writes one row of a memory cell; `true` iff the row changed.
392    fn write_row(&mut self, cell: StateCellId, row: u64, value: FourStateValue) -> bool {
393        let _ = (row, value);
394        unreachable!("write_row({cell:?}) on a SignalAccess without memory rows")
395    }
396    /// The plane words `[lo_word, lo_word + words)` of a wide cell, as
397    /// `(aval, bval)`. The default reads the whole value; `StateEngine`
398    /// reads just the window (a nonblocking write to a few bits of a huge
399    /// vector must not cost `O(N)`).
400    fn read_wide_window(
401        &self,
402        cell: StateCellId,
403        lo_word: usize,
404        words: usize,
405    ) -> (Vec<u64>, Vec<u64>) {
406        match self.read(cell) {
407            FourStateValue::Wide { aval, bval } => (
408                aval[lo_word..lo_word + words].to_vec(),
409                bval[lo_word..lo_word + words].to_vec(),
410            ),
411            FourStateValue::Narrow(_) => {
412                unreachable!("read_wide_window({cell:?}) on a narrow cell")
413            }
414        }
415    }
416    /// Stores the plane words `aval`/`bval` at `lo_word..` of a wide cell;
417    /// `true` iff the value changed. The default patches the whole value;
418    /// `StateEngine` stores just the window.
419    fn write_wide_window(
420        &mut self,
421        cell: StateCellId,
422        lo_word: usize,
423        aval: &[u64],
424        bval: &[u64],
425    ) -> bool {
426        write_wide_window_whole(self, cell, lo_word, aval, bval)
427    }
428    fn subscribe(
429        &mut self,
430        cell: StateCellId,
431        role: ListenerRole,
432        sink: Box<dyn ValueChangeSink + Send>,
433    );
434    /// Changes of other names for storage the last write touched (a window
435    /// alias of a ref port, `state::alias`), drained by the scheduler after
436    /// each commit so it can notify their waiters. Empty for every engine
437    /// that has no window alias.
438    fn take_alias_changes(&mut self) -> Vec<AliasChange> {
439        Vec::new()
440    }
441}
442
443/// [`SignalAccess::write_wide_window`] through the whole-value `write`: the
444/// default, and what `StateEngine` falls back to for a forced cell or one
445/// whose changes are recorded.
446pub(crate) fn write_wide_window_whole<A: SignalAccess + ?Sized>(
447    acc: &mut A,
448    cell: StateCellId,
449    lo_word: usize,
450    aval: &[u64],
451    bval: &[u64],
452) -> bool {
453    let FourStateValue::Wide {
454        aval: mut a,
455        bval: mut b,
456    } = acc.read(cell)
457    else {
458        unreachable!("write_wide_window({cell:?}) on a narrow cell")
459    };
460    a[lo_word..lo_word + aval.len()].copy_from_slice(aval);
461    b[lo_word..lo_word + bval.len()].copy_from_slice(bval);
462    acc.write(cell, FourStateValue::Wide { aval: a, bval: b })
463}
464
465/// Compile-time layout for one unique `(module,params)` template (state.md Key
466/// Data Structures; ledger ST-140). One `StateLayout` per distinct
467/// [`crate::netlist::TemplateKey`] (`Task 5`'s `TemplateTable` maps keys to
468/// these); `inline_words`/`wide_words` are the template-relative word counts
469/// [`Arena`] bulk-allocators (Task 7) reserve per instance.
470#[derive(Clone, Debug, PartialEq)]
471pub struct StateLayout {
472    pub slots: Vec<SlotDescriptor>,
473    pub inline_words: u32,
474    pub wide_words: u32,
475}
476
477/// One signal's slot within a [`StateLayout`] (state.md Key Data Structures;
478/// ledger ST-141).
479///
480/// **Reconciliation (ST-153):** state.md's `{key, offset, initial, net_info}`
481/// (adopted here) vs. runtime.md's independently drafted `{name, byte_offset,
482/// bit_width, is_4state, kind}` are reconciled by *composition, not
483/// duplication*, not by picking one draft wholesale:
484/// - `name` is available by composing `key` with
485///   [`crate::netlist::NetlistView::hierarchical_path`] — never stored a
486///   second time here, since a second copy could drift from the netlist
487///   track's own name table.
488/// - `byte_offset`/`bit_width` are `offset.word_offset`/`offset.bit_width`
489///   (already present on [`SlotOffset`] — ST-144 — no renaming needed).
490/// - `is_4state` is dropped, not reconciled 1:1: 4-state-ness is a
491///   whole-arena [`BuildMode`] (Task 2/10), not a per-slot property, so
492///   runtime.md's per-slot field would be redundant with the arena's own
493///   mode.
494/// - `kind` is `net_info`'s presence/absence (`Some` => net, `None` =>
495///   variable/port) plus [`NetLayout::kind`] when present.
496///
497/// This is the one, documented `SlotDescriptor` shape this plan's every later
498/// task consumes identically, and the one CODEGEN/RUNTIME are expected to
499/// consume once their own waves start.
500///
501/// `initial` is STATE's own sv-lang-free [`FourStateValue`], not
502/// `sv_lang::OwnedSVInt`: this type is consumed by `sv-sim-runtime`, which must
503/// never reach the sv-lang family (ARCHITECTURE §3). The frontend-side layout
504/// builder converts constants with `four_state_of_constant` before storing them.
505#[derive(Clone, Debug, PartialEq)]
506pub struct SlotDescriptor {
507    pub key: crate::netlist::SymbolKey,
508    pub offset: SlotOffset,
509    pub initial: Option<FourStateValue>,
510    pub net_info: Option<NetLayout>,
511}
512
513/// A net member's electrical classification, when known (ledger ST-141).
514///
515/// **Deferred conversion (see Task 3/6 of the wave-A `state` plan and the
516/// plan's Deferred section):** verified against the pinned `sv-lang=0.1.9` C
517/// API (`source/capi/CApiAst.cpp`) — while `slang::ast::NetSymbol` has a
518/// `netType` field and slang's own Python bindings expose it, `CApiAst.cpp`
519/// exposes no `slang_symbol_net_type`/equivalent accessor for it, so no
520/// function in the pinned `sv-lang` Rust crate can recover a declared `Net`
521/// symbol's real `NetKind` this wave. `SlotDescriptor::net_info` therefore
522/// stays structurally correct (an `Option`, `None` for every real member this
523/// wave's layout builder produces, `Net`-kind included) but is never `Some`
524/// for a *real* elaborated net in this plan; every `Some(NetLayout{..})` this
525/// plan's own tests construct (Task 14/15) is hand-built, exercising the
526/// multi-driver resolution *logic*, not a connection to real elaboration.
527#[derive(Clone, Copy, PartialEq, Eq, Debug)]
528pub struct NetLayout {
529    pub kind: NetKind,
530    pub shape: NetShape,
531}
532
533/// The multi-driver resolution shape a net's storage takes (ledger ST-141):
534/// how many distinct strength-bearing drivers [`NetLayout`]'s owning net has
535/// and whether the full K+1 strength lattice must be evaluated to resolve it.
536#[derive(Clone, Copy, PartialEq, Eq, Debug)]
537pub enum NetShape {
538    AliasedSingleDriver,
539    MultiDriverUniformStrength,
540    FullStrengthLattice,
541}
542
543/// Mirrors `sv_lang::NetKind`'s variant set minus `Interconnect` (state.md §4:
544/// an interconnect net never gets a slot at all, so it has no `NetLayout`
545/// value to represent) and minus the `Unknown` error placeholder (never a
546/// real declared net's kind). Kept as this crate's own copy rather than a
547/// re-export of `sv_lang::NetKind` so this module's exhaustive-match
548/// discipline (ARCHITECTURE §7 item 9) applies to a type this crate owns,
549/// independent of `sv_lang`'s own variant set ever changing underneath it.
550#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
551pub enum NetKind {
552    Wire,
553    WAnd,
554    WOr,
555    Tri,
556    TriAnd,
557    TriOr,
558    Tri0,
559    Tri1,
560    TriReg,
561    Supply0,
562    Supply1,
563    UWire,
564    UserDefined,
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use sv_sim_abi::Bit4;
571
572    #[test]
573    fn four_state_narrow_round_trips_via_abi_bit4() {
574        // Exercise the ONE documented sv_sim_abi::bit4 conversion through STATE's
575        // own FourStateValue::Narrow representation, proving the two crates agree
576        // on the packed-word shape: the FIXED accessor-ABI offset (unk at bit
577        // 64), not codegen's width-tight `<< width` packing (review fix -- the
578        // previous version of this test hardcoded `<< 1`, which only "worked"
579        // because it never exercised a width other than 1, where the two
580        // conventions alias).
581        for b in [Bit4::Zero, Bit4::One, Bit4::X, Bit4::Z] {
582            let (val, unk) = b.to_val_unk();
583            let word: u128 = (val as u128) | ((unk as u128) << 64);
584            let fv = FourStateValue::Narrow(word);
585            match fv {
586                FourStateValue::Narrow(w) => assert_eq!(w, word),
587                FourStateValue::Wide { .. } => panic!("expected Narrow"),
588            }
589        }
590    }
591
592    /// Review fix: a width other than 1 that actually distinguishes the FIXED
593    /// accessor layout from codegen's width-tight `i(2N)` packing.
594    #[test]
595    fn four_state_narrow_at_width_8_uses_fixed_64_offset_not_width_offset() {
596        let width: u32 = 8;
597        let val: u64 = 0b1011_0010;
598        let unk: u64 = 0b0000_1000;
599
600        let fixed_word: u128 = (val as u128) | ((unk as u128) << 64);
601        let width_tight_word: u128 = (val as u128) | ((unk as u128) << width);
602        assert_ne!(fixed_word, width_tight_word);
603
604        let fv = FourStateValue::Narrow(fixed_word);
605        match fv {
606            FourStateValue::Narrow(w) => {
607                assert_eq!((w >> 64) as u64, unk);
608                assert_eq!((w & u128::from(u64::MAX)) as u64, val);
609            }
610            FourStateValue::Wide { .. } => panic!("expected Narrow"),
611        }
612    }
613
614    /// ST-140: a compile-level proof that `StateLayout` has exactly the three
615    /// contracted fields, in the contracted names/order — this line fails to
616    /// compile (not merely fails an assertion) if a field were added, removed,
617    /// or renamed.
618    #[test]
619    fn state_layout_has_exactly_the_three_contracted_fields() {
620        let layout = StateLayout {
621            slots: vec![],
622            inline_words: 0,
623            wide_words: 0,
624        };
625        assert!(layout.slots.is_empty());
626        assert_eq!(layout.inline_words, 0);
627        assert_eq!(layout.wide_words, 0);
628    }
629
630    /// ST-141: a literal contract example for Task 4's real layout builder to
631    /// test against later (that test asserts the *builder* produces `None` for
632    /// a `logic` variable, not merely that the type allows it).
633    #[test]
634    fn slot_descriptor_net_info_is_none_for_a_non_net_signal() {
635        let descriptor = SlotDescriptor {
636            key: crate::netlist::SymbolKey(0),
637            offset: SlotOffset {
638                class: SlotClass::Inline,
639                word_offset: 0,
640                bit_width: 1,
641                elem_word_count: 0,
642                row_stride_words: 0,
643                num_rows: 0,
644            },
645            initial: None,
646            net_info: None,
647        };
648        assert!(descriptor.net_info.is_none());
649    }
650
651    /// ST-141/ST-153: an exhaustive `match` over `NetShape`'s 3 variants and
652    /// `NetKind`'s 13 variants, with no wildcard arm — ARCHITECTURE §7 item 9's
653    /// discipline applied to this crate's own enums, not just `sv_lang`'s.
654    /// Adding or removing a variant from either enum breaks this file at
655    /// compile time, which is the actual guard this test provides.
656    #[test]
657    fn net_shape_and_net_kind_cover_the_documented_variant_sets() {
658        fn describe_shape(shape: NetShape) -> &'static str {
659            match shape {
660                NetShape::AliasedSingleDriver => "aliased-single-driver",
661                NetShape::MultiDriverUniformStrength => "multi-driver-uniform-strength",
662                NetShape::FullStrengthLattice => "full-strength-lattice",
663            }
664        }
665
666        fn describe_kind(kind: NetKind) -> &'static str {
667            match kind {
668                NetKind::Wire => "wire",
669                NetKind::WAnd => "wand",
670                NetKind::WOr => "wor",
671                NetKind::Tri => "tri",
672                NetKind::TriAnd => "triand",
673                NetKind::TriOr => "trior",
674                NetKind::Tri0 => "tri0",
675                NetKind::Tri1 => "tri1",
676                NetKind::TriReg => "trireg",
677                NetKind::Supply0 => "supply0",
678                NetKind::Supply1 => "supply1",
679                NetKind::UWire => "uwire",
680                NetKind::UserDefined => "user-defined",
681            }
682        }
683
684        for shape in [
685            NetShape::AliasedSingleDriver,
686            NetShape::MultiDriverUniformStrength,
687            NetShape::FullStrengthLattice,
688        ] {
689            assert!(!describe_shape(shape).is_empty());
690        }
691
692        for kind in [
693            NetKind::Wire,
694            NetKind::WAnd,
695            NetKind::WOr,
696            NetKind::Tri,
697            NetKind::TriAnd,
698            NetKind::TriOr,
699            NetKind::Tri0,
700            NetKind::Tri1,
701            NetKind::TriReg,
702            NetKind::Supply0,
703            NetKind::Supply1,
704            NetKind::UWire,
705            NetKind::UserDefined,
706        ] {
707            assert!(!describe_kind(kind).is_empty());
708        }
709
710        let layout = NetLayout {
711            kind: NetKind::Tri,
712            shape: NetShape::FullStrengthLattice,
713        };
714        assert_eq!(layout.kind, NetKind::Tri);
715        assert_eq!(layout.shape, NetShape::FullStrengthLattice);
716    }
717}