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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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 42fba62253f91fc7b9ea18dabdf15d3a58455e75 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 12 Jun 2026 14:28:31 -0700 Subject: [PATCH 13/42] 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 14/42] 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 15/42] 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 16/42] 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 17/42] 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 18/42] 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 19/42] 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 20/42] 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 21/42] 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 1225df127afbb3e7b8e5e5dbd1c0f177422fc704 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 06:41:11 -0700 Subject: [PATCH 22/42] 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 c8440c4e5fb294c61d7611f77d66484678341328 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 20 Jun 2026 07:07:59 -0700 Subject: [PATCH 23/42] 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 a87fa5c20118f6813c1f354731e34d6634060c08 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 21 Jun 2026 09:26:53 -0700 Subject: [PATCH 24/42] 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----- + +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 +3SPBxodLhS7wMehdy6NKNO14vDGR/GP43+6oZ91Cyv2CYHSPpZM6+qMwMmGVkHS2 +6PrCVPhPoDywf/7UeFsC4KZMI6LIGD2YI9UEOlcCAEbRwWVjXCSwRZ9vRkxOxK4Q +xNMLAIf3YmUZPnqGVcvNssgsapvjmI3CAWpAPWlP5GTcHxrVGiYz7hNZcA0PfgxF +pmB0QXNxr/x737I9Q8FCZasSlNqocaiKF6gKBxFOKfiKx5DRZ63EZ07Z3HE6y+w3 ++97UIJhjxVrONgb7ZX9paE8NtLG/X0ZldUzqWngfnFVasnCDiQC+ls2Tu9Oa+yMJ +rMe3VM4EcZTjYoESUjKzEHP72hn+GoAk7saWWVK6xYUJPM18Ua1mGx8xwoXt/t95 +W40b92HbJrkCDQRXDI3IARAAqy/YB4Xa+oEF+GTAObJaetvMTqxwrHSzueFjXT0S +nhR1yakkiYt37PBcQViOBZ3o3ilBmxfjKzpRaSqhC8WjI3u28Gcmqd4s87WR7Mz9 +2JjqEwSb0RBinQpC/NnC7AoWA/z64BPHK75IUp6vXr3LCgJ84jMYP8AwgoVC9xL6 +qNvQXqAfNX/hPcJK1EzAk/5Fcbd6RkWpSl9FIa7Sq6ZvMkX47nyX8I5HcIL4p5ER +mdhq1h4+C8zG4vf7nWGiWeumMNIRFOFEsVAfbzbZkha2+BAfdU9q4XOvHYEOI2AS +OyuBG2/F2lgMW/iAKt9ZdVJIhAN9heKlDKC+qwoQeMupx8Tp077PlxG+UwcF1aII +y0Sk0LOVPx1fZe4/hwHIZOct4ptjdlCpjMR6qLbz2WVGT3WgkcVHnUH/YEdMi2Vf +lPQXA7sI8y/8467YTWWJRBieh2f0y0k6eHQx/rl7i6jFVsuYqrirZ265zU0Lb+bc +A/gI6YMutGCzifWGoieBo4nzqc0pPN3tayd6f6V+geTVkIp1S2Sc8cnjqId4jI3Z +gg0pxFy6wpmL+YOo8lf1m3eBmBbjCvE0+/j0HVi3G2fy8XOcNLPnO/n+Tn5ilzuS +jx551LKxeQwWikT40nKcHj0IrcXiIJVIBDA5Da7gYbtT8wsXdwbV4Lvvit1naB91 +XIMAEQEAAYkEWwQYAQgAJgIbAhYhBOtMG/1PBC9t3czskXch9jvTi0eWBQJXDI3I +BQkFo5qAAinBXSAEGQECAAYFAlcMjcgACgkQE5e8U2QNtVFBJg//QTCvdPt7SyhP +PyDhAkstWpkNl1fwh7PTiJ00e68C7QDB1nbCXQL60yQPuXhHZojoEp7/3A+d2T80 +l75lhwP+7PKIoglAPjw+uJ82fC8e70DzSsTgGmlCemUQ16GJttZoY0lA40YUnHtB +NiUWNLks2UbUBfqZCPG9vjbfM5ZI6YRqZhdgGZjIwbq+Sv9dM/OyV2TLxcW4+slR +myUv9aXHfVdDUiu2Qcc5ipbCvSFNznT/Y7wfR7CX90FkurcSaKdln62xO6Ch/SPh +JvFiGmXD32cbBs3W5fLgvz91Y5Redjk6BpMpk8XXnNEzFc30V7KUFVimnmTOt7+t +EjqZDaVp9gd1uO93uvIcXkm9hOhINd3SbMXacvObqPCw7zjtk13kZ1MPr+9x5/Ug +m1rWdLAD+GEu2C2XPr+02dyneUR0KMAzHb2Ng8Nf4uqz0kDFwke5+vzajrAz1MXb +hDytrw1u8Hreh1WJ0J+Ieg6wgUNStrMfxe5pDPJmQjRtvMuaAwC8w7q7XM9979Mr +ot0mDsB4ApJw4lLfwPmabBoPVsAGvrt5sD9fkd1qiZIMpV1Rhp7B9MYEiytaYKYq +l1v5Z9fih0Wk3Ndb+qySIGnlZJ6wq83VBSQslkNkPWTPb75e6XkH3uzkvEtMtHC+ +Aug1pQWveWd6PM0uB0Gl/oWeQDn2zJEJEHch9jvTi0eWVo8P/2OVSzfPFfPUhJSw +zmgNX2WsW6WN91wtbf0oUpORK4otjJETUTvurVHPin473mSAeIypzMO1pHS6Q1uy +Pj5Em8x7BgGza1hBLUTvTIpRfS+J54hoaQL6XGnrE3/QIl/AxGK5aqc9h7EqsTbh +Pckg6BELWueKg1PpCGWtQ1igCcsTUt/kgJ54TjT7dUyuFCAapVgY6lMlEta4dIYJ +dbeQWkZR043o6u7R0HvYHl0P13thD41guhdZsPNah6km5hd7IEXuBNo/HReSHniI +zCKolpIkJyn9X1g+SKJ5aQ6MvFd2L4pkqJKt+nNvkoQXITw9yExDHJSQChX5Qnwe +eJoU0S2Qc6W9jL9qyOw3U+su2/oPzTk2xRu1CwiYLeNjZSNYhU9Az78CsvNrZUUK +CmiZrkmN8tRlFFps3TaF/fodwuYfWPC/R9WpKbtaqjjz3PqXHYbh5NyURVw/EqvM +y1yP26PsQn41tE5Ebndl6P2YzjAZQLKNTc584BXq7Tqj55jeeH/sS2XXv5gF2S+t +m9+Nwyuavl1mC5CNaL+KbkX6w/OadINUOArQW2HC1SwqP184fN9cJCx3NeB24kKg +84M42qQPUOIHfiu0R06JKaPWibk9WAU6ssQLcrbRs5NZ0ySqJWU0tpS/W4Zlz1Yj +Ytnce0VAbz25OAACZ0adKnWgKv8OuQINBFiGv8wBEACtrmK7c12DfxkPAJSD12Va +nxLLvvjYW0KEWKxN6TMRQCawLhGwFf7FLNpab829DFMhBcNVgJ8aU0YIIu9fHroI +aGi+bkBkDkSWEhSTlYa6ISfBn6Zk9AGBWB/SIelOncuAcI/Ik6BdDzIXnDN7cXsM +gV1ql7jIbdbsdX63wZEFwqbaiL1GWd4BUKhj0H46ZTEVBLl0MfHNlYl+X3ib9WpR +S6iBAGOWs8Kqw5xVE7oJm9DDXXWOdPUE8/FVti+bmOz+ICwQETY9I2EmyNXyUG3i +aKs07VAf7SPHhgyBEkMngt5ZGcH4gs1m2l/HFQ0StNFNhXuzlHvQhDzd9M1nqpst +Ee+f8AZMgyNnM+uGHJq9VVtaNnwtMDastvNkUOs+auMXbNwsl5y/O6ZPX5I5IvJm +UhbSh0UOguGPJKUu/bl65theahz4HGBA0Q5nzgNLXVmU6aic143iixxMk+/qA59I +6KelgWGj9QBPAHU68//J4dPFtlsRKZ7vI0vD14wnMvaJFv6tyTSgNdWsQOCWi+n1 +6rGfMx1LNZTO1bO6TE6+ZLuvOchGJTYP4LbCeWLL8qDbdfz3oSKHUpyalELJljzi +n6r3qoA3TqvoGK5OWrFozuhWrWt3tIto53oJ34vJCsRZ0qvKDn9PQX9r3o56hKhn +8G9z/X5tNlfrzeSYikWQcQARAQABiQRbBBgBCAAmAhsCFiEE60wb/U8EL23dzOyR +dyH2O9OLR5YFAliGv8wFCQWjmoACKcFdIAQZAQIABgUCWIa/zAAKCRBklMbWmXwh +XluJD/4mavm5UQ84EczsNesfNL8gY3zzlCnfvnUlJHK+CoYub4wcoDXVUlnCmWgS +lZHQZgr3/qfW2MM3y/kXcbxhL/FijUzY3WlnCdnIVNjuB+QJt0LHbkP7En/o085Z +zHuzaXxfZ97qN+KPsRBTjnJ8hd3B64cVjgnXva1+pG51EK4iDF2bXiWPHvUbPiL+ +Og6C9XjpWrwIA1CWyH/4i7dtfTnbViO2aqKQNHfrXJ+xS938Lr8r5+VmUWByHqwe +BGIASOmwsJeSUHozkZYbmMdaJJ8j458zyfS6LO+HIa3+zhzidOoiEH9c5QvVf54g +NsYjPTcHj7U0DgkxCVQeiBKBLR+q6M6QHa4qax/X0Z2ZCcSDTZwqGJNaKfcFYd8X +1B2zgrxkGweeHKjfmpqfXRKrggHumLdVqHU7KS9cz1yeTL+Nw7ne+kzRMEA8sLnm +4ODRUJwUz12RqS0GG1FYV0rjJVWVzRFMfMUs+7xAptEuMdoddkQSmytkXyOKAqv8 +KQ9XUEbGWikmCxW2cOY9spOpwQa7X2oXe7FlV9RfmHYrG03k+YlIREgFqlvWwsgp +zURculd+CIFvT3vci7vFm1UiQBb5wC8bHOoRsr7OXW1267lipouZr5OrQhVnRZQV +a64cdUIKjLXEt4790uxh8ggNwktZRILIn2JHjgEQICdYWeQb1AkQdyH2O9OLR5b3 +MA/8DRZi0s7SLQwaQiJrT7GrACsIMjYo6SapUVxDMF28QfANW809ANpq2Let+yAD +mEibSgpiDiO7rq6PvYnHmPyxmTbEwMtm1bDi0j55/TybnNN6hnUo8F+o0ywCJjfo +T8GDuBX50ODoOYUMmIoYwyMz/UtNi8iHtxTBPR5b7l1Vt8EfUb3wrwGa4i22mjgL +KU49h7Oyi1VYZRrM+0hlrmaLF79tT9msDnn83mgq9qefkJuU4nBqUXui/CY5b8vJ +XC+8tD+q1wCiUM8uv2LJs/5JyK80zFJbkBXA/ZCYtU0LJEpUf7HjbIAdCMDWjpc4 +j+IyjU+Axv+NkMLgYRhaadnPRVzqY8f2T2Bs+EQWk2i61BVQMqakGtwBWIMCp2fn +GDCxIL/FCN1kIA0J0h9ommhMgZdOJaAktsddr/LwVh/hcYX8Mfy94vPs+E3Kb6Oi +iwPkkN6umQvdFa9Rhh9SUNvmtXzMo3WELLobtvVKC+fdFVatDsJurTRKLDKEvPjS +xFlJ/T8t9yItTBAZ7+ab4nJhWoEbzkVTgNizLCJNmdAEtiKa9dEZOZl0DVmxBhB1 +aqMfHA3S5UhZXmGBHwCF6PcpnM3C4XY2MjQ/sRxdFa7/HFBKOO176h6HyujQ/AyO +llmvJCCg9Hz0Wk0tjTMFsnAbh7dB2GTNQwBNZ60gUCWR+mG5Ag0EXTX8rgEQAKyR +kvTxyusp9fZoPbDw5RLeNUZJbsrXQmv92CXpkHtfH/Ldz2WEGKbuhEiyXq2lH8ME +/nRSdMiAFu/Kdsnq1tYam23rgDOcjt6X2kfSTrcM4px+pFSAkpMzg5RlKRy6pDaq +eS+f6DSiIndWFpVg4l0l8kX+kuPk6LdQQvZp+gR3Tjz+VkRoBNG8SouP6HalJ8RM +SXnAJbJGe4xK7prL02ZXNHGImE8MZbamlBPEm5oqP7pWrDlYhK72exHFM8TUNbx/ +stjI8HCC6W25JgpmgJ1+hgTx9/jvWhki4IpwZJIEdBtHowFMPoom2rMHOl8nzNkm +ZU7iWDQImCn3FfZBnyE+SloFuerYkIxLXOuIIw3yIaFbpkdiZlAm1a65u5m3nVUv +1CYRRSEIXW37eV3XVJqjBjg0UogtR1hsLbMA5AgQQmRZEgcqV65zbNhI1KheXTqg +aDAIpBvmX4uVxgfHj78Xf4rPICrQ2oELWsyeFufe1xyR1nKEsSmfH3/LffKmjpln +Szp0sauZKkml50TPrOvyyIFri5Pci9UXjGN+nNK3dwwP8vOFueTmidR+SagKZD+m +S4qkyvfmEe10PGyEtws8WROdwyMRUA4FOgcNsoNKmW57ImbjwQs+L1ma7I27tawH +xNZUQCRRKHF14cAtWljUP4yNcr5nlqnr+2mmP5+bABEBAAGJBFsEGAEIACYCGwIW +IQTrTBv9TwQvbd3M7JF3IfY704tHlgUCXTX8rgUJBaOagAIpwV0gBBkBCAAGBQJd +NfyuAAoJEHi9ZUc8s70TzUAP/1Qq69M1CMd302TMnp1Yh1O06wkCPFGnMFMVwYRX +H5ggoYUb3IoCOmIAHOEn6v9fho0rYImS+oRDFeE08dOxeI+Co0xVisVHJ1JJvdnu +216BaXEsztZ0KGyUlFidXROrwndlpE3qlz4t1wh/EEaUH2TaQjRJ+O1mXJtF6vLB +1+YvMTMz3+/3aeX/elDz9aatHSpjBVS2NzbHurb9g7mqD45nB80yTBsPYT7439O9 +m70OqsxjoDqe0bL/XlIXsM9w3ei/Us7rSfSY5zgIKf7/iu+aJcMAQC9Zir7XASUV +sbBZywfpo2v4/ACWCHJ63lFST2Qrlf4Rjj1PhF0ifvB2XMR6SewNkDgVlQV+YRPO +1XwTOmloFU8qepkt8nm0QM1lhdOQdKVe0QyNn6btyUCKI7p4pKc8/yfZm5j6EboX +iGAb3XCcSFhR6pFrad12YMcKBhFYvLCaCN6g1q5sSDxvxqfRETvEFVwqOzlfiUH9 +KVY3WJcOZ3Cpbeu3QCpPkTiVZgbnR+WU9JSGQFEi7iZTrT8tct4hIg1Pa35B1lGZ +IlpYmzvdN5YoV9ohJoa1Bxj7qialTT/Su1Eb/toOOkOlqQ7B+1NBXzv9FmiBntC4 +afykHIeEIESNX9LdmvB+kQMW7d1d7Bs0aW2okPDt02vgwH2VEtQTtfq5B98jbwNW +9mbXCRB3IfY704tHliw+EAC5FNOwkABxZZ1C8K4wUDl2Oe7mewVRhVNqvTWS4uib +vFax78HDyLNqKmfi+yRHSQsDAkKr9GzmBc1DOabp4V+IRwj0vADHbcpwoGM7EJ2G +o/0RtdZiTP98B8DMACu17NwjM1l5EUExqjGEeXp3jEZGMSE8vqjq8djkvl8s5mUM +j09Wpj3Gl464NNQ/gnB0P/2sp11T0BVb2u32zNLJKh0ZP9QxXT3z93UBOeiT9BzR +hqFMyl04xpt5rqYDUdiL7y+tZDR28INZZ7aYsCs4NkA22Fh6nI3v43Us38+Kroru +09ipLE8A5fx3G5LxMwtWJA+zZisrrky86JYEFOULGpFuKrklP2bRyaHePjMeqOzD +Y5/n5unqk4+EZAPWIM4LFOwDtTD1BWmuDdpP/RjPuPZUhoMSW0p/Vv/FuBAnpgVQ +9D/kXI3xaAxKgaPp+AzQN50dCosmn643zAGrZTiIDIp1VtXVRFAVinN/mbJkqQJv +8zM/x0bc6EUNb/K8BP/JJp+x5D13DjtXYUEG8TFHz6YKZe9QzlhK5rZY/Fttwqvy +KvIKanXEjOf5/azkdOGlSN6Z74G4l22tui3y3CM+vmRrlMiBbLkCTuPfw8rS6uzi +B5No8PYBwovbqNvpm+dGNHySFTvNyJhzWmvCVt8FZ+c4tqOmwd/D+fhon0Pg42bu ++bkCDQRheAyfARAApNhsGrvrP6Spjk5xizJwd8m0LIlRi0YbMNkqkk70sgbYQMlt +VAKnUajQPPxXTJb1bqaRvPrwi1z5qT+twvvTNrckHjkdmlUKfrtRCMDeJT7uMK4e +r3bYEkYpvLsQXSyBxtes9McVYRNqzPzrf4LnH5KaBMNvPVWke7D5iMX1U5tUHKgh +ohUJd62Z5mugc/FDlyaBPMDviyuVpHHZhc+vmdwS0m+SC/ZYbAKxU6DauXTdkkk2 +wk3R0c60bqAnXn2B3caCwjOJCX4IEUYFoSqBCa6PmYqREqtU+ch1f4gCcvtw7gvC +22C77I7fVWWAEcPMSBm/dFY904VrjKFa/yFZik+36AuVoXtD0yP29n6zWlgscQuH +EVcTLrIgV+upnJUODL88I+dBtVisoFC2HLz0PNU4NKb4EyqoMcC/ZbjfTIg1bZJ/ +QmcezRZbM1a/onO51SYwDZyXmxRwhGXyW0KOLiMCn2G4aKVJAmuNYl6XrG1cwCqj +cHj4MjUwDBcmJ4wFBPBVVJse2SVW9eYhGzLN/ICSif1m/MLSUX5QH5IaxM4dTP+N +1lAFN0Xz5l06xnsgwmCkx4l054++PLh+lONLAfavqnhIWXU49Crn44LVmhVrGU5F +a7RjmiOsX1+qcv5N4Y1N3rPu3XRJcYTwXKjRN6ZD0am/cM/nsUnTO4YlMzcAEQEA +AYkEWwQYAQgAJgIbAhYhBOtMG/1PBC9t3czskXch9jvTi0eWBQJheAyfBQkFo5qA +AinBXSAEGQEIAAYFAmF4DJ8ACgkQTrJ9sqO4i4uCCQ//Ug1HJFOguZjWaz0NNYxD +SXBsEvwnfG7+d4og4pUY53D3NxaUa6BSg62FJtPxuO+7JsfVWPHjAUz5ye4xV+MP +nxe7pmmAIc3XBdgy7NjB4EUpoyDihLBMq4AkEnYiF8Sb9wCvJW8pjbNj67LOCLPH +e8CDeyOQA8NytIIk/aeS4dwnefNRso0COZ0yydYOuqplXA/32e7IyTxsC255nRIq +8ikK/bAh5g7vOSPrW+5A4U4aGX3w4G6LnBSG2BDD/96xNZiIY0pKYPd16t3YkdUD +TW0GYJZXgowsNuDcJwwxDXHdXWZ7oQbeCLAEvUj3FOwFRsRrp4Q31TTN0q+gxtKi +A43nAK7EDM78JcYyt4m0FS6kcRzr2hO7B7jboiGLcBtGs8CDe2cYYUK3XUehAU2d +E9Zve6cXxSUDatLK2/AXJCLenMFi3lWxMgDs0Qca4mz786ivoA4ifOG3VynsB+YM +Z8bLY3mjD7gYjoU97ZSoiDb6cWIav2FFk69dGAtAvx2UOcUKHKaV3Gb8n9QV0kZJ +ZGV0QOw+vMdARIq+xX0SOclBHmnnORArqPHTOpKUOCI0bYZPf8JK/Ah0KKHoKX0d +OEe1g2bdlg3RtT1baN6guHcAg01NyunS0Adm5AsXG6RuPno7l4H6d+Trv9faI2KL +jpl0lA3BtP1g3oKy1DP4KeoJEHch9jvTi0eWxrwP/0zlWCYOsNH5Id4SZsPKe8im +evCbj3lvboTYPc4u6HvbbwbYqLerzP2ajWSCdUAK4CMrAuvFildo4k6COh6VaZdi +DOwsKoJfs6Vd5oud5a+jRnv8+oktRBf5OAVc3RLfBG1RC9qI891JTOjGrTU7dBJr +RjRWdy9YQd/epN2I0RVtUaJlxKELoFj57FPERZgg+yomiheBARK+fLYY/oFTwJK3 ++Kt3rdnBtUeVpEiL6VjU6bqvIpUG+P0u27AspcacgDewg59+thcbY4tnsdo6DSZB +Q92bBPVGzpXPEhpQ/vZM63CG8qsZfQ1jw82ovmSnkKPLnBQRabFYVl0DCl1uYHg2 +4Up66w6Lj/tT2XbCeBf2n54K9HoUMV9f7/pLoTa0dE3UYI1K4GLZdp+yxMveUEjG +nh0YOTBmoBtpdy6Udejujil6xbH2gLwbICFm+boKVWwzrYCyfl51ASiq5dmqQwd3 +tPAg9Hc6qtvZ8cswyWyNOQpZo0myvfPaKrHWa9u2GqQmeGBwhckXJxFM/zau0yx6 +NMkSFI49kTglw0A77rcmlJUAQQeoXmTKMl6NM/3AUfvL8Qfu9/74kgoFI9pmQFky +BtcQMCeB2/JQ9K9ywPhi/gIebjftfMgKQsTW+/6Nl1yZ8q38y2n1J4p/acVlFc2K +PhbmKL4CvcSdlQS4CbvFuQINBGPs+VgBEADKbgLL+vAabKV2rGSDgY+IttTAtg9w +9Uor1+Q/CIWGxi/JQy7l7XTKjmS0wvdwU+9f/eGsjxigbvAcSsV1szyKfVQQFT2m +9KhDrBqNCAvQ5Tg6ZQdNe51oHwjiIQ1i7z8QoT22VucdTYqcMLAHe+g0aNqLLSSW +LAiW4z+nerclinjiTRCw/aWZJR1ozQd2eKwAw6rk19bHcihXo2E0K1EDmdHcNA8y +typxwWWXBftCYRWXi5J02GeZazxmx/DULnFgy2J4G0ULTqGWsbf/tCt22jqgyX+v +Fj/sJPn+l3IJqpyNY5yBG6GcejeP9vRoQrapGqHkcx+37f2vjwmpj5548JI52KEC +1yZeFwp8HjGLp+zGajpnokrKd4XJHniW9+bPLq7Yp7PNn65MaYvZUjv5enKd45fF +K6vJ3Ys/fx6PBXKKBs9flRIgdXOKSvtV+bGIG0I/p/JEZ/wPxRgxHPDK5jbcI6KB +Vm3Uk+CHFC4IBAtzdSh6H4Zfw1EH3dQZMLVBB/Sj34UQhlwAOlAXtZH3vks/Kpcl +WK8gnqz3i8HN0ezvcnQlRiRO8IqlN9/PmFqZeNTerklT7Tt0jXqiopLHL0FXR2Ls +ndeORfxDE1rhVOUxloeuIsY8x6gO8h2bGg41YapROjYxZZEcakg9Nch4XAlxeqB4 +ISttfbiVxeL2DQARAQABiQRbBBgBCAAmAhsCFiEE60wb/U8EL23dzOyRdyH2O9OL +R5YFAmPs+VgFCQWjmoACKcFdIAQZAQgABgUCY+z5WAAKCRDoiXn7mzCs8kblD/48 +yE3Wpi6Cw8RBzq2uzLdkuqXh691zG6VhHUZQNb85ewGjGDu/D25u2JFrhAcmlzOr +xggvL4a8WatPXQaPqDZaSh41elM1Ya0C7cNQq7xNVA0pcN5bQ+KXXZMuQaA89BCl +TSXITz6j4O4pvhAG8y8Q2E9Mv7UYas0OhDgzVIry2s1o2Pml1qjlb9jctO9crRUi +F6v9Ru9aQkgGHYt4uyP3HzKDfoNuzX/WX3O0Fm8NNpnJk6qZsLKwg7ukUdJOIEIb +LLNLU9ZYmys3wNtDKMfm4T79abSNwNIn4dd5hapH9BAuDJnk4WnFOap9AQZPgJX2 +WXKC2DXQZeSX1VXpI3rr7FSbSec8d5bitw7s20XWyQB2+ZoetRxNgR104GIh/Laj +tatLKFc9NnP9Smhey8nrxVZFx6HuXsnGOPkbjsiFYMsxtPVYnO72nBDTDP4ZejLO +aay2KtCb8pJkCH8U0guquDGVd+S02Xx947evyvHqGt5V0yVFPD7uAu7A5QBYXvtc +tzq93S1jZDIoMP93Oe8VpUrXBBfizzHVxP6VUmxM97IE+gjVRqN9PuMrp2D9yEBU +Gk44fQW5zyuuomYac7Mpx2fnWgGA/Al9ug2uvS4oIzUyLEJxpc6M8RYluacSIjFg +CigucRsvTBy6lobG1FMvnQyze6+fAeKbbrK85OuA1AkQdyH2O9OLR5bPGRAAmgSi +hpu4US/JoWnR/aeiFf9upobXVDnBnqOAXiMUaFeS+hUuh5EWUhDLIWYvXXhPacvb +pUOlxwLsLIdPRQGGSp1/rqhVRnmWsJ34DoAKxG7Elq8EArK/pF+v4wSUMegjAPJQ +evIcLvm83z+jHmbk1AEeioBYTq45RbzlHmyLmGK/zT13KnBUWE3sFkECoco+vMli +8oPeL+JMfiMgPb2vDs+58YlHq5W26pe08BwGzY5LQM7Jt52oxsqgXEX/N95QqgSc +sc625wCIE8/Qo5pXT0TKk+5ViFojs2Ei3mgXHBXFgISdAtWBEmqN9TESqPPrHzfn +Fk9t6mPg1r5Nt37IKO7oTzu7/SXrJlXPIQ99Nlq6HO/mMVdYjbWFBPw8+NGVGemQ +chOODZsksvHJGV4gjMpW1FC37MRNsiai1UMraVxzsrCte4/oqpa7bY8VdWw6p5mv +fdroLkwHW2cS2lgC8ft7e4npiHXXLAIib+sFHcrIkZu0uJxGCJOkUwkaDrAFKWzZ +YHc2YUrW5XN7CNBo/fe90r1W9/4esn59SM2mTMarrUn1fiExwFiUci4U+3/7U4Ii +ViNeNoZ2J1+hqxudlx1OT7Ae2Wg4dLASoEHaMKby4+JVVicA8jdlocrCbpEv1hVV +47hwiKc+VTQGvCZqs8eT+pbnw1Recd13J9Ny7bO5Ag0EZbladgEQAMSm1QPtyjAr +XdM1i2Y6439Jc/AJy3ykVjxTaDi6n5z7lgQipaQBSpWbwun4Op0W5fs1t8rYE2iP +A/KKoqVoEA3o3Hts71uNK+VttkGtUneYv6TvGsV1MYt4NJJOUQF6yPsVcrXMrtJb +0BXefjmWY4sBdMLXdVDcrRIRdv7r0XBevfX+Lng2BN8z/UtwlmEihHoy60ckJJgq +47pkfFho51+PjwEZJaPtEgRsXn2sgTMNHukGTrV8ub/aKWVNBPF0wYYF5LA2NHgV +p148nS11F4OgiNpCkAZmJQCPlyp4emYfxkihjh+TZKw6KcrxwOCx7YeceKK6wWvr +HHrwjJxl2nhatDIYNIlnVkqTlBp4A9gTdCxmciZ1xXb+QllLycBYMWgu2lo1Kk40 +NOfVljIKLatY88XwmJUySYLGyX5kePI29kc+yVGycYHsSgoOlyM/Vw+GXfuj/BRi +nKItjITxb6YM25wfhgctUer/NAao7dXprFMDUOz6C720dX/f7ISsiqmi7X1U588o +mNgLvJ/O8gPnyMtk1gWrwhFZDlVYI5AlYxx3MwoHntLZlvm8iEmR+X9LkhIwZcNd +vfafIpV+8LlOaIxt+uzNzcMsDHCGomUAf/GYXbI8/x1iHoopZIh99UZObfyxyz2S +SbVtUEBHXyKXHp0bFWM1Iz2LfQwxeNRRABEBAAGJBHIEGAEKACYWIQTrTBv9TwQv +bd3M7JF3IfY704tHlgUCZbladgIbAgUJBaOagAJACRB3IfY704tHlsF0IAQZAQoA +HRYhBA8G/4a+6vTnGGbuUjLuU1WmvG5CBQJluVp2AAoJEDLuU1WmvG5CmB4P/1Rn +XKHryp3UlaOAq/UAF2YKFS9NAggVwH8PhsFc6nZpruc+CFU1s5jwCuW9aiWgQ+Tj +BFvQ0h/bHLbujlTSmfyyyo/Ij+4vSxRzlmUa8lHPqyqv7fIsQ82AAs8WE/mV8Dif +24hsxJSZEH130DTkRqtnXS0FB6sOQPGj5EKAFt3v0vN/Z1QRX2eLmZc2jO7QfkdR +strvF3borb7xdt26/PM8g8RgYaG+fqIJ/NtGQF0XI+WUxuQ+mtRGEyVpL4qnwwno +kyxjsMxsJvvGIaPULKR1CahGJD4tAlyE3DvNikMRI2SDojaGyh5cw24mJJVZmx46 +7Q3tE4dwmAu8pCGCldUQBG6eprTL/WauyJcmkJr1qsSK7gyx+Uy8mwXESY/s5bwD +kzhlzaJ0WjBxqXfoHFIElHJfhLS0efqIr6NFmPUu4cBKJKoZoFBwTPTTEmWz7tE2 +mDgVO9Z6Q9fq7CwZS6J/GchieQgAy3Rxm5BizBZsWisY3BQ4JX1w6wH0Cae4rYCe +bkutFFWBg7JA3j2nkgfzsD3kYHYf5BllL2yV589dEocNjPios56vPi5kg9UQOFO1 +SaX4Efu1eArNcNteBxKf5pH8okDcgjqj9yXZRs6fI2Uk9zzz0UL63+iRSqSj8Kv6 +iepLCzOph1DHnY2tFghpSFYqlayhdprMJVk7GmLFoiYP/1nT6wq8k/RDS3/W7HEB +J8Rtxs1vL51nU0e5K7jgbUT9kaG2KBmlnRbgkELjvu0lX6zLFiyPcc5JkvE2AyfZ +7t5cIfanOS4hc0W9C66RQo2cvUxkn2gtCrM7KCTc16Iwe/uMC2RNEneNLiCetwc5 +DhpjYExR59szzQ9Npx31pefsmkSwKdutEz8W96l29yHYgIDoLYW3b6nuBRBfp4nA +XQ1gWqfEmFNFlKZBa2pPsKNlFgpchC+EiMQ/db1ElVNyW38K7IOx6hNGpEBJwbPu +HNef9WU3n2DIIgMBHTHPvbNHiCNTfuOM1+/BMbmK59RmW66TS0UaxZsswHHLZt7v +NN7SKzXsveT9+A1d6wZlVoy8Y3gykBKnBHGRaGO0zaXczHt4YsUA4L3is6lAjbIo +pU5M3j2F1RFKRr95+HZT/NXNeGbFvsdKmvP4ELtDAuYVMgYR8GqjI5yP/ccVMsi/ +mhT+cUxO/F7+7nixw1Go637Jqr/NF5kjjrBD8EiGy8QrGm6uBR3NGad0BnMWKa2Y +oYKF1m3Fs/evBkcymR+hSwFzkXm6WSOb8hzJIayFa6kAc7uSKyR5iG00p/neibbq +M1aUAQDBwV7g9wPmcdRIjJS2MtK1JXHZCR1gVKb+EObct6RJOVw8s58ES5O9wGZm +bVtIZ+JHTbuH+tg0EoRNcCbzuQINBGd9W+0BEADBFjNINSiiMRO6vCSu0G5SqJu/ +vjWJ/dhN7Lh791sas64UU/bWDQ0mqDms0D/oWjQNgapHRXAexuIynbStlSxXO0Qa +XEdq50BCVoKXj9Nwx63WWBXaR/cwAaBbKLYGUSsMEzqMXZul7VfuOyxGPcgHnz67 +dYDyUOIdUisFiBUkTwoUNXE4Qc9kA9i2jwBrY1s6+vtMX9J5uMUw78mtBG3U6TDr +7cgwlKe6nuNbt+EXpRsaKNPq5qC/9HEyRgq9i98Voo5b1gjC4adnYFZ70SKb6PrT +kkpf6b0wi4BNJxYzUBWzYdw9UKPwB4RM9zM20PSWxMuzBfn4sPN2FC0SjdZGeu92 +dZ4NcCwNJuPhFq4fz6TD6da2mEE9H0qlJIhgaNuTHyI3YXgLk4FH/+GhylO74uMh +cMa/A1nCq8Yr+4OscWxbyN6fv8Jsg2y1wQYdnIqsEH1vx99k5Xy/nF6rWqQfdy9c +UeCD00bzJyFSQQPieiP45asekajwAXph7nRby9rACbvdZUIy+RsRJoFTS+5flChr +MvofJoOEqJ58NzCNXNSq77yISZZE6aogqgp2hgQY2UFpLoslSUqvFSx6ti8ZViXf +Z7e9zKTi4I+/cpQ+RuzkBFYBgW7ysKnUWLyopPFE2GLu7E6JTRVTTL0KAiCca6KT +v8ZNe6itGuC7WmfKFQARAQABiQRyBBgBCgAmFiEE60wb/U8EL23dzOyRdyH2O9OL +R5YFAmd9W+0CGwIFCQWjmoACQAkQdyH2O9OLR5bBdCAEGQEKAB0WIQQOIlkXQUZw +9EQsJQ39UzwHwmRkjwUCZ31b7QAKCRD9UzwHwmRkj6YZD/4h1o52LhFwu7is7fs7 +7Ko5BpBpF1QKV4GRpvYdf7o5Wm9BSvvVQNSZVbs6sPUgWLsFMJBl9E1VQgnOSgMQ +2urGB9iIIHAvnTeGYwjIlKyZRBzVROn+xY4OfUk0nK/o1jnJCpz+adseMZh9JGV/ +65GfvdJX54j1L1bf4OWrp6BEA77TDmQZ9zqYMeMzlsaiuLxjLRdW4RVInjLYOQdx +OY5TXjcJpA2FdzBxrvqDGMtUxTANzkLkzs+XXg/OsRO94SvR0NwwaBEzyLs5WFz9 +KqELMFSgSOM+x40S5nwUGoFwl4/uuCxFGrpgGZVlld888WZwJOJMyb+dfrxEsWjJ +ui5eVRtfDC68792YuBM+ATK+zo2wJ8X3IK7CEw5cK8HgmAu0avX1sOVEspPd4dJD +SfAFU+ghtmufy7As7X1uI5IOyxQ1lpDCEqDf6wmkdrCX78tmoo2d98gFlJxKVmRu +vvPNdWABXZ/YNW57lix8fWe6vFY2pcyYVRXvX/DIcJNiu+uFVC+6ZzTWMZeCo9KE +wKlVRg2aDFhwnBO58ahm845/B/7p02NL7SuZPAT8rlLdA7XpfH7KY5Q5eaOVW3gU +KOnBQRM2Unea22r15rYsYS+whiqglmh2yejmE2vOVteJ3VJkSeaj3S3GGpHZdelI +/w6xbihzj67pYAG7PoZoJtav52HYD/91FDIGqsVOnn7IlotzN6c/Z07tJnCPJKSc +736L+1iDYyy7tvslUckW0vfOO92a+ikuPQRajlzUAZrWZe+23M+bIX4T8aCi3fGC +VWsr5wUK4wiBNQgAr5iQWRg2UjWNLxGuBvp+lk9w8BGp+qZWd/8TOrOHGmXz+N2W +ZBIrtTNbL0LYMxffBxcQIV+aC8jD8MfEetV9F7SsZo1Wza0wcEXyX/xUQ5pr+aks +aDtoNYKWwnJtlRqBgb6A8LPeRrzxTZVlHrOMUDHJSKNNSbspyRi8jmhJtfU17uE9 ++rpQkzv29ZRiDi4vtub6RSpcAaw+squMq7fNberxr7SNaWa7dVnJu4XHvAhS6838 +6Ng9vMhzyLE9GLyuwJ8FCv0jCiFdRFDayyEYZ0zAZz/gWjhdB8XAGJ5US0sEnD8d +qQE4JR5iLzXEZArHyGUDl45/JbxV7O5Z5D+SlBef/nHLCY/JBHc3LGGnM0Ht8GNj +d+om6kTznz3lZjxQCj0LFHYMeO3ADyk5uj8SKe9yMXHhl25Dlye1tZalTyosEIdP +UZMFqTLSQNh0nW5iJ8QYhO9bSaksUKadhHzVzoFk067OOpZLlt/SO3a9DTgBqJnm +jZzrnsTJpU2ctkX++wX6M0WSGfkQGJWbuf1tRHdl+IkfIu+kBE+iAhZoMQAysweF +p6XgWgagK7kCDQRpsHinARAAtf8XGrdD7k8bRRhCCjjJUGkGZdzSZLyQRQtQDGNP +ofM0LQ9xb03qMXN+qCPgQtNe3FwESEkonjICP+E9en32IYo9QoV9662h91MsQYpi +vlm2G/Ink2BxTJpmKwFZQwcoZ4Eq1wP5KWn2VL1qpWnyf/82/lPqEnc/xXHtks5o +YwNiRf5B/VPz+/IzzYayIxRmxaWtBVT6MAeDkEcZiZCGIXewaV2jC745ST0MsOLt +78pXFHuV3PlnaU+JzQO9gJFIgoyrXAKKkYAqtYuXUQfIZpsioor/WMrPnJ5v2miz +ygFHYzxh4ZVqOyeQu30TNlToJ/0As4cXEdBcMsdo4ZWqLRpavoN8k5wxNHiq5Xo7 +gyVvT4x2pQ4Cdc40NMS9fwx/re9aUMK+MkYX0n2nlfgMiyZUaswS0hwVXCWBwqT9 +1qzUh6JStncd6voLsAoKjpnDFelnDTUUOXqV2/CfLeeZSgdOF5jejJcqIzFd1mbN +Ui7QR+/2EBRjTvCruzA6M73SJGcnFciDVO70Z8+bTIqZNObmy2ARm6flKMsgbIN4 +e7QROdPXrEGKxRsLCEMbimGG5DYXNZPxDkt5TpTi61topkkmxKhRIAnUA1nhw+5P +aHvGxGwbqjEeRDQJLiAqE3BHh0hDCLqJbTnWqww4zSju/r8ICIOBT7W4sqBH0zVf +qscAEQEAAYkEcgQYAQoAJhYhBOtMG/1PBC9t3czskXch9jvTi0eWBQJpsHinAhsC +BQkFo5qAAkAJEHch9jvTi0eWwXQgBBkBCgAdFiEEuNvpzK8hFvhKCEvWHQnAFQBv +6rgFAmmweKcACgkQHQnAFQBv6rh54BAAo9VvH6LxBwbzUg1HQSIg/YMel80nMQzA +I3jfIPRTSC5CHcH0zfZpx6tLjU0eBD8E17jjp7NBE/cMDOGh4ocyyZTvG+rN9jtz +jk5Hd+4U+jxXF1VcYhYvKDNK2Y0BnLhcy+krXuOudP+r6CQqCMrMd70s2appU2w3 +p+p5wsCTSZV7WvxHHe6tSRUgzQz7e5CapwV0j/SQQYNJuX9konLGT6gs1Due54+U +xlBZ6BtfdTgMC7Ln7a7xntGG533oDd8J+LM+26O+Mzu/tFEZekwQqlewjT2I6N9N +0x/5u7cNMonWjiUMZZkEuts2ugjzktRviRvbDvhdIyje6+4uHicTF7pBUuLcRw8t +6onHrsjddE3I+rWw6jkm+5R5gLiriApKSzpRnSdA94GN3OCpmWjkO/XJTrmKT2/O +j6rrCyxnrfs+AQgfoev7f0B3F3UnRDQfYO3WhMYzgZ4CjVSpGyevsq5cAPYXkvyl +RH15wdJ43EToUwYheg0fvwexH41gkjbA+f1+XK1Ll5guspnUhlMTXni+pFTTFlhj +WF7lVnjcG8Ye66ymwIlMucShFssWlfCgFWh8lJx0ZYjNLrcYm1qGPH3w4c4RUH5E +YmXeb5zsREvRMaqEYTeDIWI4xvg/KsI66olxYn9fcwzuQrCmdVrzTn9LJw8C4d6U +LsuXrfChv0Cc/g//cIc2n6IuudMs7PI2f4YX0aN9HHVc/wDgS13sfJJWuXFwIttU +upMiKeiQ7083UKL84/1KhvEVFKQHpYeHS5+LpXH31F+JIVt0lJjhRuU1I5PcRE9W +uqacfqMlavkmz7q8WF6CpuGQGcHI4nSRfJYcMWHVt8swVPAiiITU+ou2mO2K31ao +p411RcZ/vFrC5BpPSKJpsD8Gvm80iVwZBeRXrzJW6B/83tnHNPsM0fGVojxDgE7i +Wp+Dv89n8BsQ5jIN8evHHe2I/T6Jd5zik7nfJbkzPCDgRPIQn6JesfpOyn6rUXYK +07+1t/yLHtMmyZTJBBFLqoJYOE2u6JoDuzCRYlZfj9Gm/uvVts9WcwMs4ymo5ttU +2+LXnOwKAVWizRmLLpywk348XAd1dEkQ5Tv4iTSKlyIQpRxKq50mFK31W1CjQgGe +M1Ctf3LXScrlVYldo5Wn0PmEfEVDB2E9j94jGsB/dBRYWAMZZe1eXX7oAdhQIedW +xDYjKzy/ZNTFLqIgwAawvxaKOLqm8pCVCa/Hkd8x7PeL/CD4q+XEuhRanIZasbaP +wOSz6cWG1532PsdUEJMr93rjh9vvcZ2Aee4BEH9ly+D/qWUJysuljMlpxQ+mG9n0 +EFRbD9Lhk5tL9ArJlsUZ3Wg/a2N+cNFSkXzUmw0Rj/iUmZcSITcM8QOSK6U= +=CkA1 +-----END PGP PUBLIC KEY BLOCK----- From b7e46c0f3bde4cb3a7c6374f96575e0cd96a632f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 01:39:49 -0700 Subject: [PATCH 25/42] 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 26/42] 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 319706b66ab7a9dd41da198c7f5e39f711a362cc Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 22 Jun 2026 05:35:14 -0700 Subject: [PATCH 27/42] 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 28/42] 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 29/42] 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 249b2101b9ce546d4217b213c27ed3bb4faf159e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 07:36:46 -0700 Subject: [PATCH 30/42] 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 e35373af539b3417f9e1f2a754542069c3021676 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 24 Jun 2026 10:00:07 -0700 Subject: [PATCH 31/42] 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 32/42] 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 33/42] 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 34/42] 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 35/42] 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 36/42] 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 37/42] 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 38/42] 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 39/42] 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 40/42] 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 1f5ef487a956ac0a4a6fb939937a696b7216afba Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 25 Jun 2026 00:05:20 -0700 Subject: [PATCH 41/42] cleaned up redundant code --- .../systemverilog_synth_module_definition.dart | 18 ++++++------------ lib/src/utilities/namer.dart | 4 ++-- 2 files changed, 8 insertions(+), 14 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..43aa38320 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -18,7 +18,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { @override void process() { - _buildNetConnectsForNaming(pickName: true); + _buildNetConnectsForNaming(); _collapseMarkedChainableModules(); _replaceInOutConnectionInlineableModules(); } @@ -31,9 +31,8 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// [LogicNet]s. SystemVerilogSynthSubModuleInstantiation _addNetConnect( SynthLogic dst, - SynthLogic src, { - bool pickName = false, - }) { + SynthLogic src, + ) { // make an (unconnected) module representing the assignment final netConnect = _NetConnect( LogicNet(width: dst.width), @@ -51,15 +50,13 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // notify the `SynthBuilder` that it needs declaration supportingModules.add(netConnect); - if (pickName) { - netConnectSynthSubModInst.pickName(module); - } + netConnectSynthSubModInst.pickName(module); return netConnectSynthSubModInst; } /// Builds [_NetConnect] instances for [LogicNet] assignments. - void _buildNetConnectsForNaming({bool pickName = false}) { + void _buildNetConnectsForNaming() { final reducedAssignments = []; for (final assignment in assignments) { @@ -69,7 +66,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { 'Net connections should not be partial assignments.', ); - _addNetConnect(assignment.dst, assignment.src, pickName: pickName); + _addNetConnect(assignment.dst, assignment.src); } else { reducedAssignments.add(assignment); } @@ -96,8 +93,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!; @@ -156,7 +151,6 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { final netConnectSynthSubmod = _addNetConnect( subModResult, dummy, - pickName: true, )..synthLogicToInlineableSynthSubmoduleMap ??= {}; netConnectSynthSubmod.synthLogicToInlineableSynthSubmoduleMap![dummy] = diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 70b0a6e47..8753d489b 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -24,8 +24,7 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { - // ─── Shared namespace ─────────────────────────────────────────── - + /// The [Uniquifier] that manages the shared namespace for this module. final Uniquifier _uniquifier; /// Cache of resolved names for internal (non-port) signals only. @@ -70,6 +69,7 @@ class Namer { // ─── Name availability / allocation ───────────────────────────── /// Returns `true` if [name] has not yet been claimed in the namespace. + @visibleForTesting bool isAvailable(String name) => _uniquifier.isAvailable(name); // ─── Instance naming (Module → String) ────────────────────────── From ecec47436a3e36275aa7cfbe390741cc9eecd4aa Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 25 Jun 2026 16:25:46 -0700 Subject: [PATCH 42/42] stick with Set ordering --- .../utilities/synth_module_definition.dart | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index e4e080a4a..6e575f58f 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -953,7 +953,7 @@ class SynthModuleDefinition { // Reserved internal signals next. final nonReservedSignals = []; final weakSignals = []; - for (final signal in _signalsInModuleOrder(internalSignals)) { + for (final signal in internalSignals) { if (_weakNameClaimSignals.contains(signal)) { weakSignals.add(signal); } else if (signal.isReserved) { @@ -977,7 +977,7 @@ class SynthModuleDefinition { } // Then the rest of the internal signals with strong name claims. - for (final signal in _signalsInModuleOrder(nonReservedSignals)) { + for (final signal in nonReservedSignals) { signal.pickName(); } @@ -986,41 +986,11 @@ class SynthModuleDefinition { for (final submodule in weakSubmodules) { submodule.pickName(module); } - for (final signal in _signalsInModuleOrder(weakSignals)) { + for (final signal in 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) { - 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) { - 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 /// it's full array-full array assignment void _collapseArrays() {