Skip to main content

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}