Skip to main content

vj_sim/state/
layout.rs

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