sv_sim_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/// `sv-sim-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}