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 92fc410e0..8c3c79692 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,11 +11,11 @@ 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/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -52,6 +52,18 @@ abstract class Module { /// An internal mapping of input names to their sources to this [Module]. late final Map _inputSources = {}; + // ─── Central naming (Namer) ───────────────────────────────────── + + /// Central namer that owns both the signal and instance namespaces. + /// Initialized lazily on first access (after build). + @internal + late final Namer namer = _createNamer(); + + Namer _createNamer() { + assert(hasBuilt, 'Module must be built before canonical names are bound.'); + return Namer.forModule(this); + } + /// 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..f9d0a0d08 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 diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index b70c9338e..687bbab03 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,7 +6,6 @@ // // 2021 August 26 // Author: Max Korbel -// import 'package:rohd/rohd.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..062647ac3 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 diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index c3026a0d5..8fcbc014a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -11,8 +11,8 @@ 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'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a logic signal in the generated code within a module. @internal @@ -212,92 +212,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.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, + /// + /// Delegates to signal namer which handles constant value naming, priority + /// selection, and uniquification via the module's shared namespace. + String _findName() => + parentSynthModuleDefinition.module.namer.signalNameOfBest( + logics, + constValue: _constLogic, + constNameDisallowed: _constNameDisallowed, ); - } - - // 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, - ); - } /// Creates an instance to represent [initialLogic] and any that merge /// into it. @@ -404,7 +337,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}) { @@ -551,17 +484,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 37ebfb323..81c74b696 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -14,7 +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/uniquifier.dart'; +import 'package:rohd/src/utilities/namer.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. @@ -110,10 +110,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 +285,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 +454,7 @@ class SynthModuleDefinition { final receiverIsSubModuleOutput = receiver.isOutput && (receiver.parentModule?.parent == module); + if (receiverIsSubModuleOutput) { final subModule = receiver.parentModule!; @@ -513,6 +503,7 @@ class SynthModuleDefinition { _collapseArrays(); _collapseAssignments(); _assignSubmodulePortMapping(); + _pruneUnused(); process(); _pickNames(); @@ -767,49 +758,59 @@ 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]. 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..343ca1714 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,7 @@ import 'dart:collection'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; +import 'package:rohd/src/utilities/namer.dart'; /// Represents an instantiation of a module within another module. class SynthSubModuleInstantiation { @@ -25,13 +25,15 @@ 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 [Namer]'s shared namespace + /// via [Namer.allocateName]. + void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = uniquifier.getUniqueName( - initialName: module.uniqueInstanceName, + _name = parentModule.namer.allocateName( + module.uniqueInstanceName, reserved: module.reserveName, - nullStarter: 'm', ); } diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart new file mode 100644 index 000000000..d4c6eff85 --- /dev/null +++ b/lib/src/utilities/namer.dart @@ -0,0 +1,229 @@ +// 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. +/// +/// 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 [allocateName]. +@internal +class Namer { + // ─── Shared namespace ─────────────────────────────────────────── + + final Uniquifier _uniquifier; + + /// 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. + final Set _portLogics; + + // ─── Construction ─────────────────────────────────────────────── + + Namer._({ + required Uniquifier uniquifier, + required Set portLogics, + }) : _uniquifier = uniquifier, + _portLogics = portLogics; + + /// 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(Module module) { + final portLogics = { + ...module.inputs.values, + ...module.outputs.values, + ...module.inOuts.values, + }; + + final uniquifier = Uniquifier(); + for (final logic in portLogics) { + uniquifier.getUniqueName(initialName: logic.name, reserved: true); + } + + return Namer._( + uniquifier: uniquifier, + portLogics: portLogics, + ); + } + + // ─── Name availability / allocation ───────────────────────────── + + /// 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, + ); + + // ─── 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 = _uniquifier.getUniqueName( + initialName: 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); + } + + if (preferredMergeable.isNotEmpty) { + 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))) ?? + 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/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart new file mode 100644 index 000000000..65747204a --- /dev/null +++ b/test/instance_signal_name_collision_test.dart @@ -0,0 +1,87 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// instance_signal_name_collision_test.dart +// Tests that submodule instance names and signal names share a single +// namespace, so a collision between them results in uniquification. +// +// 2026 April 18 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Minimal repro modules ──────────────────────────────────────────────────── + +/// Leaf module whose default instance name is "inner". +class _Inner extends Module { + _Inner(Logic a) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + addOutput('y', width: a.width) <= a; + } +} + +/// Parent module that: +/// • instantiates [_Inner] (default instance name: "inner") +/// • names an internal wire "inner" as well +/// +/// Because both identifiers live in a single shared namespace, one of them +/// will be suffixed to avoid collision. +class _CollidingParent extends Module { + _CollidingParent(Logic a) : super(name: 'colliding_parent') { + a = addInput('a', a, width: a.width); + + // Internal wire explicitly named "inner". + final inner = Logic(name: 'inner', width: a.width, naming: Naming.reserved) + ..gets(a); + + // Submodule whose uniqueInstanceName will also be "inner". + final sub = _Inner(inner); + + addOutput('y', width: a.width) <= sub.output('y'); + } +} + +// ── Test ───────────────────────────────────────────────────────────────────── + +void main() { + group('instance / signal name collision (shared namespace)', () { + late _CollidingParent mod; + late SynthModuleDefinition def; + + setUpAll(() async { + mod = _CollidingParent(Logic(width: 8)); + await mod.build(); + def = SynthModuleDefinition(mod); + }); + + test('internal signal named "inner" retains its exact name', () { + // The reserved signal should keep its exact name. + final sl = def.internalSignals.cast().firstWhere( + (s) => s!.logics.any((l) => l.name == 'inner'), + orElse: () => null, + ); + expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); + expect(sl!.name, 'inner', + reason: 'Reserved signal "inner" must keep its exact name'); + }); + + test( + 'submodule instance is uniquified because signal ' + '"inner" already claimed the name', () { + final inst = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .cast() + .firstWhere( + (s) => s!.module.name == 'inner', + orElse: () => null, + ); + expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); + // The instance should be suffixed since the signal took "inner" first. + expect(inst!.name, isNot('inner'), + reason: 'Instance should be uniquified when signal already ' + 'claims "inner"'); + }); + }); +} diff --git a/test/name_test.dart b/test/name_test.dart index 2742c0ec8..afa757cc8 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 @@ -136,6 +136,9 @@ void main() { final nameTypes = [nameType1, nameType2]; // skip ones that actually *should* cause a failure + // + // Note: SystemVerilog does not allow using the same identifier for a + // signal and an instance. final shouldConflict = [ { NameType.internalModuleDefinition, 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..f0d7b2d31 --- /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.namer. +// +// 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 namer 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('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 namer 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('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.namer.signalNameOf uses Namer directly + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + final moduleName = mod.namer.signalNameOf(port); + final synthName = synthNames[port]; + expect(synthName, moduleName, + reason: 'SynthLogic.name and Module.namer.signalNameOf must agree ' + 'for port ${port.name}'); + } + }); + + 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. + 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'); + } + }); + }); +} diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart new file mode 100644 index 000000000..a5263a998 --- /dev/null +++ b/test/naming_namespace_test.dart @@ -0,0 +1,129 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_namespace_test.dart +// Tests for constant naming via nameOfBest and shared instance/signal +// namespace uniquification. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.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(); + }); + + 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('shared instance and signal namespace', () { + test( + 'signal and instance with same name get uniquified ' + 'in the shared namespace', () async { + final dut = _InstanceSignalCollision(); + await dut.build(); + final sv = dut.generateSynth(); + + // With a single shared namespace, one of the two "inner" identifiers + // must be suffixed to avoid collision. + expect(sv, 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'))); + }); + }); +} diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart new file mode 100644 index 000000000..ffae4ae5f --- /dev/null +++ b/test/signal_registry_test.dart @@ -0,0 +1,315 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_registry_test.dart +// Tests for Module canonical naming (Namer). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.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('allocateName', () { + 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'); + }); + + 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'); + 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)); + }); + }); + + 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); + }); + }); +}