vj_sim/state/layout.rs
1//! Task 4 — layout-builder entry point, API surface discipline, member
2//! filtering, exclusions (ledger ST-058, ST-060, ST-072, ST-125, ST-126).
3//!
4//! `build_template_layout` is the sole frontend entry point STATE's layout
5//! builder exposes this wave (ST-125): it takes a `Symbol` already obtained
6//! from `Compilation::compile() -> Design` by the caller (Task 5 wires the
7//! `top_instances()` walk that calls this once per distinct template) and
8//! never itself touches `Compilation`/`Session`. Its top-level per-member
9//! walk calls `members`/`kind`/`value_type`/`initializer` on
10//! `sv_lang::Symbol` (ST-126's hierarchy-walking API surface), plus
11//! `port_type()`, `port_internal_symbol()`, and `port_is_null_port()` for
12//! `Port`-kind members specifically (a deviation from the plan's literal
13//! call list -- see the doc comment on `ty` below: a real ANSI `Port`
14//! symbol's `value_type()` is `None`, so `value_type()` alone cannot
15//! satisfy ST-060's "produces slots ... for the ... port" requirement),
16//! filtering to `Variable`/`Net`/`Port` member kinds (ST-060); every other
17//! kind (`Genvar`, `Parameter`, `TypeParameter`, `Subroutine`, ...) is
18//! excluded by that filter alone, with no separate exclusion code path
19//! (ST-072). Two further, narrower exclusions apply only within the
20//! `Port`-kind slice of that filter, both because a value the filter would
21//! otherwise accept carries no independent storage of its own: a port with
22//! an internal symbol (already slotted on its own pass through this same
23//! loop -- see the doc comment above the first `continue` below), and a
24//! null port (an empty, dot-less port-list position -- ST-072's "not a
25//! signal" reasoning applied to a `Port`-kind member that itself has
26//! nothing to store; see the doc comment above the second `continue`
27//! below). `SymbolKind` is `#[non_exhaustive]` in the pinned `sv-lang`, so
28//! a filter (never a from-scratch exhaustive match) is the only way this
29//! crate could even attempt to enumerate it.
30//!
31//! Per-member shape derivation beyond a single inline word sized by
32//! `Type::bit_width()` (packed-struct flattening is already implied by
33//! `bit_width()` returning the flattened width, but memory/wide routing,
34//! dynamic-array rejection, sign-aware width proofs, and initial-value
35//! threading) is Task 6's job, which edits this same function body in place
36//! -- this task's own implementation is a deliberate stub: every slot it
37//! emits is `SlotClass::Inline`, `initial: None`, `net_info: None`.
38
39use super::arena::{Arena, BuildMode};
40use super::{
41 FourStateValue, NetLayout, SlotClass, SlotDescriptor, SlotOffset, StateLayout,
42 four_state_of_constant, four_state_to_owned_svint, owned_svint_to_four_state,
43};
44use crate::netlist::SymbolKey;
45use crate::symid::SymbolUid;
46use sv_lang::kinds::SymbolKind;
47use sv_lang::nodes::{
48 CheckerDeclarationSyntax, ClockingDeclarationSyntax, DefaultClockingReferenceSyntax,
49 DefaultDisableDeclarationSyntax, GenerateBlockSyntax, MemberSyntax, ModuleDeclarationSyntax,
50};
51use sv_lang::{AstNode, Node, Symbol, SyntaxTree};
52
53/// The loud, typed error this and every later layout task returns instead of
54/// silently skipping a member kind this plan claims to handle (ARCHITECTURE
55/// §7 item 9 applied to `SymbolKind`).
56///
57/// **Deviation from the plan's literal `sym.` call-site restriction:** the
58/// plan's Task 4 prose says `build_template_layout` touches no `Symbol`
59/// method outside `{members, kind, value_type, initializer}`, but this
60/// variant's own field is documented as "a hierarchical-ish name for
61/// diagnosis", and ST-126's own ledger text lists `Symbol::name()` as part
62/// of STATE's allowed hierarchy-walking API surface. `name()` is called only
63/// on this (in practice unreachable this wave, since a real `Variable`/
64/// `Net`/`Port` symbol always yields a `value_type()`) diagnostic path, never
65/// on the per-member happy path.
66#[derive(Debug)]
67pub enum LayoutError {
68 /// A member this builder's kind-filter *does* accept (`Variable`/`Net`/
69 /// `Port`) but that itself failed to yield a queryable value -- e.g.
70 /// `Symbol::value_type()` returning `None` for a symbol the filter
71 /// otherwise believed was a value. Carries the offending kind and a
72 /// hierarchical-ish name for diagnosis. Never constructed for a kind the
73 /// filter excludes on purpose (ST-072's exclusion is not an error).
74 UnsupportedMemberKind(SymbolKind, String),
75 /// Task 6: a member whose `value_type()`/`port_type()` shape this builder
76 /// cannot yet lay out -- a dynamic array, queue, or associative array
77 /// (rejected by the explicit `Type::is_dynamically_sized_array()`
78 /// predicate check, never inferred from another predicate being false);
79 /// a class handle, `string`, `chandle`, `event`, virtual interface, a
80 /// multi-dimensional fixed unpacked array (whose per-row element is
81 /// itself another unpacked array), or a `real`/`shortreal`/`realtime`
82 /// (review fix r1 rejected the first three via `bit_width() == 0`;
83 /// review fix r3 replaced that with the more general `!is_integral()`
84 /// gate -- `real`/`shortreal`/`realtime` have a NONZERO `bit_width()`
85 /// (64/32/64, per `Type::getBitWidth`) despite having no floating-point
86 /// representation implemented this wave, so the old zero-width test
87 /// alone let them fall through into ordinary 4-state slot machinery
88 /// with a silently dropped initializer; `is_integral()` is false for
89 /// every one of these shapes and true for everything this wave does
90 /// support, so one gate now gets both); or an otherwise-supported
91 /// integral member whose bit width does not fit `u16` (review fix r3:
92 /// previously silently clamped to `u16::MAX` instead of rejected, which
93 /// for a Wide scalar also undersized the arena allocation itself, since
94 /// `word_count` was derived from the clamped value). Carries a
95 /// hierarchical-ish name for diagnosis. A loud `Err`, never a guessed
96 /// slot or a panic reading a nonexistent fixed bound.
97 UnsupportedType(String),
98 /// A checker output formal whose initial value (`output int ctr = x;`,
99 /// LRM 17.2) is not a constant: it is evaluated in the instantiation's
100 /// context, and only a constant one is lowered -- rejected rather than
101 /// left at the type's default.
102 NonConstantCheckerOutputInitial(String),
103}
104
105/// Converts a real bit-width `u64` into the `u16` a [`SlotOffset`] field
106/// requires, returning `Err(LayoutError::UnsupportedType)` instead of
107/// silently clamping to `u16::MAX` when the value doesn't fit -- review fix
108/// r3: `u16::try_from(x).unwrap_or(u16::MAX)` used to clamp here, which for
109/// a real too-wide member (a legal SV construct, e.g. `logic [200000:0]
110/// huge;`) both misreports the metadata a later consumer masks against
111/// (state.md §3's `mask(bit_width)`) and, for a Wide scalar whose
112/// `word_count` this function's caller derives from the same width,
113/// would undersize the arena allocation itself relative to the member's
114/// true width -- exactly the "guessed slot" `LayoutError::UnsupportedType`'s
115/// own doc comment says must never happen.
116fn checked_bit_width(bit_width: u64, member_name: &str) -> Result<u32, LayoutError> {
117 u32::try_from(bit_width).map_err(|_| LayoutError::UnsupportedType(member_name.to_string()))
118}
119
120/// Walks `body.members()` and builds a [`StateLayout`] for one template
121/// (ST-058: a read-only walk over the frozen `Design`, deterministic on
122/// repeat calls -- no internal counter/cache outlives this call; ST-125: the
123/// sole frontend entry point, taking an already-obtained `Symbol` rather
124/// than a `Compilation`/`Session`).
125/// A memory's declared initializer as its every row's value: a
126/// `FourStateValue::Wide` whose planes are the rows' `words_per_row`-word
127/// (aval, bval) values concatenated, lowest row first (the row order
128/// `rollback::reset_to_initial` writes them in). The initializer must fold
129/// to a constant (`'{1, 2, 3}`, `'{default: 0}`, a parameter, ...): a
130/// module-level array initializer that reads another signal is rejected
131/// rather than dropped.
132fn memory_initial_rows(
133 member: &Symbol,
134 init: &sv_lang::Expression,
135 ty: &sv_lang::Type,
136 words_per_row: usize,
137) -> Result<FourStateValue, LayoutError> {
138 let unsupported = || LayoutError::UnsupportedType(member.name().to_string());
139 // An array of virtual interfaces (LRM 1800-2023 25.9) starts from
140 // interface instances and `null`, which are not constants: resolve
141 // each pattern leaf to its instance id (`netlist::vif`).
142 if ty.is_unpacked_array() && vif_array_elem(ty).is_some() {
143 let mut ids = Vec::new();
144 vif_pattern_rows(init, ty, &mut ids).ok_or_else(unsupported)?;
145 let mut aval = Vec::new();
146 for id in ids {
147 aval.push(id);
148 aval.resize(aval.len() + words_per_row - 1, 0);
149 }
150 let bval = vec![0; aval.len()];
151 return Ok(FourStateValue::Wide { aval, bval });
152 }
153 let cv = init.constant_value().ok_or_else(unsupported)?;
154 let mut aval = Vec::new();
155 let mut bval = Vec::new();
156 push_rows(&cv, ty, words_per_row, &mut aval, &mut bval).ok_or_else(unsupported)?;
157 Ok(FourStateValue::Wide { aval, bval })
158}
159
160/// The virtual-interface or class-handle element type (a class array pattern takes only `null` leaves) of a fixed unpacked array (through
161/// every dimension), `None` for any other array.
162fn vif_array_elem<'d>(ty: &sv_lang::Type<'d>) -> Option<sv_lang::Type<'d>> {
163 let mut t = *ty;
164 while t.is_unpacked_array() && !super::storage::is_collection(&t) {
165 t = t.element_type()?;
166 }
167 (t.is_virtual_interface() || t.is_class()).then_some(t)
168}
169
170/// Appends the instance id of every element of the array pattern `e` of
171/// (array) type `ty`, lowest index first: an interface instance names its
172/// id, `null` is 0, and a pattern nests per dimension.
173fn vif_pattern_rows(
174 e: &sv_lang::Expression<'_>,
175 ty: &sv_lang::Type<'_>,
176 out: &mut Vec<u64>,
177) -> Option<()> {
178 use sv_lang_kinds::ExpressionKind;
179 let mut e = *e;
180 while e.kind() == ExpressionKind::Conversion {
181 e = e.conversion_operand()?;
182 }
183 if ty.is_unpacked_array() && !super::storage::is_collection(ty) {
184 let range = ty.fixed_unpacked_array_range();
185 let elem_ty = ty.element_type()?;
186 // Any other initializer is rejected by name by the caller.
187 let k = e.kind();
188 let elems = if matches!(
189 k,
190 ExpressionKind::SimpleAssignmentPattern | ExpressionKind::StructuredAssignmentPattern
191 ) {
192 e.pattern_elements()
193 } else if k == ExpressionKind::ReplicatedAssignmentPattern {
194 let n = e.replicated_pattern_count()?.constant_value()?.as_i64()?;
195 let one = e.pattern_elements();
196 (0..n).flat_map(|_| one.iter().copied()).collect()
197 } else {
198 return None;
199 };
200 if elems.len() as u64 != u64::from(range.width()) {
201 return None;
202 }
203 // Elements run left bound to right bound; rows are stored lowest
204 // index first.
205 let mut rows = Vec::new();
206 for el in &elems {
207 let mut r = Vec::new();
208 vif_pattern_rows(el, &elem_ty, &mut r)?;
209 rows.push(r);
210 }
211 if range.left > range.right {
212 rows.reverse();
213 }
214 out.extend(rows.into_iter().flatten());
215 return Some(());
216 }
217 out.push(vif_leaf(&e)?);
218 Some(())
219}
220
221/// Appends the rows of the constant `cv` of (array) type `ty` to the planes;
222/// `None` for a value this layout cannot store.
223fn push_rows(
224 cv: &sv_lang::ConstantValue,
225 ty: &sv_lang::Type,
226 words: usize,
227 aval: &mut Vec<u64>,
228 bval: &mut Vec<u64>,
229) -> Option<()> {
230 if ty.is_unpacked_array() {
231 let elems = cv.as_array()?;
232 let range = ty.fixed_unpacked_array_range();
233 let elem_ty = ty.element_type()?;
234 if elems.len() as u64 != u64::from(range.width()) {
235 return None;
236 }
237 // Constant elements run left bound to right bound; rows are stored
238 // lowest index first.
239 if range.left > range.right {
240 for e in elems.iter().rev() {
241 push_rows(e, &elem_ty, words, aval, bval)?;
242 }
243 } else {
244 for e in elems {
245 push_rows(e, &elem_ty, words, aval, bval)?;
246 }
247 }
248 return Some(());
249 }
250 if super::storage::is_unpacked_aggregate(ty) {
251 let (mut a, mut b) =
252 super::value::bits_to_aval_bval(&super::storage::constant_bits(cv, ty)?);
253 a.resize(words, 0);
254 b.resize(words, 0);
255 aval.extend(a);
256 bval.extend(b);
257 return Some(());
258 }
259 let (mut a, mut b) = match cv {
260 sv_lang::ConstantValue::Integer(i) => {
261 super::value::bits_to_aval_bval(&super::value::svint_bits(i)?)
262 }
263 sv_lang::ConstantValue::Real(f) if ty.bit_width() == 32 => {
264 (vec![u64::from((*f as f32).to_bits())], vec![0])
265 }
266 sv_lang::ConstantValue::Real(f) => (vec![f.to_bits()], vec![0]),
267 sv_lang::ConstantValue::String(s) if ty.is_string() => {
268 (vec![crate::strings::intern(s.as_bytes())], vec![0])
269 }
270 _ => return None,
271 };
272 a.resize(words, 0);
273 b.resize(words, 0);
274 aval.extend(a);
275 bval.extend(b);
276 Some(())
277}
278
279/// A queue or dynamic array's constant initializer (`int q[$] = {1, 2}`)
280/// as a frozen collection handle (`crate::collections::frozen_from_words`),
281/// every instance sharing it until its first change; an empty associative
282/// array literal is the empty handle.
283fn collection_initial(
284 member: &Symbol,
285 init: &sv_lang::Expression,
286 ty: &sv_lang::Type,
287) -> Result<u64, LayoutError> {
288 let unsupported = || LayoutError::UnsupportedType(member.name().to_string());
289 if ty.element_type().is_some_and(|e| e.is_virtual_interface()) {
290 return vif_collection_initial(init, ty).ok_or_else(unsupported);
291 }
292 let cv = init.constant_value().ok_or_else(unsupported)?;
293 let elem_ty = ty.element_type().ok_or_else(unsupported)?;
294 let width = super::storage::storage_width(&elem_ty).ok_or_else(unsupported)?;
295 let elem_words = (2 * width).div_ceil(64).max(1) as usize;
296 let elem_words_of = |e: &sv_lang::ConstantValue| -> Result<Vec<u64>, LayoutError> {
297 let mut w = super::value::packed_words(
298 &super::storage::constant_bits(e, &elem_ty).ok_or_else(unsupported)?,
299 );
300 w.resize(elem_words, 0);
301 Ok(w)
302 };
303 if ty.is_associative_array() {
304 let mut pattern_probe = *init;
305 while pattern_probe.kind() == sv_lang::kinds::ExpressionKind::Conversion {
306 pattern_probe = pattern_probe.operand().ok_or_else(unsupported)?;
307 }
308 if matches!(cv, sv_lang::ConstantValue::Map(_, true))
309 && pattern_probe.structured_pattern_default_setter().is_none()
310 {
311 return Ok(0);
312 }
313 // `'{key: value, ...}`: the pattern's index setters (the constant
314 // value does not list its entries). A `default:` is not kept.
315 let mut pattern = *init;
316 while pattern.kind() == sv_lang::kinds::ExpressionKind::Conversion {
317 pattern = pattern.operand().ok_or_else(unsupported)?;
318 }
319 let key_ty = ty.associative_index_type().ok_or_else(unsupported)?;
320 let shape = crate::collections::Shape {
321 elem_words,
322 assoc: true,
323 key: if key_ty.is_string() {
324 crate::collections::KeyKind::Str
325 } else {
326 crate::collections::KeyKind::Int {
327 width: key_ty.bit_width() as u32,
328 signed: key_ty.is_signed(),
329 }
330 },
331 obj: false,
332 nested: crate::collections::Nested::NONE,
333 };
334 let mut entries = Vec::new();
335 for setter in pattern.structured_pattern_index_setters() {
336 let kcv = setter.index().constant_value().ok_or_else(unsupported)?;
337 let vcv = setter.expr().constant_value().ok_or_else(unsupported)?;
338 let kbits = super::storage::constant_bits(&kcv, &key_ty).ok_or_else(unsupported)?;
339 let key = shape
340 .key_from_bits(&super::value::packed_words(&kbits))
341 .ok_or_else(unsupported)?;
342 entries.push((key, elem_words_of(&vcv)?));
343 }
344 let default = match pattern.structured_pattern_default_setter() {
345 // A bare string literal as the `default:` setter keeps its
346 // integral type (no implicit conversion, unlike the index
347 // setters), so a `string` element takes its text directly.
348 Some(d)
349 if elem_ty.is_string()
350 && d.kind() == sv_lang::kinds::ExpressionKind::StringLiteral =>
351 {
352 let text = d.string_literal_value().ok_or_else(unsupported)?;
353 Some(elem_words_of(&sv_lang::ConstantValue::String(text))?)
354 }
355 Some(d) => Some(elem_words_of(&d.constant_value().ok_or_else(unsupported)?)?),
356 None => None,
357 };
358 return Ok(crate::collections::frozen_map(entries, default));
359 }
360 let mut words = Vec::new();
361 for e in cv.as_array().ok_or_else(unsupported)? {
362 words.extend(elem_words_of(e)?);
363 }
364 if words.is_empty() {
365 return Ok(0);
366 }
367 Ok(crate::collections::frozen_from_words(elem_words, &words))
368}
369
370/// A queue, dynamic array or associative array of virtual interfaces
371/// (LRM 1800-2023 25.9) initialized from instances and `null`
372/// (`virtual SBus q[$] = '{s0, s1}`): each element is a 64-bit instance id
373/// (`netlist::vif`), stored as one 2-state element of two words.
374fn vif_collection_initial(init: &sv_lang::Expression, ty: &sv_lang::Type) -> Option<u64> {
375 use sv_lang_kinds::ExpressionKind;
376 let mut pattern = *init;
377 while pattern.kind() == ExpressionKind::Conversion {
378 pattern = pattern.conversion_operand()?;
379 }
380 if ty.is_associative_array() {
381 let key_ty = ty.associative_index_type()?;
382 let shape = crate::collections::Shape {
383 elem_words: 2,
384 assoc: true,
385 key: if key_ty.is_string() {
386 crate::collections::KeyKind::Str
387 } else {
388 crate::collections::KeyKind::Int {
389 width: key_ty.bit_width() as u32,
390 signed: key_ty.is_signed(),
391 }
392 },
393 obj: false,
394 nested: crate::collections::Nested::NONE,
395 };
396 if pattern.kind() != ExpressionKind::StructuredAssignmentPattern {
397 return None;
398 }
399 let mut entries = Vec::new();
400 for setter in pattern.structured_pattern_index_setters() {
401 let kcv = setter.index().constant_value()?;
402 let kbits = super::storage::constant_bits(&kcv, &key_ty)?;
403 let key = shape.key_from_bits(&super::value::packed_words(&kbits))?;
404 entries.push((key, vec![vif_leaf(&setter.expr())?, 0]));
405 }
406 let default = match pattern.structured_pattern_default_setter() {
407 Some(d) => Some(vec![vif_leaf(&d)?, 0]),
408 None => None,
409 };
410 return Some(crate::collections::frozen_map(entries, default));
411 }
412 // Any other initializer is rejected by name by the caller.
413 let k = pattern.kind();
414 let elems = if matches!(
415 k,
416 ExpressionKind::SimpleAssignmentPattern | ExpressionKind::StructuredAssignmentPattern
417 ) {
418 pattern.pattern_elements()
419 } else if k == ExpressionKind::Concatenation {
420 pattern.operands()
421 } else if k == ExpressionKind::ReplicatedAssignmentPattern {
422 let n = pattern
423 .replicated_pattern_count()?
424 .constant_value()?
425 .as_i64()?;
426 let one = pattern.pattern_elements();
427 (0..n).flat_map(|_| one.iter().copied()).collect()
428 } else {
429 return None;
430 };
431 let mut words = Vec::new();
432 for el in &elems {
433 words.push(vif_leaf(el)?);
434 words.push(0);
435 }
436 if words.is_empty() {
437 return Some(0);
438 }
439 Some(crate::collections::frozen_from_words(2, &words))
440}
441
442/// The id a virtual-interface-typed initializer leaf names: `null` is 0, an
443/// interface instance its [`netlist::vif::instance_id`].
444fn vif_leaf(e: &sv_lang::Expression<'_>) -> Option<u64> {
445 use sv_lang_kinds::ExpressionKind;
446 let mut e = *e;
447 while e.kind() == ExpressionKind::Conversion {
448 e = e.conversion_operand()?;
449 }
450 // Any other leaf is not an initializer a virtual interface array takes:
451 // the caller rejects the declaration by name.
452 if e.kind() == ExpressionKind::NullLiteral {
453 Some(0)
454 } else if e.kind() == ExpressionKind::ArbitrarySymbol {
455 super::super::netlist::vif::instance_id(&e.referenced_symbol()?)
456 } else {
457 None
458 }
459}
460
461/// A layout of one slot per entry of `widths` holding no declared signal --
462/// the contribution cells of resolved nets' drivers (`vj`'s
463/// `nets`): each Inline or Wide by width, starting all-`z` (not driving).
464pub fn synthetic_z_layout(widths: &[u32]) -> StateLayout {
465 synthetic_z_layout_rows(widths, &[], &[])
466}
467
468/// [`synthetic_z_layout`] where cell `k` with `rows[k] > 0` is a `Memory`
469/// slot of that many rows of `widths[k]` bits, every row all-`z`: the
470/// contribution cell of a driver of an unpacked array of nets, whose elements
471/// are resolved one by one (`vj`'s `nets`). A row starts at
472/// `fills[k]` (LSB first) when that is non-empty, else all-`z`: a user
473/// nettype's driver has no `z` (its data type's default value instead); a
474/// non-memory cell with a non-empty `fills[k]` starts at it too (a specify
475/// path's staging cell starts `x`, LRM 30.5.1).
476pub fn synthetic_z_layout_rows(
477 widths: &[u32],
478 rows: &[u32],
479 fills: &[Vec<sv_lang::Bit>],
480) -> StateLayout {
481 let arena = Arena::new(BuildMode::FourState);
482 let mut slots = Vec::with_capacity(widths.len());
483 for (k, &w) in widths.iter().enumerate() {
484 let w = w.clamp(1, u32::MAX);
485 let words = w.div_ceil(64);
486 let num_rows = rows.get(k).copied().unwrap_or(0);
487 if num_rows > 0 {
488 let total = words * num_rows;
489 // SAFETY: `arena` is local and single-owned; sequential calls only.
490 let offset = unsafe { arena.alloc_wide_words(total) };
491 let fill = match fills.get(k) {
492 Some(f) if !f.is_empty() => f.clone(),
493 _ => vec![sv_lang::Bit::Z; w as usize],
494 };
495 let (a, b) = super::value::bits_to_aval_bval(&fill);
496 slots.push(SlotDescriptor {
497 key: SymbolKey(k as u32),
498 offset: SlotOffset {
499 class: SlotClass::Memory,
500 word_offset: offset,
501 bit_width: w,
502 elem_word_count: words,
503 row_stride_words: words,
504 num_rows,
505 },
506 initial: Some(FourStateValue::Wide {
507 aval: a.repeat(num_rows as usize),
508 bval: b.repeat(num_rows as usize),
509 }),
510 net_info: None,
511 });
512 continue;
513 }
514 // SAFETY: `arena` is local and single-owned; sequential calls only.
515 let (class, offset) = if words <= 1 {
516 (SlotClass::Inline, unsafe { arena.alloc_inline_word() })
517 } else {
518 (SlotClass::Wide, unsafe { arena.alloc_wide_words(words) })
519 };
520 slots.push(SlotDescriptor {
521 key: SymbolKey(k as u32),
522 offset: SlotOffset {
523 class,
524 word_offset: offset,
525 bit_width: w,
526 elem_word_count: words,
527 row_stride_words: 0,
528 num_rows: 0,
529 },
530 initial: Some(match fills.get(k) {
531 Some(f) if !f.is_empty() => super::value::bits_to_four_state(f),
532 _ => super::value::bits_to_four_state(&vec![sv_lang::Bit::Z; w as usize]),
533 }),
534 net_info: None,
535 });
536 }
537 StateLayout {
538 inline_words: arena.inline().len() as u32,
539 wide_words: arena.wide_aval().len() as u32,
540 slots,
541 }
542}
543
544/// `body`'s clocking blocks, each with its input (and inout) clock
545/// variables, in declaration order -- the extra slots of a template, in the
546/// order both the layout and the codegen signal map give them.
547pub fn clocking_blocks<'d>(body: &Symbol<'d>) -> Vec<(Symbol<'d>, Vec<Symbol<'d>>)> {
548 crate::netlist::module_items(body)
549 .into_iter()
550 .filter(|m| m.kind() == SymbolKind::ClockingBlock)
551 .map(|cb| {
552 let vars = cb
553 .members()
554 .filter(|v| {
555 v.kind() == SymbolKind::ClockVar
556 && matches!(
557 v.clock_var_direction(),
558 sv_lang::ArgumentDirection::In | sv_lang::ArgumentDirection::InOut
559 )
560 })
561 .collect();
562 (cb, vars)
563 })
564 .collect()
565}
566
567/// Task 8 (sva T8, `docs/superpowers/plans/2026-10-03-sva.md`): LRM
568/// 1800-2023 14.12's `default clocking` and 16.15's `default disable iff`
569/// -- the *scoping* half only (which single declaration, if any, covers a
570/// given scope): resolving the lexical-nesting/override rules both clauses
571/// state almost verbatim ("valid only within the scope containing the
572/// declaration ... as well as any nested module[s] ... It does not include
573/// instantiated modules" -- 14.12; "provides a default ... to all
574/// concurrent assertions in the scope and subscopes ... the default
575/// extends to any nested module ... However, if a nested module ... has a
576/// [its own] declaration, then that ... overrides any default ... from
577/// outside" -- 16.15). The *resolution*-rule half of 16.15 (composing this
578/// against an assertion's own explicit `disable iff` clause, rules
579/// (a)/(b)/(c)) is T27's job, consuming [`default_disable_of`]'s result --
580/// not reimplemented here.
581///
582/// ## Why a syntax walk, not a `sv-lang` semantic accessor
583///
584/// The vendored C++ slang already has exactly this query
585/// (`slang::ast::Compilation::getDefaultClocking`/`getDefaultDisable`,
586/// `include/slang/ast/Compilation.h`) -- but, unlike 16.13.3/16.16's clock
587/// flow (T7) or 16.14.6's procedural clock inference (T6), `sv-lang` 0.1.9
588/// exposes no FFI binding to either one at all (confirmed by grepping the
589/// installed crate's own generated `ast.rs`/`sys` surface for
590/// `default_clocking`/`default_disable` and finding nothing -- not
591/// assumed). Per the same plan-wide posture T6/T7 already apply ("wrap
592/// what's exposed, reimplement only what genuinely is not"), this task
593/// reconstructs 14.12/16.15 directly from the syntax tree, exactly as the
594/// plan's own Task 8 "Files"/"Steps" text asks: walking
595/// `module_or_generate_item_declaration`'s `default clocking
596/// clocking_identifier ;`/inline `[default] clocking ... endclocking`/
597/// `default disable iff expr ;` forms (`DefaultClockingReferenceSyntax`/
598/// `ClockingDeclarationSyntax`/`DefaultDisableDeclarationSyntax`).
599///
600/// ## Lexical nesting via `Symbol::parent()`, confirmed live
601///
602/// 14.12/16.15's "nested declaration inherits; mere instantiation does
603/// not" distinction sounds like it needs a second, syntax-ancestry walk
604/// independent of the elaborated instance tree -- but `sv_lang::Symbol::
605/// parent()` (`slang_symbol_parent_scope`) turns out to already encode
606/// exactly that distinction, confirmed live against both shapes before
607/// writing this function (not assumed from the plan's prose): for a module
608/// `m2` declared as a *nested* declaration textually inside `m1` and
609/// instantiated there, `m2`'s elaborated `InstanceBody::parent()` is `m1`'s
610/// own `InstanceBody`; for the identical textual declaration of `m2`
611/// hoisted to the top level and merely *instantiated* inside `m1`, `m2`'s
612/// `InstanceBody::parent()` is the design's `CompilationUnit` root --
613/// never `m1` -- even though `m2` is instantiated in exactly the same
614/// place in both cases. I.e. `parent()` already walks the *lexical*
615/// declaration-site scope chain, not the elaborated instantiation
616/// hierarchy, which is exactly what both clauses need told apart. The same
617/// holds for a `GenerateBlock` symbol (its `parent()` is its lexically
618/// enclosing module/generate scope). So [`each_lexical_scope`] below climbs
619/// `Symbol::parent()` alone, never a second, independent `Node::parent()`
620/// syntax-ancestry walk.
621#[derive(Debug)]
622pub enum DefaultScopeError {
623 /// 14.12: "Specifying a default clocking more than once in the same
624 /// module, interface, program, or checker shall result in a compiler
625 /// error." Carries the offending scope's name.
626 DuplicateDefaultClocking(String),
627 /// 16.15: "More than one default disable iff declaration within the
628 /// same module, interface, program declaration, or generate block
629 /// shall be an error." Carries the offending scope's name.
630 DuplicateDefaultDisable(String),
631 /// A `default clocking clocking_identifier;` reference whose name does
632 /// not resolve to any member of the scope it was declared in.
633 /// Defensive -- 14.12's own "clocking_identifier shall be the name of a
634 /// clocking block" is a semantic rule slang itself already enforces on
635 /// a fatal-error-free `Design`, so this should be unreachable, but a
636 /// loud named error is still required over a silent `None`/panic.
637 UnresolvedDefaultClockingTarget(String),
638 /// As above, but the name resolved to a real member that is not itself
639 /// a clocking block.
640 DefaultClockingTargetNotAClockingBlock(String),
641}
642
643/// The direct item list of one lexical, scope-introducing syntax node --
644/// `None` for every other node kind, in which case [`each_lexical_scope`]
645/// moves on to that `Symbol`'s own lexical parent instead of searching
646/// here. 14.12/16.15's own grammar (`module_or_generate_item_declaration`/
647/// `checker_or_generate_item_declaration`) only ever places a default
648/// declaration directly inside one of these three container shapes --
649/// never inside e.g. a bare `generate`/`endgenerate` region or an
650/// `if`/`case`/`for` generate construct's own wrapper node, neither of
651/// which carries an item list of its own (the region keyword pair has no
652/// semantic effect on scoping at all; a conditional generate construct's
653/// *branch* is what carries one, as a `GenerateBlockSyntax`, already
654/// handled below) -- so this function is transparent to those by
655/// construction, not by an omitted case.
656fn items_of<'t>(node: Node<'t>) -> Option<Vec<MemberSyntax<'t>>> {
657 if let Some(m) = ModuleDeclarationSyntax::cast(node) {
658 return Some(m.members().into_iter().collect());
659 }
660 if let Some(c) = CheckerDeclarationSyntax::cast(node) {
661 return Some(c.members().into_iter().collect());
662 }
663 if let Some(g) = GenerateBlockSyntax::cast(node) {
664 return Some(g.members().into_iter().collect());
665 }
666 None
667}
668
669/// Climbs `scope`'s lexical scope chain (`scope` itself, then
670/// `scope.parent()`, then *its* parent, ...), yielding `(scope_symbol,
671/// its_own_direct_item_list)` for each ancestor that is itself a
672/// scope-introducing declaration ([`items_of`] returns `Some`) -- silently
673/// skipping (not erroring on) every ancestor that is not one, per
674/// [`items_of`]'s own doc comment. Ends when the chain runs out
675/// (`Symbol::parent()` returns `None`), yielding nothing further -- the
676/// caller's own "found nothing in the whole chain" `Ok(None)` case (16.15
677/// rule (c)/14.12's "otherwise no default clocking applies").
678fn each_lexical_scope<'d, 't>(
679 scope: Symbol<'d>,
680 tree: &'t SyntaxTree,
681) -> impl Iterator<Item = (Symbol<'d>, Vec<MemberSyntax<'t>>)> {
682 let mut cur = Some(scope);
683 std::iter::from_fn(move || {
684 while let Some(s) = cur.take() {
685 cur = s.parent();
686 if let Some(items) = s.syntax(tree).and_then(items_of) {
687 return Some((s, items));
688 }
689 }
690 None
691 })
692}
693
694/// Resolves `name` via ordinary (non-hierarchical) scoped lookup, climbing
695/// `scope`'s own lexical parent chain (`Symbol::parent()`) when `name` is
696/// not among `scope`'s own direct members. `Symbol::find` itself only ever
697/// searches `scope`'s own members (`slang::ast::Scope::find`, a direct,
698/// non-recursive lookup -- confirmed live, not assumed: 14.12 Example 2's
699/// own `cpu`/`busA` shape -- a *nested* module's `default clocking busA;`
700/// reference naming a clocking block declared in its *enclosing* module,
701/// not its own -- fails `decl_scope.find("busA")` directly, and this
702/// climbing wrapper is what actually resolves it, matching ordinary SV
703/// unqualified-name resolution).
704fn find_in_lexical_scope<'d>(scope: Symbol<'d>, name: &str) -> Option<Symbol<'d>> {
705 let mut cur = Some(scope);
706 while let Some(s) = cur {
707 if let Some(found) = s.find(name) {
708 return Some(found);
709 }
710 cur = s.parent();
711 }
712 None
713}
714
715/// LRM 14.12's resolved default clocking block for `scope`: the nearest
716/// enclosing scope's `default clocking clocking_identifier;` reference or
717/// inline `[default] clocking ... endclocking` declaration, as an actual
718/// `ClockingBlock` [`Symbol`] -- `None` if no scope in the chain declares
719/// one.
720pub fn default_clocking_of<'d>(
721 scope: &Symbol<'d>,
722 tree: &SyntaxTree,
723) -> Result<Option<Symbol<'d>>, DefaultScopeError> {
724 for (decl_scope, items) in each_lexical_scope(*scope, tree) {
725 let mut via_ref: Option<DefaultClockingReferenceSyntax> = None;
726 let mut via_inline: Option<ClockingDeclarationSyntax> = None;
727 let mut count = 0usize;
728 for item in &items {
729 match item {
730 MemberSyntax::DefaultClockingReference(n) => {
731 count += 1;
732 via_ref = Some(*n);
733 }
734 // The inline `[default] clocking ...` form: only the
735 // `default`-flagged keyword counts (never a plain
736 // `clocking busA @(...); ...` with no `default`/`global`
737 // keyword at all, and never `global clocking`, Clause
738 // 16.18 -- T35's concern, a distinct concept this task
739 // must not conflate with 14.12's default clocking).
740 MemberSyntax::ClockingDeclaration(n)
741 if n.global_or_default().map(|t| t.text()) == Some("default") =>
742 {
743 count += 1;
744 via_inline = Some(*n);
745 }
746 // Every other `MemberSyntax` variant (variable/net
747 // declarations, procedural blocks, module instances, a
748 // non-default clocking declaration, ...) is simply not a
749 // 14.12 default-clocking declaration -- an ST-072-style
750 // filter, not a narrowed match: nothing here is silently
751 // dropped that this task claims to handle.
752 _ => {}
753 }
754 }
755 if count > 1 {
756 return Err(DefaultScopeError::DuplicateDefaultClocking(
757 decl_scope.name().to_string(),
758 ));
759 }
760 if let Some(n) = via_ref {
761 let name = n.name().map(|t| t.text()).unwrap_or("");
762 let sym = find_in_lexical_scope(decl_scope, name).ok_or_else(|| {
763 DefaultScopeError::UnresolvedDefaultClockingTarget(name.to_string())
764 })?;
765 if sym.kind() != SymbolKind::ClockingBlock {
766 return Err(DefaultScopeError::DefaultClockingTargetNotAClockingBlock(
767 name.to_string(),
768 ));
769 }
770 return Ok(Some(sym));
771 }
772 if let Some(n) = via_inline {
773 // The inline form elaborates its own `ClockingBlock` member
774 // directly in `decl_scope` (named or, per 14.12's grammar,
775 // anonymous -- `default clocking @(posedge clk); endclocking`
776 // is legal and elaborates with an empty name) -- recovered by
777 // syntax-node identity rather than by name, which works for
778 // both, confirmed live: the elaborated `ClockingBlock`
779 // member's own `.syntax(tree)` is `==` the exact
780 // `ClockingDeclarationSyntax` node found here.
781 let decl_node = AstNode::syntax(&n);
782 let sym = decl_scope.members().find(|m| {
783 m.kind() == SymbolKind::ClockingBlock && m.syntax(tree) == Some(decl_node)
784 });
785 return match sym {
786 Some(sym) => Ok(Some(sym)),
787 // Unreachable on a fatal-error-free `Design` (see this
788 // function's doc comment on `UnresolvedDefaultClockingTarget`).
789 None => Err(DefaultScopeError::UnresolvedDefaultClockingTarget(
790 "<inline default clocking>".to_string(),
791 )),
792 };
793 }
794 // Neither form found directly in `decl_scope` -- 14.12's "extends
795 // to nested ... declarations" means the search continues to the
796 // next (lexically enclosing) scope `each_lexical_scope` yields.
797 }
798 Ok(None)
799}
800
801/// LRM 16.15's resolved default disable condition for `scope`: the nearest
802/// enclosing scope's `default disable iff expr;` declaration, together
803/// with the [`Symbol`] of the scope it was *declared* in -- "Any signals
804/// referenced in the disable iff declaration that are resolved using
805/// scopes will be resolved from the scope of the declaration" (16.15), so
806/// a later compiling pass (T27) must bind `expr`'s names against that
807/// scope, never against the querying assertion's own (potentially more
808/// deeply nested) scope. `None` if no scope in the chain declares one
809/// (16.15 rule (c), "no inference ... equivalent to ... a 1'b0 disable
810/// condition").
811pub fn default_disable_of<'d, 't>(
812 scope: &Symbol<'d>,
813 tree: &'t SyntaxTree,
814) -> Result<Option<(Symbol<'d>, DefaultDisableDeclarationSyntax<'t>)>, DefaultScopeError> {
815 for (decl_scope, items) in each_lexical_scope(*scope, tree) {
816 let mut found: Option<DefaultDisableDeclarationSyntax> = None;
817 let mut count = 0usize;
818 for item in &items {
819 if let MemberSyntax::DefaultDisableDeclaration(n) = item {
820 count += 1;
821 found = Some(*n);
822 }
823 }
824 if count > 1 {
825 return Err(DefaultScopeError::DuplicateDefaultDisable(
826 decl_scope.name().to_string(),
827 ));
828 }
829 if let Some(n) = found {
830 return Ok(Some((decl_scope, n)));
831 }
832 }
833 Ok(None)
834}
835
836pub fn build_template_layout(body: &Symbol) -> Result<StateLayout, LayoutError> {
837 let arena = Arena::new(BuildMode::FourState);
838 let mut slots = Vec::new();
839 let mut next_key: u32 = 0;
840 let mut slot_of_symbol: std::collections::HashMap<crate::symid::SymbolId, usize> =
841 std::collections::HashMap::new();
842
843 // Generate blocks' items belong to the module (`netlist::module_items`).
844 //
845 // Classes plan Task 4 (`docs/superpowers/plans/2026-10-03-classes.md`,
846 // LRM 8.9): this same walk is also the entry point for a class
847 // template's *static* properties -- `body` may be a `ClassType` symbol
848 // (the caller mints a singleton segment for it, keyed by
849 // `ClassTemplateId` rather than a real elaborated `InstanceId`; see
850 // `state::instance::{class_static_instance,
851 // StateEngine::allocate_class_static_singletons}`). A static
852 // `ClassProperty` (8.9: one
853 // copy for the whole simulation) is accepted below exactly like an
854 // ordinary module `Variable` -- same `value_type()`/`initializer()`
855 // shape, so every per-member branch further down needs no class-specific
856 // case of its own. An *instance* (automatic-lifetime) `ClassProperty`
857 // is deliberately excluded here: it lives in the object heap
858 // (`vj-sim::objects`, Task 3/5), never in this arena. `state_items`'s
859 // own `static_procedural_vars` half also already reaches a class's own
860 // `Subroutine` (method/constructor) members for their static locals/
861 // formals, giving those the identical one-copy-per-class-template
862 // storage LRM 6.21 calls for ("one instance ... for all invocations",
863 // which for a method is every object's call) with no extra code.
864 for member in crate::netlist::state_items(body) {
865 let kind = member.kind();
866 let is_static_class_property = kind == SymbolKind::ClassProperty
867 && member.variable_lifetime() == sv_lang::VariableLifetime::Static;
868 if !matches!(
869 kind,
870 SymbolKind::Variable | SymbolKind::Net | SymbolKind::Port | SymbolKind::FormalArgument
871 ) && !is_static_class_property
872 {
873 // ST-072: genvars/parameters/types/subroutine-locals (subroutine
874 // bodies are never descended into by `members()` on a module
875 // body in the first place), a class's instance (non-static)
876 // properties/methods/constructor/nested classes/constraint and
877 // covergroup blocks (Task 4's own `is_static_class_property`
878 // exclusion above), and every other non-{Variable,Net,Port}
879 // kind are excluded by this filter alone -- no separate
880 // exclusion code path exists.
881 continue;
882 }
883
884 // Deviation, found via real sv-lang=0.1.9 output (not stated in the
885 // plan): an ANSI port backed by an implicit net/variable elaborates
886 // into TWO sibling members of `body` with the *same* name -- the
887 // `Port` boundary-descriptor symbol itself, and its separate
888 // internal `Net`/`Variable` symbol
889 // (`slang::ast::PortSymbol::internalSymbol`, exposed here as
890 // `port_internal_symbol()`). Slotting both would double-allocate
891 // storage for one physical signal. The internal symbol is itself a
892 // direct member of `body`, so it already gets its own slot on its
893 // own pass through this same loop; a `Port` with an internal symbol
894 // is therefore skipped here entirely, never producing a second,
895 // aliasing slot. A `Port` whose internal connection is an
896 // expression rather than a plain symbol (e.g. a concatenation of
897 // several internal signals) has nothing else to alias and falls
898 // through to get its own slot below.
899 if kind == SymbolKind::Port && member.port_internal_symbol().is_some() {
900 continue;
901 }
902
903 // ST-072 applied to a null port: `module m(a, , c);`'s middle,
904 // dot-less position elaborates as its own `Port`-kind member with
905 // NO internal symbol and NO internal connection of any kind -- it
906 // carries no value and is not "the variable/net/port" ST-060 means.
907 // Without this check it would fall through past the
908 // `port_internal_symbol()` skip above (which only catches a port
909 // *with* something to alias) straight into `member.port_type()`
910 // below, which -- verified against real sv-lang=0.1.9 output --
911 // still returns `Some(Type)` (slang's `PortSymbol::getType()` is
912 // unconditional) rather than `None`, so the old code manufactured a
913 // real `SlotDescriptor` with `bit_width == 0` for a construct with
914 // nothing to store: a phantom slot, not a loud error and not an
915 // exclusion. `port_is_null_port()` is slang's own purpose-built
916 // predicate for exactly this case (`PortSymbol::isNullPort`), so it
917 // is used here directly rather than re-deriving "null port" from
918 // `port_internal_symbol().is_none() && port_internal_expr().is_none()`.
919 if kind == SymbolKind::Port && member.port_is_null_port() {
920 continue;
921 }
922
923 // Deviation from the plan's literal "calls exactly value_type()"
924 // text: verified against real sv-lang=0.1.9 output, `value_type()`
925 // returns `None` for a real ANSI `Port`-kind symbol (its `is_value()`
926 // is false; the port's type lives behind the separate
927 // `port_type()` accessor `slang::ast::PortSymbol::getType` mirrors).
928 // Using `value_type()` unconditionally would turn every ANSI port
929 // into a spurious `UnsupportedMemberKind` error, which is exactly
930 // ST-060's "produces slots only for the variable/net/port" -- a
931 // *port* -- requirement failing in practice. `port_type()` is used
932 // only for `Port`-kind members; `Variable`/`Net` still go through
933 // `value_type()` exactly as the plan specifies.
934 let ty = if kind == SymbolKind::Port {
935 member.port_type()
936 } else {
937 member.value_type()
938 }
939 .ok_or_else(|| LayoutError::UnsupportedMemberKind(kind, member.name().to_string()))?;
940
941 // Task 6 (ST-061/ST-127): `is_signed()`/`is_four_state()` are part of
942 // this file's frozen per-member `Type` accessor call set, but neither
943 // is persisted on `SlotOffset` (ST-144 froze that field list at Task
944 // 3 -- sign/4-state-ness are operator-level concerns CODEGEN
945 // re-derives from the same `Symbol` directly, never storage-shape
946 // concerns). `is_signed`'s result IS threaded into the initial-value
947 // conversion below (the one place signedness legitimately affects
948 // this function's output, via `four_state_to_owned_svint`'s `signed`
949 // parameter) -- it never affects the word/class/width math itself,
950 // proven by `unsigned_and_signed_same_width_produce_identical_slot_shape`.
951 let is_signed = ty.is_signed();
952 let _is_four_state = ty.is_four_state();
953
954 // ST-062: `fields()` is called on every member's `Type` -- empty for
955 // any non-struct/union type per `sv_lang::Type::fields`'s own doc --
956 // solely to assert a packed struct/union's `bit_width()` already
957 // reflects the full flattened width; never to emit one slot per
958 // field (no `SlotDescriptor` is ever constructed from this check).
959 //
960 // review fix r1: the two shapes have different flatten invariants
961 // and must not share one check. A packed STRUCT concatenates its
962 // fields, so their widths sum to the struct's own `bit_width()`. A
963 // packed UNION does not: per IEEE 1800, every member of a packed
964 // union shares one bit length, and the union's own `bit_width()`
965 // *is* that shared length, not the sum of every member's width (a
966 // 2-field, 8-bit-per-field packed union has `bit_width() == 8`, not
967 // 16). Summing unconditionally made this `debug_assert_eq!` panic on
968 // any ordinary packed union with 2+ fields. `is_packed_union()` is
969 // added to this file's per-member `Type` accessor set for this one
970 // call site (Task 6's Acceptance-section whitelist is updated to
971 // match; see the plan's Task 6 section) -- no other accessor in the
972 // frozen set can distinguish a union from a struct.
973 //
974 // review fix r2: `Type::fields()` is documented (and confirmed
975 // against real sv-lang=0.1.9 output) to yield fields for a struct or
976 // union type "packed or unpacked" -- not packed-only, despite this
977 // block's name. An UNPACKED struct/union member (e.g. `struct {
978 // logic a; logic b; } x;`, a plain `Variable`-kind member this
979 // builder's own filter accepts) has a nonempty `field_syms` but a
980 // `bit_width()` of 0 (unpacked struct/union is non-integral), so the
981 // unconditional `debug_assert_eq!`/summed-width comparison below
982 // would panic on it under any debug-profile build (`cargo
983 // test`/`cargo build`; compiled out only in `--release`) instead of
984 // falling through to this same function's own `bit_width() == 0 ->
985 // Err(UnsupportedType)` rejection a few lines down -- exactly the
986 // "loud Err, never ... a panic" contract this file's own
987 // `LayoutError::UnsupportedType` doc comment promises. Gating this
988 // whole block on `ty.is_integral()` ("true for a packed/integral
989 // type", per `Type::is_integral`'s own doc) restricts the flatten
990 // invariant to the packed shapes it was written for; an unpacked
991 // struct/union instead falls through with an untouched
992 // `bit_width() == 0` and is rejected by the existing check below,
993 // never guessed at and never panicked on.
994 let field_syms: Vec<_> = ty.fields().collect();
995 if !field_syms.is_empty() && ty.is_integral() {
996 if ty.is_tagged_union() {
997 // LRM 7.3.2: a packed tagged union is `tag ++ payload`; a
998 // member is right-justified and may be narrower than the
999 // union (a `void` member has no payload), so the widths are
1000 // bounded by, not equal to, `bit_width()`.
1001 for f_ty in field_syms.iter().filter_map(|f| f.value_type()) {
1002 debug_assert!(
1003 f_ty.bit_width() <= ty.bit_width(),
1004 "packed tagged-union flatten: a member is wider than \
1005 the union for member {:?}",
1006 member.name(),
1007 );
1008 }
1009 } else if ty.is_packed_union() && ty.packed_union_is_soft() {
1010 // LRM 7.3.1: a soft packed union is as wide as its widest
1011 // member; narrower members are right-justified.
1012 for f_ty in field_syms.iter().filter_map(|f| f.value_type()) {
1013 debug_assert!(
1014 f_ty.bit_width() <= ty.bit_width(),
1015 "soft packed-union flatten: a member is wider than \
1016 the union for member {:?}",
1017 member.name(),
1018 );
1019 }
1020 } else if ty.is_packed_union() {
1021 for f_ty in field_syms.iter().filter_map(|f| f.value_type()) {
1022 debug_assert_eq!(
1023 f_ty.bit_width(),
1024 ty.bit_width(),
1025 "packed-union flatten: every field's width must equal \
1026 Type::bit_width() for member {:?}",
1027 member.name(),
1028 );
1029 }
1030 } else {
1031 let summed_width: u64 = field_syms
1032 .iter()
1033 .filter_map(|f| f.value_type())
1034 .map(|f_ty| f_ty.bit_width())
1035 .sum();
1036 debug_assert_eq!(
1037 summed_width,
1038 ty.bit_width(),
1039 "packed-struct flatten: fields() total width must equal \
1040 Type::bit_width() for member {:?}",
1041 member.name(),
1042 );
1043 }
1044 }
1045
1046 // ST-061/ST-062 memory/dynamic-array routing, verified against
1047 // /opt/slang/source/ast/types/Type.cpp: `is_dynamically_sized_array()`
1048 // MUST be checked first and separately from `is_unpacked_array()`
1049 // below -- it is also `true` for `QueueType`/`DynamicArrayType`/
1050 // `AssociativeArrayType`, all three of which ALSO satisfy
1051 // `is_unpacked_array()`, so checking `is_unpacked_array()` first
1052 // would route a queue/dynamic/associative-array member straight into
1053 // the fixed-size memory branch below and either panic reading a
1054 // nonexistent bound or synthesize a bogus layout for it.
1055 let (class, word_offset, bit_width, elem_word_count, row_stride_words, num_rows, initial) =
1056 if kind == SymbolKind::Net && super::storage::interconnect_dims(&ty).is_some() {
1057 // An `interconnect` net or array of them (LRM 6.6.8): typeless,
1058 // it only expresses connections and never holds a value, so
1059 // the simulator keeps one inert 1-bit placeholder slot for
1060 // the whole declaration. The connection analysis
1061 // (`vj::nets`) merges it into the real nets it joins.
1062 // SAFETY: same as the `alloc_wide_words` call below.
1063 let offset = unsafe { arena.alloc_inline_word() };
1064 (
1065 SlotClass::Inline,
1066 offset,
1067 1u32,
1068 1u32,
1069 0u32,
1070 0u32,
1071 Some(FourStateValue::Narrow(0)),
1072 )
1073 } else if super::storage::is_collection(&ty) {
1074 // A queue, dynamic array or associative array: a 2-state
1075 // 64-bit Inline cell holding a collection handle
1076 // (`crate::collections`), empty (0) or a constant
1077 // initializer's value.
1078 let handle = match crate::netlist::variable_initializer(&member) {
1079 Some(init) => collection_initial(&member, &init, &ty)?,
1080 None => 0,
1081 };
1082 // SAFETY: same as the `alloc_wide_words` call below.
1083 let offset = unsafe { arena.alloc_inline_word() };
1084 (
1085 SlotClass::Inline,
1086 offset,
1087 64u32,
1088 1u32,
1089 0u32,
1090 0u32,
1091 Some(FourStateValue::Narrow(u128::from(handle))),
1092 )
1093 } else if ty.is_unpacked_array() {
1094 // A fixed-size unpacked array (memory): `element_type()`
1095 // gives the per-row `Type`, `fixed_unpacked_array_range()`
1096 // gives the declared row count (ST-061) -- the one call site
1097 // for both, per Task 6's review-corrected derivation rule.
1098 // A multi-dimensional array is flattened: its rows are every
1099 // element in row-major order, lowest index first per
1100 // dimension (codegen's `crate::arrays`).
1101 let mut rows: u32 = 1;
1102 let mut elem_ty = ty;
1103 // A queue / dynamic / associative array element is a handle
1104 // row (`crate::collections`).
1105 while elem_ty.is_unpacked_array() && !super::storage::is_collection(&elem_ty) {
1106 rows = rows
1107 .checked_mul(elem_ty.fixed_unpacked_array_range().width())
1108 .ok_or_else(|| LayoutError::UnsupportedType(member.name().to_string()))?;
1109 elem_ty = elem_ty
1110 .element_type()
1111 .ok_or_else(|| LayoutError::UnsupportedType(member.name().to_string()))?;
1112 }
1113 // Integral rows, and rows of the 2-state non-integral types
1114 // stored as bits: `real`/`shortreal` (IEEE bits),
1115 // `string` (interned id) and `chandle` (pointer), both 64.
1116 let elem_bit_width_u64 = if elem_ty.is_integral() || elem_ty.is_floating() {
1117 elem_ty.bit_width()
1118 } else if elem_ty.is_string()
1119 || elem_ty.is_chandle()
1120 || elem_ty.is_covergroup()
1121 || elem_ty.is_virtual_interface()
1122 || elem_ty.is_class()
1123 || super::storage::is_collection(&elem_ty)
1124 {
1125 64
1126 } else if elem_ty.is_event() {
1127 // An array of named events (LRM 1800-2023 15.5, 6.17):
1128 // each row is one scalar event's cell, 128 bits
1129 // (`EVENT_CELL_WIDTH`), 0 until triggered. An event has
1130 // no constant value to initialize a row with.
1131 if crate::netlist::variable_initializer(&member).is_some() {
1132 return Err(LayoutError::UnsupportedType(member.name().to_string()));
1133 }
1134 super::EVENT_CELL_WIDTH as u64
1135 } else if super::storage::is_unpacked_aggregate(&elem_ty) {
1136 // An unpacked struct/union row: its flat stored form.
1137 super::storage::storage_width(&elem_ty)
1138 .ok_or_else(|| LayoutError::UnsupportedType(member.name().to_string()))?
1139 } else {
1140 return Err(LayoutError::UnsupportedType(member.name().to_string()));
1141 };
1142 // review fix r3: a too-wide row (e.g. `logic [200000:0] huge
1143 // [3:0];`) must be a loud `Err`, never silently clamped to
1144 // `u16::MAX` -- clamping here would misreport
1145 // `SlotOffset.bit_width` (used by RUNTIME's per-row
1146 // read/write masking, state.md §3's `mask(bit_width)`)
1147 // relative to the row's real width.
1148 let elem_bit_width = checked_bit_width(elem_bit_width_u64, member.name())?;
1149 // `words_per_row` is derived from the same, already-checked
1150 // `elem_bit_width_u64` (<= 65535, per the check just above),
1151 // so `.div_ceil(64).max(1)` is at most 1024 -- always
1152 // representable in `u16` -- and the cast below is total, not
1153 // a guess.
1154 let words_per_row = (elem_bit_width_u64.div_ceil(64).max(1)) as u32;
1155 let total_words = words_per_row * rows;
1156 // SAFETY: `arena` is a local, single-owned value for the
1157 // duration of this function; every call to it here is
1158 // sequential on one thread, so `alloc_wide_words`'s
1159 // single-logical-writer / no-concurrent-borrow obligation
1160 // (see its doc comment in `state::arena`) trivially holds.
1161 let offset = unsafe { arena.alloc_wide_words(total_words) };
1162 // A declared initializer (`int a[4] = '{1, 2, 3, 4}`) gives
1163 // every row its constant value (`memory_initial_rows`);
1164 // otherwise a 2-state element type's rows start at 0 (the
1165 // reset zeroes every row, `rollback::reset_to_initial`) and
1166 // a 4-state one's start all-X.
1167 //
1168 // A non-constant initializer (`int f[4] = {q, d};`,
1169 // `int a[3] = g();`; LRM 6.8) has no starting value here: the
1170 // rows take their defaults and the codegen's one-shot
1171 // variable-initializer kernel (`discover_variable_initializers`)
1172 // assigns them before time zero.
1173 let init_expr = crate::netlist::variable_initializer(&member).filter(|init| {
1174 init.constant_value().is_some()
1175 || vif_array_elem(&ty).is_some_and(|e| e.is_virtual_interface())
1176 });
1177 let initial = match init_expr {
1178 Some(init) => Some(memory_initial_rows(
1179 &member,
1180 &init,
1181 &ty,
1182 words_per_row as usize,
1183 )?),
1184 // An unpacked struct/union row mixes 4-state (X) and
1185 // 2-state (0) fields: every row gets its default bits.
1186 None if super::storage::is_unpacked_aggregate(&elem_ty) => {
1187 let bits = super::storage::default_bits(&elem_ty).ok_or_else(|| {
1188 LayoutError::UnsupportedType(member.name().to_string())
1189 })?;
1190 let (mut a, mut b) = super::value::bits_to_aval_bval(&bits);
1191 a.resize(words_per_row as usize, 0);
1192 b.resize(words_per_row as usize, 0);
1193 Some(FourStateValue::Wide {
1194 aval: a.repeat(rows as usize),
1195 bval: b.repeat(rows as usize),
1196 })
1197 }
1198 None => (!elem_ty.is_four_state()).then_some(FourStateValue::Narrow(0)),
1199 };
1200 (
1201 SlotClass::Memory,
1202 offset,
1203 elem_bit_width,
1204 words_per_row,
1205 words_per_row,
1206 rows,
1207 initial,
1208 )
1209 } else if super::storage::is_unpacked_aggregate(&ty) {
1210 // An unpacked struct/union: one flat vector of its fields'
1211 // stored bits (`storage`), Inline or Wide by width, starting
1212 // at its constant initializer or its per-field defaults
1213 // (X for 4-state fields, 0 for 2-state ones).
1214 let unsupported = || LayoutError::UnsupportedType(member.name().to_string());
1215 let width_u64 = super::storage::storage_width(&ty).ok_or_else(unsupported)?;
1216 let bit_width = checked_bit_width(width_u64, member.name())?;
1217 let bits = match crate::netlist::variable_initializer(&member) {
1218 // A tagged union's constant has no encoded form
1219 // (`ConstantValue::Union` is a string): the slot starts
1220 // undefined and codegen's variable-initializer
1221 // assignment sets it before time zero.
1222 Some(_) if super::storage::contains_tagged_union(&ty) => {
1223 super::storage::default_bits(&ty).ok_or_else(unsupported)?
1224 }
1225 Some(init) => {
1226 let cv = init.constant_value().ok_or_else(unsupported)?;
1227 super::storage::constant_bits(&cv, &ty).ok_or_else(unsupported)?
1228 }
1229 None => super::storage::default_bits(&ty).ok_or_else(unsupported)?,
1230 };
1231 let word_count = width_u64.div_ceil(64).max(1) as u32;
1232 // SAFETY: same as the `alloc_wide_words` call above.
1233 let (class, offset) = if word_count <= 1 {
1234 (SlotClass::Inline, unsafe { arena.alloc_inline_word() })
1235 } else {
1236 (SlotClass::Wide, unsafe {
1237 arena.alloc_wide_words(word_count)
1238 })
1239 };
1240 (
1241 class,
1242 offset,
1243 bit_width,
1244 word_count,
1245 0u32,
1246 0u32,
1247 Some(super::value::bits_to_four_state(&bits)),
1248 )
1249 } else if ty.is_floating() {
1250 // `real`/`realtime` (64-bit) and `shortreal` (32-bit): a
1251 // 2-state Inline cell holding the IEEE-754 bits (codegen's
1252 // `real` module), 0.0 unless a constant initializer says
1253 // otherwise.
1254 let width = ty.bit_width() as u32;
1255 let bits: u64 = match crate::netlist::variable_initializer(&member)
1256 .and_then(|init| init.constant_value())
1257 {
1258 Some(sv_lang::ConstantValue::Real(f)) if width == 32 => {
1259 u64::from((f as f32).to_bits())
1260 }
1261 Some(sv_lang::ConstantValue::Real(f)) => f.to_bits(),
1262 _ => 0,
1263 };
1264 // SAFETY: same as the `alloc_wide_words` call above.
1265 let offset = unsafe { arena.alloc_inline_word() };
1266 (
1267 SlotClass::Inline,
1268 offset,
1269 width,
1270 1u32,
1271 0u32,
1272 0u32,
1273 Some(FourStateValue::Narrow(u128::from(bits))),
1274 )
1275 } else if ty.is_string() {
1276 // A `string`: a 2-state 64-bit Inline cell holding the id of
1277 // an interned string (`crate::strings`), `""` (0) unless a
1278 // constant initializer says otherwise.
1279 let id = match crate::netlist::variable_initializer(&member)
1280 .and_then(|init| init.constant_value())
1281 {
1282 Some(sv_lang::ConstantValue::String(s)) => crate::strings::intern(s.as_bytes()),
1283 _ => 0,
1284 };
1285 // SAFETY: same as the `alloc_wide_words` call above.
1286 let offset = unsafe { arena.alloc_inline_word() };
1287 (
1288 SlotClass::Inline,
1289 offset,
1290 64u32,
1291 1u32,
1292 0u32,
1293 0u32,
1294 Some(FourStateValue::Narrow(u128::from(id))),
1295 )
1296 } else if ty.is_chandle()
1297 || ty.is_virtual_interface()
1298 || ty.is_class()
1299 || ty.is_covergroup()
1300 {
1301 // A `chandle` (a C pointer from DPI), a virtual interface
1302 // (an interface instance's id, `netlist::vif`), or a class
1303 // handle (classes plan Task 5, LRM 1800-2023 §8.4: a class
1304 // variable's own storage is just the OBJECT HANDLE -- the
1305 // referenced object's fields live in the object heap,
1306 // `vj_sim::objects`, never here): a 2-state 64-bit
1307 // Inline cell, `null` (0) initially -- a class variable's
1308 // default initial value absent an explicit `new`
1309 // initializer (8.5: "a class variable shall be initialized
1310 // to the value null").
1311
1312 // SAFETY: same as the `alloc_wide_words` call above.
1313 let offset = unsafe { arena.alloc_inline_word() };
1314 (
1315 SlotClass::Inline,
1316 offset,
1317 64u32,
1318 1u32,
1319 0u32,
1320 0u32,
1321 Some(FourStateValue::Narrow(0)),
1322 )
1323 } else if ty.is_event() {
1324 // A named `event` is a 2-state Wide cell starting at 0 (see
1325 // `EVENT_CELL_WIDTH`): `-> e` rewrites it and `@e` is an
1326 // any-change wait on it.
1327 let words = super::EVENT_CELL_WIDTH.div_ceil(64);
1328 // SAFETY: same as the `alloc_wide_words` call above.
1329 let offset = unsafe { arena.alloc_wide_words(words) };
1330 (
1331 SlotClass::Wide,
1332 offset,
1333 super::EVENT_CELL_WIDTH,
1334 words,
1335 0u32,
1336 0u32,
1337 Some(FourStateValue::Wide {
1338 aval: vec![0; words as usize],
1339 bval: vec![0; words as usize],
1340 }),
1341 )
1342 } else {
1343 // review fix r1: the plan's own Task 6 text promises
1344 // `LayoutError::UnsupportedType` rejects "a dynamic array,
1345 // queue, associative array, or class handle" (and, more
1346 // generally, any shape this builder cannot yet lay out), but
1347 // only the dynamically-sized-array predicate above was ever
1348 // checked -- a class-handle, `string`, `chandle`, `event`,
1349 // or virtual-interface member fell through to here and
1350 // called `ty.bit_width()`, which (vendored source,
1351 // `Type.cpp::getBitWidth`) returns 0 for exactly these kinds
1352 // (neither integral nor floating), silently manufacturing a
1353 // phantom `SlotClass::Inline`/`bit_width == 0` slot instead
1354 // of the loud error this file's own doc comments call for.
1355 //
1356 // review fix r3: `bit_width_u64 == 0` was not a complete
1357 // rejection, though -- a `real`/`shortreal`/`realtime`
1358 // member (e.g. `real r;`) is neither a class handle nor any
1359 // of the other zero-width shapes above, but has a NONZERO
1360 // `bit_width()` (64/32/64, per the same vendored
1361 // `getBitWidth`) despite this wave implementing no
1362 // floating-point representation, read/write path, or
1363 // initial-value threading at all (state.md's storage model
1364 // and the `SlotClass` enum define none), so it too fell
1365 // through into ordinary 4-state Inline/Wide slot machinery
1366 // with its initializer silently dropped (`four_state_of_
1367 // constant`'s `ConstantValue::as_integer()` returns `None`
1368 // for a real constant). `is_integral()` ("true for a
1369 // packed/integral type") is the correct, single gate for
1370 // both problems: false for every shape this branch must
1371 // reject (class handle, string, chandle, event, virtual
1372 // interface, or real/shortreal/realtime), true for every
1373 // packed integral scalar this wave DOES support (including
1374 // a packed struct/union, whose flattened `bit_width()` is
1375 // then always nonzero) -- so it replaces the old
1376 // `bit_width_u64 == 0` test rather than supplementing it
1377 // (leaving that test in place would make it a check that
1378 // can never fire, since every shape it caught is already
1379 // non-integral).
1380 if !ty.is_integral() {
1381 return Err(LayoutError::UnsupportedType(member.name().to_string()));
1382 }
1383 let bit_width_u64 = ty.bit_width();
1384 // review fix r3: reject (never clamp) a member wider than
1385 // `u16::MAX` bits, and derive `word_count` from the real,
1386 // untruncated `bit_width_u64` -- not from an already-clamped
1387 // `bit_width` -- so a Wide slot's arena allocation below is
1388 // never undersized relative to the member's true width (the
1389 // old `(bit_width as u32).div_ceil(64)` computed word count
1390 // from the post-clamp value, so bits above the 65535-bit
1391 // clamp point were silently unbacked by any storage at all,
1392 // not merely mis-reported).
1393 let bit_width = checked_bit_width(bit_width_u64, member.name())?;
1394 let word_count_u64 = bit_width_u64.div_ceil(64).max(1);
1395 // `word_count_u64` is derived from the same, already-checked
1396 // `bit_width_u64` (<= 65535), so it is at most 1024 --
1397 // always representable in `u32` -- and this cast is total.
1398 let word_count = word_count_u64 as u32;
1399 // SAFETY: same as the `alloc_wide_words` call above -- one
1400 // local, single-owned `arena`, sequential calls only.
1401 let (class, offset) = if word_count <= 1 {
1402 (SlotClass::Inline, unsafe { arena.alloc_inline_word() })
1403 } else {
1404 (SlotClass::Wide, unsafe {
1405 arena.alloc_wide_words(word_count)
1406 })
1407 };
1408 // ST-063/ST-127: the elaborated initial value, obtained by
1409 // constant-folding `initializer()` (the prefold cache --
1410 // `Compilation::compile()` already runs with prefolding on,
1411 // so this is a pure read) through Task 1's
1412 // `four_state_of_constant`/`four_state_to_owned_svint`, never
1413 // a locally re-derived evaluator. Absence (no initializer, or
1414 // a non-integer-constant-foldable one) stays `None`, never a
1415 // fabricated zero.
1416 //
1417 // CONTRACTS D4: `SlotDescriptor.initial` is the sv-lang-free
1418 // `FourStateValue`, so the constant is resized to the declared
1419 // width/signedness through `OwnedSVInt` (unchanged semantics)
1420 // and converted back here, once. `None` from the back
1421 // conversion (`owned_svint_to_four_state`) resets to all-X,
1422 // exactly as instance creation did before.
1423 //
1424 // A checker's output formal (`output int ctr = 0`, LRM
1425 // 17.2) takes its initial value from the instantiation's
1426 // connection, with a variable's initialization semantics
1427 // (6.8); a non-constant one is rejected, never dropped.
1428 let checker_initial = crate::netlist::checker_output_of(&member)
1429 .and_then(|o| o.initial)
1430 .map(|init| {
1431 if init.constant_value().is_some() {
1432 Ok(init)
1433 } else {
1434 Err(LayoutError::NonConstantCheckerOutputInitial(
1435 member.name().to_string(),
1436 ))
1437 }
1438 })
1439 .transpose()?;
1440 let initial = crate::netlist::variable_initializer(&member)
1441 .or(checker_initial)
1442 .and_then(|expr| expr.constant_value())
1443 .and_then(|cv| four_state_of_constant(&cv))
1444 .and_then(|fv| {
1445 owned_svint_to_four_state(&four_state_to_owned_svint(
1446 &fv, bit_width, is_signed,
1447 ))
1448 });
1449 // 17.7.2: a free checker variable with no initializer starts at
1450 // an unconstrained random value (of its own, deterministic
1451 // stream: the same run gives the same values).
1452 let initial = initial.or_else(|| {
1453 crate::netlist::free_var_kind(&member).map(|_| {
1454 let mut state = crate::netlist::free_var_seed(&member);
1455 let mut words: Vec<u64> = (0..word_count.max(1))
1456 .map(|_| crate::sched::process_rng::splitmix64_next(&mut state))
1457 .collect();
1458 if bit_width % 64 != 0
1459 && let Some(last) = words.get_mut(((bit_width - 1) / 64) as usize)
1460 {
1461 *last &= (1u64 << (bit_width % 64)) - 1;
1462 }
1463 if class == SlotClass::Inline {
1464 FourStateValue::Narrow(u128::from(words[0]))
1465 } else {
1466 FourStateValue::Wide {
1467 bval: vec![0; words.len()],
1468 aval: words,
1469 }
1470 }
1471 })
1472 });
1473 // A net with no driver is Z (LRM 1800-2023 §6.6.1); its
1474 // drivers overwrite that from time 0, and the bits nothing
1475 // drives keep it. A `trireg` starts at X instead (§6.6.4.1:
1476 // its initial charge is unknown); `tri0`/`tri1`/`supply*`
1477 // pull through net resolution (`vj`'s `nets`).
1478 let initial = initial.or_else(|| {
1479 (member.kind() == sv_lang::kinds::SymbolKind::Net
1480 && ty.is_four_state()
1481 && member.net_charge_strength().is_none())
1482 .then(|| {
1483 super::value::bits_to_four_state(&vec![sv_lang::Bit::Z; bit_width as usize])
1484 })
1485 });
1486 // A 2-state variable with no initializer starts at 0 (LRM
1487 // 1800-2023 §6.8, Table 6-7), not X.
1488 let initial = initial.or_else(|| {
1489 // ... except a tagged union (or an aggregate of one),
1490 // which is undefined, tag included (7.3.2): X.
1491 (!ty.is_four_state() && !super::storage::contains_tagged_union(&ty)).then(
1492 || {
1493 if class == SlotClass::Inline {
1494 FourStateValue::Narrow(0)
1495 } else {
1496 FourStateValue::Wide {
1497 aval: vec![0; word_count as usize],
1498 bval: vec![0; word_count as usize],
1499 }
1500 }
1501 },
1502 )
1503 });
1504 // review fix r1: `word_count` was already computed above and
1505 // correctly used to call `arena.alloc_wide_words(word_count)`
1506 // for the Wide class, but the tuple's `elem_word_count`
1507 // position was left hardcoded at `0u32` instead of threading
1508 // it through -- a real bug, since design.md's Wide
1509 // read/write contract slices `[word_offset,
1510 // word_offset+elem_word_count)` into `wide_aval`/`wide_bval`,
1511 // so a Wide slot with `elem_word_count == 0` addresses a
1512 // zero-length window into its own storage. `elem_word_count`
1513 // is documented as unused by Inline's addressing (state.md
1514 // §"Inline read/write" never references it), so a nonzero,
1515 // correctly-derived value here is harmless for Inline and
1516 // correct for Wide.
1517 // `word_count` is the same, already-bounded (<= 1024) value
1518 // computed above, so this cast is total, not a guess --
1519 // see the comment on `word_count_u64`'s derivation.
1520 let elem_word_count = word_count;
1521 (
1522 class,
1523 offset,
1524 bit_width,
1525 elem_word_count,
1526 0u32,
1527 0u32,
1528 initial,
1529 )
1530 };
1531
1532 let key = SymbolKey(next_key);
1533 next_key += 1;
1534
1535 // The hidden sampled-value cells below clone this slot's shape.
1536 slot_of_symbol.insert(member.uid(), slots.len());
1537 slots.push(SlotDescriptor {
1538 key,
1539 offset: SlotOffset {
1540 class,
1541 word_offset,
1542 bit_width,
1543 elem_word_count,
1544 row_stride_words,
1545 num_rows,
1546 },
1547 initial,
1548 net_info: None::<NetLayout>,
1549 });
1550 }
1551
1552 // A clocking block (LRM 1800-2023 §14): a 32-bit counter its sampler
1553 // bumps on each clocking event (what `@(cb)` and `##N` wait on), then
1554 // one sample cell per input clock variable. After every other slot, in
1555 // the order `vj-codegen::module::signal_slots_of` lists them.
1556 for (cb, vars) in clocking_blocks(body) {
1557 let _ = cb;
1558 // SAFETY: as for the slots above.
1559 let offset = unsafe { arena.alloc_inline_word() };
1560 slots.push(SlotDescriptor {
1561 key: SymbolKey(next_key),
1562 offset: SlotOffset {
1563 class: SlotClass::Inline,
1564 word_offset: offset,
1565 bit_width: 32,
1566 elem_word_count: 1,
1567 row_stride_words: 0,
1568 num_rows: 0,
1569 },
1570 initial: Some(FourStateValue::Narrow(0)),
1571 net_info: None,
1572 });
1573 next_key += 1;
1574 for v in vars {
1575 let ty = v
1576 .value_type()
1577 .filter(|t| t.is_integral())
1578 .ok_or_else(|| LayoutError::UnsupportedType(v.name().to_string()))?;
1579 let width = checked_bit_width(ty.bit_width(), v.name())?;
1580 let words = width.div_ceil(64).max(1);
1581 // SAFETY: as for the slots above.
1582 let (class, offset) = if words <= 1 {
1583 (SlotClass::Inline, unsafe { arena.alloc_inline_word() })
1584 } else {
1585 (SlotClass::Wide, unsafe { arena.alloc_wide_words(words) })
1586 };
1587 slots.push(SlotDescriptor {
1588 key: SymbolKey(next_key),
1589 offset: SlotOffset {
1590 class,
1591 word_offset: offset,
1592 bit_width: width,
1593 elem_word_count: words,
1594 row_stride_words: 0,
1595 num_rows: 0,
1596 },
1597 initial: (!ty.is_four_state()).then(|| {
1598 super::value::bits_to_four_state(&vec![sv_lang::Bit::Zero; width as usize])
1599 }),
1600 net_info: None,
1601 });
1602 next_key += 1;
1603 }
1604 }
1605
1606 // The sampled value functions' hidden cells (`crate::sva::sampled`): one
1607 // clone of the source signal's slot per P/S cell, after everything else,
1608 // in the order `vj-codegen::module::signal_slots_of` lists them. A
1609 // cell starts at its source's initial value, which is the default
1610 // sampled value (LRM 16.5.1) the first ticks read.
1611 let hidden = crate::sva::sampled::hidden_slots(body)
1612 .map_err(|e| LayoutError::UnsupportedType(format!("sampled value function: {e}")))?;
1613 for cell in hidden.slots() {
1614 let (class, words, bit_width, initial) = if let Some(shape) = &cell.foreign {
1615 // The history of an enclosing instance's signal (a global
1616 // clocking's event, LRM 14.14): no local slot to clone.
1617 let words = shape.width.div_ceil(64).max(1);
1618 let class = if words > 1 {
1619 SlotClass::Wide
1620 } else {
1621 SlotClass::Inline
1622 };
1623 (class, words, shape.width, shape.initial.clone())
1624 } else {
1625 let src = slot_of_symbol.get(&cell.source).ok_or_else(|| {
1626 LayoutError::UnsupportedType(format!(
1627 "sampled value function over `{}`, which has no state slot",
1628 cell.source
1629 ))
1630 })?;
1631 let s = &slots[*src];
1632 (
1633 s.offset.class,
1634 s.offset.elem_word_count,
1635 s.offset.bit_width,
1636 s.initial.clone(),
1637 )
1638 };
1639 // SAFETY: as for the slots above.
1640 let word_offset = match class {
1641 SlotClass::Inline => unsafe { arena.alloc_inline_word() },
1642 SlotClass::Wide => unsafe { arena.alloc_wide_words(words) },
1643 other => {
1644 return Err(LayoutError::UnsupportedType(format!(
1645 "sampled value function over `{}`, a {other:?} slot",
1646 cell.source
1647 )));
1648 }
1649 };
1650 slots.push(SlotDescriptor {
1651 key: SymbolKey(next_key),
1652 offset: SlotOffset {
1653 class,
1654 word_offset,
1655 bit_width,
1656 elem_word_count: words,
1657 row_stride_words: 0,
1658 num_rows: 0,
1659 },
1660 initial,
1661 net_info: None,
1662 });
1663 next_key += 1;
1664 }
1665
1666 // The end-point watcher cells (`crate::sva::endpoint`): one event-shaped,
1667 // 2-state cell per instance a procedure reads `.triggered`/`@(s)` of, after
1668 // the hidden cells, in the order `vj-codegen::module::signal_slots_of`
1669 // lists them.
1670 for _ in crate::sva::endpoint::watchers(body) {
1671 let words = super::EVENT_CELL_WIDTH.div_ceil(64);
1672 // SAFETY: as for the slots above.
1673 let word_offset = unsafe { arena.alloc_wide_words(words) };
1674 slots.push(SlotDescriptor {
1675 key: SymbolKey(next_key),
1676 offset: SlotOffset {
1677 class: SlotClass::Wide,
1678 word_offset,
1679 bit_width: super::EVENT_CELL_WIDTH,
1680 elem_word_count: words,
1681 row_stride_words: 0,
1682 num_rows: 0,
1683 },
1684 initial: Some(FourStateValue::Wide {
1685 aval: vec![0; words as usize],
1686 bval: vec![0; words as usize],
1687 }),
1688 net_info: None,
1689 });
1690 next_key += 1;
1691 }
1692
1693 // The event pool (`super::event_pool`): one hidden memory of event cells
1694 // after everything else, when the body needs event handles, mirrored
1695 // last in `vj-codegen::module::signal_slots_of`.
1696 if let Some(pool) = super::event_pool::event_pool(body) {
1697 let words_per_row = super::EVENT_CELL_WIDTH.div_ceil(64);
1698 let rows = pool.total_rows();
1699 // SAFETY: as for the slots above.
1700 let word_offset = unsafe { arena.alloc_wide_words(words_per_row * rows) };
1701 slots.push(SlotDescriptor {
1702 key: SymbolKey(next_key),
1703 offset: SlotOffset {
1704 class: SlotClass::Memory,
1705 word_offset,
1706 bit_width: super::EVENT_CELL_WIDTH,
1707 elem_word_count: words_per_row,
1708 row_stride_words: words_per_row,
1709 num_rows: rows,
1710 },
1711 initial: Some(FourStateValue::Narrow(0)),
1712 net_info: None,
1713 });
1714 }
1715
1716 Ok(StateLayout {
1717 inline_words: arena.inline().len() as u32,
1718 // Task 6 is this file's first user of `Arena::alloc_wide_words`
1719 // (Wide/Memory-class slots) -- Task 4's stub hardcoded `0` here
1720 // because it never allocated a wide word; now that wide allocation
1721 // is real, this must reflect it, or Task 7's `template_size`
1722 // (ST-135) would under-report every template with a >64-bit or
1723 // memory member.
1724 wide_words: arena.wide_aval().len() as u32,
1725 slots,
1726 })
1727}
1728
1729#[cfg(test)]
1730mod tests {
1731 use super::*;
1732 use crate::state::FourStateValue;
1733 use crate::state::value::compile_source;
1734
1735 /// ST-058: calling the layout builder twice on the *same* `Symbol` must
1736 /// produce structurally equal `StateLayout`s (slot count, offsets, key
1737 /// order) and must not mutate anything shared between the calls. Fails
1738 /// if any internal counter/cache leaked across calls instead of being
1739 /// function-local.
1740 #[test]
1741 fn layout_builder_is_read_only_and_deterministic_on_repeat_calls() {
1742 let design = compile_source("module m; logic [7:0] x; wire w; endmodule\n")
1743 .expect("valid SV source compiles");
1744 let body = design
1745 .top_instances()
1746 .next()
1747 .expect("one top instance")
1748 .instance_body()
1749 .expect("instance has a body");
1750
1751 let first = build_template_layout(&body).expect("layout builds");
1752 let second = build_template_layout(&body).expect("layout builds again");
1753
1754 assert_eq!(first.slots.len(), second.slots.len());
1755 assert_eq!(first.inline_words, second.inline_words);
1756 assert_eq!(first.wide_words, second.wide_words);
1757 for (a, b) in first.slots.iter().zip(second.slots.iter()) {
1758 assert_eq!(a.key, b.key);
1759 assert_eq!(a.offset.class, b.offset.class);
1760 assert_eq!(a.offset.word_offset, b.offset.word_offset);
1761 assert_eq!(a.offset.bit_width, b.offset.bit_width);
1762 }
1763 }
1764
1765 /// Sampled value functions append one hidden slot per P/S cell, cloning
1766 /// the source's shape and initial value (the default sampled value,
1767 /// LRM 16.5.1), after every ordinary slot.
1768 #[test]
1769 fn sampled_value_calls_append_cloned_hidden_slots() {
1770 let src = "module top; logic clk; logic [69:0] w; int n;\n\
1771 always @(posedge clk) begin n <= $past(n, 2); if ($rose(w)) n <= 0; end\n\
1772 endmodule\n";
1773 let plain = compile_source("module top; logic clk; logic [69:0] w; int n; endmodule\n")
1774 .expect("compiles");
1775 let design = compile_source(src).expect("compiles");
1776 let body = design
1777 .top_instances()
1778 .next()
1779 .unwrap()
1780 .instance_body()
1781 .unwrap();
1782 let base = build_template_layout(
1783 &plain
1784 .top_instances()
1785 .next()
1786 .unwrap()
1787 .instance_body()
1788 .unwrap(),
1789 )
1790 .expect("base layout");
1791 let hidden = crate::sva::sampled::hidden_slots(&body).expect("hidden");
1792 let layout = build_template_layout(&body).expect("layout");
1793 assert_eq!(
1794 layout.slots.len(),
1795 base.slots.len() + hidden.slots().count()
1796 );
1797 // `n` is a 2-state int: slot 2; its P cell is initialised to 0, and a
1798 // 4-state `w` clone is a Wide slot of the same width.
1799 let n_slots: Vec<_> = layout.slots[base.slots.len()..]
1800 .iter()
1801 .filter(|s| s.offset.bit_width == 32)
1802 .collect();
1803 assert_eq!(n_slots.len(), 3, "P + two S cells of n");
1804 for s in n_slots {
1805 assert!(
1806 matches!(s.initial, Some(FourStateValue::Narrow(0))),
1807 "{:?}",
1808 s.initial
1809 );
1810 }
1811 let w_slots: Vec<_> = layout.slots[base.slots.len()..]
1812 .iter()
1813 .filter(|s| s.offset.bit_width == 70)
1814 .collect();
1815 assert_eq!(w_slots.len(), 2, "P + S_0 of w");
1816 assert!(w_slots.iter().all(|s| s.offset.class == SlotClass::Wide));
1817 // Offsets are distinct from every other slot's.
1818 let mut seen = std::collections::HashSet::new();
1819 for s in &layout.slots {
1820 assert!(seen.insert((s.offset.class as u8, s.offset.word_offset)));
1821 }
1822 }
1823
1824 /// ST-060: a module containing a genvar, a parameter, a variable, a net,
1825 /// and a port must produce slots only for the variable/net/port. Fails
1826 /// if the filter accepts `Parameter`/`Genvar` too (over-inclusion) or
1827 /// drops `Port` (under-inclusion).
1828 ///
1829 /// **Deviation from the plan's literal fixture text:** the plan's source
1830 /// text (`input logic p_in;` re-declared with no matching entry in the
1831 /// module's port list) does not actually create a `Port`-kind symbol --
1832 /// verified against real `sv-lang=0.1.9` output, it produces a
1833 /// diagnostic ("port declaration 'p_in' does not match any port in the
1834 /// module's port list") and `p_in` elaborates as a plain `Variable`
1835 /// instead, so the original text would only ever exercise 2 filtered
1836 /// members even from a correct implementation. `p_in` is declared here
1837 /// as a genuine ANSI port (in the module's own port list) instead, which
1838 /// does produce a real `Port`-kind member while keeping every other part
1839 /// of the fixture (a genvar, a parameter, a variable, a net) unchanged.
1840 #[test]
1841 fn only_variable_net_port_kinds_produce_slots() {
1842 let design = compile_source(
1843 "module m #(parameter int P = 1) (input logic p_in); genvar i; logic x; wire w; endmodule\n",
1844 )
1845 .expect("valid SV source compiles");
1846 let body = design
1847 .top_instances()
1848 .next()
1849 .expect("one top instance")
1850 .instance_body()
1851 .expect("instance has a body");
1852
1853 let layout = build_template_layout(&body).expect("layout builds");
1854 assert_eq!(layout.slots.len(), 3);
1855 }
1856
1857 /// Classes plan Task 4 (`docs/superpowers/plans/2026-10-03-classes.md`,
1858 /// LRM 8.9): a class's `static` `ClassProperty` member gets a slot
1859 /// exactly like a module `Variable`; its *instance* (automatic-lifetime)
1860 /// property and its method are both excluded -- they belong to the
1861 /// object heap (Task 3/5), never this arena.
1862 #[test]
1863 fn only_static_class_properties_produce_slots() {
1864 let design = compile_source(
1865 "class Counter; static int count; int id; \
1866 function void bump(); id = id + 1; endfunction endclass\n\
1867 module m; Counter c; endmodule\n",
1868 )
1869 .expect("valid SV source compiles");
1870 let body = design
1871 .top_instances()
1872 .next()
1873 .expect("one top instance")
1874 .instance_body()
1875 .expect("instance has a body");
1876 let class_sym = body
1877 .find("c")
1878 .expect("c declared")
1879 .value_type()
1880 .expect("c has a type")
1881 .as_symbol();
1882
1883 let layout = build_template_layout(&class_sym).expect("layout builds");
1884 assert_eq!(
1885 layout.slots.len(),
1886 1,
1887 "only the static property `count` gets a slot -- not `id` (an \
1888 instance field) and not `bump` (a method)"
1889 );
1890 assert_eq!(layout.slots[0].offset.bit_width, 32);
1891 }
1892
1893 /// A `static const` class property's constant initializer is baked
1894 /// directly into the slot's own `initial` value at layout-build time --
1895 /// "evaluated once before the start of simulation" (8.9) is satisfied by
1896 /// construction, never by re-running an initializer expression on every
1897 /// read (there is no such expression-evaluation path in this builder at
1898 /// all, for a class property any more than for a module variable).
1899 #[test]
1900 fn static_const_initializer_is_baked_into_the_slot() {
1901 let design = compile_source(
1902 "class Counter; static const int FIVE = 5; endclass\n\
1903 module m; Counter c; endmodule\n",
1904 )
1905 .expect("valid SV source compiles");
1906 let body = design
1907 .top_instances()
1908 .next()
1909 .expect("one top instance")
1910 .instance_body()
1911 .expect("instance has a body");
1912 let class_sym = body
1913 .find("c")
1914 .expect("c declared")
1915 .value_type()
1916 .expect("c has a type")
1917 .as_symbol();
1918
1919 let layout = build_template_layout(&class_sym).expect("layout builds");
1920 assert_eq!(layout.slots.len(), 1);
1921 match &layout.slots[0].initial {
1922 Some(FourStateValue::Narrow(v)) => assert_eq!(*v, 5),
1923 other => panic!("expected a baked-in Narrow(5) initial value, got {other:?}"),
1924 }
1925 }
1926
1927 /// ST-072: genvars, parameters, and subroutine-local automatic variables
1928 /// must never become slots -- only the one real signal in the module
1929 /// does. `local_v` is never even visited: `members()` on the module body
1930 /// does not descend into a subroutine's own body.
1931 #[test]
1932 fn genvars_params_and_subroutine_locals_never_become_slots() {
1933 let design = compile_source(
1934 "module m; parameter int P = 1; genvar i; \
1935 function automatic int f(int a); int local_v; return a; endfunction; \
1936 logic only_real_signal; endmodule\n",
1937 )
1938 .expect("valid SV source compiles");
1939 let body = design
1940 .top_instances()
1941 .next()
1942 .expect("one top instance")
1943 .instance_body()
1944 .expect("instance has a body");
1945
1946 let layout = build_template_layout(&body).expect("layout builds");
1947 assert_eq!(layout.slots.len(), 1);
1948
1949 // Test-local lookup (SlotDescriptor deliberately carries no name --
1950 // ST-153): replicate the production {Variable,Net,Port} filter here
1951 // to identify the one real member independently and confirm it is
1952 // `only_real_signal`, not `P`/`i`/`local_v`.
1953 let members: Vec<_> = body
1954 .members()
1955 .filter(|s| {
1956 matches!(
1957 s.kind(),
1958 SymbolKind::Variable | SymbolKind::Net | SymbolKind::Port
1959 )
1960 })
1961 .collect();
1962 assert_eq!(members.len(), 1);
1963 assert_eq!(members[0].name(), "only_real_signal");
1964 }
1965
1966 /// A `Subroutine` member (a kind this function's filter does not accept
1967 /// among `{Variable,Net,Port}`) is silently -- and correctly -- excluded
1968 /// by the filter itself, never a `LayoutError`: subroutines aren't
1969 /// state. `LayoutError::UnsupportedMemberKind` is reserved for a kind
1970 /// the filter *does* accept but that fails to yield a queryable shape;
1971 /// this test proves that variant is constructible and carries the
1972 /// offending kind + name (a compile-level proof, mirroring Task 3's
1973 /// `state_layout_has_exactly_the_three_contracted_fields` -- the runtime
1974 /// half below proves the filter's exclusion is real, not merely that the
1975 /// error type happens to compile).
1976 #[test]
1977 fn unsupported_member_kind_is_a_loud_typed_error_not_a_silent_skip() {
1978 let design = compile_source(
1979 "module m; function automatic int f(int a); return a; endfunction; \
1980 logic ok_signal; endmodule\n",
1981 )
1982 .expect("valid SV source compiles");
1983 let body = design
1984 .top_instances()
1985 .next()
1986 .expect("one top instance")
1987 .instance_body()
1988 .expect("instance has a body");
1989
1990 // The subroutine is excluded by the filter -- no panic, no error,
1991 // no slot for it -- and the one real signal still gets a slot.
1992 let layout = build_template_layout(&body).expect("no unsupported member kind reached");
1993 assert_eq!(layout.slots.len(), 1);
1994
1995 let members: Vec<_> = body
1996 .members()
1997 .filter(|s| s.kind() == SymbolKind::Subroutine)
1998 .collect();
1999 assert_eq!(
2000 members.len(),
2001 1,
2002 "expected exactly one Subroutine-kind member for this fixture"
2003 );
2004
2005 // LayoutError::UnsupportedMemberKind is constructible and carries
2006 // the offending kind + name -- the loud, typed error this task and
2007 // every later layout task return instead of a silent skip or a
2008 // panic, for a kind the filter accepts but that fails to yield a
2009 // queryable shape.
2010 let err = LayoutError::UnsupportedMemberKind(SymbolKind::Variable, "x".to_string());
2011 match err {
2012 LayoutError::UnsupportedMemberKind(kind, name) => {
2013 assert_eq!(kind, SymbolKind::Variable);
2014 assert_eq!(name, "x");
2015 }
2016 LayoutError::UnsupportedType(name)
2017 | LayoutError::NonConstantCheckerOutputInitial(name) => {
2018 panic!("unexpected {name} error for this constructed value")
2019 }
2020 }
2021 }
2022
2023 /// ST-072 applied to a null port: the middle, dot-less position of
2024 /// `module m(a, , c);` elaborates as a real `Port`-kind member of
2025 /// `body` (`port_internal_symbol()` and `port_internal_expr()` both
2026 /// `None`, `port_is_null_port()` true) but carries no value at all --
2027 /// it must never become a slot. Before the fix, this member fell
2028 /// through the `port_internal_symbol().is_some()` skip (which only
2029 /// catches a port *with* something to alias) straight into
2030 /// `member.port_type()`, which -- verified against real
2031 /// `sv-lang=0.1.9` output -- still returns `Some(Type)` for a null
2032 /// port rather than `None`, so the old code emitted a phantom
2033 /// `SlotDescriptor` with `bit_width == 0` for it: 3 slots instead of
2034 /// 2, with one carrying no real signal. This is also the only test
2035 /// in this file that reaches the `port_type()` branch through a
2036 /// `Port`-kind member that is *not* immediately skipped by the
2037 /// internal-symbol check -- every other fixture's ports all have
2038 /// plain internal symbols and are skipped before that call.
2039 #[test]
2040 fn null_ports_never_become_slots() {
2041 let design = compile_source("module m(a, , c); input a; input c; endmodule\n")
2042 .expect("valid SV source compiles");
2043 let body = design
2044 .top_instances()
2045 .next()
2046 .expect("one top instance")
2047 .instance_body()
2048 .expect("instance has a body");
2049
2050 // Sanity: the middle port position is a real Port-kind member with
2051 // no internal symbol, confirming this fixture actually exercises
2052 // the null-port path rather than silently degenerating to 2 ports.
2053 let ports: Vec<_> = body
2054 .members()
2055 .filter(|s| s.kind() == SymbolKind::Port)
2056 .collect();
2057 assert_eq!(ports.len(), 3, "expected 3 Port-kind members (a, null, c)");
2058 assert_eq!(
2059 ports.iter().filter(|p| p.port_is_null_port()).count(),
2060 1,
2061 "expected exactly one null port"
2062 );
2063
2064 let layout = build_template_layout(&body).expect("layout builds");
2065 assert_eq!(
2066 layout.slots.len(),
2067 2,
2068 "only the two real ports (a, c) should produce slots -- the null port must not"
2069 );
2070 for slot in &layout.slots {
2071 assert!(
2072 slot.offset.bit_width > 0,
2073 "no slot may be a phantom zero-width slot for the null port"
2074 );
2075 }
2076 }
2077
2078 // -- Task 6: per-member shape derivation, packed-struct flatten, initial
2079 // value (ledger ST-061, ST-062, ST-063, ST-127). --
2080
2081 /// ST-061: a signed 17-bit variable must get `bit_width == 17` exactly,
2082 /// not 18/32/64 -- the concrete regression a buggy sign-aware width
2083 /// calculation would introduce.
2084 #[test]
2085 fn signed_17bit_variable_gets_correct_width_not_sign_extended_into_18bits() {
2086 let design = compile_source("module m; logic signed [16:0] x; endmodule\n")
2087 .expect("valid SV source compiles");
2088 let body = design
2089 .top_instances()
2090 .next()
2091 .expect("one top instance")
2092 .instance_body()
2093 .expect("instance has a body");
2094
2095 let layout = build_template_layout(&body).expect("layout builds");
2096 assert_eq!(layout.slots.len(), 1);
2097 assert_eq!(layout.slots[0].offset.class, SlotClass::Inline);
2098 assert_eq!(layout.slots[0].offset.bit_width, 17);
2099 }
2100
2101 /// ST-061: an 8-deep memory of 32-bit rows must get `SlotClass::Memory`
2102 /// with `elem_word_count == 1`, `row_stride_words == 1`, and
2103 /// `num_rows == 8` -- the `8` is the concrete proof `num_rows` was
2104 /// actually derived from `fixed_unpacked_array_range()`'s
2105 /// `{left:0,right:7}` (`|0-7|+1`), not left at a placeholder/zero.
2106 ///
2107 /// (Rust identifiers cannot start with a digit -- the plan's literal test
2108 /// name `8deep_memory_...` is adapted to `eight_deep_memory_...`, a
2109 /// mechanical syntax accommodation, not a behavioral deviation.)
2110 #[test]
2111 fn eight_deep_memory_of_32bit_rows_gets_memory_class_with_correct_stride_and_rows() {
2112 let design = compile_source("module m; logic [31:0] mem [0:7]; endmodule\n")
2113 .expect("valid SV source compiles");
2114 let body = design
2115 .top_instances()
2116 .next()
2117 .expect("one top instance")
2118 .instance_body()
2119 .expect("instance has a body");
2120
2121 let layout = build_template_layout(&body).expect("layout builds");
2122 assert_eq!(layout.slots.len(), 1);
2123 let offset = layout.slots[0].offset;
2124 assert_eq!(offset.class, SlotClass::Memory);
2125 assert_eq!(offset.elem_word_count, 1);
2126 assert_eq!(offset.row_stride_words, 1);
2127 assert_eq!(offset.num_rows, 8);
2128 }
2129
2130 /// LRM 1800-2023 15.5 / 6.17: an unpacked array of named events is a
2131 /// Memory slot whose rows are the 128-bit event cell (two words), one row
2132 /// per element across every dimension.
2133 #[test]
2134 fn event_array_lays_out_as_memory_of_event_cells() {
2135 let design = compile_source("module m; event ea [0:2]; event e2 [1:2][0:1]; endmodule\n")
2136 .expect("valid SV source compiles");
2137 let body = design
2138 .top_instances()
2139 .next()
2140 .expect("one top instance")
2141 .instance_body()
2142 .expect("instance has a body");
2143
2144 let layout = build_template_layout(&body).expect("layout builds");
2145 assert_eq!(layout.slots.len(), 2);
2146 let (flat, multi) = (&layout.slots[0].offset, &layout.slots[1].offset);
2147 for (offset, rows) in [(flat, 3), (multi, 4)] {
2148 assert_eq!(offset.class, SlotClass::Memory);
2149 assert_eq!(offset.bit_width, super::super::EVENT_CELL_WIDTH);
2150 assert_eq!(offset.elem_word_count, 2);
2151 assert_eq!(offset.row_stride_words, 2);
2152 assert_eq!(offset.num_rows, rows);
2153 }
2154 }
2155
2156 /// ST-062: a packed struct with 3 fields totaling 40 bits must flatten to
2157 /// exactly one slot of width 40, not three. Fails if the builder recurses
2158 /// into `fields()` and emits one `SlotDescriptor` per field instead of
2159 /// flattening.
2160 #[test]
2161 fn packed_struct_flattens_to_exactly_one_slot_of_total_width() {
2162 let design = compile_source(
2163 "module m; typedef struct packed { logic [7:0] a; logic [15:0] b; \
2164 logic [15:0] c; } s_t; s_t x; endmodule\n",
2165 )
2166 .expect("valid SV source compiles");
2167 let body = design
2168 .top_instances()
2169 .next()
2170 .expect("one top instance")
2171 .instance_body()
2172 .expect("instance has a body");
2173
2174 let layout = build_template_layout(&body).expect("layout builds");
2175 assert_eq!(
2176 layout.slots.len(),
2177 1,
2178 "expected one flattened slot, not one per field"
2179 );
2180 assert_eq!(layout.slots[0].offset.bit_width, 40);
2181 assert_eq!(layout.slots[0].offset.class, SlotClass::Inline);
2182 }
2183
2184 /// ST-063/ST-127: a variable declared with an initializer must have its
2185 /// constant-folded value threaded into `SlotDescriptor.initial`; a
2186 /// variable with no initializer must stay `None`, never a fabricated
2187 /// zero -- proving the absence-vs-present distinction is preserved.
2188 #[test]
2189 fn initializer_constant_folds_into_slot_initial_value() {
2190 let design = compile_source("module m; logic [7:0] x = 8'hAA; logic [7:0] y; endmodule\n")
2191 .expect("valid SV source compiles");
2192 let body = design
2193 .top_instances()
2194 .next()
2195 .expect("one top instance")
2196 .instance_body()
2197 .expect("instance has a body");
2198
2199 let layout = build_template_layout(&body).expect("layout builds");
2200 assert_eq!(layout.slots.len(), 2);
2201
2202 let x_initial = layout.slots[0]
2203 .initial
2204 .as_ref()
2205 .expect("x has an initializer and must have Some(initial)");
2206 assert!(
2207 matches!(x_initial, FourStateValue::Narrow(0xAA)),
2208 "x's initializer must fold to 8'hAA with no unknown bits, got {x_initial:?}"
2209 );
2210
2211 assert!(
2212 layout.slots[1].initial.is_none(),
2213 "y has no initializer and must stay None, never a fabricated zero"
2214 );
2215 }
2216
2217 /// Closes the interpretation question left open by this task's public
2218 /// names note: sign is genuinely not a storage-shape input.
2219 /// `logic [7:0] u;` and `logic signed [7:0] s;` must produce identical
2220 /// `bit_width`/`class` shape.
2221 #[test]
2222 fn unsigned_and_signed_same_width_produce_identical_slot_shape() {
2223 let design = compile_source("module m; logic [7:0] u; logic signed [7:0] s; endmodule\n")
2224 .expect("valid SV source compiles");
2225 let body = design
2226 .top_instances()
2227 .next()
2228 .expect("one top instance")
2229 .instance_body()
2230 .expect("instance has a body");
2231
2232 let layout = build_template_layout(&body).expect("layout builds");
2233 assert_eq!(layout.slots.len(), 2);
2234 assert_eq!(
2235 layout.slots[0].offset.bit_width,
2236 layout.slots[1].offset.bit_width
2237 );
2238 assert_eq!(layout.slots[0].offset.class, layout.slots[1].offset.class);
2239 }
2240
2241 /// A queue, dynamic array or associative array member is a 2-state
2242 /// 64-bit Inline slot holding a collection handle (`crate::
2243 /// collections`), empty (0) initially; the ordinary signal beside it
2244 /// keeps its own shape.
2245 #[test]
2246 fn a_collection_member_is_a_64_bit_handle_slot_starting_empty() {
2247 for src in [
2248 "module m; int q[$]; logic [7:0] ok_signal; endmodule\n",
2249 "module m; int d[]; logic [7:0] ok_signal; endmodule\n",
2250 "module m; int aa[string]; logic [7:0] ok_signal; endmodule\n",
2251 ] {
2252 let design = compile_source(src).expect("valid SV source compiles");
2253 let body = design
2254 .top_instances()
2255 .next()
2256 .expect("one top instance")
2257 .instance_body()
2258 .expect("instance has a body");
2259 let layout = build_template_layout(&body).expect("a collection member lays out");
2260 assert_eq!(layout.slots.len(), 2, "{src}");
2261 assert_eq!(layout.slots[0].offset.class, SlotClass::Inline);
2262 assert_eq!(layout.slots[0].offset.bit_width, 64);
2263 assert!(matches!(
2264 layout.slots[0].initial,
2265 Some(FourStateValue::Narrow(0))
2266 ));
2267 assert_eq!(layout.slots[1].offset.bit_width, 8);
2268 }
2269 }
2270
2271 /// A queue's constant initializer is a frozen collection holding its
2272 /// elements, shared by every instance until one changes it.
2273 #[test]
2274 fn a_queue_initializer_is_a_frozen_collection_of_its_elements() {
2275 let design = compile_source("module m; int q[$] = {4, 5, 6}; endmodule\n")
2276 .expect("valid SV source compiles");
2277 let body = design
2278 .top_instances()
2279 .next()
2280 .expect("one top instance")
2281 .instance_body()
2282 .expect("instance has a body");
2283 let layout = build_template_layout(&body).expect("lays out");
2284 let Some(FourStateValue::Narrow(h)) = layout.slots[0].initial else {
2285 panic!("expected a handle initial value");
2286 };
2287 let h = h as u64;
2288 assert_eq!(crate::collections::size(h), 3);
2289 let mut out = [0u64];
2290 assert!(crate::collections::get(h, 2, &mut out));
2291 assert_eq!(out[0], 6);
2292 }
2293
2294 // -- Task 6 review fixes r1 --
2295
2296 /// review fix r1: a >64-bit non-array (Wide-class) member must get a
2297 /// correctly-derived `elem_word_count`, not the hardcoded `0` the
2298 /// tuple's Wide arm previously returned. A 128-bit variable needs
2299 /// exactly 2 words; before the fix this was `0`, which -- per
2300 /// design.md's Wide addressing contract -- slices a zero-length window
2301 /// `[word_offset, word_offset)` into `wide_aval`/`wide_bval`, so
2302 /// `sv_get_wide`/`sv_set_wide` would never touch any real storage for
2303 /// this signal.
2304 #[test]
2305 fn wide_128bit_scalar_gets_correct_elem_word_count_not_hardcoded_zero() {
2306 let design = compile_source("module m; logic [127:0] w; endmodule\n")
2307 .expect("valid SV source compiles");
2308 let body = design
2309 .top_instances()
2310 .next()
2311 .expect("one top instance")
2312 .instance_body()
2313 .expect("instance has a body");
2314
2315 let layout = build_template_layout(&body).expect("layout builds");
2316 assert_eq!(layout.slots.len(), 1);
2317 let offset = layout.slots[0].offset;
2318 assert_eq!(offset.class, SlotClass::Wide);
2319 assert_eq!(offset.bit_width, 128);
2320 assert_eq!(
2321 offset.elem_word_count, 2,
2322 "a 128-bit Wide slot must span exactly 2 wide words, not 0"
2323 );
2324 }
2325
2326 /// review fix r1: a packed union with 2+ fields must not panic the
2327 /// packed-struct/union flatten invariant. Per IEEE 1800, a packed
2328 /// union's own `bit_width()` equals one (shared) field width, not the
2329 /// sum of every field's width -- before the fix, the sum-based
2330 /// `debug_assert_eq!` fired on this exact, ordinary construct in any
2331 /// debug build (including `cargo test`).
2332 #[test]
2333 fn packed_union_with_multiple_fields_does_not_panic_the_flatten_invariant() {
2334 let design = compile_source(
2335 "module m; typedef union packed { logic [7:0] a; logic [7:0] b; } u_t; u_t x; endmodule\n",
2336 )
2337 .expect("valid SV source compiles");
2338 let body = design
2339 .top_instances()
2340 .next()
2341 .expect("one top instance")
2342 .instance_body()
2343 .expect("instance has a body");
2344
2345 let layout = build_template_layout(&body).expect("layout builds, does not panic");
2346 assert_eq!(layout.slots.len(), 1);
2347 assert_eq!(layout.slots[0].offset.bit_width, 8);
2348 assert_eq!(layout.slots[0].offset.class, SlotClass::Inline);
2349 }
2350
2351 /// A multi-dimensional fixed-size unpacked array (e.g.
2352 /// `logic [7:0] mem2d [3:0][1:0]`) is one Memory slot of the innermost
2353 /// element's width with every dimension flattened into its rows -- never
2354 /// the zero-width slot an `element_type()` that is itself an unpacked
2355 /// array would give (review fix r1's original concern).
2356 #[test]
2357 fn multi_dimensional_unpacked_array_flattens_into_rows_of_the_innermost_element() {
2358 let design = compile_source("module m; logic [7:0] mem2d [3:0][1:0]; endmodule\n")
2359 .expect("valid SV source compiles");
2360 let body = design
2361 .top_instances()
2362 .next()
2363 .expect("one top instance")
2364 .instance_body()
2365 .expect("instance has a body");
2366
2367 let layout = build_template_layout(&body).expect("lays out");
2368 let offset = &layout.slots[0].offset;
2369 assert_eq!(offset.class, SlotClass::Memory);
2370 assert_eq!(offset.bit_width, 8);
2371 assert_eq!(offset.num_rows, 8, "4 x 2 rows");
2372 }
2373
2374 /// review fix r1 (classes plan Task 5 superseding update, see this
2375 /// test's own rename below): a `string` member must be loudly rejected
2376 /// via `LayoutError::UnsupportedType`... no -- a `string` member is a
2377 /// real 2-state 64-bit cell (see the second half of this test), never
2378 /// a phantom `SlotClass::Inline`/`bit_width == 0` slot.
2379 ///
2380 /// Classes plan Task 5 superseding update: a class-HANDLE-typed
2381 /// member is likewise no longer rejected -- LRM 1800-2023 §8.4: a
2382 /// class variable's own stored representation is just the OBJECT
2383 /// HANDLE (never the object's contents, which live in the object
2384 /// heap, `vj_sim::objects`), exactly like a `chandle`'s. This test
2385 /// was renamed from `class_handles_are_rejected_and_strings_are_id_
2386 /// cells_not_phantom_slots` (the test's own ORIGINAL claim, true before
2387 /// this plan) to `class_handles_are_64_bit_cells_and_strings_are_id_
2388 /// cells_not_phantom_slots` to assert the new, positive behavior
2389 /// instead of deleting the regression coverage outright.
2390 #[test]
2391 fn class_handles_are_64_bit_cells_and_strings_are_id_cells_not_phantom_slots() {
2392 let design = compile_source(
2393 "module m; class C; endclass C c; string s; logic [7:0] ok_signal; endmodule\n",
2394 )
2395 .expect("valid SV source compiles");
2396 let body = design
2397 .top_instances()
2398 .next()
2399 .expect("one top instance")
2400 .instance_body()
2401 .expect("instance has a body");
2402
2403 let layout = build_template_layout(&body)
2404 .expect("a class-handle member must now lay out, not be rejected");
2405 // `c` is `m`'s first slot-producing member (the `class C; endclass`
2406 // declaration itself contributes no slot at all -- it is filtered
2407 // out the same way a port/net/variable-less construct always is).
2408 let c_slot = &layout.slots[0];
2409 assert_eq!(c_slot.offset.class, SlotClass::Inline);
2410 assert_eq!(
2411 c_slot.offset.bit_width, 64,
2412 "a class handle is a plain 64-bit reference, exactly like a chandle's"
2413 );
2414 assert!(
2415 matches!(c_slot.initial, Some(FourStateValue::Narrow(0))),
2416 "a class variable's default initial value, absent an explicit `new`, is null (0)"
2417 );
2418
2419 // A string member is not a phantom zero-width slot either: it is a
2420 // 64-bit 2-state cell holding its interned id (`crate::strings`),
2421 // initialized to its declaration's value.
2422 let design2 = compile_source("module m2; string s; string t = \"hi\"; endmodule\n")
2423 .expect("valid SV source compiles");
2424 let body2 = design2
2425 .top_instances()
2426 .next()
2427 .expect("one top instance")
2428 .instance_body()
2429 .expect("instance has a body");
2430 let layout2 = build_template_layout(&body2).expect("string members lay out");
2431 assert_eq!(layout2.slots.len(), 2);
2432 for slot in &layout2.slots {
2433 assert_eq!(slot.offset.class, SlotClass::Inline);
2434 assert_eq!(slot.offset.bit_width, 64);
2435 }
2436 assert!(matches!(
2437 layout2.slots[0].initial,
2438 Some(FourStateValue::Narrow(0))
2439 ));
2440 let hi = u128::from(crate::strings::intern(b"hi"));
2441 assert!(matches!(layout2.slots[1].initial, Some(FourStateValue::Narrow(v)) if v == hi));
2442
2443 // Isolated compile of the ordinary signal alone: still Ok, proving
2444 // the rejection above is real, not a vacuously-true Err.
2445 let design3 = compile_source("module m3; logic [7:0] ok_signal; endmodule\n")
2446 .expect("valid SV source compiles");
2447 let body3 = design3
2448 .top_instances()
2449 .next()
2450 .expect("one top instance")
2451 .instance_body()
2452 .expect("instance has a body");
2453 let layout3 =
2454 build_template_layout(&body3).expect("the ordinary signal alone must still build Ok");
2455 assert_eq!(layout3.slots.len(), 1);
2456 assert_eq!(layout3.slots[0].offset.bit_width, 8);
2457 }
2458
2459 // -- Task 6 review fixes r2 --
2460
2461 /// An unpacked struct or union member is one flat vector of its
2462 /// fields' stored bits (`storage`): no panic on the packed-struct
2463 /// flatten invariant (review fix r2's original concern, since an
2464 /// unpacked aggregate's `bit_width()` is 0), the stored width, and
2465 /// per-field initial bits -- X for a 4-state field, 0 for a 2-state one
2466 /// (the `int`) and for a `real`.
2467 #[test]
2468 fn unpacked_struct_and_union_members_are_flat_stored_vectors() {
2469 let design = compile_source(
2470 "module m; struct { logic a; logic b; } x; \
2471 struct { logic [3:0] l; int i; real r; } y; \
2472 union { logic [7:0] a; logic [7:0] b; } u; endmodule\n",
2473 )
2474 .expect("valid SV source compiles");
2475 let body = design
2476 .top_instances()
2477 .next()
2478 .expect("one top instance")
2479 .instance_body()
2480 .expect("instance has a body");
2481 let layout = build_template_layout(&body).expect("unpacked aggregates lay out");
2482 assert_eq!(layout.slots.len(), 3);
2483 let widths: Vec<u32> = layout.slots.iter().map(|s| s.offset.bit_width).collect();
2484 assert_eq!(widths, [2, 4 + 32 + 64, 8]);
2485 let classes: Vec<SlotClass> = layout.slots.iter().map(|s| s.offset.class).collect();
2486 assert_eq!(
2487 classes,
2488 [SlotClass::Inline, SlotClass::Wide, SlotClass::Inline]
2489 );
2490 // y, LSB first: r (64 zeros), i (32 zeros), l (4 X).
2491 let mut y_bits = vec![sv_lang::Bit::Zero; 96];
2492 y_bits.extend([sv_lang::Bit::X; 4]);
2493 let expect = [vec![sv_lang::Bit::X; 2], y_bits, vec![sv_lang::Bit::X; 8]];
2494 for (slot, bits) in layout.slots.iter().zip(expect) {
2495 assert_eq!(
2496 format!("{:?}", slot.initial),
2497 format!("{:?}", Some(crate::state::value::bits_to_four_state(&bits))),
2498 );
2499 }
2500 }
2501
2502 // -- Task 6 review fixes r3 --
2503
2504 /// review fix r3, revised for real support: a `real`/`shortreal`/
2505 /// `realtime` member is a 2-state `Inline` cell of its IEEE width
2506 /// (64/32/64) holding the IEEE-754 bits of its initial value -- 0.0
2507 /// when there is no initializer (LRM 6.12: reals default to 0.0, never
2508 /// X), and a `shortreal` initializer rounded to single precision.
2509 #[test]
2510 fn real_members_are_inline_ieee_bit_cells_with_real_initial_values() {
2511 let design = compile_source(
2512 "module m; real r; shortreal sr = 1.0/3.0; realtime rt = 2.5; endmodule\n",
2513 )
2514 .expect("valid SV source compiles");
2515 let body = design
2516 .top_instances()
2517 .next()
2518 .expect("one top instance")
2519 .instance_body()
2520 .expect("instance has a body");
2521 let layout = build_template_layout(&body).expect("real members lay out");
2522 assert_eq!(layout.slots.len(), 3);
2523 let expect = [
2524 (64u32, 0u64),
2525 (32, u64::from((1.0f32 / 3.0).to_bits())),
2526 (64, 2.5f64.to_bits()),
2527 ];
2528 for (slot, (width, bits)) in layout.slots.iter().zip(expect) {
2529 assert_eq!(slot.offset.class, SlotClass::Inline);
2530 assert_eq!(slot.offset.bit_width, width);
2531 assert!(
2532 matches!(slot.initial, Some(FourStateValue::Narrow(v)) if v == u128::from(bits)),
2533 "expected IEEE bits {bits:#x}, got {:?}",
2534 slot.initial
2535 );
2536 }
2537 }
2538
2539 /// A fixed-size unpacked array of a 2-state non-integral type is a
2540 /// Memory slot of its stored bits -- 64-bit rows for `real`, `string`
2541 /// and `chandle`, 32-bit for `shortreal` -- whose rows start at 0
2542 /// (`initial` is `Some`, applied to every row by `reset_to_initial`); a
2543 /// 4-state integral array's rows start all-X (`initial: None`), and a
2544 /// multi-dimensional 2-state one starts at 0 too.
2545 #[test]
2546 fn non_integral_memory_rows_are_their_stored_bits_and_start_at_zero() {
2547 let design = compile_source(
2548 "module m; real r [3:0]; shortreal s [2]; string t [2]; logic [3:0] l [2];\n\
2549 endmodule\n",
2550 )
2551 .expect("valid SV source compiles");
2552 let body = design
2553 .top_instances()
2554 .next()
2555 .expect("one top instance")
2556 .instance_body()
2557 .expect("instance has a body");
2558 let layout = build_template_layout(&body).expect("lays out");
2559 let widths: Vec<u32> = layout.slots.iter().map(|s| s.offset.bit_width).collect();
2560 assert_eq!(widths, [64, 32, 64, 4]);
2561 for slot in &layout.slots[..3] {
2562 assert_eq!(slot.offset.class, SlotClass::Memory);
2563 assert!(slot.initial.is_some(), "2-state rows start at 0");
2564 }
2565 assert!(
2566 layout.slots[3].initial.is_none(),
2567 "4-state rows start all-X"
2568 );
2569
2570 let nested = compile_source("module n; int a [2][3]; endmodule\n").expect("compiles");
2571 let body = nested
2572 .top_instances()
2573 .next()
2574 .expect("one top instance")
2575 .instance_body()
2576 .expect("instance has a body");
2577 let layout = build_template_layout(&body).expect("lays out");
2578 assert_eq!(layout.slots[0].offset.num_rows, 6);
2579 assert!(layout.slots[0].initial.is_some(), "2-state rows start at 0");
2580 }
2581
2582 /// A memory's constant initializer becomes every row's initial value,
2583 /// lowest index first (`d[3:0] = '{1, 2, 3, 4}` puts 4 in row 0), and a
2584 /// non-constant one is rejected rather than dropped.
2585 #[test]
2586 fn memory_initializer_gives_every_row_its_value_lowest_index_first() {
2587 let design = compile_source(
2588 "module m; int d [3:0] = '{1, 2, 3, 4}; logic [3:0] x [2] = '{4'b10x1, 4'h5};\n\
2589 endmodule\n",
2590 )
2591 .expect("valid SV source compiles");
2592 let body = design
2593 .top_instances()
2594 .next()
2595 .expect("one top instance")
2596 .instance_body()
2597 .expect("instance has a body");
2598 let layout = build_template_layout(&body).expect("lays out");
2599 match &layout.slots[0].initial {
2600 Some(FourStateValue::Wide { aval, bval }) => {
2601 assert_eq!(aval, &[4, 3, 2, 1]);
2602 assert_eq!(bval, &[0, 0, 0, 0]);
2603 }
2604 other => panic!("expected per-row initial values, got {other:?}"),
2605 }
2606 match &layout.slots[1].initial {
2607 // 4'b10x1: aval 1011 (X has aval=1), bval 0010.
2608 Some(FourStateValue::Wide { aval, bval }) => {
2609 assert_eq!(aval, &[0b1011, 0x5]);
2610 assert_eq!(bval, &[0b0010, 0]);
2611 }
2612 other => panic!("expected per-row initial values, got {other:?}"),
2613 }
2614
2615 let dynamic =
2616 compile_source("module n; int v; int a [2] = '{v, 1}; endmodule\n").expect("compiles");
2617 let body = dynamic
2618 .top_instances()
2619 .next()
2620 .expect("one top instance")
2621 .instance_body()
2622 .expect("instance has a body");
2623 // A non-constant initializer lays out with default rows; the time-zero
2624 // var-init kernel assigns the real values (arrq12 step 2).
2625 build_template_layout(&body).expect("lays out with default rows");
2626 }
2627
2628 /// A member wider than 65535 bits (LRM 1800-2023 6.9.1: an
2629 /// implementation's vector-length limit "shall be at least 65 536 (2^16)
2630 /// bits") builds with its true width and word count, never a clamped one.
2631 #[test]
2632 fn member_wider_than_65535_bits_keeps_its_true_width() {
2633 let design = compile_source("module m; logic [70000:0] huge; endmodule\n")
2634 .expect("valid SV source compiles");
2635 let body = design
2636 .top_instances()
2637 .next()
2638 .expect("one top instance")
2639 .instance_body()
2640 .expect("instance has a body");
2641
2642 let layout = build_template_layout(&body).expect("a 70001-bit member builds");
2643 assert_eq!(layout.slots.len(), 1);
2644 let offset = layout.slots[0].offset;
2645 assert_eq!(offset.bit_width, 70001);
2646 assert_eq!(offset.class, SlotClass::Wide);
2647 assert_eq!(offset.elem_word_count, 70001u32.div_ceil(64));
2648 }
2649
2650 /// The same for a memory row's per-element width.
2651 #[test]
2652 fn memory_row_wider_than_65535_bits_keeps_its_true_width() {
2653 let design = compile_source("module m; logic [70000:0] huge_row [3:0]; endmodule\n")
2654 .expect("valid SV source compiles");
2655 let body = design
2656 .top_instances()
2657 .next()
2658 .expect("one top instance")
2659 .instance_body()
2660 .expect("instance has a body");
2661
2662 let layout = build_template_layout(&body).expect("rows of 70001 bits build");
2663 let offset = layout.slots[0].offset;
2664 assert_eq!(offset.bit_width, 70001);
2665 assert_eq!(offset.num_rows, 4);
2666 assert!(offset.row_stride_words >= 70001u32.div_ceil(64));
2667 }
2668
2669 /// A member at the former 65535-bit boundary still builds with the
2670 /// exact word count.
2671 #[test]
2672 fn member_of_65535_bits_builds_with_correct_word_count() {
2673 let design = compile_source("module m; logic [65534:0] w; endmodule\n")
2674 .expect("valid SV source compiles");
2675 let body = design
2676 .top_instances()
2677 .next()
2678 .expect("one top instance")
2679 .instance_body()
2680 .expect("instance has a body");
2681
2682 let layout = build_template_layout(&body).expect("a 65535-bit member must still build Ok");
2683 assert_eq!(layout.slots.len(), 1);
2684 let offset = layout.slots[0].offset;
2685 assert_eq!(offset.bit_width, 65535);
2686 assert_eq!(offset.class, SlotClass::Wide);
2687 assert_eq!(
2688 offset.elem_word_count,
2689 65535u32.div_ceil(64),
2690 "elem_word_count must be derived from the true 65535-bit width, not a clamped value"
2691 );
2692 }
2693
2694 // -- sva T8: default clocking / default disable iff scoping --------
2695
2696 /// The resolved default disable iff's referenced identifier's plain
2697 /// name, with no leading trivia (`Token::text()`, not `Node::text()`).
2698 fn disable_name(d: DefaultDisableDeclarationSyntax) -> String {
2699 d.expr()
2700 .syntax()
2701 .first_token()
2702 .expect("a disable iff expression has at least one token")
2703 .text()
2704 .to_string()
2705 }
2706
2707 /// `u2`, the sole module instance directly inside `body`, as its own
2708 /// `InstanceBody` -- the shared scaffolding every nested-module test
2709 /// below uses to reach the nested scope.
2710 fn nested_instance_body<'d>(body: &Symbol<'d>) -> Symbol<'d> {
2711 body.members()
2712 .find(|m| m.kind() == SymbolKind::Instance)
2713 .expect("fixture has exactly one nested module instance")
2714 .instance_body()
2715 .expect("instance has a body")
2716 }
2717
2718 /// 16.15's own `examples_with_default` module (quoted verbatim, minus
2719 /// the `property`/`assert property` statements T8 does not need: this
2720 /// task resolves `default_disable_of` at the *scope* level, never at a
2721 /// per-assertion level -- composing it against an assertion's own
2722 /// explicit `disable iff` clause, rules (a)/(b)/(c), is T27's job).
2723 /// `rst` is the module-level default; there is nothing nested here, so
2724 /// every scope in the module sees the same single default.
2725 #[test]
2726 fn examples_with_default_module_level_default_disable_resolves() {
2727 let design = compile_source(
2728 "module examples_with_default (input logic a, b, clk, rst, rst1);\n\
2729 default disable iff rst;\n\
2730 endmodule\n",
2731 )
2732 .expect("valid SV source compiles");
2733 let body = design
2734 .top_instances()
2735 .next()
2736 .expect("one top instance")
2737 .instance_body()
2738 .expect("instance has a body");
2739 let tree = design.syntax_trees().into_iter().next().unwrap();
2740
2741 let (decl_scope, found) = default_disable_of(&body, &tree)
2742 .expect("no scoping error")
2743 .expect("examples_with_default declares a module-level default");
2744 assert_eq!(disable_name(found), "rst");
2745 assert_eq!(decl_scope.name(), "examples_with_default");
2746 }
2747
2748 /// A module with no `default disable iff` anywhere resolves to `None`
2749 /// (16.15 rule (c): "no inference is performed").
2750 #[test]
2751 fn no_default_disable_declaration_resolves_to_none() {
2752 let design = compile_source("module m; bit clk, rst; endmodule\n")
2753 .expect("valid SV source compiles");
2754 let body = design
2755 .top_instances()
2756 .next()
2757 .expect("one top instance")
2758 .instance_body()
2759 .expect("instance has a body");
2760 let tree = design.syntax_trees().into_iter().next().unwrap();
2761
2762 assert!(
2763 default_disable_of(&body, &tree)
2764 .expect("no scoping error")
2765 .is_none()
2766 );
2767 }
2768
2769 /// 16.15's first `m1`/`m2` nesting example (quoted verbatim): `m2` is a
2770 /// *nested* module declaration with no `default disable iff` of its
2771 /// own, instantiated inside `m1`. "The default disable condition rst1
2772 /// applies throughout the declaration of m1 and the nested declaration
2773 /// of m2" -- `m2`'s own scope must resolve to `m1`'s `rst1`, not `None`.
2774 #[test]
2775 fn nested_module_with_no_own_default_inherits_enclosing_default() {
2776 let design = compile_source(
2777 "module m1;\n\
2778 bit clk, rst1;\n\
2779 default disable iff rst1;\n\
2780 m2 u2();\n\
2781 module m2;\n\
2782 bit rst2;\n\
2783 endmodule\n\
2784 endmodule\n",
2785 )
2786 .expect("valid SV source compiles");
2787 let m1 = design
2788 .top_instances()
2789 .next()
2790 .expect("one top instance")
2791 .instance_body()
2792 .expect("instance has a body");
2793 let tree = design.syntax_trees().into_iter().next().unwrap();
2794 let m2 = nested_instance_body(&m1);
2795
2796 let (m1_scope, m1_found) = default_disable_of(&m1, &tree)
2797 .expect("no scoping error")
2798 .expect("m1 declares its own default");
2799 assert_eq!(disable_name(m1_found), "rst1");
2800 assert_eq!(m1_scope.name(), "m1");
2801
2802 let (m2_scope, m2_found) = default_disable_of(&m2, &tree)
2803 .expect("no scoping error")
2804 .expect("m2 inherits m1's default -- it must not resolve to None");
2805 assert_eq!(disable_name(m2_found), "rst1");
2806 assert_eq!(
2807 m2_scope.name(),
2808 "m1",
2809 "the inherited default's signals resolve from ITS OWN declaring scope (m1), not m2's"
2810 );
2811 }
2812
2813 /// 16.15's second `m1`/`m2` nesting example (quoted verbatim): `m2` now
2814 /// declares its *own* `default disable iff rst2`, which "overrides the
2815 /// default disable condition declared in m1" within `m2`'s own scope,
2816 /// while `m1`'s own assertions are unaffected.
2817 #[test]
2818 fn nested_module_with_own_default_overrides_enclosing_default() {
2819 let design = compile_source(
2820 "module m1;\n\
2821 bit clk, rst1;\n\
2822 default disable iff rst1;\n\
2823 m2 u2();\n\
2824 module m2;\n\
2825 bit rst2;\n\
2826 default disable iff rst2;\n\
2827 endmodule\n\
2828 endmodule\n",
2829 )
2830 .expect("valid SV source compiles");
2831 let m1 = design
2832 .top_instances()
2833 .next()
2834 .expect("one top instance")
2835 .instance_body()
2836 .expect("instance has a body");
2837 let tree = design.syntax_trees().into_iter().next().unwrap();
2838 let m2 = nested_instance_body(&m1);
2839
2840 let (_, m1_found) = default_disable_of(&m1, &tree)
2841 .expect("no scoping error")
2842 .expect("m1 declares its own default");
2843 assert_eq!(disable_name(m1_found), "rst1");
2844
2845 let (m2_scope, m2_found) = default_disable_of(&m2, &tree)
2846 .expect("no scoping error")
2847 .expect("m2 declares its own, overriding default");
2848 assert_eq!(disable_name(m2_found), "rst2");
2849 assert_eq!(m2_scope.name(), "m2");
2850 }
2851
2852 /// 16.15: "The scope does not extend into any instances of modules" --
2853 /// when `m2` is declared SEPARATELY at the top level (not nested inside
2854 /// `m1`) and merely instantiated there, `m1`'s default must not leak
2855 /// into `m2`'s own scope, even though the instantiation site is
2856 /// identical to the nested case above.
2857 #[test]
2858 fn instantiated_but_not_nested_module_does_not_inherit_default() {
2859 let design = compile_source(
2860 "module m2;\n\
2861 bit rst2;\n\
2862 endmodule\n\
2863 module m1;\n\
2864 bit clk, rst1;\n\
2865 default disable iff rst1;\n\
2866 m2 u2();\n\
2867 endmodule\n",
2868 )
2869 .expect("valid SV source compiles");
2870 let m1 = design
2871 .top_instances()
2872 .next()
2873 .expect("one top instance")
2874 .instance_body()
2875 .expect("instance has a body");
2876 let tree = design.syntax_trees().into_iter().next().unwrap();
2877 let m2 = nested_instance_body(&m1);
2878
2879 assert!(
2880 default_disable_of(&m2, &tree)
2881 .expect("no scoping error")
2882 .is_none(),
2883 "a separately top-level-declared module must not inherit the instantiating \
2884 module's default -- only textual nesting inherits"
2885 );
2886 }
2887
2888 /// 16.15: "More than one default disable iff declaration within the
2889 /// same module ... shall be an error."
2890 #[test]
2891 fn duplicate_default_disable_in_same_scope_is_an_error() {
2892 let design = compile_source(
2893 "module m;\n\
2894 bit rst1, rst2;\n\
2895 default disable iff rst1;\n\
2896 default disable iff rst2;\n\
2897 endmodule\n",
2898 )
2899 .expect("valid SV source compiles");
2900 let body = design
2901 .top_instances()
2902 .next()
2903 .expect("one top instance")
2904 .instance_body()
2905 .expect("instance has a body");
2906 let tree = design.syntax_trees().into_iter().next().unwrap();
2907
2908 let err = default_disable_of(&body, &tree).expect_err("duplicate must be rejected");
2909 assert!(matches!(err, DefaultScopeError::DuplicateDefaultDisable(name) if name == "m"));
2910 }
2911
2912 /// 14.12 Example 2 ("Assigning an existing clocking to be the
2913 /// default"), adapted to a nested module (the construct 14.12 itself
2914 /// requires for inheritance to mean anything): `cpu` is a *nested*
2915 /// module with its own `default clocking busA;` reference form,
2916 /// pointing at a clocking block declared in the *enclosing* module --
2917 /// resolved by name in `cpu`'s own declaring scope.
2918 #[test]
2919 fn default_clocking_reference_form_resolves_named_block_in_declaring_scope() {
2920 let design = compile_source(
2921 "module processor;\n\
2922 bit clk1, clk2;\n\
2923 clocking busA @(posedge clk1); endclocking\n\
2924 clocking busB @(negedge clk2); endclocking\n\
2925 cpu u_cpu();\n\
2926 module cpu;\n\
2927 default clocking busA;\n\
2928 endmodule\n\
2929 endmodule\n",
2930 )
2931 .expect("valid SV source compiles");
2932 let processor = design
2933 .top_instances()
2934 .next()
2935 .expect("one top instance")
2936 .instance_body()
2937 .expect("instance has a body");
2938 let tree = design.syntax_trees().into_iter().next().unwrap();
2939 let cpu = nested_instance_body(&processor);
2940
2941 let cb = default_clocking_of(&cpu, &tree)
2942 .expect("no scoping error")
2943 .expect("cpu declares its own default clocking reference");
2944 assert_eq!(cb.name(), "busA");
2945 assert_eq!(cb.kind(), SymbolKind::ClockingBlock);
2946 }
2947
2948 /// A module with no `default clocking` anywhere, nested or otherwise,
2949 /// resolves to `None`.
2950 #[test]
2951 fn no_default_clocking_declaration_resolves_to_none() {
2952 let design = compile_source(
2953 "module m; bit clk; clocking cb @(posedge clk); endclocking endmodule\n",
2954 )
2955 .expect("valid SV source compiles");
2956 let body = design
2957 .top_instances()
2958 .next()
2959 .expect("one top instance")
2960 .instance_body()
2961 .expect("instance has a body");
2962 let tree = design.syntax_trees().into_iter().next().unwrap();
2963
2964 assert!(
2965 default_clocking_of(&body, &tree)
2966 .expect("no scoping error")
2967 .is_none(),
2968 "a plain, non-default clocking block must not itself count as a default"
2969 );
2970 }
2971
2972 /// A nested module with no `default clocking` of its own inherits the
2973 /// enclosing module's inline `default clocking cb @(...); endclocking`
2974 /// declaration (the inline form, not the reference form -- exercises
2975 /// the syntax-node-identity symbol recovery, not name lookup).
2976 #[test]
2977 fn nested_module_inherits_enclosing_inline_default_clocking() {
2978 let design = compile_source(
2979 "module m1;\n\
2980 bit clk;\n\
2981 default clocking cb @(posedge clk); endclocking\n\
2982 m2 u2();\n\
2983 module m2;\n\
2984 endmodule\n\
2985 endmodule\n",
2986 )
2987 .expect("valid SV source compiles");
2988 let m1 = design
2989 .top_instances()
2990 .next()
2991 .expect("one top instance")
2992 .instance_body()
2993 .expect("instance has a body");
2994 let tree = design.syntax_trees().into_iter().next().unwrap();
2995 let m2 = nested_instance_body(&m1);
2996
2997 let cb = default_clocking_of(&m2, &tree)
2998 .expect("no scoping error")
2999 .expect("m2 inherits m1's inline default clocking block");
3000 assert_eq!(cb.name(), "cb");
3001 assert_eq!(cb.kind(), SymbolKind::ClockingBlock);
3002 }
3003
3004 /// 14.12 also permits an *anonymous* inline default clocking block
3005 /// (`default clocking @(posedge clk); endclocking`, no
3006 /// `clocking_identifier` at all -- used throughout 16.14.7's own
3007 /// worked examples). Its elaborated `ClockingBlock` member has an
3008 /// empty name, so name-based lookup cannot recover it -- this is the
3009 /// syntax-node-identity path's whole reason to exist.
3010 #[test]
3011 fn anonymous_inline_default_clocking_resolves_via_syntax_identity() {
3012 let design = compile_source(
3013 "module m1;\n\
3014 bit clk;\n\
3015 default clocking @(posedge clk); endclocking\n\
3016 endmodule\n",
3017 )
3018 .expect("valid SV source compiles");
3019 let body = design
3020 .top_instances()
3021 .next()
3022 .expect("one top instance")
3023 .instance_body()
3024 .expect("instance has a body");
3025 let tree = design.syntax_trees().into_iter().next().unwrap();
3026
3027 let cb = default_clocking_of(&body, &tree)
3028 .expect("no scoping error")
3029 .expect("m1 declares its own anonymous default clocking block");
3030 assert_eq!(cb.name(), "");
3031 assert_eq!(cb.kind(), SymbolKind::ClockingBlock);
3032 }
3033
3034 /// 14.12: "Specifying a default clocking more than once in the same
3035 /// module ... shall result in a compiler error."
3036 #[test]
3037 fn duplicate_default_clocking_in_same_scope_is_an_error() {
3038 let design = compile_source(
3039 "module m;\n\
3040 bit clk1, clk2;\n\
3041 clocking a @(posedge clk1); endclocking\n\
3042 clocking b @(posedge clk2); endclocking\n\
3043 default clocking a;\n\
3044 default clocking b;\n\
3045 endmodule\n",
3046 )
3047 .expect("valid SV source compiles");
3048 let body = design
3049 .top_instances()
3050 .next()
3051 .expect("one top instance")
3052 .instance_body()
3053 .expect("instance has a body");
3054 let tree = design.syntax_trees().into_iter().next().unwrap();
3055
3056 let err = default_clocking_of(&body, &tree).expect_err("duplicate must be rejected");
3057 assert!(matches!(err, DefaultScopeError::DuplicateDefaultClocking(name) if name == "m"));
3058 }
3059}