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}