Skip to main content

vj_sim/state/
mod.rs

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