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