From 82e900e48a66704cf22c385959e5ee1d6a56be7b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 17 Apr 2026 08:30:50 -0700 Subject: [PATCH 01/58] move synthesis naming to a common naming utility so all synthesizers agree on names --- lib/src/module.dart | 39 +- lib/src/synthesizers/synth_builder.dart | 5 +- lib/src/synthesizers/synthesizer.dart | 22 +- .../systemverilog_synthesizer.dart | 6 +- .../synthesizers/utilities/synth_logic.dart | 81 +-- .../utilities/synth_module_definition.dart | 60 +- .../synth_sub_module_instantiation.dart | 14 +- lib/src/utilities/signal_namer.dart | 271 ++++++++ test/naming_cases_test.dart | 583 ++++++++++++++++++ test/naming_consistency_test.dart | 247 ++++++++ 10 files changed, 1215 insertions(+), 113 deletions(-) create mode 100644 lib/src/utilities/signal_namer.dart create mode 100644 test/naming_cases_test.dart create mode 100644 test/naming_consistency_test.dart diff --git a/lib/src/module.dart b/lib/src/module.dart index 0fd51eac7..09e11fdc7 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // module.dart @@ -11,12 +11,12 @@ import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; - import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/signal_namer.dart'; import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -52,6 +52,41 @@ abstract class Module { /// An internal mapping of input names to their sources to this [Module]. late final Map _inputSources = {}; + // ─── Canonical naming (SignalNamer) ───────────────────────────── + + /// Lazily-constructed namer that owns the [Uniquifier] and the + /// sparse Logic→String cache. Initialized on first access. + @internal + late final SignalNamer signalNamer = _createSignalNamer(); + + SignalNamer _createSignalNamer() { + assert(hasBuilt, 'Module must be built before canonical names are bound.'); + return SignalNamer.forModule( + inputs: _inputs, + outputs: _outputs, + inOuts: _inOuts, + ); + } + + /// Returns the collision-free signal name for [logic] within this module. + String signalName(Logic logic) => signalNamer.nameOf(logic); + + /// Allocates a collision-free signal name in this module's namespace. + /// + /// Used by synthesizers to name connection nets, submodule instances, + /// intermediate wires, and other artifacts that have no user-created + /// [Logic] object. The returned name is guaranteed not to collide with + /// any signal name or any previously allocated name. + /// + /// When [reserved] is `true`, the exact [baseName] (after sanitization) is + /// claimed without modification; an exception is thrown if it collides. + String allocateSignalName(String baseName, {bool reserved = false}) => + signalNamer.allocate(baseName, reserved: reserved); + + /// Returns `true` if [name] has not yet been claimed as a signal name in + /// this module's namespace. + bool isSignalNameAvailable(String name) => signalNamer.isAvailable(name); + /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 54e312ab3..3b3a6011c 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_builder.dart @@ -56,6 +56,9 @@ class SynthBuilder { } } + // Allow the synthesizer to prepare with knowledge of top module(s) + synthesizer.prepare(this.tops); + final modulesToParse = [...tops]; for (var i = 0; i < modulesToParse.length; i++) { final moduleI = modulesToParse[i]; diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index b70c9338e..2d7730208 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesizer.dart @@ -6,18 +6,34 @@ // // 2021 August 26 // Author: Max Korbel -// import 'package:rohd/rohd.dart'; /// An object capable of converting a module into some new output format abstract class Synthesizer { + /// Called by [SynthBuilder] before synthesis begins, with the top-level + /// module(s) being synthesized. + /// + /// Override this method to perform any initialization that requires + /// knowledge of the top module, such as resolving port names to [Logic] + /// objects, or computing global signal sets. + /// + /// The default implementation does nothing. + void prepare(List tops) {} + /// Determines whether [module] needs a separate definition or can just be /// described in-line. bool generatesDefinition(Module module); /// Synthesizes [module] into a [SynthesisResult], given the mapping provided /// by [getInstanceTypeOfModule]. + /// + /// Optionally a [lookupExistingResult] callback may be supplied which + /// allows the synthesizer to query already-generated `SynthesisResult`s + /// for child modules (useful when building parent output that needs + /// information from children). SynthesisResult synthesize( - Module module, String Function(Module module) getInstanceTypeOfModule); + Module module, String Function(Module module) getInstanceTypeOfModule, + {SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults}); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..b83acb9cc 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synthesizer.dart @@ -137,7 +137,9 @@ class SystemVerilogSynthesizer extends Synthesizer { @override SynthesisResult synthesize( - Module module, String Function(Module module) getInstanceTypeOfModule) { + Module module, String Function(Module module) getInstanceTypeOfModule, + {SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults}) { assert( module is! SystemVerilog || module.generatedDefinitionType != DefinitionGenerationType.none, diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 64ed3bed1..4a9c0e20a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -12,7 +12,6 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a logic signal in the generated code within a module. @internal @@ -196,81 +195,25 @@ class SynthLogic { /// The name of this, if it has been picked. String? _name; - /// Picks a [name]. + /// Picks a [name] using the module's signal namer. /// /// Must be called exactly once. - void pickName(Uniquifier uniquifier) { + void pickName() { assert(_name == null, 'Should only pick a name once.'); - _name = _findName(uniquifier); + _name = _findName(); } /// Finds the best name from the collection of [Logic]s. - String _findName(Uniquifier uniquifier) { - // check for const - if (_constLogic != null) { - if (!_constNameDisallowed) { - return _constLogic!.value.toString(); - } else { - assert( - logics.length > 1, - 'If there is a constant, but the const name is not allowed, ' - 'there needs to be another option'); - } - } - - // check for reserved - if (_reservedLogic != null) { - return uniquifier.getUniqueName( - initialName: _reservedLogic!.name, reserved: true); - } - - // check for renameable - if (_renameableLogic != null) { - return uniquifier.getUniqueName( - initialName: _renameableLogic!.preferredSynthName); - } - - // pick a preferred, available, mergeable name, if one exists - final unpreferredMergeableLogics = []; - final uniquifiableMergeableLogics = []; - for (final mergeableLogic in _mergeableLogics) { - if (Naming.isUnpreferred(mergeableLogic.name)) { - unpreferredMergeableLogics.add(mergeableLogic); - } else if (!uniquifier.isAvailable(mergeableLogic.preferredSynthName)) { - uniquifiableMergeableLogics.add(mergeableLogic); - } else { - return uniquifier.getUniqueName( - initialName: mergeableLogic.preferredSynthName); - } - } - - // uniquify a preferred, mergeable name, if one exists - if (uniquifiableMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: uniquifiableMergeableLogics.first.preferredSynthName); - } - - // pick an available unpreferred mergeable name, if one exists, otherwise - // uniquify an unpreferred mergeable name - if (unpreferredMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: unpreferredMergeableLogics - .firstWhereOrNull((element) => - uniquifier.isAvailable(element.preferredSynthName)) - ?.preferredSynthName ?? - unpreferredMergeableLogics.first.preferredSynthName); - } - - // pick anything (unnamed) and uniquify as necessary (considering preferred) - // no need to prefer an available one here, since it's all unnamed - return uniquifier.getUniqueName( - initialName: _unnamedLogics - .firstWhereOrNull((element) => - !Naming.isUnpreferred(element.preferredSynthName)) - ?.preferredSynthName ?? - _unnamedLogics.first.preferredSynthName); - } + /// + /// Delegates to signal namer which handles constant value naming, priority + /// selection, and uniquification via the module's shared namespace. + String _findName() => + parentSynthModuleDefinition.module.signalNamer.nameOfBest( + logics, + constValue: _constLogic, + constNameDisallowed: _constNameDisallowed, + ); /// Creates an instance to represent [initialLogic] and any that merge /// into it. diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index b8b78476a..dac9075e8 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -14,7 +14,6 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. @@ -110,10 +109,6 @@ class SynthModuleDefinition { @override String toString() => "module name: '${module.name}'"; - /// Used to uniquify any identifiers, including signal names - /// and module instances. - final Uniquifier _synthInstantiationNameUniquifier; - /// Indicates whether [logic] has a corresponding present [SynthLogic] in /// this definition. @internal @@ -289,14 +284,7 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) - : _synthInstantiationNameUniquifier = Uniquifier( - reservedNames: { - ...module.inputs.keys, - ...module.outputs.keys, - ...module.inOuts.keys, - }, - ), - assert( + : assert( !(module is SystemVerilog && module.generatedDefinitionType == DefinitionGenerationType.none), @@ -465,6 +453,7 @@ class SynthModuleDefinition { final receiverIsSubModuleOutput = receiver.isOutput && (receiver.parentModule?.parent == module); + if (receiverIsSubModuleOutput) { final subModule = receiver.parentModule!; @@ -513,6 +502,7 @@ class SynthModuleDefinition { _collapseArrays(); _collapseAssignments(); _assignSubmodulePortMapping(); + _pruneUnused(); process(); _pickNames(); @@ -752,49 +742,59 @@ class SynthModuleDefinition { } /// Picks names of signals and sub-modules. + /// + /// Signal names are read from [Module.signalName] (for user-created + /// [Logic] objects) or kept as literal constants. Submodule instance + /// names and synthesizer artifacts are allocated from the shared + /// [Module] namespace via [Module.allocateSignalName], guaranteeing no + /// collisions across synthesizers. void _pickNames() { - // first ports get priority + // Name allocation order matters — earlier claims get the unsuffixed name + // when there are collisions. This matches production ROHD priority: + // 1. Ports (reserved by _initNamespace, claimed via signalName) + // 2. Reserved submodule instances + // 3. Reserved internal signals + // 4. Non-reserved submodule instances + // 5. Non-reserved internal signals for (final input in inputs) { - input.pickName(_synthInstantiationNameUniquifier); + input.pickName(); } for (final output in outputs) { - output.pickName(_synthInstantiationNameUniquifier); + output.pickName(); } for (final inOut in inOuts) { - inOut.pickName(_synthInstantiationNameUniquifier); + inOut.pickName(); } - // pick names of *reserved* submodule instances - final nonReservedSubmodules = []; + // Reserved submodule instances first (they assert their exact name). for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { - submodule.pickName(_synthInstantiationNameUniquifier); + submodule.pickName(module); assert(submodule.module.name == submodule.name, 'Expect reserved names to retain their name.'); - } else { - nonReservedSubmodules.add(submodule); } } - // then *reserved* internal signals get priority + // Reserved internal signals next. final nonReservedSignals = []; for (final signal in internalSignals) { if (signal.isReserved) { - signal.pickName(_synthInstantiationNameUniquifier); + signal.pickName(); } else { nonReservedSignals.add(signal); } } - // then submodule instances - for (final submodule in nonReservedSubmodules - .where((element) => element.needsInstantiation)) { - submodule.pickName(_synthInstantiationNameUniquifier); + // Then non-reserved submodule instances. + for (final submodule in subModuleInstantiations) { + if (!submodule.module.reserveName && submodule.needsInstantiation) { + submodule.pickName(module); + } } - // then the rest of the internal signals + // Then the rest of the internal signals. for (final signal in nonReservedSignals) { - signal.pickName(_synthInstantiationNameUniquifier); + signal.pickName(); } } diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 80a415a09..4f1c3e4f2 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_sub_module_instantiation.dart @@ -11,7 +11,6 @@ import 'dart:collection'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents an instantiation of a module within another module. class SynthSubModuleInstantiation { @@ -25,13 +24,16 @@ class SynthSubModuleInstantiation { String get name => _name!; /// Selects a name for this module instance. Must be called exactly once. - void pickName(Uniquifier uniquifier) { + /// + /// Names are allocated from [parentModule]'s shared namespace via + /// [Module.allocateSignalName], ensuring no collision with signal names or + /// other submodule instances — even across multiple synthesizers. + void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = uniquifier.getUniqueName( - initialName: module.uniqueInstanceName, + _name = parentModule.allocateSignalName( + module.uniqueInstanceName, reserved: module.reserveName, - nullStarter: 'm', ); } diff --git a/lib/src/utilities/signal_namer.dart b/lib/src/utilities/signal_namer.dart new file mode 100644 index 000000000..b7d9dc090 --- /dev/null +++ b/lib/src/utilities/signal_namer.dart @@ -0,0 +1,271 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_namer.dart +// Collision-free signal naming within a module scope. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +/// Assigns collision-free names to [Logic] signals within a single module. +/// +/// Wraps a [Uniquifier] with a sparse Logic→String cache so that each +/// signal is named exactly once and every subsequent lookup is O(1). +/// +/// Port names are reserved at construction time. Internal signals are +/// named lazily on the first [nameOf] call. +@internal +class SignalNamer { + final Uniquifier _uniquifier; + + /// Sparse cache: only entries where the canonical name has been resolved. + /// Ports whose sanitized name == logic.name may be absent (fast-path + /// through [_portLogics] check). + final Map _names = {}; + + /// The set of port [Logic] objects, for O(1) port membership tests. + final Set _portLogics; + + SignalNamer._({ + required Uniquifier uniquifier, + required Map portRenames, + required Set portLogics, + }) : _uniquifier = uniquifier, + _portLogics = portLogics { + _names.addAll(portRenames); + } + + /// Creates a [SignalNamer] for the given module ports. + /// + /// Sanitized port names are reserved in the namespace. Ports whose + /// sanitized name differs from [Logic.name] are cached immediately. + factory SignalNamer.forModule({ + required Map inputs, + required Map outputs, + required Map inOuts, + }) { + final portRenames = {}; + final portLogics = {}; + final portNames = []; + + void collectPort(String rawName, Logic logic) { + final sanitized = Sanitizer.sanitizeSV(rawName); + portNames.add(sanitized); + portLogics.add(logic); + if (sanitized != logic.name) { + portRenames[logic] = sanitized; + } + } + + for (final entry in inputs.entries) { + collectPort(entry.key, entry.value); + } + for (final entry in outputs.entries) { + collectPort(entry.key, entry.value); + } + for (final entry in inOuts.entries) { + collectPort(entry.key, entry.value); + } + + // Claim each port name as reserved so that: + // (a) non-reserved signals can't steal them, and + // (b) a second reserved signal with the same name throws. + final uniquifier = Uniquifier(); + for (final name in portNames) { + uniquifier.getUniqueName(initialName: name, reserved: true); + } + + return SignalNamer._( + uniquifier: uniquifier, + portRenames: portRenames, + portLogics: portLogics, + ); + } + + /// Returns the canonical name for [logic]. + /// + /// The first call for a given [logic] allocates a collision-free name + /// via the underlying [Uniquifier]. Subsequent calls return the cached + /// result in O(1). + String nameOf(Logic logic) { + // Fast path: already named (port rename or previously-queried signal). + final cached = _names[logic]; + if (cached != null) { + return cached; + } + + // Port whose sanitized name == logic.name — already reserved. + if (_portLogics.contains(logic)) { + return logic.name; + } + + // First time seeing this internal signal — derive base name. + String baseName; + // Only treat as reserved for Uniquifier purposes if this is a true + // reserved internal signal (not a submodule port that happens to have + // Naming.reserved). + final isReservedInternal = logic.naming == Naming.reserved && !logic.isPort; + if (logic.naming == Naming.reserved || logic.isArrayMember) { + baseName = logic.name; + } else { + baseName = Sanitizer.sanitizeSV(logic.structureName); + } + + final name = _uniquifier.getUniqueName( + initialName: baseName, + reserved: isReservedInternal, + ); + _names[logic] = name; + return name; + } + + /// The base name that would be used for [logic] before uniquification. + static String baseName(Logic logic) => + (logic.naming == Naming.reserved || logic.isArrayMember) + ? logic.name + : Sanitizer.sanitizeSV(logic.structureName); + + /// Chooses the best name from a pool of merged [Logic] signals. + /// + /// When [constValue] is provided and [constNameDisallowed] is `false`, + /// the constant's value string is used directly as the name (no + /// uniquification). When [constNameDisallowed] is `true`, the constant + /// is excluded from the candidate pool and the normal priority applies. + /// + /// Priority (after constant handling): + /// 1. Port of this module (always wins — its name is already reserved). + /// 2. Reserved internal signal (exact name, throws on collision). + /// 3. Renameable signal. + /// 4. Preferred-available mergeable (base name not yet taken). + /// 5. Preferred-uniquifiable mergeable. + /// 6. Available-unpreferred mergeable. + /// 7. First unpreferred mergeable. + /// 8. Unnamed (prefer non-unpreferred base name). + /// + /// The winning name is allocated once and cached for the chosen [Logic]. + /// All other non-port [Logic]s in [candidates] are also cached to the + /// same name. + String nameOfBest( + Iterable candidates, { + Const? constValue, + bool constNameDisallowed = false, + }) { + // Constant whose literal value string is the name. + if (constValue != null && !constNameDisallowed) { + return constValue.value.toString(); + } + + // Classify using _portLogics membership (context-aware) rather than + // Logic.naming (context-independent), because submodule ports have + // Naming.reserved but should NOT be treated as reserved here. + Logic? port; + Logic? reserved; + Logic? renameable; + final preferredMergeable = []; + final unpreferredMergeable = []; + final unnamed = []; + + for (final logic in candidates) { + if (_portLogics.contains(logic)) { + port = logic; + } else if (logic.isPort) { + // Submodule port — treat as mergeable regardless of intrinsic naming, + // matching SynthModuleDefinition's namingOverride convention. + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else if (logic.naming == Naming.reserved) { + reserved = logic; + } else if (logic.naming == Naming.renameable) { + renameable = logic; + } else if (logic.naming == Naming.mergeable) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else { + unnamed.add(logic); + } + } + + // Port of this module — name already reserved in namespace. + if (port != null) { + return _nameAndCacheAll(port, candidates); + } + + // Reserved internal — must keep exact name (throws on collision). + if (reserved != null) { + return _nameAndCacheAll(reserved, candidates); + } + + // Renameable — preferred base, uniquified if needed. + if (renameable != null) { + return _nameAndCacheAll(renameable, candidates); + } + + // Preferred-available mergeable. + for (final logic in preferredMergeable) { + if (_uniquifier.isAvailable(baseName(logic))) { + return _nameAndCacheAll(logic, candidates); + } + } + + // Preferred-uniquifiable mergeable. + if (preferredMergeable.isNotEmpty) { + return _nameAndCacheAll(preferredMergeable.first, candidates); + } + + // Unpreferred mergeable — prefer available. + if (unpreferredMergeable.isNotEmpty) { + final best = unpreferredMergeable + .firstWhereOrNull((e) => _uniquifier.isAvailable(baseName(e))) ?? + unpreferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + // Unnamed — prefer non-unpreferred base name. + if (unnamed.isNotEmpty) { + final best = + unnamed.firstWhereOrNull((e) => !Naming.isUnpreferred(baseName(e))) ?? + unnamed.first; + return _nameAndCacheAll(best, candidates); + } + + throw StateError('No Logic candidates to name.'); + } + + /// Names [chosen] via [nameOf], then caches the same name for all other + /// non-port [Logic]s in [all]. + String _nameAndCacheAll(Logic chosen, Iterable all) { + final name = nameOf(chosen); + for (final logic in all) { + if (!identical(logic, chosen) && !_portLogics.contains(logic)) { + _names[logic] = name; + } + } + return name; + } + + /// Allocates a collision-free name for a non-signal artifact (wire, + /// instance, etc.). + /// + /// When [reserved] is `true`, the exact [baseName] (after sanitization) + /// is claimed without modification; an exception is thrown if it collides. + String allocate(String baseName, {bool reserved = false}) => + _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(baseName), + reserved: reserved, + ); + + /// Returns `true` if [name] has not yet been claimed in this namespace. + bool isAvailable(String name) => _uniquifier.isAvailable(name); +} diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart new file mode 100644 index 000000000..fbc1d9536 --- /dev/null +++ b/test/naming_cases_test.dart @@ -0,0 +1,583 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_cases_test.dart +// Systematic test of all signal-naming cases in the synthesis pipeline. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +// ════════════════════════════════════════════════════════ +// NAMING CROSS-PRODUCT TABLE +// ════════════════════════════════════════════════════════ +// +// Axis 1 — Naming enum (set at Logic construction time): +// reserved Exact name required; collision → exception. +// renameable Keeps name, uniquified on collision; never merged. +// mergeable May merge with equivalent signals; any merged name chosen. +// unnamed No user name; system generates one. +// +// Axis 2 — Context role (per SynthModuleDefinition): +// this-port Port of module being synthesized +// (namingOverride → reserved). +// sub-port Port of a child submodule +// (namingOverride → mergeable). +// internal Non-port signal inside the module (no override). +// const Const object (separate path via constValue). +// +// Axis 3 — Name preference: +// preferred baseName does NOT start with '_' +// unpreferred baseName starts with '_' +// +// Axis 4 — Constant context (only for Const): +// allowed Literal value string used as name. +// disallowed Feeding expressionlessInput; +// must use a wire name. +// +// ────────────────────────────────────────────────────── +// Row Naming Context Pref? Test Valid? +// Effective class → Outcome +// ────────────────────────────────────────────────────── +// 1 reserved this-port pref T1 ✓ +// port (in _portLogics) → exact sanitized name +// 2 reserved this-port unpref T2 ✓ unusual +// port → exact _-prefixed port name +// 3 reserved sub-port pref T3 ✓ +// preferred mergeable → merged, uniquified +// 4 reserved sub-port unpref T4 ✓ +// unpreferred mergeable → low-priority merge +// 5 reserved internal pref T5 ✓ +// reserved internal → exact name, throw on clash +// 6 reserved internal unpref T6 ✓ unusual +// reserved internal → exact _-prefixed name +// 7 renameable this-port pref — can't happen* +// port → exact port name +// 8 renameable sub-port pref — can't happen* +// preferred mergeable → merged +// 9 renameable internal pref T9 ✓ +// renameable → base name, uniquified +// 10 renameable internal unpref T10 ✓ unusual +// renameable → uniquified _-prefixed +// 11 mergeable this-port pref T11 ✓ +// port → exact port name (Logic.port()) +// 12 mergeable this-port unpref T12 ✓ unusual +// port → exact _-prefixed port name +// 13 mergeable sub-port pref T3 ✓ (=row 3) +// preferred mergeable → best-available merge +// 14 mergeable sub-port unpref T4 ✓ (=row 4) +// unpreferred mergeable → low-priority merge +// 15 mergeable internal pref T15 ✓ +// preferred mergeable → prefer available name +// 16 mergeable internal unpref T16 ✓ +// unpreferred mergeable → low-priority merge +// 17 unnamed this-port — — ✗ impossible** +// port → exact port name +// 18 unnamed sub-port — — ✗ impossible** +// mergeable → merged +// 19 unnamed internal (unpf) T19 ✓ +// unnamed → generated _s name +// 20 —(Const) — — T20 ✓ +// const allowed → literal value e.g. 8'h42 +// 21 —(Const) — — T21 ✓ +// const disallowed → wire name (not literal) +// ────────────────────────────────────────────────────── +// +// * Rows 7-8: addInput/addOutput always create +// Logic with Naming.reserved, so a port can +// never have intrinsic Naming.renameable. +// The namingOverride makes it moot anyway. +// +// ** Rows 17-18: addInput/addOutput require a +// non-null, non-empty name. chooseName() only +// yields Naming.unnamed for null/empty names, +// so a port can never be unnamed. +// +// ✗ unnamed + reserved: Logic(naming: reserved) +// with null/empty name throws +// NullReservedNameException / +// EmptyReservedNameException at construction +// time. Never reaches synthesizer. +// +// Additional cross-cutting concerns: +// COL Collision between mergeables +// → uniquified suffix (_0) +// MG Merge: directly-connected signals +// share SynthLogic +// INST Submodule instance names: unique, +// don't collide with ports +// ST Structure element: structureName +// = "parent.field" → sanitized ("_") +// AR Array element: isArrayMember +// → uses logic.name (index-based) +// +// ════════════════════════════════════════════════════════ + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Leaf sub-modules ────────────────────────────── + +/// A leaf module whose `in0` is an "expressionless input" — +/// meaning any constant driving it must get a real wire name, not a literal. +class _ExpressionlessSub extends Module with SystemVerilog { + @override + List get expressionlessInputs => const ['in0']; + + _ExpressionlessSub(Logic a, Logic b) : super(name: 'exprsub') { + a = addInput('in0', a, width: a.width); + b = addInput('in1', b, width: b.width); + addOutput('out', width: a.width) <= a & b; + } +} + +/// A simple sub-module with preferred-name ports. +class _SimpleSub extends Module { + _SimpleSub(Logic x) : super(name: 'simplesub') { + x = addInput('x', x, width: x.width); + addOutput('y', width: x.width) <= ~x; + } +} + +/// A sub-module with an unpreferred-name port. +class _UnprefSub extends Module { + _UnprefSub(Logic a) : super(name: 'unprefsub') { + a = addInput('_uport', a, width: a.width); + addOutput('uout', width: a.width) <= ~a; + } +} + +// ── Main test module ────────────────────────────── +// One module that exercises every valid naming case in a minimal design. +// Each signal is tagged with the row number from the table above. + +class _AllNamingCases extends Module { + // Exposed for test inspection. + // Row 1 / Row 2: ports (accessed via mod.input / mod.output). + // Row 5: + late final Logic reservedInternal; + // Row 6: + late final Logic reservedInternalUnpref; + // Row 9: + late final Logic renameableInternal; + // Row 10: + late final Logic renameableInternalUnpref; + // Row 15: + late final Logic mergeablePref; + // Row 15 collision partner: + late final Logic mergeablePrefCollide; + // Row 16: + late final Logic mergeableUnpref; + // Row 19: + late final Logic unnamed; + // Row 20: + late final Logic constAllowed; + // Row 21: + late final Logic constDisallowed; + // MG: + late final Logic mergeTarget; + + // Structure/array elements (ST, AR): + late final LogicStructure structPort; + late final LogicArray arrayPort; + + _AllNamingCases() : super(name: 'allcases') { + // ── Row 1: reserved + this-port + preferred ────────────────── + final inp = addInput('inp', Logic(width: 8), width: 8); + final out = addOutput('out', width: 8); + + // ── Row 2: reserved + this-port + unpreferred ──────────────── + final uInp = addInput('_uinp', Logic(width: 8), width: 8); + + // ── Row 11: mergeable + this-port + preferred ──────────────── + // (This is the Logic.port() → connectIO path. addInput forces + // Naming.reserved regardless of the source's naming, so intrinsic + // mergeable is overridden to reserved. We test the port keeps its + // exact name.) + final mPortInp = addInput('mport', Logic(width: 8), width: 8); + + // ── Row 12: mergeable + this-port + unpreferred ────────────── + final mPortUnpref = addInput('_muprt', Logic(width: 8), width: 8); + + // ── Row 5: reserved + internal + preferred ─────────────────── + reservedInternal = Logic(name: 'resv', width: 8, naming: Naming.reserved) + ..gets(inp ^ Const(0x01, width: 8)); + + // ── Row 6: reserved + internal + unpreferred ───────────────── + reservedInternalUnpref = + Logic(name: '_resvu', width: 8, naming: Naming.reserved) + ..gets(inp ^ Const(0x02, width: 8)); + + // ── Row 9: renameable + internal + preferred ───────────────── + renameableInternal = Logic(name: 'ren', width: 8, naming: Naming.renameable) + ..gets(inp ^ Const(0x03, width: 8)); + + // ── Row 10: renameable + internal + unpreferred ────────────── + renameableInternalUnpref = + Logic(name: '_renu', width: 8, naming: Naming.renameable) + ..gets(inp ^ Const(0x04, width: 8)); + + // ── Row 15: mergeable + internal + preferred ───────────────── + mergeablePref = Logic(name: 'mname', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x05, width: 8)); + + // ── COL: collision partner — same base name 'mname' ────────── + mergeablePrefCollide = + Logic(name: 'mname', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x06, width: 8)); + + // ── Row 16: mergeable + internal + unpreferred ─────────────── + mergeableUnpref = Logic(name: '_hidden', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x07, width: 8)); + + // ── Row 19: unnamed + internal ─────────────────────────────── + unnamed = Logic(width: 8)..gets(inp ^ Const(0x08, width: 8)); + + // ── Rows 3/13: sub-port preferred (via _SimpleSub.x / .y) ─── + // ── Row 4/14: sub-port unpreferred (via _UnprefSub._uport) ── + final sub = _SimpleSub(renameableInternal); + final subOut = sub.output('y'); + // Use a distinct expression so the submodule port doesn't merge with + // renameableInternal (which is renameable and would win). + final unpSub = _UnprefSub(inp ^ Const(0x0a, width: 8)); + + // ── MG: merge behavior — mergeTarget merges with subOut ────── + mergeTarget = Logic(name: 'mmerge', width: 8, naming: Naming.mergeable) + ..gets(subOut); + + // ── Row 20: constant with name allowed ─────────────────────── + constAllowed = + Const(0x42, width: 8).named('const_ok', naming: Naming.mergeable); + + // ── Row 21: constant with name disallowed (expressionlessInput) + constDisallowed = + Const(0x09, width: 8).named('const_wire', naming: Naming.mergeable); + // ignore: unused_local_variable + final exprSub = _ExpressionlessSub(constDisallowed, inp); + + // ── ST: structure element (structureName = "parent.field") ──── + structPort = _SimpleStruct(); + addInput('stIn', structPort, width: structPort.width); + + // ── AR: array element (isArrayMember, uses logic.name) ─────── + arrayPort = LogicArray([3], 8, name: 'arIn'); + addInputArray('arIn', arrayPort, dimensions: [3], elementWidth: 8); + + // Drive output to use all signals (prevents pruning). + out <= + mergeTarget | + mergeablePrefCollide | + mergeableUnpref | + unnamed | + constAllowed | + uInp | + mPortInp | + mPortUnpref | + reservedInternalUnpref | + renameableInternalUnpref | + unpSub.output('uout'); + } +} + +/// A minimal LogicStructure for testing structureName sanitization. +class _SimpleStruct extends LogicStructure { + final Logic field1; + final Logic field2; + + factory _SimpleStruct({String name = 'st'}) => _SimpleStruct._( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + name: name, + ); + + _SimpleStruct._(this.field1, this.field2, {required super.name}) + : super([field1, field2]); + + @override + LogicStructure clone({String? name}) => + _SimpleStruct(name: name ?? this.name); +} + +// ── Helpers ─────────────────────────────────────── + +/// Collects a map from Logic → picked name for all SynthLogics. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked (pruned/replaced) + } + } + return names; +} + +/// Finds a SynthLogic that contains [logic]. +SynthLogic? _findSynthLogic(SynthModuleDefinition def, Logic logic) { + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + if (sl.logics.contains(logic)) { + return sl; + } + } + return null; +} + +// ── Tests ──────────────────────────────────────── + +void main() { + late _AllNamingCases mod; + late SynthModuleDefinition def; + late Map names; + + setUp(() async { + mod = _AllNamingCases(); + await mod.build(); + def = SynthModuleDefinition(mod); + names = _collectNames(def); + }); + + group('naming cases', () { + // ── Row 1: reserved + this-port + preferred ──────────────── + + test('T1: reserved preferred port keeps exact name', () { + expect(names[mod.input('inp')], 'inp'); + expect(names[mod.output('out')], 'out'); + }); + + // ── Row 2: reserved + this-port + unpreferred ────────────── + + test('T2: reserved unpreferred port keeps exact _-prefixed name', () { + expect(names[mod.input('_uinp')], '_uinp'); + }); + + // ── Rows 3/13: sub-port + preferred (reserved or mergeable) ─ + + test('T3: submodule preferred port gets a name in parent', () { + final subX = mod.subModules.whereType<_SimpleSub>().first.input('x'); + final n = names[subX]; + expect(n, isNotNull, reason: 'Submodule port must be named'); + // Treated as preferred mergeable — name should not start with _. + expect(n, isNot(startsWith('_')), + reason: 'Preferred submodule port name should not be unpreferred'); + }); + + // ── Row 4/14: sub-port + unpreferred ──────────────────────── + + test('T4: submodule unpreferred port gets an unpreferred name', () { + final subUPort = + mod.subModules.whereType<_UnprefSub>().first.input('_uport'); + final n = names[subUPort]; + expect(n, isNotNull, reason: 'Submodule port must be named'); + expect(n, startsWith('_'), + reason: 'Unpreferred submodule port should keep _-prefix'); + }); + + // ── Row 5: reserved + internal + preferred ────────────────── + + test('T5: reserved preferred internal keeps exact name', () { + expect(names[mod.reservedInternal], 'resv'); + }); + + // ── Row 6: reserved + internal + unpreferred ──────────────── + + test('T6: reserved unpreferred internal keeps exact _-prefixed name', () { + expect(names[mod.reservedInternalUnpref], '_resvu'); + }); + + // ── Row 9: renameable + internal + preferred ──────────────── + + test('T9: renameable preferred internal gets its name', () { + final n = names[mod.renameableInternal]; + expect(n, isNotNull); + expect(n, contains('ren')); + }); + + // ── Row 10: renameable + internal + unpreferred ───────────── + + test('T10: renameable unpreferred internal keeps _-prefix', () { + final n = names[mod.renameableInternalUnpref]; + expect(n, isNotNull); + expect(n, startsWith('_'), + reason: 'Unpreferred renameable should keep _-prefix'); + expect(n, contains('renu')); + }); + + // ── Row 11: mergeable + this-port + preferred ─────────────── + + test('T11: mergeable-origin port (Logic.port) keeps exact port name', () { + // addInput overrides naming to reserved; the port name is exact. + expect(names[mod.input('mport')], 'mport'); + }); + + // ── Row 12: mergeable + this-port + unpreferred ───────────── + + test('T12: mergeable-origin unpreferred port keeps exact name', () { + expect(names[mod.input('_muprt')], '_muprt'); + }); + + // ── Row 15: mergeable + internal + preferred ──────────────── + + test('T15: mergeable preferred internal gets its name', () { + final n = names[mod.mergeablePref]; + expect(n, isNotNull); + expect(n, contains('mname')); + }); + + // ── COL: name collision → uniquified suffix ───────────────── + + test('COL: collision between two mergeables gets uniquified', () { + final n1 = names[mod.mergeablePref]; + final n2 = names[mod.mergeablePrefCollide]; + expect(n1, isNot(n2), reason: 'Colliding names must be uniquified'); + expect({n1, n2}, containsAll(['mname', 'mname_0'])); + }); + + // ── Row 16: mergeable + internal + unpreferred ────────────── + + test('T16: mergeable unpreferred internal keeps _-prefix', () { + final n = names[mod.mergeableUnpref]; + expect(n, isNotNull); + expect(n, startsWith('_'), + reason: 'Unpreferred mergeable should keep _-prefix'); + }); + + // ── Row 19: unnamed + internal ────────────────────────────── + + test('T19: unnamed signal gets a generated name', () { + final n = names[mod.unnamed]; + expect(n, isNotNull, reason: 'Unnamed signal must still get a name'); + // chooseName() gives unnamed signals a name starting with '_s'. + expect(n, startsWith('_'), + reason: 'Unnamed signals get unpreferred generated names'); + }); + + // ── Row 20: constant with name allowed ────────────────────── + + test('T20: constant with name allowed uses literal value', () { + final sl = _findSynthLogic(def, mod.constAllowed); + expect(sl, isNotNull); + if (sl != null && !sl.constNameDisallowed) { + expect(sl.name, contains("8'h42"), + reason: 'Allowed constant should use value literal'); + } + }); + + // ── Row 21: constant with name disallowed ─────────────────── + + test('T21: constant with name disallowed uses wire name', () { + final sl = _findSynthLogic(def, mod.constDisallowed); + expect(sl, isNotNull); + if (sl != null) { + if (sl.constNameDisallowed) { + expect(sl.name, isNot(contains("8'h09")), + reason: 'Disallowed constant should not use value literal'); + expect(sl.name, isNotEmpty); + } + } + }); + + // ── MG: merge behavior ────────────────────────────────────── + + test('MG: merged signals share the same SynthLogic', () { + final sl = _findSynthLogic(def, mod.mergeTarget); + expect(sl, isNotNull); + if (sl != null && sl.logics.length > 1) { + expect(sl.name, isNotEmpty); + } + }); + + // ── INST: submodule instance naming ───────────────────────── + + test('INST: submodule instances get collision-free names', () { + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + expect(instNames.toSet().length, instNames.length, + reason: 'Instance names must be unique'); + final portNames = {...mod.inputs.keys, ...mod.outputs.keys}; + for (final name in instNames) { + expect(portNames, isNot(contains(name)), + reason: 'Instance "$name" should not collide with a port'); + } + }); + + // ── ST: structure element naming ──────────────────────────── + + test('ST: structure element structureName is sanitized', () { + // structureName for field1 is "st.a" → sanitized to "st_a". + final stIn = mod.input('stIn'); + final n = names[stIn]; + expect(n, isNotNull); + // The port itself should keep its reserved name 'stIn'. + expect(n, 'stIn'); + }); + + // ── AR: array element naming ──────────────────────────────── + + test('AR: array port keeps its name', () { + // Array ports are registered via addInputArray with Naming.reserved. + final arIn = mod.input('arIn'); + final n = names[arIn]; + expect(n, isNotNull); + expect(n, 'arIn'); + }); + + // ── Impossible cases ──────────────────────────────────────── + + test('unnamed + reserved throws at construction time', () { + expect( + () => Logic(naming: Naming.reserved), + throwsA(isA()), + ); + expect( + () => Logic(name: '', naming: Naming.reserved), + throwsA(isA()), + ); + }); + + // ── Golden SV snapshot ────────────────────────────────────── + + test('golden SV output snapshot', () { + final sv = mod.generateSynth(); + + // Port declarations. + expect(sv, contains('input logic [7:0] inp')); + expect(sv, contains('output logic [7:0] out')); + expect(sv, contains('_uinp')); + expect(sv, contains('mport')); + expect(sv, contains('_muprt')); + + // Reserved internals. + expect(sv, contains('resv')); + expect(sv, contains('_resvu')); + + // Renameable internals. + expect(sv, contains('ren')); + expect(sv, contains('_renu')); + + // Constant literal (T20). + expect(sv, contains("8'h42")); + + // Submodule instantiations. + expect(sv, contains('simplesub')); + expect(sv, contains('exprsub')); + expect(sv, contains('unprefsub')); + }); + }); +} diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart new file mode 100644 index 000000000..53f95e6d8 --- /dev/null +++ b/test/naming_consistency_test.dart @@ -0,0 +1,247 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_consistency_test.dart +// Validates that both the SystemVerilog synthesizer and a base +// SynthModuleDefinition (used by the netlist synthesizer) produce +// consistent signal names via the shared Module.signalNamer. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Helper modules ────────────────────────────────────────────────── + +/// A simple module with ports, internal wires, and a sub-module. +class _Inner extends Module { + _Inner(Logic a, Logic b) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + addOutput('y', width: a.width) <= a & b; + } +} + +class _Outer extends Module { + _Outer(Logic a, Logic b) : super(name: 'outer') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final inner = _Inner(a, b); + addOutput('y', width: a.width) <= inner.output('y'); + } +} + +/// A module with a constant assignment (exercises const naming). +class _ConstModule extends Module { + _ConstModule(Logic a) : super(name: 'constmod') { + a = addInput('a', a, width: 8); + final c = Const(0x42, width: 8).named('myConst', naming: Naming.mergeable); + addOutput('y', width: 8) <= a + c; + } +} + +/// A module with Naming.renameable and Naming.mergeable signals. +class _MixedNaming extends Module { + _MixedNaming(Logic a) : super(name: 'mixednaming') { + a = addInput('a', a, width: 8); + final r = Logic(name: 'renamed', width: 8, naming: Naming.renameable) + ..gets(a); + final m = Logic(name: 'merged', width: 8, naming: Naming.mergeable) + ..gets(r); + addOutput('y', width: 8) <= m; + } +} + +/// A module with a FlipFlop sub-module. +class _FlopOuter extends Module { + _FlopOuter(Logic clk, Logic d) : super(name: 'flopouter') { + clk = addInput('clk', clk); + d = addInput('d', d, width: 8); + addOutput('q', width: 8) <= flop(clk, d); + } +} + +/// Builds [SynthModuleDefinition]s from both bases and collects a +/// Logic→name mapping for all present SynthLogics. +/// +/// Returns maps from Logic to its resolved signal name. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + // Skip SynthLogics whose name was never picked (replaced/pruned). + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked — skip + } + } + return names; +} + +void main() { + group('naming consistency', () { + test('SV and base SynthModuleDefinition agree on port names', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // SV synthesizer path + final svDef = SystemVerilogSynthModuleDefinition(mod); + + // Base path (same as netlist synthesizer uses) + // Since signalNamer is late final, the second constructor reuses + // the same naming state — names must be consistent. + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + // Every Logic present in both must have the same name. + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name} ' + '(${logic.runtimeType}, naming=${logic.naming})'); + } + } + + // Port names specifically must match. + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + expect(svNames[port], isNotNull, + reason: 'SV def should have port ${port.name}'); + expect(baseNames[port], isNotNull, + reason: 'Base def should have port ${port.name}'); + expect(svNames[port], baseNames[port], + reason: 'Port name must match for ${port.name}'); + } + }); + + test('constant naming is consistent', () async { + final mod = _ConstModule(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('mixed naming (renameable + mergeable) is consistent', () async { + final mod = _MixedNaming(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('flop module naming is consistent', () async { + final mod = _FlopOuter(Logic(), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('signalNamer is shared across multiple SynthModuleDefinitions', + () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // Build one def, then build another — same signalNamer instance. + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = _collectNames(def1); + final names2 = _collectNames(def2); + + for (final logic in names1.keys) { + if (names2.containsKey(logic)) { + expect(names2[logic], names1[logic], + reason: 'Shared namer should produce same name for ' + '${logic.name}'); + } + } + }); + + test('Module.signalName matches SynthLogic.name for ports', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + final synthNames = _collectNames(def); + + // Module.signalName uses SignalNamer.nameOf directly + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + final moduleName = mod.signalName(port); + final synthName = synthNames[port]; + expect(synthName, moduleName, + reason: 'SynthLogic.name and Module.signalName must agree ' + 'for port ${port.name}'); + } + }); + + test('submodule instance names are allocated from shared namespace', + () async { + // When building a single SynthModuleDefinition (as each synthesizer + // does), submodule instance names come from Module.allocateSignalName. + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toSet(); + + // The inner module instance should have a name + expect(instNames, isNotEmpty, + reason: 'Should have at least one submodule instance'); + + // All instance names should be obtainable from the module namespace + for (final name in instNames) { + expect(mod.isSignalNameAvailable(name), isFalse, + reason: 'Instance name "$name" should be claimed in namespace'); + } + }); + }); +} From 85f88cef0f472794689c9965b1be768fc5682b59 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 17 Apr 2026 08:36:09 -0700 Subject: [PATCH 02/58] dart 3.11 parameter_assignments pickiness --- analysis_options.yaml | 4 +++- lib/src/module.dart | 3 --- lib/src/signals/logic.dart | 1 - lib/src/signals/wire_net.dart | 1 - lib/src/utilities/simcompare.dart | 1 - lib/src/values/logic_value.dart | 3 --- 6 files changed, 3 insertions(+), 10 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 65d475023..2b2098177 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -129,7 +129,9 @@ linter: - overridden_fields - package_names - package_prefixed_library_names - - parameter_assignments + # parameter_assignments - disabled; ROHD idiomatically reassigns + # constructor parameters via addInput/addOutput. + # - parameter_assignments - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - prefer_asserts_with_message diff --git a/lib/src/module.dart b/lib/src/module.dart index 09e11fdc7..188b78890 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -702,7 +702,6 @@ abstract class Module { } if (source is LogicStructure) { - // ignore: parameter_assignments source = source.packed; } @@ -739,7 +738,6 @@ abstract class Module { String name, LogicType source) { _checkForSafePortName(name); - // ignore: parameter_assignments source = _validateType(source, isOutput: false, name: name); if (source.isNet || (source is LogicStructure && source.hasNets)) { @@ -848,7 +846,6 @@ abstract class Module { throw PortTypeException(source, 'Typed inOuts must be nets.'); } - // ignore: parameter_assignments source = _validateType(source, isOutput: false, name: name); _inOutDrivers.add(source); diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index 88afba0d6..4c5f99e5e 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -377,7 +377,6 @@ class Logic { // If we are connecting a `LogicStructure` to this simple `Logic`, // then pack it first. if (other is LogicStructure) { - // ignore: parameter_assignments other = other.packed; } diff --git a/lib/src/signals/wire_net.dart b/lib/src/signals/wire_net.dart index 78e8b1beb..f93529b0f 100644 --- a/lib/src/signals/wire_net.dart +++ b/lib/src/signals/wire_net.dart @@ -189,7 +189,6 @@ class _WireNetBlasted extends _Wire implements _WireNet { other as _WireNet; if (other is! _WireNetBlasted) { - // ignore: parameter_assignments other = other.toBlasted(); } diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index 3a25f4074..d7850df4e 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -282,7 +282,6 @@ abstract class SimCompare { : 'logic'); if (adjust != null) { - // ignore: parameter_assignments signalName = adjust(signalName); } diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart index 0cdc3c1df..81fc7304b 100644 --- a/lib/src/values/logic_value.dart +++ b/lib/src/values/logic_value.dart @@ -218,7 +218,6 @@ abstract class LogicValue implements Comparable { if (val.width == 1 && (!val.isValid || fill)) { if (!val.isValid) { - // ignore: parameter_assignments width ??= 1; } if (width == null) { @@ -243,7 +242,6 @@ abstract class LogicValue implements Comparable { if (val.length == 1 && (val == 'x' || val == 'z' || fill)) { if (val == 'x' || val == 'z') { - // ignore: parameter_assignments width ??= 1; } if (width == null) { @@ -269,7 +267,6 @@ abstract class LogicValue implements Comparable { if (val.length == 1 && (val.first == LogicValue.x || val.first == LogicValue.z || fill)) { if (!val.first.isValid) { - // ignore: parameter_assignments width ??= 1; } if (width == null) { From b7087c40467389ae38be40e2d4c599c0d532ebe7 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 17 Apr 2026 13:03:20 -0700 Subject: [PATCH 03/58] conflict resolved and dart format . works --- .../synthesizers/utilities/synth_logic.dart | 79 ------------------- 1 file changed, 79 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index d0a5e5d5a..b5827295b 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -221,7 +221,6 @@ class SynthLogic { } /// Finds the best name from the collection of [Logic]s. -<<<<<<< central_naming /// /// Delegates to signal namer which handles constant value naming, priority /// selection, and uniquification via the module's shared namespace. @@ -231,84 +230,6 @@ class SynthLogic { constValue: _constLogic, constNameDisallowed: _constNameDisallowed, ); -======= - String _findName(Uniquifier uniquifier) { - // check for const - if (_constLogic != null) { - if (!_constNameDisallowed) { - return _constLogic!.value.toString(); - } else { - assert( - logics.length > 1, - 'If there is a constant, but the const name is not allowed, ' - 'there needs to be another option', - ); - } - } - - // check for reserved - if (_reservedLogic != null) { - return uniquifier.getUniqueName( - initialName: _reservedLogic!.name, - reserved: true, - ); - } - - // check for renameable - if (_renameableLogic != null) { - return uniquifier.getUniqueName( - initialName: _renameableLogic!.preferredSynthName, - ); - } - - // pick a preferred, available, mergeable name, if one exists - final unpreferredMergeableLogics = []; - final uniquifiableMergeableLogics = []; - for (final mergeableLogic in _mergeableLogics) { - if (Naming.isUnpreferred(mergeableLogic.preferredSynthName)) { - unpreferredMergeableLogics.add(mergeableLogic); - } else if (!uniquifier.isAvailable(mergeableLogic.preferredSynthName)) { - uniquifiableMergeableLogics.add(mergeableLogic); - } else { - return uniquifier.getUniqueName( - initialName: mergeableLogic.preferredSynthName, - ); - } - } - - // uniquify a preferred, mergeable name, if one exists - if (uniquifiableMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: uniquifiableMergeableLogics.first.preferredSynthName, - ); - } - - // pick an available unpreferred mergeable name, if one exists, otherwise - // uniquify an unpreferred mergeable name - if (unpreferredMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: unpreferredMergeableLogics - .firstWhereOrNull( - (element) => - uniquifier.isAvailable(element.preferredSynthName), - ) - ?.preferredSynthName ?? - unpreferredMergeableLogics.first.preferredSynthName, - ); - } - - // pick anything (unnamed) and uniquify as necessary (considering preferred) - // no need to prefer an available one here, since it's all unnamed - return uniquifier.getUniqueName( - initialName: _unnamedLogics - .firstWhereOrNull( - (element) => !Naming.isUnpreferred(element.preferredSynthName), - ) - ?.preferredSynthName ?? - _unnamedLogics.first.preferredSynthName, - ); - } ->>>>>>> main /// Creates an instance to represent [initialLogic] and any that merge /// into it. From 4a55214d9448376d8900a9348422f04f0985cd06 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 18 Apr 2026 14:18:31 -0700 Subject: [PATCH 04/58] properly assign naming spaces for instances vs signals --- lib/src/module.dart | 41 ++++++- lib/src/synthesizers/synthesizer.dart | 8 +- .../systemverilog_synthesizer.dart | 3 +- .../utilities/synth_module_definition.dart | 9 +- .../synth_sub_module_instantiation.dart | 10 +- test/instance_signal_name_collision_test.dart | 108 ++++++++++++++++++ test/naming_consistency_test.dart | 16 ++- 7 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 test/instance_signal_name_collision_test.dart diff --git a/lib/src/module.dart b/lib/src/module.dart index 188b78890..8a6cd037b 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -68,25 +68,54 @@ abstract class Module { ); } + /// Separate namespace for submodule instance names. + /// + /// Instance names and signal names occupy different namespaces in + /// SystemVerilog (and most other HDLs), so they must be uniquified + /// independently to avoid false collisions. + @internal + late final Uniquifier instanceNameUniquifier = Uniquifier(); + /// Returns the collision-free signal name for [logic] within this module. String signalName(Logic logic) => signalNamer.nameOf(logic); - /// Allocates a collision-free signal name in this module's namespace. + /// Allocates a collision-free signal name in this module's signal namespace. /// - /// Used by synthesizers to name connection nets, submodule instances, - /// intermediate wires, and other artifacts that have no user-created - /// [Logic] object. The returned name is guaranteed not to collide with - /// any signal name or any previously allocated name. + /// Used by synthesizers to name connection nets, intermediate wires, and + /// other signal artifacts. The returned name is guaranteed not to collide + /// with any other signal name previously allocated in this module. /// /// When [reserved] is `true`, the exact [baseName] (after sanitization) is /// claimed without modification; an exception is thrown if it collides. String allocateSignalName(String baseName, {bool reserved = false}) => signalNamer.allocate(baseName, reserved: reserved); + /// Allocates a collision-free instance name in this module's instance + /// namespace. + /// + /// Instance names are kept separate from signal names because in + /// SystemVerilog (and other HDLs) they occupy distinct namespaces — a + /// signal and a submodule instance may legally share the same identifier + /// without collision. Mixing them into one uniquifier causes spurious + /// suffixing. + /// + /// When [reserved] is `true`, the exact [baseName] (after sanitization) is + /// claimed without modification; an exception is thrown if it collides. + String allocateInstanceName(String baseName, {bool reserved = false}) => + instanceNameUniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(baseName), + reserved: reserved, + ); + /// Returns `true` if [name] has not yet been claimed as a signal name in - /// this module's namespace. + /// this module's signal namespace. bool isSignalNameAvailable(String name) => signalNamer.isAvailable(name); + /// Returns `true` if [name] has not yet been claimed as an instance name in + /// this module's instance namespace. + bool isInstanceNameAvailable(String name) => + instanceNameUniquifier.isAvailable(name); + /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index 2d7730208..ce3d2c900 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -27,13 +27,7 @@ abstract class Synthesizer { /// Synthesizes [module] into a [SynthesisResult], given the mapping provided /// by [getInstanceTypeOfModule]. - /// - /// Optionally a [lookupExistingResult] callback may be supplied which - /// allows the synthesizer to query already-generated `SynthesisResult`s - /// for child modules (useful when building parent output that needs - /// information from children). SynthesisResult synthesize( Module module, String Function(Module module) getInstanceTypeOfModule, - {SynthesisResult? Function(Module module)? lookupExistingResult, - Map? existingResults}); + {Map? existingResults}); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index b83acb9cc..d50daf45a 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -138,8 +138,7 @@ class SystemVerilogSynthesizer extends Synthesizer { @override SynthesisResult synthesize( Module module, String Function(Module module) getInstanceTypeOfModule, - {SynthesisResult? Function(Module module)? lookupExistingResult, - Map? existingResults}) { + {Map? existingResults}) { assert( module is! SystemVerilog || module.generatedDefinitionType != DefinitionGenerationType.none, diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index dac9075e8..97722a629 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -744,10 +744,11 @@ class SynthModuleDefinition { /// Picks names of signals and sub-modules. /// /// Signal names are read from [Module.signalName] (for user-created - /// [Logic] objects) or kept as literal constants. Submodule instance - /// names and synthesizer artifacts are allocated from the shared - /// [Module] namespace via [Module.allocateSignalName], guaranteeing no - /// collisions across synthesizers. + /// [Logic] objects) or kept as literal constants and are allocated from + /// [Module.allocateSignalName] (signal namespace). Submodule instance + /// names are allocated from [Module.allocateInstanceName] (instance + /// namespace). The two namespaces are independent, matching SystemVerilog + /// semantics where signal and instance identifiers do not collide. void _pickNames() { // Name allocation order matters — earlier claims get the unsuffixed name // when there are collisions. This matches production ROHD priority: diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 4f1c3e4f2..4eaf83f57 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -25,13 +25,15 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// - /// Names are allocated from [parentModule]'s shared namespace via - /// [Module.allocateSignalName], ensuring no collision with signal names or - /// other submodule instances — even across multiple synthesizers. + /// Names are allocated from [parentModule]'s instance namespace via + /// [Module.allocateInstanceName], which is kept separate from the signal + /// namespace. In SystemVerilog (and other HDLs) instance names and signal + /// names occupy distinct namespaces, so they must be uniquified + /// independently to avoid spurious suffixing. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.allocateSignalName( + _name = parentModule.allocateInstanceName( module.uniqueInstanceName, reserved: module.reserveName, ); diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart new file mode 100644 index 000000000..0673e3522 --- /dev/null +++ b/test/instance_signal_name_collision_test.dart @@ -0,0 +1,108 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// instance_signal_name_collision_test.dart +// Regression test that demonstrates the bug present in the main branch where +// submodule instance names and signal names share a single Uniquifier. +// +// In SystemVerilog, signal identifiers and instance identifiers live in +// *separate* namespaces, so it is perfectly legal to have a signal called +// "inner" and a module instance also called "inner" in the same scope. +// +// When a single shared Uniquifier is used (main-branch behaviour), the second +// name to be allocated gets spuriously suffixed (e.g. "inner_0"), which +// produces incorrect generated SV. +// +// 2026 April 18 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Minimal repro modules ──────────────────────────────────────────────────── + +/// Leaf module whose default instance name is "inner". +class _Inner extends Module { + _Inner(Logic a) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + addOutput('y', width: a.width) <= a; + } +} + +/// Parent module that: +/// • instantiates [_Inner] (default instance name: "inner") +/// • names an internal wire "inner" as well +/// +/// In SV the two identifiers live in different namespaces, so both should +/// be emitted as "inner" without any suffix. +class _CollidingParent extends Module { + _CollidingParent(Logic a) : super(name: 'colliding_parent') { + a = addInput('a', a, width: a.width); + + // Internal wire explicitly named "inner". + final inner = Logic(name: 'inner', width: a.width, naming: Naming.reserved) + ..gets(a); + + // Submodule whose uniqueInstanceName will also be "inner". + final sub = _Inner(inner); + + addOutput('y', width: a.width) <= sub.output('y'); + } +} + +// ── Test ───────────────────────────────────────────────────────────────────── + +void main() { + group('instance / signal name collision (main-branch bug)', () { + late _CollidingParent mod; + late SynthModuleDefinition def; + + setUpAll(() async { + mod = _CollidingParent(Logic(width: 8)); + await mod.build(); + def = SynthModuleDefinition(mod); + }); + + test('internal signal named "inner" retains its exact name', () { + // Find the SynthLogic for the reserved "inner" wire. + final sl = def.internalSignals.cast().firstWhere( + (s) => s!.logics.any((l) => l.name == 'inner'), + orElse: () => null, + ); + expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); + expect(sl!.name, 'inner', + reason: 'Signal "inner" must not be suffixed to "inner_0"'); + }); + + test('submodule instance named "inner" retains its exact name', () { + final inst = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .cast() + .firstWhere( + (s) => s!.module.name == 'inner', + orElse: () => null, + ); + expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); + expect(inst!.name, 'inner', + reason: 'Instance "inner" must not be suffixed to "inner_0"'); + }); + + test('signal and instance may share the name "inner" without collision', () { + // Both should be "inner", not one of them "inner_0". + final sl = def.internalSignals.cast().firstWhere( + (s) => s!.logics.any((l) => l.name == 'inner'), + orElse: () => null, + ); + final inst = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .cast() + .firstWhere( + (s) => s!.module.name == 'inner', + orElse: () => null, + ); + expect(sl?.name, 'inner'); + expect(inst?.name, 'inner'); + }); + }); +} diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index 53f95e6d8..b569bd4d6 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -219,10 +219,12 @@ void main() { } }); - test('submodule instance names are allocated from shared namespace', + test('submodule instance names are allocated from the instance namespace', () async { - // When building a single SynthModuleDefinition (as each synthesizer - // does), submodule instance names come from Module.allocateSignalName. + // Instance names come from Module.allocateInstanceName, which is + // separate from the signal namespace (Module.allocateSignalName). + // A signal and a submodule instance may therefore share the same + // identifier without collision — matching SystemVerilog semantics. final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); @@ -237,10 +239,12 @@ void main() { expect(instNames, isNotEmpty, reason: 'Should have at least one submodule instance'); - // All instance names should be obtainable from the module namespace + // Instance names are claimed in the *instance* namespace, NOT the + // signal namespace. for (final name in instNames) { - expect(mod.isSignalNameAvailable(name), isFalse, - reason: 'Instance name "$name" should be claimed in namespace'); + expect(mod.isInstanceNameAvailable(name), isFalse, + reason: 'Instance name "$name" should be claimed in instance ' + 'namespace'); } }); }); From ed7be3696082ded32f01ccabaeb5e63b8efb1a02 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 18 Apr 2026 15:32:50 -0700 Subject: [PATCH 05/58] format issue --- test/instance_signal_name_collision_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart index 0673e3522..2cdfb2e3e 100644 --- a/test/instance_signal_name_collision_test.dart +++ b/test/instance_signal_name_collision_test.dart @@ -88,7 +88,8 @@ void main() { reason: 'Instance "inner" must not be suffixed to "inner_0"'); }); - test('signal and instance may share the name "inner" without collision', () { + test('signal and instance may share the name "inner" without collision', + () { // Both should be "inner", not one of them "inner_0". final sl = def.internalSignals.cast().firstWhere( (s) => s!.logics.any((l) => l.name == 'inner'), From ab09aed656059ee777755293f176bb354f417a84 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 19 Apr 2026 05:27:52 -0700 Subject: [PATCH 06/58] Controllable enforcement of signal vs instance name uniqueness. --- lib/src/module.dart | 37 +++++++++++++-- lib/src/synthesizers/synth_builder.dart | 3 -- lib/src/synthesizers/synthesizer.dart | 10 ---- lib/src/utilities/config.dart | 9 ++++ lib/src/utilities/signal_namer.dart | 47 +++++++++++++++---- test/instance_signal_name_collision_test.dart | 9 ++++ test/name_test.dart | 5 ++ 7 files changed, 96 insertions(+), 24 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index 8a6cd037b..475f48c68 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -65,6 +65,9 @@ abstract class Module { inputs: _inputs, outputs: _outputs, inOuts: _inOuts, + isAvailableInOtherNamespace: (name) => + !Config.ensureUniqueSignalAndInstanceNames || + instanceNameUniquifier.isAvailable(name), ); } @@ -101,11 +104,39 @@ abstract class Module { /// /// When [reserved] is `true`, the exact [baseName] (after sanitization) is /// claimed without modification; an exception is thrown if it collides. - String allocateInstanceName(String baseName, {bool reserved = false}) => - instanceNameUniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(baseName), + String allocateInstanceName(String baseName, {bool reserved = false}) { + final sanitizedBaseName = Sanitizer.sanitizeSV(baseName); + + if (!Config.ensureUniqueSignalAndInstanceNames) { + return instanceNameUniquifier.getUniqueName( + initialName: sanitizedBaseName, reserved: reserved, ); + } + + if (reserved) { + if (!instanceNameUniquifier.isAvailable(sanitizedBaseName, + reserved: true) || + !signalNamer.isAvailable(sanitizedBaseName)) { + throw UnavailableReservedNameException(sanitizedBaseName); + } + + return instanceNameUniquifier.getUniqueName( + initialName: sanitizedBaseName, + reserved: true, + ); + } + + var candidate = sanitizedBaseName; + var suffix = 0; + while (!instanceNameUniquifier.isAvailable(candidate) || + !signalNamer.isAvailable(candidate)) { + candidate = '${sanitizedBaseName}_$suffix'; + suffix++; + } + + return instanceNameUniquifier.getUniqueName(initialName: candidate); + } /// Returns `true` if [name] has not yet been claimed as a signal name in /// this module's signal namespace. diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 3b3a6011c..f9d0a0d08 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -56,9 +56,6 @@ class SynthBuilder { } } - // Allow the synthesizer to prepare with knowledge of top module(s) - synthesizer.prepare(this.tops); - final modulesToParse = [...tops]; for (var i = 0; i < modulesToParse.length; i++) { final moduleI = modulesToParse[i]; diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index ce3d2c900..7b350e8b4 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -11,16 +11,6 @@ import 'package:rohd/rohd.dart'; /// An object capable of converting a module into some new output format abstract class Synthesizer { - /// Called by [SynthBuilder] before synthesis begins, with the top-level - /// module(s) being synthesized. - /// - /// Override this method to perform any initialization that requires - /// knowledge of the top module, such as resolving port names to [Logic] - /// objects, or computing global signal sets. - /// - /// The default implementation does nothing. - void prepare(List tops) {} - /// Determines whether [module] needs a separate definition or can just be /// described in-line. bool generatesDefinition(Module module); diff --git a/lib/src/utilities/config.dart b/lib/src/utilities/config.dart index 4aa2ca8c6..89eda836a 100644 --- a/lib/src/utilities/config.dart +++ b/lib/src/utilities/config.dart @@ -11,4 +11,13 @@ class Config { /// The version of the ROHD framework. static const String version = '0.6.8'; + + /// Controls whether synthesized signal names and instance names must be + /// unique across both namespaces. + /// + /// When `true`, central naming cross-checks both namespaces during + /// allocation to avoid collisions in generated output. + /// + /// When `false`, signal and instance names are uniquified independently. + static bool ensureUniqueSignalAndInstanceNames = true; } diff --git a/lib/src/utilities/signal_namer.dart b/lib/src/utilities/signal_namer.dart index b7d9dc090..7f98fdff3 100644 --- a/lib/src/utilities/signal_namer.dart +++ b/lib/src/utilities/signal_namer.dart @@ -23,6 +23,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; @internal class SignalNamer { final Uniquifier _uniquifier; + final bool Function(String name) _isAvailableInOtherNamespace; /// Sparse cache: only entries where the canonical name has been resolved. /// Ports whose sanitized name == logic.name may be absent (fast-path @@ -36,8 +37,10 @@ class SignalNamer { required Uniquifier uniquifier, required Map portRenames, required Set portLogics, + required bool Function(String name) isAvailableInOtherNamespace, }) : _uniquifier = uniquifier, - _portLogics = portLogics { + _portLogics = portLogics, + _isAvailableInOtherNamespace = isAvailableInOtherNamespace { _names.addAll(portRenames); } @@ -49,6 +52,7 @@ class SignalNamer { required Map inputs, required Map outputs, required Map inOuts, + bool Function(String name)? isAvailableInOtherNamespace, }) { final portRenames = {}; final portLogics = {}; @@ -85,9 +89,36 @@ class SignalNamer { uniquifier: uniquifier, portRenames: portRenames, portLogics: portLogics, + isAvailableInOtherNamespace: + isAvailableInOtherNamespace ?? ((_) => true), ); } + bool _isAvailable(String name, {bool reserved = false}) => + _uniquifier.isAvailable(name, reserved: reserved) && + _isAvailableInOtherNamespace(name); + + String _allocateUniqueName(String baseName, {bool reserved = false}) { + if (reserved) { + if (!_isAvailable(baseName, reserved: true)) { + throw UnavailableReservedNameException(baseName); + } + + _uniquifier.getUniqueName(initialName: baseName, reserved: true); + return baseName; + } + + var candidate = baseName; + var suffix = 0; + while (!_isAvailable(candidate)) { + candidate = '${baseName}_$suffix'; + suffix++; + } + + _uniquifier.getUniqueName(initialName: candidate); + return candidate; + } + /// Returns the canonical name for [logic]. /// /// The first call for a given [logic] allocates a collision-free name @@ -117,8 +148,8 @@ class SignalNamer { baseName = Sanitizer.sanitizeSV(logic.structureName); } - final name = _uniquifier.getUniqueName( - initialName: baseName, + final name = _allocateUniqueName( + baseName, reserved: isReservedInternal, ); _names[logic] = name; @@ -214,7 +245,7 @@ class SignalNamer { // Preferred-available mergeable. for (final logic in preferredMergeable) { - if (_uniquifier.isAvailable(baseName(logic))) { + if (_isAvailable(baseName(logic))) { return _nameAndCacheAll(logic, candidates); } } @@ -227,7 +258,7 @@ class SignalNamer { // Unpreferred mergeable — prefer available. if (unpreferredMergeable.isNotEmpty) { final best = unpreferredMergeable - .firstWhereOrNull((e) => _uniquifier.isAvailable(baseName(e))) ?? + .firstWhereOrNull((e) => _isAvailable(baseName(e))) ?? unpreferredMergeable.first; return _nameAndCacheAll(best, candidates); } @@ -261,11 +292,11 @@ class SignalNamer { /// When [reserved] is `true`, the exact [baseName] (after sanitization) /// is claimed without modification; an exception is thrown if it collides. String allocate(String baseName, {bool reserved = false}) => - _uniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(baseName), + _allocateUniqueName( + Sanitizer.sanitizeSV(baseName), reserved: reserved, ); /// Returns `true` if [name] has not yet been claimed in this namespace. - bool isAvailable(String name) => _uniquifier.isAvailable(name); + bool isAvailable(String name) => _isAvailable(name); } diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart index 2cdfb2e3e..6ee10de92 100644 --- a/test/instance_signal_name_collision_test.dart +++ b/test/instance_signal_name_collision_test.dart @@ -18,6 +18,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/config.dart'; import 'package:test/test.dart'; // ── Minimal repro modules ──────────────────────────────────────────────────── @@ -57,13 +58,21 @@ void main() { group('instance / signal name collision (main-branch bug)', () { late _CollidingParent mod; late SynthModuleDefinition def; + late bool previousSetting; setUpAll(() async { + previousSetting = Config.ensureUniqueSignalAndInstanceNames; + Config.ensureUniqueSignalAndInstanceNames = false; + mod = _CollidingParent(Logic(width: 8)); await mod.build(); def = SynthModuleDefinition(mod); }); + tearDownAll(() { + Config.ensureUniqueSignalAndInstanceNames = previousSetting; + }); + test('internal signal named "inner" retains its exact name', () { // Find the SynthLogic for the reserved "inner" wire. final sl = def.internalSignals.cast().firstWhere( diff --git a/test/name_test.dart b/test/name_test.dart index 2742c0ec8..c863c04f5 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -136,6 +136,11 @@ void main() { final nameTypes = [nameType1, nameType2]; // skip ones that actually *should* cause a failure + // + // Note: SystemVerilog allows using the same identifier for a signal + // and an instance because they are different namespaces. However, + // Icarus Verilog rejects that pattern, so ROHD treats those as + // conflicts for simulator compatibility. final shouldConflict = [ { NameType.internalModuleDefinition, From 520d2809fdfa844260aa7614bdf55d8655330b09 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 19 Apr 2026 06:58:00 -0700 Subject: [PATCH 07/58] Refactored to Namer class. No external API changes for ROHD --- lib/src/module.dart | 93 +---- lib/src/synthesizers/synthesizer.dart | 3 +- .../systemverilog_synthesizer.dart | 3 +- .../synthesizers/utilities/synth_logic.dart | 37 +- .../utilities/synth_module_definition.dart | 9 +- .../synth_sub_module_instantiation.dart | 6 +- lib/src/utilities/config.dart | 9 - lib/src/utilities/namer.dart | 349 ++++++++++++++++++ lib/src/utilities/signal_namer.dart | 18 +- test/instance_signal_name_collision_test.dart | 8 +- test/naming_consistency_test.dart | 23 +- test/naming_namespace_test.dart | 180 +++++++++ 12 files changed, 596 insertions(+), 142 deletions(-) create mode 100644 lib/src/utilities/namer.dart create mode 100644 test/naming_namespace_test.dart diff --git a/lib/src/module.dart b/lib/src/module.dart index 475f48c68..02e02ad63 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -15,8 +15,8 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/signal_namer.dart'; import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -52,101 +52,22 @@ abstract class Module { /// An internal mapping of input names to their sources to this [Module]. late final Map _inputSources = {}; - // ─── Canonical naming (SignalNamer) ───────────────────────────── + // ─── Central naming (Namer) ───────────────────────────────────── - /// Lazily-constructed namer that owns the [Uniquifier] and the - /// sparse Logic→String cache. Initialized on first access. + /// Central namer that owns both the signal and instance namespaces. + /// Initialized lazily on first access (after build). @internal - late final SignalNamer signalNamer = _createSignalNamer(); + late final Namer namer = _createNamer(); - SignalNamer _createSignalNamer() { + Namer _createNamer() { assert(hasBuilt, 'Module must be built before canonical names are bound.'); - return SignalNamer.forModule( + return Namer.forModule( inputs: _inputs, outputs: _outputs, inOuts: _inOuts, - isAvailableInOtherNamespace: (name) => - !Config.ensureUniqueSignalAndInstanceNames || - instanceNameUniquifier.isAvailable(name), ); } - /// Separate namespace for submodule instance names. - /// - /// Instance names and signal names occupy different namespaces in - /// SystemVerilog (and most other HDLs), so they must be uniquified - /// independently to avoid false collisions. - @internal - late final Uniquifier instanceNameUniquifier = Uniquifier(); - - /// Returns the collision-free signal name for [logic] within this module. - String signalName(Logic logic) => signalNamer.nameOf(logic); - - /// Allocates a collision-free signal name in this module's signal namespace. - /// - /// Used by synthesizers to name connection nets, intermediate wires, and - /// other signal artifacts. The returned name is guaranteed not to collide - /// with any other signal name previously allocated in this module. - /// - /// When [reserved] is `true`, the exact [baseName] (after sanitization) is - /// claimed without modification; an exception is thrown if it collides. - String allocateSignalName(String baseName, {bool reserved = false}) => - signalNamer.allocate(baseName, reserved: reserved); - - /// Allocates a collision-free instance name in this module's instance - /// namespace. - /// - /// Instance names are kept separate from signal names because in - /// SystemVerilog (and other HDLs) they occupy distinct namespaces — a - /// signal and a submodule instance may legally share the same identifier - /// without collision. Mixing them into one uniquifier causes spurious - /// suffixing. - /// - /// When [reserved] is `true`, the exact [baseName] (after sanitization) is - /// claimed without modification; an exception is thrown if it collides. - String allocateInstanceName(String baseName, {bool reserved = false}) { - final sanitizedBaseName = Sanitizer.sanitizeSV(baseName); - - if (!Config.ensureUniqueSignalAndInstanceNames) { - return instanceNameUniquifier.getUniqueName( - initialName: sanitizedBaseName, - reserved: reserved, - ); - } - - if (reserved) { - if (!instanceNameUniquifier.isAvailable(sanitizedBaseName, - reserved: true) || - !signalNamer.isAvailable(sanitizedBaseName)) { - throw UnavailableReservedNameException(sanitizedBaseName); - } - - return instanceNameUniquifier.getUniqueName( - initialName: sanitizedBaseName, - reserved: true, - ); - } - - var candidate = sanitizedBaseName; - var suffix = 0; - while (!instanceNameUniquifier.isAvailable(candidate) || - !signalNamer.isAvailable(candidate)) { - candidate = '${sanitizedBaseName}_$suffix'; - suffix++; - } - - return instanceNameUniquifier.getUniqueName(initialName: candidate); - } - - /// Returns `true` if [name] has not yet been claimed as a signal name in - /// this module's signal namespace. - bool isSignalNameAvailable(String name) => signalNamer.isAvailable(name); - - /// Returns `true` if [name] has not yet been claimed as an instance name in - /// this module's instance namespace. - bool isInstanceNameAvailable(String name) => - instanceNameUniquifier.isAvailable(name); - /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index 7b350e8b4..687bbab03 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -18,6 +18,5 @@ abstract class Synthesizer { /// Synthesizes [module] into a [SynthesisResult], given the mapping provided /// by [getInstanceTypeOfModule]. SynthesisResult synthesize( - Module module, String Function(Module module) getInstanceTypeOfModule, - {Map? existingResults}); + Module module, String Function(Module module) getInstanceTypeOfModule); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d50daf45a..062647ac3 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -137,8 +137,7 @@ class SystemVerilogSynthesizer extends Synthesizer { @override SynthesisResult synthesize( - Module module, String Function(Module module) getInstanceTypeOfModule, - {Map? existingResults}) { + Module module, String Function(Module module) getInstanceTypeOfModule) { assert( module is! SystemVerilog || module.generatedDefinitionType != DefinitionGenerationType.none, diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index b5827295b..ad88bd6cc 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -11,11 +11,25 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; /// Represents a logic signal in the generated code within a module. @internal class SynthLogic { + /// Controls whether two constants with the same value driving separate + /// module inputs are merged into a single signal declaration. + /// + /// When `true` (the default), identical constants are collapsed to one + /// declaration — desirable for simulation-oriented output such as + /// SystemVerilog, where a single `assign wire = VALUE;` feeds all + /// downstream consumers. + /// + /// When `false`, each constant input keeps its own declaration. This is + /// useful for netlist/visualization outputs where seeing every individual + /// constant connection is more informative than an optimized fan-out net. + static bool mergeConstantInputs = true; + /// All [Logic]s represented, regardless of type. List get logics => UnmodifiableListView([ if (_reservedLogic != null) _reservedLogic!, @@ -225,7 +239,7 @@ class SynthLogic { /// Delegates to signal namer which handles constant value naming, priority /// selection, and uniquification via the module's shared namespace. String _findName() => - parentSynthModuleDefinition.module.signalNamer.nameOfBest( + parentSynthModuleDefinition.module.namer.signalNameOfBest( logics, constValue: _constLogic, constNameDisallowed: _constNameDisallowed, @@ -274,7 +288,12 @@ class SynthLogic { } /// Indicates whether two constants can be merged. + /// + /// Merging is only performed when [SynthLogic.mergeConstantInputs] is + /// `true`. Set it to `false` to keep each constant input as its own + /// declaration (e.g. for netlist/visualization output). static bool _constantsMergeable(SynthLogic a, SynthLogic b) => + SynthLogic.mergeConstantInputs && a.isConstant && b.isConstant && a._constLogic!.value == b._constLogic!.value && @@ -336,7 +355,7 @@ class SynthLogic { @override String toString() => '${_name == null ? 'null' : '"$name"'}, ' - 'logics contained: ${logics.map((e) => e.preferredSynthName).toList()}'; + 'logics contained: ${logics.map(Namer.baseName).toList()}'; /// Provides a definition for a range in SV from a width. static String _widthToRangeDef(int width, {bool forceRange = false}) { @@ -483,17 +502,3 @@ class SynthLogicArrayElement extends SynthLogic { ' parentArray=($parentArray), element ${logic.arrayIndex}, logic: $logic' ' logics contained: ${logics.map((e) => e.name).toList()}'; } - -extension on Logic { - /// Returns the preferred name for this [Logic] while generating in the synth - /// stack. - String get preferredSynthName => naming == Naming.reserved - // if reserved, keep the exact name - ? name - : isArrayMember - // arrays nicely name their elements already - ? name - // sanitize to remove any `.` in struct names - // the base `name` will be returned if not a structure. - : Sanitizer.sanitizeSV(structureName); -} diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 97722a629..73b4e95c3 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -743,12 +743,11 @@ class SynthModuleDefinition { /// Picks names of signals and sub-modules. /// - /// Signal names are read from [Module.signalName] (for user-created + /// Signal names are read from `Namer.signalNameOf `(for user-created /// [Logic] objects) or kept as literal constants and are allocated from - /// [Module.allocateSignalName] (signal namespace). Submodule instance - /// names are allocated from [Module.allocateInstanceName] (instance - /// namespace). The two namespaces are independent, matching SystemVerilog - /// semantics where signal and instance identifiers do not collide. + /// `Namer.allocateSignalName` (signal namespace). Submodule instance + /// names are allocated from `Namer.allocateInstanceName` (instance + /// namespace). Both namespaces are managed by the module's `Namer`. void _pickNames() { // Name allocation order matters — earlier claims get the unsuffixed name // when there are collisions. This matches production ROHD priority: diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 4eaf83f57..0cee7f1c9 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -25,15 +25,15 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// - /// Names are allocated from [parentModule]'s instance namespace via - /// [Module.allocateInstanceName], which is kept separate from the signal + /// Names are allocated from [parentModule]'s `Namer`'s instance namespace + /// via `Namer.allocateInstanceName`], which is kept separate from the signal /// namespace. In SystemVerilog (and other HDLs) instance names and signal /// names occupy distinct namespaces, so they must be uniquified /// independently to avoid spurious suffixing. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.allocateInstanceName( + _name = parentModule.namer.allocateInstanceName( module.uniqueInstanceName, reserved: module.reserveName, ); diff --git a/lib/src/utilities/config.dart b/lib/src/utilities/config.dart index 89eda836a..4aa2ca8c6 100644 --- a/lib/src/utilities/config.dart +++ b/lib/src/utilities/config.dart @@ -11,13 +11,4 @@ class Config { /// The version of the ROHD framework. static const String version = '0.6.8'; - - /// Controls whether synthesized signal names and instance names must be - /// unique across both namespaces. - /// - /// When `true`, central naming cross-checks both namespaces during - /// allocation to avoid collisions in generated output. - /// - /// When `false`, signal and instance names are uniquified independently. - static bool ensureUniqueSignalAndInstanceNames = true; } diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart new file mode 100644 index 000000000..f03f708fa --- /dev/null +++ b/lib/src/utilities/namer.dart @@ -0,0 +1,349 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// namer.dart +// Central collision-free naming for signals and instances within a module. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +/// Central namer that manages collision-free names for both signals and +/// submodule instances within a single module scope. +/// +/// Signal names and instance names occupy separate namespaces (matching +/// SystemVerilog semantics), but can optionally be cross-checked via +/// [uniquifySignalAndInstanceNames] for simulator compatibility. +/// +/// Port names are reserved at construction time. Internal signal names +/// are assigned lazily on the first [signalNameOf] call. Instance names +/// are allocated explicitly via [allocateInstanceName]. +@internal +class Namer { + /// Controls whether signal names and instance names must be unique + /// across both namespaces. + /// + /// When `true` (the default), allocations cross-check both namespaces + /// so that no identifier appears as both a signal and an instance name. + /// This is necessary for simulators like Icarus Verilog that reject + /// duplicate identifiers even across namespace boundaries. + /// + /// When `false`, signal and instance names are uniquified independently, + /// matching strict SystemVerilog semantics where instance and signal + /// identifiers occupy separate namespaces. + static bool uniquifySignalAndInstanceNames = true; + + // ─── Signal namespace ─────────────────────────────────────────── + + final Uniquifier _signalUniquifier; + + /// Sparse cache: only entries where the canonical name has been resolved. + /// Ports whose sanitized name == logic.name may be absent (fast-path + /// through [_portLogics] check). + final Map _signalNames = {}; + + /// The set of port [Logic] objects, for O(1) port membership tests. + final Set _portLogics; + + // ─── Instance namespace ───────────────────────────────────────── + + final Uniquifier _instanceUniquifier = Uniquifier(); + + // ─── Construction ─────────────────────────────────────────────── + + Namer._({ + required Uniquifier signalUniquifier, + required Map portRenames, + required Set portLogics, + }) : _signalUniquifier = signalUniquifier, + _portLogics = portLogics { + _signalNames.addAll(portRenames); + } + + /// Creates a [Namer] for the given module ports. + /// + /// Sanitized port names are reserved in the signal namespace. Ports + /// whose sanitized name differs from [Logic.name] are cached immediately. + factory Namer.forModule({ + required Map inputs, + required Map outputs, + required Map inOuts, + }) { + final portRenames = {}; + final portLogics = {}; + final portNames = []; + + void collectPort(String rawName, Logic logic) { + final sanitized = Sanitizer.sanitizeSV(rawName); + portNames.add(sanitized); + portLogics.add(logic); + if (sanitized != logic.name) { + portRenames[logic] = sanitized; + } + } + + for (final entry in inputs.entries) { + collectPort(entry.key, entry.value); + } + for (final entry in outputs.entries) { + collectPort(entry.key, entry.value); + } + for (final entry in inOuts.entries) { + collectPort(entry.key, entry.value); + } + + final uniquifier = Uniquifier(); + for (final name in portNames) { + uniquifier.getUniqueName(initialName: name, reserved: true); + } + + return Namer._( + signalUniquifier: uniquifier, + portRenames: portRenames, + portLogics: portLogics, + ); + } + + // ─── Signal availability / allocation ─────────────────────────── + + bool _isSignalAvailable(String name, {bool reserved = false}) => + _signalUniquifier.isAvailable(name, reserved: reserved) && + (!uniquifySignalAndInstanceNames || + _instanceUniquifier.isAvailable(name)); + + String _allocateUniqueSignalName(String baseName, {bool reserved = false}) { + if (reserved) { + if (!_isSignalAvailable(baseName, reserved: true)) { + throw UnavailableReservedNameException(baseName); + } + + _signalUniquifier.getUniqueName(initialName: baseName, reserved: true); + return baseName; + } + + var candidate = baseName; + var suffix = 0; + while (!_isSignalAvailable(candidate)) { + candidate = '${baseName}_$suffix'; + suffix++; + } + + _signalUniquifier.getUniqueName(initialName: candidate); + return candidate; + } + + /// Returns `true` if [name] has not yet been claimed in the signal + /// namespace. + bool isSignalNameAvailable(String name) => _isSignalAvailable(name); + + /// Allocates a collision-free name in the signal namespace. + /// + /// When [reserved] is `true`, the exact [baseName] (after sanitization) + /// is claimed without modification; an exception is thrown if it collides. + String allocateSignalName(String baseName, {bool reserved = false}) => + _allocateUniqueSignalName( + Sanitizer.sanitizeSV(baseName), + reserved: reserved, + ); + + // ─── Instance availability / allocation ───────────────────────── + + bool _isInstanceAvailable(String name, {bool reserved = false}) => + _instanceUniquifier.isAvailable(name, reserved: reserved) && + (!uniquifySignalAndInstanceNames || _signalUniquifier.isAvailable(name)); + + /// Returns `true` if [name] has not yet been claimed in the instance + /// namespace. + bool isInstanceNameAvailable(String name) => + _instanceUniquifier.isAvailable(name); + + /// Allocates a collision-free instance name. + /// + /// When [reserved] is `true`, the exact [baseName] (after sanitization) + /// is claimed without modification; an exception is thrown if it collides. + String allocateInstanceName(String baseName, {bool reserved = false}) { + final sanitizedBaseName = Sanitizer.sanitizeSV(baseName); + + if (!uniquifySignalAndInstanceNames) { + return _instanceUniquifier.getUniqueName( + initialName: sanitizedBaseName, + reserved: reserved, + ); + } + + if (reserved) { + if (!_isInstanceAvailable(sanitizedBaseName, reserved: true)) { + throw UnavailableReservedNameException(sanitizedBaseName); + } + + return _instanceUniquifier.getUniqueName( + initialName: sanitizedBaseName, + reserved: true, + ); + } + + var candidate = sanitizedBaseName; + var suffix = 0; + while (!_isInstanceAvailable(candidate)) { + candidate = '${sanitizedBaseName}_$suffix'; + suffix++; + } + + return _instanceUniquifier.getUniqueName(initialName: candidate); + } + + // ─── Signal naming (Logic → String) ───────────────────────────── + + /// Returns the canonical name for [logic]. + /// + /// The first call for a given [logic] allocates a collision-free name + /// via the underlying [Uniquifier]. Subsequent calls return the cached + /// result in O(1). + String signalNameOf(Logic logic) { + final cached = _signalNames[logic]; + if (cached != null) { + return cached; + } + + if (_portLogics.contains(logic)) { + return logic.name; + } + + String base; + final isReservedInternal = logic.naming == Naming.reserved && !logic.isPort; + if (logic.naming == Naming.reserved || logic.isArrayMember) { + base = logic.name; + } else { + base = Sanitizer.sanitizeSV(logic.structureName); + } + + final name = _allocateUniqueSignalName( + base, + reserved: isReservedInternal, + ); + _signalNames[logic] = name; + return name; + } + + /// The base name that would be used for [logic] before uniquification. + static String baseName(Logic logic) => + (logic.naming == Naming.reserved || logic.isArrayMember) + ? logic.name + : Sanitizer.sanitizeSV(logic.structureName); + + /// Chooses the best name from a pool of merged [Logic] signals. + /// + /// When [constValue] is provided and [constNameDisallowed] is `false`, + /// the constant's value string is used directly as the name (no + /// uniquification). When [constNameDisallowed] is `true`, the constant + /// is excluded from the candidate pool and the normal priority applies. + /// + /// Priority (after constant handling): + /// 1. Port of this module (always wins — its name is already reserved). + /// 2. Reserved internal signal (exact name, throws on collision). + /// 3. Renameable signal. + /// 4. Preferred-available mergeable (base name not yet taken). + /// 5. Preferred-uniquifiable mergeable. + /// 6. Available-unpreferred mergeable. + /// 7. First unpreferred mergeable. + /// 8. Unnamed (prefer non-unpreferred base name). + /// + /// The winning name is allocated once and cached for the chosen [Logic]. + /// All other non-port [Logic]s in [candidates] are also cached to the + /// same name. + String signalNameOfBest( + Iterable candidates, { + Const? constValue, + bool constNameDisallowed = false, + }) { + if (constValue != null && !constNameDisallowed) { + return constValue.value.toString(); + } + + Logic? port; + Logic? reserved; + Logic? renameable; + final preferredMergeable = []; + final unpreferredMergeable = []; + final unnamed = []; + + for (final logic in candidates) { + if (_portLogics.contains(logic)) { + port = logic; + } else if (logic.isPort) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else if (logic.naming == Naming.reserved) { + reserved = logic; + } else if (logic.naming == Naming.renameable) { + renameable = logic; + } else if (logic.naming == Naming.mergeable) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else { + unnamed.add(logic); + } + } + + if (port != null) { + return _nameAndCacheAll(port, candidates); + } + + if (reserved != null) { + return _nameAndCacheAll(reserved, candidates); + } + + if (renameable != null) { + return _nameAndCacheAll(renameable, candidates); + } + + for (final logic in preferredMergeable) { + if (_isSignalAvailable(baseName(logic))) { + return _nameAndCacheAll(logic, candidates); + } + } + + if (preferredMergeable.isNotEmpty) { + return _nameAndCacheAll(preferredMergeable.first, candidates); + } + + if (unpreferredMergeable.isNotEmpty) { + final best = unpreferredMergeable + .firstWhereOrNull((e) => _isSignalAvailable(baseName(e))) ?? + unpreferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + if (unnamed.isNotEmpty) { + final best = + unnamed.firstWhereOrNull((e) => !Naming.isUnpreferred(baseName(e))) ?? + unnamed.first; + return _nameAndCacheAll(best, candidates); + } + + throw StateError('No Logic candidates to name.'); + } + + /// Names [chosen] via [signalNameOf], then caches the same name for all + /// other non-port [Logic]s in [all]. + String _nameAndCacheAll(Logic chosen, Iterable all) { + final name = signalNameOf(chosen); + for (final logic in all) { + if (!identical(logic, chosen) && !_portLogics.contains(logic)) { + _signalNames[logic] = name; + } + } + return name; + } +} diff --git a/lib/src/utilities/signal_namer.dart b/lib/src/utilities/signal_namer.dart index 7f98fdff3..1f217489c 100644 --- a/lib/src/utilities/signal_namer.dart +++ b/lib/src/utilities/signal_namer.dart @@ -22,6 +22,19 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// named lazily on the first [nameOf] call. @internal class SignalNamer { + /// Controls whether synthesized signal names and instance names must be + /// unique across both namespaces. + /// + /// When `true` (the default), central naming cross-checks both namespaces + /// during allocation so that no identifier appears as both a signal and an + /// instance name. This is necessary for simulators like Icarus Verilog + /// that reject duplicate identifiers even across namespace boundaries. + /// + /// When `false`, signal and instance names are uniquified independently, + /// matching strict SystemVerilog semantics where instance and signal + /// identifiers occupy separate namespaces. + static bool uniquifySignalAndInstanceNames = true; + final Uniquifier _uniquifier; final bool Function(String name) _isAvailableInOtherNamespace; @@ -89,14 +102,13 @@ class SignalNamer { uniquifier: uniquifier, portRenames: portRenames, portLogics: portLogics, - isAvailableInOtherNamespace: - isAvailableInOtherNamespace ?? ((_) => true), + isAvailableInOtherNamespace: isAvailableInOtherNamespace ?? (_) => true, ); } bool _isAvailable(String name, {bool reserved = false}) => _uniquifier.isAvailable(name, reserved: reserved) && - _isAvailableInOtherNamespace(name); + (!uniquifySignalAndInstanceNames || _isAvailableInOtherNamespace(name)); String _allocateUniqueName(String baseName, {bool reserved = false}) { if (reserved) { diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart index 6ee10de92..c369f83e4 100644 --- a/test/instance_signal_name_collision_test.dart +++ b/test/instance_signal_name_collision_test.dart @@ -18,7 +18,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:test/test.dart'; // ── Minimal repro modules ──────────────────────────────────────────────────── @@ -61,8 +61,8 @@ void main() { late bool previousSetting; setUpAll(() async { - previousSetting = Config.ensureUniqueSignalAndInstanceNames; - Config.ensureUniqueSignalAndInstanceNames = false; + previousSetting = Namer.uniquifySignalAndInstanceNames; + Namer.uniquifySignalAndInstanceNames = false; mod = _CollidingParent(Logic(width: 8)); await mod.build(); @@ -70,7 +70,7 @@ void main() { }); tearDownAll(() { - Config.ensureUniqueSignalAndInstanceNames = previousSetting; + Namer.uniquifySignalAndInstanceNames = previousSetting; }); test('internal signal named "inner" retains its exact name', () { diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index b569bd4d6..c79221baa 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -4,7 +4,7 @@ // naming_consistency_test.dart // Validates that both the SystemVerilog synthesizer and a base // SynthModuleDefinition (used by the netlist synthesizer) produce -// consistent signal names via the shared Module.signalNamer. +// consistent signal names via the shared Module.namer. // // 2026 April 10 // Author: Desmond Kirkpatrick @@ -100,7 +100,7 @@ void main() { final svDef = SystemVerilogSynthModuleDefinition(mod); // Base path (same as netlist synthesizer uses) - // Since signalNamer is late final, the second constructor reuses + // Since namer is late final, the second constructor reuses // the same naming state — names must be consistent. final baseDef = SynthModuleDefinition(mod); @@ -181,12 +181,11 @@ void main() { } }); - test('signalNamer is shared across multiple SynthModuleDefinitions', - () async { + test('namer is shared across multiple SynthModuleDefinitions', () async { final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); - // Build one def, then build another — same signalNamer instance. + // Build one def, then build another — same namer instance. final def1 = SynthModuleDefinition(mod); final def2 = SynthModuleDefinition(mod); @@ -202,27 +201,27 @@ void main() { } }); - test('Module.signalName matches SynthLogic.name for ports', () async { + test('Namer.signalNameOf matches SynthLogic.name for ports', () async { final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); final def = SynthModuleDefinition(mod); final synthNames = _collectNames(def); - // Module.signalName uses SignalNamer.nameOf directly + // Module.namer.signalNameOf uses Namer directly for (final port in [...mod.inputs.values, ...mod.outputs.values]) { - final moduleName = mod.signalName(port); + final moduleName = mod.namer.signalNameOf(port); final synthName = synthNames[port]; expect(synthName, moduleName, - reason: 'SynthLogic.name and Module.signalName must agree ' + reason: 'SynthLogic.name and Module.namer.signalNameOf must agree ' 'for port ${port.name}'); } }); test('submodule instance names are allocated from the instance namespace', () async { - // Instance names come from Module.allocateInstanceName, which is - // separate from the signal namespace (Module.allocateSignalName). + // Instance names come from Module.namer.allocateInstanceName, which is + // separate from the signal namespace (Module.namer.allocateSignalName). // A signal and a submodule instance may therefore share the same // identifier without collision — matching SystemVerilog semantics. final mod = _Outer(Logic(width: 8), Logic(width: 8)); @@ -242,7 +241,7 @@ void main() { // Instance names are claimed in the *instance* namespace, NOT the // signal namespace. for (final name in instNames) { - expect(mod.isInstanceNameAvailable(name), isFalse, + expect(mod.namer.isInstanceNameAvailable(name), isFalse, reason: 'Instance name "$name" should be claimed in instance ' 'namespace'); } diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart new file mode 100644 index 000000000..32e55629d --- /dev/null +++ b/test/naming_namespace_test.dart @@ -0,0 +1,180 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_namespace_test.dart +// Tests for constant naming via nameOfBest, the tryMerge guard for +// constNameDisallowed, and separate instance/signal namespaces. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; +import 'package:test/test.dart'; + +/// A simple submodule whose instance name can collide with a signal name. +class _Inner extends Module { + _Inner(Logic a, {super.name = 'inner'}) { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// Top module that has a signal named the same as a submodule instance. +class _InstanceSignalCollision extends Module { + _InstanceSignalCollision({String instanceName = 'inner'}) + : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // Create a signal whose name matches the submodule instance name. + final sig = Logic(name: instanceName); + sig <= ~a; + + final sub = _Inner(sig, name: instanceName); + o <= sub.output('b'); + } +} + +/// Top module with two submodule instances that have the same name. +class _DuplicateInstances extends Module { + _DuplicateInstances() : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + final sub1 = _Inner(a, name: 'blk'); + final sub2 = _Inner(sub1.output('b'), name: 'blk'); + o <= sub2.output('b'); + } +} + +/// Module that uses a constant in a connection chain, exercising constant +/// naming through nameOfBest. +class _ConstantNamingModule extends Module { + _ConstantNamingModule() : super(name: 'const_mod') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // A constant "1" drives one input of the AND gate. + o <= a & Const(1); + } +} + +/// Module with a mux where one input is a constant, exercising the +/// constNameDisallowed path — the mux output cannot use the constant's +/// literal as its name because it also carries non-constant values. +class _ConstNameDisallowedModule extends Module { + _ConstNameDisallowedModule() : super(name: 'const_disallow') { + final a = addInput('a', Logic()); + final sel = addInput('sel', Logic()); + final o = addOutput('o'); + + // mux output can be the constant OR a, so the constant name is disallowed. + o <= mux(sel, Const(1), a); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + // Restore default. + Namer.uniquifySignalAndInstanceNames = true; + }); + + group('constant naming via nameOfBest', () { + test('constant value appears as literal in SV output', () async { + final dut = _ConstantNamingModule(); + await dut.build(); + final sv = dut.generateSynth(); + + // The constant "1" should appear as a literal 1'h1 in the output, + // not as a declared signal. + expect(sv, contains("1'h1")); + }); + + test('constNameDisallowed falls through to signal naming', () async { + final dut = _ConstNameDisallowedModule(); + await dut.build(); + final sv = dut.generateSynth(); + + // The output assignment should NOT use the raw constant literal + // as a wire name; a proper signal name should be used instead. + // The constant still appears as a literal in the mux expression. + expect(sv, contains("1'h1")); + // The output 'o' should be assigned from something. + expect(sv, contains('o')); + }); + }); + + group('separate instance and signal namespaces', () { + test( + 'signal and instance with same name do not collide ' + 'when namespaces are independent', () async { + Namer.uniquifySignalAndInstanceNames = false; + final dut = _InstanceSignalCollision(); + await dut.build(); + final sv = dut.generateSynth(); + + // With independent namespaces, the signal keeps its name 'inner' + // and the instance also keeps 'inner' — no spurious _0 suffix. + expect(sv, contains(RegExp(r'logic\s+inner[,;\s]'))); + expect(sv, isNot(contains('inner_0'))); + }); + + test( + 'signal and instance get suffixed when ' + 'ensureUniqueSignalAndInstanceNames is true', () async { + Namer.uniquifySignalAndInstanceNames = true; + final dut = _InstanceSignalCollision(); + await dut.build(); + final sv = dut.generateSynth(); + + // With cross-namespace checking enabled, the signal 'inner' is + // allocated first (during signal naming); when the instance tries + // to claim 'inner', it sees the signal namespace has it, so the + // instance OR signal gets a suffix. + expect(sv, contains('inner_0')); + }); + + test( + 'signal and instance do not spuriously suffix when ' + 'ensureUniqueSignalAndInstanceNames is false', () async { + Namer.uniquifySignalAndInstanceNames = false; + final dut = _InstanceSignalCollision(); + await dut.build(); + final sv = dut.generateSynth(); + + // With independent namespaces, no spurious suffixing. + expect(sv, isNot(contains('inner_0'))); + }); + + test('duplicate instance names get uniquified', () async { + final dut = _DuplicateInstances(); + await dut.build(); + final sv = dut.generateSynth(); + + // Two instances named 'blk' — one should be 'blk', the other 'blk_0'. + expect(sv, contains('blk')); + expect(sv, contains(RegExp(r'blk_\d'))); + }); + }); + + group('instance namespace independence', () { + test('allocateInstanceName is independent from allocateSignalName', + () async { + final dut = _InstanceSignalCollision(); + await dut.build(); + + // After build, the signal namer has 'inner' claimed. + // With independent namespaces, instance namespace should also accept + // 'inner' without conflict. + Namer.uniquifySignalAndInstanceNames = false; + + // The instance namespace should show 'inner' as available before + // any instance allocation. + // (After synthesis, names are already allocated, so we just verify + // the module built without error.) + expect(dut.generateSynth(), isNotEmpty); + }); + }); +} From 61d031928feb5156f1ab8bf697571bc9077b665e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 19 Apr 2026 20:14:20 -0700 Subject: [PATCH 08/58] signal registry --- test/signal_registry_test.dart | 142 +++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 test/signal_registry_test.dart diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart new file mode 100644 index 000000000..152b6091a --- /dev/null +++ b/test/signal_registry_test.dart @@ -0,0 +1,142 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_registry_test.dart +// Tests for Module canonical naming (SynthesisNameRegistry). +// +// 2026 April 14 + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +// ──────────────────────────────────────────────────────────────── +// Simple test modules +// ──────────────────────────────────────────────────────────────── + +class _GateMod extends Module { + _GateMod(Logic a, Logic b) : super(name: 'gatetestmodule') { + a = addInput('a', a); + b = addInput('b', b); + final aBar = addOutput('a_bar'); + final aAndB = addOutput('a_and_b'); + aBar <= ~a; + aAndB <= a & b; + } +} + +class _Counter extends Module { + _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi([ + SimpleClockGenerator(10).clk, + reset, + ], [ + If(reset, then: [ + val < 0, + ], orElse: [ + If(en, then: [val < nextVal]), + ]), + ]); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('signalName basics', () { + test('returns port names after build', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.namer.signalNameOf(mod.input('b')), equals('b')); + expect(mod.namer.signalNameOf(mod.output('a_bar')), equals('a_bar')); + expect(mod.namer.signalNameOf(mod.output('a_and_b')), equals('a_and_b')); + }); + + test('returns internal signal names', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('en')), equals('en')); + expect(mod.namer.signalNameOf(mod.input('reset')), equals('reset')); + expect(mod.namer.signalNameOf(mod.output('val')), equals('val')); + }); + + test('agrees with signalName after synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + for (final entry in mod.inputs.entries) { + expect( + mod.namer.signalNameOf(entry.value), + isNotNull, + reason: 'signalName should work for input ${entry.key}', + ); + } + for (final entry in mod.outputs.entries) { + expect( + mod.namer.signalNameOf(entry.value), + isNotNull, + reason: 'signalName should work for output ${entry.key}', + ); + } + }); + }); + + group('allocateSignalName', () { + test('avoids collision with existing names', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + final allocated = mod.namer.allocateSignalName('en'); + expect(allocated, isNot(equals('en')), + reason: 'Should not collide with existing port name'); + expect(allocated, contains('en'), + reason: 'Should be based on the requested name'); + }); + + test('successive allocations are unique', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + final a = mod.namer.allocateSignalName('wire'); + final b = mod.namer.allocateSignalName('wire'); + expect(a, isNot(equals(b)), reason: 'Each allocation should be unique'); + }); + }); + + group('sparse storage', () { + test('identity names not stored in renames', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.input('a').name, equals('a')); + }); + }); + + group('determinism', () { + test('same module produces identical canonical names', () async { + Future> buildAndGetNames() async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + return { + for (final sig in mod.signals) sig.name: mod.namer.signalNameOf(sig), + }; + } + + final names1 = await buildAndGetNames(); + await Simulator.reset(); + final names2 = await buildAndGetNames(); + + expect(names1, equals(names2)); + }); + }); +} From becdb369f6715cf29bd8f66050dfbd6cfe83c79a Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 1 May 2026 13:02:53 -0700 Subject: [PATCH 09/58] module context name uniquification instead of signal/instance split --- .../utilities/synth_module_definition.dart | 8 +- .../synth_sub_module_instantiation.dart | 7 +- lib/src/utilities/namer.dart | 106 ++---- lib/src/utilities/signal_namer.dart | 314 ------------------ test/instance_signal_name_collision_test.dart | 59 +--- test/naming_consistency_test.dart | 13 +- test/naming_namespace_test.dart | 65 +--- 7 files changed, 59 insertions(+), 513 deletions(-) delete mode 100644 lib/src/utilities/signal_namer.dart diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 73b4e95c3..1a4c97393 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -743,11 +743,11 @@ class SynthModuleDefinition { /// Picks names of signals and sub-modules. /// - /// Signal names are read from `Namer.signalNameOf `(for user-created + /// Signal names are read from `Namer.signalNameOf` (for user-created /// [Logic] objects) or kept as literal constants and are allocated from - /// `Namer.allocateSignalName` (signal namespace). Submodule instance - /// names are allocated from `Namer.allocateInstanceName` (instance - /// namespace). Both namespaces are managed by the module's `Namer`. + /// `Namer.allocateSignalName`. Submodule instance names are allocated + /// from `Namer.allocateInstanceName`. All names share a single + /// namespace managed by the module's `Namer`. void _pickNames() { // Name allocation order matters — earlier claims get the unsuffixed name // when there are collisions. This matches production ROHD priority: diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 0cee7f1c9..67f9e2832 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -25,11 +25,8 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// - /// Names are allocated from [parentModule]'s `Namer`'s instance namespace - /// via `Namer.allocateInstanceName`], which is kept separate from the signal - /// namespace. In SystemVerilog (and other HDLs) instance names and signal - /// names occupy distinct namespaces, so they must be uniquified - /// independently to avoid spurious suffixing. + /// Names are allocated from [parentModule]'s `Namer`'s shared namespace + /// via `Namer.allocateInstanceName`. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index f03f708fa..481dc64e3 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -16,31 +16,17 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// Central namer that manages collision-free names for both signals and /// submodule instances within a single module scope. /// -/// Signal names and instance names occupy separate namespaces (matching -/// SystemVerilog semantics), but can optionally be cross-checked via -/// [uniquifySignalAndInstanceNames] for simulator compatibility. +/// All identifiers (signals and instances) share a single namespace, +/// ensuring no name collisions in the generated SystemVerilog. /// /// Port names are reserved at construction time. Internal signal names /// are assigned lazily on the first [signalNameOf] call. Instance names /// are allocated explicitly via [allocateInstanceName]. @internal class Namer { - /// Controls whether signal names and instance names must be unique - /// across both namespaces. - /// - /// When `true` (the default), allocations cross-check both namespaces - /// so that no identifier appears as both a signal and an instance name. - /// This is necessary for simulators like Icarus Verilog that reject - /// duplicate identifiers even across namespace boundaries. - /// - /// When `false`, signal and instance names are uniquified independently, - /// matching strict SystemVerilog semantics where instance and signal - /// identifiers occupy separate namespaces. - static bool uniquifySignalAndInstanceNames = true; - - // ─── Signal namespace ─────────────────────────────────────────── + // ─── Shared namespace ─────────────────────────────────────────── - final Uniquifier _signalUniquifier; + final Uniquifier _uniquifier; /// Sparse cache: only entries where the canonical name has been resolved. /// Ports whose sanitized name == logic.name may be absent (fast-path @@ -50,17 +36,13 @@ class Namer { /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; - // ─── Instance namespace ───────────────────────────────────────── - - final Uniquifier _instanceUniquifier = Uniquifier(); - // ─── Construction ─────────────────────────────────────────────── Namer._({ - required Uniquifier signalUniquifier, + required Uniquifier uniquifier, required Map portRenames, required Set portLogics, - }) : _signalUniquifier = signalUniquifier, + }) : _uniquifier = uniquifier, _portLogics = portLogics { _signalNames.addAll(portRenames); } @@ -103,99 +85,65 @@ class Namer { } return Namer._( - signalUniquifier: uniquifier, + uniquifier: uniquifier, portRenames: portRenames, portLogics: portLogics, ); } - // ─── Signal availability / allocation ─────────────────────────── + // ─── Name availability / allocation ───────────────────────────── - bool _isSignalAvailable(String name, {bool reserved = false}) => - _signalUniquifier.isAvailable(name, reserved: reserved) && - (!uniquifySignalAndInstanceNames || - _instanceUniquifier.isAvailable(name)); + bool _isAvailable(String name, {bool reserved = false}) => + _uniquifier.isAvailable(name, reserved: reserved); - String _allocateUniqueSignalName(String baseName, {bool reserved = false}) { + String _allocateUniqueName(String baseName, {bool reserved = false}) { if (reserved) { - if (!_isSignalAvailable(baseName, reserved: true)) { + if (!_isAvailable(baseName, reserved: true)) { throw UnavailableReservedNameException(baseName); } - _signalUniquifier.getUniqueName(initialName: baseName, reserved: true); + _uniquifier.getUniqueName(initialName: baseName, reserved: true); return baseName; } var candidate = baseName; var suffix = 0; - while (!_isSignalAvailable(candidate)) { + while (!_isAvailable(candidate)) { candidate = '${baseName}_$suffix'; suffix++; } - _signalUniquifier.getUniqueName(initialName: candidate); + _uniquifier.getUniqueName(initialName: candidate); return candidate; } - /// Returns `true` if [name] has not yet been claimed in the signal - /// namespace. - bool isSignalNameAvailable(String name) => _isSignalAvailable(name); + /// Returns `true` if [name] has not yet been claimed in the namespace. + bool isNameAvailable(String name) => _isAvailable(name); /// Allocates a collision-free name in the signal namespace. /// /// When [reserved] is `true`, the exact [baseName] (after sanitization) /// is claimed without modification; an exception is thrown if it collides. String allocateSignalName(String baseName, {bool reserved = false}) => - _allocateUniqueSignalName( + _allocateUniqueName( Sanitizer.sanitizeSV(baseName), reserved: reserved, ); - // ─── Instance availability / allocation ───────────────────────── - - bool _isInstanceAvailable(String name, {bool reserved = false}) => - _instanceUniquifier.isAvailable(name, reserved: reserved) && - (!uniquifySignalAndInstanceNames || _signalUniquifier.isAvailable(name)); + // ─── Instance allocation ──────────────────────────────────────── - /// Returns `true` if [name] has not yet been claimed in the instance - /// namespace. - bool isInstanceNameAvailable(String name) => - _instanceUniquifier.isAvailable(name); + /// Returns `true` if [name] has not yet been claimed in the namespace. + bool isInstanceNameAvailable(String name) => _isAvailable(name); /// Allocates a collision-free instance name. /// /// When [reserved] is `true`, the exact [baseName] (after sanitization) /// is claimed without modification; an exception is thrown if it collides. - String allocateInstanceName(String baseName, {bool reserved = false}) { - final sanitizedBaseName = Sanitizer.sanitizeSV(baseName); - - if (!uniquifySignalAndInstanceNames) { - return _instanceUniquifier.getUniqueName( - initialName: sanitizedBaseName, + String allocateInstanceName(String baseName, {bool reserved = false}) => + _allocateUniqueName( + Sanitizer.sanitizeSV(baseName), reserved: reserved, ); - } - - if (reserved) { - if (!_isInstanceAvailable(sanitizedBaseName, reserved: true)) { - throw UnavailableReservedNameException(sanitizedBaseName); - } - - return _instanceUniquifier.getUniqueName( - initialName: sanitizedBaseName, - reserved: true, - ); - } - - var candidate = sanitizedBaseName; - var suffix = 0; - while (!_isInstanceAvailable(candidate)) { - candidate = '${sanitizedBaseName}_$suffix'; - suffix++; - } - - return _instanceUniquifier.getUniqueName(initialName: candidate); - } // ─── Signal naming (Logic → String) ───────────────────────────── @@ -222,7 +170,7 @@ class Namer { base = Sanitizer.sanitizeSV(logic.structureName); } - final name = _allocateUniqueSignalName( + final name = _allocateUniqueName( base, reserved: isReservedInternal, ); @@ -309,7 +257,7 @@ class Namer { } for (final logic in preferredMergeable) { - if (_isSignalAvailable(baseName(logic))) { + if (_isAvailable(baseName(logic))) { return _nameAndCacheAll(logic, candidates); } } @@ -320,7 +268,7 @@ class Namer { if (unpreferredMergeable.isNotEmpty) { final best = unpreferredMergeable - .firstWhereOrNull((e) => _isSignalAvailable(baseName(e))) ?? + .firstWhereOrNull((e) => _isAvailable(baseName(e))) ?? unpreferredMergeable.first; return _nameAndCacheAll(best, candidates); } diff --git a/lib/src/utilities/signal_namer.dart b/lib/src/utilities/signal_namer.dart deleted file mode 100644 index 1f217489c..000000000 --- a/lib/src/utilities/signal_namer.dart +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (C) 2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// signal_namer.dart -// Collision-free signal naming within a module scope. -// -// 2026 April 10 -// Author: Desmond Kirkpatrick - -import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; - -/// Assigns collision-free names to [Logic] signals within a single module. -/// -/// Wraps a [Uniquifier] with a sparse Logic→String cache so that each -/// signal is named exactly once and every subsequent lookup is O(1). -/// -/// Port names are reserved at construction time. Internal signals are -/// named lazily on the first [nameOf] call. -@internal -class SignalNamer { - /// Controls whether synthesized signal names and instance names must be - /// unique across both namespaces. - /// - /// When `true` (the default), central naming cross-checks both namespaces - /// during allocation so that no identifier appears as both a signal and an - /// instance name. This is necessary for simulators like Icarus Verilog - /// that reject duplicate identifiers even across namespace boundaries. - /// - /// When `false`, signal and instance names are uniquified independently, - /// matching strict SystemVerilog semantics where instance and signal - /// identifiers occupy separate namespaces. - static bool uniquifySignalAndInstanceNames = true; - - final Uniquifier _uniquifier; - final bool Function(String name) _isAvailableInOtherNamespace; - - /// Sparse cache: only entries where the canonical name has been resolved. - /// Ports whose sanitized name == logic.name may be absent (fast-path - /// through [_portLogics] check). - final Map _names = {}; - - /// The set of port [Logic] objects, for O(1) port membership tests. - final Set _portLogics; - - SignalNamer._({ - required Uniquifier uniquifier, - required Map portRenames, - required Set portLogics, - required bool Function(String name) isAvailableInOtherNamespace, - }) : _uniquifier = uniquifier, - _portLogics = portLogics, - _isAvailableInOtherNamespace = isAvailableInOtherNamespace { - _names.addAll(portRenames); - } - - /// Creates a [SignalNamer] for the given module ports. - /// - /// Sanitized port names are reserved in the namespace. Ports whose - /// sanitized name differs from [Logic.name] are cached immediately. - factory SignalNamer.forModule({ - required Map inputs, - required Map outputs, - required Map inOuts, - bool Function(String name)? isAvailableInOtherNamespace, - }) { - final portRenames = {}; - final portLogics = {}; - final portNames = []; - - void collectPort(String rawName, Logic logic) { - final sanitized = Sanitizer.sanitizeSV(rawName); - portNames.add(sanitized); - portLogics.add(logic); - if (sanitized != logic.name) { - portRenames[logic] = sanitized; - } - } - - for (final entry in inputs.entries) { - collectPort(entry.key, entry.value); - } - for (final entry in outputs.entries) { - collectPort(entry.key, entry.value); - } - for (final entry in inOuts.entries) { - collectPort(entry.key, entry.value); - } - - // Claim each port name as reserved so that: - // (a) non-reserved signals can't steal them, and - // (b) a second reserved signal with the same name throws. - final uniquifier = Uniquifier(); - for (final name in portNames) { - uniquifier.getUniqueName(initialName: name, reserved: true); - } - - return SignalNamer._( - uniquifier: uniquifier, - portRenames: portRenames, - portLogics: portLogics, - isAvailableInOtherNamespace: isAvailableInOtherNamespace ?? (_) => true, - ); - } - - bool _isAvailable(String name, {bool reserved = false}) => - _uniquifier.isAvailable(name, reserved: reserved) && - (!uniquifySignalAndInstanceNames || _isAvailableInOtherNamespace(name)); - - String _allocateUniqueName(String baseName, {bool reserved = false}) { - if (reserved) { - if (!_isAvailable(baseName, reserved: true)) { - throw UnavailableReservedNameException(baseName); - } - - _uniquifier.getUniqueName(initialName: baseName, reserved: true); - return baseName; - } - - var candidate = baseName; - var suffix = 0; - while (!_isAvailable(candidate)) { - candidate = '${baseName}_$suffix'; - suffix++; - } - - _uniquifier.getUniqueName(initialName: candidate); - return candidate; - } - - /// Returns the canonical name for [logic]. - /// - /// The first call for a given [logic] allocates a collision-free name - /// via the underlying [Uniquifier]. Subsequent calls return the cached - /// result in O(1). - String nameOf(Logic logic) { - // Fast path: already named (port rename or previously-queried signal). - final cached = _names[logic]; - if (cached != null) { - return cached; - } - - // Port whose sanitized name == logic.name — already reserved. - if (_portLogics.contains(logic)) { - return logic.name; - } - - // First time seeing this internal signal — derive base name. - String baseName; - // Only treat as reserved for Uniquifier purposes if this is a true - // reserved internal signal (not a submodule port that happens to have - // Naming.reserved). - final isReservedInternal = logic.naming == Naming.reserved && !logic.isPort; - if (logic.naming == Naming.reserved || logic.isArrayMember) { - baseName = logic.name; - } else { - baseName = Sanitizer.sanitizeSV(logic.structureName); - } - - final name = _allocateUniqueName( - baseName, - reserved: isReservedInternal, - ); - _names[logic] = name; - return name; - } - - /// The base name that would be used for [logic] before uniquification. - static String baseName(Logic logic) => - (logic.naming == Naming.reserved || logic.isArrayMember) - ? logic.name - : Sanitizer.sanitizeSV(logic.structureName); - - /// Chooses the best name from a pool of merged [Logic] signals. - /// - /// When [constValue] is provided and [constNameDisallowed] is `false`, - /// the constant's value string is used directly as the name (no - /// uniquification). When [constNameDisallowed] is `true`, the constant - /// is excluded from the candidate pool and the normal priority applies. - /// - /// Priority (after constant handling): - /// 1. Port of this module (always wins — its name is already reserved). - /// 2. Reserved internal signal (exact name, throws on collision). - /// 3. Renameable signal. - /// 4. Preferred-available mergeable (base name not yet taken). - /// 5. Preferred-uniquifiable mergeable. - /// 6. Available-unpreferred mergeable. - /// 7. First unpreferred mergeable. - /// 8. Unnamed (prefer non-unpreferred base name). - /// - /// The winning name is allocated once and cached for the chosen [Logic]. - /// All other non-port [Logic]s in [candidates] are also cached to the - /// same name. - String nameOfBest( - Iterable candidates, { - Const? constValue, - bool constNameDisallowed = false, - }) { - // Constant whose literal value string is the name. - if (constValue != null && !constNameDisallowed) { - return constValue.value.toString(); - } - - // Classify using _portLogics membership (context-aware) rather than - // Logic.naming (context-independent), because submodule ports have - // Naming.reserved but should NOT be treated as reserved here. - Logic? port; - Logic? reserved; - Logic? renameable; - final preferredMergeable = []; - final unpreferredMergeable = []; - final unnamed = []; - - for (final logic in candidates) { - if (_portLogics.contains(logic)) { - port = logic; - } else if (logic.isPort) { - // Submodule port — treat as mergeable regardless of intrinsic naming, - // matching SynthModuleDefinition's namingOverride convention. - if (Naming.isUnpreferred(baseName(logic))) { - unpreferredMergeable.add(logic); - } else { - preferredMergeable.add(logic); - } - } else if (logic.naming == Naming.reserved) { - reserved = logic; - } else if (logic.naming == Naming.renameable) { - renameable = logic; - } else if (logic.naming == Naming.mergeable) { - if (Naming.isUnpreferred(baseName(logic))) { - unpreferredMergeable.add(logic); - } else { - preferredMergeable.add(logic); - } - } else { - unnamed.add(logic); - } - } - - // Port of this module — name already reserved in namespace. - if (port != null) { - return _nameAndCacheAll(port, candidates); - } - - // Reserved internal — must keep exact name (throws on collision). - if (reserved != null) { - return _nameAndCacheAll(reserved, candidates); - } - - // Renameable — preferred base, uniquified if needed. - if (renameable != null) { - return _nameAndCacheAll(renameable, candidates); - } - - // Preferred-available mergeable. - for (final logic in preferredMergeable) { - if (_isAvailable(baseName(logic))) { - return _nameAndCacheAll(logic, candidates); - } - } - - // Preferred-uniquifiable mergeable. - if (preferredMergeable.isNotEmpty) { - return _nameAndCacheAll(preferredMergeable.first, candidates); - } - - // Unpreferred mergeable — prefer available. - if (unpreferredMergeable.isNotEmpty) { - final best = unpreferredMergeable - .firstWhereOrNull((e) => _isAvailable(baseName(e))) ?? - unpreferredMergeable.first; - return _nameAndCacheAll(best, candidates); - } - - // Unnamed — prefer non-unpreferred base name. - if (unnamed.isNotEmpty) { - final best = - unnamed.firstWhereOrNull((e) => !Naming.isUnpreferred(baseName(e))) ?? - unnamed.first; - return _nameAndCacheAll(best, candidates); - } - - throw StateError('No Logic candidates to name.'); - } - - /// Names [chosen] via [nameOf], then caches the same name for all other - /// non-port [Logic]s in [all]. - String _nameAndCacheAll(Logic chosen, Iterable all) { - final name = nameOf(chosen); - for (final logic in all) { - if (!identical(logic, chosen) && !_portLogics.contains(logic)) { - _names[logic] = name; - } - } - return name; - } - - /// Allocates a collision-free name for a non-signal artifact (wire, - /// instance, etc.). - /// - /// When [reserved] is `true`, the exact [baseName] (after sanitization) - /// is claimed without modification; an exception is thrown if it collides. - String allocate(String baseName, {bool reserved = false}) => - _allocateUniqueName( - Sanitizer.sanitizeSV(baseName), - reserved: reserved, - ); - - /// Returns `true` if [name] has not yet been claimed in this namespace. - bool isAvailable(String name) => _isAvailable(name); -} diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart index c369f83e4..65747204a 100644 --- a/test/instance_signal_name_collision_test.dart +++ b/test/instance_signal_name_collision_test.dart @@ -2,23 +2,14 @@ // SPDX-License-Identifier: BSD-3-Clause // // instance_signal_name_collision_test.dart -// Regression test that demonstrates the bug present in the main branch where -// submodule instance names and signal names share a single Uniquifier. -// -// In SystemVerilog, signal identifiers and instance identifiers live in -// *separate* namespaces, so it is perfectly legal to have a signal called -// "inner" and a module instance also called "inner" in the same scope. -// -// When a single shared Uniquifier is used (main-branch behaviour), the second -// name to be allocated gets spuriously suffixed (e.g. "inner_0"), which -// produces incorrect generated SV. +// Tests that submodule instance names and signal names share a single +// namespace, so a collision between them results in uniquification. // // 2026 April 18 // Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/namer.dart'; import 'package:test/test.dart'; // ── Minimal repro modules ──────────────────────────────────────────────────── @@ -35,8 +26,8 @@ class _Inner extends Module { /// • instantiates [_Inner] (default instance name: "inner") /// • names an internal wire "inner" as well /// -/// In SV the two identifiers live in different namespaces, so both should -/// be emitted as "inner" without any suffix. +/// Because both identifiers live in a single shared namespace, one of them +/// will be suffixed to avoid collision. class _CollidingParent extends Module { _CollidingParent(Logic a) : super(name: 'colliding_parent') { a = addInput('a', a, width: a.width); @@ -55,36 +46,30 @@ class _CollidingParent extends Module { // ── Test ───────────────────────────────────────────────────────────────────── void main() { - group('instance / signal name collision (main-branch bug)', () { + group('instance / signal name collision (shared namespace)', () { late _CollidingParent mod; late SynthModuleDefinition def; - late bool previousSetting; setUpAll(() async { - previousSetting = Namer.uniquifySignalAndInstanceNames; - Namer.uniquifySignalAndInstanceNames = false; - mod = _CollidingParent(Logic(width: 8)); await mod.build(); def = SynthModuleDefinition(mod); }); - tearDownAll(() { - Namer.uniquifySignalAndInstanceNames = previousSetting; - }); - test('internal signal named "inner" retains its exact name', () { - // Find the SynthLogic for the reserved "inner" wire. + // The reserved signal should keep its exact name. final sl = def.internalSignals.cast().firstWhere( (s) => s!.logics.any((l) => l.name == 'inner'), orElse: () => null, ); expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); expect(sl!.name, 'inner', - reason: 'Signal "inner" must not be suffixed to "inner_0"'); + reason: 'Reserved signal "inner" must keep its exact name'); }); - test('submodule instance named "inner" retains its exact name', () { + test( + 'submodule instance is uniquified because signal ' + '"inner" already claimed the name', () { final inst = def.subModuleInstantiations .where((s) => s.needsInstantiation) .cast() @@ -93,26 +78,10 @@ void main() { orElse: () => null, ); expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); - expect(inst!.name, 'inner', - reason: 'Instance "inner" must not be suffixed to "inner_0"'); - }); - - test('signal and instance may share the name "inner" without collision', - () { - // Both should be "inner", not one of them "inner_0". - final sl = def.internalSignals.cast().firstWhere( - (s) => s!.logics.any((l) => l.name == 'inner'), - orElse: () => null, - ); - final inst = def.subModuleInstantiations - .where((s) => s.needsInstantiation) - .cast() - .firstWhere( - (s) => s!.module.name == 'inner', - orElse: () => null, - ); - expect(sl?.name, 'inner'); - expect(inst?.name, 'inner'); + // The instance should be suffixed since the signal took "inner" first. + expect(inst!.name, isNot('inner'), + reason: 'Instance should be uniquified when signal already ' + 'claims "inner"'); }); }); } diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index c79221baa..8c9397082 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -218,12 +218,10 @@ void main() { } }); - test('submodule instance names are allocated from the instance namespace', + test('submodule instance names are allocated from the shared namespace', () async { - // Instance names come from Module.namer.allocateInstanceName, which is - // separate from the signal namespace (Module.namer.allocateSignalName). - // A signal and a submodule instance may therefore share the same - // identifier without collision — matching SystemVerilog semantics. + // Instance names come from Module.namer.allocateInstanceName, which + // shares the same namespace as signal names. final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); @@ -238,11 +236,10 @@ void main() { expect(instNames, isNotEmpty, reason: 'Should have at least one submodule instance'); - // Instance names are claimed in the *instance* namespace, NOT the - // signal namespace. + // Instance names are claimed in the shared namespace. for (final name in instNames) { expect(mod.namer.isInstanceNameAvailable(name), isFalse, - reason: 'Instance name "$name" should be claimed in instance ' + reason: 'Instance name "$name" should be claimed in the ' 'namespace'); } }); diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart index 32e55629d..a5263a998 100644 --- a/test/naming_namespace_test.dart +++ b/test/naming_namespace_test.dart @@ -2,14 +2,13 @@ // SPDX-License-Identifier: BSD-3-Clause // // naming_namespace_test.dart -// Tests for constant naming via nameOfBest, the tryMerge guard for -// constNameDisallowed, and separate instance/signal namespaces. +// Tests for constant naming via nameOfBest and shared instance/signal +// namespace uniquification. // // 2026 April // Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/namer.dart'; import 'package:test/test.dart'; /// A simple submodule whose instance name can collide with a signal name. @@ -77,8 +76,6 @@ class _ConstNameDisallowedModule extends Module { void main() { tearDown(() async { await Simulator.reset(); - // Restore default. - Namer.uniquifySignalAndInstanceNames = true; }); group('constant naming via nameOfBest', () { @@ -106,48 +103,19 @@ void main() { }); }); - group('separate instance and signal namespaces', () { + group('shared instance and signal namespace', () { test( - 'signal and instance with same name do not collide ' - 'when namespaces are independent', () async { - Namer.uniquifySignalAndInstanceNames = false; + 'signal and instance with same name get uniquified ' + 'in the shared namespace', () async { final dut = _InstanceSignalCollision(); await dut.build(); final sv = dut.generateSynth(); - // With independent namespaces, the signal keeps its name 'inner' - // and the instance also keeps 'inner' — no spurious _0 suffix. - expect(sv, contains(RegExp(r'logic\s+inner[,;\s]'))); - expect(sv, isNot(contains('inner_0'))); - }); - - test( - 'signal and instance get suffixed when ' - 'ensureUniqueSignalAndInstanceNames is true', () async { - Namer.uniquifySignalAndInstanceNames = true; - final dut = _InstanceSignalCollision(); - await dut.build(); - final sv = dut.generateSynth(); - - // With cross-namespace checking enabled, the signal 'inner' is - // allocated first (during signal naming); when the instance tries - // to claim 'inner', it sees the signal namespace has it, so the - // instance OR signal gets a suffix. + // With a single shared namespace, one of the two "inner" identifiers + // must be suffixed to avoid collision. expect(sv, contains('inner_0')); }); - test( - 'signal and instance do not spuriously suffix when ' - 'ensureUniqueSignalAndInstanceNames is false', () async { - Namer.uniquifySignalAndInstanceNames = false; - final dut = _InstanceSignalCollision(); - await dut.build(); - final sv = dut.generateSynth(); - - // With independent namespaces, no spurious suffixing. - expect(sv, isNot(contains('inner_0'))); - }); - test('duplicate instance names get uniquified', () async { final dut = _DuplicateInstances(); await dut.build(); @@ -158,23 +126,4 @@ void main() { expect(sv, contains(RegExp(r'blk_\d'))); }); }); - - group('instance namespace independence', () { - test('allocateInstanceName is independent from allocateSignalName', - () async { - final dut = _InstanceSignalCollision(); - await dut.build(); - - // After build, the signal namer has 'inner' claimed. - // With independent namespaces, instance namespace should also accept - // 'inner' without conflict. - Namer.uniquifySignalAndInstanceNames = false; - - // The instance namespace should show 'inner' as available before - // any instance allocation. - // (After synthesis, names are already allocated, so we just verify - // the module built without error.) - expect(dut.generateSynth(), isNotEmpty); - }); - }); } From d5904a6d83601318ee0efee98b7ec7a4b8fa5c93 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 3 May 2026 12:23:27 -0700 Subject: [PATCH 10/58] cleanup of port vs signal name assumptions, constant merging and signal/instance naming routine names --- .../synthesizers/utilities/synth_logic.dart | 18 --- .../utilities/synth_module_definition.dart | 4 +- .../synth_sub_module_instantiation.dart | 4 +- lib/src/utilities/namer.dart | 114 ++++-------------- test/name_test.dart | 4 +- test/naming_consistency_test.dart | 4 +- test/signal_registry_test.dart | 11 +- 7 files changed, 39 insertions(+), 120 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index ad88bd6cc..8fcbc014a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -17,19 +17,6 @@ import 'package:rohd/src/utilities/sanitizer.dart'; /// Represents a logic signal in the generated code within a module. @internal class SynthLogic { - /// Controls whether two constants with the same value driving separate - /// module inputs are merged into a single signal declaration. - /// - /// When `true` (the default), identical constants are collapsed to one - /// declaration — desirable for simulation-oriented output such as - /// SystemVerilog, where a single `assign wire = VALUE;` feeds all - /// downstream consumers. - /// - /// When `false`, each constant input keeps its own declaration. This is - /// useful for netlist/visualization outputs where seeing every individual - /// constant connection is more informative than an optimized fan-out net. - static bool mergeConstantInputs = true; - /// All [Logic]s represented, regardless of type. List get logics => UnmodifiableListView([ if (_reservedLogic != null) _reservedLogic!, @@ -288,12 +275,7 @@ class SynthLogic { } /// Indicates whether two constants can be merged. - /// - /// Merging is only performed when [SynthLogic.mergeConstantInputs] is - /// `true`. Set it to `false` to keep each constant input as its own - /// declaration (e.g. for netlist/visualization output). static bool _constantsMergeable(SynthLogic a, SynthLogic b) => - SynthLogic.mergeConstantInputs && a.isConstant && b.isConstant && a._constLogic!.value == b._constLogic!.value && diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 9b7a6e42c..9ea120646 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -760,8 +760,8 @@ class SynthModuleDefinition { /// /// Signal names are read from `Namer.signalNameOf` (for user-created /// [Logic] objects) or kept as literal constants and are allocated from - /// `Namer.allocateSignalName`. Submodule instance names are allocated - /// from `Namer.allocateInstanceName`. All names share a single + /// `Namer.signalNameOf`. Submodule instance names are allocated + /// from `Namer.allocateRawName`. All names share a single /// namespace managed by the module's `Namer`. void _pickNames() { // Name allocation order matters — earlier claims get the unsuffixed name diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 67f9e2832..cf7da28e8 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -26,11 +26,11 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// /// Names are allocated from [parentModule]'s `Namer`'s shared namespace - /// via `Namer.allocateInstanceName`. + /// via `Namer.allocateName`. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.allocateInstanceName( + _name = parentModule.namer.allocateRawName( module.uniqueInstanceName, reserved: module.reserveName, ); diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 481dc64e3..efbe8e3e4 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -21,16 +21,15 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// /// Port names are reserved at construction time. Internal signal names /// are assigned lazily on the first [signalNameOf] call. Instance names -/// are allocated explicitly via [allocateInstanceName]. +/// are allocated explicitly via [allocateRawName]. @internal class Namer { // ─── Shared namespace ─────────────────────────────────────────── final Uniquifier _uniquifier; - /// Sparse cache: only entries where the canonical name has been resolved. - /// Ports whose sanitized name == logic.name may be absent (fast-path - /// through [_portLogics] check). + /// Cache of resolved names for internal (non-port) signals only. + /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; /// The set of port [Logic] objects, for O(1) port membership tests. @@ -40,108 +39,48 @@ class Namer { Namer._({ required Uniquifier uniquifier, - required Map portRenames, required Set portLogics, }) : _uniquifier = uniquifier, - _portLogics = portLogics { - _signalNames.addAll(portRenames); - } + _portLogics = portLogics; /// Creates a [Namer] for the given module ports. /// - /// Sanitized port names are reserved in the signal namespace. Ports - /// whose sanitized name differs from [Logic.name] are cached immediately. + /// Port names are reserved in the shared namespace. Port names are + /// guaranteed sanitary by [Module]'s `_checkForSafePortName`. factory Namer.forModule({ required Map inputs, required Map outputs, required Map inOuts, }) { - final portRenames = {}; - final portLogics = {}; - final portNames = []; - - void collectPort(String rawName, Logic logic) { - final sanitized = Sanitizer.sanitizeSV(rawName); - portNames.add(sanitized); - portLogics.add(logic); - if (sanitized != logic.name) { - portRenames[logic] = sanitized; - } - } - - for (final entry in inputs.entries) { - collectPort(entry.key, entry.value); - } - for (final entry in outputs.entries) { - collectPort(entry.key, entry.value); - } - for (final entry in inOuts.entries) { - collectPort(entry.key, entry.value); - } + final portLogics = { + ...inputs.values, + ...outputs.values, + ...inOuts.values, + }; final uniquifier = Uniquifier(); - for (final name in portNames) { - uniquifier.getUniqueName(initialName: name, reserved: true); + for (final logic in portLogics) { + uniquifier.getUniqueName(initialName: logic.name, reserved: true); } return Namer._( uniquifier: uniquifier, - portRenames: portRenames, portLogics: portLogics, ); } // ─── Name availability / allocation ───────────────────────────── - bool _isAvailable(String name, {bool reserved = false}) => - _uniquifier.isAvailable(name, reserved: reserved); - - String _allocateUniqueName(String baseName, {bool reserved = false}) { - if (reserved) { - if (!_isAvailable(baseName, reserved: true)) { - throw UnavailableReservedNameException(baseName); - } - - _uniquifier.getUniqueName(initialName: baseName, reserved: true); - return baseName; - } - - var candidate = baseName; - var suffix = 0; - while (!_isAvailable(candidate)) { - candidate = '${baseName}_$suffix'; - suffix++; - } - - _uniquifier.getUniqueName(initialName: candidate); - return candidate; - } - /// Returns `true` if [name] has not yet been claimed in the namespace. - bool isNameAvailable(String name) => _isAvailable(name); + bool isAvailable(String name) => _uniquifier.isAvailable(name); - /// Allocates a collision-free name in the signal namespace. + /// Allocates a collision-free name in the shared namespace. /// /// When [reserved] is `true`, the exact [baseName] (after sanitization) /// is claimed without modification; an exception is thrown if it collides. - String allocateSignalName(String baseName, {bool reserved = false}) => - _allocateUniqueName( - Sanitizer.sanitizeSV(baseName), - reserved: reserved, - ); - - // ─── Instance allocation ──────────────────────────────────────── - - /// Returns `true` if [name] has not yet been claimed in the namespace. - bool isInstanceNameAvailable(String name) => _isAvailable(name); - - /// Allocates a collision-free instance name. - /// - /// When [reserved] is `true`, the exact [baseName] (after sanitization) - /// is claimed without modification; an exception is thrown if it collides. - String allocateInstanceName(String baseName, {bool reserved = false}) => - _allocateUniqueName( - Sanitizer.sanitizeSV(baseName), + String allocateRawName(String baseName, {bool reserved = false}) => + _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(baseName), reserved: reserved, ); @@ -170,8 +109,8 @@ class Namer { base = Sanitizer.sanitizeSV(logic.structureName); } - final name = _allocateUniqueName( - base, + final name = _uniquifier.getUniqueName( + initialName: base, reserved: isReservedInternal, ); _signalNames[logic] = name; @@ -256,19 +195,16 @@ class Namer { return _nameAndCacheAll(renameable, candidates); } - for (final logic in preferredMergeable) { - if (_isAvailable(baseName(logic))) { - return _nameAndCacheAll(logic, candidates); - } - } - if (preferredMergeable.isNotEmpty) { - return _nameAndCacheAll(preferredMergeable.first, candidates); + final best = preferredMergeable + .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? + preferredMergeable.first; + return _nameAndCacheAll(best, candidates); } if (unpreferredMergeable.isNotEmpty) { final best = unpreferredMergeable - .firstWhereOrNull((e) => _isAvailable(baseName(e))) ?? + .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? unpreferredMergeable.first; return _nameAndCacheAll(best, candidates); } diff --git a/test/name_test.dart b/test/name_test.dart index c863c04f5..bde8a9c9f 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -1,7 +1,7 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// definition_name_test.dart +// name_test.dart // Tests for definition names (including reserving them) of Modules. // // 2022 March 7 diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index 8c9397082..f0d7b2d31 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -220,7 +220,7 @@ void main() { test('submodule instance names are allocated from the shared namespace', () async { - // Instance names come from Module.namer.allocateInstanceName, which + // Instance names come from Module.namer.allocateName, which // shares the same namespace as signal names. final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); @@ -238,7 +238,7 @@ void main() { // Instance names are claimed in the shared namespace. for (final name in instNames) { - expect(mod.namer.isInstanceNameAvailable(name), isFalse, + expect(mod.namer.isAvailable(name), isFalse, reason: 'Instance name "$name" should be claimed in the ' 'namespace'); } diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart index 152b6091a..d1719c85e 100644 --- a/test/signal_registry_test.dart +++ b/test/signal_registry_test.dart @@ -2,9 +2,10 @@ // SPDX-License-Identifier: BSD-3-Clause // // signal_registry_test.dart -// Tests for Module canonical naming (SynthesisNameRegistry). +// Tests for Module canonical naming (Namer). // // 2026 April 14 +// Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; @@ -90,12 +91,12 @@ void main() { }); }); - group('allocateSignalName', () { + group('allocateName', () { test('avoids collision with existing names', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); - final allocated = mod.namer.allocateSignalName('en'); + final allocated = mod.namer.allocateRawName('en'); expect(allocated, isNot(equals('en')), reason: 'Should not collide with existing port name'); expect(allocated, contains('en'), @@ -106,8 +107,8 @@ void main() { final mod = _Counter(Logic(), Logic()); await mod.build(); - final a = mod.namer.allocateSignalName('wire'); - final b = mod.namer.allocateSignalName('wire'); + final a = mod.namer.allocateRawName('wire'); + final b = mod.namer.allocateRawName('wire'); expect(a, isNot(equals(b)), reason: 'Each allocation should be unique'); }); }); From 6dfe0f92b2cf89e5fe9c5c350d1277bac1e147aa Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 12 May 2026 06:56:45 -0700 Subject: [PATCH 11/58] simplified forModule, improved code doc --- lib/src/exceptions/logic/put_exception.dart | 7 ++++--- lib/src/module.dart | 6 +----- .../utilities/synth_module_definition.dart | 9 +++++---- .../synth_sub_module_instantiation.dart | 7 ++++--- lib/src/utilities/namer.dart | 18 +++++++----------- test/name_test.dart | 6 ++---- test/signal_registry_test.dart | 6 +++--- 7 files changed, 26 insertions(+), 33 deletions(-) diff --git a/lib/src/exceptions/logic/put_exception.dart b/lib/src/exceptions/logic/put_exception.dart index 36d8f8015..96bd51602 100644 --- a/lib/src/exceptions/logic/put_exception.dart +++ b/lib/src/exceptions/logic/put_exception.dart @@ -9,10 +9,11 @@ import 'package:rohd/rohd.dart'; -/// An exception that thrown when a [Logic] signal fails to `put`. +/// An exception that thrown when a [Logic] signal fails to [Logic.put]. class PutException extends RohdException { - /// Creates an exception for when a `put` fails on a `Logic` with [context] as - /// to where the + /// Creates an exception for when a [Logic.put] fails on a [Logic] with + /// [context] as to where the failure occurred and [message] describing the + /// failure. PutException(String context, String message) : super('Failed to put value on signal ($context): $message'); } diff --git a/lib/src/module.dart b/lib/src/module.dart index 02e02ad63..8c3c79692 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -61,11 +61,7 @@ abstract class Module { Namer _createNamer() { assert(hasBuilt, 'Module must be built before canonical names are bound.'); - return Namer.forModule( - inputs: _inputs, - outputs: _outputs, - inOuts: _inOuts, - ); + return Namer.forModule(this); } /// An internal mapping of inOut names to their sources to this [Module]. diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 9ea120646..81c74b696 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -14,6 +14,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/namer.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. @@ -758,11 +759,11 @@ class SynthModuleDefinition { /// Picks names of signals and sub-modules. /// - /// Signal names are read from `Namer.signalNameOf` (for user-created + /// Signal names are read from [Namer.signalNameOf] for user-created /// [Logic] objects) or kept as literal constants and are allocated from - /// `Namer.signalNameOf`. Submodule instance names are allocated - /// from `Namer.allocateRawName`. All names share a single - /// namespace managed by the module's `Namer`. + /// [Namer.signalNameOf]. Submodule instance names are allocated + /// from [Namer.allocateName]. All names share a single + /// namespace managed by the module's [Namer]. void _pickNames() { // Name allocation order matters — earlier claims get the unsuffixed name // when there are collisions. This matches production ROHD priority: diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index cf7da28e8..343ca1714 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -11,6 +11,7 @@ import 'dart:collection'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/namer.dart'; /// Represents an instantiation of a module within another module. class SynthSubModuleInstantiation { @@ -25,12 +26,12 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// - /// Names are allocated from [parentModule]'s `Namer`'s shared namespace - /// via `Namer.allocateName`. + /// Names are allocated from [parentModule]'s [Namer]'s shared namespace + /// via [Namer.allocateName]. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.allocateRawName( + _name = parentModule.namer.allocateName( module.uniqueInstanceName, reserved: module.reserveName, ); diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index efbe8e3e4..d4c6eff85 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -21,7 +21,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// /// Port names are reserved at construction time. Internal signal names /// are assigned lazily on the first [signalNameOf] call. Instance names -/// are allocated explicitly via [allocateRawName]. +/// are allocated explicitly via [allocateName]. @internal class Namer { // ─── Shared namespace ─────────────────────────────────────────── @@ -43,19 +43,15 @@ class Namer { }) : _uniquifier = uniquifier, _portLogics = portLogics; - /// Creates a [Namer] for the given module ports. + /// Creates a [Namer] for the given [module]'s ports. /// /// Port names are reserved in the shared namespace. Port names are /// guaranteed sanitary by [Module]'s `_checkForSafePortName`. - factory Namer.forModule({ - required Map inputs, - required Map outputs, - required Map inOuts, - }) { + factory Namer.forModule(Module module) { final portLogics = { - ...inputs.values, - ...outputs.values, - ...inOuts.values, + ...module.inputs.values, + ...module.outputs.values, + ...module.inOuts.values, }; final uniquifier = Uniquifier(); @@ -78,7 +74,7 @@ class Namer { /// /// When [reserved] is `true`, the exact [baseName] (after sanitization) /// is claimed without modification; an exception is thrown if it collides. - String allocateRawName(String baseName, {bool reserved = false}) => + String allocateName(String baseName, {bool reserved = false}) => _uniquifier.getUniqueName( initialName: Sanitizer.sanitizeSV(baseName), reserved: reserved, diff --git a/test/name_test.dart b/test/name_test.dart index bde8a9c9f..afa757cc8 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -137,10 +137,8 @@ void main() { // skip ones that actually *should* cause a failure // - // Note: SystemVerilog allows using the same identifier for a signal - // and an instance because they are different namespaces. However, - // Icarus Verilog rejects that pattern, so ROHD treats those as - // conflicts for simulator compatibility. + // Note: SystemVerilog does not allow using the same identifier for a + // signal and an instance. final shouldConflict = [ { NameType.internalModuleDefinition, diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart index d1719c85e..5fd19c2e3 100644 --- a/test/signal_registry_test.dart +++ b/test/signal_registry_test.dart @@ -96,7 +96,7 @@ void main() { final mod = _Counter(Logic(), Logic()); await mod.build(); - final allocated = mod.namer.allocateRawName('en'); + final allocated = mod.namer.allocateName('en'); expect(allocated, isNot(equals('en')), reason: 'Should not collide with existing port name'); expect(allocated, contains('en'), @@ -107,8 +107,8 @@ void main() { final mod = _Counter(Logic(), Logic()); await mod.build(); - final a = mod.namer.allocateRawName('wire'); - final b = mod.namer.allocateRawName('wire'); + final a = mod.namer.allocateName('wire'); + final b = mod.namer.allocateName('wire'); expect(a, isNot(equals(b)), reason: 'Each allocation should be unique'); }); }); From 3c90e5dc6096d2cb1b7c64be963b6828dfd06c09 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 12 May 2026 08:21:35 -0700 Subject: [PATCH 12/58] more coverage for Namer --- test/signal_registry_test.dart | 172 +++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart index 5fd19c2e3..ffae4ae5f 100644 --- a/test/signal_registry_test.dart +++ b/test/signal_registry_test.dart @@ -8,6 +8,7 @@ // Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:test/test.dart'; // ──────────────────────────────────────────────────────────────── @@ -140,4 +141,175 @@ void main() { expect(names1, equals(names2)); }); }); + + group('isAvailable', () { + test('port names are not available', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.isAvailable('a'), isFalse); + expect(mod.namer.isAvailable('b'), isFalse); + expect(mod.namer.isAvailable('a_bar'), isFalse); + expect(mod.namer.isAvailable('a_and_b'), isFalse); + }); + + test('unallocated names are available', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.isAvailable('xyz'), isTrue); + expect(mod.namer.isAvailable('new_signal'), isTrue); + }); + + test('allocated names become unavailable', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final name = mod.namer.allocateName('wire'); + expect(mod.namer.isAvailable(name), isFalse); + }); + }); + + group('allocateName reserved', () { + test('reserved allocation claims exact name', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final name = mod.namer.allocateName('my_wire', reserved: true); + expect(name, equals('my_wire')); + expect(mod.namer.isAvailable('my_wire'), isFalse); + }); + + test('reserved collision throws', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + // 'a' is already a port name + expect( + () => mod.namer.allocateName('a', reserved: true), + throwsException, + ); + }); + }); + + group('baseName', () { + test('reserved signal uses name directly', () { + final sig = Logic(name: 'myReserved', naming: Naming.reserved); + expect(Namer.baseName(sig), equals('myReserved')); + }); + + test('renameable signal uses sanitized structureName', () { + final sig = Logic(name: 'mySignal', naming: Naming.renameable); + // structureName for a top-level signal equals its name + expect(Namer.baseName(sig), contains('mySignal')); + }); + + test('unpreferred name detected', () { + expect(Naming.isUnpreferred('_hidden'), isTrue); + expect(Naming.isUnpreferred('visible'), isFalse); + }); + }); + + group('signalNameOfBest', () { + test('const value returns value string', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final c = Const(LogicValue.ofString('01')); + final sig = Logic(name: 'x'); + final name = mod.namer.signalNameOfBest( + [sig], + constValue: c, + ); + expect(name, equals(c.value.toString())); + }); + + test('constNameDisallowed falls through to candidates', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final c = Const(LogicValue.ofString('01')); + final sig = Logic(name: 'fallback', naming: Naming.renameable); + final name = mod.namer.signalNameOfBest( + [sig], + constValue: c, + constNameDisallowed: true, + ); + expect(name, isNot(equals(c.value.toString()))); + expect(name, contains('fallback')); + }); + + test('port wins over other candidates', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final port = mod.input('a'); // this module's port + final reserved = Logic(name: 'res', naming: Naming.reserved); + final name = mod.namer.signalNameOfBest([reserved, port]); + expect(name, equals('a')); + }); + + test('reserved wins over mergeable', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final reserved = Logic(name: 'special', naming: Naming.reserved); + final mergeable = Logic(name: 'other', naming: Naming.mergeable); + final name = mod.namer.signalNameOfBest([mergeable, reserved]); + expect(name, equals('special')); + }); + + test('renameable wins over mergeable', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final renameable = Logic(name: 'ren', naming: Naming.renameable); + final mergeable = Logic(name: 'mrg', naming: Naming.mergeable); + final name = mod.namer.signalNameOfBest([mergeable, renameable]); + expect(name, contains('ren')); + }); + + test('preferred mergeable wins over unpreferred', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final preferred = Logic(name: 'good', naming: Naming.mergeable); + final unpreferred = + Logic(name: Naming.unpreferredName('bad'), naming: Naming.mergeable); + final name = mod.namer.signalNameOfBest([unpreferred, preferred]); + expect(name, contains('good')); + }); + + test('caches name for all candidates', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final s1 = Logic(name: 'winner', naming: Naming.renameable); + final s2 = Logic(name: 'loser', naming: Naming.mergeable); + final name = mod.namer.signalNameOfBest([s1, s2]); + + // Both should resolve to the same cached name + expect(mod.namer.signalNameOf(s1), equals(name)); + expect(mod.namer.signalNameOf(s2), equals(name)); + }); + + test('empty candidates throws', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect( + () => mod.namer.signalNameOfBest([]), + throwsA(isA()), + ); + }); + + test('unnamed signals get a name', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final unnamed = Logic(naming: Naming.unnamed); + final name = mod.namer.signalNameOfBest([unnamed]); + expect(name, isNotEmpty); + }); + }); } From 3e103f938a99b24a35be5091fc73201ba2944976 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 6 May 2026 12:32:17 -0700 Subject: [PATCH 13/58] Add ModuleServices singleton and SvService Introduces a singleton service registry (ModuleServices) that provides a unified query surface for DevTools and inspection tools. Module.build() now registers the root module with ModuleServices.instance. Also adds SvService which wraps SystemVerilog synthesis and registers with ModuleServices for DevTools access to SV metadata. This is a clean separation: no netlist code is included. The netlist branch will later extend ModuleServices with a netlistService field. --- lib/rohd.dart | 1 + lib/src/diagnostics/module_services.dart | 78 ++++++++++ lib/src/module.dart | 3 +- .../systemverilog/sv_service.dart | 114 +++++++++++++++ .../systemverilog/systemverilog.dart | 1 + test/module_services_test.dart | 134 ++++++++++++++++++ 6 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 lib/src/diagnostics/module_services.dart create mode 100644 lib/src/synthesizers/systemverilog/sv_service.dart create mode 100644 test/module_services_test.dart diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..d0ea2a266 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,7 @@ // Copyright (C) 2021-2023 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/module_services.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..a0e583b59 --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,78 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services.dart +// Singleton service registry for DevTools integration. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; + +/// Singleton service registry that provides a unified query surface for +/// DevTools and other inspection tools. +/// +/// Services register themselves here on construction; DevTools evaluates +/// getters on [instance] via `EvalOnDartLibrary` to pull data. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJSON] — set by [Module.build]. +/// +/// **Opt-in (registered by service constructors):** +/// - [svService] — SystemVerilog synthesis results. +/// +/// Additional services (netlist, trace, waveform) can be added by setting +/// the corresponding field after construction. +class ModuleServices { + ModuleServices._(); + + /// The singleton instance. + static final ModuleServices instance = ModuleServices._(); + + // ─── Hierarchy (auto-registered by Module.build) ────────────── + + /// The most recently built top-level [Module]. + /// + /// Set automatically at the end of [Module.build]. + Module? rootModule; + + /// Returns the module hierarchy as a JSON string. + /// + /// DevTools evaluates this via `EvalOnDartLibrary` to display + /// the module hierarchy. + String get hierarchyJSON { + ModuleTree.rootModuleInstance = rootModule; + return ModuleTree.instance.hierarchyJSON; + } + + /// Returns the primary inspector JSON for DevTools. + /// + /// Returns the hierarchy JSON. Downstream branches (e.g. netlist) may + /// override this to return richer data when available. + String get inspectorJSON => hierarchyJSON; + + // ─── SystemVerilog service (opt-in) ─────────────────────────── + + /// The active [SvService], if one has been registered. + SvService? svService; + + /// Returns SV synthesis metadata as JSON, or an unavailable status. + String get svJSON => + svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); + + // ─── Helpers ────────────────────────────────────────────────── + + static String _unavailable(String service) => jsonEncode({ + 'status': 'unavailable', + 'reason': '$service service not registered', + }); + + /// Resets all services. Intended for test teardown. + void reset() { + rootModule = null; + svService = null; + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 02e02ad63..9f6ec634e 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,7 +13,6 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; -import 'package:rohd/src/diagnostics/inspector_service.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; @@ -333,7 +332,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart new file mode 100644 index 000000000..e1adf43cd --- /dev/null +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -0,0 +1,114 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sv_service.dart +// Service wrapper for SystemVerilog synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; + +/// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. +/// +/// Provides access to the generated SV file contents and per-module +/// synthesis results, and optionally registers with [ModuleServices] +/// for DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final sv = SvService(dut); +/// +/// // Write individual .sv files: +/// sv.writeFiles('build/'); +/// +/// // Or get the concatenated output (like generateSynth): +/// print(sv.allContents); +/// ``` +class SvService { + /// The top-level [Module] being synthesized. + final Module module; + + /// The underlying [SynthBuilder] that drove synthesis. + late final SynthBuilder synthBuilder; + + /// The generated file contents (one per unique module definition). + late final List fileContents; + + /// Creates an [SvService] for [module]. + /// + /// [module] must already be built. Set [register] to `true` (the + /// default) to register this service with [ModuleServices] for + /// DevTools access. + SvService(this.module, {bool register = true}) { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating SvService. ' + 'Call build() first.'); + } + + synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); + fileContents = synthBuilder.getSynthFileContents(); + + if (register) { + ModuleServices.instance.svService = this; + } + } + + /// All [SynthesisResult]s produced by synthesis. + Set get synthesisResults => synthBuilder.synthesisResults; + + /// Returns the concatenated SystemVerilog output as a single string, + /// matching the format of [Module.generateSynth]. + String get allContents => fileContents.map((fc) => fc.contents).join('\n\n'); + + /// Returns a map from module definition name to its SV file contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + + /// Returns a map from module definition name + /// ([Module.definitionName]) to its SV file contents. + /// + /// This uses the original definition name (not uniquified), matching + /// the keys used by FLC trace data. + Map get contentsByDefinitionName { + final result = {}; + for (final sr in synthesisResults) { + final defName = sr.module.definitionName; + final instanceName = sr.instanceTypeName; + // Find the file content matching this instance type name. + final fc = fileContents.firstWhereOrNull((f) => f.name == instanceName); + if (fc != null) { + result[defName] = fc.contents; + } + } + return result; + } + + /// Writes each module's SV to a separate file in [directory]. + /// + /// Files are named `.sv`. + void writeFiles(String directory) { + final dir = Directory(directory)..createSync(recursive: true); + for (final fc in fileContents) { + File('${dir.path}/${fc.name}.sv').writeAsStringSync(fc.contents); + } + } + + /// Returns a JSON-serialisable summary of the SV synthesis. + /// + /// Contains the list of generated module definition names. + Map toJson() => { + 'modules': [ + for (final fc in fileContents) fc.name, + ], + }; +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog.dart b/lib/src/synthesizers/systemverilog/systemverilog.dart index 281b05df9..e5f772e44 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,5 +1,6 @@ // Copyright (C) 2021-2024 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'sv_service.dart'; export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; diff --git a/test/module_services_test.dart b/test/module_services_test.dart new file mode 100644 index 000000000..f9b0ac075 --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,134 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Unit tests for ModuleServices and SvService. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class SimpleModule extends Module { + SimpleModule(Logic a) : super(name: 'simple') { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +void main() { + tearDown(() { + ModuleServices.instance.reset(); + }); + + group('ModuleServices', () { + test('rootModule is set after build', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, equals(mod)); + }); + + test('hierarchyJSON returns valid JSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final json = ModuleServices.instance.hierarchyJSON; + expect(() => jsonDecode(json), returnsNormally); + }); + + test('inspectorJSON matches hierarchyJSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.inspectorJSON, + equals(ModuleServices.instance.hierarchyJSON)); + }); + + test('svJSON returns unavailable when no service registered', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final result = + jsonDecode(ModuleServices.instance.svJSON) as Map; + expect(result['status'], equals('unavailable')); + }); + + test('reset clears all services', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, isNotNull); + ModuleServices.instance.reset(); + expect(ModuleServices.instance.rootModule, isNull); + expect(ModuleServices.instance.svService, isNull); + }); + }); + + group('SvService', () { + test('registers with ModuleServices on creation', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(ModuleServices.instance.svService, equals(sv)); + }); + + test('allContents is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.allContents, isNotEmpty); + }); + + test('contentsByName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByName, isNotEmpty); + }); + + test('contentsByDefinitionName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByDefinitionName, isNotEmpty); + expect(sv.contentsByDefinitionName.containsKey('SimpleModule'), isTrue); + }); + + test('svJSON returns valid JSON after registration', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + SvService(mod); + final result = + jsonDecode(ModuleServices.instance.svJSON) as Map; + expect(result['modules'], isList); + }); + + test('writeFiles creates SV files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + sv.writeFiles(dir.path); + final files = dir.listSync().whereType().toList(); + expect(files, isNotEmpty); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('register false does not register', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.reset(); + SvService(mod, register: false); + expect(ModuleServices.instance.svService, isNull); + }); + + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + expect(() => SvService(mod), throwsException); + }); + }); +} From 674f518f161eb9871a4bab2edf481dd439208986 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 12 May 2026 12:12:28 -0700 Subject: [PATCH 14/58] fix: use tearoff for tearDown (unnecessary_lambdas) --- test/module_services_test.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/module_services_test.dart b/test/module_services_test.dart index f9b0ac075..f8994577e 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -21,9 +21,7 @@ class SimpleModule extends Module { } void main() { - tearDown(() { - ModuleServices.instance.reset(); - }); + tearDown(ModuleServices.instance.reset); group('ModuleServices', () { test('rootModule is set after build', () async { From 99474cac705ea2a9d8ff8427757c9d3b29eb173f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 21 May 2026 16:12:07 -0700 Subject: [PATCH 15/58] dart v3.12 analysis --- doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart index 5765f08bf..18efb5a2d 100644 --- a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart +++ b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart @@ -7,7 +7,6 @@ import '../../chapter_3/answers/helper.dart'; import '../../chapter_5/answers/full_subtractor.dart'; class FullSubtractorComb extends FullSubtractor { - @override FullSubtractorComb(super.a, super.b, super.borrowIn) { // Declare input and output final a = input('a'); From 42fba62253f91fc7b9ea18dabdf15d3a58455e75 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 12 Jun 2026 14:28:31 -0700 Subject: [PATCH 16/58] canonical names at last, even in the comments --- lib/src/module.dart | 12 ++++++ .../utilities/synth_module_definition.dart | 18 ++++++++- .../synth_sub_module_instantiation.dart | 9 ++--- lib/src/utilities/namer.dart | 40 +++++++++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index 8c3c79692..ffeff9fc8 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -214,6 +214,18 @@ abstract class Module { this, 'Module must be built to access uniquified name.'); String _uniqueInstanceName; + /// A stable identity used to memoize this module's canonical instance name + /// across repeated synthesis passes (e.g. netlist then SystemVerilog). + /// + /// Defaults to the [Module] itself, which is correct for modules that are + /// part of the built hierarchy and therefore persist across passes. + /// Synthesis-time throwaway modules that are *recreated* on every pass (and + /// thus have a fresh [Module] identity each time) must override this to + /// return a stable identity — typically the [Logic] they drive — so their + /// instance name does not drift run-to-run. + @internal + Object get instanceNameKey => this; + /// If true, guarantees [uniqueInstanceName] matches [name] or else the /// [build] will fail. final bool reserveName; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 81c74b696..001f32eef 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -19,17 +19,30 @@ import 'package:rohd/src/utilities/namer.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. class _BusSubsetForStructSlice extends BusSubset { + /// The stable destination [Logic] this slice drives. + /// + /// Used as the [instanceNameKey] so that, although a fresh + /// [_BusSubsetForStructSlice] is created on every synthesis pass, its + /// canonical instance name is memoized against the persistent destination + /// signal and therefore does not drift run-to-run. + final Logic _destination; + /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. _BusSubsetForStructSlice( super.bus, super.startIndex, - super.endIndex, - ) : super(name: 'struct_slice'); + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super(name: 'struct_slice'); // we override this since it's added post-build @override bool get hasBuilt => true; + + @override + Object get instanceNameKey => _destination; } /// Represents the definition of a module. @@ -265,6 +278,7 @@ class SynthModuleDefinition { width: port.width, name: 'DUMMY'), idx, idx + leafElement.width - 1, + destination: leafElement, ); final ssmi = getSynthSubModuleInstantiation(subsetMod); diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 343ca1714..ee35f1bac 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -27,14 +27,13 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// /// Names are allocated from [parentModule]'s [Namer]'s shared namespace - /// via [Namer.allocateName]. + /// via [Namer.instanceNameOf], which memoizes by [Module] identity so the + /// same instance receives an identical canonical name across repeated + /// synthesis passes (e.g. netlist then SystemVerilog). void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.allocateName( - module.uniqueInstanceName, - reserved: module.reserveName, - ); + _name = parentModule.namer.instanceNameOf(module); } /// A mapping of input port name to [SynthLogic]. diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index d4c6eff85..368a90ef7 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -32,6 +32,16 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; + /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. + /// + /// Allocating an instance name mutates [_uniquifier], so without this + /// cache a second synthesis pass over the same (already-built) module + /// hierarchy would re-allocate and drift the numeric suffixes. Caching + /// by the stable [Module.instanceNameKey] (the [Module] itself for built + /// modules, or the driven [Logic] for synthesis-time throwaway modules) + /// keeps instance names canonical across repeated synthesizer runs. + final Map _instanceNames = {}; + /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; @@ -80,6 +90,36 @@ class Namer { reserved: reserved, ); + // ─── Instance naming (Module → String) ────────────────────────── + + /// Returns the canonical instance name for the [submodule]. + /// + /// The first call for a given [submodule] allocates a collision-free + /// name in the shared namespace (mutating the underlying [Uniquifier]). + /// Subsequent calls return the cached result in O(1). + /// + /// Caching is essential for determinism: instance-name allocation + /// mutates the shared namespace, so re-synthesizing the same built + /// module (e.g. running the netlist synthesizer followed by the + /// SystemVerilog synthesizer, or two SystemVerilog passes) would + /// otherwise consume fresh suffixes each pass and produce non-canonical + /// names. Keying by [Module] identity — which is stable across passes — + /// guarantees identical names every time. + String instanceNameOf(Module submodule) { + final key = submodule.instanceNameKey; + final cached = _instanceNames[key]; + if (cached != null) { + return cached; + } + + final name = _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(submodule.uniqueInstanceName), + reserved: submodule.reserveName, + ); + _instanceNames[key] = name; + return name; + } + // ─── Signal naming (Logic → String) ───────────────────────────── /// Returns the canonical name for [logic]. From 6a41f8d120b86e12911c3d8470f63376f2bf092b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 12 Jun 2026 14:51:10 -0700 Subject: [PATCH 17/58] pesky override rule surfaced again on tutorials file --- doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart index 5765f08bf..18efb5a2d 100644 --- a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart +++ b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart @@ -7,7 +7,6 @@ import '../../chapter_3/answers/helper.dart'; import '../../chapter_5/answers/full_subtractor.dart'; class FullSubtractorComb extends FullSubtractor { - @override FullSubtractorComb(super.a, super.b, super.borrowIn) { // Declare input and output final a = input('a'); From 139280f4b88dd10f60b0b612f6800469211a1bb4 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 12 Jun 2026 15:24:29 -0700 Subject: [PATCH 18/58] new keyring-based installation for dart in codespaces --- tool/gh_codespaces/install_dart.sh | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index abbe39a0c..23b6f2f28 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -11,21 +11,22 @@ set -euo pipefail -# Add Dart repository key. - -declare -r input_pubkey_file='tool/gh_codespaces/pubkeys/dart.pub' -declare -r output_pubkey_file='/usr/share/keyrings/dart.gpg' +set -euo pipefail -sudo gpg --output ${output_pubkey_file} --dearmor ${input_pubkey_file} +sudo apt-get update +sudo apt-get install -y wget gpg apt-transport-https -# Add Dart repository. +sudo mkdir -p /usr/share/keyrings +wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ + | gpg --dearmor \ + | sudo tee /usr/share/keyrings/dart.gpg >/dev/null -declare -r dart_repository_url='https://storage.googleapis.com/download.dartlang.org/linux/debian' -declare -r dart_repository_file='/etc/apt/sources.list.d/dart.list' +# Add Dart repository key. -echo "deb [signed-by=${output_pubkey_file}] ${dart_repository_url} stable main" | sudo tee ${dart_repository_file} +echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" \ + | sudo tee /etc/apt/sources.list.d/dart_stable.list # Install Dart. sudo apt-get update -sudo apt-get install dart +sudo apt-get install -y dart From 62e4a2cdf77296c19d40f2857758f73b2ddb64ff Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 12 Jun 2026 15:24:57 -0700 Subject: [PATCH 19/58] new keyring-based installation for dart in codespaces --- tool/gh_codespaces/install_dart.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index 23b6f2f28..3fc47fcd7 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -11,8 +11,6 @@ set -euo pipefail -set -euo pipefail - sudo apt-get update sudo apt-get install -y wget gpg apt-transport-https From caecb020830d17351b4a74294578b41f5d17f217 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 14 Jun 2026 10:42:18 -0700 Subject: [PATCH 20/58] keyring-style dart installation rather than holding keys --- tool/gh_codespaces/install_dart.sh | 21 ++- tool/gh_codespaces/pubkeys/dart.pub | 267 ---------------------------- 2 files changed, 10 insertions(+), 278 deletions(-) delete mode 100644 tool/gh_codespaces/pubkeys/dart.pub diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index abbe39a0c..3fc47fcd7 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -11,21 +11,20 @@ set -euo pipefail -# Add Dart repository key. - -declare -r input_pubkey_file='tool/gh_codespaces/pubkeys/dart.pub' -declare -r output_pubkey_file='/usr/share/keyrings/dart.gpg' - -sudo gpg --output ${output_pubkey_file} --dearmor ${input_pubkey_file} +sudo apt-get update +sudo apt-get install -y wget gpg apt-transport-https -# Add Dart repository. +sudo mkdir -p /usr/share/keyrings +wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ + | gpg --dearmor \ + | sudo tee /usr/share/keyrings/dart.gpg >/dev/null -declare -r dart_repository_url='https://storage.googleapis.com/download.dartlang.org/linux/debian' -declare -r dart_repository_file='/etc/apt/sources.list.d/dart.list' +# Add Dart repository key. -echo "deb [signed-by=${output_pubkey_file}] ${dart_repository_url} stable main" | sudo tee ${dart_repository_file} +echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" \ + | sudo tee /etc/apt/sources.list.d/dart_stable.list # Install Dart. sudo apt-get update -sudo apt-get install dart +sudo apt-get install -y dart diff --git a/tool/gh_codespaces/pubkeys/dart.pub b/tool/gh_codespaces/pubkeys/dart.pub deleted file mode 100644 index 0366239cb..000000000 --- a/tool/gh_codespaces/pubkeys/dart.pub +++ /dev/null @@ -1,267 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.2.2 (GNU/Linux) - -mQGiBEXwb0YRBADQva2NLpYXxgjNkbuP0LnPoEXruGmvi3XMIxjEUFuGNCP4Rj/a -kv2E5VixBP1vcQFDRJ+p1puh8NU0XERlhpyZrVMzzS/RdWdyXf7E5S8oqNXsoD1z -fvmI+i9b2EhHAA19Kgw7ifV8vMa4tkwslEmcTiwiw8lyUl28Wh4Et8SxzwCggDcA -feGqtn3PP5YAdD0km4S4XeMEAJjlrqPoPv2Gf//tfznY2UyS9PUqFCPLHgFLe80u -QhI2U5jt6jUKN4fHauvR6z3seSAsh1YyzyZCKxJFEKXCCqnrFSoh4WSJsbFNc4PN -b0V0SqiTCkWADZyLT5wll8sWuQ5ylTf3z1ENoHf+G3um3/wk/+xmEHvj9HCTBEXP -78X0A/0Tqlhc2RBnEf+AqxWvM8sk8LzJI/XGjwBvKfXe+l3rnSR2kEAvGzj5Sg0X -4XmfTg4Jl8BNjWyvm2Wmjfet41LPmYJKsux3g0b8yzQxeOA4pQKKAU3Z4+rgzGmf -HdwCG5MNT2A5XxD/eDd+L4fRx0HbFkIQoAi1J3YWQSiTk15fw7RMR29vZ2xlLCBJ -bmMuIExpbnV4IFBhY2thZ2UgU2lnbmluZyBLZXkgPGxpbnV4LXBhY2thZ2VzLWtl -eW1hc3RlckBnb29nbGUuY29tPohjBBMRAgAjAhsDBgsJCAcDAgQVAggDBBYCAwEC -HgECF4AFAkYVdn8CGQEACgkQoECDD3+sWZHKSgCfdq3HtNYJLv+XZleb6HN4zOcF -AJEAniSFbuv8V5FSHxeRimHx25671az+uQINBEXwb0sQCACuA8HT2nr+FM5y/kzI -A51ZcC46KFtIDgjQJ31Q3OrkYP8LbxOpKMRIzvOZrsjOlFmDVqitiVc7qj3lYp6U -rgNVaFv6Qu4bo2/ctjNHDDBdv6nufmusJUWq/9TwieepM/cwnXd+HMxu1XBKRVk9 -XyAZ9SvfcW4EtxVgysI+XlptKFa5JCqFM3qJllVohMmr7lMwO8+sxTWTXqxsptJo -pZeKz+UBEEqPyw7CUIVYGC9ENEtIMFvAvPqnhj1GS96REMpry+5s9WKuLEaclWpd -K3krttbDlY1NaeQUCRvBYZ8iAG9YSLHUHMTuI2oea07Rh4dtIAqPwAX8xn36JAYG -2vgLAAMFB/wKqaycjWAZwIe98Yt0qHsdkpmIbarD9fGiA6kfkK/UxjL/k7tmS4Vm -CljrrDZkPSQ/19mpdRcGXtb0NI9+nyM5trweTvtPw+HPkDiJlTaiCcx+izg79Fj9 -KcofuNb3lPdXZb9tzf5oDnmm/B+4vkeTuEZJ//IFty8cmvCpzvY+DAz1Vo9rA+Zn -cpWY1n6z6oSS9AsyT/IFlWWBZZ17SpMHu+h4Bxy62+AbPHKGSujEGQhWq8ZRoJAT -G0KSObnmZ7FwFWu1e9XFoUCt0bSjiJWTIyaObMrWu/LvJ3e9I87HseSJStfw6fki -5og9qFEkMrIrBCp3QGuQWBq/rTdMuwNFiEkEGBECAAkFAkXwb0sCGwwACgkQoECD -D3+sWZF/WACfeNAu1/1hwZtUo1bR+MWiCjpvHtwAnA1R3IHqFLQ2X3xJ40XPuAyY -/FJG -=Quqp ------END PGP PUBLIC KEY BLOCK----- ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFcMjNMBEAC6Wr5QuLIFgz1V1EFPlg8ty2TsjQEl4VWftUAqWlMevJFWvYEx -BOsOZ6kNFfBfjAxgJNWTkxZrHzDl74R7KW/nUx6X57bpFjUyRaB8F3/NpWKSeIGS -pJT+0m2SgUNhLAn1WY/iNJGNaMl7lgUnaP+/ZsSNT9hyTBiH3Ev5VvAtMGhVI/u8 -P0EtTjXp4o2U+VqFTBGmZ6PJVhCFjZUeRByloHw8dGOshfXKgriebpioHvU8iQ2U -GV3WNIirB2Rq1wkKxXJ/9Iw+4l5m4GmXMs7n3XaYQoBj28H86YA1cYWSm5LR5iU2 -TneI1fJ3vwF2vpSXVBUUDk67PZhg6ZwGRT7GFWskC0z8PsWd5jwK20mA8EVKq0vN -BFmMK6i4fJU+ux17Rgvnc9tDSCzFZ1/4f43EZ41uTmmNXIDsaPCqwjvSS5ICadt2 -xeqTWDlzONUpOs5yBjF1cfJSdVxsfshvln2JXUwgIdKl4DLbZybuNFXnPffNLb2v -PtRJHO48O2UbeXS8n27PcuMoLRd7+r7TsqG2vBH4t/cB/1vsvWMbqnQlaJ5VsjeW -Tp8Gv9FJiKuU8PKiWsF4EGR/kAFyCB8QbJeQ6HrOT0CXLOaYHRu2TvJ4taY9doXn -98TgU03XTLcYoSp49cdkkis4K+9hd2dUqARVCG7UVd9PY60VVCKi47BVKQARAQAB -tFRHb29nbGUgSW5jLiAoTGludXggUGFja2FnZXMgU2lnbmluZyBBdXRob3JpdHkp -IDxsaW51eC1wYWNrYWdlcy1rZXltYXN0ZXJAZ29vZ2xlLmNvbT6JAk4EEwEIADgC -GwMCHgECF4AWIQTrTBv9TwQvbd3M7JF3IfY704tHlgUCVwyM0wULCQgHAgYVCgkI -CwIEFgIDAQAKCRB3IfY704tHlkGrD/9aIOPxoABbhHDa+GbM1XHSeV99q2UOIsYc -A5Jg3k2+Vbjr/006cL9Kk+rdbruZJtERo2z+HVVhkJisvySbsd0UbWfiY5AdHzNP -azpitbX9cNYi0ghDZsD5UgP3cWdx21BJPO0v9PBG9U4z1TQ+pmsQphtNzMC4tK+A -H/7WTXnVPzKXTYziIEIPgHeassSj7Yfwa8kLiBR5tAehHDNNMi/mMf4d6a+wO46x -hhRx/BLjoaIxsZw9f5VxDAqGbCrW8IccwJX8vTc89y+6vpzSurdqYrplZWGpcnfT -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 -bVtIZ+JHTbuH+tg0EoRNcCbz -=JIbr ------END PGP PUBLIC KEY BLOCK----- From 089faf88031f1cb74bc03e0e1e756f001fc1a2ca Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 14 Jun 2026 11:36:04 -0700 Subject: [PATCH 21/58] new dart analyzer failure with bad override --- doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart index 5765f08bf..18efb5a2d 100644 --- a/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart +++ b/doc/tutorials/chapter_6/answers/exercise_2_n_bit_subtractor.dart @@ -7,7 +7,6 @@ import '../../chapter_3/answers/helper.dart'; import '../../chapter_5/answers/full_subtractor.dart'; class FullSubtractorComb extends FullSubtractor { - @override FullSubtractorComb(super.a, super.b, super.borrowIn) { // Declare input and output final a = input('a'); From 1232afbe490bbf246590886e2adae8765aba699d Mon Sep 17 00:00:00 2001 From: Desmond Kirkpatrick Date: Mon, 15 Jun 2026 06:04:29 -0700 Subject: [PATCH 22/58] Potential fix for pull request finding Clarify comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tool/gh_codespaces/install_dart.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index 3fc47fcd7..f170dc247 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -19,7 +19,7 @@ wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ | gpg --dearmor \ | sudo tee /usr/share/keyrings/dart.gpg >/dev/null -# Add Dart repository key. +# Add Dart repository. echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" \ | sudo tee /etc/apt/sources.list.d/dart_stable.list From e558a4db178c46f674cb05196c63d08061a5a590 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 17 Jun 2026 07:46:19 -0700 Subject: [PATCH 23/58] Orthogonalize: simplify Namer by removing instance name caching This aligns central_naming with the simplified naming approach already adopted by all downstream branches (module_services, netlist, source_debug, systemc_trace, fst-writer). Changes: - Remove Namer._instanceNames cache field - Remove Namer.instanceNameOf(Module) method - Update synthesizers to use Namer.allocateName(String) directly - Remove destination tracking from _BusSubsetForStructSlice Benefit: Eliminates duplication across 5+ branches, making each branch truly orthogonal and mergeable without conflicts. Trade-off: Instance names no longer cached across synthesis passes, but all downstreams already use this simpler approach. --- .../utilities/synth_module_definition.dart | 10 ++-------- .../utilities/synth_sub_module_instantiation.dart | 9 +++++---- lib/src/utilities/namer.dart | 9 --------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 001f32eef..38a207b74 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -25,24 +25,19 @@ class _BusSubsetForStructSlice extends BusSubset { /// [_BusSubsetForStructSlice] is created on every synthesis pass, its /// canonical instance name is memoized against the persistent destination /// signal and therefore does not drift run-to-run. - final Logic _destination; /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. _BusSubsetForStructSlice( super.bus, super.startIndex, - super.endIndex, { - required Logic destination, - }) : _destination = destination, - super(name: 'struct_slice'); + super.endIndex, + ) : super(name: 'struct_slice'); // we override this since it's added post-build @override bool get hasBuilt => true; - @override - Object get instanceNameKey => _destination; } /// Represents the definition of a module. @@ -278,7 +273,6 @@ class SynthModuleDefinition { width: port.width, name: 'DUMMY'), idx, idx + leafElement.width - 1, - destination: leafElement, ); final ssmi = getSynthSubModuleInstantiation(subsetMod); diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index ee35f1bac..6c711c330 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -27,13 +27,14 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// /// Names are allocated from [parentModule]'s [Namer]'s shared namespace - /// via [Namer.instanceNameOf], which memoizes by [Module] identity so the - /// same instance receives an identical canonical name across repeated - /// synthesis passes (e.g. netlist then SystemVerilog). + /// via [Namer.allocateName]. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.instanceNameOf(module); + _name = parentModule.namer.allocateName( + module.uniqueInstanceName, + reserved: module.reserveName, + ); } /// A mapping of input port name to [SynthLogic]. diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 368a90ef7..af770cf04 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -32,15 +32,6 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; - /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. - /// - /// Allocating an instance name mutates [_uniquifier], so without this - /// cache a second synthesis pass over the same (already-built) module - /// hierarchy would re-allocate and drift the numeric suffixes. Caching - /// by the stable [Module.instanceNameKey] (the [Module] itself for built - /// modules, or the driven [Logic] for synthesis-time throwaway modules) - /// keeps instance names canonical across repeated synthesizer runs. - final Map _instanceNames = {}; /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; From 8ef68207135ae2c8b738d55be6e5e81f6251e426 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 17 Jun 2026 07:55:24 -0700 Subject: [PATCH 24/58] Fix orthogonalized Namer: remove stale instanceNameOf method --- lib/src/utilities/namer.dart | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index af770cf04..3afc9d873 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -81,36 +81,6 @@ class Namer { reserved: reserved, ); - // ─── Instance naming (Module → String) ────────────────────────── - - /// Returns the canonical instance name for the [submodule]. - /// - /// The first call for a given [submodule] allocates a collision-free - /// name in the shared namespace (mutating the underlying [Uniquifier]). - /// Subsequent calls return the cached result in O(1). - /// - /// Caching is essential for determinism: instance-name allocation - /// mutates the shared namespace, so re-synthesizing the same built - /// module (e.g. running the netlist synthesizer followed by the - /// SystemVerilog synthesizer, or two SystemVerilog passes) would - /// otherwise consume fresh suffixes each pass and produce non-canonical - /// names. Keying by [Module] identity — which is stable across passes — - /// guarantees identical names every time. - String instanceNameOf(Module submodule) { - final key = submodule.instanceNameKey; - final cached = _instanceNames[key]; - if (cached != null) { - return cached; - } - - final name = _uniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(submodule.uniqueInstanceName), - reserved: submodule.reserveName, - ); - _instanceNames[key] = name; - return name; - } - // ─── Signal naming (Logic → String) ───────────────────────────── /// Returns the canonical name for [logic]. From d019f4251f7c370e8edde1f9414bafda83b94772 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 17 Jun 2026 07:55:24 -0700 Subject: [PATCH 25/58] Fix orthogonalized Namer: remove stale instanceNameOf method --- lib/src/utilities/namer.dart | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index af770cf04..3afc9d873 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -81,36 +81,6 @@ class Namer { reserved: reserved, ); - // ─── Instance naming (Module → String) ────────────────────────── - - /// Returns the canonical instance name for the [submodule]. - /// - /// The first call for a given [submodule] allocates a collision-free - /// name in the shared namespace (mutating the underlying [Uniquifier]). - /// Subsequent calls return the cached result in O(1). - /// - /// Caching is essential for determinism: instance-name allocation - /// mutates the shared namespace, so re-synthesizing the same built - /// module (e.g. running the netlist synthesizer followed by the - /// SystemVerilog synthesizer, or two SystemVerilog passes) would - /// otherwise consume fresh suffixes each pass and produce non-canonical - /// names. Keying by [Module] identity — which is stable across passes — - /// guarantees identical names every time. - String instanceNameOf(Module submodule) { - final key = submodule.instanceNameKey; - final cached = _instanceNames[key]; - if (cached != null) { - return cached; - } - - final name = _uniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(submodule.uniqueInstanceName), - reserved: submodule.reserveName, - ); - _instanceNames[key] = name; - return name; - } - // ─── Signal naming (Logic → String) ───────────────────────────── /// Returns the canonical name for [logic]. From f75950dc8636b5f6471c45ec9481c2f3f2d0f3c6 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 17 Jun 2026 14:28:39 -0700 Subject: [PATCH 26/58] cleanup of SvService --- lib/src/module.dart | 13 +------------ .../synthesizers/systemverilog/sv_service.dart | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index 122ee7f98..30f63187b 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1141,18 +1141,7 @@ abstract class Module { throw ModuleNotBuiltException(this); } - final synthHeader = ''' -/** - * Generated by ROHD - www.github.com/intel/rohd - * Generation time: ${Timestamper.stamp()} - * ROHD Version: ${Config.version} - */ - -'''; - return synthHeader + - SynthBuilder(this, SystemVerilogSynthesizer()) - .getSynthFileContents() - .join('\n\n////////////////////\n\n'); + return SvService(this, register: false).synthOutput; } } diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart index e1adf43cd..b255c2aba 100644 --- a/lib/src/synthesizers/systemverilog/sv_service.dart +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -11,6 +11,8 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; /// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. /// @@ -63,9 +65,22 @@ class SvService { Set get synthesisResults => synthBuilder.synthesisResults; /// Returns the concatenated SystemVerilog output as a single string, - /// matching the format of [Module.generateSynth]. + /// without the ROHD generation header. String get allContents => fileContents.map((fc) => fc.contents).join('\n\n'); + /// The ROHD generation header prepended to single-file output. + String get _synthHeader => ''' +/** + * Generated by ROHD - www.github.com/intel/rohd + * Generation time: ${Timestamper.stamp()} + * ROHD Version: ${Config.version} + */ + +'''; + + /// Returns the full single-file SystemVerilog output with header. + String get synthOutput => _synthHeader + allContents; + /// Returns a map from module definition name to its SV file contents. /// /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition From 7720b1444ab8934637c4f0ef81c9c446ee2d900d Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 17 Jun 2026 16:56:30 -0700 Subject: [PATCH 27/58] cleaner SvService maintaining backward compat --- lib/rohd.dart | 1 + lib/src/diagnostics/module_services.dart | 14 +- lib/src/diagnostics/waveform_service.dart | 464 ++++++++++++++++++ lib/src/module.dart | 6 +- .../systemverilog/sv_service.dart | 80 ++- 5 files changed, 551 insertions(+), 14 deletions(-) create mode 100644 lib/src/diagnostics/waveform_service.dart diff --git a/lib/rohd.dart b/lib/rohd.dart index d0ea2a266..1a6cf43b6 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause export 'src/diagnostics/module_services.dart'; +export 'src/diagnostics/waveform_service.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart index a0e583b59..8f17b5746 100644 --- a/lib/src/diagnostics/module_services.dart +++ b/lib/src/diagnostics/module_services.dart @@ -23,8 +23,9 @@ import 'package:rohd/src/diagnostics/inspector_service.dart'; /// /// **Opt-in (registered by service constructors):** /// - [svService] — SystemVerilog synthesis results. +/// - [waveformService] — waveform capture (file output + optional streaming). /// -/// Additional services (netlist, trace, waveform) can be added by setting +/// Additional services (netlist, trace) can be added by setting /// the corresponding field after construction. class ModuleServices { ModuleServices._(); @@ -63,6 +64,16 @@ class ModuleServices { String get svJSON => svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); + // ─── Waveform service (opt-in) ─────────────────────────────── + + /// The active [WaveformService], if one has been registered. + WaveformService? waveformService; + + /// Returns waveform service metadata as JSON, or an unavailable status. + String get waveformJSON => waveformService != null + ? jsonEncode(waveformService!.toJson()) + : _unavailable('waveform'); + // ─── Helpers ────────────────────────────────────────────────── static String _unavailable(String service) => jsonEncode({ @@ -74,5 +85,6 @@ class ModuleServices { void reset() { rootModule = null; svService = null; + waveformService = null; } } diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart new file mode 100644 index 000000000..f165fc8b0 --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,464 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_service.dart +// Base waveform service: file output with filtering, timescale, and +// flush/overwrite control. Designed to be subclassed by the DevTools +// streaming variant. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:collection'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +// ─── Supporting types ──────────────────────────────────────────────────────── + +/// The output format for waveform capture. +enum WaveOutputFormat { + /// Value Change Dump — the classic text-based waveform format. + vcd, + + /// Fast Signal Trace — a compact binary format. + /// + /// Requires an FST writer to be available; see the DevTools subclass for + /// a fully FST-backed implementation. + fst, +} + +/// Policy applied when the output file already exists at construction time. +enum OverwritePolicy { + /// Silently overwrite any existing file. + overwrite, + + /// Throw a [FileSystemException] if the file already exists. + failIfExists, +} + +// ─── Options ───────────────────────────────────────────────────────────────── + +/// Configuration for [WaveformService]. +/// +/// Example — capture only ports, write FST, fail if file exists: +/// ```dart +/// final svc = WaveformService( +/// dut, +/// options: WaveformServiceOptions( +/// outputPath: 'build/waves.fst', +/// format: WaveOutputFormat.fst, +/// signalFilter: (sig) => sig.isPort, +/// overwritePolicy: OverwritePolicy.failIfExists, +/// ), +/// ); +/// ``` +class WaveformServiceOptions { + /// Path of the output waveform file. + /// + /// The parent directory is created if necessary. + /// Defaults to `'waves.vcd'`. + final String outputPath; + + /// Output format. Defaults to [WaveOutputFormat.vcd]. + final WaveOutputFormat format; + + /// Optional predicate that determines whether a given [Logic] signal is + /// captured. + /// + /// When `null`, all non-[Const] signals in the hierarchy are captured + /// (matching the existing [WaveDumper] behaviour). + final bool Function(Logic signal)? signalFilter; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + /// + /// Defaults to `'1ps'`. + final String timescale; + + /// Simulation time at which recording begins. + /// + /// Signals are still collected before this time so they appear in the + /// scope definition, but value-change events are suppressed until + /// [startTime] is reached. `null` means "from the very start". + final int? startTime; + + /// Simulation time at which recording ends. + /// + /// Value-change events after this time are suppressed. + /// `null` means "until end of simulation". + final int? stopTime; + + /// Number of characters accumulated in the write buffer before it is + /// flushed to disk. + /// + /// Larger values reduce I/O syscalls but increase peak memory usage. + /// Defaults to `100000`. + final int flushBufferSize; + + /// What to do when the output file already exists. + /// + /// Defaults to [OverwritePolicy.overwrite]. + final OverwritePolicy overwritePolicy; + + /// Whether to register this service with [ModuleServices] for inspection. + /// + /// Defaults to `true`. + final bool register; + + /// Whether to enable DevTools streaming. + /// + /// When `true`, the service (or a subclass) may attach VM service + /// extensions and in-memory signal tracking for live DevTools access. + /// + /// The base [WaveformService] stores this flag but takes no action on it. + /// The DevTools subclass uses it to conditionally register extensions. + /// + /// Defaults to `false`. + final bool enableDevToolsStreaming; + + /// Creates [WaveformServiceOptions]. + const WaveformServiceOptions({ + this.outputPath = 'waves.vcd', + this.format = WaveOutputFormat.vcd, + this.signalFilter, + this.timescale = '1ps', + this.startTime, + this.stopTime, + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + this.register = true, + this.enableDevToolsStreaming = false, + }); +} + +// ─── Service ───────────────────────────────────────────────────────────────── + +/// A waveform capture service that writes signal changes to a file. +/// +/// This is the base class for waveform capture. It handles: +/// - Signal collection (with optional [WaveformServiceOptions.signalFilter]) +/// - VCD file output with configurable [WaveformServiceOptions.timescale] +/// - Selective recording via [WaveformServiceOptions.startTime] / +/// [WaveformServiceOptions.stopTime] +/// - Periodic buffer flushing and [WaveformServiceOptions.overwritePolicy] +/// - Optional registration with [ModuleServices] +/// +/// **Subclassing for DevTools streaming:** +/// +/// Override the protected hooks below to intercept the simulation event loop +/// without re-implementing the file-writing logic: +/// +/// - [onSignalCollected] — called once per tracked signal at startup; use +/// it to register signals in a VM-service index. +/// - [onValueChange] — called for every value-change event within the +/// [WaveformServiceOptions.startTime]/[WaveformServiceOptions.stopTime] +/// window; use it to feed an in-memory store for streaming. +/// - [onTimestampCapture] — called once per simulation timestamp that +/// contains at least one change; the full changed-signal set is passed. +/// - [onSimulationEnd] — called after the final timestamp is written and +/// the file is closed; use it to finalise any streaming buffers. +/// +/// Example subclass skeleton: +/// ```dart +/// class DevToolsWaveformService extends WaveformService { +/// DevToolsWaveformService(super.module, {super.options}); +/// +/// @override +/// void onSignalCollected(Logic signal) { +/// super.onSignalCollected(signal); +/// _registerWithVmService(signal); +/// } +/// +/// @override +/// void onValueChange(Logic signal, int timestamp) { +/// super.onValueChange(signal, timestamp); +/// _recordInMemory(signal, timestamp); +/// } +/// } +/// ``` +class WaveformService { + /// The top-level [Module] being captured. + final Module module; + + /// The options controlling this service. + final WaveformServiceOptions options; + + // ─── Internal file-writing state ───────────────────────────── + + /// The output file. + late final File _outputFile; + + /// Sink writing into [_outputFile]. + late final IOSink _outFileSink; + + /// Write buffer; flushed when it exceeds + /// [WaveformServiceOptions.flushBufferSize]. + final StringBuffer _fileBuffer = StringBuffer(); + + /// Counter for assigning compact signal markers in the VCD. + int _signalMarkerIdx = 0; + + /// Maps each captured [Logic] to its VCD marker string. + final Map _signalToMarkerMap = {}; + + /// Signals that changed during the current simulation timestamp. + final Set _changedThisTimestamp = HashSet(); + + /// The timestamp currently being accumulated. + int _currentDumpingTimestamp = Simulator.time; + + // ─── Constructor ───────────────────────────────────────────── + + /// Creates a [WaveformService] for [module]. + /// + /// [module] must be built before construction. + /// + /// Provide [options] to configure format, path, filtering, timescale, + /// start/stop times, flush size, and overwrite policy. + WaveformService(this.module, + {this.options = const WaveformServiceOptions()}) { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating WaveformService. ' + 'Call build() first.'); + } + + if (options.overwritePolicy == OverwritePolicy.failIfExists) { + final f = File(options.outputPath); + if (f.existsSync()) { + throw FileSystemException( + 'Waveform output file already exists and overwritePolicy is ' + 'failIfExists.', + options.outputPath); + } + } + + _outputFile = File(options.outputPath)..createSync(recursive: true); + _outFileSink = _outputFile.openWrite(); + + _collectSignals(); + _writeHeader(); + _writeScope(); + + Simulator.preTick.listen((_) { + if (Simulator.time != _currentDumpingTimestamp) { + if (_changedThisTimestamp.isNotEmpty) { + _captureTimestamp(_currentDumpingTimestamp); + } + _currentDumpingTimestamp = Simulator.time; + } + }); + + Simulator.registerEndOfSimulationAction(() async { + _captureTimestamp(Simulator.time); + await _terminate(); + onSimulationEnd(); + }); + + if (options.register) { + ModuleServices.instance.waveformService = this; + } + } + + // ─── Extensibility hooks ────────────────────────────────────── + + /// Called once for each [Logic] signal that passes + /// [WaveformServiceOptions.signalFilter] during initial signal collection. + /// + /// Override in a subclass to register signals with an in-memory store, + /// VM service index, or FST handle map. Always call `super` first. + @protected + void onSignalCollected(Logic signal) {} + + /// Called for every value-change event on [signal] at [timestamp]. + /// + /// Only called within the [WaveformServiceOptions.startTime] / + /// [WaveformServiceOptions.stopTime] window. + /// + /// Override in a subclass to feed an in-memory waveform store or + /// streaming buffer. Always call `super` first. + @protected + void onValueChange(Logic signal, int timestamp) {} + + /// Called once per simulation timestamp that contains at least one change, + /// after all value-change events for that timestamp have been processed. + /// + /// [changed] is the set of signals that changed at [timestamp]. + /// + /// Override in a subclass to flush incremental streaming payloads. + /// Always call `super` first. + @protected + void onTimestampCapture(int timestamp, Set changed) {} + + /// Called after the final timestamp has been written and the file is closed. + /// + /// Override in a subclass to finalise any streaming buffers or emit + /// end-of-simulation notifications. + @protected + void onSimulationEnd() {} + + // ─── Internal signal collection ────────────────────────────── + + void _collectSignals() { + final modulesToParse = [module]; + for (var i = 0; i < modulesToParse.length; i++) { + final m = modulesToParse[i]; + for (final sig in m.signals) { + if (sig is Const) { + continue; + } + if (options.signalFilter != null && !options.signalFilter!(sig)) { + continue; + } + + _signalToMarkerMap[sig] = 's${_signalMarkerIdx++}'; + onSignalCollected(sig); + + sig.changed.listen((_) { + _changedThisTimestamp.add(sig); + }); + } + + for (final subm in m.subModules) { + if (subm is InlineSystemVerilog) { + continue; + } + modulesToParse.add(subm); + } + } + } + + // ─── VCD output helpers ─────────────────────────────────────── + + void _writeHeader() { + final header = ''' +\$date + ${Timestamper.stamp()} +\$end +\$version + ROHD v${Config.version} +\$end +\$comment + Generated by ROHD - www.github.com/intel/rohd +\$end +\$timescale ${options.timescale} \$end +'''; + _writeToBuffer(header); + } + + void _writeScope() { + var scopeString = _computeScopeString(module); + scopeString += '\$enddefinitions \$end\n'; + scopeString += '\$dumpvars\n'; + _writeToBuffer(scopeString); + _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + _writeToBuffer('\$end\n'); + } + + String _computeScopeString(Module m, {int indent = 0}) { + final moduleSignalUniquifier = Uniquifier(); + final padding = List.filled(indent, ' ').join(); + var scopeString = '$padding\$scope module ${m.uniqueInstanceName} \$end\n'; + final innerScopeString = StringBuffer(); + + for (final sig in m.signals) { + if (!_signalToMarkerMap.containsKey(sig)) { + continue; + } + final width = sig.width; + final marker = _signalToMarkerMap[sig]; + var signalName = Sanitizer.sanitizeSV(sig.name); + signalName = moduleSignalUniquifier.getUniqueName( + initialName: signalName, reserved: sig.isPort); + innerScopeString + .write(' $padding\$var wire $width $marker $signalName \$end\n'); + } + for (final subModule in m.subModules) { + innerScopeString + .write(_computeScopeString(subModule, indent: indent + 1)); + } + if (innerScopeString.isEmpty) { + return ''; + } + + scopeString += innerScopeString.toString(); + scopeString += '$padding\$upscope \$end\n'; + return scopeString; + } + + bool _isInRecordingWindow(int timestamp) { + if (options.startTime != null && timestamp < options.startTime!) { + return false; + } + if (options.stopTime != null && timestamp > options.stopTime!) { + return false; + } + return true; + } + + void _captureTimestamp(int timestamp) { + if (!_isInRecordingWindow(timestamp)) { + _changedThisTimestamp.clear(); + return; + } + + _writeToBuffer('#$timestamp\n'); + + final snapshot = Set.of(_changedThisTimestamp); + for (final sig in snapshot) { + _writeSignalValueUpdate(sig); + onValueChange(sig, timestamp); + } + _changedThisTimestamp.clear(); + + onTimestampCapture(timestamp, snapshot); + } + + void _writeSignalValueUpdate(Logic signal) { + final binaryValue = signal.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); + final updateValue = signal.width > 1 + ? 'b$binaryValue ' + : signal.value.toString(includeWidth: false); + final marker = _signalToMarkerMap[signal]; + _writeToBuffer('$updateValue$marker\n'); + } + + // ─── Buffered I/O ───────────────────────────────────────────── + + void _writeToBuffer(String contents) { + _fileBuffer.write(contents); + if (_fileBuffer.length > options.flushBufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + _outFileSink.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } + + Future _terminate() async { + _flushBuffer(); + await _outFileSink.flush(); + await _outFileSink.close(); + } + + // ─── Inspection ─────────────────────────────────────────────── + + /// Returns a JSON-serialisable summary of this service. + Map toJson() => { + 'outputPath': options.outputPath, + 'format': options.format.name, + 'signalCount': _signalToMarkerMap.length, + 'timescale': options.timescale, + if (options.startTime != null) 'startTime': options.startTime!, + if (options.stopTime != null) 'stopTime': options.stopTime!, + }; +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 30f63187b..56bb344b4 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,10 +13,8 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; -import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a synthesizable hardware entity with clearly defined interface @@ -1136,6 +1134,10 @@ abstract class Module { /// /// Currently returns one long file in SystemVerilog, but in the future /// may have other output formats, languages, files, etc. + /// + /// Deprecated: use [SvService] instead, which provides richer access to + /// per-module file contents, named maps, and individual file writing. + @Deprecated('Use SvService instead.') String generateSynth() { if (!_hasBuilt) { throw ModuleNotBuiltException(this); diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart index b255c2aba..8b8cb47d2 100644 --- a/lib/src/synthesizers/systemverilog/sv_service.dart +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -14,6 +14,36 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/timestamper.dart'; +/// Configuration for constructing an [SvService]. +/// +/// Example — write a single concatenated `.sv` file: +/// ```dart +/// final sv = SvService( +/// dut, +/// options: SvServiceOptions(outputPath: 'build/out.sv'), +/// ); +/// ``` +/// +/// For writing one file per module definition, call [SvService.writeFiles] +/// after construction. +class SvServiceOptions { + /// Whether to register this service with [ModuleServices]. + final bool register; + + /// Optional path where the single-file SV output should be written. + /// + /// If non-null, the concatenated SV output (with generation header) is + /// written to this file on construction. The parent directory is created + /// if needed. Use [SvService.writeFiles] for per-module file output. + final String? outputPath; + + /// Creates [SvService] options. + const SvServiceOptions({ + this.register = true, + this.outputPath, + }); +} + /// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. /// /// Provides access to the generated SV file contents and per-module @@ -33,6 +63,11 @@ import 'package:rohd/src/utilities/timestamper.dart'; /// print(sv.allContents); /// ``` class SvService { + /// The separator inserted between module definitions in the + /// concatenated single-file output from [allContents]. + /// + /// Matches the format historically produced by `Module.generateSynth()`. + static const moduleSeparator = '\n\n////////////////////\n\n'; /// The top-level [Module] being synthesized. final Module module; @@ -44,19 +79,37 @@ class SvService { /// Creates an [SvService] for [module]. /// - /// [module] must already be built. Set [register] to `true` (the - /// default) to register this service with [ModuleServices] for - /// DevTools access. - SvService(this.module, {bool register = true}) { + /// [module] must already be built. + /// + /// You can provide [options] to control registration and output path. + /// For backward compatibility, [register] and [outputPath] can still be + /// provided directly; those values override [options] when specified. + /// + /// If an output path is provided (via [outputPath] or `options.outputPath`), + /// the concatenated SV output (with header) is written to that file. + /// The parent directory is created if needed. + SvService(this.module, + {SvServiceOptions options = const SvServiceOptions(), + bool? register, + String? outputPath}) { if (!module.hasBuilt) { throw Exception('Module must be built before creating SvService. ' 'Call build() first.'); } + final effectiveRegister = register ?? options.register; + final effectiveOutputPath = outputPath ?? options.outputPath; + synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); fileContents = synthBuilder.getSynthFileContents(); - if (register) { + if (effectiveOutputPath != null) { + final file = File(effectiveOutputPath); + file.parent.createSync(recursive: true); + file.writeAsStringSync(synthOutput); + } + + if (effectiveRegister) { ModuleServices.instance.svService = this; } } @@ -64,12 +117,16 @@ class SvService { /// All [SynthesisResult]s produced by synthesis. Set get synthesisResults => synthBuilder.synthesisResults; - /// Returns the concatenated SystemVerilog output as a single string, - /// without the ROHD generation header. - String get allContents => fileContents.map((fc) => fc.contents).join('\n\n'); + /// Returns the concatenated SystemVerilog module definitions as a single + /// string, without the generation header. + /// + /// For the full output with header (matching `Module.generateSynth()`), + /// use [synthOutput]. + String get allContents => + fileContents.map((fc) => fc.contents).join(moduleSeparator); /// The ROHD generation header prepended to single-file output. - String get _synthHeader => ''' + String get synthHeader => ''' /** * Generated by ROHD - www.github.com/intel/rohd * Generation time: ${Timestamper.stamp()} @@ -78,8 +135,9 @@ class SvService { '''; - /// Returns the full single-file SystemVerilog output with header. - String get synthOutput => _synthHeader + allContents; + /// Returns the full single-file SystemVerilog output with header, + /// identical to `Module.generateSynth()`. + String get synthOutput => synthHeader + allContents; /// Returns a map from module definition name to its SV file contents. /// From 93dfdcdfca8b6d7809de1ca2cd941644b72ba2c6 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 18 Jun 2026 12:24:43 -0700 Subject: [PATCH 28/58] simplify services with direct options --- README.md | 4 +- benchmark/many_submodules_benchmark.dart | 2 +- benchmark/wave_dump_benchmark.dart | 2 +- doc/tutorials/chapter_2/helper.dart | 3 +- .../chapter_3/answers/exercise_sv.dart | 2 +- doc/tutorials/chapter_3/full_adder.dart | 2 +- .../chapter_4/answers/exercise_1_sv.dart | 2 +- .../chapter_4/answers/exercise_2_sv.dart | 2 +- .../chapter_4/basic_generation_sv.dart | 2 +- .../chapter_5/answers/full_adder.dart | 2 +- .../chapter_5/answers/full_subtractor.dart | 2 +- .../chapter_5/answers/n_bit_subtractor.dart | 2 +- doc/tutorials/chapter_5/n_bit_adder.dart | 2 +- .../answers/exercise_1_d_flip_flop.dart | 4 +- doc/tutorials/chapter_7/shift_register.dart | 2 +- .../chapter_8/answers/exercise_1_spi.dart | 4 +- .../answers/exercise_2_toycapsule_fsm.dart | 4 +- .../answers/exercise_3_pipeline.dart | 4 +- .../chapter_8/carry_save_multiplier.dart | 2 +- .../chapter_8/counter_interface.dart | 4 +- doc/tutorials/chapter_8/oven_fsm.dart | 2 +- .../rohd_vf_example/lib/rohd_vf_example.dart | 2 +- .../chapter_9/rohd_vf_example/pubspec.yaml | 8 +- example/example.dart | 4 +- example/fir_filter.dart | 4 +- example/logic_array.dart | 4 +- example/oven_fsm.dart | 2 +- example/tree.dart | 2 +- lib/rohd.dart | 1 + lib/src/diagnostics/waveform_service.dart | 214 +++++++----------- lib/src/module.dart | 1 + .../systemverilog/sv_service.dart | 54 +---- .../utilities/synth_module_definition.dart | 13 +- .../synth_sub_module_instantiation.dart | 9 +- lib/src/utilities/namer.dart | 33 ++- lib/src/utilities/simcompare.dart | 2 +- lib/src/wave_dumper.dart | 7 +- test/array_collapsing_test.dart | 12 +- test/bus_test.dart | 4 +- test/collapse_test.dart | 2 +- test/config_test.dart | 2 +- test/counter_wintf_test.dart | 2 +- test/external_test.dart | 2 +- test/fsm_test.dart | 6 +- test/gate_test.dart | 2 +- test/inout_loopback_test.dart | 10 +- test/instance_signal_name_collision_test.dart | 87 ------- test/logic_array_test.dart | 21 +- test/logic_name_config_test.dart | 22 +- test/logic_name_test.dart | 20 +- test/logic_structure_test.dart | 2 +- test/math_test.dart | 4 +- test/module_merging_test.dart | 4 +- test/module_test.dart | 15 +- test/multimodule4_test.dart | 2 +- test/multimodule5_test.dart | 2 +- test/name_test.dart | 8 +- test/naming_cases_test.dart | 2 +- test/naming_consistency_test.dart | 26 ++- test/naming_namespace_test.dart | 8 +- test/net_bus_test.dart | 53 ++--- test/net_test.dart | 14 +- test/pair_interface_hier_test.dart | 2 +- test/pair_interface_hier_w_modify_test.dart | 2 +- test/pair_interface_test.dart | 2 +- test/provider_consumer_test.dart | 2 +- test/provider_consumer_w_modify_test.dart | 2 +- test/sequential_test.dart | 2 +- test/sv_gen_test.dart | 32 +-- test/sv_param_passthrough_test.dart | 2 +- test/swizzle_test.dart | 16 +- test/typed_port_test.dart | 10 +- test/wave_dumper_test.dart | 7 +- 73 files changed, 364 insertions(+), 462 deletions(-) delete mode 100644 test/instance_signal_name_collision_test.dart diff --git a/README.md b/README.md index b812acc2f..5b3606b40 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ You can also open this repository in a GitHub Codespace to run the example in yo - Easy **IP integration** and **interfaces**; using an IP is as easy as an import. Reduces tedious, redundant, and error prone aspects of integration - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer -- Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform capture service** to .vcd file format +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption via **SvService** - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/benchmark/many_submodules_benchmark.dart b/benchmark/many_submodules_benchmark.dart index 763261a4c..277170143 100644 --- a/benchmark/many_submodules_benchmark.dart +++ b/benchmark/many_submodules_benchmark.dart @@ -33,7 +33,7 @@ class ManySubmodulesBenchmark extends AsyncBenchmarkBase { Future run() async { final dut = ManySubmodulesModule(Logic(), numSubModules: 10000); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; } } diff --git a/benchmark/wave_dump_benchmark.dart b/benchmark/wave_dump_benchmark.dart index 777b42eb0..1adfbdbc1 100644 --- a/benchmark/wave_dump_benchmark.dart +++ b/benchmark/wave_dump_benchmark.dart @@ -56,7 +56,7 @@ class WaveDumpBenchmark extends AsyncBenchmarkBase { _mod = _ModuleToDump(Logic(), _clk); await _mod.build(); - WaveDumper(_mod, outputPath: _vcdTemporaryPath); + WaveformService(_mod, outputPath: _vcdTemporaryPath); await Simulator.run(); diff --git a/doc/tutorials/chapter_2/helper.dart b/doc/tutorials/chapter_2/helper.dart index ecd0b5d98..af1dc6eaf 100644 --- a/doc/tutorials/chapter_2/helper.dart +++ b/doc/tutorials/chapter_2/helper.dart @@ -13,7 +13,8 @@ import 'package:rohd/rohd.dart'; Future displaySystemVerilog(Module mod) async { await mod.build(); - print('\nYour System Verilog Equivalent Code: \n ${mod.generateSynth()}'); + print('\nYour System Verilog Equivalent Code: \n' + '${SvService(mod).synthOutput}'); } class LogicInitialization extends Module { diff --git a/doc/tutorials/chapter_3/answers/exercise_sv.dart b/doc/tutorials/chapter_3/answers/exercise_sv.dart index b4ead1f2c..fce71a6fe 100644 --- a/doc/tutorials/chapter_3/answers/exercise_sv.dart +++ b/doc/tutorials/chapter_3/answers/exercise_sv.dart @@ -33,7 +33,7 @@ void main() async { await fSub.build(); // ignore: avoid_print - print(fSub.generateSynth()); + print(SvService(fSub).synthOutput); test('should return 0 when a and b equal 1', () async { a.put(1); diff --git a/doc/tutorials/chapter_3/full_adder.dart b/doc/tutorials/chapter_3/full_adder.dart index 5e798a69d..27f82896c 100644 --- a/doc/tutorials/chapter_3/full_adder.dart +++ b/doc/tutorials/chapter_3/full_adder.dart @@ -77,5 +77,5 @@ void main() async { final mod = FullAdderModule(a, b, cIn, faOps); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); } diff --git a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart index b324d4201..a3c7c8bf2 100644 --- a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart @@ -10,7 +10,7 @@ void main() async { final mod = NBitAdder(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 255 when both inputs are added', () { a.put(127); diff --git a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart index c4d3137db..a2716e6f8 100644 --- a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart @@ -9,7 +9,7 @@ void main() async { final mod = NBitSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 5 when a is 25 and b is 20', () { a.put(25); diff --git a/doc/tutorials/chapter_4/basic_generation_sv.dart b/doc/tutorials/chapter_4/basic_generation_sv.dart index 4c3ff86c0..8e23637b2 100644 --- a/doc/tutorials/chapter_4/basic_generation_sv.dart +++ b/doc/tutorials/chapter_4/basic_generation_sv.dart @@ -78,7 +78,7 @@ void main() async { await nbitAdder.build(); - print(nbitAdder.generateSynth()); + print(SvService(nbitAdder).synthOutput); test('should return 10 when both inputs are 5.', () async { a.put(5); diff --git a/doc/tutorials/chapter_5/answers/full_adder.dart b/doc/tutorials/chapter_5/answers/full_adder.dart index b7b54d5ed..f994a9387 100644 --- a/doc/tutorials/chapter_5/answers/full_adder.dart +++ b/doc/tutorials/chapter_5/answers/full_adder.dart @@ -49,7 +49,7 @@ void main() async { final mod = FullAdder(a: a, b: b, carryIn: cIn); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return true if result sum similar to truth table.', () async { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/full_subtractor.dart b/doc/tutorials/chapter_5/answers/full_subtractor.dart index 162b2d72c..bf4e8acf0 100644 --- a/doc/tutorials/chapter_5/answers/full_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/full_subtractor.dart @@ -44,7 +44,7 @@ Future main() async { await diff.build(); - print(diff.generateSynth()); + print(SvService(diff).synthOutput); test('should return true if results matched truth table', () async { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart index 2f12ab6fe..224999e5e 100644 --- a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart @@ -36,7 +36,7 @@ Future main() async { final mod = NBitFullSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 1 when a is 8 and b is 7.', () { a.put(8); diff --git a/doc/tutorials/chapter_5/n_bit_adder.dart b/doc/tutorials/chapter_5/n_bit_adder.dart index 8a4a61944..9eb0c1a63 100644 --- a/doc/tutorials/chapter_5/n_bit_adder.dart +++ b/doc/tutorials/chapter_5/n_bit_adder.dart @@ -79,7 +79,7 @@ void main() async { await nbitAdder.build(); - // print(nbitAdder.generateSynth()); + // print(SvService(nbitAdder).synthOutput); test('should return 20 when A and B perform add.', () async { a.put(15); diff --git a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart index 15395d7ec..bd63338bf 100644 --- a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart +++ b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart @@ -44,7 +44,7 @@ Future main() async { final dff = DFlipFlop(data, reset, clk); await dff.build(); - print(dff.generateSynth()); + print(SvService(dff).synthOutput); data.inject(1); reset.inject(1); @@ -60,7 +60,7 @@ Future main() async { unawaited(Simulator.run()); - WaveDumper(dff, + WaveformService(dff, outputPath: 'doc/tutorials/chapter_7/answers/d_flip_flop.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_7/shift_register.dart b/doc/tutorials/chapter_7/shift_register.dart index 1dc98c4f1..2324a860b 100644 --- a/doc/tutorials/chapter_7/shift_register.dart +++ b/doc/tutorials/chapter_7/shift_register.dart @@ -69,7 +69,7 @@ void main() async { // kick-off the simulator, but we don't want to wait unawaited(Simulator.run()); - WaveDumper(shiftReg, + WaveformService(shiftReg, outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart index d8315156d..2cbc89916 100644 --- a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart +++ b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart @@ -139,7 +139,7 @@ void main() async { await tb.build(); - print(tb.generateSynth()); + print(SvService(tb).synthOutput); testInterface.cs.inject(0); testInterface.sdi.inject(0); @@ -163,7 +163,7 @@ void main() async { Simulator.setMaxSimTime(100); unawaited(Simulator.run()); - WaveDumper(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); + WaveformService(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); await drive(LogicValue.ofString('01010101')); } diff --git a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart index 6912f1313..26d18cd16 100644 --- a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart +++ b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart @@ -49,13 +49,13 @@ Future main(List args) async { final toyCap = ToyCapsuleFSM(clk, reset, dispenseBtn, coin); await toyCap.build(); - print(toyCap.generateSynth()); + print(SvService(toyCap).synthOutput); toyCap.toyCapsuleStateMachine.generateDiagram(); reset.inject(1); - WaveDumper(toyCap, outputPath: 'toyCapsuleFSM.vcd'); + WaveformService(toyCap, outputPath: 'toyCapsuleFSM.vcd'); Simulator.setMaxSimTime(100); Simulator.registerAction(25, () { diff --git a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart index ef93701b3..2a0350698 100644 --- a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart +++ b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart @@ -34,14 +34,14 @@ void main(List args) async { final pipe = Pipeline4Stages(clk, reset, a); await pipe.build(); - // print(pipe.generateSynth()); + // print(SvService(pipe).synthOutput); a.inject(5); reset.inject(1); Simulator.registerAction(10, () => reset.put(0)); - WaveDumper(pipe, outputPath: 'answer_1.vcd'); + WaveformService(pipe, outputPath: 'answer_1.vcd'); Simulator.registerAction(50, () async { // stage 4 / result: 30 + (30 * 3) = 120 diff --git a/doc/tutorials/chapter_8/carry_save_multiplier.dart b/doc/tutorials/chapter_8/carry_save_multiplier.dart index 82b9521da..783e54f6f 100644 --- a/doc/tutorials/chapter_8/carry_save_multiplier.dart +++ b/doc/tutorials/chapter_8/carry_save_multiplier.dart @@ -108,7 +108,7 @@ void main() async { reset.inject(1); // Attach a waveform dumper so we can see what happens. - WaveDumper(csm, outputPath: 'csm.vcd'); + WaveformService(csm, outputPath: 'csm.vcd'); Simulator.registerAction(10, () { reset.inject(0); diff --git a/doc/tutorials/chapter_8/counter_interface.dart b/doc/tutorials/chapter_8/counter_interface.dart index 41a49eb0e..6d5261620 100644 --- a/doc/tutorials/chapter_8/counter_interface.dart +++ b/doc/tutorials/chapter_8/counter_interface.dart @@ -63,9 +63,9 @@ Future main() async { await counter.build(); - print(counter.generateSynth()); + print(SvService(counter).synthOutput); - WaveDumper(counter, + WaveformService(counter, outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); Simulator.registerAction(25, () { intf.en.put(1); diff --git a/doc/tutorials/chapter_8/oven_fsm.dart b/doc/tutorials/chapter_8/oven_fsm.dart index 172d83006..01f653198 100644 --- a/doc/tutorials/chapter_8/oven_fsm.dart +++ b/doc/tutorials/chapter_8/oven_fsm.dart @@ -192,7 +192,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); + WaveformService(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); } if (!noPrint) { diff --git a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart index 4b1ef9c34..77d2a5a32 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart +++ b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart @@ -315,7 +315,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + WaveformService(tb.counter); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml b/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml index e763ab748..27d99489a 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml +++ b/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml @@ -8,10 +8,14 @@ environment: # Add regular dependencies here. dependencies: - rohd: ^0.4.2 - rohd_vf: ^0.4.1 + rohd: ^0.6.0 + rohd_vf: ^0.6.0 logging: ^1.0.1 +dependency_overrides: + rohd: + path: ../../../../ + dev_dependencies: lints: ^2.0.0 test: ^1.21.0 diff --git a/example/example.dart b/example/example.dart index 2ddbfc738..d2c2ee81d 100644 --- a/example/example.dart +++ b/example/example.dart @@ -61,7 +61,7 @@ Future main({bool noPrint = false}) async { // Let's see what this module looks like as SystemVerilog, so we can pass it // to other tools. - final systemVerilogCode = counter.generateSynth(); + final systemVerilogCode = SvService(counter).synthOutput; if (!noPrint) { print(systemVerilogCode); } @@ -70,7 +70,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(counter); + WaveformService(counter); } // Let's also print a message every time the value on the counter changes, diff --git a/example/fir_filter.dart b/example/fir_filter.dart index 1f17f0d3d..d16168552 100644 --- a/example/fir_filter.dart +++ b/example/fir_filter.dart @@ -96,7 +96,7 @@ Future main({bool noPrint = false}) async { await firFilter.build(); // Generate SystemVerilog code. - final systemVerilogCode = firFilter.generateSynth(); + final systemVerilogCode = SvService(firFilter).synthOutput; if (!noPrint) { // Print SystemVerilog code to console. print(systemVerilogCode); @@ -108,7 +108,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper. if (!noPrint) { - WaveDumper(firFilter); + WaveformService(firFilter); } // Let's set the initial setting. diff --git a/example/logic_array.dart b/example/logic_array.dart index f772c4929..0b149717e 100644 --- a/example/logic_array.dart +++ b/example/logic_array.dart @@ -58,14 +58,14 @@ Future main({bool noPrint = false}) async { // Build the module await logicArrayExample.build(); - final systemVerilogCode = logicArrayExample.generateSynth(); + final systemVerilogCode = SvService(logicArrayExample).synthOutput; if (!noPrint) { print(systemVerilogCode); } // Simulate the module if (!noPrint) { - WaveDumper(logicArrayExample); + WaveformService(logicArrayExample); } // Set the input values diff --git a/example/oven_fsm.dart b/example/oven_fsm.dart index 2788baa55..50c30197e 100644 --- a/example/oven_fsm.dart +++ b/example/oven_fsm.dart @@ -225,7 +225,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'oven.vcd'); + WaveformService(oven, outputPath: 'oven.vcd'); } // Kick off the simulation. diff --git a/example/tree.dart b/example/tree.dart index f5c30a979..12eb86a63 100644 --- a/example/tree.dart +++ b/example/tree.dart @@ -85,7 +85,7 @@ Future main({bool noPrint = false}) async { // Below will generate an output of the ROHD-generated SystemVerilog: await tree.build(); - final generatedSystemVerilog = tree.generateSynth(); + final generatedSystemVerilog = SvService(tree).synthOutput; if (!noPrint) { print(generatedSystemVerilog); } diff --git a/lib/rohd.dart b/lib/rohd.dart index 1a6cf43b6..d110c49e5 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -14,6 +14,7 @@ export 'src/signals/signals.dart'; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; +export 'src/synthesizers/systemverilog/sv_service.dart'; export 'src/utilities/naming.dart'; export 'src/values/values.dart'; export 'src/wave_dumper.dart'; diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index f165fc8b0..336377e36 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -42,110 +42,15 @@ enum OverwritePolicy { failIfExists, } -// ─── Options ───────────────────────────────────────────────────────────────── - -/// Configuration for [WaveformService]. -/// -/// Example — capture only ports, write FST, fail if file exists: -/// ```dart -/// final svc = WaveformService( -/// dut, -/// options: WaveformServiceOptions( -/// outputPath: 'build/waves.fst', -/// format: WaveOutputFormat.fst, -/// signalFilter: (sig) => sig.isPort, -/// overwritePolicy: OverwritePolicy.failIfExists, -/// ), -/// ); -/// ``` -class WaveformServiceOptions { - /// Path of the output waveform file. - /// - /// The parent directory is created if necessary. - /// Defaults to `'waves.vcd'`. - final String outputPath; - - /// Output format. Defaults to [WaveOutputFormat.vcd]. - final WaveOutputFormat format; - - /// Optional predicate that determines whether a given [Logic] signal is - /// captured. - /// - /// When `null`, all non-[Const] signals in the hierarchy are captured - /// (matching the existing [WaveDumper] behaviour). - final bool Function(Logic signal)? signalFilter; - - /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. - /// - /// Defaults to `'1ps'`. - final String timescale; - - /// Simulation time at which recording begins. - /// - /// Signals are still collected before this time so they appear in the - /// scope definition, but value-change events are suppressed until - /// [startTime] is reached. `null` means "from the very start". - final int? startTime; - - /// Simulation time at which recording ends. - /// - /// Value-change events after this time are suppressed. - /// `null` means "until end of simulation". - final int? stopTime; - - /// Number of characters accumulated in the write buffer before it is - /// flushed to disk. - /// - /// Larger values reduce I/O syscalls but increase peak memory usage. - /// Defaults to `100000`. - final int flushBufferSize; - - /// What to do when the output file already exists. - /// - /// Defaults to [OverwritePolicy.overwrite]. - final OverwritePolicy overwritePolicy; - - /// Whether to register this service with [ModuleServices] for inspection. - /// - /// Defaults to `true`. - final bool register; - - /// Whether to enable DevTools streaming. - /// - /// When `true`, the service (or a subclass) may attach VM service - /// extensions and in-memory signal tracking for live DevTools access. - /// - /// The base [WaveformService] stores this flag but takes no action on it. - /// The DevTools subclass uses it to conditionally register extensions. - /// - /// Defaults to `false`. - final bool enableDevToolsStreaming; - - /// Creates [WaveformServiceOptions]. - const WaveformServiceOptions({ - this.outputPath = 'waves.vcd', - this.format = WaveOutputFormat.vcd, - this.signalFilter, - this.timescale = '1ps', - this.startTime, - this.stopTime, - this.flushBufferSize = 100000, - this.overwritePolicy = OverwritePolicy.overwrite, - this.register = true, - this.enableDevToolsStreaming = false, - }); -} - // ─── Service ───────────────────────────────────────────────────────────────── /// A waveform capture service that writes signal changes to a file. /// /// This is the base class for waveform capture. It handles: -/// - Signal collection (with optional [WaveformServiceOptions.signalFilter]) -/// - VCD file output with configurable [WaveformServiceOptions.timescale] -/// - Selective recording via [WaveformServiceOptions.startTime] / -/// [WaveformServiceOptions.stopTime] -/// - Periodic buffer flushing and [WaveformServiceOptions.overwritePolicy] +/// - Signal collection (with optional [signalFilter]) +/// - VCD file output with configurable [timescale] +/// - Selective recording via [startTime] / [stopTime] +/// - Periodic buffer flushing and [overwritePolicy] /// - Optional registration with [ModuleServices] /// /// **Subclassing for DevTools streaming:** @@ -156,8 +61,8 @@ class WaveformServiceOptions { /// - [onSignalCollected] — called once per tracked signal at startup; use /// it to register signals in a VM-service index. /// - [onValueChange] — called for every value-change event within the -/// [WaveformServiceOptions.startTime]/[WaveformServiceOptions.stopTime] -/// window; use it to feed an in-memory store for streaming. +/// [startTime]/[stopTime] window; use it to feed an in-memory store for +/// streaming. /// - [onTimestampCapture] — called once per simulation timestamp that /// contains at least one change; the full changed-signal set is passed. /// - [onSimulationEnd] — called after the final timestamp is written and @@ -166,7 +71,7 @@ class WaveformServiceOptions { /// Example subclass skeleton: /// ```dart /// class DevToolsWaveformService extends WaveformService { -/// DevToolsWaveformService(super.module, {super.options}); +/// DevToolsWaveformService(super.module, {super.outputPath}); /// /// @override /// void onSignalCollected(Logic signal) { @@ -185,8 +90,52 @@ class WaveformService { /// The top-level [Module] being captured. final Module module; - /// The options controlling this service. - final WaveformServiceOptions options; + /// Path of the output waveform file. + /// + /// The parent directory is created if necessary. + final String outputPath; + + /// Output format. + final WaveOutputFormat format; + + /// Optional predicate that determines whether a given [Logic] signal is + /// captured. + /// + /// When `null`, all non-[Const] signals in the hierarchy are captured, + /// matching the legacy waveform dumper behaviour. + final bool Function(Logic signal)? signalFilter; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Simulation time at which recording begins. + /// + /// Signals are still collected before this time so they appear in the scope + /// definition, but value-change events are suppressed until [startTime] is + /// reached. `null` means "from the very start". + final int? startTime; + + /// Simulation time at which recording ends. + /// + /// Value-change events after this time are suppressed. `null` means "until + /// end of simulation". + final int? stopTime; + + /// Number of characters accumulated in the write buffer before it is flushed + /// to disk. + final int flushBufferSize; + + /// What to do when the output file already exists. + final OverwritePolicy overwritePolicy; + + /// Whether to register this service with [ModuleServices] for inspection. + final bool register; + + /// Whether to enable DevTools streaming. + /// + /// The base [WaveformService] stores this flag but takes no action on it. + /// The DevTools subclass uses it to conditionally register extensions. + final bool enableDevToolsStreaming; // ─── Internal file-writing state ───────────────────────────── @@ -196,8 +145,7 @@ class WaveformService { /// Sink writing into [_outputFile]. late final IOSink _outFileSink; - /// Write buffer; flushed when it exceeds - /// [WaveformServiceOptions.flushBufferSize]. + /// Write buffer; flushed when it exceeds [flushBufferSize]. final StringBuffer _fileBuffer = StringBuffer(); /// Counter for assigning compact signal markers in the VCD. @@ -218,26 +166,37 @@ class WaveformService { /// /// [module] must be built before construction. /// - /// Provide [options] to configure format, path, filtering, timescale, - /// start/stop times, flush size, and overwrite policy. - WaveformService(this.module, - {this.options = const WaveformServiceOptions()}) { + /// Use the optional constructor parameters to configure format, path, + /// filtering, timescale, start/stop times, flush size, and overwrite policy. + WaveformService( + this.module, { + this.outputPath = 'waves.vcd', + this.format = WaveOutputFormat.vcd, + this.signalFilter, + this.timescale = '1ps', + this.startTime, + this.stopTime, + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + this.register = true, + this.enableDevToolsStreaming = false, + }) { if (!module.hasBuilt) { throw Exception('Module must be built before creating WaveformService. ' 'Call build() first.'); } - if (options.overwritePolicy == OverwritePolicy.failIfExists) { - final f = File(options.outputPath); + if (overwritePolicy == OverwritePolicy.failIfExists) { + final f = File(outputPath); if (f.existsSync()) { throw FileSystemException( 'Waveform output file already exists and overwritePolicy is ' 'failIfExists.', - options.outputPath); + outputPath); } } - _outputFile = File(options.outputPath)..createSync(recursive: true); + _outputFile = File(outputPath)..createSync(recursive: true); _outFileSink = _outputFile.openWrite(); _collectSignals(); @@ -259,7 +218,7 @@ class WaveformService { onSimulationEnd(); }); - if (options.register) { + if (register) { ModuleServices.instance.waveformService = this; } } @@ -267,7 +226,7 @@ class WaveformService { // ─── Extensibility hooks ────────────────────────────────────── /// Called once for each [Logic] signal that passes - /// [WaveformServiceOptions.signalFilter] during initial signal collection. + /// [signalFilter] during initial signal collection. /// /// Override in a subclass to register signals with an in-memory store, /// VM service index, or FST handle map. Always call `super` first. @@ -276,8 +235,7 @@ class WaveformService { /// Called for every value-change event on [signal] at [timestamp]. /// - /// Only called within the [WaveformServiceOptions.startTime] / - /// [WaveformServiceOptions.stopTime] window. + /// Only called within the [startTime] / [stopTime] window. /// /// Override in a subclass to feed an in-memory waveform store or /// streaming buffer. Always call `super` first. @@ -311,7 +269,7 @@ class WaveformService { if (sig is Const) { continue; } - if (options.signalFilter != null && !options.signalFilter!(sig)) { + if (signalFilter != null && !signalFilter!(sig)) { continue; } @@ -345,7 +303,7 @@ class WaveformService { \$comment Generated by ROHD - www.github.com/intel/rohd \$end -\$timescale ${options.timescale} \$end +\$timescale $timescale \$end '''; _writeToBuffer(header); } @@ -391,10 +349,10 @@ class WaveformService { } bool _isInRecordingWindow(int timestamp) { - if (options.startTime != null && timestamp < options.startTime!) { + if (startTime != null && timestamp < startTime!) { return false; } - if (options.stopTime != null && timestamp > options.stopTime!) { + if (stopTime != null && timestamp > stopTime!) { return false; } return true; @@ -434,7 +392,7 @@ class WaveformService { void _writeToBuffer(String contents) { _fileBuffer.write(contents); - if (_fileBuffer.length > options.flushBufferSize) { + if (_fileBuffer.length > flushBufferSize) { _flushBuffer(); } } @@ -454,11 +412,11 @@ class WaveformService { /// Returns a JSON-serialisable summary of this service. Map toJson() => { - 'outputPath': options.outputPath, - 'format': options.format.name, + 'outputPath': outputPath, + 'format': format.name, 'signalCount': _signalToMarkerMap.length, - 'timescale': options.timescale, - if (options.startTime != null) 'startTime': options.startTime!, - if (options.stopTime != null) 'stopTime': options.stopTime!, + 'timescale': timescale, + if (startTime != null) 'startTime': startTime!, + if (stopTime != null) 'stopTime': stopTime!, }; } diff --git a/lib/src/module.dart b/lib/src/module.dart index 56bb344b4..c045e0a48 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1137,6 +1137,7 @@ abstract class Module { /// /// Deprecated: use [SvService] instead, which provides richer access to /// per-module file contents, named maps, and individual file writing. + /// For the legacy one-shot API, prefer [SvService.synthOutput]. @Deprecated('Use SvService instead.') String generateSynth() { if (!_hasBuilt) { diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart index 8b8cb47d2..84e046e9f 100644 --- a/lib/src/synthesizers/systemverilog/sv_service.dart +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -14,36 +14,6 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/timestamper.dart'; -/// Configuration for constructing an [SvService]. -/// -/// Example — write a single concatenated `.sv` file: -/// ```dart -/// final sv = SvService( -/// dut, -/// options: SvServiceOptions(outputPath: 'build/out.sv'), -/// ); -/// ``` -/// -/// For writing one file per module definition, call [SvService.writeFiles] -/// after construction. -class SvServiceOptions { - /// Whether to register this service with [ModuleServices]. - final bool register; - - /// Optional path where the single-file SV output should be written. - /// - /// If non-null, the concatenated SV output (with generation header) is - /// written to this file on construction. The parent directory is created - /// if needed. Use [SvService.writeFiles] for per-module file output. - final String? outputPath; - - /// Creates [SvService] options. - const SvServiceOptions({ - this.register = true, - this.outputPath, - }); -} - /// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. /// /// Provides access to the generated SV file contents and per-module @@ -68,6 +38,7 @@ class SvService { /// /// Matches the format historically produced by `Module.generateSynth()`. static const moduleSeparator = '\n\n////////////////////\n\n'; + /// The top-level [Module] being synthesized. final Module module; @@ -81,35 +52,24 @@ class SvService { /// /// [module] must already be built. /// - /// You can provide [options] to control registration and output path. - /// For backward compatibility, [register] and [outputPath] can still be - /// provided directly; those values override [options] when specified. - /// - /// If an output path is provided (via [outputPath] or `options.outputPath`), - /// the concatenated SV output (with header) is written to that file. - /// The parent directory is created if needed. - SvService(this.module, - {SvServiceOptions options = const SvServiceOptions(), - bool? register, - String? outputPath}) { + /// If [outputPath] is provided, the concatenated SV output (with header) is + /// written to that file. The parent directory is created if needed. + SvService(this.module, {bool register = true, String? outputPath}) { if (!module.hasBuilt) { throw Exception('Module must be built before creating SvService. ' 'Call build() first.'); } - final effectiveRegister = register ?? options.register; - final effectiveOutputPath = outputPath ?? options.outputPath; - synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); fileContents = synthBuilder.getSynthFileContents(); - if (effectiveOutputPath != null) { - final file = File(effectiveOutputPath); + if (outputPath != null) { + final file = File(outputPath); file.parent.createSync(recursive: true); file.writeAsStringSync(synthOutput); } - if (effectiveRegister) { + if (register) { ModuleServices.instance.svService = this; } } diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 38a207b74..55794d3d1 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -25,19 +25,21 @@ class _BusSubsetForStructSlice extends BusSubset { /// [_BusSubsetForStructSlice] is created on every synthesis pass, its /// canonical instance name is memoized against the persistent destination /// signal and therefore does not drift run-to-run. + final Logic _destination; /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. - _BusSubsetForStructSlice( - super.bus, - super.startIndex, - super.endIndex, - ) : super(name: 'struct_slice'); + _BusSubsetForStructSlice(super.bus, super.startIndex, super.endIndex, + {required Logic destination}) + : _destination = destination, + super(name: 'struct_slice'); // we override this since it's added post-build @override bool get hasBuilt => true; + @override + Object get instanceNameKey => _destination; } /// Represents the definition of a module. @@ -273,6 +275,7 @@ class SynthModuleDefinition { width: port.width, name: 'DUMMY'), idx, idx + leafElement.width - 1, + destination: leafElement, ); final ssmi = getSynthSubModuleInstantiation(subsetMod); diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 6c711c330..cc553a24c 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -27,14 +27,13 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// /// Names are allocated from [parentModule]'s [Namer]'s shared namespace - /// via [Namer.allocateName]. + /// via [Namer.instanceNameOf], which memoizes by [Module.instanceNameKey] so + /// the same instance receives an identical canonical name across repeated + /// synthesis passes. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.allocateName( - module.uniqueInstanceName, - reserved: module.reserveName, - ); + _name = parentModule.namer.instanceNameOf(module); } /// A mapping of input port name to [SynthLogic]. diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 3afc9d873..6b7ca04d4 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -21,7 +21,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// /// Port names are reserved at construction time. Internal signal names /// are assigned lazily on the first [signalNameOf] call. Instance names -/// are allocated explicitly via [allocateName]. +/// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { // ─── Shared namespace ─────────────────────────────────────────── @@ -32,6 +32,14 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; + /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. + /// + /// Allocating an instance name mutates [_uniquifier], so without this cache a + /// second synthesis pass over the same already-built module hierarchy would + /// re-allocate names and drift numeric suffixes. Caching by the stable + /// [Module.instanceNameKey] keeps instance names canonical across repeated + /// synthesizer runs. + final Map _instanceNames = {}; /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; @@ -81,6 +89,29 @@ class Namer { reserved: reserved, ); + // ─── Instance naming (Module → String) ────────────────────────── + + /// Returns the canonical instance name for [submodule]. + /// + /// The first call for a stable [Module.instanceNameKey] allocates a + /// collision-free name in the shared namespace. Subsequent calls return the + /// cached result so repeated synthesis passes produce consistent instance + /// names. + String instanceNameOf(Module submodule) { + final key = submodule.instanceNameKey; + final cached = _instanceNames[key]; + if (cached != null) { + return cached; + } + + final name = _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(submodule.uniqueInstanceName), + reserved: submodule.reserveName, + ); + _instanceNames[key] = name; + return name; + } + // ─── Signal naming (Logic → String) ───────────────────────────── /// Returns the canonical name for [logic]. diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..6fb1aa569 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -342,7 +342,7 @@ abstract class SimCompare { allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); final moduleInstance = '$topModule dut($moduleConnections);'; final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = module.generateSynth(); + final generatedVerilog = SvService(module).synthOutput; // so that when they run in parallel, they dont step on each other final uniqueId = diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 3a37e55ea..9948d47fb 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -17,11 +17,14 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// A waveform dumper for simulations. /// -/// Outputs to vcd format at [outputPath]. [module] must be built prior to +/// Deprecated: use [WaveformService] instead. +/// +/// Outputs to VCD format at [outputPath]. [module] must be built prior to /// attaching the [WaveDumper]. /// /// The waves will only dump to the file periodically and then once the /// simulation has completed. +@Deprecated('Use WaveformService instead') class WaveDumper { /// The [Module] being dumped. final Module module; @@ -58,6 +61,8 @@ class WaveDumper { /// Attaches a [WaveDumper] to record all signal changes in a simulation of /// [module] in a VCD file at [outputPath]. + /// @Deprecated: use [WaveformService] instead. + @Deprecated('Use WaveformService instead') WaveDumper(this.module, {this.outputPath = 'waves.vcd'}) : _outputFile = File(outputPath)..createSync(recursive: true) { if (!module.hasBuilt) { diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 3a4c6b74e..e3c1f1c78 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -97,7 +97,7 @@ void main() { test('simple 1d collapse', () async { final mod = SimpleLAPassthrough(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign laOut = laIn;')); }); @@ -105,7 +105,7 @@ void main() { test('array collapse for cross-module connection', () async { final mod = ArrayTopMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(RegExp(r'ArraySubModIn.*\.inp\(inp\)'))); expect(sv, contains(RegExp(r'ArraySubModOut.*\.arrOut\(inp\)'))); @@ -116,7 +116,7 @@ void main() { LogicArray([3, 3], 1), LogicArray([3, 3], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -134,7 +134,7 @@ void main() { final mod = ArrayWithShuffledAssignment(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).allContents; expect(sv, contains('assign b[0] = a[3];')); expect(sv, contains('assign b[3] = a[0];')); @@ -151,7 +151,7 @@ void main() { LogicArray([3, 3], 1, numUnpackedDimensions: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -168,7 +168,7 @@ void main() { final mod = ArrayModule(LogicArray([4, 4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign d = c[0];')); expect(sv, contains('assign b = a;')); diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..7fbd7f44b 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -228,7 +228,7 @@ void main() { final mod = SingleBitBusSubsetMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign result = oneBit')); final vectors = [ @@ -401,7 +401,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains("assign const_subset = 16'habcd;"), true); }); }); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..70d6ff73d 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -57,7 +57,7 @@ void main() { test('collapse pretty', () async { final mod = CollapseTestModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure e=a&b&c is in there, to prove there was some inlining expect(sv, contains(RegExp('e.*=.*a.*&.*b.*&.*c'))); diff --git a/test/config_test.dart b/test/config_test.dart index 28cd2e7d8..1730ab77a 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -46,7 +46,7 @@ void main() async { final mod = SimpleModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(version)); }); diff --git a/test/counter_wintf_test.dart b/test/counter_wintf_test.dart index 7889369cc..91986aea2 100644 --- a/test/counter_wintf_test.dart +++ b/test/counter_wintf_test.dart @@ -145,7 +145,7 @@ void main() { test('interface ports dont get doubled up', () async { final mod = Counter(CounterInterface(8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(!sv.contains('en_0'), true); }); diff --git a/test/external_test.dart b/test/external_test.dart index 09ac84c87..3ba33c649 100644 --- a/test/external_test.dart +++ b/test/external_test.dart @@ -31,7 +31,7 @@ void main() { test('instantiate', () async { final mod = TopModule(Logic(width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure we instantiate the external module properly expect( diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..b4e879403 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -183,7 +183,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("b = 1'h0;")); }); @@ -192,7 +192,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('priority case')); }); @@ -201,7 +201,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('MyStates_state1 : begin')); }); diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..4fabc3074 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -537,7 +537,7 @@ void main() { final gtm = ShiftTestModule(Logic(width: 3), Logic(width: 8), constant: 0); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = SvService(gtm).synthOutput; expect(sv, isNot(contains("0'h0"))); diff --git a/test/inout_loopback_test.dart b/test/inout_loopback_test.dart index 0c3b5b343..b2fe711b2 100644 --- a/test/inout_loopback_test.dart +++ b/test/inout_loopback_test.dart @@ -237,7 +237,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The outer module should NOT contain an internal net_connect // for the loopback — the submodule ports should just be wired to the @@ -268,7 +268,7 @@ void main() { final mod = SimpleOuterLoopback(LogicNet(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; final outerModuleSv = _extractModuleSv(sv, 'simpleOuter'); expect(outerModuleSv, isNot(contains('net_connect')), @@ -284,7 +284,7 @@ void main() { final mod = LoopbackPairTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Check for net_connect in the top module final topModuleSv = _extractModuleSv(sv, 'LoopbackPairTop'); @@ -309,7 +309,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The inner module SHOULD have a net_connect (connecting ioA <= ioB). final innerModuleSv = _extractModuleSv(sv, 'innerConnected'); @@ -347,7 +347,7 @@ void main() { final mod = OuterClkLoopback(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The outer module should NOT have a net_connect — the loopback net // is only used as port connections in the inner instantiation. diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart deleted file mode 100644 index 65747204a..000000000 --- a/test/instance_signal_name_collision_test.dart +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// instance_signal_name_collision_test.dart -// Tests that submodule instance names and signal names share a single -// namespace, so a collision between them results in uniquification. -// -// 2026 April 18 -// Author: Desmond Kirkpatrick - -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:test/test.dart'; - -// ── Minimal repro modules ──────────────────────────────────────────────────── - -/// Leaf module whose default instance name is "inner". -class _Inner extends Module { - _Inner(Logic a) : super(name: 'inner') { - a = addInput('a', a, width: a.width); - addOutput('y', width: a.width) <= a; - } -} - -/// Parent module that: -/// • instantiates [_Inner] (default instance name: "inner") -/// • names an internal wire "inner" as well -/// -/// Because both identifiers live in a single shared namespace, one of them -/// will be suffixed to avoid collision. -class _CollidingParent extends Module { - _CollidingParent(Logic a) : super(name: 'colliding_parent') { - a = addInput('a', a, width: a.width); - - // Internal wire explicitly named "inner". - final inner = Logic(name: 'inner', width: a.width, naming: Naming.reserved) - ..gets(a); - - // Submodule whose uniqueInstanceName will also be "inner". - final sub = _Inner(inner); - - addOutput('y', width: a.width) <= sub.output('y'); - } -} - -// ── Test ───────────────────────────────────────────────────────────────────── - -void main() { - group('instance / signal name collision (shared namespace)', () { - late _CollidingParent mod; - late SynthModuleDefinition def; - - setUpAll(() async { - mod = _CollidingParent(Logic(width: 8)); - await mod.build(); - def = SynthModuleDefinition(mod); - }); - - test('internal signal named "inner" retains its exact name', () { - // The reserved signal should keep its exact name. - final sl = def.internalSignals.cast().firstWhere( - (s) => s!.logics.any((l) => l.name == 'inner'), - orElse: () => null, - ); - expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); - expect(sl!.name, 'inner', - reason: 'Reserved signal "inner" must keep its exact name'); - }); - - test( - 'submodule instance is uniquified because signal ' - '"inner" already claimed the name', () { - final inst = def.subModuleInstantiations - .where((s) => s.needsInstantiation) - .cast() - .firstWhere( - (s) => s!.module.name == 'inner', - orElse: () => null, - ); - expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); - // The instance should be suffixed since the signal took "inner" first. - expect(inst!.name, isNot('inner'), - reason: 'Instance should be uniquified when signal already ' - 'claims "inner"'); - }); - }); -} diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..f6d9ca784 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -751,7 +751,7 @@ void main() { ]; if (checkNoSwizzle) { - expect(mod.generateSynth().contains('swizzle'), false, + expect(SvService(mod).synthOutput.contains('swizzle'), false, reason: 'Expected no swizzles but found one.'); } @@ -800,7 +800,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains(RegExp(r'\[7:0\]\s*laIn\s*\[2:0\]')), true); expect(sv.contains(RegExp(r'\[7:0\]\s*laOut\s*\[2:0\]')), true); }); @@ -818,7 +818,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv.contains(RegExp( r'\[2:0\]\s*\[1:0\]\s*\[7:0\]\s*laIn\s*\[4:0\]\s*\[3:0\]')), @@ -846,7 +846,7 @@ void main() { await testArrayPassthrough(mod); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [2:0][1:0][2:0][7:0] laIn')); expect(sv, contains('output logic [2:0][1:0][2:0][7:0] laOut')); }); @@ -861,7 +861,7 @@ void main() { await testArrayPassthrough(mod, noSvSim: true); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [1:0][2:0][7:0] laIn [2:0]')); expect(sv, contains('output logic [1:0][2:0][7:0] laOut [2:0]')); }); @@ -928,7 +928,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('logic [2:0][3:0][7:0] intermediate [1:0]'), true); }); }); @@ -981,7 +981,7 @@ void main() { test('3d', () async { final mod = SimpleArraysAndHierarchy(LogicArray([2], 8)); await testArrayPassthrough(mod); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('SimpleLAPassthrough simple_la_passthrough')); }); @@ -992,7 +992,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - expect(mod.generateSynth(), contains('SimpleLAPassthrough')); + expect(SvService(mod).synthOutput, contains('SimpleLAPassthrough')); }); }); @@ -1001,7 +1001,7 @@ void main() { final mod = FancyArraysAndHierarchy(LogicArray([4, 3, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure the 4th one is there (since we expect 4) expect(sv, contains('SimpleLAPassthrough simple_la_passthrough_2')); @@ -1043,7 +1043,8 @@ void main() { final mod = WithSetArrayOffsetModule(LogicArray([2, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); // make sure we're reassigning both times it overlaps! expect( diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 1a5fb1d98..b2cc6c95d 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -30,7 +30,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('intermediate')); }); @@ -45,7 +45,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // no intermediate expect(sv.contains('intermediate'), isFalse); @@ -58,7 +58,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // just the ports expect('logic'.allMatches(sv).length, 3); @@ -71,7 +71,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // just the ports expect('logic'.allMatches(sv).length, 3); @@ -90,7 +90,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // held one sticks expect(sv, contains('intermediate_1 = in1')); @@ -108,7 +108,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -124,7 +124,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -145,7 +145,7 @@ void main() { out1 <= intermediate | intermediate2; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -167,7 +167,7 @@ void main() { out1 <= ~intermediatePost; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('goodname')); }); @@ -220,7 +220,7 @@ void main() { out1 <= ~prev; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains(expectedName), reason: 'Amongst ${l.map((e) => e.name).toList()},' @@ -235,7 +235,7 @@ void main() { intermediate <= in1; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('intermediate')); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 8ce9d5f40..457d3ed52 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -225,13 +225,13 @@ void main() { final mod = LogicWithInternalSignalModule(Logic()); await mod.build(); - expect(mod.generateSynth(), contains('shouldExist')); + expect(SvService(mod).synthOutput, contains('shouldExist')); }); test('unconnected port does not duplicate internal signal', () async { final pMod = ParentMod(Logic(), Logic()); await pMod.build(); - final sv = pMod.generateSynth(); + final sv = SvService(pMod).synthOutput; expect(RegExp('logic a[,;\n]').allMatches(sv).length, 2); }); @@ -239,7 +239,7 @@ void main() { test('assigns and gates', () async { final mod = SensitiveNaming(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('e = a & d')); expect(sv, contains('b = a')); expect(sv, contains('d = c')); @@ -248,7 +248,7 @@ void main() { test('bus subset', () async { final mod = BusSubsetNaming(Logic(width: 32)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('c = b[3]')); }); }); @@ -257,7 +257,7 @@ void main() { test('unconnected floating', () async { final mod = DrivenOutputModule(null); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // shouldn't add a Z in there if left floating expect(!sv.contains('z'), true); @@ -266,7 +266,7 @@ void main() { test('driven to z', () async { final mod = DrivenOutputModule(Const('z')); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // should add a Z if it's explicitly added expect(sv, contains('z')); @@ -279,7 +279,7 @@ void main() { portANaming: Naming.renameable, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -295,7 +295,7 @@ void main() { () async { final mod = NameCollisionArrayTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -312,7 +312,7 @@ void main() { await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('_wow_______')); }); @@ -321,7 +321,7 @@ void main() { final mod = StructElementNamingModule(VariousNamingStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign outp[0] = outp_renameable;')); expect(sv, contains('assign outp[1] = reserved_outp;')); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index fdc522e96..85c75468f 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -175,7 +175,7 @@ void main() { final mod = StructModuleWithInstrumentation(Const(0, width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('swizzle'), isFalse, reason: 'Should not pack from instrumentation!'); diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..041ce7ed6 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -91,7 +91,7 @@ void main() { final mod = AddWithCarryMod(Logic(width: 8), Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign {carry, sum} = a + b')); }); @@ -119,7 +119,7 @@ void main() { final gtm = MathTestModule(Logic(width: 8), Logic(width: 8)); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = SvService(gtm).synthOutput; final lines = sv.split('\n'); // ensure we never lshift by a constant directly diff --git a/test/module_merging_test.dart b/test/module_merging_test.dart index 5a5590ce9..bc14a4228 100644 --- a/test/module_merging_test.dart +++ b/test/module_merging_test.dart @@ -91,7 +91,7 @@ void main() async { () async { final dut = TrunkWithLeaves(Logic(), Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect('module ComplicatedLeaf'.allMatches(sv).length, 1); }); @@ -99,7 +99,7 @@ void main() async { test('different reserved definition name modules stay separate', () async { final dut = ParentOfDifferentModuleDefNames(Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('module def1')); expect(sv, contains('module def2')); diff --git a/test/module_test.dart b/test/module_test.dart index 08502d6f8..ff6714e01 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -303,8 +303,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,(a ? 1'h0 : 1'h1)}")); @@ -320,8 +320,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,a}")); @@ -336,7 +336,8 @@ void main() { TopStructInoutWrap(LogicNet(), LogicNet(), LogicNet(width: 2)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); expect( sv, @@ -352,7 +353,7 @@ void main() { expect( mod.internalSignals.firstWhereOrNull((e) => e.name == 't0'), isNotNull); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign a_concat[0] = t0;')); }); @@ -363,7 +364,7 @@ void main() { expect(mod.internalSignals.firstWhereOrNull((e) => e.name == 'unconnected'), isNotNull); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign a_arr[1] = unconnected;')); }); diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 52470dc8c..779ae13ed 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -54,7 +54,7 @@ void main() { .isNotEmpty, 'Should find a z two levels deep'); - final synth = ftm.generateSynth(); + final synth = SvService(ftm).synthOutput; // "z = 1" means it correctly traversed down from inputs assert(synth.contains('z = 1'), diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index b7642bb24..fb20a6106 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -35,7 +35,7 @@ void main() { final mod = TopModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('Passthrough')); }); diff --git a/test/name_test.dart b/test/name_test.dart index afa757cc8..c41573f92 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -194,20 +194,20 @@ void main() { test('respected with no conflicts', () async { final mod = SpeciallyNamedModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module specialName (')); }); test('uniquified with conflicts', () async { final mod = TopModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module specialName (')); expect(sv, contains('module specialName_0 (')); }); test('reserved throws exception with conflicts', () async { final mod = TopModule(Logic(), true, false); await mod.build(); - expect(mod.generateSynth, throwsException); + expect(() => SvService(mod).synthOutput, throwsException); }); }); @@ -215,7 +215,7 @@ void main() { test('uniquified with conflicts', () async { final mod = TopModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('specialInstanceName(')); expect(sv, contains('specialInstanceName_0(')); diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart index fbc1d9536..0ae049180 100644 --- a/test/naming_cases_test.dart +++ b/test/naming_cases_test.dart @@ -554,7 +554,7 @@ void main() { // ── Golden SV snapshot ────────────────────────────────────── test('golden SV output snapshot', () { - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Port declarations. expect(sv, contains('input logic [7:0] inp')); diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index f0d7b2d31..ba5e161bf 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -220,8 +220,8 @@ void main() { test('submodule instance names are allocated from the shared namespace', () async { - // Instance names come from Module.namer.allocateName, which - // shares the same namespace as signal names. + // Instance names come from Module.namer.instanceNameOf, which shares the + // same namespace as signal names. final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); @@ -243,5 +243,27 @@ void main() { 'namespace'); } }); + + test('submodule instance names are stable across repeated definitions', + () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = def1.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + final names2 = def2.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + + expect(names2, names1, + reason: 'Repeated synthesis passes should reuse cached instance ' + 'names instead of drifting numeric suffixes.'); + }); }); } diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart index a5263a998..2c596847c 100644 --- a/test/naming_namespace_test.dart +++ b/test/naming_namespace_test.dart @@ -82,7 +82,7 @@ void main() { test('constant value appears as literal in SV output', () async { final dut = _ConstantNamingModule(); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // The constant "1" should appear as a literal 1'h1 in the output, // not as a declared signal. @@ -92,7 +92,7 @@ void main() { test('constNameDisallowed falls through to signal naming', () async { final dut = _ConstNameDisallowedModule(); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // The output assignment should NOT use the raw constant literal // as a wire name; a proper signal name should be used instead. @@ -109,7 +109,7 @@ void main() { 'in the shared namespace', () async { final dut = _InstanceSignalCollision(); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // With a single shared namespace, one of the two "inner" identifiers // must be suffixed to avoid collision. @@ -119,7 +119,7 @@ void main() { test('duplicate instance names get uniquified', () async { final dut = _DuplicateInstances(); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // Two instances named 'blk' — one should be 'blk', the other 'blk_0'. expect(sv, contains('blk')); diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index 2d5fccdc0..9bcd680a2 100644 --- a/test/net_bus_test.dart +++ b/test/net_bus_test.dart @@ -255,7 +255,8 @@ void main() { final mod = NicePortPassingTop(LogicNet(width: 8), LogicNet(width: 8)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); expect(sv.contains('net_connect'), isFalse); expect(sv, @@ -314,7 +315,7 @@ void main() { final dut = DoubleNetPassthrough(LogicNet(width: 8), LogicNet(width: 8)); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect( sv, @@ -455,7 +456,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, contains( @@ -517,7 +518,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -590,7 +591,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); if (netTypeName == LogicNet) { expect( sv, @@ -620,7 +621,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); if (netTypeName == LogicNet) { expect( sv, @@ -750,8 +751,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect(sv, contains('net_connect (swizzled, ({in0[0],in1[0]}));')); }); @@ -764,8 +765,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -781,8 +782,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -799,8 +800,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -817,8 +818,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -835,8 +836,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect(sv, contains('assign _in1 = in0;')); expect( @@ -852,8 +853,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -942,7 +943,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); checkSV(sv); final vectors = [ @@ -962,7 +963,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); checkSV(sv); final vectors = [ @@ -1204,8 +1205,8 @@ void main() { final mod = ReplicateMod(LogicNet(width: 4), 2); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -1225,8 +1226,8 @@ void main() { final mod = ReplicateMod(LogicNet(width: 4), 2); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, diff --git a/test/net_test.dart b/test/net_test.dart index c8d15b7d7..be2c51d1f 100644 --- a/test/net_test.dart +++ b/test/net_test.dart @@ -461,7 +461,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('intermediate1')); expect(sv, contains('intermediate2')); expect(sv, contains('intermediate3')); @@ -504,7 +504,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -515,7 +515,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -526,7 +526,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(' submod'.allMatches(sv).length, 2); }); }); @@ -611,7 +611,7 @@ void main() { isNotNull); } - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // test that " _b;" is not present (indication that a leftover internal // signal was there) @@ -631,7 +631,7 @@ void main() { final mod = NetArrayTopMod(Logic(width: 8), NetArrayIntf()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // print(sv); expect(sv, contains('wire [1:0][1:0][7:0] bd3')); }); @@ -677,7 +677,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign c = _a_and_b;')); expect(sv, contains('assign d = _aIntermediate_or_bIntermediate;')); diff --git a/test/pair_interface_hier_test.dart b/test/pair_interface_hier_test.dart index e665b7102..71c7cbc92 100644 --- a/test/pair_interface_hier_test.dart +++ b/test/pair_interface_hier_test.dart @@ -91,7 +91,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_hier_w_modify_test.dart b/test/pair_interface_hier_w_modify_test.dart index 47c65318c..fe6255c8c 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_w_modify_test.dart @@ -93,7 +93,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 0167335f3..536576d1b 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -192,7 +192,7 @@ void main() { await mod.build(); // Make sure the "modify" went through: - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic simple_clk')); }); diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 97c15f648..8e5f17177 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -176,7 +176,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' module Provider ( diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index d34c8e374..87b8495aa 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -146,7 +146,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' module Provider ( diff --git a/test/sequential_test.dart b/test/sequential_test.dart index ade256cf3..324777f0a 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -241,7 +241,7 @@ void main() { final mod = NegedgeTriggeredSeq(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('always_ff @(negedge')); final vectors = [ diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..dc9e68ddd 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -698,7 +698,7 @@ void main() { final mod = TieOffSubsetTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("assign banana_tieoff = 2'h0;")); expect(sv, contains("assign apple_tieoff = 2'h0;")); @@ -719,7 +719,7 @@ void main() { final mod = TieOffPortTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("assign banana = 1'h0;")); expect(sv, contains(".apple(1'h0)")); @@ -751,7 +751,7 @@ void main() { test('input, output, and internal signals are sorted', () async { final mod = AlphabeticalModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -768,7 +768,7 @@ void main() { () async { final mod = AlphabeticalWidthsModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -794,7 +794,7 @@ void main() { final mod = AlphabeticalSubmodulePorts(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; checkPortConnectionOrder(sv, ['l', 'a', 'w', 'm', 'x', 'b']); }); @@ -803,7 +803,7 @@ void main() { final mod = TopWithExpressions(Logic(), Logic(width: 5)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('.a((a | (b[2])))')); }); @@ -812,7 +812,7 @@ void main() { final mod = ModuleWithFloatingSignals(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // only expect 1 assignment to xylophone expect('assign'.allMatches(sv).length, 1); @@ -826,8 +826,8 @@ void main() { final mod = TopCustomSvWrap(Logic(), Logic(), useOld: useOld, banExpressions: banExpressions); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (banExpressions) { expect(sv, contains('assign my_fancy_new_signal <= ^fer_swizzle;')); @@ -846,7 +846,7 @@ void main() { final mod = ModuleWithCustomDefinitionEmptyPorts(Logic(), acceptsEmptyPortConnections: acceptsEmptyPortConnections); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; if (acceptsEmptyPortConnections) { expect(sv, contains('.b()')); @@ -861,7 +861,7 @@ void main() { test('custom definition', () async { final mod = TopWithCustomDef(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module CustomDefinitionModule (')); expect(sv, contains('// this is a custom definition!')); @@ -879,7 +879,7 @@ void main() { final mod = ModWithUselessWireMods(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('swizzle'))); expect(sv, isNot(contains('replicate'))); @@ -897,7 +897,7 @@ endmodule : ModWithUselessWireMods''')); test('partial array assignment sv', () async { final mod = ModWithPartialArrayAssignment(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign b = aArr[0];')); expect(sv, contains('assign aArr[0] = a;')); @@ -1041,7 +1041,7 @@ endmodule : ModWithUselessWireMods''')); final mod = OutToInOutTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign myNet = myOut;')); @@ -1057,7 +1057,7 @@ endmodule : ModWithUselessWireMods''')); () async { final mod = _StructLeafNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('_in0')), reason: 'Struct leaf from unnamed Logic() should use its ' @@ -1067,7 +1067,7 @@ endmodule : ModWithUselessWireMods''')); test('const merge not blocked by constNameDisallowed', () async { final mod = _ConstNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; final constAssignments = RegExp(r"assign \w+ = 8'h0;").allMatches(sv).length; diff --git a/test/sv_param_passthrough_test.dart b/test/sv_param_passthrough_test.dart index e7b0876dd..041eb8ce5 100644 --- a/test/sv_param_passthrough_test.dart +++ b/test/sv_param_passthrough_test.dart @@ -162,7 +162,7 @@ void main() { () async { final mod = TopForEmptyParams(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('#'), isFalse); }); } diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index 6e1fc0949..691497eab 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -123,7 +123,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('/*')); expect(sv, contains('*/')); @@ -146,7 +146,7 @@ void main() { final mod = SingleElementSwizzle(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Single element should not have braces or bit range annotations // Look for bit range annotations specifically (/* number */) @@ -171,7 +171,7 @@ void main() { final mod = AllSingleBitSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have bit range annotations for single bits expect(sv, contains('/*')); @@ -202,7 +202,7 @@ void main() { final mod = NestedSwizzle(Logic(width: 4), Logic(width: 3)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should contain annotations for both inner and outer swizzles expect(sv, contains('/*')); @@ -222,7 +222,7 @@ void main() { final mod = InlinedSwizzle(Logic(width: 4), Logic(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have annotations even when swizzle is part of larger expression expect(sv, contains('/*')); @@ -242,7 +242,7 @@ void main() { final mod = VariedWidthSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have aligned bit range annotations expect(sv, contains('/*')); @@ -280,7 +280,7 @@ void main() { // Create a module with indices requiring different digit widths final largeModule = LargeWidthSwizzle(); await largeModule.build(); - final sv = largeModule.generateSynth(); + final sv = SvService(largeModule).synthOutput; // Should have properly aligned annotations despite different digit counts expect(sv, contains('/*')); @@ -323,7 +323,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' assign b = { diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..8f27bd0fb 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -229,7 +229,7 @@ void main() { final mod = SimpleStructModuleContainer(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('internal_struct'))); @@ -251,7 +251,7 @@ void main() { expect(mod.anyOut, isA()); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [3:0][1:0] anyIn')); expect(sv, contains('output logic [3:0][1:0] anyOut')); @@ -277,7 +277,7 @@ void main() { final mod = ParentModuleWithStructsContainingPorts(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // if naming is wrong, these names will appear in the SV in ports expect( @@ -357,7 +357,7 @@ void main() { SimpleStructModuleContainer(LogicNet(), LogicNet(), asNet: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('internal_struct'))); @@ -502,7 +502,7 @@ void main() { final mod = ModuleWithOneBitStructPort(OneBitStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // no slicing on single-bit signals expect(sv, contains('assign outStruct = outStruct_oneBit')); diff --git a/test/wave_dumper_test.dart b/test/wave_dumper_test.dart index 07aafc8c8..a94d06620 100644 --- a/test/wave_dumper_test.dart +++ b/test/wave_dumper_test.dart @@ -40,11 +40,11 @@ const tempDumpDir = 'tmp_test'; /// Gets the path of the VCD file based on a name. String temporaryDumpPath(String name) => '$tempDumpDir/temp_dump_$name.vcd'; -/// Attaches a [WaveDumper] to [module] to VCD with [name]. +/// Attaches a [WaveformService] to [module] to VCD with [name]. void createTemporaryDump(Module module, String name) { Directory(tempDumpDir).createSync(recursive: true); final tmpDumpFile = temporaryDumpPath(name); - WaveDumper(module, outputPath: tmpDumpFile); + WaveformService(module, outputPath: tmpDumpFile); } /// Deletes the temporary VCD file associated with [name]. @@ -241,7 +241,8 @@ void main() { const dir1Path = '$tempDumpDir/dir1'; - final waveDumper = WaveDumper(mod, outputPath: '$dir1Path/dir2/waves.vcd'); + final waveDumper = + WaveformService(mod, outputPath: '$dir1Path/dir2/waves.vcd'); expect(File(waveDumper.outputPath).existsSync(), equals(true)); From 973300cb88c38f7770e520bcacc4a7e2bb0fc7b0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 06:12:09 -0700 Subject: [PATCH 29/58] Formalize module service inspection contracts --- lib/src/diagnostics/module_services.dart | 127 +++++++++++++++++- .../systemverilog/sv_service.dart | 10 +- test/module_services_test.dart | 73 +++++++++- 3 files changed, 197 insertions(+), 13 deletions(-) diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart index 8f17b5746..0e0f431a4 100644 --- a/lib/src/diagnostics/module_services.dart +++ b/lib/src/diagnostics/module_services.dart @@ -8,10 +8,44 @@ // Author: Desmond Kirkpatrick import 'dart:convert'; +import 'dart:io'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; +/// The read-only netlist surface used by [ModuleServices]. +/// +/// Feature branches provide concrete implementations without making this +/// package layer depend on netlist synthesis classes. +abstract interface class NetlistInspectionService { + /// Returns a compact hierarchy-oriented netlist JSON string. + String get slimJson; + + /// Returns the full netlist hierarchy JSON string. + String toJson(); + + /// Returns netlist JSON for a single module [definitionName]. + String moduleJson(String definitionName); +} + +/// The read-only source-trace surface used by [ModuleServices]. +/// +/// Feature branches provide concrete implementations without making this +/// package layer depend on source-debug tracing classes. +abstract interface class TraceInspectionService { + /// The top-level module associated with this trace. + Module get module; + + /// Returns the FLC hierarchy object, or `null` when unavailable. + Map? get flcHierarchy; + + /// Returns the full FLC hierarchy JSON string. + String get flcJson; + + /// Returns FLC JSON for a single module [definitionName]. + String flcModuleJson(String definitionName); +} + /// Singleton service registry that provides a unified query surface for /// DevTools and other inspection tools. /// @@ -23,10 +57,13 @@ import 'package:rohd/src/diagnostics/inspector_service.dart'; /// /// **Opt-in (registered by service constructors):** /// - [svService] — SystemVerilog synthesis results. +/// - [netlistService] — netlist inspection data. /// - [waveformService] — waveform capture (file output + optional streaming). +/// - [traceService] — source trace / FLC data. /// -/// Additional services (netlist, trace) can be added by setting -/// the corresponding field after construction. +/// Concrete feature services implement the small inspection interfaces above, +/// so this branch stays a formal dependency without importing future feature +/// branches. class ModuleServices { ModuleServices._(); @@ -49,11 +86,16 @@ class ModuleServices { return ModuleTree.instance.hierarchyJSON; } - /// Returns the primary inspector JSON for DevTools. + /// Returns the unified inspector JSON, the primary DevTools design entry + /// point. /// - /// Returns the hierarchy JSON. Downstream branches (e.g. netlist) may - /// override this to return richer data when available. - String get inspectorJSON => hierarchyJSON; + /// When a netlist inspection service is registered, this returns the compact + /// netlist view. Otherwise it falls back to the module hierarchy JSON. + String get inspectorJSON => netlistService?.slimJson ?? hierarchyJSON; + + /// Returns full inspector JSON for a single module [definitionName]. + String inspectorModuleJSON(String definitionName) => + netlistModuleJSON(definitionName); // ─── SystemVerilog service (opt-in) ─────────────────────────── @@ -64,6 +106,21 @@ class ModuleServices { String get svJSON => svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); + // ─── Netlist service (opt-in) ───────────────────────────────── + + /// The active netlist inspection service, if one has been registered. + NetlistInspectionService? netlistService; + + /// Returns the full netlist hierarchy as JSON, or an unavailable status. + String get netlistJSON => netlistService != null + ? netlistService!.toJson() + : _unavailable('netlist'); + + /// Returns netlist JSON for a single module [definitionName]. + String netlistModuleJSON(String definitionName) => netlistService != null + ? netlistService!.moduleJson(definitionName) + : _unavailable('netlist'); + // ─── Waveform service (opt-in) ─────────────────────────────── /// The active [WaveformService], if one has been registered. @@ -74,6 +131,61 @@ class ModuleServices { ? jsonEncode(waveformService!.toJson()) : _unavailable('waveform'); + // ─── Trace service (opt-in) ─────────────────────────────────── + + /// The active source-trace inspection service, if one has been registered. + TraceInspectionService? traceService; + + /// Cached path to the FLC file written by [flcFilePath]. + String? _flcFilePathCache; + + /// Returns the FLC hierarchy JSON, or an unavailable status. + String get flcJSON => + traceService != null ? traceService!.flcJson : _unavailable('trace'); + + /// Returns FLC JSON for a single module [definitionName]. + String flcModuleJSON(String definitionName) => traceService != null + ? traceService!.flcModuleJson(definitionName) + : _unavailable('trace'); + + /// Writes the FLC hierarchy to a temporary file and returns the path. + /// + /// Returns a JSON error string when trace data is unavailable or the write + /// fails. + String get flcFilePath { + if (_flcFilePathCache != null) { + if (File(_flcFilePathCache!).existsSync()) { + return _flcFilePathCache!; + } + _flcFilePathCache = null; + } + + final service = traceService; + if (service == null) { + return _unavailable('trace'); + } + final hierarchy = service.flcHierarchy; + if (hierarchy == null) { + return _unavailable('trace'); + } + + try { + final dir = Directory('${Directory.systemTemp.path}/rohd_devtools_flc') + ..createSync(recursive: true); + final path = '${dir.path}/${service.module.definitionName}.flc.json'; + File(path).writeAsStringSync( + const JsonEncoder.withIndent(' ').convert(hierarchy), + ); + _flcFilePathCache = path; + return path; + } on Exception catch (error) { + return jsonEncode({ + 'status': 'error', + 'reason': error.toString(), + }); + } + } + // ─── Helpers ────────────────────────────────────────────────── static String _unavailable(String service) => jsonEncode({ @@ -85,6 +197,9 @@ class ModuleServices { void reset() { rootModule = null; svService = null; + netlistService = null; waveformService = null; + traceService = null; + _flcFilePathCache = null; } } diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart index 84e046e9f..776b090b0 100644 --- a/lib/src/synthesizers/systemverilog/sv_service.dart +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -56,8 +56,10 @@ class SvService { /// written to that file. The parent directory is created if needed. SvService(this.module, {bool register = true, String? outputPath}) { if (!module.hasBuilt) { - throw Exception('Module must be built before creating SvService. ' - 'Call build() first.'); + throw Exception( + 'Module must be built before creating SvService. ' + 'Call build() first.', + ); } synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); @@ -140,8 +142,6 @@ class SvService { /// /// Contains the list of generated module definition names. Map toJson() => { - 'modules': [ - for (final fc in fileContents) fc.name, - ], + 'modules': [for (final fc in fileContents) fc.name], }; } diff --git a/test/module_services_test.dart b/test/module_services_test.dart index f8994577e..c49cad315 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -20,6 +20,37 @@ class SimpleModule extends Module { } } +class FakeNetlistService implements NetlistInspectionService { + @override + String get slimJson => jsonEncode({'kind': 'slim'}); + + @override + String toJson() => jsonEncode({'kind': 'full'}); + + @override + String moduleJson(String definitionName) => + jsonEncode({'module': definitionName}); +} + +class FakeTraceService implements TraceInspectionService { + FakeTraceService(this.module); + + @override + final Module module; + + @override + Map? get flcHierarchy => { + 'modules': {module.definitionName: {}}, + }; + + @override + String get flcJson => jsonEncode(flcHierarchy); + + @override + String flcModuleJson(String definitionName) => + jsonEncode({'module': definitionName}); +} + void main() { tearDown(ModuleServices.instance.reset); @@ -40,8 +71,43 @@ void main() { test('inspectorJSON matches hierarchyJSON', () async { final mod = SimpleModule(Logic()); await mod.build(); - expect(ModuleServices.instance.inspectorJSON, - equals(ModuleServices.instance.hierarchyJSON)); + expect( + ModuleServices.instance.inspectorJSON, + equals(ModuleServices.instance.hierarchyJSON), + ); + }); + + test('inspectorJSON uses registered netlist service', () async { + ModuleServices.instance.netlistService = FakeNetlistService(); + final inspectorJson = jsonDecode(ModuleServices.instance.inspectorJSON) + as Map; + final netlistJson = jsonDecode(ModuleServices.instance.netlistJSON) + as Map; + final moduleJson = jsonDecode( + ModuleServices.instance.inspectorModuleJSON('SimpleModule'), + ) as Map; + + expect(inspectorJson['kind'], equals('slim')); + expect(netlistJson['kind'], equals('full')); + expect(moduleJson['module'], equals('SimpleModule')); + }); + + test('trace service exposes FLC JSON and file path', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.traceService = FakeTraceService(mod); + final flcJson = + jsonDecode(ModuleServices.instance.flcJSON) as Map; + final moduleJson = + jsonDecode(ModuleServices.instance.flcModuleJSON('SimpleModule')) + as Map; + + expect(flcJson, isA>()); + expect(moduleJson['module'], equals('SimpleModule')); + + final flcPath = ModuleServices.instance.flcFilePath; + expect(flcPath, isNot(startsWith('{'))); + expect(File(flcPath).existsSync(), isTrue); }); test('svJSON returns unavailable when no service registered', () async { @@ -59,6 +125,9 @@ void main() { ModuleServices.instance.reset(); expect(ModuleServices.instance.rootModule, isNull); expect(ModuleServices.instance.svService, isNull); + expect(ModuleServices.instance.netlistService, isNull); + expect(ModuleServices.instance.waveformService, isNull); + expect(ModuleServices.instance.traceService, isNull); }); }); From 1225df127afbb3e7b8e5e5dbd1c0f177422fc704 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 06:41:11 -0700 Subject: [PATCH 30/58] Clean DevTools extension analysis and formatting --- .../utilities/synth_module_definition.dart | 1 - .../utilities/synth_sub_module_instantiation.dart | 10 +++++----- lib/src/utilities/namer.dart | 1 - rohd_devtools_extension/pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 38a207b74..f1fbb3931 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -37,7 +37,6 @@ class _BusSubsetForStructSlice extends BusSubset { // we override this since it's added post-build @override bool get hasBuilt => true; - } /// Represents the definition of a module. diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 6c711c330..343ca1714 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -27,14 +27,14 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// /// Names are allocated from [parentModule]'s [Namer]'s shared namespace - /// via [Namer.allocateName]. + /// via [Namer.allocateName]. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.allocateName( - module.uniqueInstanceName, - reserved: module.reserveName, - ); + _name = parentModule.namer.allocateName( + module.uniqueInstanceName, + reserved: module.reserveName, + ); } /// A mapping of input port name to [SynthLogic]. diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 3afc9d873..d4c6eff85 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -32,7 +32,6 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; - /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 0aa366e78..8b5bb226f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.1.0 + bloc_lint: ^0.3.7 flutter: uses-material-design: true From 6284152bb339eee7ffb7dee099d677fb1774000c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 06:41:12 -0700 Subject: [PATCH 31/58] Clean DevTools extension analysis --- rohd_devtools_extension/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 0aa366e78..8b5bb226f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.1.0 + bloc_lint: ^0.3.7 flutter: uses-material-design: true From cc64482cf2b17963a145afeaa5b28cfb91a8f699 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 07:07:59 -0700 Subject: [PATCH 32/58] Keep DevTools extension changes on owning branches --- rohd_devtools_extension/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 8b5bb226f..0aa366e78 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.3.7 + bloc_lint: ^0.1.0 flutter: uses-material-design: true From c8440c4e5fb294c61d7611f77d66484678341328 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 07:07:59 -0700 Subject: [PATCH 33/58] Keep DevTools extension changes on owning branches --- rohd_devtools_extension/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 8b5bb226f..0aa366e78 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.3.7 + bloc_lint: ^0.1.0 flutter: uses-material-design: true From 02f142610d95b280e2118ef2ba5b4fbe8f31ad4b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 08:13:37 -0700 Subject: [PATCH 34/58] Restore stable instance name caching --- .../utilities/synth_module_definition.dart | 11 ++++++-- .../synth_sub_module_instantiation.dart | 9 +++--- lib/src/utilities/namer.dart | 28 +++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index f1fbb3931..001f32eef 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -25,18 +25,24 @@ class _BusSubsetForStructSlice extends BusSubset { /// [_BusSubsetForStructSlice] is created on every synthesis pass, its /// canonical instance name is memoized against the persistent destination /// signal and therefore does not drift run-to-run. + final Logic _destination; /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. _BusSubsetForStructSlice( super.bus, super.startIndex, - super.endIndex, - ) : super(name: 'struct_slice'); + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super(name: 'struct_slice'); // we override this since it's added post-build @override bool get hasBuilt => true; + + @override + Object get instanceNameKey => _destination; } /// Represents the definition of a module. @@ -272,6 +278,7 @@ class SynthModuleDefinition { width: port.width, name: 'DUMMY'), idx, idx + leafElement.width - 1, + destination: leafElement, ); final ssmi = getSynthSubModuleInstantiation(subsetMod); diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 343ca1714..cc553a24c 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -27,14 +27,13 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// /// Names are allocated from [parentModule]'s [Namer]'s shared namespace - /// via [Namer.allocateName]. + /// via [Namer.instanceNameOf], which memoizes by [Module.instanceNameKey] so + /// the same instance receives an identical canonical name across repeated + /// synthesis passes. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.allocateName( - module.uniqueInstanceName, - reserved: module.reserveName, - ); + _name = parentModule.namer.instanceNameOf(module); } /// A mapping of input port name to [SynthLogic]. diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index d4c6eff85..39131dfc7 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -32,6 +32,13 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; + /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. + /// + /// Instance-name allocation mutates [_uniquifier]. Without this cache, + /// repeated synthesis passes over the same module hierarchy would allocate + /// fresh suffixes for the same submodule instances. + final Map _instanceNames = {}; + /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; @@ -80,6 +87,27 @@ class Namer { reserved: reserved, ); + // ─── Instance naming (Module → String) ────────────────────────── + + /// Returns the canonical instance name for [submodule]. + /// + /// The first call allocates a collision-free name in the shared namespace; + /// later calls for the same [Module.instanceNameKey] return the cached name. + String instanceNameOf(Module submodule) { + final key = submodule.instanceNameKey; + final cached = _instanceNames[key]; + if (cached != null) { + return cached; + } + + final name = allocateName( + submodule.uniqueInstanceName, + reserved: submodule.reserveName, + ); + _instanceNames[key] = name; + return name; + } + // ─── Signal naming (Logic → String) ───────────────────────────── /// Returns the canonical name for [logic]. From a87fa5c20118f6813c1f354731e34d6634060c08 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 21 Jun 2026 09:26:53 -0700 Subject: [PATCH 35/58] added back pubkeys, and made a wget a fallback solution with loud warning --- tool/gh_codespaces/install_dart.sh | 80 +++++++- tool/gh_codespaces/pubkeys/dart.pub | 305 ++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+), 5 deletions(-) create mode 100644 tool/gh_codespaces/pubkeys/dart.pub diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index f170dc247..d0bfdfe91 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -8,21 +8,91 @@ # # 2023 February 5 # Author: Chykon +# +# 2026 June 21 +# Updated to add fallback logic for fetching the latest Dart repository key from Google if the locally cached key fails verification (e.g. due to key rotation). +# Author: Desmond A. Kirkpatrick set -euo pipefail +declare -r cached_pubkey_file="$(dirname "${BASH_SOURCE[0]}")/pubkeys/dart.pub" +declare -r keyring_file='/usr/share/keyrings/dart.gpg' +declare -r dart_repository_file='/etc/apt/sources.list.d/dart_stable.list' +declare -r dart_repository_url='https://storage.googleapis.com/download.dartlang.org/linux/debian' +declare -r google_signing_key_url='https://dl-ssl.google.com/linux/linux_signing_key.pub' + sudo apt-get update sudo apt-get install -y wget gpg apt-transport-https sudo mkdir -p /usr/share/keyrings -wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub \ - | gpg --dearmor \ - | sudo tee /usr/share/keyrings/dart.gpg >/dev/null # Add Dart repository. -echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" \ - | sudo tee /etc/apt/sources.list.d/dart_stable.list +echo "deb [signed-by=${keyring_file}] ${dart_repository_url} stable main" \ + | sudo tee "${dart_repository_file}" + +# Install the repository key from the locally cached, ASCII-armored public key. +install_key_from_file() { + sudo gpg --yes --output "${keyring_file}" --dearmor "${1}" +} + +# Install the repository key by fetching the latest key from Google. +install_key_from_google() { + wget -qO- "${google_signing_key_url}" \ + | gpg --dearmor \ + | sudo tee "${keyring_file}" >/dev/null +} + +# Emit a prominent warning that stands out in CI logs (and as a GitHub Actions +# annotation when available) without failing the build. +warn_loudly() { + local message="${1}" + { + echo '' + echo '################################################################################' + echo '## install_dart WARNING' + echo "## ${message}" + echo '################################################################################' + echo '' + } >&2 + # Surface a GitHub Actions warning annotation (non-fatal) when running in CI. + if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + echo "::warning title=install_dart cached key bypassed::${message}" + fi +} + +# Verify that the installed keyring can authenticate the Dart repository by +# refreshing only the Dart sources list and checking for signature/key errors. +dart_repository_verified() { + local update_log + if ! update_log=$(sudo apt-get update \ + -o Dir::Etc::sourcelist="${dart_repository_file}" \ + -o Dir::Etc::sourceparts="-" \ + -o APT::Get::List-Cleanup="0" 2>&1); then + return 1 + fi + if echo "${update_log}" \ + | grep -Eiq 'NO_PUBKEY|EXPKEYSIG|REVKEYSIG|BADSIG|not signed|could.?n.?t be verified'; then + return 1 + fi + return 0 +} + +# Prefer the locally cached key. If it can no longer authenticate the repository +# (e.g. the key has been rotated), fall back to fetching the latest key from +# Google so the install can still proceed. +install_key_from_file "${cached_pubkey_file}" + +if dart_repository_verified; then + echo 'install_dart: using locally cached Dart repository key.' +else + install_key_from_google + if ! dart_repository_verified; then + echo 'install_dart: Dart repository key verification failed even after fetching the latest key from Google.' >&2 + exit 1 + fi + warn_loudly "Cached Dart repository key (${cached_pubkey_file}) failed verification and was bypassed; installed using the latest key fetched from Google. Please refresh the cached key." +fi # Install Dart. diff --git a/tool/gh_codespaces/pubkeys/dart.pub b/tool/gh_codespaces/pubkeys/dart.pub new file mode 100644 index 000000000..839f8a235 --- /dev/null +++ b/tool/gh_codespaces/pubkeys/dart.pub @@ -0,0 +1,305 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=CkA1 +-----END PGP PUBLIC KEY BLOCK----- From 7aa11789a0a0f75df1f23efd1d07c45e8a753600 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 21 Jun 2026 21:33:23 -0700 Subject: [PATCH 36/58] trailing comma reduction --- lib/src/utilities/simcompare.dart | 77 ++++++++++++------------------- 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index 6fb1aa569..eb1c736f5 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -104,10 +104,7 @@ class Vector { final outputPort = module.tryInOut(outputName) ?? module.output(outputName); final expected = expectedOutput.value; - final expectedValue = LogicValue.of( - expected, - width: outputPort.width, - ); + final expectedValue = LogicValue.of(expected, width: outputPort.width); final inputStimulus = inputValues.toString(); if (outputPort is LogicArray) { @@ -125,12 +122,8 @@ class Vector { } final checks = checksList.join('\n'); - final tbVerilog = [ - assignments, - '#$_offset', - checks, - '#${_period - _offset}', - ].join('\n'); + final tbVerilog = + [assignments, '#$_offset', checks, '#${_period - _offset}'].join('\n'); return tbVerilog; } } @@ -202,12 +195,10 @@ abstract class SimCompare { throw NonSupportedTypeException(value); } } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { - Simulator.throwException(err as Exception, stackTrace); - }, - )); + }).catchError(test: (error) => error is Exception, + (Object err, StackTrace stackTrace) { + Simulator.throwException(err as Exception, stackTrace); + })); } }); timestamp += Vector._period; @@ -224,23 +215,20 @@ abstract class SimCompare { RegExp(r'sorry: constant selects in always_\* processes' ' are not currently supported'), RegExp('warning: always_comb process has no sensitivities'), - RegExp('finish called at'), + RegExp('finish called at') ]; /// Executes [vectors] against the Icarus Verilog simulator and checks /// that it passes. - static void checkIverilogVector( - Module module, - List vectors, { - String? moduleName, - bool dontDeleteTmpFiles = false, - bool dumpWaves = false, - List iverilogExtraArgs = const [], - bool allowWarnings = false, - bool maskKnownWarnings = true, - bool enableChecking = true, - bool buildOnly = false, - }) { + static void checkIverilogVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool enableChecking = true, + bool buildOnly = false}) { final result = iverilogVector(module, vectors, moduleName: moduleName, dontDeleteTmpFiles: dontDeleteTmpFiles, @@ -255,17 +243,14 @@ abstract class SimCompare { } /// Executes [vectors] against the Icarus Verilog simulator. - static bool iverilogVector( - Module module, - List vectors, { - String? moduleName, - bool dontDeleteTmpFiles = false, - bool dumpWaves = false, - List iverilogExtraArgs = const [], - bool allowWarnings = false, - bool maskKnownWarnings = true, - bool buildOnly = false, - }) { + static bool iverilogVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool buildOnly = false}) { if (kIsWeb) { // if running in web mode, then we can't run icarus verilog return true; @@ -307,7 +292,7 @@ abstract class SimCompare { final topModule = moduleName ?? module.definitionName; final allSignals = { for (final v in vectors) ...v.inputValues.keys, - for (final v in vectors) ...v.expectedOutputValues.keys, + for (final v in vectors) ...v.expectedOutputValues.keys }; late final tbWireUniquifier = Uniquifier(); @@ -335,7 +320,7 @@ abstract class SimCompare { final sigDecl = signalDeclaration(logicName, adjust: toTbWireName, signalTypeOverride: 'wire'); return '$sigDecl; assign $wireName = $logicName;'; - }), + }) ].join('\n'); final moduleConnections = @@ -370,7 +355,7 @@ abstract class SimCompare { stimulus, r'$finish;', // so the test doesn't run forever if there's a clock gen 'end', - 'endmodule', + 'endmodule' ].join('\n'); Directory(dir).createSync(recursive: true); @@ -397,11 +382,7 @@ abstract class SimCompare { } return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), caseSensitive: false)); } From f30baf765c99b88245fc01d93083b7f9456f04ae Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 01:25:46 -0700 Subject: [PATCH 37/58] Restrict module_services_test to VM platform Uses dart:io (File, Directory, Process) which requires the Dart VM. Matches the pattern used in wave_dumper_test.dart and vcd_parser_test.dart. --- test/module_services_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/module_services_test.dart b/test/module_services_test.dart index c49cad315..9179a6dd3 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -7,6 +7,9 @@ // 2026 April 25 // Author: Desmond Kirkpatrick +@TestOn('vm') +library; + import 'dart:convert'; import 'dart:io'; From b7e46c0f3bde4cb3a7c6374f96575e0cd96a632f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 01:39:49 -0700 Subject: [PATCH 38/58] Add instanceNameOf to Namer: cached instance-name lookup instanceNameOf(Module) allocates a collision-free instance name on the first call and returns the cached result thereafter. The _instanceNames Map is keyed by Module.instanceNameKey so repeated synthesis passes over the same hierarchy always produce stable names. This method belongs in central_naming because it is pure naming infrastructure with no dependency on any feature branch. --- lib/src/utilities/namer.dart | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index d4c6eff85..478e0fe3b 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -21,7 +21,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// /// Port names are reserved at construction time. Internal signal names /// are assigned lazily on the first [signalNameOf] call. Instance names -/// are allocated explicitly via [allocateName]. +/// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { // ─── Shared namespace ─────────────────────────────────────────── @@ -32,6 +32,13 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; + /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. + /// + /// Instance-name allocation mutates [_uniquifier]. Without this cache, + /// repeated synthesis passes over the same module hierarchy would allocate + /// fresh suffixes for the same submodule instances. + final Map _instanceNames = {}; + /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; @@ -80,6 +87,27 @@ class Namer { reserved: reserved, ); + // ─── Instance naming (Module → String) ────────────────────────── + + /// Returns the canonical instance name for [submodule]. + /// + /// The first call allocates a collision-free name in the shared namespace; + /// later calls for the same [Module.instanceNameKey] return the cached name. + String instanceNameOf(Module submodule) { + final key = submodule.instanceNameKey; + final cached = _instanceNames[key]; + if (cached != null) { + return cached; + } + + final name = allocateName( + submodule.uniqueInstanceName, + reserved: submodule.reserveName, + ); + _instanceNames[key] = name; + return name; + } + // ─── Signal naming (Logic → String) ───────────────────────────── /// Returns the canonical name for [logic]. From 0f13c7b8eaf93f059db8cd10909a92c6a8b9ba44 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 01:43:39 -0700 Subject: [PATCH 39/58] Move instanceNameOf stability test to central_naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update comment: 'allocateName' → 'instanceNameOf' - Add 'submodule instance names are stable across repeated definitions' test (the canonical 'run synthesis twice, same names' regression test) Both belong here since they directly exercise Namer.instanceNameOf, which is now defined in central_naming. --- test/naming_consistency_test.dart | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index f0d7b2d31..ba5e161bf 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -220,8 +220,8 @@ void main() { test('submodule instance names are allocated from the shared namespace', () async { - // Instance names come from Module.namer.allocateName, which - // shares the same namespace as signal names. + // Instance names come from Module.namer.instanceNameOf, which shares the + // same namespace as signal names. final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); @@ -243,5 +243,27 @@ void main() { 'namespace'); } }); + + test('submodule instance names are stable across repeated definitions', + () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = def1.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + final names2 = def2.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + + expect(names2, names1, + reason: 'Repeated synthesis passes should reuse cached instance ' + 'names instead of drifting numeric suffixes.'); + }); }); } From 367715352deb457e5d63f08473332c5c4554fd98 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 01:45:04 -0700 Subject: [PATCH 40/58] Remove duplicate instanceNameOf and stability test: now in central_naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - namer.dart: update docstring ('allocateName' → 'instanceNameOf'); _instanceNames and instanceNameOf method are identical to central_naming - naming_consistency_test: update comment and add 'stable across repeated definitions' test — both moved to central_naming where they belong --- lib/src/utilities/namer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 39131dfc7..478e0fe3b 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -21,7 +21,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// /// Port names are reserved at construction time. Internal signal names /// are assigned lazily on the first [signalNameOf] call. Instance names -/// are allocated explicitly via [allocateName]. +/// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { // ─── Shared namespace ─────────────────────────────────────────── From 319706b66ab7a9dd41da198c7f5e39f711a362cc Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 05:35:14 -0700 Subject: [PATCH 41/58] Wire pickName through instanceNameOf for stable instance names SynthSubModuleInstantiation.pickName was calling namer.allocateName() directly, bypassing the _instanceNames cache in Namer.instanceNameOf. This caused the second SynthModuleDefinition build over the same module hierarchy to see 'inner' already taken and allocate 'inner_0' instead. Fix: call namer.instanceNameOf(module) which caches on first allocation and returns the same name on subsequent synthesis passes. --- .../utilities/synth_sub_module_instantiation.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 343ca1714..65878d40d 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -26,15 +26,13 @@ class SynthSubModuleInstantiation { /// Selects a name for this module instance. Must be called exactly once. /// - /// Names are allocated from [parentModule]'s [Namer]'s shared namespace - /// via [Namer.allocateName]. + /// Names are allocated (and cached) via [Namer.instanceNameOf] so that + /// repeated synthesis passes over the same hierarchy always produce the + /// same instance name. void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = parentModule.namer.allocateName( - module.uniqueInstanceName, - reserved: module.reserveName, - ); + _name = parentModule.namer.instanceNameOf(module); } /// A mapping of input port name to [SynthLogic]. From 11bc2cd693e278cc2b5b22f0752c2f1a4777fd5f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 05:44:22 -0700 Subject: [PATCH 42/58] Add instance-signal namespace collision stability tests Two new tests in 'shared instance and signal namespace': 1. 'instance name wins the shared namespace; signal gets the suffix' Asserts deterministic ordering: non-reserved instances are picked before non-reserved signals, so the instance keeps the bare name and the colliding signal is uniquified to inner_0. 2. 'instance-signal collision resolution is stable across repeated synthesis passes' Calls generateSynth() twice and verifies the module body is identical (timestamp stripped). Guards against name drift where the second pass would assign different suffixes. --- test/naming_namespace_test.dart | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart index a5263a998..296d9bd49 100644 --- a/test/naming_namespace_test.dart +++ b/test/naming_namespace_test.dart @@ -116,6 +116,47 @@ void main() { expect(sv, contains('inner_0')); }); + test('instance name wins the shared namespace; signal gets the suffix', + () async { + // Non-reserved submodule instances are picked before non-reserved + // internal signals, so the instance claims the bare name and the + // colliding signal is uniquified. + final dut = _InstanceSignalCollision(); + await dut.build(); + + final instanceName = + dut.namer.instanceNameOf(dut.subModules.first); + expect(instanceName, equals('inner'), + reason: 'Instance should win the shared namespace ' + 'and keep the bare name'); + + final sv = dut.generateSynth(); + // The wire (signal) must carry the suffix, not the instance. + expect(sv, contains('inner_0'), + reason: 'Colliding signal should be renamed to inner_0'); + expect(sv, isNot(contains('inner_0 inner')), + reason: 'Instance itself must not be named inner_0'); + }); + + test( + 'instance-signal collision resolution is stable across ' + 'repeated synthesis passes', () async { + final dut = _InstanceSignalCollision(); + await dut.build(); + + // Strip the generated header (contains a wall-clock timestamp) before + // comparing so the test does not fail on timing jitter. + String stripHeader(String sv) => + sv.replaceFirst(RegExp(r'/\*\*.*?\*/\n', dotAll: true), ''); + + final sv1 = stripHeader(dut.generateSynth()); + final sv2 = stripHeader(dut.generateSynth()); + + expect(sv2, equals(sv1), + reason: 'Repeated synthesis passes must produce identical ' + 'SV output; instance and signal names must not drift.'); + }); + test('duplicate instance names get uniquified', () async { final dut = _DuplicateInstances(); await dut.build(); From 3b8a8a8100f8a5845faaa9523d07da0313aa9c45 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 05:45:23 -0700 Subject: [PATCH 43/58] consistency in naming --- test/naming_namespace_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart index 296d9bd49..9f1c0c31f 100644 --- a/test/naming_namespace_test.dart +++ b/test/naming_namespace_test.dart @@ -124,8 +124,7 @@ void main() { final dut = _InstanceSignalCollision(); await dut.build(); - final instanceName = - dut.namer.instanceNameOf(dut.subModules.first); + final instanceName = dut.namer.instanceNameOf(dut.subModules.first); expect(instanceName, equals('inner'), reason: 'Instance should win the shared namespace ' 'and keep the bare name'); From 887e541c94853368ffd3c8ec4b0b3c5c66af5c48 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 23 Jun 2026 08:53:30 -0700 Subject: [PATCH 44/58] Slim ModuleServices to a type-keyed service registry Introduce ModuleService/OutputService/CodegenService base types and replace the leaky ModuleServices singleton (per-service getters and inspection interfaces) with a generic register/lookup registry keyed by service type. SvService now extends CodegenService, exposes a stable cached synthOutput, and registers itself via the new registry. WaveformService registers as a ModuleService. Rewrites module_services_test.dart to the new API. --- lib/rohd.dart | 1 + lib/src/diagnostics/module_service.dart | 72 +++++++ lib/src/diagnostics/module_services.dart | 182 +++--------------- lib/src/diagnostics/waveform_service.dart | 10 +- .../systemverilog/sv_service.dart | 65 ++++++- test/module_services_test.dart | 155 ++++++++------- 6 files changed, 238 insertions(+), 247 deletions(-) create mode 100644 lib/src/diagnostics/module_service.dart diff --git a/lib/rohd.dart b/lib/rohd.dart index d110c49e5..a8a6082fd 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,7 @@ // Copyright (C) 2021-2023 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/module_service.dart'; export 'src/diagnostics/module_services.dart'; export 'src/diagnostics/waveform_service.dart'; export 'src/exceptions/exceptions.dart'; diff --git a/lib/src/diagnostics/module_service.dart b/lib/src/diagnostics/module_service.dart new file mode 100644 index 000000000..6e72e1819 --- /dev/null +++ b/lib/src/diagnostics/module_service.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_service.dart +// Common base types shared by all module-scoped services. +// +// 2026 June 23 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// The common contract implemented by every module-scoped service that +/// registers with [ModuleServices]. +/// +/// A service wraps some derived view of a built [Module] (synthesis output, +/// netlist, source trace, waveform, etc.) and exposes a JSON-serialisable +/// summary via [toJson]. Concrete services additionally expose their own +/// format-specific accessors; consumers reach them through +/// [ModuleServices.lookup] or the service's own `current` accessor rather than +/// through getters on the registry. +abstract interface class ModuleService { + /// The top-level [Module] this service operates on. + Module get module; + + /// A JSON-serialisable summary of this service. + Map toJson(); +} + +/// A [ModuleService] that emits output to one or more files. +/// +/// Establishes the common output convention shared by synthesis, netlist, +/// trace, and waveform services: +/// - [outputPath] — the default file or directory written by [write]. +/// - [multiFile] — whether [write] emits one file per module definition +/// (a directory) or a single combined file. +/// - [write] — performs the write, honouring [multiFile]. +abstract class OutputService implements ModuleService { + /// The default location written by [write]. + /// + /// Interpreted as a directory when [multiFile] is `true`, otherwise as a + /// single file path. May be `null` when no default has been configured, in + /// which case a path must be passed to [write]. + String? get outputPath; + + /// Whether [write] emits one file per module definition (`true`) or a single + /// combined file (`false`). + bool get multiFile; + + /// Writes this service's output to [path], or to [outputPath] when [path] is + /// omitted. + void write([String? path]); +} + +/// An [OutputService] that generates source-code text, keyed per module +/// definition. +/// +/// Shared by the language code-generation services (e.g. SystemVerilog and +/// SystemC), which all produce a combined single-file [output] as well as +/// per-definition contents. +abstract class CodegenService extends OutputService { + /// The combined single-file generated output (including any header). + String get output; + + /// The generated output keyed by module definition name + /// ([Module.definitionName]). + Map get contentsByDefinitionName; + + /// The generated output for a single module [definitionName], or `null` when + /// that definition was not generated. + String? moduleOutput(String definitionName) => + contentsByDefinitionName[definitionName]; +} diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart index 0e0f431a4..98f2341f7 100644 --- a/lib/src/diagnostics/module_services.dart +++ b/lib/src/diagnostics/module_services.dart @@ -2,68 +2,27 @@ // SPDX-License-Identifier: BSD-3-Clause // // module_services.dart -// Singleton service registry for DevTools integration. +// Slim, type-keyed registry of module-scoped services for DevTools and other +// inspection tools. // // 2026 April 25 // Author: Desmond Kirkpatrick -import 'dart:convert'; -import 'dart:io'; - import 'package:rohd/rohd.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; -/// The read-only netlist surface used by [ModuleServices]. -/// -/// Feature branches provide concrete implementations without making this -/// package layer depend on netlist synthesis classes. -abstract interface class NetlistInspectionService { - /// Returns a compact hierarchy-oriented netlist JSON string. - String get slimJson; - - /// Returns the full netlist hierarchy JSON string. - String toJson(); - - /// Returns netlist JSON for a single module [definitionName]. - String moduleJson(String definitionName); -} - -/// The read-only source-trace surface used by [ModuleServices]. +/// A slim, type-keyed registry of [ModuleService]s. /// -/// Feature branches provide concrete implementations without making this -/// package layer depend on source-debug tracing classes. -abstract interface class TraceInspectionService { - /// The top-level module associated with this trace. - Module get module; - - /// Returns the FLC hierarchy object, or `null` when unavailable. - Map? get flcHierarchy; - - /// Returns the full FLC hierarchy JSON string. - String get flcJson; - - /// Returns FLC JSON for a single module [definitionName]. - String flcModuleJson(String definitionName); -} - -/// Singleton service registry that provides a unified query surface for -/// DevTools and other inspection tools. +/// Services register themselves here on construction (keyed by their concrete +/// type) and are retrieved with [lookup]. The registry intentionally exposes +/// no per-format accessors: each service owns its own JSON and output methods, +/// reached through [lookup] or the service's own static `current` accessor. /// -/// Services register themselves here on construction; DevTools evaluates -/// getters on [instance] via `EvalOnDartLibrary` to pull data. +/// The registry references no specific service type, so it is identical across +/// all feature branches that contribute services. /// /// **Auto-registered:** /// - [rootModule] / [hierarchyJSON] — set by [Module.build]. -/// -/// **Opt-in (registered by service constructors):** -/// - [svService] — SystemVerilog synthesis results. -/// - [netlistService] — netlist inspection data. -/// - [waveformService] — waveform capture (file output + optional streaming). -/// - [traceService] — source trace / FLC data. -/// -/// Concrete feature services implement the small inspection interfaces above, -/// so this branch stays a formal dependency without importing future feature -/// branches. class ModuleServices { ModuleServices._(); @@ -79,127 +38,36 @@ class ModuleServices { /// Returns the module hierarchy as a JSON string. /// - /// DevTools evaluates this via `EvalOnDartLibrary` to display - /// the module hierarchy. + /// DevTools evaluates this via `EvalOnDartLibrary` to display the module + /// hierarchy. Richer design views (e.g. a slim netlist) are composed by the + /// DevTools client from the relevant registered service. String get hierarchyJSON { ModuleTree.rootModuleInstance = rootModule; return ModuleTree.instance.hierarchyJSON; } - /// Returns the unified inspector JSON, the primary DevTools design entry - /// point. - /// - /// When a netlist inspection service is registered, this returns the compact - /// netlist view. Otherwise it falls back to the module hierarchy JSON. - String get inspectorJSON => netlistService?.slimJson ?? hierarchyJSON; - - /// Returns full inspector JSON for a single module [definitionName]. - String inspectorModuleJSON(String definitionName) => - netlistModuleJSON(definitionName); - - // ─── SystemVerilog service (opt-in) ─────────────────────────── + // ─── Type-keyed service registry ────────────────────────────── - /// The active [SvService], if one has been registered. - SvService? svService; + final Map _services = {}; - /// Returns SV synthesis metadata as JSON, or an unavailable status. - String get svJSON => - svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); - - // ─── Netlist service (opt-in) ───────────────────────────────── - - /// The active netlist inspection service, if one has been registered. - NetlistInspectionService? netlistService; - - /// Returns the full netlist hierarchy as JSON, or an unavailable status. - String get netlistJSON => netlistService != null - ? netlistService!.toJson() - : _unavailable('netlist'); - - /// Returns netlist JSON for a single module [definitionName]. - String netlistModuleJSON(String definitionName) => netlistService != null - ? netlistService!.moduleJson(definitionName) - : _unavailable('netlist'); - - // ─── Waveform service (opt-in) ─────────────────────────────── - - /// The active [WaveformService], if one has been registered. - WaveformService? waveformService; - - /// Returns waveform service metadata as JSON, or an unavailable status. - String get waveformJSON => waveformService != null - ? jsonEncode(waveformService!.toJson()) - : _unavailable('waveform'); - - // ─── Trace service (opt-in) ─────────────────────────────────── - - /// The active source-trace inspection service, if one has been registered. - TraceInspectionService? traceService; - - /// Cached path to the FLC file written by [flcFilePath]. - String? _flcFilePathCache; - - /// Returns the FLC hierarchy JSON, or an unavailable status. - String get flcJSON => - traceService != null ? traceService!.flcJson : _unavailable('trace'); - - /// Returns FLC JSON for a single module [definitionName]. - String flcModuleJSON(String definitionName) => traceService != null - ? traceService!.flcModuleJson(definitionName) - : _unavailable('trace'); - - /// Writes the FLC hierarchy to a temporary file and returns the path. + /// Registers [service] under the type argument [T]. /// - /// Returns a JSON error string when trace data is unavailable or the write - /// fails. - String get flcFilePath { - if (_flcFilePathCache != null) { - if (File(_flcFilePathCache!).existsSync()) { - return _flcFilePathCache!; - } - _flcFilePathCache = null; - } - - final service = traceService; - if (service == null) { - return _unavailable('trace'); - } - final hierarchy = service.flcHierarchy; - if (hierarchy == null) { - return _unavailable('trace'); - } - - try { - final dir = Directory('${Directory.systemTemp.path}/rohd_devtools_flc') - ..createSync(recursive: true); - final path = '${dir.path}/${service.module.definitionName}.flc.json'; - File(path).writeAsStringSync( - const JsonEncoder.withIndent(' ').convert(hierarchy), - ); - _flcFilePathCache = path; - return path; - } on Exception catch (error) { - return jsonEncode({ - 'status': 'error', - 'reason': error.toString(), - }); - } + /// Replaces any previously registered service of the same type. + void register(T service) { + _services[T] = service; } - // ─── Helpers ────────────────────────────────────────────────── + /// Returns the registered service of type [T], or `null` if none. + T? lookup() => _services[T] as T?; - static String _unavailable(String service) => jsonEncode({ - 'status': 'unavailable', - 'reason': '$service service not registered', - }); + /// Removes the registered service of type [T], if any. + void unregister() { + _services.remove(T); + } /// Resets all services. Intended for test teardown. void reset() { rootModule = null; - svService = null; - netlistService = null; - waveformService = null; - traceService = null; - _flcFilePathCache = null; + _services.clear(); } } diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index 336377e36..33eb83d6e 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -86,8 +86,12 @@ enum OverwritePolicy { /// } /// } /// ``` -class WaveformService { +class WaveformService implements ModuleService { + /// The most recently registered [WaveformService], or `null`. + static WaveformService? current; + /// The top-level [Module] being captured. + @override final Module module; /// Path of the output waveform file. @@ -219,7 +223,8 @@ class WaveformService { }); if (register) { - ModuleServices.instance.waveformService = this; + current = this; + ModuleServices.instance.register(this); } } @@ -411,6 +416,7 @@ class WaveformService { // ─── Inspection ─────────────────────────────────────────────── /// Returns a JSON-serialisable summary of this service. + @override Map toJson() => { 'outputPath': outputPath, 'format': format.name, diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart index 776b090b0..97cf0c45e 100644 --- a/lib/src/synthesizers/systemverilog/sv_service.dart +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -32,16 +32,31 @@ import 'package:rohd/src/utilities/timestamper.dart'; /// // Or get the concatenated output (like generateSynth): /// print(sv.allContents); /// ``` -class SvService { +class SvService extends CodegenService { /// The separator inserted between module definitions in the /// concatenated single-file output from [allContents]. /// /// Matches the format historically produced by `Module.generateSynth()`. static const moduleSeparator = '\n\n////////////////////\n\n'; + /// The most recently registered [SvService], or `null`. + static SvService? current; + /// The top-level [Module] being synthesized. + @override final Module module; + /// The default location written by [write]. + /// + /// A directory when [multiFile] is `true`, otherwise a single file path. + @override + final String? outputPath; + + /// Whether [write] emits one `.sv` file per module definition (`true`) or a + /// single concatenated file (`false`). + @override + final bool multiFile; + /// The underlying [SynthBuilder] that drove synthesis. late final SynthBuilder synthBuilder; @@ -52,9 +67,11 @@ class SvService { /// /// [module] must already be built. /// - /// If [outputPath] is provided, the concatenated SV output (with header) is - /// written to that file. The parent directory is created if needed. - SvService(this.module, {bool register = true, String? outputPath}) { + /// If [outputPath] is provided, output is written immediately: a directory + /// of per-module files when [multiFile] is `true`, otherwise the + /// concatenated SV output (with header) to that single file. + SvService(this.module, + {bool register = true, this.outputPath, this.multiFile = false}) { if (!module.hasBuilt) { throw Exception( 'Module must be built before creating SvService. ' @@ -66,13 +83,12 @@ class SvService { fileContents = synthBuilder.getSynthFileContents(); if (outputPath != null) { - final file = File(outputPath); - file.parent.createSync(recursive: true); - file.writeAsStringSync(synthOutput); + write(); } if (register) { - ModuleServices.instance.svService = this; + current = this; + ModuleServices.instance.register(this); } } @@ -99,7 +115,14 @@ class SvService { /// Returns the full single-file SystemVerilog output with header, /// identical to `Module.generateSynth()`. - String get synthOutput => synthHeader + allContents; + /// + /// Computed once and cached so the timestamped header is stable for the + /// lifetime of this service. + late final String synthOutput = synthHeader + allContents; + + /// The combined single-file generated output (alias for [synthOutput]). + @override + String get output => synthOutput; /// Returns a map from module definition name to its SV file contents. /// @@ -114,6 +137,7 @@ class SvService { /// /// This uses the original definition name (not uniquified), matching /// the keys used by FLC trace data. + @override Map get contentsByDefinitionName { final result = {}; for (final sr in synthesisResults) { @@ -138,9 +162,32 @@ class SvService { } } + /// Writes the SV output to [path], or to [outputPath] when [path] is omitted. + /// + /// When [multiFile] is `true`, writes one `.sv` file per module definition + /// into the target directory (see [writeFiles]); otherwise writes the + /// concatenated [synthOutput] to the target file. + @override + void write([String? path]) { + final target = path ?? outputPath; + if (target == null) { + throw ArgumentError( + 'No output path provided: pass a path to write() or set outputPath.', + ); + } + if (multiFile) { + writeFiles(target); + } else { + File(target) + ..parent.createSync(recursive: true) + ..writeAsStringSync(synthOutput); + } + } + /// Returns a JSON-serialisable summary of the SV synthesis. /// /// Contains the list of generated module definition names. + @override Map toJson() => { 'modules': [for (final fc in fileContents) fc.name], }; diff --git a/test/module_services_test.dart b/test/module_services_test.dart index 9179a6dd3..246263191 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause // // module_services_test.dart -// Unit tests for ModuleServices and SvService. +// Unit tests for ModuleServices, the service base types, and SvService. // // 2026 April 25 // Author: Desmond Kirkpatrick @@ -23,41 +23,21 @@ class SimpleModule extends Module { } } -class FakeNetlistService implements NetlistInspectionService { - @override - String get slimJson => jsonEncode({'kind': 'slim'}); - - @override - String toJson() => jsonEncode({'kind': 'full'}); - - @override - String moduleJson(String definitionName) => - jsonEncode({'module': definitionName}); -} - -class FakeTraceService implements TraceInspectionService { - FakeTraceService(this.module); +/// A minimal [ModuleService] used to exercise the type-keyed registry. +class FakeService implements ModuleService { + FakeService(this.module); @override final Module module; @override - Map? get flcHierarchy => { - 'modules': {module.definitionName: {}}, - }; - - @override - String get flcJson => jsonEncode(flcHierarchy); - - @override - String flcModuleJson(String definitionName) => - jsonEncode({'module': definitionName}); + Map toJson() => {'kind': 'fake'}; } void main() { tearDown(ModuleServices.instance.reset); - group('ModuleServices', () { + group('ModuleServices registry', () { test('rootModule is set after build', () async { final mod = SimpleModule(Logic()); await mod.build(); @@ -71,75 +51,51 @@ void main() { expect(() => jsonDecode(json), returnsNormally); }); - test('inspectorJSON matches hierarchyJSON', () async { + test('register and lookup round-trips a service', () async { final mod = SimpleModule(Logic()); await mod.build(); - expect( - ModuleServices.instance.inspectorJSON, - equals(ModuleServices.instance.hierarchyJSON), - ); + final fake = FakeService(mod); + ModuleServices.instance.register(fake); + expect(ModuleServices.instance.lookup(), same(fake)); }); - test('inspectorJSON uses registered netlist service', () async { - ModuleServices.instance.netlistService = FakeNetlistService(); - final inspectorJson = jsonDecode(ModuleServices.instance.inspectorJSON) - as Map; - final netlistJson = jsonDecode(ModuleServices.instance.netlistJSON) - as Map; - final moduleJson = jsonDecode( - ModuleServices.instance.inspectorModuleJSON('SimpleModule'), - ) as Map; - - expect(inspectorJson['kind'], equals('slim')); - expect(netlistJson['kind'], equals('full')); - expect(moduleJson['module'], equals('SimpleModule')); + test('lookup returns null when no service registered', () { + expect(ModuleServices.instance.lookup(), isNull); }); - test('trace service exposes FLC JSON and file path', () async { + test('unregister removes a service', () async { final mod = SimpleModule(Logic()); await mod.build(); - ModuleServices.instance.traceService = FakeTraceService(mod); - final flcJson = - jsonDecode(ModuleServices.instance.flcJSON) as Map; - final moduleJson = - jsonDecode(ModuleServices.instance.flcModuleJSON('SimpleModule')) - as Map; - - expect(flcJson, isA>()); - expect(moduleJson['module'], equals('SimpleModule')); - - final flcPath = ModuleServices.instance.flcFilePath; - expect(flcPath, isNot(startsWith('{'))); - expect(File(flcPath).existsSync(), isTrue); + ModuleServices.instance.register(FakeService(mod)); + ModuleServices.instance.unregister(); + expect(ModuleServices.instance.lookup(), isNull); }); - test('svJSON returns unavailable when no service registered', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - final result = - jsonDecode(ModuleServices.instance.svJSON) as Map; - expect(result['status'], equals('unavailable')); - }); - - test('reset clears all services', () async { + test('reset clears rootModule and all services', () async { final mod = SimpleModule(Logic()); await mod.build(); + ModuleServices.instance.register(FakeService(mod)); expect(ModuleServices.instance.rootModule, isNotNull); + ModuleServices.instance.reset(); expect(ModuleServices.instance.rootModule, isNull); - expect(ModuleServices.instance.svService, isNull); - expect(ModuleServices.instance.netlistService, isNull); - expect(ModuleServices.instance.waveformService, isNull); - expect(ModuleServices.instance.traceService, isNull); + expect(ModuleServices.instance.lookup(), isNull); }); }); group('SvService', () { - test('registers with ModuleServices on creation', () async { + test('registers with ModuleServices and sets current', () async { final mod = SimpleModule(Logic()); await mod.build(); final sv = SvService(mod); - expect(ModuleServices.instance.svService, equals(sv)); + expect(ModuleServices.instance.lookup(), same(sv)); + expect(SvService.current, same(sv)); + }); + + test('is a CodegenService', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(SvService(mod), isA()); }); test('allContents is non-empty', () async { @@ -149,6 +105,13 @@ void main() { expect(sv.allContents, isNotEmpty); }); + test('output equals synthOutput', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.output, equals(sv.synthOutput)); + }); + test('contentsByName has entries', () async { final mod = SimpleModule(Logic()); await mod.build(); @@ -164,13 +127,19 @@ void main() { expect(sv.contentsByDefinitionName.containsKey('SimpleModule'), isTrue); }); - test('svJSON returns valid JSON after registration', () async { + test('moduleOutput returns the definition contents', () async { final mod = SimpleModule(Logic()); await mod.build(); - SvService(mod); - final result = - jsonDecode(ModuleServices.instance.svJSON) as Map; - expect(result['modules'], isList); + final sv = SvService(mod); + expect(sv.moduleOutput('SimpleModule'), isNotNull); + expect(sv.moduleOutput('DoesNotExist'), isNull); + }); + + test('toJson lists generated modules', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.toJson()['modules'], isList); }); test('writeFiles creates SV files', () async { @@ -188,12 +157,40 @@ void main() { } }); + test('write() emits a single file', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod, register: false); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + final path = '${dir.path}/out.sv'; + sv.write(path); + expect(File(path).readAsStringSync(), equals(sv.synthOutput)); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('write() with multiFile emits a directory of files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + // Construction with outputPath writes immediately. + SvService(mod, register: false, outputPath: dir.path, multiFile: true); + final files = dir.listSync().whereType().toList(); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + test('register false does not register', () async { final mod = SimpleModule(Logic()); await mod.build(); ModuleServices.instance.reset(); SvService(mod, register: false); - expect(ModuleServices.instance.svService, isNull); + expect(ModuleServices.instance.lookup(), isNull); }); test('throws if module not built', () { From 5eb94a81283621c8f4e923eb60bdb79a0f47bc91 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 23 Jun 2026 11:09:37 -0700 Subject: [PATCH 45/58] Keep generateSynth as a non-deprecated facade over SvService Defer deprecation of Module.generateSynth so simcompare and other call sites need not migrate per-branch; this keeps simcompare.dart identical to base and removes a recurring cross-branch merge conflict. SvService remains the richer API. --- lib/src/module.dart | 7 +++---- lib/src/utilities/simcompare.dart | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index c045e0a48..d5cf5edb5 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1135,10 +1135,9 @@ abstract class Module { /// Currently returns one long file in SystemVerilog, but in the future /// may have other output formats, languages, files, etc. /// - /// Deprecated: use [SvService] instead, which provides richer access to - /// per-module file contents, named maps, and individual file writing. - /// For the legacy one-shot API, prefer [SvService.synthOutput]. - @Deprecated('Use SvService instead.') + /// For richer access to per-module file contents, named maps, and individual + /// file writing, see [SvService] (and [SvService.synthOutput] for the + /// equivalent one-shot string). String generateSynth() { if (!_hasBuilt) { throw ModuleNotBuiltException(this); diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index eb1c736f5..b2e67a6a8 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -327,7 +327,7 @@ abstract class SimCompare { allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); final moduleInstance = '$topModule dut($moduleConnections);'; final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = SvService(module).synthOutput; + final generatedVerilog = module.generateSynth(); // so that when they run in parallel, they dont step on each other final uniqueId = From 249b2101b9ce546d4217b213c27ed3bb4faf159e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 07:36:46 -0700 Subject: [PATCH 46/58] Clean up Namer allocation API --- .../utilities/synth_module_definition.dart | 16 +-- lib/src/utilities/namer.dart | 47 +++---- test/naming_consistency_test.dart | 13 +- test/signal_registry_test.dart | 120 ++++++++++-------- 4 files changed, 98 insertions(+), 98 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index f1fbb3931..13589a4dc 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -19,13 +19,6 @@ import 'package:rohd/src/utilities/namer.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. class _BusSubsetForStructSlice extends BusSubset { - /// The stable destination [Logic] this slice drives. - /// - /// Used as the [instanceNameKey] so that, although a fresh - /// [_BusSubsetForStructSlice] is created on every synthesis pass, its - /// canonical instance name is memoized against the persistent destination - /// signal and therefore does not drift run-to-run. - /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. _BusSubsetForStructSlice( @@ -766,11 +759,10 @@ class SynthModuleDefinition { /// Picks names of signals and sub-modules. /// - /// Signal names are read from [Namer.signalNameOf] for user-created - /// [Logic] objects) or kept as literal constants and are allocated from - /// [Namer.signalNameOf]. Submodule instance names are allocated - /// from [Namer.allocateName]. All names share a single - /// namespace managed by the module's [Namer]. + /// Signal names are selected through [Namer.signalNameOfBest] or kept as + /// literal constants. Submodule names are selected through + /// [Namer.instanceNameOf]. All non-constant names share a single namespace + /// managed by the module's [Namer]. void _pickNames() { // Name allocation order matters — earlier claims get the unsuffixed name // when there are collisions. This matches production ROHD priority: diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 478e0fe3b..70b0a6e47 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -20,7 +20,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// ensuring no name collisions in the generated SystemVerilog. /// /// Port names are reserved at construction time. Internal signal names -/// are assigned lazily on the first [signalNameOf] call. Instance names +/// are assigned lazily on the first [signalNameOfBest] call. Instance names /// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { @@ -34,7 +34,7 @@ class Namer { /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. /// - /// Instance-name allocation mutates [_uniquifier]. Without this cache, + /// Instance-name lookup claims names in [_uniquifier]. Without this cache, /// repeated synthesis passes over the same module hierarchy would allocate /// fresh suffixes for the same submodule instances. final Map _instanceNames = {}; @@ -44,10 +44,8 @@ class Namer { // ─── Construction ─────────────────────────────────────────────── - Namer._({ - required Uniquifier uniquifier, - required Set portLogics, - }) : _uniquifier = uniquifier, + Namer._({required Uniquifier uniquifier, required Set portLogics}) + : _uniquifier = uniquifier, _portLogics = portLogics; /// Creates a [Namer] for the given [module]'s ports. @@ -66,10 +64,7 @@ class Namer { uniquifier.getUniqueName(initialName: logic.name, reserved: true); } - return Namer._( - uniquifier: uniquifier, - portLogics: portLogics, - ); + return Namer._(uniquifier: uniquifier, portLogics: portLogics); } // ─── Name availability / allocation ───────────────────────────── @@ -77,16 +72,6 @@ class Namer { /// Returns `true` if [name] has not yet been claimed in the namespace. bool isAvailable(String name) => _uniquifier.isAvailable(name); - /// Allocates a collision-free name in the shared namespace. - /// - /// When [reserved] is `true`, the exact [baseName] (after sanitization) - /// is claimed without modification; an exception is thrown if it collides. - String allocateName(String baseName, {bool reserved = false}) => - _uniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(baseName), - reserved: reserved, - ); - // ─── Instance naming (Module → String) ────────────────────────── /// Returns the canonical instance name for [submodule]. @@ -100,8 +85,8 @@ class Namer { return cached; } - final name = allocateName( - submodule.uniqueInstanceName, + final name = _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(submodule.uniqueInstanceName), reserved: submodule.reserveName, ); _instanceNames[key] = name; @@ -115,7 +100,7 @@ class Namer { /// The first call for a given [logic] allocates a collision-free name /// via the underlying [Uniquifier]. Subsequent calls return the cached /// result in O(1). - String signalNameOf(Logic logic) { + String _signalNameOf(Logic logic) { final cached = _signalNames[logic]; if (cached != null) { return cached; @@ -220,15 +205,17 @@ class Namer { } if (preferredMergeable.isNotEmpty) { - final best = preferredMergeable - .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? + final best = preferredMergeable.firstWhereOrNull( + (e) => isAvailable(baseName(e)), + ) ?? preferredMergeable.first; return _nameAndCacheAll(best, candidates); } if (unpreferredMergeable.isNotEmpty) { - final best = unpreferredMergeable - .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? + final best = unpreferredMergeable.firstWhereOrNull( + (e) => isAvailable(baseName(e)), + ) ?? unpreferredMergeable.first; return _nameAndCacheAll(best, candidates); } @@ -243,10 +230,10 @@ class Namer { throw StateError('No Logic candidates to name.'); } - /// Names [chosen] via [signalNameOf], then caches the same name for all - /// other non-port [Logic]s in [all]. + /// Names [chosen] with the single-signal allocator, then caches the + /// same name for all other non-port [Logic]s in [all]. String _nameAndCacheAll(Logic chosen, Iterable all) { - final name = signalNameOf(chosen); + final name = _signalNameOf(chosen); for (final logic in all) { if (!identical(logic, chosen) && !_portLogics.contains(logic)) { _signalNames[logic] = name; diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index ba5e161bf..246862b14 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -201,27 +201,28 @@ void main() { } }); - test('Namer.signalNameOf matches SynthLogic.name for ports', () async { + test('Namer.signalNameOfBest matches SynthLogic.name for ports', () async { final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); final def = SynthModuleDefinition(mod); final synthNames = _collectNames(def); - // Module.namer.signalNameOf uses Namer directly + // Module.namer.signalNameOfBest uses Namer directly for (final port in [...mod.inputs.values, ...mod.outputs.values]) { - final moduleName = mod.namer.signalNameOf(port); + final moduleName = mod.namer.signalNameOfBest([port]); final synthName = synthNames[port]; expect(synthName, moduleName, - reason: 'SynthLogic.name and Module.namer.signalNameOf must agree ' + reason: + 'SynthLogic.name and Module.namer.signalNameOfBest must agree ' 'for port ${port.name}'); } }); test('submodule instance names are allocated from the shared namespace', () async { - // Instance names come from Module.namer.instanceNameOf, which shares the - // same namespace as signal names. + // Instance names come from Module.namer.instanceNameOf, + // which shares the same namespace as signal names. final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart index ffae4ae5f..2a2ded5ef 100644 --- a/test/signal_registry_test.dart +++ b/test/signal_registry_test.dart @@ -33,16 +33,18 @@ class _Counter extends Module { final val = addOutput('val', width: width); final nextVal = Logic(name: 'nextVal', width: width); nextVal <= val + 1; - Sequential.multi([ - SimpleClockGenerator(10).clk, - reset, - ], [ - If(reset, then: [ - val < 0, - ], orElse: [ - If(en, then: [val < nextVal]), - ]), - ]); + Sequential.multi( + [SimpleClockGenerator(10).clk, reset], + [ + If( + reset, + then: [val < 0], + orElse: [ + If(en, then: [val < nextVal]), + ], + ), + ], + ); } } @@ -56,60 +58,77 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); - expect(mod.namer.signalNameOf(mod.input('b')), equals('b')); - expect(mod.namer.signalNameOf(mod.output('a_bar')), equals('a_bar')); - expect(mod.namer.signalNameOf(mod.output('a_and_b')), equals('a_and_b')); + expect(mod.namer.signalNameOfBest([mod.input('a')]), equals('a')); + expect(mod.namer.signalNameOfBest([mod.input('b')]), equals('b')); + expect( + mod.namer.signalNameOfBest([mod.output('a_bar')]), + equals('a_bar'), + ); + expect( + mod.namer.signalNameOfBest([mod.output('a_and_b')]), + equals('a_and_b'), + ); }); test('returns internal signal names', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); - expect(mod.namer.signalNameOf(mod.input('en')), equals('en')); - expect(mod.namer.signalNameOf(mod.input('reset')), equals('reset')); - expect(mod.namer.signalNameOf(mod.output('val')), equals('val')); + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + expect(mod.namer.signalNameOfBest([mod.output('val')]), equals('val')); }); - test('agrees with signalName after synth', () async { + test('agrees with signalNameOfBest after synth', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); for (final entry in mod.inputs.entries) { expect( - mod.namer.signalNameOf(entry.value), + mod.namer.signalNameOfBest([entry.value]), isNotNull, - reason: 'signalName should work for input ${entry.key}', + reason: 'signalNameOfBest should work for input ${entry.key}', ); } for (final entry in mod.outputs.entries) { expect( - mod.namer.signalNameOf(entry.value), + mod.namer.signalNameOfBest([entry.value]), isNotNull, - reason: 'signalName should work for output ${entry.key}', + reason: 'signalNameOfBest should work for output ${entry.key}', ); } }); }); - group('allocateName', () { + group('single-signal allocation', () { test('avoids collision with existing names', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); - final allocated = mod.namer.allocateName('en'); - expect(allocated, isNot(equals('en')), - reason: 'Should not collide with existing port name'); - expect(allocated, contains('en'), - reason: 'Should be based on the requested name'); + final sig = Logic(name: 'en', naming: Naming.renameable); + final allocated = mod.namer.signalNameOfBest([sig]); + expect( + allocated, + isNot(equals('en')), + reason: 'Should not collide with existing port name', + ); + expect( + allocated, + contains('en'), + reason: 'Should be based on the requested name', + ); }); test('successive allocations are unique', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); - final a = mod.namer.allocateName('wire'); - final b = mod.namer.allocateName('wire'); + final a = mod.namer.signalNameOfBest([ + Logic(name: 'wire', naming: Naming.renameable), + ]); + final b = mod.namer.signalNameOfBest([ + Logic(name: 'wire', naming: Naming.renameable), + ]); expect(a, isNot(equals(b)), reason: 'Each allocation should be unique'); }); }); @@ -119,7 +138,7 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.namer.signalNameOfBest([mod.input('a')]), equals('a')); expect(mod.input('a').name, equals('a')); }); }); @@ -130,7 +149,8 @@ void main() { final mod = _Counter(Logic(), Logic()); await mod.build(); return { - for (final sig in mod.signals) sig.name: mod.namer.signalNameOf(sig), + for (final sig in mod.signals) + sig.name: mod.namer.signalNameOfBest([sig]), }; } @@ -165,17 +185,20 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - final name = mod.namer.allocateName('wire'); + final name = mod.namer.signalNameOfBest([ + Logic(name: 'wire', naming: Naming.renameable), + ]); expect(mod.namer.isAvailable(name), isFalse); }); }); - group('allocateName reserved', () { - test('reserved allocation claims exact name', () async { + group('reserved single-signal allocation', () { + test('reserved signal claims exact name', () async { final mod = _GateMod(Logic(), Logic()); await mod.build(); - final name = mod.namer.allocateName('my_wire', reserved: true); + final sig = Logic(name: 'my_wire', naming: Naming.reserved); + final name = mod.namer.signalNameOfBest([sig]); expect(name, equals('my_wire')); expect(mod.namer.isAvailable('my_wire'), isFalse); }); @@ -184,9 +207,10 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - // 'a' is already a port name expect( - () => mod.namer.allocateName('a', reserved: true), + () => mod.namer.signalNameOfBest([ + Logic(name: 'a', naming: Naming.reserved), + ]), throwsException, ); }); @@ -217,10 +241,7 @@ void main() { final c = Const(LogicValue.ofString('01')); final sig = Logic(name: 'x'); - final name = mod.namer.signalNameOfBest( - [sig], - constValue: c, - ); + final name = mod.namer.signalNameOfBest([sig], constValue: c); expect(name, equals(c.value.toString())); }); @@ -274,8 +295,10 @@ void main() { await mod.build(); final preferred = Logic(name: 'good', naming: Naming.mergeable); - final unpreferred = - Logic(name: Naming.unpreferredName('bad'), naming: Naming.mergeable); + final unpreferred = Logic( + name: Naming.unpreferredName('bad'), + naming: Naming.mergeable, + ); final name = mod.namer.signalNameOfBest([unpreferred, preferred]); expect(name, contains('good')); }); @@ -289,18 +312,15 @@ void main() { final name = mod.namer.signalNameOfBest([s1, s2]); // Both should resolve to the same cached name - expect(mod.namer.signalNameOf(s1), equals(name)); - expect(mod.namer.signalNameOf(s2), equals(name)); + expect(mod.namer.signalNameOfBest([s1]), equals(name)); + expect(mod.namer.signalNameOfBest([s2]), equals(name)); }); test('empty candidates throws', () async { final mod = _GateMod(Logic(), Logic()); await mod.build(); - expect( - () => mod.namer.signalNameOfBest([]), - throwsA(isA()), - ); + expect(() => mod.namer.signalNameOfBest([]), throwsA(isA())); }); test('unnamed signals get a name', () async { From c0f9a080c544733d83c251abbc19db67b3f34843 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 07:36:46 -0700 Subject: [PATCH 47/58] Clean up Namer allocation API --- .../utilities/synth_module_definition.dart | 252 ++++++++++-------- lib/src/utilities/namer.dart | 26 +- test/naming_consistency_test.dart | 179 ++++++++----- test/signal_registry_test.dart | 120 +++++---- 4 files changed, 333 insertions(+), 244 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 001f32eef..bc6af3376 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -19,12 +19,6 @@ import 'package:rohd/src/utilities/namer.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. class _BusSubsetForStructSlice extends BusSubset { - /// The stable destination [Logic] this slice drives. - /// - /// Used as the [instanceNameKey] so that, although a fresh - /// [_BusSubsetForStructSlice] is created on every synthesis pass, its - /// canonical instance name is memoized against the persistent destination - /// signal and therefore does not drift run-to-run. final Logic _destination; /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during @@ -141,9 +135,7 @@ class SynthModuleDefinition { /// Either accesses a previously created [SynthLogic] corresponding to /// [logic], or else creates a new one and adds it to the [logicToSynthMap]. - SynthLogic? getSynthLogic( - Logic? logic, - ) { + SynthLogic? getSynthLogic(Logic? logic) { if (logic == null) { return null; } else if (!(logic.parentModule == module || @@ -253,8 +245,14 @@ class SynthModuleDefinition { for (final leafElement in port.leafElements) { final leafSynth = getSynthLogic(leafElement)!; internalSignals.add(leafSynth); - assignments.add(PartialSynthAssignment(leafSynth, portSynth, - dstUpperIndex: idx + leafElement.width - 1, dstLowerIndex: idx)); + assignments.add( + PartialSynthAssignment( + leafSynth, + portSynth, + dstUpperIndex: idx + leafElement.width - 1, + dstLowerIndex: idx, + ), + ); idx += leafElement.width; } } @@ -275,7 +273,9 @@ class SynthModuleDefinition { // this is DISCONNECTED, just a module used for synthesizing final subsetMod = _BusSubsetForStructSlice( (port.isNet ? LogicNet.new : Logic.new)( - width: port.width, name: 'DUMMY'), + width: port.width, + name: 'DUMMY', + ), idx, idx + leafElement.width - 1, destination: leafElement, @@ -300,11 +300,11 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) : assert( - !(module is SystemVerilog && - module.generatedDefinitionType == - DefinitionGenerationType.none), - 'Do not build a definition for a module' - ' which generates no definition!') { + !(module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none), + 'Do not build a definition for a module' + ' which generates no definition!', + ) { // start by traversing output signals final logicsToTraverse = TraverseableCollection() ..addAll(module.outputs.values) @@ -343,8 +343,9 @@ class SynthModuleDefinition { // find any named signals sitting around that don't do anything // this is not necessary for functionality, just nice naming inclusion logicsToTraverse.addAll( - module.internalSignals - .where((element) => element.naming != Naming.unnamed), + module.internalSignals.where( + (element) => element.naming != Naming.unnamed, + ), ); // make sure floating modules are included @@ -377,9 +378,10 @@ class SynthModuleDefinition { final receiver = logicsToTraverse[i]; assert( - receiver.parentModule != null, - 'Any signal traced by this should have been detected by build,' - ' but $receiver was not.'); + receiver.parentModule != null, + 'Any signal traced by this should have been detected by build,' + ' but $receiver was not.', + ); if (receiver.parentModule != module && !module.subModules.contains(receiver.parentModule)) { @@ -402,10 +404,12 @@ class SynthModuleDefinition { if (receiver is LogicNet) { // only for the leaves, that's why only `LogicNet` and not array/struct - logicsToTraverse.addAll([ - ...receiver.srcConnections, - ...receiver.dstConnections - ].where((element) => element.parentModule == module)); + logicsToTraverse.addAll( + [ + ...receiver.srcConnections, + ...receiver.dstConnections, + ].where((element) => element.parentModule == module), + ); for (final srcConnection in receiver.srcConnections) { if (srcConnection.parentModule == module || @@ -413,10 +417,7 @@ class SynthModuleDefinition { srcConnection.parentModule!.parent == module)) { final netSynthDriver = getSynthLogic(srcConnection)!; - assignments.add(SynthAssignment( - netSynthDriver, - synthReceiver, - )); + assignments.add(SynthAssignment(netSynthDriver, synthReceiver)); } } } @@ -445,10 +446,11 @@ class SynthModuleDefinition { inOuts.add(synthReceiver); } else { assert( - !inputs.contains(synthReceiver) && - !outputs.contains(synthReceiver) && - !inOuts.contains(synthReceiver), - 'Internal signals should not be ports also.'); + !inputs.contains(synthReceiver) && + !outputs.contains(synthReceiver) && + !inOuts.contains(synthReceiver), + 'Internal signals should not be ports also.', + ); internalSignals.add(synthReceiver); } @@ -459,8 +461,9 @@ class SynthModuleDefinition { if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setInOutMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setInOutMapping(receiver.name, synthReceiver); } logicsToTraverse.addAll(subModule.inOuts.values); @@ -475,8 +478,9 @@ class SynthModuleDefinition { // array elements are not named ports, just contained in array if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setOutputMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setOutputMapping(receiver.name, synthReceiver); } logicsToTraverse @@ -507,8 +511,9 @@ class SynthModuleDefinition { // array elements are not named ports, just contained in array if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setInputMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setInputMapping(receiver.name, synthReceiver); } } } @@ -581,8 +586,9 @@ class SynthModuleDefinition { final logics = internalSignal.logics; if (internalSignal.isArray) { - if (logics.any((logicArray) => - logicArray.elements.any(logicHasPresentSynthLogic))) { + if (logics.any( + (logicArray) => logicArray.elements.any(logicHasPresentSynthLogic), + )) { // if it's an array, can only remove if all elements are removed reducedInternalSignals.add(internalSignal); } else { @@ -594,26 +600,31 @@ class SynthModuleDefinition { continue; } - final isCustomSvModPort = logics.any((logic) => - logic.isPort && - isSubmoduleAndPresent(logic.parentModule) && - ((logic.parentModule! is SystemVerilog && - !(logic.parentModule! as SystemVerilog) - .acceptsEmptyPortConnections) || - // ignore: deprecated_member_use_from_same_package - logic.parentModule! is CustomSystemVerilog)); + final isCustomSvModPort = logics.any( + (logic) => + logic.isPort && + isSubmoduleAndPresent(logic.parentModule) && + ((logic.parentModule! is SystemVerilog && + !(logic.parentModule! as SystemVerilog) + .acceptsEmptyPortConnections) || + // ignore: deprecated_member_use_from_same_package + logic.parentModule! is CustomSystemVerilog), + ); if (!isCustomSvModPort) { if (internalSignal.isNet) { final anyInternalConnections = [ ...internalSignal.srcConnections, - ...internalSignal.dstConnections + ...internalSignal.dstConnections, ] - .where((e) => - (e.parentModule == module || - ( // in case of sub-module output driving a net - e.parentModule?.parent == module && e.isOutput)) && - logicHasPresentSynthLogic(e)) + .where( + (e) => + (e.parentModule == module || + ( // in case of sub-module output driving a net + e.parentModule?.parent == module && + e.isOutput)) && + logicHasPresentSynthLogic(e), + ) .isNotEmpty; if (anyInternalConnections) { @@ -624,9 +635,11 @@ class SynthModuleDefinition { final connectedSubModules = logics .map((e) => e.parentModule) .nonNulls - .where((e) => - e != module && - getSynthSubModuleInstantiation(e).needsInstantiation) + .where( + (e) => + e != module && + getSynthSubModuleInstantiation(e).needsInstantiation, + ) .toSet(); if (connectedSubModules.length > 1) { @@ -637,13 +650,15 @@ class SynthModuleDefinition { // If the signal appears in multiple inout port mappings on the // same (single) connected submodule, it's a loopback and needs // a wire declaration so both ports can reference it by name. - final hasInOutLoopback = connectedSubModules.any((m) => - getSynthSubModuleInstantiation(m) - .inOutMapping - .values - .where((v) => v == internalSignal) - .length > - 1); + final hasInOutLoopback = connectedSubModules.any( + (m) => + getSynthSubModuleInstantiation(m) + .inOutMapping + .values + .where((v) => v == internalSignal) + .length > + 1, + ); if (hasInOutLoopback) { reducedInternalSignals.add(internalSignal); @@ -701,39 +716,44 @@ class SynthModuleDefinition { continue; } - for (final subModuleInstantiation - in subModuleInstantiations.where((e) => e.needsInstantiation)) { + for (final subModuleInstantiation in subModuleInstantiations.where( + (e) => e.needsInstantiation, + )) { final subModule = subModuleInstantiation.module; if (subModule is SystemVerilog && subModule.isWiresOnly) { final inputs = { ...subModuleInstantiation.inputMapping, - ...subModuleInstantiation.inOutMapping + ...subModuleInstantiation.inOutMapping, }; final outputs = { ...subModuleInstantiation.outputMapping, - ...subModuleInstantiation.inOutMapping + ...subModuleInstantiation.inOutMapping, }; // if all the inputs or all the outputs are not used, we can remove // the module - final allOutputsUnused = outputs.values.every((output) => - output.declarationCleared || - (output.isClearable && - !output.isStructPortElement() && - !output.hasDstConnectionsPresent())); + final allOutputsUnused = outputs.values.every( + (output) => + output.declarationCleared || + (output.isClearable && + !output.isStructPortElement() && + !output.hasDstConnectionsPresent()), + ); if (allOutputsUnused) { subModuleInstantiation.clearInstantiation(); changed = true; continue; } - final allInputsUnused = inputs.values.every((input) => - input.declarationCleared || - (input.isClearable && - !input.isStructPortElement() && - !input.hasSrcConnectionsPresent())); + final allInputsUnused = inputs.values.every( + (input) => + input.declarationCleared || + (input.isClearable && + !input.isStructPortElement() && + !input.hasSrcConnectionsPresent()), + ); if (allInputsUnused) { subModuleInstantiation.clearInstantiation(); changed = true; @@ -751,33 +771,38 @@ class SynthModuleDefinition { for (final inputName in submoduleInstantiation.module.inputs.keys) { final orig = submoduleInstantiation.inputMapping[inputName]!; submoduleInstantiation.setInputMapping( - inputName, orig.replacement ?? orig, - replace: true); + inputName, + orig.replacement ?? orig, + replace: true, + ); } for (final outputName in submoduleInstantiation.module.outputs.keys) { final orig = submoduleInstantiation.outputMapping[outputName]!; submoduleInstantiation.setOutputMapping( - outputName, orig.replacement ?? orig, - replace: true); + outputName, + orig.replacement ?? orig, + replace: true, + ); } for (final inOutName in submoduleInstantiation.module.inOuts.keys) { final orig = submoduleInstantiation.inOutMapping[inOutName]!; submoduleInstantiation.setInOutMapping( - inOutName, orig.replacement ?? orig, - replace: true); + inOutName, + orig.replacement ?? orig, + replace: true, + ); } } } /// Picks names of signals and sub-modules. /// - /// Signal names are read from [Namer.signalNameOf] for user-created - /// [Logic] objects) or kept as literal constants and are allocated from - /// [Namer.signalNameOf]. Submodule instance names are allocated - /// from [Namer.allocateName]. All names share a single - /// namespace managed by the module's [Namer]. + /// Signal names are selected through [Namer.signalNameOfBest] or kept as + /// literal constants. Submodule names are selected through + /// [Namer.instanceNameOf]. All non-constant names share a single namespace + /// managed by the module's [Namer]. void _pickNames() { // Name allocation order matters — earlier claims get the unsuffixed name // when there are collisions. This matches production ROHD priority: @@ -800,8 +825,10 @@ class SynthModuleDefinition { for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { submodule.pickName(module); - assert(submodule.module.name == submodule.name, - 'Expect reserved names to retain their name.'); + assert( + submodule.module.name == submodule.name, + 'Expect reserved names to retain their name.', + ); } } @@ -867,9 +894,10 @@ class SynthModuleDefinition { for (final MapEntry(key: (srcArray, dstArray), value: arrAssignments) in groupedAssignments.entries) { assert( - srcArray.logics.first.elements.length == - dstArray.logics.first.elements.length, - 'should be equal lengths of elements in both arrays by now'); + srcArray.logics.first.elements.length == + dstArray.logics.first.elements.length, + 'should be equal lengths of elements in both arrays by now', + ); // first requirement is that all elements have been assigned var shouldMerge = @@ -924,8 +952,10 @@ class SynthModuleDefinition { assignments.where((a) => !a.src.isConstant && !a.dst.isConstant), assignments.where((a) => a.src.isConstant || a.dst.isConstant), ])) { - assert(assignment is! PartialSynthAssignment, - 'Partial assignments should have been removed before this.'); + assert( + assignment is! PartialSynthAssignment, + 'Partial assignments should have been removed before this.', + ); final dst = assignment.dst; final src = assignment.src; @@ -937,8 +967,10 @@ class SynthModuleDefinition { continue; } - assert(dst != src, - 'No circular assignment allowed between $dst and $src.'); + assert( + dst != src, + 'No circular assignment allowed between $dst and $src.', + ); final mergeResults = SynthLogic.tryMerge(dst, src); @@ -978,14 +1010,18 @@ class SynthModuleDefinition { /// Performs updates to this definition after merging away a signal as part of /// [_collapseAssignments]. - void _applyAssignmentMergeUpdates( - {required SynthLogic mergedAway, required SynthLogic kept}) { + void _applyAssignmentMergeUpdates({ + required SynthLogic mergedAway, + required SynthLogic kept, + }) { final foundInternal = internalSignals.remove(mergedAway); if (!foundInternal) { final foundKept = internalSignals.remove(kept); - assert(foundKept, - 'One of the two should be internal since we cant merge ports.'); + assert( + foundKept, + 'One of the two should be internal since we cant merge ports.', + ); if (inputs.contains(mergedAway)) { inputs @@ -1009,8 +1045,8 @@ class SynthModuleDefinition { // should all be the same synth, and arrays only merge with arrays final keptElement = getSynthLogic(keptElementLogic)!; final mergedAwayElement = getSynthLogic( - (mergedAway.logics.first as LogicArray) - .elements[keptElementIndex])!; + (mergedAway.logics.first as LogicArray).elements[keptElementIndex], + )!; if (keptElement == mergedAwayElement) { continue; @@ -1019,7 +1055,9 @@ class SynthModuleDefinition { keptElement.adopt(mergedAwayElement, force: true); _applyAssignmentMergeUpdates( - mergedAway: mergedAwayElement, kept: keptElement); + mergedAway: mergedAwayElement, + kept: keptElement, + ); } } } diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 478e0fe3b..c5dd39a2d 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -20,7 +20,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// ensuring no name collisions in the generated SystemVerilog. /// /// Port names are reserved at construction time. Internal signal names -/// are assigned lazily on the first [signalNameOf] call. Instance names +/// are assigned lazily on the first [signalNameOfBest] call. Instance names /// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { @@ -34,7 +34,7 @@ class Namer { /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. /// - /// Instance-name allocation mutates [_uniquifier]. Without this cache, + /// Instance-name lookup claims names in [_uniquifier]. Without this cache, /// repeated synthesis passes over the same module hierarchy would allocate /// fresh suffixes for the same submodule instances. final Map _instanceNames = {}; @@ -77,16 +77,6 @@ class Namer { /// Returns `true` if [name] has not yet been claimed in the namespace. bool isAvailable(String name) => _uniquifier.isAvailable(name); - /// Allocates a collision-free name in the shared namespace. - /// - /// When [reserved] is `true`, the exact [baseName] (after sanitization) - /// is claimed without modification; an exception is thrown if it collides. - String allocateName(String baseName, {bool reserved = false}) => - _uniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(baseName), - reserved: reserved, - ); - // ─── Instance naming (Module → String) ────────────────────────── /// Returns the canonical instance name for [submodule]. @@ -100,8 +90,8 @@ class Namer { return cached; } - final name = allocateName( - submodule.uniqueInstanceName, + final name = _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(submodule.uniqueInstanceName), reserved: submodule.reserveName, ); _instanceNames[key] = name; @@ -115,7 +105,7 @@ class Namer { /// The first call for a given [logic] allocates a collision-free name /// via the underlying [Uniquifier]. Subsequent calls return the cached /// result in O(1). - String signalNameOf(Logic logic) { + String _signalNameOf(Logic logic) { final cached = _signalNames[logic]; if (cached != null) { return cached; @@ -243,10 +233,10 @@ class Namer { throw StateError('No Logic candidates to name.'); } - /// Names [chosen] via [signalNameOf], then caches the same name for all - /// other non-port [Logic]s in [all]. + /// Names [chosen] with the single-signal allocator, then caches the + /// same name for all other non-port [Logic]s in [all]. String _nameAndCacheAll(Logic chosen, Iterable all) { - final name = signalNameOf(chosen); + final name = _signalNameOf(chosen); for (final logic in all) { if (!identical(logic, chosen) && !_portLogics.contains(logic)) { _signalNames[logic] = name; diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index ba5e161bf..71af40d48 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -110,20 +110,32 @@ void main() { // Every Logic present in both must have the same name. for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name} ' - '(${logic.runtimeType}, naming=${logic.naming})'); + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name} ' + '(${logic.runtimeType}, naming=${logic.naming})', + ); } } // Port names specifically must match. for (final port in [...mod.inputs.values, ...mod.outputs.values]) { - expect(svNames[port], isNotNull, - reason: 'SV def should have port ${port.name}'); - expect(baseNames[port], isNotNull, - reason: 'Base def should have port ${port.name}'); - expect(svNames[port], baseNames[port], - reason: 'Port name must match for ${port.name}'); + expect( + svNames[port], + isNotNull, + reason: 'SV def should have port ${port.name}', + ); + expect( + baseNames[port], + isNotNull, + reason: 'Base def should have port ${port.name}', + ); + expect( + svNames[port], + baseNames[port], + reason: 'Port name must match for ${port.name}', + ); } }); @@ -139,8 +151,11 @@ void main() { for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name}'); + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); } } }); @@ -157,8 +172,11 @@ void main() { for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name}'); + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); } } }); @@ -175,8 +193,11 @@ void main() { for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name}'); + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); } } }); @@ -194,76 +215,96 @@ void main() { for (final logic in names1.keys) { if (names2.containsKey(logic)) { - expect(names2[logic], names1[logic], - reason: 'Shared namer should produce same name for ' - '${logic.name}'); + expect( + names2[logic], + names1[logic], + reason: 'Shared namer should produce same name for ' + '${logic.name}', + ); } } }); - test('Namer.signalNameOf matches SynthLogic.name for ports', () async { + test('Namer.signalNameOfBest matches SynthLogic.name for ports', () async { final mod = _Outer(Logic(width: 8), Logic(width: 8)); await mod.build(); final def = SynthModuleDefinition(mod); final synthNames = _collectNames(def); - // Module.namer.signalNameOf uses Namer directly + // Module.namer.signalNameOfBest uses Namer directly for (final port in [...mod.inputs.values, ...mod.outputs.values]) { - final moduleName = mod.namer.signalNameOf(port); + final moduleName = mod.namer.signalNameOfBest([port]); final synthName = synthNames[port]; - expect(synthName, moduleName, - reason: 'SynthLogic.name and Module.namer.signalNameOf must agree ' - 'for port ${port.name}'); + expect( + synthName, + moduleName, + reason: + 'SynthLogic.name and Module.namer.signalNameOfBest must agree ' + 'for port ${port.name}', + ); } }); - test('submodule instance names are allocated from the shared namespace', - () async { - // Instance names come from Module.namer.instanceNameOf, which shares the - // same namespace as signal names. - final mod = _Outer(Logic(width: 8), Logic(width: 8)); - await mod.build(); - - final def = SynthModuleDefinition(mod); - - final instNames = def.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .toSet(); - - // The inner module instance should have a name - expect(instNames, isNotEmpty, - reason: 'Should have at least one submodule instance'); - - // Instance names are claimed in the shared namespace. - for (final name in instNames) { - expect(mod.namer.isAvailable(name), isFalse, + test( + 'submodule instance names are allocated from the shared namespace', + () async { + // Instance names come from Module.namer.instanceNameOf, + // which shares the same namespace as signal names. + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toSet(); + + // The inner module instance should have a name + expect( + instNames, + isNotEmpty, + reason: 'Should have at least one submodule instance', + ); + + // Instance names are claimed in the shared namespace. + for (final name in instNames) { + expect( + mod.namer.isAvailable(name), + isFalse, reason: 'Instance name "$name" should be claimed in the ' - 'namespace'); - } - }); - - test('submodule instance names are stable across repeated definitions', - () async { - final mod = _Outer(Logic(width: 8), Logic(width: 8)); - await mod.build(); - - final def1 = SynthModuleDefinition(mod); - final def2 = SynthModuleDefinition(mod); - - final names1 = def1.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .toList(); - final names2 = def2.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .toList(); - - expect(names2, names1, + 'namespace', + ); + } + }, + ); + + test( + 'submodule instance names are stable across repeated definitions', + () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = def1.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + final names2 = def2.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + + expect( + names2, + names1, reason: 'Repeated synthesis passes should reuse cached instance ' - 'names instead of drifting numeric suffixes.'); - }); + 'names instead of drifting numeric suffixes.', + ); + }, + ); }); } diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart index ffae4ae5f..2a2ded5ef 100644 --- a/test/signal_registry_test.dart +++ b/test/signal_registry_test.dart @@ -33,16 +33,18 @@ class _Counter extends Module { final val = addOutput('val', width: width); final nextVal = Logic(name: 'nextVal', width: width); nextVal <= val + 1; - Sequential.multi([ - SimpleClockGenerator(10).clk, - reset, - ], [ - If(reset, then: [ - val < 0, - ], orElse: [ - If(en, then: [val < nextVal]), - ]), - ]); + Sequential.multi( + [SimpleClockGenerator(10).clk, reset], + [ + If( + reset, + then: [val < 0], + orElse: [ + If(en, then: [val < nextVal]), + ], + ), + ], + ); } } @@ -56,60 +58,77 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); - expect(mod.namer.signalNameOf(mod.input('b')), equals('b')); - expect(mod.namer.signalNameOf(mod.output('a_bar')), equals('a_bar')); - expect(mod.namer.signalNameOf(mod.output('a_and_b')), equals('a_and_b')); + expect(mod.namer.signalNameOfBest([mod.input('a')]), equals('a')); + expect(mod.namer.signalNameOfBest([mod.input('b')]), equals('b')); + expect( + mod.namer.signalNameOfBest([mod.output('a_bar')]), + equals('a_bar'), + ); + expect( + mod.namer.signalNameOfBest([mod.output('a_and_b')]), + equals('a_and_b'), + ); }); test('returns internal signal names', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); - expect(mod.namer.signalNameOf(mod.input('en')), equals('en')); - expect(mod.namer.signalNameOf(mod.input('reset')), equals('reset')); - expect(mod.namer.signalNameOf(mod.output('val')), equals('val')); + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + expect(mod.namer.signalNameOfBest([mod.output('val')]), equals('val')); }); - test('agrees with signalName after synth', () async { + test('agrees with signalNameOfBest after synth', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); for (final entry in mod.inputs.entries) { expect( - mod.namer.signalNameOf(entry.value), + mod.namer.signalNameOfBest([entry.value]), isNotNull, - reason: 'signalName should work for input ${entry.key}', + reason: 'signalNameOfBest should work for input ${entry.key}', ); } for (final entry in mod.outputs.entries) { expect( - mod.namer.signalNameOf(entry.value), + mod.namer.signalNameOfBest([entry.value]), isNotNull, - reason: 'signalName should work for output ${entry.key}', + reason: 'signalNameOfBest should work for output ${entry.key}', ); } }); }); - group('allocateName', () { + group('single-signal allocation', () { test('avoids collision with existing names', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); - final allocated = mod.namer.allocateName('en'); - expect(allocated, isNot(equals('en')), - reason: 'Should not collide with existing port name'); - expect(allocated, contains('en'), - reason: 'Should be based on the requested name'); + final sig = Logic(name: 'en', naming: Naming.renameable); + final allocated = mod.namer.signalNameOfBest([sig]); + expect( + allocated, + isNot(equals('en')), + reason: 'Should not collide with existing port name', + ); + expect( + allocated, + contains('en'), + reason: 'Should be based on the requested name', + ); }); test('successive allocations are unique', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); - final a = mod.namer.allocateName('wire'); - final b = mod.namer.allocateName('wire'); + final a = mod.namer.signalNameOfBest([ + Logic(name: 'wire', naming: Naming.renameable), + ]); + final b = mod.namer.signalNameOfBest([ + Logic(name: 'wire', naming: Naming.renameable), + ]); expect(a, isNot(equals(b)), reason: 'Each allocation should be unique'); }); }); @@ -119,7 +138,7 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.namer.signalNameOfBest([mod.input('a')]), equals('a')); expect(mod.input('a').name, equals('a')); }); }); @@ -130,7 +149,8 @@ void main() { final mod = _Counter(Logic(), Logic()); await mod.build(); return { - for (final sig in mod.signals) sig.name: mod.namer.signalNameOf(sig), + for (final sig in mod.signals) + sig.name: mod.namer.signalNameOfBest([sig]), }; } @@ -165,17 +185,20 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - final name = mod.namer.allocateName('wire'); + final name = mod.namer.signalNameOfBest([ + Logic(name: 'wire', naming: Naming.renameable), + ]); expect(mod.namer.isAvailable(name), isFalse); }); }); - group('allocateName reserved', () { - test('reserved allocation claims exact name', () async { + group('reserved single-signal allocation', () { + test('reserved signal claims exact name', () async { final mod = _GateMod(Logic(), Logic()); await mod.build(); - final name = mod.namer.allocateName('my_wire', reserved: true); + final sig = Logic(name: 'my_wire', naming: Naming.reserved); + final name = mod.namer.signalNameOfBest([sig]); expect(name, equals('my_wire')); expect(mod.namer.isAvailable('my_wire'), isFalse); }); @@ -184,9 +207,10 @@ void main() { final mod = _GateMod(Logic(), Logic()); await mod.build(); - // 'a' is already a port name expect( - () => mod.namer.allocateName('a', reserved: true), + () => mod.namer.signalNameOfBest([ + Logic(name: 'a', naming: Naming.reserved), + ]), throwsException, ); }); @@ -217,10 +241,7 @@ void main() { final c = Const(LogicValue.ofString('01')); final sig = Logic(name: 'x'); - final name = mod.namer.signalNameOfBest( - [sig], - constValue: c, - ); + final name = mod.namer.signalNameOfBest([sig], constValue: c); expect(name, equals(c.value.toString())); }); @@ -274,8 +295,10 @@ void main() { await mod.build(); final preferred = Logic(name: 'good', naming: Naming.mergeable); - final unpreferred = - Logic(name: Naming.unpreferredName('bad'), naming: Naming.mergeable); + final unpreferred = Logic( + name: Naming.unpreferredName('bad'), + naming: Naming.mergeable, + ); final name = mod.namer.signalNameOfBest([unpreferred, preferred]); expect(name, contains('good')); }); @@ -289,18 +312,15 @@ void main() { final name = mod.namer.signalNameOfBest([s1, s2]); // Both should resolve to the same cached name - expect(mod.namer.signalNameOf(s1), equals(name)); - expect(mod.namer.signalNameOf(s2), equals(name)); + expect(mod.namer.signalNameOfBest([s1]), equals(name)); + expect(mod.namer.signalNameOfBest([s2]), equals(name)); }); test('empty candidates throws', () async { final mod = _GateMod(Logic(), Logic()); await mod.build(); - expect( - () => mod.namer.signalNameOfBest([]), - throwsA(isA()), - ); + expect(() => mod.namer.signalNameOfBest([]), throwsA(isA())); }); test('unnamed signals get a name', () async { From e35373af539b3417f9e1f2a754542069c3021676 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 10:00:07 -0700 Subject: [PATCH 48/58] Stabilize naming around collapsible synth objects --- ...systemverilog_synth_module_definition.dart | 167 +++---- .../utilities/synth_module_definition.dart | 433 +++++++++++++----- .../synth_sub_module_instantiation.dart | 75 +-- test/naming_consistency_test.dart | 228 ++++++--- 4 files changed, 574 insertions(+), 329 deletions(-) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 3c82c4a58..65a2c3181 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -17,29 +17,44 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { SystemVerilogSynthModuleDefinition(super.module); @override - void process() { + void prepareForNaming() { _replaceNetConnections(); - _collapseChainableModules(); + super.prepareForNaming(); + _clearMarkedChainableInstantiations(); _replaceInOutConnectionInlineableModules(); } + @override + void process() { + _collapseMarkedChainableModules(); + } + @override SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => SystemVerilogSynthSubModuleInstantiation(m); + void _clearMarkedChainableInstantiations() { + for (final subModuleInstantiation in chainableModulesToCollapse) { + subModuleInstantiation.clearInstantiation(); + } + } + /// Creates a new [_NetConnect] module to synthesize assignment between two /// [LogicNet]s. SystemVerilogSynthSubModuleInstantiation _addNetConnect( - SynthLogic dst, SynthLogic src) { + SynthLogic dst, + SynthLogic src, + ) { // make an (unconnected) module representing the assignment - final netConnect = - _NetConnect(LogicNet(width: dst.width), LogicNet(width: src.width)); + final netConnect = _NetConnect( + LogicNet(width: dst.width), + LogicNet(width: src.width), + ); // instantiate the module within the definition final netConnectSynthSubModInst = (getSynthSubModuleInstantiation(netConnect) - as SystemVerilogSynthSubModuleInstantiation) - + as SystemVerilogSynthSubModuleInstantiation) // map inouts to the appropriate `_SynthLogic`s ..setInOutMapping(_NetConnect.n0Name, dst) ..setInOutMapping(_NetConnect.n1Name, src); @@ -56,8 +71,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { for (final assignment in assignments) { if (assignment.src.isNet && assignment.dst.isNet) { - assert(assignment is! PartialSynthAssignment, - 'Net connections should not be partial assignments.'); + assert( + assignment is! PartialSynthAssignment, + 'Net connections should not be partial assignments.', + ); _addNetConnect(assignment.dst, assignment.src); } else { @@ -73,8 +90,8 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } - /// Collapses chainable, inlineable modules. - void _collapseChainableModules() { + /// Collapses chainable, inlineable modules after naming. + void _collapseMarkedChainableModules() { // collapse multiple lines of in-line assignments into one where they are // unnamed one-liners // for example, be capable of creating lines like: @@ -82,95 +99,11 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // assign _d_and_e = d & e // assign y = _d_and_e - // Also feed collapsed chained modules into other modules - // Need to consider order of operations in systemverilog or else add () - // everywhere! (for now add the parentheses) - - // Algorithm: - // - find submodule instantiations that are inlineable - // - filter to those who only output as input to one other module - // - pass an override to the submodule instantiation that the corresponding - // input should map to the output of another submodule instantiation - // do not collapse if signal feeds to multiple inputs of other modules - - final inlineableSubmoduleInstantiations = module.subModules - .whereType() - .map((m) => getSynthSubModuleInstantiation(m) - as SystemVerilogSynthSubModuleInstantiation); - - // number of times each signal name is used by any module - final signalUsage = {}; - - for (final subModuleInstantiation in subModuleInstantiations) { - for (final inSynthLogic in [ - ...subModuleInstantiation.inputMapping.values, - ...subModuleInstantiation.inOutMapping.values - ]) { - if (inputs.contains(inSynthLogic) || inOuts.contains(inSynthLogic)) { - // dont worry about inputs to THIS module - continue; - } - - subModuleInstantiation as SystemVerilogSynthSubModuleInstantiation; - - if (subModuleInstantiation.inlineResultLogic == inSynthLogic) { - // don't worry about the result signal - continue; - } - - signalUsage.update( - inSynthLogic, - (value) => value + 1, - ifAbsent: () => 1, - ); - } - } - - final singleUseSignals = {}; - signalUsage.forEach((signal, signalUsageCount) { - // don't collapse if: - // - used more than once - // - inline modules for preferred names - if (signalUsageCount == 1 && signal.mergeable) { - singleUseSignals.add(signal); - } - }); - - // partial assignments are a special case, count as a usage - for (final partialAssignment - in assignments.whereType()) { - singleUseSignals.remove(partialAssignment.src); - } - - final singleUsageInlineableSubmoduleInstantiations = - inlineableSubmoduleInstantiations.where((subModuleInstantiation) { - // inlineable modules have only 1 result signal - final resultSynthLogic = subModuleInstantiation.inlineResultLogic!; - - return singleUseSignals.contains(resultSynthLogic) && - - // don't inline modules if they were cleared from instantiation - subModuleInstantiation.needsInstantiation; - }); - - // remove any inlineability for those that want no expressions - for (final instantiation in subModuleInstantiations) { - final subModule = instantiation.module; - if (subModule is SystemVerilog) { - singleUseSignals.removeAll(subModule.expressionlessInputs.map((e) => - instantiation.inputMapping[e] ?? instantiation.inOutMapping[e])); - } - // ignore: deprecated_member_use_from_same_package - else if (subModule is CustomSystemVerilog) { - singleUseSignals.removeAll(subModule.expressionlessInputs.map((e) => - instantiation.inputMapping[e] ?? instantiation.inOutMapping[e])); - } - } - final synthLogicToInlineableSynthSubmoduleMap = {}; for (final subModuleInstantiation - in singleUsageInlineableSubmoduleInstantiations) { + in chainableModulesToCollapse + .cast()) { (subModuleInstantiation.module as InlineSystemVerilog).resultSignalName; // inlineable modules have only 1 result signal @@ -189,8 +122,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { for (final subModuleInstantiation in subModuleInstantiations) { subModuleInstantiation as SystemVerilogSynthSubModuleInstantiation; - subModuleInstantiation.synthLogicToInlineableSynthSubmoduleMap = - synthLogicToInlineableSynthSubmoduleMap; + subModuleInstantiation.synthLogicToInlineableSynthSubmoduleMap = { + ...?subModuleInstantiation.synthLogicToInlineableSynthSubmoduleMap, + ...synthLogicToInlineableSynthSubmoduleMap, + }; } } @@ -199,11 +134,12 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// [_NetConnect] assignment instead of a normal assignment. void _replaceInOutConnectionInlineableModules() { for (final subModuleInstantiation in subModuleInstantiations.toList().where( - (e) => - e.module is InlineSystemVerilog && - e.needsInstantiation && - e.outputMapping.isEmpty && - e.inOutMapping.isNotEmpty)) { + (e) => + e.module is InlineSystemVerilog && + e.needsInstantiation && + e.outputMapping.isEmpty && + e.inOutMapping.isNotEmpty, + )) { // algorithm: // - mark module as not needing declaration // - add a net_connect @@ -258,21 +194,23 @@ class _NetConnect extends Module with SystemVerilog { static final String n1Name = Naming.unpreferredName('n1'); _NetConnect(LogicNet n0, LogicNet n1) - : assert(n0.width == n1.width, 'Widths must be equal.'), - width = n0.width, - super( - definitionName: _definitionName, - name: _definitionName, - ) { + : assert(n0.width == n1.width, 'Widths must be equal.'), + width = n0.width, + super(definitionName: _definitionName, name: _definitionName) { n0 = addInOut(n0Name, n0, width: width); n1 = addInOut(n1Name, n1, width: width); } @override String instantiationVerilog( - String instanceType, String instanceName, Map ports) { - assert(instanceType == _definitionName, - 'Instance type selected should match the definition name.'); + String instanceType, + String instanceName, + Map ports, + ) { + assert( + instanceType == _definitionName, + 'Instance type selected should match the definition name.', + ); return '$instanceType' ' #(.WIDTH($width))' ' $instanceName' @@ -280,7 +218,8 @@ class _NetConnect extends Module with SystemVerilog { } @override - String? definitionVerilog(String definitionType) => ''' + String? definitionVerilog(String definitionType) => + ''' // A special module for connecting two nets bidirectionally module $definitionType #(parameter int WIDTH=1) (w, w); inout wire[WIDTH-1:0] w; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 13589a4dc..ba429e749 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -21,11 +21,8 @@ import 'package:rohd/src/utilities/namer.dart'; class _BusSubsetForStructSlice extends BusSubset { /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. - _BusSubsetForStructSlice( - super.bus, - super.startIndex, - super.endIndex, - ) : super(name: 'struct_slice'); + _BusSubsetForStructSlice(super.bus, super.startIndex, super.endIndex) + : super(name: 'struct_slice'); // we override this since it's added post-build @override @@ -68,13 +65,21 @@ class SynthModuleDefinition { /// A mapping from the original [Module]s to the /// [SynthSubModuleInstantiation]s that represent them. final Map - moduleToSubModuleInstantiationMap = {}; + moduleToSubModuleInstantiationMap = {}; /// All the sub-module instantiations used within this definition which are /// still present (not removed). Iterable get subModuleInstantiations => moduleToSubModuleInstantiationMap.values; + /// Chainable inline modules that should claim names after emitted objects. + @protected + final Set chainableModulesToCollapse = {}; + + final Set _weakNameClaimSubmodules = {}; + + final Set _weakNameClaimSignals = {}; + /// Indicates that [m] is a submodule used within this definition. /// /// This is only valid to call after all the submodules have been detected. @@ -128,9 +133,7 @@ class SynthModuleDefinition { /// Either accesses a previously created [SynthLogic] corresponding to /// [logic], or else creates a new one and adds it to the [logicToSynthMap]. - SynthLogic? getSynthLogic( - Logic? logic, - ) { + SynthLogic? getSynthLogic(Logic? logic) { if (logic == null) { return null; } else if (!(logic.parentModule == module || @@ -165,7 +168,8 @@ class SynthModuleDefinition { parentSynthModuleDefinition: this, ); } else { - final disallowConstName = (logic.isInput || logic.isInOut) && + final disallowConstName = + (logic.isInput || logic.isInOut) && // ignore: deprecated_member_use_from_same_package ((logic.parentModule is CustomSystemVerilog && // ignore: deprecated_member_use_from_same_package @@ -173,8 +177,7 @@ class SynthModuleDefinition { .expressionlessInputs .contains(logic.name)) || (logic.parentModule is SystemVerilog && - (logic.parentModule! as SystemVerilog) - .expressionlessInputs + (logic.parentModule! as SystemVerilog).expressionlessInputs .contains(logic.name))); final Naming? namingOverride; @@ -240,8 +243,14 @@ class SynthModuleDefinition { for (final leafElement in port.leafElements) { final leafSynth = getSynthLogic(leafElement)!; internalSignals.add(leafSynth); - assignments.add(PartialSynthAssignment(leafSynth, portSynth, - dstUpperIndex: idx + leafElement.width - 1, dstLowerIndex: idx)); + assignments.add( + PartialSynthAssignment( + leafSynth, + portSynth, + dstUpperIndex: idx + leafElement.width - 1, + dstLowerIndex: idx, + ), + ); idx += leafElement.width; } } @@ -262,7 +271,9 @@ class SynthModuleDefinition { // this is DISCONNECTED, just a module used for synthesizing final subsetMod = _BusSubsetForStructSlice( (port.isNet ? LogicNet.new : Logic.new)( - width: port.width, name: 'DUMMY'), + width: port.width, + name: 'DUMMY', + ), idx, idx + leafElement.width - 1, ); @@ -285,12 +296,12 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) - : assert( - !(module is SystemVerilog && - module.generatedDefinitionType == - DefinitionGenerationType.none), - 'Do not build a definition for a module' - ' which generates no definition!') { + : assert( + !(module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none), + 'Do not build a definition for a module' + ' which generates no definition!', + ) { // start by traversing output signals final logicsToTraverse = TraverseableCollection() ..addAll(module.outputs.values) @@ -329,8 +340,9 @@ class SynthModuleDefinition { // find any named signals sitting around that don't do anything // this is not necessary for functionality, just nice naming inclusion logicsToTraverse.addAll( - module.internalSignals - .where((element) => element.naming != Naming.unnamed), + module.internalSignals.where( + (element) => element.naming != Naming.unnamed, + ), ); // make sure floating modules are included @@ -363,9 +375,10 @@ class SynthModuleDefinition { final receiver = logicsToTraverse[i]; assert( - receiver.parentModule != null, - 'Any signal traced by this should have been detected by build,' - ' but $receiver was not.'); + receiver.parentModule != null, + 'Any signal traced by this should have been detected by build,' + ' but $receiver was not.', + ); if (receiver.parentModule != module && !module.subModules.contains(receiver.parentModule)) { @@ -388,10 +401,12 @@ class SynthModuleDefinition { if (receiver is LogicNet) { // only for the leaves, that's why only `LogicNet` and not array/struct - logicsToTraverse.addAll([ - ...receiver.srcConnections, - ...receiver.dstConnections - ].where((element) => element.parentModule == module)); + logicsToTraverse.addAll( + [ + ...receiver.srcConnections, + ...receiver.dstConnections, + ].where((element) => element.parentModule == module), + ); for (final srcConnection in receiver.srcConnections) { if (srcConnection.parentModule == module || @@ -399,10 +414,7 @@ class SynthModuleDefinition { srcConnection.parentModule!.parent == module)) { final netSynthDriver = getSynthLogic(srcConnection)!; - assignments.add(SynthAssignment( - netSynthDriver, - synthReceiver, - )); + assignments.add(SynthAssignment(netSynthDriver, synthReceiver)); } } } @@ -431,10 +443,11 @@ class SynthModuleDefinition { inOuts.add(synthReceiver); } else { assert( - !inputs.contains(synthReceiver) && - !outputs.contains(synthReceiver) && - !inOuts.contains(synthReceiver), - 'Internal signals should not be ports also.'); + !inputs.contains(synthReceiver) && + !outputs.contains(synthReceiver) && + !inOuts.contains(synthReceiver), + 'Internal signals should not be ports also.', + ); internalSignals.add(synthReceiver); } @@ -445,8 +458,9 @@ class SynthModuleDefinition { if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setInOutMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setInOutMapping(receiver.name, synthReceiver); } logicsToTraverse.addAll(subModule.inOuts.values); @@ -461,8 +475,9 @@ class SynthModuleDefinition { // array elements are not named ports, just contained in array if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setOutputMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setOutputMapping(receiver.name, synthReceiver); } logicsToTraverse @@ -493,8 +508,9 @@ class SynthModuleDefinition { // array elements are not named ports, just contained in array if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setInputMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setInputMapping(receiver.name, synthReceiver); } } } @@ -505,8 +521,109 @@ class SynthModuleDefinition { _assignSubmodulePortMapping(); _pruneUnused(); - process(); + prepareForNaming(); _pickNames(); + process(); + } + + /// Performs any synthesis-specific analysis needed before names are picked. + @protected + @visibleForOverriding + void prepareForNaming() { + chainableModulesToCollapse + ..clear() + ..addAll(_findChainableModulesToCollapse()); + _weakNameClaimSubmodules.clear(); + _weakNameClaimSignals.clear(); + + for (final subModuleInstantiation in chainableModulesToCollapse) { + _weakNameClaimSubmodules.add(subModuleInstantiation); + final resultLogic = _inlineResultLogic(subModuleInstantiation); + if (resultLogic != null) { + _weakNameClaimSignals.add(resultLogic); + } + } + } + + /// Finds chainable, inlineable modules. + Iterable _findChainableModulesToCollapse() { + final inlineableSubmoduleInstantiations = subModuleInstantiations.where( + (submoduleInstantiation) => + submoduleInstantiation.module is InlineSystemVerilog, + ); + + final signalUsage = {}; + + for (final subModuleInstantiation in subModuleInstantiations) { + for (final inSynthLogic in [ + ...subModuleInstantiation.inputMapping.values, + ...subModuleInstantiation.inOutMapping.values, + ]) { + if (inputs.contains(inSynthLogic) || inOuts.contains(inSynthLogic)) { + continue; + } + + if (_inlineResultLogic(subModuleInstantiation) == inSynthLogic) { + continue; + } + + signalUsage.update( + inSynthLogic, + (value) => value + 1, + ifAbsent: () => 1, + ); + } + } + + final singleUseSignals = {}; + signalUsage.forEach((signal, signalUsageCount) { + if (signalUsageCount == 1 && signal.mergeable) { + singleUseSignals.add(signal); + } + }); + + for (final partialAssignment + in assignments.whereType()) { + singleUseSignals.remove(partialAssignment.src); + } + + for (final instantiation in subModuleInstantiations) { + final subModule = instantiation.module; + if (subModule is SystemVerilog) { + singleUseSignals.removeAll( + subModule.expressionlessInputs.map( + (e) => + instantiation.inputMapping[e] ?? instantiation.inOutMapping[e], + ), + ); + // ignore: deprecated_member_use_from_same_package + } else if (subModule is CustomSystemVerilog) { + singleUseSignals.removeAll( + subModule.expressionlessInputs.map( + (e) => + instantiation.inputMapping[e] ?? instantiation.inOutMapping[e], + ), + ); + } + } + + return inlineableSubmoduleInstantiations.where((subModuleInstantiation) { + final resultSynthLogic = _inlineResultLogic(subModuleInstantiation); + + return resultSynthLogic != null && + singleUseSignals.contains(resultSynthLogic) && + subModuleInstantiation.needsInstantiation; + }); + } + + SynthLogic? _inlineResultLogic(SynthSubModuleInstantiation instantiation) { + final subModule = instantiation.module; + if (subModule is! InlineSystemVerilog) { + return null; + } + + return instantiation.outputMapping[subModule.resultSignalName] ?? + instantiation.inOutMapping[subModule.resultSignalName]; } /// Performs additional processing on the current definition to simplify, @@ -567,8 +684,9 @@ class SynthModuleDefinition { final logics = internalSignal.logics; if (internalSignal.isArray) { - if (logics.any((logicArray) => - logicArray.elements.any(logicHasPresentSynthLogic))) { + if (logics.any( + (logicArray) => logicArray.elements.any(logicHasPresentSynthLogic), + )) { // if it's an array, can only remove if all elements are removed reducedInternalSignals.add(internalSignal); } else { @@ -580,27 +698,33 @@ class SynthModuleDefinition { continue; } - final isCustomSvModPort = logics.any((logic) => - logic.isPort && - isSubmoduleAndPresent(logic.parentModule) && - ((logic.parentModule! is SystemVerilog && - !(logic.parentModule! as SystemVerilog) - .acceptsEmptyPortConnections) || - // ignore: deprecated_member_use_from_same_package - logic.parentModule! is CustomSystemVerilog)); + final isCustomSvModPort = logics.any( + (logic) => + logic.isPort && + isSubmoduleAndPresent(logic.parentModule) && + ((logic.parentModule! is SystemVerilog && + !(logic.parentModule! as SystemVerilog) + .acceptsEmptyPortConnections) || + // ignore: deprecated_member_use_from_same_package + logic.parentModule! is CustomSystemVerilog), + ); if (!isCustomSvModPort) { if (internalSignal.isNet) { - final anyInternalConnections = [ - ...internalSignal.srcConnections, - ...internalSignal.dstConnections - ] - .where((e) => - (e.parentModule == module || - ( // in case of sub-module output driving a net - e.parentModule?.parent == module && e.isOutput)) && - logicHasPresentSynthLogic(e)) - .isNotEmpty; + final anyInternalConnections = + [ + ...internalSignal.srcConnections, + ...internalSignal.dstConnections, + ] + .where( + (e) => + (e.parentModule == module || + ( // in case of sub-module output driving a net + e.parentModule?.parent == module && + e.isOutput)) && + logicHasPresentSynthLogic(e), + ) + .isNotEmpty; if (anyInternalConnections) { reducedInternalSignals.add(internalSignal); @@ -610,9 +734,11 @@ class SynthModuleDefinition { final connectedSubModules = logics .map((e) => e.parentModule) .nonNulls - .where((e) => - e != module && - getSynthSubModuleInstantiation(e).needsInstantiation) + .where( + (e) => + e != module && + getSynthSubModuleInstantiation(e).needsInstantiation, + ) .toSet(); if (connectedSubModules.length > 1) { @@ -623,13 +749,13 @@ class SynthModuleDefinition { // If the signal appears in multiple inout port mappings on the // same (single) connected submodule, it's a loopback and needs // a wire declaration so both ports can reference it by name. - final hasInOutLoopback = connectedSubModules.any((m) => - getSynthSubModuleInstantiation(m) - .inOutMapping - .values - .where((v) => v == internalSignal) - .length > - 1); + final hasInOutLoopback = connectedSubModules.any( + (m) => + getSynthSubModuleInstantiation(m).inOutMapping.values + .where((v) => v == internalSignal) + .length > + 1, + ); if (hasInOutLoopback) { reducedInternalSignals.add(internalSignal); @@ -687,39 +813,44 @@ class SynthModuleDefinition { continue; } - for (final subModuleInstantiation - in subModuleInstantiations.where((e) => e.needsInstantiation)) { + for (final subModuleInstantiation in subModuleInstantiations.where( + (e) => e.needsInstantiation, + )) { final subModule = subModuleInstantiation.module; if (subModule is SystemVerilog && subModule.isWiresOnly) { final inputs = { ...subModuleInstantiation.inputMapping, - ...subModuleInstantiation.inOutMapping + ...subModuleInstantiation.inOutMapping, }; final outputs = { ...subModuleInstantiation.outputMapping, - ...subModuleInstantiation.inOutMapping + ...subModuleInstantiation.inOutMapping, }; // if all the inputs or all the outputs are not used, we can remove // the module - final allOutputsUnused = outputs.values.every((output) => - output.declarationCleared || - (output.isClearable && - !output.isStructPortElement() && - !output.hasDstConnectionsPresent())); + final allOutputsUnused = outputs.values.every( + (output) => + output.declarationCleared || + (output.isClearable && + !output.isStructPortElement() && + !output.hasDstConnectionsPresent()), + ); if (allOutputsUnused) { subModuleInstantiation.clearInstantiation(); changed = true; continue; } - final allInputsUnused = inputs.values.every((input) => - input.declarationCleared || - (input.isClearable && - !input.isStructPortElement() && - !input.hasSrcConnectionsPresent())); + final allInputsUnused = inputs.values.every( + (input) => + input.declarationCleared || + (input.isClearable && + !input.isStructPortElement() && + !input.hasSrcConnectionsPresent()), + ); if (allInputsUnused) { subModuleInstantiation.clearInstantiation(); changed = true; @@ -737,22 +868,28 @@ class SynthModuleDefinition { for (final inputName in submoduleInstantiation.module.inputs.keys) { final orig = submoduleInstantiation.inputMapping[inputName]!; submoduleInstantiation.setInputMapping( - inputName, orig.replacement ?? orig, - replace: true); + inputName, + orig.replacement ?? orig, + replace: true, + ); } for (final outputName in submoduleInstantiation.module.outputs.keys) { final orig = submoduleInstantiation.outputMapping[outputName]!; submoduleInstantiation.setOutputMapping( - outputName, orig.replacement ?? orig, - replace: true); + outputName, + orig.replacement ?? orig, + replace: true, + ); } for (final inOutName in submoduleInstantiation.module.inOuts.keys) { final orig = submoduleInstantiation.inOutMapping[inOutName]!; submoduleInstantiation.setInOutMapping( - inOutName, orig.replacement ?? orig, - replace: true); + inOutName, + orig.replacement ?? orig, + replace: true, + ); } } } @@ -785,32 +922,78 @@ class SynthModuleDefinition { for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { submodule.pickName(module); - assert(submodule.module.name == submodule.name, - 'Expect reserved names to retain their name.'); + assert( + submodule.module.name == submodule.name, + 'Expect reserved names to retain their name.', + ); } } // Reserved internal signals next. final nonReservedSignals = []; - for (final signal in internalSignals) { - if (signal.isReserved) { + final weakSignals = []; + for (final signal in _signalsInModuleOrder(internalSignals)) { + if (_weakNameClaimSignals.contains(signal)) { + weakSignals.add(signal); + } else if (signal.isReserved) { signal.pickName(); } else { nonReservedSignals.add(signal); } } - // Then non-reserved submodule instances. + // Then non-reserved submodule instances with strong name claims. + final weakSubmodules = []; for (final submodule in subModuleInstantiations) { - if (!submodule.module.reserveName && submodule.needsInstantiation) { + if (submodule.module.reserveName) { + continue; + } + if (_weakNameClaimSubmodules.contains(submodule)) { + weakSubmodules.add(submodule); + } else if (submodule.needsInstantiation) { submodule.pickName(module); } } - // Then the rest of the internal signals. - for (final signal in nonReservedSignals) { + // Then the rest of the internal signals with strong name claims. + for (final signal in _signalsInModuleOrder(nonReservedSignals)) { signal.pickName(); } + + // Finally, weak claims reserve stable names after emitted objects have + // had first chance at the shortest basenames. + for (final submodule in weakSubmodules) { + submodule.pickName(module); + } + for (final signal in _signalsInModuleOrder(weakSignals)) { + signal.pickName(); + } + } + + List _signalsInModuleOrder(Iterable signals) { + final logicOrder = {}; + var nextOrder = 0; + for (final logic in module.signals) { + logicOrder[logic] = nextOrder++; + } + + int orderOf(SynthLogic signal) => + signal.logics + .map((logic) => logicOrder[logic]) + .whereType() + .minOrNull ?? + nextOrder; + + final indexedSignals = signals.indexed.toList() + ..sort((a, b) { + final byModuleOrder = orderOf(a.$2).compareTo(orderOf(b.$2)); + if (byModuleOrder != 0) { + return byModuleOrder; + } + return a.$1.compareTo(b.$1); + }); + + return indexedSignals.map((entry) => entry.$2).toList(growable: false); } /// Merges bit blasted array assignments into one single assignment when @@ -852,9 +1035,10 @@ class SynthModuleDefinition { for (final MapEntry(key: (srcArray, dstArray), value: arrAssignments) in groupedAssignments.entries) { assert( - srcArray.logics.first.elements.length == - dstArray.logics.first.elements.length, - 'should be equal lengths of elements in both arrays by now'); + srcArray.logics.first.elements.length == + dstArray.logics.first.elements.length, + 'should be equal lengths of elements in both arrays by now', + ); // first requirement is that all elements have been assigned var shouldMerge = @@ -896,8 +1080,9 @@ class SynthModuleDefinition { var prevAssignmentCount = 0; // grab the partial assignments since they can't be merged - final partialAssignments = - assignments.whereType().toList(); + final partialAssignments = assignments + .whereType() + .toList(); assignments.removeWhere((e) => e is PartialSynthAssignment); while (prevAssignmentCount != assignments.length) { @@ -909,8 +1094,10 @@ class SynthModuleDefinition { assignments.where((a) => !a.src.isConstant && !a.dst.isConstant), assignments.where((a) => a.src.isConstant || a.dst.isConstant), ])) { - assert(assignment is! PartialSynthAssignment, - 'Partial assignments should have been removed before this.'); + assert( + assignment is! PartialSynthAssignment, + 'Partial assignments should have been removed before this.', + ); final dst = assignment.dst; final src = assignment.src; @@ -922,8 +1109,10 @@ class SynthModuleDefinition { continue; } - assert(dst != src, - 'No circular assignment allowed between $dst and $src.'); + assert( + dst != src, + 'No circular assignment allowed between $dst and $src.', + ); final mergeResults = SynthLogic.tryMerge(dst, src); @@ -963,14 +1152,18 @@ class SynthModuleDefinition { /// Performs updates to this definition after merging away a signal as part of /// [_collapseAssignments]. - void _applyAssignmentMergeUpdates( - {required SynthLogic mergedAway, required SynthLogic kept}) { + void _applyAssignmentMergeUpdates({ + required SynthLogic mergedAway, + required SynthLogic kept, + }) { final foundInternal = internalSignals.remove(mergedAway); if (!foundInternal) { final foundKept = internalSignals.remove(kept); - assert(foundKept, - 'One of the two should be internal since we cant merge ports.'); + assert( + foundKept, + 'One of the two should be internal since we cant merge ports.', + ); if (inputs.contains(mergedAway)) { inputs @@ -994,8 +1187,8 @@ class SynthModuleDefinition { // should all be the same synth, and arrays only merge with arrays final keptElement = getSynthLogic(keptElementLogic)!; final mergedAwayElement = getSynthLogic( - (mergedAway.logics.first as LogicArray) - .elements[keptElementIndex])!; + (mergedAway.logics.first as LogicArray).elements[keptElementIndex], + )!; if (keptElement == mergedAwayElement) { continue; @@ -1004,7 +1197,9 @@ class SynthModuleDefinition { keptElement.adopt(mergedAwayElement, force: true); _applyAssignmentMergeUpdates( - mergedAway: mergedAwayElement, kept: keptElement); + mergedAway: mergedAwayElement, + kept: keptElement, + ); } } } diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 65878d40d..1eccf9da9 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -36,55 +36,76 @@ class SynthSubModuleInstantiation { } /// A mapping of input port name to [SynthLogic]. - late final Map inputMapping = - UnmodifiableMapView(_inputMapping); + late final Map inputMapping = UnmodifiableMapView( + _inputMapping, + ); final Map _inputMapping = {}; /// Adds an input mapping from [name] to [synthLogic]. - void setInputMapping(String name, SynthLogic synthLogic, - {bool replace = false}) { - assert(module.inputs.containsKey(name), - 'Input $name not found in module ${module.name}.'); + void setInputMapping( + String name, + SynthLogic synthLogic, { + bool replace = false, + }) { assert( - (replace && _inputMapping.containsKey(name)) || - !_inputMapping.containsKey(name), - 'A mapping already exists to this input: $name.'); + module.inputs.containsKey(name), + 'Input $name not found in module ${module.name}.', + ); + assert( + (replace && _inputMapping.containsKey(name)) || + !_inputMapping.containsKey(name), + 'A mapping already exists to this input: $name.', + ); _inputMapping[name] = synthLogic; } /// A mapping of output port name to [SynthLogic]. - late final Map outputMapping = - UnmodifiableMapView(_outputMapping); + late final Map outputMapping = UnmodifiableMapView( + _outputMapping, + ); final Map _outputMapping = {}; /// Adds an output mapping from [name] to [synthLogic]. - void setOutputMapping(String name, SynthLogic synthLogic, - {bool replace = false}) { - assert(module.outputs.containsKey(name), - 'Output $name not found in module ${module.name}.'); + void setOutputMapping( + String name, + SynthLogic synthLogic, { + bool replace = false, + }) { + assert( + module.outputs.containsKey(name), + 'Output $name not found in module ${module.name}.', + ); assert( - (replace && _outputMapping.containsKey(name)) || - !_outputMapping.containsKey(name), - 'A mapping already exists to this output: $name.'); + (replace && _outputMapping.containsKey(name)) || + !_outputMapping.containsKey(name), + 'A mapping already exists to this output: $name.', + ); _outputMapping[name] = synthLogic; } /// A mapping of output port name to [SynthLogic]. - late final Map inOutMapping = - UnmodifiableMapView(_inOutMapping); + late final Map inOutMapping = UnmodifiableMapView( + _inOutMapping, + ); final Map _inOutMapping = {}; /// Adds an inOut mapping from [name] to [synthLogic]. - void setInOutMapping(String name, SynthLogic synthLogic, - {bool replace = false}) { - assert(module.inOuts.containsKey(name), - 'InOut $name not found in module ${module.name}.'); + void setInOutMapping( + String name, + SynthLogic synthLogic, { + bool replace = false, + }) { + assert( + module.inOuts.containsKey(name), + 'InOut $name not found in module ${module.name}.', + ); assert( - (replace && _inOutMapping.containsKey(name)) || - !_inOutMapping.containsKey(name), - 'A mapping already exists to this output: $name.'); + (replace && _inOutMapping.containsKey(name)) || + !_inOutMapping.containsKey(name), + 'A mapping already exists to this output: $name.', + ); _inOutMapping[name] = synthLogic; } diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index 246862b14..a021b7cff 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -64,6 +64,37 @@ class _FlopOuter extends Module { } } +class _CollapsedInstanceCollidingNames extends Module { + late final Logic retainedDup; + + _CollapsedInstanceCollidingNames(Logic a, Logic b) + : super(name: 'collapsedInstanceCollidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + + final collapsedInstanceOut = And2Gate(a, b, name: 'dup').out; + retainedDup = Logic(name: 'dup'); + + retainedDup <= a | b; + y <= collapsedInstanceOut ^ retainedDup; + z <= retainedDup; + } +} + +Future _retainedDupNameAfter( + SynthModuleDefinition Function(_CollapsedInstanceCollidingNames) + createDefinition, +) async { + final mod = _CollapsedInstanceCollidingNames(Logic(), Logic()); + await mod.build(); + + createDefinition(mod); + + return mod.namer.signalNameOfBest([mod.retainedDup]); +} + /// Builds [SynthModuleDefinition]s from both bases and collects a /// Logic→name mapping for all present SynthLogics. /// @@ -110,20 +141,33 @@ void main() { // Every Logic present in both must have the same name. for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name} ' - '(${logic.runtimeType}, naming=${logic.naming})'); + expect( + baseNames[logic], + svNames[logic], + reason: + 'Name mismatch for ${logic.name} ' + '(${logic.runtimeType}, naming=${logic.naming})', + ); } } // Port names specifically must match. for (final port in [...mod.inputs.values, ...mod.outputs.values]) { - expect(svNames[port], isNotNull, - reason: 'SV def should have port ${port.name}'); - expect(baseNames[port], isNotNull, - reason: 'Base def should have port ${port.name}'); - expect(svNames[port], baseNames[port], - reason: 'Port name must match for ${port.name}'); + expect( + svNames[port], + isNotNull, + reason: 'SV def should have port ${port.name}', + ); + expect( + baseNames[port], + isNotNull, + reason: 'Base def should have port ${port.name}', + ); + expect( + svNames[port], + baseNames[port], + reason: 'Port name must match for ${port.name}', + ); } }); @@ -139,8 +183,11 @@ void main() { for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name}'); + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); } } }); @@ -157,8 +204,11 @@ void main() { for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name}'); + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); } } }); @@ -175,8 +225,11 @@ void main() { for (final logic in svNames.keys) { if (baseNames.containsKey(logic)) { - expect(baseNames[logic], svNames[logic], - reason: 'Name mismatch for ${logic.name}'); + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); } } }); @@ -194,9 +247,13 @@ void main() { for (final logic in names1.keys) { if (names2.containsKey(logic)) { - expect(names2[logic], names1[logic], - reason: 'Shared namer should produce same name for ' - '${logic.name}'); + expect( + names2[logic], + names1[logic], + reason: + 'Shared namer should produce same name for ' + '${logic.name}', + ); } } }); @@ -212,59 +269,92 @@ void main() { for (final port in [...mod.inputs.values, ...mod.outputs.values]) { final moduleName = mod.namer.signalNameOfBest([port]); final synthName = synthNames[port]; - expect(synthName, moduleName, - reason: - 'SynthLogic.name and Module.namer.signalNameOfBest must agree ' - 'for port ${port.name}'); + expect( + synthName, + moduleName, + reason: + 'SynthLogic.name and Module.namer.signalNameOfBest must agree ' + 'for port ${port.name}', + ); } }); - test('submodule instance names are allocated from the shared namespace', - () async { - // Instance names come from Module.namer.instanceNameOf, - // which shares the same namespace as signal names. - final mod = _Outer(Logic(width: 8), Logic(width: 8)); - await mod.build(); - - final def = SynthModuleDefinition(mod); - - final instNames = def.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .toSet(); - - // The inner module instance should have a name - expect(instNames, isNotEmpty, - reason: 'Should have at least one submodule instance'); - - // Instance names are claimed in the shared namespace. - for (final name in instNames) { - expect(mod.namer.isAvailable(name), isFalse, - reason: 'Instance name "$name" should be claimed in the ' - 'namespace'); - } - }); - - test('submodule instance names are stable across repeated definitions', - () async { - final mod = _Outer(Logic(width: 8), Logic(width: 8)); - await mod.build(); - - final def1 = SynthModuleDefinition(mod); - final def2 = SynthModuleDefinition(mod); - - final names1 = def1.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .toList(); - final names2 = def2.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .toList(); - - expect(names2, names1, - reason: 'Repeated synthesis passes should reuse cached instance ' - 'names instead of drifting numeric suffixes.'); - }); + test( + 'submodule instance names are allocated from the shared namespace', + () async { + // Instance names come from Module.namer.instanceNameOf, + // which shares the same namespace as signal names. + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toSet(); + + // The inner module instance should have a name + expect( + instNames, + isNotEmpty, + reason: 'Should have at least one submodule instance', + ); + + // Instance names are claimed in the shared namespace. + for (final name in instNames) { + expect( + mod.namer.isAvailable(name), + isFalse, + reason: + 'Instance name "$name" should be claimed in the ' + 'namespace', + ); + } + }, + ); + + test( + 'submodule instance names are stable across repeated definitions', + () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = def1.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + final names2 = def2.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + + expect( + names2, + names1, + reason: + 'Repeated synthesis passes should reuse cached instance ' + 'names instead of drifting numeric suffixes.', + ); + }, + ); + + test( + 'collapsed instance does not steal basename from retained signal', + () async { + final baseName = await _retainedDupNameAfter(SynthModuleDefinition.new); + await Simulator.reset(); + + final svName = await _retainedDupNameAfter( + SystemVerilogSynthModuleDefinition.new, + ); + + expect(svName, equals(baseName)); + expect(baseName, equals('dup')); + }, + ); }); } From ddd96f19be497a8a2b79dd976c24a8e90a67fc8c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 10:16:32 -0700 Subject: [PATCH 49/58] heuristic to mark potentially collapsed nodes for lower priority naming --- ...systemverilog_synth_module_definition.dart | 28 +++++----- .../utilities/synth_module_definition.dart | 54 +++++++++---------- test/naming_consistency_test.dart | 16 +++--- 3 files changed, 46 insertions(+), 52 deletions(-) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 65a2c3181..a01bb4820 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -54,7 +54,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // instantiate the module within the definition final netConnectSynthSubModInst = (getSynthSubModuleInstantiation(netConnect) - as SystemVerilogSynthSubModuleInstantiation) + as SystemVerilogSynthSubModuleInstantiation) // map inouts to the appropriate `_SynthLogic`s ..setInOutMapping(_NetConnect.n0Name, dst) ..setInOutMapping(_NetConnect.n1Name, src); @@ -101,9 +101,8 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { final synthLogicToInlineableSynthSubmoduleMap = {}; - for (final subModuleInstantiation - in chainableModulesToCollapse - .cast()) { + for (final subModuleInstantiation in chainableModulesToCollapse + .cast()) { (subModuleInstantiation.module as InlineSystemVerilog).resultSignalName; // inlineable modules have only 1 result signal @@ -134,12 +133,12 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// [_NetConnect] assignment instead of a normal assignment. void _replaceInOutConnectionInlineableModules() { for (final subModuleInstantiation in subModuleInstantiations.toList().where( - (e) => - e.module is InlineSystemVerilog && - e.needsInstantiation && - e.outputMapping.isEmpty && - e.inOutMapping.isNotEmpty, - )) { + (e) => + e.module is InlineSystemVerilog && + e.needsInstantiation && + e.outputMapping.isEmpty && + e.inOutMapping.isNotEmpty, + )) { // algorithm: // - mark module as not needing declaration // - add a net_connect @@ -194,9 +193,9 @@ class _NetConnect extends Module with SystemVerilog { static final String n1Name = Naming.unpreferredName('n1'); _NetConnect(LogicNet n0, LogicNet n1) - : assert(n0.width == n1.width, 'Widths must be equal.'), - width = n0.width, - super(definitionName: _definitionName, name: _definitionName) { + : assert(n0.width == n1.width, 'Widths must be equal.'), + width = n0.width, + super(definitionName: _definitionName, name: _definitionName) { n0 = addInOut(n0Name, n0, width: width); n1 = addInOut(n1Name, n1, width: width); } @@ -218,8 +217,7 @@ class _NetConnect extends Module with SystemVerilog { } @override - String? definitionVerilog(String definitionType) => - ''' + String? definitionVerilog(String definitionType) => ''' // A special module for connecting two nets bidirectionally module $definitionType #(parameter int WIDTH=1) (w, w); inout wire[WIDTH-1:0] w; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index ba429e749..8a728bbe3 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -22,7 +22,7 @@ class _BusSubsetForStructSlice extends BusSubset { /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. _BusSubsetForStructSlice(super.bus, super.startIndex, super.endIndex) - : super(name: 'struct_slice'); + : super(name: 'struct_slice'); // we override this since it's added post-build @override @@ -65,7 +65,7 @@ class SynthModuleDefinition { /// A mapping from the original [Module]s to the /// [SynthSubModuleInstantiation]s that represent them. final Map - moduleToSubModuleInstantiationMap = {}; + moduleToSubModuleInstantiationMap = {}; /// All the sub-module instantiations used within this definition which are /// still present (not removed). @@ -168,8 +168,7 @@ class SynthModuleDefinition { parentSynthModuleDefinition: this, ); } else { - final disallowConstName = - (logic.isInput || logic.isInOut) && + final disallowConstName = (logic.isInput || logic.isInOut) && // ignore: deprecated_member_use_from_same_package ((logic.parentModule is CustomSystemVerilog && // ignore: deprecated_member_use_from_same_package @@ -177,7 +176,8 @@ class SynthModuleDefinition { .expressionlessInputs .contains(logic.name)) || (logic.parentModule is SystemVerilog && - (logic.parentModule! as SystemVerilog).expressionlessInputs + (logic.parentModule! as SystemVerilog) + .expressionlessInputs .contains(logic.name))); final Naming? namingOverride; @@ -296,12 +296,12 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) - : assert( - !(module is SystemVerilog && - module.generatedDefinitionType == DefinitionGenerationType.none), - 'Do not build a definition for a module' - ' which generates no definition!', - ) { + : assert( + !(module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none), + 'Do not build a definition for a module' + ' which generates no definition!', + ) { // start by traversing output signals final logicsToTraverse = TraverseableCollection() ..addAll(module.outputs.values) @@ -711,20 +711,19 @@ class SynthModuleDefinition { if (!isCustomSvModPort) { if (internalSignal.isNet) { - final anyInternalConnections = - [ - ...internalSignal.srcConnections, - ...internalSignal.dstConnections, - ] - .where( - (e) => - (e.parentModule == module || - ( // in case of sub-module output driving a net + final anyInternalConnections = [ + ...internalSignal.srcConnections, + ...internalSignal.dstConnections, + ] + .where( + (e) => + (e.parentModule == module || + ( // in case of sub-module output driving a net e.parentModule?.parent == module && e.isOutput)) && - logicHasPresentSynthLogic(e), - ) - .isNotEmpty; + logicHasPresentSynthLogic(e), + ) + .isNotEmpty; if (anyInternalConnections) { reducedInternalSignals.add(internalSignal); @@ -751,7 +750,9 @@ class SynthModuleDefinition { // a wire declaration so both ports can reference it by name. final hasInOutLoopback = connectedSubModules.any( (m) => - getSynthSubModuleInstantiation(m).inOutMapping.values + getSynthSubModuleInstantiation(m) + .inOutMapping + .values .where((v) => v == internalSignal) .length > 1, @@ -1080,9 +1081,8 @@ class SynthModuleDefinition { var prevAssignmentCount = 0; // grab the partial assignments since they can't be merged - final partialAssignments = assignments - .whereType() - .toList(); + final partialAssignments = + assignments.whereType().toList(); assignments.removeWhere((e) => e is PartialSynthAssignment); while (prevAssignmentCount != assignments.length) { diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index a021b7cff..150a9b751 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -68,7 +68,7 @@ class _CollapsedInstanceCollidingNames extends Module { late final Logic retainedDup; _CollapsedInstanceCollidingNames(Logic a, Logic b) - : super(name: 'collapsedInstanceCollidingNames') { + : super(name: 'collapsedInstanceCollidingNames') { a = addInput('a', a); b = addInput('b', b); final y = addOutput('y'); @@ -85,7 +85,7 @@ class _CollapsedInstanceCollidingNames extends Module { Future _retainedDupNameAfter( SynthModuleDefinition Function(_CollapsedInstanceCollidingNames) - createDefinition, + createDefinition, ) async { final mod = _CollapsedInstanceCollidingNames(Logic(), Logic()); await mod.build(); @@ -144,8 +144,7 @@ void main() { expect( baseNames[logic], svNames[logic], - reason: - 'Name mismatch for ${logic.name} ' + reason: 'Name mismatch for ${logic.name} ' '(${logic.runtimeType}, naming=${logic.naming})', ); } @@ -250,8 +249,7 @@ void main() { expect( names2[logic], names1[logic], - reason: - 'Shared namer should produce same name for ' + reason: 'Shared namer should produce same name for ' '${logic.name}', ); } @@ -306,8 +304,7 @@ void main() { expect( mod.namer.isAvailable(name), isFalse, - reason: - 'Instance name "$name" should be claimed in the ' + reason: 'Instance name "$name" should be claimed in the ' 'namespace', ); } @@ -335,8 +332,7 @@ void main() { expect( names2, names1, - reason: - 'Repeated synthesis passes should reuse cached instance ' + reason: 'Repeated synthesis passes should reuse cached instance ' 'names instead of drifting numeric suffixes.', ); }, From dd07852f0e938c639285b1e8559790291bdd3e8e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 10:46:07 -0700 Subject: [PATCH 50/58] bias collapsible Logics for weak naming --- ...systemverilog_synth_module_definition.dart | 38 +++++++++---------- .../utilities/synth_module_definition.dart | 24 ++++++++++-- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index a01bb4820..e07482a25 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -16,35 +16,24 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// Creates a new [SystemVerilogSynthModuleDefinition] for the given [module]. SystemVerilogSynthModuleDefinition(super.module); - @override - void prepareForNaming() { - _replaceNetConnections(); - super.prepareForNaming(); - _clearMarkedChainableInstantiations(); - _replaceInOutConnectionInlineableModules(); - } - @override void process() { + _buildNetConnectsForNaming(pickName: true); _collapseMarkedChainableModules(); + _replaceInOutConnectionInlineableModules(); } @override SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => SystemVerilogSynthSubModuleInstantiation(m); - void _clearMarkedChainableInstantiations() { - for (final subModuleInstantiation in chainableModulesToCollapse) { - subModuleInstantiation.clearInstantiation(); - } - } - /// Creates a new [_NetConnect] module to synthesize assignment between two /// [LogicNet]s. SystemVerilogSynthSubModuleInstantiation _addNetConnect( SynthLogic dst, - SynthLogic src, - ) { + SynthLogic src, { + bool pickName = false, + }) { // make an (unconnected) module representing the assignment final netConnect = _NetConnect( LogicNet(width: dst.width), @@ -62,11 +51,15 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // notify the `SynthBuilder` that it needs declaration supportingModules.add(netConnect); + if (pickName) { + netConnectSynthSubModInst.pickName(module); + } + return netConnectSynthSubModInst; } - /// Replace all [assignments] between two [LogicNet]s with a [_NetConnect]. - void _replaceNetConnections() { + /// Builds [_NetConnect] instances for [LogicNet] assignments. + void _buildNetConnectsForNaming({bool pickName = false}) { final reducedAssignments = []; for (final assignment in assignments) { @@ -76,7 +69,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { 'Net connections should not be partial assignments.', ); - _addNetConnect(assignment.dst, assignment.src); + _addNetConnect(assignment.dst, assignment.src, pickName: pickName); } else { reducedAssignments.add(assignment); } @@ -160,8 +153,11 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { parentSynthModuleDefinition: this, ); - final netConnectSynthSubmod = _addNetConnect(subModResult, dummy) - ..synthLogicToInlineableSynthSubmoduleMap ??= {}; + final netConnectSynthSubmod = _addNetConnect( + subModResult, + dummy, + pickName: true, + )..synthLogicToInlineableSynthSubmoduleMap ??= {}; netConnectSynthSubmod.synthLogicToInlineableSynthSubmoduleMap![dummy] = subModuleInstantiation; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 8a728bbe3..8cdc9711f 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -76,6 +76,9 @@ class SynthModuleDefinition { @protected final Set chainableModulesToCollapse = {}; + // Weak-name marks do not remove objects from naming. They make likely + // collapsed objects claim names after unmarked objects, so in a collision + // the unmarked object keeps the basename and the marked object gets a suffix. final Set _weakNameClaimSubmodules = {}; final Set _weakNameClaimSignals = {}; @@ -521,15 +524,30 @@ class SynthModuleDefinition { _assignSubmodulePortMapping(); _pruneUnused(); + + // Naming has two base-owned phases: mark likely-collapsed objects as weak + // name claimants, then pick names. After that, synthesizers may + // process/collapse the marked objects. prepareForNaming(); _pickNames(); process(); } - /// Performs any synthesis-specific analysis needed before names are picked. - @protected - @visibleForOverriding + /// Performs base-owned preparation before names are picked. + /// + /// Synthesizers must not override this method. + @nonVirtual void prepareForNaming() { + _markPotentiallyCollapsedObjectsForNaming(); + } + + /// Marks objects likely to be collapsed by some synthesizers as weak name + /// claimants. + /// + /// Marked objects are still named. They just claim names after unmarked + /// objects, biasing collision resolution so unmarked objects keep basenames + /// and marked objects receive suffixes like `_1` or `_2`. + void _markPotentiallyCollapsedObjectsForNaming() { chainableModulesToCollapse ..clear() ..addAll(_findChainableModulesToCollapse()); From 9bf137e4485eed3336fc74dde5c63f96ae1be67c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 10:54:47 -0700 Subject: [PATCH 51/58] update naming heuristic pickNames comment --- .../utilities/synth_module_definition.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 8cdc9711f..7b7ce6519 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -920,13 +920,16 @@ class SynthModuleDefinition { /// [Namer.instanceNameOf]. All non-constant names share a single namespace /// managed by the module's [Namer]. void _pickNames() { - // Name allocation order matters — earlier claims get the unsuffixed name - // when there are collisions. This matches production ROHD priority: + // Name allocation order matters -- earlier claims get the unsuffixed name + // when there are collisions. Weak-name claimants are intentionally deferred + // so emitted objects get first chance at the shortest basenames: // 1. Ports (reserved by _initNamespace, claimed via signalName) // 2. Reserved submodule instances - // 3. Reserved internal signals - // 4. Non-reserved submodule instances - // 5. Non-reserved internal signals + // 3. Reserved internal signals with strong claims + // 4. Non-reserved submodule instances with strong claims + // 5. Non-reserved internal signals with strong claims + // 6. Weak submodule instances + // 7. Weak internal signals for (final input in inputs) { input.pickName(); } From efdf60ba9c5d5ee4096af2dd128f4f2b68859e32 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 11:45:29 -0700 Subject: [PATCH 52/58] pana error on getter --- .../utilities/synth_module_definition.dart | 16 ++++++++------ test/instance_signal_name_collision_test.dart | 21 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 7b7ce6519..8adf754fd 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -999,12 +999,16 @@ class SynthModuleDefinition { logicOrder[logic] = nextOrder++; } - int orderOf(SynthLogic signal) => - signal.logics - .map((logic) => logicOrder[logic]) - .whereType() - .minOrNull ?? - nextOrder; + int orderOf(SynthLogic signal) { + var earliestOrder = nextOrder; + for (final logic in signal.logics) { + final order = logicOrder[logic]; + if (order != null && order < earliestOrder) { + earliestOrder = order; + } + } + return earliestOrder; + } final indexedSignals = signals.indexed.toList() ..sort((a, b) { diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart index 65747204a..68f0e9a80 100644 --- a/test/instance_signal_name_collision_test.dart +++ b/test/instance_signal_name_collision_test.dart @@ -63,8 +63,11 @@ void main() { orElse: () => null, ); expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); - expect(sl!.name, 'inner', - reason: 'Reserved signal "inner" must keep its exact name'); + expect( + sl!.name, + 'inner', + reason: 'Reserved signal "inner" must keep its exact name', + ); }); test( @@ -73,15 +76,15 @@ void main() { final inst = def.subModuleInstantiations .where((s) => s.needsInstantiation) .cast() - .firstWhere( - (s) => s!.module.name == 'inner', - orElse: () => null, - ); + .firstWhere((s) => s!.module.name == 'inner', orElse: () => null); expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); // The instance should be suffixed since the signal took "inner" first. - expect(inst!.name, isNot('inner'), - reason: 'Instance should be uniquified when signal already ' - 'claims "inner"'); + expect( + inst!.name, + isNot('inner'), + reason: 'Instance should be uniquified when signal already ' + 'claims "inner"', + ); }); }); } From a783956d332de6881421e746e55dd5bc9a000d36 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 20:21:36 -0700 Subject: [PATCH 53/58] small change to reduce conflicts --- .../utilities/synth_module_definition.dart | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 8adf754fd..d8faf8446 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -300,11 +300,11 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) : assert( - !(module is SystemVerilog && - module.generatedDefinitionType == DefinitionGenerationType.none), - 'Do not build a definition for a module' - ' which generates no definition!', - ) { + !(module is SystemVerilog && + module.generatedDefinitionType == + DefinitionGenerationType.none), + 'Do not build a definition for a module' + ' which generates no definition!') { // start by traversing output signals final logicsToTraverse = TraverseableCollection() ..addAll(module.outputs.values) @@ -944,10 +944,8 @@ class SynthModuleDefinition { for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { submodule.pickName(module); - assert( - submodule.module.name == submodule.name, - 'Expect reserved names to retain their name.', - ); + assert(submodule.module.name == submodule.name, + 'Expect reserved names to retain their name.'); } } From fa058876114eb5ae3e4ee66a68e081aba1b73f98 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 20:30:11 -0700 Subject: [PATCH 54/58] small change to reduce conflicts2 --- lib/src/synthesizers/utilities/synth_module_definition.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index d8faf8446..871f5c56e 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -922,7 +922,7 @@ class SynthModuleDefinition { void _pickNames() { // Name allocation order matters -- earlier claims get the unsuffixed name // when there are collisions. Weak-name claimants are intentionally deferred - // so emitted objects get first chance at the shortest basenames: + // so emitted objects get 1st chance at the shortest basenames: // 1. Ports (reserved by _initNamespace, claimed via signalName) // 2. Reserved submodule instances // 3. Reserved internal signals with strong claims From 5edbfde275f938952a0fed9c851e025ebb077c83 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 20:34:50 -0700 Subject: [PATCH 55/58] small change to reduce conflicts3 --- lib/src/synthesizers/utilities/synth_module_definition.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 871f5c56e..398f365f3 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -920,9 +920,9 @@ class SynthModuleDefinition { /// [Namer.instanceNameOf]. All non-constant names share a single namespace /// managed by the module's [Namer]. void _pickNames() { - // Name allocation order matters -- earlier claims get the unsuffixed name - // when there are collisions. Weak-name claimants are intentionally deferred - // so emitted objects get 1st chance at the shortest basenames: + // Name allocation order matters -- earlier claims receive the unsuffixed + // name when there are collisions. Weak-name claimants are intentionally + // deferred so emitted objects get 1st chance at the shortest basenames: // 1. Ports (reserved by _initNamespace, claimed via signalName) // 2. Reserved submodule instances // 3. Reserved internal signals with strong claims From afec985b9be29da9307eceaa00f88c50a6b6b5d5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 20:36:17 -0700 Subject: [PATCH 56/58] small change to reduce conflicts4 --- lib/src/synthesizers/utilities/synth_module_definition.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 398f365f3..363157278 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -922,7 +922,7 @@ class SynthModuleDefinition { void _pickNames() { // Name allocation order matters -- earlier claims receive the unsuffixed // name when there are collisions. Weak-name claimants are intentionally - // deferred so emitted objects get 1st chance at the shortest basenames: + // deferred so emitted objects receive 1st chance at the shortest basenames: // 1. Ports (reserved by _initNamespace, claimed via signalName) // 2. Reserved submodule instances // 3. Reserved internal signals with strong claims From 2e933cd8a944ecfe31f4c5b124c9b58fc25b434b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 22:04:03 -0700 Subject: [PATCH 57/58] small change to reduce conflicts5 --- lib/src/synthesizers/utilities/synth_module_definition.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 363157278..e4e080a4a 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -920,6 +920,7 @@ class SynthModuleDefinition { /// [Namer.instanceNameOf]. All non-constant names share a single namespace /// managed by the module's [Namer]. void _pickNames() { + // first ports get priority // Name allocation order matters -- earlier claims receive the unsuffixed // name when there are collisions. Weak-name claimants are intentionally // deferred so emitted objects receive 1st chance at the shortest basenames: From fc1849501a2a592a8fc0245c5d55155b7b622629 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 25 Jun 2026 11:57:52 -0700 Subject: [PATCH 58/58] Clean up synth module definition helpers --- ...systemverilog_synth_module_definition.dart | 2 -- .../utilities/synth_module_definition.dart | 29 +++++++------------ 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index e07482a25..4d15ff5fa 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -96,8 +96,6 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { {}; for (final subModuleInstantiation in chainableModulesToCollapse .cast()) { - (subModuleInstantiation.module as InlineSystemVerilog).resultSignalName; - // inlineable modules have only 1 result signal final resultSynthLogic = subModuleInstantiation.inlineResultLogic!; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index e0686dd2a..f1f7e890e 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -133,15 +133,7 @@ class SynthModuleDefinition { @internal bool logicHasPresentSynthLogic(Logic logic) { final synthLogic = logicToSynthMap[logic]; - if (synthLogic == null) { - return false; - } else if (synthLogic.declarationCleared) { - return false; - } else if (synthLogic.isStructPortElement()) { - return true; - } else { - return true; - } + return synthLogic != null && !synthLogic.declarationCleared; } /// Either accesses a previously created [SynthLogic] corresponding to @@ -618,17 +610,16 @@ class SynthModuleDefinition { for (final instantiation in subModuleInstantiations) { final subModule = instantiation.module; - if (subModule is SystemVerilog) { - singleUseSignals.removeAll( - subModule.expressionlessInputs.map( - (e) => - instantiation.inputMapping[e] ?? instantiation.inOutMapping[e], - ), - ); - // ignore: deprecated_member_use_from_same_package - } else if (subModule is CustomSystemVerilog) { + final expressionlessInputs = subModule is SystemVerilog + ? subModule.expressionlessInputs + // ignore: deprecated_member_use_from_same_package + : subModule is CustomSystemVerilog + ? subModule.expressionlessInputs + : const []; + + if (expressionlessInputs.isNotEmpty) { singleUseSignals.removeAll( - subModule.expressionlessInputs.map( + expressionlessInputs.map( (e) => instantiation.inputMapping[e] ?? instantiation.inOutMapping[e], ),