Skip to main content

vj_sim/netlist/
classes.rs

1//! Classes plan (`docs/superpowers/plans/2026-10-03-classes.md`), Task 2:
2//! class template dedup (`ClassTemplateId`/`ClassTemplateKey`) and member
3//! classification (`ClassMembers`) — LRM 8.4, 8.5, 8.9 (classification
4//! only; `fields`/`methods` are not yet lowered into any storage/codegen
5//! shape, that starts Task 5). Pure data, no codegen dependency yet (this
6//! task's own acceptance text): nothing in `elaborate.rs`'s `Netlist`/
7//! `Template` pipeline calls into this module yet, so `Template`'s already-
8//! "stable from here on" shape (`template.rs`'s own doc comment) is left
9//! untouched.
10//!
11//! **`ClassTemplateId`/`ClassTemplateKey` vs. `TemplateId`/`TemplateKey`**
12//! (`dedup.rs`, `mod.rs`): structurally parallel (same two-field key shape,
13//! same hash-then-verify dedup discipline, ARCHITECTURE §7 item 7), but the
14//! *ground truth* half of "hash-then-verify" is different, for a reason
15//! specific to classes rather than modules:
16//!
17//! - A module/interface/program `InstanceBody` is dedup'd the hard way
18//!   (`dedup.rs::merge_groups_within_bucket`, `Symbol::instance_body_has_same_type`)
19//!   because `Design::compile()` forces `DisableInstanceCaching` — `sv-lang`
20//!   itself never pointer-shares two instance bodies, even syntactically
21//!   identical ones (verified live in `netlist_dedup.rs`'s
22//!   `disable_instance_caching_means_no_pointer_sharing_before_our_own_dedup`).
23//! - A class specialization is the opposite: `GenericClassDefSymbol` caches
24//!   every specialization it ever builds, keyed by its resolved parameter
25//!   binding (`vendor/include/slang/ast/symbols/ClassSymbols.h`'s own
26//!   `specMap`/`uninstantiatedSpecMap`, `vendor/source/ast/symbols/ClassSymbols.cpp`),
27//!   so two `new Box#(int)` (or `Box#(int)`-typed) references anywhere in one
28//!   `Compilation` resolve to the LITERAL SAME `ClassType` symbol —
29//!   `Symbol`'s own pointer-identity `PartialEq` (`ast.rs`'s
30//!   `self.raw.ptr == other.raw.ptr`), not merely a structurally-equivalent
31//!   one. Verified live in `netlist_classes.rs`'s
32//!   `same_specialization_is_pointer_identical_not_merely_structurally_equal`
33//!   test.
34//!
35//! So this module's "verify" ground truth ([`same_specialization`]) is
36//! symbol-identity, never a re-derived member-by-member structural
37//! comparison — it is *stronger* than `instance_body_has_same_type` (exact
38//! identity, not "same shape"), and it is exactly as cheap as the `==` it
39//! is. `params_hash` is still only a candidate-bucketing FILTER, never
40//! trusted alone: a 64-bit `DefaultHasher` digest of two DIFFERENT
41//! parameter bindings could in principle collide (ARCHITECTURE §7 item 7's
42//! own rationale for `TemplateKey.params_hash`), and
43//! [`same_specialization`] is what catches that without ever trusting the
44//! hash — see `netlist_classes.rs`'s
45//! `hash_bucket_collision_between_different_specializations_is_rejected`.
46
47use std::collections::HashMap;
48use std::hash::{Hash, Hasher};
49
50use sv_lang_kinds::SymbolKind;
51
52use crate::netlist::{ClassTemplateId, ClassTemplateKey, ElabError, hash_then_verify_dedup};
53use crate::symid::SymbolId;
54
55// `ClassTemplateId`/`ClassTemplateKey` themselves now live, unconditionally,
56// in `netlist/mod.rs` alongside `TemplateId`/`TemplateKey` (Task 3 deviation:
57// see that module's doc comment on `ClassTemplateId` for why) — this module
58// keeps their sv-lang-backed construction (`class_template_key_of`,
59// `same_specialization`, dedup) and documents their shape here as before.
60//
61// `ClassTemplateKey` is the candidate-bucketing key for class-template dedup
62// (the FILTER half of hash-then-verify — see this module's doc comment
63// above). `definition` is a dense id for the class's OWN definition
64// identity: for a specialization of a generic class, the generic class's
65// `GenericClassDef` symbol (`Type::class_generic_class()`) — shared by every
66// specialization of that one generic class, so two different specializations
67// of `Box#(T)` get the SAME `definition` and are told apart only by
68// `params_hash`; for a plain (non-generic) class, the class's own `ClassType`
69// symbol itself (every textually distinct `class Foo; endclass` is its own
70// definition, exactly like a module's `Definition`).
71
72/// Interns a class/generic-class DEFINITION symbol to a stable dense id,
73/// keyed by [`crate::symid::SymbolUid::uid`] — the same stable-key discipline
74/// [`super::dedup::DefinitionTable`] uses for module `Definition`s, kept as
75/// its own small table rather than shared with that one because the two
76/// intern different symbol kinds (`Definition` there, `ClassType`/
77/// `GenericClassDef` here) and nothing requires their dense ids to share one
78/// namespace.
79#[derive(Default)]
80pub struct ClassDefinitionTable {
81    by_id: HashMap<SymbolId, u32>,
82}
83
84impl ClassDefinitionTable {
85    pub fn intern(&mut self, def: sv_lang::Symbol<'_>) -> u32 {
86        use crate::symid::SymbolUid;
87        let next_id = self.by_id.len() as u32;
88        *self.by_id.entry(def.uid()).or_insert(next_id)
89    }
90}
91
92/// The definition-identity half of a [`ClassTemplateKey`]: the generic
93/// class's own symbol for a specialization, or the class's own symbol for a
94/// plain (non-generic) class — see [`ClassTemplateKey::definition`]'s doc
95/// comment.
96fn class_definition_symbol<'d>(ty: sv_lang::Type<'d>) -> sv_lang::Symbol<'d> {
97    let ty = ty.canonical();
98    match ty.class_generic_class() {
99        Some(generic) => generic,
100        None => ty.as_symbol(),
101    }
102}
103
104/// Task 2's `class_param_binding_hash` (parallel to
105/// `vj-codegen::mangle::param_binding_hash`, which this cannot literally
106/// reuse: that function reads `sym.parameters()`, whose own doc/SAFETY
107/// comment — quoted in `dedup.rs::template_key_of` — says its count "is 0
108/// for non-instances", so it is populated only on an `Instance`-kind symbol,
109/// never a `ClassType`). A specialized class's bound parameters are instead
110/// real `Parameter`/`TypeParameter` members of the class's OWN scope
111/// (`slang::ast::ParameterBuilder::createParam` adds them there at
112/// specialization time, verified against
113/// `vendor/source/ast/symbols/ClassSymbols.cpp`), so this walks
114/// `class_sym.members()` directly and hashes each one's `(name(),
115/// parameter_value())` — `parameter_value()` is documented to print "its
116/// value or target type", which is exactly what distinguishes a `parameter
117/// int W = 8` binding from a `parameter type T = int` one without this
118/// function needing two different code paths for value vs. type parameters.
119/// A plain (non-generic) class has no `Parameter`/`TypeParameter` members at
120/// all, so this returns the same fixed empty-input hash for every one of
121/// them — harmless, since `ClassTemplateKey.definition` already tells two
122/// different plain classes apart on its own.
123///
124/// Deterministic (fixed-key `DefaultHasher`, not `HashMap`'s per-process-
125/// randomized `RandomState`) for the same reason `template_key_of` is: a
126/// candidate-bucketing filter must hash the same input to the same output
127/// across independent `elaborate()`-style runs.
128pub fn class_param_binding_hash(class_sym: &sv_lang::Symbol<'_>) -> u64 {
129    let mut hasher = std::collections::hash_map::DefaultHasher::new();
130    for member in class_sym.members() {
131        if matches!(
132            member.kind(),
133            SymbolKind::Parameter | SymbolKind::TypeParameter
134        ) {
135            member.name().hash(&mut hasher);
136            member.parameter_value().hash(&mut hasher);
137        }
138    }
139    hasher.finish()
140}
141
142/// Builds one specialization's [`ClassTemplateKey`] — the definition half
143/// via [`class_definition_symbol`]/[`ClassDefinitionTable::intern`], the
144/// parameter-binding half via [`class_param_binding_hash`].
145pub fn class_template_key_of(
146    defs: &mut ClassDefinitionTable,
147    ty: sv_lang::Type<'_>,
148) -> ClassTemplateKey {
149    let ty = ty.canonical();
150    let definition = defs.intern(class_definition_symbol(ty));
151    let params_hash = class_param_binding_hash(&ty.as_symbol());
152    ClassTemplateKey {
153        definition,
154        params_hash,
155    }
156}
157
158/// Classes plan Task 9 (LRM 8.25): a string that is equal for two class
159/// types iff they are the same specialization of the same class, and that
160/// is stable across everything a `Symbol` is not -- `sv-lang` 0.1.9 exposes
161/// no pointer, `Hash` or `Ord` on a `Symbol` (so a registry keyed by the
162/// specialization itself, never just by its generic's [`SymbolUid::uid`],
163/// cannot be a plain map), and every specialization of one generic class
164/// shares that one `uid` (as do their corresponding members), which is
165/// exactly the ambiguity this key resolves. Composed of:
166///
167/// - the ENCLOSING class's own key, if `ty` is declared inside another class
168///   (8.25's nested classes): `Outer#(int)::Inner` and `Outer#(string)::
169///   Inner` are different classes with the same `Inner` uid;
170/// - `ty`'s own definition uid (the generic class's, for a specialization);
171/// - every bound `Parameter`/`TypeParameter` (the same members
172///   [`class_param_binding_hash`] hashes) as `name=value`. A value
173///   parameter prints through `parameter_value()`; a TYPE parameter
174///   recurses into this function when its target is itself a class (so
175///   `Box#(Outer1::Inner)` and `Box#(Outer2::Inner)`, whose target types
176///   both PRINT as `Inner`, stay distinct) and otherwise uses the target's
177///   canonical SV spelling.
178///
179/// Unlike [`class_param_binding_hash`] this is exact, not a bucketing
180/// filter: it is what the codegen registry and the symbol mangler key on.
181/// A residual ambiguity (two different types that print identically) is
182/// caught loudly by the registry's own collision check, never silently
183/// merged.
184pub fn class_specialization_key(ty: sv_lang::Type<'_>) -> String {
185    let mut out = String::new();
186    push_class_key(ty.canonical().as_symbol(), &mut out);
187    out
188}
189
190fn push_class_key(class_sym: sv_lang::Symbol<'_>, out: &mut String) {
191    use crate::symid::SymbolUid;
192    use std::fmt::Write as _;
193
194    if let Some(parent) = class_sym.parent()
195        && parent.kind() == SymbolKind::ClassType
196    {
197        push_class_key(parent, out);
198        out.push('/');
199    }
200    let ty = class_sym.as_type();
201    let definition = match ty.and_then(|t| t.class_generic_class()) {
202        Some(generic) => generic,
203        None => class_sym,
204    };
205    let _ = write!(out, "{:?}", definition.uid());
206    for member in class_sym.members() {
207        // Only a specialization's bound parameters take part in its
208        // identity; every other member kind is the class's body. (`if`,
209        // not a `match` with a wildcard: a membership test, not a
210        // classification of `SymbolKind` -- `xtask lint-unimplemented`.)
211        if member.kind() == SymbolKind::Parameter {
212            let _ = write!(
213                out,
214                "#{}={}",
215                member.name(),
216                member.parameter_value().unwrap_or_default()
217            );
218        } else if member.kind() == SymbolKind::TypeParameter {
219            let _ = write!(out, "#{}=", member.name());
220            match member.declared_type().map(|t| t.canonical()) {
221                Some(target) if target.is_class() => {
222                    out.push('<');
223                    push_class_key(target.as_symbol(), out);
224                    out.push('>');
225                }
226                Some(target) => out.push_str(&target.to_sv_string()),
227                None => out.push_str(&member.parameter_value().unwrap_or_default()),
228            }
229        }
230    }
231}
232
233/// The VERIFY ground truth for class-template dedup (this module's doc
234/// comment): two specializations are the same template iff they are
235/// literally the same `ClassType` symbol. Never a structural re-derivation
236/// (unlike `dedup.rs`'s `instance_body_has_same_type`) — slang's own
237/// specialization cache already guarantees pointer identity for a genuinely
238/// matching binding, so re-deriving "same shape" here would be strictly
239/// weaker, not stronger, than just comparing identity directly. Exposed
240/// (not just an internal helper of [`assign_class_template_ids`]) so a test
241/// can drive [`hash_then_verify_dedup`] directly with a forced, fabricated
242/// [`ClassTemplateKey`] collision between two genuinely different classes —
243/// the same "bypass the real hash, exercise only the verify step" shape
244/// `netlist_dedup.rs`'s own
245/// `hash_bucket_collision_between_different_modules_is_rejected_by_pairwise_oracle`
246/// uses for module `Template`s.
247pub fn same_specialization(a: sv_lang::Type<'_>, b: sv_lang::Type<'_>) -> bool {
248    a.canonical().as_symbol() == b.canonical().as_symbol()
249}
250
251/// Task 2 step 5: hash-then-verify dedup over a caller-supplied slice of
252/// resolved class `Type`s (every element some specialization's or plain
253/// class's own `ClassType`), returning one [`ClassTemplateId`] per input
254/// element, index-aligned with `types` — two elements get the same id iff
255/// [`same_specialization`] confirms they are the one underlying symbol.
256/// Built directly on [`hash_then_verify_dedup`] (`dedup_algorithm.rs`'s
257/// already-tested, generic, sv-lang-free algorithm) rather than a
258/// re-implementation of its bucket/merge loop, tagging each `Type` with its
259/// own [`ClassTemplateKey`] up front (a plain, non-`Fn`-constrained loop,
260/// since building that key needs `&mut ClassDefinitionTable`) exactly the
261/// way `dedup.rs::merge_groups_within_bucket` tags each module body with its
262/// own `TemplateKey` before bucketing.
263pub fn assign_class_template_ids(types: &[sv_lang::Type<'_>]) -> Vec<ClassTemplateId> {
264    let mut defs = ClassDefinitionTable::default();
265    let tagged: Vec<(ClassTemplateKey, sv_lang::Type<'_>)> = types
266        .iter()
267        .map(|ty| (class_template_key_of(&mut defs, *ty), *ty))
268        .collect();
269
270    let groups = hash_then_verify_dedup(
271        &tagged,
272        |(key, _ty)| *key,
273        |(_, a), (_, b)| same_specialization(*a, *b),
274    );
275
276    let mut ids = vec![ClassTemplateId(0); types.len()];
277    for (group_index, indices) in groups.into_iter().enumerate() {
278        for i in indices {
279            ids[i] = ClassTemplateId(group_index as u32);
280        }
281    }
282    ids
283}
284
285/// Task 2 step 5's discovery mechanism, named explicitly in the plan: "walks
286/// every instantiated specialization (every `new()` call site's resolved
287/// type), not just syntactic class declarations". This is the only way a
288/// generic class specialization becomes reachable at all when it is
289/// otherwise never a direct scope member anywhere in `design.root()`'s own
290/// symbol tree — `GenericClassDefSymbol`'s `specMap` (this module's own doc
291/// comment) is a private cache, not a scope, so a specialization that is
292/// only ever reached through a `new Box#(int)` expression has no OTHER
293/// discoverable home in the AST.
294///
295/// Visits every symbol in `eval`'s design (`Symbol::visit`, the same
296/// design-wide walk `check_no_class_declarations`/`discover_processes`
297/// already use elsewhere in this workspace) and, at each one, walks its
298/// `initializer()` (a variable/property's `= expr`) and `body()` (a
299/// procedural block's or subroutine's — including a class method's and the
300/// constructor's — root statement) generically via [`sv_lang::SemNode::children`]
301/// — the one tree accessor that already spans every AST domain (expression,
302/// statement, timing control, …) — checking only
303/// `ExpressionKind::NewClass` and otherwise recursing unconditionally. This
304/// is a "collect one kind, visit everything the same way" walk, not a
305/// classification match, so it carries none of ARCHITECTURE §7 item 9's
306/// exhaustive-kind-match obligation: it never branches on, nor chooses
307/// different behavior for, any other node kind.
308///
309/// **Scope boundary, stated rather than silently narrowed**: this walk finds
310/// a specialization only where a `new(...)` expression names it. A
311/// specialization referenced ONLY through a declared variable's type with no
312/// `new()` anywhere in the design (e.g. a bare `Box#(int) handle;`, always
313/// `null`, never allocated) is not discovered by this function — exactly the
314/// plan's own literal text for this step, and consistent with this task's
315/// job being "the identity a `new()`-allocated object's `ObjEntry.template`
316/// field will need" (Task 5), not a bare declared type's set of reachable
317/// specializations. Task 9 (parameterized classes) revisits specialization
318/// discovery as real codegen consumes it.
319pub fn collect_new_class_types<'d>(eval: &'d sv_lang::EvalSession<'d>) -> Vec<sv_lang::Type<'d>> {
320    discover_classes(eval).0
321}
322
323/// Every class a static property of which is initialized by a NON-constant
324/// expression (`static bit m__initialized = __deferred_init();`) that the
325/// design names anywhere -- a `new`, a variable's or typedef's declared type,
326/// a base class, or a declaration -- not only a `new` site. LRM 6.21 / 8.9:
327/// a static property is initialized once, before time zero, whether or not
328/// anything ever touches it (UVM registers every `uvm_*_utils` type this
329/// way), so each such class needs a time-zero initializer.
330pub fn classes_with_dynamic_static_init<'d>(
331    eval: &'d sv_lang::EvalSession<'d>,
332) -> Vec<sv_lang::Symbol<'d>> {
333    let (_, scanned) = discover_classes(eval);
334    scanned
335        .into_iter()
336        .filter(|cls| {
337            classify_members(*cls).is_ok_and(|m| {
338                m.statics.iter().any(|s| {
339                    s.initializer()
340                        .is_some_and(|init| init.constant_value().is_none())
341                })
342            })
343        })
344        .collect()
345}
346
347/// Every method and constructor of every class the design names (each class
348/// once, discovery order): the code a class-handle call may run.
349pub fn class_method_symbols<'d>(eval: &'d sv_lang::EvalSession<'d>) -> Vec<sv_lang::Symbol<'d>> {
350    let (_, scanned) = discover_classes(eval);
351    scanned
352        .into_iter()
353        .filter_map(|cls| classify_members(cls).ok())
354        .flat_map(|m| m.methods.into_iter().chain(m.ctor))
355        .collect()
356}
357
358/// [`collect_new_class_types`]'s walk, also returning every class symbol it
359/// scanned (each exactly once, in discovery order).
360fn discover_classes<'d>(
361    eval: &'d sv_lang::EvalSession<'d>,
362) -> (Vec<sv_lang::Type<'d>>, Vec<sv_lang::Symbol<'d>>) {
363    let mut out = Vec::new();
364    let mut seen = std::collections::HashSet::new();
365    let mut pending = Vec::new();
366    let mut scanned = Vec::new();
367    scan_new_class_types(
368        eval.design().root(),
369        &mut out,
370        &mut seen,
371        &mut pending,
372        &mut scanned,
373    );
374    // A specialization is scanned once, when first named; scanning it can
375    // name more (a `new` of another one, a base class).
376    let mut done = 0;
377    loop {
378        while done < out.len() {
379            let ty = out[done];
380            done += 1;
381            pending.push(ty.as_symbol());
382            pending.extend(base_chain(ty).into_iter().map(|b| b.as_symbol()));
383        }
384        let Some(cls) = pending.pop() else { break };
385        if seen.insert(cls.hierarchical_path().to_string()) {
386            scanned.push(cls);
387            scan_new_class_types(cls, &mut out, &mut seen, &mut pending, &mut scanned);
388        }
389    }
390    (out, scanned)
391}
392
393/// One [`collect_new_class_types`] walk of the symbol tree under `root`.
394///
395/// A class specialization that nothing in the tree DECLARES -- only a type
396/// alias (`typedef uvm_object_registry#(T, "T") type_id;`, whose `get()` is
397/// reached through `type_id::get()`), a variable's declared type or a `new`
398/// names it -- is not a child of any scope the walk descends, so its
399/// methods' `new` sites would be missed. Every class a symbol's declared
400/// type names is therefore queued in `pending` for the same scan (once per
401/// specialization, keyed by its hierarchical path).
402fn scan_new_class_types<'d>(
403    root: sv_lang::Symbol<'d>,
404    out: &mut Vec<sv_lang::Type<'d>>,
405    seen: &mut std::collections::HashSet<String>,
406    pending: &mut Vec<sv_lang::Symbol<'d>>,
407    scanned: &mut Vec<sv_lang::Symbol<'d>>,
408) {
409    root.visit(|sym| {
410        // A module elaborated only as an uninstantiated definition (not a
411        // top, never instantiated) or a generate branch not taken runs no
412        // code: its `new` sites -- whose class parameters may be error types
413        // there -- are not the design's.
414        if crate::netlist::is_uninstantiated_scope(&sym) {
415            return sv_lang::Walk::Skip;
416        }
417        if sym.kind() == SymbolKind::ClassType && seen.insert(sym.hierarchical_path().to_string()) {
418            scanned.push(sym);
419        }
420        if let Some(ty) = sym.declared_type()
421            && ty.canonical().is_class()
422        {
423            pending.push(ty.canonical().as_symbol());
424        }
425        if let Some(init) = sym.initializer() {
426            collect_new_class_types_from(init.as_sem_node(), out, pending);
427        }
428        if let Some(body) = crate::process::cfg::proc_body(&sym) {
429            collect_new_class_types_from(body.as_sem_node(), out, pending);
430        }
431        // LRM 8.24: an out-of-block method (`extern function f(); ... function
432        // C::f();`) is a member of the class only as a `MethodPrototype`; its
433        // body symbol is reached through the prototype, not by the walk.
434        if sym.kind() == SymbolKind::MethodPrototype
435            && let Some(imp) = sym.method_prototype_subroutine()
436        {
437            // Walk the whole out-of-block subroutine symbol, not only its body
438            // statement: its locals (`static T x = new;`) carry initializers.
439            scan_new_class_types(imp, out, seen, pending, scanned);
440        }
441        sv_lang::Walk::Continue
442    });
443}
444
445fn collect_new_class_types_from<'d>(
446    node: sv_lang::SemNode<'d>,
447    out: &mut Vec<sv_lang::Type<'d>>,
448    pending: &mut Vec<sv_lang::Symbol<'d>>,
449) {
450    if let Some(expr) = node.as_expression() {
451        if expr.kind() == sv_lang_kinds::ExpressionKind::NewClass
452            && let Some(ty) = expr.expr_type()
453        {
454            // LRM 8.25: `typedef vector#(4) Vfour; Vfour f = new;` types the
455            // `new` as the TypeAlias; every consumer keys on the class symbol.
456            out.push(ty.canonical());
457        }
458        // A static method called through its class (`uvm_typeid#(T)::get()`)
459        // names that specialization without any declaration: scan it too.
460        if expr.kind() == sv_lang_kinds::ExpressionKind::Call
461            && expr.is_system_call() != Some(true)
462            && let Some(cls) = expr
463                .call_subroutine()
464                .and_then(|s| s.parent())
465                .filter(|p| p.kind() == SymbolKind::ClassType)
466        {
467            pending.push(cls);
468        }
469    }
470    for child in crate::process::cfg::live_children(&node) {
471        collect_new_class_types_from(child, out, pending);
472    }
473}
474
475/// Task 2 step 1's `base_chain`: `ty`'s own ancestors, root (outermost) base
476/// first, `ty` itself NOT included — `ObjectLayout::for_template` (Task 6)
477/// concatenates this with `ty`'s own declared properties afterward, so
478/// including `ty` here would double it. Repeats `Type::class_base()` until
479/// it returns `None` (a root class with no `extends` clause).
480pub fn base_chain<'d>(ty: sv_lang::Type<'d>) -> Vec<sv_lang::Type<'d>> {
481    let mut chain = Vec::new();
482    let mut cur = ty.class_base();
483    while let Some(base) = cur {
484        cur = base.class_base();
485        chain.push(base);
486    }
487    chain.reverse();
488    chain
489}
490
491/// Task 2 step 1's `flattened_interfaces`: a thin wrapper over
492/// `Type::class_implemented_interfaces()`, which slang itself already
493/// returns pre-flattened through another implemented interface's own
494/// `extends` chain (confirmed live, Task 8: `interface class ic2 extends
495/// ic1; class x implements ic2;` makes `x`'s own call return BOTH `ic2`
496/// and `ic1`).
497///
498/// Correction (Task 8, found running its own `interface_class_via_base_
499/// deferred_impl.sv` fixture -- this module's own earlier doc comment
500/// claimed `ty.class_implemented_interfaces()` was ALSO already flattened
501/// "transitively ... through a base class", which is false): `ty`'s own
502/// call reflects only `ty`'s OWN `implements` clause (`ClassType::
503/// implementsIfaces` is populated solely by `handleImplements`, which runs
504/// only when `ty` itself declares one) -- a `Derived extends Base` with no
505/// `implements` clause of its own, whose `Base` directly `implements
506/// IFoo`, has an EMPTY `flattened_interfaces(derived_ty)` even though LRM
507/// 8.26's own text ("a subclass implicitly implements all of the interface
508/// classes implemented by its superclass") makes `Derived` an `IFoo` too.
509/// A caller that needs the FULL transitive set across a base chain must
510/// call this once per level and union the results -- `VtableLayout::
511/// for_template`'s own Task 8 section does exactly that; this function
512/// itself stays the same thin, one-level wrapper `base_chain`'s own shape
513/// expects so Task 8 (interface classes) has one stable name to call.
514pub fn flattened_interfaces<'d>(ty: sv_lang::Type<'d>) -> Vec<sv_lang::Type<'d>> {
515    ty.class_implemented_interfaces().collect()
516}
517
518/// Task 2 step 2's `ClassMembers`: one class's own (non-inherited) members,
519/// split by role. `statics`/`fields` are `ClassProperty` members split by
520/// `variable_lifetime()` (8.9: a `static` property has one copy for the
521/// whole simulation; an ordinary, "automatic"-lifetime one is per-object);
522/// `methods` is every USER-declared `Subroutine`/`MethodPrototype` member
523/// that is not the constructor; `ctor` is the `Subroutine` member flagged
524/// `MethodFlags::CONSTRUCTOR` (8.7), if this class declares one. All four
525/// are in `members()`'s own declaration order — this task's own tests rely
526/// on that order, and no later task may re-sort them.
527///
528/// "USER-declared" is load-bearing: `slang` adds 8 implicit, bodied
529/// `Subroutine` members to EVERY class — `randomize`, `pre_randomize`,
530/// `post_randomize`, `get_randstate`, `set_randstate`, `srandom`,
531/// `rand_mode`, `constraint_mode` (LRM 18.6/18.12–18.14's "every class
532/// ... shall have the following built-in methods") — each flagged
533/// `MethodFlags::BUILT_IN`, verified live against a plain `class Base; int
534/// a; function void bump(); endclass` fixture
535/// (`netlist_classes.rs`'s own regression: `class_sym.members()` for that
536/// class yields `bump` PLUS all 8 of these, not `bump` alone). `classify_members`
537/// excludes them from `methods` — this plan's own header is explicit that
538/// `randomize()`/constraint solving (18.1–18.12/18.16–18.17) is the sibling
539/// constraints plan's job, not this one's, and dumping these 8 into
540/// `methods` would make Task 7's vtable-slot assignment try to mangle/lower
541/// them as if they were ordinary user methods.
542#[derive(Debug, Clone)]
543pub struct ClassMembers<'d> {
544    pub statics: Vec<sv_lang::Symbol<'d>>,
545    pub fields: Vec<sv_lang::Symbol<'d>>,
546    pub methods: Vec<sv_lang::Symbol<'d>>,
547    pub ctor: Option<sv_lang::Symbol<'d>>,
548}
549
550/// IEEE 1800-2023 18.5.1 checks on one constraint block of a class: an
551/// explicit `extern constraint p;` with no external block is an error, and
552/// "it shall be an error if a constraint block of the same name as a
553/// prototype appears in the same class declaration". slang only warns
554/// (MemberImplNotFound) and accepts the duplicate, so this catches both once
555/// per class, whether or not anything ever instantiates or randomizes it.
556fn check_constraint_member<'d>(
557    class_sym: sv_lang::Symbol<'d>,
558    member: sv_lang::Symbol<'d>,
559    seen: &mut std::collections::HashSet<String>,
560) -> Result<(), ElabError> {
561    if !seen.insert(member.name().to_string()) {
562        return Err(ElabError::Frontend(format!(
563            "class `{}` declares the constraint block `{}` more than once \
564             (IEEE 1800-2023 18.5.1)",
565            class_sym.name(),
566            member.name()
567        )));
568    }
569    let flags = member.constraint_block_flags();
570    if flags.contains(sv_lang::ConstraintBlockFlags::EXPLICIT_EXTERN)
571        && !flags.contains(sv_lang::ConstraintBlockFlags::PURE)
572        && member
573            .constraint_block_constraints()
574            .and_then(|c| c.as_constraint())
575            .is_none_or(|c| c.kind() == sv_lang::kinds::ConstraintKind::Invalid)
576    {
577        return Err(ElabError::Frontend(format!(
578            "`extern constraint {};` in class `{}` has no external constraint \
579             block (IEEE 1800-2023 18.5.1)",
580            member.name(),
581            class_sym.name()
582        )));
583    }
584    Ok(())
585}
586
587/// Runs the 18.5.1 constraint-prototype checks over `class_sym` without
588/// classifying the rest of the class, so a declared-but-unused class is still
589/// diagnosed.
590pub fn check_class_constraints(class_sym: sv_lang::Symbol<'_>) -> Result<(), ElabError> {
591    let mut seen = std::collections::HashSet::new();
592    for member in class_sym.members() {
593        if member.kind() == SymbolKind::ConstraintBlock {
594            check_constraint_member(class_sym, member, &mut seen)?;
595        }
596    }
597    Ok(())
598}
599
600/// Task 2 step 2: one pass over `class_sym.members()`, classifying every
601/// member into [`ClassMembers`]. Every `SymbolKind` a `ClassType`'s own
602/// scope can legitimately contain is explicitly listed — never a blanket
603/// `_` — so a genuinely unrecognized kind still falls to the typed-error arm
604/// below (ARCHITECTURE §7 item 9), matching `template.rs::build_members`'s
605/// own "known-benign, not a blanket `_`" pattern for the kinds this task
606/// does not itself classify: `Parameter`/`TypeParameter` (a specialization's
607/// own bound parameters — contribute to [`class_param_binding_hash`], not
608/// here), `ClassType`/`GenericClassDef` (a nested class declaration — Task
609/// 9's job, its own independent `ClassTemplateId` if ever specialized/`new`'d,
610/// not a member of ITS enclosing class's `ClassMembers`), `ConstraintBlock`/
611/// `CovergroupType` (this plan's own header: `randomize()`/constraint
612/// solving and coverage are explicitly the sibling constraints/coverage
613/// plans' job, not this one's), and the usual scope noise every symbol table
614/// in this workspace already treats as contributing nothing
615/// (`TypeAlias`/`ForwardingTypedef`/`EmptyMember`/`TransparentMember`/
616/// `DeferredMember`/`Attribute`/`WildcardImport`/`ExplicitImport`/`LetDecl`).
617pub fn classify_members<'d>(class_sym: sv_lang::Symbol<'d>) -> Result<ClassMembers<'d>, ElabError> {
618    use sv_lang::{MethodFlags, VariableLifetime};
619
620    let mut statics = Vec::new();
621    let mut fields = Vec::new();
622    let mut methods = Vec::new();
623    let mut ctor = None;
624    let mut constraint_names = std::collections::HashSet::new();
625
626    for member in class_sym.members() {
627        match member.kind() {
628            SymbolKind::ClassProperty => match member.variable_lifetime() {
629                VariableLifetime::Static => statics.push(member),
630                VariableLifetime::Automatic => fields.push(member),
631            },
632            SymbolKind::Subroutine => {
633                let flags = member.subroutine_flags();
634                if flags.contains(MethodFlags::CONSTRUCTOR) {
635                    ctor = Some(member);
636                } else if !flags.contains(MethodFlags::BUILT_IN) {
637                    // `MethodFlags::BUILT_IN` excludes slang's 8 implicit
638                    // randomize-family methods every class carries — see
639                    // this function's own doc comment above.
640                    methods.push(member);
641                }
642            }
643            SymbolKind::MethodPrototype => {
644                if !member
645                    .method_prototype_flags()
646                    .contains(MethodFlags::BUILT_IN)
647                {
648                    methods.push(member);
649                }
650            }
651            SymbolKind::ConstraintBlock => {
652                check_constraint_member(class_sym, member, &mut constraint_names)?;
653            }
654            SymbolKind::Parameter
655            | SymbolKind::TypeParameter
656            | SymbolKind::ClassType
657            | SymbolKind::GenericClassDef
658            | SymbolKind::CovergroupType
659            | SymbolKind::TypeAlias
660            | SymbolKind::ForwardingTypedef
661            | SymbolKind::EmptyMember
662            | SymbolKind::TransparentMember
663            | SymbolKind::DeferredMember
664            | SymbolKind::Attribute
665            | SymbolKind::WildcardImport
666            | SymbolKind::ExplicitImport
667            | SymbolKind::LetDecl => {}
668            other => {
669                return Err(ElabError::UnsupportedConstruct {
670                    hier_path: member.hierarchical_path(),
671                    detail: format!(
672                        "{other:?} class member not yet classified by this elaboration pass"
673                    ),
674                });
675            }
676        }
677    }
678
679    Ok(ClassMembers {
680        statics,
681        fields,
682        methods,
683        ctor,
684    })
685}
686
687/// Task 4 (`docs/superpowers/plans/2026-10-03-classes.md`, LRM 8.9): every
688/// distinct class template reachable from `eval`'s design (via
689/// [`collect_new_class_types`]/[`assign_class_template_ids`] — the same
690/// `new()`-site discovery Task 2 already built; see that function's own
691/// doc comment for the scope boundary this inherits: a specialization
692/// referenced only through a declared variable's type, with no `new()`
693/// anywhere in the design, is not discovered) that declares at least one
694/// `static` property — paired with one representative `Symbol` for that
695/// template. "Representative" is not a choice between distinct symbols:
696/// any two class `Type`s sharing a `ClassTemplateId` are, by
697/// [`same_specialization`]'s own pointer-identity ground truth, the
698/// literal same `ClassType`/specialized symbol.
699///
700/// A class with no `static` member (per [`classify_members`]) contributes
701/// no entry: it needs no singleton segment at all. This function is the
702/// one place that decides "needs a segment" — never re-derived at a call
703/// site (`state::instance::StateEngine::allocate_class_static_singletons`'s
704/// own caller is expected to build its `layouts` argument from exactly this
705/// list, never from a separately-filtered one).
706///
707/// Deduplicated and returned in ascending [`ClassTemplateId`] order —
708/// deterministic across repeat calls on the same `eval`, like every other
709/// query this module exposes.
710pub fn class_templates_with_statics<'d>(
711    eval: &'d sv_lang::EvalSession<'d>,
712) -> Result<Vec<(crate::netlist::ClassTemplateId, sv_lang::Symbol<'d>)>, ElabError> {
713    let types = collect_new_class_types(eval);
714    let ids = assign_class_template_ids(&types);
715
716    let mut by_id: HashMap<crate::netlist::ClassTemplateId, sv_lang::Symbol<'d>> = HashMap::new();
717    for (ty, id) in types.iter().zip(ids.iter()) {
718        by_id.entry(*id).or_insert_with(|| ty.as_symbol());
719    }
720
721    let mut ids_sorted: Vec<u32> = by_id.keys().map(|id| id.0).collect();
722    ids_sorted.sort_unstable();
723
724    let mut out = Vec::new();
725    for raw in ids_sorted {
726        let id = crate::netlist::ClassTemplateId(raw);
727        let sym = by_id[&id];
728        let members = classify_members(sym)?;
729        if !members.statics.is_empty() {
730            out.push((id, sym));
731        }
732    }
733    Ok(out)
734}