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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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 -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+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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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() { From d27c8670238b6ddfa22d27f1689bb1c50556c32b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 27 Jun 2026 01:06:25 -0700 Subject: [PATCH 43/77] fix: restore _BusSubsetForStructSlice._destination for stable instance naming --- .../utilities/synth_module_definition.dart | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 6e575f58f..905302d20 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -19,14 +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'); + _BusSubsetForStructSlice( + super.bus, + super.startIndex, + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super(name: 'struct_slice'); // we override this since it's added post-build @override bool get hasBuilt => true; + + @override + Object get instanceNameKey => _destination; } /// Represents the definition of a module. @@ -279,6 +295,7 @@ class SynthModuleDefinition { ), idx, idx + leafElement.width - 1, + destination: leafElement, ); final ssmi = getSynthSubModuleInstantiation(subsetMod); From da6c140e91b1eec0acf0be2ab4ba074d41bf9261 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 27 Jun 2026 01:19:40 -0700 Subject: [PATCH 44/77] test: struct-slice instance names stable across repeated synth passes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add regression test for _BusSubsetForStructSlice.instanceNameKey. Each SynthModuleDefinition pass creates fresh _BusSubsetForStructSlice instances for any submodule with a LogicStructure output port. Without the instanceNameKey override those instances use 'this' as the cache key, so the namer allocates a new suffix every pass ('struct_slice' → 'struct_slice_0'). Restoring _destination and overriding instanceNameKey => _destination pins the cache to the stable destination Logic, keeping names consistent. Fixes: _BusSubsetForStructSlice._destination removed in 249b2101. --- test/naming_consistency_test.dart | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index 150a9b751..e4e87d060 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -352,5 +352,95 @@ void main() { expect(baseName, equals('dup')); }, ); + + test( + 'struct-slice submodule names are stable across repeated definitions', + () async { + // Regression test for _BusSubsetForStructSlice.instanceNameKey: + // Each SynthModuleDefinition pass creates fresh _BusSubsetForStructSlice + // instances for any submodule with a LogicStructure output port. + // Without the _destination override the namer cache misses on those + // fresh instances and allocates new suffixes every pass, so the same + // struct field would be named "struct_slice" on pass 1 and + // "struct_slice_0" on pass 2. + final mod = _StructSliceParentMod(); + await mod.build(); + + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + // Only consider instantiations that are emitted (struct_slice and the + // child module itself both have needsInstantiation=true here). + final sliceNames1 = def1.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .where((n) => n.startsWith('struct_slice')) + .toList() + ..sort(); + final sliceNames2 = def2.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .where((n) => n.startsWith('struct_slice')) + .toList() + ..sort(); + + expect( + sliceNames1, + isNotEmpty, + reason: 'Expected at least one struct_slice instance to be ' + 'created for the _TwoFieldStruct output port.', + ); + expect( + sliceNames2, + sliceNames1, + reason: 'struct_slice instance names must not drift across repeated ' + 'synthesis passes — requires _BusSubsetForStructSlice to override ' + 'instanceNameKey with the stable destination Logic.', + ); + }, + ); }); } + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +class _TwoFieldStruct extends LogicStructure { + final Logic a; + final Logic b; + + factory _TwoFieldStruct({String name = 'st'}) => + _TwoFieldStruct._(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4), + name: name); + + _TwoFieldStruct._(this.a, this.b, {required super.name}) + : super([a, b]); + + @override + LogicStructure clone({String? name}) => + _TwoFieldStruct(name: name ?? this.name); +} + +/// A module with a [LogicStructure] output port. +/// +/// When this module is instantiated inside a parent, synthesis of the parent +/// calls [_subsetReceiveStructPort] for this module's struct output, creating +/// one [_BusSubsetForStructSlice] per leaf element. +class _StructOutputMod extends Module { + late final _TwoFieldStruct out; + + _StructOutputMod() : super(name: '_StructOutputMod') { + out = addTypedOutput('out', _TwoFieldStruct.new) + ..a.gets(Const(0, width: 4)) + ..b.gets(Const(0, width: 4)); + } +} + +/// Parent module whose synthesis pass creates [_BusSubsetForStructSlice] +/// instances for [_StructOutputMod]'s struct output port. +class _StructSliceParentMod extends Module { + _StructSliceParentMod() : super(name: '_StructSliceParentMod') { + final sub = _StructOutputMod(); + // Consume one leaf so the parent has a meaningful output. + addOutput('flat', width: 4) <= sub.out.a; + } +} From 513060132706d45dc77b2962969f675960dbf861 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 27 Jun 2026 07:47:30 -0700 Subject: [PATCH 45/77] chore: remove stale moduleOrder signal tests from naming_consistency_test --- test/naming_consistency_test.dart | 307 +++++++----------------------- 1 file changed, 65 insertions(+), 242 deletions(-) diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart index e4e87d060..ba5e161bf 100644 --- a/test/naming_consistency_test.dart +++ b/test/naming_consistency_test.dart @@ -64,37 +64,6 @@ 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. /// @@ -141,32 +110,20 @@ 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}'); } }); @@ -182,11 +139,8 @@ 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}'); } } }); @@ -203,11 +157,8 @@ 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}'); } } }); @@ -224,11 +175,8 @@ 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}'); } } }); @@ -246,201 +194,76 @@ void main() { for (final logic in names1.keys) { if (names2.containsKey(logic)) { - expect( - names2[logic], - names1[logic], - reason: 'Shared namer should produce same name for ' - '${logic.name}', - ); + expect(names2[logic], names1[logic], + reason: 'Shared namer should produce same name for ' + '${logic.name}'); } } }); - test('Namer.signalNameOfBest 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.namer.signalNameOfBest uses Namer directly + // Module.namer.signalNameOf uses Namer directly for (final port in [...mod.inputs.values, ...mod.outputs.values]) { - final moduleName = mod.namer.signalNameOfBest([port]); + final moduleName = mod.namer.signalNameOf(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.signalNameOf 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( - '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')); - }, - ); - - test( - 'struct-slice submodule names are stable across repeated definitions', - () async { - // Regression test for _BusSubsetForStructSlice.instanceNameKey: - // Each SynthModuleDefinition pass creates fresh _BusSubsetForStructSlice - // instances for any submodule with a LogicStructure output port. - // Without the _destination override the namer cache misses on those - // fresh instances and allocates new suffixes every pass, so the same - // struct field would be named "struct_slice" on pass 1 and - // "struct_slice_0" on pass 2. - final mod = _StructSliceParentMod(); - await mod.build(); - - final def1 = SynthModuleDefinition(mod); - final def2 = SynthModuleDefinition(mod); - - // Only consider instantiations that are emitted (struct_slice and the - // child module itself both have needsInstantiation=true here). - final sliceNames1 = def1.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .where((n) => n.startsWith('struct_slice')) - .toList() - ..sort(); - final sliceNames2 = def2.subModuleInstantiations - .where((s) => s.needsInstantiation) - .map((s) => s.name) - .where((n) => n.startsWith('struct_slice')) - .toList() - ..sort(); - - expect( - sliceNames1, - isNotEmpty, - reason: 'Expected at least one struct_slice instance to be ' - 'created for the _TwoFieldStruct output port.', - ); - expect( - sliceNames2, - sliceNames1, - reason: 'struct_slice instance names must not drift across repeated ' - 'synthesis passes — requires _BusSubsetForStructSlice to override ' - 'instanceNameKey with the stable destination Logic.', - ); - }, - ); - }); -} + 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(); -// ─── Fixtures ───────────────────────────────────────────────────────────────── + final def = SynthModuleDefinition(mod); -class _TwoFieldStruct extends LogicStructure { - final Logic a; - final Logic b; + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toSet(); - factory _TwoFieldStruct({String name = 'st'}) => - _TwoFieldStruct._(Logic(name: 'a', width: 4), Logic(name: 'b', width: 4), - name: name); + // The inner module instance should have a name + expect(instNames, isNotEmpty, + reason: 'Should have at least one submodule instance'); - _TwoFieldStruct._(this.a, this.b, {required super.name}) - : super([a, b]); + // 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'); + } + }); - @override - LogicStructure clone({String? name}) => - _TwoFieldStruct(name: name ?? this.name); -} + test('submodule instance names are stable across repeated definitions', + () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); -/// A module with a [LogicStructure] output port. -/// -/// When this module is instantiated inside a parent, synthesis of the parent -/// calls [_subsetReceiveStructPort] for this module's struct output, creating -/// one [_BusSubsetForStructSlice] per leaf element. -class _StructOutputMod extends Module { - late final _TwoFieldStruct out; - - _StructOutputMod() : super(name: '_StructOutputMod') { - out = addTypedOutput('out', _TwoFieldStruct.new) - ..a.gets(Const(0, width: 4)) - ..b.gets(Const(0, width: 4)); - } -} + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); -/// Parent module whose synthesis pass creates [_BusSubsetForStructSlice] -/// instances for [_StructOutputMod]'s struct output port. -class _StructSliceParentMod extends Module { - _StructSliceParentMod() : super(name: '_StructSliceParentMod') { - final sub = _StructOutputMod(); - // Consume one leaf so the parent has a meaningful output. - addOutput('flat', width: 4) <= sub.out.a; - } + 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 7fc952d2057d6216f5bef218f76ecedeff56291e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 27 Jun 2026 12:48:41 -0700 Subject: [PATCH 46/77] feat(Namer): expose public signalNameOf for wave-dumper and tests --- lib/src/utilities/namer.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 8753d489b..4fd08b814 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -126,6 +126,12 @@ class Namer { return name; } + /// Returns the synthesis-level signal name for [logic]. + /// + /// Equivalent to the internal [_signalNameOf] allocation but exposed for + /// use in wave-dumping and tests. + String signalNameOf(Logic logic) => _signalNameOf(logic); + /// The base name that would be used for [logic] before uniquification. static String baseName(Logic logic) => (logic.naming == Naming.reserved || logic.isArrayMember) From 526e05c8d943f73ec82a38786f2abe451f15b676 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 29 Jun 2026 12:41:52 -0700 Subject: [PATCH 47/77] Add Namer.sourceLogicOf reverse name-source map Records which Logic the namer chose as the source of each signal name (an additive reverse map; does not influence naming). Lets source-trace and cross-probe callers attribute a merged net to its declared signal rather than an arbitrary internal signal that merged into the same net. --- lib/src/utilities/namer.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 4fd08b814..ac66d6ea4 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -31,6 +31,16 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; + /// Reverse map: final signal name -> the [Logic] the namer selected as the + /// *source* of that name (the `chosen` candidate in [signalNameOfBest]). + /// + /// This records a decision the namer already makes; it does not influence + /// naming. It lets callers recover *which* merged [Logic] gave a net its + /// name, so source-trace / cross-probe data can be attributed to the + /// declared signal rather than to an arbitrary internal signal that merged + /// into the same net. + final Map _nameSources = {}; + /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. /// /// Instance-name lookup claims names in [_uniquifier]. Without this cache, @@ -132,6 +142,11 @@ class Namer { /// use in wave-dumping and tests. String signalNameOf(Logic logic) => _signalNameOf(logic); + /// Returns the [Logic] the namer chose as the source of signal [name], or + /// `null` if [name] has no single source [Logic] (e.g. a constant net) or + /// has not been named yet. + Logic? sourceLogicOf(String name) => _nameSources[name]; + /// The base name that would be used for [logic] before uniquification. static String baseName(Logic logic) => (logic.naming == Naming.reserved || logic.isArrayMember) @@ -240,6 +255,7 @@ class Namer { /// same name for all other non-port [Logic]s in [all]. String _nameAndCacheAll(Logic chosen, Iterable all) { final name = _signalNameOf(chosen); + _nameSources[name] = chosen; for (final logic in all) { if (!identical(logic, chosen) && !_portLogics.contains(logic)) { _signalNames[logic] = name; From ea3c759a40620e6287fdb6d90e2bcfb197c8f219 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 1 Jul 2026 05:42:24 -0700 Subject: [PATCH 48/77] cleanup copyright / signalNameOf export --- .../systemverilog_synth_module_definition.dart | 6 +++--- lib/src/utilities/namer.dart | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 43aa38320..f152e38f9 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synth_module_definition.dart @@ -18,7 +18,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { @override void process() { - _buildNetConnectsForNaming(); + _replaceNetConnections(); _collapseMarkedChainableModules(); _replaceInOutConnectionInlineableModules(); } @@ -56,7 +56,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } /// Builds [_NetConnect] instances for [LogicNet] assignments. - void _buildNetConnectsForNaming() { + void _replaceNetConnections() { final reducedAssignments = []; for (final assignment in assignments) { diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index ac66d6ea4..c0851f357 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -140,6 +140,7 @@ class Namer { /// /// Equivalent to the internal [_signalNameOf] allocation but exposed for /// use in wave-dumping and tests. + @visibleForTesting String signalNameOf(Logic logic) => _signalNameOf(logic); /// Returns the [Logic] the namer chose as the source of signal [name], or From 32055a95c7476ce57491ab5b1514da2d880180aa Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 1 Jul 2026 09:30:38 -0700 Subject: [PATCH 49/77] copyright and make method private --- lib/src/synthesizers/utilities/synth_module_definition.dart | 5 ++--- tool/gh_codespaces/install_dart.sh | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 905302d20..29d1757eb 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -545,7 +545,7 @@ class SynthModuleDefinition { // 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(); + _prepareForNaming(); _pickNames(); process(); } @@ -553,8 +553,7 @@ class SynthModuleDefinition { /// Performs base-owned preparation before names are picked. /// /// Synthesizers must not override this method. - @nonVirtual - void prepareForNaming() { + void _prepareForNaming() { _markPotentiallyCollapsedObjectsForNaming(); } diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index d0bfdfe91..f9da69f77 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # install_dart.sh From 666d21128a51a376ba04396e802806cbb236a034 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 1 Jul 2026 09:42:54 -0700 Subject: [PATCH 50/77] remove _nameSources backward map, relocate to FLC production --- lib/src/utilities/namer.dart | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index c0851f357..dc585612d 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -31,16 +31,6 @@ class Namer { /// Port names are returned directly from [_portLogics] and never cached here. final Map _signalNames = {}; - /// Reverse map: final signal name -> the [Logic] the namer selected as the - /// *source* of that name (the `chosen` candidate in [signalNameOfBest]). - /// - /// This records a decision the namer already makes; it does not influence - /// naming. It lets callers recover *which* merged [Logic] gave a net its - /// name, so source-trace / cross-probe data can be attributed to the - /// declared signal rather than to an arbitrary internal signal that merged - /// into the same net. - final Map _nameSources = {}; - /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. /// /// Instance-name lookup claims names in [_uniquifier]. Without this cache, @@ -143,11 +133,6 @@ class Namer { @visibleForTesting String signalNameOf(Logic logic) => _signalNameOf(logic); - /// Returns the [Logic] the namer chose as the source of signal [name], or - /// `null` if [name] has no single source [Logic] (e.g. a constant net) or - /// has not been named yet. - Logic? sourceLogicOf(String name) => _nameSources[name]; - /// The base name that would be used for [logic] before uniquification. static String baseName(Logic logic) => (logic.naming == Naming.reserved || logic.isArrayMember) @@ -256,7 +241,6 @@ class Namer { /// same name for all other non-port [Logic]s in [all]. String _nameAndCacheAll(Logic chosen, Iterable all) { final name = _signalNameOf(chosen); - _nameSources[name] = chosen; for (final logic in all) { if (!identical(logic, chosen) && !_portLogics.contains(logic)) { _signalNames[logic] = name; From f8979b2bd717d8f66a9b9863b2cc8d29bc321998 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 2 Jul 2026 14:47:45 -0700 Subject: [PATCH 51/77] Split netlist compute layer --- example/filter_bank.dart | 118 + lib/src/examples/filter_bank_modules.dart | 937 ++++++++ lib/src/module.dart | 401 ++-- .../netlist/leaf_cell_mapper.dart | 482 ++++ lib/src/synthesizers/netlist/netlist.dart | 15 + .../synthesizers/netlist/netlist_options.dart | 100 + .../synthesizers/netlist/netlist_passes.dart | 356 +++ .../netlist/netlist_synthesis_result.dart | 85 + .../netlist/netlist_synthesizer.dart | 2110 +++++++++++++++++ .../synthesizers/netlist/netlist_utils.dart | 470 ++++ lib/src/synthesizers/synthesizers.dart | 3 +- test/netlist_example_test.dart | 292 +++ test/netlist_synthesizer_test.dart | 1337 +++++++++++ test/netlist_test.dart | 759 ++++++ test/struct_port_pruning_test.dart | 143 ++ test/synth_name_parity_test.dart | 364 +++ 16 files changed, 7824 insertions(+), 148 deletions(-) create mode 100644 example/filter_bank.dart create mode 100644 lib/src/examples/filter_bank_modules.dart create mode 100644 lib/src/synthesizers/netlist/leaf_cell_mapper.dart create mode 100644 lib/src/synthesizers/netlist/netlist.dart create mode 100644 lib/src/synthesizers/netlist/netlist_options.dart create mode 100644 lib/src/synthesizers/netlist/netlist_passes.dart create mode 100644 lib/src/synthesizers/netlist/netlist_synthesis_result.dart create mode 100644 lib/src/synthesizers/netlist/netlist_synthesizer.dart create mode 100644 lib/src/synthesizers/netlist/netlist_utils.dart create mode 100644 test/netlist_example_test.dart create mode 100644 test/netlist_synthesizer_test.dart create mode 100644 test/netlist_test.dart create mode 100644 test/struct_port_pruning_test.dart create mode 100644 test/synth_name_parity_test.dart diff --git a/example/filter_bank.dart b/example/filter_bank.dart new file mode 100644 index 000000000..39dd3fc5b --- /dev/null +++ b/example/filter_bank.dart @@ -0,0 +1,118 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank.dart +// A polyphase FIR filter bank design example exercising: +// - Deep hierarchy with shared sub-module definitions +// - Interface (FilterDataInterface) +// - LogicStructure (FilterSample) +// - LogicArray (coefficient storage) +// - Pipeline (pipelined MAC accumulation) +// - FiniteStateMachine (FilterController) +// +// The filter bank has two channels that share an identical MacUnit definition. +// A controller FSM sequences: idle → loading → running → draining → done. +// +// 2026 March 26 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; + +// Import module definitions. +import 'package:rohd/src/examples/filter_bank_modules.dart'; + +// Re-export so downstream consumers (e.g. devtools loopback) can use. +export 'package:rohd/src/examples/filter_bank_modules.dart'; + +// ────────────────────────────────────────────────────────────────── +// Standalone simulation entry point +// ────────────────────────────────────────────────────────────────── + +Future main({bool noPrint = false}) async { + const dataWidth = 16; + const numTaps = 3; + + // Low-pass-ish coefficients (scaled integers) + const coeffs0 = [1, 2, 1]; // channel 0: symmetric LPF kernel + const coeffs1 = [1, -2, 1]; // channel 1: high-pass kernel + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [coeffs0, coeffs1], + ); + + // Before we can simulate or generate code, we need to build it. + await dut.build(); + + // Set a maximum time for the simulation so it doesn't keep running forever. + Simulator.setMaxSimTime(500); + + // Attach a waveform dumper so we can see what happens. + if (!noPrint) { + WaveDumper(dut, outputPath: 'filter_bank.vcd'); + } + + // Kick off the simulation. + unawaited(Simulator.run()); + + // ── Reset ── + reset.inject(1); + start.inject(0); + samples[0].data.inject(0); + samples[0].valid.inject(0); + samples[1].data.inject(0); + samples[1].valid.inject(0); + inputDone.inject(0); + + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + + // ── Start filtering ── + await clk.nextPosedge; + start.inject(1); + await clk.nextPosedge; + start.inject(0); + samples[0].valid.inject(1); + samples[1].valid.inject(1); + + // ── Feed sample stream: impulse response test ── + // Send a single '1' followed by zeros to get the impulse response + samples[0].data.inject(1); + samples[1].data.inject(1); + await clk.nextPosedge; + + for (var i = 0; i < 8; i++) { + samples[0].data.inject(0); + samples[1].data.inject(0); + await clk.nextPosedge; + } + + // ── Signal end of input ── + samples[0].valid.inject(0); + samples[1].valid.inject(0); + inputDone.inject(1); + await clk.nextPosedge; + inputDone.inject(0); + + // ── Wait for drain ── + for (var i = 0; i < 15; i++) { + await clk.nextPosedge; + } + + await Simulator.endSimulation(); +} diff --git a/lib/src/examples/filter_bank_modules.dart b/lib/src/examples/filter_bank_modules.dart new file mode 100644 index 000000000..10ec1d329 --- /dev/null +++ b/lib/src/examples/filter_bank_modules.dart @@ -0,0 +1,937 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_modules.dart +// Module class definitions for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick +// +// Architecture: each FilterChannel uses a single MacUnit that is +// time-multiplexed across taps. A tap counter sequences CoeffBank +// and a delay-line mux so the MAC accumulates one tap per clock cycle. +// After numTaps cycles the accumulated result is latched as the output +// sample and the accumulator resets for the next input sample. +// +// ROHD features exercised: +// - LogicStructure (FilterSample) +// - Interface (FilterDataInterface) +// - LogicArray (CoeffBank coefficient ROM, delay line) +// - Pipeline (MacUnit multiply-accumulate) +// - FiniteStateMachine (FilterController) +// - Multiple instantiation (two FilterChannels share one definition) +// +// Separated from filter_bank.dart so these classes can be imported +// in web-targeted code (no dart:io dependency). +// +// 2026 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +// ────────────────────────────────────────────────────────────────── +// LogicStructure: a typed sample word carrying data + valid + channel +// ────────────────────────────────────────────────────────────────── + +/// A structured signal bundling a data sample with metadata. +/// +/// Packs three fields — [data], and [valid] — into a single +/// bus that can be driven and sampled as a unit. Used throughout the +/// [FilterBank] to carry tagged samples between modules. +class FilterSample extends LogicStructure { + /// The sample data word. + late final Logic data; + + /// Whether this sample is valid. + late final Logic valid; + + /// Creates a [FilterSample] with the given [dataWidth] (default 16) + /// and optional [name]. + FilterSample({int dataWidth = 16, String? name}) + : super( + [ + Logic(name: 'data', width: dataWidth), + Logic(name: 'valid'), + ], + name: name ?? 'filter_sample', + ) { + data = elements[0]; + valid = elements[1]; + } + + // Private constructor for clone to share element structure. + FilterSample._clone(super.elements, {required super.name}) { + data = elements[0]; + valid = elements[1]; + } + + @override + + /// Returns a structural clone of this sample, preserving element names. + FilterSample clone({String? name}) => FilterSample._clone( + elements.map((e) => e.clone(name: e.name)), + name: name ?? this.name, + ); +} + +// ────────────────────────────────────────────────────────────────── +// Interface: tagged port bundle for filter data I/O +// ────────────────────────────────────────────────────────────────── + +/// Tags for grouping port directions in [FilterDataInterface]. +enum FilterPortTag { + /// Ports carrying data into the filter (`sampleIn`, `validIn`). + inputPorts, + + /// Ports carrying data out of the filter (`dataOut`, `validOut`). + outputPorts, +} + +/// An interface carrying sample data and control into/out of filter modules. +/// +/// Groups ports by [FilterPortTag] so that [connectIO] can wire +/// inputs and outputs in a single call. +class FilterDataInterface extends Interface { + /// Input sample data bus. + Logic get sampleIn => port('sampleIn'); + + /// Input valid strobe. + Logic get validIn => port('validIn'); + + /// Output filtered data bus. + Logic get dataOut => port('dataOut'); + + /// Output valid strobe. + Logic get validOut => port('validOut'); + + /// The data width used by this interface. + final int _dataWidth; + + /// Creates a [FilterDataInterface] with the given [dataWidth] + /// (default 16 bits). + FilterDataInterface({int dataWidth = 16}) : _dataWidth = dataWidth { + setPorts([ + Logic.port('sampleIn', dataWidth), + Logic.port('validIn'), + ], [ + FilterPortTag.inputPorts + ]); + + setPorts([ + Logic.port('dataOut', dataWidth), + Logic.port('validOut'), + ], [ + FilterPortTag.outputPorts + ]); + } + + @override + + /// Returns a new interface with the same data width. + FilterDataInterface clone() => FilterDataInterface(dataWidth: _dataWidth); +} + +// ────────────────────────────────────────────────────────────────── +// CoeffBank: stores FIR tap coefficients in a LogicArray +// ────────────────────────────────────────────────────────────────── + +/// A coefficient storage module backed by a [LogicArray] input port. +/// +/// Accepts a [LogicArray] of per-tap coefficients via [addInputArray] +/// and a tap index, then mux-selects the corresponding coefficient. +class CoeffBank extends Module { + /// The coefficient value at the selected index. + Logic get coeffOut => output('coeffOut'); + + /// The per-tap coefficient array (registered input port). + @protected + LogicArray get coeffArray => input('coeffArray') as LogicArray; + + /// The tap index input. + @protected + Logic get tapIndex => input('tapIndex'); + + /// Number of taps. + final int numTaps; + + /// Data width. + final int dataWidth; + + /// Creates a [CoeffBank] with [numTaps] taps at [dataWidth] bits. + /// + /// [coefficients] is a [LogicArray] with one element per tap — + /// registered as an input port via [addInputArray]. + /// [tapIndex] selects the active coefficient. + CoeffBank(Logic tapIndex, LogicArray coefficients, + {required this.numTaps, + required this.dataWidth, + super.name = 'CoeffBank'}) + : super(definitionName: 'CoeffBank_T${numTaps}_W$dataWidth') { + // Register ports + tapIndex = addInput('tapIndex', tapIndex, width: tapIndex.width); + final coeffArray = addInputArray('coeffArray', coefficients, + dimensions: [numTaps], elementWidth: dataWidth); + final coeffOut = addOutput('coeffOut', width: dataWidth); + + // Mux-chain ROM: priority-select coefficient by tap index. + Logic selected = Const(0, width: dataWidth); + for (var i = numTaps - 1; i >= 0; i--) { + selected = mux( + tapIndex.eq(Const(i, width: tapIndex.width)).named('tapMatch$i'), + coeffArray.elements[i], + selected, + ); + } + coeffOut <= selected; + } +} + +// ────────────────────────────────────────────────────────────────── +// MacUnit: a single multiply-accumulate pipeline stage +// ────────────────────────────────────────────────────────────────── + +/// A pipelined multiply-accumulate unit. +/// +/// Pipeline stage 0: multiply sample × coefficient +/// Pipeline stage 1: add product to running accumulator +class MacUnit extends Module { + /// Accumulated result. + Logic get result => output('result'); + + /// Sample data input. + @protected + Logic get sampleInPin => input('sampleIn'); + + /// Coefficient input. + @protected + Logic get coeffInPin => input('coeffIn'); + + /// Accumulator input. + @protected + Logic get accumInPin => input('accumIn'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Data width. + final int dataWidth; + + /// Creates a [MacUnit] that multiplies [sampleIn] by [coeffIn] in + /// stage 0 and adds the product to [accumIn] in stage 1. + /// + /// [clk], [reset], and [enable] control the pipeline registers. + MacUnit(Logic sampleIn, Logic coeffIn, Logic accumIn, Logic clk, Logic reset, + Logic enable, + {required this.dataWidth, super.name = 'MacUnit'}) + : super(definitionName: 'MacUnit_W$dataWidth') { + sampleIn = addInput('sampleIn', sampleIn, width: dataWidth); + coeffIn = addInput('coeffIn', coeffIn, width: dataWidth); + accumIn = addInput('accumIn', accumIn, width: dataWidth); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + final result = addOutput('result', width: dataWidth); + + // A 2-stage pipeline: multiply, then accumulate + final pipe = Pipeline( + clk, + reset: reset, + stages: [ + // Stage 0: multiply + (p) => [ + // Product = sample * coefficient (truncated to dataWidth) + p.get(sampleIn) < + (p.get(sampleIn) * p.get(coeffIn)).named('product'), + ], + // Stage 1: accumulate + (p) => [ + p.get(sampleIn) < + (p.get(sampleIn) + p.get(accumIn)).named('macSum'), + ], + ], + signals: [sampleIn, coeffIn, accumIn], + ); + + result <= pipe.get(sampleIn); + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterChannel: one polyphase FIR channel with time-multiplexed MAC +// ────────────────────────────────────────────────────────────────── + +/// A single polyphase FIR filter channel with [numTaps] taps. +/// +/// Uses a [FilterDataInterface] for its sample I/O ports. +/// +/// Architecture: +/// - A delay line (shift register) captures incoming samples. +/// - A tap counter cycles 0 … numTaps-1 each sample period. +/// - [CoeffBank] provides the coefficient for the current tap. +/// - A mux selects the delay-line sample for the current tap. +/// - A single [MacUnit] multiplies the selected sample by the +/// coefficient and adds it to a running accumulator. +/// - After all taps are processed the accumulator is latched as +/// the output and the accumulator resets for the next sample. +class FilterChannel extends Module { + /// The data interface for this channel (internal use only). + @protected + late final FilterDataInterface intf; + + /// Filtered output. + Logic get dataOut => intf.dataOut; + + /// Output valid. + Logic get validOut => intf.validOut; + + /// Number of FIR taps in this channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Creates a [FilterChannel] with [numTaps] taps at [dataWidth] bits. + /// + /// [srcIntf] provides the sample/valid input ports. [coefficients] + /// supplies per-tap constant coefficients. + FilterChannel( + FilterDataInterface srcIntf, + Logic clk, + Logic reset, + Logic enable, { + required this.numTaps, + required this.dataWidth, + required List coefficients, + super.name = 'FilterChannel', + }) : super(definitionName: 'FilterChannel_T${numTaps}_W$dataWidth') { + // Connect the Interface — creates module input/output ports + intf = FilterDataInterface(dataWidth: dataWidth) + ..connectIO(this, srcIntf, + inputTags: [FilterPortTag.inputPorts], + outputTags: [FilterPortTag.outputPorts]); + + final sampleIn = intf.sampleIn; + final validIn = intf.validIn; + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + + final tapIdxWidth = _bitsFor(numTaps); + + // ── Delay line (shift register via explicit flop bank + gates) ── + // AND gate: shift enable = enable & validIn & tapCounter==0 + // Samples shift in only when starting a new accumulation cycle. + final tapCounter = Logic(width: tapIdxWidth, name: 'tapCounter'); + final atFirstTap = + tapCounter.eq(Const(0, width: tapIdxWidth)).named('atFirstTap'); + final shiftEn = Logic(name: 'shiftEn'); + shiftEn <= (enable & validIn).named('enableAndValid') & atFirstTap; + + // LogicArray-backed delay line: one element per tap register. + final delayLine = LogicArray([numTaps], dataWidth, name: 'delayLine'); + for (var i = 0; i < numTaps; i++) { + final tapInput = (i == 0) ? sampleIn : delayLine.elements[i - 1]; + // Mux: hold current value or shift in new sample + final tapNext = Logic(width: dataWidth, name: 'nextTap$i'); + tapNext <= mux(shiftEn, tapInput, delayLine.elements[i]); + // Flop: register the next-state value + delayLine.elements[i] <= flop(clk, reset: reset, tapNext); + } + + // ── Coefficient bank — driven by tapCounter ── + // Build a LogicArray of constants from the coefficient list and + // pass it as an input port to CoeffBank (demonstrates addInputArray + // on a sub-module). + final coeffArray = LogicArray([numTaps], dataWidth, name: 'coeffArray'); + for (var i = 0; i < numTaps; i++) { + coeffArray.elements[i] <= Const(coefficients[i], width: dataWidth); + } + + final coeffBank = CoeffBank( + tapCounter, + coeffArray, + numTaps: numTaps, + dataWidth: dataWidth, + name: 'coeffBank', + ); + + // ── Delay-line mux — select sample for current tap ── + var selectedSample = delayLine.elements[0]; + for (var i = 1; i < numTaps; i++) { + final tapSelect = + tapCounter.eq(Const(i, width: tapIdxWidth)).named('tapSelect$i'); + selectedSample = mux(tapSelect, delayLine.elements[i], selectedSample) + .named('tapMux$i'); + } + + // ── Running accumulator (feedback register) ── + final accumReg = Logic(width: dataWidth, name: 'accumReg'); + // Reset accumulator at the start of each new sample (tap 0). + // Combinational block: equivalent to `always_comb` in SystemVerilog. + final accumFeedback = Logic(width: dataWidth, name: 'accumFeedback'); + Combinational([ + If(atFirstTap, then: [ + accumFeedback < Const(0, width: dataWidth), + ], orElse: [ + accumFeedback < accumReg, + ]), + ]); + + // ── Single MAC unit — time-multiplexed across taps ── + final mac = MacUnit( + selectedSample, + coeffBank.coeffOut, + accumFeedback, + clk, + reset, + enable, + dataWidth: dataWidth, + name: 'mac', + ); + + // Register the MAC result for accumulator feedback. + accumReg <= flop(clk, reset: reset, mac.result); + + // ── Tap counter: cycles 0 … numTaps-1 while enabled ── + // Sequential block: equivalent to `always_ff @(posedge clk)` in SV. + // When enabled, the counter increments and wraps at numTaps-1. + // When disabled, it resets to 0. + final lastTap = + tapCounter.eq(Const(numTaps - 1, width: tapIdxWidth)).named('lastTap'); + Sequential(clk, reset: reset, [ + If(enable, then: [ + If(lastTap, then: [ + tapCounter < Const(0, width: tapIdxWidth), + ], orElse: [ + tapCounter < tapCounter + Const(1, width: tapIdxWidth), + ]), + ], orElse: [ + tapCounter < Const(0, width: tapIdxWidth), + ]), + ]); + + // ── Output latch: capture accumulator when all taps processed ── + // The MAC pipeline has 2 stages, so the result is ready 2 cycles + // after the last tap enters. A 2-stage shift register of lastTap + // creates the latch strobe. + final lastTapD1 = Logic(name: 'lastTapD1'); + final lastTapD2 = Logic(name: 'lastTapD2'); + final outputReg = Logic(width: dataWidth, name: 'outputReg'); + + // Sequential block with If: latch strobe delay and output register. + Sequential(clk, reset: reset, [ + lastTapD1 < lastTap, + lastTapD2 < lastTapD1, + If(lastTapD2, then: [ + outputReg < accumReg, + ]), + ]); + + // ── Valid pipeline: track whether we have a valid output ── + // validIn is high during data injection. After the MAC pipeline + // latency (numTaps + 2 cycles), outputs become valid. + final validPipe = Logic(name: 'validPipe'); + final outputReady = (lastTapD2 & enable).named('outputReady'); + + // Sequential block: register the valid strobe and hold it. + Sequential(clk, reset: reset, [ + If(enable, then: [ + validPipe < outputReady, + ]), + ]); + + // Combinational block: gate the output to zero when not valid. + final dataOut = intf.dataOut; + final validOut = intf.validOut; + Combinational([ + If(validPipe, then: [ + dataOut < outputReg, + ], orElse: [ + dataOut < Const(0, width: dataWidth), + ]), + validOut < validPipe, + ]); + } + + /// Minimum bits needed to represent [n] values. + static int _bitsFor(int n) { + if (n <= 1) { + return 1; + } + var bits = 0; + var v = n - 1; + while (v > 0) { + bits++; + v >>= 1; + } + return bits; + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterController: FSM sequencing the filter bank +// ────────────────────────────────────────────────────────────────── + +/// States for the [FilterController] finite state machine. +enum FilterState { + /// Waiting for the start signal. + idle, + + /// Accepting initial samples into the delay line. + loading, + + /// Normal filtering operation. + running, + + /// Flushing the pipeline after the input stream ends. + draining, + + /// Processing complete. + done, +} + +/// Controls the filter bank operation via a [FiniteStateMachine]. +/// +/// - idle: waiting for start signal +/// - loading: accepting initial samples into delay line +/// - running: normal filtering +/// - draining: flushing pipeline after input stream ends +/// - done: processing complete +class FilterController extends Module { + /// Encoded FSM state (3 bits). + Logic get state => output('state'); + + /// High while the filter channels should be processing. + Logic get filterEnable => output('filterEnable'); + + /// High during the initial sample-loading phase. + Logic get loadingPhase => output('loadingPhase'); + + /// Asserted when the filter bank has finished processing. + Logic get doneFlag => output('doneFlag'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input valid. + @protected + Logic get inputValidPin => input('inputValid'); + + /// Input done. + @protected + Logic get inputDonePin => input('inputDone'); + + late final FiniteStateMachine _fsm; + + /// Returns the FSM's current state index for a given [FilterState]. + int? getStateIndex(FilterState s) => _fsm.getStateIndex(s); + + /// Creates a [FilterController] that sequences the filter bank. + /// + /// After [start] is asserted the FSM moves through loading → running + /// → draining (for [drainCycles] cycles) → done. + FilterController( + Logic clk, Logic reset, Logic start, Logic inputValid, Logic inputDone, + {required int drainCycles, super.name = 'FilterController'}) + : super(definitionName: 'FilterController') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputValid = addInput('inputValid', inputValid); + inputDone = addInput('inputDone', inputDone); + + final filterEnable = addOutput('filterEnable'); + final loadingPhase = addOutput('loadingPhase'); + final doneFlag = addOutput('doneFlag'); + final state = addOutput('state', width: 3); + + // Drain counter + final drainCount = Logic(width: 8, name: 'drainCount'); + final drainDone = + drainCount.eq(Const(drainCycles, width: 8)).named('drainDone'); + + _fsm = FiniteStateMachine( + clk, + reset, + FilterState.idle, + [ + State( + FilterState.idle, + events: { + start: FilterState.loading, + }, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.loading, + events: { + inputValid: FilterState.running, + }, + actions: [ + filterEnable < 1, + loadingPhase < 1, + doneFlag < 0, + ], + ), + State( + FilterState.running, + events: { + inputDone: FilterState.draining, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.draining, + events: { + drainDone: FilterState.done, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.done, + events: {}, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 1, + ], + ), + ], + ); + + state <= _fsm.currentState.zeroExtend(state.width); + + // Drain counter: Sequential block increments while draining, + // resets to zero otherwise. + final drainIdx = _fsm.getStateIndex(FilterState.draining)!; + final isDraining = Logic(name: 'isDraining'); + isDraining <= _fsm.currentState.eq(Const(drainIdx, width: _fsm.stateWidth)); + + Sequential(clk, reset: reset, [ + If(isDraining, then: [ + drainCount < drainCount + Const(1, width: 8), + ], orElse: [ + drainCount < Const(0, width: 8), + ]), + ]); + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterBank: top-level 2-channel polyphase FIR filter +// ────────────────────────────────────────────────────────────────── + +/// A 2-channel polyphase FIR filter bank. +/// +/// Hierarchy: +/// ```text +/// FilterBank (top) +/// ├── FilterController (FSM) +/// ├── FilterChannel 'ch0' +/// │ ├── CoeffBank (coefficient ROM via LogicArray + mux chain) +/// │ └── MacUnit 'mac' (pipelined multiply-accumulate) +/// └── FilterChannel 'ch1' +/// ├── CoeffBank +/// └── MacUnit 'mac' +/// ``` +/// +/// Each channel time-multiplexes a single MacUnit across all taps, +/// sequenced by a tap counter that drives the CoeffBank tap index +/// and a delay-line sample mux. +/// +/// Uses: +/// - [FilterDataInterface] for I/O port bundles +/// - [FilterSample] LogicStructure for structured sample signals +/// - [LogicArray] in CoeffBank for coefficient storage +/// - [Pipeline] in MacUnit for pipelined MAC +/// - [FiniteStateMachine] in FilterController for sequencing +/// - Multiple instantiation: two [FilterChannel]s share one definition +/// - [LogicNet] / [addInOut] for bidirectional shared data bus + +// ────────────────────────────────────────────────────────────────── +// SharedDataBus: bidirectional port for coefficient/status I/O +// ────────────────────────────────────────────────────────────────── + +/// A module with a bidirectional data bus for loading/reading data. +/// +/// In real hardware, a shared data bus is common for: +/// - Loading filter coefficients from external memory +/// - Reading diagnostic status or filter output snapshots +/// +/// Direction is controlled by `writeEnable`: when high, the module's +/// internal [TriStateBuffer] drives `storedValue` onto `dataBus`; +/// when low, the external driver owns the bus and the module latches +/// the incoming value into a register. +/// +/// Exercises `addInOut` / `LogicNet` / [TriStateBuffer] / inout port +/// direction through the full ROHD stack: synthesis, hierarchy, +/// waveform capture, and DevTools rendering. +class SharedDataBus extends Module { + /// The bidirectional data bus port. + Logic get dataBus => inOut('dataBus'); + + /// The stored value (latched when the bus is driven externally). + Logic get storedValue => output('storedValue'); + + /// Write-enable input. + @protected + Logic get writeEnablePin => input('writeEnable'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Data width in bits. + final int dataWidth; + + /// Creates a [SharedDataBus] with a [dataWidth]-bit bidirectional port. + /// + /// [dataBusNet] is the external [LogicNet] to connect. + /// [writeEnable] controls bus direction: 1 = module drives bus, + /// 0 = external drives bus (module reads). + /// [clk] and [reset] provide synchronous storage. + SharedDataBus( + LogicNet dataBusNet, + Logic writeEnable, + Logic clk, + Logic reset, { + required this.dataWidth, + super.name = 'SharedDataBus', + }) : super(definitionName: 'SharedDataBus') { + final bus = addInOut('dataBus', dataBusNet, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + final storedValue = addOutput('storedValue', width: dataWidth); + + // Latch the bus value on clock edge when the external side is driving. + storedValue <= + flop( + clk, + bus, + reset: reset, + en: ~writeEnable, + resetValue: Const(0, width: dataWidth), + ); + + // Drive the latched value back onto the bus when writeEnable is high. + // TriStateBuffer drives its out (a LogicNet) with storedValue when + // enabled; otherwise it outputs high-Z. Joining out↔bus makes the + // two nets share the same wire. + TriStateBuffer(storedValue, enable: writeEnable, name: 'busDriver') + .out + .gets(bus); + } +} + +/// The top-level polyphase FIR filter bank. +class FilterBank extends Module { + /// Per-channel filtered outputs as a [LogicArray]. + /// + /// `channelOut.elements[i]` is the filtered output of channel `i`. + LogicArray get channelOut => output('channelOut') as LogicArray; + + /// Channel 0 filtered output (convenience getter). + Logic get out0 => channelOut.elements[0]; + + /// Channel 1 filtered output (convenience getter). + Logic get out1 => channelOut.elements[1]; + + /// Output valid (aligned with filtered outputs). + Logic get validOut => output('validOut'); + + /// Done signal from the controller FSM. + Logic get done => output('done'); + + /// Controller state (for debug visibility). + Logic get state => output('state'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input [FilterSample] port for channel [ch]. + @protected + FilterSample samplePin(int ch) => input('sample$ch') as FilterSample; + + /// Input-done strobe. + @protected + Logic get inputDonePin => input('inputDone'); + + /// Number of FIR taps per channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Number of filter channels. + final int numChannels; + + /// Creates a [FilterBank] with [numChannels] channels (default 2). + /// + /// Each channel has [numTaps] FIR taps at [dataWidth] bits. + /// [coefficients] is a list of per-channel coefficient lists — + /// `coefficients[i]` supplies the tap weights for channel `i`. + /// [samples] is a [LogicArray] with one element per channel. + /// [inputDone] when the input stream is complete. + /// + /// Optionally pass [dataBus] (a `LogicNet`) and [writeEnable] to + /// attach a bidirectional shared data bus via [SharedDataBus]. + /// The bus latches external data when [writeEnable] is low and + /// drives `storedValue` output. + FilterBank( + Logic clk, + Logic reset, + Logic start, + List samples, + Logic inputDone, { + required this.numTaps, + required this.dataWidth, + required List> coefficients, + this.numChannels = 2, + LogicNet? dataBus, + Logic? writeEnable, + super.name = 'FilterBank', + String? definitionName, + }) : super(definitionName: definitionName ?? 'FilterBank') { + if (coefficients.length != numChannels) { + throw Exception( + 'coefficients must have $numChannels entries (one per channel).'); + } + + // ── Register ports ── + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputDone = addInput('inputDone', inputDone); + + // One typed FilterSample input port per channel. + final inPorts = []; + for (var ch = 0; ch < numChannels; ch++) { + inPorts.add(addTypedInput('sample$ch', samples[ch])); + } + + final channelOut = addTypedOutput( + 'channelOut', + ({name = 'channelOut'}) => + LogicArray([numChannels], dataWidth, name: name)); + final validOut = addOutput('validOut'); + final done = addOutput('done'); + final state = addOutput('state', width: 3); + + // ── Controller FSM ── + // Drain cycles: numTaps cycles per accumulation + pipeline depth (2) + 1 + final controller = FilterController( + clk, + reset, + start, + inPorts[0].valid, // valid is shared across channels + inputDone, + drainCycles: numTaps + 3, + name: 'controller', + ); + + final filterEnable = controller.filterEnable; + + // ── Per-channel filter instantiation ── + final srcIntfs = []; + for (var ch = 0; ch < numChannels; ch++) { + final srcIntf = FilterDataInterface(dataWidth: dataWidth); + srcIntf.sampleIn <= inPorts[ch].data; + srcIntf.validIn <= inPorts[ch].valid; + + FilterChannel( + srcIntf, + clk, + reset, + filterEnable, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients[ch], + name: 'ch$ch', + ); + + srcIntfs.add(srcIntf); + } + + // ── Connect outputs ── + for (var ch = 0; ch < numChannels; ch++) { + channelOut.elements[ch] <= srcIntfs[ch].dataOut; + } + validOut <= srcIntfs[0].validOut; + done <= controller.doneFlag; + state <= controller.state; + + // ── Optional shared data bus (inOut port) ── + if (dataBus != null && writeEnable != null) { + final busPort = addInOut('dataBus', dataBus, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + final storedValue = addOutput('storedValue', width: dataWidth); + + final sharedBus = SharedDataBus( + LogicNet(name: 'busNet', width: dataWidth)..gets(busPort), + writeEnable, + clk, + reset, + dataWidth: dataWidth, + ); + storedValue <= sharedBus.storedValue; + } + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index ffeff9fc8..8a4bb9b58 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -115,11 +115,11 @@ abstract class Module { /// /// This does not contain any signals within [subModules]. Iterable get signals => UnmodifiableListView([ - ..._inputs.values, - ..._outputs.values, - ..._inOuts.values, - ...internalSignals, - ]); + ..._inputs.values, + ..._outputs.values, + ..._inOuts.values, + ...internalSignals, + ]); /// Accesses the [Logic] associated with this [Module]s [input] port /// named [name]. @@ -128,14 +128,16 @@ abstract class Module { Logic input(String name) => _inputs.containsKey(name) ? _inputs[name]! : throw PortDoesNotExistException( - 'Input name "$name" not found as an input to this Module.'); + 'Input name "$name" not found as an input to this Module.', + ); /// The original `source` provided to the creation of the [input] port [name] /// via [addInput] or [addInputArray]. Logic inputSource(String name) => _inputSources[name] ?? (throw PortDoesNotExistException( - '$name is not an input of this Module.')); + '$name is not an input of this Module.', + )); /// Provides the [input] named [name] if it exists, otherwise `null`. /// @@ -150,7 +152,8 @@ abstract class Module { Logic output(String name) => _outputs.containsKey(name) ? _outputs[name]! : throw PortDoesNotExistException( - 'Output name "$name" not found as an output of this Module.'); + 'Output name "$name" not found as an output of this Module.', + ); /// Provides the [output] named [name] if it exists, otherwise `null`. Logic? tryOutput(String name) => _outputs[name]; @@ -162,14 +165,16 @@ abstract class Module { Logic inOut(String name) => _inOuts.containsKey(name) ? _inOuts[name]! : throw PortDoesNotExistException( - 'InOut name "$name" not found as an in/out of this Module.'); + 'InOut name "$name" not found as an in/out of this Module.', + ); /// The original `source` provided to the creation of the [inOut] port [name] /// via [addInOut] or [addInOutArray]. Logic inOutSource(String name) => _inOutSources[name] ?? (throw PortDoesNotExistException( - '$name is not an inOut of this Module.')); + '$name is not an inOut of this Module.', + )); /// Provides the [inOut] named [name] if it exists, otherwise `null`. Logic? tryInOut(String name) => _inOuts[name]; @@ -211,7 +216,9 @@ abstract class Module { String get uniqueInstanceName => hasBuilt || reserveName ? _uniqueInstanceName : throw ModuleNotBuiltException( - this, 'Module must be built to access uniquified name.'); + this, + 'Module must be built to access uniquified name.', + ); String _uniqueInstanceName; /// A stable identity used to memoize this module's canonical instance name @@ -255,15 +262,17 @@ abstract class Module { /// /// If [reserveDefinitionName] is set, then code generation will fail if /// it is unable to keep from uniquifying [definitionName] to avoid conflicts. - Module( - {this.name = 'unnamed_module', - this.reserveName = false, - String? definitionName, - this.reserveDefinitionName = false}) - : _uniqueInstanceName = - Naming.validatedName(name, reserveName: reserveName) ?? name, - _definitionName = Naming.validatedName(definitionName, - reserveName: reserveDefinitionName); + Module({ + this.name = 'unnamed_module', + this.reserveName = false, + String? definitionName, + this.reserveDefinitionName = false, + }) : _uniqueInstanceName = + Naming.validatedName(name, reserveName: reserveName) ?? name, + _definitionName = Naming.validatedName( + definitionName, + reserveName: reserveDefinitionName, + ); /// Returns an [Iterable] of [Module]s representing the hierarchical path to /// this [Module]. @@ -274,7 +283,9 @@ abstract class Module { Iterable hierarchy() { if (!hasBuilt) { throw ModuleNotBuiltException( - this, 'Module must be built before accessing hierarchy.'); + this, + 'Module must be built before accessing hierarchy.', + ); } Module? pModule = this; final hierarchyQueue = Queue(); @@ -316,7 +327,8 @@ abstract class Module { Future build() async { if (hasBuilt) { throw Exception( - 'This Module has already been built, and can only be built once.'); + 'This Module has already been built, and can only be built once.', + ); } // construct the list of modules within this module @@ -333,8 +345,9 @@ abstract class Module { final uniquifier = Uniquifier(); for (final module in _subModules) { module._uniqueInstanceName = uniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(module.name), - reserved: module.reserveName); + initialName: Sanitizer.sanitizeSV(module.name), + reserved: module.reserveName, + ); } _checkValidHierarchy(visited: {}); @@ -357,15 +370,17 @@ abstract class Module { if (hierarchy.contains(this)) { final loopHierarchy = _hierarchyListToString(newHierarchy); throw InvalidHierarchyException( - 'Module $this is a submodule of itself: $loopHierarchy'); + 'Module $this is a submodule of itself: $loopHierarchy', + ); } if (visited.containsKey(this)) { final otherHierarchy = _hierarchyListToString(visited[this]!); final thisHierarchy = _hierarchyListToString(hierarchy); throw InvalidHierarchyException( - 'Module $this exists at more than one hierarchy: ' - '$otherHierarchy and $thisHierarchy'); + 'Module $this exists at more than one hierarchy: ' + '$otherHierarchy and $thisHierarchy', + ); } visited[this] = newHierarchy; @@ -383,9 +398,11 @@ abstract class Module { /// Adds a [Module] to this as a subModule. Future _addAndBuildModule(Module module) async { if (module.parent != null) { - throw Exception('This Module "$this" already has a parent. ' - 'If you are hitting this as a user of ROHD, please file ' - 'a bug at https://github.com/intel/rohd/issues.'); + throw Exception( + 'This Module "$this" already has a parent. ' + 'If you are hitting this as a user of ROHD, please file ' + 'a bug at https://github.com/intel/rohd/issues.', + ); } _subModules.add(module); @@ -415,8 +432,10 @@ abstract class Module { static bool isUnpreferred(String name) => Naming.isUnpreferred(name); /// Searches for [Logic]s and [Module]s within this [Module] from its inputs. - Future _traceInputForModuleContents(Logic signal, - {bool dontAddSignal = false}) async { + Future _traceInputForModuleContents( + Logic signal, { + bool dontAddSignal = false, + }) async { if (isOutput(signal) || _inOutDrivers.contains(signal)) { return; } @@ -427,8 +446,9 @@ abstract class Module { } try { - final subModule = - (signal.isInput || signal.isInOut) ? signal.parentModule : null; + final subModule = (signal.isInput || signal.isInOut) + ? signal.parentModule + : null; final subModuleParent = subModule?.parent; @@ -452,20 +472,28 @@ abstract class Module { await _addAndBuildModule(subModule); } for (final subModuleOutput in subModule._outputs.values) { - await _traceInputForModuleContents(subModuleOutput, - dontAddSignal: true); + await _traceInputForModuleContents( + subModuleOutput, + dontAddSignal: true, + ); } for (final subModuleInput in subModule._inputs.values) { - await _traceOutputForModuleContents(subModuleInput, - dontAddSignal: true); + await _traceOutputForModuleContents( + subModuleInput, + dontAddSignal: true, + ); } for (final subModuleInOutDriver in subModule._inOutDrivers) { final subModDontAddSignal = subModuleInOutDriver.isPort; - await _traceInputForModuleContents(subModuleInOutDriver, - dontAddSignal: subModDontAddSignal); - await _traceOutputForModuleContents(subModuleInOutDriver, - dontAddSignal: subModDontAddSignal); + await _traceInputForModuleContents( + subModuleInOutDriver, + dontAddSignal: subModDontAddSignal, + ); + await _traceOutputForModuleContents( + subModuleInOutDriver, + dontAddSignal: subModDontAddSignal, + ); } } else { if (!dontAddSignal && @@ -476,17 +504,25 @@ abstract class Module { // handle expanding the search for arrays if (signal.parentStructure != null) { - await _traceInputForModuleContents(signal.parentStructure!, - dontAddSignal: signal.isPort); - await _traceOutputForModuleContents(signal.parentStructure!, - dontAddSignal: signal.isPort); + await _traceInputForModuleContents( + signal.parentStructure!, + dontAddSignal: signal.isPort, + ); + await _traceOutputForModuleContents( + signal.parentStructure!, + dontAddSignal: signal.isPort, + ); } if (signal is LogicStructure) { for (final elem in signal.elements) { - await _traceInputForModuleContents(elem, - dontAddSignal: elem.isPort); - await _traceOutputForModuleContents(elem, - dontAddSignal: elem.isPort); + await _traceInputForModuleContents( + elem, + dontAddSignal: elem.isPort, + ); + await _traceOutputForModuleContents( + elem, + dontAddSignal: elem.isPort, + ); } } @@ -497,10 +533,11 @@ abstract class Module { if (!dontAddSignal && isInput(signal)) { throw PortRulesViolationException( - this, - signal.name, - 'Input $signal of module $this is dependent on' - ' another input of the same module.'); + this, + signal.name, + 'Input $signal of module $this is dependent on' + ' another input of the same module.', + ); } for (final dstConnection in signal.dstConnections) { @@ -520,13 +557,15 @@ abstract class Module { // extra searching in both directions for nets if (signal.isNet && !isPort(signal)) { await _traceOutputForModuleContents(signal); - for (final srcConnection - in signal.srcConnections.where((element) => element.isNet)) { + for (final srcConnection in signal.srcConnections.where( + (element) => element.isNet, + )) { await _traceInputForModuleContents(srcConnection); await _traceOutputForModuleContents(srcConnection); } - for (final dstConnection - in signal.dstConnections.where((element) => element.isNet)) { + for (final dstConnection in signal.dstConnections.where( + (element) => element.isNet, + )) { await _traceInputForModuleContents(dstConnection); await _traceOutputForModuleContents(dstConnection); } @@ -534,16 +573,19 @@ abstract class Module { } } on PortRulesViolationException catch (e) { throw PortRulesViolationException.trace( - module: this, - signal: signal, - lowerException: e, - traceDirection: 'from inputs'); + module: this, + signal: signal, + lowerException: e, + traceDirection: 'from inputs', + ); } } /// Searches for [Logic]s and [Module]s within this [Module] from its outputs. - Future _traceOutputForModuleContents(Logic signal, - {bool dontAddSignal = false}) async { + Future _traceOutputForModuleContents( + Logic signal, { + bool dontAddSignal = false, + }) async { if (isInput(signal) || _inOutDrivers.contains(signal)) { return; } @@ -554,8 +596,9 @@ abstract class Module { } try { - final subModule = - (signal.isOutput || signal.isInOut) ? signal.parentModule : null; + final subModule = (signal.isOutput || signal.isInOut) + ? signal.parentModule + : null; final subModuleParent = subModule?.parent; @@ -579,20 +622,28 @@ abstract class Module { await _addAndBuildModule(subModule); } for (final subModuleInput in subModule._inputs.values) { - await _traceOutputForModuleContents(subModuleInput, - dontAddSignal: true); + await _traceOutputForModuleContents( + subModuleInput, + dontAddSignal: true, + ); } for (final subModuleOutput in subModule._outputs.values) { - await _traceInputForModuleContents(subModuleOutput, - dontAddSignal: true); + await _traceInputForModuleContents( + subModuleOutput, + dontAddSignal: true, + ); } for (final subModuleInOutDriver in subModule._inOutDrivers) { final subModDontAddSignal = subModuleInOutDriver.isPort; - await _traceInputForModuleContents(subModuleInOutDriver, - dontAddSignal: subModDontAddSignal); - await _traceOutputForModuleContents(subModuleInOutDriver, - dontAddSignal: subModDontAddSignal); + await _traceInputForModuleContents( + subModuleInOutDriver, + dontAddSignal: subModDontAddSignal, + ); + await _traceOutputForModuleContents( + subModuleInOutDriver, + dontAddSignal: subModDontAddSignal, + ); } } else { if (!dontAddSignal && @@ -603,17 +654,25 @@ abstract class Module { // handle expanding the search for arrays if (signal.parentStructure != null) { - await _traceOutputForModuleContents(signal.parentStructure!, - dontAddSignal: signal.isPort); - await _traceInputForModuleContents(signal.parentStructure!, - dontAddSignal: signal.isPort); + await _traceOutputForModuleContents( + signal.parentStructure!, + dontAddSignal: signal.isPort, + ); + await _traceInputForModuleContents( + signal.parentStructure!, + dontAddSignal: signal.isPort, + ); } if (signal is LogicStructure) { for (final elem in signal.elements) { - await _traceOutputForModuleContents(elem, - dontAddSignal: elem.isPort); - await _traceInputForModuleContents(elem, - dontAddSignal: elem.isPort); + await _traceOutputForModuleContents( + elem, + dontAddSignal: elem.isPort, + ); + await _traceInputForModuleContents( + elem, + dontAddSignal: elem.isPort, + ); } } @@ -625,13 +684,15 @@ abstract class Module { // extra searching in both directions for nets if (signal.isNet && !isPort(signal)) { await _traceInputForModuleContents(signal); - for (final srcConnection - in signal.srcConnections.where((element) => element.isNet)) { + for (final srcConnection in signal.srcConnections.where( + (element) => element.isNet, + )) { await _traceOutputForModuleContents(srcConnection); await _traceInputForModuleContents(srcConnection); } - for (final dstConnection - in signal.dstConnections.where((element) => element.isNet)) { + for (final dstConnection in signal.dstConnections.where( + (element) => element.isNet, + )) { await _traceOutputForModuleContents(dstConnection); await _traceInputForModuleContents(dstConnection); } @@ -639,8 +700,10 @@ abstract class Module { if (signal is LogicStructure) { for (final elem in signal.elements) { - await _traceOutputForModuleContents(elem, - dontAddSignal: elem.isPort); + await _traceOutputForModuleContents( + elem, + dontAddSignal: elem.isPort, + ); } } else { for (final srcConnection in signal.srcConnections) { @@ -650,10 +713,11 @@ abstract class Module { } } on PortRulesViolationException catch (e) { throw PortRulesViolationException.trace( - module: this, - signal: signal, - lowerException: e, - traceDirection: 'from outputs'); + module: this, + signal: signal, + lowerException: e, + traceDirection: 'from outputs', + ); } } @@ -674,7 +738,8 @@ abstract class Module { inputs.containsKey(name) || inOuts.containsKey(name)) { throw UnavailableReservedNameException.withMessage( - 'Already defined a port with name "$name" in module "${this.name}".'); + 'Already defined a port with name "$name" in module "${this.name}".', + ); } } @@ -724,7 +789,9 @@ abstract class Module { /// only be used within this [Module]. The provided [source] is accessible via /// [inputSource]. LogicType addTypedInput( - String name, LogicType source) { + String name, + LogicType source, + ) { _checkForSafePortName(name); source = _validateType(source, isOutput: false, name: name); @@ -736,8 +803,10 @@ abstract class Module { final inPort = (source.clone(name: name) as LogicType)..gets(source); if (inPort.name != name) { - throw PortTypeException.forIntendedName(name, - 'The `clone` method for $source failed to update the signal name.'); + throw PortTypeException.forIntendedName( + name, + 'The `clone` method for $source failed to update the signal name.', + ); } if (inPort is LogicStructure) { @@ -828,7 +897,9 @@ abstract class Module { /// only be used within this [Module]. The provided [source] is accessible via /// [inOutSource]. LogicType addTypedInOut( - String name, LogicType source) { + String name, + LogicType source, + ) { _checkForSafePortName(name); if (!source.isNet) { @@ -859,8 +930,10 @@ abstract class Module { final inOutPort = (source.clone(name: name) as LogicType)..gets(source); if (inOutPort.name != name) { - throw PortTypeException.forIntendedName(name, - 'The `clone` method for $source failed to update the signal name.'); + throw PortTypeException.forIntendedName( + name, + 'The `clone` method for $source failed to update the signal name.', + ); } if (inOutPort is LogicStructure) { @@ -895,15 +968,16 @@ abstract class Module { }) { _checkForSafePortName(name); - final inArr = LogicArray( - name: name, - dimensions, - elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.reserved, - ) - ..gets(source) - ..setAllParentModule(this); + final inArr = + LogicArray( + name: name, + dimensions, + elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.reserved, + ) + ..gets(source) + ..setAllParentModule(this); _inputs[name] = inArr; @@ -929,8 +1003,11 @@ abstract class Module { /// Checks that the [logic] meets type requirements for `Typed` [Logic]s and /// returns a potentially modified [logic] to use. - LogicType _validateType(LogicType logic, - {required String name, required bool isOutput}) { + LogicType _validateType( + LogicType logic, { + required String name, + required bool isOutput, + }) { const exceptionMessage = 'Cannot use `Const` (or `LogicStructure` with `Const`s) as a port type.' ' Try passing in a `Logic` or parameterizing' @@ -975,7 +1052,9 @@ abstract class Module { /// /// The return value is the same as what is returned by [output]. LogicType addTypedOutput( - String name, LogicType Function({String name}) logicGenerator) { + String name, + LogicType Function({String name}) logicGenerator, + ) { _checkForSafePortName(name); // must make a new clone of it, to avoid people using ports of other modules @@ -985,14 +1064,17 @@ abstract class Module { if (outPort.isNet || (outPort is LogicStructure && outPort.hasNets)) { throw PortTypeException( - outPort, 'Typed outputs cannot have nets in them.'); + outPort, + 'Typed outputs cannot have nets in them.', + ); } if (outPort.name != name) { throw PortTypeException.forIntendedName( - name, - 'The `logicGenerator` function failed to' - ' update the signal name on $outPort.'); + name, + 'The `logicGenerator` function failed to' + ' update the signal name on $outPort.', + ); } if (outPort is LogicStructure) { @@ -1064,15 +1146,16 @@ abstract class Module { } } - final inOutArr = LogicArray.net( - name: name, - dimensions, - elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.reserved, - ) - ..gets(source) - ..setAllParentModule(this); + final inOutArr = + LogicArray.net( + name: name, + dimensions, + elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.reserved, + ) + ..gets(source) + ..setAllParentModule(this); // there may be packed arrays created by the `gets` above, so this makes // sure we catch all of those. @@ -1087,34 +1170,42 @@ abstract class Module { /// Connects the [source] to this [Module] using [Interface.connectIO] and /// returns a copy of the [source] that can be used within this module. - InterfaceType addInterfacePorts, - TagType extends Enum>(InterfaceType source, - {Iterable? inputTags, - Iterable? outputTags, - Iterable? inOutTags, - String Function(String original)? uniquify}) => - (source.clone() as InterfaceType) - ..connectIO(this, source, - inputTags: inputTags, - outputTags: outputTags, - inOutTags: inOutTags, - uniquify: uniquify); + InterfaceType addInterfacePorts< + InterfaceType extends Interface, + TagType extends Enum + >( + InterfaceType source, { + Iterable? inputTags, + Iterable? outputTags, + Iterable? inOutTags, + String Function(String original)? uniquify, + }) => (source.clone() as InterfaceType) + ..connectIO( + this, + source, + inputTags: inputTags, + outputTags: outputTags, + inOutTags: inOutTags, + uniquify: uniquify, + ); /// Connects the [source] to this [Module] using [PairInterface.pairConnectIO] /// and returns a copy of the [source] that can be used within this module. InterfaceType addPairInterfacePorts( - InterfaceType source, PairRole role, - {String Function(String original)? uniquify}) => + InterfaceType source, + PairRole role, { + String Function(String original)? uniquify, + }) => (source.clone() as InterfaceType) ..pairConnectIO(this, source, role, uniquify: uniquify); @override String toString() => [ - '"$name" ($definitionName) : ', - if (_inputs.isNotEmpty) '${_inputs.keys}', - if (_outputs.isNotEmpty) '=> ${_outputs.keys}', - if (_inOuts.isNotEmpty) '; ${_inOuts.keys}' - ].join(' '); + '"$name" ($definitionName) : ', + if (_inputs.isNotEmpty) '${_inputs.keys}', + if (_outputs.isNotEmpty) '=> ${_outputs.keys}', + if (_inOuts.isNotEmpty) '; ${_inOuts.keys}', + ].join(' '); /// Returns a pretty-print [String] of the heirarchy of all [Module]s within /// this [Module]. @@ -1142,7 +1233,8 @@ abstract class Module { throw ModuleNotBuiltException(this); } - final synthHeader = ''' + final synthHeader = + ''' /** * Generated by ROHD - www.github.com/intel/rohd * Generation time: ${Timestamper.stamp()} @@ -1151,9 +1243,24 @@ abstract class Module { '''; return synthHeader + - SynthBuilder(this, SystemVerilogSynthesizer()) - .getSynthFileContents() - .join('\n\n////////////////////\n\n'); + SynthBuilder( + this, + SystemVerilogSynthesizer(), + ).getSynthFileContents().join('\n\n////////////////////\n\n'); + } + + /// Returns a synthesized netlist JSON representation of this [Module]. + String generateNetlist({ + NetlistOptions options = const NetlistOptions(), + String? packageRoot, + }) { + if (!_hasBuilt) { + throw ModuleNotBuiltException(this); + } + + return NetlistSynthesizer( + options: options, + ).synthesizeToJson(this, packageRoot: packageRoot); } } diff --git a/lib/src/synthesizers/netlist/leaf_cell_mapper.dart b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart new file mode 100644 index 000000000..40b34bdbc --- /dev/null +++ b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart @@ -0,0 +1,482 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_cell_mapper.dart +// Maps ROHD leaf modules to Yosys-primitive cell representations. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// The result of mapping a leaf ROHD module to a Yosys-style cell. +typedef LeafCellMapping = ({ + String cellType, + Map portDirs, + Map> connections, + Map parameters, +}); + +/// Context provided to each leaf-cell mapping handler. +/// +/// Contains the module instance plus the raw ROHD port directions and +/// connections built by the synthesizer, so handlers can remap them to +/// Yosys-primitive port names. +class LeafCellContext { + /// The ROHD [Module] being mapped. + final Module module; + + /// Raw ROHD port-direction map (`{'portName': 'input'|'output'|'inout'}`). + final Map rawPortDirs; + + /// Raw ROHD connection map (`{'portName': [wireId, ...]}`). + final Map> rawConns; + + /// Creates a [LeafCellContext]. + const LeafCellContext(this.module, this.rawPortDirs, this.rawConns); + + // ── Shared helper methods ─────────────────────────────────────────── + + /// Find the first input port name matching [prefix]. + String? findInput(String prefix) { + for (final k in module.inputs.keys) { + if (k.startsWith(prefix)) { + return k; + } + } + return null; + } + + /// The first output port name, or `null` if there are none. + String? get firstOutput => + module.outputs.keys.isEmpty ? null : module.outputs.keys.first; + + /// The first input port name, or `null` if there are none. + String? get firstInput => + module.inputs.keys.isEmpty ? null : module.inputs.keys.first; + + /// Width (number of wire IDs) for a given ROHD port name. + int width(String portName) => rawConns[portName]?.length ?? 0; + + /// Build new port-direction and connection maps from a + /// `{rohdPortName: yosysPortName}` mapping. + ({Map portDirs, Map> connections}) remap( + Map nameMap, + ) { + final pd = {}; + final cn = >{}; + for (final e in nameMap.entries) { + final rohdName = e.key; + final netlistPortName = e.value; + pd[netlistPortName] = rawPortDirs[rohdName] ?? 'output'; + cn[netlistPortName] = rawConns[rohdName] ?? []; + } + return (portDirs: pd, connections: cn); + } +} + +/// Signature for a leaf-cell mapping handler. +/// +/// Returns a [LeafCellMapping] if the handler recognises the module, +/// or `null` to let the next handler try. +typedef LeafCellHandler = LeafCellMapping? Function(LeafCellContext ctx); + +/// Maps ROHD leaf [Module]s to Yosys-primitive cell representations. +/// +/// Handlers are registered via [register] and tried in registration order. +/// A singleton instance with all built-in ROHD types pre-registered is +/// available via [LeafCellMapper.defaultMapper]. +/// +/// ```dart +/// final mapper = LeafCellMapper.defaultMapper; +/// final result = mapper.map(sub, rawPortDirs, rawConns); +/// ``` +class LeafCellMapper { + /// Ordered list of registered handlers. + final _handlers = []; + + /// Creates an empty [LeafCellMapper] with no registered handlers. + LeafCellMapper(); + + /// The default mapper with all built-in ROHD leaf types registered. + static final defaultMapper = LeafCellMapper._withDefaults(); + + /// Register a mapping [handler]. + /// + /// Handlers are tried in registration order; the first non-null result + /// wins. Register more-specific handlers before less-specific ones. + void register(LeafCellHandler handler) { + _handlers.add(handler); + } + + /// Try to map [module] to a Yosys-primitive cell. + /// + /// Returns `null` if no registered handler matches. + LeafCellMapping? map( + Module module, + Map rawPortDirs, + Map> rawConns, + ) { + final ctx = LeafCellContext(module, rawPortDirs, rawConns); + for (final handler in _handlers) { + final result = handler(ctx); + if (result != null) { + return result; + } + } + return null; + } + + // ══════════════════════════════════════════════════════════════════════ + // Reusable mapping patterns + // ══════════════════════════════════════════════════════════════════════ + + /// Map a single-input, single-output gate (e.g. `$not`, `$reduce_and`). + static LeafCellMapping? unaryAY(LeafCellContext ctx, String cellType) { + final inN = ctx.firstInput; + final out = ctx.firstOutput; + if (inN == null || out == null) { + return null; + } + final r = ctx.remap({inN: 'A', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(inN), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + /// Map a two-input gate with ports A, B, Y (e.g. `$and`, `$eq`, `$shl`). + static LeafCellMapping? binaryABY( + LeafCellContext ctx, + String cellType, { + required String inAPrefix, + required String inBPrefix, + }) { + final a = ctx.findInput(inAPrefix); + final b = ctx.findInput(inBPrefix); + final out = ctx.firstOutput; + if (a == null || b == null || out == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(a), + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + // ══════════════════════════════════════════════════════════════════════ + // Built-in handler registration + // ══════════════════════════════════════════════════════════════════════ + + /// Creates a [LeafCellMapper] with built-in handlers for common ROHD leaf + /// types. + factory LeafCellMapper._withDefaults() { + final m = LeafCellMapper(); + + // Helper to reduce boilerplate for type-map-based handlers. + void registerByTypeMap( + Map typeMap, + LeafCellMapping? Function(LeafCellContext ctx, String cellType) handler, + ) { + m.register((ctx) { + final cellType = typeMap[ctx.module.runtimeType]; + return cellType == null ? null : handler(ctx, cellType); + }); + } + + m + // ── BusSubset → $slice ──────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! BusSubset) { + return null; + } + final sub = ctx.module as BusSubset; + final inName = sub.inputs.keys.first; + final outName = sub.outputs.keys.first; + final r = ctx.remap({inName: 'A', outName: 'Y'}); + return ( + cellType: r'$slice', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'OFFSET': sub.startIndex, + 'A_WIDTH': ctx.width(inName), + 'Y_WIDTH': ctx.width(outName), + }, + ); + }) + // ── Swizzle → $concat ───────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Swizzle) { + return null; + } + final outName = ctx.firstOutput; + final inputKeys = ctx.module.inputs.keys.toList(); + + // Filter out zero-width inputs (degenerate concat operands). + final nonZeroKeys = inputKeys.where((k) => ctx.width(k) > 0).toList(); + + if (nonZeroKeys.length == 2 && outName != null) { + final r = ctx.remap({ + nonZeroKeys[0]: 'A', + nonZeroKeys[1]: 'B', + outName: 'Y', + }); + return ( + cellType: r'$concat', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(nonZeroKeys[0]), + 'B_WIDTH': ctx.width(nonZeroKeys[1]), + }, + ); + } + + // Single non-zero input ⇒ emit as $buf. + if (nonZeroKeys.length == 1 && outName != null) { + final r = ctx.remap({nonZeroKeys[0]: 'A', outName: 'Y'}); + return ( + cellType: r'$buf', + portDirs: r.portDirs, + connections: r.connections, + parameters: {'WIDTH': ctx.width(nonZeroKeys[0])}, + ); + } + + if (nonZeroKeys.isEmpty) { + return null; + } + + // N-input concat: per-input range labels, output is Y. + final pd = {}; + final cn = >{}; + final params = {}; + var bitOffset = 0; + for (var i = 0; i < nonZeroKeys.length; i++) { + final ik = nonZeroKeys[i]; + final w = ctx.width(ik); + final label = w == 1 + ? '[$bitOffset]' + : '[${bitOffset + w - 1}:$bitOffset]'; + pd[label] = 'input'; + cn[label] = ctx.rawConns[ik] ?? []; + params['IN${i}_WIDTH'] = w; + bitOffset += w; + } + if (outName != null) { + pd['Y'] = 'output'; + cn['Y'] = ctx.rawConns[outName] ?? []; + } + return ( + cellType: r'$concat', + portDirs: pd, + connections: cn, + parameters: params, + ); + }) + // ── NOT gate ────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! NotGate) { + return null; + } + return unaryAY(ctx, r'$not'); + }) + // ── Mux ─────────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Mux) { + return null; + } + final ctrl = ctx.findInput('_control') ?? ctx.findInput('control'); + final d0 = ctx.findInput('_d0') ?? ctx.findInput('d0'); + final d1 = ctx.findInput('_d1') ?? ctx.findInput('d1'); + final out = ctx.firstOutput; + if (ctrl == null || d0 == null || d1 == null || out == null) { + return null; + } + // Yosys: S=select, A=d0 (when S=0), B=d1 (when S=1). + final r = ctx.remap({ctrl: 'S', d0: 'A', d1: 'B', out: 'Y'}); + return ( + cellType: r'$mux', + portDirs: r.portDirs, + connections: r.connections, + parameters: {'WIDTH': ctx.width(d0)}, + ); + }) + // ── Add ─────────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Add) { + return null; + } + final in0 = ctx.findInput('_in0') ?? ctx.findInput('in0'); + final in1 = ctx.findInput('_in1') ?? ctx.findInput('in1'); + final sumName = ctx.module.outputs.keys.firstWhere( + (k) => !k.contains('carry'), + orElse: () => '', + ); + final carryName = ctx.module.outputs.keys.firstWhere( + (k) => k.contains('carry'), + orElse: () => '', + ); + if (in0 == null || in1 == null || sumName.isEmpty) { + return null; + } + final pd = {'A': 'input', 'B': 'input', 'Y': 'output'}; + final cn = >{ + 'A': ctx.rawConns[in0] ?? [], + 'B': ctx.rawConns[in1] ?? [], + 'Y': ctx.rawConns[sumName] ?? [], + }; + if (carryName.isNotEmpty) { + pd['CO'] = 'output'; + cn['CO'] = ctx.rawConns[carryName] ?? []; + } + return ( + cellType: r'$add', + portDirs: pd, + connections: cn, + parameters: { + 'A_WIDTH': ctx.width(in0), + 'B_WIDTH': ctx.width(in1), + 'Y_WIDTH': ctx.width(sumName), + }, + ); + }) + // ── FlipFlop → $dff ─────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! FlipFlop) { + return null; + } + final clk = ctx.findInput('_clk') ?? ctx.findInput('clk'); + final d = ctx.findInput('_d') ?? ctx.findInput('d'); + final en = ctx.findInput('_en') ?? ctx.findInput('en'); + final rst = ctx.findInput('_reset') ?? ctx.findInput('reset'); + final q = ctx.firstOutput; + if (clk == null || d == null || q == null) { + return null; + } + final pd = { + '_clk': 'input', + '_d': 'input', + '_q': 'output', + }; + final cn = >{ + '_clk': ctx.rawConns[clk] ?? [], + '_d': ctx.rawConns[d] ?? [], + '_q': ctx.rawConns[q] ?? [], + }; + if (en != null && ctx.rawConns.containsKey(en)) { + pd['_en'] = 'input'; + cn['_en'] = ctx.rawConns[en] ?? []; + } + if (rst != null && ctx.rawConns.containsKey(rst)) { + pd['_reset'] = 'input'; + cn['_reset'] = ctx.rawConns[rst] ?? []; + } + final rstVal = + ctx.findInput('_resetValue') ?? ctx.findInput('resetValue'); + if (rstVal != null && ctx.rawConns.containsKey(rstVal)) { + pd['_resetValue'] = 'input'; + cn['_resetValue'] = ctx.rawConns[rstVal] ?? []; + } + return ( + cellType: r'$dff', + portDirs: pd, + connections: cn, + parameters: { + 'WIDTH': ctx.width(d), + 'CLK_POLARITY': 1, + }, + ); + }); + + // ── Type-map-based gates ─────────────────────────────────────────── + final gateRegistrations = + < + ( + Map, + LeafCellMapping? Function(LeafCellContext, String), + ) + >[ + ( + const { + And2Gate: r'$and', + Or2Gate: r'$or', + Xor2Gate: r'$xor', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + AndUnary: r'$reduce_and', + OrUnary: r'$reduce_or', + XorUnary: r'$reduce_xor', + }, + unaryAY, + ), + ( + const { + Multiply: r'$mul', + Subtract: r'$sub', + Equals: r'$eq', + NotEquals: r'$ne', + LessThan: r'$lt', + GreaterThan: r'$gt', + LessThanOrEqual: r'$le', + GreaterThanOrEqual: r'$ge', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + LShift: r'$shl', + RShift: r'$shr', + ARShift: r'$shiftx', + }, + (ctx, type) => binaryABY( + ctx, + type, + inAPrefix: '_in', + inBPrefix: '_shiftAmount', + ), + ), + ]; + for (final (typeMap, handler) in gateRegistrations) { + registerByTypeMap(typeMap, handler); + } + + // ── TriStateBuffer → $tribuf ────────────────────────────────────── + m.register((ctx) { + if (ctx.module is! TriStateBuffer) { + return null; + } + final tsb = ctx.module as TriStateBuffer; + final inName = tsb.inputs.keys.first; // data input + final enName = tsb.inputs.keys.last; // enable + final outName = tsb.inOuts.keys.first; // inout output + final r = ctx.remap({inName: 'A', enName: 'EN', outName: 'Y'}); + return ( + cellType: r'$tribuf', + portDirs: r.portDirs, + connections: r.connections, + parameters: {'WIDTH': ctx.width(inName)}, + ); + }); + + return m; + } +} diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart new file mode 100644 index 000000000..268416b31 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -0,0 +1,15 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist.dart +// Barrel file for netlist synthesis library. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +export 'leaf_cell_mapper.dart'; +export 'netlist_options.dart'; +export 'netlist_passes.dart'; +export 'netlist_synthesis_result.dart'; +export 'netlist_synthesizer.dart'; +export 'netlist_utils.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_options.dart new file mode 100644 index 000000000..3d4095e4b --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_options.dart @@ -0,0 +1,100 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_options.dart +// Configuration for netlist synthesis. +// +// 2026 March 12 +// Author: Desmond Kirkpatrick + +import 'package:rohd/src/synthesizers/netlist/leaf_cell_mapper.dart'; + +/// The current format version for netlist JSON produced by ROHD. +const String netlistFormatVersion = '0.0.5'; + +/// Configuration options for netlist synthesis. +/// +/// The netlist synthesizer serves two main consumer flows, both configured +/// through these options: +/// +/// **Flow 1 — Slim JSON** ([NetlistOptions.slimMode]): +/// Batch synthesis of the entire design, producing a lightweight +/// representation with ports, signals, and cell stubs but **no cell +/// connections**. Used for the initial DevTools hierarchy load. +/// +/// **Flow 2 — Full JSON, incremental** (`NetlistSynthesizer.synthesizeToJson`): +/// Returns the complete netlist (with cell connections) for a single +/// module definition on demand. Results are cached; the first call +/// may trigger a lazy `SynthBuilder` run on the requested subtree. +/// +/// Both flows run the identical pipeline: `SynthBuilder` → +/// `collectModuleEntries` → `applyPostProcessingPasses`. Flow 1 +/// then strips cell connections from the cached data; Flow 2 returns +/// it verbatim. This guarantees cell keys and wire IDs are stable +/// across both flows. +/// +/// Bundles all parameters that control netlist generation into a single +/// object, making it easier to pass through call chains and to store +/// for incremental synthesis. +/// +/// Example usage: +/// ```dart +/// const options = NetlistOptions( +/// collapseTransparentClusters: true, +/// ); +/// final synth = NetlistSynthesizer(options: options); +/// ``` +class NetlistOptions { + /// The leaf-cell mapper used to convert ROHD leaf modules to Yosys + /// primitive cell types. When `null`, [LeafCellMapper.defaultMapper] + /// is used. + final LeafCellMapper? leafCellMapper; + + /// When `true`, a single unified pass finds connected components of + /// all transparent cells (`$buf`, `$slice`, `$concat`, + /// `$struct_unpack`, `$struct_pack`), traces each cluster's output + /// bits back to their ultimate source bits, and replaces every + /// multi-cell cluster with a direct `$buf`. This subsumes all of + /// the individual collapse passes above. + final bool collapseTransparentClusters; + + /// When `true`, dead-cell elimination is performed after aliasing to + /// remove cells whose inputs are entirely undriven or whose outputs + /// are entirely unconsumed. + final bool enableDCE; + + /// When `true`, the synthesizer produces "slim" output: the full + /// synthesis pipeline runs (including all post-processing passes), + /// but cell connection maps are stripped from the result. + /// Netnames and ports are still emitted with full wire-ID fidelity, + /// so a subsequent full-mode synthesis of the same module will + /// produce compatible wire IDs. + final bool slimMode; + + /// When `true`, contiguous ascending runs of ≥3 integer bit IDs in + /// `bits` arrays and cell `connections` arrays are replaced with + /// `"start:end"` range strings (e.g. `[52, 53, 54, 55]` → `["52:55"]`). + /// + /// This is backward-compatible: Yosys-format arrays already mix + /// integers with constant strings `"0"` and `"1"`. Parsers can + /// detect range strings by the presence of `:`. + final bool compressBitRanges; + + /// When `true`, the JSON output uses no indentation (compact form). + /// When `false` (default), the JSON is pretty-printed with two-space + /// indentation. + final bool compactJson; + + /// Creates a [NetlistOptions] with the given configuration. + /// + /// All parameters have sensible defaults matching the current + /// netlist synthesizer behaviour. + const NetlistOptions({ + this.leafCellMapper, + this.collapseTransparentClusters = false, + this.enableDCE = true, + this.slimMode = false, + this.compressBitRanges = false, + this.compactJson = false, + }); +} diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart new file mode 100644 index 000000000..f5e498740 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -0,0 +1,356 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_passes.dart +// Post-processing optimization passes for netlist synthesis. +// +// These passes operate on the modules map (definition name → module data) +// produced by [NetlistSynthesizer.synthesize]. They simplify the netlist +// by grouping struct conversions, collapsing redundant cells, and inserting +// buffer cells for cleaner schematic rendering. +// +// 2025 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Post-processing optimization passes for netlist synthesis. +/// +/// All methods are static — no instances are created. +class NetlistPasses { + NetlistPasses._(); + + /// Collects a combined modules map from [SynthesisResult]s suitable for + /// JSON emission. + static Map> collectModuleEntries( + Iterable results, { + Module? topModule, + }) { + final allModules = >{}; + for (final result in results) { + if (result is NetlistSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = Map.from(result.attributes); + if (topModule != null && result.module == topModule) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }; + } + } + return allModules; + } + + // ════════════════════════════════════════════════════════════════════ + // Unified transparent-cell clustering + // ════════════════════════════════════════════════════════════════════ + + /// Transparent cell types that only reshuffle / rename bits. + static const _transparentTypes = { + r'$buf', + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + /// Unified transparent-cell clustering pass. + /// + /// **Phase 1 — Cluster identification:** + /// Builds an undirected graph over transparent cells (two cells are + /// neighbours when one's output wire feeds the other's input) and + /// finds connected components via BFS. + /// + /// **Phase 2 — Cluster collapse:** + /// For every multi-cell component, traces each externally-consumed + /// output bit backward through the component's bit-level mapping + /// until reaching an external source bit, then replaces the entire + /// component with a single `$buf` wired from traced sources to + /// destinations. + static void applyTransparentClustering( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + + // ── Gather transparent cells ── + + final tCells = { + for (final e in cells.entries) + if (_transparentTypes.contains(e.value['type'] as String?)) e.key, + }; + if (tCells.isEmpty) { + continue; + } + + // ── Wire maps ── + + final wireConsumers = >{}; + + for (final e in cells.entries) { + final dirs = e.value['port_directions'] as Map? ?? {}; + final conns = e.value['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) == 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is int) { + (wireConsumers[b] ??= {}).add(e.key); + } + } + } + } + + // Bits consumed by module output / inout ports. + final portOutBits = {}; + for (final pv in ports.values) { + final pm = pv as Map; + final dir = pm['direction'] as String?; + if (dir == 'output' || dir == 'inout') { + for (final b in pm['bits'] as List) { + if (b is int) { + portOutBits.add(b); + } + } + } + } + + // ── Phase 1: connected components ── + + final adj = >{for (final tc in tCells) tc: {}}; + + for (final tc in tCells) { + final dirs = + cells[tc]!['port_directions'] as Map? ?? {}; + final conns = cells[tc]!['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + for (final c in wireConsumers[b] ?? const {}) { + if (c != tc && tCells.contains(c)) { + adj[tc]!.add(c); + adj[c]!.add(tc); + } + } + } + } + } + + final visited = {}; + final components = >[]; + + for (final tc in tCells) { + if (!visited.add(tc)) { + continue; + } + final comp = {tc}; + final stack = [tc]; + while (stack.isNotEmpty) { + final cur = stack.removeLast(); + for (final nb in adj[cur]!) { + if (visited.add(nb)) { + comp.add(nb); + stack.add(nb); + } + } + } + if (comp.length >= 2) { + components.add(comp); + } + } + + if (components.isEmpty) { + continue; + } + + // ── Phase 2: trace & replace ── + + final cellsToRemove = {}; + final cellsToAdd = >{}; + + for (final comp in components) { + // Build output-bit → input-bit map for the whole cluster. + final bitMap = {}; + for (final cn in comp) { + _mapCellBits(cells[cn]!, bitMap); + } + + // External output bits: produced by the cluster but consumed + // by something outside it (another cell or module output port). + final extOut = []; + for (final cn in comp) { + final dirs = + cells[cn]!['port_directions'] as Map? ?? {}; + final conns = + cells[cn]!['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + if (portOutBits.contains(b) || + (wireConsumers[b]?.any((c) => !comp.contains(c)) ?? false)) { + extOut.add(b); + } + } + } + } + + if (extOut.isEmpty) { + // Fully dead cluster — remove. + cellsToRemove.addAll(comp); + continue; + } + + // Trace each external output back through the cluster to an + // external source bit. + final aList = []; + final yList = []; + var ok = true; + + for (final ob in extOut) { + Object cur = ob; + final seen = {}; + while (cur is int && bitMap.containsKey(cur)) { + if (!seen.add(cur)) { + ok = false; + break; + } + cur = bitMap[cur]!; + } + if (!ok) { + break; + } + aList.add(cur); + yList.add(ob); + } + + if (!ok) { + continue; + } + + cellsToAdd['cluster_buf_${comp.first}'] = NetlistUtils.makeBufCell( + aList.length, + aList, + yList, + ); + cellsToRemove.addAll(comp); + } + + cellsToRemove.forEach(cells.remove); + cells.addAll(cellsToAdd); + } + } + + /// Populates [bitMap] with output-wire-bit → input-wire-bit entries + /// for a single transparent cell. + static void _mapCellBits(Map cell, Map bitMap) { + final type = cell['type']! as String; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final params = cell['parameters'] as Map? ?? {}; + + switch (type) { + case r'$buf': + _mapPairwise(conns['A'] as List, conns['Y'] as List, bitMap); + + case r'$slice': + final a = conns['A'] as List; + final y = conns['Y'] as List; + final off = params['OFFSET'] as int? ?? 0; + for (var i = 0; i < y.length; i++) { + if (y[i] is int && (off + i) < a.length) { + bitMap[y[i] as int] = a[off + i] as Object; + } + } + + case r'$concat': + final y = conns['Y'] as List; + // Input ports are in connection-map order; their bits + // concatenate to form Y (first port at LSB). + final inBits = [ + for (final pe in conns.entries) + if ((dirs[pe.key] as String?) != 'output') + ...(pe.value as List).cast(), + ]; + _mapPairwise(inBits, y, bitMap); + + case r'$struct_unpack': + final a = conns['A'] as List; + final fc = params['FIELD_COUNT'] as int? ?? 0; + for (var f = 0; f < fc; f++) { + final fn = params['FIELD_${f}_NAME'] as String?; + final fo = params['FIELD_${f}_OFFSET'] as int? ?? 0; + if (fn == null) { + continue; + } + final fb = conns[fn] as List?; + if (fb == null) { + continue; + } + for (var i = 0; i < fb.length; i++) { + if (fb[i] is int && (fo + i) < a.length) { + bitMap[fb[i] as int] = a[fo + i] as Object; + } + } + } + + case r'$struct_pack': + final y = conns['Y'] as List; + final fc = params['FIELD_COUNT'] as int? ?? 0; + final src = List.filled(y.length, null); + for (var f = 0; f < fc; f++) { + final fn = params['FIELD_${f}_NAME'] as String?; + final fo = params['FIELD_${f}_OFFSET'] as int? ?? 0; + if (fn == null) { + continue; + } + final fb = conns[fn] as List?; + if (fb == null) { + continue; + } + for (var i = 0; i < fb.length; i++) { + if ((fo + i) < src.length) { + src[fo + i] = fb[i]; + } + } + } + for (var i = 0; i < y.length; i++) { + if (y[i] is int && src[i] != null) { + bitMap[y[i] as int] = src[i]!; + } + } + } + } + + /// Maps `Y[i]` → `A[i]` for identity-shaped cells. + static void _mapPairwise( + List a, + List y, + Map bitMap, + ) { + for (var i = 0; i < y.length && i < a.length; i++) { + if (y[i] is int) { + bitMap[y[i] as int] = a[i] as Object; + } + } + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart new file mode 100644 index 000000000..f07d29962 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -0,0 +1,85 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesis_result.dart +// A simple SynthesisResult that holds netlist data for one module. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; + +/// A [SynthesisResult] that holds the netlist representation of a single +/// module level: its ports, cells, and netnames. +class NetlistSynthesisResult extends SynthesisResult { + /// The ports map: name → {direction, bits}. + final Map> ports; + + /// The cells map: instance name → cell data. + final Map> cells; + + /// The netnames map: net name → {bits, attributes}. + final Map netnames; + + /// Attributes for this module (e.g., top marker). + final Map attributes; + + /// Cached JSON string for comparison and output. + late final String _cachedJson = _buildJson(); + + /// Creates a [NetlistSynthesisResult] for [module]. + NetlistSynthesisResult( + super.module, + super.getInstanceTypeOfModule, { + required this.ports, + required this.cells, + required this.netnames, + this.attributes = const {}, + }); + + String _buildJson() { + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + return const JsonEncoder().convert(moduleEntry); + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is NetlistSynthesisResult && _cachedJson == other._cachedJson; + + @override + int get matchHashCode => _cachedJson.hashCode; + + @override + @Deprecated('Use `toSynthFileContents()` instead.') + String toFileContents() => toSynthFileContents().first.contents; + + @override + List toSynthFileContents() { + final typeName = instanceTypeName; + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + final contents = const JsonEncoder.withIndent(' ').convert({ + 'creator': 'NetlistSynthesizer (rohd)', + 'version': netlistFormatVersion, + 'modules': {typeName: moduleEntry}, + }); + return [ + SynthFileContents( + name: '$typeName.rohd.json', + description: 'netlist for $typeName', + contents: contents, + ), + ]; + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart new file mode 100644 index 000000000..221398414 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -0,0 +1,2110 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer.dart +// A netlist synthesizer built on [SynthModuleDefinition]. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// +/// Skips SystemVerilog-specific processing (chain collapsing, net connects, +/// inOut inline replacement) since netlist represents all sub-modules as +/// cells rather than inline assignment expressions. +class _NetlistSynthModuleDefinition extends SynthModuleDefinition { + _NetlistSynthModuleDefinition(Module module) : super(module) { + // Create explicit $slice cells for LogicArray input ports so the + // netlist shows select gates for element extraction rather than + // flat bit aliasing. + module.inputs.values.whereType().forEach( + _subsetReceiveArrayPort, + ); + + // Same for LogicArray outputs on submodules (received into this scope). + final subModuleOutputArrays = + module.subModules + .expand((sub) => sub.outputs.values) + .whereType() + .toSet() + ..forEach(_subsetReceiveArrayPort); + + // Create explicit $concat cells for internal LogicArrays whose elements + // are driven independently (e.g. by constants) and then consumed by + // submodule input ports. This parallels what _subsetReceiveArrayPort does + // on the decomposition side. + // + // Skip arrays that were merged with a port array's SynthLogic — those + // are already structurally decomposed by the $slice cells created above + // and reassembling them would create a circular driver on the port bus. + // Also skip submodule output arrays that already received $slice cells. + final portArrays = { + ...module.inputs.values.whereType(), + ...module.outputs.values.whereType(), + ...module.inOuts.values.whereType(), + }; + final excludedArrays = { + ...portArrays, + ...subModuleOutputArrays, + }; + // For multi-dimensional arrays, also exclude nested sub-arrays. + // E.g. LogicArray([10,2],8) has 10 children each being LogicArray([2],8). + // Without this, the sub-arrays get spurious $concat cells creating + // multi-driver conflicts on the parent port's bits. + void addNestedArrays(LogicArray arr) { + for (final elem in arr.elements) { + if (elem is LogicArray) { + excludedArrays.add(elem); + addNestedArrays(elem); + } + } + } + + { + ...portArrays, + ...subModuleOutputArrays, + }.forEach(addNestedArrays); + final portArraySynthLogics = {}; + for (final pa in excludedArrays) { + final sl = logicToSynthMap[pa]; + if (sl != null) { + portArraySynthLogics.add(sl.replacement ?? sl); + } + } + module.internalSignals + .whereType() + .where((sig) { + if (excludedArrays.contains(sig)) { + return false; + } + final sl = logicToSynthMap[sig]; + if (sl == null) { + return false; + } + final resolved = sl.replacement ?? sl; + return !portArraySynthLogics.contains(resolved); + }) + .forEach(_concatAssembleArray); + } + + /// Creates explicit `$slice` cells for each element of a [LogicArray] port. + /// + /// Each element gets a [_BusSubsetForArraySlice] that extracts its bit range + /// from the packed parent bus. This produces explicit select gates in the + /// netlist, making array decomposition visible and traceable. + void _subsetReceiveArrayPort(LogicArray port) { + final portSynth = getSynthLogic(port)!; + + var idx = 0; + for (final element in port.elements) { + final elemSynth = getSynthLogic(element)!; + internalSignals.add(elemSynth); + + final subsetMod = _BusSubsetForArraySlice( + Logic(width: port.width, name: 'DUMMY'), + idx, + idx + element.width - 1, + ); + + getSynthSubModuleInstantiation(subsetMod) + ..setOutputMapping(subsetMod.subset.name, elemSynth) + ..setInputMapping(subsetMod.original.name, portSynth) + // Pick a name now — this may be called after _pickNames() has run. + ..pickName(module); + + idx += element.width; + } + } + + /// Creates an explicit `$concat` cell that assembles a [LogicArray]'s + /// elements into the full packed array bus. + /// + /// This is the assembly counterpart to [_subsetReceiveArrayPort]: when + /// individual array elements are driven independently (e.g. by constants), + /// this makes the concatenation explicit as a visible gate in the netlist. + void _concatAssembleArray(LogicArray array) { + final arraySynth = getSynthLogic(array)!; + + // Build dummy signals matching each element's width. + final dummyElements = []; + for (final element in array.elements) { + dummyElements.add(Logic(width: element.width, name: 'DUMMY')); + } + + // Pass reversed dummies so that Swizzle's internal reversal cancels out, + // leaving in0 aligned with element[0] (LSB) and inN with element[N]. + final concatMod = _SwizzleForArrayConcat(dummyElements.reversed.toList()); + + final ssmi = getSynthSubModuleInstantiation(concatMod) + // Map the concat output to the full array. + ..setOutputMapping(concatMod.out.name, arraySynth); + + // Map each element input. + // Because we reversed dummies above, in0 corresponds to element[0], + // in1 to element[1], etc. + for (var i = 0; i < array.elements.length; i++) { + final elemSynth = getSynthLogic(array.elements[i])!; + internalSignals.add(elemSynth); + final inputName = concatMod.inputs.keys.elementAt(i); + ssmi.setInputMapping(inputName, elemSynth); + } + + // Pick a name now — this may be called after _pickNames() has run. + ssmi.pickName(module); + } + + @override + void process() { + // No SV-specific transformations -- we want every sub-module to remain + // as a cell in the JSON. + } +} + +/// A simple [Synthesizer] that produces netlist-compatible JSON. +/// +/// Leverages [SynthModuleDefinition] for signal tracing, naming, and +/// constant resolution, then maps the resulting [SynthLogic]s to integer +/// wire-bit IDs for netlist JSON output. +/// +/// Leaf modules (those with no sub-modules, or special cases like [FlipFlop]) +/// do *not* get their own module definition -- they appear only as cells +/// inside their parent. +/// +/// Usage: +/// ```dart +/// const options = NetlistOptions(collapseTransparentClusters: true); +/// final synth = NetlistSynthesizer(options: options); +/// final builder = SynthBuilder(topModule, synth); +/// final json = synth.synthesizeToJson(topModule); +/// ``` +class NetlistSynthesizer extends Synthesizer { + /// The configuration options controlling netlist synthesis. + /// + /// See [NetlistOptions] for documentation on individual fields. + final NetlistOptions options; + + /// Convenience accessor for the leaf-cell mapper. + LeafCellMapper get leafCellMapper => + options.leafCellMapper ?? LeafCellMapper.defaultMapper; + + /// Creates a [NetlistSynthesizer]. + /// + /// All synthesis parameters are bundled in [options]; see + /// [NetlistOptions] for documentation on each field. + NetlistSynthesizer({this.options = const NetlistOptions()}); + + @override + bool generatesDefinition(Module module) => + // Only modules with sub-modules generate their own module definition. + // Leaf modules (no children) become cells inside their parent. + // FlipFlop has internal Sequential sub-modules but should be emitted as + // a flat Yosys $dff primitive, not as a hierarchical module. + module is! FlipFlop && module.subModules.isNotEmpty; + + @override + SynthesisResult synthesize( + Module module, + String Function(Module module) getInstanceTypeOfModule, { + SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults, + }) { + final isTop = module.parent == null; + final attr = {'src': 'generated'}; + if (isTop) { + attr['top'] = 1; + } + + // -- Build SynthModuleDefinition ------------------------------------ + // This does all signal tracing, naming, constant handling, + // assignment collapsing, and unused signal pruning. + final canBuildSynthDef = + !(module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none); + final synthDef = canBuildSynthDef + ? _NetlistSynthModuleDefinition(module) + : null; + + // -- Wire-ID allocation --------------------------------------------- + // Start wire IDs at 2 to avoid collision with Yosys constant string + // bits "0" and "1". JavaScript viewers coerce object keys to strings, + // so integer wire ID 0 becomes "0", clashing with the constant-bit + // string "0". + var nextId = 2; + + // Map from SynthLogic -> assigned wire-bit IDs. + final synthLogicIds = >{}; + + /// Allocate or retrieve wire IDs for a [SynthLogic]. + /// For constants, do NOT follow the replacement chain to ensure each + /// constant usage gets its own separate driver cell in netlist. + List getIds(SynthLogic sl) { + var resolved = sl; + // For non-constants, follow replacement chain to resolve merged logics. + // For constants, keep them separate to create distinct const drivers. + if (!sl.isConstant) { + resolved = NetlistUtils.resolveReplacement(resolved); + } + final ids = synthLogicIds.putIfAbsent( + resolved, + () => List.generate(resolved.width, (_) => nextId++), + ); + return ids; + } + + // -- Ports ----------------------------------------------------------- + final ports = >{}; + + final portGroups = [ + ('input', synthDef?.inputs, module.inputs), + ('output', synthDef?.outputs, module.outputs), + ('inout', synthDef?.inOuts, module.inOuts), + ]; + for (final (direction, synthLogics, modulePorts) in portGroups) { + if (synthLogics != null) { + for (final sl in synthLogics) { + final ids = getIds(sl); + final portName = NetlistUtils.portNameForSynthLogic(sl, modulePorts); + if (portName != null) { + final portLogic = modulePorts[portName]; + ports[portName] = { + 'direction': direction, + 'bits': ids, + if (portLogic != null) + 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), + }; + } + } + } else { + for (final entry in modulePorts.entries) { + final ids = List.generate(entry.value.width, (_) => nextId++); + ports[entry.key] = { + 'direction': direction, + 'bits': ids, + 'logic_type': NetlistUtils.buildLogicType(entry.value, ids), + }; + } + } + } + + // -- Pre-allocate IDs for internal signals in Module order ----------- + // This ensures that internals get IDs in the same order as + // Module.internalSignals, matching the diagnostics signal collection. + // Signals already allocated during the port phase are skipped by + // putIfAbsent. Synthesis-generated wires get IDs later (during cell + // emission), so they are naturally appended after internals. + // + // Three-tier ordering guarantee: + // Tier 0 (ports): inputs → outputs → inOuts [above] + // Tier 1 (internals): module.internalSignals [here] + // Tier 2 (synth): cell emission wires [below] + if (synthDef != null) { + module.internalSignals + .map((sig) => synthDef.logicToSynthMap[sig]) + .whereType() + .where((sl) => !sl.isConstant) + .forEach(getIds); + } + + // -- Cell emission --------------------------------------------------- + final cells = >{}; + + // Track constant SynthLogics consumed exclusively by + // Combinational/Sequential so we can suppress their driver cells. + final blockedConstSynthLogics = {}; + + // Track emitted cell keys per instance for purging later. + final emittedCellKeys = {}; + + if (synthDef != null) { + for (final instance in synthDef.subModuleInstantiations) { + if (!instance.needsInstantiation) { + continue; + } + + final sub = instance.module; + + final isLeaf = !generatesDefinition(sub); + final defaultCellType = isLeaf + ? sub.definitionName + : getInstanceTypeOfModule(sub); + + // Build port directions and connections from instance mappings. + final rawPortDirs = {}; + final rawConnections = >{}; + + for (final (dir, mapping) in [ + ('input', instance.inputMapping), + ('output', instance.outputMapping), + ('inout', instance.inOutMapping), + ]) { + for (final e in mapping.entries) { + rawPortDirs[e.key] = dir; + final ids = getIds(e.value); + rawConnections[e.key] = ids.cast(); + } + } + + // Map leaf cells to Yosys primitive types where possible. + final mapped = isLeaf + ? leafCellMapper.map(sub, rawPortDirs, rawConnections) + : null; + + final cellPortDirs = mapped?.portDirs ?? rawPortDirs; + final cellConns = mapped?.connections ?? rawConnections; + + // Use the SSMI's uniquified name as cell key to avoid + // collisions between identically-named modules (e.g. multiple + // struct_slice instances that share the same Module.name). + final cellKey = instance.name; + emittedCellKeys[instance] = cellKey; + + // -- Collapse bit-slice ports on Combinational / Sequential ---- + if (sub is Combinational || sub is Sequential) { + NetlistUtils.collapseAlwaysBlockPorts( + synthDef, + instance, + cellPortDirs, + cellConns, + getIds, + ); + } + + // -- Filter constant inputs from Combinational / Sequential ---- + if (sub is Combinational || sub is Sequential) { + final portsToRemove = []; + for (final pe in cellConns.entries) { + final portName = pe.key; + final synthLogic = + instance.inputMapping[portName] ?? + instance.inOutMapping[portName]; + if (synthLogic != null && + NetlistUtils.isConstantSynthLogic(synthLogic)) { + portsToRemove.add(portName); + blockedConstSynthLogics.add(synthLogic.replacement ?? synthLogic); + } + } + for (final p in portsToRemove) { + cellConns.remove(p); + cellPortDirs.remove(p); + } + } + + // -- Rename Seq/Comb ports to Namer wire names ----------------- + // The port names from _Always.addInput/addOutput are internal + // (e.g. `_out`, `_enable`). Replace them with the Namer's + // resolved wire name so they match SystemVerilog and WaveDumper. + if (sub is Combinational || sub is Sequential) { + final renames = {}; + for (final portName in cellConns.keys.toList()) { + final sl = + instance.inputMapping[portName] ?? + instance.outputMapping[portName] ?? + instance.inOutMapping[portName]; + if (sl == null) { + continue; // aggregated port, already renamed + } + final resolved = NetlistUtils.resolveReplacement(sl); + final namerName = NetlistUtils.tryGetSynthLogicName(resolved); + if (namerName != null && namerName != portName) { + renames[portName] = namerName; + } + } + for (final entry in renames.entries) { + final bits = cellConns.remove(entry.key)!; + final dir = cellPortDirs.remove(entry.key)!; + var newName = entry.value; + // Avoid collision with existing port names. + if (cellConns.containsKey(newName)) { + newName = '${entry.value}_${entry.key}'; + } + cellConns[newName] = bits; + cellPortDirs[newName] = dir; + } + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': mapped?.cellType ?? defaultCellType, + 'parameters': mapped?.parameters ?? {}, + 'attributes': {}, + 'port_directions': cellPortDirs, + 'connections': cellConns, + }; + } + } + + // -- Remove cells that were cleared by collapseAlwaysBlockPorts ------ + // Because the iteration order may process a Swizzle/BusSubset cell + // BEFORE the Combinational/Sequential that clears it, we need to purge + // stale cells after all collapsing has been applied. + if (synthDef != null) { + synthDef.subModuleInstantiations + .where((i) => !i.needsInstantiation) + .map((i) => emittedCellKeys[i]) + .whereType() + .forEach(cells.remove); + } + + // -- Wire-ID aliasing from remaining assignments ------------------- + // SynthModuleDefinition._collapseAssignments may leave assignments + // between non-mergeable SynthLogics (e.g., reserved port + + // renameable internal signal). In SV synthesis these become + // `assign` statements. In netlist we need the two sides to + // share wire IDs so that the netlist is properly connected. + // + // Similarly, PartialSynthAssignments for output struct ports tell + // us which leaf-field IDs should compose the port's bits, and + // input-struct BusSubsets (which may be pruned) tell us which + // leaf-field IDs should be carved from the port's bits. + final idAlias = {}; + + // Pending $struct_field cells collected during Step 3. + // Each entry records a single field extraction from a parent struct. + // The `parentLogic` and `fullParentIds` fields are used to group + // entries from the same LogicStructure into a single multi-port + // `$struct_unpack` cell. + final structFieldCells = + < + ({ + List parentIds, + List elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds, + }) + >[]; + + // Pending $struct_compose cells: for output struct ports, instead of + // aliasing port bits to leaf bits (which causes "shorting"), we + // collect composition operations and emit explicit cells later. + // Each entry records: field (src) → port sub-range [lower:upper]. + final structComposeCells = + < + ({ + List srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + }) + >[]; + + // Track struct ports (both output ports of the current module AND + // sub-module input struct ports) so Step 3 can skip $struct_field + // collection for them ($struct_pack handles these instead). + final outputStructPortLogics = {}; + + if (synthDef != null) { + // 1. Non-partial assignments: src drives dst → dst IDs become + // src IDs (the driver's IDs are canonical). + for (final assignment in synthDef.assignments.where( + (a) => a is! PartialSynthAssignment, + )) { + final srcIds = getIds(assignment.src); + final dstIds = getIds(assignment.dst); + final len = srcIds.length < dstIds.length + ? srcIds.length + : dstIds.length; + for (var i = 0; i < len; i++) { + if (dstIds[i] != srcIds[i]) { + idAlias[dstIds[i]] = srcIds[i]; + } + } + } + + // 2. Partial assignments (output / sub-module struct ports): + // src → dst[lower:upper]. The port-slice IDs become the + // leaf's IDs so that the port is composed from its fields. + // + // For struct ports (both output ports of the current module + // AND sub-module input struct ports), we keep distinct port + // and field IDs and instead collect pending $struct_pack + // cells. This avoids "shorting" where field wires are + // aliased directly to port bits, which creates multi-driver + // conflicts with $struct_unpack cells emitted in Step 3. + // + // For non-struct sub-module input ports, we alias as before. + + /// Recursively add [struct] and all its nested [LogicStructure] + /// descendants (excluding [LogicArray]) to [set]. + void addStructAndDescendants(LogicStructure struct, Set set) { + set.add(struct); + for (final elem in struct.elements) { + if (elem is LogicStructure && elem is! LogicArray) { + addStructAndDescendants(elem, set); + } + } + } + + for (final pa + in synthDef.assignments.whereType()) { + final srcIds = getIds(pa.src); + final dstIds = getIds(pa.dst); + + // Detect: is pa.dst an output struct port of the current module? + final isCurrentModuleOutputPort = + pa.dst.isPort(module) && pa.dst.logics.any((l) => l.isOutput); + + // Detect: is pa.dst a sub-module input struct port? + // (LogicStructure but not LogicArray, and not an output of the + // current module.) + final isSubModuleInputStructPort = + !isCurrentModuleOutputPort && + pa.dst.logics.any((l) => l is LogicStructure && l is! LogicArray); + + if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { + // Record as pending compose cell instead of aliasing. + structComposeCells.add(( + srcIds: srcIds, + dstIds: dstIds, + dstLowerIndex: pa.dstLowerIndex, + dstUpperIndex: pa.dstUpperIndex, + srcSynthLogic: pa.src, + dstSynthLogic: pa.dst, + )); + // Track the Logic (and nested structs) so Step 3 skips + // $struct_unpack for them. + for (final l in pa.dst.logics) { + if (l is LogicStructure && l is! LogicArray) { + addStructAndDescendants(l, outputStructPortLogics); + } + } + } else { + // Non-struct sub-module input port: alias as before. + for (var i = 0; i < srcIds.length; i++) { + final dstIdx = pa.dstLowerIndex + i; + if (dstIdx < dstIds.length && dstIds[dstIdx] != srcIds[i]) { + idAlias[dstIds[dstIdx]] = srcIds[i]; + } + } + } + } + + // 3. LogicStructure and LogicArray: child IDs → parent-slice IDs. + // + // LogicArray elements alias their IDs to matching parent bits + // so array connectivity works. + // + // Non-array LogicStructure elements are NOT aliased. Instead, + // their parent→element mappings are collected in + // [structFieldCells] and emitted as explicit $struct_field + // cells after alias resolution. This preserves element signals + // (e.g. "a_mantissa") as distinct named wires visible in the + // schematic, rather than collapsing them into parent bit ranges. + // + // For arrays with explicit $slice/$concat cells (from + // _BusSubsetForArraySlice / _SwizzleForArrayConcat), aliasing + // is skipped entirely — the cells provide the structural link. + // + // Applied to ALL instances (ports AND internal signals) since + // internal arrays/structs (e.g. constant-driven coefficients) + // also need child→parent aliasing. + // + // - LogicStructure (non-array): walks leafElements (recursive) + // - LogicArray: walks elements (direct children only, since + // each element is already a flat bitvector). + // For input array ports that have _BusSubsetForArraySlice + // cells, we skip aliasing so the $slice cells provide the + // structural connection (see _subsetReceiveArrayPort). + // + // When a child ID was already aliased (e.g. by step 1 to a + // constant driver), we also redirect that prior target to the + // parent ID so the transitive chain resolves correctly: + // constId → childId → parentId. + void aliasChildToParent(int childId, int parentId) { + if (childId == parentId) { + return; + } + // If childId already aliases somewhere (e.g. constId → childId + // was set in step 1 as childId → constId), redirect that old + // target to parentId as well, so constId → parentId. + final existing = idAlias[childId]; + if (existing != null && existing != parentId) { + idAlias[existing] = parentId; + } + idAlias[childId] = parentId; + } + + // Collect LogicArray ports that have explicit array_slice or + // array_concat submodules so we can skip aliasing them (the + // $slice/$concat cells provide the structural link). + final arraysWithExplicitCells = {}; + for (final inst in synthDef.subModuleInstantiations) { + if (inst.module is _BusSubsetForArraySlice) { + // The input of the BusSubset is the array port. + for (final inputSL in inst.inputMapping.values) { + final logic = synthDef.logicToSynthMap.entries + .where( + (e) => e.value == inputSL || e.value.replacement == inputSL, + ) + .map((e) => e.key) + .firstOrNull; + if (logic != null && logic is LogicArray) { + arraysWithExplicitCells.add(logic); + } + // Also check the resolved replacement chain. + final resolved = NetlistUtils.resolveReplacement(inputSL); + final logic2 = synthDef.logicToSynthMap.entries + .where((e) => e.value == resolved) + .map((e) => e.key) + .firstOrNull; + if (logic2 != null && logic2 is LogicArray) { + arraysWithExplicitCells.add(logic2); + } + } + } + if (inst.module is _SwizzleForArrayConcat) { + // The output of the Swizzle is the array signal. + for (final outputSL in inst.outputMapping.values) { + final logic = synthDef.logicToSynthMap.entries + .where( + (e) => e.value == outputSL || e.value.replacement == outputSL, + ) + .map((e) => e.key) + .firstOrNull; + if (logic != null && logic is LogicArray) { + arraysWithExplicitCells.add(logic); + } + } + } + } + + for (final entry in synthDef.logicToSynthMap.entries) { + final logic = entry.key; + if (logic is! LogicStructure) { + continue; + } + final parentSL = entry.value; + final parentIds = getIds(parentSL); + + if (logic is LogicArray) { + // Skip aliasing for arrays that have explicit $slice/$concat cells. + if (arraysWithExplicitCells.contains(logic)) { + continue; + } + // Array: alias each element's IDs to matching parent slice. + var idx = 0; + for (final element in logic.elements) { + final elemSL = synthDef.logicToSynthMap[element]; + if (elemSL != null) { + final elemIds = getIds(elemSL); + for ( + var i = 0; + i < elemIds.length && idx + i < parentIds.length; + i++ + ) { + aliasChildToParent(elemIds[i], parentIds[idx + i]); + } + } + idx += element.width; + } + } else { + // Struct: collect element→parent mappings for $struct_field + // cell emission instead of aliasing. This preserves named + // field signals as distinct wires connected through explicit + // cells, making them visible in the schematic and evaluable + // by the netlist evaluator. + // + // Skip output struct ports of the current module — those are + // handled by $struct_compose cells (from Step 2). + if (outputStructPortLogics.contains(logic)) { + continue; + } + var idx = 0; + for (final elem in logic.elements) { + final elemSL = synthDef.logicToSynthMap[elem]; + if (elemSL != null) { + final elemIds = getIds(elemSL); + final sliceLen = elemIds.length < parentIds.length - idx + ? elemIds.length + : parentIds.length - idx; + if (sliceLen > 0) { + structFieldCells.add(( + parentIds: parentIds.sublist(idx, idx + sliceLen), + elemIds: elemIds.sublist(0, sliceLen), + offset: idx, + width: sliceLen, + elemLogic: elem, + parentLogic: logic, + fullParentIds: parentIds, + )); + } + } else if (elem is LogicStructure && elem is! LogicArray) { + // Nested InterfaceStructure: the intermediate struct + // itself has no SynthLogic, but its leaf elements do + // (created by _subsetReceiveStructPort). Walk leaf + // elements and emit struct field entries for each, + // using the top-level parent as the parent Logic. + var leafIdx = idx; + for (final leaf in elem.leafElements) { + final leafSL = synthDef.logicToSynthMap[leaf]; + if (leafSL != null) { + final leafIds = getIds(leafSL); + final sliceLen = leafIds.length < parentIds.length - leafIdx + ? leafIds.length + : parentIds.length - leafIdx; + if (sliceLen > 0) { + structFieldCells.add(( + parentIds: parentIds.sublist(leafIdx, leafIdx + sliceLen), + elemIds: leafIds.sublist(0, sliceLen), + offset: leafIdx, + width: sliceLen, + elemLogic: leaf, + parentLogic: logic, + fullParentIds: parentIds, + )); + } + } + leafIdx += leaf.width; + } + } + idx += elem.width; + } + } + } + } + + // Transitively resolve an alias chain to its canonical ID. + // Uses a visited set to detect cycles created by conflicting + // child→parent and assignment aliasing directions. + int resolveAlias(int id) { + var resolved = id; + final visited = {}; + while (idAlias.containsKey(resolved)) { + if (!visited.add(resolved)) { + // Cycle detected — break the cycle by removing this entry. + idAlias.remove(resolved); + break; + } + resolved = idAlias[resolved]!; + } + return resolved; + } + + // Apply aliases to a list of bit IDs / string constants. + List applyAlias(List bits) => idAlias.isEmpty + ? bits + : bits.map((b) => b is int ? resolveAlias(b) : b).toList(); + + // -- Break shared wire IDs for array_slice cells ----------------------- + // (Populated inside the alias block below; declared here so netnames + // can reference it later.) + final arraySliceOldToNew = {}; + + // Alias port bits. + if (idAlias.isNotEmpty) { + for (final p in ports.values) { + p['bits'] = applyAlias((p['bits']! as List).cast()); + } + // Alias cell connections. + for (final c in cells.values) { + final conns = c['connections']! as Map; + for (final key in conns.keys.toList()) { + conns[key] = applyAlias((conns[key] as List).cast()); + } + } + + // After aliasing, the slice output Y bits share the same wire IDs + // as the corresponding sub-range of input A (because LogicArray + // elements share the parent's bit storage). This makes the slice + // trivial and it would be elided below. + // + // To preserve the structural decomposition in the schematic, we + // allocate fresh wire IDs for each array_slice Y output, then + // redirect all other cells that consume those IDs as inputs to + // read from the fresh IDs instead. The slice input A keeps the + // original parent-array IDs, so the data flow becomes: + // parent (original IDs) → slice A → slice Y (fresh IDs) → consumer + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith('array_slice')) { + continue; + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'output') { + continue; + } + final oldBits = (portEntry.value as List).cast(); + conns[portEntry.key] = [ + for (final b in oldBits) + b is int ? arraySliceOldToNew.putIfAbsent(b, () => nextId++) : b, + ]; + } + } + + // Redirect other cells: any input port bit that matches an old ID + // gets replaced with the corresponding fresh ID. + if (arraySliceOldToNew.isNotEmpty) { + for (final cellEntry in cells.entries) { + if (cellEntry.key.startsWith('array_slice')) { + continue; // skip the slice cells themselves + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'input') { + continue; + } + final bits = (portEntry.value as List).cast(); + final newBits = [ + for (final b in bits) b is int ? (arraySliceOldToNew[b] ?? b) : b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + + // -- Elide trivial $slice cells ---------------------------------- + // Also elide struct_slice cells (`_BusSubsetForStructSlice` + // instances from `_subsetReceiveStructPort`) because the new + // `$struct_unpack` cells emitted below supersede them with + // better-named field-level connections. + cells.removeWhere((cellKey, cell) { + if (cell['type'] != r'$slice') { + return false; + } + // Unconditionally remove struct_slice cells — they are + // duplicated by $struct_unpack cells which carry field names. + if (cellKey.startsWith('struct_slice')) { + return true; + } + final params = cell['parameters'] as Map?; + final offset = params?['OFFSET']; + if (offset is! int) { + return false; + } + final conns = cell['connections']! as Map; + final aBits = conns['A'] as List?; + final yBits = conns['Y'] as List?; + if (aBits == null || yBits == null) { + return false; + } + return yBits.indexed.every( + (e) => offset + e.$1 < aBits.length && e.$2 == aBits[offset + e.$1], + ); + }); + } + + // -- Emit $struct_unpack cells for LogicStructure elements ---------- + // Group per-field entries by their parent LogicStructure and emit a + // single multi-port cell per group. Each group has: + // • input port A: the full parent bus (packed bitvector) + // • one output port per non-trivial field: bits for that field + // This replaces the old per-field $struct_field cells. + if (synthDef != null && structFieldCells.isNotEmpty) { + // Group by parent Logic identity. + final groups = + < + Logic, + List< + ({ + List parentIds, + List elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds, + }) + > + >{}; + for (final sf in structFieldCells) { + (groups[sf.parentLogic] ??= []).add(sf); + } + + var suIdx = 0; + for (final entry in groups.entries) { + final parentLogic = entry.key; + final fields = entry.value; + final fullParentIds = fields.first.fullParentIds; + final resolvedParentBits = applyAlias(fullParentIds.cast()); + + // Filter out trivial fields (input slice == output after aliasing). + final nonTrivialFields = fields + .map((sf) { + final resolvedElemBits = applyAlias(sf.elemIds.cast()); + return ( + resolvedElemBits: resolvedElemBits, + offset: sf.offset, + width: sf.width, + elemLogic: sf.elemLogic, + ); + }) + .where( + (f) => !f.resolvedElemBits.indexed.every((e) { + final (i, bit) = e; + return f.offset + i < resolvedParentBits.length && + bit == resolvedParentBits[f.offset + i]; + }), + ) + .toList(); + + if (nonTrivialFields.isEmpty) { + continue; + } + + // Derive struct name for the cell key. + final structName = Sanitizer.sanitizeSV(parentLogic.name); + + // Build element range table for the parent struct so we can + // derive proper field names even when the leaf Logic objects + // have unpreferred names like `_swizzled`. + // Same strategy as $struct_pack: walk the hierarchy collecting + // (start, end, name, path, indexInParent) and look up the + // narrowest non-unpreferred range for each field offset. + final suElementRanges = + < + ({ + int start, + int end, + String name, + String path, + int indexInParent, + }) + >[]; + if (parentLogic is LogicStructure) { + void walkStruct( + LogicStructure struct, + int baseOffset, + String parentPath, + ) { + var offset = baseOffset; + for (var idx = 0; idx < struct.elements.length; idx++) { + final elem = struct.elements[idx]; + final elemEnd = offset + elem.width; + final elemPath = parentPath.isEmpty + ? elem.name + : '${parentPath}_${elem.name}'; + suElementRanges.add(( + start: offset, + end: elemEnd, + name: elem.name, + path: elemPath, + indexInParent: idx, + )); + if (elem is LogicStructure && elem is! LogicArray) { + walkStruct(elem, offset, elemPath); + } + offset = elemEnd; + } + } + + walkStruct(parentLogic, 0, ''); + } + + String suFieldNameFor(int fieldOffset, String fallbackName) { + ({int start, int end, String name, String path, int indexInParent})? + bestNamed; + ({int start, int end, String name, String path, int indexInParent})? + bestAny; + ({int start, int end, String name, String path, int indexInParent})? + narrowest; + + for (final r in suElementRanges) { + if (fieldOffset >= r.start && fieldOffset < r.end) { + final span = r.end - r.start; + if (narrowest == null || + span < (narrowest.end - narrowest.start)) { + narrowest = r; + } + if (bestAny == null || span < (bestAny.end - bestAny.start)) { + bestAny = r; + } + if (!Naming.isUnpreferred(r.name)) { + if (bestNamed == null || + span < (bestNamed.end - bestNamed.start)) { + bestNamed = r; + } + } + } + } + + if (bestNamed != null) { + if (narrowest != null && + (narrowest.end - narrowest.start) < + (bestNamed.end - bestNamed.start)) { + final bestNamedPrefix = bestNamed.path; + if (narrowest.path.length > bestNamedPrefix.length && + narrowest.path.startsWith(bestNamedPrefix)) { + final suffix = narrowest.path.substring( + bestNamedPrefix.length + 1, + ); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + // All matching elements have unpreferred names — use the + // narrowest element's positional index as discriminator. + if (narrowest != null && Naming.isUnpreferred(narrowest.name)) { + return 'anonymous_${narrowest.indexInParent}'; + } + return bestAny?.name ?? fallbackName; + } + + // Build port_directions and connections with one output per field. + final portDirs = {'A': 'input'}; + final conns = >{'A': resolvedParentBits}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = suFieldNameFor(f.offset, f.elemLogic.name); + // Disambiguate duplicate field names with index suffix. + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = 'output'; + conns[portName] = f.resolvedElemBits; + } + + // Parameters list field metadata for the schematic viewer. + final params = { + 'STRUCT_NAME': parentLogic.name, + 'FIELD_COUNT': nonTrivialFields.length, + }; + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + params['FIELD_${i}_NAME'] = suFieldNameFor( + f.offset, + f.elemLogic.name, + ); + params['FIELD_${i}_OFFSET'] = f.offset; + params['FIELD_${i}_WIDTH'] = f.width; + } + + cells['struct_unpack_${suIdx}_$structName'] = { + 'hide_name': 0, + 'type': r'$struct_unpack', + 'parameters': params, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': conns, + }; + suIdx++; + } + } + + // -- Emit $struct_pack cells for output struct ports ------------------ + // Group compose entries by destination port and emit a single + // multi-port cell per group. Each group has: + // • one input port per non-trivial field + // • output port Y: the full packed output bus + // This replaces the old per-field $struct_compose cells. + if (structComposeCells.isNotEmpty) { + // Group by destination SynthLogic identity. + final composeGroups = + < + SynthLogic, + List< + ({ + List srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + }) + > + >{}; + for (final sc in structComposeCells) { + (composeGroups[sc.dstSynthLogic] ??= []).add(sc); + } + + var spIdx = 0; + for (final entry in composeGroups.entries) { + final dstSynthLogic = entry.key; + final fields = entry.value; + final resolvedDstBits = applyAlias(fields.first.dstIds.cast()); + + // Filter out trivial fields. + final nonTrivialFields = fields + .map((sc) { + final resolvedSrcBits = applyAlias(sc.srcIds.cast()); + final yBits = resolvedDstBits.sublist( + sc.dstLowerIndex, + sc.dstUpperIndex + 1, + ); + return ( + resolvedSrcBits: resolvedSrcBits, + yBits: yBits, + dstLowerIndex: sc.dstLowerIndex, + dstUpperIndex: sc.dstUpperIndex, + srcSynthLogic: sc.srcSynthLogic, + ); + }) + .where( + (f) => !f.resolvedSrcBits + .take(f.yBits.length) + .indexed + .every((e) => e.$2 == f.yBits[e.$1]), + ) + .toList(); + + if (nonTrivialFields.isEmpty) { + continue; + } + + // Derive struct name from the destination Logic. + final dstLogic = dstSynthLogic.logics.firstOrNull; + final structName = dstLogic != null + ? Sanitizer.sanitizeSV(dstLogic.name) + : 'struct_$spIdx'; + + // Build a lookup from bit offset to the best struct element + // name, so that field names come from the struct definition + // (e.g. "data", "last", "poison") rather than the source + // signal name (which may be an internal like "_swizzled"). + // + // Elements pack LSB-first via `rswizzle`, so element[0] + // starts at offset 0, element[1] at element[0].width, etc. + // + // We collect (start, end, name, path, parentElementIndex) + // ranges for every element at every nesting level. The + // `path` carries the chain of parent struct names so we can + // produce qualified names like "mmu_info_mmuSid". When + // leaf names are unpreferred, `parentElementIndex` provides + // a fallback discriminator like "mmu_info_0". + final dstElementRanges = + < + ({ + int start, + int end, + String name, + String path, + int indexInParent, + }) + >[]; + if (dstLogic is LogicStructure) { + void walkStruct( + LogicStructure struct, + int baseOffset, + String parentPath, + ) { + var offset = baseOffset; + for (var idx = 0; idx < struct.elements.length; idx++) { + final elem = struct.elements[idx]; + final elemEnd = offset + elem.width; + final elemPath = parentPath.isEmpty + ? elem.name + : '${parentPath}_${elem.name}'; + dstElementRanges.add(( + start: offset, + end: elemEnd, + name: elem.name, + path: elemPath, + indexInParent: idx, + )); + if (elem is LogicStructure && elem is! LogicArray) { + walkStruct(elem, offset, elemPath); + } + offset = elemEnd; + } + } + + walkStruct(dstLogic, 0, ''); + } + + /// Look up the field name for a compose entry by finding the + /// best struct element whose range contains [dstLowerIndex]. + /// + /// Strategy (deepest-first): + /// 1. Find the narrowest element with a non-unpreferred name. + /// 2. If a narrower unpreferred leaf exists under a named + /// parent, try to qualify with the leaf's proper name + /// (e.g. `mmu_info_mmuSid`). + /// 3. If the leaf name is also unpreferred, fall back to the + /// parent name qualified by the leaf's positional index + /// (e.g. `mmu_info_0`, `mmu_info_1`). + /// 4. Falls back to the resolved source SynthLogic name. + String fieldNameFor(int dstLowerIndex, SynthLogic srcSynthLogic) { + ({int start, int end, String name, String path, int indexInParent})? + bestNamed; + ({int start, int end, String name, String path, int indexInParent})? + bestAny; + ({int start, int end, String name, String path, int indexInParent})? + narrowest; + + for (final r in dstElementRanges) { + if (dstLowerIndex >= r.start && dstLowerIndex < r.end) { + final span = r.end - r.start; + if (narrowest == null || + span < (narrowest.end - narrowest.start)) { + narrowest = r; + } + if (bestAny == null || span < (bestAny.end - bestAny.start)) { + bestAny = r; + } + if (!Naming.isUnpreferred(r.name)) { + if (bestNamed == null || + span < (bestNamed.end - bestNamed.start)) { + bestNamed = r; + } + } + } + } + + if (bestNamed != null) { + // Check if there's a narrower child element under + // bestNamed that we can use to discriminate. + if (narrowest != null && + (narrowest.end - narrowest.start) < + (bestNamed.end - bestNamed.start)) { + final bestNamedPrefix = bestNamed.path; + // Try using the child's proper name as qualifier. + if (narrowest.path.length > bestNamedPrefix.length && + narrowest.path.startsWith(bestNamedPrefix)) { + final suffix = narrowest.path.substring( + bestNamedPrefix.length + 1, + ); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + // Child has unpreferred name — use positional index. + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + return bestAny?.name ?? + NetlistUtils.resolveReplacement(srcSynthLogic).name; + } + + // Build port_directions and connections. + final portDirs = {}; + final conns = >{}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = fieldNameFor(f.dstLowerIndex, f.srcSynthLogic); + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = 'input'; + conns[portName] = f.resolvedSrcBits; + } + + // Output port Y: full destination bus. + portDirs['Y'] = 'output'; + conns['Y'] = resolvedDstBits; + + // Parameters list field metadata for the schematic viewer. + final params = { + 'STRUCT_NAME': dstLogic?.name ?? 'struct', + 'FIELD_COUNT': nonTrivialFields.length, + }; + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + params['FIELD_${i}_NAME'] = fieldNameFor( + f.dstLowerIndex, + f.srcSynthLogic, + ); + params['FIELD_${i}_OFFSET'] = f.dstLowerIndex; + params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; + } + + cells['struct_pack_${spIdx}_$structName'] = { + 'hide_name': 0, + 'type': r'$struct_pack', + 'parameters': params, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': conns, + }; + spIdx++; + } + } + + // -- Passthrough buffer insertion ------------------------------------ + // When a signal passes directly from an input port to an output port, + // they share the same wire IDs after aliasing. This causes the signal + // to appear routed *around* the module in the netlist rather than + // *through* it. Insert a `$buf` cell to break the wire-ID sharing, + // giving the output port fresh IDs driven by the buffer. + { + final inputBitIds = ports.values + .where((p) => p['direction'] == 'input' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType() + .toSet(); + + // Check each output port for overlap with input bits. + var bufIdx = 0; + for (final p in ports.entries.where( + (p) => p.value['direction'] == 'output', + )) { + final outBits = (p.value['bits']! as List).cast(); + if (!outBits.any((b) => b is int && inputBitIds.contains(b))) { + continue; + } + + // Allocate fresh wire IDs for the output side of the buffer. + final freshBits = List.generate( + outBits.length, + (_) => nextId++, + ); + + // Insert a $buf cell: input = original (shared) IDs, + // output = fresh IDs. + cells['passthrough_buf_$bufIdx'] = NetlistUtils.makeBufCell( + outBits.length, + outBits, + freshBits, + ); + + // Update the output port to use the fresh IDs. + p.value['bits'] = freshBits; + bufIdx++; + } + } + + // -- Dead-cell elimination (DCE) ------------------------------------- + // After aliasing and elision, some cells may have inputs whose wire + // IDs are not driven by any cell output or module input port. This + // typically happens when a LogicStructure's `packed` representation + // creates a Swizzle chain whose inputs reference sub-module-internal + // signals that are not accessible from the synthesised module's + // scope. Iteratively remove such dead cells using both forward + // (all-inputs-undriven) and backward (all-outputs-unconsumed) DCE. + if (options.enableDCE) { + var dceChanged = true; + while (dceChanged) { + dceChanged = false; + + // Build set of driven wire IDs (from input/inout ports and cell + // outputs). + final drivenIds = { + ...ports.values + .where( + (p) => p['direction'] == 'input' || p['direction'] == 'inout', + ) + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + // Build set of consumed wire IDs (from output/inout ports and + // cell inputs). + final consumedIds = { + ...ports.values + .where( + (p) => p['direction'] == 'output' || p['direction'] == 'inout', + ) + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + // Forward DCE: remove cells whose inputs are ALL undriven. + cells + ..removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + final inputPorts = conns.entries.where( + (pe) => pdirs[pe.key] == 'input', + ); + if (inputPorts.isEmpty) { + return false; + } + final allUndriven = !inputPorts + .expand((pe) => pe.value as List) + .any((b) => (b is int && drivenIds.contains(b)) || b is String); + if (allUndriven) { + dceChanged = true; + return true; + } + return false; + }) + // Backward DCE: remove cells whose outputs are ALL unconsumed. + // Preserve non-leaf cells (user module instances) — their type + // does not start with '$' (Yosys primitive convention). Users + // expect to see all instantiated modules in the schematic even + // when outputs are unconnected. + ..removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + final cellType = cell['type'] as String? ?? ''; + if (!cellType.startsWith(r'$')) { + return false; + } + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + final outputPorts = conns.entries.where( + (pe) => pdirs[pe.key] == 'output', + ); + if (outputPorts.isEmpty) { + return false; + } + final allUnconsumed = !outputPorts + .expand((pe) => pe.value as List) + .whereType() + .any(consumedIds.contains); + if (allUnconsumed) { + dceChanged = true; + return true; + } + return false; + }); + } + } + + // -- Constant driver cells ------------------------------------------- + // Generated AFTER the aliasing pass so that constants discovered + // during aliasing (via getIds(assignment.src)) are included. + // Constant IDs may have been redirected by step 3 (struct/array + // child→parent aliasing), so apply alias resolution to their + // connection bits. + { + var constIdx = 0; + final emittedConstWires = {}; + for (final entry + in synthLogicIds.entries + .where((e) => e.key.isConstant) + .where((e) => !blockedConstSynthLogics.contains(e.key)) + .where((e) => e.value.isNotEmpty)) { + final sl = entry.key; + final constValue = NetlistUtils.constValueFromSynthLogic(sl); + if (constValue == null) { + continue; + } + final ids = entry.value; + + // Resolve aliases and skip if these wires are already driven + // by a previously emitted $const cell (can happen when aliasing + // merges two SynthLogic constants onto the same wire IDs). + final resolvedIds = applyAlias(ids.cast()); + final firstWire = resolvedIds.firstWhere( + (b) => b is int, + orElse: () => -1, + ); + if (firstWire is int && firstWire >= 0) { + if (emittedConstWires.contains(firstWire)) { + continue; + } + emittedConstWires.addAll(resolvedIds.whereType()); + } + + final valuePart = NetlistUtils.constValuePart(constValue); + final cellName = 'const_${constIdx}_$valuePart'; + final valueLiteral = valuePart.replaceFirst('_', "'"); + + cells[cellName] = { + 'hide_name': 0, + 'type': r'$const', + 'parameters': {}, + 'attributes': {}, + 'port_directions': {valueLiteral: 'output'}, + 'connections': >{valueLiteral: resolvedIds}, + }; + constIdx++; + } + } + + // -- Remove floating $const cells ------------------------------------ + // The $const cells were emitted after the main DCE pass, so they + // may reference wire IDs that no cell input or output port consumes. + if (options.enableDCE) { + final consumedByInputs = { + ...ports.values + .where( + (p) => p['direction'] == 'output' || p['direction'] == 'inout', + ) + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + cells.removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + if (cell['type'] != r'$const') { + return false; + } + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return !conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType() + .any(consumedByInputs.contains); + }); + } + + // -- Break shared wire IDs for array_concat cells -------------------- + // After aliasing, the concat inputs share the same wire IDs as the + // concat Y output (because LogicArray elements share the parent's + // bit storage). This makes the concat transparent -- constants + // appear to drive the parent array directly. + // + // To fix: allocate fresh wire IDs for each concat input port, + // then redirect all other cells whose outputs used those old IDs + // to drive the fresh IDs instead. The concat Y output keeps the + // original parent-array IDs, so the data flow becomes: + // const → fresh_IDs → concat input → concat Y (= parent IDs) + final arrayConcatOldToNew = {}; + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith('array_concat')) { + continue; + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'input') { + continue; + } + final oldBits = (portEntry.value as List).cast(); + conns[portEntry.key] = [ + for (final b in oldBits) + b is int ? arrayConcatOldToNew.putIfAbsent(b, () => nextId++) : b, + ]; + } + } + + // Redirect other cells: any output port bit that matches an old ID + // gets replaced with the corresponding fresh ID. + if (arrayConcatOldToNew.isNotEmpty) { + for (final cellEntry in cells.entries) { + if (cellEntry.key.startsWith('array_concat')) { + continue; // skip the concat cells themselves + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'output') { + continue; + } + final bits = (portEntry.value as List).cast(); + final newBits = [ + for (final b in bits) b is int ? (arrayConcatOldToNew[b] ?? b) : b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + + // -- Netnames -------------------------------------------------------- + final netnames = {}; + final emittedNames = {}; + + // InlineSystemVerilog modules are pure combinational — all their + // signals are derivable from the gate netlist. + final isInlineSV = module is InlineSystemVerilog; + + void addNetname( + String name, + List bits, { + bool hideName = false, + bool computed = false, + Map? logicType, + }) { + if (emittedNames.contains(name)) { + return; + } + emittedNames.add(name); + netnames[name] = { + 'bits': bits, + if (hideName) 'hide_name': 1, + if (logicType != null) 'logic_type': logicType, + 'attributes': { + if (computed || isInlineSV) 'computed': 1, + }, + }; + } + + // Port nets (already aliased above). + for (final p in ports.entries) { + addNetname( + Sanitizer.sanitizeSV(p.key), + (p.value['bits']! as List).cast(), + logicType: p.value['logic_type'] as Map?, + ); + } + + // Named signals from SynthModuleDefinition. + if (synthDef != null) { + for (final entry in synthLogicIds.entries.where( + (e) => !e.key.isConstant && !e.key.declarationCleared, + )) { + final sl = entry.key; + final name = NetlistUtils.tryGetSynthLogicName(sl); + if (name != null) { + var bits = applyAlias(entry.value.cast()); + // For element signals whose IDs were remapped by the + // array_slice fresh-ID pass, apply that mapping so the + // element netname matches the slice output (fresh) IDs. + if (arraySliceOldToNew.isNotEmpty && sl is SynthLogicArrayElement) { + bits = bits + .map((b) => b is int ? (arraySliceOldToNew[b] ?? b) : b) + .toList(); + } + // For element signals whose IDs were remapped by the + // array_concat fresh-ID pass, apply that mapping so the + // element netname matches the concat input (fresh) IDs. + if (arrayConcatOldToNew.isNotEmpty && sl is SynthLogicArrayElement) { + bits = bits + .map((b) => b is int ? (arrayConcatOldToNew[b] ?? b) : b) + .toList(); + } + final typeLogic = NetlistUtils.typeLogicFromSynthLogic(sl); + addNetname( + Sanitizer.sanitizeSV(name), + bits, + logicType: typeLogic != null + ? NetlistUtils.buildLogicType(typeLogic, bits) + : null, + ); + } + } + } + + // Constant netnames for non-blocked constants (already aliased via + // cell connections above). + for (final cellEntry in cells.entries.where( + (e) => e.value['type'] == r'$const', + )) { + final conns = + cellEntry.value['connections'] as Map>?; + if (conns != null && conns.isNotEmpty) { + addNetname(cellEntry.key, conns.values.first, computed: true); + } + } + + // -- Ensure every bit ID in cell connections has a netname ------------ + { + final coveredIds = netnames.values + .expand( + (nn) => ((nn! as Map)['bits'] as List?) ?? [], + ) + .whereType() + .toSet(); + + for (final cellEntry in cells.entries) { + final cellName = cellEntry.key; + final conns = + cellEntry.value['connections'] as Map? ?? {}; + for (final connEntry in conns.entries) { + final portName = connEntry.key; + final bits = connEntry.value as List; + final missingBits = []; + for (final b in bits) { + if (b is int && !coveredIds.contains(b)) { + missingBits.add(b); + coveredIds.add(b); + } + } + if (missingBits.isNotEmpty) { + addNetname( + Sanitizer.sanitizeSV('${cellName}_$portName'), + missingBits, + hideName: true, + ); + } + } + } + } + + // -- Remove orphaned netnames after DCE -------------------------------- + // DCE may remove cells that drove certain named signals. Remove + // netnames whose integer bits are ALL undriven (not output of any + // remaining cell, not a module input/inout port bit). This prevents + // dead signals from appearing in the schematic viewer. + if (options.enableDCE) { + final drivenBits = { + ...ports.values + .where( + (p) => p['direction'] == 'input' || p['direction'] == 'inout', + ) + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = + cell['connections'] as Map? ?? const {}; + final pdirs = + cell['port_directions'] as Map? ?? const {}; + return conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + netnames.removeWhere((name, nnRaw) { + final nn = nnRaw as Map?; + if (nn == null) { + return false; + } + final bits = nn['bits'] as List?; + if (bits == null) { + return false; + } + final intBits = bits.whereType(); + if (intBits.isEmpty) { + return false; + } + return !intBits.any(drivenBits.contains); + }); + } + + // -- Slim: strip cell connections ------------------------------------ + // The full pipeline ran identically, so the cell set (keys, ordering) + // is canonical. Now drop the connection maps to reduce the output + // size. This is the ONLY difference between slim and full output. + if (options.slimMode) { + for (final cell in cells.values) { + cell.remove('connections'); + } + } + + // -- Structural validation ------------------------------------------- + // Debug-mode checks to catch netlist bugs early. These verify that + // every signal has a driver and every cell is connected. + assert(() { + _validateNetlist(ports, cells, netnames, module.name); + return true; + }(), 'Netlist structural validation failed for ${module.name}'); + + return NetlistSynthesisResult( + module, + getInstanceTypeOfModule, + ports: ports, + cells: cells, + netnames: netnames, + attributes: attr, + ); + } + + /// Validates structural integrity of the netlist. + /// + /// Checks: + /// 1. No non-transparent cell is fully disconnected — every logic gate + /// must have at least one output bit consumed by another cell's + /// input or a module output/inout port. + /// 2. No `$const` cell drives wire IDs that nothing consumes. + /// + /// These fire only under `assert()` (i.e. `--enable-asserts`) so they + /// don't affect production runs. + static void _validateNetlist( + Map> ports, + Map> cells, + Map netnames, + String moduleName, + ) { + // Collect all wire IDs consumed by cell input ports or module + // output/inout ports. + final consumedBits = { + ...ports.values + .where((p) => p['direction'] == 'output' || p['direction'] == 'inout') + .expand((p) => (p['bits'] as List?) ?? []) + .whereType(), + ...cells.values.expand((c) { + final conns = c['connections'] as Map? ?? {}; + final pdirs = c['port_directions'] as Map? ?? {}; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => (pe.value as List?) ?? []) + .whereType(); + }), + }; + + const transparentTypes = { + r'$buf', + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + // Check 1: no logic gate is fully disconnected. + for (final entry in cells.entries) { + final cell = entry.value; + final type = cell['type'] as String? ?? ''; + if (transparentTypes.contains(type) || type == r'$const') { + continue; + } + + final conns = cell['connections'] as Map?; + final pdirs = cell['port_directions'] as Map?; + if (conns == null || pdirs == null) { + continue; + } + + final outputBits = conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => (pe.value as List?) ?? []) + .whereType() + .toList(); + + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + // ignore: avoid_print + print( + '[netlist-validate] WARNING: $moduleName: ' + 'cell "${entry.key}" (type: $type) has no consumed outputs ' + '— fully disconnected logic gate', + ); + } + } + + // Check 2: no $const cell drives unconsumed wires. + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$const') { + continue; + } + + final conns = cell['connections'] as Map?; + final pdirs = cell['port_directions'] as Map?; + if (conns == null || pdirs == null) { + continue; + } + + final outputBits = conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => (pe.value as List?) ?? []) + .whereType() + .toList(); + + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + // ignore: avoid_print + print( + '[netlist-validate] WARNING: $moduleName: ' + r'$const cell "${entry.key}" drives wires consumed by nothing ' + '— floating constant', + ); + } + } + } + + /// Apply all post-processing passes to the modules map. + /// + /// This is the canonical pass ordering used by both netlist flows: + /// **Flow 1** (slim batch via `_synthesizeSlimModules`) and + /// **Flow 2** (incremental full via `moduleNetlistJson`). + /// Also used internally by [buildModulesMap] / [synthesizeToJson]. + void applyPostProcessingPasses(Map> modules) { + if (options.collapseTransparentClusters) { + NetlistPasses.applyTransparentClustering(modules); + } + } + + /// Build the processed modules map from a [SynthBuilder]'s results. + /// + /// Returns the intermediate module map (definition name → module data) + /// after all post-processing passes have been applied. This allows + /// callers to retain per-module results for incremental serving while + /// avoiding redundant re-synthesis. + Map> buildModulesMap( + SynthBuilder synth, + Module top, + ) { + final swEntries = Stopwatch()..start(); + final modules = NetlistPasses.collectModuleEntries( + synth.synthesisResults, + topModule: top, + ); + swEntries.stop(); + + final swPasses = Stopwatch()..start(); + applyPostProcessingPasses(modules); + swPasses.stop(); + + return modules; + } + + /// Generate the combined netlist JSON from a [SynthBuilder]'s results. + String generateCombinedJson(SynthBuilder synth, Module top) { + final swCollect = Stopwatch()..start(); + final modules = buildModulesMap(synth, top); + swCollect.stop(); + + final swCompress = Stopwatch()..start(); + if (options.compressBitRanges) { + _compressModulesMap(modules); + } + swCompress.stop(); + + final combined = { + 'creator': 'NetlistSynthesizer (rohd)', + 'version': netlistFormatVersion, + 'modules': modules, + }; + + final swEncode = Stopwatch()..start(); + final encoder = options.compactJson + ? const JsonEncoder() + : const JsonEncoder.withIndent(' '); + final result = encoder.convert(combined); + swEncode.stop(); + + return result; + } + + /// Compresses a list of bit IDs by replacing contiguous ascending runs of + /// 3 or more integers with `"start:end"` range strings. + static List _compressBits(List bits) { + final result = []; + final pending = []; + + void flushPending() { + if (pending.isEmpty) { + return; + } + var i = 0; + while (i < pending.length) { + var j = i; + while (j + 1 < pending.length && pending[j + 1] == pending[j] + 1) { + j++; + } + final runLen = j - i + 1; + if (runLen >= 3) { + result.add('${pending[i]}:${pending[j]}'); + } else { + for (var k = i; k <= j; k++) { + result.add(pending[k]); + } + } + i = j + 1; + } + pending.clear(); + } + + for (final element in bits) { + if (element is int) { + pending.add(element); + } else { + flushPending(); + result.add(element); + } + } + flushPending(); + return result; + } + + /// Applies [_compressBits] to all `bits` arrays and cell `connections` + /// arrays in a modules map. + static void _compressModulesMap(Map> modules) { + for (final moduleDef in modules.values) { + final ports = moduleDef['ports'] as Map>?; + if (ports != null) { + for (final port in ports.values) { + final bits = port['bits']; + if (bits is List) { + port['bits'] = _compressBits(bits.cast()); + } + } + } + + final cells = moduleDef['cells'] as Map>?; + if (cells != null) { + for (final cell in cells.values) { + final conns = cell['connections'] as Map>?; + if (conns != null) { + for (final key in conns.keys.toList()) { + conns[key] = _compressBits(conns[key]!); + } + } + } + } + + final netnames = moduleDef['netnames'] as Map?; + if (netnames != null) { + for (final entry in netnames.values) { + if (entry is Map) { + final bits = entry['bits']; + if (bits is List) { + entry['bits'] = _compressBits(bits.cast()); + } + } + } + } + } + } + + /// Convenience: synthesize [top] into a combined netlist JSON string. + /// + /// Builds a [SynthBuilder] internally and returns the full JSON. + /// + /// The [packageRoot] parameter is accepted for API compatibility with + /// downstream trace-enabled branches. + String synthesizeToJson(Module top, {String? packageRoot}) { + final sb = SynthBuilder(top, this); + return generateCombinedJson(sb, top); + } +} + +/// A version of [BusSubset] that creates explicit `$slice` cells for +/// [LogicArray] element extraction in the netlist. +/// +/// When a [LogicArray] port is decomposed into its elements, each element +/// gets its own [_BusSubsetForArraySlice] so the netlist shows explicit +/// select gates rather than flat bit aliasing. +class _BusSubsetForArraySlice extends BusSubset { + _BusSubsetForArraySlice(super.bus, super.startIndex, super.endIndex) + : super(name: 'array_slice'); + + @override + bool get hasBuilt => true; +} + +/// A version of [Swizzle] that creates explicit `$concat` cells for +/// [LogicArray] element assembly in the netlist. +/// +/// When a [LogicArray]'s elements are driven independently (e.g. by +/// constants), this creates a visible concat gate in the netlist that +/// assembles the element signals into the full packed array bus. +class _SwizzleForArrayConcat extends Swizzle { + _SwizzleForArrayConcat(super.signals) : super(name: 'array_concat'); + + @override + bool get hasBuilt => true; +} diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart new file mode 100644 index 000000000..3c90ccba5 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -0,0 +1,470 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_utils.dart +// Shared utility functions for netlist synthesis and post-processing passes. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Shared utility functions for netlist synthesis and post-processing passes. +/// +/// All methods are static — no instances are created. +class NetlistUtils { + NetlistUtils._(); + + /// Find the port name in [portMap] that corresponds to [sl]. + static String? portNameForSynthLogic( + SynthLogic sl, + Map portMap, + ) { + for (final e in portMap.entries) { + if (sl.logics.contains(e.value)) { + return e.key; + } + } + return null; + } + + /// Safely retrieve the name from a [SynthLogic], returning null if + /// retrieval fails (e.g. name not yet picked, or the SynthLogic has + /// been replaced). + static String? tryGetSynthLogicName(SynthLogic sl) { + try { + return sl.name; + // ignore: avoid_catches_without_on_clauses + } catch (_) { + return null; + } + } + + /// Resolves [sl] to the end of its replacement chain. + static SynthLogic resolveReplacement(SynthLogic sl) { + var r = sl; + while (r.replacement != null) { + r = r.replacement!; + } + return r; + } + + /// Create a `$buf` cell map. + static Map makeBufCell( + int width, + List aBits, + List yBits, + ) => { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': width}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{'A': aBits, 'Y': yBits}, + }; + + /// Collapses bit-slice ports of a Combinational/Sequential cell into + /// aggregate ports. + /// + /// **Input side**: When a Combinational references individual struct fields, + /// each field creates a BusSubset in the parent scope, and each slice + /// becomes a separate input port. This method detects groups of input + /// ports whose SynthLogics are outputs of BusSubset submodule + /// instantiations that slice the same root signal. For each group + /// forming a contiguous bit range, the N individual ports are replaced + /// with a single aggregate port connected to the corresponding sub-range + /// of the root signal's wire IDs. + /// + /// **Output side**: Similarly, Combinational output ports that feed into + /// the inputs of the same Swizzle submodule are collapsed into a single + /// aggregate port connected to the Swizzle's output wire IDs. + static void collapseAlwaysBlockPorts( + SynthModuleDefinition synthDef, + SynthSubModuleInstantiation instance, + Map portDirs, + Map> connections, + List Function(SynthLogic) getIds, + ) { + // ── Input-side collapsing (BusSubset → Combinational) ────────────── + + // Build reverse lookup: resolved BusSubset output SynthLogic → + // (BusSubset module, resolved root input SynthLogic, + // SynthSubModuleInstantiation). + final busSubsetLookup = + {}; + for (final bsInst in synthDef.subModuleInstantiations) { + if (bsInst.module is! BusSubset) { + continue; + } + final bsMod = bsInst.module as BusSubset; + + // BusSubset has input 'original' and output 'subset' + final outputSL = bsInst.outputMapping.values.firstOrNull; + final inputSL = bsInst.inputMapping.values.firstOrNull; + if (outputSL == null || inputSL == null) { + continue; + } + + final resolvedOutput = resolveReplacement(outputSL); + final resolvedInput = resolveReplacement(inputSL); + + busSubsetLookup[resolvedOutput] = (bsMod, resolvedInput, bsInst); + } + + // Group input ports by root signal, also tracking the BusSubset + // instantiations that produced each port. + final inputGroups = + < + SynthLogic, + List< + ( + String portName, + int startIdx, + int width, + SynthSubModuleInstantiation bsInst, + ) + > + >{}; + + for (final e in instance.inputMapping.entries) { + final portName = e.key; + if (!connections.containsKey(portName)) { + continue; // already filtered + } + + final resolved = resolveReplacement(e.value); + final info = busSubsetLookup[resolved]; + if (info != null) { + final (bsMod, rootSL, bsInst) = info; + final width = bsMod.endIndex - bsMod.startIndex + 1; + inputGroups.putIfAbsent(rootSL, () => []).add(( + portName, + bsMod.startIndex, + width, + bsInst, + )); + } + } + + // Collapse each group with > 1 contiguous member. + for (final entry in inputGroups.entries) { + if (entry.value.length <= 1) { + continue; + } + + final rootSL = entry.key; + final ports = entry.value..sort((a, b) => a.$2.compareTo(b.$2)); + + // Verify contiguous non-overlapping coverage. + var expectedBit = ports.first.$2; + var contiguous = true; + for (final (_, startIdx, width, _) in ports) { + if (startIdx != expectedBit) { + contiguous = false; + break; + } + expectedBit += width; + } + if (!contiguous) { + continue; + } + + final minBit = ports.first.$2; + final maxBit = ports.last.$2 + ports.last.$3 - 1; + + // Get the root signal's full wire IDs and extract the sub-range. + final rootIds = getIds(rootSL); + if (maxBit >= rootIds.length) { + continue; // safety check + } + final aggBits = rootIds.sublist(minBit, maxBit + 1).cast(); + + // Choose a name for the aggregate port. + final rootName = tryGetSynthLogicName(rootSL) ?? 'agg_${minBit}_$maxBit'; + + // Replace individual ports with the aggregate. The bypassed + // BusSubset cells are left in place; the post-synthesis DCE pass + // will remove them if their outputs are no longer consumed. + for (final (portName, _, _, _) in ports) { + connections.remove(portName); + portDirs.remove(portName); + } + connections[rootName] = aggBits; + portDirs[rootName] = 'input'; + } + + // ── Output-side collapsing (Combinational → Swizzle) ─────────────── + + // Build reverse lookup: resolved Swizzle input SynthLogic → + // (Swizzle port name, bit offset within the Swizzle output, + // port width, resolved Swizzle output SynthLogic, + // SynthSubModuleInstantiation). + final swizzleLookup = + < + SynthLogic, + ( + String portName, + int offset, + int width, + SynthLogic, + SynthSubModuleInstantiation, + ) + >{}; + for (final szInst in synthDef.subModuleInstantiations) { + if (szInst.module is! Swizzle) { + continue; + } + final outputSL = szInst.outputMapping.values.firstOrNull; + if (outputSL == null) { + continue; + } + final resolvedOutput = resolveReplacement(outputSL); + + // Swizzle inputs are in0, in1, ... with bit-0 first. + var offset = 0; + for (final inEntry in szInst.inputMapping.entries) { + final resolvedInput = resolveReplacement(inEntry.value); + final w = resolvedInput.width; + swizzleLookup[resolvedInput] = ( + inEntry.key, + offset, + w, + resolvedOutput, + szInst, + ); + offset += w; + } + } + + // Group output ports by Swizzle output signal. + final outputGroups = + < + SynthLogic, + List< + ( + String portName, + int offset, + int width, + SynthSubModuleInstantiation szInst, + ) + > + >{}; + + for (final e in instance.outputMapping.entries) { + final portName = e.key; + if (!connections.containsKey(portName)) { + continue; + } + + final resolved = resolveReplacement(e.value); + final info = swizzleLookup[resolved]; + if (info != null) { + final (_, offset, width, swizzleOutputSL, szInst) = info; + outputGroups.putIfAbsent(swizzleOutputSL, () => []).add(( + portName, + offset, + width, + szInst, + )); + } + } + + // Collapse each group with > 1 contiguous member. + for (final entry in outputGroups.entries) { + if (entry.value.length <= 1) { + continue; + } + + // Skip collapsing when any member's SynthLogic is a port of the + // parent module. Collapsing replaces the individual output ports + // with a single aggregate that uses the downstream Swizzle's bit + // IDs, which would orphan the module-level port bits (they would + // no longer be driven by any cell). + final parentModule = synthDef.module; + final hasModulePort = entry.value.any((member) { + final sl = instance.outputMapping[member.$1]; + if (sl == null) { + return false; + } + final resolved = resolveReplacement(sl); + return resolved.isPort(parentModule); + }); + if (hasModulePort) { + continue; + } + + final swizOutSL = entry.key; + final ports = entry.value..sort((a, b) => a.$2.compareTo(b.$2)); + + // Verify contiguous. + var expectedBit = ports.first.$2; + var contiguous = true; + for (final (_, offset, width, _) in ports) { + if (offset != expectedBit) { + contiguous = false; + break; + } + expectedBit += width; + } + if (!contiguous) { + continue; + } + + final minBit = ports.first.$2; + final maxBit = ports.last.$2 + ports.last.$3 - 1; + + final outIds = getIds(swizOutSL); + if (maxBit >= outIds.length) { + continue; + } + final aggBits = outIds.sublist(minBit, maxBit + 1).cast(); + + final outName = + tryGetSynthLogicName(swizOutSL) ?? 'agg_out_${minBit}_$maxBit'; + + // Replace individual ports with the aggregate. The bypassed + // Swizzle cells are left in place; the post-synthesis DCE pass + // will remove them if their outputs are no longer consumed. + for (final (portName, _, _, _) in ports) { + connections.remove(portName); + portDirs.remove(portName); + } + connections[outName] = aggBits; + portDirs[outName] = 'output'; + } + } + + /// Builds a JSON-serializable type descriptor for [logic]. + /// + /// Returns: + /// - For a plain [Logic] or [LogicArray]: `{'width': N}` (bitvector is the + /// default) + /// - For a [LogicStructure] (non-array): `{'typeName': className, 'fields': + /// [field, ...]}` where each field is `{'name': fieldName, 'width': W}` for + /// leaf fields or `{'name': fieldName, 'type': {...}}` for nested + /// [LogicStructure]s. + /// + /// Fields are listed in LSB-to-MSB order (matching ROHD's element ordering + /// via `rswizzle`: `elements[0]` occupies the lowest bits). + /// + /// When [bits] is provided, each field entry also includes a `'bits'` key + /// containing the slice of [bits] that belongs to that field. This allows + /// consumers to identify which net IDs map to which field even when the + /// signal is only partially connected (where computing offsets from the flat + /// top-level `bits` array would be ambiguous). + static Map buildLogicType( + Logic logic, [ + List? bits, + ]) { + if (logic is LogicArray) { + final result = { + 'width': logic.width, + 'arrayDims': logic.dimensions, + 'elementWidth': logic.elementWidth, + }; + // If the leaf elements are LogicStructures (array of structs), + // include the element type metadata for recursive expansion. + if (logic.elements.isNotEmpty) { + final first = logic.elements.first; + if (first is LogicStructure && first is! LogicArray) { + result['elementType'] = buildLogicType(first); + } else if (first is LogicArray) { + // Nested array — encode inner dimensions via recursive call. + result['elementType'] = buildLogicType(first); + } + } + return result; + } else if (logic is LogicStructure) { + var offset = 0; + final fields = logic.elements.map((e) { + final fieldBits = bits?.sublist(offset, offset + e.width); + offset += e.width; + if (e is LogicStructure && e is! LogicArray) { + return { + 'name': e.name, + if (fieldBits != null) 'bits': fieldBits, + 'type': buildLogicType(e, fieldBits), + }; + } else if (e is LogicArray) { + return { + 'name': e.name, + 'width': e.width, + if (fieldBits != null) 'bits': fieldBits, + 'type': buildLogicType(e, fieldBits), + }; + } else { + return { + 'name': e.name, + 'width': e.width, + if (fieldBits != null) 'bits': fieldBits, + }; + } + }).toList(); + return {'typeName': logic.runtimeType.toString(), 'fields': fields}; + } else { + return {'width': logic.width}; + } + } + + /// Returns the most type-specific [Logic] from [sl]'s [Logic] list for + /// use in [buildLogicType]. + /// + /// Prefers a [LogicStructure] (non-array) over a plain [Logic], since it + /// carries richer field metadata. + static Logic? typeLogicFromSynthLogic(SynthLogic sl) { + final logics = sl.logics; + return logics + .whereType() + .where((l) => l is! LogicArray) + .firstOrNull ?? + logics.firstOrNull; + } + + /// Check if a SynthLogic is a constant (following replacement chain). + static bool isConstantSynthLogic(SynthLogic sl) => + resolveReplacement(sl).isConstant; + + /// Extract the Const value from a constant SynthLogic. + static Const? constValueFromSynthLogic(SynthLogic sl) { + final resolved = resolveReplacement(sl); + for (final logic in resolved.logics) { + if (logic is Const) { + return logic; + } + } + return null; + } + + /// Value portion of a constant name: `_h` or `_b`. + static String constValuePart(Const c) { + final bitChars = []; + var hasXZ = false; + for (var i = c.width - 1; i >= 0; i--) { + final v = c.value[i]; + switch (v) { + case LogicValue.zero: + bitChars.add('0'); + case LogicValue.one: + bitChars.add('1'); + case LogicValue.x: + bitChars.add('x'); + hasXZ = true; + case LogicValue.z: + bitChars.add('z'); + hasXZ = true; + } + } + if (hasXZ) { + return '${c.width}_b${bitChars.join()}'; + } + var value = BigInt.zero; + for (var i = c.width - 1; i >= 0; i--) { + value = value << 1; + if (c.value[i] == LogicValue.one) { + value = value | BigInt.one; + } + } + return '${c.width}_h${value.toRadixString(16)}'; + } +} diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..da5d76586 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,6 +1,7 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'netlist/netlist.dart'; export 'synth_builder.dart'; export 'synth_file_contents.dart'; export 'synthesis_result.dart'; diff --git a/test/netlist_example_test.dart b/test/netlist_example_test.dart new file mode 100644 index 000000000..6a1d8cffa --- /dev/null +++ b/test/netlist_example_test.dart @@ -0,0 +1,292 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_example_test.dart +// Convert examples to netlist JSON and check the produced output. + +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +void main() { + // Detect whether running in JS (dart2js) environment. In JS many + // `dart:io` APIs are unsupported; when running tests with + // `--platform node` we skip filesystem and loader assertions. + const isJS = identical(0, 0.0); + + // Helper used by the tests to synthesize `top` and optionally write the + // produced JSON to `outPath` when running on VM. Returns the decoded + // modules map from the Yosys-format JSON. + Future> convertTestWriteNetlist( + Module top, + String outPath, + ) async { + final synth = SynthBuilder(top, NetlistSynthesizer()); + final jsonStr = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + top, + ); + if (!isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(jsonStr); + } + final decoded = jsonDecode(jsonStr) as Map; + return decoded['modules'] as Map; + } + + test('Netlist dump for example Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + counter.generateSynth(); + + final modules = await convertTestWriteNetlist( + counter, + 'build/Counter.rohd.json', + ); + + expect( + modules, + isNotEmpty, + reason: 'Counter netlist should have module definitions', + ); + // The top module should have cells (sub-module instances or gates) + final topMod = modules[counter.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'Counter should have cells'); + }); + + group('SynthBuilder netlist generation for examples', () { + test('SynthBuilder netlist for Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + + final modules = await convertTestWriteNetlist( + counter, + 'build/Counter.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'Counter synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = FirFilter(en, resetB, clk, inputVal, [ + 0, + 0, + 0, + 1, + ], bitWidth: 8); + await fir.build(); + + final synth = SynthBuilder(fir, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + fir, + 'build/FirFilter.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'FirFilter synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = LogicArrayExample( + arrayA, + id, + selectIndexValue, + selectFromValue, + ); + await la.build(); + + final synth = SynthBuilder(la, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + la, + 'build/LogicArrayExample.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'LogicArrayExample synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + final synth = SynthBuilder(oven, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + oven, + 'build/OvenModule.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'OvenModule synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + final synth = SynthBuilder(tree, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser (pure Dart or JS). + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + tree, + ); + expect( + json, + isNotEmpty, + reason: 'TreeOfTwoInputModules should produce non-empty JSON', + ); + if (!isJS) { + final file = File('build/TreeOfTwoInputModules.synth.rohd.json'); + await file.create(recursive: true); + await file.writeAsString(json); + } + }); + }); + + test('Netlist dump for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await fir.build(); + + const outPath = 'build/FirFilter.rohd.json'; + final modules = await convertTestWriteNetlist(fir, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'FirFilter netlist should have module definitions', + ); + }); + + test('Netlist dump for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = LogicArrayExample(arrayA, id, selectIndexValue, selectFromValue); + await la.build(); + + const outPath = 'build/LogicArrayExample.rohd.json'; + final modules = await convertTestWriteNetlist(la, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'LogicArrayExample netlist should have module definitions', + ); + }); + + test('Netlist dump for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + const outPath = 'build/OvenModule.rohd.json'; + final modules = await convertTestWriteNetlist(oven, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'OvenModule netlist should have module definitions', + ); + }); + + test('Netlist dump for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser. + const outPath = 'build/TreeOfTwoInputModules.rohd.json'; + final synth = SynthBuilder(tree, NetlistSynthesizer()); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + tree, + ); + expect( + json, + isNotEmpty, + reason: 'TreeOfTwoInputModules should produce non-empty JSON', + ); + if (!isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(json); + expect(file.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + } + }); +} diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart new file mode 100644 index 000000000..bdeb5139c --- /dev/null +++ b/test/netlist_synthesizer_test.dart @@ -0,0 +1,1337 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer_test.dart +// Comprehensive tests for the netlist synthesizer covering leaf cell +// mapping, structural validation, options permutations, and real +// example designs. +// +// 2026 April 13 +// Author: Auto-generated + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +// ──────────────────────────────────────────────────────────────────── +// Tiny helper modules for targeted gate-level tests +// ──────────────────────────────────────────────────────────────────── + +/// Exercises And2Gate. +class AndModule extends Module { + Logic get y => output('y'); + AndModule(Logic a, Logic b) : super(name: 'andmod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a & b; + } +} + +/// Exercises Or2Gate. +class OrModule extends Module { + Logic get y => output('y'); + OrModule(Logic a, Logic b) : super(name: 'ormod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a | b; + } +} + +/// Exercises Xor2Gate. +class XorModule extends Module { + Logic get y => output('y'); + XorModule(Logic a, Logic b) : super(name: 'xormod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a ^ b; + } +} + +/// Exercises NotGate. +class NotModule extends Module { + Logic get y => output('y'); + NotModule(Logic a) : super(name: 'notmod') { + a = addInput('a', a); + addOutput('y') <= ~a; + } +} + +/// Exercises Mux. +class MuxModule extends Module { + Logic get y => output('y'); + MuxModule(Logic sel, Logic a, Logic b, {int width = 8}) : super(name: 'mux') { + sel = addInput('sel', sel); + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y', width: width) <= mux(sel, a, b); + } +} + +/// Exercises FlipFlop. +class FlopModule extends Module { + Logic get q => output('q'); + FlopModule(Logic clk, Logic d, {int width = 8}) : super(name: 'flopmod') { + clk = addInput('clk', clk); + d = addInput('d', d, width: width); + addOutput('q', width: width) <= flop(clk, d); + } +} + +/// Exercises Add. +class AddModule extends Module { + Logic get sum => output('sum'); + AddModule(Logic a, Logic b, {int width = 8}) : super(name: 'addmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('sum', width: width) <= a + b; + } +} + +/// Exercises Multiply. +class MulModule extends Module { + Logic get prod => output('prod'); + MulModule(Logic a, Logic b, {int width = 8}) : super(name: 'mulmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('prod', width: width) <= a * b; + } +} + +/// Exercises BusSubset ($slice). +class SliceModule extends Module { + Logic get y => output('y'); + SliceModule(Logic a) : super(name: 'slicemod') { + a = addInput('a', a, width: 8); + addOutput('y', width: 4) <= a.getRange(2, 6); + } +} + +/// Exercises comparison operators. +class CompareModule extends Module { + Logic get lt => output('lt'); + Logic get gt => output('gt'); + Logic get eq => output('eq'); + CompareModule(Logic a, Logic b, {int width = 8}) : super(name: 'cmpmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('lt') <= LessThan(a, b).out; + addOutput('gt') <= GreaterThan(a, b).out; + addOutput('eq') <= a.eq(b); + } +} + +/// Exercises shift operations. +class ShiftModule extends Module { + Logic get shl => output('shl'); + Logic get shr => output('shr'); + ShiftModule(Logic a, Logic amt, {int width = 8}) : super(name: 'shiftmod') { + a = addInput('a', a, width: width); + amt = addInput('amt', amt, width: width); + addOutput('shl', width: width) <= a << amt; + addOutput('shr', width: width) <= a >>> amt; + } +} + +/// Exercises Xor2Gate. +class XorGateModule extends Module { + Logic get y => output('y'); + XorGateModule(Logic a, Logic b) : super(name: 'xormod2') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a ^ b; + } +} + +/// Exercises Subtract. +class SubModule extends Module { + Logic get diff => output('diff'); + SubModule(Logic a, Logic b, {int width = 8}) : super(name: 'submod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('diff', width: width) <= a - b; + } +} + +/// Exercises Swizzle ($concat). +class SwizzleModule extends Module { + Logic get y => output('y'); + SwizzleModule(Logic a, Logic b, {int width = 4}) : super(name: 'swizmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y', width: width * 2) <= [a, b].swizzle(); + } +} + +/// Exercises arithmetic right shift (ARShift). +class ARShiftModule extends Module { + Logic get y => output('y'); + ARShiftModule(Logic a, Logic amt, {int width = 8}) + : super(name: 'arshiftmod') { + a = addInput('a', a, width: width); + amt = addInput('amt', amt, width: width); + addOutput('y', width: width) <= a >> amt; + } +} + +/// Exercises unary reduction ops. +class ReduceModule extends Module { + Logic get andR => output('andR'); + Logic get orR => output('orR'); + Logic get xorR => output('xorR'); + ReduceModule(Logic a, {int width = 8}) : super(name: 'reducemod') { + a = addInput('a', a, width: width); + addOutput('andR') <= a.and(); + addOutput('orR') <= a.or(); + addOutput('xorR') <= a.xor(); + } +} + +/// Exercises individual comparison ops for cell-type checking. +class LtModule extends Module { + Logic get y => output('y'); + LtModule(Logic a, Logic b, {int width = 8}) : super(name: 'ltmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.lt(b); + } +} + +class GtModule extends Module { + Logic get y => output('y'); + GtModule(Logic a, Logic b, {int width = 8}) : super(name: 'gtmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.gt(b); + } +} + +class EqModule extends Module { + Logic get y => output('y'); + EqModule(Logic a, Logic b, {int width = 8}) : super(name: 'eqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.eq(b); + } +} + +class NeqModule extends Module { + Logic get y => output('y'); + NeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'neqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.neq(b); + } +} + +class LeqModule extends Module { + Logic get y => output('y'); + LeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'leqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.lte(b); + } +} + +class GeqModule extends Module { + Logic get y => output('y'); + GeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'geqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.gte(b); + } +} + +/// Exercises TriStateBuffer. +class TriBufModule extends Module { + Logic get bus => inOut('bus'); + TriBufModule(LogicNet busNet, Logic data, Logic en) + : super(name: 'tribufmod') { + final bus = addInOut('bus', busNet, width: data.width); + data = addInput('data', data, width: data.width); + en = addInput('en', en); + TriStateBuffer(data, enable: en, name: 'tsb').out.gets(bus); + } +} + +/// Exercises Combinational with If. +class CombIfModule extends Module { + Logic get y => output('y'); + CombIfModule(Logic sel, Logic a, Logic b, {int width = 8}) + : super(name: 'combif') { + sel = addInput('sel', sel); + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final y = addOutput('y', width: width); + Combinational([ + If(sel, then: [y < a], orElse: [y < b]), + ]); + } +} + +/// Exercises Sequential with If. +class SeqIfModule extends Module { + Logic get q => output('q'); + SeqIfModule(Logic clk, Logic en, Logic d, {int width = 8}) + : super(name: 'seqif') { + clk = addInput('clk', clk); + en = addInput('en', en); + d = addInput('d', d, width: width); + final q = addOutput('q', width: width); + Sequential(clk, [ + If(en, then: [q < d]), + ]); + } +} + +/// Module with multiple instances of the same sub-module (dedup test). +class DedupTop extends Module { + Logic get y0 => output('y0'); + Logic get y1 => output('y1'); + DedupTop(Logic a, Logic b, {int width = 8}) + : super(name: 'deduptop', definitionName: 'DedupTop') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y0', width: width) <= AddModule(a, b, width: width).sum; + addOutput('y1', width: width) <= AddModule(a, b, width: width).sum; + } +} + +/// Module with different-width instances (no dedup). +class NoDedupTop extends Module { + Logic get y0 => output('y0'); + Logic get y1 => output('y1'); + NoDedupTop(Logic a4, Logic b4, Logic a8, Logic b8) + : super(name: 'nodeduptop', definitionName: 'NoDedupTop') { + a4 = addInput('a4', a4, width: 4); + b4 = addInput('b4', b4, width: 4); + a8 = addInput('a8', a8, width: 8); + b8 = addInput('b8', b8, width: 8); + addOutput('y0', width: 4) <= AddModule(a4, b4, width: 4).sum; + addOutput('y1', width: 8) <= AddModule(a8, b8).sum; + } +} + +/// A module with a named constant (Logic..gets(Const)) used inside a +/// Combinational block — exercises the named-constant fix. +class _NamedConstModule extends Module { + _NamedConstModule(Logic clk, Logic reset) : super(name: 'namedConstMod') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + final dataIn = addInput('dataIn', Logic(width: 8), width: 8); + final result = addOutput('result', width: 8); + + // Named constant driven by Const — this is the pattern from + // _dynamicInputToLogic in SummationBase. + final myConst = Logic(name: 'myConst', width: 8)..gets(Const(0, width: 8)); + + Combinational([result < mux(dataIn.or(), dataIn, myConst)]); + } +} + +// ──────────────────────────────────────────────────────────────────── +// Helpers +// ──────────────────────────────────────────────────────────────────── + +/// Build a FilterBank module for testing (not yet built). +FilterBank _buildFilterBank() { + const dataWidth = 16; + const numTaps = 3; + const coeffs0 = [1, 2, 1]; + const coeffs1 = [1, -2, 1]; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [coeffs0, coeffs1], + ); +} + +/// Build a module and synthesize to a parsed JSON map. +Future> _synthToMap( + Module mod, { + NetlistOptions options = const NetlistOptions(), +}) async { + await mod.build(); + final synth = SynthBuilder(mod, NetlistSynthesizer(options: options)); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson(mod); + return jsonDecode(json) as Map; +} + +/// Extract the `modules` map from a synthesized JSON map. +Map _modules(Map json) => + json['modules'] as Map; + +/// Get cells map from a module definition. +Map _cells(Map moduleDef) => + moduleDef['cells'] as Map? ?? {}; + +/// Get ports map from a module definition. +Map _ports(Map moduleDef) => + moduleDef['ports'] as Map? ?? {}; + +/// Get netnames map from a module definition. +Map _netnames(Map moduleDef) => + moduleDef['netnames'] as Map? ?? {}; + +/// Check that a module definition has a port with given name and direction. +void _expectPort( + Map moduleDef, + String portName, + String direction, +) { + final ports = _ports(moduleDef); + expect(ports, contains(portName), reason: 'Expected port "$portName"'); + final port = ports[portName] as Map; + expect( + port['direction'], + equals(direction), + reason: 'Port "$portName" should be "$direction"', + ); +} + +/// Returns true if any cell in any module definition has the given type. +bool _hasCellType(Map json, String cellType) { + final mod = _modules(json); + return mod.values.any((m) { + final def = m as Map; + return _cells(def).values.any((c) { + final cell = c as Map; + return (cell['type'] as String) == cellType; + }); + }); +} + +// ──────────────────────────────────────────────────────────────────── +// Tests +// ──────────────────────────────────────────────────────────────────── + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // ── Group 1: Leaf cell mapper — individual gate mappings ─────────── + + group('leaf cell mapping', () { + test(r'And2Gate maps to $and cell', () async { + final json = await _synthToMap(AndModule(Logic(), Logic())); + expect(_hasCellType(json, r'$and'), isTrue); + }); + + test(r'Or2Gate maps to $or cell', () async { + final json = await _synthToMap(OrModule(Logic(), Logic())); + expect(_hasCellType(json, r'$or'), isTrue); + }); + + test(r'Xor2Gate maps to $xor cell', () async { + final json = await _synthToMap(XorGateModule(Logic(), Logic())); + expect(_hasCellType(json, r'$xor'), isTrue); + }); + + test(r'NotGate maps to $not cell', () async { + final json = await _synthToMap(NotModule(Logic())); + expect(_hasCellType(json, r'$not'), isTrue); + }); + + test(r'Mux maps to $mux cell', () async { + final json = await _synthToMap( + MuxModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$mux'), isTrue); + }); + + test(r'FlipFlop maps to $dff cell', () async { + final clk = SimpleClockGenerator(10).clk; + final json = await _synthToMap(FlopModule(clk, Logic(width: 8))); + expect(_hasCellType(json, r'$dff'), isTrue); + }); + + test(r'Add maps to $add cell', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$add'), isTrue); + }); + + test(r'Subtract maps to $sub cell', () async { + final json = await _synthToMap( + SubModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$sub'), isTrue); + }); + + test(r'Multiply maps to $mul cell', () async { + final json = await _synthToMap( + MulModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$mul'), isTrue); + }); + + test(r'BusSubset maps to $slice cell', () async { + final json = await _synthToMap(SliceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$slice'), isTrue); + }); + + test(r'Swizzle maps to $concat cell', () async { + final json = await _synthToMap( + SwizzleModule(Logic(width: 4), Logic(width: 4)), + ); + expect(_hasCellType(json, r'$concat'), isTrue); + }); + + test(r'LessThan maps to $lt cell', () async { + final json = await _synthToMap( + LtModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$lt'), isTrue); + }); + + test(r'GreaterThan maps to $gt cell', () async { + final json = await _synthToMap( + GtModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$gt'), isTrue); + }); + + test(r'Equals maps to $eq cell', () async { + final json = await _synthToMap( + EqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$eq'), isTrue); + }); + + test(r'NotEquals maps to $ne cell', () async { + final json = await _synthToMap( + NeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$ne'), isTrue); + }); + + test(r'LessThanOrEqual maps to $le cell', () async { + final json = await _synthToMap( + LeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$le'), isTrue); + }); + + test(r'GreaterThanOrEqual maps to $ge cell', () async { + final json = await _synthToMap( + GeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$ge'), isTrue); + }); + + test(r'LShift maps to $shl cell', () async { + final json = await _synthToMap( + ShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shl'), isTrue); + }); + + test(r'RShift maps to $shr cell', () async { + final json = await _synthToMap( + ShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shr'), isTrue); + }); + + test(r'ARShift maps to $shiftx cell', () async { + final json = await _synthToMap( + ARShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shiftx'), isTrue); + }); + + test(r'AndUnary maps to $reduce_and cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_and'), isTrue); + }); + + test(r'OrUnary maps to $reduce_or cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_or'), isTrue); + }); + + test(r'XorUnary maps to $reduce_xor cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_xor'), isTrue); + }); + + test(r'TriStateBuffer maps to $tribuf cell', () async { + final busNet = LogicNet(width: 8); + final json = await _synthToMap( + TriBufModule(busNet, Logic(width: 8), Logic()), + ); + expect(_hasCellType(json, r'$tribuf'), isTrue); + }); + }); + + // ── Group 2: Structural content validation ───────────────────────── + + group('structural validation', () { + test('ports have correct direction', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + // Find the top-level or AddModule definition + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + final ports = _ports(d); + for (final port in ports.entries) { + final p = port.value as Map; + expect( + ['input', 'output', 'inout'].contains(p['direction']), + isTrue, + reason: 'Port ${port.key} should have valid direction', + ); + // Each port should have bits + expect( + p['bits'], + isNotNull, + reason: 'Port ${port.key} should have bits array', + ); + } + } + }); + + test('cells have type and connections', () async { + final json = await _synthToMap( + MuxModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + expect(c['type'], isNotNull, reason: 'Every cell should have a type'); + expect( + c['connections'], + isNotNull, + reason: 'Every cell should have connections', + ); + } + } + }); + + test('netnames have bits arrays', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + for (final nn in _netnames(d).values) { + final n = nn as Map; + expect( + n['bits'], + isA>(), + reason: 'Each netname should have a bits list', + ); + } + } + }); + + test('inOut ports have direction inout', () async { + final busNet = LogicNet(width: 8); + final json = await _synthToMap( + TriBufModule(busNet, Logic(width: 8), Logic()), + ); + final mod = _modules(json); + // Find the TriBufModule definition + final tribufDef = mod.values.firstWhere((m) { + final d = m as Map; + return _ports(d).values.any((p) { + final port = p as Map; + return port['direction'] == 'inout'; + }); + }, orElse: () => {}) as Map; + expect( + tribufDef, + isNotEmpty, + reason: 'Should have a module with inout ports', + ); + }); + + test('Combinational If produces Combinational cell', () async { + final json = await _synthToMap( + CombIfModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + // Combinational blocks become Combinational cell type + expect( + _hasCellType(json, 'Combinational'), + isTrue, + reason: 'Combinational If should produce a Combinational cell', + ); + }); + + test('Sequential If produces dff cells', () async { + final clk = SimpleClockGenerator(10).clk; + final json = await _synthToMap( + SeqIfModule(clk, Logic(), Logic(width: 8)), + ); + final mod = _modules(json); + final hasSeq = mod.values.any((m) { + final def = m as Map; + final cells = _cells(def); + return cells.values.any((c) { + final cell = c as Map; + return (cell['type'] as String).contains('Sequential'); + }); + }); + expect( + hasSeq, + isTrue, + reason: 'Sequential If should contain Sequential cells', + ); + }); + }); + + // ── Group 3: Module deduplication ────────────────────────────────── + + group('deduplication', () { + test('identical sub-modules are deduplicated', () async { + final json = await _synthToMap( + DedupTop(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + // AddModule should appear only once as a definition + final addDefs = mod.keys.where((k) => k.contains('Add')).toList(); + expect( + addDefs.length, + equals(1), + reason: 'Two identical AddModules should produce one definition', + ); + // But should be instantiated twice in the top-level cells + final topDef = mod.entries + .firstWhere((e) => e.key.contains('DedupTop')) + .value as Map; + final addCells = _cells(topDef).values.where((c) { + final cell = c as Map; + return (cell['type'] as String).contains('Add'); + }).toList(); + expect( + addCells.length, + equals(2), + reason: 'Top module should instantiate AddModule twice', + ); + }); + + test('different-width sub-modules are not deduplicated', () async { + final json = await _synthToMap( + NoDedupTop( + Logic(width: 4), + Logic(width: 4), + Logic(width: 8), + Logic(width: 8), + ), + ); + final mod = _modules(json); + // Should have two distinct AddModule definitions (different widths) + final addDefs = mod.keys.where((k) => k.contains('Add')).toList(); + expect( + addDefs.length, + greaterThanOrEqualTo(2), + reason: 'Different-width AddModules should NOT be deduplicated', + ); + }); + }); + + // ── Group 4: NetlistOptions permutations ───────────────────────── + + group('NetlistOptions', () { + late Module filterBank; + + setUp(() async { + await Simulator.reset(); + filterBank = _buildFilterBank(); + await filterBank.build(); + }); + + test('default options produce valid netlist', () async { + final synth = SynthBuilder(filterBank, NetlistSynthesizer()); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + filterBank, + ); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('slimMode omits connections', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + ); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + filterBank, + ); + final parsed = jsonDecode(json) as Map; + final mod = _modules(parsed); + expect(mod, isNotEmpty); + // In slim mode, cells should exist but connections should be empty + for (final def in mod.values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + final conns = c['connections'] as Map?; + if (conns != null) { + expect( + conns, + isEmpty, + reason: 'Slim mode cells should have empty connections', + ); + } + } + } + }); + + test('DCE disabled still produces valid netlist', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + ); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + filterBank, + ); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('all optimizations disabled produces valid netlist', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + ); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + filterBank, + ); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('slim and full produce same module definitions', () async { + final fullSynth = SynthBuilder(filterBank, NetlistSynthesizer()); + final fullJson = (fullSynth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final fullParsed = jsonDecode(fullJson) as Map; + + // Rebuild for slim + await Simulator.reset(); + final fb2 = _buildFilterBank(); + await fb2.build(); + final slimSynth = SynthBuilder( + fb2, + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + ); + final slimJson = + (slimSynth.synthesizer as NetlistSynthesizer).synthesizeToJson(fb2); + final slimParsed = jsonDecode(slimJson) as Map; + + // Same module definition names + expect( + _modules(slimParsed).keys.toSet(), + equals(_modules(fullParsed).keys.toSet()), + reason: 'Slim and full should have identical module definition names', + ); + }); + }); + + // ── Group 5: Example designs — structural checks ─────────────────── + + group('example designs', () { + test('Counter netlist has FlipFlop and FSM-related cells', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = Counter(en, reset, clk); + final json = await _synthToMap(counter); + final mod = _modules(json); + + expect( + mod, + isNotEmpty, + reason: 'Counter should produce module definitions', + ); + // Should have a Counter definition + expect(mod.keys.any((k) => k.contains('Counter')), isTrue); + }); + + test('FirFilter netlist has pipeline and multiplier cells', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + final fir = FirFilter( + en, + resetB, + clk, + inputVal, + [ + 0, + 0, + 0, + 1, + ], + bitWidth: 8); + final json = await _synthToMap(fir); + final mod = _modules(json); + + expect( + mod, + isNotEmpty, + reason: 'FirFilter should produce module definitions', + ); + }); + + test('OvenModule netlist has FSM states', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final oven = OvenModule(button, reset, clk); + final json = await _synthToMap(oven); + final mod = _modules(json); + + expect(mod, isNotEmpty); + // Should have OvenModule definition + expect( + mod.keys.any((k) => k.contains('Oven') || k.contains('oven')), + isTrue, + ); + }); + + test('LogicArrayExample netlist has array-related cells', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + final la = LogicArrayExample( + arrayA, + id, + selectIndexValue, + selectFromValue, + ); + final json = await _synthToMap(la); + final mod = _modules(json); + + expect(mod, isNotEmpty); + }); + + test('TreeOfTwoInputModules netlist has recursive hierarchy', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + final synth = SynthBuilder(tree, NetlistSynthesizer()); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + tree, + ); + expect(json, isNotEmpty); + final parsed = jsonDecode(json) as Map; + final mod = _modules(parsed); + expect(mod, isNotEmpty, reason: 'Tree should have module definitions'); + }); + }); + + // ── Group 6: FilterBank deep structural checks ───────────────────── + + group('FilterBank netlist structure', () { + late Map json; + + setUpAll(() async { + final fb = _buildFilterBank(); + json = await _synthToMap(fb); + }); + + test('contains expected module definitions', () { + final mod = _modules(json); + final defNames = mod.keys.toSet(); + + // FilterBank, FilterChannel, CoeffBank, MacUnit, FilterController + // should all appear (possibly with parameterized suffixes) + expect( + defNames.any((k) => k.contains('FilterBank')), + isTrue, + reason: 'Should have FilterBank definition', + ); + expect( + defNames.any((k) => k.contains('FilterChannel')), + isTrue, + reason: 'Should have FilterChannel definition', + ); + expect( + defNames.any((k) => k.contains('CoeffBank')), + isTrue, + reason: 'Should have CoeffBank definition', + ); + expect( + defNames.any((k) => k.contains('MacUnit')), + isTrue, + reason: 'Should have MacUnit definition', + ); + expect( + defNames.any((k) => k.contains('FilterController')), + isTrue, + reason: 'Should have FilterController definition', + ); + }); + + test('FilterBank has array ports', () { + final mod = _modules(json); + final fbDef = mod.entries + .firstWhere((e) => e.key.contains('FilterBank')) + .value as Map; + final ports = _ports(fbDef); + + // Should have sample0/sample1 and channelOut as array ports + expect( + ports.keys.any((k) => k.contains('sample') || k.contains('channelOut')), + isTrue, + reason: 'FilterBank should have array port signals', + ); + }); + + test('FilterBank top instantiates two FilterChannels', () { + final mod = _modules(json); + final fbDef = mod.entries + .firstWhere((e) => e.key.contains('FilterBank')) + .value as Map; + final cells = _cells(fbDef); + + final channelCells = cells.entries.where((e) { + final cell = e.value as Map; + return (cell['type'] as String).contains('FilterChannel'); + }).toList(); + + expect( + channelCells.length, + equals(2), + reason: 'FilterBank should instantiate 2 FilterChannels', + ); + }); + + test( + 'FilterChannels with different coefficients get separate definitions', + () { + final mod = _modules(json); + final channelDefs = + mod.keys.where((k) => k.contains('FilterChannel')).toList(); + + expect( + channelDefs.length, + equals(2), + reason: 'Two FilterChannels with different coefficients ' + 'should produce distinct definitions', + ); + }, + ); + + test('MacUnit definition contains Pipeline-generated cells', () { + final mod = _modules(json); + final macDef = mod.entries + .firstWhere((e) => e.key.contains('MacUnit')) + .value as Map; + final cells = _cells(macDef); + + // Pipeline generates Sequential cells for stage registers + final hasSeq = cells.values.any((c) { + final cell = c as Map; + final type = cell['type'] as String; + return type.contains('Sequential'); + }); + expect( + hasSeq, + isTrue, + reason: 'MacUnit Pipeline should produce Sequential cells', + ); + }); + + test('CoeffBank has coeffArray input port', () { + final mod = _modules(json); + final coeffDef = mod.entries + .firstWhere((e) => e.key.contains('CoeffBank')) + .value as Map; + final ports = _ports(coeffDef); + + // Should have coeffArray-related port names + expect( + ports.keys.any((k) => k.contains('coeffArray')), + isTrue, + reason: 'CoeffBank should have coeffArray port', + ); + + // tapIndex should be input + expect( + ports.keys.any((k) => k.contains('tapIndex')), + isTrue, + reason: 'CoeffBank should have tapIndex port', + ); + }); + + test('FilterController has FSM state output', () { + final mod = _modules(json); + final ctrlDef = mod.entries + .firstWhere((e) => e.key.contains('FilterController')) + .value as Map; + final ports = _ports(ctrlDef); + + _expectPort(ctrlDef, 'state', 'output'); + _expectPort(ctrlDef, 'filterEnable', 'output'); + _expectPort(ctrlDef, 'doneFlag', 'output'); + expect(ports.keys.any((k) => k.contains('clk')), isTrue); + expect(ports.keys.any((k) => k.contains('reset')), isTrue); + }); + + test('all module definitions have valid JSON structure', () { + final mod = _modules(json); + for (final entry in mod.entries) { + final defName = entry.key; + final def = entry.value as Map; + + // Every definition must have ports and cells + expect( + def.containsKey('ports'), + isTrue, + reason: '$defName should have ports', + ); + expect( + def.containsKey('cells'), + isTrue, + reason: '$defName should have cells', + ); + + // All ports must have direction and bits + for (final port in _ports(def).entries) { + final p = port.value as Map; + expect( + p.containsKey('direction'), + isTrue, + reason: '$defName.${port.key} should have direction', + ); + expect( + p.containsKey('bits'), + isTrue, + reason: '$defName.${port.key} should have bits', + ); + } + + // All cells must have type + for (final cell in _cells(def).entries) { + final c = cell.value as Map; + expect( + c.containsKey('type'), + isTrue, + reason: '$defName cell ${cell.key} should have type', + ); + } + } + }); + }); + + // ── Group 7: Wire ID and structural invariants ───────────────────── + + group('wire ID and structural invariants', () { + test('all wire IDs are >= 2 (0 and 1 reserved for constants)', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final entry in mod.entries) { + final def = entry.value as Map; + // Check ports + for (final port in _ports(def).entries) { + final p = port.value as Map; + final bits = p['bits'] as List; + for (final bit in bits) { + if (bit is int) { + expect( + bit, + greaterThanOrEqualTo(2), + reason: 'Wire ID ${port.key} bit $bit should be >= 2', + ); + } + } + } + } + }); + + test(r'FilterBank contains $const cells for constant drivers', () async { + final json = await _synthToMap(_buildFilterBank()); + expect( + _hasCellType(json, r'$const'), + isTrue, + reason: r'FilterBank should have $const cells for constant values', + ); + }); + + test('passthrough buffers prevent input-output wire sharing', () async { + // A module whose output directly comes from an input should get a + // $buf for wire-ID isolation. + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + // Verify input and output port bits don't overlap in any definition + for (final entry in mod.entries) { + final def = entry.value as Map; + final ports = _ports(def); + final inputBits = {}; + final outputBits = {}; + for (final port in ports.entries) { + final p = port.value as Map; + final bits = (p['bits'] as List).whereType().toSet(); + final dir = p['direction'] as String; + if (dir == 'input') { + inputBits.addAll(bits); + } else if (dir == 'output') { + outputBits.addAll(bits); + } + } + expect( + inputBits.intersection(outputBits), + isEmpty, + reason: '${entry.key}: input and output ports should not share wire ' + 'IDs (passthrough buffer should break sharing)', + ); + } + }); + }); + + // ── Group 9: DCE (dead-cell elimination) verification ────────────── + + group('dead-cell elimination', () { + test('DCE enabled produces fewer cells than DCE disabled', () async { + final fbDce = _buildFilterBank(); + final jsonDce = await _synthToMap(fbDce); + int countCells(Map j) { + var total = 0; + for (final def in _modules(j).values) { + total += _cells(def as Map).length; + } + return total; + } + + final fbNoDce = _buildFilterBank(); + final jsonNoDce = await _synthToMap( + fbNoDce, + options: const NetlistOptions(enableDCE: false), + ); + + final dceCells = countCells(jsonDce); + final noDceCells = countCells(jsonNoDce); + expect( + dceCells, + lessThanOrEqualTo(noDceCells), + reason: 'DCE should remove at least as many cells as no-DCE', + ); + }); + + test(r'DCE removes floating $const cells', () async { + // With DCE disabled, there may be more $const cells + final fbDce = _buildFilterBank(); + final jsonDce = await _synthToMap(fbDce); + int countConstCells(Map j) { + var total = 0; + for (final def in _modules(j).values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + if ((c['type'] as String) == r'$const') { + total++; + } + } + } + return total; + } + + final fbNoDce = _buildFilterBank(); + final jsonNoDce = await _synthToMap( + fbNoDce, + options: const NetlistOptions(enableDCE: false), + ); + + expect( + countConstCells(jsonDce), + lessThanOrEqualTo(countConstCells(jsonNoDce)), + reason: r'DCE should not produce more $const cells than no-DCE', + ); + }); + }); + + // ── Group 10: Post-processing option combinations ────────────────── + + group('post-processing options', () { + test('collapseTransparentClusters produces valid netlist', () async { + final fb = _buildFilterBank(); + final json = await _synthToMap( + fb, + options: const NetlistOptions(collapseTransparentClusters: true), + ); + expect(_modules(json), isNotEmpty); + }); + }); + + // ── Group 11: Named constant signals ───────────────────────────── + + group('named constant signals', () { + test(r'Logic..gets(Const) produces $const cell and netname', () async { + final mod = _NamedConstModule(Logic(name: 'clk'), Logic(name: 'reset')); + final json = await _synthToMap(mod); + final mods = _modules(json); + + // Find the module definition for _NamedConstModule. + final modDef = mods.values.firstWhere((m) { + final def = m as Map; + return (def['cells'] as Map?)?.isNotEmpty ?? false; + }, orElse: () => mods.values.first) as Map; + + final netnames = _netnames(modDef); + final cells = _cells(modDef); + + // The signal 'myConst' should appear as a netname. + expect( + netnames.keys.any((n) => n.contains('myConst')), + isTrue, + reason: "Logic('myConst')..gets(Const(0)) should produce a netname", + ); + + // There should be a $const cell driving it. + expect( + cells.values.any( + (c) => (c as Map)['type'] == r'$const', + ), + isTrue, + reason: r'Named constant should have a $const driver cell', + ); + + // The netname bits should be integer wire IDs (not string literals). + final constNetname = netnames.entries.firstWhere( + (e) => e.key.contains('myConst'), + ); + final bits = (constNetname.value as Map)['bits'] as List; + expect( + bits.every((b) => b is int), + isTrue, + reason: 'Named constant netname should have integer wire IDs ' + r'(driven by a $const cell)', + ); + }); + }); +} diff --git a/test/netlist_test.dart b/test/netlist_test.dart new file mode 100644 index 000000000..daafb22dc --- /dev/null +++ b/test/netlist_test.dart @@ -0,0 +1,759 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_test.dart +// Tests for the netlist synthesizer: JSON structure, SynthBuilder, +// NetlistSynthesisResult, collectModuleEntries, NetlistOptions, +// and example-based smoke tests. +// +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +// --------------------------------------------------------------------------- +// Simple test modules (self-contained, no example imports needed) +// --------------------------------------------------------------------------- + +/// A trivial module that inverts a single-bit input. +class _InverterModule extends Module { + Logic get out => output('out'); + + _InverterModule(Logic inp) : super(name: 'inverter') { + inp = addInput('inp', inp); + final out = addOutput('out'); + out <= ~inp; + } +} + +/// A module that instantiates two sub-modules: an inverter and an AND gate. +class _CompositeModule extends Module { + Logic get out => output('out'); + + _CompositeModule(Logic a, Logic b) : super(name: 'composite') { + a = addInput('a', a); + b = addInput('b', b); + final out = addOutput('out'); + + final invA = _InverterModule(a); + out <= (_InverterModule(invA.out).out & b); + } +} + +/// A simple adder module with a configurable width. +class _AdderModule extends Module { + Logic get sum => output('sum'); + + _AdderModule(Logic a, Logic b, {int width = 8}) : super(name: 'adder') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final sum = addOutput('sum', width: width); + sum <= a + b; + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Detect whether running in JS (dart2js) environment. +const _isJS = identical(0, 0.0); + +/// Synthesize [top] and optionally write the produced JSON to [outPath]. +/// Returns the decoded modules map from the Yosys-format JSON. +Future> _synthesizeAndWrite( + Module top, + String outPath, +) async { + final synth = SynthBuilder(top, NetlistSynthesizer()); + final jsonStr = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + top, + ); + if (!_isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(jsonStr); + } + final decoded = jsonDecode(jsonStr) as Map; + return decoded['modules'] as Map; +} + +/// Build a FilterBank with default test parameters. +FilterBank _buildFilterBank({ + int dataWidth = 16, + int numTaps = 3, + List> coefficients = const [ + [1, 2, 1], + [1, -2, 1], + ], +}) { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate( + coefficients.length, + (ch) => FilterSample(dataWidth: dataWidth, name: 'sample$ch'), + ); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients, + ); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // ── Example smoke tests ─────────────────────────────────────────────── + // + // Each example is synthesized once, verifying that the netlist is + // non-empty and (on VM) that the JSON file is written successfully. + + group('Example netlist smoke tests', () { + test('Counter', () async { + final counter = Counter( + Logic(name: 'en'), + Logic(name: 'reset'), + SimpleClockGenerator(10).clk, + ); + await counter.build(); + + final modules = await _synthesizeAndWrite( + counter, + 'build/Counter.rohd.json', + ); + expect(modules, isNotEmpty); + + final topMod = modules[counter.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'Counter should have cells'); + }); + + test('FIR filter', () async { + final fir = FirFilter( + Logic(name: 'en'), + Logic(name: 'resetB'), + SimpleClockGenerator(10).clk, + Logic(name: 'inputVal', width: 8), + [0, 0, 0, 1], + bitWidth: 8, + ); + await fir.build(); + + final modules = await _synthesizeAndWrite( + fir, + 'build/FirFilter.rohd.json', + ); + expect(modules, isNotEmpty); + if (!_isJS) { + expect(File('build/FirFilter.rohd.json').existsSync(), isTrue); + } + }); + + test('LogicArray', () async { + final la = LogicArrayExample( + LogicArray([4], 8, name: 'arrayA'), + Logic(name: 'id', width: 3), + Logic(name: 'selectIndexValue', width: 8), + Logic(name: 'selectFromValue', width: 8), + ); + await la.build(); + + final modules = await _synthesizeAndWrite( + la, + 'build/LogicArrayExample.rohd.json', + ); + expect(modules, isNotEmpty); + }); + + test('OvenModule', () async { + final oven = OvenModule( + Logic(name: 'button', width: 2), + Logic(name: 'reset'), + SimpleClockGenerator(10).clk, + ); + await oven.build(); + + final modules = await _synthesizeAndWrite( + oven, + 'build/OvenModule.rohd.json', + ); + expect(modules, isNotEmpty); + }); + + test('TreeOfTwoInputModules', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser. + final json = NetlistSynthesizer().synthesizeToJson(tree); + expect(json, isNotEmpty); + if (!_isJS) { + final file = File('build/TreeOfTwoInputModules.rohd.json'); + await file.create(recursive: true); + await file.writeAsString(json); + } + }); + + test('FilterBank', () async { + final fb = _buildFilterBank(); + await fb.build(); + + final modules = await _synthesizeAndWrite( + fb, + 'build/FilterBank.smoke.rohd.json', + ); + expect(modules, isNotEmpty); + expect( + modules.length, + greaterThan(1), + reason: 'FilterBank should have sub-module definitions', + ); + }); + }); + + // ── JSON structure ──────────────────────────────────────────────────── + + group('JSON structure', () { + test('synthesizeToJson returns valid JSON with modules key', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + expect(json, isNotEmpty); + final decoded = jsonDecode(json) as Map; + expect(decoded, contains('modules')); + }); + + test( + 'top module is present with correct ports and top attribute', + () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + expect(modules, contains(mod.definitionName)); + + final topMod = modules[mod.definitionName] as Map; + + // Port directions + final ports = topMod['ports'] as Map; + expect(ports, contains('inp')); + expect(ports, contains('out')); + expect((ports['inp'] as Map)['direction'], equals('input')); + expect((ports['out'] as Map)['direction'], equals('output')); + + // Top attribute + final attrs = topMod['attributes'] as Map?; + expect(attrs, isNotNull); + expect(attrs!['top'], equals(1)); + }, + ); + + test('port bit widths match module interface', () async { + const width = 16; + final mod = _AdderModule( + Logic(name: 'a', width: width), + Logic(name: 'b', width: width), + width: width, + ); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + final topMod = modules[mod.definitionName] as Map; + final ports = topMod['ports'] as Map; + + expect((ports['a'] as Map)['bits'], hasLength(width)); + expect((ports['b'] as Map)['bits'], hasLength(width)); + expect((ports['sum'] as Map)['bits'], hasLength(width)); + }); + + test('cells have connections in default mode', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + final topMod = modules[mod.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + + final hasConnections = cells.values.any((cell) { + final c = cell as Map; + final conns = c['connections'] as Map?; + return conns != null && conns.isNotEmpty; + }); + expect(hasConnections, isTrue); + }); + + test( + 'generateCombinedJson and synthesizeToJson produce same module keys', + () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + + final fromCombined = synthesizer.generateCombinedJson(synth, mod); + final fromConvenience = NetlistSynthesizer().synthesizeToJson(mod); + + final combinedModules = + (jsonDecode(fromCombined) as Map)['modules'] as Map; + final convenienceModules = + (jsonDecode(fromConvenience) as Map)['modules'] as Map; + expect( + combinedModules.keys.toSet(), + equals(convenienceModules.keys.toSet()), + ); + }, + ); + }); + + // ── SynthBuilder ────────────────────────────────────────────────────── + + group('SynthBuilder', () { + test('synthesisResults are NetlistSynthesisResult instances', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + expect(synth.synthesisResults, isNotEmpty); + for (final result in synth.synthesisResults) { + expect(result, isA()); + } + }); + + test('composite module includes sub-module definitions', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + final names = synth.synthesisResults + .map((r) => r.instanceTypeName) + .toSet(); + expect(names, contains(mod.definitionName)); + expect(synth.synthesisResults.length, greaterThan(1)); + }); + + test('toSynthFileContents produces valid JSON per definition', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final fileContents = SynthBuilder( + mod, + NetlistSynthesizer(), + ).getSynthFileContents(); + expect(fileContents, isNotEmpty); + for (final fc in fileContents) { + expect(fc.name, isNotEmpty); + expect(jsonDecode(fc.contents), isA>()); + } + }); + }); + + // ── NetlistSynthesisResult maps ─────────────────────────────────────── + + group('NetlistSynthesisResult maps', () { + test('ports map has direction and bits for each port', () async { + final mod = _AdderModule( + Logic(name: 'a', width: 8), + Logic(name: 'b', width: 8), + ); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()).synthesisResults + .whereType() + .firstWhere((r) => r.module == mod); + + for (final portName in ['a', 'b', 'sum']) { + expect(result.ports, contains(portName)); + final port = result.ports[portName]!; + expect(port, contains('direction')); + expect(port, contains('bits')); + } + }); + + test('netnames map is populated', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()).synthesisResults + .whereType() + .firstWhere((r) => r.module == mod); + expect(result.netnames, isNotEmpty); + }); + }); + + // ── collectModuleEntries ────────────────────────────────────────────── + + group('collectModuleEntries', () { + test('gathers results with correct structure and top attribute', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + final modulesMap = NetlistPasses.collectModuleEntries( + synth.synthesisResults, + topModule: mod, + ); + + expect(modulesMap, contains(mod.definitionName)); + expect(modulesMap.length, greaterThan(1)); + + // Top attribute + final topAttrs = + modulesMap[mod.definitionName]!['attributes']! + as Map; + expect(topAttrs['top'], equals(1)); + + // Every entry has the expected sections + for (final entry in modulesMap.values) { + expect(entry, contains('ports')); + expect(entry, contains('cells')); + expect(entry, contains('netnames')); + } + }); + }); + + // ── buildModulesMap ─────────────────────────────────────────────────── + + group('buildModulesMap', () { + test('returns map with all definitions and expected sections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + final modulesMap = synthesizer.buildModulesMap(synth, mod); + + expect(modulesMap, contains(mod.definitionName)); + expect(modulesMap.length, greaterThan(1)); + for (final modEntry in modulesMap.entries) { + final data = modEntry.value; + expect(data, contains('ports'), reason: modEntry.key); + expect(data, contains('cells'), reason: modEntry.key); + expect(data, contains('netnames'), reason: modEntry.key); + } + }); + }); + + // ── NetlistOptions ─────────────────────────────────────────────────── + + group('NetlistOptions', () { + test('slimMode omits cell connections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final slimSynth = NetlistSynthesizer( + options: const NetlistOptions(slimMode: true), + ); + final json = slimSynth.synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + + for (final modEntry in modules.values) { + final data = modEntry as Map; + final cells = data['cells'] as Map? ?? {}; + for (final cell in cells.values) { + final c = cell as Map; + final conns = c['connections'] as Map?; + if (conns != null) { + expect(conns, isEmpty, reason: 'slim mode should omit connections'); + } + } + } + }); + }); + + // ── FilterBank (multi-channel, dedup, loopback) ─────────────────────── + + group('FilterBank netlist', () { + test('produces valid netlist with multiple module definitions', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final modules = await _synthesizeAndWrite( + mod, + 'build/FilterBank.rohd.json', + ); + expect(modules, isNotEmpty); + expect( + modules.length, + greaterThan(1), + reason: 'FilterBank should have sub-module definitions', + ); + + // Top module should have cells + final topMod = modules[mod.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'FilterBank should have cells'); + }); + + test('FilterChannel definitions are deduplicated', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final parsed = jsonDecode(json) as Map; + final modules = parsed['modules'] as Map; + final channelDefs = modules.keys + .where((k) => k.contains('FilterChannel')) + .toList(); + // Two channels with different coefficients should produce + // separate definitions (not fully deduplicated). + expect( + channelDefs, + isNotEmpty, + reason: 'FilterChannel definitions should be present', + ); + }); + + test('all module entries have ports, cells, and netnames', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + final modulesMap = synthesizer.buildModulesMap(synth, mod); + + for (final entry in modulesMap.entries) { + final data = entry.value; + expect(data, contains('ports'), reason: '${entry.key} missing ports'); + expect(data, contains('cells'), reason: '${entry.key} missing cells'); + expect( + data, + contains('netnames'), + reason: '${entry.key} missing netnames', + ); + } + }); + + test('ports have correct directions on sub-modules', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + + for (final result + in synth.synthesisResults.whereType()) { + for (final port in result.ports.entries) { + final dir = port.value['direction']! as String; + expect( + ['input', 'output', 'inout'], + contains(dir), + reason: + '${result.instanceTypeName}.${port.key} ' + 'has invalid direction', + ); + } + } + }); + }); + + // ----------------------------------------------------------------------- + // Bit-range compression & compact JSON + // ----------------------------------------------------------------------- + group('Bit-range compression', () { + test('compressBitRanges option produces range strings in JSON', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthCompressed = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressed = synthCompressed.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = synthNormal.synthesizeToJson(mod); + + // Compressed should be shorter. + expect(jsonCompressed.length, lessThan(jsonNormal.length)); + + // Both should parse as valid JSON with the same module keys. + final decodedCompressed = jsonDecode(jsonCompressed) as Map; + final decodedNormal = jsonDecode(jsonNormal) as Map; + expect( + (decodedCompressed['modules'] as Map).keys.toSet(), + equals((decodedNormal['modules'] as Map).keys.toSet()), + ); + + // Compressed JSON should contain range strings like "2:9". + expect(jsonCompressed, contains(RegExp(r'"\d+:\d+"'))); + // Normal JSON should NOT contain range strings. + expect(jsonNormal, isNot(contains(RegExp(r'"\d+:\d+"')))); + }); + + test('compressed ranges preserve constant bit strings', () async { + // Use a module that produces constant "0"/"1" bits in the netlist. + final a = Logic(name: 'a'); + final mod = _InverterModule(a); + await mod.build(); + + final synth = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final json = synth.synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + + // Should still be valid JSON. + expect(decoded['modules'], isNotNull); + }); + + test('compactJson option removes indentation', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthCompact = NetlistSynthesizer( + options: const NetlistOptions(compactJson: true), + ); + final jsonCompact = synthCompact.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = synthNormal.synthesizeToJson(mod); + + // Compact should be shorter. + expect(jsonCompact.length, lessThan(jsonNormal.length)); + // Compact should have no leading whitespace lines. + expect(jsonCompact, isNot(contains('\n '))); + // Both should be valid JSON with the same module keys. + final decodedCompact = jsonDecode(jsonCompact) as Map; + final decodedNormal = jsonDecode(jsonNormal) as Map; + expect( + (decodedCompact['modules'] as Map).keys.toSet(), + equals((decodedNormal['modules'] as Map).keys.toSet()), + ); + }); + + test('both options together produce smallest output', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthBoth = NetlistSynthesizer( + options: const NetlistOptions( + compressBitRanges: true, + compactJson: true, + ), + ); + final jsonBoth = synthBoth.synthesizeToJson(mod); + + final synthCompressOnly = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressOnly = synthCompressOnly.synthesizeToJson(mod); + + final synthCompactOnly = NetlistSynthesizer( + options: const NetlistOptions(compactJson: true), + ); + final jsonCompactOnly = synthCompactOnly.synthesizeToJson(mod); + + expect(jsonBoth.length, lessThan(jsonCompressOnly.length)); + expect(jsonBoth.length, lessThan(jsonCompactOnly.length)); + }); + + test('compressed FilterBank round-trips: range strings expand to ' + 'same bit IDs as uncompressed', () async { + final mod = _buildFilterBank(); + await mod.build(); + + // Generate both compressed and uncompressed. + final synthNormal = NetlistSynthesizer(); + final jsonNormal = synthNormal.synthesizeToJson(mod); + final normalModules = + (jsonDecode(jsonNormal) as Map)['modules'] + as Map; + + final synthCompressed = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressed = synthCompressed.synthesizeToJson(mod); + final compressedModules = + (jsonDecode(jsonCompressed) as Map)['modules'] + as Map; + + // Compressed should be smaller. + expect(jsonCompressed.length, lessThan(jsonNormal.length)); + + // Same module keys. + expect(compressedModules.keys.toSet(), normalModules.keys.toSet()); + + // Verify compressed JSON contains range strings. + expect(jsonCompressed, contains(RegExp(r'"\d+:\d+"'))); + + // For each module, expand compressed port bits and compare to normal. + for (final modName in normalModules.keys) { + final normalPorts = + (normalModules[modName] as Map)['ports'] + as Map?; + final compPorts = + (compressedModules[modName] as Map)['ports'] + as Map?; + if (normalPorts == null || compPorts == null) { + continue; + } + + for (final portName in normalPorts.keys) { + final normalBits = + (normalPorts[portName] as Map)['bits'] as List; + final compBits = + (compPorts[portName] as Map)['bits'] as List; + + // Expand any range strings in the compressed bits. + final expanded = []; + for (final b in compBits) { + if (b is String && b.contains(':')) { + final parts = b.split(':'); + final start = int.parse(parts[0]); + final end = int.parse(parts[1]); + for (var i = start; i <= end; i++) { + expanded.add(i); + } + } else { + expanded.add(b); + } + } + + expect( + expanded, + normalBits, + reason: 'round-trip failed for $modName.$portName', + ); + } + } + }); + }); +} diff --git a/test/struct_port_pruning_test.dart b/test/struct_port_pruning_test.dart new file mode 100644 index 000000000..9d7f3be76 --- /dev/null +++ b/test/struct_port_pruning_test.dart @@ -0,0 +1,143 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// struct_port_pruning_test.dart +// Verifies that struct port elements on submodules are not incorrectly +// pruned during SV synthesis. Exercises the `submoduleOutputSynths` / +// `submoduleInputSynths` fix in `_pruneUnused`. +// +// 2026 April 17 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +// ── Struct definition ────────────────────────────────────────── + +class PairStruct extends LogicStructure { + PairStruct({Logic? a, Logic? b, super.name = 'pair'}) + : super([a ?? Logic(name: 'a'), b ?? Logic(name: 'b')]); + + @override + PairStruct clone({String? name}) => PairStruct(name: name); +} + +// ── Leaf submodule with a struct output port ─────────────────── + +class StructProducer extends Module { + Logic get out => PairStruct()..gets(output('out')); + + StructProducer(Logic x, Logic y) : super(name: 'struct_producer') { + x = addInput('x', x); + y = addInput('y', y); + + final s = PairStruct(a: x, b: y); + addOutput('out', width: s.width) <= s; + } +} + +// ── Leaf submodule with a struct input port ──────────────────── + +class StructConsumer extends Module { + Logic get sum => output('sum'); + + StructConsumer(Logic pair) : super(name: 'struct_consumer') { + pair = addInput('pair', pair, width: pair.width); + + final s = PairStruct()..gets(pair); + addOutput('sum') <= s.elements[0] ^ s.elements[1]; + } +} + +// ── Top module: struct output from submodule → struct input ─── + +class StructPipeTop extends Module { + Logic get result => output('result'); + + StructPipeTop(Logic x, Logic y) : super(name: 'struct_pipe_top') { + x = addInput('x', x); + y = addInput('y', y); + + final producer = StructProducer(x, y); + final consumer = StructConsumer(producer.out); + + addOutput('result') <= consumer.sum; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('struct port pruning', () { + test('SV output retains struct element signals from submodule', () async { + final dut = StructPipeTop(Logic(), Logic()); + await dut.build(); + + final svStr = dut.generateSynth(); + + // The struct_producer submodule should appear in the SV. + expect( + svStr, + contains('struct_producer'), + reason: 'Submodule with struct output should not be pruned', + ); + + // The struct_consumer submodule should appear in the SV. + expect( + svStr, + contains('struct_consumer'), + reason: 'Submodule with struct input should not be pruned', + ); + + // The output port 'out' of struct_producer (width 2) must have a + // connection in the parent — it should not be pruned away. + expect( + svStr, + contains('.out('), + reason: 'Struct output port connection should not be pruned', + ); + + // The input port 'pair' of struct_consumer must be connected. + expect( + svStr, + contains('.pair('), + reason: 'Struct input port connection should not be pruned', + ); + }); + + test('struct element signals survive SV synthesis for producer', () async { + final dut = StructProducer(Logic(), Logic()); + await dut.build(); + + final svStr = dut.generateSynth(); + + // Inside StructProducer, the struct elements (a, b from PairStruct) + // drive the output via struct_slice decomposition. They must not + // be pruned. + expect(svStr, contains('out'), reason: 'Output port should appear in SV'); + expect( + svStr, + contains('input'), + reason: 'Input ports should appear in SV', + ); + }); + + test('struct element signals survive SV synthesis for consumer', () async { + final dut = StructConsumer(Logic(width: 2)); + await dut.build(); + + final svStr = dut.generateSynth(); + + // Inside StructConsumer, the struct elements are extracted from the + // packed input. The XOR of elements drives the output. + expect(svStr, contains('sum'), reason: 'Output port should appear in SV'); + expect( + svStr, + contains('pair'), + reason: 'Input struct port should appear in SV', + ); + }); + }); +} diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart new file mode 100644 index 000000000..8905b9639 --- /dev/null +++ b/test/synth_name_parity_test.dart @@ -0,0 +1,364 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_name_parity_test.dart +// Tests that verify canonicalNameOf works consistently across +// different synthesis paths (SV and netlist). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; + +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]), + ], + ), + ], + ); + } +} + +class _CollidingNames extends Module { + late final Logic firstDup; + late final Logic secondDup; + + _CollidingNames(Logic a, Logic b) : super(name: 'collidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + + firstDup = Logic(name: 'dup'); + secondDup = Logic(name: 'dup'); + + firstDup <= a & b; + secondDup <= a | b; + y <= firstDup ^ secondDup; + } +} + +class _PartiallyInlineCollidingNames extends Module { + late final Logic inlinedDup; + late final Logic retainedDup; + + _PartiallyInlineCollidingNames(Logic a, Logic b) + : super(name: 'partiallyInlineCollidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + + inlinedDup = Logic(name: 'dup'); + retainedDup = Logic(name: 'dup'); + + inlinedDup <= a & b; + retainedDup <= a | b; + y <= inlinedDup ^ retainedDup; + z <= retainedDup & a; + } +} + +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; + } +} + +class _ReverseInternalSignalOrderSynthModuleDefinition + extends SynthModuleDefinition { + _ReverseInternalSignalOrderSynthModuleDefinition(super.module); + + @override + void process() { + internalSignals + ..clear() + ..addAll(internalSignals.toList().reversed); + } +} + +Future> _collisionNamesAfter( + Iterable synthesize, +) async { + final mod = _CollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'firstDup': mod.namer.signalNameOfBest([mod.firstDup]), + 'secondDup': mod.namer.signalNameOfBest([mod.secondDup]), + }; +} + +Future> _collisionNamesAfterSynthDefinition( + SynthModuleDefinition Function(_CollidingNames) createSynthDefinition, +) async { + final mod = _CollidingNames(Logic(), Logic()); + await mod.build(); + + createSynthDefinition(mod); + + return { + 'firstDup': mod.namer.signalNameOfBest([mod.firstDup]), + 'secondDup': mod.namer.signalNameOfBest([mod.secondDup]), + }; +} + +Future> _partialInlineCollisionNamesAfter( + Iterable synthesize, +) async { + final mod = _PartiallyInlineCollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'retainedDup': mod.namer.signalNameOfBest([mod.retainedDup]), + 'inlinedDup': mod.namer.signalNameOfBest([mod.inlinedDup]), + }; +} + +Future> _collapsedInstanceCollisionNamesAfter( + Iterable synthesize, +) async { + final mod = _CollapsedInstanceCollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'retainedDup': mod.namer.signalNameOfBest([mod.retainedDup]), + }; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('canonicalNameOf after netlist synthesis', () { + test('counter — returns names after netlist synthesis', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + mod.generateNetlist(); + + 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('filter_bank — returns names for sub-module signals', () async { + const dataWidth = 16; + const numTaps = 3; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + dut.generateNetlist(); + + expect(dut.namer.signalNameOfBest([dut.input('clk')]), equals('clk')); + expect(dut.namer.signalNameOfBest([dut.input('reset')]), equals('reset')); + expect(dut.namer.signalNameOfBest([dut.output('done')]), equals('done')); + }); + }); + + group('canonicalNameOf after SV synthesis', () { + test('counter — returns canonical name after SV synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + mod.generateSynth(); + + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + }); + }); + + group('cross-synthesizer parity', () { + test( + 'counter — SV and netlist produce identical canonicalNameOf', + () async { + final modNetlist = _Counter(Logic(), Logic()); + await modNetlist.build(); + modNetlist.generateNetlist(); + await Simulator.reset(); + + final modSv = _Counter(Logic(), Logic()); + await modSv.build(); + modSv.generateSynth(); + + // Both paths use the same Namer, so names must match. + final enNetlist = modNetlist.namer.signalNameOfBest([ + modNetlist.input('en'), + ]); + final enSv = modSv.namer.signalNameOfBest([modSv.input('en')]); + + expect( + enSv, + equals(enNetlist), + reason: 'SV and netlist should produce identical canonical names', + ); + }, + ); + + test( + 'colliding mergeable names remain stable across synthesis order', + () async { + void runNetlist(_CollidingNames mod) => mod.generateNetlist(); + void runSv(_CollidingNames mod) => mod.generateSynth(); + + final netlistOnly = await _collisionNamesAfter([runNetlist]); + await Simulator.reset(); + + final svOnly = await _collisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _collisionNamesAfter([runNetlist, runSv]); + await Simulator.reset(); + + final svThenNetlist = await _collisionNamesAfter([runSv, runNetlist]); + + expect(netlistOnly, equals(svOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + + expect( + netlistOnly['secondDup'], + isNot(equals(netlistOnly['firstDup'])), + ); + }, + ); + + test( + 'colliding mergeable names ignore internal signal walk order', + () async { + final forward = await _collisionNamesAfterSynthDefinition( + SynthModuleDefinition.new, + ); + await Simulator.reset(); + + final reversed = await _collisionNamesAfterSynthDefinition( + _ReverseInternalSignalOrderSynthModuleDefinition.new, + ); + + expect(reversed, equals(forward)); + expect(forward['firstDup'], equals('dup')); + expect(forward['secondDup'], equals('dup_0')); + }, + ); + + test('colliding names stay stable when SV inlines one signal', () async { + void runNetlist(_PartiallyInlineCollidingNames mod) => + mod.generateNetlist(); + void runSv(_PartiallyInlineCollidingNames mod) => mod.generateSynth(); + + final netlistOnly = await _partialInlineCollisionNamesAfter([runNetlist]); + await Simulator.reset(); + + final svOnly = await _partialInlineCollisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _partialInlineCollisionNamesAfter([ + runNetlist, + runSv, + ]); + await Simulator.reset(); + + final svThenNetlist = await _partialInlineCollisionNamesAfter([ + runSv, + runNetlist, + ]); + + expect(svOnly, equals(netlistOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + expect(netlistOnly['inlinedDup'], equals('dup')); + expect(netlistOnly['retainedDup'], equals('dup_0')); + }); + + test( + 'signal names stay stable when SV collapses a colliding instance', + () async { + void runNetlist(_CollapsedInstanceCollidingNames mod) => + mod.generateNetlist(); + void runSv(_CollapsedInstanceCollidingNames mod) => mod.generateSynth(); + + final netlistOnly = await _collapsedInstanceCollisionNamesAfter([ + runNetlist, + ]); + await Simulator.reset(); + + final svOnly = await _collapsedInstanceCollisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _collapsedInstanceCollisionNamesAfter([ + runNetlist, + runSv, + ]); + await Simulator.reset(); + + final svThenNetlist = await _collapsedInstanceCollisionNamesAfter([ + runSv, + runNetlist, + ]); + + expect(svOnly, equals(netlistOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + expect(netlistOnly['retainedDup'], equals('dup')); + }, + ); + }); +} From e6fc88a2d64686f0e51e02041ae057a9ea9fdfb0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 2 Jul 2026 16:53:18 -0700 Subject: [PATCH 52/77] formatted --- lib/src/module.dart | 108 ++++---- .../netlist/leaf_cell_mapper.dart | 96 ++++--- .../netlist/netlist_synthesizer.dart | 246 +++++++++--------- .../synthesizers/netlist/netlist_utils.dart | 56 ++-- test/netlist_example_test.dart | 18 +- test/netlist_test.dart | 47 ++-- test/struct_port_pruning_test.dart | 2 +- test/synth_name_parity_test.dart | 4 +- 8 files changed, 276 insertions(+), 301 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index 8a4bb9b58..a1a86476d 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -115,11 +115,11 @@ abstract class Module { /// /// This does not contain any signals within [subModules]. Iterable get signals => UnmodifiableListView([ - ..._inputs.values, - ..._outputs.values, - ..._inOuts.values, - ...internalSignals, - ]); + ..._inputs.values, + ..._outputs.values, + ..._inOuts.values, + ...internalSignals, + ]); /// Accesses the [Logic] associated with this [Module]s [input] port /// named [name]. @@ -267,12 +267,12 @@ abstract class Module { this.reserveName = false, String? definitionName, this.reserveDefinitionName = false, - }) : _uniqueInstanceName = - Naming.validatedName(name, reserveName: reserveName) ?? name, - _definitionName = Naming.validatedName( - definitionName, - reserveName: reserveDefinitionName, - ); + }) : _uniqueInstanceName = + Naming.validatedName(name, reserveName: reserveName) ?? name, + _definitionName = Naming.validatedName( + definitionName, + reserveName: reserveDefinitionName, + ); /// Returns an [Iterable] of [Module]s representing the hierarchical path to /// this [Module]. @@ -446,9 +446,8 @@ abstract class Module { } try { - final subModule = (signal.isInput || signal.isInOut) - ? signal.parentModule - : null; + final subModule = + (signal.isInput || signal.isInOut) ? signal.parentModule : null; final subModuleParent = subModule?.parent; @@ -596,9 +595,8 @@ abstract class Module { } try { - final subModule = (signal.isOutput || signal.isInOut) - ? signal.parentModule - : null; + final subModule = + (signal.isOutput || signal.isInOut) ? signal.parentModule : null; final subModuleParent = subModule?.parent; @@ -968,16 +966,15 @@ abstract class Module { }) { _checkForSafePortName(name); - final inArr = - LogicArray( - name: name, - dimensions, - elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.reserved, - ) - ..gets(source) - ..setAllParentModule(this); + final inArr = LogicArray( + name: name, + dimensions, + elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.reserved, + ) + ..gets(source) + ..setAllParentModule(this); _inputs[name] = inArr; @@ -1146,16 +1143,15 @@ abstract class Module { } } - final inOutArr = - LogicArray.net( - name: name, - dimensions, - elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.reserved, - ) - ..gets(source) - ..setAllParentModule(this); + final inOutArr = LogicArray.net( + name: name, + dimensions, + elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.reserved, + ) + ..gets(source) + ..setAllParentModule(this); // there may be packed arrays created by the `gets` above, so this makes // sure we catch all of those. @@ -1170,24 +1166,23 @@ abstract class Module { /// Connects the [source] to this [Module] using [Interface.connectIO] and /// returns a copy of the [source] that can be used within this module. - InterfaceType addInterfacePorts< - InterfaceType extends Interface, - TagType extends Enum - >( + InterfaceType addInterfacePorts, + TagType extends Enum>( InterfaceType source, { Iterable? inputTags, Iterable? outputTags, Iterable? inOutTags, String Function(String original)? uniquify, - }) => (source.clone() as InterfaceType) - ..connectIO( - this, - source, - inputTags: inputTags, - outputTags: outputTags, - inOutTags: inOutTags, - uniquify: uniquify, - ); + }) => + (source.clone() as InterfaceType) + ..connectIO( + this, + source, + inputTags: inputTags, + outputTags: outputTags, + inOutTags: inOutTags, + uniquify: uniquify, + ); /// Connects the [source] to this [Module] using [PairInterface.pairConnectIO] /// and returns a copy of the [source] that can be used within this module. @@ -1201,11 +1196,11 @@ abstract class Module { @override String toString() => [ - '"$name" ($definitionName) : ', - if (_inputs.isNotEmpty) '${_inputs.keys}', - if (_outputs.isNotEmpty) '=> ${_outputs.keys}', - if (_inOuts.isNotEmpty) '; ${_inOuts.keys}', - ].join(' '); + '"$name" ($definitionName) : ', + if (_inputs.isNotEmpty) '${_inputs.keys}', + if (_outputs.isNotEmpty) '=> ${_outputs.keys}', + if (_inOuts.isNotEmpty) '; ${_inOuts.keys}', + ].join(' '); /// Returns a pretty-print [String] of the heirarchy of all [Module]s within /// this [Module]. @@ -1233,8 +1228,7 @@ abstract class Module { throw ModuleNotBuiltException(this); } - final synthHeader = - ''' + final synthHeader = ''' /** * Generated by ROHD - www.github.com/intel/rohd * Generation time: ${Timestamper.stamp()} diff --git a/lib/src/synthesizers/netlist/leaf_cell_mapper.dart b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart index 40b34bdbc..0ec82ef37 100644 --- a/lib/src/synthesizers/netlist/leaf_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart @@ -268,9 +268,8 @@ class LeafCellMapper { for (var i = 0; i < nonZeroKeys.length; i++) { final ik = nonZeroKeys[i]; final w = ctx.width(ik); - final label = w == 1 - ? '[$bitOffset]' - : '[${bitOffset + w - 1}:$bitOffset]'; + final label = + w == 1 ? '[$bitOffset]' : '[${bitOffset + w - 1}:$bitOffset]'; pd[label] = 'input'; cn[label] = ctx.rawConns[ik] ?? []; params['IN${i}_WIDTH'] = w; @@ -403,58 +402,55 @@ class LeafCellMapper { }); // ── Type-map-based gates ─────────────────────────────────────────── - final gateRegistrations = - < - ( - Map, - LeafCellMapping? Function(LeafCellContext, String), - ) - >[ - ( - const { - And2Gate: r'$and', - Or2Gate: r'$or', - Xor2Gate: r'$xor', - }, - (ctx, type) => - binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), - ), - ( - const { - AndUnary: r'$reduce_and', - OrUnary: r'$reduce_or', - XorUnary: r'$reduce_xor', - }, - unaryAY, - ), - ( - const { - Multiply: r'$mul', - Subtract: r'$sub', - Equals: r'$eq', - NotEquals: r'$ne', - LessThan: r'$lt', - GreaterThan: r'$gt', - LessThanOrEqual: r'$le', - GreaterThanOrEqual: r'$ge', - }, - (ctx, type) => - binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), - ), - ( - const { - LShift: r'$shl', - RShift: r'$shr', - ARShift: r'$shiftx', - }, - (ctx, type) => binaryABY( + final gateRegistrations = <( + Map, + LeafCellMapping? Function(LeafCellContext, String), + )>[ + ( + const { + And2Gate: r'$and', + Or2Gate: r'$or', + Xor2Gate: r'$xor', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + AndUnary: r'$reduce_and', + OrUnary: r'$reduce_or', + XorUnary: r'$reduce_xor', + }, + unaryAY, + ), + ( + const { + Multiply: r'$mul', + Subtract: r'$sub', + Equals: r'$eq', + NotEquals: r'$ne', + LessThan: r'$lt', + GreaterThan: r'$gt', + LessThanOrEqual: r'$le', + GreaterThanOrEqual: r'$ge', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + LShift: r'$shl', + RShift: r'$shr', + ARShift: r'$shiftx', + }, + (ctx, type) => binaryABY( ctx, type, inAPrefix: '_in', inBPrefix: '_shiftAmount', ), - ), - ]; + ), + ]; for (final (typeMap, handler) in gateRegistrations) { registerByTypeMap(typeMap, handler); } diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 221398414..e19c56470 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -23,16 +23,15 @@ class _NetlistSynthModuleDefinition extends SynthModuleDefinition { // netlist shows select gates for element extraction rather than // flat bit aliasing. module.inputs.values.whereType().forEach( - _subsetReceiveArrayPort, - ); + _subsetReceiveArrayPort, + ); // Same for LogicArray outputs on submodules (received into this scope). - final subModuleOutputArrays = - module.subModules - .expand((sub) => sub.outputs.values) - .whereType() - .toSet() - ..forEach(_subsetReceiveArrayPort); + final subModuleOutputArrays = module.subModules + .expand((sub) => sub.outputs.values) + .whereType() + .toSet() + ..forEach(_subsetReceiveArrayPort); // Create explicit $concat cells for internal LogicArrays whose elements // are driven independently (e.g. by constants) and then consumed by @@ -76,20 +75,17 @@ class _NetlistSynthModuleDefinition extends SynthModuleDefinition { portArraySynthLogics.add(sl.replacement ?? sl); } } - module.internalSignals - .whereType() - .where((sig) { - if (excludedArrays.contains(sig)) { - return false; - } - final sl = logicToSynthMap[sig]; - if (sl == null) { - return false; - } - final resolved = sl.replacement ?? sl; - return !portArraySynthLogics.contains(resolved); - }) - .forEach(_concatAssembleArray); + module.internalSignals.whereType().where((sig) { + if (excludedArrays.contains(sig)) { + return false; + } + final sl = logicToSynthMap[sig]; + if (sl == null) { + return false; + } + final resolved = sl.replacement ?? sl; + return !portArraySynthLogics.contains(resolved); + }).forEach(_concatAssembleArray); } /// Creates explicit `$slice` cells for each element of a [LogicArray] port. @@ -222,12 +218,10 @@ class NetlistSynthesizer extends Synthesizer { // -- Build SynthModuleDefinition ------------------------------------ // This does all signal tracing, naming, constant handling, // assignment collapsing, and unused signal pruning. - final canBuildSynthDef = - !(module is SystemVerilog && - module.generatedDefinitionType == DefinitionGenerationType.none); - final synthDef = canBuildSynthDef - ? _NetlistSynthModuleDefinition(module) - : null; + final canBuildSynthDef = !(module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none); + final synthDef = + canBuildSynthDef ? _NetlistSynthModuleDefinition(module) : null; // -- Wire-ID allocation --------------------------------------------- // Start wire IDs at 2 to avoid collision with Yosys constant string @@ -329,9 +323,8 @@ class NetlistSynthesizer extends Synthesizer { final sub = instance.module; final isLeaf = !generatesDefinition(sub); - final defaultCellType = isLeaf - ? sub.definitionName - : getInstanceTypeOfModule(sub); + final defaultCellType = + isLeaf ? sub.definitionName : getInstanceTypeOfModule(sub); // Build port directions and connections from instance mappings. final rawPortDirs = {}; @@ -379,8 +372,7 @@ class NetlistSynthesizer extends Synthesizer { final portsToRemove = []; for (final pe in cellConns.entries) { final portName = pe.key; - final synthLogic = - instance.inputMapping[portName] ?? + final synthLogic = instance.inputMapping[portName] ?? instance.inOutMapping[portName]; if (synthLogic != null && NetlistUtils.isConstantSynthLogic(synthLogic)) { @@ -401,8 +393,7 @@ class NetlistSynthesizer extends Synthesizer { if (sub is Combinational || sub is Sequential) { final renames = {}; for (final portName in cellConns.keys.toList()) { - final sl = - instance.inputMapping[portName] ?? + final sl = instance.inputMapping[portName] ?? instance.outputMapping[portName] ?? instance.inOutMapping[portName]; if (sl == null) { @@ -468,34 +459,28 @@ class NetlistSynthesizer extends Synthesizer { // The `parentLogic` and `fullParentIds` fields are used to group // entries from the same LogicStructure into a single multi-port // `$struct_unpack` cell. - final structFieldCells = - < - ({ - List parentIds, - List elemIds, - int offset, - int width, - Logic elemLogic, - Logic parentLogic, - List fullParentIds, - }) - >[]; + final structFieldCells = <({ + List parentIds, + List elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds, + })>[]; // Pending $struct_compose cells: for output struct ports, instead of // aliasing port bits to leaf bits (which causes "shorting"), we // collect composition operations and emit explicit cells later. // Each entry records: field (src) → port sub-range [lower:upper]. - final structComposeCells = - < - ({ - List srcIds, - List dstIds, - int dstLowerIndex, - int dstUpperIndex, - SynthLogic srcSynthLogic, - SynthLogic dstSynthLogic, - }) - >[]; + final structComposeCells = <({ + List srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + })>[]; // Track struct ports (both output ports of the current module AND // sub-module input struct ports) so Step 3 can skip $struct_field @@ -510,9 +495,8 @@ class NetlistSynthesizer extends Synthesizer { )) { final srcIds = getIds(assignment.src); final dstIds = getIds(assignment.dst); - final len = srcIds.length < dstIds.length - ? srcIds.length - : dstIds.length; + final len = + srcIds.length < dstIds.length ? srcIds.length : dstIds.length; for (var i = 0; i < len; i++) { if (dstIds[i] != srcIds[i]) { idAlias[dstIds[i]] = srcIds[i]; @@ -556,8 +540,7 @@ class NetlistSynthesizer extends Synthesizer { // Detect: is pa.dst a sub-module input struct port? // (LogicStructure but not LogicArray, and not an output of the // current module.) - final isSubModuleInputStructPort = - !isCurrentModuleOutputPort && + final isSubModuleInputStructPort = !isCurrentModuleOutputPort && pa.dst.logics.any((l) => l is LogicStructure && l is! LogicArray); if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { @@ -696,11 +679,9 @@ class NetlistSynthesizer extends Synthesizer { final elemSL = synthDef.logicToSynthMap[element]; if (elemSL != null) { final elemIds = getIds(elemSL); - for ( - var i = 0; - i < elemIds.length && idx + i < parentIds.length; - i++ - ) { + for (var i = 0; + i < elemIds.length && idx + i < parentIds.length; + i++) { aliasChildToParent(elemIds[i], parentIds[idx + i]); } } @@ -909,10 +890,8 @@ class NetlistSynthesizer extends Synthesizer { // This replaces the old per-field $struct_field cells. if (synthDef != null && structFieldCells.isNotEmpty) { // Group by parent Logic identity. - final groups = - < - Logic, - List< + final groups = parentIds, List elemIds, @@ -921,9 +900,7 @@ class NetlistSynthesizer extends Synthesizer { Logic elemLogic, Logic parentLogic, List fullParentIds, - }) - > - >{}; + })>>{}; for (final sf in structFieldCells) { (groups[sf.parentLogic] ??= []).add(sf); } @@ -968,16 +945,13 @@ class NetlistSynthesizer extends Synthesizer { // Same strategy as $struct_pack: walk the hierarchy collecting // (start, end, name, path, indexInParent) and look up the // narrowest non-unpreferred range for each field offset. - final suElementRanges = - < - ({ - int start, - int end, - String name, - String path, - int indexInParent, - }) - >[]; + final suElementRanges = <({ + int start, + int end, + String name, + String path, + int indexInParent, + })>[]; if (parentLogic is LogicStructure) { void walkStruct( LogicStructure struct, @@ -988,9 +962,8 @@ class NetlistSynthesizer extends Synthesizer { for (var idx = 0; idx < struct.elements.length; idx++) { final elem = struct.elements[idx]; final elemEnd = offset + elem.width; - final elemPath = parentPath.isEmpty - ? elem.name - : '${parentPath}_${elem.name}'; + final elemPath = + parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; suElementRanges.add(( start: offset, end: elemEnd, @@ -1009,12 +982,27 @@ class NetlistSynthesizer extends Synthesizer { } String suFieldNameFor(int fieldOffset, String fallbackName) { - ({int start, int end, String name, String path, int indexInParent})? - bestNamed; - ({int start, int end, String name, String path, int indexInParent})? - bestAny; - ({int start, int end, String name, String path, int indexInParent})? - narrowest; + ({ + int start, + int end, + String name, + String path, + int indexInParent + })? bestNamed; + ({ + int start, + int end, + String name, + String path, + int indexInParent + })? bestAny; + ({ + int start, + int end, + String name, + String path, + int indexInParent + })? narrowest; for (final r in suElementRanges) { if (fieldOffset >= r.start && fieldOffset < r.end) { @@ -1112,10 +1100,8 @@ class NetlistSynthesizer extends Synthesizer { // This replaces the old per-field $struct_compose cells. if (structComposeCells.isNotEmpty) { // Group by destination SynthLogic identity. - final composeGroups = - < - SynthLogic, - List< + final composeGroups = srcIds, List dstIds, @@ -1123,9 +1109,7 @@ class NetlistSynthesizer extends Synthesizer { int dstUpperIndex, SynthLogic srcSynthLogic, SynthLogic dstSynthLogic, - }) - > - >{}; + })>>{}; for (final sc in structComposeCells) { (composeGroups[sc.dstSynthLogic] ??= []).add(sc); } @@ -1184,16 +1168,13 @@ class NetlistSynthesizer extends Synthesizer { // produce qualified names like "mmu_info_mmuSid". When // leaf names are unpreferred, `parentElementIndex` provides // a fallback discriminator like "mmu_info_0". - final dstElementRanges = - < - ({ - int start, - int end, - String name, - String path, - int indexInParent, - }) - >[]; + final dstElementRanges = <({ + int start, + int end, + String name, + String path, + int indexInParent, + })>[]; if (dstLogic is LogicStructure) { void walkStruct( LogicStructure struct, @@ -1204,9 +1185,8 @@ class NetlistSynthesizer extends Synthesizer { for (var idx = 0; idx < struct.elements.length; idx++) { final elem = struct.elements[idx]; final elemEnd = offset + elem.width; - final elemPath = parentPath.isEmpty - ? elem.name - : '${parentPath}_${elem.name}'; + final elemPath = + parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; dstElementRanges.add(( start: offset, end: elemEnd, @@ -1237,12 +1217,27 @@ class NetlistSynthesizer extends Synthesizer { /// (e.g. `mmu_info_0`, `mmu_info_1`). /// 4. Falls back to the resolved source SynthLogic name. String fieldNameFor(int dstLowerIndex, SynthLogic srcSynthLogic) { - ({int start, int end, String name, String path, int indexInParent})? - bestNamed; - ({int start, int end, String name, String path, int indexInParent})? - bestAny; - ({int start, int end, String name, String path, int indexInParent})? - narrowest; + ({ + int start, + int end, + String name, + String path, + int indexInParent + })? bestNamed; + ({ + int start, + int end, + String name, + String path, + int indexInParent + })? bestAny; + ({ + int start, + int end, + String name, + String path, + int indexInParent + })? narrowest; for (final r in dstElementRanges) { if (dstLowerIndex >= r.start && dstLowerIndex < r.end) { @@ -1493,11 +1488,10 @@ class NetlistSynthesizer extends Synthesizer { { var constIdx = 0; final emittedConstWires = {}; - for (final entry - in synthLogicIds.entries - .where((e) => e.key.isConstant) - .where((e) => !blockedConstSynthLogics.contains(e.key)) - .where((e) => e.value.isNotEmpty)) { + for (final entry in synthLogicIds.entries + .where((e) => e.key.isConstant) + .where((e) => !blockedConstSynthLogics.contains(e.key)) + .where((e) => e.value.isNotEmpty)) { final sl = entry.key; final constValue = NetlistUtils.constValueFromSynthLogic(sl); if (constValue == null) { @@ -2090,7 +2084,7 @@ class NetlistSynthesizer extends Synthesizer { /// select gates rather than flat bit aliasing. class _BusSubsetForArraySlice extends BusSubset { _BusSubsetForArraySlice(super.bus, super.startIndex, super.endIndex) - : super(name: 'array_slice'); + : super(name: 'array_slice'); @override bool get hasBuilt => true; diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 3c90ccba5..2a2d5a8ab 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -55,14 +55,15 @@ class NetlistUtils { int width, List aBits, List yBits, - ) => { - 'hide_name': 0, - 'type': r'$buf', - 'parameters': {'WIDTH': width}, - 'attributes': {}, - 'port_directions': {'A': 'input', 'Y': 'output'}, - 'connections': >{'A': aBits, 'Y': yBits}, - }; + ) => + { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': width}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{'A': aBits, 'Y': yBits}, + }; /// Collapses bit-slice ports of a Combinational/Sequential cell into /// aggregate ports. @@ -114,18 +115,14 @@ class NetlistUtils { // Group input ports by root signal, also tracking the BusSubset // instantiations that produced each port. - final inputGroups = - < - SynthLogic, - List< + final inputGroups = - >{}; + )>>{}; for (final e in instance.inputMapping.entries) { final portName = e.key; @@ -200,17 +197,14 @@ class NetlistUtils { // (Swizzle port name, bit offset within the Swizzle output, // port width, resolved Swizzle output SynthLogic, // SynthSubModuleInstantiation). - final swizzleLookup = - < - SynthLogic, - ( - String portName, - int offset, - int width, - SynthLogic, - SynthSubModuleInstantiation, - ) - >{}; + final swizzleLookup = {}; for (final szInst in synthDef.subModuleInstantiations) { if (szInst.module is! Swizzle) { continue; @@ -238,18 +232,14 @@ class NetlistUtils { } // Group output ports by Swizzle output signal. - final outputGroups = - < - SynthLogic, - List< + final outputGroups = - >{}; + )>>{}; for (final e in instance.outputMapping.entries) { final portName = e.key; diff --git a/test/netlist_example_test.dart b/test/netlist_example_test.dart index 6a1d8cffa..08b56b7cc 100644 --- a/test/netlist_example_test.dart +++ b/test/netlist_example_test.dart @@ -96,12 +96,18 @@ void main() { final clk = SimpleClockGenerator(10).clk; final inputVal = Logic(name: 'inputVal', width: 8); - final fir = FirFilter(en, resetB, clk, inputVal, [ - 0, - 0, - 0, - 1, - ], bitWidth: 8); + final fir = FirFilter( + en, + resetB, + clk, + inputVal, + [ + 0, + 0, + 0, + 1, + ], + bitWidth: 8); await fir.build(); final synth = SynthBuilder(fir, NetlistSynthesizer()); diff --git a/test/netlist_test.dart b/test/netlist_test.dart index daafb22dc..69841f2f7 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -359,9 +359,8 @@ void main() { await mod.build(); final synth = SynthBuilder(mod, NetlistSynthesizer()); - final names = synth.synthesisResults - .map((r) => r.instanceTypeName) - .toSet(); + final names = + synth.synthesisResults.map((r) => r.instanceTypeName).toSet(); expect(names, contains(mod.definitionName)); expect(synth.synthesisResults.length, greaterThan(1)); }); @@ -392,7 +391,8 @@ void main() { ); await mod.build(); - final result = SynthBuilder(mod, NetlistSynthesizer()).synthesisResults + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults .whereType() .firstWhere((r) => r.module == mod); @@ -408,7 +408,8 @@ void main() { final mod = _InverterModule(Logic(name: 'inp')); await mod.build(); - final result = SynthBuilder(mod, NetlistSynthesizer()).synthesisResults + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults .whereType() .firstWhere((r) => r.module == mod); expect(result.netnames, isNotEmpty); @@ -432,9 +433,8 @@ void main() { expect(modulesMap.length, greaterThan(1)); // Top attribute - final topAttrs = - modulesMap[mod.definitionName]!['attributes']! - as Map; + final topAttrs = modulesMap[mod.definitionName]!['attributes']! + as Map; expect(topAttrs['top'], equals(1)); // Every entry has the expected sections @@ -527,9 +527,8 @@ void main() { final json = NetlistSynthesizer().synthesizeToJson(mod); final parsed = jsonDecode(json) as Map; final modules = parsed['modules'] as Map; - final channelDefs = modules.keys - .where((k) => k.contains('FilterChannel')) - .toList(); + final channelDefs = + modules.keys.where((k) => k.contains('FilterChannel')).toList(); // Two channels with different coefficients should produce // separate definitions (not fully deduplicated). expect( @@ -573,8 +572,7 @@ void main() { expect( ['input', 'output', 'inout'], contains(dir), - reason: - '${result.instanceTypeName}.${port.key} ' + reason: '${result.instanceTypeName}.${port.key} ' 'has invalid direction', ); } @@ -685,7 +683,8 @@ void main() { expect(jsonBoth.length, lessThan(jsonCompactOnly.length)); }); - test('compressed FilterBank round-trips: range strings expand to ' + test( + 'compressed FilterBank round-trips: range strings expand to ' 'same bit IDs as uncompressed', () async { final mod = _buildFilterBank(); await mod.build(); @@ -693,17 +692,15 @@ void main() { // Generate both compressed and uncompressed. final synthNormal = NetlistSynthesizer(); final jsonNormal = synthNormal.synthesizeToJson(mod); - final normalModules = - (jsonDecode(jsonNormal) as Map)['modules'] - as Map; + final normalModules = (jsonDecode(jsonNormal) + as Map)['modules'] as Map; final synthCompressed = NetlistSynthesizer( options: const NetlistOptions(compressBitRanges: true), ); final jsonCompressed = synthCompressed.synthesizeToJson(mod); - final compressedModules = - (jsonDecode(jsonCompressed) as Map)['modules'] - as Map; + final compressedModules = (jsonDecode(jsonCompressed) + as Map)['modules'] as Map; // Compressed should be smaller. expect(jsonCompressed.length, lessThan(jsonNormal.length)); @@ -716,12 +713,10 @@ void main() { // For each module, expand compressed port bits and compare to normal. for (final modName in normalModules.keys) { - final normalPorts = - (normalModules[modName] as Map)['ports'] - as Map?; - final compPorts = - (compressedModules[modName] as Map)['ports'] - as Map?; + final normalPorts = (normalModules[modName] + as Map)['ports'] as Map?; + final compPorts = (compressedModules[modName] + as Map)['ports'] as Map?; if (normalPorts == null || compPorts == null) { continue; } diff --git a/test/struct_port_pruning_test.dart b/test/struct_port_pruning_test.dart index 9d7f3be76..b13346ebe 100644 --- a/test/struct_port_pruning_test.dart +++ b/test/struct_port_pruning_test.dart @@ -16,7 +16,7 @@ import 'package:test/test.dart'; class PairStruct extends LogicStructure { PairStruct({Logic? a, Logic? b, super.name = 'pair'}) - : super([a ?? Logic(name: 'a'), b ?? Logic(name: 'b')]); + : super([a ?? Logic(name: 'a'), b ?? Logic(name: 'b')]); @override PairStruct clone({String? name}) => PairStruct(name: name); diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index 8905b9639..ea7022aad 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -59,7 +59,7 @@ class _PartiallyInlineCollidingNames extends Module { late final Logic retainedDup; _PartiallyInlineCollidingNames(Logic a, Logic b) - : super(name: 'partiallyInlineCollidingNames') { + : super(name: 'partiallyInlineCollidingNames') { a = addInput('a', a); b = addInput('b', b); final y = addOutput('y'); @@ -79,7 +79,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'); From f73526dbe25890b72a26699c6140fd267408a8d0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 10 Jul 2026 08:04:11 -0700 Subject: [PATCH 53/77] Refactor netlist synthesis helpers --- analysis_options.yaml | 3 + lib/src/synthesizers/netlist/netlist.dart | 1 - ...l_mapper.dart => netlist_cell_mapper.dart} | 83 +- .../netlist/netlist_module_translation.dart | 546 +++++++ .../synthesizers/netlist/netlist_options.dart | 51 +- .../synthesizers/netlist/netlist_passes.dart | 55 +- .../netlist_synth_module_definition.dart | 138 ++ .../netlist/netlist_synthesis_result.dart | 2 + .../netlist/netlist_synthesizer.dart | 1374 ++--------------- .../synthesizers/netlist/netlist_utils.dart | 30 +- .../netlist/netlist_validation.dart | 109 ++ lib/src/synthesizers/synthesizers.dart | 1 + .../systemverilog_synthesizer.dart | 13 +- .../utilities/synth_array_concat.dart | 41 + .../utilities/synth_array_slice.dart | 43 + .../synthesizers/utilities/synth_logic.dart | 6 + .../utilities/synth_module_definition.dart | 31 +- .../utilities/synth_module_stop_policy.dart | 76 + .../utilities/synth_structure_concat.dart | 41 + .../utilities/synth_structure_layout.dart | 115 ++ .../utilities/synth_structure_slice.dart | 43 + lib/src/synthesizers/utilities/utilities.dart | 5 + lib/src/utilities/namer.dart | 109 ++ test/netlist_synthesizer_test.dart | 121 +- test/netlist_test.dart | 30 + test/synth_structure_layout_test.dart | 74 + 26 files changed, 1803 insertions(+), 1338 deletions(-) rename lib/src/synthesizers/netlist/{leaf_cell_mapper.dart => netlist_cell_mapper.dart} (89%) create mode 100644 lib/src/synthesizers/netlist/netlist_module_translation.dart create mode 100644 lib/src/synthesizers/netlist/netlist_synth_module_definition.dart create mode 100644 lib/src/synthesizers/netlist/netlist_validation.dart create mode 100644 lib/src/synthesizers/utilities/synth_array_concat.dart create mode 100644 lib/src/synthesizers/utilities/synth_array_slice.dart create mode 100644 lib/src/synthesizers/utilities/synth_module_stop_policy.dart create mode 100644 lib/src/synthesizers/utilities/synth_structure_concat.dart create mode 100644 lib/src/synthesizers/utilities/synth_structure_layout.dart create mode 100644 lib/src/synthesizers/utilities/synth_structure_slice.dart create mode 100644 test/synth_structure_layout_test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 2b2098177..4d6df7488 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,9 @@ # Lints set up with some guidance from here: # https://rydmike.com/blog_flutter_linting.html +formatter: + trailing_commas: preserve + analyzer: language: strict-casts: true diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart index 268416b31..16cbd75ef 100644 --- a/lib/src/synthesizers/netlist/netlist.dart +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -7,7 +7,6 @@ // 2026 February 11 // Author: Desmond Kirkpatrick -export 'leaf_cell_mapper.dart'; export 'netlist_options.dart'; export 'netlist_passes.dart'; export 'netlist_synthesis_result.dart'; diff --git a/lib/src/synthesizers/netlist/leaf_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart similarity index 89% rename from lib/src/synthesizers/netlist/leaf_cell_mapper.dart rename to lib/src/synthesizers/netlist/netlist_cell_mapper.dart index 0ec82ef37..f674911f7 100644 --- a/lib/src/synthesizers/netlist/leaf_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -1,28 +1,31 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// leaf_cell_mapper.dart -// Maps ROHD leaf modules to Yosys-primitive cell representations. +// netlist_cell_mapper.dart +// Maps selected ROHD modules to Yosys-primitive cell representations. // // 2026 February 11 // Author: Desmond Kirkpatrick +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -/// The result of mapping a leaf ROHD module to a Yosys-style cell. -typedef LeafCellMapping = ({ +/// The result of mapping a netlist cell module to a Yosys-style cell. +@internal +typedef NetlistCellMapping = ({ String cellType, Map portDirs, Map> connections, Map parameters, }); -/// Context provided to each leaf-cell mapping handler. +/// Context provided to each netlist-cell mapping handler. /// /// Contains the module instance plus the raw ROHD port directions and /// connections built by the synthesizer, so handlers can remap them to /// Yosys-primitive port names. -class LeafCellContext { +@internal +class NetlistCellContext { /// The ROHD [Module] being mapped. final Module module; @@ -32,8 +35,8 @@ class LeafCellContext { /// Raw ROHD connection map (`{'portName': [wireId, ...]}`). final Map> rawConns; - /// Creates a [LeafCellContext]. - const LeafCellContext(this.module, this.rawPortDirs, this.rawConns); + /// Creates a [NetlistCellContext]. + const NetlistCellContext(this.module, this.rawPortDirs, this.rawConns); // ── Shared helper methods ─────────────────────────────────────────── @@ -75,49 +78,48 @@ class LeafCellContext { } } -/// Signature for a leaf-cell mapping handler. +/// Signature for a netlist-cell mapping handler. /// -/// Returns a [LeafCellMapping] if the handler recognises the module, +/// Returns a [NetlistCellMapping] if the handler recognises the module, /// or `null` to let the next handler try. -typedef LeafCellHandler = LeafCellMapping? Function(LeafCellContext ctx); +@internal +typedef NetlistCellHandler = NetlistCellMapping? Function( + NetlistCellContext ctx); -/// Maps ROHD leaf [Module]s to Yosys-primitive cell representations. +/// Maps modules already selected as netlist leaves to Yosys-primitive cell +/// representations. /// /// Handlers are registered via [register] and tried in registration order. -/// A singleton instance with all built-in ROHD types pre-registered is -/// available via [LeafCellMapper.defaultMapper]. -/// -/// ```dart -/// final mapper = LeafCellMapper.defaultMapper; -/// final result = mapper.map(sub, rawPortDirs, rawConns); -/// ``` -class LeafCellMapper { +/// Hierarchy stopping is controlled separately by [SynthModuleStopPolicy]. +@internal +class NetlistCellMapper { /// Ordered list of registered handlers. - final _handlers = []; + final _handlers = []; - /// Creates an empty [LeafCellMapper] with no registered handlers. - LeafCellMapper(); + /// Creates an empty [NetlistCellMapper] with no registered handlers. + NetlistCellMapper(); - /// The default mapper with all built-in ROHD leaf types registered. - static final defaultMapper = LeafCellMapper._withDefaults(); + /// Creates a mapper with all built-in ROHD netlist cell types registered. + factory NetlistCellMapper.withDefaults() => + NetlistCellMapper().._registerDefaults(); /// Register a mapping [handler]. /// /// Handlers are tried in registration order; the first non-null result /// wins. Register more-specific handlers before less-specific ones. - void register(LeafCellHandler handler) { + void register(NetlistCellHandler handler) { _handlers.add(handler); } /// Try to map [module] to a Yosys-primitive cell. /// /// Returns `null` if no registered handler matches. - LeafCellMapping? map( + NetlistCellMapping? map( Module module, Map rawPortDirs, Map> rawConns, ) { - final ctx = LeafCellContext(module, rawPortDirs, rawConns); + final ctx = NetlistCellContext(module, rawPortDirs, rawConns); for (final handler in _handlers) { final result = handler(ctx); if (result != null) { @@ -132,7 +134,7 @@ class LeafCellMapper { // ══════════════════════════════════════════════════════════════════════ /// Map a single-input, single-output gate (e.g. `$not`, `$reduce_and`). - static LeafCellMapping? unaryAY(LeafCellContext ctx, String cellType) { + static NetlistCellMapping? unaryAY(NetlistCellContext ctx, String cellType) { final inN = ctx.firstInput; final out = ctx.firstOutput; if (inN == null || out == null) { @@ -151,8 +153,8 @@ class LeafCellMapper { } /// Map a two-input gate with ports A, B, Y (e.g. `$and`, `$eq`, `$shl`). - static LeafCellMapping? binaryABY( - LeafCellContext ctx, + static NetlistCellMapping? binaryABY( + NetlistCellContext ctx, String cellType, { required String inAPrefix, required String inBPrefix, @@ -180,23 +182,20 @@ class LeafCellMapper { // Built-in handler registration // ══════════════════════════════════════════════════════════════════════ - /// Creates a [LeafCellMapper] with built-in handlers for common ROHD leaf - /// types. - factory LeafCellMapper._withDefaults() { - final m = LeafCellMapper(); - + void _registerDefaults() { // Helper to reduce boilerplate for type-map-based handlers. void registerByTypeMap( Map typeMap, - LeafCellMapping? Function(LeafCellContext ctx, String cellType) handler, + NetlistCellMapping? Function(NetlistCellContext ctx, String cellType) + handler, ) { - m.register((ctx) { + register((ctx) { final cellType = typeMap[ctx.module.runtimeType]; return cellType == null ? null : handler(ctx, cellType); }); } - m + this // ── BusSubset → $slice ──────────────────────────────────────────── ..register((ctx) { if (ctx.module is! BusSubset) { @@ -404,7 +403,7 @@ class LeafCellMapper { // ── Type-map-based gates ─────────────────────────────────────────── final gateRegistrations = <( Map, - LeafCellMapping? Function(LeafCellContext, String), + NetlistCellMapping? Function(NetlistCellContext, String), )>[ ( const { @@ -456,7 +455,7 @@ class LeafCellMapper { } // ── TriStateBuffer → $tribuf ────────────────────────────────────── - m.register((ctx) { + register((ctx) { if (ctx.module is! TriStateBuffer) { return null; } @@ -472,7 +471,5 @@ class LeafCellMapper { parameters: {'WIDTH': ctx.width(inName)}, ); }); - - return m; } } diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart new file mode 100644 index 000000000..780e58cf7 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -0,0 +1,546 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// netlist_module_translation.dart +// Per-module state and ordered phases for netlist synthesis. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// Mutable state for translating one module level into a netlist. +@internal +class NetlistModuleTranslation { + /// The module being translated. + final Module module; + + /// The synthesis definition for this module level, when one can be built. + final NetlistSynthModuleDefinition? synthDef; + + final NetlistCellMapper _netlistCellMapper; + final bool Function(Module module) _generatesDefinition; + final String Function(Module module) _getInstanceTypeOfModule; + + /// The next available integer wire identifier. + int nextId = 2; + + /// Wire identifiers allocated for each synthesis logic. + final Map> synthLogicIds = {}; + + /// Emitted module ports. + final Map> ports = {}; + + /// Emitted cells. + final Map> cells = {}; + + /// Emitted netnames. + final Map netnames = {}; + + /// Constants consumed only by procedural cells, which need no driver cell. + final Set blockedConstSynthLogics = {}; + + /// Creates translation state for one [module]. + NetlistModuleTranslation( + this.module, { + required NetlistCellMapper netlistCellMapper, + required bool Function(Module module) generatesDefinition, + required String Function(Module module) getInstanceTypeOfModule, + }) : _netlistCellMapper = netlistCellMapper, + _generatesDefinition = generatesDefinition, + _getInstanceTypeOfModule = getInstanceTypeOfModule, + synthDef = module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none + ? null + : NetlistSynthModuleDefinition(module); + + /// Allocates the next wire identifier. + int allocateWireId() => nextId++; + + /// Allocates or returns the wire identifiers for [synthLogic]. + List getIds(SynthLogic synthLogic) { + final resolved = synthLogic.isConstant ? synthLogic : synthLogic.resolved; + return synthLogicIds.putIfAbsent( + resolved, + () => List.generate(resolved.width, (_) => allocateWireId()), + ); + } + + /// Emits input, output, and inout ports in canonical allocation order. + void processPorts() { + final portGroups = [ + ('input', synthDef?.inputs, module.inputs), + ('output', synthDef?.outputs, module.outputs), + ('inout', synthDef?.inOuts, module.inOuts), + ]; + for (final (direction, synthLogics, modulePorts) in portGroups) { + if (synthLogics != null) { + for (final synthLogic in synthLogics) { + final ids = getIds(synthLogic); + final portName = + NetlistUtils.portNameForSynthLogic(synthLogic, modulePorts); + if (portName != null) { + final portLogic = modulePorts[portName]; + ports[portName] = { + 'direction': direction, + 'bits': ids, + if (portLogic != null) + 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), + }; + } + } + } else { + for (final entry in modulePorts.entries) { + final ids = List.generate( + entry.value.width, + (_) => allocateWireId(), + ); + ports[entry.key] = { + 'direction': direction, + 'bits': ids, + 'logic_type': NetlistUtils.buildLogicType(entry.value, ids), + }; + } + } + } + } + + /// Preallocates internal wires in [Module.internalSignals] order. + void processInternalWires() { + final definition = synthDef; + if (definition == null) { + return; + } + module.internalSignals + .map((signal) => definition.logicToSynthMap[signal]) + .whereType() + .where((synthLogic) => !synthLogic.isConstant) + .forEach(getIds); + } + + /// Emits cells and removes instances cleared by procedural-port collapsing. + void processCells() { + final definition = synthDef; + if (definition == null) { + return; + } + + final emittedCellKeys = {}; + for (final instance in definition.subModuleInstantiations) { + if (!instance.needsInstantiation) { + continue; + } + + final submodule = instance.module; + final isLeaf = !_generatesDefinition(submodule); + final defaultCellType = isLeaf + ? submodule.definitionName + : _getInstanceTypeOfModule(submodule); + final rawPortDirs = {}; + final rawConnections = >{}; + + for (final (direction, mapping) in [ + ('input', instance.inputMapping), + ('output', instance.outputMapping), + ('inout', instance.inOutMapping), + ]) { + for (final entry in mapping.entries) { + rawPortDirs[entry.key] = direction; + rawConnections[entry.key] = getIds(entry.value).cast(); + } + } + + final mapped = isLeaf + ? _netlistCellMapper.map(submodule, rawPortDirs, rawConnections) + : null; + final cellPortDirs = mapped?.portDirs ?? rawPortDirs; + final cellConnections = mapped?.connections ?? rawConnections; + final cellKey = instance.name; + emittedCellKeys[instance] = cellKey; + + if (submodule is Combinational || submodule is Sequential) { + NetlistUtils.collapseAlwaysBlockPorts( + definition, + instance, + cellPortDirs, + cellConnections, + getIds, + ); + _filterProceduralConstants( + instance, + cellPortDirs, + cellConnections, + ); + _renameProceduralPorts( + instance, + cellPortDirs, + cellConnections, + ); + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': mapped?.cellType ?? defaultCellType, + 'parameters': mapped?.parameters ?? {}, + 'attributes': {}, + 'port_directions': cellPortDirs, + 'connections': cellConnections, + }; + } + + definition.subModuleInstantiations + .where((instance) => !instance.needsInstantiation) + .map((instance) => emittedCellKeys[instance]) + .whereType() + .forEach(cells.remove); + } + + /// Emits port and internal netnames, fills unnamed connection coverage, + /// and optionally removes names for undriven wires. + void processNetnames({ + required List Function(List bits) applyAlias, + required Map arraySliceOldToNew, + required Map arrayConcatOldToNew, + required bool pruneUndriven, + required Set drivenBits, + }) { + final emittedNames = {}; + final isInlineSystemVerilog = module is InlineSystemVerilog; + + void addNetname( + String name, + List bits, { + bool hideName = false, + bool computed = false, + Map? logicType, + }) { + if (!emittedNames.add(name)) { + return; + } + netnames[name] = { + 'bits': bits, + if (hideName) 'hide_name': 1, + if (logicType != null) 'logic_type': logicType, + 'attributes': { + if (computed || isInlineSystemVerilog) 'computed': 1, + }, + }; + } + + for (final port in ports.entries) { + addNetname( + Sanitizer.sanitizeSV(port.key), + (port.value['bits']! as List).cast(), + logicType: port.value['logic_type'] as Map?, + ); + } + + if (synthDef != null) { + for (final entry in synthLogicIds.entries.where( + (entry) => !entry.key.isConstant && !entry.key.declarationCleared, + )) { + final synthLogic = entry.key; + final name = NetlistUtils.tryGetSynthLogicName(synthLogic); + if (name == null) { + continue; + } + var bits = applyAlias(entry.value.cast()); + if (arraySliceOldToNew.isNotEmpty && + synthLogic is SynthLogicArrayElement) { + bits = [ + for (final bit in bits) + bit is int ? (arraySliceOldToNew[bit] ?? bit) : bit, + ]; + } + if (arrayConcatOldToNew.isNotEmpty && + synthLogic is SynthLogicArrayElement) { + bits = [ + for (final bit in bits) + bit is int ? (arrayConcatOldToNew[bit] ?? bit) : bit, + ]; + } + final typeLogic = NetlistUtils.typeLogicFromSynthLogic(synthLogic); + addNetname( + Sanitizer.sanitizeSV(name), + bits, + logicType: typeLogic == null + ? null + : NetlistUtils.buildLogicType(typeLogic, bits), + ); + } + } + + for (final cell in cells.entries.where( + (entry) => entry.value['type'] == r'$const', + )) { + final connections = + cell.value['connections'] as Map>?; + if (connections != null && connections.isNotEmpty) { + addNetname(cell.key, connections.values.first, computed: true); + } + } + + final coveredIds = netnames.values + .expand( + (netname) => + ((netname! as Map)['bits'] as List?) ?? [], + ) + .whereType() + .toSet(); + for (final cell in cells.entries) { + final connections = + cell.value['connections'] as Map? ?? {}; + for (final connection in connections.entries) { + final missingBits = []; + for (final bit in connection.value as List) { + if (bit is int && coveredIds.add(bit)) { + missingBits.add(bit); + } + } + if (missingBits.isNotEmpty) { + addNetname( + Sanitizer.sanitizeSV('${cell.key}_${connection.key}'), + missingBits, + hideName: true, + ); + } + } + } + + if (pruneUndriven) { + netnames.removeWhere((_, rawNetname) { + final netname = rawNetname as Map?; + final bits = netname?['bits'] as List?; + if (bits == null) { + return false; + } + final integerBits = bits.whereType(); + return integerBits.isNotEmpty && !integerBits.any(drivenBits.contains); + }); + } + } + + /// Separates passthrough outputs and removes dead cells when requested. + void processCellCleanup({required bool enableDce}) { + final inputBitIds = ports.values + .where( + (port) => + port['direction'] == 'input' || port['direction'] == 'inout', + ) + .expand((port) => port['bits']! as List) + .whereType() + .toSet(); + var bufferIndex = 0; + for (final port in ports.entries.where( + (entry) => entry.value['direction'] == 'output', + )) { + final outputBits = (port.value['bits']! as List).cast(); + if (!outputBits.any( + (bit) => bit is int && inputBitIds.contains(bit), + )) { + continue; + } + final freshBits = List.generate( + outputBits.length, + (_) => allocateWireId(), + ); + cells['passthrough_buf_$bufferIndex'] = NetlistUtils.makeBufCell( + outputBits.length, + outputBits, + freshBits, + ); + port.value['bits'] = freshBits; + bufferIndex++; + } + + if (!enableDce) { + return; + } + var changed = true; + while (changed) { + changed = false; + final drivenIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'input', 'inout'}, + cellDirection: 'output', + ); + final consumedIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'output', 'inout'}, + cellDirection: 'input', + ); + + cells + ..removeWhere((_, rawCell) { + final cell = rawCell as Map; + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + final inputPorts = connections.entries.where( + (port) => directions[port.key] == 'input', + ); + if (inputPorts.isEmpty) { + return false; + } + final allUndriven = !inputPorts + .expand((port) => port.value as List) + .any( + (bit) => + (bit is int && drivenIds.contains(bit)) || bit is String, + ); + if (allUndriven) { + changed = true; + } + return allUndriven; + }) + ..removeWhere((_, rawCell) { + final cell = rawCell as Map; + final cellType = cell['type'] as String? ?? ''; + if (!cellType.startsWith(r'$')) { + return false; + } + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + final outputPorts = connections.entries.where( + (port) => directions[port.key] == 'output', + ); + if (outputPorts.isEmpty) { + return false; + } + final allUnconsumed = !outputPorts + .expand((port) => port.value as List) + .whereType() + .any(consumedIds.contains); + if (allUnconsumed) { + changed = true; + } + return allUnconsumed; + }); + } + } + + /// Emits constant driver cells and optionally removes floating constants. + void processConstants({ + required List Function(List bits) applyAlias, + required bool pruneFloating, + }) { + var constantIndex = 0; + final emittedConstantWires = {}; + for (final entry in synthLogicIds.entries + .where((entry) => entry.key.isConstant) + .where((entry) => !blockedConstSynthLogics.contains(entry.key)) + .where((entry) => entry.value.isNotEmpty)) { + final constant = NetlistUtils.constValueFromSynthLogic(entry.key); + if (constant == null) { + continue; + } + final resolvedIds = applyAlias(entry.value.cast()); + final firstWire = resolvedIds.firstWhere( + (bit) => bit is int, + orElse: () => -1, + ); + if (firstWire is int && firstWire >= 0) { + if (emittedConstantWires.contains(firstWire)) { + continue; + } + emittedConstantWires.addAll(resolvedIds.whereType()); + } + + final valuePart = NetlistUtils.constValuePart(constant); + final cellName = 'const_${constantIndex}_$valuePart'; + final valueLiteral = valuePart.replaceFirst('_', "'"); + cells[cellName] = { + 'hide_name': 0, + 'type': r'$const', + 'parameters': {}, + 'attributes': {}, + 'port_directions': {valueLiteral: 'output'}, + 'connections': >{valueLiteral: resolvedIds}, + }; + constantIndex++; + } + + if (!pruneFloating) { + return; + } + final consumedIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'output', 'inout'}, + cellDirection: 'input', + ); + cells.removeWhere((_, rawCell) { + final cell = rawCell as Map; + if (cell['type'] != r'$const') { + return false; + } + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + return !connections.entries + .where((port) => directions[port.key] == 'output') + .expand((port) => port.value as List) + .whereType() + .any(consumedIds.contains); + }); + } + + void _filterProceduralConstants( + SynthSubModuleInstantiation instance, + Map portDirections, + Map> connections, + ) { + final portsToRemove = []; + for (final port in connections.entries) { + final synthLogic = + instance.inputMapping[port.key] ?? instance.inOutMapping[port.key]; + if (synthLogic != null && NetlistUtils.isConstantSynthLogic(synthLogic)) { + portsToRemove.add(port.key); + blockedConstSynthLogics.add(synthLogic.resolved); + } + } + for (final portName in portsToRemove) { + connections.remove(portName); + portDirections.remove(portName); + } + } + + void _renameProceduralPorts( + SynthSubModuleInstantiation instance, + Map portDirections, + Map> connections, + ) { + final renames = {}; + for (final portName in connections.keys.toList()) { + final synthLogic = instance.inputMapping[portName] ?? + instance.outputMapping[portName] ?? + instance.inOutMapping[portName]; + if (synthLogic == null) { + continue; + } + final resolvedName = + NetlistUtils.tryGetSynthLogicName(synthLogic.resolved); + if (resolvedName != null && resolvedName != portName) { + renames[portName] = resolvedName; + } + } + + for (final rename in renames.entries) { + final bits = connections.remove(rename.key)!; + final direction = portDirections.remove(rename.key)!; + var newName = rename.value; + if (connections.containsKey(newName)) { + newName = '${rename.value}_${rename.key}'; + } + connections[newName] = bits; + portDirections[newName] = direction; + } + } +} diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_options.dart index 3d4095e4b..ea1fdc002 100644 --- a/lib/src/synthesizers/netlist/netlist_options.dart +++ b/lib/src/synthesizers/netlist/netlist_options.dart @@ -7,7 +7,10 @@ // 2026 March 12 // Author: Desmond Kirkpatrick -import 'package:rohd/src/synthesizers/netlist/leaf_cell_mapper.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart' hide SynthModuleStopPolicy; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_module_stop_policy.dart'; /// The current format version for netlist JSON produced by ROHD. const String netlistFormatVersion = '0.0.5'; @@ -27,11 +30,10 @@ const String netlistFormatVersion = '0.0.5'; /// module definition on demand. Results are cached; the first call /// may trigger a lazy `SynthBuilder` run on the requested subtree. /// -/// Both flows run the identical pipeline: `SynthBuilder` → -/// `collectModuleEntries` → `applyPostProcessingPasses`. Flow 1 -/// then strips cell connections from the cached data; Flow 2 returns -/// it verbatim. This guarantees cell keys and wire IDs are stable -/// across both flows. +/// Both flows retain complete per-module synthesis results. Flow 1 skips cell +/// connection copying while collecting the emitted JSON projection. This keeps +/// slim output lightweight while guaranteeing a later expanded request has the +/// same cell keys, wire IDs, and connectivity as an initially expanded request. /// /// Bundles all parameters that control netlist generation into a single /// object, making it easier to pass through call chains and to store @@ -45,10 +47,25 @@ const String netlistFormatVersion = '0.0.5'; /// final synth = NetlistSynthesizer(options: options); /// ``` class NetlistOptions { - /// The leaf-cell mapper used to convert ROHD leaf modules to Yosys - /// primitive cell types. When `null`, [LeafCellMapper.defaultMapper] - /// is used. - final LeafCellMapper? leafCellMapper; + /// The policy used to decide which modules stop hierarchy traversal and are + /// emitted as cells in their parent instead of as separate module + /// definitions. When `null`, [SynthModuleStopPolicy.netlist] is used. + /// + /// When this is provided, it owns the complete stopping policy and + /// [leafModuleTypes] is ignored. + final SynthModuleStopPolicy? moduleStopPolicy; + + /// Exact [Module.runtimeType]s that should stop netlist hierarchy traversal + /// and be emitted as cells in their parent. Defaults to [FlipFlop], which + /// contains internal sequential submodules but should be emitted as a `$dff` + /// netlist cell. + final List leafModuleTypes; + + /// The netlist-internal mapper used to convert selected leaf modules to + /// Yosys primitive cell types. When `null`, each synthesizer creates its own + /// mapper containing the default handlers. + @internal + final NetlistCellMapper? netlistCellMapper; /// When `true`, a single unified pass finds connected components of /// all transparent cells (`$buf`, `$slice`, `$concat`, @@ -63,12 +80,10 @@ class NetlistOptions { /// are entirely unconsumed. final bool enableDCE; - /// When `true`, the synthesizer produces "slim" output: the full - /// synthesis pipeline runs (including all post-processing passes), - /// but cell connection maps are stripped from the result. - /// Netnames and ports are still emitted with full wire-ID fidelity, - /// so a subsequent full-mode synthesis of the same module will - /// produce compatible wire IDs. + /// When `true`, the synthesizer produces "slim" output: cell connection maps + /// are not copied into the emitted JSON projection. Netnames and ports are + /// still emitted with full wire-ID fidelity, while per-module synthesis + /// results retain complete connectivity. final bool slimMode; /// When `true`, contiguous ascending runs of ≥3 integer bit IDs in @@ -90,7 +105,9 @@ class NetlistOptions { /// All parameters have sensible defaults matching the current /// netlist synthesizer behaviour. const NetlistOptions({ - this.leafCellMapper, + this.moduleStopPolicy, + this.leafModuleTypes = const [FlipFlop], + this.netlistCellMapper, this.collapseTransparentClusters = false, this.enableDCE = true, this.slimMode = false, diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart index f5e498740..fe7f0f88e 100644 --- a/lib/src/synthesizers/netlist/netlist_passes.dart +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -12,11 +12,13 @@ // 2025 February 11 // Author: Desmond Kirkpatrick +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; /// Post-processing optimization passes for netlist synthesis. /// /// All methods are static — no instances are created. +@internal class NetlistPasses { NetlistPasses._(); @@ -25,26 +27,71 @@ class NetlistPasses { static Map> collectModuleEntries( Iterable results, { Module? topModule, + bool includeCellConnections = true, }) { final allModules = >{}; for (final result in results) { if (result is NetlistSynthesisResult) { final typeName = result.instanceTypeName; - final attrs = Map.from(result.attributes); + final attrs = _copyObjectMap(result.attributes); if (topModule != null && result.module == topModule) { attrs['top'] = 1; } allModules[typeName] = { 'attributes': attrs, - 'ports': result.ports, - 'cells': result.cells, - 'netnames': result.netnames, + 'ports': _copyNestedMaps(result.ports), + 'cells': _copyCells( + result.cells, + includeConnections: includeCellConnections, + ), + 'netnames': _copyObjectMap(result.netnames), }; } } return allModules; } + static Map> _copyCells( + Map> source, { + required bool includeConnections, + }) => + { + for (final entry in source.entries) + entry.key: _copyObjectMap( + includeConnections + ? entry.value + : (Map.of(entry.value)..remove('connections')), + ), + }; + + static Map> _copyNestedMaps( + Map> source, + ) => + { + for (final entry in source.entries) + entry.key: _copyObjectMap(entry.value), + }; + + static Map _copyObjectMap(Map source) => { + for (final entry in source.entries) + entry.key: _copyJsonValue(entry.value), + }; + + static Object? _copyJsonValue(Object? value) { + if (value is Map) { + return { + for (final entry in value.entries) + entry.key as String: _copyJsonValue(entry.value), + }; + } + if (value is List) { + return [ + for (final element in value) _copyJsonValue(element), + ]; + } + return value; + } + // ════════════════════════════════════════════════════════════════════ // Unified transparent-cell clustering // ════════════════════════════════════════════════════════════════════ diff --git a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart new file mode 100644 index 000000000..6745c15c8 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -0,0 +1,138 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// netlist_synth_module_definition.dart +// Synth module definition specialization for netlist synthesis. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthModuleDefinition] that preserves cells for netlist synthesis. +@internal +class NetlistSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a netlist synthesis definition for [module]. + NetlistSynthModuleDefinition(Module module) : super(module) { + // Create explicit $slice cells for LogicArray input ports so the + // netlist shows select gates for element extraction rather than + // flat bit aliasing. + module.inputs.values.whereType().forEach( + _subsetReceiveArrayPort, + ); + + // Same for LogicArray outputs on submodules (received into this scope). + final subModuleOutputArrays = module.subModules + .expand((sub) => sub.outputs.values) + .whereType() + .toSet() + ..forEach(_subsetReceiveArrayPort); + + // Create explicit $concat cells for internal LogicArrays whose elements + // are driven independently (e.g. by constants) and then consumed by + // submodule input ports. This parallels what _subsetReceiveArrayPort does + // on the decomposition side. + // + // Skip arrays that were merged with a port array's SynthLogic; those are + // already structurally decomposed by the $slice cells created above. + // Also skip submodule output arrays that already received $slice cells. + final portArrays = { + ...module.inputs.values.whereType(), + ...module.outputs.values.whereType(), + ...module.inOuts.values.whereType(), + }; + final excludedArrays = { + ...portArrays, + ...subModuleOutputArrays, + }; + + void addNestedArrays(LogicArray array) { + for (final element in array.elements) { + if (element is LogicArray) { + excludedArrays.add(element); + addNestedArrays(element); + } + } + } + + { + ...portArrays, + ...subModuleOutputArrays, + }.forEach(addNestedArrays); + final portArraySynthLogics = {}; + for (final portArray in excludedArrays) { + final synthLogic = logicToSynthMap[portArray]; + if (synthLogic != null) { + portArraySynthLogics.add(synthLogic.resolved); + } + } + module.internalSignals.whereType().where((signal) { + if (excludedArrays.contains(signal)) { + return false; + } + final synthLogic = logicToSynthMap[signal]; + if (synthLogic == null) { + return false; + } + return !portArraySynthLogics.contains(synthLogic.resolved); + }).forEach(_concatAssembleArray); + } + + void _subsetReceiveArrayPort(LogicArray port) { + final portSynth = getSynthLogic(port)!; + + var index = 0; + for (final element in port.elements) { + final elementSynth = getSynthLogic(element)!; + internalSignals.add(elementSynth); + + final subsetModule = SynthArraySlice( + Logic(width: port.width, name: 'DUMMY'), + index, + index + element.width - 1, + destination: element, + ); + + getSynthSubModuleInstantiation(subsetModule) + ..setOutputMapping(subsetModule.subset.name, elementSynth) + ..setInputMapping(subsetModule.original.name, portSynth) + ..pickName(module); + + index += element.width; + } + } + + void _concatAssembleArray(LogicArray array) { + final arraySynth = getSynthLogic(array)!; + final dummyElements = [ + for (final element in array.elements) + Logic(width: element.width, name: 'DUMMY'), + ]; + + // Swizzle reverses its inputs, so reverse here to keep in0 aligned with + // element[0], the least-significant array element. + final concatModule = SynthArrayConcat( + dummyElements.reversed.toList(), + destination: array, + ); + final instantiation = getSynthSubModuleInstantiation(concatModule) + ..setOutputMapping(concatModule.out.name, arraySynth); + + for (var index = 0; index < array.elements.length; index++) { + final elementSynth = getSynthLogic(array.elements[index])!; + internalSignals.add(elementSynth); + final inputName = concatModule.inputs.keys.elementAt(index); + instantiation.setInputMapping(inputName, elementSynth); + } + + instantiation.pickName(module); + } + + @override + void process() { + // Netlist synthesis preserves every submodule as a cell. + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart index f07d29962..8984cdafc 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -9,10 +9,12 @@ import 'dart:convert'; +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; /// A [SynthesisResult] that holds the netlist representation of a single /// module level: its ports, cells, and netnames. +@internal class NetlistSynthesisResult extends SynthesisResult { /// The ports map: name → {direction, bits}. final Map> ports; diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index e19c56470..16104c319 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -9,158 +9,15 @@ import 'dart:convert'; +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -/// -/// Skips SystemVerilog-specific processing (chain collapsing, net connects, -/// inOut inline replacement) since netlist represents all sub-modules as -/// cells rather than inline assignment expressions. -class _NetlistSynthModuleDefinition extends SynthModuleDefinition { - _NetlistSynthModuleDefinition(Module module) : super(module) { - // Create explicit $slice cells for LogicArray input ports so the - // netlist shows select gates for element extraction rather than - // flat bit aliasing. - module.inputs.values.whereType().forEach( - _subsetReceiveArrayPort, - ); - - // Same for LogicArray outputs on submodules (received into this scope). - final subModuleOutputArrays = module.subModules - .expand((sub) => sub.outputs.values) - .whereType() - .toSet() - ..forEach(_subsetReceiveArrayPort); - - // Create explicit $concat cells for internal LogicArrays whose elements - // are driven independently (e.g. by constants) and then consumed by - // submodule input ports. This parallels what _subsetReceiveArrayPort does - // on the decomposition side. - // - // Skip arrays that were merged with a port array's SynthLogic — those - // are already structurally decomposed by the $slice cells created above - // and reassembling them would create a circular driver on the port bus. - // Also skip submodule output arrays that already received $slice cells. - final portArrays = { - ...module.inputs.values.whereType(), - ...module.outputs.values.whereType(), - ...module.inOuts.values.whereType(), - }; - final excludedArrays = { - ...portArrays, - ...subModuleOutputArrays, - }; - // For multi-dimensional arrays, also exclude nested sub-arrays. - // E.g. LogicArray([10,2],8) has 10 children each being LogicArray([2],8). - // Without this, the sub-arrays get spurious $concat cells creating - // multi-driver conflicts on the parent port's bits. - void addNestedArrays(LogicArray arr) { - for (final elem in arr.elements) { - if (elem is LogicArray) { - excludedArrays.add(elem); - addNestedArrays(elem); - } - } - } - - { - ...portArrays, - ...subModuleOutputArrays, - }.forEach(addNestedArrays); - final portArraySynthLogics = {}; - for (final pa in excludedArrays) { - final sl = logicToSynthMap[pa]; - if (sl != null) { - portArraySynthLogics.add(sl.replacement ?? sl); - } - } - module.internalSignals.whereType().where((sig) { - if (excludedArrays.contains(sig)) { - return false; - } - final sl = logicToSynthMap[sig]; - if (sl == null) { - return false; - } - final resolved = sl.replacement ?? sl; - return !portArraySynthLogics.contains(resolved); - }).forEach(_concatAssembleArray); - } - - /// Creates explicit `$slice` cells for each element of a [LogicArray] port. - /// - /// Each element gets a [_BusSubsetForArraySlice] that extracts its bit range - /// from the packed parent bus. This produces explicit select gates in the - /// netlist, making array decomposition visible and traceable. - void _subsetReceiveArrayPort(LogicArray port) { - final portSynth = getSynthLogic(port)!; - - var idx = 0; - for (final element in port.elements) { - final elemSynth = getSynthLogic(element)!; - internalSignals.add(elemSynth); - - final subsetMod = _BusSubsetForArraySlice( - Logic(width: port.width, name: 'DUMMY'), - idx, - idx + element.width - 1, - ); - - getSynthSubModuleInstantiation(subsetMod) - ..setOutputMapping(subsetMod.subset.name, elemSynth) - ..setInputMapping(subsetMod.original.name, portSynth) - // Pick a name now — this may be called after _pickNames() has run. - ..pickName(module); - - idx += element.width; - } - } - - /// Creates an explicit `$concat` cell that assembles a [LogicArray]'s - /// elements into the full packed array bus. - /// - /// This is the assembly counterpart to [_subsetReceiveArrayPort]: when - /// individual array elements are driven independently (e.g. by constants), - /// this makes the concatenation explicit as a visible gate in the netlist. - void _concatAssembleArray(LogicArray array) { - final arraySynth = getSynthLogic(array)!; - - // Build dummy signals matching each element's width. - final dummyElements = []; - for (final element in array.elements) { - dummyElements.add(Logic(width: element.width, name: 'DUMMY')); - } - - // Pass reversed dummies so that Swizzle's internal reversal cancels out, - // leaving in0 aligned with element[0] (LSB) and inN with element[N]. - final concatMod = _SwizzleForArrayConcat(dummyElements.reversed.toList()); - - final ssmi = getSynthSubModuleInstantiation(concatMod) - // Map the concat output to the full array. - ..setOutputMapping(concatMod.out.name, arraySynth); - - // Map each element input. - // Because we reversed dummies above, in0 corresponds to element[0], - // in1 to element[1], etc. - for (var i = 0; i < array.elements.length; i++) { - final elemSynth = getSynthLogic(array.elements[i])!; - internalSignals.add(elemSynth); - final inputName = concatMod.inputs.keys.elementAt(i); - ssmi.setInputMapping(inputName, elemSynth); - } - - // Pick a name now — this may be called after _pickNames() has run. - ssmi.pickName(module); - } - - @override - void process() { - // No SV-specific transformations -- we want every sub-module to remain - // as a cell in the JSON. - } -} - /// A simple [Synthesizer] that produces netlist-compatible JSON. /// /// Leverages [SynthModuleDefinition] for signal tracing, naming, and @@ -184,23 +41,32 @@ class NetlistSynthesizer extends Synthesizer { /// See [NetlistOptions] for documentation on individual fields. final NetlistOptions options; - /// Convenience accessor for the leaf-cell mapper. - LeafCellMapper get leafCellMapper => - options.leafCellMapper ?? LeafCellMapper.defaultMapper; + final SynthModuleStopPolicy _moduleStopPolicy; + + final NetlistCellMapper _netlistCellMapper; + + /// The hierarchy stopping policy used by this synthesizer. + SynthModuleStopPolicy get moduleStopPolicy => _moduleStopPolicy; + + /// Convenience accessor for the netlist-cell mapper. + @visibleForTesting + NetlistCellMapper get netlistCellMapper => _netlistCellMapper; /// Creates a [NetlistSynthesizer]. /// /// All synthesis parameters are bundled in [options]; see /// [NetlistOptions] for documentation on each field. - NetlistSynthesizer({this.options = const NetlistOptions()}); + NetlistSynthesizer({this.options = const NetlistOptions()}) + : _moduleStopPolicy = options.moduleStopPolicy ?? + SynthModuleStopPolicy.netlist( + leafModuleTypes: options.leafModuleTypes, + ), + _netlistCellMapper = + options.netlistCellMapper ?? NetlistCellMapper.withDefaults(); @override bool generatesDefinition(Module module) => - // Only modules with sub-modules generate their own module definition. - // Leaf modules (no children) become cells inside their parent. - // FlipFlop has internal Sequential sub-modules but should be emitted as - // a flat Yosys $dff primitive, not as a hierarchical module. - module is! FlipFlop && module.subModules.isNotEmpty; + moduleStopPolicy.generatesDefinition(module); @override SynthesisResult synthesize( @@ -215,231 +81,19 @@ class NetlistSynthesizer extends Synthesizer { attr['top'] = 1; } - // -- Build SynthModuleDefinition ------------------------------------ - // This does all signal tracing, naming, constant handling, - // assignment collapsing, and unused signal pruning. - final canBuildSynthDef = !(module is SystemVerilog && - module.generatedDefinitionType == DefinitionGenerationType.none); - final synthDef = - canBuildSynthDef ? _NetlistSynthModuleDefinition(module) : null; - - // -- Wire-ID allocation --------------------------------------------- - // Start wire IDs at 2 to avoid collision with Yosys constant string - // bits "0" and "1". JavaScript viewers coerce object keys to strings, - // so integer wire ID 0 becomes "0", clashing with the constant-bit - // string "0". - var nextId = 2; - - // Map from SynthLogic -> assigned wire-bit IDs. - final synthLogicIds = >{}; - - /// Allocate or retrieve wire IDs for a [SynthLogic]. - /// For constants, do NOT follow the replacement chain to ensure each - /// constant usage gets its own separate driver cell in netlist. - List getIds(SynthLogic sl) { - var resolved = sl; - // For non-constants, follow replacement chain to resolve merged logics. - // For constants, keep them separate to create distinct const drivers. - if (!sl.isConstant) { - resolved = NetlistUtils.resolveReplacement(resolved); - } - final ids = synthLogicIds.putIfAbsent( - resolved, - () => List.generate(resolved.width, (_) => nextId++), - ); - return ids; - } - - // -- Ports ----------------------------------------------------------- - final ports = >{}; - - final portGroups = [ - ('input', synthDef?.inputs, module.inputs), - ('output', synthDef?.outputs, module.outputs), - ('inout', synthDef?.inOuts, module.inOuts), - ]; - for (final (direction, synthLogics, modulePorts) in portGroups) { - if (synthLogics != null) { - for (final sl in synthLogics) { - final ids = getIds(sl); - final portName = NetlistUtils.portNameForSynthLogic(sl, modulePorts); - if (portName != null) { - final portLogic = modulePorts[portName]; - ports[portName] = { - 'direction': direction, - 'bits': ids, - if (portLogic != null) - 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), - }; - } - } - } else { - for (final entry in modulePorts.entries) { - final ids = List.generate(entry.value.width, (_) => nextId++); - ports[entry.key] = { - 'direction': direction, - 'bits': ids, - 'logic_type': NetlistUtils.buildLogicType(entry.value, ids), - }; - } - } - } - - // -- Pre-allocate IDs for internal signals in Module order ----------- - // This ensures that internals get IDs in the same order as - // Module.internalSignals, matching the diagnostics signal collection. - // Signals already allocated during the port phase are skipped by - // putIfAbsent. Synthesis-generated wires get IDs later (during cell - // emission), so they are naturally appended after internals. - // - // Three-tier ordering guarantee: - // Tier 0 (ports): inputs → outputs → inOuts [above] - // Tier 1 (internals): module.internalSignals [here] - // Tier 2 (synth): cell emission wires [below] - if (synthDef != null) { - module.internalSignals - .map((sig) => synthDef.logicToSynthMap[sig]) - .whereType() - .where((sl) => !sl.isConstant) - .forEach(getIds); - } - - // -- Cell emission --------------------------------------------------- - final cells = >{}; - - // Track constant SynthLogics consumed exclusively by - // Combinational/Sequential so we can suppress their driver cells. - final blockedConstSynthLogics = {}; - - // Track emitted cell keys per instance for purging later. - final emittedCellKeys = {}; - - if (synthDef != null) { - for (final instance in synthDef.subModuleInstantiations) { - if (!instance.needsInstantiation) { - continue; - } - - final sub = instance.module; - - final isLeaf = !generatesDefinition(sub); - final defaultCellType = - isLeaf ? sub.definitionName : getInstanceTypeOfModule(sub); - - // Build port directions and connections from instance mappings. - final rawPortDirs = {}; - final rawConnections = >{}; - - for (final (dir, mapping) in [ - ('input', instance.inputMapping), - ('output', instance.outputMapping), - ('inout', instance.inOutMapping), - ]) { - for (final e in mapping.entries) { - rawPortDirs[e.key] = dir; - final ids = getIds(e.value); - rawConnections[e.key] = ids.cast(); - } - } - - // Map leaf cells to Yosys primitive types where possible. - final mapped = isLeaf - ? leafCellMapper.map(sub, rawPortDirs, rawConnections) - : null; - - final cellPortDirs = mapped?.portDirs ?? rawPortDirs; - final cellConns = mapped?.connections ?? rawConnections; - - // Use the SSMI's uniquified name as cell key to avoid - // collisions between identically-named modules (e.g. multiple - // struct_slice instances that share the same Module.name). - final cellKey = instance.name; - emittedCellKeys[instance] = cellKey; - - // -- Collapse bit-slice ports on Combinational / Sequential ---- - if (sub is Combinational || sub is Sequential) { - NetlistUtils.collapseAlwaysBlockPorts( - synthDef, - instance, - cellPortDirs, - cellConns, - getIds, - ); - } - - // -- Filter constant inputs from Combinational / Sequential ---- - if (sub is Combinational || sub is Sequential) { - final portsToRemove = []; - for (final pe in cellConns.entries) { - final portName = pe.key; - final synthLogic = instance.inputMapping[portName] ?? - instance.inOutMapping[portName]; - if (synthLogic != null && - NetlistUtils.isConstantSynthLogic(synthLogic)) { - portsToRemove.add(portName); - blockedConstSynthLogics.add(synthLogic.replacement ?? synthLogic); - } - } - for (final p in portsToRemove) { - cellConns.remove(p); - cellPortDirs.remove(p); - } - } - - // -- Rename Seq/Comb ports to Namer wire names ----------------- - // The port names from _Always.addInput/addOutput are internal - // (e.g. `_out`, `_enable`). Replace them with the Namer's - // resolved wire name so they match SystemVerilog and WaveDumper. - if (sub is Combinational || sub is Sequential) { - final renames = {}; - for (final portName in cellConns.keys.toList()) { - final sl = instance.inputMapping[portName] ?? - instance.outputMapping[portName] ?? - instance.inOutMapping[portName]; - if (sl == null) { - continue; // aggregated port, already renamed - } - final resolved = NetlistUtils.resolveReplacement(sl); - final namerName = NetlistUtils.tryGetSynthLogicName(resolved); - if (namerName != null && namerName != portName) { - renames[portName] = namerName; - } - } - for (final entry in renames.entries) { - final bits = cellConns.remove(entry.key)!; - final dir = cellPortDirs.remove(entry.key)!; - var newName = entry.value; - // Avoid collision with existing port names. - if (cellConns.containsKey(newName)) { - newName = '${entry.value}_${entry.key}'; - } - cellConns[newName] = bits; - cellPortDirs[newName] = dir; - } - } - - cells[cellKey] = { - 'hide_name': 0, - 'type': mapped?.cellType ?? defaultCellType, - 'parameters': mapped?.parameters ?? {}, - 'attributes': {}, - 'port_directions': cellPortDirs, - 'connections': cellConns, - }; - } - } - - // -- Remove cells that were cleared by collapseAlwaysBlockPorts ------ - // Because the iteration order may process a Swizzle/BusSubset cell - // BEFORE the Combinational/Sequential that clears it, we need to purge - // stale cells after all collapsing has been applied. - if (synthDef != null) { - synthDef.subModuleInstantiations - .where((i) => !i.needsInstantiation) - .map((i) => emittedCellKeys[i]) - .whereType() - .forEach(cells.remove); - } + final translation = NetlistModuleTranslation( + module, + netlistCellMapper: netlistCellMapper, + generatesDefinition: generatesDefinition, + getInstanceTypeOfModule: getInstanceTypeOfModule, + ) + ..processPorts() + ..processInternalWires() + ..processCells(); + final synthDef = translation.synthDef; + final ports = translation.ports; + final cells = translation.cells; + final getIds = translation.getIds; // -- Wire-ID aliasing from remaining assignments ------------------- // SynthModuleDefinition._collapseAssignments may leave assignments @@ -460,7 +114,6 @@ class NetlistSynthesizer extends Synthesizer { // entries from the same LogicStructure into a single multi-port // `$struct_unpack` cell. final structFieldCells = <({ - List parentIds, List elemIds, int offset, int width, @@ -469,11 +122,11 @@ class NetlistSynthesizer extends Synthesizer { List fullParentIds, })>[]; - // Pending $struct_compose cells: for output struct ports, instead of + // Pending $struct_pack fields: for output struct ports, instead of // aliasing port bits to leaf bits (which causes "shorting"), we - // collect composition operations and emit explicit cells later. + // collect structure-pack field operations and emit explicit cells later. // Each entry records: field (src) → port sub-range [lower:upper]. - final structComposeCells = <({ + final structPackFields = <({ List srcIds, List dstIds, int dstLowerIndex, @@ -545,7 +198,7 @@ class NetlistSynthesizer extends Synthesizer { if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { // Record as pending compose cell instead of aliasing. - structComposeCells.add(( + structPackFields.add(( srcIds: srcIds, dstIds: dstIds, dstLowerIndex: pa.dstLowerIndex, @@ -584,7 +237,7 @@ class NetlistSynthesizer extends Synthesizer { // schematic, rather than collapsing them into parent bit ranges. // // For arrays with explicit $slice/$concat cells (from - // _BusSubsetForArraySlice / _SwizzleForArrayConcat), aliasing + // SynthArraySlice / SynthArrayConcat), aliasing // is skipped entirely — the cells provide the structural link. // // Applied to ALL instances (ports AND internal signals) since @@ -594,7 +247,7 @@ class NetlistSynthesizer extends Synthesizer { // - LogicStructure (non-array): walks leafElements (recursive) // - LogicArray: walks elements (direct children only, since // each element is already a flat bitvector). - // For input array ports that have _BusSubsetForArraySlice + // For input array ports that have SynthArraySlice // cells, we skip aliasing so the $slice cells provide the // structural connection (see _subsetReceiveArrayPort). // @@ -616,12 +269,12 @@ class NetlistSynthesizer extends Synthesizer { idAlias[childId] = parentId; } - // Collect LogicArray ports that have explicit array_slice or - // array_concat submodules so we can skip aliasing them (the + // Collect LogicArray ports that have explicit array slice or + // array concat submodules so we can skip aliasing them (the // $slice/$concat cells provide the structural link). final arraysWithExplicitCells = {}; for (final inst in synthDef.subModuleInstantiations) { - if (inst.module is _BusSubsetForArraySlice) { + if (inst.module is SynthArraySlice) { // The input of the BusSubset is the array port. for (final inputSL in inst.inputMapping.values) { final logic = synthDef.logicToSynthMap.entries @@ -634,7 +287,7 @@ class NetlistSynthesizer extends Synthesizer { arraysWithExplicitCells.add(logic); } // Also check the resolved replacement chain. - final resolved = NetlistUtils.resolveReplacement(inputSL); + final resolved = inputSL.resolved; final logic2 = synthDef.logicToSynthMap.entries .where((e) => e.value == resolved) .map((e) => e.key) @@ -644,7 +297,7 @@ class NetlistSynthesizer extends Synthesizer { } } } - if (inst.module is _SwizzleForArrayConcat) { + if (inst.module is SynthArrayConcat) { // The output of the Swizzle is the array signal. for (final outputSL in inst.outputMapping.values) { final logic = synthDef.logicToSynthMap.entries @@ -695,7 +348,7 @@ class NetlistSynthesizer extends Synthesizer { // by the netlist evaluator. // // Skip output struct ports of the current module — those are - // handled by $struct_compose cells (from Step 2). + // handled by $struct_pack cells (from Step 2). if (outputStructPortLogics.contains(logic)) { continue; } @@ -709,7 +362,6 @@ class NetlistSynthesizer extends Synthesizer { : parentIds.length - idx; if (sliceLen > 0) { structFieldCells.add(( - parentIds: parentIds.sublist(idx, idx + sliceLen), elemIds: elemIds.sublist(0, sliceLen), offset: idx, width: sliceLen, @@ -734,7 +386,6 @@ class NetlistSynthesizer extends Synthesizer { : parentIds.length - leafIdx; if (sliceLen > 0) { structFieldCells.add(( - parentIds: parentIds.sublist(leafIdx, leafIdx + sliceLen), elemIds: leafIds.sublist(0, sliceLen), offset: leafIdx, width: sliceLen, @@ -775,7 +426,7 @@ class NetlistSynthesizer extends Synthesizer { ? bits : bits.map((b) => b is int ? resolveAlias(b) : b).toList(); - // -- Break shared wire IDs for array_slice cells ----------------------- + // -- Break shared wire IDs for array slice cells ----------------------- // (Populated inside the alias block below; declared here so netnames // can reference it later.) final arraySliceOldToNew = {}; @@ -799,14 +450,14 @@ class NetlistSynthesizer extends Synthesizer { // trivial and it would be elided below. // // To preserve the structural decomposition in the schematic, we - // allocate fresh wire IDs for each array_slice Y output, then + // allocate fresh wire IDs for each array slice Y output, then // redirect all other cells that consume those IDs as inputs to // read from the fresh IDs instead. The slice input A keeps the // original parent-array IDs, so the data flow becomes: // parent (original IDs) → slice A → slice Y (fresh IDs) → consumer for (final cellEntry in cells.entries) { - if (!cellEntry.key.startsWith('array_slice')) { + if (!cellEntry.key.startsWith(Namer.synthArraySliceOperationName)) { continue; } final cell = cellEntry.value as Map; @@ -820,7 +471,12 @@ class NetlistSynthesizer extends Synthesizer { final oldBits = (portEntry.value as List).cast(); conns[portEntry.key] = [ for (final b in oldBits) - b is int ? arraySliceOldToNew.putIfAbsent(b, () => nextId++) : b, + b is int + ? arraySliceOldToNew.putIfAbsent( + b, + translation.allocateWireId, + ) + : b, ]; } } @@ -829,7 +485,7 @@ class NetlistSynthesizer extends Synthesizer { // gets replaced with the corresponding fresh ID. if (arraySliceOldToNew.isNotEmpty) { for (final cellEntry in cells.entries) { - if (cellEntry.key.startsWith('array_slice')) { + if (cellEntry.key.startsWith(Namer.synthArraySliceOperationName)) { continue; // skip the slice cells themselves } final cell = cellEntry.value as Map; @@ -852,7 +508,7 @@ class NetlistSynthesizer extends Synthesizer { } // -- Elide trivial $slice cells ---------------------------------- - // Also elide struct_slice cells (`_BusSubsetForStructSlice` + // Also elide struct_slice cells ([SynthStructureSlice] // instances from `_subsetReceiveStructPort`) because the new // `$struct_unpack` cells emitted below supersede them with // better-named field-level connections. @@ -862,7 +518,7 @@ class NetlistSynthesizer extends Synthesizer { } // Unconditionally remove struct_slice cells — they are // duplicated by $struct_unpack cells which carry field names. - if (cellKey.startsWith('struct_slice')) { + if (cellKey.startsWith(Namer.synthStructureSliceOperationName)) { return true; } final params = cell['parameters'] as Map?; @@ -893,7 +549,6 @@ class NetlistSynthesizer extends Synthesizer { final groups = parentIds, List elemIds, int offset, int width, @@ -939,115 +594,9 @@ class NetlistSynthesizer extends Synthesizer { // Derive struct name for the cell key. final structName = Sanitizer.sanitizeSV(parentLogic.name); - // Build element range table for the parent struct so we can - // derive proper field names even when the leaf Logic objects - // have unpreferred names like `_swizzled`. - // Same strategy as $struct_pack: walk the hierarchy collecting - // (start, end, name, path, indexInParent) and look up the - // narrowest non-unpreferred range for each field offset. - final suElementRanges = <({ - int start, - int end, - String name, - String path, - int indexInParent, - })>[]; - if (parentLogic is LogicStructure) { - void walkStruct( - LogicStructure struct, - int baseOffset, - String parentPath, - ) { - var offset = baseOffset; - for (var idx = 0; idx < struct.elements.length; idx++) { - final elem = struct.elements[idx]; - final elemEnd = offset + elem.width; - final elemPath = - parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; - suElementRanges.add(( - start: offset, - end: elemEnd, - name: elem.name, - path: elemPath, - indexInParent: idx, - )); - if (elem is LogicStructure && elem is! LogicArray) { - walkStruct(elem, offset, elemPath); - } - offset = elemEnd; - } - } - - walkStruct(parentLogic, 0, ''); - } - - String suFieldNameFor(int fieldOffset, String fallbackName) { - ({ - int start, - int end, - String name, - String path, - int indexInParent - })? bestNamed; - ({ - int start, - int end, - String name, - String path, - int indexInParent - })? bestAny; - ({ - int start, - int end, - String name, - String path, - int indexInParent - })? narrowest; - - for (final r in suElementRanges) { - if (fieldOffset >= r.start && fieldOffset < r.end) { - final span = r.end - r.start; - if (narrowest == null || - span < (narrowest.end - narrowest.start)) { - narrowest = r; - } - if (bestAny == null || span < (bestAny.end - bestAny.start)) { - bestAny = r; - } - if (!Naming.isUnpreferred(r.name)) { - if (bestNamed == null || - span < (bestNamed.end - bestNamed.start)) { - bestNamed = r; - } - } - } - } - - if (bestNamed != null) { - if (narrowest != null && - (narrowest.end - narrowest.start) < - (bestNamed.end - bestNamed.start)) { - final bestNamedPrefix = bestNamed.path; - if (narrowest.path.length > bestNamedPrefix.length && - narrowest.path.startsWith(bestNamedPrefix)) { - final suffix = narrowest.path.substring( - bestNamedPrefix.length + 1, - ); - if (!Naming.isUnpreferred(suffix)) { - return '${bestNamed.name}_$suffix'; - } - } - return '${bestNamed.name}_${narrowest.indexInParent}'; - } - return bestNamed.name; - } - // All matching elements have unpreferred names — use the - // narrowest element's positional index as discriminator. - if (narrowest != null && Naming.isUnpreferred(narrowest.name)) { - return 'anonymous_${narrowest.indexInParent}'; - } - return bestAny?.name ?? fallbackName; - } + final structLayout = parentLogic is LogicStructure + ? SynthStructureLayout(parentLogic) + : null; // Build port_directions and connections with one output per field. final portDirs = {'A': 'input'}; @@ -1055,7 +604,12 @@ class NetlistSynthesizer extends Synthesizer { for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - final fieldName = suFieldNameFor(f.offset, f.elemLogic.name); + final fieldName = structLayout?.fieldNameAt( + f.offset, + fallbackName: f.elemLogic.name, + anonymousUnpreferred: true, + ) ?? + f.elemLogic.name; // Disambiguate duplicate field names with index suffix. var portName = fieldName; if (portDirs.containsKey(portName)) { @@ -1072,10 +626,12 @@ class NetlistSynthesizer extends Synthesizer { }; for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - params['FIELD_${i}_NAME'] = suFieldNameFor( - f.offset, - f.elemLogic.name, - ); + params['FIELD_${i}_NAME'] = structLayout?.fieldNameAt( + f.offset, + fallbackName: f.elemLogic.name, + anonymousUnpreferred: true, + ) ?? + f.elemLogic.name; params['FIELD_${i}_OFFSET'] = f.offset; params['FIELD_${i}_WIDTH'] = f.width; } @@ -1097,10 +653,10 @@ class NetlistSynthesizer extends Synthesizer { // multi-port cell per group. Each group has: // • one input port per non-trivial field // • output port Y: the full packed output bus - // This replaces the old per-field $struct_compose cells. - if (structComposeCells.isNotEmpty) { + // This emits explicit structure packing cells. + if (structPackFields.isNotEmpty) { // Group by destination SynthLogic identity. - final composeGroups = srcIds, @@ -1110,12 +666,11 @@ class NetlistSynthesizer extends Synthesizer { SynthLogic srcSynthLogic, SynthLogic dstSynthLogic, })>>{}; - for (final sc in structComposeCells) { - (composeGroups[sc.dstSynthLogic] ??= []).add(sc); + for (final sc in structPackFields) { + (packGroups[sc.dstSynthLogic] ??= []).add(sc); } - var spIdx = 0; - for (final entry in composeGroups.entries) { + for (final entry in packGroups.entries) { final dstSynthLogic = entry.key; final fields = entry.value; final resolvedDstBits = applyAlias(fields.first.dstIds.cast()); @@ -1148,141 +703,18 @@ class NetlistSynthesizer extends Synthesizer { continue; } - // Derive struct name from the destination Logic. + // Derive struct metadata from the destination Logic. final dstLogic = dstSynthLogic.logics.firstOrNull; - final structName = dstLogic != null - ? Sanitizer.sanitizeSV(dstLogic.name) - : 'struct_$spIdx'; - - // Build a lookup from bit offset to the best struct element - // name, so that field names come from the struct definition - // (e.g. "data", "last", "poison") rather than the source - // signal name (which may be an internal like "_swizzled"). - // - // Elements pack LSB-first via `rswizzle`, so element[0] - // starts at offset 0, element[1] at element[0].width, etc. - // - // We collect (start, end, name, path, parentElementIndex) - // ranges for every element at every nesting level. The - // `path` carries the chain of parent struct names so we can - // produce qualified names like "mmu_info_mmuSid". When - // leaf names are unpreferred, `parentElementIndex` provides - // a fallback discriminator like "mmu_info_0". - final dstElementRanges = <({ - int start, - int end, - String name, - String path, - int indexInParent, - })>[]; - if (dstLogic is LogicStructure) { - void walkStruct( - LogicStructure struct, - int baseOffset, - String parentPath, - ) { - var offset = baseOffset; - for (var idx = 0; idx < struct.elements.length; idx++) { - final elem = struct.elements[idx]; - final elemEnd = offset + elem.width; - final elemPath = - parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; - dstElementRanges.add(( - start: offset, - end: elemEnd, - name: elem.name, - path: elemPath, - indexInParent: idx, - )); - if (elem is LogicStructure && elem is! LogicArray) { - walkStruct(elem, offset, elemPath); - } - offset = elemEnd; - } - } - - walkStruct(dstLogic, 0, ''); - } - - /// Look up the field name for a compose entry by finding the - /// best struct element whose range contains [dstLowerIndex]. - /// - /// Strategy (deepest-first): - /// 1. Find the narrowest element with a non-unpreferred name. - /// 2. If a narrower unpreferred leaf exists under a named - /// parent, try to qualify with the leaf's proper name - /// (e.g. `mmu_info_mmuSid`). - /// 3. If the leaf name is also unpreferred, fall back to the - /// parent name qualified by the leaf's positional index - /// (e.g. `mmu_info_0`, `mmu_info_1`). - /// 4. Falls back to the resolved source SynthLogic name. - String fieldNameFor(int dstLowerIndex, SynthLogic srcSynthLogic) { - ({ - int start, - int end, - String name, - String path, - int indexInParent - })? bestNamed; - ({ - int start, - int end, - String name, - String path, - int indexInParent - })? bestAny; - ({ - int start, - int end, - String name, - String path, - int indexInParent - })? narrowest; - - for (final r in dstElementRanges) { - if (dstLowerIndex >= r.start && dstLowerIndex < r.end) { - final span = r.end - r.start; - if (narrowest == null || - span < (narrowest.end - narrowest.start)) { - narrowest = r; - } - if (bestAny == null || span < (bestAny.end - bestAny.start)) { - bestAny = r; - } - if (!Naming.isUnpreferred(r.name)) { - if (bestNamed == null || - span < (bestNamed.end - bestNamed.start)) { - bestNamed = r; - } - } - } - } - - if (bestNamed != null) { - // Check if there's a narrower child element under - // bestNamed that we can use to discriminate. - if (narrowest != null && - (narrowest.end - narrowest.start) < - (bestNamed.end - bestNamed.start)) { - final bestNamedPrefix = bestNamed.path; - // Try using the child's proper name as qualifier. - if (narrowest.path.length > bestNamedPrefix.length && - narrowest.path.startsWith(bestNamedPrefix)) { - final suffix = narrowest.path.substring( - bestNamedPrefix.length + 1, - ); - if (!Naming.isUnpreferred(suffix)) { - return '${bestNamed.name}_$suffix'; - } - } - // Child has unpreferred name — use positional index. - return '${bestNamed.name}_${narrowest.indexInParent}'; - } - return bestNamed.name; - } - return bestAny?.name ?? - NetlistUtils.resolveReplacement(srcSynthLogic).name; - } + final structName = + dstLogic != null ? Sanitizer.sanitizeSV(dstLogic.name) : 'struct'; + final structLayout = + dstLogic is LogicStructure ? SynthStructureLayout(dstLogic) : null; + final cellName = dstLogic != null + ? Namer.synthOperationInstanceName( + operationName: Namer.synthStructureConcatOperationName, + destination: dstLogic, + ) + : Namer.synthStructureConcatOperationName; // Build port_directions and connections. final portDirs = {}; @@ -1290,7 +722,11 @@ class NetlistSynthesizer extends Synthesizer { for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - final fieldName = fieldNameFor(f.dstLowerIndex, f.srcSynthLogic); + final fieldName = structLayout?.fieldNameAt( + f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name, + ) ?? + f.srcSynthLogic.resolved.name; var portName = fieldName; if (portDirs.containsKey(portName)) { portName = '${fieldName}_$i'; @@ -1310,15 +746,16 @@ class NetlistSynthesizer extends Synthesizer { }; for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - params['FIELD_${i}_NAME'] = fieldNameFor( - f.dstLowerIndex, - f.srcSynthLogic, - ); + params['FIELD_${i}_NAME'] = structLayout?.fieldNameAt( + f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name, + ) ?? + f.srcSynthLogic.resolved.name; params['FIELD_${i}_OFFSET'] = f.dstLowerIndex; params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; } - cells['struct_pack_${spIdx}_$structName'] = { + cells['${cellName}_$structName'] = { 'hide_name': 0, 'type': r'$struct_pack', 'parameters': params, @@ -1326,248 +763,17 @@ class NetlistSynthesizer extends Synthesizer { 'port_directions': portDirs, 'connections': conns, }; - spIdx++; - } - } - - // -- Passthrough buffer insertion ------------------------------------ - // When a signal passes directly from an input port to an output port, - // they share the same wire IDs after aliasing. This causes the signal - // to appear routed *around* the module in the netlist rather than - // *through* it. Insert a `$buf` cell to break the wire-ID sharing, - // giving the output port fresh IDs driven by the buffer. - { - final inputBitIds = ports.values - .where((p) => p['direction'] == 'input' || p['direction'] == 'inout') - .expand((p) => p['bits']! as List) - .whereType() - .toSet(); - - // Check each output port for overlap with input bits. - var bufIdx = 0; - for (final p in ports.entries.where( - (p) => p.value['direction'] == 'output', - )) { - final outBits = (p.value['bits']! as List).cast(); - if (!outBits.any((b) => b is int && inputBitIds.contains(b))) { - continue; - } - - // Allocate fresh wire IDs for the output side of the buffer. - final freshBits = List.generate( - outBits.length, - (_) => nextId++, - ); - - // Insert a $buf cell: input = original (shared) IDs, - // output = fresh IDs. - cells['passthrough_buf_$bufIdx'] = NetlistUtils.makeBufCell( - outBits.length, - outBits, - freshBits, - ); - - // Update the output port to use the fresh IDs. - p.value['bits'] = freshBits; - bufIdx++; - } - } - - // -- Dead-cell elimination (DCE) ------------------------------------- - // After aliasing and elision, some cells may have inputs whose wire - // IDs are not driven by any cell output or module input port. This - // typically happens when a LogicStructure's `packed` representation - // creates a Swizzle chain whose inputs reference sub-module-internal - // signals that are not accessible from the synthesised module's - // scope. Iteratively remove such dead cells using both forward - // (all-inputs-undriven) and backward (all-outputs-unconsumed) DCE. - if (options.enableDCE) { - var dceChanged = true; - while (dceChanged) { - dceChanged = false; - - // Build set of driven wire IDs (from input/inout ports and cell - // outputs). - final drivenIds = { - ...ports.values - .where( - (p) => p['direction'] == 'input' || p['direction'] == 'inout', - ) - .expand((p) => p['bits']! as List) - .whereType(), - ...cells.values.expand((c) { - final cell = c as Map; - final conns = cell['connections']! as Map; - final pdirs = cell['port_directions']! as Map; - return conns.entries - .where((pe) => pdirs[pe.key] == 'output') - .expand((pe) => pe.value as List) - .whereType(); - }), - }; - - // Build set of consumed wire IDs (from output/inout ports and - // cell inputs). - final consumedIds = { - ...ports.values - .where( - (p) => p['direction'] == 'output' || p['direction'] == 'inout', - ) - .expand((p) => p['bits']! as List) - .whereType(), - ...cells.values.expand((c) { - final cell = c as Map; - final conns = cell['connections']! as Map; - final pdirs = cell['port_directions']! as Map; - return conns.entries - .where((pe) => pdirs[pe.key] == 'input') - .expand((pe) => pe.value as List) - .whereType(); - }), - }; - - // Forward DCE: remove cells whose inputs are ALL undriven. - cells - ..removeWhere((cellKey, cellVal) { - final cell = cellVal as Map; - final conns = cell['connections']! as Map; - final pdirs = cell['port_directions']! as Map; - final inputPorts = conns.entries.where( - (pe) => pdirs[pe.key] == 'input', - ); - if (inputPorts.isEmpty) { - return false; - } - final allUndriven = !inputPorts - .expand((pe) => pe.value as List) - .any((b) => (b is int && drivenIds.contains(b)) || b is String); - if (allUndriven) { - dceChanged = true; - return true; - } - return false; - }) - // Backward DCE: remove cells whose outputs are ALL unconsumed. - // Preserve non-leaf cells (user module instances) — their type - // does not start with '$' (Yosys primitive convention). Users - // expect to see all instantiated modules in the schematic even - // when outputs are unconnected. - ..removeWhere((cellKey, cellVal) { - final cell = cellVal as Map; - final cellType = cell['type'] as String? ?? ''; - if (!cellType.startsWith(r'$')) { - return false; - } - final conns = cell['connections']! as Map; - final pdirs = cell['port_directions']! as Map; - final outputPorts = conns.entries.where( - (pe) => pdirs[pe.key] == 'output', - ); - if (outputPorts.isEmpty) { - return false; - } - final allUnconsumed = !outputPorts - .expand((pe) => pe.value as List) - .whereType() - .any(consumedIds.contains); - if (allUnconsumed) { - dceChanged = true; - return true; - } - return false; - }); - } - } - - // -- Constant driver cells ------------------------------------------- - // Generated AFTER the aliasing pass so that constants discovered - // during aliasing (via getIds(assignment.src)) are included. - // Constant IDs may have been redirected by step 3 (struct/array - // child→parent aliasing), so apply alias resolution to their - // connection bits. - { - var constIdx = 0; - final emittedConstWires = {}; - for (final entry in synthLogicIds.entries - .where((e) => e.key.isConstant) - .where((e) => !blockedConstSynthLogics.contains(e.key)) - .where((e) => e.value.isNotEmpty)) { - final sl = entry.key; - final constValue = NetlistUtils.constValueFromSynthLogic(sl); - if (constValue == null) { - continue; - } - final ids = entry.value; - - // Resolve aliases and skip if these wires are already driven - // by a previously emitted $const cell (can happen when aliasing - // merges two SynthLogic constants onto the same wire IDs). - final resolvedIds = applyAlias(ids.cast()); - final firstWire = resolvedIds.firstWhere( - (b) => b is int, - orElse: () => -1, - ); - if (firstWire is int && firstWire >= 0) { - if (emittedConstWires.contains(firstWire)) { - continue; - } - emittedConstWires.addAll(resolvedIds.whereType()); - } - - final valuePart = NetlistUtils.constValuePart(constValue); - final cellName = 'const_${constIdx}_$valuePart'; - final valueLiteral = valuePart.replaceFirst('_', "'"); - - cells[cellName] = { - 'hide_name': 0, - 'type': r'$const', - 'parameters': {}, - 'attributes': {}, - 'port_directions': {valueLiteral: 'output'}, - 'connections': >{valueLiteral: resolvedIds}, - }; - constIdx++; } } - // -- Remove floating $const cells ------------------------------------ - // The $const cells were emitted after the main DCE pass, so they - // may reference wire IDs that no cell input or output port consumes. - if (options.enableDCE) { - final consumedByInputs = { - ...ports.values - .where( - (p) => p['direction'] == 'output' || p['direction'] == 'inout', - ) - .expand((p) => p['bits']! as List) - .whereType(), - ...cells.values.expand((c) { - final cell = c as Map; - final conns = cell['connections']! as Map; - final pdirs = cell['port_directions']! as Map; - return conns.entries - .where((pe) => pdirs[pe.key] == 'input') - .expand((pe) => pe.value as List) - .whereType(); - }), - }; - - cells.removeWhere((cellKey, cellVal) { - final cell = cellVal as Map; - if (cell['type'] != r'$const') { - return false; - } - final conns = cell['connections']! as Map; - final pdirs = cell['port_directions']! as Map; - return !conns.entries - .where((pe) => pdirs[pe.key] == 'output') - .expand((pe) => pe.value as List) - .whereType() - .any(consumedByInputs.contains); - }); - } + translation + ..processCellCleanup(enableDce: options.enableDCE) + ..processConstants( + applyAlias: applyAlias, + pruneFloating: options.enableDCE, + ); - // -- Break shared wire IDs for array_concat cells -------------------- + // -- Break shared wire IDs for array concat cells -------------------- // After aliasing, the concat inputs share the same wire IDs as the // concat Y output (because LogicArray elements share the parent's // bit storage). This makes the concat transparent -- constants @@ -1581,7 +787,7 @@ class NetlistSynthesizer extends Synthesizer { final arrayConcatOldToNew = {}; for (final cellEntry in cells.entries) { - if (!cellEntry.key.startsWith('array_concat')) { + if (!cellEntry.key.startsWith(Namer.synthArrayConcatOperationName)) { continue; } final cell = cellEntry.value as Map; @@ -1595,7 +801,12 @@ class NetlistSynthesizer extends Synthesizer { final oldBits = (portEntry.value as List).cast(); conns[portEntry.key] = [ for (final b in oldBits) - b is int ? arrayConcatOldToNew.putIfAbsent(b, () => nextId++) : b, + b is int + ? arrayConcatOldToNew.putIfAbsent( + b, + translation.allocateWireId, + ) + : b, ]; } } @@ -1604,7 +815,7 @@ class NetlistSynthesizer extends Synthesizer { // gets replaced with the corresponding fresh ID. if (arrayConcatOldToNew.isNotEmpty) { for (final cellEntry in cells.entries) { - if (cellEntry.key.startsWith('array_concat')) { + if (cellEntry.key.startsWith(Namer.synthArrayConcatOperationName)) { continue; // skip the concat cells themselves } final cell = cellEntry.value as Map; @@ -1626,185 +837,27 @@ class NetlistSynthesizer extends Synthesizer { } } - // -- Netnames -------------------------------------------------------- - final netnames = {}; - final emittedNames = {}; - - // InlineSystemVerilog modules are pure combinational — all their - // signals are derivable from the gate netlist. - final isInlineSV = module is InlineSystemVerilog; - - void addNetname( - String name, - List bits, { - bool hideName = false, - bool computed = false, - Map? logicType, - }) { - if (emittedNames.contains(name)) { - return; - } - emittedNames.add(name); - netnames[name] = { - 'bits': bits, - if (hideName) 'hide_name': 1, - if (logicType != null) 'logic_type': logicType, - 'attributes': { - if (computed || isInlineSV) 'computed': 1, - }, - }; - } - - // Port nets (already aliased above). - for (final p in ports.entries) { - addNetname( - Sanitizer.sanitizeSV(p.key), - (p.value['bits']! as List).cast(), - logicType: p.value['logic_type'] as Map?, - ); - } - - // Named signals from SynthModuleDefinition. - if (synthDef != null) { - for (final entry in synthLogicIds.entries.where( - (e) => !e.key.isConstant && !e.key.declarationCleared, - )) { - final sl = entry.key; - final name = NetlistUtils.tryGetSynthLogicName(sl); - if (name != null) { - var bits = applyAlias(entry.value.cast()); - // For element signals whose IDs were remapped by the - // array_slice fresh-ID pass, apply that mapping so the - // element netname matches the slice output (fresh) IDs. - if (arraySliceOldToNew.isNotEmpty && sl is SynthLogicArrayElement) { - bits = bits - .map((b) => b is int ? (arraySliceOldToNew[b] ?? b) : b) - .toList(); - } - // For element signals whose IDs were remapped by the - // array_concat fresh-ID pass, apply that mapping so the - // element netname matches the concat input (fresh) IDs. - if (arrayConcatOldToNew.isNotEmpty && sl is SynthLogicArrayElement) { - bits = bits - .map((b) => b is int ? (arrayConcatOldToNew[b] ?? b) : b) - .toList(); - } - final typeLogic = NetlistUtils.typeLogicFromSynthLogic(sl); - addNetname( - Sanitizer.sanitizeSV(name), - bits, - logicType: typeLogic != null - ? NetlistUtils.buildLogicType(typeLogic, bits) - : null, - ); - } - } - } - - // Constant netnames for non-blocked constants (already aliased via - // cell connections above). - for (final cellEntry in cells.entries.where( - (e) => e.value['type'] == r'$const', - )) { - final conns = - cellEntry.value['connections'] as Map>?; - if (conns != null && conns.isNotEmpty) { - addNetname(cellEntry.key, conns.values.first, computed: true); - } - } - - // -- Ensure every bit ID in cell connections has a netname ------------ - { - final coveredIds = netnames.values - .expand( - (nn) => ((nn! as Map)['bits'] as List?) ?? [], - ) - .whereType() - .toSet(); - - for (final cellEntry in cells.entries) { - final cellName = cellEntry.key; - final conns = - cellEntry.value['connections'] as Map? ?? {}; - for (final connEntry in conns.entries) { - final portName = connEntry.key; - final bits = connEntry.value as List; - final missingBits = []; - for (final b in bits) { - if (b is int && !coveredIds.contains(b)) { - missingBits.add(b); - coveredIds.add(b); - } - } - if (missingBits.isNotEmpty) { - addNetname( - Sanitizer.sanitizeSV('${cellName}_$portName'), - missingBits, - hideName: true, - ); - } - } - } - } - - // -- Remove orphaned netnames after DCE -------------------------------- - // DCE may remove cells that drove certain named signals. Remove - // netnames whose integer bits are ALL undriven (not output of any - // remaining cell, not a module input/inout port bit). This prevents - // dead signals from appearing in the schematic viewer. - if (options.enableDCE) { - final drivenBits = { - ...ports.values - .where( - (p) => p['direction'] == 'input' || p['direction'] == 'inout', + translation.processNetnames( + applyAlias: applyAlias, + arraySliceOldToNew: arraySliceOldToNew, + arrayConcatOldToNew: arrayConcatOldToNew, + pruneUndriven: options.enableDCE, + drivenBits: options.enableDCE + ? NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'input', 'inout'}, + cellDirection: 'output', ) - .expand((p) => p['bits']! as List) - .whereType(), - ...cells.values.expand((c) { - final cell = c as Map; - final conns = - cell['connections'] as Map? ?? const {}; - final pdirs = - cell['port_directions'] as Map? ?? const {}; - return conns.entries - .where((pe) => pdirs[pe.key] == 'output') - .expand((pe) => pe.value as List) - .whereType(); - }), - }; - - netnames.removeWhere((name, nnRaw) { - final nn = nnRaw as Map?; - if (nn == null) { - return false; - } - final bits = nn['bits'] as List?; - if (bits == null) { - return false; - } - final intBits = bits.whereType(); - if (intBits.isEmpty) { - return false; - } - return !intBits.any(drivenBits.contains); - }); - } - - // -- Slim: strip cell connections ------------------------------------ - // The full pipeline ran identically, so the cell set (keys, ordering) - // is canonical. Now drop the connection maps to reduce the output - // size. This is the ONLY difference between slim and full output. - if (options.slimMode) { - for (final cell in cells.values) { - cell.remove('connections'); - } - } + : const {}, + ); + final netnames = translation.netnames; // -- Structural validation ------------------------------------------- // Debug-mode checks to catch netlist bugs early. These verify that // every signal has a driver and every cell is connected. assert(() { - _validateNetlist(ports, cells, netnames, module.name); + NetlistValidation.validate(ports, cells, module.name); return true; }(), 'Netlist structural validation failed for ${module.name}'); @@ -1818,107 +871,6 @@ class NetlistSynthesizer extends Synthesizer { ); } - /// Validates structural integrity of the netlist. - /// - /// Checks: - /// 1. No non-transparent cell is fully disconnected — every logic gate - /// must have at least one output bit consumed by another cell's - /// input or a module output/inout port. - /// 2. No `$const` cell drives wire IDs that nothing consumes. - /// - /// These fire only under `assert()` (i.e. `--enable-asserts`) so they - /// don't affect production runs. - static void _validateNetlist( - Map> ports, - Map> cells, - Map netnames, - String moduleName, - ) { - // Collect all wire IDs consumed by cell input ports or module - // output/inout ports. - final consumedBits = { - ...ports.values - .where((p) => p['direction'] == 'output' || p['direction'] == 'inout') - .expand((p) => (p['bits'] as List?) ?? []) - .whereType(), - ...cells.values.expand((c) { - final conns = c['connections'] as Map? ?? {}; - final pdirs = c['port_directions'] as Map? ?? {}; - return conns.entries - .where((pe) => pdirs[pe.key] == 'input') - .expand((pe) => (pe.value as List?) ?? []) - .whereType(); - }), - }; - - const transparentTypes = { - r'$buf', - r'$slice', - r'$concat', - r'$struct_unpack', - r'$struct_pack', - }; - - // Check 1: no logic gate is fully disconnected. - for (final entry in cells.entries) { - final cell = entry.value; - final type = cell['type'] as String? ?? ''; - if (transparentTypes.contains(type) || type == r'$const') { - continue; - } - - final conns = cell['connections'] as Map?; - final pdirs = cell['port_directions'] as Map?; - if (conns == null || pdirs == null) { - continue; - } - - final outputBits = conns.entries - .where((pe) => pdirs[pe.key] == 'output') - .expand((pe) => (pe.value as List?) ?? []) - .whereType() - .toList(); - - if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - // ignore: avoid_print - print( - '[netlist-validate] WARNING: $moduleName: ' - 'cell "${entry.key}" (type: $type) has no consumed outputs ' - '— fully disconnected logic gate', - ); - } - } - - // Check 2: no $const cell drives unconsumed wires. - for (final entry in cells.entries) { - final cell = entry.value; - if (cell['type'] != r'$const') { - continue; - } - - final conns = cell['connections'] as Map?; - final pdirs = cell['port_directions'] as Map?; - if (conns == null || pdirs == null) { - continue; - } - - final outputBits = conns.entries - .where((pe) => pdirs[pe.key] == 'output') - .expand((pe) => (pe.value as List?) ?? []) - .whereType() - .toList(); - - if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - // ignore: avoid_print - print( - '[netlist-validate] WARNING: $moduleName: ' - r'$const cell "${entry.key}" drives wires consumed by nothing ' - '— floating constant', - ); - } - } - } - /// Apply all post-processing passes to the modules map. /// /// This is the canonical pass ordering used by both netlist flows: @@ -1936,15 +888,19 @@ class NetlistSynthesizer extends Synthesizer { /// Returns the intermediate module map (definition name → module data) /// after all post-processing passes have been applied. This allows /// callers to retain per-module results for incremental serving while - /// avoiding redundant re-synthesis. + /// avoiding redundant re-synthesis. [slimMode] overrides the configured + /// default for this projection without modifying the retained results. Map> buildModulesMap( SynthBuilder synth, - Module top, - ) { + Module top, { + bool? slimMode, + }) { + final effectiveSlimMode = slimMode ?? options.slimMode; final swEntries = Stopwatch()..start(); final modules = NetlistPasses.collectModuleEntries( synth.synthesisResults, topModule: top, + includeCellConnections: !effectiveSlimMode, ); swEntries.stop(); @@ -1956,9 +912,13 @@ class NetlistSynthesizer extends Synthesizer { } /// Generate the combined netlist JSON from a [SynthBuilder]'s results. - String generateCombinedJson(SynthBuilder synth, Module top) { + String generateCombinedJson( + SynthBuilder synth, + Module top, { + bool? slimMode, + }) { final swCollect = Stopwatch()..start(); - final modules = buildModulesMap(synth, top); + final modules = buildModulesMap(synth, top, slimMode: slimMode); swCollect.stop(); final swCompress = Stopwatch()..start(); @@ -2041,10 +1001,10 @@ class NetlistSynthesizer extends Synthesizer { final cells = moduleDef['cells'] as Map>?; if (cells != null) { for (final cell in cells.values) { - final conns = cell['connections'] as Map>?; + final conns = cell['connections'] as Map?; if (conns != null) { for (final key in conns.keys.toList()) { - conns[key] = _compressBits(conns[key]!); + conns[key] = _compressBits((conns[key] as List).cast()); } } } @@ -2069,36 +1029,14 @@ class NetlistSynthesizer extends Synthesizer { /// Builds a [SynthBuilder] internally and returns the full JSON. /// /// The [packageRoot] parameter is accepted for API compatibility with - /// downstream trace-enabled branches. - String synthesizeToJson(Module top, {String? packageRoot}) { + /// downstream trace-enabled branches. [slimMode] overrides the configured + /// output mode for this call, allowing expansion after a slim request. + String synthesizeToJson( + Module top, { + String? packageRoot, + bool? slimMode, + }) { final sb = SynthBuilder(top, this); - return generateCombinedJson(sb, top); + return generateCombinedJson(sb, top, slimMode: slimMode); } } - -/// A version of [BusSubset] that creates explicit `$slice` cells for -/// [LogicArray] element extraction in the netlist. -/// -/// When a [LogicArray] port is decomposed into its elements, each element -/// gets its own [_BusSubsetForArraySlice] so the netlist shows explicit -/// select gates rather than flat bit aliasing. -class _BusSubsetForArraySlice extends BusSubset { - _BusSubsetForArraySlice(super.bus, super.startIndex, super.endIndex) - : super(name: 'array_slice'); - - @override - bool get hasBuilt => true; -} - -/// A version of [Swizzle] that creates explicit `$concat` cells for -/// [LogicArray] element assembly in the netlist. -/// -/// When a [LogicArray]'s elements are driven independently (e.g. by -/// constants), this creates a visible concat gate in the netlist that -/// assembles the element signals into the full packed array bus. -class _SwizzleForArrayConcat extends Swizzle { - _SwizzleForArrayConcat(super.signals) : super(name: 'array_concat'); - - @override - bool get hasBuilt => true; -} diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 2a2d5a8ab..ae4853fe7 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -7,12 +7,14 @@ // 2026 February 11 // Author: Desmond Kirkpatrick +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// Shared utility functions for netlist synthesis and post-processing passes. /// /// All methods are static — no instances are created. +@internal class NetlistUtils { NetlistUtils._(); @@ -41,15 +43,6 @@ class NetlistUtils { } } - /// Resolves [sl] to the end of its replacement chain. - static SynthLogic resolveReplacement(SynthLogic sl) { - var r = sl; - while (r.replacement != null) { - r = r.replacement!; - } - return r; - } - /// Create a `$buf` cell map. static Map makeBufCell( int width, @@ -107,8 +100,8 @@ class NetlistUtils { continue; } - final resolvedOutput = resolveReplacement(outputSL); - final resolvedInput = resolveReplacement(inputSL); + final resolvedOutput = outputSL.resolved; + final resolvedInput = inputSL.resolved; busSubsetLookup[resolvedOutput] = (bsMod, resolvedInput, bsInst); } @@ -130,7 +123,7 @@ class NetlistUtils { continue; // already filtered } - final resolved = resolveReplacement(e.value); + final resolved = e.value.resolved; final info = busSubsetLookup[resolved]; if (info != null) { final (bsMod, rootSL, bsInst) = info; @@ -213,12 +206,12 @@ class NetlistUtils { if (outputSL == null) { continue; } - final resolvedOutput = resolveReplacement(outputSL); + final resolvedOutput = outputSL.resolved; // Swizzle inputs are in0, in1, ... with bit-0 first. var offset = 0; for (final inEntry in szInst.inputMapping.entries) { - final resolvedInput = resolveReplacement(inEntry.value); + final resolvedInput = inEntry.value.resolved; final w = resolvedInput.width; swizzleLookup[resolvedInput] = ( inEntry.key, @@ -247,7 +240,7 @@ class NetlistUtils { continue; } - final resolved = resolveReplacement(e.value); + final resolved = e.value.resolved; final info = swizzleLookup[resolved]; if (info != null) { final (_, offset, width, swizzleOutputSL, szInst) = info; @@ -277,7 +270,7 @@ class NetlistUtils { if (sl == null) { return false; } - final resolved = resolveReplacement(sl); + final resolved = sl.resolved; return resolved.isPort(parentModule); }); if (hasModulePort) { @@ -412,12 +405,11 @@ class NetlistUtils { } /// Check if a SynthLogic is a constant (following replacement chain). - static bool isConstantSynthLogic(SynthLogic sl) => - resolveReplacement(sl).isConstant; + static bool isConstantSynthLogic(SynthLogic sl) => sl.resolved.isConstant; /// Extract the Const value from a constant SynthLogic. static Const? constValueFromSynthLogic(SynthLogic sl) { - final resolved = resolveReplacement(sl); + final resolved = sl.resolved; for (final logic in resolved.logics) { if (logic is Const) { return logic; diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart new file mode 100644 index 000000000..8c16357b0 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -0,0 +1,109 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// netlist_validation.dart +// Structural validation utilities for emitted netlists. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +/// Graph queries and structural checks for an emitted module netlist. +@internal +class NetlistValidation { + NetlistValidation._(); + + /// Collects module-port and cell-connection bits with matching directions. + static Set connectedBits( + Map> ports, + Map> cells, { + required Set portDirections, + required String cellDirection, + }) => + { + ...ports.values + .where((port) => portDirections.contains(port['direction'])) + .expand((port) => (port['bits'] as List?) ?? const []) + .whereType(), + ...cells.values.expand((cell) { + final connections = + cell['connections'] as Map? ?? const {}; + final directions = + cell['port_directions'] as Map? ?? const {}; + return connections.entries + .where((port) => directions[port.key] == cellDirection) + .expand((port) => (port.value as List?) ?? const []) + .whereType(); + }), + }; + + /// Reports disconnected cells and floating constants in debug builds. + static void validate( + Map> ports, + Map> cells, + String moduleName, + ) { + final consumedBits = connectedBits( + ports, + cells, + portDirections: const {'output', 'inout'}, + cellDirection: 'input', + ); + const transparentTypes = { + r'$buf', + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + for (final entry in cells.entries) { + final cell = entry.value; + final type = cell['type'] as String? ?? ''; + if (transparentTypes.contains(type) || type == r'$const') { + continue; + } + final outputBits = _cellBits(cell, 'output'); + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + // ignore: avoid_print + print( + '[netlist-validate] WARNING: $moduleName: ' + 'cell "${entry.key}" (type: $type) has no consumed outputs ' + '— fully disconnected logic gate', + ); + } + } + + for (final entry in cells.entries.where( + (entry) => entry.value['type'] == r'$const', + )) { + final outputBits = _cellBits(entry.value, 'output'); + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + // ignore: avoid_print + print( + '[netlist-validate] WARNING: $moduleName: ' + r'$const cell "${entry.key}" drives wires consumed by nothing ' + '— floating constant', + ); + } + } + } + + static List _cellBits( + Map cell, + String direction, + ) { + final connections = cell['connections'] as Map?; + final directions = cell['port_directions'] as Map?; + if (connections == null || directions == null) { + return const []; + } + return connections.entries + .where((port) => directions[port.key] == direction) + .expand((port) => (port.value as List?) ?? const []) + .whereType() + .toList(); + } +} diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index da5d76586..6b1eed42b 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -7,3 +7,4 @@ export 'synth_file_contents.dart'; export 'synthesis_result.dart'; export 'synthesizer.dart'; export 'systemverilog/systemverilog.dart'; +export 'utilities/synth_module_stop_policy.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index 062647ac3..5253a4b23 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -15,12 +15,17 @@ import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synthesis_resu /// /// Attempts to maintain signal naming and structure as much as possible. class SystemVerilogSynthesizer extends Synthesizer { + /// The hierarchy stopping policy used by this synthesizer. + final SynthModuleStopPolicy moduleStopPolicy; + + /// Creates a [SystemVerilogSynthesizer]. + SystemVerilogSynthesizer({SynthModuleStopPolicy? moduleStopPolicy}) + : moduleStopPolicy = + moduleStopPolicy ?? SynthModuleStopPolicy.systemVerilog(); + @override bool generatesDefinition(Module module) => - // ignore: deprecated_member_use_from_same_package - !((module is CustomSystemVerilog) || - (module is SystemVerilog && - module.generatedDefinitionType == DefinitionGenerationType.none)); + moduleStopPolicy.generatesDefinition(module); /// Creates a line of SystemVerilog that instantiates [module]. /// diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart new file mode 100644 index 000000000..521783d5a --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -0,0 +1,41 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// synth_array_concat.dart +// Shared array concatenation helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; + +/// A [Swizzle] used by synthesis backends to explicitly assemble a +/// [LogicArray] from its elements. +@internal +class SynthArrayConcat extends Swizzle { + final LogicArray _destination; + + /// Creates a synthesis array concatenation from [signals]. + SynthArrayConcat( + super.signals, { + required LogicArray destination, + }) : _destination = destination, + super( + name: Namer.synthOperationInstanceName( + operationName: Namer.synthArrayConcatOperationName, + destination: destination, + ), + ); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: Namer.synthArrayConcatOperationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart new file mode 100644 index 000000000..b85e49799 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -0,0 +1,43 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// synth_array_slice.dart +// Shared array slice helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; + +/// A [BusSubset] used by synthesis backends to explicitly extract a +/// [LogicArray] element from its packed parent representation. +@internal +class SynthArraySlice extends BusSubset { + final Logic _destination; + + /// Creates a synthesis array slice over the selected indices of [bus]. + SynthArraySlice( + super.bus, + super.startIndex, + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super( + name: Namer.synthOperationInstanceName( + operationName: Namer.synthArraySliceOperationName, + destination: destination, + ), + ); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: Namer.synthArraySliceOperationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 8fcbc014a..a5d9ba08d 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -34,6 +34,12 @@ class SynthLogic { _replacement = newReplacement; } + /// This [SynthLogic] resolved through any [replacement] that may have + /// occurred during merging, so that identity comparisons are consistent. + /// + /// Returns `this` if there is no [replacement]. + SynthLogic get resolved => replacement ?? this; + /// The parent [SynthModuleDefinition] that this [SynthLogic] belongs to. final SynthModuleDefinition parentSynthModuleDefinition; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 29d1757eb..90d1048f5 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -16,35 +16,6 @@ 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. -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, { - 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. @internal class SynthModuleDefinition { @@ -288,7 +259,7 @@ class SynthModuleDefinition { internalSignals.add(leafSynth); // this is DISCONNECTED, just a module used for synthesizing - final subsetMod = _BusSubsetForStructSlice( + final subsetMod = SynthStructureSlice( (port.isNet ? LogicNet.new : Logic.new)( width: port.width, name: 'DUMMY', diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart new file mode 100644 index 000000000..bed6da42c --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -0,0 +1,76 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// synth_module_stop_policy.dart +// Shared module hierarchy stopping policy for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Determines whether a synthesizer should stop hierarchy traversal at a +/// [Module] and treat it as a leaf in its parent. +typedef SynthModuleLeafPredicate = bool Function(Module module); + +/// Determines whether a [Module] would normally receive its own synthesized +/// definition before leaf predicates are applied. +typedef SynthModuleDefinitionPredicate = bool Function(Module module); + +/// Shared hierarchy stopping policy for synthesis backends. +/// +/// A synthesizer configures this with backend-specific leaf module types, +/// predicates, and a default definition rule, then queries [isLeaf] or +/// [generatesDefinition] while walking a module hierarchy. +class SynthModuleStopPolicy { + final Set _leafModuleTypes; + final List _leafPredicates; + final SynthModuleDefinitionPredicate _generatesDefinitionByDefault; + + /// Creates a module stopping policy. + SynthModuleStopPolicy({ + SynthModuleDefinitionPredicate? generatesDefinitionByDefault, + Iterable leafModuleTypes = const [], + Iterable leafPredicates = const [], + }) : _generatesDefinitionByDefault = + generatesDefinitionByDefault ?? ((_) => true), + _leafModuleTypes = Set.unmodifiable(leafModuleTypes), + _leafPredicates = List.unmodifiable(leafPredicates); + + /// Creates the default SystemVerilog stopping policy. + factory SynthModuleStopPolicy.systemVerilog() => SynthModuleStopPolicy( + leafPredicates: [ + (module) { + // ignore: deprecated_member_use_from_same_package + if (module is CustomSystemVerilog) { + return true; + } + + return module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none; + }, + ], + ); + + /// Creates the default netlist stopping policy. + factory SynthModuleStopPolicy.netlist({ + Iterable leafModuleTypes = const [FlipFlop], + Iterable leafPredicates = const [], + }) => + SynthModuleStopPolicy( + generatesDefinitionByDefault: (module) => module.subModules.isNotEmpty, + leafModuleTypes: leafModuleTypes, + leafPredicates: leafPredicates, + ); + + /// Returns `true` when [module] should be treated as a leaf cell in its + /// parent instead of receiving its own generated definition. + bool isLeaf(Module module) => + !_generatesDefinitionByDefault(module) || + _leafModuleTypes.contains(module.runtimeType) || + _leafPredicates.any((predicate) => predicate(module)); + + /// Returns `true` when [module] should receive its own generated definition. + bool generatesDefinition(Module module) => !isLeaf(module); +} diff --git a/lib/src/synthesizers/utilities/synth_structure_concat.dart b/lib/src/synthesizers/utilities/synth_structure_concat.dart new file mode 100644 index 000000000..08cd4c880 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_concat.dart @@ -0,0 +1,41 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// synth_structure_concat.dart +// Shared structure concatenation helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; + +/// A [Swizzle] used by synthesis backends to explicitly assemble a +/// [LogicStructure] from its leaf elements. +@internal +class SynthStructureConcat extends Swizzle { + final LogicStructure _destination; + + /// Creates a synthesis structure concatenation from [signals]. + SynthStructureConcat( + super.signals, { + required LogicStructure destination, + }) : _destination = destination, + super( + name: Namer.synthOperationInstanceName( + operationName: Namer.synthStructureConcatOperationName, + destination: destination, + ), + ); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: Namer.synthStructureConcatOperationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart new file mode 100644 index 000000000..a580a8b2e --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -0,0 +1,115 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// synth_structure_layout.dart +// Shared packed LogicStructure layout utility for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Provides bit ranges and field names for a packed [LogicStructure]. +class SynthStructureLayout { + final List< + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })> _ranges = []; + + /// Creates a layout with elements ordered from least to most significant. + SynthStructureLayout(LogicStructure structure) { + _addStructure(structure, 0, ''); + } + + void _addStructure( + LogicStructure structure, + int baseOffset, + String parentPath, + ) { + var offset = baseOffset; + for (var index = 0; index < structure.elements.length; index++) { + final element = structure.elements[index]; + final end = offset + element.width; + final path = + parentPath.isEmpty ? element.name : '${parentPath}_${element.name}'; + _ranges.add(( + start: offset, + end: end, + name: element.name, + path: path, + indexInParent: index, + )); + if (element is LogicStructure && element is! LogicArray) { + _addStructure(element, offset, path); + } + offset = end; + } + } + + /// Returns the best field name containing [bitOffset]. + /// + /// When [anonymousUnpreferred] is true, an unpreferred leaf with no named + /// ancestor is represented by its index rather than its raw name. + String fieldNameAt( + int bitOffset, { + required String fallbackName, + bool anonymousUnpreferred = false, + }) { + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestNamed; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? narrowest; + + for (final range in _ranges) { + if (bitOffset < range.start || bitOffset >= range.end) { + continue; + } + final span = range.end - range.start; + if (narrowest == null || span < narrowest.end - narrowest.start) { + narrowest = range; + } + if (!Naming.isUnpreferred(range.name) && + (bestNamed == null || span < bestNamed.end - bestNamed.start)) { + bestNamed = range; + } + } + + if (bestNamed != null) { + if (narrowest != null && + narrowest.end - narrowest.start < bestNamed.end - bestNamed.start) { + final prefix = bestNamed.path; + if (narrowest.path.length > prefix.length && + narrowest.path.startsWith(prefix)) { + final suffix = narrowest.path.substring(prefix.length + 1); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + + if (anonymousUnpreferred && + narrowest != null && + Naming.isUnpreferred(narrowest.name)) { + return 'anonymous_${narrowest.indexInParent}'; + } + return narrowest?.name ?? fallbackName; + } +} diff --git a/lib/src/synthesizers/utilities/synth_structure_slice.dart b/lib/src/synthesizers/utilities/synth_structure_slice.dart new file mode 100644 index 000000000..c4a7d2790 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_slice.dart @@ -0,0 +1,43 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// synth_structure_slice.dart +// Shared structure slice helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; + +/// A [BusSubset] used by synthesis backends to explicitly extract a +/// [LogicStructure] leaf from its packed parent representation. +@internal +class SynthStructureSlice extends BusSubset { + final Logic _destination; + + /// Creates a synthesis structure slice over the selected indices of [bus]. + SynthStructureSlice( + super.bus, + super.startIndex, + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super( + name: Namer.synthOperationInstanceName( + operationName: Namer.synthStructureSliceOperationName, + destination: destination, + ), + ); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: Namer.synthStructureSliceOperationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index c3cccdf32..c34b4db60 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,7 +1,12 @@ // Copyright (C) 2024-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'synth_array_concat.dart'; +export 'synth_array_slice.dart'; export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; +export 'synth_structure_concat.dart'; +export 'synth_structure_layout.dart'; +export 'synth_structure_slice.dart'; export 'synth_sub_module_instantiation.dart'; diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index dc585612d..5c5c0c965 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -24,6 +24,32 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { + /// Canonical base name for synthesis-created array slice operations. + static const String synthArraySliceOperationName = 'array_slice'; + + /// Canonical base name for synthesis-created array concat operations. + static const String synthArrayConcatOperationName = 'array_concat'; + + /// Canonical base name for synthesis-created structure slice operations. + static const String synthStructureSliceOperationName = 'struct_slice'; + + /// Canonical base name for synthesis-created structure concat operations. + static const String synthStructureConcatOperationName = 'struct_concat'; + + /// Returns the canonical base instance name for a synthesis-created + /// structural operation that targets [destination]. + /// + /// The numeric suffix is derived from [destination]'s structural position, + /// not from the order in which a backend asks for names. This keeps helper + /// operation names stable across output formats that traverse a module in + /// different orders. + static String synthOperationInstanceName({ + required String operationName, + required Logic destination, + }) => + '${Sanitizer.sanitizeSV(operationName)}_' + '${_synthOperationDestinationSuffix(destination)}'; + /// The [Uniquifier] that manages the shared namespace for this module. final Uniquifier _uniquifier; @@ -72,6 +98,89 @@ class Namer { @visibleForTesting bool isAvailable(String name) => _uniquifier.isAvailable(name); + static String _synthOperationDestinationSuffix(Logic destination) { + final parts = [ + ..._modulePathIndices(destination.parentModule), + ..._logicLocationIndices(destination), + ]; + + return parts.isEmpty ? '0' : parts.join('_'); + } + + static List _modulePathIndices(Module? module) { + if (module == null) { + return const [0]; + } + + final parent = module.parent; + if (parent == null) { + return const [0]; + } + + final siblings = parent.subModules.toList(); + final index = + siblings.indexWhere((submodule) => identical(submodule, module)); + return [ + ..._modulePathIndices(parent), + if (index < 0) 0 else index, + ]; + } + + static List _logicLocationIndices(Logic destination) { + final elementPath = []; + var root = destination; + while (root.parentStructure != null) { + final parent = root.parentStructure!; + final index = + parent.elements.indexWhere((element) => identical(element, root)); + elementPath.insert(0, index < 0 ? root.arrayIndex ?? 0 : index); + root = parent; + } + + final module = root.parentModule; + if (module == null) { + return [0, ...elementPath]; + } + + final location = _logicLocationInModule(module, root); + return [...location, ...elementPath]; + } + + static List _logicLocationInModule(Module module, Logic root) { + final inputIndex = _identityIndex(module.inputs.values, root); + if (inputIndex >= 0) { + return [0, inputIndex]; + } + + final outputIndex = _identityIndex(module.outputs.values, root); + if (outputIndex >= 0) { + return [1, outputIndex]; + } + + final inOutIndex = _identityIndex(module.inOuts.values, root); + if (inOutIndex >= 0) { + return [2, inOutIndex]; + } + + final internalIndex = _identityIndex(module.internalSignals, root); + if (internalIndex >= 0) { + return [3, internalIndex]; + } + + return const [4, 0]; + } + + static int _identityIndex(Iterable logics, Logic target) { + var index = 0; + for (final logic in logics) { + if (identical(logic, target)) { + return index; + } + index++; + } + return -1; + } + // ─── Instance naming (Module → String) ────────────────────────── /// Returns the canonical instance name for [submodule]. diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index bdeb5139c..a70df2e7a 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause // // netlist_synthesizer_test.dart -// Comprehensive tests for the netlist synthesizer covering leaf cell +// Comprehensive tests for the netlist synthesizer covering netlist cell // mapping, structural validation, options permutations, and real // example designs. // @@ -95,6 +95,19 @@ class AddModule extends Module { } } +/// Wraps an [AddModule] so stop-policy tests can choose whether the child +/// receives its own definition or is emitted as a netlist cell. +class AddWrapperModule extends Module { + Logic get sum => output('sum'); + + AddWrapperModule({int width = 8}) : super(name: 'addwrapper') { + final a = addInput('a', Logic(width: width), width: width); + final b = addInput('b', Logic(width: width), width: width); + final child = AddModule(a, b, width: width); + addOutput('sum', width: width) <= child.sum; + } +} + /// Exercises Multiply. class MulModule extends Module { Logic get prod => output('prod'); @@ -431,7 +444,7 @@ void main() { // ── Group 1: Leaf cell mapper — individual gate mappings ─────────── - group('leaf cell mapping', () { + group('netlist cell mapping', () { test(r'And2Gate maps to $and cell', () async { final json = await _synthToMap(AndModule(Logic(), Logic())); expect(_hasCellType(json, r'$and'), isTrue); @@ -804,6 +817,31 @@ void main() { } }); + test('slim then expanded matches initially expanded output', () async { + final module = _buildFilterBank(); + await module.build(); + + final translator = NetlistSynthesizer( + options: const NetlistOptions(slimMode: true), + ); + final slim = translator.synthesizeToJson(module); + final expanded = translator.synthesizeToJson(module, slimMode: false); + final initiallyExpanded = NetlistSynthesizer().synthesizeToJson(module); + + final slimModules = _modules( + jsonDecode(slim) as Map, + ); + expect( + slimModules.values + .expand((definition) => _cells( + definition as Map, + ).values) + .every((cell) => !(cell as Map).containsKey('connections')), + isTrue, + ); + expect(expanded, initiallyExpanded); + }); + test('DCE disabled still produces valid netlist', () async { final synth = SynthBuilder( filterBank, @@ -1148,6 +1186,85 @@ void main() { // ── Group 7: Wire ID and structural invariants ───────────────────── group('wire ID and structural invariants', () { + test('default synthesizers do not share mutable leaf mappers', () { + final first = NetlistSynthesizer(); + final second = NetlistSynthesizer(); + + expect(identical(first.netlistCellMapper, second.netlistCellMapper), + isFalse); + }); + + test('leaf module type option controls which modules stop traversal', + () async { + final module = AddWrapperModule(); + await module.build(); + + final childDefinitionName = module.subModules.single.definitionName; + final synthesizer = NetlistSynthesizer( + options: const NetlistOptions(leafModuleTypes: [AddModule]), + ); + + final json = jsonDecode(synthesizer.synthesizeToJson(module)) + as Map; + final modules = json['modules'] as Map; + final top = modules[module.definitionName] as Map; + final cells = _cells(top); + + expect(modules, isNot(contains(childDefinitionName))); + expect( + cells.values, + contains(predicate>( + (cell) => cell['type'] == childDefinitionName, + )), + ); + }); + + test('repeated translation of the same module is identical', () async { + final module = LogicArrayExample( + LogicArray([4], 8, name: 'arrayA'), + Logic(name: 'id', width: 3), + Logic(name: 'selectIndexValue', width: 8), + Logic(name: 'selectFromValue', width: 8), + ); + await module.build(); + + final synthesizer = NetlistSynthesizer(); + final first = synthesizer.synthesizeToJson(module); + final second = synthesizer.synthesizeToJson(module); + + expect(second, first); + }); + + test('reusing a synthesizer resets wire IDs for each module', () async { + final firstModule = AddModule( + Logic(name: 'firstA', width: 8), + Logic(name: 'firstB', width: 8), + ); + final secondModule = AddModule( + Logic(name: 'secondA', width: 8), + Logic(name: 'secondB', width: 8), + ); + await firstModule.build(); + await secondModule.build(); + + final synthesizer = NetlistSynthesizer(); + + int firstWireId(Module module) { + final json = jsonDecode(synthesizer.synthesizeToJson(module)) + as Map; + final definition = + _modules(json)[module.definitionName] as Map; + return _ports(definition) + .values + .expand((port) => (port as Map)['bits'] as List) + .whereType() + .reduce((first, second) => first < second ? first : second); + } + + expect(firstWireId(firstModule), 2); + expect(firstWireId(secondModule), 2); + }); + test('all wire IDs are >= 2 (0 and 1 reserved for constants)', () async { final json = await _synthToMap( AddModule(Logic(width: 8), Logic(width: 8)), diff --git a/test/netlist_test.dart b/test/netlist_test.dart index 69841f2f7..5f5baa8e4 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -584,6 +584,36 @@ void main() { // Bit-range compression & compact JSON // ----------------------------------------------------------------------- group('Bit-range compression', () { + test('post-processing does not mutate synthesis results', () async { + final module = _AdderModule( + Logic(name: 'a', width: 8), + Logic(name: 'b', width: 8), + ); + await module.build(); + + final synthesizer = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final builder = SynthBuilder(module, synthesizer); + final result = builder.synthesisResults + .whereType() + .firstWhere((result) => result.module == module); + final before = jsonEncode({ + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }); + + synthesizer.generateCombinedJson(builder, module); + + final after = jsonEncode({ + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }); + expect(after, before); + }); + test('compressBitRanges option produces range strings in JSON', () async { final a = Logic(name: 'a', width: 8); final mod = _AdderModule(a, Logic(name: 'b', width: 8)); diff --git a/test/synth_structure_layout_test.dart b/test/synth_structure_layout_test.dart new file mode 100644 index 000000000..6b0dcaeb7 --- /dev/null +++ b/test/synth_structure_layout_test.dart @@ -0,0 +1,74 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +// +// synth_structure_layout_test.dart +// Tests for packed LogicStructure layout synthesis utilities. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +void main() { + group('SynthStructureLayout', () { + test('uses least-significant-first element offsets', () { + final structure = LogicStructure([ + Logic(name: 'low', width: 2), + Logic(name: 'high', width: 3), + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), 'low'); + expect(layout.fieldNameAt(1, fallbackName: 'fallback'), 'low'); + expect(layout.fieldNameAt(2, fallbackName: 'fallback'), 'high'); + expect(layout.fieldNameAt(4, fallbackName: 'fallback'), 'high'); + expect(layout.fieldNameAt(5, fallbackName: 'fallback'), 'fallback'); + }); + + test('qualifies an unpreferred nested leaf by parent and index', () { + final nested = LogicStructure([ + Logic(name: Naming.unpreferredName('first'), width: 2), + Logic(name: Naming.unpreferredName('second'), width: 2), + ], name: 'payload'); + final structure = LogicStructure([ + Logic(name: 'header'), + nested, + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(1, fallbackName: 'fallback'), 'payload_0'); + expect(layout.fieldNameAt(3, fallbackName: 'fallback'), 'payload_1'); + }); + + test('supports unpack-specific anonymous field names', () { + final fieldName = Naming.unpreferredName('field'); + final structure = LogicStructure([Logic(name: fieldName)]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), fieldName); + expect( + layout.fieldNameAt( + 0, + fallbackName: 'fallback', + anonymousUnpreferred: true, + ), + 'anonymous_0', + ); + }); + + test('does not recurse into LogicArray elements', () { + final structure = LogicStructure([ + LogicArray([2], 3, name: 'entries'), + Logic(name: 'tail'), + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), 'entries'); + expect(layout.fieldNameAt(5, fallbackName: 'fallback'), 'entries'); + expect(layout.fieldNameAt(6, fallbackName: 'fallback'), 'tail'); + }); + }); +} From 179fea0ade8e3341dbb3568b01515409c5dcd0d9 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 10 Jul 2026 08:19:21 -0700 Subject: [PATCH 54/77] Fix netlist top marker selection --- .../netlist/netlist_synthesizer.dart | 4 -- test/netlist_test.dart | 37 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 16104c319..a58e6bceb 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -75,11 +75,7 @@ class NetlistSynthesizer extends Synthesizer { SynthesisResult? Function(Module module)? lookupExistingResult, Map? existingResults, }) { - final isTop = module.parent == null; final attr = {'src': 'generated'}; - if (isTop) { - attr['top'] = 1; - } final translation = NetlistModuleTranslation( module, diff --git a/test/netlist_test.dart b/test/netlist_test.dart index 5f5baa8e4..75bbc8ef4 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -51,6 +51,18 @@ class _CompositeModule extends Module { } } +/// A wrapper that lets tests synthesize a built submodule as the requested top. +class _CompositeWrapperModule extends Module { + late final _CompositeModule child; + + _CompositeWrapperModule(Logic a, Logic b) : super(name: 'composite_wrapper') { + a = addInput('a', a); + b = addInput('b', b); + child = _CompositeModule(a, b); + addOutput('out') <= child.out; + } +} + /// A simple adder module with a configurable width. class _AdderModule extends Module { Logic get sum => output('sum'); @@ -278,6 +290,31 @@ void main() { }, ); + test('requested submodule can be synthesized as top', () async { + final wrapper = + _CompositeWrapperModule(Logic(name: 'a'), Logic(name: 'b')); + await wrapper.build(); + + final submodule = wrapper.child; + final json = NetlistSynthesizer().synthesizeToJson(submodule); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + + expect(modules, contains(submodule.definitionName)); + expect(modules, isNot(contains(wrapper.definitionName))); + + final topModules = modules.values.where((module) { + final attrs = (module as Map)['attributes'] + as Map?; + return attrs?['top'] == 1; + }); + expect(topModules, hasLength(1)); + + final attrs = (modules[submodule.definitionName] + as Map)['attributes'] as Map; + expect(attrs['top'], equals(1)); + }); + test('port bit widths match module interface', () async { const width = 16; final mod = _AdderModule( From 9199a405bd408bb13c359603d1856df52e276113 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 10 Jul 2026 09:23:11 -0700 Subject: [PATCH 55/77] cleanup formatting tail commas --- lib/src/module.dart | 324 +++++++----------- lib/src/synthesizers/netlist/netlist.dart | 3 - .../netlist/netlist_module_translation.dart | 1 + .../synthesizers/netlist/netlist_passes.dart | 2 + .../netlist/netlist_synthesizer.dart | 2 + test/netlist_test.dart | 2 + 6 files changed, 124 insertions(+), 210 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index a1a86476d..b79d90485 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -118,7 +118,7 @@ abstract class Module { ..._inputs.values, ..._outputs.values, ..._inOuts.values, - ...internalSignals, + ...internalSignals ]); /// Accesses the [Logic] associated with this [Module]s [input] port @@ -128,16 +128,14 @@ abstract class Module { Logic input(String name) => _inputs.containsKey(name) ? _inputs[name]! : throw PortDoesNotExistException( - 'Input name "$name" not found as an input to this Module.', - ); + 'Input name "$name" not found as an input to this Module.'); /// The original `source` provided to the creation of the [input] port [name] /// via [addInput] or [addInputArray]. Logic inputSource(String name) => _inputSources[name] ?? (throw PortDoesNotExistException( - '$name is not an input of this Module.', - )); + '$name is not an input of this Module.')); /// Provides the [input] named [name] if it exists, otherwise `null`. /// @@ -152,8 +150,7 @@ abstract class Module { Logic output(String name) => _outputs.containsKey(name) ? _outputs[name]! : throw PortDoesNotExistException( - 'Output name "$name" not found as an output of this Module.', - ); + 'Output name "$name" not found as an output of this Module.'); /// Provides the [output] named [name] if it exists, otherwise `null`. Logic? tryOutput(String name) => _outputs[name]; @@ -165,16 +162,14 @@ abstract class Module { Logic inOut(String name) => _inOuts.containsKey(name) ? _inOuts[name]! : throw PortDoesNotExistException( - 'InOut name "$name" not found as an in/out of this Module.', - ); + 'InOut name "$name" not found as an in/out of this Module.'); /// The original `source` provided to the creation of the [inOut] port [name] /// via [addInOut] or [addInOutArray]. Logic inOutSource(String name) => _inOutSources[name] ?? (throw PortDoesNotExistException( - '$name is not an inOut of this Module.', - )); + '$name is not an inOut of this Module.')); /// Provides the [inOut] named [name] if it exists, otherwise `null`. Logic? tryInOut(String name) => _inOuts[name]; @@ -216,9 +211,7 @@ abstract class Module { String get uniqueInstanceName => hasBuilt || reserveName ? _uniqueInstanceName : throw ModuleNotBuiltException( - this, - 'Module must be built to access uniquified name.', - ); + this, 'Module must be built to access uniquified name.'); String _uniqueInstanceName; /// A stable identity used to memoize this module's canonical instance name @@ -262,17 +255,15 @@ abstract class Module { /// /// If [reserveDefinitionName] is set, then code generation will fail if /// it is unable to keep from uniquifying [definitionName] to avoid conflicts. - Module({ - this.name = 'unnamed_module', - this.reserveName = false, - String? definitionName, - this.reserveDefinitionName = false, - }) : _uniqueInstanceName = + Module( + {this.name = 'unnamed_module', + this.reserveName = false, + String? definitionName, + this.reserveDefinitionName = false}) + : _uniqueInstanceName = Naming.validatedName(name, reserveName: reserveName) ?? name, - _definitionName = Naming.validatedName( - definitionName, - reserveName: reserveDefinitionName, - ); + _definitionName = Naming.validatedName(definitionName, + reserveName: reserveDefinitionName); /// Returns an [Iterable] of [Module]s representing the hierarchical path to /// this [Module]. @@ -283,9 +274,7 @@ abstract class Module { Iterable hierarchy() { if (!hasBuilt) { throw ModuleNotBuiltException( - this, - 'Module must be built before accessing hierarchy.', - ); + this, 'Module must be built before accessing hierarchy.'); } Module? pModule = this; final hierarchyQueue = Queue(); @@ -327,8 +316,7 @@ abstract class Module { Future build() async { if (hasBuilt) { throw Exception( - 'This Module has already been built, and can only be built once.', - ); + 'This Module has already been built, and can only be built once.'); } // construct the list of modules within this module @@ -345,9 +333,8 @@ abstract class Module { final uniquifier = Uniquifier(); for (final module in _subModules) { module._uniqueInstanceName = uniquifier.getUniqueName( - initialName: Sanitizer.sanitizeSV(module.name), - reserved: module.reserveName, - ); + initialName: Sanitizer.sanitizeSV(module.name), + reserved: module.reserveName); } _checkValidHierarchy(visited: {}); @@ -370,17 +357,15 @@ abstract class Module { if (hierarchy.contains(this)) { final loopHierarchy = _hierarchyListToString(newHierarchy); throw InvalidHierarchyException( - 'Module $this is a submodule of itself: $loopHierarchy', - ); + 'Module $this is a submodule of itself: $loopHierarchy'); } if (visited.containsKey(this)) { final otherHierarchy = _hierarchyListToString(visited[this]!); final thisHierarchy = _hierarchyListToString(hierarchy); throw InvalidHierarchyException( - 'Module $this exists at more than one hierarchy: ' - '$otherHierarchy and $thisHierarchy', - ); + 'Module $this exists at more than one hierarchy: ' + '$otherHierarchy and $thisHierarchy'); } visited[this] = newHierarchy; @@ -398,11 +383,9 @@ abstract class Module { /// Adds a [Module] to this as a subModule. Future _addAndBuildModule(Module module) async { if (module.parent != null) { - throw Exception( - 'This Module "$this" already has a parent. ' - 'If you are hitting this as a user of ROHD, please file ' - 'a bug at https://github.com/intel/rohd/issues.', - ); + throw Exception('This Module "$this" already has a parent. ' + 'If you are hitting this as a user of ROHD, please file ' + 'a bug at https://github.com/intel/rohd/issues.'); } _subModules.add(module); @@ -432,10 +415,8 @@ abstract class Module { static bool isUnpreferred(String name) => Naming.isUnpreferred(name); /// Searches for [Logic]s and [Module]s within this [Module] from its inputs. - Future _traceInputForModuleContents( - Logic signal, { - bool dontAddSignal = false, - }) async { + Future _traceInputForModuleContents(Logic signal, + {bool dontAddSignal = false}) async { if (isOutput(signal) || _inOutDrivers.contains(signal)) { return; } @@ -471,28 +452,20 @@ abstract class Module { await _addAndBuildModule(subModule); } for (final subModuleOutput in subModule._outputs.values) { - await _traceInputForModuleContents( - subModuleOutput, - dontAddSignal: true, - ); + await _traceInputForModuleContents(subModuleOutput, + dontAddSignal: true); } for (final subModuleInput in subModule._inputs.values) { - await _traceOutputForModuleContents( - subModuleInput, - dontAddSignal: true, - ); + await _traceOutputForModuleContents(subModuleInput, + dontAddSignal: true); } for (final subModuleInOutDriver in subModule._inOutDrivers) { final subModDontAddSignal = subModuleInOutDriver.isPort; - await _traceInputForModuleContents( - subModuleInOutDriver, - dontAddSignal: subModDontAddSignal, - ); - await _traceOutputForModuleContents( - subModuleInOutDriver, - dontAddSignal: subModDontAddSignal, - ); + await _traceInputForModuleContents(subModuleInOutDriver, + dontAddSignal: subModDontAddSignal); + await _traceOutputForModuleContents(subModuleInOutDriver, + dontAddSignal: subModDontAddSignal); } } else { if (!dontAddSignal && @@ -503,25 +476,17 @@ abstract class Module { // handle expanding the search for arrays if (signal.parentStructure != null) { - await _traceInputForModuleContents( - signal.parentStructure!, - dontAddSignal: signal.isPort, - ); - await _traceOutputForModuleContents( - signal.parentStructure!, - dontAddSignal: signal.isPort, - ); + await _traceInputForModuleContents(signal.parentStructure!, + dontAddSignal: signal.isPort); + await _traceOutputForModuleContents(signal.parentStructure!, + dontAddSignal: signal.isPort); } if (signal is LogicStructure) { for (final elem in signal.elements) { - await _traceInputForModuleContents( - elem, - dontAddSignal: elem.isPort, - ); - await _traceOutputForModuleContents( - elem, - dontAddSignal: elem.isPort, - ); + await _traceInputForModuleContents(elem, + dontAddSignal: elem.isPort); + await _traceOutputForModuleContents(elem, + dontAddSignal: elem.isPort); } } @@ -532,11 +497,10 @@ abstract class Module { if (!dontAddSignal && isInput(signal)) { throw PortRulesViolationException( - this, - signal.name, - 'Input $signal of module $this is dependent on' - ' another input of the same module.', - ); + this, + signal.name, + 'Input $signal of module $this is dependent on' + ' another input of the same module.'); } for (final dstConnection in signal.dstConnections) { @@ -556,15 +520,13 @@ abstract class Module { // extra searching in both directions for nets if (signal.isNet && !isPort(signal)) { await _traceOutputForModuleContents(signal); - for (final srcConnection in signal.srcConnections.where( - (element) => element.isNet, - )) { + for (final srcConnection + in signal.srcConnections.where((element) => element.isNet)) { await _traceInputForModuleContents(srcConnection); await _traceOutputForModuleContents(srcConnection); } - for (final dstConnection in signal.dstConnections.where( - (element) => element.isNet, - )) { + for (final dstConnection + in signal.dstConnections.where((element) => element.isNet)) { await _traceInputForModuleContents(dstConnection); await _traceOutputForModuleContents(dstConnection); } @@ -572,19 +534,16 @@ abstract class Module { } } on PortRulesViolationException catch (e) { throw PortRulesViolationException.trace( - module: this, - signal: signal, - lowerException: e, - traceDirection: 'from inputs', - ); + module: this, + signal: signal, + lowerException: e, + traceDirection: 'from inputs'); } } /// Searches for [Logic]s and [Module]s within this [Module] from its outputs. - Future _traceOutputForModuleContents( - Logic signal, { - bool dontAddSignal = false, - }) async { + Future _traceOutputForModuleContents(Logic signal, + {bool dontAddSignal = false}) async { if (isInput(signal) || _inOutDrivers.contains(signal)) { return; } @@ -620,28 +579,20 @@ abstract class Module { await _addAndBuildModule(subModule); } for (final subModuleInput in subModule._inputs.values) { - await _traceOutputForModuleContents( - subModuleInput, - dontAddSignal: true, - ); + await _traceOutputForModuleContents(subModuleInput, + dontAddSignal: true); } for (final subModuleOutput in subModule._outputs.values) { - await _traceInputForModuleContents( - subModuleOutput, - dontAddSignal: true, - ); + await _traceInputForModuleContents(subModuleOutput, + dontAddSignal: true); } for (final subModuleInOutDriver in subModule._inOutDrivers) { final subModDontAddSignal = subModuleInOutDriver.isPort; - await _traceInputForModuleContents( - subModuleInOutDriver, - dontAddSignal: subModDontAddSignal, - ); - await _traceOutputForModuleContents( - subModuleInOutDriver, - dontAddSignal: subModDontAddSignal, - ); + await _traceInputForModuleContents(subModuleInOutDriver, + dontAddSignal: subModDontAddSignal); + await _traceOutputForModuleContents(subModuleInOutDriver, + dontAddSignal: subModDontAddSignal); } } else { if (!dontAddSignal && @@ -652,25 +603,17 @@ abstract class Module { // handle expanding the search for arrays if (signal.parentStructure != null) { - await _traceOutputForModuleContents( - signal.parentStructure!, - dontAddSignal: signal.isPort, - ); - await _traceInputForModuleContents( - signal.parentStructure!, - dontAddSignal: signal.isPort, - ); + await _traceOutputForModuleContents(signal.parentStructure!, + dontAddSignal: signal.isPort); + await _traceInputForModuleContents(signal.parentStructure!, + dontAddSignal: signal.isPort); } if (signal is LogicStructure) { for (final elem in signal.elements) { - await _traceOutputForModuleContents( - elem, - dontAddSignal: elem.isPort, - ); - await _traceInputForModuleContents( - elem, - dontAddSignal: elem.isPort, - ); + await _traceOutputForModuleContents(elem, + dontAddSignal: elem.isPort); + await _traceInputForModuleContents(elem, + dontAddSignal: elem.isPort); } } @@ -682,15 +625,13 @@ abstract class Module { // extra searching in both directions for nets if (signal.isNet && !isPort(signal)) { await _traceInputForModuleContents(signal); - for (final srcConnection in signal.srcConnections.where( - (element) => element.isNet, - )) { + for (final srcConnection + in signal.srcConnections.where((element) => element.isNet)) { await _traceOutputForModuleContents(srcConnection); await _traceInputForModuleContents(srcConnection); } - for (final dstConnection in signal.dstConnections.where( - (element) => element.isNet, - )) { + for (final dstConnection + in signal.dstConnections.where((element) => element.isNet)) { await _traceOutputForModuleContents(dstConnection); await _traceInputForModuleContents(dstConnection); } @@ -698,10 +639,8 @@ abstract class Module { if (signal is LogicStructure) { for (final elem in signal.elements) { - await _traceOutputForModuleContents( - elem, - dontAddSignal: elem.isPort, - ); + await _traceOutputForModuleContents(elem, + dontAddSignal: elem.isPort); } } else { for (final srcConnection in signal.srcConnections) { @@ -711,11 +650,10 @@ abstract class Module { } } on PortRulesViolationException catch (e) { throw PortRulesViolationException.trace( - module: this, - signal: signal, - lowerException: e, - traceDirection: 'from outputs', - ); + module: this, + signal: signal, + lowerException: e, + traceDirection: 'from outputs'); } } @@ -736,8 +674,7 @@ abstract class Module { inputs.containsKey(name) || inOuts.containsKey(name)) { throw UnavailableReservedNameException.withMessage( - 'Already defined a port with name "$name" in module "${this.name}".', - ); + 'Already defined a port with name "$name" in module "${this.name}".'); } } @@ -787,9 +724,7 @@ abstract class Module { /// only be used within this [Module]. The provided [source] is accessible via /// [inputSource]. LogicType addTypedInput( - String name, - LogicType source, - ) { + String name, LogicType source) { _checkForSafePortName(name); source = _validateType(source, isOutput: false, name: name); @@ -801,10 +736,8 @@ abstract class Module { final inPort = (source.clone(name: name) as LogicType)..gets(source); if (inPort.name != name) { - throw PortTypeException.forIntendedName( - name, - 'The `clone` method for $source failed to update the signal name.', - ); + throw PortTypeException.forIntendedName(name, + 'The `clone` method for $source failed to update the signal name.'); } if (inPort is LogicStructure) { @@ -895,9 +828,7 @@ abstract class Module { /// only be used within this [Module]. The provided [source] is accessible via /// [inOutSource]. LogicType addTypedInOut( - String name, - LogicType source, - ) { + String name, LogicType source) { _checkForSafePortName(name); if (!source.isNet) { @@ -928,10 +859,8 @@ abstract class Module { final inOutPort = (source.clone(name: name) as LogicType)..gets(source); if (inOutPort.name != name) { - throw PortTypeException.forIntendedName( - name, - 'The `clone` method for $source failed to update the signal name.', - ); + throw PortTypeException.forIntendedName(name, + 'The `clone` method for $source failed to update the signal name.'); } if (inOutPort is LogicStructure) { @@ -1000,11 +929,8 @@ abstract class Module { /// Checks that the [logic] meets type requirements for `Typed` [Logic]s and /// returns a potentially modified [logic] to use. - LogicType _validateType( - LogicType logic, { - required String name, - required bool isOutput, - }) { + LogicType _validateType(LogicType logic, + {required String name, required bool isOutput}) { const exceptionMessage = 'Cannot use `Const` (or `LogicStructure` with `Const`s) as a port type.' ' Try passing in a `Logic` or parameterizing' @@ -1049,9 +975,7 @@ abstract class Module { /// /// The return value is the same as what is returned by [output]. LogicType addTypedOutput( - String name, - LogicType Function({String name}) logicGenerator, - ) { + String name, LogicType Function({String name}) logicGenerator) { _checkForSafePortName(name); // must make a new clone of it, to avoid people using ports of other modules @@ -1061,17 +985,14 @@ abstract class Module { if (outPort.isNet || (outPort is LogicStructure && outPort.hasNets)) { throw PortTypeException( - outPort, - 'Typed outputs cannot have nets in them.', - ); + outPort, 'Typed outputs cannot have nets in them.'); } if (outPort.name != name) { throw PortTypeException.forIntendedName( - name, - 'The `logicGenerator` function failed to' - ' update the signal name on $outPort.', - ); + name, + 'The `logicGenerator` function failed to' + ' update the signal name on $outPort.'); } if (outPort is LogicStructure) { @@ -1167,30 +1088,23 @@ abstract class Module { /// Connects the [source] to this [Module] using [Interface.connectIO] and /// returns a copy of the [source] that can be used within this module. InterfaceType addInterfacePorts, - TagType extends Enum>( - InterfaceType source, { - Iterable? inputTags, - Iterable? outputTags, - Iterable? inOutTags, - String Function(String original)? uniquify, - }) => + TagType extends Enum>(InterfaceType source, + {Iterable? inputTags, + Iterable? outputTags, + Iterable? inOutTags, + String Function(String original)? uniquify}) => (source.clone() as InterfaceType) - ..connectIO( - this, - source, - inputTags: inputTags, - outputTags: outputTags, - inOutTags: inOutTags, - uniquify: uniquify, - ); + ..connectIO(this, source, + inputTags: inputTags, + outputTags: outputTags, + inOutTags: inOutTags, + uniquify: uniquify); /// Connects the [source] to this [Module] using [PairInterface.pairConnectIO] /// and returns a copy of the [source] that can be used within this module. InterfaceType addPairInterfacePorts( - InterfaceType source, - PairRole role, { - String Function(String original)? uniquify, - }) => + InterfaceType source, PairRole role, + {String Function(String original)? uniquify}) => (source.clone() as InterfaceType) ..pairConnectIO(this, source, role, uniquify: uniquify); @@ -1199,7 +1113,7 @@ abstract class Module { '"$name" ($definitionName) : ', if (_inputs.isNotEmpty) '${_inputs.keys}', if (_outputs.isNotEmpty) '=> ${_outputs.keys}', - if (_inOuts.isNotEmpty) '; ${_inOuts.keys}', + if (_inOuts.isNotEmpty) '; ${_inOuts.keys}' ].join(' '); /// Returns a pretty-print [String] of the heirarchy of all [Module]s within @@ -1237,24 +1151,20 @@ abstract class Module { '''; return synthHeader + - SynthBuilder( - this, - SystemVerilogSynthesizer(), - ).getSynthFileContents().join('\n\n////////////////////\n\n'); + SynthBuilder(this, SystemVerilogSynthesizer()) + .getSynthFileContents() + .join('\n\n////////////////////\n\n'); } /// Returns a synthesized netlist JSON representation of this [Module]. - String generateNetlist({ - NetlistOptions options = const NetlistOptions(), - String? packageRoot, - }) { + String generateNetlist( + {NetlistOptions options = const NetlistOptions(), String? packageRoot}) { if (!_hasBuilt) { throw ModuleNotBuiltException(this); } - return NetlistSynthesizer( - options: options, - ).synthesizeToJson(this, packageRoot: packageRoot); + return NetlistSynthesizer(options: options) + .synthesizeToJson(this, packageRoot: packageRoot); } } diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart index 16cbd75ef..c9c819b17 100644 --- a/lib/src/synthesizers/netlist/netlist.dart +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -8,7 +8,4 @@ // Author: Desmond Kirkpatrick export 'netlist_options.dart'; -export 'netlist_passes.dart'; -export 'netlist_synthesis_result.dart'; export 'netlist_synthesizer.dart'; -export 'netlist_utils.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 780e58cf7..964fbe930 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -12,6 +12,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart index fe7f0f88e..365047ac5 100644 --- a/lib/src/synthesizers/netlist/netlist_passes.dart +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -14,6 +14,8 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; /// Post-processing optimization passes for netlist synthesis. /// diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index a58e6bceb..42f5a765c 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -13,6 +13,8 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; diff --git a/test/netlist_test.dart b/test/netlist_test.dart index 75bbc8ef4..8255a4041 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -14,6 +14,8 @@ import 'dart:io'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:test/test.dart'; import '../example/example.dart'; From 0baf624c9b93b12c38b27448453be655aa435753 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 10 Jul 2026 10:14:17 -0700 Subject: [PATCH 56/77] filter bank cleanup --- example/filter_bank.dart | 4 +- lib/src/examples/filter_bank/coeff_bank.dart | 62 ++ lib/src/examples/filter_bank/filter_bank.dart | 205 ++++ .../filter_bank/filter_bank_modules.dart | 17 + .../examples/filter_bank/filter_channel.dart | 234 +++++ .../filter_bank/filter_controller.dart | 177 ++++ .../filter_bank/filter_data_interface.dart | 63 ++ .../examples/filter_bank/filter_sample.dart | 51 + lib/src/examples/filter_bank/mac_unit.dart | 86 ++ .../examples/filter_bank/shared_data_bus.dart | 88 ++ lib/src/examples/filter_bank_modules.dart | 937 ------------------ .../synthesizers/netlist/netlist_utils.dart | 9 +- .../synthesizers/utilities/synth_logic.dart | 15 + test/netlist_synthesizer_test.dart | 40 +- test/netlist_test.dart | 2 +- 15 files changed, 1041 insertions(+), 949 deletions(-) create mode 100644 lib/src/examples/filter_bank/coeff_bank.dart create mode 100644 lib/src/examples/filter_bank/filter_bank.dart create mode 100644 lib/src/examples/filter_bank/filter_bank_modules.dart create mode 100644 lib/src/examples/filter_bank/filter_channel.dart create mode 100644 lib/src/examples/filter_bank/filter_controller.dart create mode 100644 lib/src/examples/filter_bank/filter_data_interface.dart create mode 100644 lib/src/examples/filter_bank/filter_sample.dart create mode 100644 lib/src/examples/filter_bank/mac_unit.dart create mode 100644 lib/src/examples/filter_bank/shared_data_bus.dart delete mode 100644 lib/src/examples/filter_bank_modules.dart diff --git a/example/filter_bank.dart b/example/filter_bank.dart index 39dd3fc5b..d8aea6292 100644 --- a/example/filter_bank.dart +++ b/example/filter_bank.dart @@ -21,10 +21,10 @@ import 'dart:async'; import 'package:rohd/rohd.dart'; // Import module definitions. -import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; // Re-export so downstream consumers (e.g. devtools loopback) can use. -export 'package:rohd/src/examples/filter_bank_modules.dart'; +export 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; // ────────────────────────────────────────────────────────────────── // Standalone simulation entry point diff --git a/lib/src/examples/filter_bank/coeff_bank.dart b/lib/src/examples/filter_bank/coeff_bank.dart new file mode 100644 index 000000000..da7523f6d --- /dev/null +++ b/lib/src/examples/filter_bank/coeff_bank.dart @@ -0,0 +1,62 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// coeff_bank.dart +// Coefficient storage module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A coefficient storage module backed by a [LogicArray] input port. +/// +/// Accepts a [LogicArray] of per-tap coefficients via [addInputArray] +/// and a tap index, then mux-selects the corresponding coefficient. +class CoeffBank extends Module { + /// The coefficient value at the selected index. + Logic get coeffOut => output('coeffOut'); + + /// The per-tap coefficient array (registered input port). + @protected + LogicArray get coeffArray => input('coeffArray') as LogicArray; + + /// The tap index input. + @protected + Logic get tapIndex => input('tapIndex'); + + /// Number of taps. + final int numTaps; + + /// Data width. + final int dataWidth; + + /// Creates a [CoeffBank] with [numTaps] taps at [dataWidth] bits. + /// + /// [coefficients] is a [LogicArray] with one element per tap — + /// registered as an input port via [addInputArray]. + /// [tapIndex] selects the active coefficient. + CoeffBank(Logic tapIndex, LogicArray coefficients, + {required this.numTaps, + required this.dataWidth, + super.name = 'CoeffBank'}) + : super(definitionName: 'CoeffBank_T${numTaps}_W$dataWidth') { + // Register ports + tapIndex = addInput('tapIndex', tapIndex, width: tapIndex.width); + final coeffArray = addInputArray('coeffArray', coefficients, + dimensions: [numTaps], elementWidth: dataWidth); + final coeffOut = addOutput('coeffOut', width: dataWidth); + + // Mux-chain ROM: priority-select coefficient by tap index. + Logic selected = Const(0, width: dataWidth); + for (var i = numTaps - 1; i >= 0; i--) { + selected = mux( + tapIndex.eq(Const(i, width: tapIndex.width)).named('tapMatch$i'), + coeffArray.elements[i], + selected, + ); + } + coeffOut <= selected; + } +} diff --git a/lib/src/examples/filter_bank/filter_bank.dart b/lib/src/examples/filter_bank/filter_bank.dart new file mode 100644 index 000000000..e56bcd6bc --- /dev/null +++ b/lib/src/examples/filter_bank/filter_bank.dart @@ -0,0 +1,205 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank.dart +// Top-level polyphase FIR filter bank module for the example library. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank/filter_channel.dart'; +import 'package:rohd/src/examples/filter_bank/filter_controller.dart'; +import 'package:rohd/src/examples/filter_bank/filter_data_interface.dart'; +import 'package:rohd/src/examples/filter_bank/filter_sample.dart'; +import 'package:rohd/src/examples/filter_bank/shared_data_bus.dart'; + +/// A 2-channel polyphase FIR filter bank. +/// +/// Hierarchy: +/// ```text +/// FilterBank (top) +/// ├── FilterController (FSM) +/// ├── FilterChannel 'ch0' +/// │ ├── CoeffBank (coefficient ROM via LogicArray + mux chain) +/// │ └── MacUnit 'mac' (pipelined multiply-accumulate) +/// └── FilterChannel 'ch1' +/// ├── CoeffBank +/// └── MacUnit 'mac' +/// ``` +/// +/// Each channel time-multiplexes a single MacUnit across all taps, +/// sequenced by a tap counter that drives the CoeffBank tap index +/// and a delay-line sample mux. +/// +/// Uses: +/// - [FilterDataInterface] for I/O port bundles +/// - [FilterSample] LogicStructure for structured sample signals +/// - [LogicArray] in CoeffBank for coefficient storage +/// - [Pipeline] in MacUnit for pipelined MAC +/// - [FiniteStateMachine] in FilterController for sequencing +/// - Multiple instantiation: two [FilterChannel]s share one definition +/// - [LogicNet] / [addInOut] for bidirectional shared data bus +class FilterBank extends Module { + /// Per-channel filtered outputs as a [LogicArray]. + /// + /// `channelOut.elements[i]` is the filtered output of channel `i`. + LogicArray get channelOut => output('channelOut') as LogicArray; + + /// Channel 0 filtered output (convenience getter). + Logic get out0 => channelOut.elements[0]; + + /// Channel 1 filtered output (convenience getter). + Logic get out1 => channelOut.elements[1]; + + /// Output valid (aligned with filtered outputs). + Logic get validOut => output('validOut'); + + /// Done signal from the controller FSM. + Logic get done => output('done'); + + /// Controller state (for debug visibility). + Logic get state => output('state'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input [FilterSample] port for channel [ch]. + @protected + FilterSample samplePin(int ch) => input('sample$ch') as FilterSample; + + /// Input-done strobe. + @protected + Logic get inputDonePin => input('inputDone'); + + /// Number of FIR taps per channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Number of filter channels. + final int numChannels; + + /// Creates a [FilterBank] with [numChannels] channels (default 2). + /// + /// Each channel has [numTaps] FIR taps at [dataWidth] bits. + /// [coefficients] is a list of per-channel coefficient lists — + /// `coefficients[i]` supplies the tap weights for channel `i`. + /// [samples] is a [LogicArray] with one element per channel. + /// [inputDone] when the input stream is complete. + /// + /// Optionally pass [dataBus] (a `LogicNet`) and [writeEnable] to + /// attach a bidirectional shared data bus via [SharedDataBus]. + /// The bus latches external data when [writeEnable] is low and + /// drives `storedValue` output. + FilterBank( + Logic clk, + Logic reset, + Logic start, + List samples, + Logic inputDone, { + required this.numTaps, + required this.dataWidth, + required List> coefficients, + this.numChannels = 2, + LogicNet? dataBus, + Logic? writeEnable, + super.name = 'FilterBank', + String? definitionName, + }) : super(definitionName: definitionName ?? 'FilterBank') { + if (coefficients.length != numChannels) { + throw Exception( + 'coefficients must have $numChannels entries (one per channel).'); + } + + // ── Register ports ── + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputDone = addInput('inputDone', inputDone); + + // One typed FilterSample input port per channel. + final inPorts = []; + for (var ch = 0; ch < numChannels; ch++) { + inPorts.add(addTypedInput('sample$ch', samples[ch])); + } + + final channelOut = addTypedOutput( + 'channelOut', + ({name = 'channelOut'}) => + LogicArray([numChannels], dataWidth, name: name)); + final validOut = addOutput('validOut'); + final done = addOutput('done'); + final state = addOutput('state', width: 3); + + // ── Controller FSM ── + // Drain cycles: numTaps cycles per accumulation + pipeline depth (2) + 1 + final controller = FilterController( + clk, + reset, + start, + inPorts[0].valid, // valid is shared across channels + inputDone, + drainCycles: numTaps + 3, + name: 'controller', + ); + + final filterEnable = controller.filterEnable; + + // ── Per-channel filter instantiation ── + final srcIntfs = []; + for (var ch = 0; ch < numChannels; ch++) { + final srcIntf = FilterDataInterface(dataWidth: dataWidth); + srcIntf.sampleIn <= inPorts[ch].data; + srcIntf.validIn <= inPorts[ch].valid; + + FilterChannel( + srcIntf, + clk, + reset, + filterEnable, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients[ch], + name: 'ch$ch', + ); + + srcIntfs.add(srcIntf); + } + + // ── Connect outputs ── + for (var ch = 0; ch < numChannels; ch++) { + channelOut.elements[ch] <= srcIntfs[ch].dataOut; + } + validOut <= srcIntfs[0].validOut; + done <= controller.doneFlag; + state <= controller.state; + + // ── Optional shared data bus (inOut port) ── + if (dataBus != null && writeEnable != null) { + final busPort = addInOut('dataBus', dataBus, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + final storedValue = addOutput('storedValue', width: dataWidth); + + final sharedBus = SharedDataBus( + LogicNet(name: 'busNet', width: dataWidth)..gets(busPort), + writeEnable, + clk, + reset, + dataWidth: dataWidth, + ); + storedValue <= sharedBus.storedValue; + } + } +} diff --git a/lib/src/examples/filter_bank/filter_bank_modules.dart b/lib/src/examples/filter_bank/filter_bank_modules.dart new file mode 100644 index 000000000..5341784d8 --- /dev/null +++ b/lib/src/examples/filter_bank/filter_bank_modules.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_modules.dart +// Barrel file for the polyphase FIR filter bank example modules. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +export 'coeff_bank.dart'; +export 'filter_bank.dart'; +export 'filter_channel.dart'; +export 'filter_controller.dart'; +export 'filter_data_interface.dart'; +export 'filter_sample.dart'; +export 'mac_unit.dart'; +export 'shared_data_bus.dart'; diff --git a/lib/src/examples/filter_bank/filter_channel.dart b/lib/src/examples/filter_bank/filter_channel.dart new file mode 100644 index 000000000..a36cac48f --- /dev/null +++ b/lib/src/examples/filter_bank/filter_channel.dart @@ -0,0 +1,234 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_channel.dart +// Single FIR channel module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank/coeff_bank.dart'; +import 'package:rohd/src/examples/filter_bank/filter_data_interface.dart'; +import 'package:rohd/src/examples/filter_bank/mac_unit.dart'; + +/// A single polyphase FIR filter channel with [numTaps] taps. +/// +/// Uses a [FilterDataInterface] for its sample I/O ports. +/// +/// Architecture: +/// - A delay line (shift register) captures incoming samples. +/// - A tap counter cycles 0 … numTaps-1 each sample period. +/// - [CoeffBank] provides the coefficient for the current tap. +/// - A mux selects the delay-line sample for the current tap. +/// - A single [MacUnit] multiplies the selected sample by the +/// coefficient and adds it to a running accumulator. +/// - After all taps are processed the accumulator is latched as +/// the output and the accumulator resets for the next sample. +class FilterChannel extends Module { + /// The data interface for this channel (internal use only). + @protected + late final FilterDataInterface intf; + + /// Filtered output. + Logic get dataOut => intf.dataOut; + + /// Output valid. + Logic get validOut => intf.validOut; + + /// Number of FIR taps in this channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Creates a [FilterChannel] with [numTaps] taps at [dataWidth] bits. + /// + /// [srcIntf] provides the sample/valid input ports. [coefficients] + /// supplies per-tap constant coefficients. + FilterChannel( + FilterDataInterface srcIntf, + Logic clk, + Logic reset, + Logic enable, { + required this.numTaps, + required this.dataWidth, + required List coefficients, + super.name = 'FilterChannel', + }) : super(definitionName: 'FilterChannel_T${numTaps}_W$dataWidth') { + // Connect the Interface — creates module input/output ports + intf = FilterDataInterface(dataWidth: dataWidth) + ..connectIO(this, srcIntf, + inputTags: [FilterPortTag.inputPorts], + outputTags: [FilterPortTag.outputPorts]); + + final sampleIn = intf.sampleIn; + final validIn = intf.validIn; + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + + final tapIdxWidth = _bitsFor(numTaps); + + // ── Delay line (shift register via explicit flop bank + gates) ── + // AND gate: shift enable = enable & validIn & tapCounter==0 + // Samples shift in only when starting a new accumulation cycle. + final tapCounter = Logic(width: tapIdxWidth, name: 'tapCounter'); + final atFirstTap = + tapCounter.eq(Const(0, width: tapIdxWidth)).named('atFirstTap'); + final shiftEn = Logic(name: 'shiftEn'); + shiftEn <= (enable & validIn).named('enableAndValid') & atFirstTap; + + // LogicArray-backed delay line: one element per tap register. + final delayLine = LogicArray([numTaps], dataWidth, name: 'delayLine'); + for (var i = 0; i < numTaps; i++) { + final tapInput = (i == 0) ? sampleIn : delayLine.elements[i - 1]; + // Mux: hold current value or shift in new sample + final tapNext = Logic(width: dataWidth, name: 'nextTap$i'); + tapNext <= mux(shiftEn, tapInput, delayLine.elements[i]); + // Flop: register the next-state value + delayLine.elements[i] <= flop(clk, reset: reset, tapNext); + } + + // ── Coefficient bank — driven by tapCounter ── + // Build a LogicArray of constants from the coefficient list and + // pass it as an input port to CoeffBank (demonstrates addInputArray + // on a sub-module). + final coeffArray = LogicArray([numTaps], dataWidth, name: 'coeffArray'); + for (var i = 0; i < numTaps; i++) { + coeffArray.elements[i] <= Const(coefficients[i], width: dataWidth); + } + + final coeffBank = CoeffBank( + tapCounter, + coeffArray, + numTaps: numTaps, + dataWidth: dataWidth, + name: 'coeffBank', + ); + + // ── Delay-line mux — select sample for current tap ── + var selectedSample = delayLine.elements[0]; + for (var i = 1; i < numTaps; i++) { + final tapSelect = + tapCounter.eq(Const(i, width: tapIdxWidth)).named('tapSelect$i'); + selectedSample = mux(tapSelect, delayLine.elements[i], selectedSample) + .named('tapMux$i'); + } + + // ── Running accumulator (feedback register) ── + final accumReg = Logic(width: dataWidth, name: 'accumReg'); + // Reset accumulator at the start of each new sample (tap 0). + // Combinational block: equivalent to `always_comb` in SystemVerilog. + final accumFeedback = Logic(width: dataWidth, name: 'accumFeedback'); + Combinational([ + If(atFirstTap, then: [ + accumFeedback < Const(0, width: dataWidth), + ], orElse: [ + accumFeedback < accumReg, + ]), + ]); + + // ── Single MAC unit — time-multiplexed across taps ── + final mac = MacUnit( + selectedSample, + coeffBank.coeffOut, + accumFeedback, + clk, + reset, + enable, + dataWidth: dataWidth, + name: 'mac', + ); + + // Register the MAC result for accumulator feedback. + accumReg <= flop(clk, reset: reset, mac.result); + + // ── Tap counter: cycles 0 … numTaps-1 while enabled ── + // Sequential block: equivalent to `always_ff @(posedge clk)` in SV. + // When enabled, the counter increments and wraps at numTaps-1. + // When disabled, it resets to 0. + final lastTap = + tapCounter.eq(Const(numTaps - 1, width: tapIdxWidth)).named('lastTap'); + Sequential(clk, reset: reset, [ + If(enable, then: [ + If(lastTap, then: [ + tapCounter < Const(0, width: tapIdxWidth), + ], orElse: [ + tapCounter < tapCounter + Const(1, width: tapIdxWidth), + ]), + ], orElse: [ + tapCounter < Const(0, width: tapIdxWidth), + ]), + ]); + + // ── Output latch: capture accumulator when all taps processed ── + // The MAC pipeline has 2 stages, so the result is ready 2 cycles + // after the last tap enters. A 2-stage shift register of lastTap + // creates the latch strobe. + final lastTapD1 = Logic(name: 'lastTapD1'); + final lastTapD2 = Logic(name: 'lastTapD2'); + final outputReg = Logic(width: dataWidth, name: 'outputReg'); + + // Sequential block with If: latch strobe delay and output register. + Sequential(clk, reset: reset, [ + lastTapD1 < lastTap, + lastTapD2 < lastTapD1, + If(lastTapD2, then: [ + outputReg < accumReg, + ]), + ]); + + // ── Valid pipeline: track whether we have a valid output ── + // validIn is high during data injection. After the MAC pipeline + // latency (numTaps + 2 cycles), outputs become valid. + final validPipe = Logic(name: 'validPipe'); + final outputReady = (lastTapD2 & enable).named('outputReady'); + + // Sequential block: register the valid strobe and hold it. + Sequential(clk, reset: reset, [ + If(enable, then: [ + validPipe < outputReady, + ]), + ]); + + // Combinational block: gate the output to zero when not valid. + final dataOut = intf.dataOut; + final validOut = intf.validOut; + Combinational([ + If(validPipe, then: [ + dataOut < outputReg, + ], orElse: [ + dataOut < Const(0, width: dataWidth), + ]), + validOut < validPipe, + ]); + } + + /// Minimum bits needed to represent [n] values. + static int _bitsFor(int n) { + if (n <= 1) { + return 1; + } + var bits = 0; + var v = n - 1; + while (v > 0) { + bits++; + v >>= 1; + } + return bits; + } +} diff --git a/lib/src/examples/filter_bank/filter_controller.dart b/lib/src/examples/filter_bank/filter_controller.dart new file mode 100644 index 000000000..cfc730ef4 --- /dev/null +++ b/lib/src/examples/filter_bank/filter_controller.dart @@ -0,0 +1,177 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_controller.dart +// FSM controller module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// States for the [FilterController] finite state machine. +enum FilterState { + /// Waiting for the start signal. + idle, + + /// Accepting initial samples into the delay line. + loading, + + /// Normal filtering operation. + running, + + /// Flushing the pipeline after the input stream ends. + draining, + + /// Processing complete. + done, +} + +/// Controls the filter bank operation via a [FiniteStateMachine]. +/// +/// - idle: waiting for start signal +/// - loading: accepting initial samples into delay line +/// - running: normal filtering +/// - draining: flushing pipeline after input stream ends +/// - done: processing complete +class FilterController extends Module { + /// Encoded FSM state (3 bits). + Logic get state => output('state'); + + /// High while the filter channels should be processing. + Logic get filterEnable => output('filterEnable'); + + /// High during the initial sample-loading phase. + Logic get loadingPhase => output('loadingPhase'); + + /// Asserted when the filter bank has finished processing. + Logic get doneFlag => output('doneFlag'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input valid. + @protected + Logic get inputValidPin => input('inputValid'); + + /// Input done. + @protected + Logic get inputDonePin => input('inputDone'); + + late final FiniteStateMachine _fsm; + + /// Returns the FSM's current state index for a given [FilterState]. + int? getStateIndex(FilterState s) => _fsm.getStateIndex(s); + + /// Creates a [FilterController] that sequences the filter bank. + /// + /// After [start] is asserted the FSM moves through loading → running + /// → draining (for [drainCycles] cycles) → done. + FilterController( + Logic clk, Logic reset, Logic start, Logic inputValid, Logic inputDone, + {required int drainCycles, super.name = 'FilterController'}) + : super(definitionName: 'FilterController') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputValid = addInput('inputValid', inputValid); + inputDone = addInput('inputDone', inputDone); + + final filterEnable = addOutput('filterEnable'); + final loadingPhase = addOutput('loadingPhase'); + final doneFlag = addOutput('doneFlag'); + final state = addOutput('state', width: 3); + + // Drain counter + final drainCount = Logic(width: 8, name: 'drainCount'); + final drainDone = + drainCount.eq(Const(drainCycles, width: 8)).named('drainDone'); + + _fsm = FiniteStateMachine( + clk, + reset, + FilterState.idle, + [ + State( + FilterState.idle, + events: { + start: FilterState.loading, + }, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.loading, + events: { + inputValid: FilterState.running, + }, + actions: [ + filterEnable < 1, + loadingPhase < 1, + doneFlag < 0, + ], + ), + State( + FilterState.running, + events: { + inputDone: FilterState.draining, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.draining, + events: { + drainDone: FilterState.done, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.done, + events: {}, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 1, + ], + ), + ], + ); + + state <= _fsm.currentState.zeroExtend(state.width); + + // Drain counter: Sequential block increments while draining, + // resets to zero otherwise. + final drainIdx = _fsm.getStateIndex(FilterState.draining)!; + final isDraining = Logic(name: 'isDraining'); + isDraining <= _fsm.currentState.eq(Const(drainIdx, width: _fsm.stateWidth)); + + Sequential(clk, reset: reset, [ + If(isDraining, then: [ + drainCount < drainCount + Const(1, width: 8), + ], orElse: [ + drainCount < Const(0, width: 8), + ]), + ]); + } +} diff --git a/lib/src/examples/filter_bank/filter_data_interface.dart b/lib/src/examples/filter_bank/filter_data_interface.dart new file mode 100644 index 000000000..06faf7dba --- /dev/null +++ b/lib/src/examples/filter_bank/filter_data_interface.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_data_interface.dart +// Interface definition for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Tags for grouping port directions in [FilterDataInterface]. +enum FilterPortTag { + /// Ports carrying data into the filter (`sampleIn`, `validIn`). + inputPorts, + + /// Ports carrying data out of the filter (`dataOut`, `validOut`). + outputPorts, +} + +/// An interface carrying sample data and control into/out of filter modules. +/// +/// Groups ports by [FilterPortTag] so that [connectIO] can wire +/// inputs and outputs in a single call. +class FilterDataInterface extends Interface { + /// Input sample data bus. + Logic get sampleIn => port('sampleIn'); + + /// Input valid strobe. + Logic get validIn => port('validIn'); + + /// Output filtered data bus. + Logic get dataOut => port('dataOut'); + + /// Output valid strobe. + Logic get validOut => port('validOut'); + + /// The data width used by this interface. + final int _dataWidth; + + /// Creates a [FilterDataInterface] with the given [dataWidth] + /// (default 16 bits). + FilterDataInterface({int dataWidth = 16}) : _dataWidth = dataWidth { + setPorts([ + Logic.port('sampleIn', dataWidth), + Logic.port('validIn'), + ], [ + FilterPortTag.inputPorts + ]); + + setPorts([ + Logic.port('dataOut', dataWidth), + Logic.port('validOut'), + ], [ + FilterPortTag.outputPorts + ]); + } + + @override + + /// Returns a new interface with the same data width. + FilterDataInterface clone() => FilterDataInterface(dataWidth: _dataWidth); +} diff --git a/lib/src/examples/filter_bank/filter_sample.dart b/lib/src/examples/filter_bank/filter_sample.dart new file mode 100644 index 000000000..28d40dc89 --- /dev/null +++ b/lib/src/examples/filter_bank/filter_sample.dart @@ -0,0 +1,51 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_sample.dart +// LogicStructure sample word for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// A structured signal bundling a data sample with metadata. +/// +/// Packs three fields — [data], and [valid] — into a single +/// bus that can be driven and sampled as a unit. Used throughout the +/// filter bank to carry tagged samples between modules. +class FilterSample extends LogicStructure { + /// The sample data word. + late final Logic data; + + /// Whether this sample is valid. + late final Logic valid; + + /// Creates a [FilterSample] with the given [dataWidth] (default 16) + /// and optional [name]. + FilterSample({int dataWidth = 16, String? name}) + : super( + [ + Logic(name: 'data', width: dataWidth), + Logic(name: 'valid'), + ], + name: name ?? 'filter_sample', + ) { + data = elements[0]; + valid = elements[1]; + } + + // Private constructor for clone to share element structure. + FilterSample._clone(super.elements, {required super.name}) { + data = elements[0]; + valid = elements[1]; + } + + @override + + /// Returns a structural clone of this sample, preserving element names. + FilterSample clone({String? name}) => FilterSample._clone( + elements.map((e) => e.clone(name: e.name)), + name: name ?? this.name, + ); +} diff --git a/lib/src/examples/filter_bank/mac_unit.dart b/lib/src/examples/filter_bank/mac_unit.dart new file mode 100644 index 000000000..3bf43603a --- /dev/null +++ b/lib/src/examples/filter_bank/mac_unit.dart @@ -0,0 +1,86 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// mac_unit.dart +// Multiply-accumulate module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A pipelined multiply-accumulate unit. +/// +/// Pipeline stage 0: multiply sample × coefficient +/// Pipeline stage 1: add product to running accumulator +class MacUnit extends Module { + /// Accumulated result. + Logic get result => output('result'); + + /// Sample data input. + @protected + Logic get sampleInPin => input('sampleIn'); + + /// Coefficient input. + @protected + Logic get coeffInPin => input('coeffIn'); + + /// Accumulator input. + @protected + Logic get accumInPin => input('accumIn'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Data width. + final int dataWidth; + + /// Creates a [MacUnit] that multiplies [sampleIn] by [coeffIn] in + /// stage 0 and adds the product to [accumIn] in stage 1. + /// + /// [clk], [reset], and [enable] control the pipeline registers. + MacUnit(Logic sampleIn, Logic coeffIn, Logic accumIn, Logic clk, Logic reset, + Logic enable, + {required this.dataWidth, super.name = 'MacUnit'}) + : super(definitionName: 'MacUnit_W$dataWidth') { + sampleIn = addInput('sampleIn', sampleIn, width: dataWidth); + coeffIn = addInput('coeffIn', coeffIn, width: dataWidth); + accumIn = addInput('accumIn', accumIn, width: dataWidth); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + final result = addOutput('result', width: dataWidth); + + // A 2-stage pipeline: multiply, then accumulate + final pipe = Pipeline( + clk, + reset: reset, + stages: [ + // Stage 0: multiply + (p) => [ + // Product = sample * coefficient (truncated to dataWidth) + p.get(sampleIn) < + (p.get(sampleIn) * p.get(coeffIn)).named('product'), + ], + // Stage 1: accumulate + (p) => [ + p.get(sampleIn) < + (p.get(sampleIn) + p.get(accumIn)).named('macSum'), + ], + ], + signals: [sampleIn, coeffIn, accumIn], + ); + + result <= pipe.get(sampleIn); + } +} diff --git a/lib/src/examples/filter_bank/shared_data_bus.dart b/lib/src/examples/filter_bank/shared_data_bus.dart new file mode 100644 index 000000000..1d86462d5 --- /dev/null +++ b/lib/src/examples/filter_bank/shared_data_bus.dart @@ -0,0 +1,88 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// shared_data_bus.dart +// Bidirectional data bus module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A module with a bidirectional data bus for loading/reading data. +/// +/// In real hardware, a shared data bus is common for: +/// - Loading filter coefficients from external memory +/// - Reading diagnostic status or filter output snapshots +/// +/// Direction is controlled by `writeEnable`: when high, the module's +/// internal [TriStateBuffer] drives `storedValue` onto `dataBus`; +/// when low, the external driver owns the bus and the module latches +/// the incoming value into a register. +/// +/// Exercises `addInOut` / `LogicNet` / [TriStateBuffer] / inout port +/// direction through the full ROHD stack: synthesis, hierarchy, +/// waveform capture, and DevTools rendering. +class SharedDataBus extends Module { + /// The bidirectional data bus port. + Logic get dataBus => inOut('dataBus'); + + /// The stored value (latched when the bus is driven externally). + Logic get storedValue => output('storedValue'); + + /// Write-enable input. + @protected + Logic get writeEnablePin => input('writeEnable'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Data width in bits. + final int dataWidth; + + /// Creates a [SharedDataBus] with a [dataWidth]-bit bidirectional port. + /// + /// [dataBusNet] is the external [LogicNet] to connect. + /// [writeEnable] controls bus direction: 1 = module drives bus, + /// 0 = external drives bus (module reads). + /// [clk] and [reset] provide synchronous storage. + SharedDataBus( + LogicNet dataBusNet, + Logic writeEnable, + Logic clk, + Logic reset, { + required this.dataWidth, + super.name = 'SharedDataBus', + }) : super(definitionName: 'SharedDataBus') { + final bus = addInOut('dataBus', dataBusNet, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + final storedValue = addOutput('storedValue', width: dataWidth); + + // Latch the bus value on clock edge when the external side is driving. + storedValue <= + flop( + clk, + bus, + reset: reset, + en: ~writeEnable, + resetValue: Const(0, width: dataWidth), + ); + + // Drive the latched value back onto the bus when writeEnable is high. + // TriStateBuffer drives its out (a LogicNet) with storedValue when + // enabled; otherwise it outputs high-Z. Joining out↔bus makes the + // two nets share the same wire. + TriStateBuffer(storedValue, enable: writeEnable, name: 'busDriver') + .out + .gets(bus); + } +} diff --git a/lib/src/examples/filter_bank_modules.dart b/lib/src/examples/filter_bank_modules.dart deleted file mode 100644 index 10ec1d329..000000000 --- a/lib/src/examples/filter_bank_modules.dart +++ /dev/null @@ -1,937 +0,0 @@ -// Copyright (C) 2025-2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// filter_bank_modules.dart -// Module class definitions for the polyphase FIR filter bank example. -// -// 2025 March 26 -// Author: Desmond Kirkpatrick -// -// Architecture: each FilterChannel uses a single MacUnit that is -// time-multiplexed across taps. A tap counter sequences CoeffBank -// and a delay-line mux so the MAC accumulates one tap per clock cycle. -// After numTaps cycles the accumulated result is latched as the output -// sample and the accumulator resets for the next input sample. -// -// ROHD features exercised: -// - LogicStructure (FilterSample) -// - Interface (FilterDataInterface) -// - LogicArray (CoeffBank coefficient ROM, delay line) -// - Pipeline (MacUnit multiply-accumulate) -// - FiniteStateMachine (FilterController) -// - Multiple instantiation (two FilterChannels share one definition) -// -// Separated from filter_bank.dart so these classes can be imported -// in web-targeted code (no dart:io dependency). -// -// 2026 March 26 -// Author: Desmond Kirkpatrick - -import 'package:meta/meta.dart'; -import 'package:rohd/rohd.dart'; - -// ────────────────────────────────────────────────────────────────── -// LogicStructure: a typed sample word carrying data + valid + channel -// ────────────────────────────────────────────────────────────────── - -/// A structured signal bundling a data sample with metadata. -/// -/// Packs three fields — [data], and [valid] — into a single -/// bus that can be driven and sampled as a unit. Used throughout the -/// [FilterBank] to carry tagged samples between modules. -class FilterSample extends LogicStructure { - /// The sample data word. - late final Logic data; - - /// Whether this sample is valid. - late final Logic valid; - - /// Creates a [FilterSample] with the given [dataWidth] (default 16) - /// and optional [name]. - FilterSample({int dataWidth = 16, String? name}) - : super( - [ - Logic(name: 'data', width: dataWidth), - Logic(name: 'valid'), - ], - name: name ?? 'filter_sample', - ) { - data = elements[0]; - valid = elements[1]; - } - - // Private constructor for clone to share element structure. - FilterSample._clone(super.elements, {required super.name}) { - data = elements[0]; - valid = elements[1]; - } - - @override - - /// Returns a structural clone of this sample, preserving element names. - FilterSample clone({String? name}) => FilterSample._clone( - elements.map((e) => e.clone(name: e.name)), - name: name ?? this.name, - ); -} - -// ────────────────────────────────────────────────────────────────── -// Interface: tagged port bundle for filter data I/O -// ────────────────────────────────────────────────────────────────── - -/// Tags for grouping port directions in [FilterDataInterface]. -enum FilterPortTag { - /// Ports carrying data into the filter (`sampleIn`, `validIn`). - inputPorts, - - /// Ports carrying data out of the filter (`dataOut`, `validOut`). - outputPorts, -} - -/// An interface carrying sample data and control into/out of filter modules. -/// -/// Groups ports by [FilterPortTag] so that [connectIO] can wire -/// inputs and outputs in a single call. -class FilterDataInterface extends Interface { - /// Input sample data bus. - Logic get sampleIn => port('sampleIn'); - - /// Input valid strobe. - Logic get validIn => port('validIn'); - - /// Output filtered data bus. - Logic get dataOut => port('dataOut'); - - /// Output valid strobe. - Logic get validOut => port('validOut'); - - /// The data width used by this interface. - final int _dataWidth; - - /// Creates a [FilterDataInterface] with the given [dataWidth] - /// (default 16 bits). - FilterDataInterface({int dataWidth = 16}) : _dataWidth = dataWidth { - setPorts([ - Logic.port('sampleIn', dataWidth), - Logic.port('validIn'), - ], [ - FilterPortTag.inputPorts - ]); - - setPorts([ - Logic.port('dataOut', dataWidth), - Logic.port('validOut'), - ], [ - FilterPortTag.outputPorts - ]); - } - - @override - - /// Returns a new interface with the same data width. - FilterDataInterface clone() => FilterDataInterface(dataWidth: _dataWidth); -} - -// ────────────────────────────────────────────────────────────────── -// CoeffBank: stores FIR tap coefficients in a LogicArray -// ────────────────────────────────────────────────────────────────── - -/// A coefficient storage module backed by a [LogicArray] input port. -/// -/// Accepts a [LogicArray] of per-tap coefficients via [addInputArray] -/// and a tap index, then mux-selects the corresponding coefficient. -class CoeffBank extends Module { - /// The coefficient value at the selected index. - Logic get coeffOut => output('coeffOut'); - - /// The per-tap coefficient array (registered input port). - @protected - LogicArray get coeffArray => input('coeffArray') as LogicArray; - - /// The tap index input. - @protected - Logic get tapIndex => input('tapIndex'); - - /// Number of taps. - final int numTaps; - - /// Data width. - final int dataWidth; - - /// Creates a [CoeffBank] with [numTaps] taps at [dataWidth] bits. - /// - /// [coefficients] is a [LogicArray] with one element per tap — - /// registered as an input port via [addInputArray]. - /// [tapIndex] selects the active coefficient. - CoeffBank(Logic tapIndex, LogicArray coefficients, - {required this.numTaps, - required this.dataWidth, - super.name = 'CoeffBank'}) - : super(definitionName: 'CoeffBank_T${numTaps}_W$dataWidth') { - // Register ports - tapIndex = addInput('tapIndex', tapIndex, width: tapIndex.width); - final coeffArray = addInputArray('coeffArray', coefficients, - dimensions: [numTaps], elementWidth: dataWidth); - final coeffOut = addOutput('coeffOut', width: dataWidth); - - // Mux-chain ROM: priority-select coefficient by tap index. - Logic selected = Const(0, width: dataWidth); - for (var i = numTaps - 1; i >= 0; i--) { - selected = mux( - tapIndex.eq(Const(i, width: tapIndex.width)).named('tapMatch$i'), - coeffArray.elements[i], - selected, - ); - } - coeffOut <= selected; - } -} - -// ────────────────────────────────────────────────────────────────── -// MacUnit: a single multiply-accumulate pipeline stage -// ────────────────────────────────────────────────────────────────── - -/// A pipelined multiply-accumulate unit. -/// -/// Pipeline stage 0: multiply sample × coefficient -/// Pipeline stage 1: add product to running accumulator -class MacUnit extends Module { - /// Accumulated result. - Logic get result => output('result'); - - /// Sample data input. - @protected - Logic get sampleInPin => input('sampleIn'); - - /// Coefficient input. - @protected - Logic get coeffInPin => input('coeffIn'); - - /// Accumulator input. - @protected - Logic get accumInPin => input('accumIn'); - - /// Clock input. - @protected - Logic get clkPin => input('clk'); - - /// Reset input. - @protected - Logic get resetPin => input('reset'); - - /// Enable input. - @protected - Logic get enablePin => input('enable'); - - /// Data width. - final int dataWidth; - - /// Creates a [MacUnit] that multiplies [sampleIn] by [coeffIn] in - /// stage 0 and adds the product to [accumIn] in stage 1. - /// - /// [clk], [reset], and [enable] control the pipeline registers. - MacUnit(Logic sampleIn, Logic coeffIn, Logic accumIn, Logic clk, Logic reset, - Logic enable, - {required this.dataWidth, super.name = 'MacUnit'}) - : super(definitionName: 'MacUnit_W$dataWidth') { - sampleIn = addInput('sampleIn', sampleIn, width: dataWidth); - coeffIn = addInput('coeffIn', coeffIn, width: dataWidth); - accumIn = addInput('accumIn', accumIn, width: dataWidth); - clk = addInput('clk', clk); - reset = addInput('reset', reset); - enable = addInput('enable', enable); - final result = addOutput('result', width: dataWidth); - - // A 2-stage pipeline: multiply, then accumulate - final pipe = Pipeline( - clk, - reset: reset, - stages: [ - // Stage 0: multiply - (p) => [ - // Product = sample * coefficient (truncated to dataWidth) - p.get(sampleIn) < - (p.get(sampleIn) * p.get(coeffIn)).named('product'), - ], - // Stage 1: accumulate - (p) => [ - p.get(sampleIn) < - (p.get(sampleIn) + p.get(accumIn)).named('macSum'), - ], - ], - signals: [sampleIn, coeffIn, accumIn], - ); - - result <= pipe.get(sampleIn); - } -} - -// ────────────────────────────────────────────────────────────────── -// FilterChannel: one polyphase FIR channel with time-multiplexed MAC -// ────────────────────────────────────────────────────────────────── - -/// A single polyphase FIR filter channel with [numTaps] taps. -/// -/// Uses a [FilterDataInterface] for its sample I/O ports. -/// -/// Architecture: -/// - A delay line (shift register) captures incoming samples. -/// - A tap counter cycles 0 … numTaps-1 each sample period. -/// - [CoeffBank] provides the coefficient for the current tap. -/// - A mux selects the delay-line sample for the current tap. -/// - A single [MacUnit] multiplies the selected sample by the -/// coefficient and adds it to a running accumulator. -/// - After all taps are processed the accumulator is latched as -/// the output and the accumulator resets for the next sample. -class FilterChannel extends Module { - /// The data interface for this channel (internal use only). - @protected - late final FilterDataInterface intf; - - /// Filtered output. - Logic get dataOut => intf.dataOut; - - /// Output valid. - Logic get validOut => intf.validOut; - - /// Number of FIR taps in this channel. - final int numTaps; - - /// Bit width of each data sample. - final int dataWidth; - - /// Clock input. - @protected - Logic get clkPin => input('clk'); - - /// Reset input. - @protected - Logic get resetPin => input('reset'); - - /// Enable input. - @protected - Logic get enablePin => input('enable'); - - /// Creates a [FilterChannel] with [numTaps] taps at [dataWidth] bits. - /// - /// [srcIntf] provides the sample/valid input ports. [coefficients] - /// supplies per-tap constant coefficients. - FilterChannel( - FilterDataInterface srcIntf, - Logic clk, - Logic reset, - Logic enable, { - required this.numTaps, - required this.dataWidth, - required List coefficients, - super.name = 'FilterChannel', - }) : super(definitionName: 'FilterChannel_T${numTaps}_W$dataWidth') { - // Connect the Interface — creates module input/output ports - intf = FilterDataInterface(dataWidth: dataWidth) - ..connectIO(this, srcIntf, - inputTags: [FilterPortTag.inputPorts], - outputTags: [FilterPortTag.outputPorts]); - - final sampleIn = intf.sampleIn; - final validIn = intf.validIn; - clk = addInput('clk', clk); - reset = addInput('reset', reset); - enable = addInput('enable', enable); - - final tapIdxWidth = _bitsFor(numTaps); - - // ── Delay line (shift register via explicit flop bank + gates) ── - // AND gate: shift enable = enable & validIn & tapCounter==0 - // Samples shift in only when starting a new accumulation cycle. - final tapCounter = Logic(width: tapIdxWidth, name: 'tapCounter'); - final atFirstTap = - tapCounter.eq(Const(0, width: tapIdxWidth)).named('atFirstTap'); - final shiftEn = Logic(name: 'shiftEn'); - shiftEn <= (enable & validIn).named('enableAndValid') & atFirstTap; - - // LogicArray-backed delay line: one element per tap register. - final delayLine = LogicArray([numTaps], dataWidth, name: 'delayLine'); - for (var i = 0; i < numTaps; i++) { - final tapInput = (i == 0) ? sampleIn : delayLine.elements[i - 1]; - // Mux: hold current value or shift in new sample - final tapNext = Logic(width: dataWidth, name: 'nextTap$i'); - tapNext <= mux(shiftEn, tapInput, delayLine.elements[i]); - // Flop: register the next-state value - delayLine.elements[i] <= flop(clk, reset: reset, tapNext); - } - - // ── Coefficient bank — driven by tapCounter ── - // Build a LogicArray of constants from the coefficient list and - // pass it as an input port to CoeffBank (demonstrates addInputArray - // on a sub-module). - final coeffArray = LogicArray([numTaps], dataWidth, name: 'coeffArray'); - for (var i = 0; i < numTaps; i++) { - coeffArray.elements[i] <= Const(coefficients[i], width: dataWidth); - } - - final coeffBank = CoeffBank( - tapCounter, - coeffArray, - numTaps: numTaps, - dataWidth: dataWidth, - name: 'coeffBank', - ); - - // ── Delay-line mux — select sample for current tap ── - var selectedSample = delayLine.elements[0]; - for (var i = 1; i < numTaps; i++) { - final tapSelect = - tapCounter.eq(Const(i, width: tapIdxWidth)).named('tapSelect$i'); - selectedSample = mux(tapSelect, delayLine.elements[i], selectedSample) - .named('tapMux$i'); - } - - // ── Running accumulator (feedback register) ── - final accumReg = Logic(width: dataWidth, name: 'accumReg'); - // Reset accumulator at the start of each new sample (tap 0). - // Combinational block: equivalent to `always_comb` in SystemVerilog. - final accumFeedback = Logic(width: dataWidth, name: 'accumFeedback'); - Combinational([ - If(atFirstTap, then: [ - accumFeedback < Const(0, width: dataWidth), - ], orElse: [ - accumFeedback < accumReg, - ]), - ]); - - // ── Single MAC unit — time-multiplexed across taps ── - final mac = MacUnit( - selectedSample, - coeffBank.coeffOut, - accumFeedback, - clk, - reset, - enable, - dataWidth: dataWidth, - name: 'mac', - ); - - // Register the MAC result for accumulator feedback. - accumReg <= flop(clk, reset: reset, mac.result); - - // ── Tap counter: cycles 0 … numTaps-1 while enabled ── - // Sequential block: equivalent to `always_ff @(posedge clk)` in SV. - // When enabled, the counter increments and wraps at numTaps-1. - // When disabled, it resets to 0. - final lastTap = - tapCounter.eq(Const(numTaps - 1, width: tapIdxWidth)).named('lastTap'); - Sequential(clk, reset: reset, [ - If(enable, then: [ - If(lastTap, then: [ - tapCounter < Const(0, width: tapIdxWidth), - ], orElse: [ - tapCounter < tapCounter + Const(1, width: tapIdxWidth), - ]), - ], orElse: [ - tapCounter < Const(0, width: tapIdxWidth), - ]), - ]); - - // ── Output latch: capture accumulator when all taps processed ── - // The MAC pipeline has 2 stages, so the result is ready 2 cycles - // after the last tap enters. A 2-stage shift register of lastTap - // creates the latch strobe. - final lastTapD1 = Logic(name: 'lastTapD1'); - final lastTapD2 = Logic(name: 'lastTapD2'); - final outputReg = Logic(width: dataWidth, name: 'outputReg'); - - // Sequential block with If: latch strobe delay and output register. - Sequential(clk, reset: reset, [ - lastTapD1 < lastTap, - lastTapD2 < lastTapD1, - If(lastTapD2, then: [ - outputReg < accumReg, - ]), - ]); - - // ── Valid pipeline: track whether we have a valid output ── - // validIn is high during data injection. After the MAC pipeline - // latency (numTaps + 2 cycles), outputs become valid. - final validPipe = Logic(name: 'validPipe'); - final outputReady = (lastTapD2 & enable).named('outputReady'); - - // Sequential block: register the valid strobe and hold it. - Sequential(clk, reset: reset, [ - If(enable, then: [ - validPipe < outputReady, - ]), - ]); - - // Combinational block: gate the output to zero when not valid. - final dataOut = intf.dataOut; - final validOut = intf.validOut; - Combinational([ - If(validPipe, then: [ - dataOut < outputReg, - ], orElse: [ - dataOut < Const(0, width: dataWidth), - ]), - validOut < validPipe, - ]); - } - - /// Minimum bits needed to represent [n] values. - static int _bitsFor(int n) { - if (n <= 1) { - return 1; - } - var bits = 0; - var v = n - 1; - while (v > 0) { - bits++; - v >>= 1; - } - return bits; - } -} - -// ────────────────────────────────────────────────────────────────── -// FilterController: FSM sequencing the filter bank -// ────────────────────────────────────────────────────────────────── - -/// States for the [FilterController] finite state machine. -enum FilterState { - /// Waiting for the start signal. - idle, - - /// Accepting initial samples into the delay line. - loading, - - /// Normal filtering operation. - running, - - /// Flushing the pipeline after the input stream ends. - draining, - - /// Processing complete. - done, -} - -/// Controls the filter bank operation via a [FiniteStateMachine]. -/// -/// - idle: waiting for start signal -/// - loading: accepting initial samples into delay line -/// - running: normal filtering -/// - draining: flushing pipeline after input stream ends -/// - done: processing complete -class FilterController extends Module { - /// Encoded FSM state (3 bits). - Logic get state => output('state'); - - /// High while the filter channels should be processing. - Logic get filterEnable => output('filterEnable'); - - /// High during the initial sample-loading phase. - Logic get loadingPhase => output('loadingPhase'); - - /// Asserted when the filter bank has finished processing. - Logic get doneFlag => output('doneFlag'); - - /// Clock input. - @protected - Logic get clkPin => input('clk'); - - /// Reset input. - @protected - Logic get resetPin => input('reset'); - - /// Start input. - @protected - Logic get startPin => input('start'); - - /// Input valid. - @protected - Logic get inputValidPin => input('inputValid'); - - /// Input done. - @protected - Logic get inputDonePin => input('inputDone'); - - late final FiniteStateMachine _fsm; - - /// Returns the FSM's current state index for a given [FilterState]. - int? getStateIndex(FilterState s) => _fsm.getStateIndex(s); - - /// Creates a [FilterController] that sequences the filter bank. - /// - /// After [start] is asserted the FSM moves through loading → running - /// → draining (for [drainCycles] cycles) → done. - FilterController( - Logic clk, Logic reset, Logic start, Logic inputValid, Logic inputDone, - {required int drainCycles, super.name = 'FilterController'}) - : super(definitionName: 'FilterController') { - clk = addInput('clk', clk); - reset = addInput('reset', reset); - start = addInput('start', start); - inputValid = addInput('inputValid', inputValid); - inputDone = addInput('inputDone', inputDone); - - final filterEnable = addOutput('filterEnable'); - final loadingPhase = addOutput('loadingPhase'); - final doneFlag = addOutput('doneFlag'); - final state = addOutput('state', width: 3); - - // Drain counter - final drainCount = Logic(width: 8, name: 'drainCount'); - final drainDone = - drainCount.eq(Const(drainCycles, width: 8)).named('drainDone'); - - _fsm = FiniteStateMachine( - clk, - reset, - FilterState.idle, - [ - State( - FilterState.idle, - events: { - start: FilterState.loading, - }, - actions: [ - filterEnable < 0, - loadingPhase < 0, - doneFlag < 0, - ], - ), - State( - FilterState.loading, - events: { - inputValid: FilterState.running, - }, - actions: [ - filterEnable < 1, - loadingPhase < 1, - doneFlag < 0, - ], - ), - State( - FilterState.running, - events: { - inputDone: FilterState.draining, - }, - actions: [ - filterEnable < 1, - loadingPhase < 0, - doneFlag < 0, - ], - ), - State( - FilterState.draining, - events: { - drainDone: FilterState.done, - }, - actions: [ - filterEnable < 1, - loadingPhase < 0, - doneFlag < 0, - ], - ), - State( - FilterState.done, - events: {}, - actions: [ - filterEnable < 0, - loadingPhase < 0, - doneFlag < 1, - ], - ), - ], - ); - - state <= _fsm.currentState.zeroExtend(state.width); - - // Drain counter: Sequential block increments while draining, - // resets to zero otherwise. - final drainIdx = _fsm.getStateIndex(FilterState.draining)!; - final isDraining = Logic(name: 'isDraining'); - isDraining <= _fsm.currentState.eq(Const(drainIdx, width: _fsm.stateWidth)); - - Sequential(clk, reset: reset, [ - If(isDraining, then: [ - drainCount < drainCount + Const(1, width: 8), - ], orElse: [ - drainCount < Const(0, width: 8), - ]), - ]); - } -} - -// ────────────────────────────────────────────────────────────────── -// FilterBank: top-level 2-channel polyphase FIR filter -// ────────────────────────────────────────────────────────────────── - -/// A 2-channel polyphase FIR filter bank. -/// -/// Hierarchy: -/// ```text -/// FilterBank (top) -/// ├── FilterController (FSM) -/// ├── FilterChannel 'ch0' -/// │ ├── CoeffBank (coefficient ROM via LogicArray + mux chain) -/// │ └── MacUnit 'mac' (pipelined multiply-accumulate) -/// └── FilterChannel 'ch1' -/// ├── CoeffBank -/// └── MacUnit 'mac' -/// ``` -/// -/// Each channel time-multiplexes a single MacUnit across all taps, -/// sequenced by a tap counter that drives the CoeffBank tap index -/// and a delay-line sample mux. -/// -/// Uses: -/// - [FilterDataInterface] for I/O port bundles -/// - [FilterSample] LogicStructure for structured sample signals -/// - [LogicArray] in CoeffBank for coefficient storage -/// - [Pipeline] in MacUnit for pipelined MAC -/// - [FiniteStateMachine] in FilterController for sequencing -/// - Multiple instantiation: two [FilterChannel]s share one definition -/// - [LogicNet] / [addInOut] for bidirectional shared data bus - -// ────────────────────────────────────────────────────────────────── -// SharedDataBus: bidirectional port for coefficient/status I/O -// ────────────────────────────────────────────────────────────────── - -/// A module with a bidirectional data bus for loading/reading data. -/// -/// In real hardware, a shared data bus is common for: -/// - Loading filter coefficients from external memory -/// - Reading diagnostic status or filter output snapshots -/// -/// Direction is controlled by `writeEnable`: when high, the module's -/// internal [TriStateBuffer] drives `storedValue` onto `dataBus`; -/// when low, the external driver owns the bus and the module latches -/// the incoming value into a register. -/// -/// Exercises `addInOut` / `LogicNet` / [TriStateBuffer] / inout port -/// direction through the full ROHD stack: synthesis, hierarchy, -/// waveform capture, and DevTools rendering. -class SharedDataBus extends Module { - /// The bidirectional data bus port. - Logic get dataBus => inOut('dataBus'); - - /// The stored value (latched when the bus is driven externally). - Logic get storedValue => output('storedValue'); - - /// Write-enable input. - @protected - Logic get writeEnablePin => input('writeEnable'); - - /// Clock input. - @protected - Logic get clkPin => input('clk'); - - /// Reset input. - @protected - Logic get resetPin => input('reset'); - - /// Data width in bits. - final int dataWidth; - - /// Creates a [SharedDataBus] with a [dataWidth]-bit bidirectional port. - /// - /// [dataBusNet] is the external [LogicNet] to connect. - /// [writeEnable] controls bus direction: 1 = module drives bus, - /// 0 = external drives bus (module reads). - /// [clk] and [reset] provide synchronous storage. - SharedDataBus( - LogicNet dataBusNet, - Logic writeEnable, - Logic clk, - Logic reset, { - required this.dataWidth, - super.name = 'SharedDataBus', - }) : super(definitionName: 'SharedDataBus') { - final bus = addInOut('dataBus', dataBusNet, width: dataWidth); - writeEnable = addInput('writeEnable', writeEnable); - clk = addInput('clk', clk); - reset = addInput('reset', reset); - - final storedValue = addOutput('storedValue', width: dataWidth); - - // Latch the bus value on clock edge when the external side is driving. - storedValue <= - flop( - clk, - bus, - reset: reset, - en: ~writeEnable, - resetValue: Const(0, width: dataWidth), - ); - - // Drive the latched value back onto the bus when writeEnable is high. - // TriStateBuffer drives its out (a LogicNet) with storedValue when - // enabled; otherwise it outputs high-Z. Joining out↔bus makes the - // two nets share the same wire. - TriStateBuffer(storedValue, enable: writeEnable, name: 'busDriver') - .out - .gets(bus); - } -} - -/// The top-level polyphase FIR filter bank. -class FilterBank extends Module { - /// Per-channel filtered outputs as a [LogicArray]. - /// - /// `channelOut.elements[i]` is the filtered output of channel `i`. - LogicArray get channelOut => output('channelOut') as LogicArray; - - /// Channel 0 filtered output (convenience getter). - Logic get out0 => channelOut.elements[0]; - - /// Channel 1 filtered output (convenience getter). - Logic get out1 => channelOut.elements[1]; - - /// Output valid (aligned with filtered outputs). - Logic get validOut => output('validOut'); - - /// Done signal from the controller FSM. - Logic get done => output('done'); - - /// Controller state (for debug visibility). - Logic get state => output('state'); - - /// Clock input. - @protected - Logic get clkPin => input('clk'); - - /// Reset input. - @protected - Logic get resetPin => input('reset'); - - /// Start input. - @protected - Logic get startPin => input('start'); - - /// Input [FilterSample] port for channel [ch]. - @protected - FilterSample samplePin(int ch) => input('sample$ch') as FilterSample; - - /// Input-done strobe. - @protected - Logic get inputDonePin => input('inputDone'); - - /// Number of FIR taps per channel. - final int numTaps; - - /// Bit width of each data sample. - final int dataWidth; - - /// Number of filter channels. - final int numChannels; - - /// Creates a [FilterBank] with [numChannels] channels (default 2). - /// - /// Each channel has [numTaps] FIR taps at [dataWidth] bits. - /// [coefficients] is a list of per-channel coefficient lists — - /// `coefficients[i]` supplies the tap weights for channel `i`. - /// [samples] is a [LogicArray] with one element per channel. - /// [inputDone] when the input stream is complete. - /// - /// Optionally pass [dataBus] (a `LogicNet`) and [writeEnable] to - /// attach a bidirectional shared data bus via [SharedDataBus]. - /// The bus latches external data when [writeEnable] is low and - /// drives `storedValue` output. - FilterBank( - Logic clk, - Logic reset, - Logic start, - List samples, - Logic inputDone, { - required this.numTaps, - required this.dataWidth, - required List> coefficients, - this.numChannels = 2, - LogicNet? dataBus, - Logic? writeEnable, - super.name = 'FilterBank', - String? definitionName, - }) : super(definitionName: definitionName ?? 'FilterBank') { - if (coefficients.length != numChannels) { - throw Exception( - 'coefficients must have $numChannels entries (one per channel).'); - } - - // ── Register ports ── - clk = addInput('clk', clk); - reset = addInput('reset', reset); - start = addInput('start', start); - inputDone = addInput('inputDone', inputDone); - - // One typed FilterSample input port per channel. - final inPorts = []; - for (var ch = 0; ch < numChannels; ch++) { - inPorts.add(addTypedInput('sample$ch', samples[ch])); - } - - final channelOut = addTypedOutput( - 'channelOut', - ({name = 'channelOut'}) => - LogicArray([numChannels], dataWidth, name: name)); - final validOut = addOutput('validOut'); - final done = addOutput('done'); - final state = addOutput('state', width: 3); - - // ── Controller FSM ── - // Drain cycles: numTaps cycles per accumulation + pipeline depth (2) + 1 - final controller = FilterController( - clk, - reset, - start, - inPorts[0].valid, // valid is shared across channels - inputDone, - drainCycles: numTaps + 3, - name: 'controller', - ); - - final filterEnable = controller.filterEnable; - - // ── Per-channel filter instantiation ── - final srcIntfs = []; - for (var ch = 0; ch < numChannels; ch++) { - final srcIntf = FilterDataInterface(dataWidth: dataWidth); - srcIntf.sampleIn <= inPorts[ch].data; - srcIntf.validIn <= inPorts[ch].valid; - - FilterChannel( - srcIntf, - clk, - reset, - filterEnable, - numTaps: numTaps, - dataWidth: dataWidth, - coefficients: coefficients[ch], - name: 'ch$ch', - ); - - srcIntfs.add(srcIntf); - } - - // ── Connect outputs ── - for (var ch = 0; ch < numChannels; ch++) { - channelOut.elements[ch] <= srcIntfs[ch].dataOut; - } - validOut <= srcIntfs[0].validOut; - done <= controller.doneFlag; - state <= controller.state; - - // ── Optional shared data bus (inOut port) ── - if (dataBus != null && writeEnable != null) { - final busPort = addInOut('dataBus', dataBus, width: dataWidth); - writeEnable = addInput('writeEnable', writeEnable); - final storedValue = addOutput('storedValue', width: dataWidth); - - final sharedBus = SharedDataBus( - LogicNet(name: 'busNet', width: dataWidth)..gets(busPort), - writeEnable, - clk, - reset, - dataWidth: dataWidth, - ); - storedValue <= sharedBus.storedValue; - } - } -} diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index ae4853fe7..c9eda24b6 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -34,14 +34,7 @@ class NetlistUtils { /// Safely retrieve the name from a [SynthLogic], returning null if /// retrieval fails (e.g. name not yet picked, or the SynthLogic has /// been replaced). - static String? tryGetSynthLogicName(SynthLogic sl) { - try { - return sl.name; - // ignore: avoid_catches_without_on_clauses - } catch (_) { - return null; - } - } + static String? tryGetSynthLogicName(SynthLogic sl) => sl.nameOrNull; /// Create a `$buf` cell map. static Map makeBufCell( diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index a5d9ba08d..eba1d96aa 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -215,6 +215,21 @@ class SynthLogic { return _name!; } + /// The chosen name of this, or `null` if a name has not been picked or this + /// has been replaced. + String? get nameOrNull { + if (_name == null || _replacement != null) { + return null; + } + + assert( + isConstant || Sanitizer.isSanitary(_name!), + 'Signal names should be sanitary, but found $_name.', + ); + + return _name; + } + /// The name of this, if it has been picked. String? _name; diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index a70df2e7a..81e6db4a4 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -12,7 +12,7 @@ import 'dart:convert'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; import 'package:test/test.dart'; import '../example/example.dart'; @@ -842,6 +842,44 @@ void main() { expect(expanded, initiallyExpanded); }); + test('filter bank can stop traversal at an opaque custom SV module', + () async { + final synthesizer = NetlistSynthesizer( + options: const NetlistOptions(leafModuleTypes: [FlipFlop, MacUnit]), + ); + final json = jsonDecode(synthesizer.synthesizeToJson( + filterBank, + )) as Map; + final modules = _modules(json); + + expect( + modules.keys.any((name) => name.contains('MacUnit')), + isFalse, + reason: 'MacUnit is treated like externally supplied/custom SV, so ' + 'the netlist should not emit a definition for it.', + ); + + final channelDefs = modules.entries.where( + (entry) => entry.key.contains('FilterChannel'), + ); + expect(channelDefs, isNotEmpty); + + final macCells = channelDefs.expand((entry) { + final def = entry.value as Map; + return _cells(def).values.where((cell) { + final cellMap = cell as Map; + return (cellMap['type'] as String).contains('MacUnit'); + }); + }).toList(); + + expect( + macCells, + isNotEmpty, + reason: 'FilterChannel should still instantiate the opaque MacUnit ' + 'cell; only hierarchy traversal stops at that boundary.', + ); + }); + test('DCE disabled still produces valid netlist', () async { final synth = SynthBuilder( filterBank, diff --git a/test/netlist_test.dart b/test/netlist_test.dart index 8255a4041..b0941c2ec 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -13,7 +13,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:test/test.dart'; From 8c48a81fc35d555cf2ceb59f8208ebc459477ef6 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 10 Jul 2026 13:31:35 -0700 Subject: [PATCH 57/77] Move filter bank example modules out of lib --- example/filter_bank.dart | 4 ++-- .../examples => example}/filter_bank/coeff_bank.dart | 0 .../examples => example}/filter_bank/filter_bank.dart | 11 ++++++----- .../filter_bank/filter_bank_modules.dart | 0 .../filter_bank/filter_channel.dart | 7 ++++--- .../filter_bank/filter_controller.dart | 0 .../filter_bank/filter_data_interface.dart | 0 .../filter_bank/filter_sample.dart | 0 .../examples => example}/filter_bank/mac_unit.dart | 0 .../filter_bank/shared_data_bus.dart | 0 test/netlist_synthesizer_test.dart | 2 +- test/netlist_test.dart | 2 +- 12 files changed, 14 insertions(+), 12 deletions(-) rename {lib/src/examples => example}/filter_bank/coeff_bank.dart (100%) rename {lib/src/examples => example}/filter_bank/filter_bank.dart (94%) rename {lib/src/examples => example}/filter_bank/filter_bank_modules.dart (100%) rename {lib/src/examples => example}/filter_bank/filter_channel.dart (97%) rename {lib/src/examples => example}/filter_bank/filter_controller.dart (100%) rename {lib/src/examples => example}/filter_bank/filter_data_interface.dart (100%) rename {lib/src/examples => example}/filter_bank/filter_sample.dart (100%) rename {lib/src/examples => example}/filter_bank/mac_unit.dart (100%) rename {lib/src/examples => example}/filter_bank/shared_data_bus.dart (100%) diff --git a/example/filter_bank.dart b/example/filter_bank.dart index d8aea6292..710096c3e 100644 --- a/example/filter_bank.dart +++ b/example/filter_bank.dart @@ -21,10 +21,10 @@ import 'dart:async'; import 'package:rohd/rohd.dart'; // Import module definitions. -import 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; +import 'filter_bank/filter_bank_modules.dart'; // Re-export so downstream consumers (e.g. devtools loopback) can use. -export 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; +export 'filter_bank/filter_bank_modules.dart'; // ────────────────────────────────────────────────────────────────── // Standalone simulation entry point diff --git a/lib/src/examples/filter_bank/coeff_bank.dart b/example/filter_bank/coeff_bank.dart similarity index 100% rename from lib/src/examples/filter_bank/coeff_bank.dart rename to example/filter_bank/coeff_bank.dart diff --git a/lib/src/examples/filter_bank/filter_bank.dart b/example/filter_bank/filter_bank.dart similarity index 94% rename from lib/src/examples/filter_bank/filter_bank.dart rename to example/filter_bank/filter_bank.dart index e56bcd6bc..48e7fff48 100644 --- a/lib/src/examples/filter_bank/filter_bank.dart +++ b/example/filter_bank/filter_bank.dart @@ -9,11 +9,12 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/examples/filter_bank/filter_channel.dart'; -import 'package:rohd/src/examples/filter_bank/filter_controller.dart'; -import 'package:rohd/src/examples/filter_bank/filter_data_interface.dart'; -import 'package:rohd/src/examples/filter_bank/filter_sample.dart'; -import 'package:rohd/src/examples/filter_bank/shared_data_bus.dart'; + +import 'filter_channel.dart'; +import 'filter_controller.dart'; +import 'filter_data_interface.dart'; +import 'filter_sample.dart'; +import 'shared_data_bus.dart'; /// A 2-channel polyphase FIR filter bank. /// diff --git a/lib/src/examples/filter_bank/filter_bank_modules.dart b/example/filter_bank/filter_bank_modules.dart similarity index 100% rename from lib/src/examples/filter_bank/filter_bank_modules.dart rename to example/filter_bank/filter_bank_modules.dart diff --git a/lib/src/examples/filter_bank/filter_channel.dart b/example/filter_bank/filter_channel.dart similarity index 97% rename from lib/src/examples/filter_bank/filter_channel.dart rename to example/filter_bank/filter_channel.dart index a36cac48f..317c9f934 100644 --- a/lib/src/examples/filter_bank/filter_channel.dart +++ b/example/filter_bank/filter_channel.dart @@ -9,9 +9,10 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/examples/filter_bank/coeff_bank.dart'; -import 'package:rohd/src/examples/filter_bank/filter_data_interface.dart'; -import 'package:rohd/src/examples/filter_bank/mac_unit.dart'; + +import 'coeff_bank.dart'; +import 'filter_data_interface.dart'; +import 'mac_unit.dart'; /// A single polyphase FIR filter channel with [numTaps] taps. /// diff --git a/lib/src/examples/filter_bank/filter_controller.dart b/example/filter_bank/filter_controller.dart similarity index 100% rename from lib/src/examples/filter_bank/filter_controller.dart rename to example/filter_bank/filter_controller.dart diff --git a/lib/src/examples/filter_bank/filter_data_interface.dart b/example/filter_bank/filter_data_interface.dart similarity index 100% rename from lib/src/examples/filter_bank/filter_data_interface.dart rename to example/filter_bank/filter_data_interface.dart diff --git a/lib/src/examples/filter_bank/filter_sample.dart b/example/filter_bank/filter_sample.dart similarity index 100% rename from lib/src/examples/filter_bank/filter_sample.dart rename to example/filter_bank/filter_sample.dart diff --git a/lib/src/examples/filter_bank/mac_unit.dart b/example/filter_bank/mac_unit.dart similarity index 100% rename from lib/src/examples/filter_bank/mac_unit.dart rename to example/filter_bank/mac_unit.dart diff --git a/lib/src/examples/filter_bank/shared_data_bus.dart b/example/filter_bank/shared_data_bus.dart similarity index 100% rename from lib/src/examples/filter_bank/shared_data_bus.dart rename to example/filter_bank/shared_data_bus.dart diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 81e6db4a4..8dce2dac7 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -12,10 +12,10 @@ import 'dart:convert'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; import 'package:test/test.dart'; import '../example/example.dart'; +import '../example/filter_bank/filter_bank_modules.dart'; import '../example/fir_filter.dart'; import '../example/logic_array.dart'; import '../example/oven_fsm.dart'; diff --git a/test/netlist_test.dart b/test/netlist_test.dart index b0941c2ec..64f893973 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -13,12 +13,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/examples/filter_bank/filter_bank_modules.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:test/test.dart'; import '../example/example.dart'; +import '../example/filter_bank/filter_bank_modules.dart'; import '../example/fir_filter.dart'; import '../example/logic_array.dart'; import '../example/oven_fsm.dart'; From 1c7cd05b1a93ce4e373d434bfdd84d6d076c109d Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 15 Jul 2026 08:22:24 -0700 Subject: [PATCH 58/77] major bug fixes for LogicArray and LogicStruct --- .../netlist/netlist_cell_mapper.dart | 1 + .../netlist/netlist_module_translation.dart | 268 +++++++- .../synthesizers/netlist/netlist_options.dart | 7 +- .../synthesizers/netlist/netlist_passes.dart | 397 ++++++++++- .../netlist_synth_module_definition.dart | 3 +- .../netlist/netlist_synthesis_result.dart | 3 +- .../netlist/netlist_synthesizer.dart | 189 +++--- .../synthesizers/netlist/netlist_utils.dart | 1 + .../netlist/netlist_validation.dart | 133 +++- .../utilities/synth_array_concat.dart | 14 +- .../utilities/synth_array_slice.dart | 8 +- .../synthesizers/utilities/synth_logic.dart | 9 + .../utilities/synth_module_definition.dart | 103 ++- .../utilities/synth_operation_namer.dart | 143 ++++ .../utilities/synth_structure_concat.dart | 14 +- .../utilities/synth_structure_slice.dart | 8 +- lib/src/synthesizers/utilities/utilities.dart | 1 + lib/src/utilities/namer.dart | 109 --- test/netlist_synthesizer_test.dart | 625 ++++++++++++++++-- test/netlist_test.dart | 10 +- 20 files changed, 1712 insertions(+), 334 deletions(-) create mode 100644 lib/src/synthesizers/utilities/synth_operation_namer.dart diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart index f674911f7..618fc595a 100644 --- a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -182,6 +182,7 @@ class NetlistCellMapper { // Built-in handler registration // ══════════════════════════════════════════════════════════════════════ + /// Registers the built-in ROHD-to-Yosys primitive cell mappings. void _registerDefaults() { // Helper to reduce boilerplate for type-map-based handlers. void registerByTypeMap( diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 964fbe930..1dac184d8 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // netlist_module_translation.dart // Per-module state and ordered phases for netlist synthesis. @@ -31,6 +30,9 @@ class NetlistModuleTranslation { final String Function(Module module) _getInstanceTypeOfModule; /// The next available integer wire identifier. + /// + /// Starts at 2 so consumers never confuse wire IDs 0 or 1 with the + /// Yosys-JSON constant bit strings `"0"` and `"1"`. int nextId = 2; /// Wire identifiers allocated for each synthesis logic. @@ -84,17 +86,29 @@ class NetlistModuleTranslation { for (final (direction, synthLogics, modulePorts) in portGroups) { if (synthLogics != null) { for (final synthLogic in synthLogics) { - final ids = getIds(synthLogic); - final portName = - NetlistUtils.portNameForSynthLogic(synthLogic, modulePorts); + final portName = NetlistUtils.portNameForSynthLogic( + synthLogic, + modulePorts, + ); if (portName != null) { final portLogic = modulePorts[portName]; + final emitOutputArrayConcat = direction == 'output' && + portLogic is LogicArray && + !_hasExistingOutputArrayConcat(synthLogic) && + !_hasDirectSubmoduleOutputDriver(synthLogic); + final originalIds = getIds(synthLogic); + final ids = emitOutputArrayConcat + ? List.generate(synthLogic.width, (_) => allocateWireId()) + : originalIds; ports[portName] = { 'direction': direction, 'bits': ids, if (portLogic != null) 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), }; + if (emitOutputArrayConcat) { + _emitOutputArrayConcat(portName, portLogic, ids); + } } } } else { @@ -113,6 +127,123 @@ class NetlistModuleTranslation { } } + /// Emits a concat cell that assembles a LogicArray output port. + void _emitOutputArrayConcat( + String portName, + LogicArray array, + List outputIds, + ) { + _emitOutputArrayConcatForArray(portName, array, outputIds); + } + + /// Recursively emits concat cells for nested LogicArray output elements. + bool _emitOutputArrayConcatForArray( + String concatName, + LogicArray array, + List outputIds, + ) { + final definition = synthDef; + if (definition == null) { + return false; + } + + final concatConnections = >{}; + final concatDirections = {}; + var lowerIndex = 0; + + for (final (index, element) in array.elements.indexed) { + final synthLogic = definition.logicToSynthMap[element]; + if (synthLogic == null) { + return false; + } + var elementIds = getIds(synthLogic); + if (element is LogicArray && + !_hasExistingOutputArrayConcat(synthLogic) && + !_hasDirectSubmoduleOutputDriver(synthLogic)) { + final aggregateIds = List.generate( + synthLogic.width, + (_) => allocateWireId(), + ); + if (!_emitOutputArrayConcatForArray( + '${concatName}_$index', + element, + aggregateIds, + )) { + return false; + } + elementIds = aggregateIds; + } + final upperIndex = lowerIndex + elementIds.length - 1; + concatConnections['[$upperIndex:$lowerIndex]'] = + elementIds.cast(); + concatDirections['[$upperIndex:$lowerIndex]'] = 'input'; + lowerIndex = upperIndex + 1; + } + + if (lowerIndex != outputIds.length) { + return false; + } + + concatConnections['Y'] = outputIds.cast(); + concatDirections['Y'] = 'output'; + + cells['array_concat_output_$concatName'] = { + 'hide_name': 0, + 'type': r'$concat', + 'parameters': { + for (var index = 0; index < array.elements.length; index++) + 'IN${index}_WIDTH': array.elements[index].width, + }, + 'attributes': {}, + 'port_directions': concatDirections, + 'connections': concatConnections, + }; + + return true; + } + + /// Checks whether [synthLogic] is already driven by an output concat cell. + bool _hasExistingOutputArrayConcat(SynthLogic synthLogic) { + final definition = synthDef; + if (definition == null) { + return false; + } + + for (final instance in definition.subModuleInstantiations) { + if (instance.module is! SynthArrayConcat) { + continue; + } + if (instance.outputMapping.values.any( + (outputLogic) => outputLogic.resolved == synthLogic.resolved, + )) { + return true; + } + } + + return false; + } + + /// Checks whether [synthLogic] is driven directly by a non-concat submodule. + bool _hasDirectSubmoduleOutputDriver(SynthLogic synthLogic) { + final definition = synthDef; + if (definition == null) { + return false; + } + + for (final instance in definition.subModuleInstantiations) { + if (instance.module is SynthArrayConcat) { + continue; + } + if (instance.outputMapping.values.any( + (outputLogic) => outputLogic.resolved == synthLogic.resolved, + )) { + return true; + } + } + + return false; + } + /// Preallocates internal wires in [Module.internalSignals] order. void processInternalWires() { final definition = synthDef; @@ -174,16 +305,55 @@ class NetlistModuleTranslation { cellConnections, getIds, ); - _filterProceduralConstants( - instance, - cellPortDirs, - cellConnections, - ); - _renameProceduralPorts( - instance, - cellPortDirs, - cellConnections, - ); + _filterProceduralConstants(instance, cellPortDirs, cellConnections); + _renameProceduralPorts(instance, cellPortDirs, cellConnections); + } + + if (!isLeaf) { + for (final portEntry in submodule.inputs.entries) { + final portName = portEntry.key; + final port = portEntry.value; + if (port is! LogicArray || cellPortDirs[portName] != 'input') { + continue; + } + final bits = cellConnections[portName]; + if (bits == null || bits.length != port.width) { + continue; + } + + final concatConnections = >{}; + final concatDirections = {}; + var lowerIndex = 0; + for (final element in port.elements) { + final upperIndex = lowerIndex + element.width - 1; + final concatPort = '[$upperIndex:$lowerIndex]'; + concatConnections[concatPort] = bits.sublist( + lowerIndex, + upperIndex + 1, + ); + concatDirections[concatPort] = 'input'; + lowerIndex = upperIndex + 1; + } + + final concatOutput = [ + for (var i = 0; i < bits.length; i++) allocateWireId(), + ]; + concatConnections['Y'] = concatOutput; + concatDirections['Y'] = 'output'; + cellConnections[portName] = concatOutput; + + cells['array_concat_${cellKey}_$portName'] = { + 'hide_name': 0, + 'type': r'$concat', + 'parameters': { + for (var index = 0; index < port.elements.length; index++) + 'IN${index}_WIDTH': port.elements[index].width, + }, + 'attributes': {}, + 'port_directions': concatDirections, + 'connections': concatConnections, + }; + } } cells[cellKey] = { @@ -243,6 +413,64 @@ class NetlistModuleTranslation { ); } + final aggregateConstructors = + <({List inputBits, List outputBits})>[]; + for (final cellEntry in cells.entries) { + final cell = cellEntry.value; + final cellType = cell['type'] as String?; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final inputBits = []; + final outputBits = []; + + if (cellType == r'$concat') { + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if (dirs[portEntry.key] == 'output') { + outputBits.addAll(bits); + } else { + inputBits.addAll(bits); + } + } + } else if (cellType == r'$struct_pack') { + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if (portEntry.key == 'Y' && dirs[portEntry.key] == 'output') { + outputBits.addAll(bits); + } else if (dirs[portEntry.key] == 'input') { + inputBits.addAll(bits); + } + } + } else { + continue; + } + + if (inputBits.length == outputBits.length) { + aggregateConstructors.add(( + inputBits: inputBits, + outputBits: outputBits, + )); + } + } + + List resolveAggregateBits(List bits) { + for (final constructorBits in aggregateConstructors) { + if (bits.length == constructorBits.inputBits.length) { + var matches = true; + for (var index = 0; index < bits.length; index++) { + if (bits[index] != constructorBits.inputBits[index]) { + matches = false; + break; + } + } + if (matches) { + return constructorBits.outputBits; + } + } + } + return bits; + } + if (synthDef != null) { for (final entry in synthLogicIds.entries.where( (entry) => !entry.key.isConstant && !entry.key.declarationCleared, @@ -267,6 +495,7 @@ class NetlistModuleTranslation { bit is int ? (arrayConcatOldToNew[bit] ?? bit) : bit, ]; } + bits = resolveAggregateBits(bits); final typeLogic = NetlistUtils.typeLogicFromSynthLogic(synthLogic); addNetname( Sanitizer.sanitizeSV(name), @@ -343,9 +572,7 @@ class NetlistModuleTranslation { (entry) => entry.value['direction'] == 'output', )) { final outputBits = (port.value['bits']! as List).cast(); - if (!outputBits.any( - (bit) => bit is int && inputBitIds.contains(bit), - )) { + if (!outputBits.any((bit) => bit is int && inputBitIds.contains(bit))) { continue; } final freshBits = List.generate( @@ -493,6 +720,7 @@ class NetlistModuleTranslation { }); } + /// Removes procedural constant ports and records their constants as blocked. void _filterProceduralConstants( SynthSubModuleInstantiation instance, Map portDirections, @@ -513,6 +741,7 @@ class NetlistModuleTranslation { } } + /// Renames procedural ports to match their resolved synth logic names. void _renameProceduralPorts( SynthSubModuleInstantiation instance, Map portDirections, @@ -526,8 +755,9 @@ class NetlistModuleTranslation { if (synthLogic == null) { continue; } - final resolvedName = - NetlistUtils.tryGetSynthLogicName(synthLogic.resolved); + final resolvedName = NetlistUtils.tryGetSynthLogicName( + synthLogic.resolved, + ); if (resolvedName != null && resolvedName != portName) { renames[portName] = resolvedName; } diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_options.dart index ea1fdc002..b7317ba33 100644 --- a/lib/src/synthesizers/netlist/netlist_options.dart +++ b/lib/src/synthesizers/netlist/netlist_options.dart @@ -12,20 +12,17 @@ import 'package:rohd/rohd.dart' hide SynthModuleStopPolicy; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/utilities/synth_module_stop_policy.dart'; -/// The current format version for netlist JSON produced by ROHD. -const String netlistFormatVersion = '0.0.5'; - /// Configuration options for netlist synthesis. /// /// The netlist synthesizer serves two main consumer flows, both configured /// through these options: /// -/// **Flow 1 — Slim JSON** ([NetlistOptions.slimMode]): +/// **Flow 1 — Slim JSON** (`NetlistService.slimJson`): /// Batch synthesis of the entire design, producing a lightweight /// representation with ports, signals, and cell stubs but **no cell /// connections**. Used for the initial DevTools hierarchy load. /// -/// **Flow 2 — Full JSON, incremental** (`NetlistSynthesizer.synthesizeToJson`): +/// **Flow 2 — Full JSON, incremental** (`NetlistService.moduleJson`): /// Returns the complete netlist (with cell connections) for a single /// module definition on demand. Results are cached; the first call /// may trigger a lazy `SynthBuilder` run on the requested subtree. diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart index 365047ac5..ae220199d 100644 --- a/lib/src/synthesizers/netlist/netlist_passes.dart +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -4,11 +4,6 @@ // netlist_passes.dart // Post-processing optimization passes for netlist synthesis. // -// These passes operate on the modules map (definition name → module data) -// produced by [NetlistSynthesizer.synthesize]. They simplify the netlist -// by grouping struct conversions, collapsing redundant cells, and inserting -// buffer cells for cleaner schematic rendering. -// // 2025 February 11 // Author: Desmond Kirkpatrick @@ -22,6 +17,7 @@ import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; /// All methods are static — no instances are created. @internal class NetlistPasses { + /// Prevents construction of this static utility class. NetlistPasses._(); /// Collects a combined modules map from [SynthesisResult]s suitable for @@ -53,6 +49,7 @@ class NetlistPasses { return allModules; } + /// Deep-copies cell maps, optionally omitting connection payloads. static Map> _copyCells( Map> source, { required bool includeConnections, @@ -66,6 +63,7 @@ class NetlistPasses { ), }; + /// Deep-copies a map whose values are JSON-like object maps. static Map> _copyNestedMaps( Map> source, ) => @@ -74,11 +72,13 @@ class NetlistPasses { entry.key: _copyObjectMap(entry.value), }; + /// Deep-copies a JSON-like object map. static Map _copyObjectMap(Map source) => { for (final entry in source.entries) entry.key: _copyJsonValue(entry.value), }; + /// Deep-copies a JSON-like value while preserving scalar objects. static Object? _copyJsonValue(Object? value) { if (value is Map) { return { @@ -87,9 +87,7 @@ class NetlistPasses { }; } if (value is List) { - return [ - for (final element in value) _copyJsonValue(element), - ]; + return [for (final element in value) _copyJsonValue(element)]; } return value; } @@ -233,6 +231,16 @@ class NetlistPasses { final cellsToAdd = >{}; for (final comp in components) { + if (comp.any( + (cn) => const { + r'$concat', + r'$struct_pack', + r'$struct_unpack', + }.contains(cells[cn]!['type']), + )) { + continue; + } + // Build output-bit → input-bit map for the whole cluster. final bitMap = {}; for (final cn in comp) { @@ -309,6 +317,379 @@ class NetlistPasses { } } + /// Removes transparent helper cells whose outputs are not consumed by any + /// other cell or module output. + static void removeUnconsumedTransparentCells( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + var changed = true; + while (changed) { + changed = false; + final consumedBits = {}; + + for (final cell in cells.values) { + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + for (final entry in conns.entries) { + final direction = dirs[entry.key] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + consumedBits.addAll((entry.value as List).whereType()); + } + } + for (final port in ports.values) { + final portMap = port as Map; + final direction = portMap['direction'] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + consumedBits.addAll((portMap['bits'] as List).whereType()); + } + + cells.removeWhere((_, cell) { + if (!_transparentTypes.contains(cell['type'] as String?)) { + return false; + } + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final outputBits = {}; + for (final entry in conns.entries) { + final direction = dirs[entry.key] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + outputBits.addAll((entry.value as List).whereType()); + } + final remove = + outputBits.isNotEmpty && !outputBits.any(consumedBits.contains); + changed = changed || remove; + return remove; + }); + } + } + } + + /// Removes `$concat` cells that only rename an already-named bit vector. + /// + /// Explicit array concat cells are useful when they show a real regrouping, + /// but a concat whose flattened inputs exactly match an existing netname is + /// just an alias. Redirect its consumers to the named source bits and remove + /// the cell. + static void removeTrivialConcatAliases( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + final netnames = moduleDef['netnames'] as Map?; + if (cells == null || cells.isEmpty || netnames == null) { + continue; + } + + final namedBitVectors = [ + for (final rawNetname in netnames.values) + if (rawNetname is Map && rawNetname['bits'] is List) + (rawNetname['bits'] as List).cast(), + ]; + if (namedBitVectors.isEmpty) { + continue; + } + + var changed = true; + while (changed) { + changed = false; + final replacementByOutputBit = {}; + final cellsToRemove = {}; + + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$concat' || + entry.key.startsWith('array_concat_output_')) { + continue; + } + + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final outputBits = []; + final inputBits = []; + + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if ((dirs[portEntry.key] as String?) == 'output') { + outputBits.addAll(bits); + } else { + inputBits.addAll(bits); + } + } + + if (outputBits.length != inputBits.length || + !_matchesNamedVector(inputBits, namedBitVectors)) { + continue; + } + + for (var index = 0; index < outputBits.length; index++) { + final outputBit = outputBits[index]; + if (outputBit is int) { + replacementByOutputBit[outputBit] = inputBits[index]; + } + } + cellsToRemove.add(entry.key); + } + + if (replacementByOutputBit.isEmpty) { + continue; + } + + void rewriteBits(List bits) { + for (var index = 0; index < bits.length; index++) { + final bit = bits[index]; + if (bit is int && replacementByOutputBit.containsKey(bit)) { + bits[index] = replacementByOutputBit[bit]!; + } + } + } + + final ports = moduleDef['ports'] as Map? ?? {}; + for (final rawPort in ports.values) { + final port = rawPort as Map; + rewriteBits((port['bits'] as List).cast()); + } + + for (final entry in cells.entries) { + if (cellsToRemove.contains(entry.key)) { + continue; + } + final cell = entry.value; + final conns = cell['connections'] as Map? ?? {}; + for (final rawBits in conns.values) { + rewriteBits((rawBits as List).cast()); + } + } + + for (final rawNetname in netnames.values) { + if (rawNetname is Map && rawNetname['bits'] is List) { + rewriteBits((rawNetname['bits'] as List).cast()); + } + } + + cellsToRemove.forEach(cells.remove); + changed = true; + } + } + } + + /// Replaces a `$concat` of adjacent `$slice` outputs from the same source + /// with one wider `$slice`. + static void collapseConcatOfAdjacentSlices( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + final cellsToRemove = {}; + + for (final concatEntry in cells.entries.toList()) { + final concat = concatEntry.value; + if (concat['type'] != r'$concat' || + concatEntry.key.startsWith('array_concat_output_')) { + continue; + } + + final concatDirs = + concat['port_directions'] as Map? ?? {}; + final concatConns = + concat['connections'] as Map? ?? {}; + final inputSliceRefs = <({String name, Map cell})>[]; + final outputBits = []; + var valid = true; + + for (final portEntry in concatConns.entries) { + if ((concatDirs[portEntry.key] as String?) == 'output') { + outputBits.addAll((portEntry.value as List).cast()); + continue; + } + + final inputBits = (portEntry.value as List).cast(); + final sliceEntry = _findSliceDrivingBits(cells, inputBits); + if (sliceEntry == null) { + valid = false; + break; + } + inputSliceRefs.add((name: sliceEntry.key, cell: sliceEntry.value)); + } + + if (!valid || inputSliceRefs.isEmpty || outputBits.isEmpty) { + continue; + } + + final firstSlice = inputSliceRefs.first.cell; + final firstParams = + firstSlice['parameters'] as Map? ?? {}; + final firstConnections = + firstSlice['connections'] as Map?; + final sourceRawBits = firstConnections?['A'] as List?; + if (sourceRawBits == null) { + continue; + } + final sourceBits = sourceRawBits.cast(); + final startOffset = firstParams['OFFSET'] as int?; + final sourceWidth = firstParams['A_WIDTH'] as int?; + if (startOffset == null || sourceWidth == null) { + continue; + } + + var expectedOffset = startOffset; + var combinedWidth = 0; + for (final sliceRef in inputSliceRefs) { + final slice = sliceRef.cell; + final params = slice['parameters'] as Map? ?? {}; + final conns = slice['connections'] as Map? ?? {}; + final sliceSourceBits = (conns['A'] as List).cast(); + final offset = params['OFFSET'] as int?; + final width = params['Y_WIDTH'] as int?; + + if (offset != expectedOffset || + width == null || + params['A_WIDTH'] != sourceWidth || + !_sameBits(sliceSourceBits, sourceBits)) { + valid = false; + break; + } + + expectedOffset += width; + combinedWidth += width; + } + + if (!valid || combinedWidth != outputBits.length) { + continue; + } + + cells[concatEntry.key] = { + 'hide_name': concat['hide_name'] ?? 0, + 'type': r'$slice', + 'parameters': { + 'OFFSET': startOffset, + 'A_WIDTH': sourceWidth, + 'Y_WIDTH': combinedWidth, + }, + 'attributes': concat['attributes'] ?? {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{ + 'A': sourceBits, + 'Y': outputBits, + }, + }; + + for (final sliceRef in inputSliceRefs) { + if (!_sliceOutputConsumedOutside( + sliceRef.name, + sliceRef.cell, + cells, + ports, + )) { + cellsToRemove.add(sliceRef.name); + } + } + } + + cellsToRemove.forEach(cells.remove); + } + } + + /// Finds a slice cell whose output bits exactly match [bits]. + static MapEntry>? _findSliceDrivingBits( + Map> cells, + List bits, + ) { + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$slice') { + continue; + } + final conns = cell['connections'] as Map? ?? {}; + final yBits = (conns['Y'] as List?)?.cast(); + if (yBits != null && _sameBits(yBits, bits)) { + return entry; + } + } + return null; + } + + /// Checks whether a slice output is still consumed outside that slice cell. + static bool _sliceOutputConsumedOutside( + String sliceName, + Map slice, + Map> cells, + Map ports, + ) { + final sliceConns = slice['connections'] as Map? ?? {}; + final outputBits = + ((sliceConns['Y'] as List?) ?? const []).whereType(); + final outputBitSet = outputBits.toSet(); + if (outputBitSet.isEmpty) { + return false; + } + + for (final rawPort in ports.values) { + final port = rawPort as Map; + final direction = port['direction'] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + final bits = (port['bits'] as List).whereType(); + if (bits.any(outputBitSet.contains)) { + return true; + } + } + + for (final entry in cells.entries) { + if (entry.key == sliceName) { + continue; + } + final cell = entry.value; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + for (final portEntry in conns.entries) { + final direction = dirs[portEntry.key] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + final bits = (portEntry.value as List).whereType(); + if (bits.any(outputBitSet.contains)) { + return true; + } + } + } + return false; + } + + /// Checks whether [bits] exactly matches any known named bit vector. + static bool _matchesNamedVector( + List bits, + List> namedBitVectors, + ) => + namedBitVectors.any( + (namedBits) => + namedBits.length == bits.length && + namedBits.indexed.every((entry) => entry.$2 == bits[entry.$1]), + ); + + /// Checks whether two bit vectors have identical contents and order. + static bool _sameBits(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); + /// Populates [bitMap] with output-wire-bit → input-wire-bit entries /// for a single transparent cell. static void _mapCellBits(Map cell, Map bitMap) { diff --git a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart index 6745c15c8..4ba4fe581 100644 --- a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // netlist_synth_module_definition.dart // Synth module definition specialization for netlist synthesis. @@ -81,6 +80,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { }).forEach(_concatAssembleArray); } + /// Adds slice cells that decompose a LogicArray port into element signals. void _subsetReceiveArrayPort(LogicArray port) { final portSynth = getSynthLogic(port)!; @@ -105,6 +105,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { } } + /// Adds a concat cell that assembles independent LogicArray element signals. void _concatAssembleArray(LogicArray array) { final arraySynth = getSynthLogic(array)!; final dummyElements = [ diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart index 8984cdafc..ddf6855c0 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -41,6 +41,7 @@ class NetlistSynthesisResult extends SynthesisResult { this.attributes = const {}, }); + /// Builds the JSON representation for this single module entry. String _buildJson() { final moduleEntry = { 'attributes': attributes, @@ -73,7 +74,7 @@ class NetlistSynthesisResult extends SynthesisResult { }; final contents = const JsonEncoder.withIndent(' ').convert({ 'creator': 'NetlistSynthesizer (rohd)', - 'version': netlistFormatVersion, + 'version': NetlistSynthesizer.formatVersion, 'modules': {typeName: moduleEntry}, }); return [ diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 42f5a765c..05ce7ad79 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -17,7 +17,6 @@ import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; /// A simple [Synthesizer] that produces netlist-compatible JSON. @@ -38,6 +37,9 @@ import 'package:rohd/src/utilities/sanitizer.dart'; /// final json = synth.synthesizeToJson(topModule); /// ``` class NetlistSynthesizer extends Synthesizer { + /// The current format version for netlist JSON produced by this synthesizer. + static const String formatVersion = '0.0.5'; + /// The configuration options controlling netlist synthesis. /// /// See [NetlistOptions] for documentation on individual fields. @@ -267,8 +269,8 @@ class NetlistSynthesizer extends Synthesizer { idAlias[childId] = parentId; } - // Collect LogicArray ports that have explicit array slice or - // array concat submodules so we can skip aliasing them (the + // Collect LogicArray ports that have explicit array_slice or + // array_concat submodules so we can skip aliasing them (the // $slice/$concat cells provide the structural link). final arraysWithExplicitCells = {}; for (final inst in synthDef.subModuleInstantiations) { @@ -424,7 +426,7 @@ class NetlistSynthesizer extends Synthesizer { ? bits : bits.map((b) => b is int ? resolveAlias(b) : b).toList(); - // -- Break shared wire IDs for array slice cells ----------------------- + // -- Break shared wire IDs for array slice/concat cells ----------------- // (Populated inside the alias block below; declared here so netnames // can reference it later.) final arraySliceOldToNew = {}; @@ -448,14 +450,16 @@ class NetlistSynthesizer extends Synthesizer { // trivial and it would be elided below. // // To preserve the structural decomposition in the schematic, we - // allocate fresh wire IDs for each array slice Y output, then + // allocate fresh wire IDs for each array_slice Y output, then // redirect all other cells that consume those IDs as inputs to // read from the fresh IDs instead. The slice input A keeps the // original parent-array IDs, so the data flow becomes: // parent (original IDs) → slice A → slice Y (fresh IDs) → consumer for (final cellEntry in cells.entries) { - if (!cellEntry.key.startsWith(Namer.synthArraySliceOperationName)) { + if (!cellEntry.key.startsWith( + SynthOperationNamer.arraySliceOperationName, + )) { continue; } final cell = cellEntry.value as Map; @@ -483,7 +487,9 @@ class NetlistSynthesizer extends Synthesizer { // gets replaced with the corresponding fresh ID. if (arraySliceOldToNew.isNotEmpty) { for (final cellEntry in cells.entries) { - if (cellEntry.key.startsWith(Namer.synthArraySliceOperationName)) { + if (cellEntry.key.startsWith( + SynthOperationNamer.arraySliceOperationName, + )) { continue; // skip the slice cells themselves } final cell = cellEntry.value as Map; @@ -504,38 +510,39 @@ class NetlistSynthesizer extends Synthesizer { } } } - - // -- Elide trivial $slice cells ---------------------------------- - // Also elide struct_slice cells ([SynthStructureSlice] - // instances from `_subsetReceiveStructPort`) because the new - // `$struct_unpack` cells emitted below supersede them with - // better-named field-level connections. - cells.removeWhere((cellKey, cell) { - if (cell['type'] != r'$slice') { - return false; - } - // Unconditionally remove struct_slice cells — they are - // duplicated by $struct_unpack cells which carry field names. - if (cellKey.startsWith(Namer.synthStructureSliceOperationName)) { - return true; - } - final params = cell['parameters'] as Map?; - final offset = params?['OFFSET']; - if (offset is! int) { - return false; - } - final conns = cell['connections']! as Map; - final aBits = conns['A'] as List?; - final yBits = conns['Y'] as List?; - if (aBits == null || yBits == null) { - return false; - } - return yBits.indexed.every( - (e) => offset + e.$1 < aBits.length && e.$2 == aBits[offset + e.$1], - ); - }); } + // -- Elide trivial $slice cells ---------------------------------- + // Also elide struct_slice cells ([SynthStructureSlice] instances from + // `_subsetReceiveStructPort`) because the new `$struct_unpack` cells + // emitted below supersede them with better-named field-level connections. + cells.removeWhere((cellKey, cell) { + if (cell['type'] != r'$slice') { + return false; + } + // Unconditionally remove struct_slice cells — they are duplicated by + // $struct_unpack cells which carry field names. + if (cellKey.startsWith( + SynthOperationNamer.structureSliceOperationName, + )) { + return true; + } + final params = cell['parameters'] as Map?; + final offset = params?['OFFSET']; + if (offset is! int) { + return false; + } + final conns = cell['connections']! as Map; + final aBits = conns['A'] as List?; + final yBits = conns['Y'] as List?; + if (aBits == null || yBits == null) { + return false; + } + return yBits.indexed.every( + (e) => offset + e.$1 < aBits.length && e.$2 == aBits[offset + e.$1], + ); + }); + // -- Emit $struct_unpack cells for LogicStructure elements ---------- // Group per-field entries by their parent LogicStructure and emit a // single multi-port cell per group. Each group has: @@ -708,11 +715,11 @@ class NetlistSynthesizer extends Synthesizer { final structLayout = dstLogic is LogicStructure ? SynthStructureLayout(dstLogic) : null; final cellName = dstLogic != null - ? Namer.synthOperationInstanceName( - operationName: Namer.synthStructureConcatOperationName, + ? SynthOperationNamer.instanceName( + operationName: SynthOperationNamer.structureConcatOperationName, destination: dstLogic, ) - : Namer.synthStructureConcatOperationName; + : SynthOperationNamer.structureConcatOperationName; // Build port_directions and connections. final portDirs = {}; @@ -771,21 +778,30 @@ class NetlistSynthesizer extends Synthesizer { pruneFloating: options.enableDCE, ); - // -- Break shared wire IDs for array concat cells -------------------- - // After aliasing, the concat inputs share the same wire IDs as the - // concat Y output (because LogicArray elements share the parent's - // bit storage). This makes the concat transparent -- constants - // appear to drive the parent array directly. + // -- Break shared wire IDs for array_concat cells -------------------- + // After aliasing, concat Y can share wire IDs with the independently + // driven element inputs (because LogicArray elements share the parent's + // bit storage). This makes concat Y a second driver of the element wires. // - // To fix: allocate fresh wire IDs for each concat input port, - // then redirect all other cells whose outputs used those old IDs - // to drive the fresh IDs instead. The concat Y output keeps the - // original parent-array IDs, so the data flow becomes: - // const → fresh_IDs → concat input → concat Y (= parent IDs) + // Allocate fresh IDs for concat Y and redirect downstream consumers to + // those fresh IDs. The concat inputs keep the original element IDs, so + // data flow is: + // element drivers → concat input → concat Y (fresh IDs) → consumer final arrayConcatOldToNew = {}; + final arrayConcatOldOutputBits = >{}; + final outputPortBitSets = [ + for (final port in ports.values) + if ((port as Map)['direction'] == 'output') + (port['bits'] as List).whereType().toSet(), + ]; for (final cellEntry in cells.entries) { - if (!cellEntry.key.startsWith(Namer.synthArrayConcatOperationName)) { + if (!cellEntry.key.startsWith( + SynthOperationNamer.arrayConcatOperationName, + )) { + continue; + } + if (cellEntry.key.startsWith('array_concat_output_')) { continue; } final cell = cellEntry.value as Map; @@ -793,40 +809,62 @@ class NetlistSynthesizer extends Synthesizer { final dirs = cell['port_directions'] as Map; for (final portEntry in conns.entries.toList()) { - if (dirs[portEntry.key] != 'input') { + if (dirs[portEntry.key] != 'output') { continue; } final oldBits = (portEntry.value as List).cast(); + final oldBitSet = oldBits.whereType().toSet(); + if (outputPortBitSets.any( + (outputBits) => + outputBits.length == oldBitSet.length && + outputBits.containsAll(oldBitSet), + )) { + continue; + } + arrayConcatOldOutputBits[cellEntry.key] = oldBitSet; conns[portEntry.key] = [ for (final b in oldBits) b is int - ? arrayConcatOldToNew.putIfAbsent( - b, - translation.allocateWireId, - ) + ? arrayConcatOldToNew.putIfAbsent(b, translation.allocateWireId) : b, ]; } } - // Redirect other cells: any output port bit that matches an old ID - // gets replaced with the corresponding fresh ID. + // Redirect downstream consumers: any input port or module output bit that + // matches an old concat Y ID gets replaced with the corresponding fresh ID. if (arrayConcatOldToNew.isNotEmpty) { - for (final cellEntry in cells.entries) { - if (cellEntry.key.startsWith(Namer.synthArrayConcatOperationName)) { - continue; // skip the concat cells themselves + for (final portEntry in ports.values) { + final port = portEntry as Map; + if (port['direction'] != 'output') { + continue; } + final bits = (port['bits'] as List).cast(); + final newBits = [ + for (final b in bits) b is int ? (arrayConcatOldToNew[b] ?? b) : b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + port['bits'] = newBits; + } + } + + for (final cellEntry in cells.entries) { final cell = cellEntry.value as Map; final conns = cell['connections'] as Map; final dirs = cell['port_directions'] as Map; + final selfOldOutputBits = + arrayConcatOldOutputBits[cellEntry.key] ?? const {}; for (final portEntry in conns.entries.toList()) { - if (dirs[portEntry.key] != 'output') { + if (dirs[portEntry.key] != 'input') { continue; } final bits = (portEntry.value as List).cast(); final newBits = [ - for (final b in bits) b is int ? (arrayConcatOldToNew[b] ?? b) : b, + for (final b in bits) + b is int && !selfOldOutputBits.contains(b) + ? (arrayConcatOldToNew[b] ?? b) + : b, ]; if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { conns[portEntry.key] = newBits; @@ -852,12 +890,14 @@ class NetlistSynthesizer extends Synthesizer { final netnames = translation.netnames; // -- Structural validation ------------------------------------------- - // Debug-mode checks to catch netlist bugs early. These verify that - // every signal has a driver and every cell is connected. - assert(() { - NetlistValidation.validate(ports, cells, module.name); - return true; - }(), 'Netlist structural validation failed for ${module.name}'); + // Always catch netlist shorts, even when assertions are disabled. + NetlistValidation.validate( + ports, + cells, + module.name, + netnames: netnames, + throwOnMultipleDrivers: true, + ); return NetlistSynthesisResult( module, @@ -877,7 +917,10 @@ class NetlistSynthesizer extends Synthesizer { /// Also used internally by [buildModulesMap] / [synthesizeToJson]. void applyPostProcessingPasses(Map> modules) { if (options.collapseTransparentClusters) { + NetlistPasses.collapseConcatOfAdjacentSlices(modules); + NetlistPasses.removeTrivialConcatAliases(modules); NetlistPasses.applyTransparentClustering(modules); + NetlistPasses.removeUnconsumedTransparentCells(modules); } } @@ -927,7 +970,7 @@ class NetlistSynthesizer extends Synthesizer { final combined = { 'creator': 'NetlistSynthesizer (rohd)', - 'version': netlistFormatVersion, + 'version': formatVersion, 'modules': modules, }; @@ -1029,11 +1072,7 @@ class NetlistSynthesizer extends Synthesizer { /// The [packageRoot] parameter is accepted for API compatibility with /// downstream trace-enabled branches. [slimMode] overrides the configured /// output mode for this call, allowing expansion after a slim request. - String synthesizeToJson( - Module top, { - String? packageRoot, - bool? slimMode, - }) { + String synthesizeToJson(Module top, {String? packageRoot, bool? slimMode}) { final sb = SynthBuilder(top, this); return generateCombinedJson(sb, top, slimMode: slimMode); } diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index c9eda24b6..4c7244c55 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -16,6 +16,7 @@ import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// All methods are static — no instances are created. @internal class NetlistUtils { + /// Prevents construction of this static utility class. NetlistUtils._(); /// Find the port name in [portMap] that corresponds to [sl]. diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index 8c16357b0..918891207 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // netlist_validation.dart // Structural validation utilities for emitted netlists. @@ -13,6 +12,7 @@ import 'package:meta/meta.dart'; /// Graph queries and structural checks for an emitted module netlist. @internal class NetlistValidation { + /// Prevents construction of this static utility class. NetlistValidation._(); /// Collects module-port and cell-connection bits with matching directions. @@ -39,18 +39,36 @@ class NetlistValidation { }), }; - /// Reports disconnected cells and floating constants in debug builds. - static void validate( + /// Reports disconnected cells, floating constants, and shorted wires. + static List validate( Map> ports, Map> cells, - String moduleName, - ) { + String moduleName, { + Map? netnames, + bool throwOnMultipleDrivers = false, + bool printWarnings = true, + }) { + final warnings = []; + final multipleDriverWarnings = []; + + void report(String message, {bool multipleDriver = false}) { + warnings.add(message); + if (multipleDriver) { + multipleDriverWarnings.add(message); + } + if (printWarnings) { + // ignore: avoid_print + print(message); + } + } + final consumedBits = connectedBits( ports, cells, portDirections: const {'output', 'inout'}, cellDirection: 'input', ); + final driversByBit = _driversByBit(ports, cells); const transparentTypes = { r'$buf', r'$slice', @@ -59,6 +77,53 @@ class NetlistValidation { r'$struct_pack', }; + for (final entry in driversByBit.entries) { + if (entry.value.length <= 1) { + continue; + } + report( + '[netlist-validate] WARNING: $moduleName: ' + 'wire bit ${entry.key} has multiple drivers: ' + '${entry.value.join(', ')}', + multipleDriver: true, + ); + } + + if (netnames != null) { + for (final entry in netnames.entries) { + final netname = entry.value; + if (netname is! Map) { + continue; + } + final logicType = netname['logic_type']; + if (logicType is! Map || + (logicType['arrayDims'] is! List && logicType['fields'] is! List)) { + continue; + } + final bits = (netname['bits'] as List?)?.whereType() ?? const []; + final aggregateDrivers = { + for (final bit in bits) ...driversByBit[bit] ?? const [], + }; + if (aggregateDrivers.length <= 1) { + continue; + } + report( + '[netlist-validate] WARNING: $moduleName: ' + 'aggregate net "${entry.key}" is reached from multiple drivers: ' + '${aggregateDrivers.join(', ')}', + multipleDriver: true, + ); + } + } + + if (throwOnMultipleDrivers && multipleDriverWarnings.isNotEmpty) { + throw StateError( + 'Netlist validation failed for $moduleName: ' + '${multipleDriverWarnings.length} multiple-driver wire bit(s) found.\n' + '${multipleDriverWarnings.join('\n')}', + ); + } + for (final entry in cells.entries) { final cell = entry.value; final type = cell['type'] as String? ?? ''; @@ -67,8 +132,7 @@ class NetlistValidation { } final outputBits = _cellBits(cell, 'output'); if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - // ignore: avoid_print - print( + report( '[netlist-validate] WARNING: $moduleName: ' 'cell "${entry.key}" (type: $type) has no consumed outputs ' '— fully disconnected logic gate', @@ -81,20 +145,65 @@ class NetlistValidation { )) { final outputBits = _cellBits(entry.value, 'output'); if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - // ignore: avoid_print - print( + report( '[netlist-validate] WARNING: $moduleName: ' r'$const cell "${entry.key}" drives wires consumed by nothing ' '— floating constant', ); } } + + return warnings; } - static List _cellBits( - Map cell, - String direction, + /// Collects the port and cell output drivers for each integer bit ID. + static Map> _driversByBit( + Map> ports, + Map> cells, ) { + final drivers = >{}; + + void addDriver(int bit, String driver) => + (drivers[bit] ??= []).add(driver); + + for (final entry in ports.entries) { + final direction = entry.value['direction'] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + for (final bit in (entry.value['bits'] as List?) ?? const []) { + if (bit is int) { + addDriver(bit, 'port ${entry.key} ($direction)'); + } + } + } + + for (final entry in cells.entries) { + final connections = entry.value['connections'] as Map?; + final directions = + entry.value['port_directions'] as Map?; + if (connections == null || directions == null) { + continue; + } + final type = entry.value['type'] as String? ?? 'unknown'; + for (final port in connections.entries) { + final direction = directions[port.key] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + for (final bit in (port.value as List?) ?? const []) { + if (bit is int) { + addDriver(bit, 'cell ${entry.key}.${port.key} ($type)'); + } + } + } + } + + return drivers; + } + + /// Returns integer bits connected to ports with the requested [direction]. + static List _cellBits(Map cell, String direction) { final connections = cell['connections'] as Map?; final directions = cell['port_directions'] as Map?; if (connections == null || directions == null) { diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index 521783d5a..eaf882ed1 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -10,7 +10,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/namer.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [Swizzle] used by synthesis backends to explicitly assemble a /// [LogicArray] from its elements. @@ -19,13 +19,11 @@ class SynthArrayConcat extends Swizzle { final LogicArray _destination; /// Creates a synthesis array concatenation from [signals]. - SynthArrayConcat( - super.signals, { - required LogicArray destination, - }) : _destination = destination, + SynthArrayConcat(super.signals, {required LogicArray destination}) + : _destination = destination, super( - name: Namer.synthOperationInstanceName( - operationName: Namer.synthArrayConcatOperationName, + name: SynthOperationNamer.instanceName( + operationName: SynthOperationNamer.arrayConcatOperationName, destination: destination, ), ); @@ -35,7 +33,7 @@ class SynthArrayConcat extends Swizzle { @override Object get instanceNameKey => ( - operationName: Namer.synthArrayConcatOperationName, + operationName: SynthOperationNamer.arrayConcatOperationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart index b85e49799..08568beac 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -10,7 +10,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/namer.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [BusSubset] used by synthesis backends to explicitly extract a /// [LogicArray] element from its packed parent representation. @@ -26,8 +26,8 @@ class SynthArraySlice extends BusSubset { required Logic destination, }) : _destination = destination, super( - name: Namer.synthOperationInstanceName( - operationName: Namer.synthArraySliceOperationName, + name: SynthOperationNamer.instanceName( + operationName: SynthOperationNamer.arraySliceOperationName, destination: destination, ), ); @@ -37,7 +37,7 @@ class SynthArraySlice extends BusSubset { @override Object get instanceNameKey => ( - operationName: Namer.synthArraySliceOperationName, + operationName: SynthOperationNamer.arraySliceOperationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index eba1d96aa..349c946f2 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -131,6 +131,15 @@ class SynthLogic { /// may be additional conditions that prevent clearing. bool get isClearable => mergeable; + /// Like [isClearable], but additionally permits a *renameable* signal (whose + /// chosen name carries no semantic meaning and would simply be dropped). + /// + /// This is `false` only for [isReserved] (e.g. ports) and constant signals, + /// whose identities must be preserved. It is used by the aggregate-collapse + /// optimizations to eliminate intermediate (possibly named) nets. + bool get isClearableOrRenameable => + _reservedLogic == null && _constLogic == null; + /// The source connections to any [Logic] in this [SynthLogic] which are not /// also contained within this [SynthLogic]. Iterable get srcConnections { diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 90d1048f5..e3b7f5629 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -288,11 +288,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) @@ -546,6 +546,9 @@ class SynthModuleDefinition { final resultLogic = _inlineResultLogic(subModuleInstantiation); if (resultLogic != null) { _weakNameClaimSignals.add(resultLogic); + if (resultLogic is SynthLogicArrayElement) { + _weakNameClaimSignals.add(resultLogic.parentArray.resolved); + } } } } @@ -580,9 +583,91 @@ class SynthModuleDefinition { } } + // Arrays which are used as a whole (not just element-by-element) anywhere: + // as a port of this module, in a submodule port mapping, or in an + // assignment. We must not inline away elements of such arrays, since the + // array declaration is still needed and elements could lose connections. + final aggregateUsedArrays = {}; + void markIfAggregateArray(SynthLogic? synthLogic) { + if (synthLogic != null && synthLogic.isArray) { + aggregateUsedArrays.add(synthLogic.resolved); + } + } + + [...inputs, ...outputs, ...inOuts].forEach(markIfAggregateArray); + for (final subModuleInstantiation in subModuleInstantiations) { + [ + ...subModuleInstantiation.inputMapping.values, + ...subModuleInstantiation.outputMapping.values, + ...subModuleInstantiation.inOutMapping.values, + ].forEach(markIfAggregateArray); + } + for (final assignment in assignments) { + markIfAggregateArray(assignment.src); + markIfAggregateArray(assignment.dst); + } + + // Signals still referenced directly by an assignment must not be inlined + // away. This is especially important for array elements, whose assignments + // are not collapsed away like mergeable signals. + final assignmentReferencedSignals = { + for (final assignment in assignments) ...[assignment.src, assignment.dst], + }; + + final inlineableResultLogics = {}; + for (final subModuleInstantiation in inlineableSubmoduleInstantiations) { + final resultLogic = _inlineResultLogic(subModuleInstantiation); + if (resultLogic != null && subModuleInstantiation.needsInstantiation) { + inlineableResultLogics.add(resultLogic.resolved); + } + } + + bool isInlineableArrayElementCandidate(SynthLogic signal) => + signal is SynthLogicArrayElement && + inlineableResultLogics.contains(signal.resolved) && + signal.isClearable && + !aggregateUsedArrays.contains(signal.parentArray.resolved) && + !assignmentReferencedSignals.contains(signal.resolved); + + final candidateElements = {}; + signalUsage.forEach((signal, signalUsageCount) { + if (signalUsageCount == 1 && isInlineableArrayElementCandidate(signal)) { + candidateElements.add(signal.resolved); + } + }); + + // Only inline array elements when the whole parent array will be replaced. + // Partial inlining is unsafe: the array would remain declared and its + // remaining elements could change behavior (for example `x` vs `z` on + // undriven bits). + final approvedElements = {}; + final candidatesByArray = >{}; + for (final element in candidateElements) { + candidatesByArray + .putIfAbsent( + (element as SynthLogicArrayElement).parentArray.resolved, + () => {}, + ) + .add(element); + } + candidatesByArray.forEach((parentArray, arrayCandidates) { + final allElementSynthLogics = parentArray.logics + .whereType() + .expand((logicArray) => logicArray.elements) + .map(getSynthLogic) + .nonNulls + .map((e) => e.resolved) + .toSet(); + if (allElementSynthLogics.isNotEmpty && + allElementSynthLogics.every(candidateElements.contains)) { + approvedElements.addAll(arrayCandidates); + } + }); + final singleUseSignals = {}; signalUsage.forEach((signal, signalUsageCount) { - if (signalUsageCount == 1 && signal.mergeable) { + if (signalUsageCount == 1 && + (signal.mergeable || approvedElements.contains(signal.resolved))) { singleUseSignals.add(signal); } }); @@ -932,8 +1017,10 @@ class SynthModuleDefinition { for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { submodule.pickName(module); - assert(submodule.module.name == submodule.name, - 'Expect reserved names to retain their name.'); + assert( + submodule.module.name == submodule.name, + 'Expect reserved names to retain their name.', + ); } } diff --git a/lib/src/synthesizers/utilities/synth_operation_namer.dart b/lib/src/synthesizers/utilities/synth_operation_namer.dart new file mode 100644 index 000000000..145f5b619 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_operation_namer.dart @@ -0,0 +1,143 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_operation_namer.dart +// Stable naming for synthesis-created helper operations. +// +// 2026 July 14 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// Stable names for synthesis-created helper operations. +@internal +class SynthOperationNamer { + /// Canonical base name for synthesis-created array slice operations. + /// + /// Netlist synthesis uses these when a [LogicArray] port or submodule output + /// must be decomposed into element wires, such as a child reading + /// `values.elements[0]` from an aggregate array input port. + static const String arraySliceOperationName = 'array_slice'; + + /// Canonical base name for synthesis-created array concat operations. + /// + /// Netlist synthesis uses these when independently driven [LogicArray] + /// elements must be reassembled for an aggregate consumer, such as + /// `values.elements[0] <= a` and `values.elements[1] <= b` feeding a child + /// array input port. + static const String arrayConcatOperationName = 'array_concat'; + + /// Canonical base name for synthesis-created structure slice operations. + /// + /// Synthesis uses these internally when a [LogicStructure] aggregate must + /// expose field wires. The netlist projection can replace them with + /// `$struct_unpack` cells so field names are preserved. + static const String structureSliceOperationName = 'struct_slice'; + + /// Canonical base name for synthesis-created structure concat operations. + /// + /// Netlist synthesis uses these when independently driven [LogicStructure] + /// fields must be packed back into an aggregate struct output or submodule + /// input. The emitted netlist projection represents this as `$struct_pack`. + static const String structureConcatOperationName = 'struct_concat'; + + SynthOperationNamer._(); + + /// Returns the canonical instance name for a synthesis-created operation + /// that targets [destination]. + /// + /// The numeric suffix is derived from [destination]'s structural position, + /// not from the order in which a backend asks for names. This keeps helper + /// operation names stable across output formats that traverse a module in + /// different orders. + static String instanceName({ + required String operationName, + required Logic destination, + }) => + '${Sanitizer.sanitizeSV(operationName)}_' + '${_destinationSuffix(destination)}'; + + static String _destinationSuffix(Logic destination) { + final parts = [ + ..._modulePathIndices(destination.parentModule), + ..._logicLocationIndices(destination), + ]; + + return parts.isEmpty ? '0' : parts.join('_'); + } + + static List _modulePathIndices(Module? module) { + if (module == null) { + return const [0]; + } + + final parent = module.parent; + if (parent == null) { + return const [0]; + } + + final siblings = parent.subModules.toList(); + final index = siblings.indexWhere( + (submodule) => identical(submodule, module), + ); + return [..._modulePathIndices(parent), if (index < 0) 0 else index]; + } + + static List _logicLocationIndices(Logic destination) { + final elementPath = []; + var root = destination; + while (root.parentStructure != null) { + final parent = root.parentStructure!; + final index = parent.elements.indexWhere( + (element) => identical(element, root), + ); + elementPath.insert(0, index < 0 ? root.arrayIndex ?? 0 : index); + root = parent; + } + + final module = root.parentModule; + if (module == null) { + return [0, ...elementPath]; + } + + final location = _logicLocationInModule(module, root); + return [...location, ...elementPath]; + } + + static List _logicLocationInModule(Module module, Logic root) { + final inputIndex = _identityIndex(module.inputs.values, root); + if (inputIndex >= 0) { + return [0, inputIndex]; + } + + final outputIndex = _identityIndex(module.outputs.values, root); + if (outputIndex >= 0) { + return [1, outputIndex]; + } + + final inOutIndex = _identityIndex(module.inOuts.values, root); + if (inOutIndex >= 0) { + return [2, inOutIndex]; + } + + final internalIndex = _identityIndex(module.internalSignals, root); + if (internalIndex >= 0) { + return [3, internalIndex]; + } + + return const [4, 0]; + } + + static int _identityIndex(Iterable logics, Logic target) { + var index = 0; + for (final logic in logics) { + if (identical(logic, target)) { + return index; + } + index++; + } + return -1; + } +} diff --git a/lib/src/synthesizers/utilities/synth_structure_concat.dart b/lib/src/synthesizers/utilities/synth_structure_concat.dart index 08cd4c880..870a4543d 100644 --- a/lib/src/synthesizers/utilities/synth_structure_concat.dart +++ b/lib/src/synthesizers/utilities/synth_structure_concat.dart @@ -10,7 +10,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/namer.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [Swizzle] used by synthesis backends to explicitly assemble a /// [LogicStructure] from its leaf elements. @@ -19,13 +19,11 @@ class SynthStructureConcat extends Swizzle { final LogicStructure _destination; /// Creates a synthesis structure concatenation from [signals]. - SynthStructureConcat( - super.signals, { - required LogicStructure destination, - }) : _destination = destination, + SynthStructureConcat(super.signals, {required LogicStructure destination}) + : _destination = destination, super( - name: Namer.synthOperationInstanceName( - operationName: Namer.synthStructureConcatOperationName, + name: SynthOperationNamer.instanceName( + operationName: SynthOperationNamer.structureConcatOperationName, destination: destination, ), ); @@ -35,7 +33,7 @@ class SynthStructureConcat extends Swizzle { @override Object get instanceNameKey => ( - operationName: Namer.synthStructureConcatOperationName, + operationName: SynthOperationNamer.structureConcatOperationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/synth_structure_slice.dart b/lib/src/synthesizers/utilities/synth_structure_slice.dart index c4a7d2790..bfb085c20 100644 --- a/lib/src/synthesizers/utilities/synth_structure_slice.dart +++ b/lib/src/synthesizers/utilities/synth_structure_slice.dart @@ -10,7 +10,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/namer.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [BusSubset] used by synthesis backends to explicitly extract a /// [LogicStructure] leaf from its packed parent representation. @@ -26,8 +26,8 @@ class SynthStructureSlice extends BusSubset { required Logic destination, }) : _destination = destination, super( - name: Namer.synthOperationInstanceName( - operationName: Namer.synthStructureSliceOperationName, + name: SynthOperationNamer.instanceName( + operationName: SynthOperationNamer.structureSliceOperationName, destination: destination, ), ); @@ -37,7 +37,7 @@ class SynthStructureSlice extends BusSubset { @override Object get instanceNameKey => ( - operationName: Namer.synthStructureSliceOperationName, + operationName: SynthOperationNamer.structureSliceOperationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index c34b4db60..5b44a8c2a 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -6,6 +6,7 @@ export 'synth_array_slice.dart'; export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; +export 'synth_operation_namer.dart'; export 'synth_structure_concat.dart'; export 'synth_structure_layout.dart'; export 'synth_structure_slice.dart'; diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index 5c5c0c965..dc585612d 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -24,32 +24,6 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// are assigned lazily on the first [instanceNameOf] call. @internal class Namer { - /// Canonical base name for synthesis-created array slice operations. - static const String synthArraySliceOperationName = 'array_slice'; - - /// Canonical base name for synthesis-created array concat operations. - static const String synthArrayConcatOperationName = 'array_concat'; - - /// Canonical base name for synthesis-created structure slice operations. - static const String synthStructureSliceOperationName = 'struct_slice'; - - /// Canonical base name for synthesis-created structure concat operations. - static const String synthStructureConcatOperationName = 'struct_concat'; - - /// Returns the canonical base instance name for a synthesis-created - /// structural operation that targets [destination]. - /// - /// The numeric suffix is derived from [destination]'s structural position, - /// not from the order in which a backend asks for names. This keeps helper - /// operation names stable across output formats that traverse a module in - /// different orders. - static String synthOperationInstanceName({ - required String operationName, - required Logic destination, - }) => - '${Sanitizer.sanitizeSV(operationName)}_' - '${_synthOperationDestinationSuffix(destination)}'; - /// The [Uniquifier] that manages the shared namespace for this module. final Uniquifier _uniquifier; @@ -98,89 +72,6 @@ class Namer { @visibleForTesting bool isAvailable(String name) => _uniquifier.isAvailable(name); - static String _synthOperationDestinationSuffix(Logic destination) { - final parts = [ - ..._modulePathIndices(destination.parentModule), - ..._logicLocationIndices(destination), - ]; - - return parts.isEmpty ? '0' : parts.join('_'); - } - - static List _modulePathIndices(Module? module) { - if (module == null) { - return const [0]; - } - - final parent = module.parent; - if (parent == null) { - return const [0]; - } - - final siblings = parent.subModules.toList(); - final index = - siblings.indexWhere((submodule) => identical(submodule, module)); - return [ - ..._modulePathIndices(parent), - if (index < 0) 0 else index, - ]; - } - - static List _logicLocationIndices(Logic destination) { - final elementPath = []; - var root = destination; - while (root.parentStructure != null) { - final parent = root.parentStructure!; - final index = - parent.elements.indexWhere((element) => identical(element, root)); - elementPath.insert(0, index < 0 ? root.arrayIndex ?? 0 : index); - root = parent; - } - - final module = root.parentModule; - if (module == null) { - return [0, ...elementPath]; - } - - final location = _logicLocationInModule(module, root); - return [...location, ...elementPath]; - } - - static List _logicLocationInModule(Module module, Logic root) { - final inputIndex = _identityIndex(module.inputs.values, root); - if (inputIndex >= 0) { - return [0, inputIndex]; - } - - final outputIndex = _identityIndex(module.outputs.values, root); - if (outputIndex >= 0) { - return [1, outputIndex]; - } - - final inOutIndex = _identityIndex(module.inOuts.values, root); - if (inOutIndex >= 0) { - return [2, inOutIndex]; - } - - final internalIndex = _identityIndex(module.internalSignals, root); - if (internalIndex >= 0) { - return [3, internalIndex]; - } - - return const [4, 0]; - } - - static int _identityIndex(Iterable logics, Logic target) { - var index = 0; - for (final logic in logics) { - if (identical(logic, target)) { - return index; - } - index++; - } - return -1; - } - // ─── Instance naming (Module → String) ────────────────────────── /// Returns the canonical instance name for [submodule]. diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 8dce2dac7..6f7ede4fb 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2,16 +2,18 @@ // SPDX-License-Identifier: BSD-3-Clause // // netlist_synthesizer_test.dart -// Comprehensive tests for the netlist synthesizer covering netlist cell -// mapping, structural validation, options permutations, and real -// example designs. +// Comprehensive tests for the netlist synthesizer. // // 2026 April 13 // Author: Auto-generated +import 'dart:async'; import 'dart:convert'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; import 'package:test/test.dart'; import '../example/example.dart'; @@ -183,6 +185,130 @@ class SwizzleModule extends Module { } } +/// Child with a LogicArray input, used to exercise array port netlisting. +class ArrayInputChildModule extends Module { + LogicArray get values => input('values') as LogicArray; + + Logic get packedOut => output('packedOut'); + + ArrayInputChildModule(LogicArray values) : super(name: 'arrayinputchild') { + values = addInputArray( + 'values', + values, + dimensions: values.dimensions, + elementWidth: values.elementWidth, + ); + addOutput('packedOut', width: values.width) <= + [for (final element in values.elements.reversed) element].swizzle(); + } +} + +/// Child with a LogicArray output, used to exercise regrouping array output +/// elements into another child array input. +class ArrayOutputChildModule extends Module { + LogicArray get values => output('values') as LogicArray; + + ArrayOutputChildModule() : super(name: 'arrayoutputchild') { + addOutputArray('values', dimensions: [4], elementWidth: 8); + } +} + +/// Parent whose internal LogicArray elements independently feed a child array +/// input port. +class InternalArrayToChildModule extends Module { + InternalArrayToChildModule() : super(name: 'internalarraytochild') { + final first = addInput('first', Logic(width: 8), width: 8); + final second = addInput('second', Logic(width: 8), width: 8); + final values = LogicArray([2], 8, name: 'values'); + + values.elements[0] <= first; + values.elements[1] <= second; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent whose internal LogicArray groups elements from another child output +/// array before feeding a child input port. +class RegroupedArrayOutputToChildModule extends Module { + RegroupedArrayOutputToChildModule() + : super(name: 'regroupedarrayoutputtochild') { + final source = ArrayOutputChildModule(); + final values = LogicArray([2], 8, name: 'values'); + + values.elements[0] <= source.values.elements[2]; + values.elements[1] <= source.values.elements[3]; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent with nested internal LogicArrays, used to exercise concat-to-concat +/// consumer rewrites after array concat outputs receive fresh wire IDs. +class NestedInternalArrayToChildModule extends Module { + NestedInternalArrayToChildModule() + : super(name: 'nestedinternalarraytochild') { + final inputs = [ + for (var index = 0; index < 4; index++) + addInput('in$index', Logic(width: 8), width: 8), + ]; + final lower = LogicArray([2], 8, name: 'lower'); + final upper = LogicArray([2], 8, name: 'upper'); + final values = LogicArray([2], 16, name: 'values'); + + lower.elements[0] <= inputs[0]; + lower.elements[1] <= inputs[1]; + upper.elements[0] <= inputs[2]; + upper.elements[1] <= inputs[3]; + values.elements[0] <= lower; + values.elements[1] <= upper; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Simple two-field structure used to demonstrate netlist struct unpack/pack +/// cells. +class NetlistPairStruct extends LogicStructure { + Logic get low => elements[0]; + + Logic get high => elements[1]; + + NetlistPairStruct({super.name = 'pair'}) + : super([ + Logic(name: 'low', width: 4), + Logic(name: 'high', width: 4), + ]); + + @override + NetlistPairStruct clone({String? name}) => NetlistPairStruct(name: name); +} + +/// Consumes fields of a typed structure input independently, requiring the +/// netlist to unpack the aggregate port into named field connections. +class StructInputConsumerModule extends Module { + StructInputConsumerModule(NetlistPairStruct pair) + : super(name: 'structinputconsumer') { + pair = addTypedInput('pair', pair); + addOutput('packedOut', width: pair.width) <= + [pair.high, pair.low].swizzle(); + } +} + +/// Drives fields of a typed structure output independently, requiring the +/// netlist to pack the field wires back into the aggregate output port. +class StructOutputProducerModule extends Module { + StructOutputProducerModule() : super(name: 'structoutputproducer') { + final low = addInput('low', Logic(width: 4), width: 4); + final high = addInput('high', Logic(width: 4), width: 4); + final pair = NetlistPairStruct(name: 'pairValue'); + + pair.low <= low; + pair.high <= high ^ Const(1, width: 4); + addTypedOutput('pair', pair.clone).gets(pair); + } +} + /// Exercises arithmetic right shift (ARShift). class ARShiftModule extends Module { Logic get y => output('y'); @@ -828,57 +954,56 @@ void main() { final expanded = translator.synthesizeToJson(module, slimMode: false); final initiallyExpanded = NetlistSynthesizer().synthesizeToJson(module); - final slimModules = _modules( - jsonDecode(slim) as Map, - ); + final slimModules = _modules(jsonDecode(slim) as Map); expect( slimModules.values - .expand((definition) => _cells( - definition as Map, - ).values) + .expand( + (definition) => _cells(definition as Map).values, + ) .every((cell) => !(cell as Map).containsKey('connections')), isTrue, ); expect(expanded, initiallyExpanded); }); - test('filter bank can stop traversal at an opaque custom SV module', - () async { - final synthesizer = NetlistSynthesizer( - options: const NetlistOptions(leafModuleTypes: [FlipFlop, MacUnit]), - ); - final json = jsonDecode(synthesizer.synthesizeToJson( - filterBank, - )) as Map; - final modules = _modules(json); + test( + 'filter bank can stop traversal at an opaque custom SV module', + () async { + final synthesizer = NetlistSynthesizer( + options: const NetlistOptions(leafModuleTypes: [FlipFlop, MacUnit]), + ); + final json = jsonDecode(synthesizer.synthesizeToJson(filterBank)) + as Map; + final modules = _modules(json); - expect( - modules.keys.any((name) => name.contains('MacUnit')), - isFalse, - reason: 'MacUnit is treated like externally supplied/custom SV, so ' - 'the netlist should not emit a definition for it.', - ); + expect( + modules.keys.any((name) => name.contains('MacUnit')), + isFalse, + reason: 'MacUnit is treated like externally supplied/custom SV, so ' + 'the netlist should not emit a definition for it.', + ); - final channelDefs = modules.entries.where( - (entry) => entry.key.contains('FilterChannel'), - ); - expect(channelDefs, isNotEmpty); + final channelDefs = modules.entries.where( + (entry) => entry.key.contains('FilterChannel'), + ); + expect(channelDefs, isNotEmpty); - final macCells = channelDefs.expand((entry) { - final def = entry.value as Map; - return _cells(def).values.where((cell) { - final cellMap = cell as Map; - return (cellMap['type'] as String).contains('MacUnit'); - }); - }).toList(); + final macCells = channelDefs.expand((entry) { + final def = entry.value as Map; + return _cells(def).values.where((cell) { + final cellMap = cell as Map; + return (cellMap['type'] as String).contains('MacUnit'); + }); + }).toList(); - expect( - macCells, - isNotEmpty, - reason: 'FilterChannel should still instantiate the opaque MacUnit ' - 'cell; only hierarchy traversal stops at that boundary.', - ); - }); + expect( + macCells, + isNotEmpty, + reason: 'FilterChannel should still instantiate the opaque MacUnit ' + 'cell; only hierarchy traversal stops at that boundary.', + ); + }, + ); test('DCE disabled still produces valid netlist', () async { final synth = SynthBuilder( @@ -1221,41 +1346,47 @@ void main() { }); }); - // ── Group 7: Wire ID and structural invariants ───────────────────── + // ── Group 8: Wire ID and structural invariants ───────────────────── group('wire ID and structural invariants', () { test('default synthesizers do not share mutable leaf mappers', () { final first = NetlistSynthesizer(); final second = NetlistSynthesizer(); - expect(identical(first.netlistCellMapper, second.netlistCellMapper), - isFalse); + expect( + identical(first.netlistCellMapper, second.netlistCellMapper), + isFalse, + ); }); - test('leaf module type option controls which modules stop traversal', - () async { - final module = AddWrapperModule(); - await module.build(); - - final childDefinitionName = module.subModules.single.definitionName; - final synthesizer = NetlistSynthesizer( - options: const NetlistOptions(leafModuleTypes: [AddModule]), - ); + test( + 'leaf module type option controls which modules stop traversal', + () async { + final module = AddWrapperModule(); + await module.build(); + + final childDefinitionName = module.subModules.single.definitionName; + final synthesizer = NetlistSynthesizer( + options: const NetlistOptions(leafModuleTypes: [AddModule]), + ); - final json = jsonDecode(synthesizer.synthesizeToJson(module)) - as Map; - final modules = json['modules'] as Map; - final top = modules[module.definitionName] as Map; - final cells = _cells(top); + final json = jsonDecode(synthesizer.synthesizeToJson(module)) + as Map; + final modules = json['modules'] as Map; + final top = modules[module.definitionName] as Map; + final cells = _cells(top); - expect(modules, isNot(contains(childDefinitionName))); - expect( - cells.values, - contains(predicate>( - (cell) => cell['type'] == childDefinitionName, - )), - ); - }); + expect(modules, isNot(contains(childDefinitionName))); + expect( + cells.values, + contains( + predicate>( + (cell) => cell['type'] == childDefinitionName, + ), + ), + ); + }, + ); test('repeated translation of the same module is identical', () async { final module = LogicArrayExample( @@ -1303,6 +1434,323 @@ void main() { expect(firstWireId(secondModule), 2); }); + test('array concat outputs use fresh wire IDs', () async { + final warnings = []; + final json = await runZoned( + () => _synthToMap(InternalArrayToChildModule()), + zoneSpecification: ZoneSpecification( + print: (_, __, ___, line) => warnings.add(line), + ), + ); + final moduleDef = + _modules(json)['InternalArrayToChildModule'] as Map; + final cells = _cells(moduleDef); + final arrayConcats = cells.entries.where( + (entry) => entry.key.startsWith('array_concat'), + ); + + expect( + warnings.where((warning) => warning.contains('multiple drivers')), + isEmpty, + ); + expect(arrayConcats, isNotEmpty); + for (final arrayConcat in arrayConcats) { + final connections = (arrayConcat.value + as Map)['connections'] as Map; + final inputBits = connections.entries + .where((entry) => entry.key != 'Y') + .expand((entry) => entry.value as List) + .toSet(); + final outputBits = + (connections['Y'] as List).whereType().toSet(); + + expect(inputBits.intersection(outputBits), isEmpty); + } + }); + + test('regrouped array output elements get explicit concat', () async { + final module = RegroupedArrayOutputToChildModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final arrayConcatEntries = cells.entries.where( + (entry) => entry.key.startsWith('array_concat'), + ); + + expect(arrayConcatEntries, isNotEmpty, reason: cells.keys.join(', ')); + expect( + arrayConcatEntries.any((entry) { + final connections = (entry.value + as Map)['connections'] as Map; + final outputBits = connections['Y'] as List?; + return outputBits?.length == 16; + }), + isTrue, + ); + }); + + test('nested array concats feed downstream concat inputs', () async { + final json = await _synthToMap(NestedInternalArrayToChildModule()); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final concatEntries = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$concat'; + }).toList(); + + final concatOutputBits = {}; + for (final entry in concatEntries) { + final cell = entry.value as Map; + final connections = cell['connections'] as Map; + concatOutputBits.addAll((connections['Y'] as List).whereType()); + } + + final concatInputConsumers = {}; + for (final entry in concatEntries) { + final cell = entry.value as Map; + final connections = cell['connections'] as Map; + final directions = cell['port_directions'] as Map; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] == 'input') { + concatInputConsumers.addAll( + (portEntry.value as List).whereType(), + ); + } + } + } + + expect(concatOutputBits.intersection(concatInputConsumers), isNotEmpty); + }); + + test('struct input fields get explicit unpack cell', () async { + final module = StructInputConsumerModule(NetlistPairStruct()); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef); + final structUnpacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$struct_unpack'; + }).toList(); + + expect(structUnpacks, isNotEmpty, reason: cells.keys.join(', ')); + expect( + structUnpacks.any((entry) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map; + return directions['A'] == 'input' && + directions['low'] == 'output' && + directions['high'] == 'output'; + }), + isTrue, + ); + }); + + test('struct output fields get explicit pack cell', () async { + final module = StructOutputProducerModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final structPacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return entry.key.startsWith( + SynthOperationNamer.structureConcatOperationName, + ) && + cell['type'] == r'$struct_pack'; + }).toList(); + + expect(structPacks, isNotEmpty, reason: cells.keys.join(', ')); + expect( + structPacks.any((entry) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map; + return directions['low'] == 'input' && + directions['high'] == 'input' && + directions['Y'] == 'output'; + }), + isTrue, + ); + }); + + test('struct aggregate netnames cannot span multiple drivers', () { + final ports = >{}; + final cells = >{ + 'first_driver': { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': 8}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': List.generate(8, (index) => 100 + index), + 'Y': List.generate(8, (index) => 200 + index), + }, + }, + 'second_driver': { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': 8}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': List.generate(8, (index) => 300 + index), + 'Y': List.generate(8, (index) => 400 + index), + }, + }, + }; + final netnames = { + 'values': { + 'bits': [ + ...List.generate(8, (index) => 200 + index), + ...List.generate(8, (index) => 400 + index), + ], + 'logic_type': { + 'typeName': 'PairStructure', + 'fields': [ + {'name': 'first', 'width': 8}, + {'name': 'second', 'width': 8}, + ], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + cells, + 'struct_module', + netnames: netnames, + throwOnMultipleDrivers: true, + printWarnings: false, + ), + throwsStateError, + ); + }); + + test('optimized netlist removes concat aliases of named vectors', () async { + final json = await _synthToMap( + NestedInternalArrayToChildModule(), + options: const NetlistOptions(collapseTransparentClusters: true), + ); + + for (final moduleDef in _modules( + json, + ).values.cast>()) { + final namedBitVectors = [ + for (final netname + in (moduleDef['netnames'] as Map).values) + if (netname is Map && netname['bits'] is List) + (netname['bits'] as List).cast(), + ]; + + for (final entry in _cells(moduleDef).entries) { + final cell = entry.value as Map; + if (cell['type'] != r'$concat') { + continue; + } + + final connections = cell['connections'] as Map; + final directions = cell['port_directions'] as Map; + final inputBits = [ + for (final portEntry in connections.entries) + if (directions[portEntry.key] != 'output') + ...(portEntry.value as List).cast(), + ]; + + expect( + namedBitVectors.any( + (bits) => + bits.length == inputBits.length && + bits.indexed.every( + (bitEntry) => bitEntry.$2 == inputBits[bitEntry.$1], + ), + ), + isFalse, + reason: '${entry.key} aliases an already named vector', + ); + } + } + }); + + test('concat of adjacent slices collapses to one slice', () { + final sourceBits = List.generate(32, (index) => 100 + index); + final modules = >{ + 'top': { + 'attributes': {}, + 'ports': >{}, + 'netnames': >{}, + 'cells': >{ + 'slice0': { + 'hide_name': 0, + 'type': r'$slice', + 'parameters': {'OFFSET': 8, 'A_WIDTH': 32, 'Y_WIDTH': 4}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': sourceBits, + 'Y': [1, 2, 3, 4], + }, + }, + 'slice1': { + 'hide_name': 0, + 'type': r'$slice', + 'parameters': {'OFFSET': 12, 'A_WIDTH': 32, 'Y_WIDTH': 4}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': sourceBits, + 'Y': [5, 6, 7, 8], + }, + }, + 'concat': { + 'hide_name': 0, + 'type': r'$concat', + 'parameters': {'IN0_WIDTH': 4, 'IN1_WIDTH': 4}, + 'attributes': {}, + 'port_directions': { + '[3:0]': 'input', + '[7:4]': 'input', + 'Y': 'output', + }, + 'connections': { + '[3:0]': [1, 2, 3, 4], + '[7:4]': [5, 6, 7, 8], + 'Y': [9, 10, 11, 12, 13, 14, 15, 16], + }, + }, + }, + }, + }; + + NetlistPasses.collapseConcatOfAdjacentSlices(modules); + + final topModule = modules['top']!; + final cells = topModule['cells']! as Map>; + final concat = cells['concat']!; + final concatConnections = concat['connections']! as Map; + expect(cells, isNot(contains('slice0'))); + expect(cells, isNot(contains('slice1'))); + expect(concat['type'], equals(r'$slice')); + expect( + concat['parameters'], + equals({'OFFSET': 8, 'A_WIDTH': 32, 'Y_WIDTH': 8}), + ); + expect(concatConnections['A'], equals(sourceBits)); + expect(concatConnections['Y'], equals([9, 10, 11, 12, 13, 14, 15, 16])); + }); + test('all wire IDs are >= 2 (0 and 1 reserved for constants)', () async { final json = await _synthToMap( AddModule(Logic(width: 8), Logic(width: 8)), @@ -1441,6 +1889,49 @@ void main() { ); expect(_modules(json), isNotEmpty); }); + + test('validation warns on multiple drivers', () { + final warnings = []; + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + 'y': { + 'direction': 'output', + 'bits': [2], + }, + }; + final cells = { + 'driver': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [1], + }, + }, + }; + + runZoned( + () => NetlistValidation.validate(ports, cells, 'ShortedModule'), + zoneSpecification: ZoneSpecification( + print: (_, __, ___, line) => warnings.add(line), + ), + ); + + expect(warnings, contains(contains('wire bit 1 has multiple drivers'))); + expect( + () => NetlistValidation.validate( + ports, + cells, + 'ShortedModule', + throwOnMultipleDrivers: true, + printWarnings: false, + ), + throwsStateError, + ); + }); }); // ── Group 11: Named constant signals ───────────────────────────── diff --git a/test/netlist_test.dart b/test/netlist_test.dart index 64f893973..41e150758 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -2,9 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause // // netlist_test.dart -// Tests for the netlist synthesizer: JSON structure, SynthBuilder, -// NetlistSynthesisResult, collectModuleEntries, NetlistOptions, -// and example-based smoke tests. +// Tests for the netlist synthesizer public surface. // // 2026 March 31 // Author: Desmond Kirkpatrick @@ -293,8 +291,10 @@ void main() { ); test('requested submodule can be synthesized as top', () async { - final wrapper = - _CompositeWrapperModule(Logic(name: 'a'), Logic(name: 'b')); + final wrapper = _CompositeWrapperModule( + Logic(name: 'a'), + Logic(name: 'b'), + ); await wrapper.build(); final submodule = wrapper.child; From cbda4ebddf03d0de7e132fd56908fee689a29133 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 17 Jul 2026 11:16:46 -0700 Subject: [PATCH 59/77] conflict resolved, sv_gen test fix --- dart_test.yaml | 3 +++ .../systemverilog/systemverilog_synthesizer.dart | 13 ++++--------- .../utilities/synth_module_definition.dart | 13 +++++++++++-- 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 dart_test.yaml diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 000000000..f150a85f7 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,3 @@ +tags: + benchmark: + timeout: 2x \ No newline at end of file diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index 5253a4b23..062647ac3 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -15,17 +15,12 @@ import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synthesis_resu /// /// Attempts to maintain signal naming and structure as much as possible. class SystemVerilogSynthesizer extends Synthesizer { - /// The hierarchy stopping policy used by this synthesizer. - final SynthModuleStopPolicy moduleStopPolicy; - - /// Creates a [SystemVerilogSynthesizer]. - SystemVerilogSynthesizer({SynthModuleStopPolicy? moduleStopPolicy}) - : moduleStopPolicy = - moduleStopPolicy ?? SynthModuleStopPolicy.systemVerilog(); - @override bool generatesDefinition(Module module) => - moduleStopPolicy.generatesDefinition(module); + // ignore: deprecated_member_use_from_same_package + !((module is CustomSystemVerilog) || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none)); /// Creates a line of SystemVerilog that instantiates [module]. /// diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index e3b7f5629..23a36e8ed 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -16,6 +16,12 @@ import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; +/// Adapts structure slicing to the shared synthesis operation. +class _BusSubsetForStructSlice extends SynthStructureSlice { + _BusSubsetForStructSlice(super.bus, super.startIndex, super.endIndex, + {required super.destination}); +} + /// Represents the definition of a module. @internal class SynthModuleDefinition { @@ -259,7 +265,7 @@ class SynthModuleDefinition { internalSignals.add(leafSynth); // this is DISCONNECTED, just a module used for synthesizing - final subsetMod = SynthStructureSlice( + final subsetMod = _BusSubsetForStructSlice( (port.isNet ? LogicNet.new : Logic.new)( width: port.width, name: 'DUMMY', @@ -611,7 +617,10 @@ class SynthModuleDefinition { // away. This is especially important for array elements, whose assignments // are not collapsed away like mergeable signals. final assignmentReferencedSignals = { - for (final assignment in assignments) ...[assignment.src, assignment.dst], + for (final assignment in assignments) ...[ + assignment.src, + assignment.dst, + ], }; final inlineableResultLogics = {}; From 57fc237767bb0f5b6bb423aff71f3198bf27b47c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 17 Jul 2026 11:39:13 -0700 Subject: [PATCH 60/77] inout fix --- .../netlist/netlist_validation.dart | 2 +- test/netlist_synthesizer_test.dart | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index 918891207..d50fd1175 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -168,7 +168,7 @@ class NetlistValidation { for (final entry in ports.entries) { final direction = entry.value['direction'] as String?; - if (direction != 'input' && direction != 'inout') { + if (direction != 'input') { continue; } for (final bit in (entry.value['bits'] as List?) ?? const []) { diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 6f7ede4fb..61a18673d 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -1932,6 +1932,46 @@ void main() { throwsStateError, ); }); + + test('validation allows cells to drive inout ports', () { + final warnings = []; + final ports = { + 'bus': { + 'direction': 'inout', + 'bits': [1], + }, + }; + final cells = { + 'driver': { + 'type': r'$tribuf', + 'port_directions': {'A': 'input', 'EN': 'input', 'Y': 'output'}, + 'connections': { + 'A': [2], + 'EN': [3], + 'Y': [1], + }, + }, + }; + + runZoned( + () => NetlistValidation.validate(ports, cells, 'InOutModule'), + zoneSpecification: ZoneSpecification( + print: (_, __, ___, line) => warnings.add(line), + ), + ); + + expect(warnings, isEmpty); + expect( + () => NetlistValidation.validate( + ports, + cells, + 'InOutModule', + throwOnMultipleDrivers: true, + printWarnings: false, + ), + returnsNormally, + ); + }); }); // ── Group 11: Named constant signals ───────────────────────────── From 5a8a25fee0ca2ba63be06aeed728299574e4c25f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 17 Jul 2026 12:43:16 -0700 Subject: [PATCH 61/77] attempt with better VS links --- doc/user_guide/_get-started/03-development-recommendations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user_guide/_get-started/03-development-recommendations.md b/doc/user_guide/_get-started/03-development-recommendations.md index d224e54df..6ffb5ab7b 100644 --- a/doc/user_guide/_get-started/03-development-recommendations.md +++ b/doc/user_guide/_get-started/03-development-recommendations.md @@ -10,9 +10,9 @@ toc: true - The [ROHD Cosimulation](https://github.com/intel/rohd-cosim) package allows you to cosimulate the ROHD simulator with a variety of SystemVerilog simulators. - The [ROHD Hardware Component Library](https://github.com/intel/rohd-vf) provides a set of reusable and configurable components for design and verification. - Visual Studio Code (vscode) is a great, free IDE with excellent support for Dart. It works well on all platforms, including native Windows or Windows Subsystem for Linux (WSL) which allows you to run a native Linux kernel (e.g. Ubuntu) within Windows. You can also use vscode to develop on a remote machine with the Remote SSH extension. - - vscode: + - vscode: - WSL: - - Remote SSH: + - Remote SSH: - Dart extension for vscode: Head over to the [user guide]({{ site.baseurl }}{% link _docs/A01-sample-example.md %}) to learn more about how to use ROHD. From 8ac1936d0aef110e7acfb954238d87c7272165c0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 17 Jul 2026 13:36:17 -0700 Subject: [PATCH 62/77] bump copyright --- lib/src/synthesizers/utilities/utilities.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index 5b44a8c2a..55aa62720 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'synth_array_concat.dart'; From 2001f470d4b4e1f8a1eb2835a51a058f4655eaf0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 24 Jul 2026 07:53:14 -0700 Subject: [PATCH 63/77] major cleanup from PR comments -- complete at this point --- lib/src/module.dart | 4 +- .../synthesizers/netlist/netlist_options.dart | 5 ++ .../synthesizers/netlist/netlist_passes.dart | 86 +++--------------- .../netlist/netlist_synthesis_result.dart | 1 + .../netlist/netlist_synthesizer.dart | 5 +- .../synthesizers/netlist/netlist_utils.dart | 7 +- .../netlist/netlist_validation.dart | 51 ++++++----- lib/src/synthesizers/synth_builder.dart | 7 ++ lib/src/synthesizers/synthesis_result.dart | 9 +- .../utilities/synth_array_concat.dart | 1 - .../utilities/synth_array_slice.dart | 1 - .../synthesizers/utilities/synth_logic.dart | 14 ++- .../utilities/synth_module_stop_policy.dart | 1 - .../utilities/synth_structure_concat.dart | 1 - .../utilities/synth_structure_layout.dart | 1 - .../utilities/synth_structure_slice.dart | 1 - test/netlist_synthesizer_test.dart | 64 +++++++++++++ test/netlist_test.dart | 89 +++++++++++++++++++ test/synth_builder_test.dart | 47 ++++++++++ test/synth_structure_layout_test.dart | 1 - 20 files changed, 276 insertions(+), 120 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index b79d90485..d2d172f27 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1156,7 +1156,9 @@ abstract class Module { .join('\n\n////////////////////\n\n'); } - /// Returns a synthesized netlist JSON representation of this [Module]. + /// Testing-only convenience for a synthesized netlist JSON representation of + /// this [Module]. Use [NetlistSynthesizer] directly outside tests. + @visibleForTesting String generateNetlist( {NetlistOptions options = const NetlistOptions(), String? packageRoot}) { if (!_hasBuilt) { diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_options.dart index b7317ba33..8c64d0426 100644 --- a/lib/src/synthesizers/netlist/netlist_options.dart +++ b/lib/src/synthesizers/netlist/netlist_options.dart @@ -77,6 +77,10 @@ class NetlistOptions { /// are entirely unconsumed. final bool enableDCE; + /// When `true`, validation reports debug warnings for cells whose output + /// bits are not consumed. Multiple-driver validation always runs. + final bool validateUnconnectedOutputs; + /// When `true`, the synthesizer produces "slim" output: cell connection maps /// are not copied into the emitted JSON projection. Netnames and ports are /// still emitted with full wire-ID fidelity, while per-module synthesis @@ -107,6 +111,7 @@ class NetlistOptions { this.netlistCellMapper, this.collapseTransparentClusters = false, this.enableDCE = true, + this.validateUnconnectedOutputs = true, this.slimMode = false, this.compressBitRanges = false, this.compactJson = false, diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart index ae220199d..ff0af154a 100644 --- a/lib/src/synthesizers/netlist/netlist_passes.dart +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -96,8 +96,9 @@ class NetlistPasses { // Unified transparent-cell clustering // ════════════════════════════════════════════════════════════════════ - /// Transparent cell types that only reshuffle / rename bits. - static const _transparentTypes = { + /// Transparent cell types that only reshuffle / rename bits and can be + /// cleaned up when their outputs are unconsumed. + static const _transparentCleanupTypes = { r'$buf', r'$slice', r'$concat', @@ -105,6 +106,12 @@ class NetlistPasses { r'$struct_pack', }; + /// Transparent cell types whose bit mappings can be safely clustered. + static const _clusterableTransparentTypes = { + r'$buf', + r'$slice', + }; + /// Unified transparent-cell clustering pass. /// /// **Phase 1 — Cluster identification:** @@ -133,7 +140,10 @@ class NetlistPasses { final tCells = { for (final e in cells.entries) - if (_transparentTypes.contains(e.value['type'] as String?)) e.key, + if (_clusterableTransparentTypes.contains( + e.value['type'] as String?, + )) + e.key, }; if (tCells.isEmpty) { continue; @@ -231,16 +241,6 @@ class NetlistPasses { final cellsToAdd = >{}; for (final comp in components) { - if (comp.any( - (cn) => const { - r'$concat', - r'$struct_pack', - r'$struct_unpack', - }.contains(cells[cn]!['type']), - )) { - continue; - } - // Build output-bit → input-bit map for the whole cluster. final bitMap = {}; for (final cn in comp) { @@ -355,7 +355,7 @@ class NetlistPasses { } cells.removeWhere((_, cell) { - if (!_transparentTypes.contains(cell['type'] as String?)) { + if (!_transparentCleanupTypes.contains(cell['type'] as String?)) { return false; } final dirs = cell['port_directions'] as Map? ?? {}; @@ -694,7 +694,6 @@ class NetlistPasses { /// for a single transparent cell. static void _mapCellBits(Map cell, Map bitMap) { final type = cell['type']! as String; - final dirs = cell['port_directions'] as Map? ?? {}; final conns = cell['connections'] as Map? ?? {}; final params = cell['parameters'] as Map? ?? {}; @@ -711,63 +710,6 @@ class NetlistPasses { bitMap[y[i] as int] = a[off + i] as Object; } } - - case r'$concat': - final y = conns['Y'] as List; - // Input ports are in connection-map order; their bits - // concatenate to form Y (first port at LSB). - final inBits = [ - for (final pe in conns.entries) - if ((dirs[pe.key] as String?) != 'output') - ...(pe.value as List).cast(), - ]; - _mapPairwise(inBits, y, bitMap); - - case r'$struct_unpack': - final a = conns['A'] as List; - final fc = params['FIELD_COUNT'] as int? ?? 0; - for (var f = 0; f < fc; f++) { - final fn = params['FIELD_${f}_NAME'] as String?; - final fo = params['FIELD_${f}_OFFSET'] as int? ?? 0; - if (fn == null) { - continue; - } - final fb = conns[fn] as List?; - if (fb == null) { - continue; - } - for (var i = 0; i < fb.length; i++) { - if (fb[i] is int && (fo + i) < a.length) { - bitMap[fb[i] as int] = a[fo + i] as Object; - } - } - } - - case r'$struct_pack': - final y = conns['Y'] as List; - final fc = params['FIELD_COUNT'] as int? ?? 0; - final src = List.filled(y.length, null); - for (var f = 0; f < fc; f++) { - final fn = params['FIELD_${f}_NAME'] as String?; - final fo = params['FIELD_${f}_OFFSET'] as int? ?? 0; - if (fn == null) { - continue; - } - final fb = conns[fn] as List?; - if (fb == null) { - continue; - } - for (var i = 0; i < fb.length; i++) { - if ((fo + i) < src.length) { - src[fo + i] = fb[i]; - } - } - } - for (var i = 0; i < y.length; i++) { - if (y[i] is int && src[i] != null) { - bitMap[y[i] as int] = src[i]!; - } - } } } diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart index ddf6855c0..49c424608 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -39,6 +39,7 @@ class NetlistSynthesisResult extends SynthesisResult { required this.cells, required this.netnames, this.attributes = const {}, + super.warnings, }); /// Builds the JSON representation for this single module entry. diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 05ce7ad79..152948e57 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -891,12 +891,14 @@ class NetlistSynthesizer extends Synthesizer { // -- Structural validation ------------------------------------------- // Always catch netlist shorts, even when assertions are disabled. - NetlistValidation.validate( + final warnings = NetlistValidation.validate( ports, cells, module.name, netnames: netnames, throwOnMultipleDrivers: true, + printWarnings: false, + checkUnconnectedOutputs: options.validateUnconnectedOutputs, ); return NetlistSynthesisResult( @@ -906,6 +908,7 @@ class NetlistSynthesizer extends Synthesizer { cells: cells, netnames: netnames, attributes: attr, + warnings: warnings, ); } diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 4c7244c55..ffcd08e40 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -13,12 +13,9 @@ import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// Shared utility functions for netlist synthesis and post-processing passes. /// -/// All methods are static — no instances are created. +/// All methods are static. @internal -class NetlistUtils { - /// Prevents construction of this static utility class. - NetlistUtils._(); - +abstract class NetlistUtils { /// Find the port name in [portMap] that corresponds to [sl]. static String? portNameForSynthLogic( SynthLogic sl, diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index d50fd1175..85809678c 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -47,6 +47,7 @@ class NetlistValidation { Map? netnames, bool throwOnMultipleDrivers = false, bool printWarnings = true, + bool checkUnconnectedOutputs = true, }) { final warnings = []; final multipleDriverWarnings = []; @@ -124,32 +125,34 @@ class NetlistValidation { ); } - for (final entry in cells.entries) { - final cell = entry.value; - final type = cell['type'] as String? ?? ''; - if (transparentTypes.contains(type) || type == r'$const') { - continue; - } - final outputBits = _cellBits(cell, 'output'); - if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - report( - '[netlist-validate] WARNING: $moduleName: ' - 'cell "${entry.key}" (type: $type) has no consumed outputs ' - '— fully disconnected logic gate', - ); + if (checkUnconnectedOutputs) { + for (final entry in cells.entries) { + final cell = entry.value; + final type = cell['type'] as String? ?? ''; + if (transparentTypes.contains(type) || type == r'$const') { + continue; + } + final outputBits = _cellBits(cell, 'output'); + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + report( + '[netlist-validate] WARNING: $moduleName: ' + 'cell "${entry.key}" (type: $type) has no consumed outputs ' + '— fully disconnected logic gate', + ); + } } - } - for (final entry in cells.entries.where( - (entry) => entry.value['type'] == r'$const', - )) { - final outputBits = _cellBits(entry.value, 'output'); - if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - report( - '[netlist-validate] WARNING: $moduleName: ' - r'$const cell "${entry.key}" drives wires consumed by nothing ' - '— floating constant', - ); + for (final entry in cells.entries.where( + (entry) => entry.value['type'] == r'$const', + )) { + final outputBits = _cellBits(entry.value, 'output'); + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + report( + '[netlist-validate] WARNING: $moduleName: ' + r'$const cell "${entry.key}" drives wires consumed by nothing ' + '— floating constant', + ); + } } } diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index f9d0a0d08..bb05d4abc 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -38,6 +38,13 @@ class SynthBuilder { Set get synthesisResults => UnmodifiableSetView(_synthesisResults); + /// Non-fatal warnings reported by generated [SynthesisResult]s. + List get warnings => UnmodifiableListView( + _synthesisResults + .expand((synthesisResult) => synthesisResult.warnings) + .toList(growable: false), + ); + /// [Uniquifier] for instance type names. final Uniquifier _instanceTypeUniquifier = Uniquifier(); diff --git a/lib/src/synthesizers/synthesis_result.dart b/lib/src/synthesizers/synthesis_result.dart index 27abb8fe9..4154832a2 100644 --- a/lib/src/synthesizers/synthesis_result.dart +++ b/lib/src/synthesizers/synthesis_result.dart @@ -23,9 +23,16 @@ abstract class SynthesisResult { /// The name of the definition type for this module instance. String get instanceTypeName => getInstanceTypeOfModule(module); + /// Non-fatal warnings reported while producing this synthesis result. + final List warnings; + /// Represents a constant computed synthesis result for [module] given /// the provided type mapping in [getInstanceTypeOfModule]. - const SynthesisResult(this.module, this.getInstanceTypeOfModule); + const SynthesisResult( + this.module, + this.getInstanceTypeOfModule, { + this.warnings = const [], + }); /// Whether two implementations are identical or not /// diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index eaf882ed1..e0c64aa71 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // synth_array_concat.dart // Shared array concatenation helper for synthesis backends. diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart index 08568beac..b1b13e5ce 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // synth_array_slice.dart // Shared array slice helper for synthesis backends. diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 349c946f2..19a1ad3d9 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -77,6 +77,11 @@ class SynthLogic { /// Indicates that this has a reserved name. bool get isReserved => _reservedLogic != null; + /// Whether this contains a renameable or reserved name that must remain in + /// generated output. + bool get hasPreservedName => + _reservedLogic != null || _renameableLogic != null; + /// The [Logic] whose name is reserved, if there is one. Logic? _reservedLogic; @@ -131,15 +136,6 @@ class SynthLogic { /// may be additional conditions that prevent clearing. bool get isClearable => mergeable; - /// Like [isClearable], but additionally permits a *renameable* signal (whose - /// chosen name carries no semantic meaning and would simply be dropped). - /// - /// This is `false` only for [isReserved] (e.g. ports) and constant signals, - /// whose identities must be preserved. It is used by the aggregate-collapse - /// optimizations to eliminate intermediate (possibly named) nets. - bool get isClearableOrRenameable => - _reservedLogic == null && _constLogic == null; - /// The source connections to any [Logic] in this [SynthLogic] which are not /// also contained within this [SynthLogic]. Iterable get srcConnections { diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart index bed6da42c..08fb7799f 100644 --- a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // synth_module_stop_policy.dart // Shared module hierarchy stopping policy for synthesis backends. diff --git a/lib/src/synthesizers/utilities/synth_structure_concat.dart b/lib/src/synthesizers/utilities/synth_structure_concat.dart index 870a4543d..d78d8721c 100644 --- a/lib/src/synthesizers/utilities/synth_structure_concat.dart +++ b/lib/src/synthesizers/utilities/synth_structure_concat.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // synth_structure_concat.dart // Shared structure concatenation helper for synthesis backends. diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart index a580a8b2e..c3715cd82 100644 --- a/lib/src/synthesizers/utilities/synth_structure_layout.dart +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // synth_structure_layout.dart // Shared packed LogicStructure layout utility for synthesis backends. diff --git a/lib/src/synthesizers/utilities/synth_structure_slice.dart b/lib/src/synthesizers/utilities/synth_structure_slice.dart index bfb085c20..d33b6f80f 100644 --- a/lib/src/synthesizers/utilities/synth_structure_slice.dart +++ b/lib/src/synthesizers/utilities/synth_structure_slice.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // synth_structure_slice.dart // Shared structure slice helper for synthesis backends. diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 61a18673d..7453d0cfe 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -1921,6 +1921,25 @@ void main() { ); expect(warnings, contains(contains('wire bit 1 has multiple drivers'))); + warnings.clear(); + + final capturedWarnings = runZoned( + () => NetlistValidation.validate( + ports, + cells, + 'ShortedModule', + printWarnings: false, + ), + zoneSpecification: ZoneSpecification( + print: (_, __, ___, line) => warnings.add(line), + ), + ); + + expect( + capturedWarnings, + contains(contains('wire bit 1 has multiple drivers')), + ); + expect(warnings, isEmpty); expect( () => NetlistValidation.validate( ports, @@ -1933,6 +1952,51 @@ void main() { ); }); + test('validation can skip unconnected output warnings', () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + 'b': { + 'direction': 'input', + 'bits': [2], + }, + }; + final cells = { + 'unusedAnd': { + 'type': r'$and', + 'port_directions': {'A': 'input', 'B': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'B': [2], + 'Y': [3], + }, + }, + }; + + expect( + NetlistValidation.validate( + ports, + cells, + 'UnusedOutputModule', + printWarnings: false, + ), + contains(contains('unusedAnd')), + ); + + expect( + NetlistValidation.validate( + ports, + cells, + 'UnusedOutputModule', + printWarnings: false, + checkUnconnectedOutputs: false, + ), + isEmpty, + ); + }); + test('validation allows cells to drive inout ports', () { final warnings = []; final ports = { diff --git a/test/netlist_test.dart b/test/netlist_test.dart index 41e150758..a3b00b434 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -75,6 +75,34 @@ class _AdderModule extends Module { } } +/// Example for the netlist-only adjacent-slice/concat collapse. +class _AdjacentSliceConcatExample extends Module { + Logic get out => output('out'); + + _AdjacentSliceConcatExample(Logic data) + : super(definitionName: 'AdjacentSliceConcatExample') { + data = addInput('data', data, width: 8); + + final low = data.getRange(0, 4).named('low'); + final high = data.getRange(4, 8).named('high'); + addOutput('out', width: 8) <= [high, low].swizzle(); + } +} + +/// Example for the netlist-only transparent slice/buf cluster collapse. +class _SliceAliasClusterExample extends Module { + Logic get out => output('out'); + + _SliceAliasClusterExample(Logic data) + : super(definitionName: 'SliceAliasClusterExample') { + data = addInput('data', data, width: 8); + + final low = data.getRange(0, 4).named('low'); + final alias = Swizzle([low]).out.named('alias'); + addOutput('out', width: 4) <= alias; + } +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -131,6 +159,20 @@ FilterBank _buildFilterBank({ ); } +Map _topModuleFromJson(Module module, String json) { + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + return modules[module.definitionName] as Map; +} + +int _cellCount(Map module, String cellType) { + final cells = module['cells'] as Map? ?? {}; + return cells.values.where((cell) { + final cellMap = cell as Map; + return cellMap['type'] == cellType; + }).length; +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -535,6 +577,53 @@ void main() { }); }); + // ── Netlist-only transparent optimizations ─────────────────────────── + + group('Netlist-only transparent optimizations', () { + test('adjacent slices feeding concat collapse into one wider slice', + () async { + final mod = _AdjacentSliceConcatExample(Logic(name: 'data', width: 8)); + await mod.build(); + + final rawTop = _topModuleFromJson( + mod, + NetlistSynthesizer().synthesizeToJson(mod), + ); + expect(_cellCount(rawTop, r'$slice'), equals(2)); + expect(_cellCount(rawTop, r'$concat'), equals(1)); + + final collapsedTop = _topModuleFromJson( + mod, + NetlistSynthesizer( + options: const NetlistOptions(collapseTransparentClusters: true), + ).synthesizeToJson(mod), + ); + expect(_cellCount(collapsedTop, r'$slice'), equals(1)); + expect(_cellCount(collapsedTop, r'$concat'), equals(0)); + }); + + test('slice feeding alias buffer collapses into one buffer', () async { + final mod = _SliceAliasClusterExample(Logic(name: 'data', width: 8)); + await mod.build(); + + final rawTop = _topModuleFromJson( + mod, + NetlistSynthesizer().synthesizeToJson(mod), + ); + expect(_cellCount(rawTop, r'$slice'), equals(1)); + expect(_cellCount(rawTop, r'$buf'), equals(1)); + + final collapsedTop = _topModuleFromJson( + mod, + NetlistSynthesizer( + options: const NetlistOptions(collapseTransparentClusters: true), + ).synthesizeToJson(mod), + ); + expect(_cellCount(collapsedTop, r'$slice'), equals(0)); + expect(_cellCount(collapsedTop, r'$buf'), equals(1)); + }); + }); + // ── FilterBank (multi-channel, dedup, loopback) ─────────────────────── group('FilterBank netlist', () { diff --git a/test/synth_builder_test.dart b/test/synth_builder_test.dart index 3cda6989b..9b1831621 100644 --- a/test/synth_builder_test.dart +++ b/test/synth_builder_test.dart @@ -48,6 +48,44 @@ class BModule extends Module { } } +class _WarningSynthesizer extends Synthesizer { + @override + bool generatesDefinition(Module module) => true; + + @override + SynthesisResult synthesize( + Module module, + String Function(Module module) getInstanceTypeOfModule, + ) => + _WarningSynthesisResult( + module, + getInstanceTypeOfModule, + warnings: ['warning for ${module.name}'], + ); +} + +class _WarningSynthesisResult extends SynthesisResult { + const _WarningSynthesisResult( + super.module, + super.getInstanceTypeOfModule, { + super.warnings, + }); + + @override + int get matchHashCode => module.definitionName.hashCode; + + @override + bool matchesImplementation(SynthesisResult other) => + other is _WarningSynthesisResult && + other.module.definitionName == module.definitionName; + + @override + String toFileContents() => ''; + + @override + List toSynthFileContents() => const []; +} + void main() { tearDown(() async { await Simulator.reset(); @@ -103,5 +141,14 @@ void main() { expect(synthResults.where((e) => e.module is AModule).length, 2); expect(synthResults.where((e) => e.module is BModule).length, 1); }); + + test('collects warnings from synthesis results', () async { + final mod = AModule(Logic(width: 4)); + await mod.build(); + + final synthBuilder = SynthBuilder(mod, _WarningSynthesizer()); + + expect(synthBuilder.warnings, ['warning for amodule']); + }); }); } diff --git a/test/synth_structure_layout_test.dart b/test/synth_structure_layout_test.dart index 6b0dcaeb7..d25714c92 100644 --- a/test/synth_structure_layout_test.dart +++ b/test/synth_structure_layout_test.dart @@ -1,6 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause - // // synth_structure_layout_test.dart // Tests for packed LogicStructure layout synthesis utilities. From d43f406b21ea3ed149459da1a9a3d4c8d22b3aed Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 30 Jul 2026 13:12:57 -0700 Subject: [PATCH 64/77] much better testing of netlist passes --- .devcontainer/devcontainer.json | 2 +- .github/configs/mlc_config.json | 6 + .github/workflows/build_devtool.yml | 5 +- .github/workflows/general.yml | 5 +- CHANGELOG.md | 11 + CONTRIBUTING.md | 2 +- LICENSE | 2 +- analysis_options.yaml | 5 +- devtools_options.yaml | 4 + doc/user_guide/_docs/A03-constant.md | 21 +- doc/user_guide/_docs/A21-generation.md | 25 + .../03-development-recommendations.md | 4 +- lib/src/module.dart | 10 +- lib/src/modules/bus.dart | 135 +- lib/src/modules/gates.dart | 17 +- lib/src/signals/const.dart | 209 +- lib/src/signals/logic.dart | 68 +- lib/src/signals/wire.dart | 41 + .../synthesizers/netlist/netlist_options.dart | 3 +- .../netlist/netlist_synthesizer.dart | 302 +- lib/src/synthesizers/synthesizers.dart | 2 +- .../systemverilog/systemverilog.dart | 3 +- ...systemverilog_synth_module_definition.dart | 1318 ++++- .../systemverilog_synthesis_result.dart | 58 +- .../systemverilog_synthesizer.dart | 14 +- ...stemverilog_synthesizer_configuration.dart | 58 + .../utilities/synth_assignment.dart | 29 + .../synthesizers/utilities/synth_logic.dart | 68 +- .../utilities/synth_module_definition.dart | 2317 ++++++++- lib/src/synthesizers/utilities/utilities.dart | 3 +- lib/src/utilities/namer.dart | 6 + lib/src/utilities/simcompare.dart | 13 +- lib/src/values/logic_value.dart | 22 +- packages/rohd_hierarchy/LICENSE | 28 + packages/rohd_hierarchy/README.md | 246 + packages/rohd_hierarchy/analysis_options.yaml | 1 + .../rohd_hierarchy/lib/rohd_hierarchy.dart | 50 + .../lib/src/base_hierarchy_adapter.dart | 80 + .../lib/src/hierarchy_constants.dart | 14 + .../lib/src/hierarchy_models.dart | 16 + .../lib/src/hierarchy_occurrence.dart | 258 + .../lib/src/hierarchy_query.dart | 152 + .../lib/src/hierarchy_search_controller.dart | 216 + .../lib/src/hierarchy_search_result.dart | 63 + .../lib/src/hierarchy_service.dart | 875 ++++ .../lib/src/netlist_hierarchy_adapter.dart | 224 + .../lib/src/occurrence_address.dart | 144 + .../lib/src/occurrence_search_result.dart | 45 + .../rohd_hierarchy/lib/src/prefix_query.dart | 61 + .../rohd_hierarchy/lib/src/regex_query.dart | 177 + .../lib/src/signal_occurrence.dart | 274 + .../lib/src/signal_search_result.dart | 49 + packages/rohd_hierarchy/pubspec.yaml | 18 + .../test/adapter_search_parity_test.dart | 285 + .../test/address_conversion_test.dart | 286 ++ .../test/devtools_search_flow_test.dart | 214 + .../test/filter_bank_integration_test.dart | 719 +++ .../test/fixtures/filter_bank.json | 1183 +++++ .../hierarchy_path_vs_signal_id_test.dart | 194 + .../test/hierarchy_query_test.dart | 655 +++ .../hierarchy_search_controller_test.dart | 505 ++ .../test/module_search_test.dart | 325 ++ .../test/occurrence_address_test.dart | 335 ++ .../test/regex_search_test.dart | 546 ++ .../test/rohd_signal_resolve_test.dart | 72 + .../test/signal_search_result_test.dart | 236 + rohd_devtools_extension/.vscode/launch.json | 11 + rohd_devtools_extension/.vscode/tasks.json | 137 + rohd_devtools_extension/LICENSE | 28 + rohd_devtools_extension/Makefile | 23 + rohd_devtools_extension/README.md | 88 +- rohd_devtools_extension/analysis_options.yaml | 270 +- .../assets/help/details_help.md | 28 + .../assets/help/devtools_help.md | 34 + .../assets/icons/rohd_logo.png | Bin 0 -> 5225 bytes rohd_devtools_extension/lib/main.dart | 108 +- .../lib/main_standalone.dart | 52 + .../lib/rohd_devtools/const/app_theme.dart | 193 + .../lib/rohd_devtools/cubit/cubits.dart | 13 + .../cubit/details_tab_cubit.dart | 31 + .../cubit/rohd_service_cubit.dart | 221 +- .../cubit/rohd_service_state.dart | 14 +- .../cubit/selected_module_cubit.dart | 7 +- .../cubit/selected_module_state.dart | 8 +- .../cubit/signal_search_term_cubit.dart | 5 +- .../rohd_devtools/cubit/snapshot_cubit.dart | 233 + .../lib/rohd_devtools/cubit/theme_cubit.dart | 39 + .../cubit/tree_search_term_cubit.dart | 5 +- .../models/dtd_vm_service_info.dart | 74 + .../rohd_devtools/models/signal_model.dart | 43 +- .../lib/rohd_devtools/models/tree_model.dart | 82 +- .../lib/rohd_devtools/rohd_devtools.dart | 5 +- .../services/connection_state_machine.dart | 618 +++ .../services/io_vm_connection_strategy.dart | 145 + .../platform_vm_connection_strategy.dart | 21 + .../platform_vm_connection_strategy_stub.dart | 22 + .../services/service_manager_bridge.dart | 11 + .../services/service_manager_bridge_io.dart | 15 + .../services/service_manager_bridge_web.dart | 11 + .../lib/rohd_devtools/services/services.dart | 16 + .../services/signal_service.dart | 8 +- .../services/signal_value_source.dart | 42 + .../services/signal_value_source_binding.dart | 30 + .../rohd_devtools/services/tree_service.dart | 86 +- .../vm_service_signal_value_source.dart | 382 ++ .../services/web_vm_connection_strategy.dart | 171 + .../rohd_devtools/ui/details_help_button.dart | 40 + .../lib/rohd_devtools/ui/devtool_appbar.dart | 90 +- .../ui/devtools_connection_host.dart | 1568 ++++++ .../ui/devtools_help_button.dart | 41 + .../rohd_devtools/ui/module_tree_card.dart | 140 +- .../ui/module_tree_details_navbar.dart | 147 +- .../lib/rohd_devtools/ui/platform_icon.dart | 118 + .../lib/rohd_devtools/ui/schematic_icon.dart | 125 + .../rohd_devtools/ui/signal_details_card.dart | 218 +- .../lib/rohd_devtools/ui/signal_table.dart | 203 +- .../ui/signal_table_text_field.dart | 122 +- .../ui/simulation_time_display.dart | 30 + .../ui/standalone_app_shell.dart | 380 ++ .../lib/rohd_devtools/ui/ui.dart | 21 + .../rohd_devtools/ui/vm_connection_form.dart | 729 +++ .../view/rohd_devtools_page.dart | 66 +- .../view/tree_structure_page.dart | 391 +- .../lib/rohd_devtools_observer.dart | 5 +- rohd_devtools_extension/linux/.gitignore | 1 + rohd_devtools_extension/linux/CMakeLists.txt | 138 + .../linux/flutter/CMakeLists.txt | 88 + .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 + .../linux/runner/CMakeLists.txt | 26 + rohd_devtools_extension/linux/runner/main.cc | 6 + .../linux/runner/my_application.cc | 144 + .../linux/runner/my_application.h | 18 + .../packages/rohd_devtools_widgets/LICENSE | 28 + .../packages/rohd_devtools_widgets/README.md | 13 + .../analysis_options.yaml | 1 + .../lib/rohd_devtools_widgets.dart | 41 + .../lib/src/app_bar_overlay.dart | 168 + .../lib/src/bit_expansion_menu.dart | 139 + .../lib/src/bit_field_utils.dart | 257 + .../lib/src/capture_boundary.dart | 83 + .../lib/src/cross_probe_button.dart | 48 + .../lib/src/cross_probe_menu.dart | 253 + .../lib/src/cross_probe_service.dart | 157 + .../lib/src/export_button.dart | 53 + .../lib/src/export_toast.dart | 48 + .../lib/src/logic_type_utils.dart | 408 ++ .../lib/src/markdown_help_button.dart | 486 ++ .../lib/src/rohd_extension_client.dart | 133 + .../lib/src/rohd_extension_status.dart | 223 + .../lib/src/save_png_native.dart | 20 + .../lib/src/save_png_stub.dart | 14 + .../lib/src/save_png_web.dart | 32 + .../rohd_devtools_widgets/pubspec.yaml | 18 + .../test/logic_type_utils_test.dart | 40 + rohd_devtools_extension/pubspec.yaml | 31 +- .../fixtures/tree_model.stub.dart | 98 +- .../tree_structure/model_tree_card_test.dart | 41 +- .../tree_structure_page_test.dart | 96 +- .../tool/test_devtools_install.dart | 231 + rohd_devtools_extension/web/favicon.png | Bin 917 -> 952 bytes .../web/icons/Icon-192.png | Bin 5292 -> 4911 bytes .../web/icons/Icon-512.png | Bin 8252 -> 50043 bytes .../web/icons/Icon-maskable-192.png | Bin 5594 -> 4911 bytes .../web/icons/Icon-maskable-512.png | Bin 20998 -> 50043 bytes rohd_devtools_extension/web/index.html | 7 - rohd_devtools_extension/web/manifest.json | 35 - rohd_extension/.gitignore | 3 + rohd_extension/.markdownlint.json | 4 + rohd_extension/.nvmrc | 1 + rohd_extension/.vscodeignore | 12 + rohd_extension/LICENSE | 28 + rohd_extension/Makefile | 107 + rohd_extension/README.md | 356 ++ rohd_extension/dart/LICENSE | 28 + rohd_extension/dart/lib/dtd_service.dart | 147 + rohd_extension/dart/lib/flc_data.dart | 429 ++ .../dart/lib/rohd_source_navigator.dart | 11 + rohd_extension/dart/lib/source_navigator.dart | 217 + rohd_extension/dart/pubspec.yaml | 15 + rohd_extension/dart/test/flc_data_test.dart | 405 ++ rohd_extension/package-lock.json | 3887 ++++++++++++++ rohd_extension/package.json | 100 + rohd_extension/resources/rohd_icon.png | Bin 0 -> 5225 bytes rohd_extension/snippets/rohd.json | 385 ++ rohd_extension/src/conditional_completions.ts | 1087 ++++ rohd_extension/src/debug_tracker.ts | 384 ++ rohd_extension/src/dtd_bridge.ts | 439 ++ rohd_extension/src/extension.ts | 113 + rohd_extension/src/flc_service.ts | 515 ++ rohd_extension/src/source_navigator.ts | 482 ++ rohd_extension/src/uri_forwarder.ts | 295 ++ rohd_extension/tool/install.sh | 67 + rohd_extension/tsconfig.json | 15 + test/array_collapsing_test.dart | 4572 ++++++++++++++++- test/benchmark_test.dart | 2 +- test/collapse_test.dart | 20 +- test/const_radix_test.dart | 164 + test/gate_test.dart | 230 +- test/logic_array_test.dart | 2 +- test/logic_test.dart | 22 + test/logic_value_test.dart | 9 + test/net_bus_test.dart | 14 +- test/netlist_synthesizer_test.dart | 129 +- test/pair_interface_hier_test.dart | 2 +- test/pair_interface_hier_w_modify_test.dart | 2 +- test/pair_interface_test.dart | 2 +- test/provider_consumer_test.dart | 68 +- test/provider_consumer_w_modify_test.dart | 68 +- test/replication_test.dart | 30 +- test/sv_gen_test.dart | 22 +- test/swizzle_test.dart | 207 +- test/synth_builder_test.dart | 47 - test/systemverilog_port_types_test.dart | 168 + test/typed_port_test.dart | 2 +- tool/generate_coverage.sh | 27 +- tool/gh_actions/devtool/build_web.sh | 18 - tool/gh_actions/devtool/install_devtools.sh | 65 + .../devtool/test_devtools_install.sh | 62 + tool/run_checks.sh | 4 +- 221 files changed, 38952 insertions(+), 1313 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart create mode 100644 packages/rohd_hierarchy/LICENSE create mode 100644 packages/rohd_hierarchy/README.md create mode 100644 packages/rohd_hierarchy/analysis_options.yaml create mode 100644 packages/rohd_hierarchy/lib/rohd_hierarchy.dart create mode 100644 packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_constants.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_models.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_query.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_service.dart create mode 100644 packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart create mode 100644 packages/rohd_hierarchy/lib/src/occurrence_address.dart create mode 100644 packages/rohd_hierarchy/lib/src/occurrence_search_result.dart create mode 100644 packages/rohd_hierarchy/lib/src/prefix_query.dart create mode 100644 packages/rohd_hierarchy/lib/src/regex_query.dart create mode 100644 packages/rohd_hierarchy/lib/src/signal_occurrence.dart create mode 100644 packages/rohd_hierarchy/lib/src/signal_search_result.dart create mode 100644 packages/rohd_hierarchy/pubspec.yaml create mode 100644 packages/rohd_hierarchy/test/adapter_search_parity_test.dart create mode 100644 packages/rohd_hierarchy/test/address_conversion_test.dart create mode 100644 packages/rohd_hierarchy/test/devtools_search_flow_test.dart create mode 100644 packages/rohd_hierarchy/test/filter_bank_integration_test.dart create mode 100644 packages/rohd_hierarchy/test/fixtures/filter_bank.json create mode 100644 packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart create mode 100644 packages/rohd_hierarchy/test/hierarchy_query_test.dart create mode 100644 packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart create mode 100644 packages/rohd_hierarchy/test/module_search_test.dart create mode 100644 packages/rohd_hierarchy/test/occurrence_address_test.dart create mode 100644 packages/rohd_hierarchy/test/regex_search_test.dart create mode 100644 packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart create mode 100644 packages/rohd_hierarchy/test/signal_search_result_test.dart create mode 100644 rohd_devtools_extension/.vscode/tasks.json create mode 100644 rohd_devtools_extension/LICENSE create mode 100644 rohd_devtools_extension/Makefile create mode 100644 rohd_devtools_extension/assets/help/details_help.md create mode 100644 rohd_devtools_extension/assets/help/devtools_help.md create mode 100644 rohd_devtools_extension/assets/icons/rohd_logo.png create mode 100644 rohd_devtools_extension/lib/main_standalone.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/const/app_theme.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/cubit/cubits.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/cubit/details_tab_cubit.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/cubit/snapshot_cubit.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/cubit/theme_cubit.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/models/dtd_vm_service_info.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/connection_state_machine.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/io_vm_connection_strategy.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy_stub.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_io.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_web.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/services.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source_binding.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/services/web_vm_connection_strategy.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/details_help_button.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/devtools_connection_host.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/devtools_help_button.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/platform_icon.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/schematic_icon.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/simulation_time_display.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/standalone_app_shell.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/ui.dart create mode 100644 rohd_devtools_extension/lib/rohd_devtools/ui/vm_connection_form.dart create mode 100644 rohd_devtools_extension/linux/.gitignore create mode 100644 rohd_devtools_extension/linux/CMakeLists.txt create mode 100644 rohd_devtools_extension/linux/flutter/CMakeLists.txt create mode 100644 rohd_devtools_extension/linux/flutter/generated_plugin_registrant.cc create mode 100644 rohd_devtools_extension/linux/flutter/generated_plugin_registrant.h create mode 100644 rohd_devtools_extension/linux/flutter/generated_plugins.cmake create mode 100644 rohd_devtools_extension/linux/runner/CMakeLists.txt create mode 100644 rohd_devtools_extension/linux/runner/main.cc create mode 100644 rohd_devtools_extension/linux/runner/my_application.cc create mode 100644 rohd_devtools_extension/linux/runner/my_application.h create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/LICENSE create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/README.md create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/capture_boundary.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_toast.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_client.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/rohd_extension_status.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/save_png_native.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/save_png_stub.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/save_png_web.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/test/logic_type_utils_test.dart create mode 100644 rohd_devtools_extension/tool/test_devtools_install.dart delete mode 100644 rohd_devtools_extension/web/manifest.json create mode 100644 rohd_extension/.gitignore create mode 100644 rohd_extension/.markdownlint.json create mode 100644 rohd_extension/.nvmrc create mode 100644 rohd_extension/.vscodeignore create mode 100644 rohd_extension/LICENSE create mode 100644 rohd_extension/Makefile create mode 100644 rohd_extension/README.md create mode 100644 rohd_extension/dart/LICENSE create mode 100644 rohd_extension/dart/lib/dtd_service.dart create mode 100644 rohd_extension/dart/lib/flc_data.dart create mode 100644 rohd_extension/dart/lib/rohd_source_navigator.dart create mode 100644 rohd_extension/dart/lib/source_navigator.dart create mode 100644 rohd_extension/dart/pubspec.yaml create mode 100644 rohd_extension/dart/test/flc_data_test.dart create mode 100644 rohd_extension/package-lock.json create mode 100644 rohd_extension/package.json create mode 100644 rohd_extension/resources/rohd_icon.png create mode 100644 rohd_extension/snippets/rohd.json create mode 100644 rohd_extension/src/conditional_completions.ts create mode 100644 rohd_extension/src/debug_tracker.ts create mode 100644 rohd_extension/src/dtd_bridge.ts create mode 100644 rohd_extension/src/extension.ts create mode 100644 rohd_extension/src/flc_service.ts create mode 100644 rohd_extension/src/source_navigator.ts create mode 100644 rohd_extension/src/uri_forwarder.ts create mode 100755 rohd_extension/tool/install.sh create mode 100644 rohd_extension/tsconfig.json create mode 100644 test/const_radix_test.dart create mode 100644 test/systemverilog_port_types_test.dart delete mode 100755 tool/gh_actions/devtool/build_web.sh create mode 100755 tool/gh_actions/devtool/install_devtools.sh create mode 100755 tool/gh_actions/devtool/test_devtools_install.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5c23bb9c0..12e172c74 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu { - "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", "updateContentCommand": "tool/gh_codespaces/run_setup.sh", diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index e041e169a..0c4421967 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -12,6 +12,12 @@ { "pattern":"^https://github.com" }, + { + "pattern":"^https://pymtl3.readthedocs.io/en/latest/$" + }, + { + "pattern":"^https://docs.cocotb.org/en/stable/$" + }, { "pattern":"^https://www.nandland.com" }, diff --git a/.github/workflows/build_devtool.yml b/.github/workflows/build_devtool.yml index 240dce3ce..6263bb692 100644 --- a/.github/workflows/build_devtool.yml +++ b/.github/workflows/build_devtool.yml @@ -28,7 +28,10 @@ jobs: run: tool/gh_actions/devtool/run_devtool_test.sh - name: Build Static Web - run: tool/gh_actions/devtool/build_web.sh + run: tool/gh_actions/devtool/install_devtools.sh + + - name: Test DevTools Installation + run: tool/gh_actions/devtool/test_devtools_install.sh extension/devtools - name: Create artifact branch and commit run: | diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 673f550d3..f2ed2cf65 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -117,5 +117,8 @@ jobs: run: tool/gh_actions/devtool/run_devtool_test.sh - name: Build Static Web - run: tool/gh_actions/devtool/build_web.sh + run: tool/gh_actions/devtool/install_devtools.sh + + - name: Test DevTools Installation + run: tool/gh_actions/devtool/test_devtools_install.sh extension/devtools diff --git a/CHANGELOG.md b/CHANGELOG.md index 74c6b4765..0e4abc553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## Next release + +- Improved simulation performance and generated outputs by avoiding module creation for four-state-safe operations involving `Const`s, zero shifts, and muxes with constant controls (). +- Enforced that `Const` values cannot be changed through `put` or `inject`, including through another `Logic` driven by a `Const` (). +- Added per-`Const` preferred radix control for generated literals, normalized `Const` names to reflect their displayed radix, and enhanced `LogicValue.toRadixString` to omit its width and radix decorator or digit separators (). +- Improved generated SystemVerilog to collapse contiguous partial array and range assignments into packed slice assignments when safe (). +- Added per-direction configuration of explicit or implicit object and data types for generated SystemVerilog ports. Defaults preserve the existing `input logic`, `output logic`, and `inout wire` declarations (). +- Improved `Logic.getRange` and `slice` on filled `Const`s to return direct constants instead of constructing `BusSubset` modules (). +- Improved generated SystemVerilog for swizzles to compact adjacent bit selections into legal slice expressions (). +- Improved generated SystemVerilog to collapse a variety of intermediate `LogicArray`s and net buses (e.g. from bit-blasting, aggregate connections, `assignSubset`) into inline concatenations on their consuming connections, eliminating unnecessary intermediate declarations, `assign`s, and `net_connect`s when it is safe to do so (). + ## 0.6.9 - Fixed a bug where unnamed or mergeable inOut loop-back connections on a single module could be incorrectly omitted from the generated SystemVerilog (). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb333f2a0..6bb9116ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ To run the complete ROHD test suite for development, you need to install [Icarus #### On your own system -[Visual Studio Code (VSCode)](https://code.visualstudio.com/) is a great IDE for development. You can find installation instructions for VSCode here: +[Visual Studio Code (VSCode)](https://code.visualstudio.com/) is a great IDE for development. You can find installation instructions for VSCode here: The Dart extension extends VSCode with support for the Dart programming language and provides tools for effectively editing, refactoring and running. Check out the detailed information: diff --git a/LICENSE b/LICENSE index cfbbee995..9e00cc822 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (C) 2021-2023 Intel Corporation +Copyright (C) 2021-2026 Intel Corporation Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/analysis_options.yaml b/analysis_options.yaml index 4d6df7488..4cce8f7b5 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,9 +1,6 @@ # Lints set up with some guidance from here: # https://rydmike.com/blog_flutter_linting.html -formatter: - trailing_commas: preserve - analyzer: language: strict-casts: true @@ -11,7 +8,9 @@ analyzer: strict-raw-types: true exclude: - doc/tutorials/chapter_9/rohd_vf_example + - packages/rohd_hierarchy - rohd_devtools_extension + - rohd_extension # keep up to date, matching https://dart.dev/tools/linter-rules/all # some lints are not yet available, so disabled and marked with [not currently recognized] diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..f17ff0ca6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,4 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: + - rohd: true \ No newline at end of file diff --git a/doc/user_guide/_docs/A03-constant.md b/doc/user_guide/_docs/A03-constant.md index 21c059730..1e7fe840c 100644 --- a/doc/user_guide/_docs/A03-constant.md +++ b/doc/user_guide/_docs/A03-constant.md @@ -2,7 +2,7 @@ title: "Constants" permalink: /docs/constant/ excerpt: "Constants" -last_modified_at: 2022-12-06 +last_modified_at: 2026-07-14 toc: true --- @@ -10,13 +10,28 @@ Constants can often be inferred by ROHD automatically, but can also be explicitl ```dart // a 16 bit constant with value 5 -var x = Const(5, width:16); +var x = Const(5, width: 16); ``` +## Preferred radix + +The optional `preferredRadix` controls how a `Const` is displayed in generated outputs. Supported radices are binary (2), octal (8), decimal (10), and hexadecimal (16). The radix only affects formatting, not the value of the constant. + +```dart +var binary = Const(42, width: 8, preferredRadix: 2); // 8'b101010 +var octal = Const(42, width: 8, preferredRadix: 8); // 8'o52 +var decimal = Const(42, width: 8, preferredRadix: 10); // 8'd42 +var hexadecimal = Const(42, width: 8, preferredRadix: 16); // 8'h2a +``` + +Constants containing `x` or `z` may fall back to binary so all bit states can be represented. + +## Binary integer conversion + There is a convenience function for converting binary to an integer: ```dart -// this is equvialent to and shorter than int.parse('010101', radix:2) +// this is equivalent to and shorter than int.parse('010101', radix: 2) // you can put underscores to help with readability, they are ignored bin('01_0101') ``` diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 00d3d25bb..e4e625952 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -28,6 +28,31 @@ void main() async { The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. +## Controlling port types + +Generated ports default to `input logic`, `output logic`, and `inout wire`, preserving the traditional ROHD declarations. Use a `SystemVerilogSynthesizerConfiguration` to independently control whether object types, such as `wire` and `var`, and data types, such as `logic`, are explicit for each port direction: + +```dart +final generatedSv = myModule.generateSynth( + configuration: const SystemVerilogSynthesizerConfiguration( + inputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.explicit, + dataType: SystemVerilogPortType.implicit, + ), + outputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.implicit, + ), + inOutPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.explicit, + ), + ), +); +``` + +The same configuration can be passed directly to `SystemVerilogSynthesizer` when using `SynthBuilder`. + ## Controlling naming ### Modules diff --git a/doc/user_guide/_get-started/03-development-recommendations.md b/doc/user_guide/_get-started/03-development-recommendations.md index 6ffb5ab7b..d224e54df 100644 --- a/doc/user_guide/_get-started/03-development-recommendations.md +++ b/doc/user_guide/_get-started/03-development-recommendations.md @@ -10,9 +10,9 @@ toc: true - The [ROHD Cosimulation](https://github.com/intel/rohd-cosim) package allows you to cosimulate the ROHD simulator with a variety of SystemVerilog simulators. - The [ROHD Hardware Component Library](https://github.com/intel/rohd-vf) provides a set of reusable and configurable components for design and verification. - Visual Studio Code (vscode) is a great, free IDE with excellent support for Dart. It works well on all platforms, including native Windows or Windows Subsystem for Linux (WSL) which allows you to run a native Linux kernel (e.g. Ubuntu) within Windows. You can also use vscode to develop on a remote machine with the Remote SSH extension. - - vscode: + - vscode: - WSL: - - Remote SSH: + - Remote SSH: - Dart extension for vscode: Head over to the [user guide]({{ site.baseurl }}{% link _docs/A01-sample-example.md %}) to learn more about how to use ROHD. diff --git a/lib/src/module.dart b/lib/src/module.dart index d2d172f27..e8e70adcd 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1137,7 +1137,12 @@ abstract class Module { /// /// Currently returns one long file in SystemVerilog, but in the future /// may have other output formats, languages, files, etc. - String generateSynth() { + /// + /// The [configuration] controls options specific to SystemVerilog output. + String generateSynth({ + SystemVerilogSynthesizerConfiguration configuration = + const SystemVerilogSynthesizerConfiguration(), + }) { if (!_hasBuilt) { throw ModuleNotBuiltException(this); } @@ -1151,7 +1156,8 @@ abstract class Module { '''; return synthHeader + - SynthBuilder(this, SystemVerilogSynthesizer()) + SynthBuilder( + this, SystemVerilogSynthesizer(configuration: configuration)) .getSynthFileContents() .join('\n\n////////////////////\n\n'); } diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index f3dfd8693..3a797fbf4 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // bus.dart @@ -183,13 +183,20 @@ class BusSubset extends Module with InlineSystemVerilog { class Swizzle extends Module with InlineSystemVerilog { final String _out = Naming.unpreferredName('swizzled'); + /// A regular expression that will have matches if an expression is a single + /// bit select of a signal or packed array element. + static final RegExp _singleBitSelectRegex = + RegExp(r'^\(?([A-Za-z_][A-Za-z0-9_$]*(?:\[\d+\])*)\[(\d+)\]\)?$'); + /// The output port containing concatenated signals. late final Logic out; final List _swizzleInputs = []; - /// Whether this [Swizzle] is for [LogicNet]s. - final bool _isNet; + /// Whether this [Swizzle] concatenates [LogicNet]s (so its output is a net + /// that can be driven bidirectionally). + @internal + final bool isNet; @internal @override @@ -197,10 +204,10 @@ class Swizzle extends Module with InlineSystemVerilog { /// Constructs a [Module] which concatenates [signals] into one large [out]. Swizzle(List signals, {super.name = 'swizzle'}) - : _isNet = signals.any((e) => e.isNet) { + : isNet = signals.any((e) => e.isNet) { var outputWidth = 0; - final inputCreator = _isNet ? addInOut : addInput; + final inputCreator = isNet ? addInOut : addInput; var idx = 0; for (final signal in signals.reversed) { @@ -212,7 +219,7 @@ class Swizzle extends Module with InlineSystemVerilog { outputWidth += signal.width; } - if (_isNet) { + if (isNet) { out = LogicNet(name: _out, width: outputWidth, naming: Naming.unnamed); final internalOut = addInOut(_out, out, width: outputWidth); @@ -252,32 +259,32 @@ class Swizzle extends Module with InlineSystemVerilog { String inlineVerilog(Map inputs) { assert( inputs.length == _swizzleInputs.length || - (inputs.length == _swizzleInputs.length + 1 && _isNet), + (inputs.length == _swizzleInputs.length + 1 && isNet), 'This swizzle has ${_swizzleInputs.length} inputs,' ' but saw $inputs with ${inputs.length} values.'); // Calculate all width descriptions upfront to determine alignment final validInputs = _swizzleInputs.reversed.where((e) => e.width > 0).toList(); + final operands = _collapseContiguousBitSelects(validInputs, inputs); // If there's only one element, no need for width descriptions - if (validInputs.length == 1) { - final inName = inputs[validInputs.first.name]!; - return inName; + if (operands.length == 1) { + return operands.first.expression; } final widthDescriptions = <({int upper, int? lower})>[]; var upperIndex = out.width - 1; // First pass: calculate all width descriptions - for (final e in validInputs) { - if (e.width > 1) { - final lowerIndex = upperIndex - e.width + 1; + for (final operand in operands) { + if (operand.width > 1) { + final lowerIndex = upperIndex - operand.width + 1; widthDescriptions.add((upper: upperIndex, lower: lowerIndex)); } else { widthDescriptions.add((upper: upperIndex, lower: null)); } - upperIndex -= e.width; + upperIndex -= operand.width; } // Find maximum width for alignment @@ -299,8 +306,7 @@ class Swizzle extends Module with InlineSystemVerilog { final inputLines = []; var descIndex = 0; - for (final e in validInputs) { - final inName = inputs[e.name]!; + for (final operand in operands) { final desc = widthDescriptions[descIndex++]; String alignedDesc; @@ -315,10 +321,10 @@ class Swizzle extends Module with InlineSystemVerilog { alignedDesc = desc.upper.toString().padLeft(totalWidth); } - upperIndex -= e.width; + upperIndex -= operand.width; final maybeComma = upperIndex >= 0 ? ',' : ' '; // space at end for alignment - inputLines.add('$inName$maybeComma /* $alignedDesc */'); + inputLines.add('${operand.expression}$maybeComma /* $alignedDesc */'); } return ''' @@ -326,4 +332,97 @@ class Swizzle extends Module with InlineSystemVerilog { ${inputLines.join('\n')} }'''; } + + /// Rewrites runs of adjacent descending single-bit selects from the same + /// packed signal into wider SystemVerilog slices. + /// + /// For example, `a[7], a[6], a[5]` becomes `a[7:5]`, and + /// `a[0][1], a[0][0]` becomes `a[0][1:0]`. Ascending runs are intentionally + /// left expanded because SystemVerilog slices cannot reverse bit order with + /// `lower:upper` syntax. + List<({String expression, int width})> _collapseContiguousBitSelects( + List validInputs, + Map inputs, + ) { + final operands = <({String expression, int width})>[]; + + var index = 0; + while (index < validInputs.length) { + final input = validInputs[index]; + final expression = inputs[input.name]!; + final selectedBit = _singleBitSelect(input, expression); + if (selectedBit == null) { + operands.add((expression: expression, width: input.width)); + index++; + continue; + } + + var lowerIndex = selectedBit.index; + var endIndex = index + 1; + while (endIndex < validInputs.length) { + final nextInput = validInputs[endIndex]; + final nextExpression = inputs[nextInput.name]!; + final nextSelectedBit = _singleBitSelect(nextInput, nextExpression); + if (nextSelectedBit == null || + nextSelectedBit.source != selectedBit.source || + nextSelectedBit.index != lowerIndex - 1) { + break; + } + + lowerIndex = nextSelectedBit.index; + endIndex++; + } + + if (endIndex == index + 1) { + operands.add((expression: expression, width: input.width)); + } else { + operands.add(( + expression: '${selectedBit.source}[${selectedBit.index}:$lowerIndex]', + width: endIndex - index, + )); + } + index = endIndex; + } + + return operands; + } + + /// Parses [expression] as a single-bit select of a packed signal when it is + /// safe to participate in slice collapsing. + /// + /// Returns `null` for multi-bit inputs, non-select expressions, or selects + /// sourced from unpacked arrays. + ({String source, int index})? _singleBitSelect( + Logic input, + String expression, + ) { + if (input.width != 1 || _hasUnpackedArraySource(input.srcConnection)) { + return null; + } + + final match = _singleBitSelectRegex.firstMatch(expression); + if (match == null) { + return null; + } + + return (source: match.group(1)!, index: int.parse(match.group(2)!)); + } + + /// Walks up [logic]'s containing structures to detect unpacked arrays. + /// + /// SystemVerilog packed slices are not interchangeable with unpacked array + /// indexing, so any unpacked array source disables bit-select collapsing. + bool _hasUnpackedArraySource(Logic? logic) { + var current = logic; + while (current?.parentStructure != null) { + final parentStructure = current!.parentStructure!; + if (parentStructure is LogicArray && + parentStructure.numUnpackedDimensions > 0) { + return true; + } + current = parentStructure; + } + + return false; + } } diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index 2780cde9d..9bdc858eb 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -815,7 +815,22 @@ class LShift extends _ShiftGate { /// ```SystemVerilog /// control ? d1 : d0 /// ``` -Logic mux(Logic control, Logic d1, Logic d0) => Mux(control, d1, d0).out; +/// +/// If [control] is a valid [Const], returns the selected input directly. +Logic mux(Logic control, Logic d1, Logic d0) { + if (control.width != 1) { + throw PortWidthMismatchException(control, 1); + } + if (d0.width != d1.width) { + throw PortWidthMismatchException.equalWidth(d0, d1); + } + + if (control is Const && control.value.isValid) { + return control.value == LogicValue.one ? d1 : d0; + } + + return Mux(control, d1, d0).out; +} /// A mux (multiplexer) module. /// diff --git a/lib/src/signals/const.dart b/lib/src/signals/const.dart index 3885b3d4f..72c2544b2 100644 --- a/lib/src/signals/const.dart +++ b/lib/src/signals/const.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // const.dart @@ -9,8 +9,60 @@ part of 'signals.dart'; +/// Returns [preferredRadix] if it is supported for generated literals. +/// +/// Throws a [LogicValueConversionException] for unsupported radices. +int? _validatePreferredRadix(int? preferredRadix) { + if (preferredRadix != null && + !const {2, 8, 10, 16}.contains(preferredRadix)) { + throw LogicValueConversionException( + 'Unsupported preferred radix: $preferredRadix'); + } + + return preferredRadix; +} + +/// Creates an identifier-friendly name for a constant [value]. +/// +/// Fully known values use [preferredRadix], or decimal if none is +/// preferred. Values containing `x` or `z` use binary so no state is lost. +String _constName(LogicValue value, int? preferredRadix) { + final radix = value.isValid ? preferredRadix ?? 10 : 2; + final digits = value.toRadixString( + radix: radix, + includeWidth: false, + sepChar: '', + ); + final prefix = switch (radix) { + 2 => '0b', + 8 => '0o', + 10 => '', + 16 => '0x', + _ => throw StateError('Unexpected radix: $radix'), + }; + + return 'const_$prefix$digits'; +} + /// Represents a [Logic] that never changes value. +/// +/// Attempts to assign, [put], or [inject] a new value throw an +/// [UnassignableException], including through another [Logic] driven by the +/// [Const]. class Const extends Logic { + /// The explanation included when an attempt is made to modify a [Const]. + static const _unassignableMessage = + 'A `Const` value cannot be modified, including through another `Logic` ' + 'driven by it.'; + + /// The preferred radix for displaying this constant in generated outputs. + /// + /// Supported radices are binary (2), octal (8), decimal (10), and + /// hexadecimal (16). If omitted, generated outputs select a radix + /// automatically. A generator may fall back to another radix when the + /// preferred radix cannot represent this constant's value. + final int? preferredRadix; + /// Constructs a [Const] with the specified value. /// /// [val] should be processable by [LogicValue.of]. @@ -18,18 +70,155 @@ class Const extends Logic { /// If a [width] is provided, the [Const] will be that width. If not, and /// [val] is a [LogicValue], the [Const] will be the width of [val]. /// Otherwise, the [Const] will be 1 bit wide. - Const(dynamic val, {int? width, bool fill = false}) + /// + /// [preferredRadix] controls how the constant is displayed in generated + /// outputs and its normalized name. Supported values are 2, 8, 10, and 16. + /// If omitted, generated outputs select a radix automatically and the name + /// uses decimal. Values containing `x` or `z` may fall back to binary. + Const(dynamic val, {int? width, bool fill = false, int? preferredRadix}) + : this._( + LogicValue.of(val, + width: width ?? (val is LogicValue ? val.width : 1), + fill: fill), + preferredRadix: _validatePreferredRadix(preferredRadix)); + + /// Constructs a [Const] from an already normalized [value]. + Const._(LogicValue value, {required this.preferredRadix}) : super( - name: 'const_$val', - width: width ?? (val is LogicValue ? val.width : 1), - // we don't care about maintaining this node unless necessary - naming: Naming.unnamed, - ) { - _wire.put(val, fill: fill, signalName: name); + name: _constName(value, preferredRadix), + width: value.width, + // we don't care about maintaining this node unless necessary + naming: Naming.unnamed) { + _wire + ..put(value, signalName: name) + ..makeImmutable(this, reason: _unassignableMessage); - makeUnassignable(reason: '`Const` signals are unassignable.'); + makeUnassignable(reason: _unassignableMessage); } @override - Const clone({String? name}) => Const(value, width: width); + Const clone({String? name}) => + Const(value, width: width, preferredRadix: preferredRadix); + + @override + void put(dynamic val, {bool fill = false}) => + throw UnassignableException(this, reason: _unassignableMessage); + + @override + void inject(dynamic val, {bool fill = false}) => + throw UnassignableException(this, reason: _unassignableMessage); + + /// Verifies that [other] has the same width as this [Const]. + void _checkMatchingWidth(Logic other) { + if (width != other.width) { + throw PortWidthMismatchException.equalWidth(this, other); + } + } + + @override + Logic operator ~() => Const(~value); + + @override + Logic operator &(Logic other) { + _checkMatchingWidth(other); + + if (other is Const) { + return Const(value & other.value); + } else if (value.isValid && value.isZero) { + return this; + } + + return And2Gate(this, other).out; + } + + @override + Logic operator |(Logic other) { + _checkMatchingWidth(other); + + if (other is Const) { + return Const(value | other.value); + } else if (value.isValid && + value == LogicValue.filled(width, LogicValue.one)) { + return this; + } + + return Or2Gate(this, other).out; + } + + @override + Logic operator ^(Logic other) { + _checkMatchingWidth(other); + + if (other is Const) { + return Const(value ^ other.value); + } + + return Xor2Gate(this, other).out; + } + + @override + Logic operator >>(dynamic other) { + if (Logic._isZeroShiftAmount(other)) { + return this; + } else if (other is Logic && other is! Const) { + return ARShift(this, other).out; + } + + return Const(value >> (other is Const ? other.value : other)); + } + + @override + Logic operator <<(dynamic other) { + if (Logic._isZeroShiftAmount(other)) { + return this; + } else if (other is Logic && other is! Const) { + return LShift(this, other).out; + } + + return Const(value << (other is Const ? other.value : other)); + } + + @override + Logic operator >>>(dynamic other) { + if (Logic._isZeroShiftAmount(other)) { + return this; + } else if (other is Logic && other is! Const) { + return RShift(this, other).out; + } + + return Const(value >>> (other is Const ? other.value : other)); + } + + @override + Logic and() => Const(value.and()); + + @override + Logic or() => Const(value.or()); + + @override + Logic xor() => Const(value.xor()); + + @override + Logic eq(dynamic other) { + if (other is Logic) { + _checkMatchingWidth(other); + return other is Const + ? Const(value.eq(other.value)) + : Equals(this, other).out; + } + + return Const(value.eq(LogicValue.of(other, width: width))); + } + + @override + Logic neq(dynamic other) { + if (other is Logic) { + _checkMatchingWidth(other); + return other is Const + ? Const(value.neq(other.value)) + : NotEquals(this, other).out; + } + + return Const(value.neq(LogicValue.of(other, width: width))); + } } diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index 4c5f99e5e..f964d320a 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -445,10 +445,22 @@ class Logic { Logic operator ~() => NotGate(this).out; /// Logical bitwise AND. - Logic operator &(Logic other) => And2Gate(this, other).out; + Logic operator &(Logic other) { + if (other is Const) { + return other & this; + } + + return And2Gate(this, other).out; + } /// Logical bitwise OR. - Logic operator |(Logic other) => Or2Gate(this, other).out; + Logic operator |(Logic other) { + if (other is Const) { + return other | this; + } + + return Or2Gate(this, other).out; + } /// Logical bitwise XOR. Logic operator ^(Logic other) => Xor2Gate(this, other).out; @@ -478,6 +490,10 @@ class Logic { /// /// If [isNet] and [other] is constant, then the result will also be a net. Logic operator >>(dynamic other) { + if (_isZeroShiftAmount(other)) { + return this; + } + if (isNet) { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); @@ -498,6 +514,10 @@ class Logic { /// /// If [isNet] and [other] is constant, then the result will also be a net. Logic operator <<(dynamic other) { + if (_isZeroShiftAmount(other)) { + return this; + } + if (isNet) { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); @@ -518,6 +538,10 @@ class Logic { /// /// If [isNet] and [other] is constant, then the result will also be a net. Logic operator >>>(dynamic other) { + if (_isZeroShiftAmount(other)) { + return this; + } + if (isNet) { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); @@ -543,6 +567,17 @@ class Logic { } } + /// Indicates whether [other] represents a known constant shift of zero. + static bool _isZeroShiftAmount(dynamic other) { + if (other is Const) { + return other.value.isValid && other.value.isZero; + } else if (other is Logic) { + return false; + } else { + return LogicValue.ofInferWidth(other).isZero; + } + } + /// Unary AND. Logic and() => AndUnary(this).out; @@ -768,10 +803,31 @@ class Logic { return this; } + final constantFillValue = _constantFillValueOf(this); + if (constantFillValue != null) { + return Const( + LogicValue.filled( + (modifiedEndIndex - modifiedStartIndex).abs() + 1, + constantFillValue, + ), + ); + } + // Create a new bus subset return BusSubset(this, modifiedStartIndex, modifiedEndIndex).subset; } + static LogicValue? _constantFillValueOf(Logic logic) { + if (logic is! Const || logic.width == 0) { + return null; + } + + final fillValue = logic.value[0]; + return logic.value == LogicValue.filled(logic.width, fillValue) + ? fillValue + : null; + } + /// Returns a version of this [Logic] with the bit order reversed. late final Logic reversed = (isNet ? LogicNet.new : Logic.new)( name: 'reversed_$name', naming: Naming.unnamed, width: width) @@ -906,8 +962,16 @@ class Logic { /// The input [multiplier] cannot be negative or 0; an exception will be /// thrown, otherwise. /// + /// If [multiplier] is 1, then this signal is returned directly, since + /// replicating once is a no-op and would otherwise generate a redundant + /// `1{...}` in the output SystemVerilog. + /// /// If [isNet], then the result will also be a net. Logic replicate(int multiplier) { + if (multiplier == 1) { + return this; + } + if (isNet) { // many SV simulators don't support replication of nets return List.generate(multiplier, (i) => this).swizzle(); diff --git a/lib/src/signals/wire.dart b/lib/src/signals/wire.dart index 883e3eaf0..812b09866 100644 --- a/lib/src/signals/wire.dart +++ b/lib/src/signals/wire.dart @@ -26,6 +26,39 @@ class _Wire { /// The current active value of this signal. LogicValue _currentValue; + /// The [Logic] whose immutability makes this wire immutable. + Logic? _immutableOwner; + + /// Additional context explaining why this wire is immutable. + String? _immutableReason; + + /// Makes this wire immutable on behalf of [owner]. + /// + /// If this wire is already immutable, the original owner and reason are + /// preserved. + void makeImmutable(Logic owner, {String? reason}) { + _immutableOwner ??= owner; + _immutableReason ??= reason; + } + + /// Throws an [UnassignableException] if this wire is immutable, identifying + /// [signalName] as the signal through which the update was attempted. + void _assertMutable({required String signalName}) { + final immutableOwner = _immutableOwner; + if (immutableOwner != null) { + final attemptedUpdateReason = + 'Signal "$signalName" cannot be updated because it is driven by ' + 'immutable signal "$immutableOwner".'; + throw UnassignableException( + immutableOwner, + reason: [ + attemptedUpdateReason, + if (_immutableReason != null) _immutableReason, + ].join(' '), + ); + } + } + /// The last value of this signal before the [Simulator] tick. /// /// This is useful for detecting when to trigger an edge. @@ -143,6 +176,11 @@ class _Wire { /// Tells this [_Wire] to adopt all the behavior of [other] so that /// it can replace [other]. Returns the [_Wire] that has adopted everything. _Wire _adopt(_Wire other) { + if (_immutableOwner == null && other._immutableOwner != null) { + _immutableOwner = other._immutableOwner; + _immutableReason = other._immutableReason; + } + _glitchController.emitter.adopt(other._glitchController.emitter); other._migrateChangedTriggers(this); @@ -218,6 +256,7 @@ class _Wire { /// /// This function calls [put()] in [Simulator.injectAction()]. void inject(dynamic val, {required String signalName, bool fill = false}) { + _assertMutable(signalName: signalName); Simulator.injectAction(() => put(val, signalName: signalName, fill: fill)); } @@ -233,6 +272,8 @@ class _Wire { /// This function is used for propagating glitches through connected signals. /// Use this function for custom definitions of [Module] behavior. void put(dynamic val, {required String signalName, bool fill = false}) { + _assertMutable(signalName: signalName); + var newValue = LogicValue.of(val, fill: fill, width: width); if (newValue.width != width) { diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_options.dart index 8c64d0426..750e69e93 100644 --- a/lib/src/synthesizers/netlist/netlist_options.dart +++ b/lib/src/synthesizers/netlist/netlist_options.dart @@ -8,9 +8,8 @@ // Author: Desmond Kirkpatrick import 'package:meta/meta.dart'; -import 'package:rohd/rohd.dart' hide SynthModuleStopPolicy; +import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; -import 'package:rohd/src/synthesizers/utilities/synth_module_stop_policy.dart'; /// Configuration options for netlist synthesis. /// diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 152948e57..c24f5932a 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -63,8 +63,7 @@ class NetlistSynthesizer extends Synthesizer { NetlistSynthesizer({this.options = const NetlistOptions()}) : _moduleStopPolicy = options.moduleStopPolicy ?? SynthModuleStopPolicy.netlist( - leafModuleTypes: options.leafModuleTypes, - ), + leafModuleTypes: options.leafModuleTypes), _netlistCellMapper = options.netlistCellMapper ?? NetlistCellMapper.withDefaults(); @@ -81,12 +80,10 @@ class NetlistSynthesizer extends Synthesizer { }) { final attr = {'src': 'generated'}; - final translation = NetlistModuleTranslation( - module, - netlistCellMapper: netlistCellMapper, - generatesDefinition: generatesDefinition, - getInstanceTypeOfModule: getInstanceTypeOfModule, - ) + final translation = NetlistModuleTranslation(module, + netlistCellMapper: netlistCellMapper, + generatesDefinition: generatesDefinition, + getInstanceTypeOfModule: getInstanceTypeOfModule) ..processPorts() ..processInternalWires() ..processCells(); @@ -119,7 +116,7 @@ class NetlistSynthesizer extends Synthesizer { int width, Logic elemLogic, Logic parentLogic, - List fullParentIds, + List fullParentIds })>[]; // Pending $struct_pack fields: for output struct ports, instead of @@ -132,7 +129,7 @@ class NetlistSynthesizer extends Synthesizer { int dstLowerIndex, int dstUpperIndex, SynthLogic srcSynthLogic, - SynthLogic dstSynthLogic, + SynthLogic dstSynthLogic })>[]; // Track struct ports (both output ports of the current module AND @@ -143,9 +140,45 @@ class NetlistSynthesizer extends Synthesizer { if (synthDef != null) { // 1. Non-partial assignments: src drives dst → dst IDs become // src IDs (the driver's IDs are canonical). - for (final assignment in synthDef.assignments.where( - (a) => a is! PartialSynthAssignment, - )) { + void aliasArrayChildren(SynthLogic src, SynthLogic dst) { + final srcLogic = src.logics.firstOrNull; + final dstLogic = dst.logics.firstOrNull; + if (srcLogic is! LogicArray || dstLogic is! LogicArray) { + return; + } + if (srcLogic.elements.length != dstLogic.elements.length) { + return; + } + + for (final (index, srcElement) in srcLogic.elements.indexed) { + final dstElement = dstLogic.elements[index]; + final srcElementSynth = synthDef.logicToSynthMap[srcElement]; + final dstElementSynth = synthDef.logicToSynthMap[dstElement]; + if (srcElementSynth == null || dstElementSynth == null) { + continue; + } + + final srcElementLogic = srcElementSynth.logics.firstOrNull; + final dstElementLogic = dstElementSynth.logics.firstOrNull; + if (srcElementLogic is LogicArray && dstElementLogic is LogicArray) { + aliasArrayChildren(srcElementSynth, dstElementSynth); + } + + final srcElementIds = getIds(srcElementSynth); + final dstElementIds = getIds(dstElementSynth); + final len = srcElementIds.length < dstElementIds.length + ? srcElementIds.length + : dstElementIds.length; + for (var i = 0; i < len; i++) { + if (dstElementIds[i] != srcElementIds[i]) { + idAlias[dstElementIds[i]] = srcElementIds[i]; + } + } + } + } + + for (final assignment + in synthDef.assignments.where((a) => a is! PartialSynthAssignment)) { final srcIds = getIds(assignment.src); final dstIds = getIds(assignment.dst); final len = @@ -155,6 +188,7 @@ class NetlistSynthesizer extends Synthesizer { idAlias[dstIds[i]] = srcIds[i]; } } + aliasArrayChildren(assignment.src, assignment.dst); } // 2. Partial assignments (output / sub-module struct ports): @@ -522,9 +556,7 @@ class NetlistSynthesizer extends Synthesizer { } // Unconditionally remove struct_slice cells — they are duplicated by // $struct_unpack cells which carry field names. - if (cellKey.startsWith( - SynthOperationNamer.structureSliceOperationName, - )) { + if (cellKey.startsWith(SynthOperationNamer.structureSliceOperationName)) { return true; } final params = cell['parameters'] as Map?; @@ -559,7 +591,7 @@ class NetlistSynthesizer extends Synthesizer { int width, Logic elemLogic, Logic parentLogic, - List fullParentIds, + List fullParentIds })>>{}; for (final sf in structFieldCells) { (groups[sf.parentLogic] ??= []).add(sf); @@ -580,16 +612,14 @@ class NetlistSynthesizer extends Synthesizer { resolvedElemBits: resolvedElemBits, offset: sf.offset, width: sf.width, - elemLogic: sf.elemLogic, + elemLogic: sf.elemLogic ); }) - .where( - (f) => !f.resolvedElemBits.indexed.every((e) { - final (i, bit) = e; - return f.offset + i < resolvedParentBits.length && - bit == resolvedParentBits[f.offset + i]; - }), - ) + .where((f) => !f.resolvedElemBits.indexed.every((e) { + final (i, bit) = e; + return f.offset + i < resolvedParentBits.length && + bit == resolvedParentBits[f.offset + i]; + })) .toList(); if (nonTrivialFields.isEmpty) { @@ -609,11 +639,8 @@ class NetlistSynthesizer extends Synthesizer { for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - final fieldName = structLayout?.fieldNameAt( - f.offset, - fallbackName: f.elemLogic.name, - anonymousUnpreferred: true, - ) ?? + final fieldName = structLayout?.fieldNameAt(f.offset, + fallbackName: f.elemLogic.name, anonymousUnpreferred: true) ?? f.elemLogic.name; // Disambiguate duplicate field names with index suffix. var portName = fieldName; @@ -631,11 +658,8 @@ class NetlistSynthesizer extends Synthesizer { }; for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - params['FIELD_${i}_NAME'] = structLayout?.fieldNameAt( - f.offset, - fallbackName: f.elemLogic.name, - anonymousUnpreferred: true, - ) ?? + params['FIELD_${i}_NAME'] = structLayout?.fieldNameAt(f.offset, + fallbackName: f.elemLogic.name, anonymousUnpreferred: true) ?? f.elemLogic.name; params['FIELD_${i}_OFFSET'] = f.offset; params['FIELD_${i}_WIDTH'] = f.width; @@ -647,7 +671,7 @@ class NetlistSynthesizer extends Synthesizer { 'parameters': params, 'attributes': {}, 'port_directions': portDirs, - 'connections': conns, + 'connections': conns }; suIdx++; } @@ -685,23 +709,19 @@ class NetlistSynthesizer extends Synthesizer { .map((sc) { final resolvedSrcBits = applyAlias(sc.srcIds.cast()); final yBits = resolvedDstBits.sublist( - sc.dstLowerIndex, - sc.dstUpperIndex + 1, - ); + sc.dstLowerIndex, sc.dstUpperIndex + 1); return ( resolvedSrcBits: resolvedSrcBits, yBits: yBits, dstLowerIndex: sc.dstLowerIndex, dstUpperIndex: sc.dstUpperIndex, - srcSynthLogic: sc.srcSynthLogic, + srcSynthLogic: sc.srcSynthLogic ); }) - .where( - (f) => !f.resolvedSrcBits - .take(f.yBits.length) - .indexed - .every((e) => e.$2 == f.yBits[e.$1]), - ) + .where((f) => !f.resolvedSrcBits + .take(f.yBits.length) + .indexed + .every((e) => e.$2 == f.yBits[e.$1])) .toList(); if (nonTrivialFields.isEmpty) { @@ -717,8 +737,7 @@ class NetlistSynthesizer extends Synthesizer { final cellName = dstLogic != null ? SynthOperationNamer.instanceName( operationName: SynthOperationNamer.structureConcatOperationName, - destination: dstLogic, - ) + destination: dstLogic) : SynthOperationNamer.structureConcatOperationName; // Build port_directions and connections. @@ -727,10 +746,8 @@ class NetlistSynthesizer extends Synthesizer { for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - final fieldName = structLayout?.fieldNameAt( - f.dstLowerIndex, - fallbackName: f.srcSynthLogic.resolved.name, - ) ?? + final fieldName = structLayout?.fieldNameAt(f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name) ?? f.srcSynthLogic.resolved.name; var portName = fieldName; if (portDirs.containsKey(portName)) { @@ -751,10 +768,8 @@ class NetlistSynthesizer extends Synthesizer { }; for (var i = 0; i < nonTrivialFields.length; i++) { final f = nonTrivialFields[i]; - params['FIELD_${i}_NAME'] = structLayout?.fieldNameAt( - f.dstLowerIndex, - fallbackName: f.srcSynthLogic.resolved.name, - ) ?? + params['FIELD_${i}_NAME'] = structLayout?.fieldNameAt(f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name) ?? f.srcSynthLogic.resolved.name; params['FIELD_${i}_OFFSET'] = f.dstLowerIndex; params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; @@ -766,7 +781,7 @@ class NetlistSynthesizer extends Synthesizer { 'parameters': params, 'attributes': {}, 'port_directions': portDirs, - 'connections': conns, + 'connections': conns }; } } @@ -774,9 +789,7 @@ class NetlistSynthesizer extends Synthesizer { translation ..processCellCleanup(enableDce: options.enableDCE) ..processConstants( - applyAlias: applyAlias, - pruneFloating: options.enableDCE, - ); + applyAlias: applyAlias, pruneFloating: options.enableDCE); // -- Break shared wire IDs for array_concat cells -------------------- // After aliasing, concat Y can share wire IDs with the independently @@ -788,7 +801,8 @@ class NetlistSynthesizer extends Synthesizer { // data flow is: // element drivers → concat input → concat Y (fresh IDs) → consumer final arrayConcatOldToNew = {}; - final arrayConcatOldOutputBits = >{}; + final arrayConcatReplacements = + <({String cellKey, List oldBits, List newBits})>[]; final outputPortBitSets = [ for (final port in ports.values) if ((port as Map)['direction'] == 'output') @@ -796,9 +810,8 @@ class NetlistSynthesizer extends Synthesizer { ]; for (final cellEntry in cells.entries) { - if (!cellEntry.key.startsWith( - SynthOperationNamer.arrayConcatOperationName, - )) { + if (!cellEntry.key + .startsWith(SynthOperationNamer.arrayConcatOperationName)) { continue; } if (cellEntry.key.startsWith('array_concat_output_')) { @@ -814,35 +827,81 @@ class NetlistSynthesizer extends Synthesizer { } final oldBits = (portEntry.value as List).cast(); final oldBitSet = oldBits.whereType().toSet(); - if (outputPortBitSets.any( - (outputBits) => - outputBits.length == oldBitSet.length && - outputBits.containsAll(oldBitSet), - )) { + if (outputPortBitSets.any((outputBits) => + outputBits.length == oldBitSet.length && + outputBits.containsAll(oldBitSet))) { continue; } - arrayConcatOldOutputBits[cellEntry.key] = oldBitSet; - conns[portEntry.key] = [ - for (final b in oldBits) - b is int - ? arrayConcatOldToNew.putIfAbsent(b, translation.allocateWireId) - : b, + final newBits = [ + for (final b in oldBits) b is int ? translation.allocateWireId() : b, ]; + conns[portEntry.key] = newBits; + arrayConcatReplacements + .add((cellKey: cellEntry.key, oldBits: oldBits, newBits: newBits)); + } + } + + final arrayConcatOutputProducers = >{}; + for (final (index, replacement) in arrayConcatReplacements.indexed) { + for (final bit in replacement.oldBits) { + if (bit is int) { + (arrayConcatOutputProducers[bit] ??= []).add(index); + } } } + List rewriteArrayConcatConsumerBits( + List bits, { + String? consumingCellKey, + }) { + for (final replacement in arrayConcatReplacements) { + if (replacement.cellKey == consumingCellKey || + replacement.oldBits.length != bits.length) { + continue; + } + if (bits.indexed + .every((entry) => entry.$2 == replacement.oldBits[entry.$1])) { + return replacement.newBits; + } + } + + final newBits = []; + var changed = false; + for (final bit in bits) { + if (bit is! int) { + newBits.add(bit); + continue; + } + final producerIndices = arrayConcatOutputProducers[bit] + ?.where((index) => + arrayConcatReplacements[index].cellKey != consumingCellKey) + .toList(); + if (producerIndices == null || producerIndices.length != 1) { + newBits.add(bit); + continue; + } + final producer = arrayConcatReplacements[producerIndices.single]; + final bitIndex = producer.oldBits.indexOf(bit); + if (bitIndex < 0) { + newBits.add(bit); + continue; + } + newBits.add(producer.newBits[bitIndex]); + changed = true; + } + return changed ? newBits : bits; + } + // Redirect downstream consumers: any input port or module output bit that // matches an old concat Y ID gets replaced with the corresponding fresh ID. - if (arrayConcatOldToNew.isNotEmpty) { + if (arrayConcatReplacements.isNotEmpty) { for (final portEntry in ports.values) { final port = portEntry as Map; if (port['direction'] != 'output') { continue; } final bits = (port['bits'] as List).cast(); - final newBits = [ - for (final b in bits) b is int ? (arrayConcatOldToNew[b] ?? b) : b, - ]; + final newBits = rewriteArrayConcatConsumerBits(bits); if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { port['bits'] = newBits; } @@ -852,20 +911,14 @@ class NetlistSynthesizer extends Synthesizer { final cell = cellEntry.value as Map; final conns = cell['connections'] as Map; final dirs = cell['port_directions'] as Map; - final selfOldOutputBits = - arrayConcatOldOutputBits[cellEntry.key] ?? const {}; for (final portEntry in conns.entries.toList()) { if (dirs[portEntry.key] != 'input') { continue; } final bits = (portEntry.value as List).cast(); - final newBits = [ - for (final b in bits) - b is int && !selfOldOutputBits.contains(b) - ? (arrayConcatOldToNew[b] ?? b) - : b, - ]; + final newBits = rewriteArrayConcatConsumerBits(bits, + consumingCellKey: cellEntry.key); if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { conns[portEntry.key] = newBits; } @@ -874,42 +927,31 @@ class NetlistSynthesizer extends Synthesizer { } translation.processNetnames( - applyAlias: applyAlias, - arraySliceOldToNew: arraySliceOldToNew, - arrayConcatOldToNew: arrayConcatOldToNew, - pruneUndriven: options.enableDCE, - drivenBits: options.enableDCE - ? NetlistValidation.connectedBits( - ports, - cells, - portDirections: const {'input', 'inout'}, - cellDirection: 'output', - ) - : const {}, - ); + applyAlias: applyAlias, + arraySliceOldToNew: arraySliceOldToNew, + arrayConcatOldToNew: arrayConcatOldToNew, + pruneUndriven: options.enableDCE, + drivenBits: options.enableDCE + ? NetlistValidation.connectedBits(ports, cells, + portDirections: const {'input', 'inout'}, + cellDirection: 'output') + : const {}); final netnames = translation.netnames; // -- Structural validation ------------------------------------------- // Always catch netlist shorts, even when assertions are disabled. - final warnings = NetlistValidation.validate( - ports, - cells, - module.name, - netnames: netnames, - throwOnMultipleDrivers: true, - printWarnings: false, - checkUnconnectedOutputs: options.validateUnconnectedOutputs, - ); - - return NetlistSynthesisResult( - module, - getInstanceTypeOfModule, - ports: ports, - cells: cells, - netnames: netnames, - attributes: attr, - warnings: warnings, - ); + final warnings = NetlistValidation.validate(ports, cells, module.name, + netnames: netnames, + throwOnMultipleDrivers: true, + printWarnings: false, + checkUnconnectedOutputs: options.validateUnconnectedOutputs); + + return NetlistSynthesisResult(module, getInstanceTypeOfModule, + ports: ports, + cells: cells, + netnames: netnames, + attributes: attr, + warnings: warnings); } /// Apply all post-processing passes to the modules map. @@ -935,17 +977,12 @@ class NetlistSynthesizer extends Synthesizer { /// avoiding redundant re-synthesis. [slimMode] overrides the configured /// default for this projection without modifying the retained results. Map> buildModulesMap( - SynthBuilder synth, - Module top, { - bool? slimMode, - }) { + SynthBuilder synth, Module top, + {bool? slimMode}) { final effectiveSlimMode = slimMode ?? options.slimMode; final swEntries = Stopwatch()..start(); - final modules = NetlistPasses.collectModuleEntries( - synth.synthesisResults, - topModule: top, - includeCellConnections: !effectiveSlimMode, - ); + final modules = NetlistPasses.collectModuleEntries(synth.synthesisResults, + topModule: top, includeCellConnections: !effectiveSlimMode); swEntries.stop(); final swPasses = Stopwatch()..start(); @@ -956,11 +993,8 @@ class NetlistSynthesizer extends Synthesizer { } /// Generate the combined netlist JSON from a [SynthBuilder]'s results. - String generateCombinedJson( - SynthBuilder synth, - Module top, { - bool? slimMode, - }) { + String generateCombinedJson(SynthBuilder synth, Module top, + {bool? slimMode}) { final swCollect = Stopwatch()..start(); final modules = buildModulesMap(synth, top, slimMode: slimMode); swCollect.stop(); @@ -974,7 +1008,7 @@ class NetlistSynthesizer extends Synthesizer { final combined = { 'creator': 'NetlistSynthesizer (rohd)', 'version': formatVersion, - 'modules': modules, + 'modules': modules }; final swEncode = Stopwatch()..start(); diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index 6b1eed42b..a2b5831be 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2026 Intel Corporation +// Copyright (C) 2021-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'netlist/netlist.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog.dart b/lib/src/synthesizers/systemverilog/systemverilog.dart index 281b05df9..6990cbe2a 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,5 +1,6 @@ -// Copyright (C) 2021-2024 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; +export 'systemverilog_synthesizer_configuration.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index f152e38f9..18ff4caed 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -16,13 +16,193 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// Creates a new [SystemVerilogSynthModuleDefinition] for the given [module]. SystemVerilogSynthModuleDefinition(super.module); + /// A shared mapping from [SynthLogic]s which are the result of an inlineable + /// submodule to the instantiation that produces them. + /// + /// Populated by aggregate-collapse passes, then distributed alongside + /// [_collapseMarkedChainableModules] so inline rendering (including + /// recursive, multi-level chains) can resolve them. + final Map + _inlineableSubmoduleMap = {}; + @override void process() { + _inlinePackedRangesIntoSubmoduleInputs(); + _collapseAggregateConnections(); + _collapseWholeNetBuses(); + _forwardPassthroughElementsIntoInlineables(); _replaceNetConnections(); _collapseMarkedChainableModules(); _replaceInOutConnectionInlineableModules(); } + /// Inlines a fully covered packed bus into its sole submodule input. + /// + /// Each driver must cover the next contiguous destination range and supply + /// its entire source. Constant-backed intermediates are resolved to their + /// literal before the sources are joined into an inline concatenation. + /// + /// This remains SystemVerilog-specific because the replacement is an inline + /// [Swizzle] expression. Backend-neutral range discovery and composition are + /// handled by the base [SynthModuleDefinition] before [process] is called. + void _inlinePackedRangesIntoSubmoduleInputs() { + final inputUses = >{}; + final allMappedSignals = {}; + final outputOrInOutMappedSignals = {}; + for (final instantiation in subModuleInstantiations) { + instantiation as SystemVerilogSynthSubModuleInstantiation; + for (final entry in instantiation.inputMapping.entries) { + if (instantiation.module is! InlineSystemVerilog) { + allMappedSignals.add(entry.value.resolved); + } + inputUses.putIfAbsent(entry.value.resolved, () => []).add(( + instantiation: instantiation, + portName: entry.key, + )); + } + for (final signal in [ + ...instantiation.outputMapping.values, + ...instantiation.inOutMapping.values, + ]) { + if (instantiation.module is! InlineSystemVerilog) { + allMappedSignals.add(signal.resolved); + outputOrInOutMappedSignals.add(signal.resolved); + } + } + } + + final assignmentsByDestination = >{}; + final assignmentsBySource = >{}; + for (final assignment in assignments) { + assignmentsByDestination + .putIfAbsent(assignment.dst.resolved, () => []) + .add(assignment); + assignmentsBySource + .putIfAbsent(assignment.src.resolved, () => []) + .add(assignment); + } + + final removedAssignments = {}; + final removedSignals = {}; + for (final entry in inputUses.entries) { + final bus = entry.key; + final use = entry.value.singleOrNull; + if (use == null || + bus.isArray || + bus.isNet || + bus.isConstant || + bus.logics.any( + (logic) => logic is LogicStructure || logic.parentStructure != null, + ) || + bus.isPort(module) || + outputOrInOutMappedSignals.contains(bus) || + !bus.isClearable || + !internalSignals.contains(bus) || + use.instantiation.module is InlineSystemVerilog || + (use.instantiation.module is SystemVerilog && + (use.instantiation.module as SystemVerilog) + .expressionlessInputs + .contains(use.portName))) { + continue; + } + + final drivers = List.of( + assignmentsByDestination[bus] ?? const [], + )..sort((a, b) => + _packedDestinationLower(a).compareTo(_packedDestinationLower(b))); + if (drivers.isEmpty || (assignmentsBySource[bus]?.isNotEmpty ?? false)) { + continue; + } + + var nextDestinationBit = 0; + var canInline = true; + final sources = []; + final constantDrivers = {}; + final constantIntermediates = {}; + for (final driver in drivers) { + final lower = _packedDestinationLower(driver); + final upper = _packedDestinationUpper(driver); + if (driver is! PartialSynthAssignment || + lower != nextDestinationBit || + upper >= bus.width || + (driver is RangeSynthAssignment && + (driver.srcLowerIndex != 0 || + driver.srcUpperIndex != driver.src.width - 1))) { + canInline = false; + break; + } + + var source = driver.src.resolved; + if (source.isArray || source.width != upper - lower + 1) { + canInline = false; + break; + } + + final sourceDrivers = + assignmentsByDestination[source] ?? const []; + final sourceConsumers = + assignmentsBySource[source] ?? const []; + final removableConstantIntermediate = sourceDrivers.length == 1 && + sourceConsumers.length == 1 && + sourceConsumers.single == driver && + sourceDrivers.single is! PartialSynthAssignment && + sourceDrivers.single.src.resolved.isConstant && + !source.hasPreservedName && + !allMappedSignals.contains(source) && + internalSignals.contains(source); + if (removableConstantIntermediate) { + constantDrivers.add(sourceDrivers.single); + constantIntermediates.add(source); + source = sourceDrivers.single.src.resolved; + } + + sources.add(source); + nextDestinationBit = upper + 1; + } + + if (!canInline || nextDestinationBit != bus.width) { + continue; + } + + _addSwizzleConnect(bus, sources); + removedAssignments + ..addAll(drivers) + ..addAll(constantDrivers); + removedSignals + ..add(bus) + ..addAll(constantIntermediates); + } + + if (removedAssignments.isNotEmpty) { + final retainedAssignments = [ + for (final assignment in assignments) + if (!removedAssignments.contains(assignment)) assignment, + ]; + assignments + ..clear() + ..addAll(retainedAssignments); + for (final signal in removedSignals) { + signal.clearDeclaration(); + internalSignals.remove(signal); + } + } + } + + /// Returns the lower destination bit selected by [assignment]. + int _packedDestinationLower(SynthAssignment assignment) => + assignment is PartialSynthAssignment ? assignment.dstLowerIndex : 0; + + /// Returns the upper destination bit selected by [assignment]. + int _packedDestinationUpper(SynthAssignment assignment) => + assignment is PartialSynthAssignment + ? assignment.dstUpperIndex + : assignment.dst.width - 1; + @override SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => SystemVerilogSynthSubModuleInstantiation(m); @@ -80,6 +260,1040 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } + /// Collapses an internal aggregate signal (e.g. a [LogicArray]) whose + /// individual elements are each connected one-to-one to some other signal, + /// and which is itself used as a whole in exactly one place, by replacing + /// that single aggregate use with an inlined concatenation ([Swizzle]) of the + /// per-element sources. + /// + /// For example, a parent with individual nets `sig0..sigN` each connected to + /// an element of an internal array `arr` that feeds a single child array port + /// would otherwise emit one `net_connect` (or `assign`) per element plus a + /// declaration of `arr`. This collapses all of that into a single + /// `.port({sigN, ..., sig0})` connection and drops `arr`. + /// + /// This works for both nets (bidirectional `inout` connections, where the + /// concatenation is a legal net lvalue) and non-nets (a driven + /// concatenation). + void _collapseAggregateConnections() { + // Loop until no further collapses occur: collapsing one aggregate can make + // another eligible (e.g. chains of aggregates). Each collapse strictly + // removes work (clears an aggregate and its element assignments) and an + // already-collapsed aggregate is no longer a candidate, so this terminates. + var changed = true; + while (changed) { + changed = false; + + // Resolved view of every net [BusSubset], used to discover element + // sources that are tied through a pass-through net bus rather than via a + // direct assignment (see [_traceNetSubsetSource]). + final netSubsets = _netSubsetLookups(); + final logicSubsets = _logicSubsetLookups(); + + // Count how many times each array [SynthLogic] is used "as a whole", + // along with where (a submodule port mapping is the only use we can + // currently inline into). + final aggregateUseCount = {}; + final aggregatePortUse = {}; + + void noteWholeUse(SynthLogic? synthLogic) { + if (synthLogic != null && synthLogic.isArray) { + aggregateUseCount.update(synthLogic.resolved, (v) => v + 1, + ifAbsent: () => 1); + } + } + + for (final instantiation in subModuleInstantiations) { + instantiation as SystemVerilogSynthSubModuleInstantiation; + for (final entry in instantiation.inputMapping.entries) { + noteWholeUse(entry.value); + if (entry.value.isArray) { + aggregatePortUse[entry.value.resolved] = + (instantiation: instantiation, portName: entry.key); + } + } + for (final entry in instantiation.inOutMapping.entries) { + noteWholeUse(entry.value); + if (entry.value.isArray) { + aggregatePortUse[entry.value.resolved] = + (instantiation: instantiation, portName: entry.key); + } + } + // outputs of submodules can't be replaced by an inline concatenation + instantiation.outputMapping.values.forEach(noteWholeUse); + } + + // Assignments and this-module ports also count as whole-aggregate uses + // (and we cannot inline into those today, so they just disqualify). + [...inputs, ...outputs, ...inOuts].forEach(noteWholeUse); + for (final assignment in assignments) { + noteWholeUse(assignment.src); + noteWholeUse(assignment.dst); + } + + // Map from each array element to the assignment(s) connecting it, and a + // count of every place each array element is used (submodule port + // mappings and assignments). An element is only safe to inline if its + // sole appearance is its single connecting assignment; if it is read + // anywhere else (e.g. by a reduction), the aggregate must stay intact. + final elementAssignments = >{}; + final assignmentsBySignal = _assignmentsBySignal(); + final elementUseCount = {}; + void noteElementUse(SynthLogic? synthLogic) { + if (synthLogic is SynthLogicArrayElement) { + elementUseCount.update(synthLogic.resolved, (v) => v + 1, + ifAbsent: () => 1); + } + } + + for (final instantiation in subModuleInstantiations) { + instantiation as SystemVerilogSynthSubModuleInstantiation; + instantiation.inputMapping.values.forEach(noteElementUse); + instantiation.outputMapping.values.forEach(noteElementUse); + instantiation.inOutMapping.values.forEach(noteElementUse); + } + for (final assignment in assignments) { + noteElementUse(assignment.src); + noteElementUse(assignment.dst); + for (final end in [assignment.src, assignment.dst]) { + if (end is SynthLogicArrayElement) { + elementAssignments + .putIfAbsent(end.resolved, () => []) + .add(assignment); + } + } + } + + final removedAssignments = {}; + for (final aggEntry in aggregatePortUse.entries) { + final agg = aggEntry.key; + final use = aggEntry.value; + + // Restriction: exactly one whole-aggregate use, which is the port use + // we found. This single-use restriction mirrors the other collapsing + // mechanisms; it could be relaxed later (e.g. duplicating the + // concatenation into multiple consumers). + if ((aggregateUseCount[agg] ?? 0) != 1) { + continue; + } + + // The aggregate must be a clearable internal signal: mergeable (so its + // name carries no meaning to preserve) and not a port. This mirrors + // the conservatism of the other collapsing mechanisms: renameable or + // reserved aggregates are left intact so their declared names survive. + if (agg.declarationCleared || + !agg.isClearable || + agg.isPort(module) || + !internalSignals.contains(agg)) { + continue; + } + + // The consuming port must accept expressions. + final consumer = use.instantiation.module; + if (consumer is SystemVerilog && + consumer.expressionlessInputs.contains(use.portName)) { + continue; + } + + // Gather each element's single source (the other end of its single + // connecting assignment), in element order (index 0 = LSB). + final elementLogics = agg.logics + .whereType() + .first + .elements + .map(getSynthLogic) + .map((e) => e?.resolved) + .toList(); + + final aggregateLogic = agg.logics.singleOrNull; + final isPackedBitArray = aggregateLogic is LogicArray && + aggregateLogic.dimensions.length == 1 && + aggregateLogic.elementWidth == 1 && + aggregateLogic.numUnpackedDimensions == 0; + final packedSubsetSource = isPackedBitArray + ? _packedLogicSubsetSource( + elementLogics, + logicSubsets, + expectedWidth: agg.width, + ) + : null; + if (packedSubsetSource != null && + use.instantiation.inputMapping.containsKey(use.portName)) { + use.instantiation.setInputMapping( + use.portName, + packedSubsetSource.source, + replace: true, + ); + for (final subset in packedSubsetSource.subsets) { + subset.clearInstantiation(); + } + agg.clearDeclaration(); + internalSignals.remove(agg); + changed = true; + continue; + } + + final elementSources = []; + var allElementsSingleSourced = true; + + // Net [BusSubset]s consumed while tracing element sources through + // pass-through buses; their instantiations are cleared and the buses + // dropped only if the whole collapse succeeds. + final tracedSubsets = {}; + final tracedBuses = {}; + + for (final element in elementLogics) { + if (element == null) { + allElementsSingleSourced = false; + break; + } + + // Preferred path: the element is connected by exactly one assignment + // and used nowhere else. + final connectingAssignments = elementAssignments[element]; + if (connectingAssignments != null && + connectingAssignments.length == 1 && + (elementUseCount[element] ?? 0) == 1) { + final assignment = connectingAssignments.single; + if (assignment is PartialSynthAssignment) { + allElementsSingleSourced = false; + break; + } + final source = assignment.src.resolved == element + ? assignment.dst.resolved + : assignment.src.resolved; + if (source == element) { + allElementsSingleSourced = false; + break; + } + elementSources.add(source); + continue; + } + + // Fallback (net) path: the element is the [subset] of a [BusSubset] + // whose [original] is a pass-through bus, and the same bit of that + // bus is tied to exactly one other net. Trace through to that net so + // the bus and its [BusSubset]s can be eliminated. + final traced = _traceNetSubsetSource( + element, + netSubsets, + assignmentsBySignal, + elementUseCount, + elementAssignments, + tracedSubsets, + tracedBuses, + ); + if (traced == null) { + allElementsSingleSourced = false; + break; + } + elementSources.add(traced); + } + + if (!allElementsSingleSourced || elementSources.isEmpty) { + continue; + } + + // A traced pass-through bus may only be dropped if it is fully consumed + // by this collapse: every net [BusSubset] referencing it must have been + // traced, and it must be a clearable internal non-port. + if (!_tracedBusesFullyConsumed( + tracedBuses, tracedSubsets, netSubsets)) { + continue; + } + + // Net aggregates can only collapse if their sources are also nets (the + // concatenation is a net lvalue), and non-net likewise. + if (elementSources.any((e) => e.isNet != agg.isNet)) { + continue; + } + + // If every source is an element of one common parent array (in any + // order), the aggregate is just a re-arrangement of an existing array + // and there is nothing to consolidate; passing that array (or letting + // the other collapsing mechanisms merge it) yields cleaner output, so + // leave it alone. The value of this optimization is consolidating + // otherwise-separate signals into a single concatenation. + if (elementSources.every((e) => e is SynthLogicArrayElement)) { + final parents = elementSources + .map((e) => (e as SynthLogicArrayElement).parentArray.resolved) + .toSet(); + if (parents.length == 1) { + continue; + } + } + + // Fabricate a swizzle (post-build instrumentation, not connected to the + // real hardware) whose concatenation reproduces the aggregate, and + // register it so the single aggregate use renders as the inline + // concatenation. + _addSwizzleConnect(agg, elementSources); + + // Remove the now-inlined element assignments and clear the aggregate + // declaration. + for (final element in elementLogics.nonNulls) { + removedAssignments.addAll( + elementAssignments[element] ?? const [], + ); + } + agg.clearDeclaration(); + internalSignals.remove(agg); + + // Drop any pass-through buses and clear the [BusSubset]s traced through + // them. + for (final subsetInst in tracedSubsets) { + subsetInst.clearInstantiation(); + } + for (final bus in tracedBuses) { + bus.clearDeclaration(); + internalSignals.remove(bus); + } + + changed = true; + } + assignments.removeWhere(removedAssignments.contains); + } + } + + /// Indexes non-net [BusSubset] views by their resolved subset output. + Map> _logicSubsetLookups() { + final bySubset = >{}; + for (final instantiation in subModuleInstantiations) { + instantiation as SystemVerilogSynthSubModuleInstantiation; + final subsetModule = instantiation.module; + if (subsetModule is! BusSubset || subsetModule.original.isNet) { + continue; + } + final original = + instantiation.inputMapping[subsetModule.original.name]?.resolved; + final subset = + instantiation.outputMapping[subsetModule.subset.name]?.resolved; + if (original == null || subset == null) { + continue; + } + final view = ( + inst: instantiation, + original: original, + subset: subset, + start: subsetModule.startIndex, + end: subsetModule.endIndex, + ); + bySubset.putIfAbsent(subset, () => []).add(view); + } + return bySubset; + } + + /// Reconstructs one fully covered packed source from ordered element subset + /// mappings, returning the source and helper instantiations to clear. + ({ + SynthLogic source, + Set subsets, + })? _packedLogicSubsetSource( + List elements, + Map> subsetsByOutput, { + required int expectedWidth, + }) { + SynthLogic? source; + var nextBit = 0; + final subsets = {}; + for (final element in elements) { + if (element == null) { + return null; + } + final view = subsetsByOutput[element]?.singleOrNull; + if (view == null || + view.start != nextBit || + view.end != nextBit + element.width - 1 || + (source != null && source != view.original)) { + return null; + } + source ??= view.original; + subsets.add(view.inst); + nextBit += element.width; + } + if (source == null || + source.width != expectedWidth || + nextBit != expectedWidth) { + return null; + } + return (source: source, subsets: subsets); + } + + /// Builds resolved lookups over every net [BusSubset] instantiation: a list + /// of all views, plus maps keyed by the resolved `original` bus and the + /// resolved `subset`. Reversed slices (`start > end`) are skipped because + /// they cannot be safely re-expressed as a simple concatenation here. + ({ + Map> byOriginal, + Map> bySubset, + }) _netSubsetLookups() { + final byOriginal = >{}; + final bySubset = >{}; + + for (final instantiation in subModuleInstantiations) { + instantiation as SystemVerilogSynthSubModuleInstantiation; + final m = instantiation.module; + if (m is! BusSubset || !m.original.isNet) { + continue; + } + if (m.startIndex > m.endIndex) { + // Reversed slice: skip conservatively. + continue; + } + final original = instantiation.inOutMapping[m.original.name]?.resolved; + final subset = instantiation.inOutMapping[m.subset.name]?.resolved; + if (original == null || subset == null) { + continue; + } + final view = ( + inst: instantiation, + original: original, + subset: subset, + start: m.startIndex, + end: m.endIndex, + ); + byOriginal.putIfAbsent(original, () => []).add(view); + bySubset.putIfAbsent(subset, () => []).add(view); + } + + return (byOriginal: byOriginal, bySubset: bySubset); + } + + /// Attempts to find the real source for array `element` when it is the + /// `subset` of a single-bit net [BusSubset] off a pass-through bus. + /// + /// The shape handled (the "bit-wise" case) is: the bus bit `i` is sliced once + /// to feed `element` (`bus[i] -> element`) and once more to tie to some other + /// net (`bus[i] -> net`). This traces `element` to that net, recording the + /// consumed [BusSubset]s and bus so they can be dropped once the whole + /// aggregate collapse is committed. + /// + /// Returns the resolved source net, or `null` if `element` does not match + /// this shape. + SynthLogic? _traceNetSubsetSource( + SynthLogic element, + ({ + Map> byOriginal, + Map> bySubset, + }) netSubsets, + Map> assignmentsBySignal, + Map elementUseCount, + Map> elementAssignments, + Set tracedSubsets, + Set tracedBuses, + ) { + // [element] must only be used as the subset side of a single BusSubset. + final asSubset = netSubsets.bySubset[element]; + if (asSubset == null || asSubset.length != 1) { + return null; + } + final elementView = asSubset.single; + + // Only single-bit slices are handled (one element per bus bit). + if (elementView.start != elementView.end) { + return null; + } + final bit = elementView.start; + final bus = elementView.original; + + // The bus must be a clearable internal non-port pass-through. + if (bus.declarationCleared || + !bus.isClearable || + bus.isPort(module) || + !internalSignals.contains(bus)) { + return null; + } + + // Find the sibling slice of the same bus bit that ties to another net. + final siblings = (netSubsets.byOriginal[bus] ?? []) + .where((v) => v.start == bit && v.end == bit && v.subset != element) + .toList(); + if (siblings.length != 1) { + return null; + } + final siblingView = siblings.single; + final sourceNet = siblingView.subset; + + if (sourceNet == element || + (!_isPort(sourceNet) && + _hasSubmoduleUseOutsideThroughAssignment( + sourceNet, + { + elementView.inst, + siblingView.inst, + }, + assignmentsBySignal, + ))) { + return null; + } + + tracedSubsets + ..add(elementView.inst) + ..add(siblingView.inst); + tracedBuses.add(bus); + + return sourceNet; + } + + /// Verifies that every pass-through bus in `tracedBuses` is fully consumed: + /// every net [BusSubset] referencing it (as its `original`) was traced into + /// `tracedSubsets`. This ensures dropping the bus leaves no dangling + /// references. + bool _tracedBusesFullyConsumed( + Set tracedBuses, + Set tracedSubsets, + ({ + Map> byOriginal, + Map> bySubset, + }) netSubsets, + ) { + for (final bus in tracedBuses) { + final allViews = netSubsets.byOriginal[bus] ?? const []; + for (final view in allViews) { + if (!tracedSubsets.contains(view.inst)) { + return false; + } + } + // The bus must not be referenced as a *subset* of some other slice + // (i.e. it must be a true root pass-through, not itself sliced into). + if ((netSubsets.bySubset[bus] ?? const []).isNotEmpty) { + return false; + } + } + return true; + } + + /// Collapses a flat (non-array) internal net bus that is used as a whole in + /// exactly one submodule port, and whose every bit is tied 1:1 to some other + /// net via single-bit net [BusSubset]s, into an inline concatenation of those + /// nets. + /// + /// This is the flat analogue of [_collapseAggregateConnections]: where that + /// method handles arrays whose *elements* are individually connected, this + /// handles a plain bus whose *bits* are individually tied (e.g. each bit + /// driven to/from a separate net), and which is then passed whole to a child. + /// It turns + /// ```systemverilog + /// wire [7:0] bus; + /// net_connect (bus[0], a0); ... net_connect (bus[7], a7); + /// child c (.data(bus)); + /// ``` + /// into `child c (.data({a7, ..., a0}));`, dropping `bus` and all the + /// `net_connect`s. + void _collapseWholeNetBuses() { + var changed = true; + while (changed) { + changed = false; + + final netSubsets = _netSubsetLookups(); + final assignmentsBySignal = _assignmentsBySignal(); + + // Count whole uses of each candidate bus and remember the single + // submodule port use we can inline into. Uses as the [original] of a + // [BusSubset] are definers, not whole uses, so they are excluded here. + final wholeUseCount = {}; + final portUse = {}; + + final busOriginals = netSubsets.byOriginal.keys.toSet(); + + void noteWholeUse(SynthLogic? synthLogic) { + if (synthLogic == null) { + return; + } + final resolved = synthLogic.resolved; + if (busOriginals.contains(resolved)) { + wholeUseCount.update(resolved, (v) => v + 1, ifAbsent: () => 1); + } + } + + for (final instantiation in subModuleInstantiations) { + instantiation as SystemVerilogSynthSubModuleInstantiation; + + // A BusSubset reading this bus as `original` is a definer, not a + // whole use; skip those mappings entirely. + final isDefiner = instantiation.module is BusSubset; + + for (final entry in instantiation.inOutMapping.entries) { + if (isDefiner) { + continue; + } + noteWholeUse(entry.value); + final resolved = entry.value.resolved; + if (busOriginals.contains(resolved)) { + portUse[resolved] = + (instantiation: instantiation, portName: entry.key); + } + } + // Nets only connect via inouts, but reads on input/output ports (or + // outputs) would still count as disqualifying whole uses. + if (!isDefiner) { + instantiation.inputMapping.values.forEach(noteWholeUse); + instantiation.outputMapping.values.forEach(noteWholeUse); + } + } + + // This-module ports and assignments are whole uses we cannot inline + // into, so they disqualify the bus. + [...inputs, ...outputs, ...inOuts].forEach(noteWholeUse); + for (final assignment in assignments) { + noteWholeUse(assignment.src); + noteWholeUse(assignment.dst); + } + + for (final busEntry in portUse.entries) { + final bus = busEntry.key; + final use = busEntry.value; + + // Exactly one whole use: the port use we found. + if ((wholeUseCount[bus] ?? 0) != 1) { + continue; + } + + // The bus must be a clearable internal non-port net. + if (bus.declarationCleared || + !bus.isNet || + bus.isArray || + !bus.isClearable || + bus.isPort(module) || + !internalSignals.contains(bus)) { + continue; + } + + // The bus must not itself be a subset of another bus. + if ((netSubsets.bySubset[bus] ?? const []).isNotEmpty) { + continue; + } + + // The consuming port must accept expressions. + final consumer = use.instantiation.module; + if (consumer is SystemVerilog && + consumer.expressionlessInputs.contains(use.portName)) { + continue; + } + + // A whole-width use of a [Swizzle] is its concatenation *result*, which + // already drives this bus; the per-bit subsets are then reads of the + // bus (e.g. bit-blasting it onto an array port) rather than its + // definers, so collapsing here would double-drive the bus and orphan + // its real consumer. Skip these. + if (consumer is Swizzle) { + continue; + } + + // Every bit of the bus must be tiled exactly once by a single-bit + // [BusSubset] definer covering [0, width). A slice that also feeds + // real logic is a consumer too, so it cannot be removed here. + final definers = netSubsets.byOriginal[bus]!; + if (definers.any((v) => v.start != v.end)) { + continue; + } + if (definers.length != bus.width) { + continue; + } + final byBit = {}; + var tiledExactly = true; + for (final view in definers) { + if (byBit.containsKey(view.start)) { + tiledExactly = false; + break; + } + byBit[view.start] = view; + } + if (!tiledExactly) { + continue; + } + for (var bit = 0; bit < bus.width; bit++) { + if (!byBit.containsKey(bit)) { + tiledExactly = false; + break; + } + } + if (!tiledExactly) { + continue; + } + final definerInstantiations = {for (final view in definers) view.inst}; + if (definers.any((view) => + !_isPort(view.subset) && + _hasSubmoduleUseOutsideThroughAssignment( + view.subset, + definerInstantiations, + assignmentsBySignal, + ))) { + continue; + } + + // Build the per-bit source nets, LSB-first. + final elementSources = [ + for (var bit = 0; bit < bus.width; bit++) byBit[bit]!.subset, + ]; + + // The sources must all be nets (the concatenation is a net lvalue). + if (elementSources.any((e) => !e.isNet)) { + continue; + } + + // Inline the bus as a concatenation of its per-bit nets and drop the + // bus plus its definer [BusSubset]s. + _addSwizzleConnect(bus, elementSources); + + for (final view in definers) { + view.inst.clearInstantiation(); + } + bus.clearDeclaration(); + internalSignals.remove(bus); + + changed = true; + } + } + } + + Map> _assignmentsBySignal() { + final assignmentsBySignal = >{}; + for (final assignment in assignments) { + for (final signal in [assignment.src.resolved, assignment.dst.resolved]) { + assignmentsBySignal.putIfAbsent(signal, () => []).add(assignment); + } + } + return assignmentsBySignal; + } + + bool _isPort(SynthLogic signal) => + signal.resolved.isPort(module) || + signal.resolved.isStructPortElement(module); + + bool _hasSubmoduleUseOutside( + SynthLogic signal, + Set ignoredInstantiations, + ) { + for (final instantiation in subModuleInstantiations) { + instantiation as SystemVerilogSynthSubModuleInstantiation; + if (ignoredInstantiations.contains(instantiation)) { + continue; + } + if (instantiation.module is BusSubset) { + continue; + } + final mappedSignals = [ + ...instantiation.inputMapping.values, + ...instantiation.outputMapping.values, + ...instantiation.inOutMapping.values, + ]; + if (mappedSignals.any( + (mappedSignal) => _signalsShareReferenceBase( + mappedSignal.resolved, + signal.resolved, + ), + )) { + return true; + } + } + return false; + } + + bool _hasSubmoduleUseOutsideThroughAssignment( + SynthLogic signal, + Set ignoredInstantiations, + Map> assignmentsBySignal, + ) { + if (_hasSubmoduleUseOutside(signal, ignoredInstantiations)) { + return true; + } + + for (final assignment + in assignmentsBySignal[signal.resolved] ?? const []) { + final other = assignment.src.resolved == signal.resolved + ? assignment.dst.resolved + : assignment.src.resolved; + if (!_isPort(other) && + _hasSubmoduleUseOutside(other, ignoredInstantiations)) { + return true; + } + } + return false; + } + + bool _signalsShareReferenceBase(SynthLogic a, SynthLogic b) => + a == b || _referenceBase(a) == b || _referenceBase(b) == a; + + /// Returns the packed object referenced by [signal] for usage comparisons. + SynthLogic _referenceBase(SynthLogic signal) => switch (signal) { + SynthLogicArrayElement() => signal.parentArray.resolved, + SynthLogicPackedBitReference() => signal.packedBase.resolved, + _ => signal.resolved, + }; + + /// Forwards the per-element sources of a pass-through [LogicArray] directly + /// into the single inlineable submodule (e.g. a [Swizzle]) that consumes + /// those elements, then drops the now-dead array. + /// + /// `assignSubset` (e.g. via `connectPorts`/`Logic.assignSubset`) introduces + /// an intermediate net [LogicArray] (`*_subset`) whose elements are each tied + /// by a single assignment to one external signal and then read + /// element-by-element by an inlineable submodule. Array elements never merge + /// away (they carry positional `x`/`z` semantics), so without this pass each + /// such element would materialize as its own `net_connect`. When *every* + /// element of the array is a pure pass-through, we rewire the consuming + /// submodule's ports straight to the per-element sources and drop the + /// intermediate array entirely, leaving a single inline concatenation. + void _forwardPassthroughElementsIntoInlineables() { + // Loop until no further forwarding occurs: forwarding one array can make + // another eligible. Each forward strictly removes work (an array and its + // element assignments), so this terminates. + var changed = true; + while (changed) { + changed = false; + + // Only instantiations that will actually render count as real uses: a + // module that still needs instantiation, or a fabricated inlineable (e.g. + // a [_SwizzleConnect] from an earlier collapse) registered in + // [_inlineableSubmoduleMap]. Dead/consumed definers (e.g. a [BusSubset] + // already cleared by [_collapseWholeNetBuses]) carry stale port mappings + // that must NOT be mistaken for live uses. + final activeInlineables = _inlineableSubmoduleMap.values.toSet(); + bool isActive(SystemVerilogSynthSubModuleInstantiation inst) => + inst.needsInstantiation || activeInlineables.contains(inst); + + final activeInstantiations = [ + for (final inst in subModuleInstantiations) + if (isActive(inst as SystemVerilogSynthSubModuleInstantiation)) inst + ]; + + // Arrays used "as a whole" anywhere must keep their elements intact, so + // their elements are not eligible for forwarding. + final aggregateUsedArrays = {}; + void markIfAggregateArray(SynthLogic? synthLogic) { + if (synthLogic != null && synthLogic.isArray) { + aggregateUsedArrays.add(synthLogic.resolved); + } + } + + [...inputs, ...outputs, ...inOuts].forEach(markIfAggregateArray); + for (final inst in activeInstantiations) { + inst.inputMapping.values.forEach(markIfAggregateArray); + inst.outputMapping.values.forEach(markIfAggregateArray); + inst.inOutMapping.values.forEach(markIfAggregateArray); + } + for (final assignment in assignments) { + markIfAggregateArray(assignment.src); + markIfAggregateArray(assignment.dst); + } + + // For each array element: the inlineable-submodule input/inout ports that + // read it, the assignments connecting it, and whether it has any other + // (disqualifying) use. + final inlineablePortReads = >{}; + final elementAssignments = >{}; + final disqualified = {}; + + void noteOtherUse(SynthLogic? s) { + if (s is SynthLogicArrayElement) { + disqualified.add(s.resolved); + } + } + + for (final inst in activeInstantiations) { + final isInlineable = inst.module is InlineSystemVerilog; + final resultLogic = + isInlineable ? inst.inlineResultLogic?.resolved : null; + + void handleRead(String name, SynthLogic value, + {required bool isInOut}) { + if (value is! SynthLogicArrayElement) { + return; + } + final resolved = value.resolved; + // A read by an inlineable submodule input/inout port (but not its own + // result) is the only "use" we can forward into; anything else + // disqualifies the element. + if (isInlineable && resolved != resultLogic) { + inlineablePortReads + .putIfAbsent(resolved, () => []) + .add((instantiation: inst, portName: name, isInOut: isInOut)); + } else { + noteOtherUse(value); + } + } + + inst.inputMapping.forEach((n, v) => handleRead(n, v, isInOut: false)); + inst.inOutMapping.forEach((n, v) => handleRead(n, v, isInOut: true)); + inst.outputMapping.values.forEach(noteOtherUse); + } + + for (final assignment in assignments) { + for (final end in [assignment.src, assignment.dst]) { + if (end is SynthLogicArrayElement) { + elementAssignments + .putIfAbsent(end.resolved, () => []) + .add(assignment); + } + } + } + + // An element is a pure pass-through if it is read by exactly one + // inlineable submodule port, connected by exactly one assignment to some + // other signal, used nowhere else, and lives in a clearable internal + // array that is not used as a whole. + bool isPassThrough(SynthLogic element) { + if (disqualified.contains(element)) { + return false; + } + final reads = inlineablePortReads[element]; + final assigns = elementAssignments[element]; + if (reads == null || reads.length != 1) { + return false; + } + if (assigns == null || assigns.length != 1) { + return false; + } + final assignment = assigns.single; + final partner = assignment.src.resolved == element + ? assignment.dst + : assignment.src; + if (partner.resolved == element) { + return false; + } + if (!element.isClearable) { + return false; + } + final parentArray = + (element as SynthLogicArrayElement).parentArray.resolved; + if (aggregateUsedArrays.contains(parentArray)) { + return false; + } + return true; + } + + // Group candidate pass-through elements by parent array. + final candidatesByArray = >{}; + for (final element in inlineablePortReads.keys) { + if (isPassThrough(element)) { + candidatesByArray + .putIfAbsent( + (element as SynthLogicArrayElement).parentArray.resolved, + () => {}) + .add(element); + } + } + + // Only forward when EVERY element of an array is a pass-through, so the + // whole array can be dropped. Partial forwarding is unsafe: the array + // would stay declared and its remaining (e.g. differently-driven or + // undriven `x`/`z`) elements could change behavior. + final droppedArrays = []; + final removedAssignments = {}; + candidatesByArray.forEach((parentArray, arrayCandidates) { + if (parentArray.declarationCleared || + !parentArray.isClearable || + parentArray.isPort(module) || + !internalSignals.contains(parentArray)) { + return; + } + + final allElements = parentArray.logics + .whereType() + .expand((logicArray) => logicArray.elements) + .map(getSynthLogic) + .nonNulls + .map((e) => e.resolved) + .toSet(); + + if (allElements.isEmpty || + !allElements.every(arrayCandidates.contains)) { + return; + } + + // Rewire each element's consuming port straight to the element's + // source, and remove the now-redundant element assignment. + for (final element in arrayCandidates) { + final read = inlineablePortReads[element]!.single; + final assignment = elementAssignments[element]!.single; + final partner = assignment.src.resolved == element + ? assignment.dst + : assignment.src; + + if (read.isInOut) { + read.instantiation + .setInOutMapping(read.portName, partner, replace: true); + } else { + read.instantiation + .setInputMapping(read.portName, partner, replace: true); + } + + removedAssignments.add(assignment); + (element as SynthLogicArrayElement).clearDeclaration(); + } + + droppedArrays.add(parentArray); + changed = true; + }); + + assignments.removeWhere(removedAssignments.contains); + _dropEmptiedArrays(droppedArrays); + } + } + + /// Fabricates a [_SwizzleConnect] that represents [agg] as the inline + /// concatenation of [elementSources] (ordered with index 0 as the LSB), and + /// registers it in [_inlineableSubmoduleMap]. + void _addSwizzleConnect(SynthLogic agg, List elementSources) { + final isNet = agg.isNet; + + // The [Swizzle] concatenates its `signals` with `signals[0]` as the MSB, + // i.e. `out = {signals[0], signals[1], ..., signals[last]}`. Our + // [elementSources] are LSB-first (index 0 is the LSB), so we reverse them + // to build the swizzle's MSB-first signal list. Only the widths and order + // of these dummy signals matter; they are throwaway placeholders since the + // real sources are mapped onto the ports below. + final dummySignals = [ + for (final source in elementSources.reversed) + isNet ? LogicNet(width: source.width) : Logic(width: source.width), + ]; + + final swizzle = _SwizzleConnect(dummySignals); + + final swizzleInst = getSynthSubModuleInstantiation(swizzle) + as SystemVerilogSynthSubModuleInstantiation; + + // [orderedInputPortNames] are in creation order (`in0`, `in1`, ...), which + // the [Swizzle] assigns to `signals.reversed`; i.e. `in0` is the LSB. So + // port `in${i}` corresponds to element `i` (LSB-first). + for (var i = 0; i < elementSources.length; i++) { + final portName = swizzle.orderedInputPortNames[i]; + final source = elementSources[i]; + if (isNet) { + swizzleInst.setInOutMapping(portName, source); + } else { + swizzleInst.setInputMapping(portName, source); + } + } + + if (isNet) { + swizzleInst.setInOutMapping(swizzle.resultSignalName, agg); + } else { + swizzleInst.setOutputMapping(swizzle.resultSignalName, agg); + } + + swizzleInst.clearInstantiation(); + + supportingModules.add(swizzle); + + _inlineableSubmoduleMap[agg] = swizzleInst; + } + /// Collapses chainable, inlineable modules after naming. void _collapseMarkedChainableModules() { // collapse multiple lines of in-line assignments into one where they are @@ -89,8 +1303,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // assign _d_and_e = d & e // assign y = _d_and_e - final synthLogicToInlineableSynthSubmoduleMap = - {}; + final synthLogicToInlineableSynthSubmoduleMap = { + ..._inlineableSubmoduleMap, + }; + final inlinedParentArrays = {}; for (final subModuleInstantiation in chainableModulesToCollapse .cast()) { // inlineable modules have only 1 result signal @@ -99,6 +1315,14 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // clear declaration of intermediate signal replaced by inline internalSignals.remove(resultSynthLogic); + // when inlining an array element, its declaration comes from the parent + // array, so clear it explicitly and remember the parent array so we can + // (potentially) drop the whole array if all its elements are inlined + if (resultSynthLogic is SynthLogicArrayElement) { + resultSynthLogic.clearDeclaration(); + inlinedParentArrays.add(resultSynthLogic.parentArray.resolved); + } + // clear declaration of instantiation for inline module subModuleInstantiation.clearInstantiation(); @@ -106,6 +1330,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { subModuleInstantiation; } + // Drop any parent array whose elements have all been inlined away, so the + // now-unused array declaration is not emitted. + _dropEmptiedArrays(inlinedParentArrays); + for (final subModuleInstantiation in subModuleInstantiations) { subModuleInstantiation as SystemVerilogSynthSubModuleInstantiation; @@ -116,6 +1344,48 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } + /// Drops the declarations of any [LogicArray] (seeded from [emptiedArrays]) + /// whose elements have all been inlined or forwarded away. + /// + /// This considers the whole array hierarchy (collecting ancestors) so that, + /// for multi-dimensional arrays, a parent array whose sub-arrays were all + /// dropped is itself dropped. A fixed-point loop is used since dropping one + /// sub-array can make its parent droppable regardless of visitation order. + void _dropEmptiedArrays(Iterable emptiedArrays) { + final candidateParentArrays = {}; + final ancestorsToCollect = [...emptiedArrays]; + while (ancestorsToCollect.isNotEmpty) { + final parentArray = ancestorsToCollect.removeLast().resolved; + if (candidateParentArrays.add(parentArray) && + parentArray is SynthLogicArrayElement) { + ancestorsToCollect.add(parentArray.parentArray); + } + } + + bool elementsAllAbsent(SynthLogic parentArray) => + parentArray.logics.every((logic) => + !(logic as LogicArray).elements.any(logicHasPresentSynthLogic)); + + var droppedAny = true; + while (droppedAny) { + droppedAny = false; + for (final parentArray in candidateParentArrays) { + if (parentArray.declarationCleared || + !parentArray.isClearable || + parentArray.isPort(module) || + parentArray.isStructPortElement(module)) { + continue; + } + + if (elementsAllAbsent(parentArray)) { + parentArray.clearDeclaration(); + internalSignals.remove(parentArray); + droppedAny = true; + } + } + } + } + /// Finds all [InlineSystemVerilog] modules where all ports are [LogicNet]s /// and which have not had their declarations cleared and replaces them with a /// [_NetConnect] assignment instead of a normal assignment. @@ -159,6 +1429,27 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } +/// A resolved view of a net [BusSubset] instantiation: the resolved `original` +/// and `subset` signals and the (non-reversed) `[start, end]` inclusive bit +/// range of the slice on the original bus. +typedef _NetSubsetView = ({ + SystemVerilogSynthSubModuleInstantiation inst, + SynthLogic original, + SynthLogic subset, + int start, + int end, +}); + +/// A resolved view of a non-net [BusSubset] used to reconstruct a packed bus +/// from an array input's element mappings. +typedef _LogicSubsetView = ({ + SystemVerilogSynthSubModuleInstantiation inst, + SynthLogic original, + SynthLogic subset, + int start, + int end, +}); + /// A special [Module] for connecting or assigning two SystemVerilog nets /// together bidirectionally. /// @@ -213,3 +1504,26 @@ module $definitionType #(parameter int WIDTH=1) (w, w); inout wire[WIDTH-1:0] w; endmodule'''; } + +/// A [Swizzle] fabricated post-build purely as instrumentation for +/// SystemVerilog generation; it is never connected to the real hardware +/// hierarchy or used in simulation. +/// +/// Used by [SystemVerilogSynthModuleDefinition._collapseAggregateConnections] +/// to render an aggregate's single use as an inline concatenation of its +/// per-element sources. +class _SwizzleConnect extends Swizzle { + /// The names of the input ports, in the same order as the `signals` passed to + /// the constructor. + late final List orderedInputPortNames; + + @override + // forced since it is generated post-build + bool get hasBuilt => true; + + _SwizzleConnect(super.signals) { + orderedInputPortNames = (isNet ? inOuts.keys : inputs.keys) + .where((name) => name != resultSignalName) + .toList(); + } +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 72471eef1..4ec0c540e 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synthesis_result.dart @@ -58,6 +58,9 @@ class SystemVerilogCustomDefinitionSynthesisResult extends SynthesisResult { /// A [SynthesisResult] representing a conversion of a [Module] to /// SystemVerilog. class SystemVerilogSynthesisResult extends SynthesisResult { + /// Configuration controlling generated SystemVerilog. + final SystemVerilogSynthesizerConfiguration configuration; + /// A cached copy of the generated ports. late final String _portsString; @@ -75,8 +78,11 @@ class SystemVerilogSynthesisResult extends SynthesisResult { _synthModuleDefinition.supportingModules; /// Creates a new [SystemVerilogSynthesisResult] for the given [module]. - SystemVerilogSynthesisResult(super.module, super.getInstanceTypeOfModule) - : _synthModuleDefinition = SystemVerilogSynthModuleDefinition(module) { + SystemVerilogSynthesisResult( + super.module, + super.getInstanceTypeOfModule, { + this.configuration = const SystemVerilogSynthesizerConfiguration(), + }) : _synthModuleDefinition = SystemVerilogSynthModuleDefinition(module) { _portsString = _verilogPorts(); _moduleContentsString = _verilogModuleContents(getInstanceTypeOfModule); _parameterString = _verilogParameters(module); @@ -111,7 +117,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { Iterable _verilogInputs() => _synthModuleDefinition.inputs.map((sig) { assert(module.tryInput(sig.name) != null, 'Named input ${sig.name} not found in module ${module.name}.'); - return 'input ${sig.definitionType()} ${sig.definitionName()}'; + return _verilogPort('input', 'wire', configuration.inputPortType, sig); }); /// Representation of all output port declarations in generated SV. @@ -119,16 +125,26 @@ class SystemVerilogSynthesisResult extends SynthesisResult { _synthModuleDefinition.outputs.map((sig) { assert(module.tryOutput(sig.name) != null, 'Named output ${sig.name} not found in module ${module.name}.'); - return 'output ${sig.definitionType()} ${sig.definitionName()}'; + return _verilogPort('output', 'var', configuration.outputPortType, sig); }); /// Representation of all inout port declarations in generated SV. Iterable _verilogInOuts() => _synthModuleDefinition.inOuts.map((sig) { assert(module.tryInOut(sig.name) != null, 'Named inOut ${sig.name} not found in module ${module.name}.'); - return 'inout ${sig.definitionType()} ${sig.definitionName()}'; + return _verilogPort('inout', 'wire', configuration.inOutPortType, sig); }); + /// Representation of a port declaration in generated SV. + String _verilogPort(String direction, String objectType, + SystemVerilogPortTypeConfiguration portType, SynthLogic sig) => + [ + direction, + if (portType.objectType == SystemVerilogPortType.explicit) objectType, + if (portType.dataType == SystemVerilogPortType.explicit) 'logic', + sig.definitionName(), + ].join(' '); + /// Representation of all internal net declarations in generated SV. String _verilogInternalSignals() { final declarations = []; @@ -143,21 +159,37 @@ class SystemVerilogSynthesisResult extends SynthesisResult { /// Representation of all assignments in generated SV. String _verilogAssignments() { final assignmentLines = []; + String rangeString(int upperIndex, int lowerIndex) => + upperIndex == lowerIndex + ? '[$upperIndex]' + : '[$upperIndex:$lowerIndex]'; + for (final assignment in _synthModuleDefinition.assignments) { assert( !(assignment.src.isNet && assignment.dst.isNet), 'Net connections should have been implemented as' ' bidirectional net connections.'); - var sliceString = ''; - if (assignment is PartialSynthAssignment && assignment.width > 1) { - sliceString = assignment.dstUpperIndex == assignment.dstLowerIndex - ? '[${assignment.dstUpperIndex}]' - : '[${assignment.dstUpperIndex}:${assignment.dstLowerIndex}]'; + var dstSliceString = ''; + var srcSliceString = ''; + if (assignment is RangeSynthAssignment) { + dstSliceString = rangeString( + assignment.dstUpperIndex, + assignment.dstLowerIndex, + ); + srcSliceString = rangeString( + assignment.srcUpperIndex, + assignment.srcLowerIndex, + ); + } else if (assignment is PartialSynthAssignment && assignment.width > 1) { + dstSliceString = rangeString( + assignment.dstUpperIndex, + assignment.dstLowerIndex, + ); } - assignmentLines.add('assign ${assignment.dst.name}$sliceString' - ' = ${assignment.src.name};'); + assignmentLines.add('assign ${assignment.dst.name}$dstSliceString' + ' = ${assignment.src.name}$srcSliceString;'); } return assignmentLines.join('\n'); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index 062647ac3..5aea3cf58 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -15,6 +15,14 @@ import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synthesis_resu /// /// Attempts to maintain signal naming and structure as much as possible. class SystemVerilogSynthesizer extends Synthesizer { + /// Configuration controlling generated SystemVerilog. + final SystemVerilogSynthesizerConfiguration configuration; + + /// Creates a SystemVerilog synthesizer with the specified [configuration]. + SystemVerilogSynthesizer({ + this.configuration = const SystemVerilogSynthesizerConfiguration(), + }); + @override bool generatesDefinition(Module module) => // ignore: deprecated_member_use_from_same_package @@ -147,6 +155,10 @@ class SystemVerilogSynthesizer extends Synthesizer { module.generatedDefinitionType == DefinitionGenerationType.custom ? SystemVerilogCustomDefinitionSynthesisResult( module, getInstanceTypeOfModule) - : SystemVerilogSynthesisResult(module, getInstanceTypeOfModule); + : SystemVerilogSynthesisResult( + module, + getInstanceTypeOfModule, + configuration: configuration, + ); } } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart new file mode 100644 index 000000000..876f20298 --- /dev/null +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart @@ -0,0 +1,58 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_synthesizer_configuration.dart +// Configuration for SystemVerilog synthesis. +// +// 2026 July 10 +// Author: Max Korbel + +/// Controls whether a type is included in a SystemVerilog port declaration. +enum SystemVerilogPortType { + /// The type is included in the port declaration. + explicit, + + /// The type is inferred according to SystemVerilog's defaults. + implicit, +} + +/// Configuration for types in a SystemVerilog port declaration. +class SystemVerilogPortTypeConfiguration { + /// Whether the object type, such as `wire` or `var`, is explicit. + final SystemVerilogPortType objectType; + + /// Whether the data type, such as `logic`, is explicit. + final SystemVerilogPortType dataType; + + /// Creates a new configuration for types in a SystemVerilog port + /// declaration. + const SystemVerilogPortTypeConfiguration({ + this.objectType = SystemVerilogPortType.explicit, + this.dataType = SystemVerilogPortType.explicit, + }); +} + +/// Configuration for SystemVerilog synthesis. +class SystemVerilogSynthesizerConfiguration { + /// Type configuration for input ports. + final SystemVerilogPortTypeConfiguration inputPortType; + + /// Type configuration for output ports. + final SystemVerilogPortTypeConfiguration outputPortType; + + /// Type configuration for inout ports. + final SystemVerilogPortTypeConfiguration inOutPortType; + + /// Creates a new configuration for SystemVerilog synthesis. + const SystemVerilogSynthesizerConfiguration({ + this.inputPortType = const SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + ), + this.outputPortType = const SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + ), + this.inOutPortType = const SystemVerilogPortTypeConfiguration( + dataType: SystemVerilogPortType.implicit, + ), + }); +} diff --git a/lib/src/synthesizers/utilities/synth_assignment.dart b/lib/src/synthesizers/utilities/synth_assignment.dart index 945558eab..aaafda601 100644 --- a/lib/src/synthesizers/utilities/synth_assignment.dart +++ b/lib/src/synthesizers/utilities/synth_assignment.dart @@ -74,3 +74,32 @@ class PartialSynthAssignment extends SynthAssignment { @override String toString() => '$dst[$dstUpperIndex:$dstLowerIndex] <= $src'; } + +/// Represents an assignment from a partial source to a partial destination. +class RangeSynthAssignment extends PartialSynthAssignment { + /// The upper index of the source. + int srcUpperIndex; + + /// The lower index of the source. + int srcLowerIndex; + + /// The width of the source and destination ranges. + @override + int get width => dstUpperIndex - dstLowerIndex + 1; + + @override + bool _checkWidths() => width == srcUpperIndex - srcLowerIndex + 1; + + /// Constructs a representation of a range-to-range assignment. + RangeSynthAssignment(super._src, super._dst, + {required this.srcUpperIndex, + required this.srcLowerIndex, + required super.dstUpperIndex, + required super.dstLowerIndex}) + : assert(srcLowerIndex >= 0, 'Invalid source lower index'), + assert(srcUpperIndex < _src.width, 'Invalid source upper index'); + + @override + String toString() => '$dst[$dstUpperIndex:$dstLowerIndex] <= ' + '$src[$srcUpperIndex:$srcLowerIndex]'; +} diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 19a1ad3d9..d5fe10223 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -417,6 +417,70 @@ class SynthLogic { } } +/// A non-owning reference to one bit of a packed [SynthLogic]. +/// +/// This exists for port mappings that must render an indexed packed signal, +/// such as `.result(bus[7])`. It has no declaration or independently selected +/// name; [name] is always derived from [packedBase] and [bitIndex]. +class SynthLogicPackedBitReference extends SynthLogic { + /// The packed signal containing the referenced bit. + final SynthLogic packedBase; + + /// The bit selected from [packedBase]. + final int bitIndex; + + /// Creates a reference to [bitIndex] of [packedBase]. + SynthLogicPackedBitReference( + this.packedBase, + this.bitIndex, { + required super.parentSynthModuleDefinition, + }) : assert( + !packedBase.isArray, 'Packed reference base must not be an array.'), + assert(!packedBase.isNet, 'Packed reference base must not be a net.'), + assert( + !packedBase.isConstant, + 'Packed reference base must not be a constant.', + ), + assert(bitIndex >= 0, 'Packed reference index must not be negative.'), + assert( + bitIndex < packedBase.width, + 'Packed reference index must fit within its base.', + ), + super(Logic()); + + @override + bool get needsDeclaration => false; + + @override + bool get mergeable => false; + + @override + bool isPort([Module? module]) => packedBase.resolved.isPort(module); + + @override + bool hasSrcConnectionsPresent() => + packedBase.resolved.hasSrcConnectionsPresent(); + + @override + bool hasDstConnectionsPresent() => + packedBase.resolved.hasDstConnectionsPresent(); + + @override + String get name { + final resolvedBase = packedBase.resolved; + assert( + bitIndex < resolvedBase.width, + 'Packed reference index must fit within its resolved base.', + ); + final reference = '${resolvedBase.name}[$bitIndex]'; + assert( + Sanitizer.isSanitary(resolvedBase.name), + 'Packed reference base should be sanitary, but found $reference.', + ); + return reference; + } +} + /// Represents an element of a [LogicArray]. /// /// Does not fully override or properly implement all characteristics of @@ -478,8 +542,8 @@ class SynthLogicArrayElement extends SynthLogic { @override String get name { - final parentArrayname = parentArray.replacement?.name ?? parentArray.name; - final n = '$parentArrayname[${logic.arrayIndex!}]'; + final parentArrayName = parentArray.replacement?.name ?? parentArray.name; + final n = '$parentArrayName[${logic.arrayIndex!}]'; assert( Sanitizer.isSanitary( n.substring(0, n.contains('[') ? n.indexOf('[') : null), diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 23a36e8ed..5cd689882 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -16,10 +16,104 @@ import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; -/// Adapts structure slicing to the shared synthesis operation. -class _BusSubsetForStructSlice extends SynthStructureSlice { - _BusSubsetForStructSlice(super.bus, super.startIndex, super.endIndex, - {required super.destination}); +/// 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, { + 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; +} + +/// A packed range of a base [SynthLogic], inclusive of [lower] and [upper]. +class _SynthRangeRef { + /// The signal whose packed range is referenced. + final SynthLogic base; + + /// The lower index of the range. + final int lower; + + /// The upper index of the range. + final int upper; + + /// The number of bits in this range. + int get width => upper - lower + 1; + + /// Creates a range reference. + const _SynthRangeRef(this.base, this.lower, this.upper) + : assert(lower >= 0, 'Invalid lower index'), + assert(upper >= lower, 'Invalid upper index'); + + /// Creates a range reference if the bounds are valid for [base]. + static _SynthRangeRef? tryCreate(SynthLogic base, int lower, int upper) { + if (lower < 0 || upper < lower || upper >= base.width) { + return null; + } + return _SynthRangeRef(base, lower, upper); + } + + /// Whether [other] is fully contained in this range. + bool contains(_SynthRangeRef other) => + base == other.base && other.lower >= lower && other.upper <= upper; +} + +/// Submodule users indexed both by an exact mapped signal and by the mapped +/// signal's array reference base. +typedef _SubmoduleSignalUseIndex = ({ + Map> exact, + Map> byReferenceBase, +}); + +/// Yields maximal contiguous runs in [sortedItems] according to +/// [continuesRun]. +Iterable<({int start, int end})> _contiguousRuns( + List sortedItems, + bool Function(T previous, T current) continuesRun, +) sync* { + if (sortedItems.isEmpty) { + return; + } + + var start = 0; + for (var index = 1; index < sortedItems.length; index++) { + if (!continuesRun(sortedItems[index - 1], sortedItems[index])) { + yield (start: start, end: index - 1); + start = index; + } + } + yield (start: start, end: sortedItems.length - 1); +} + +/// Groups [assignments] by the key selected by [keyOf]. +Map> _assignmentsBy( + Iterable assignments, + K Function(SynthAssignment assignment) keyOf, +) { + final assignmentsByKey = >{}; + for (final assignment in assignments) { + assignmentsByKey.putIfAbsent(keyOf(assignment), () => []).add(assignment); + } + return assignmentsByKey; } /// Represents the definition of a module. @@ -72,8 +166,10 @@ class SynthModuleDefinition { // 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. + /// Submodules that should claim names after emitted objects. final Set _weakNameClaimSubmodules = {}; + /// Signals that should claim names after emitted objects. final Set _weakNameClaimSignals = {}; /// Indicates that [m] is a submodule used within this definition. @@ -294,11 +390,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) @@ -492,7 +588,10 @@ class SynthModuleDefinition { // as completely undriven). // make a new const node, it will merge away if not needed - final newReceiverConst = getSynthLogic(Const(receiver.value))!; + final newReceiverConst = getSynthLogic(Const( + receiver.value, + preferredRadix: receiver.preferredRadix, + ))!; internalSignals.add(newReceiverConst); assignments.add(SynthAssignment(newReceiverConst, synthReceiver)); } @@ -514,10 +613,18 @@ class SynthModuleDefinition { // The order of these is important! _collapseArrays(); + _collapseSimpleRangeAssignments(); + _collapseWideArrayElementRangeSources(); + _collapseChainedRangeAssignments(); + _collapseGeneratedSubsetSwizzleRangeAssignments(); + _collapseConstantBackedRangeIntermediates(); _collapseAssignments(); _assignSubmodulePortMapping(); _pruneUnused(); + _collapseConstantBackedRangeIntermediates(); + _pruneUnused(); + _pruneClearedSubsetConstantSources(); // Naming has two base-owned phases: mark likely-collapsed objects as weak // name claimants, then pick names. After that, synthesizers may @@ -706,13 +813,65 @@ class SynthModuleDefinition { } } - return inlineableSubmoduleInstantiations.where((subModuleInstantiation) { - final resultSynthLogic = _inlineResultLogic(subModuleInstantiation); + final collapseCandidates = inlineableSubmoduleInstantiations.where( + (subModuleInstantiation) { + final resultSynthLogic = _inlineResultLogic(subModuleInstantiation); + + return resultSynthLogic != null && + singleUseSignals.contains(resultSynthLogic) && + subModuleInstantiation.needsInstantiation; + }, + ).toList(); + + // Keep every module in an inline dependency cycle materialized so that + // rendering expressions cannot recurse through the cycle indefinitely. + final candidateByResult = { + for (final candidate in collapseCandidates) + _inlineResultLogic(candidate)!.resolved: candidate, + }; + final visited = {}; + final activeIndices = {}; + final activePath = []; + final cyclicCandidates = {}; + + void findCycles(SynthSubModuleInstantiation candidate) { + visited.add(candidate); + activeIndices[candidate] = activePath.length; + activePath.add(candidate); + + final resultSignalName = + (candidate.module as InlineSystemVerilog).resultSignalName; + for (final input in [ + ...candidate.inputMapping.values, + ...candidate.inOutMapping.entries + .where((entry) => entry.key != resultSignalName) + .map((entry) => entry.value), + ]) { + final dependency = candidateByResult[input.resolved]; + if (dependency == null) { + continue; + } - return resultSynthLogic != null && - singleUseSignals.contains(resultSynthLogic) && - subModuleInstantiation.needsInstantiation; - }); + final cycleStart = activeIndices[dependency]; + if (cycleStart != null) { + cyclicCandidates.addAll(activePath.skip(cycleStart)); + } else if (!visited.contains(dependency)) { + findCycles(dependency); + } + } + + activePath.removeLast(); + activeIndices.remove(candidate); + } + + for (final candidate in collapseCandidates) { + if (!visited.contains(candidate)) { + findCycles(candidate); + } + } + + return collapseCandidates + .where((candidate) => !cyclicCandidates.contains(candidate)); } SynthLogic? _inlineResultLogic(SynthSubModuleInstantiation instantiation) { @@ -754,6 +913,10 @@ class SynthModuleDefinition { // - assignments that are removable: // - the driver has no driver OR the receiver has no receivers + final assignmentConnectedSubmoduleMappingSignals = + _assignmentConnectedSubmoduleMappingSignals(); + final sharedSubmoduleMappingSignals = _sharedSubmoduleMappingSignals(); + final reducedInternalSignals = []; for (final internalSignal in internalSignals) { // if it's cleared already, just skip it @@ -780,6 +943,13 @@ class SynthModuleDefinition { continue; } + if (assignmentConnectedSubmoduleMappingSignals + .contains(internalSignal.resolved) || + sharedSubmoduleMappingSignals.contains(internalSignal.resolved)) { + reducedInternalSignals.add(internalSignal); + continue; + } + final logics = internalSignal.logics; if (internalSignal.isArray) { @@ -964,12 +1134,15 @@ class SynthModuleDefinition { /// Updates all sub-module instantiations with information about which /// [SynthLogic] should be used for their ports. void _assignSubmodulePortMapping() { + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + for (final submoduleInstantiation in subModuleInstantiations) { for (final inputName in submoduleInstantiation.module.inputs.keys) { final orig = submoduleInstantiation.inputMapping[inputName]!; submoduleInstantiation.setInputMapping( inputName, - orig.replacement ?? orig, + _resolvedSubmoduleInputMapping(orig, assignmentsByDestination), replace: true, ); } @@ -994,14 +1167,249 @@ class SynthModuleDefinition { } } - /// Picks names of signals and sub-modules. - /// + /// Resolves a submodule input mapping through any replacement and, when the + /// mapped signal is fully driven by a packed scalar assignment, through that + /// driver as well. + SynthLogic _resolvedSubmoduleInputMapping( + SynthLogic mappedSignal, + Map> assignmentsByDestination, + ) { + final resolved = mappedSignal.replacement ?? mappedSignal; + return _fullWidthInputMappingSource(resolved, assignmentsByDestination) ?? + resolved; + } + + /// Returns the source of a single full-width scalar [RangeSynthAssignment] + /// into [mappedSignal], or `null` when the mapped signal must remain named. + SynthLogic? _fullWidthInputMappingSource( + SynthLogic mappedSignal, + Map> assignmentsByDestination, + ) { + final drivers = assignmentsByDestination[mappedSignal.resolved]; + if (drivers == null || drivers.length != 1) { + return null; + } + + final driver = drivers.single; + if (driver is! RangeSynthAssignment) { + return null; + } + + final sourceRange = _assignmentSourceRange(driver); + final destinationRange = _assignmentDestinationRange(driver); + if (destinationRange.base != mappedSignal.resolved || + destinationRange.lower != 0 || + destinationRange.upper != mappedSignal.width - 1 || + sourceRange.width != mappedSignal.width || + sourceRange.lower != 0 || + sourceRange.upper != sourceRange.base.width - 1 || + sourceRange.base.isArray) { + return null; + } + + return sourceRange.base; + } + + /// Finds submodule mapping signals that participate in scalar or packed range + /// assignments across a submodule boundary and therefore must not be pruned + /// before rendering. + Set _assignmentConnectedSubmoduleMappingSignals() { + final submoduleInputMappingReferences = + _submoduleMappingReferences(includeInputs: true, includeOutputs: false); + final submoduleOutputMappingReferences = + _submoduleMappingReferences(includeInputs: false, includeOutputs: true); + final assignmentConnectedSignals = {}; + + var foundInlineDependency = true; + while (foundInlineDependency) { + foundInlineDependency = false; + for (final instantiation in subModuleInstantiations) { + final inlineModule = instantiation.module; + if (inlineModule is! BusSubset || inlineModule.original.isNet) { + continue; + } + final mappedOutputs = [ + ...instantiation.outputMapping.values, + ...instantiation.inOutMapping.values, + ]; + if (!mappedOutputs.any( + (mapped) => _isPackedBitArrayElement(mapped.resolved), + )) { + continue; + } + final outputReferences = { + for (final mapped in mappedOutputs) mapped.resolved, + for (final mapped in mappedOutputs) _referenceBase(mapped.resolved), + }; + if (outputReferences + .every((ref) => !submoduleInputMappingReferences.contains(ref))) { + continue; + } + for (final mapped in [ + ...instantiation.inputMapping.values, + ...instantiation.inOutMapping.values, + ]) { + final resolved = mapped.resolved; + foundInlineDependency |= + submoduleInputMappingReferences.add(resolved); + foundInlineDependency |= + submoduleInputMappingReferences.add(_referenceBase(resolved)); + } + } + } + + for (final assignment in assignments) { + final sourceRange = _assignmentSourceRange(assignment); + final destinationRange = _assignmentDestinationRange(assignment); + + final sourceBase = sourceRange.base.resolved; + final destinationBase = destinationRange.base.resolved; + final destinationReferenceBase = _referenceBase(destinationBase); + final sourceIsMappedOutput = + submoduleOutputMappingReferences.contains(sourceBase); + + if (submoduleInputMappingReferences.contains(destinationBase) && + sourceIsMappedOutput) { + assignmentConnectedSignals.add(destinationBase); + } else if (submoduleInputMappingReferences.contains(destinationBase) && + !destinationReferenceBase.isArray && + !sourceBase.isArray) { + assignmentConnectedSignals.add(destinationBase); + } + if (sourceIsMappedOutput) { + assignmentConnectedSignals.add(sourceBase); + } + } + + return assignmentConnectedSignals; + } + + /// Finds signals that are used by multiple active submodule port mappings. + /// After full assignment merging, these shared mapping signals may be the + /// only remaining evidence that a parent-level wire is still required. + Set _sharedSubmoduleMappingSignals() { + final inputMappingUseCounts = {}; + final outputMappingUseCounts = {}; + final inOutMappingUseCounts = {}; + + void addReferences(Map useCounts, SynthLogic signal) { + final resolved = signal.resolved; + for (final reference in {resolved, _referenceBase(resolved)}) { + useCounts.update( + reference, + (count) => count + 1, + ifAbsent: () => 1, + ); + } + } + + for (final submoduleInstantiation in subModuleInstantiations) { + if (!submoduleInstantiation.needsInstantiation || + submoduleInstantiation.module is InlineSystemVerilog) { + continue; + } + + for (final signal in submoduleInstantiation.inputMapping.values) { + addReferences(inputMappingUseCounts, signal); + } + for (final signal in submoduleInstantiation.outputMapping.values) { + addReferences(outputMappingUseCounts, signal); + } + for (final signal in submoduleInstantiation.inOutMapping.values) { + addReferences(inOutMappingUseCounts, signal); + } + } + + final mappingReferences = { + ...inputMappingUseCounts.keys, + ...outputMappingUseCounts.keys, + ...inOutMappingUseCounts.keys, + }; + + final sharedMappingSignals = {}; + for (final reference in mappingReferences) { + final inputUses = inputMappingUseCounts[reference] ?? 0; + final outputUses = outputMappingUseCounts[reference] ?? 0; + final inOutUses = inOutMappingUseCounts[reference] ?? 0; + + if ((inputUses > 0 && outputUses > 0) || + (inOutUses > 0 && (inputUses > 0 || outputUses > 0)) || + inOutUses > 1) { + sharedMappingSignals.add(reference); + } + } + + return sharedMappingSignals; + } + + /// Collects active submodule mapping signals plus their reference bases, so + /// array elements and aggregate mappings compare consistently. + Set _submoduleMappingReferences({ + required bool includeInputs, + required bool includeOutputs, + }) { + final mappedReferences = {}; + + for (final submoduleInstantiation in subModuleInstantiations) { + if (!submoduleInstantiation.needsInstantiation || + submoduleInstantiation.module is InlineSystemVerilog) { + continue; + } + + final mappedSignals = [ + if (includeInputs) ...submoduleInstantiation.inputMapping.values, + if (includeOutputs) ...submoduleInstantiation.outputMapping.values, + ...submoduleInstantiation.inOutMapping.values, + ]; + for (final mappedSignal in mappedSignals) { + final resolved = mappedSignal.resolved; + mappedReferences + ..add(resolved) + ..add(_referenceBase(resolved)); + } + } + + return mappedReferences; + } + + /// Indexes every submodule port mapping by its exact signal and, for array + /// elements, by the direct parent array used for aggregate comparisons. + _SubmoduleSignalUseIndex _submoduleSignalUseIndex() { + final exact = >{}; + final byReferenceBase = >{}; + for (final instantiation in subModuleInstantiations) { + for (final mappedSignal in { + ...instantiation.inputMapping.values, + ...instantiation.outputMapping.values, + ...instantiation.inOutMapping.values, + }) { + final resolved = mappedSignal.resolved; + exact.putIfAbsent(resolved, () => {}).add(instantiation); + byReferenceBase + .putIfAbsent(_referenceBase(resolved), () => {}) + .add(instantiation); + } + } + return (exact: exact, byReferenceBase: byReferenceBase); + } + + /// Whether [signal] is an element of a one-dimensional packed bit array. + bool _isPackedBitArrayElement(SynthLogic signal) { + if (signal is! SynthLogicArrayElement) { + return false; + } + final parentLogic = signal.parentArray.resolved.logics.singleOrNull; + return parentLogic is LogicArray && + parentLogic.dimensions.length == 1 && + parentLogic.elementWidth == 1 && + parentLogic.numUnpackedDimensions == 0; + } + /// 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() { - // 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: @@ -1021,15 +1429,12 @@ class SynthModuleDefinition { for (final inOut in inOuts) { inOut.pickName(); } - // Reserved submodule instances first (they assert their exact name). 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.'); } } @@ -1074,8 +1479,8 @@ class SynthModuleDefinition { } } - /// Merges bit blasted array assignments into one single assignment when - /// it's full array-full array assignment + /// Merges bit blasted array assignments into fewer assignments when they are + /// full array-to-array assignments or contiguous same-offset array ranges. void _collapseArrays() { final boringArrayPairs = <(SynthLogic, SynthLogic)>[]; @@ -1091,15 +1496,13 @@ class SynthModuleDefinition { final dst = assignment.dst; if (src is SynthLogicArrayElement && dst is SynthLogicArrayElement) { - final srcArray = src.parentArray; - final dstArray = dst.parentArray; + final srcArray = src.parentArray.resolved; + final dstArray = dst.parentArray.resolved; assert(srcArray.logics.length == 1, 'should be 1 name for the array'); assert(dstArray.logics.length == 1, 'should be 1 name for the array'); - if (srcArray.logics.first.elements.length != - dstArray.logics.first.elements.length || - boringArrayPairs.contains((srcArray, dstArray))) { + if (boringArrayPairs.contains((srcArray, dstArray))) { reducedAssignments.add(assignment); } else { groupedAssignments[(srcArray, dstArray)] ??= []; @@ -1112,14 +1515,9 @@ 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', - ); - // first requirement is that all elements have been assigned - var shouldMerge = + var shouldMerge = srcArray.logics.first.elements.length == + dstArray.logics.first.elements.length && arrAssignments.length == srcArray.logics.first.elements.length; if (shouldMerge) { @@ -1140,7 +1538,9 @@ class SynthModuleDefinition { if (shouldMerge) { reducedAssignments.add(SynthAssignment(srcArray, dstArray)); } else { - reducedAssignments.addAll(arrAssignments); + reducedAssignments.addAll( + _collapseArrayElementRanges(srcArray, dstArray, arrAssignments), + ); boringArrayPairs.add((srcArray, dstArray)); } } @@ -1152,6 +1552,1843 @@ class SynthModuleDefinition { } } + /// Collapses contiguous same-offset element assignments into range + /// assignments, when the array shapes are simple enough to render as packed + /// ranges across the first dimension. + List _collapseArrayElementRanges( + SynthLogic srcArray, + SynthLogic dstArray, + List arrAssignments, + ) { + if (!_canCollapseArrayElementRanges(srcArray, dstArray) || + arrAssignments.length < 2) { + return arrAssignments; + } + + int srcIndex(SynthAssignment assignment) => + (assignment.src as SynthLogicArrayElement).logic.arrayIndex!; + int dstIndex(SynthAssignment assignment) => + (assignment.dst as SynthLogicArrayElement).logic.arrayIndex!; + + final seenDstIndices = {}; + final assignmentsByOffset = >{}; + for (final assignment in arrAssignments) { + final srcElementIndex = srcIndex(assignment); + final dstElementIndex = dstIndex(assignment); + if (!seenDstIndices.add(dstElementIndex)) { + return arrAssignments; + } + assignmentsByOffset + .putIfAbsent(dstElementIndex - srcElementIndex, () => []) + .add(assignment); + } + + final collapsedAssignments = <({int dstLowerIndex, SynthAssignment a})>[]; + + void addRun(List group, int start, int end) { + if (end == start) { + final assignment = group[start]; + collapsedAssignments.add(( + dstLowerIndex: dstIndex(assignment), + a: assignment, + )); + return; + } + + final first = group[start]; + final last = group[end]; + collapsedAssignments.add(( + dstLowerIndex: dstIndex(first), + a: RangeSynthAssignment( + srcArray, + dstArray, + srcUpperIndex: srcIndex(last), + srcLowerIndex: srcIndex(first), + dstUpperIndex: dstIndex(last), + dstLowerIndex: dstIndex(first), + ), + )); + } + + for (final group in assignmentsByOffset.values) { + group.sort((a, b) => dstIndex(a).compareTo(dstIndex(b))); + + for (final run in _contiguousRuns( + group, + (previous, current) => + dstIndex(current) == dstIndex(previous) + 1 && + srcIndex(current) == srcIndex(previous) + 1, + )) { + addRun(group, run.start, run.end); + } + } + + collapsedAssignments.sort( + (a, b) => a.dstLowerIndex.compareTo(b.dstLowerIndex), + ); + return [for (final assignment in collapsedAssignments) assignment.a]; + } + + /// Whether [srcArray] and [dstArray] are safe for range assignment collapse. + bool _canCollapseArrayElementRanges( + SynthLogic srcArray, + SynthLogic dstArray, + ) { + if (srcArray == dstArray || srcArray.isNet || dstArray.isNet) { + return false; + } + if (srcArray.logics.length != 1 || dstArray.logics.length != 1) { + return false; + } + final srcLogic = srcArray.logics.first; + final dstLogic = dstArray.logics.first; + if (srcLogic is! LogicArray || dstLogic is! LogicArray) { + return false; + } + + return srcLogic.dimensions.length == 1 && + dstLogic.dimensions.length == 1 && + srcLogic.elementWidth == 1 && + dstLogic.elementWidth == 1 && + srcLogic.numUnpackedDimensions == 0 && + dstLogic.numUnpackedDimensions == 0; + } + + /// Width-oriented collapse of bit-blasted assignments into packed ranges. + /// + /// This pass looks for individual one-bit assignments that are really pieces + /// of the same packed connection. For example, a sequence like + /// `dst[1] <= src[13]`, `dst[2] <= src[14]`, ... can become one + /// `dst[4:1] <= src[16:13]` [RangeSynthAssignment]. Sources may be direct + /// array elements or temporary [BusSubset] outputs; temporary outputs are + /// traced back to the original packed base before grouping. + /// + /// Candidates are grouped by source base, destination base, and constant + /// index offset. A group is then split into contiguous runs, so the pass + /// grows assignments in width without composing through arbitrary depth. + /// Depth composition through intermediates is handled by + /// [_collapseChainedRangeAssignments]. + void _collapseSimpleRangeAssignments() { + final busSubsetRanges = _busSubsetSourceRanges(); + if (busSubsetRanges.isEmpty) { + return; + } + + final generatedSubsetSets = _generatedSubsetIntermediateSets(); + final generatedSubsetCandidates = generatedSubsetSets.candidates; + final generatedSubsetIntermediates = generatedSubsetSets.intermediates; + + final swizzleInputSignals = { + for (final instantiation in subModuleInstantiations) + if (instantiation.module is Swizzle && + !(instantiation.module as Swizzle).isNet) + ...instantiation.inputMapping.values.map((signal) => signal.resolved), + }; + final assignmentsBySourceBase = _assignmentsBy( + assignments, (assignment) => _referenceBase(assignment.src)); + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + final assignmentsBySource = + _assignmentsBy(assignments, (assignment) => assignment.src.resolved); + final submoduleSignalUses = _submoduleSignalUseIndex(); + final knownSourceRanges = { + for (final entry in busSubsetRanges.entries) entry.key: entry.value.range, + }; + + final reducedAssignments = []; + final groupedCandidates = <(SynthLogic, SynthLogic, int), + List< + ({ + SynthAssignment assignment, + _SynthRangeRef src, + _SynthRangeRef dst, + SynthSubModuleInstantiation? sourceSubmodule, + SynthLogic? sourceSignal, + })>>{}; + + for (final assignment in assignments) { + if (assignment is PartialSynthAssignment) { + reducedAssignments.add(assignment); + continue; + } + + final src = _simpleAssignmentSourceRange(assignment, busSubsetRanges); + final resolvedSrc = src == null + ? null + : ( + range: _resolveKnownRangeThroughFullWidthDrivers( + src.range, + assignmentsByDestination, + knownSourceRanges, + ), + sourceSubmodule: src.sourceSubmodule, + sourceSignal: src.sourceSignal, + ); + final dst = _simpleAssignmentDestinationRange(assignment); + // A candidate is one bit of a future cross-object range assignment: + // `dstBase[dstBit] <= srcBase[srcBit]`. The bases must be different; + // this pass is packing plumbing between objects, not proving that an + // overlapping intra-object move like `a[4:1] <= a[16:13]` is safe. + if (resolvedSrc == null || + dst == null || + resolvedSrc.range.width != 1 || + dst.width != 1 || + resolvedSrc.range.base == dst.base || + (resolvedSrc.sourceSubmodule != null && + resolvedSrc.range.lower != dst.lower && + !generatedSubsetIntermediates.contains(dst.base)) || + (resolvedSrc.sourceSubmodule != null && + !_hasOnlySwizzleConsumers( + dst.base, + assignmentsBySourceBase, + swizzleInputSignals, + )) || + !_canUsePackedRangeSource(resolvedSrc.range.base) || + !_canUsePackedRangeBase(dst.base)) { + reducedAssignments.add(assignment); + continue; + } + + // Keep only assignments that share a stable bit offset. Each group can + // later become a packed range assignment such as + // `dstBase[4:1] <= srcBase[16:13]`. + groupedCandidates.putIfAbsent(( + resolvedSrc.range.base, + dst.base, + dst.lower - resolvedSrc.range.lower + ), () => []).add(( + assignment: assignment, + src: resolvedSrc.range, + dst: dst, + sourceSubmodule: resolvedSrc.sourceSubmodule, + sourceSignal: resolvedSrc.sourceSignal, + )); + } + + // Generated `assignSubset` arrays need full-coverage accounting. If only + // part of such an array is collapsed away, the remaining helper can change + // undriven/floating behavior. A fully covered helper can be replaced as a + // single range safely; partial helpers stay conservative unless they are + // known generated intermediates with live sources. + final fullyCoveredGeneratedSubsets = {}; + final generatedSubsetBits = >{}; + for (final group in groupedCandidates.values) { + for (final candidate in group) { + if (generatedSubsetCandidates.contains(candidate.dst.base)) { + generatedSubsetBits + .putIfAbsent(candidate.dst.base, () => {}) + .add(candidate.dst.lower); + } + } + } + for (final entry in generatedSubsetBits.entries) { + if (entry.value.length == entry.key.width && + Iterable.generate(entry.key.width).every(entry.value.contains)) { + fullyCoveredGeneratedSubsets.add(entry.key); + } + } + + var changed = false; + for (final group in groupedCandidates.values) { + group.sort((a, b) => a.dst.lower.compareTo(b.dst.lower)); + + for (final run in _contiguousRuns( + group, + (previous, current) => + current.dst.lower == previous.dst.lower + 1 && + current.src.lower == previous.src.lower + 1, + )) { + changed |= _addSimpleRangeRun( + reducedAssignments, + group, + run.start, + run.end, + assignmentsBySource, + generatedSubsetCandidates, + generatedSubsetIntermediates, + fullyCoveredGeneratedSubsets, + submoduleSignalUses, + ); + } + } + + if (changed) { + assignments + ..clear() + ..addAll(reducedAssignments); + } + } + + /// Whether every assignment consuming [base] feeds a [Swizzle] input. + bool _hasOnlySwizzleConsumers( + SynthLogic base, + Map> assignmentsBySourceBase, + Set swizzleInputSignals, + ) { + final consumers = assignmentsBySourceBase[base] ?? const []; + return consumers.isNotEmpty && + consumers.every( + (assignment) => swizzleInputSignals.contains(assignment.dst.resolved), + ); + } + + /// Adds one contiguous run from [_collapseSimpleRangeAssignments]. + /// + /// Single-bit runs are left alone because there is no width to recover. Wider + /// runs become [RangeSynthAssignment]s. When the source bits came from a + /// temporary [BusSubset], the temporary module and signal are cleared only if + /// every remaining use is covered by the assignments being replaced. + bool _addSimpleRangeRun( + List reducedAssignments, + List< + ({ + SynthAssignment assignment, + _SynthRangeRef src, + _SynthRangeRef dst, + SynthSubModuleInstantiation? sourceSubmodule, + SynthLogic? sourceSignal, + })> + group, + int start, + int end, + Map> assignmentsBySource, + Set generatedSubsetCandidates, + Set generatedSubsetIntermediates, + Set fullyCoveredGeneratedSubsets, + _SubmoduleSignalUseIndex submoduleSignalUses, + ) { + if (start == end) { + reducedAssignments.add(group[start].assignment); + return false; + } + + final first = group[start]; + final last = group[end]; + if (generatedSubsetCandidates.contains(first.dst.base) && + !fullyCoveredGeneratedSubsets.contains(first.dst.base) && + (!generatedSubsetIntermediates.contains(first.dst.base) || + !group.getRange(start, end + 1).every((candidate) => + _canPartiallyCollapseGeneratedSubsetSource( + candidate.src.base)))) { + for (var index = start; index <= end; index++) { + reducedAssignments.add(group[index].assignment); + } + return false; + } + + // Selecting a range directly from a literal is not legal SystemVerilog. + // A run that covers the whole constant instead becomes a partial + // destination assignment, such as `dst[6:4] = 3'h0`. + reducedAssignments.add(first.src.base.isConstant && + first.src.lower == 0 && + last.src.upper == first.src.base.width - 1 + ? PartialSynthAssignment( + first.src.base, + first.dst.base, + dstUpperIndex: last.dst.upper, + dstLowerIndex: first.dst.lower, + ) + : RangeSynthAssignment( + first.src.base, + first.dst.base, + srcUpperIndex: last.src.upper, + srcLowerIndex: first.src.lower, + dstUpperIndex: last.dst.upper, + dstLowerIndex: first.dst.lower, + )); + final replacedAssignments = { + for (var index = start; index <= end; index++) group[index].assignment, + }; + for (var index = start; index <= end; index++) { + final sourceSignal = group[index].sourceSignal?.resolved; + final sourceSubmodule = group[index].sourceSubmodule; + if (sourceSignal != null && + sourceSubmodule != null && + _canClearReplacedSourceSignal( + sourceSignal, + assignmentsBySource, + replacedAssignments, + submoduleSignalUses: submoduleSignalUses, + allowedInstantiation: sourceSubmodule, + )) { + sourceSubmodule.clearInstantiation(); + sourceSignal.clearDeclaration(); + } + } + return true; + } + + /// Whether a replaced temporary source has no assignment or submodule users + /// outside [replacedAssignments] and [allowedInstantiation]. + bool _canClearReplacedSourceSignal( + SynthLogic sourceSignal, + Map> assignmentsBySource, + Set replacedAssignments, { + required _SubmoduleSignalUseIndex submoduleSignalUses, + required SynthSubModuleInstantiation allowedInstantiation, + }) { + final assignmentUsers = assignmentsBySource[sourceSignal] ?? const []; + return assignmentUsers.every(replacedAssignments.contains) && + !_hasSubmoduleSignalUse( + sourceSignal, + submoduleSignalUses, + allowedInstantiation: allowedInstantiation, + ); + } + + /// Whether [signal] is mapped by an instantiation other than + /// [allowedInstantiation]. + bool _hasSubmoduleSignalUse( + SynthLogic signal, + _SubmoduleSignalUseIndex submoduleSignalUses, { + required SynthSubModuleInstantiation allowedInstantiation, + }) { + final resolved = signal.resolved; + final users = { + ...?submoduleSignalUses.exact[resolved], + ...?submoduleSignalUses.byReferenceBase[resolved], + ...?submoduleSignalUses.exact[_referenceBase(resolved)], + }; + return users.any((instantiation) => instantiation != allowedInstantiation); + } + + /// Whether [sourceBase] remains meaningful when only part of a generated + /// subset helper is collapsed. + bool _canPartiallyCollapseGeneratedSubsetSource(SynthLogic sourceBase) => + (sourceBase.isConstant && !sourceBase.isFloatingConstant) || + _isLiveRangeSource(sourceBase); + + /// Whether [source] resolves through full-width drivers to a constant. + /// + /// [visiting] prevents cycles in malformed or bidirectional connection + /// graphs from recursing indefinitely. + bool _isConstantBackedSource( + SynthLogic source, + Map> assignmentsByDestination, [ + Set visiting = const {}, + ]) { + if (source.isConstant) { + return true; + } + if (visiting.contains(source)) { + return false; + } + + final driver = _singleFullWidthAssignment( + source, + assignmentsByDestination, + ); + return driver != null && + _isConstantBackedSource( + _assignmentSourceRange(driver).base, + assignmentsByDestination, + {...visiting, source}, + ); + } + + /// Whether [sourceBase] has a live identity or connection outside disposable + /// range intermediates. + bool _isLiveRangeSource(SynthLogic sourceBase) => + !sourceBase.isConstant && + (sourceBase.hasSrcConnectionsPresent() || + sourceBase.isStructPortElement(module) || + !internalSignals.contains(sourceBase) || + !sourceBase.isClearable); + + /// Composes temporary bus slices through full-width assignments into wide + /// array-element destinations. + void _collapseWideArrayElementRangeSources() { + final busSubsetRanges = _busSubsetSourceRanges(); + if (busSubsetRanges.isEmpty) { + return; + } + + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + final assignmentsBySource = + _assignmentsBy(assignments, (assignment) => assignment.src.resolved); + final submoduleSignalUses = _submoduleSignalUseIndex(); + final knownSourceRanges = { + for (final entry in busSubsetRanges.entries) entry.key: entry.value.range, + }; + + var changed = false; + final updatedAssignments = []; + for (final assignment in assignments) { + if (assignment is PartialSynthAssignment || + assignment.dst.resolved is! SynthLogicArrayElement) { + updatedAssignments.add(assignment); + continue; + } + + final busSubsetRange = busSubsetRanges[assignment.src.resolved]; + if (busSubsetRange == null) { + updatedAssignments.add(assignment); + continue; + } + if (!internalSignals.contains(busSubsetRange.range.base) || + !busSubsetRange.range.base.isClearable) { + updatedAssignments.add(assignment); + continue; + } + + final dst = assignment.dst.resolved; + if (dst.width <= 1 || dst.logics.any((logic) => logic is LogicArray)) { + updatedAssignments.add(assignment); + continue; + } + + final src = _resolveKnownRangeThroughFullWidthDrivers( + busSubsetRange.range, + assignmentsByDestination, + knownSourceRanges, + ); + if (src.base == dst || + src.width != dst.width || + !_canUsePackedRangeBase(src.base) || + !_isLiveRangeSource(src.base) || + (internalSignals.contains(src.base) && src.base.isClearable) || + dst.isNet || + dst.isConstant) { + updatedAssignments.add(assignment); + continue; + } + + updatedAssignments.add( + RangeSynthAssignment( + src.base, + dst, + srcUpperIndex: src.upper, + srcLowerIndex: src.lower, + dstUpperIndex: dst.width - 1, + dstLowerIndex: 0, + ), + ); + if (_canClearReplacedSourceSignal( + assignment.src.resolved, + assignmentsBySource, + {assignment}, + submoduleSignalUses: submoduleSignalUses, + allowedInstantiation: busSubsetRange.inst, + )) { + busSubsetRange.inst.clearInstantiation(); + assignment.src.clearDeclaration(); + } + changed = true; + } + + if (changed) { + assignments + ..clear() + ..addAll(updatedAssignments); + } + } + + /// Maps each usable non-net [BusSubset] output to its packed source range and + /// helper instantiation, resolving through full-width temporary drivers. + Map + _busSubsetSourceRanges() { + final directRanges = {}; + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + + for (final instantiation in subModuleInstantiations) { + final module = instantiation.module; + if (module is! BusSubset || + module.original.isNet || + module.startIndex > module.endIndex) { + continue; + } + + final original = instantiation.inputMapping[module.original.name]; + final subset = instantiation.outputMapping[module.subset.name]; + if (original == null || + subset == null || + original.isNet || + subset.isNet || + !_canUsePackedRangeSource(original.resolved)) { + continue; + } + + directRanges[subset.resolved] = ( + range: _SynthRangeRef( + original.resolved, + module.startIndex, + module.endIndex, + ), + inst: instantiation, + ); + } + + final ranges = {}; + for (final entry in directRanges.entries) { + final resolvedRange = _resolveFullWidthDrivenRange( + entry.value.range, + assignmentsByDestination, + directRanges, + ); + if (!_canUsePackedRangeSource(resolvedRange.base)) { + continue; + } + + ranges[entry.key] = (range: resolvedRange, inst: entry.value.inst); + } + return ranges; + } + + /// Resolves [range] through full-width assignments and known [BusSubset] + /// ranges while preserving its relative bit offsets. + _SynthRangeRef _resolveFullWidthDrivenRange( + _SynthRangeRef range, + Map> assignmentsByDestination, + Map + directRanges, [ + Set visiting = const {}, + ]) { + if (visiting.contains(range.base)) { + return range; + } + + final sourceRange = _singleFullWidthSourceRange( + range.base, + assignmentsByDestination, + directRanges, + {...visiting, range.base}, + ); + if (sourceRange == null) { + return range; + } + + return _SynthRangeRef.tryCreate( + sourceRange.base, + sourceRange.lower + range.lower, + sourceRange.lower + range.upper, + ) ?? + range; + } + + /// Returns the source range of [signal]'s sole full-width driver. + /// + /// Known [BusSubset] sources are recursively resolved, with [visiting] + /// preventing cycles. + _SynthRangeRef? _singleFullWidthSourceRange( + SynthLogic signal, + Map> assignmentsByDestination, + Map + directRanges, + Set visiting, + ) { + final driver = _singleFullWidthAssignment(signal, assignmentsByDestination); + if (driver == null) { + return null; + } + + if (driver is RangeSynthAssignment) { + return _SynthRangeRef( + driver.src.resolved, + driver.srcLowerIndex, + driver.srcUpperIndex, + ); + } + + final driverSrc = driver.src.resolved; + final directRange = directRanges[driverSrc]; + if (directRange != null) { + return _resolveFullWidthDrivenRange( + directRange.range, + assignmentsByDestination, + directRanges, + visiting, + ); + } + + return _SynthRangeRef(driverSrc, 0, driverSrc.width - 1); + } + + /// Returns [signal]'s only assignment when it covers the full destination. + SynthAssignment? _singleFullWidthAssignment( + SynthLogic signal, + Map> assignmentsByDestination, + ) { + final drivers = assignmentsByDestination[signal.resolved]; + if (drivers == null || drivers.length != 1) { + return null; + } + + final driver = drivers.single; + if (driver is RangeSynthAssignment) { + if (driver.dstLowerIndex != 0 || + driver.dstUpperIndex != signal.width - 1 || + driver.width != signal.width) { + return null; + } + return driver; + } + + if (driver is PartialSynthAssignment || driver.width != signal.width) { + return null; + } + + return driver; + } + + /// Resolves the packed source represented by an array-element assignment or + /// a temporary [BusSubset] output. + ({ + _SynthRangeRef range, + SynthSubModuleInstantiation? sourceSubmodule, + SynthLogic? sourceSignal, + })? _simpleAssignmentSourceRange( + SynthAssignment assignment, + Map + busSubsetRanges, + ) { + final src = assignment.src.resolved; + final busSubsetRange = busSubsetRanges[src]; + if (busSubsetRange != null) { + return ( + range: busSubsetRange.range, + sourceSubmodule: busSubsetRange.inst, + sourceSignal: src, + ); + } + + final arrayElementRange = _arrayElementRange(src); + if (arrayElementRange == null) { + return null; + } + return ( + range: arrayElementRange, + sourceSubmodule: null, + sourceSignal: null, + ); + } + + /// Returns the packed destination range for an array-element assignment. + _SynthRangeRef? _simpleAssignmentDestinationRange( + SynthAssignment assignment) => + _arrayElementRange(assignment.dst.resolved); + + /// Returns the one-bit packed range represented by [signal], when its parent + /// array has a supported packed layout. + _SynthRangeRef? _arrayElementRange(SynthLogic signal) { + if (signal is! SynthLogicArrayElement) { + return null; + } + final parentArray = signal.parentArray.resolved; + if (!_canUsePackedRangeBase(parentArray)) { + return null; + } + final index = signal.logic.arrayIndex!; + return _SynthRangeRef(parentArray, index, index); + } + + /// Whether [base] can be referenced by packed indices in a backend-neutral + /// range assignment. + bool _canUsePackedRangeBase(SynthLogic base) { + if (base.isNet || base.isConstant) { + return false; + } + if (!base.isArray) { + return true; + } + if (base.logics.length != 1) { + return false; + } + final logic = base.logics.first; + return logic is LogicArray && + logic.dimensions.length == 1 && + logic.elementWidth == 1 && + logic.numUnpackedDimensions == 0; + } + + /// Constants can be regrouped after named constant subsets are bit-blasted, + /// but floating constants must retain their undriven semantics. + bool _canUsePackedRangeSource(SynthLogic base) => + (base.isConstant && !base.isFloatingConstant) || + _canUsePackedRangeBase(base); + + /// Depth-oriented composition of range assignments through an intermediate. + /// + /// Unlike [_collapseSimpleRangeAssignments], this pass does not try to find + /// more adjacent bits. It looks for a single producer and a single consumer + /// around an internal helper and composes their ranges, for example + /// `mid[3:0] <= src[7:4]` plus `dst[1:0] <= mid[3:2]` becoming + /// `dst[1:0] <= src[7:6]`. + void _collapseChainedRangeAssignments() { + final generatedSubsetIntermediates = _generatedSubsetIntermediates(); + final mappedIntermediates = {}; + for (final instantiation in subModuleInstantiations) { + for (final mapped in { + ...instantiation.inputMapping.values, + ...instantiation.outputMapping.values, + ...instantiation.inOutMapping.values, + }) { + mappedIntermediates.add(mapped.resolved); + if (mapped is SynthLogicArrayElement) { + mappedIntermediates.add(mapped.parentArray.resolved); + } + } + } + final activeAssignments = LinkedHashSet.of(assignments); + final assignmentsByDestination = >{}; + final assignmentsBySource = >{}; + + /// Adds [assignment] to both mutable chain indexes. + void addToIndexes(SynthAssignment assignment) { + assignmentsByDestination + .putIfAbsent(_referenceBase(assignment.dst), () => {}) + .add(assignment); + assignmentsBySource + .putIfAbsent(_referenceBase(assignment.src), () => {}) + .add(assignment); + } + + /// Removes [assignment] from both mutable chain indexes. + void removeFromIndexes(SynthAssignment assignment) { + assignmentsByDestination[_referenceBase(assignment.dst)] + ?.remove(assignment); + assignmentsBySource[_referenceBase(assignment.src)]?.remove(assignment); + } + + assignments.forEach(addToIndexes); + final workQueue = ListQueue.from( + assignments.whereType(), + ); + + while (workQueue.isNotEmpty) { + final producer = workQueue.removeFirst(); + if (!activeAssignments.contains(producer)) { + continue; + } + + final intermediate = producer.dst.resolved; + final producers = assignmentsByDestination[intermediate]; + final consumers = assignmentsBySource[intermediate]; + if (producers?.length != 1 || + consumers?.length != 1 || + producers!.single != producer) { + continue; + } + + final consumer = consumers!.single; + final replacement = _composeChainedRangeAssignment( + producer: producer, + consumer: consumer, + intermediate: intermediate, + generatedSubsetIntermediates: generatedSubsetIntermediates, + mappedIntermediates: mappedIntermediates, + ); + if (replacement == null) { + continue; + } + + activeAssignments + ..remove(producer) + ..remove(consumer); + removeFromIndexes(producer); + removeFromIndexes(consumer); + activeAssignments.add(replacement); + addToIndexes(replacement); + workQueue.add(replacement); + intermediate.clearDeclaration(); + } + + assignments + ..clear() + ..addAll(activeAssignments); + } + + /// Composes [producer] and [consumer] through [intermediate]. + /// + /// The selected intermediate ranges must contain one another so their + /// offsets can be translated without changing width or overlap semantics. + /// Generated `assignSubset` intermediates additionally allow a producer to + /// replace part of a wider consumer because their complete coverage is + /// validated by the generated-subset analysis. + RangeSynthAssignment? _composeChainedRangeAssignment({ + required PartialSynthAssignment producer, + required SynthAssignment consumer, + required SynthLogic intermediate, + required Set generatedSubsetIntermediates, + required Set mappedIntermediates, + }) { + if (!_isRangeChainIntermediate( + intermediate, + mappedIntermediates: mappedIntermediates, + )) { + return null; + } + + final producerSrc = _assignmentSourceRange(producer); + final producerDst = _assignmentDestinationRange(producer); + final consumerSrc = _assignmentSourceRange(consumer); + final consumerDst = _assignmentDestinationRange(consumer); + + if (producerDst.base != intermediate || consumerSrc.base != intermediate) { + return null; + } + if (producerSrc.base == consumerDst.base || + producerSrc.base.isNet || + consumerDst.base.isNet || + producerSrc.base.isConstant || + consumerDst.base.isConstant) { + return null; + } + + late final _SynthRangeRef replacementSrc; + late final _SynthRangeRef replacementDst; + if (consumer is PartialSynthAssignment) { + if (!producerDst.contains(consumerSrc)) { + return null; + } + + final sourceLower = + producerSrc.lower + (consumerSrc.lower - producerDst.lower); + final sourceUpper = + producerSrc.lower + (consumerSrc.upper - producerDst.lower); + final sourceRange = _SynthRangeRef.tryCreate( + producerSrc.base, + sourceLower, + sourceUpper, + ); + if (sourceRange == null) { + return null; + } + replacementSrc = sourceRange; + replacementDst = consumerDst; + } else { + if (!consumerSrc.contains(producerDst)) { + return null; + } + final producerCoversWholeConsumer = + producerDst.lower == consumerSrc.lower && + producerDst.upper == consumerSrc.upper; + if (!producerCoversWholeConsumer) { + if (!generatedSubsetIntermediates.contains(intermediate) || + consumerDst.base is SynthLogicArrayElement) { + return null; + } + } + + final dstLower = + consumerDst.lower + (producerDst.lower - consumerSrc.lower); + final dstUpper = + consumerDst.lower + (producerDst.upper - consumerSrc.lower); + final destinationRange = _SynthRangeRef.tryCreate( + consumerDst.base, + dstLower, + dstUpper, + ); + if (destinationRange == null) { + return null; + } + replacementSrc = producerSrc; + replacementDst = destinationRange; + } + + if (replacementSrc.width != replacementDst.width || + replacementSrc.lower < 0 || + replacementSrc.upper >= replacementSrc.base.width || + replacementDst.lower < 0 || + replacementDst.upper >= replacementDst.base.width) { + return null; + } + + return RangeSynthAssignment( + replacementSrc.base, + replacementDst.base, + srcUpperIndex: replacementSrc.upper, + srcLowerIndex: replacementSrc.lower, + dstUpperIndex: replacementDst.upper, + dstLowerIndex: replacementDst.lower, + ); + } + + /// Replaces a named constant intermediate with its literal at each range + /// consumer. + /// + /// For example, `tie = 3'h0` plus `bus[6:4] = tie` becomes + /// `bus[6:4] = 3'h0`. The intermediate is removed only when every consumer + /// was replaced, so fanout and unsupported uses remain conservative. + void _collapseConstantBackedRangeIntermediates() { + final mappedSignals = _submoduleMappingReferences( + includeInputs: true, + includeOutputs: true, + ); + while (true) { + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + final assignmentsBySource = + _assignmentsBy(assignments, (assignment) => assignment.src.resolved); + final claimedAssignments = {}; + final replacements = {}; + final removedProducers = {}; + final clearedIntermediates = {}; + + for (final intermediate in internalSignals.toList()) { + final producers = assignmentsByDestination[intermediate.resolved]; + final consumers = assignmentsBySource[intermediate.resolved]; + if (producers?.length != 1 || + consumers == null || + consumers.isEmpty || + intermediate.hasPreservedName || + mappedSignals.contains(intermediate.resolved)) { + continue; + } + + final producer = producers!.single; + if (claimedAssignments.contains(producer)) { + continue; + } + if (intermediate.width == 0 || + producer.src.width == 0 || + producer.dst.width == 0) { + continue; + } + final producerSrc = _assignmentSourceRange(producer); + final producerDst = _assignmentDestinationRange(producer); + if (!producerSrc.base.isConstant || + producerSrc.base.isFloatingConstant || + producerSrc.lower != 0 || + producerSrc.upper != producerSrc.base.width - 1 || + producerDst.base != intermediate.resolved || + producerDst.lower != 0 || + producerDst.upper != intermediate.width - 1) { + continue; + } + + final intermediateReplacements = + {}; + for (final consumer in consumers) { + if (consumer is! PartialSynthAssignment || + claimedAssignments.contains(consumer)) { + continue; + } + final consumerSrc = _assignmentSourceRange(consumer); + final consumerDst = _assignmentDestinationRange(consumer); + final feedsArraySubset = + (assignmentsBySource[consumerDst.base.resolved] ?? const []) + .any((downstream) => downstream.dst.dstConnections.any( + (destination) => destination.isArrayMember, + )); + if (consumerSrc.base != intermediate.resolved || + _referenceBase(consumerDst.base).isArray || + feedsArraySubset || + consumerSrc.lower != 0 || + consumerSrc.upper != intermediate.width - 1 || + consumerDst.width != producerSrc.width) { + continue; + } + intermediateReplacements[consumer] = PartialSynthAssignment( + producerSrc.base, + consumerDst.base, + dstUpperIndex: consumerDst.upper, + dstLowerIndex: consumerDst.lower, + ); + } + if (intermediateReplacements.isEmpty) { + continue; + } + + replacements.addAll(intermediateReplacements); + claimedAssignments.addAll(intermediateReplacements.keys); + if (intermediateReplacements.length == consumers.length) { + claimedAssignments.add(producer); + removedProducers.add(producer); + clearedIntermediates.add(intermediate); + } + } + + if (replacements.isEmpty) { + break; + } + + final updatedAssignments = [ + for (final assignment in assignments) + if (replacements.containsKey(assignment)) + replacements[assignment]! + else if (!removedProducers.contains(assignment)) + assignment, + ]; + assignments + ..clear() + ..addAll(updatedAssignments); + for (final intermediate in clearedIntermediates) { + intermediate.clearDeclaration(); + } + internalSignals.removeAll(clearedIntermediates); + } + } + + /// Drops constant-backed signals referenced only by cleared [BusSubset] + /// helpers. + /// + /// Active packed-array mappings and current-module array ports are excluded + /// because their elements can still depend on a constant after ordinary + /// assignment pruning. + void _pruneClearedSubsetConstantSources() { + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + final assignmentsBySource = + _assignmentsBy(assignments, (assignment) => assignment.src.resolved); + final mappedInstantiationsBySignal = + >{}; + for (final instantiation in subModuleInstantiations) { + for (final mapped in { + ...instantiation.inputMapping.values, + ...instantiation.outputMapping.values, + ...instantiation.inOutMapping.values, + }) { + mappedInstantiationsBySignal + .putIfAbsent(mapped.resolved, () => {}) + .add(instantiation); + } + } + + final protectedArrayBases = { + for (final port in [...inputs, ...outputs, ...inOuts]) + if (_referenceBase(port.resolved).isArray) _arrayRoot(port.resolved), + for (final instantiation in subModuleInstantiations) + if (instantiation.needsInstantiation) + for (final mapped in instantiation.inputMapping.values) + if (_referenceBase(mapped.resolved).isArray) + _arrayRoot(mapped.resolved), + }; + final removedAssignments = {}; + final removedSignals = {}; + for (final signal in internalSignals.toList()) { + final producers = assignmentsByDestination[signal.resolved] ?? const []; + final consumers = assignmentsBySource[signal.resolved] ?? const []; + final constantBacked = signal.isConstant || + (producers.isNotEmpty && + producers.every( + (producer) => producer.src.resolved.isConstant, + )); + if (!constantBacked || + signal.hasPreservedName || + consumers.isNotEmpty || + (signal is SynthLogicArrayElement && + protectedArrayBases.contains(_arrayRoot(signal))) || + signal.dstConnections.any((destination) { + final synthDestination = getSynthLogic(destination)?.resolved; + return synthDestination != null && + protectedArrayBases.contains( + _arrayRoot(synthDestination), + ); + })) { + continue; + } + + final mappedInstantiations = + mappedInstantiationsBySignal[signal.resolved] ?? const {}; + final feedsProtectedArray = mappedInstantiations.any( + (instantiation) => + !instantiation.needsInstantiation && + instantiation.module is BusSubset && + instantiation.inputMapping.values.any( + (mapped) => mapped.resolved == signal.resolved, + ) && + instantiation.outputMapping.values.any( + (mapped) => protectedArrayBases.contains(_arrayRoot(mapped)), + ), + ); + if (feedsProtectedArray) { + continue; + } + if (mappedInstantiations.isEmpty || + mappedInstantiations.every( + (instantiation) => + !instantiation.needsInstantiation && + instantiation.module is BusSubset, + )) { + removedAssignments.addAll(producers); + removedSignals.add(signal); + } + } + + if (removedAssignments.isNotEmpty) { + final retainedAssignments = [ + for (final assignment in assignments) + if (!removedAssignments.contains(assignment)) assignment, + ]; + assignments + ..clear() + ..addAll(retainedAssignments); + } + for (final signal in removedSignals) { + signal.clearDeclaration(); + } + internalSignals.removeAll(removedSignals); + } + + /// Collapses generated `assignSubset` helpers feeding packed swizzles. + /// + /// `Logic.assignSubset` builds a temporary array and then swizzles that array + /// back into a packed value. When every producer bit for that helper is + /// accounted for, the helper and swizzle can be replaced with direct packed + /// range assignments to the swizzle output. This is a specialized helper + /// pass: it requires full coverage so partially driven helpers keep their + /// original undriven/floating behavior. + void _collapseGeneratedSubsetSwizzleRangeAssignments() { + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + final assignmentsBySourceBase = _assignmentsBy( + assignments, (assignment) => _referenceBase(assignment.src)); + + final allSwizzleSourceRanges = + _fullPackedSwizzleSourceRanges(assignmentsByDestination); + final generatedSubsetIntermediates = _generatedSubsetIntermediatesFrom( + allSwizzleSourceRanges, + assignmentsBySourceBase, + ); + final swizzleSourceRanges = { + for (final entry in allSwizzleSourceRanges.entries) + if (generatedSubsetIntermediates.contains(entry.value.range.base)) + entry.key: entry.value, + }; + if (swizzleSourceRanges.isEmpty) { + return; + } + + final swizzlesByBase = inputAssignments, + })>>{}; + for (final entry in swizzleSourceRanges.entries) { + swizzlesByBase.putIfAbsent(entry.value.range.base, () => []).add(( + output: entry.key, + range: entry.value.range, + inst: entry.value.inst, + inputAssignments: entry.value.inputAssignments, + )); + } + + final assignmentsByBaseDestination = _assignmentsBy( + assignments, (assignment) => _referenceBase(assignment.dst)); + final assignmentsBySource = _assignmentsBy( + assignments, (assignment) => _referenceBase(assignment.src)); + final submoduleSignalUses = _submoduleSignalUseIndex(); + final realOutputMappingsBySignal = >{}; + for (final instantiation in subModuleInstantiations) { + if (!instantiation.needsInstantiation || + instantiation.module is InlineSystemVerilog) { + continue; + } + for (final entry in instantiation.outputMapping.entries) { + realOutputMappingsBySignal + .putIfAbsent(entry.value.resolved, () => []) + .add((instantiation: instantiation, portName: entry.key)); + } + } + final knownSourceRanges = { + for (final entry in _busSubsetSourceRanges().entries) + entry.key: entry.value.range, + }; + + final replacements = {}; + final consumedAssignments = {}; + final outputMappingReplacements = <({ + SynthSubModuleInstantiation instantiation, + String portName, + SynthLogicPackedBitReference reference, + })>[]; + for (final entry in swizzlesByBase.entries) { + final intermediate = entry.key; + final swizzles = entry.value; + final swizzle = swizzles.singleOrNull; + final sourceUsers = assignmentsBySource[intermediate] ?? const []; + final producers = assignmentsByBaseDestination[intermediate]; + if (swizzles.length != 1 || + swizzle == null || + producers == null || + producers.isEmpty || + sourceUsers.any( + (sourceUser) => !swizzle.inputAssignments.contains(sourceUser), + )) { + continue; + } + + if (!_isRangeChainIntermediate( + intermediate, + allowedInstantiation: swizzle.inst, + )) { + continue; + } + + final output = swizzle.output.resolved; + if (output.isNet || output.isConstant) { + continue; + } + + final producerReplacements = {}; + final mappedProducerAssignments = {}; + final producerOutputMappingReplacements = <({ + SynthSubModuleInstantiation instantiation, + String portName, + SynthLogicPackedBitReference reference, + })>[]; + final resolvedProducerSources = { + for (final producer in producers) + producer: _resolveKnownRangeThroughFullWidthDrivers( + _assignmentSourceRange(producer), + assignmentsByDestination, + knownSourceRanges, + ), + }; + final commonSourceBase = resolvedProducerSources.values + .map((source) => source.base) + .toSet() + ..removeWhere((source) => !source.isArray); + final packedArraySource = commonSourceBase.singleOrNull; + final coveredPackedArrayBits = {}; + var preservesWholePackedArray = + packedArraySource != null && packedArraySource.width == output.width; + if (preservesWholePackedArray) { + for (final producer in producers) { + final source = resolvedProducerSources[producer]!; + final destination = _assignmentDestinationRange(producer); + if (source.base != packedArraySource || + destination.base != intermediate || + source.width != destination.width || + !swizzle.range.contains(destination) || + source.lower != destination.lower - swizzle.range.lower) { + preservesWholePackedArray = false; + break; + } + for (var bit = source.lower; bit <= source.upper; bit++) { + if (!coveredPackedArrayBits.add(bit)) { + preservesWholePackedArray = false; + break; + } + } + if (!preservesWholePackedArray) { + break; + } + } + } + if (preservesWholePackedArray && + coveredPackedArrayBits.length == output.width) { + // Keep complete ordered packed arrays on the existing swizzle path, + // which renders the array's packed selection without an unnecessary + // destination selection. + continue; + } + final hasConstantProducer = resolvedProducerSources.values.any( + (source) => _isConstantBackedSource( + source.base, + assignmentsByDestination, + ), + ); + final realMappedOutputProducerCount = producers + .where((producer) => + realOutputMappingsBySignal[producer.src.resolved]?.isNotEmpty ?? + false) + .length; + final seenDestinationBits = {}; + var canReplaceAll = true; + for (final producer in producers) { + final producerSrc = resolvedProducerSources[producer]!; + final producerDst = _assignmentDestinationRange(producer); + if (producerDst.base != intermediate || + !swizzle.range.contains(producerDst) || + producerSrc.base == output || + producerSrc.base.isNet || + (producerSrc.base.isConstant && + (producerSrc.lower != 0 || + producerSrc.upper != producerSrc.base.width - 1))) { + canReplaceAll = false; + break; + } + if (!producerSrc.base.isConstant && + !_isLiveRangeSource(producerSrc.base)) { + canReplaceAll = false; + break; + } + + final dstLower = producerDst.lower - swizzle.range.lower; + final dstUpper = producerDst.upper - swizzle.range.lower; + if (producerSrc.width != dstUpper - dstLower + 1 || + dstLower < 0 || + dstUpper >= output.width) { + canReplaceAll = false; + break; + } + for (var index = dstLower; index <= dstUpper; index++) { + if (!seenDestinationBits.add(index)) { + canReplaceAll = false; + break; + } + } + if (!canReplaceAll) { + break; + } + + final outputMappings = + realOutputMappingsBySignal[producer.src.resolved] ?? const []; + final producerSourceUsers = + assignmentsBySource[producer.src.resolved] ?? const []; + final canMapDirectly = realMappedOutputProducerCount == 1 && + (!hasConstantProducer || + producerDst.lower == 0 || + producerDst.upper == intermediate.width - 1); + if (canMapDirectly && + outputMappings.length == 1 && + producerSourceUsers.length == 1 && + producerSourceUsers.single == producer && + producerSrc.width == 1 && + producerDst.width == 1 && + !_hasSubmoduleSignalUse( + producer.src.resolved, + submoduleSignalUses, + allowedInstantiation: outputMappings.single.instantiation, + )) { + final packedReference = SynthLogicPackedBitReference( + output, + dstLower, + parentSynthModuleDefinition: this, + ); + producerOutputMappingReplacements.add(( + instantiation: outputMappings.single.instantiation, + portName: outputMappings.single.portName, + reference: packedReference, + )); + mappedProducerAssignments.add(producer); + } else { + producerReplacements[producer] = + producerSrc.base.width == 1 || producerSrc.base.isConstant + ? PartialSynthAssignment( + producerSrc.base, + output, + dstUpperIndex: dstUpper, + dstLowerIndex: dstLower, + ) + : RangeSynthAssignment( + producerSrc.base, + output, + srcUpperIndex: producerSrc.upper, + srcLowerIndex: producerSrc.lower, + dstUpperIndex: dstUpper, + dstLowerIndex: dstLower, + ); + } + } + if (!canReplaceAll) { + continue; + } + if (seenDestinationBits.length != output.width && + (output.logics.any((logic) => logic.isArrayMember) || + _hasArrayMemberConsumer(output, assignmentsBySource))) { + continue; + } + + replacements.addAll(producerReplacements); + consumedAssignments + ..addAll(swizzle.inputAssignments) + ..addAll(mappedProducerAssignments); + outputMappingReplacements.addAll(producerOutputMappingReplacements); + intermediate.clearDeclaration(); + swizzle.inst.clearInstantiation(); + } + + if (replacements.isNotEmpty || outputMappingReplacements.isNotEmpty) { + for (final replacement in outputMappingReplacements) { + replacement.instantiation.setOutputMapping( + replacement.portName, + replacement.reference, + replace: true, + ); + } + final updatedAssignments = [ + for (final assignment in assignments) + if (replacements.containsKey(assignment)) + replacements[assignment]! + else if (!consumedAssignments.contains(assignment)) + assignment, + ]; + assignments + ..clear() + ..addAll(updatedAssignments); + } + } + + /// Whether [source] drives any array member assignment. + bool _hasArrayMemberConsumer( + SynthLogic source, + Map> assignmentsBySource, + ) { + final consumers = assignmentsBySource[source] ?? const []; + return consumers.any( + (assignment) => + assignment.dst is SynthLogicArrayElement || + assignment.dst.resolved.logics.any((logic) => logic.isArrayMember), + ); + } + + /// Resolves [range] through clearable full-width drivers and + /// [knownSourceRanges], retaining the original relative selection. + _SynthRangeRef _resolveKnownRangeThroughFullWidthDrivers( + _SynthRangeRef range, + Map> assignmentsByDestination, + Map knownSourceRanges, [ + Set visiting = const {}, + ]) { + if (visiting.contains(range.base) || !range.base.isClearable) { + return range; + } + + final driver = _singleFullWidthAssignment( + range.base, + assignmentsByDestination, + ); + if (driver == null) { + return range; + } + + final sourceRange = driver is RangeSynthAssignment + ? _SynthRangeRef( + driver.src.resolved, + driver.srcLowerIndex, + driver.srcUpperIndex, + ) + : knownSourceRanges[driver.src.resolved] ?? + _SynthRangeRef(driver.src.resolved, 0, driver.src.width - 1); + + final resolvedSourceRange = _resolveKnownRangeThroughFullWidthDrivers( + sourceRange, + assignmentsByDestination, + knownSourceRanges, + {...visiting, range.base}, + ); + if (resolvedSourceRange.width != range.base.width) { + return range; + } + + return _SynthRangeRef.tryCreate( + resolvedSourceRange.base, + resolvedSourceRange.lower + range.lower, + resolvedSourceRange.lower + range.upper, + ) ?? + range; + } + + /// Finds unnamed generated-subset candidates and the subset intermediates + /// that feed a live full-width swizzle consumer. + ({Set candidates, Set intermediates}) + _generatedSubsetIntermediateSets() { + final assignmentsByDestination = + _assignmentsBy(assignments, (assignment) => assignment.dst.resolved); + final assignmentsBySourceBase = _assignmentsBy( + assignments, (assignment) => _referenceBase(assignment.src)); + + final swizzleSourceRanges = + _fullPackedSwizzleSourceRanges(assignmentsByDestination); + final candidates = { + for (final entry in swizzleSourceRanges.entries) + if (_isGeneratedSubsetIntermediateCandidate(entry.value.range.base)) + entry.value.range.base, + }; + + return ( + candidates: candidates, + intermediates: _generatedSubsetIntermediatesFrom( + swizzleSourceRanges, + assignmentsBySourceBase, + ), + ); + } + + /// Finds generated subset intermediates eligible for range composition. + Set _generatedSubsetIntermediates() => + _generatedSubsetIntermediateSets().intermediates; + + /// Filters [swizzleSourceRanges] to generated subset intermediates with a + /// live full-width internal consumer. + Set _generatedSubsetIntermediatesFrom( + Map< + SynthLogic, + ({ + _SynthRangeRef range, + SynthSubModuleInstantiation inst, + List inputAssignments, + })> + swizzleSourceRanges, + Map> assignmentsBySourceBase, + ) { + final generatedSubsetIntermediates = {}; + for (final entry in swizzleSourceRanges.entries) { + final intermediate = entry.value.range.base; + if (_isGeneratedSubsetIntermediateCandidate(intermediate) && + _hasInternalFullWidthSwizzleConsumer( + entry.key, + assignmentsBySourceBase, + )) { + generatedSubsetIntermediates.add(intermediate); + } + } + + return generatedSubsetIntermediates; + } + + /// Maps each non-net [Swizzle] output that reconstructs a complete packed + /// array to its source range, helper, and any full-width input assignments. + Map< + SynthLogic, + ({ + _SynthRangeRef range, + SynthSubModuleInstantiation inst, + List inputAssignments, + })> _fullPackedSwizzleSourceRanges( + Map> assignmentsByDestination, + ) { + final ranges = inputAssignments, + })>{}; + + for (final instantiation in subModuleInstantiations) { + final module = instantiation.module; + if (module is! Swizzle || module.isNet) { + continue; + } + + final output = instantiation.outputMapping[module.resultSignalName]; + if (output == null) { + continue; + } + + final indexedInputs = <({int index, SynthLogic signal})>[]; + final inputAssignments = []; + var hasUnindexedInput = false; + for (final entry in instantiation.inputMapping.entries) { + final index = _swizzleInputIndex(entry.key); + if (index == null) { + hasUnindexedInput = true; + break; + } + final inputDriver = _singleFullWidthAssignment( + entry.value.resolved, + assignmentsByDestination, + ); + if (inputDriver != null) { + inputAssignments.add(inputDriver); + } + indexedInputs.add(( + index: index, + signal: inputDriver?.src.resolved ?? entry.value.resolved, + )); + } + if (hasUnindexedInput || indexedInputs.isEmpty) { + continue; + } + + indexedInputs.sort((a, b) => a.index.compareTo(b.index)); + SynthLogic? parentArray; + var allInputsMatch = true; + for (final (expectedIndex, input) in indexedInputs.indexed) { + if (input.index != expectedIndex || + input.signal is! SynthLogicArrayElement) { + allInputsMatch = false; + break; + } + + final element = input.signal as SynthLogicArrayElement; + final array = element.parentArray.resolved; + parentArray ??= array; + if (array != parentArray || + !_canUsePackedRangeBase(array) || + element.logic.arrayIndex != expectedIndex) { + allInputsMatch = false; + break; + } + } + + if (!allInputsMatch || parentArray == null) { + continue; + } + + final arrayLogic = parentArray.logics.first; + if (arrayLogic is! LogicArray || + indexedInputs.length != arrayLogic.elements.length) { + continue; + } + + ranges[output.resolved] = ( + range: _SynthRangeRef(parentArray, 0, indexedInputs.length - 1), + inst: instantiation, + inputAssignments: inputAssignments, + ); + } + + return ranges; + } + + /// Parses the numeric index from a [Swizzle] input named `inN`. + int? _swizzleInputIndex(String portName) { + final match = RegExp(r'in(\d+)$').firstMatch(portName); + return match == null ? null : int.parse(match.group(1)!); + } + + /// Whether [intermediate] is a disposable non-net signal that can be removed + /// from a range chain without crossing a port or unpacked-array boundary. + bool _isRangeChainIntermediate( + SynthLogic intermediate, { + SynthSubModuleInstantiation? allowedInstantiation, + Set? mappedIntermediates, + }) { + if (!internalSignals.contains(intermediate) || + intermediate.isNet || + intermediate.isConstant || + !intermediate.isClearable || + intermediate.isPort(module) || + intermediate.isStructPortElement(module)) { + return false; + } + + if (mappedIntermediates?.contains(intermediate) ?? false) { + return false; + } + + for (final instantiation in mappedIntermediates == null + ? subModuleInstantiations + : const []) { + if (instantiation == allowedInstantiation) { + continue; + } + final mappedSignals = [ + ...instantiation.inputMapping.values, + ...instantiation.outputMapping.values, + ...instantiation.inOutMapping.values, + ]; + if (mappedSignals.any((signal) => + signal.resolved == intermediate || + (signal is SynthLogicArrayElement && + signal.parentArray.resolved == intermediate))) { + return false; + } + } + + final logic = intermediate.logics.firstOrNull; + return logic is! LogicArray || logic.numUnpackedDimensions == 0; + } + + /// Whether [intermediate] has the unnamed packed-array shape generated by + /// `assignSubset`. + bool _isGeneratedSubsetIntermediateCandidate(SynthLogic intermediate) { + if (!internalSignals.contains(intermediate) || + !intermediate.isClearable || + intermediate.isPort(module) || + intermediate.isStructPortElement(module) || + !_canUsePackedRangeBase(intermediate)) { + return false; + } + + final arrayLogic = intermediate.logics.singleOrNull; + return arrayLogic is LogicArray && arrayLogic.naming == Naming.unnamed; + } + + /// Whether [swizzleOutput] feeds a full-width disposable internal signal. + bool _hasInternalFullWidthSwizzleConsumer( + SynthLogic swizzleOutput, + Map> assignmentsBySourceBase, + ) { + final consumers = assignmentsBySourceBase[swizzleOutput] ?? const []; + return consumers.any((assignment) { + if (assignment is PartialSynthAssignment || + assignment.width != swizzleOutput.width) { + return false; + } + + final dst = assignment.dst.resolved; + if (!internalSignals.contains(dst) || + dst.isNet || + dst.isConstant || + dst.isPort(module) || + dst.isStructPortElement(module) || + dst.logics.any((logic) => logic.isArrayMember) || + dst.width != swizzleOutput.width) { + return false; + } + + return dst.logics.singleOrNull is! LogicArray; + }); + } + + /// Returns the packed source range represented by [assignment]. + _SynthRangeRef _assignmentSourceRange(SynthAssignment assignment) { + if (assignment is RangeSynthAssignment) { + return _SynthRangeRef( + assignment.src.resolved, + assignment.srcLowerIndex, + assignment.srcUpperIndex, + ); + } + + final arrayElementRange = _arrayElementRange(assignment.src.resolved); + if (arrayElementRange != null) { + return arrayElementRange; + } + + return _SynthRangeRef( + assignment.src.resolved, + 0, + assignment.src.width - 1, + ); + } + + /// Returns the packed destination range represented by [assignment]. + _SynthRangeRef _assignmentDestinationRange(SynthAssignment assignment) { + if (assignment is PartialSynthAssignment) { + return _SynthRangeRef( + assignment.dst.resolved, + assignment.dstLowerIndex, + assignment.dstUpperIndex, + ); + } + + final arrayElementRange = _arrayElementRange(assignment.dst.resolved); + if (arrayElementRange != null) { + return arrayElementRange; + } + + return _SynthRangeRef( + assignment.dst.resolved, + 0, + assignment.dst.width - 1, + ); + } + + /// Returns the packed object referenced by [signal] for grouping and usage + /// comparisons. + SynthLogic _referenceBase(SynthLogic signal) => switch (signal) { + SynthLogicArrayElement() => signal.parentArray.resolved, + SynthLogicPackedBitReference() => signal.packedBase.resolved, + _ => signal.resolved, + }; + + /// Returns the outermost array containing [signal], or [signal] itself when + /// it is not an array element. + SynthLogic _arrayRoot(SynthLogic signal) { + var root = signal.resolved; + while (root is SynthLogicArrayElement) { + root = root.parentArray.resolved; + } + return root; + } + /// Collapses assignments that don't need to remain present. void _collapseAssignments() { // there might be more assign statements than necessary, so let's ditch them diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index 55aa62720..1387bfb50 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2026 Intel Corporation +// Copyright (C) 2024-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'synth_array_concat.dart'; @@ -6,6 +6,7 @@ export 'synth_array_slice.dart'; export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; +export 'synth_module_stop_policy.dart'; export 'synth_operation_namer.dart'; export 'synth_structure_concat.dart'; export 'synth_structure_layout.dart'; diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index dc585612d..d5f20e783 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -165,6 +165,12 @@ class Namer { bool constNameDisallowed = false, }) { if (constValue != null && !constNameDisallowed) { + final preferredRadix = constValue.preferredRadix; + if (preferredRadix != null && constValue.value.isValid) { + return constValue.value + .toRadixString(radix: preferredRadix, sepChar: ''); + } + return constValue.value.toString(); } diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..f8c07eb71 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // simcompare.dart @@ -240,6 +240,8 @@ abstract class SimCompare { bool maskKnownWarnings = true, bool enableChecking = true, bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), }) { final result = iverilogVector(module, vectors, moduleName: moduleName, @@ -248,7 +250,8 @@ abstract class SimCompare { iverilogExtraArgs: iverilogExtraArgs, allowWarnings: allowWarnings, maskKnownWarnings: maskKnownWarnings, - buildOnly: buildOnly); + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration); if (enableChecking) { expect(result, true); } @@ -265,6 +268,8 @@ abstract class SimCompare { bool allowWarnings = false, bool maskKnownWarnings = true, bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), }) { if (kIsWeb) { // if running in web mode, then we can't run icarus verilog @@ -342,7 +347,9 @@ abstract class SimCompare { allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); final moduleInstance = '$topModule dut($moduleConnections);'; final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = module.generateSynth(); + final generatedVerilog = module.generateSynth( + configuration: synthesizerConfiguration, + ); // so that when they run in parallel, they dont step on each other final uniqueId = diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart index 81fc7304b..4efeef4ab 100644 --- a/lib/src/values/logic_value.dart +++ b/lib/src/values/logic_value.dart @@ -607,12 +607,16 @@ abstract class LogicValue implements Comparable { static String _reverse(String inString) => String.fromCharCodes(inString.runes.toList().reversed); - /// Return the radix encoding of the current [LogicValue] as a sequence - /// of radix characters prefixed by the length and encoding format. - /// Output format is: `'`. + /// Return the radix encoding of the current [LogicValue] as a sequence of + /// radix characters. By default, the output is prefixed by the length and + /// encoding format: `'`. /// - /// [ofRadixString] can parse a [String] produced by [toRadixString] and - /// construct a [LogicValue]. + /// If [includeWidth] is `false`, the length and encoding format are omitted. + /// The resulting string is not self-describing and cannot be parsed by + /// [ofRadixString] without restoring that information. + /// + /// [ofRadixString] can parse a decorated [String] produced by + /// [toRadixString] and construct a [LogicValue]. /// /// Here is the number 1492 printed as a radix string: /// - Binary: `15'b101_1101_0100` @@ -624,7 +628,8 @@ abstract class LogicValue implements Comparable { /// Separators are output according to [chunkSize] starting from the /// LSB(right), default is 4 characters. The default separator is '_'. /// [sepChar] can be set to another character, but not in [radixStringChars], - /// otherwise it will throw an exception. + /// or to an empty string to disable separators. Otherwise, it will throw an + /// exception. /// - [chunkSize] = default: `61'h2_9ebc_5f06_5bf7` /// - [chunkSize] = 10: `61'h29e_bc5f065bf7` /// @@ -650,8 +655,9 @@ abstract class LogicValue implements Comparable { {int radix = 2, int chunkSize = 4, bool leadingZeros = false, + bool includeWidth = true, String sepChar = '_'}) { - if (radixStringChars.contains(sepChar)) { + if (sepChar.isNotEmpty && radixStringChars.contains(sepChar)) { throw LogicValueConversionException('separation character invalid'); } final radixStr = switch (radix) { @@ -730,7 +736,7 @@ abstract class LogicValue implements Comparable { ? spaceString.substring(1, spaceString.length) : spaceString : '0'; - return '$width$radixStr$fullString'; + return '${includeWidth ? '$width$radixStr' : ''}$fullString'; } /// Create a [LogicValue] from a length/radix-encoded string of the diff --git a/packages/rohd_hierarchy/LICENSE b/packages/rohd_hierarchy/LICENSE new file mode 100644 index 000000000..9e00cc822 --- /dev/null +++ b/packages/rohd_hierarchy/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (C) 2021-2026 Intel Corporation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/rohd_hierarchy/README.md b/packages/rohd_hierarchy/README.md new file mode 100644 index 000000000..f99e9cc88 --- /dev/null +++ b/packages/rohd_hierarchy/README.md @@ -0,0 +1,246 @@ +# rohd_hierarchy + +An incremental design dictionary for hardware module hierarchies. + +## Motivation + +A remote agent — a debugger, a waveform viewer, a schematic renderer, an +AI assistant — needs to understand the structure of a hardware design in order +to ask useful questions about it. Transferring the full design every time is +wasteful. What both sides of a link really need is a shared **dictionary** of +the design: the modules, occurrences, and signals that make it up, plus +a compact way to refer to any object by address. + +Once both sides share the same dictionary, communication becomes cheap: +either side can request data about a specific object by its address alone, +without re-transmitting structural context. + +### What is a design dictionary? + +A design dictionary captures the **hierarchy and connectivity** of a +hardware design: + +- **Occurrences** — unfolded module instances in the hierarchy tree. + Each has a `name`, an optional `definition` (the + module type), child occurrences, and signals. +- **Signals** — named wires within an occurrence. Each has a `name`, + `width`, optional `direction` (input/output/inout), and optional + `value`. + +In the occurrence view of a design, every `Logic` wire and every module +instance is unique. Both are occurrences: either signal/logic occurrences +or module occurrences. + +The full "unfolded" view of a design is its **address space**: every +occurrence and every signal reachable by walking the hierarchy tree. + +### Compact, canonical addressing + +`rohd_hierarchy` assigns each object a **canonical address** — a short +sequence of child indices (e.g. `0.2.4`) that uniquely identifies it within +the tree. + +Addresses are **relative within each occurrence**: an occurrence's address +table maps local indices to its children and signals without relying on any +global namespace. This locality property is what makes the dictionary +**incrementally expandable** — a remote agent can: + +1. Request the top-level dictionary table (the root occurrence's children + and signals). +2. Drill into any child by requesting that child's dictionary table. +3. Continue expanding only the parts of the hierarchy it actually needs. + +At each step, both sides agree on the addresses, so subsequent data +requests (waveform samples, signal values, schematic fragments) carry +only the compact address, not the full path or structural description. + +## Package overview + +`rohd_hierarchy` is a source-agnostic Dart package that implements this +dictionary model. It provides data models, search utilities, and adapter +interfaces that work independently of any particular HDL toolchain or +transport layer. + +### Data models + +- **`HierarchyOccurrence`** — An occurrence of a module instance in the + unfolded hierarchy tree, with children, signals, name, an optional + `definition` (module type), and a primitive flag. Call `buildAddresses()` + to assign a canonical `OccurrenceAddress` to every occurrence and signal + in O(n). Use `signalCount` and `computedSignalCount` for efficient + subtree counts. +- **`OccurrenceAddress`** — An immutable, index-based path through the + tree (e.g. `[0, 2, 4]`). Supports conversion to/from dot-separated + strings. Works as an O(1) cache key. +- **`SignalOccurrence`** — Signal metadata: name, width, optional + direction, and optional value. Signals with a `direction` serve as + ports (input, output, inout). + +### Services & adapters + +- **`HierarchyService`** — A mixin providing tree-walking search and + navigation: `searchSignals()`, `searchOccurrences()`, + `autocompletePaths()`, regex/glob search (`searchSignalsRegex()`, + `searchOccurrencesRegex()`), and address↔pathname conversion. +- **`BaseHierarchyAdapter`** — An abstract class wrapping a + `HierarchyOccurrence` tree with `HierarchyService`. Use + `BaseHierarchyAdapter.fromTree()` to wrap an existing tree. +- **`NetlistHierarchyAdapter`** — A concrete adapter that parses netlist + JSON into a `HierarchyOccurrence` tree. + +### Search queries + +- **`HierarchyQuery`** — Abstract base class for pluggable search + strategies. The matching logic is decoupled from tree traversal. +- **`PrefixQuery`** — Prefix-substring matching. Segments split on `/` + or `.` are matched case-sensitively via `startsWith` (signals) or + `contains` (occurrences). Created via `HierarchyQuery.prefix()`. +- **`RegexQuery`** — Regex/glob matching. Each segment is compiled as a + regex. Supports `*` (any chars), `?` (one char), `**` (zero or more + hierarchy levels), character classes (`[0-9]`), alternation + (`(clk|reset)`), and quantifiers. Created via `HierarchyQuery.regex()`. + +### Search controller + +- **`HierarchySearchController`** — A pure-Dart controller for + keyboard-navigable search result lists, with `updateQuery()`, + `selectNext()` / `selectPrevious()`, `tabComplete()`, and scroll-offset + helpers. Factories `forSignals()` and `forOccurrences()` cover the + common cases. + +## Usage + +### Building a dictionary from a netlist + +```dart +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +final dict = NetlistHierarchyAdapter.fromJson(netlistJsonString); +final root = dict.root; // the top-level dictionary table +``` + +### Wrapping an existing tree + +When you already have a `HierarchyOccurrence` tree (e.g. from a VCD +parser, a ROHD simulation, or any other source), wrap it to gain search +and address resolution: + +```dart +final dict = BaseHierarchyAdapter.fromTree(rootNode); +``` + +### Incremental expansion by a remote agent + +A remote agent does not need the full tree up front. It can expand the +dictionary one level at a time: + +```dart +// Agent receives the root table +final root = dict.root; + +// Agent picks a child to expand (e.g. child 2) +final child = root.children[2]; + +// The child's own children and signals are its local dictionary table. +// The agent now knows addresses 2.0, 2.1, ... for that subtree. +``` + +### Compact address-based communication + +Once both sides share the dictionary, data requests use addresses only: + +```dart +// Resolve a human-readable pathname to a canonical address +final addr = dict.pathnameToAddress('Counter/clk'); + +// Send the compact address over the wire: "0.1" +final wire = addr!.toDotString(); + +// The other side resolves it back +final resolved = dict.occurrenceByAddress(OccurrenceAddress.fromDotString(wire)); +final pathname = dict.addressToPathname(addr!); +``` + +### Searching the dictionary + +#### Prefix search (default) + +Segments are split on `/` or `.` and matched as case-sensitive substrings: + +```dart +// Find all signals whose path contains 'cpu' then 'clk' +final signals = dict.searchSignals('cpu/clk'); + +// Find occurrences containing 'counter' +final modules = dict.searchOccurrences('counter'); + +// Tab-completion for partial paths +final completions = dict.autocompletePaths('Top/CPU/'); +``` + +#### Regex / glob search + +Each segment is a regex anchored to the full name. Glob wildcards `*` +and `?` are auto-converted. Use `**` to match across hierarchy levels: + +```dart +// All 'clk' signals anywhere in the design +final clocks = dict.searchSignalsRegex('Top/**/clk'); + +// Signals named d0–d15 in any regfile +final data = dict.searchSignalsRegex('Top/**/regfile/d[0-9]+'); + +// Either 'clk' or 'reset' anywhere +final resets = dict.searchSignalsRegex('Top/**/(clk|reset)'); + +// All cache channels ch0–ch2 +final channels = dict.searchOccurrencesRegex('Top/mem_ctrl/ch[0-2]'); + +// Signals containing 'mux' in their name +final muxed = dict.searchSignalsRegex('Top/**/.*mux.*'); + +// All signals in a specific module +final all = dict.searchSignalsRegex('Top/CPU/ALU/*'); +``` + +### Constructing occurrences manually + +```dart +final root = HierarchyOccurrence( + name: 'Counter', + definition: 'Counter', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'count', width: 8, direction: 'output'), + ], + children: [ + HierarchyOccurrence( + name: 'adder', + definition: 'Adder', + signals: [ + SignalOccurrence(name: 'a', width: 8), + SignalOccurrence(name: 'b', width: 8), + SignalOccurrence(name: 'sum', width: 8), + ], + ), + ], +); + +// Assign canonical addresses +root.buildAddresses(); + +// Now every occurrence and signal has an address +print(root.children.first.path()); // 'Counter/adder' +print(root.signals.first.path()); // 'Counter/clk' +``` + +## Design principles + +| Principle | How it is achieved | +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Source-agnostic** | The data model is independent of any HDL toolchain. `NetlistHierarchyAdapter` handles netlist JSON; `BaseHierarchyAdapter.fromTree()` wraps any tree. | +| **Incremental** | Addresses are relative within each occurrence. A remote agent expands only the subtrees it needs, one dictionary table at a time. | +| **Compact** | `OccurrenceAddress` is a short index path (e.g. `0.2.4`), not a full dotted pathname. Both sides resolve it locally. | +| **Canonical** | `buildAddresses()` assigns deterministic indices in tree order. The same design always produces the same addresses. | +| **No global namespace** | Each occurrence's address table is self-contained. Adding or removing a sibling subtree does not invalidate addresses in unrelated parts of the tree. | +| **Transport-independent** | The package defines the dictionary model, not the wire protocol. Any transport (VM service, JSON-RPC, gRPC, WebSocket) can carry the compact addresses. | diff --git a/packages/rohd_hierarchy/analysis_options.yaml b/packages/rohd_hierarchy/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/packages/rohd_hierarchy/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/packages/rohd_hierarchy/lib/rohd_hierarchy.dart b/packages/rohd_hierarchy/lib/rohd_hierarchy.dart new file mode 100644 index 000000000..86ed7c336 --- /dev/null +++ b/packages/rohd_hierarchy/lib/rohd_hierarchy.dart @@ -0,0 +1,50 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_hierarchy.dart +// Main library export for rohd_hierarchy package. +// +// 2026 January +// Author: Desmond Kirkpatrick + +/// Generic hierarchy data models for hardware module navigation. +/// +/// This library provides source-agnostic data models for representing +/// hardware module hierarchies: +/// +/// ## Core Data Models +/// - `OccurrenceAddress` - Efficient index-based addressing for tree navigation +/// - `HierarchyOccurrence` - An occurrence of a module definition in the tree +/// - `SignalOccurrence` - A signal in the hierarchy +/// +/// ## Search & Navigation +/// - `SignalSearchResult` - Result of a signal search with enriched metadata +/// - `OccurrenceSearchResult` - Result of an occurrence search with metadata +/// - `HierarchyService` - Abstract interface for hierarchy navigation +/// - `HierarchySearchController` - Pure Dart search state controller +/// +/// ## Adapters +/// - `BaseHierarchyAdapter` - Base class with shared adapter implementation +/// - `NetlistHierarchyAdapter` - Adapter for netlist JSON format +/// +/// This package has no dependencies and can be used standalone by any +/// application that needs to navigate hardware hierarchies. +/// +/// ## Quick Start +/// ```dart +/// 1. Create hierarchy +/// final root = HierarchyOccurrence(id: 'top', name: 'top'); +/// root.buildAddresses(); // Enable address-based navigation +/// +/// 2. Search +/// final service = BaseHierarchyAdapter.fromTree(root); +/// final results = service.searchSignals('clk'); +/// ``` +library; + +export 'src/base_hierarchy_adapter.dart'; +export 'src/hierarchy_models.dart'; +export 'src/hierarchy_query.dart'; +export 'src/hierarchy_search_controller.dart'; +export 'src/hierarchy_service.dart'; +export 'src/netlist_hierarchy_adapter.dart'; diff --git a/packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart b/packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart new file mode 100644 index 000000000..772007497 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart @@ -0,0 +1,80 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// base_hierarchy_adapter.dart +// Base class with shared implementation for hierarchy adapters. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_models.dart'; +import 'package:rohd_hierarchy/src/hierarchy_service.dart'; + +/// Base class providing shared implementation for hierarchy adapters. +/// +/// The [HierarchyOccurrence] tree rooted at [root] is the single source of +/// truth. Children and signals are read directly from each occurrence's +/// [HierarchyOccurrence.children] and [HierarchyOccurrence.signals] lists. +/// Lookups use [OccurrenceAddress]-based navigation. +/// +/// Concrete adapters should: +/// 1. Extend this class +/// 2. Build a complete [HierarchyOccurrence] tree (with children and signals +/// populated on each occurrence) +/// 3. Set the [root] occurrence +/// +/// Search, autocomplete, and signal lookup are implemented by +/// [HierarchyService] via recursive tree walking. +abstract class BaseHierarchyAdapter with HierarchyService { + HierarchyOccurrence? _root; + + /// Creates a [BaseHierarchyAdapter]. + BaseHierarchyAdapter(); + + /// Creates an adapter wrapping an existing [HierarchyOccurrence] tree. + /// + /// The tree itself is the single source of truth — children and signals + /// are read directly from the [HierarchyOccurrence] lists. + /// + /// Example usage: + /// ```dart + /// final treeRoot = await dataSource.evalModuleTree(); + /// final service = BaseHierarchyAdapter.fromTree(treeRoot); + /// final paths = service.searchSignalPaths('clk'); + /// ``` + factory BaseHierarchyAdapter.fromTree( + HierarchyOccurrence rootNode, + ) = _TreeBackedAdapter; + + /// Sets the root occurrence. Call this once during initialisation. + set root(HierarchyOccurrence node) { + _root = node; + } + + // ───────────────────────────────────────────────────────────────────────── + // HierarchyService concrete accessors — all tree-walking, no flat maps + // ───────────────────────────────────────────────────────────────────────── + + @override + HierarchyOccurrence get root { + if (_root == null) { + throw StateError( + 'Root occurrence not set. Call setRoot() during initialization.'); + } + return _root!; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tree-backed implementation returned by BaseHierarchyAdapter.fromTree() +// ───────────────────────────────────────────────────────────────────────────── + +/// Private adapter that wraps an existing [HierarchyOccurrence] tree. +/// +/// Children and signals are read directly from the tree occurrences. +class _TreeBackedAdapter extends BaseHierarchyAdapter { + _TreeBackedAdapter(HierarchyOccurrence rootNode) { + root = rootNode; + rootNode.buildAddresses(); + } +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_constants.dart b/packages/rohd_hierarchy/lib/src/hierarchy_constants.dart new file mode 100644 index 000000000..afb8b8722 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_constants.dart @@ -0,0 +1,14 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_constants.dart +// Shared constants for hierarchy path formatting and parsing. +// +// 2026 July +// Author: Desmond Kirkpatrick + +/// Canonical separator used in hierarchy paths returned by this package. +const String hierarchyPathSeparator = '/'; + +/// Default maximum number of results returned by hierarchy search methods. +const int defaultHierarchySearchLimit = 100; diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart new file mode 100644 index 000000000..2f7cb3f76 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -0,0 +1,16 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_models.dart +// Barrel file re-exporting all hierarchy data model classes. +// +// 2026 January +// Author: Desmond Kirkpatrick + +export 'hierarchy_constants.dart'; +export 'hierarchy_occurrence.dart'; +export 'hierarchy_search_result.dart'; +export 'occurrence_address.dart'; +export 'occurrence_search_result.dart'; +export 'signal_occurrence.dart'; +export 'signal_search_result.dart'; diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart new file mode 100644 index 000000000..06fbf28f7 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart @@ -0,0 +1,258 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_occurrence.dart +// An occurrence of a module definition in the unfolded hierarchy tree. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; +import 'package:rohd_hierarchy/src/occurrence_address.dart'; +import 'package:rohd_hierarchy/src/signal_occurrence.dart'; + +/// An occurrence of a module definition in the unfolded hierarchy tree. +/// +/// This is the core structural data model, independent of waveform data. +/// Path strings are computed on demand from parent references rather than +/// stored — call [path] with your desired separator. +class HierarchyOccurrence { + /// Display name of this occurrence (instance name within its parent). + final String name; + + /// Definition (module) name for this occurrence. + final String? definition; + + /// Whether this occurrence is a primitive cell (gate, operator, register, + /// etc.) whose internal structure is not useful for design navigation. + /// + /// Set by the parser/adapter that creates the occurrence. The netlist + /// adapter sets this for cells that lack a module definition in the JSON or + /// whose definition starts with `$` (netlist built-in primitives). + /// Tool-specific primitives (e.g. ROHD's FlipFlop → `$dff`) are handled by + /// the synthesizer mapping them to `$`-prefixed definitions before the JSON + /// is written. + final bool isPrimitive; + + /// Signals within this occurrence (includes both internal signals and + /// ports). Empty for leaf occurrences. + final List signals; + + /// Child occurrences. Populated from sub-modules in the hierarchy. + final List children; + + /// Hierarchical address for this occurrence. + /// Assigned by [buildAddresses] to enable efficient navigation. + /// Format: [child0, child1, ..., childN] for nested occurrences. + OccurrenceAddress? get address => _address; + OccurrenceAddress? _address; + + /// Parent occurrence, or `null` for the root. + /// Set by [buildAddresses]. + HierarchyOccurrence? get parent => _parent; + HierarchyOccurrence? _parent; + + /// Creates a [HierarchyOccurrence] with the given properties. + HierarchyOccurrence({ + required this.name, + this.definition, + this.isPrimitive = false, + List? signals, + List? children, + }) : signals = signals ?? [], + children = children ?? []; + + /// Compute the full hierarchical path by walking up the parent chain. + /// + /// Uses [separator] between path segments (default `/`). + /// Returns just [name] for the root (no parent). + String path({String separator = hierarchyPathSeparator}) { + if (_parent == null) { + return name; + } + final parts = []; + HierarchyOccurrence? cur = this; + while (cur != null) { + parts.add(cur.name); + cur = cur._parent; + } + return parts.reversed.join(separator); + } + + /// Returns only signals that are ports (have a direction). + List get ports => signals.where((s) => s.isPort).toList(); + + // ───────────────── Name → offset (index) lookups ───────────────── + + /// Lazily-built index: child name → offset in [children]. + Map? _childNameIndex; + + /// Lazily-built index: signal name → offset in [signals]. + Map? _signalNameIndex; + + /// Return the offset (index) of the child with [name] in [children], + /// or -1 if not found. Case-sensitive. + /// O(1) after first call (lazily builds index). + int childIndexByName(String name) { + _childNameIndex ??= { + for (var i = 0; i < children.length; i++) children[i].name: i, + }; + return _childNameIndex![name] ?? -1; + } + + /// Return the offset (index) of the signal with [name] in [signals], + /// or -1 if not found. Case-sensitive. + /// O(1) after first call (lazily builds index). + int signalIndexByName(String name) { + _signalNameIndex ??= { + for (var i = 0; i < signals.length; i++) signals[i].name: i, + }; + return _signalNameIndex![name] ?? -1; + } + + /// Whether [cellType] represents a netlist built-in primitive cell type. + /// + /// Returns `true` for `$`-prefixed types (`$mux`, `$dff`, `$and`, etc.) + /// which are netlist built-in operators and primitives. + /// + /// Tool-specific primitive types (e.g. ROHD's `FlipFlop`) should be + /// handled by the producer: the synthesizer should map them to + /// `$`-prefixed cell types in the JSON output, or the adapter should + /// set [isPrimitive] on the occurrence at construction time. + /// + /// Use this before a [HierarchyOccurrence] exists (e.g. when deciding + /// whether to recurse into a netlist cell definition). For an existing + /// occurrence, use the getter [isPrimitiveCell] instead. + static bool isPrimitiveType(String cellType) => cellType.startsWith(r'$'); + + /// Whether this occurrence represents a primitive cell that should be hidden + /// from the occurrence tree. + /// + /// Checks the [isPrimitive] field (set by the adapter at construction time) + /// and falls back to [isPrimitiveType] on the occurrence's [definition]. + bool get isPrimitiveCell => + isPrimitive || (definition != null && isPrimitiveType(definition!)); + + /// Returns only input signals. + List get inputs => + signals.where((s) => s.direction == 'input').toList(); + + /// Returns only output signals. + List get outputs => + signals.where((s) => s.direction == 'output').toList(); + + /// Returns only inout signals. + List get inouts => + signals.where((s) => s.direction == 'inout').toList(); + + /// Number of port signals in this occurrence. + int get portCount => signals.where((s) => s.isPort).length; + + /// Finds the sub-field [SignalOccurrence] entries for a struct/array signal. + /// + /// Given a `parentSignal` that has `logicType` metadata (struct fields or + /// array dims), looks up the expected sub-field signal names in this + /// occurrence's signal list using the Namer/Sanitizer naming convention: + /// `{parentSignalName}_{fieldName}` + /// + /// Returns a list of resolved sub-field signals in field order. + /// Entries may be null if a particular sub-field signal wasn't found + /// (e.g. the netlist didn't emit it, or it was optimized away). + List<({SignalOccurrence? signal, String fieldLabel, int width, int startBit})> + findSubFieldSignals(SignalOccurrence parentSignal) { + final descriptors = parentSignal.subFieldDescriptors; + if (descriptors.isEmpty) { + return const []; + } + + return descriptors.map((d) { + final idx = signalIndexByName(d.expectedName); + final sig = idx >= 0 ? signals[idx] : null; + return ( + signal: sig, + fieldLabel: d.fieldLabel, + width: d.width, + startBit: d.startBit, + ); + }).toList(); + } + + /// Collect all signals under this occurrence in depth-first order. + /// + /// Visits this occurrence's [signals] first, then recurses into + /// [children] in order. Useful for flat iteration or counting, but + /// signals should always be identified by their [OccurrenceAddress] or + /// path — never by a positional index in this list. + /// + /// Production code should use [signalCount], [computedSignalCount], or + /// a recursive visitor instead of materializing the full list. + @visibleForTesting + List depthFirstSignals() => + [...signals, ...children.expand((c) => c.depthFirstSignals())]; + + /// Total number of signals in this subtree (O(n) recursive count). + /// + /// Equivalent to `depthFirstSignals().length` but avoids allocating the + /// intermediate list. + int get signalCount => + signals.length + children.fold(0, (sum, c) => sum + c.signalCount); + + /// Number of computed signals in this subtree. + /// + /// Equivalent to + /// `depthFirstSignals().where((s) => s.isComputed).length` + /// but avoids allocating the intermediate list. + int get computedSignalCount => + signals.where((s) => s.isComputed).length + + children.fold(0, (sum, c) => sum + c.computedSignalCount); + + /// Build hierarchical addresses for this occurrence and all descendants. + /// + /// This performs a single O(n) tree traversal to assign [OccurrenceAddress] + /// to every occurrence and signal in the tree. Call this once after tree + /// construction to enable efficient address-based navigation. + /// + /// **Signal address ordering**: ports (signals with a non-null + /// [SignalOccurrence.direction]) are assigned indices first + /// (`0 .. portCount-1`), followed by internal signals + /// (`portCount .. signals.length-1`). Within each group the + /// original list order is preserved. + /// + /// This means a port's [SignalOccurrence.portIndex] always equals its + /// signal address index, which consumers (e.g. schematic hyperedges) can + /// rely on remaining stable across incremental expansion. + /// + /// Example: + /// ```dart + /// root.buildAddresses(); // Assign addresses to all occurrences/signals + /// final signalAddr = signals[0].address; // Now available + /// ``` + void buildAddresses([OccurrenceAddress startAddr = OccurrenceAddress.root]) { + _address = startAddr; + + // Assign ports first, then internal signals, so that port indices + // are stable across incremental hierarchy expansion. + var idx = 0; + for (final s in signals) { + if (s.isPort) { + s + ..address = startAddr.signal(idx++) + ..parent = this; + } + } + for (final s in signals) { + if (!s.isPort) { + s + ..address = startAddr.signal(idx++) + ..parent = this; + } + } + + for (final (i, c) in children.indexed) { + c + .._parent = this + ..buildAddresses(startAddr.child(i)); + } + } +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_query.dart b/packages/rohd_hierarchy/lib/src/hierarchy_query.dart new file mode 100644 index 000000000..c350a2391 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_query.dart @@ -0,0 +1,152 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_query.dart +// Pluggable search query abstraction for hierarchy search. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; +import 'package:rohd_hierarchy/src/hierarchy_service.dart'; +import 'package:rohd_hierarchy/src/prefix_query.dart'; +import 'package:rohd_hierarchy/src/regex_query.dart'; + +// Re-export so callers importing hierarchy_query.dart get the concrete types. +export 'package:rohd_hierarchy/src/prefix_query.dart'; +export 'package:rohd_hierarchy/src/regex_query.dart'; + +/// What kind of hierarchy elements a query should match. +enum SearchTarget { + /// Match only [HierarchyOccurrence] nodes (modules, instances). + occurrences, + + /// Match only signals within occurrences. + signals, + + /// Match both occurrences and signals. + both, +} + +/// Abstract base class for hierarchy search queries. +/// +/// A [HierarchyQuery] encapsulates the *matching strategy* (how names are +/// compared) independently of the *tree traversal* (which is always +/// performed by [HierarchyService]). +/// +/// ## Contract with [HierarchyService] +/// +/// The service walks the [HierarchyOccurrence] tree depth-first. +/// At each node it calls: +/// +/// 1. [matchOccurrence] — does this occurrence name satisfy the query at the +/// current match state? Returns a set of successor states (empty = +/// prune this branch). +/// 2. [matchSignal] — does this signal name satisfy the query at the +/// current match state? +/// 3. [isComplete] — have all parts of the query been consumed at the +/// given state? +/// +/// "Match state" is an opaque integer that the query owns. It typically +/// tracks how many segments/tokens of the query have been consumed so far. +/// The initial state is always `0`. +/// +/// ## Crossing hierarchy boundaries +/// +/// If [crossesBoundaries] is true the service will, at each depth, +/// additionally try advancing with the *current* state even when the +/// occurrence doesn't match — allowing matches to span across +/// intermediate hierarchy levels (like `**` in glob patterns). +/// +/// ## Subclassing +/// +/// Implement a concrete query by overriding at least [matchOccurrence], +/// [matchSignal], [isComplete], and [segmentCount]. +/// +/// The factory [HierarchyQuery.prefix] creates the default +/// prefix-substring query. [HierarchyQuery.regex] creates a +/// regex/glob query. +/// +/// ```dart +/// // Custom fuzzy query +/// class FuzzyQuery extends HierarchyQuery { +/// FuzzyQuery(String rawQuery) +/// : super(rawQuery, target: SearchTarget.signals); +/// ... +/// } +/// ``` +abstract class HierarchyQuery { + /// The original user-supplied query string. + final String rawQuery; + + /// What this query matches — occurrences, signals, or both. + final SearchTarget target; + + /// Whether this query can match across hierarchy boundaries. + /// + /// When true, the tree walker will try the current match state at + /// deeper levels even when intermediate occurrences don't match. + /// Conceptually equivalent to an implicit `**` between segments. + final bool crossesBoundaries; + + /// Creates a query from [rawQuery]. + /// + /// Subclasses should parse/compile the query in their constructor. + const HierarchyQuery( + this.rawQuery, { + this.target = SearchTarget.signals, + this.crossesBoundaries = false, + }); + + /// Number of logical segments in the parsed query. + /// + /// Used by the tree walker to know when the query is fully consumed. + int get segmentCount; + + /// Whether the query is empty / trivial (should return no results). + bool get isEmpty => rawQuery.trim().isEmpty; + + /// Try matching an occurrence name at match state [stateIndex]. + /// + /// Returns a set of successor states. Multiple successors arise when + /// the query is ambiguous at this point (e.g. a glob-star `**` can + /// consume zero or more levels). + /// + /// An empty set means "no match — prune this subtree". + Set matchOccurrence(String occurrenceName, int stateIndex); + + /// Whether [signalName] matches the query at state [stateIndex]. + /// + /// Only called when [target] includes signals. + bool matchSignal(String signalName, int stateIndex); + + /// Whether the query is fully consumed at [stateIndex]. + /// + /// Returns true when all segments have been matched and the current + /// tree position is a valid result. + bool isComplete(int stateIndex); + + // ──────────────── Built-in query factories ──────────────── + + /// Create a **prefix-substring** query. + /// + /// The query is split on `/` or `.` into segments. Each segment is + /// matched via `startsWith` (for signals) or + /// `contains` (for occurrences) against names at successive depths. + factory HierarchyQuery.prefix( + String rawQuery, { + SearchTarget target, + }) = PrefixQuery; + + /// Create a **regex/glob** query. + /// + /// Segments are separated by `/`. Each segment is compiled as a + /// case-sensitive regex anchored to the full name. The special + /// segment `**` matches zero or more hierarchy levels. + /// + /// Glob wildcards `*` and `?` are auto-converted to regex equivalents. + factory HierarchyQuery.regex( + String rawQuery, { + SearchTarget target, + }) = RegexQuery; +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart b/packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart new file mode 100644 index 000000000..a82375b4b --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart @@ -0,0 +1,216 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_search_controller.dart +// Pure Dart controller for hierarchy search list navigation. +// +// 2026 February +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +/// Pure Dart controller for hierarchy search list navigation. +/// +/// Manages search results and keyboard-style list selection without +/// any Flutter dependency. Widgets call controller methods, then +/// refresh their own UI (e.g. `setState`). +/// +/// Generic over the result type [R] — typically [SignalSearchResult] +/// or [OccurrenceSearchResult]. +/// +/// ```dart +/// // In a Flutter widget: +/// final controller = HierarchySearchController.forSignals(hierarchy); +/// +/// void _onSearchChanged() { +/// controller.updateQuery(_textController.text); +/// setState(() {}); +/// } +/// ``` +class HierarchySearchController { + /// The search function that produces results from a normalised query. + final List Function(String normalizedQuery) _searchFn; + + /// Normalises a raw user query (e.g. replaces `.` with `/`). + final String Function(String rawQuery) _normalizeFn; + + List _results = []; + int _selectedIndex = 0; + + /// Create a controller with custom search and normalise functions. + HierarchySearchController({ + required List Function(String normalizedQuery) searchFn, + required String Function(String rawQuery) normalizeFn, + }) : _searchFn = searchFn, + _normalizeFn = normalizeFn; + + /// Create a controller for **signal** search on the given + /// [HierarchyService]. + /// + /// When the query contains glob/regex metacharacters, normalisation + /// is skipped so that `.` keeps its regex meaning (use `/` as the + /// hierarchy separator in regex patterns). + factory HierarchySearchController.forSignals( + HierarchyService hierarchy, + ) => + HierarchySearchController( + searchFn: (q) => hierarchy.searchSignals(q) as List, + normalizeFn: (q) => HierarchyService.hasRegexChars(q) + ? q + : HierarchySearchResult.normalizeQuery(q), + ); + + /// Create a controller for **occurrence** search on the given + /// [HierarchyService]. + /// + /// When the query contains glob/regex metacharacters, normalisation + /// is skipped so that `.` keeps its regex meaning (use `/` as the + /// hierarchy separator in regex patterns). + factory HierarchySearchController.forOccurrences( + HierarchyService hierarchy, + ) => + HierarchySearchController( + searchFn: (q) => hierarchy.searchOccurrences(q) as List, + normalizeFn: (q) => HierarchyService.hasRegexChars(q) + ? q + : HierarchySearchResult.normalizeQuery(q), + ); + + // ─────────────── State accessors ─────────────── + + /// The current search results. + List get results => _results; + + /// Index of the currently highlighted result. + int get selectedIndex => _selectedIndex; + + /// Whether there are any results. + bool get hasResults => _results.isNotEmpty; + + /// A human-readable counter string, e.g. `"3/12"`, or empty when + /// there are no results. + String get counterText => + hasResults ? '${_selectedIndex + 1}/${_results.length}' : ''; + + /// The currently selected result, or `null` if the list is empty. + R? get currentSelection => _results.isEmpty ? null : _results[_selectedIndex]; + + // ─────────────── Mutations ─────────────── + + /// Update search results for [rawQuery]. + /// + /// Normalises the query, runs the search function, and resets the + /// selection to the first result. The caller should rebuild its UI + /// after calling this. + void updateQuery(String rawQuery) { + if (rawQuery.isEmpty) { + _results = []; + _selectedIndex = 0; + return; + } + final normalized = _normalizeFn(rawQuery); + _results = _searchFn(normalized); + _selectedIndex = 0; + } + + /// Move selection to the next result, wrapping around. + void selectNext() { + if (_results.isEmpty) { + return; + } + _selectedIndex = (_selectedIndex + 1) % _results.length; + } + + /// Move selection to the previous result, wrapping around. + void selectPrevious() { + if (_results.isEmpty) { + return; + } + _selectedIndex = (_selectedIndex - 1 + _results.length) % _results.length; + } + + /// Move selection to a specific [index]. + /// + /// Clamps to valid range. Useful for tap-to-select in a list view. + void selectAt(int index) { + if (_results.isEmpty) { + return; + } + _selectedIndex = index.clamp(0, _results.length - 1); + } + + /// Clear all results and reset the selection index. + void clear() { + _results = []; + _selectedIndex = 0; + } + + // ─────────────── Tab-completion ─────────────── + + /// Compute the tab-completion expansion for [currentQuery]. + /// + /// Finds the longest common prefix of all current result display paths + /// and returns it if it is strictly longer than [currentQuery]. + /// Returns `null` when there is nothing to expand. + /// + /// [displayPath] extracts the comparable path string from each result. + /// The default implementation handles [SignalSearchResult] and + /// [OccurrenceSearchResult] automatically; pass a custom extractor for + /// other result types. + String? tabComplete( + String currentQuery, { + String Function(R result)? displayPath, + }) { + if (_results.isEmpty) { + return null; + } + + final extractor = displayPath ?? _defaultDisplayPath; + final paths = _results.map(extractor).toList(); + final prefix = HierarchyService.longestCommonPrefix(paths); + if (prefix == null) { + return null; + } + + // Normalise the query the same way UpdateQuery does so lengths are + // comparable (e.g. dots → slashes). + final normalizedQuery = _normalizeFn(currentQuery); + if (prefix.length <= normalizedQuery.length) { + return null; + } + return prefix; + } + + /// Default display-path extractor for the well-known result types. + static String _defaultDisplayPath(T result) { + if (result is HierarchySearchResult) { + return result.displayPath; + } + return result.toString(); + } + + // ─────────────── Scroll helper ─────────────── + + /// Compute the scroll offset needed to reveal the selected item in a + /// fixed-height list. + /// + /// Returns `null` if the item is already visible. The caller should + /// call `scrollController.jumpTo(offset)` with the returned value. + /// + /// This is a pure calculation with no Flutter dependency. + static double? scrollOffsetToReveal({ + required int selectedIndex, + required double itemHeight, + required double viewportHeight, + required double currentOffset, + }) { + final target = selectedIndex * itemHeight; + if (target < currentOffset) { + return target; + } + if (target + itemHeight > currentOffset + viewportHeight) { + return target + itemHeight - viewportHeight; + } + return null; + } +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart b/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart new file mode 100644 index 000000000..00bb0167c --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_search_result.dart +// Base class for hierarchy search results. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; + +/// Base class for hierarchy search results. +/// +/// Holds the common fields shared by signal and occurrence search +/// results: a canonical ID string, pre-split path segments, and display +/// helpers that strip the top-level module name. +@immutable +abstract class HierarchySearchResult { + /// The full hierarchical path that was found. + /// Example: `"Top/counter/clk"` or `"Top/CPU/ALU"`. + final String id; + + /// The hierarchical path segments. + /// Example: `["Top", "counter", "clk"]`. + final List path; + + /// Creates a hierarchy search result. + const HierarchySearchResult({required this.id, required this.path}); + + /// The leaf name (last path segment). + String get name => path.isNotEmpty ? path.last : id; + + // ───────────────────── Display helpers ───────────────────── + + /// Display path with the top-level module name stripped. + /// + /// For `Top/counter/clk` this returns `counter/clk`. + /// For a single-segment path returns the original [id]. + String get displayPath => displaySegments.join(hierarchyPathSeparator); + + /// Path segments with the top-level module name stripped. + /// + /// For `["Top", "counter", "clk"]` returns `["counter", "clk"]`. + List get displaySegments => path.length > 1 ? path.sublist(1) : path; + + /// Normalize a user query for hierarchy search. + /// + /// Converts common separators (`.`) to the canonical `/` separator. + static String normalizeQuery(String query) => + query.replaceAll('.', hierarchyPathSeparator); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HierarchySearchResult && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_service.dart b/packages/rohd_hierarchy/lib/src/hierarchy_service.dart new file mode 100644 index 000000000..791293429 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_service.dart @@ -0,0 +1,875 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_service.dart +// Abstract interface for source-agnostic hardware hierarchy navigation. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_models.dart'; + +/// A source-agnostic interface for navigating hardware hierarchy. +/// +/// All search and navigation is driven by walking the [HierarchyOccurrence] +/// tree. Occurrences hold their [HierarchyOccurrence.name], +/// [HierarchyOccurrence.children], and [HierarchyOccurrence.signals]. Full +/// paths are constructed on the fly by joining names with +/// [hierarchyPathSeparator] +/// — no pre-baked path strings are needed for search. +/// +/// Key methods: +/// - [searchSignals] — incremental signal search +/// - [searchOccurrences] — find occurrences by name +/// - [matchOccurrences] — find occurrences, returning [HierarchyOccurrence] +/// objects +/// - [autocompletePaths] — incremental path completion +abstract mixin class HierarchyService { + /// The root occurrence for the hierarchy. + HierarchyOccurrence get root; + + // ───────────── Address-based occurrence/signal lookup ──────────────── + + /// Find an occurrence by its [OccurrenceAddress]. O(depth). + HierarchyOccurrence? occurrenceByAddress(OccurrenceAddress address) => + address.path.fold( + root, + (node, idx) => node != null && idx >= 0 && idx < node.children.length + ? node.children[idx] + : null); + + /// Find a signal by its [OccurrenceAddress]. + /// + /// The parent portion of [address] navigates to the owning occurrence; + /// the last index selects the signal within that occurrence. O(depth). + SignalOccurrence? signalByAddress(OccurrenceAddress address) { + if (address.path.isEmpty) { + return null; + } + final node = occurrenceByAddress( + OccurrenceAddress(address.path.sublist(0, address.path.length - 1))); + final sigIdx = address.path.last; + return (node != null && sigIdx >= 0 && sigIdx < node.signals.length) + ? node.signals[sigIdx] + : null; + } + + // ───────────── Address ↔ pathname conversion ────────────────── + + /// Convert a pathname (e.g. `"Top/sub/clk"` or `"Top.sub.clk"`) to a + /// [OccurrenceAddress] by walking the tree. + /// + /// Delegates to [OccurrenceAddress.tryFromPathname]. + OccurrenceAddress? pathnameToAddress(String pathname) => + OccurrenceAddress.tryFromPathname(pathname, root); + + /// Resolve a `/`-separated pathname to a [HierarchyOccurrence]. + /// + /// Convenience that composes [pathnameToAddress] and [occurrenceByAddress]. + /// Returns `null` when [pathname] does not match any occurrence in the + /// tree. + HierarchyOccurrence? occurrenceByPathname(String pathname) { + final addr = pathnameToAddress(pathname); + return addr == null ? null : occurrenceByAddress(addr); + } + + /// Convert a [OccurrenceAddress] back to a `/`-separated pathname by + /// walking the tree using child indices. + /// + /// Returns `null` if the address doesn't resolve in the current tree + /// (e.g. out-of-bounds indices). O(depth). + /// + /// For signal addresses, the last index is resolved as a signal within + /// the parent occurrence. For pure occurrence addresses, every index + /// is a child. + /// + /// Set [asSignal] to `true` when you know the address points to a signal + /// (the last index is a signal offset rather than a child offset). + /// When `false` (default), all indices are treated as child offsets. + String? addressToPathname(OccurrenceAddress address, + {bool asSignal = false}) { + if (address.path.isEmpty) { + return root.name; + } + + final indices = address.path; + final moduleEndIdx = asSignal ? indices.length - 1 : indices.length; + + final walked = indices + .sublist(0, moduleEndIdx) + .fold<({List parts, HierarchyOccurrence node})?>(( + parts: [root.name], + node: root, + ), (cur, idx) { + if (cur == null || idx < 0 || idx >= cur.node.children.length) { + return null; + } + final child = cur.node.children[idx]; + return (parts: [...cur.parts, child.name], node: child); + }); + if (walked == null) { + return null; + } + + if (asSignal && indices.isNotEmpty) { + final sigIdx = indices.last; + return (sigIdx >= 0 && sigIdx < walked.node.signals.length) + ? [...walked.parts, walked.node.signals[sigIdx].name] + .join(hierarchyPathSeparator) + : null; + } + return walked.parts.join(hierarchyPathSeparator); + } + + /// Resolve a signal identifier from a VCD or FST waveform file to an + /// [OccurrenceAddress]. + /// + /// VCD/FST waveform readers often expose hierarchy names as dot-separated + /// strings such as `"dut.adder.clk"`. This delegates to + /// [pathnameToAddress], which accepts both dot-separated waveform IDs and + /// slash-separated hierarchy paths. + OccurrenceAddress? waveformIdToAddress(String waveformId) => + pathnameToAddress(waveformId); + + // ───────────────────── Search / autocomplete ───────────────────── + + /// Find hierarchical signal paths matching [query]. + /// + /// Walks the tree, matching name segments incrementally. When the last + /// query segment partially matches a signal name at or below the current + /// node the full path is returned (e.g. `Top/block/signal`). + /// + /// Returns up to [limit] results. + List searchSignalPaths(String query, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + if (query.trim().isEmpty) { + return const []; + } + final parts = _splitPath(query); + final results = []; + _searchSignalsRecursive( + root, [root.name], parts, 0, results, effectiveLimit); + return results; + } + + /// Whether [query] contains glob or regex metacharacters that should + /// trigger the regex search engine instead of the plain substring search. + static bool hasRegexChars(String query) => + query.contains('*') || + query.contains('?') || + query.contains('[') || + query.contains('(') || + query.contains('|') || + query.contains('+'); + + /// Check if an occurrence or any of its descendants match [searchTerm]. + /// + /// The search term is split on `/` or `.` into hierarchical segments. + /// Each segment is matched via substring containment + /// against occurrence names at successive depths. + /// + /// Returns `true` if [searchTerm] is null/empty, or if the occurrence + /// (or a descendant) matches all segments in order. + /// + /// This is useful for tree-view filtering: show an occurrence only when + /// it or one of its descendants matches the user's query. + static bool isOccurrenceMatching( + HierarchyOccurrence node, String? searchTerm) { + if (searchTerm == null || searchTerm.isEmpty) { + return true; + } + + final normalizedQuery = searchTerm.replaceAll('.', hierarchyPathSeparator); + final queryParts = normalizedQuery + .split(hierarchyPathSeparator) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + return _isOccurrenceMatchingRecursive(node, queryParts, 0); + } + + static bool _isOccurrenceMatchingRecursive( + HierarchyOccurrence node, List queryParts, int queryIdx) { + if (queryIdx >= queryParts.length) { + return true; + } + + final currentQueryPart = queryParts[queryIdx]; + final nodeName = node.name; + + final matched = nodeName.contains(currentQueryPart); + final nextQueryIdx = matched ? queryIdx + 1 : queryIdx; + + if (nextQueryIdx >= queryParts.length) { + return true; + } + + return node.children.any((child) => + _isOccurrenceMatchingRecursive(child, queryParts, nextQueryIdx)); + } + + /// Search for signals and return enriched [SignalSearchResult] objects. + /// + /// Automatically dispatches to [searchSignalsRegex] when the query + /// contains glob or regex metacharacters (`*`, `?`, `[`, `(`, `|`, + /// `+`). Otherwise uses [searchSignalPaths] for prefix-based matching. + List searchSignals(String query, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + if (hasRegexChars(query)) { + final pattern = (query.startsWith('**/') || query.startsWith('*/')) + ? query + : '*/$query'; + return searchSignalsRegex(pattern, limit: effectiveLimit); + } + return _toSignalResults(searchSignalPaths(query, limit: effectiveLimit)); + } + + /// Find hierarchical occurrence paths matching [query]. + /// + /// Similar to [searchSignalPaths] but for occurrences instead of + /// signals. Walks the tree, matching name segments incrementally. When + /// the query segments match occurrence names at or below the current + /// level the full path is returned (e.g. `Top/CPU/ALU`). + /// + /// Returns up to [limit] results. + List searchOccurrencePaths(String query, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + if (query.trim().isEmpty) { + return const []; + } + final parts = _splitPath(query); + final results = []; + _searchOccurrencePathsRecursive( + root, [root.name], parts, 0, results, effectiveLimit); + return results; + } + + /// Find hierarchy occurrences whose path matches [query]. + /// + /// Like [searchOccurrencePaths] but returns the [HierarchyOccurrence] objects + /// themselves instead of path strings. + List matchOccurrences(String query, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + if (query.trim().isEmpty) { + return const []; + } + final parts = _splitPath(query); + final results = []; + _matchOccurrencesRecursive(root, parts, 0, results, effectiveLimit); + return results; + } + + /// Autocomplete suggestions for a partial hierarchical path. + /// + /// The partial path is split into segments. Completed segments navigate + /// down the tree; the final (possibly empty) segment is used as a prefix + /// filter on children at that level. Returns up to [limit] full paths + /// (with `/` appended for nodes that have children). + List autocompletePaths(String partialPath, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + final normalized = partialPath.replaceAll('.', hierarchyPathSeparator); + final endsWithSep = normalized.endsWith(hierarchyPathSeparator); + final parts = _splitPath(partialPath); + + // Navigate to the deepest complete segment. + var current = root; + final completedParts = [root.name]; + + final navParts = endsWithSep || parts.isEmpty + ? parts + : parts.sublist(0, parts.length - 1); + for (final seg in navParts) { + // If the segment matches the current node name, stay at this level + // (handles the root name appearing as the first path segment). + if (current.name == seg) { + continue; + } + final child = current.children.where((c) => c.name == seg).firstOrNull; + if (child == null) { + return const []; + } + current = child; + completedParts.add(child.name); + } + + // The trailing prefix to filter on (empty if path ends with separator). + final prefix = (endsWithSep || parts.isEmpty) ? '' : parts.last; + + final suggestions = []; + + // When the prefix matches the current (root-level) node itself and we + // haven't navigated past it, suggest the root path so that typing a + // partial root name produces a completion. + if (prefix.isNotEmpty && + completedParts.length == 1 && + current == root && + current.name.startsWith(prefix)) { + final rootPath = current.name; + suggestions.add(current.children.isNotEmpty + ? '$rootPath$hierarchyPathSeparator' + : rootPath); + } + + for (final child in current.children) { + if (prefix.isEmpty || child.name.startsWith(prefix)) { + final pathParts = [...completedParts, child.name]; + final path = pathParts.join(hierarchyPathSeparator); + suggestions.add( + child.children.isNotEmpty ? '$path$hierarchyPathSeparator' : path); + if (suggestions.length >= effectiveLimit) { + break; + } + } + } + return suggestions; + } + + /// Search for occurrences and return enriched + /// [OccurrenceSearchResult] objects. + /// + /// Automatically dispatches to [searchOccurrencesRegex] when the query + /// contains glob or regex metacharacters (`*`, `?`, `[`, `(`, `|`, + /// `+`). Otherwise uses [searchOccurrencePaths] for prefix-based matching. + List searchOccurrences(String query, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + if (hasRegexChars(query)) { + final pattern = (query.startsWith('**/') || query.startsWith('*/')) + ? query + : '**/$query'; + return searchOccurrencesRegex(pattern, limit: effectiveLimit); + } + return _toOccurrenceResults( + searchOccurrencePaths(query, limit: effectiveLimit)); + } + + // ───────────────── Regex search ───────────────── + + /// Search for signals whose hierarchical path matches a regex [pattern]. + /// + /// The pattern is split on `/` or `.` into segments. Each segment is + /// compiled as a [RegExp] and matched against the + /// corresponding depth in the hierarchy tree. Special segments: + /// + /// - `**` — matches zero or more hierarchy levels (glob-star). Use this + /// to search across hierarchy boundaries, e.g. `Top/**/clk` finds + /// `Top/CPU/ALU/clk`, `Top/Memory/clk`, etc. + /// - Any other string is compiled as a regex anchored to the full name + /// (`^…$`). Plain names therefore match exactly and regex meta- + /// characters like `.*`, `[0-9]+`, etc. work as expected. + /// + /// Returns up to [limit] full hierarchical signal paths. + /// + /// Examples: + /// ```text + /// 'Top/CPU/clk' — exact match at each level + /// 'Top/CPU/.*' — all signals in Top/CPU + /// 'Top/.*/clk' — clk signal one level below Top + /// 'Top/**/clk' — clk signal at any depth below Top + /// 'Top/**/c.*' — signals starting with 'c' at any depth + /// '**/(clk|reset)' — clk or reset anywhere in hierarchy + /// 'Top/CPU/d[0-9]+' — signals like d0, d1, d12 in Top/CPU + /// ``` + List searchSignalPathsRegex(String pattern, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + if (pattern.trim().isEmpty) { + return const []; + } + final segments = _splitRegexPattern(pattern); + final compiled = _compileSegments(segments); + final results = []; + _searchSignalsRegex( + root, [root.name], compiled, 0, results, effectiveLimit); + return results; + } + + /// Search for signals by regex pattern and return enriched results. + List searchSignalsRegex(String pattern, {int? limit}) => + _toSignalResults(searchSignalPathsRegex(pattern, limit: limit)); + + /// Search for occurrence paths matching a regex [pattern]. + /// + /// Same segment syntax as [searchSignalPathsRegex] but matches + /// occurrences instead of signals. + /// + /// Returns up to [limit] full hierarchical occurrence paths. + List searchOccurrencePathsRegex(String pattern, {int? limit}) { + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + if (pattern.trim().isEmpty) { + return const []; + } + final segments = _splitRegexPattern(pattern); + final compiled = _compileSegments(segments); + final results = []; + _matchOccurrencesRegex( + root, [root.name], compiled, 0, results, effectiveLimit); + return results; + } + + /// Search for occurrences by regex pattern and return enriched results. + List searchOccurrencesRegex(String pattern, + {int? limit}) => + _toOccurrenceResults(searchOccurrencePathsRegex(pattern, limit: limit)); + + // ─────────────────── Utility helpers ─────────────────── + + /// Returns the longest common prefix shared by all [paths]. + /// + /// Comparison is case-sensitive. Returns `null` when [paths] is empty + /// or no common prefix exists. + static String? longestCommonPrefix(List paths) { + if (paths.isEmpty) { + return null; + } + final prefix = paths.skip(1).fold(paths.first, (pre, s) { + if (pre == null || pre.isEmpty) { + return null; + } + final end = pre.length < s.length ? pre.length : s.length; + final j = + Iterable.generate(end).takeWhile((i) => pre[i] == s[i]).length; + return j > 0 ? pre.substring(0, j) : null; + }); + return prefix; + } + + // ─────────────────── Private helpers ─────────────────── + + /// Split a query or path on `/` or `.` into non-empty segments. + static List _splitPath(String input) => input + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + /// Split a path on `/` or `.` into non-empty segments, preserving case. + /// + /// Use this when the result is for display or building [SignalSearchResult] + /// path parts — not for matching. + static List _splitPathPreserveCase(String input) => input + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) + .where((s) => s.isNotEmpty) + .toList(); + + /// Enrich signal paths into [SignalSearchResult] objects. + List _toSignalResults(List paths) => + paths.map((fullPath) { + final addr = OccurrenceAddress.tryFromPathname(fullPath, root); + return SignalSearchResult( + signalId: fullPath, + path: _splitPathPreserveCase(fullPath), + signal: addr != null ? signalByAddress(addr) : null, + ); + }).toList(); + + /// Enrich occurrence paths into [OccurrenceSearchResult] objects. + List _toOccurrenceResults(List paths) => + paths.map((fullPath) { + final addr = OccurrenceAddress.tryFromPathname(fullPath, root); + return OccurrenceSearchResult( + occurrenceId: fullPath, + path: _splitPathPreserveCase(fullPath), + occurrence: (addr != null ? occurrenceByAddress(addr) : null) ?? root, + ); + }).toList(); + + /// Recursively search for signals matching query parts. + /// + /// Walks the tree maintaining the path of names. When the accumulated match + /// depth reaches the query length, checks signals at that node. Partial + /// last-segment matching also checks signals at partially-matched nodes. + /// + /// Uses [HierarchyOccurrence.children] and [HierarchyOccurrence.signals] + /// directly. + void _searchSignalsRecursive( + HierarchyOccurrence node, + List pathSoFar, + List queryParts, + int qIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + // Try matching current node name against current query part + final nodeName = node.name; + final currentQuery = qIdx < queryParts.length ? queryParts[qIdx] : null; + final matched = currentQuery != null && nodeName.startsWith(currentQuery); + final nextIdx = matched ? qIdx + 1 : qIdx; + + // Determine how many query parts remain after any node-name match. + final remaining = queryParts.length - nextIdx; + + // If 0 or 1 query parts remain, search signals at this node. + if (remaining <= 1) { + // When the current node consumed the last segment (remaining==0, + // matched==true), reuse that segment as the signal filter so that + // e.g. "a" doesn't return every signal under a module named "alu". + // When remaining==0 because we're recursing into a subtree where + // a parent already consumed all segments, use empty (return all). + final signalQuery = remaining == 1 + ? queryParts[nextIdx] + : (matched && qIdx < queryParts.length ? queryParts[qIdx] : ''); + for (final signal in node.signals) { + if (results.length >= limit) { + return; + } + if (signalQuery.isEmpty || signal.name.startsWith(signalQuery)) { + final fullPath = + [...pathSoFar, signal.name].join(hierarchyPathSeparator); + results.add(fullPath); + } + } + } + + // Recurse into children + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _searchSignalsRecursive( + child, + [...pathSoFar, child.name], + queryParts, + nextIdx, + results, + limit, + ); + } + } + + /// Recursively search for occurrences matching query parts. + /// + /// Similar to [_searchSignalsRecursive] but matches occurrences instead + /// of signals. Walks the tree maintaining the path of names. When the + /// query segments match occurrence names, adds them to results. + void _searchOccurrencePathsRecursive( + HierarchyOccurrence node, + List pathSoFar, + List queryParts, + int qIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + // Try matching current node name against current query part + final nodeName = node.name; + final currentQuery = qIdx < queryParts.length ? queryParts[qIdx] : null; + final matched = currentQuery != null && nodeName.contains(currentQuery); + final nextIdx = matched ? qIdx + 1 : qIdx; + + // If all query parts are matched, this node is a result + if (nextIdx >= queryParts.length) { + final fullPath = pathSoFar.join(hierarchyPathSeparator); + results.add(fullPath); + if (results.length >= limit) { + return; + } + } + + // Recurse into children + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _searchOccurrencePathsRecursive( + child, + [...pathSoFar, child.name], + queryParts, + nextIdx, + results, + limit, + ); + } + } + + /// Recursively search for occurrences matching query parts, returning + /// the occurrences. + void _matchOccurrencesRecursive( + HierarchyOccurrence node, + List queryParts, + int qIdx, + List results, + int limit) { + if (results.length >= limit) { + return; + } + + final matched = + qIdx < queryParts.length && node.name.contains(queryParts[qIdx]); + final nextIdx = matched ? qIdx + 1 : qIdx; + + if (nextIdx >= queryParts.length) { + results.add(node); + if (results.length >= limit) { + return; + } + } + + for (final child in node.children) { + _matchOccurrencesRecursive(child, queryParts, nextIdx, results, limit); + if (results.length >= limit) { + return; + } + } + } + + // ─────────────── Regex search helpers ─────────────── + + /// A compiled regex segment. `isGlobStar` indicates a `**` segment that + /// matches zero or more hierarchy levels. + static const _globStarSentinel = '**'; + + /// Split `pattern` into segments on `/` only. + /// + /// Unlike [_splitPath] (which also splits on `.`), regex patterns use only + /// `/` as the hierarchy separator because `.` has meaning inside regular + /// expressions (e.g. `.*`, `a.b`). + List _splitRegexPattern(String input) => input + .split(hierarchyPathSeparator) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + /// Convert glob-style `*` and `?` wildcards to regex equivalents. + /// + /// A standalone `*` (not preceded/followed by another regex metachar) + /// becomes `.*` (match anything). `?` becomes `.` (match one char). + /// This lets users write natural patterns like `*m`, `clk*`, `*data*` + /// without needing to know regex syntax. + String _globToRegex(String segment) { + final buf = StringBuffer(); + for (var i = 0; i < segment.length; i++) { + final c = segment[i]; + if (c == '*') { + // If already preceded by `.` (i.e. user wrote `.*`), skip conversion. + if (buf.toString().endsWith('.')) { + buf.write('*'); + } else { + buf.write('.*'); + } + } else if (c == '?') { + // If already preceded by a valid quantifier target, keep literal `?`. + // Otherwise treat as single-char wildcard `.`. + if (i > 0 && !'.?*+'.contains(segment[i - 1])) { + buf.write('?'); + } else { + buf.write('.'); + } + } else { + buf.write(c); + } + } + return buf.toString(); + } + + /// Compile string segments into [_RegexSegment] list. + /// + /// Each segment is first run through [_globToRegex] so that glob-style + /// wildcards (`*`, `?`) work alongside full regex syntax. + List<_RegexSegment> _compileSegments(List segments) => + segments.map((s) { + if (s == _globStarSentinel) { + return _RegexSegment.globStar(); + } + final pattern = _globToRegex(s); + // Anchor the regex to match the full name. + return _RegexSegment(RegExp('^$pattern\$')); + }).toList(); + + /// Recursive signal search driven by compiled regex segments. + /// + /// [segIdx] is the index into [segments] that we are currently trying to + /// match at this tree depth. + void _searchSignalsRegex( + HierarchyOccurrence node, + List pathSoFar, + List<_RegexSegment> segments, + int segIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + // Determine how many segments remain after consuming the current node. + final consumed = _matchNode(node.name, segments, segIdx); + + for (final nextIdx in consumed) { + if (results.length >= limit) { + return; + } + + // Try to match signals at this node. + // Find all indices reachable from nextIdx by skipping glob-stars + // where a signal-level regex (or end-of-pattern) can be applied. + for (final sigIdx in _signalReachableIndices(segments, nextIdx)) { + if (results.length >= limit) { + return; + } + if (sigIdx >= segments.length) { + // All segments consumed: collect all signals at this node. + for (final signal in node.signals) { + if (results.length >= limit) { + return; + } + final fullPath = + [...pathSoFar, signal.name].join(hierarchyPathSeparator); + results.add(fullPath); + } + } else { + // sigIdx points to a non-** regex that should match signal names. + final sigSeg = segments[sigIdx]; + // Only use as signal-level match if this is the last non-** segment + // (possibly followed by more **'s that can match zero levels). + if (_allGlobStarAfter(segments, sigIdx + 1)) { + for (final signal in node.signals) { + if (results.length >= limit) { + return; + } + if (sigSeg.regex!.hasMatch(signal.name)) { + results.add( + [...pathSoFar, signal.name].join(hierarchyPathSeparator)); + } + } + } + } + } + + // Recurse into children. + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _searchSignalsRegex( + child, + [...pathSoFar, child.name], + segments, + nextIdx, + results, + limit, + ); + } + } + } + + /// Recursive occurrence search driven by compiled regex segments. + void _matchOccurrencesRegex( + HierarchyOccurrence node, + List pathSoFar, + List<_RegexSegment> segments, + int segIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + final consumed = _matchNode(node.name, segments, segIdx); + + for (final nextIdx in consumed) { + if (results.length >= limit) { + return; + } + + // All segments consumed (or only trailing **'s remain) → match. + if (_allGlobStarAfter(segments, nextIdx)) { + results.add(pathSoFar.join(hierarchyPathSeparator)); + if (results.length >= limit) { + return; + } + } + + // Recurse into children. + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _matchOccurrencesRegex( + child, + [...pathSoFar, child.name], + segments, + nextIdx, + results, + limit, + ); + } + } + } + + /// Try to match [nodeName] against the segment at [segIdx]. + /// + /// Returns a set of possible next-segment indices (branching is needed + /// because `**` can consume zero or more levels). + Set _matchNode( + String nodeName, List<_RegexSegment> segments, int segIdx) { + final results = {}; + if (segIdx >= segments.length) { + // No more segments to match — nothing to advance to. + return results; + } + + final seg = segments[segIdx]; + + if (seg.isGlobStar) { + // ** matches zero levels (skip the **) … + results + ..addAll(_matchNode(nodeName, segments, segIdx + 1)) + // … or consumes this node and stays at ** (one-or-more levels). + ..add(segIdx); + } else if (seg.regex!.hasMatch(nodeName)) { + results.add(segIdx + 1); + } + // If the segment doesn't match at all, return empty → prune this branch. + return results; + } + + /// Returns indices in [segments] reachable from [fromIdx] by skipping + /// consecutive `**` glob-star segments. Always includes [fromIdx] itself + /// if it is in range (or == segments.length, meaning "past the end"). + Set _signalReachableIndices(List<_RegexSegment> segments, int fromIdx) { + final result = {}; + var i = fromIdx; + // Walk forward: each time we see a **, we can skip it (zero levels). + while (i < segments.length) { + if (segments[i].isGlobStar) { + // ** can match zero levels → skip and also record i (stay at **). + result.add(i + 1); // skip the ** + i++; + } else { + result.add(i); + break; // stop at first non-** segment + } + } + // If we walked past the end, record that too. + if (i >= segments.length) { + result.add(segments.length); + } + return result; + } + + /// Returns true if all segments from [fromIdx] onward are glob-stars + /// (or if [fromIdx] >= length, i.e. no more segments). + bool _allGlobStarAfter(List<_RegexSegment> segments, int fromIdx) => + segments.skip(fromIdx).every((s) => s.isGlobStar); +} + +/// Internal representation of a compiled regex segment. +class _RegexSegment { + final RegExp? regex; + final bool isGlobStar; + + _RegexSegment(this.regex) : isGlobStar = false; + _RegexSegment.globStar() + : regex = null, + isGlobStar = true; +} diff --git a/packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart b/packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart new file mode 100644 index 000000000..6ec675293 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart @@ -0,0 +1,224 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_hierarchy_adapter.dart +// Hierarchy adapter for netlist JSON format (derived from Yosys JSON) +// using rohd_hierarchy. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd_hierarchy/src/base_hierarchy_adapter.dart'; +import 'package:rohd_hierarchy/src/hierarchy_models.dart'; + +/// Adapter that exposes a netlist as a source-agnostic hierarchy. +/// +/// Extends [BaseHierarchyAdapter] from rohd_hierarchy package, using the shared +/// implementation for search, autocomplete, and lookup methods. +/// Only the netlist format-specific +/// JSON parsing logic is implemented here. +/// +/// Features: +/// - Parses ports, netnames, and cells from netlist JSON +/// - Filters auto-generated netnames (`hide_name`, `$`-prefixed, port dupes) +/// - Extracts `port_directions` on primitive cells for signal visibility +/// - Supports optional root-name override for VCD name alignment +class NetlistHierarchyAdapter extends BaseHierarchyAdapter { + NetlistHierarchyAdapter._(); + + /// Convenience factory to parse a netlist JSON string directly. + /// + /// [rootNameOverride] replaces the top-module name derived from the JSON. + /// Use this when VCD scopes use instance names that differ from the + /// definition names in the netlist output (often capitalized). + factory NetlistHierarchyAdapter.fromJson( + String netlistJson, { + String? rootNameOverride, + }) { + final obj = jsonDecode(netlistJson); + if (obj is! Map) { + throw const FormatException('Invalid netlist JSON root'); + } + return NetlistHierarchyAdapter.fromMap( + obj, + rootNameOverride: rootNameOverride, + ); + } + + /// Factory to parse a pre-decoded netlist JSON map. + /// + /// [netlistJson] must contain a top-level `modules` key. + /// [rootNameOverride] optionally replaces the detected top-module name. + factory NetlistHierarchyAdapter.fromMap( + Map netlistJson, { + String? rootNameOverride, + }) { + final adapter = NetlistHierarchyAdapter._() + .._buildFromNetlist(netlistJson, rootNameOverride: rootNameOverride); + return adapter; + } + + void _buildFromNetlist( + Map netlistJson, { + String? rootNameOverride, + }) { + final modules = netlistJson['modules'] as Map?; + if (modules == null || modules.isEmpty) { + throw const FormatException('Netlist JSON contained no modules'); + } + + // Find top module or default to first + final topName = modules.entries + .where( + (e) => + ((e.value as Map)['attributes'] + as Map?)?['top'] == + 1, + ) + .map((e) => e.key) + .firstOrNull ?? + modules.keys.first; + + final resolvedRootName = rootNameOverride ?? topName; + + final rootNode = _parseModule( + name: resolvedRootName, + definition: topName, + moduleData: modules[topName] as Map, + allModules: modules, + ); + root = rootNode; + rootNode.buildAddresses(); + } + + /// Parse a module definition and return the created + /// [HierarchyOccurrence]. + HierarchyOccurrence _parseModule({ + required String name, + required String definition, + required Map moduleData, + required Map allModules, + }) { + // Ports (signals with direction) + final portsData = moduleData['ports'] as Map?; + final signalsList = [ + if (portsData != null) + ...portsData.entries.indexed.map((entry) { + final (idx, kv) = entry; + final p = kv.value as Map; + final dir = p['direction']?.toString() ?? 'inout'; + final bits = (p['bits'] as List?)?.length ?? 0; + final logicType = p['logic_type'] as Map?; + return SignalOccurrence( + name: kv.key, + direction: dir, + width: bits > 0 ? bits : 1, + portIndex: idx, + logicType: logicType, + ); + }), + ]; + + // Netnames (internal signals without direction). + // Netlist `netnames` contains ALL named signals including port-connected + // ones. We skip names already covered by `ports` above, as well as + // auto-generated names (hide_name=1 or $-prefixed). + final netsData = moduleData['netnames'] as Map?; + if (netsData != null) { + final portNames = portsData?.keys.toSet() ?? {}; + signalsList.addAll( + netsData.entries + .where( + (entry) => + !portNames.contains(entry.key) && + !entry.key.startsWith(r'$') && + () { + final h = (entry.value as Map)['hide_name']; + return h != 1 && h != '1'; + }(), + ) + .map((entry) { + final netData = entry.value as Map; + final bits = (netData['bits'] as List?)?.length ?? 0; + final attrs = netData['attributes'] as Map?; + final isComputed = + attrs?['computed'] == 1 || attrs?['computed'] == true; + final logicType = netData['logic_type'] as Map?; + return SignalOccurrence( + name: entry.key, + width: bits > 0 ? bits : 1, + isComputed: isComputed, + logicType: logicType, + ); + }), + ); + } + + // Cells -> submodules or instances + final childNodes = []; + final cells = moduleData['cells'] as Map?; + if (cells != null) { + for (final entry in cells.entries) { + final cellName = entry.key; + final cellData = entry.value as Map; + final cellType = cellData['type']?.toString() ?? ''; + + if (allModules.containsKey(cellType) && + !HierarchyOccurrence.isPrimitiveType(cellType)) { + final childNode = _parseModule( + name: cellName, + definition: cellType, + moduleData: allModules[cellType] as Map, + allModules: allModules, + ); + childNodes.add(childNode); + } else { + // Primitive cell — create leaf occurrence. + // Extract port signals from `port_directions` when available so + // that primitive I/O appears in signal search results. + final isCellComputed = cellType.startsWith(r'$'); + final portDirections = + cellData['port_directions'] as Map?; + final connections = cellData['connections'] as Map?; + final portWidths = cellData['port_widths'] as Map?; + final cellSignals = [ + if (portDirections != null) + ...portDirections.entries.indexed.map((pEntry) { + final (pIdx, kv) = pEntry; + final pName = kv.key; + final pDir = kv.value.toString(); + final bits = (connections?[pName] as List?)?.length ?? + (portWidths?[pName] as int?) ?? + 1; + return SignalOccurrence( + name: pName, + direction: pDir, + width: bits, + isComputed: isCellComputed, + portIndex: pIdx, + ); + }), + ]; + + final instNode = HierarchyOccurrence( + name: cellName, + definition: cellType, + isPrimitive: true, + signals: cellSignals, + ); + childNodes.add(instNode); + } + } + } + + // Create the occurrence with children and signals embedded + return HierarchyOccurrence( + name: name, + definition: definition, + signals: signalsList, + children: childNodes, + ); + } +} diff --git a/packages/rohd_hierarchy/lib/src/occurrence_address.dart b/packages/rohd_hierarchy/lib/src/occurrence_address.dart new file mode 100644 index 000000000..06f059c88 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_address.dart @@ -0,0 +1,144 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_address.dart +// Efficient hierarchical address using indices instead of strings. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; + +/// Efficient hierarchical address using indices instead of strings. +/// +/// Format: [index0, index1, ...] or [] for root. +/// Example: [0, 2, 4] means root's 0th child, then 2nd child of that, then +/// the 4th child (occurrence) or 4th signal, depending on context. +/// +/// Advantages: +/// - O(1) address creation (just append index) +/// - O(depth) tree navigation (direct array indexing) +/// - Deterministic serialization (no parsing needed) +/// - Natural alignment with waveform dictionary (integer indices) +/// - Supports hierarchical queries (ancestor matching, batching by prefix) +/// +/// This replaces string-based path lookups with typed, semantic addressing. +@immutable +class OccurrenceAddress { + /// Path through tree as indices stored as immutable list. + /// Empty list represents the root occurrence. + /// Non-empty list: indices navigate through the hierarchy. The last index + /// refers to either a child occurrence or a signal, depending on context. + final List path; + + /// Create a hierarchy address from a path list. + const OccurrenceAddress(this.path); + + /// Root address (empty path). + static const OccurrenceAddress root = OccurrenceAddress([]); + + /// Create a child address by appending an occurrence index. + /// Use this when navigating to a child occurrence. + OccurrenceAddress child(int childIndex) => + OccurrenceAddress([...path, childIndex]); + + /// Create a signal address by appending signal index. + /// Use this when addressing a signal within current occurrence. + OccurrenceAddress signal(int signalIndex) => + OccurrenceAddress([...path, signalIndex]); + + /// Serialize to a dot-separated string suitable for use as a JSON key. + /// + /// Examples: `""` (root), `"0"`, `"0.2.4"`. + /// Round-trips with [OccurrenceAddress.fromDotString]. + String toDotString() => path.join('.'); + + /// Deserialize from a dot-separated string produced by [toDotString]. + /// + /// An empty string returns [root]. + factory OccurrenceAddress.fromDotString(String s) { + if (s.isEmpty) { + return root; + } + return OccurrenceAddress(s.split('.').map(int.parse).toList()); + } + + @override + String toString() { + if (path.isEmpty) { + return '[ROOT]'; + } + return '[${path.join(".")}]'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OccurrenceAddress && + const ListEquality().equals(path, other.path); + + @override + int get hashCode => Object.hashAll(path); + + /// Resolve a pathname string (e.g. `"Top/counter/clk"` or + /// `"Top.counter.clk"`) to a [OccurrenceAddress] by walking [root]. + /// + /// Supports both `/` hierarchy paths and dot-separated signal identifiers + /// commonly produced by VCD/FST waveform files. If the first segment matches + /// [root]'s name, it is skipped — the root occurrence is always at the empty + /// address. + /// + /// The last segment is first tried as a **signal** name within the + /// current occurrence; if that fails it is tried as a **child** + /// occurrence name. + /// This mirrors the pathname convention where a signal path has one more + /// segment than its parent module path. + /// + /// Returns `null` if any segment cannot be resolved. + /// + /// ```dart + /// final addr = OccurrenceAddress.tryFromPathname('Top/cpu/clk', root); + /// if (addr != null) { + /// final signal = service.signalByAddress(addr); + /// } + /// ``` + static OccurrenceAddress? tryFromPathname( + String pathname, + HierarchyOccurrence root, + ) { + final rootAddr = root.address ?? OccurrenceAddress.root; + final parts = pathname + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) + .where((s) => s.isNotEmpty) + .toList(); + + // Skip leading segment that matches the root name. + final segments = + parts.isNotEmpty && parts.first == root.name ? parts.skip(1) : parts; + + ({HierarchyOccurrence node, OccurrenceAddress addr})? step( + ({HierarchyOccurrence node, OccurrenceAddress addr})? cur, + String segment, + ) { + if (cur == null) { + return null; + } + final si = cur.node.signalIndexByName(segment); + if (identical(segment, segments.last) && si >= 0) { + return (node: cur.node, addr: cur.addr.signal(si)); + } + final ci = cur.node.childIndexByName(segment); + return ci >= 0 + ? (node: cur.node.children[ci], addr: cur.addr.child(ci)) + : null; + } + + return segments.fold<({HierarchyOccurrence node, OccurrenceAddress addr})?>( + (node: root, addr: rootAddr), step)?.addr; + } +} diff --git a/packages/rohd_hierarchy/lib/src/occurrence_search_result.dart b/packages/rohd_hierarchy/lib/src/occurrence_search_result.dart new file mode 100644 index 000000000..fafe8623c --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_search_result.dart @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_search_result.dart +// Result of a module/node search with enriched metadata. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; +import 'package:rohd_hierarchy/src/hierarchy_search_result.dart'; + +/// Result of an occurrence search with enriched metadata. +/// +/// Contains the occurrence's full path, parsed path segments, and the full +/// [HierarchyOccurrence] object. This mirrors `SignalSearchResult` for +/// occurrences and provides a consistent search results interface. +@immutable +class OccurrenceSearchResult extends HierarchySearchResult { + /// Alias for [id] — the occurrence's full hierarchical path. + String get occurrenceId => id; + + /// The underlying [HierarchyOccurrence] from the hierarchy service. + /// Contains the occurrence's name, type, children, and signals. + final HierarchyOccurrence occurrence; + + /// Creates an occurrence search result. + const OccurrenceSearchResult({ + required String occurrenceId, + required super.path, + required this.occurrence, + }) : super(id: occurrenceId); + + /// Whether this occurrence has sub-hierarchy (i.e. is not a primitive + /// leaf). + bool get isModule => !occurrence.isPrimitive; + + /// Number of direct child occurrences. + int get childCount => occurrence.children.length; + + @override + String toString() => 'OccurrenceSearchResult($id)'; +} diff --git a/packages/rohd_hierarchy/lib/src/prefix_query.dart b/packages/rohd_hierarchy/lib/src/prefix_query.dart new file mode 100644 index 000000000..4f12b7ad7 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/prefix_query.dart @@ -0,0 +1,61 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// prefix_query.dart +// Prefix-substring query implementation for hierarchy search. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; +import 'package:rohd_hierarchy/src/hierarchy_query.dart'; + +/// Prefix-substring query: segments are matched via `startsWith` (signals) +/// or `contains` (occurrences) at successive hierarchy depths. +class PrefixQuery extends HierarchyQuery { + /// Non-empty segments parsed from the raw query. + late final List segments; + + /// Create a prefix query from [rawQuery]. + PrefixQuery( + super.rawQuery, { + super.target = SearchTarget.signals, + }) : super(crossesBoundaries: false) { + segments = rawQuery + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + } + + @override + int get segmentCount => segments.length; + + @override + Set matchOccurrence(String occurrenceName, int stateIndex) { + if (stateIndex >= segments.length) { + return {stateIndex}; + } + final name = occurrenceName; + if (name.contains(segments[stateIndex])) { + return {stateIndex + 1}; + } + return const {}; + } + + @override + bool matchSignal(String signalName, int stateIndex) { + if (stateIndex >= segments.length) { + return true; + } + // Only the last segment can match a signal name. + if (stateIndex != segments.length - 1) { + return false; + } + return signalName.startsWith(segments[stateIndex]); + } + + @override + bool isComplete(int stateIndex) => stateIndex >= segments.length; +} diff --git a/packages/rohd_hierarchy/lib/src/regex_query.dart b/packages/rohd_hierarchy/lib/src/regex_query.dart new file mode 100644 index 000000000..4b367b7e6 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/regex_query.dart @@ -0,0 +1,177 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// regex_query.dart +// Regex/glob query implementation for hierarchy search. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; +import 'package:rohd_hierarchy/src/hierarchy_query.dart'; + +/// Regex/glob query: each segment is a compiled regex, with `**` support +/// for crossing hierarchy boundaries. +/// +/// ## Segment syntax +/// +/// The query string is split on `/` into segments. Each segment is +/// independently compiled as a case-sensitive [RegExp] anchored to the +/// full occurrence or signal name (`^…$`). This means: +/// +/// - **Plain names** match exactly: `Top/CPU/clk`. +/// - **Glob wildcards** are auto-converted before compilation: +/// - `*` → `.*` (match any characters) +/// - `?` → `.` (match one character) +/// - These compose naturally: `clk*` matches `clk`, `clk_gated`, +/// `clk_div2`, etc. +/// - **Full regex** is supported within each segment since the string +/// is passed to [RegExp]: +/// - `d[0-9]+` — signals named `d0`, `d1`, `d12`, … +/// - `(clk|reset)` — either `clk` or `reset` +/// - `data_[a-z]{2}` — `data_ab`, `data_xy`, … +/// - `.*mux.*` — any name containing `mux` +/// - `ch[0-3]` — `ch0`, `ch1`, `ch2`, `ch3` +/// - `r[0-9]{1,2}` — `r0` through `r99` +/// - **`**`** (double-star, as its own segment) matches zero or more +/// hierarchy levels, allowing searches to cross boundaries: +/// - `Top/**/clk` — `clk` at any depth below `Top` +/// - `**/d[0-9]+` — any signal like `d0` anywhere +/// - `Top/**/ch*/data_*` — `data_*` signals inside `ch*` modules +/// +/// ## Interaction between glob and regex +/// +/// Glob conversion happens *before* regex compilation, so `*` and `?` +/// are always expanded. If you need a literal `*` or `?` in the regex, +/// escape them: `\*`, `\?`. All other regex metacharacters (`.`, `+`, +/// `|`, `(`, `)`, `[`, `]`, `{`, `}`, `^`, `$`) work as-is inside +/// each segment. +/// +/// ## Examples +/// +/// ```text +/// Query Matches +/// ───────────────────────────────────────────────────────────── +/// Top/CPU/clk exact: Top → CPU → clk +/// Top/CPU/* all signals in Top/CPU +/// Top/*/clk clk one level below Top +/// Top/**/clk clk at any depth below Top +/// Top/**/c.* signals starting with 'c' anywhere +/// **/clk clk anywhere in hierarchy +/// **/(clk|reset) clk or reset anywhere +/// Top/CPU/d[0-9]+ d0, d1, d12, … in Top/CPU +/// Top/**/ch[0-3]/data_* data_* in ch0–ch3 at any depth +/// Top/mem_*/addr[0-9]* addr0, addr1, … in mem_* modules +/// **/.*mux.* any name containing 'mux' anywhere +/// ``` +class RegexQuery extends HierarchyQuery { + /// Compiled segments — either a regex or a glob-star sentinel. + late final List segments; + + /// Create a regex query from [rawQuery]. + /// + /// A standalone `*` is converted to `.*`, `?` to `.`. The segment + /// `**` matches zero or more hierarchy levels. + RegexQuery( + super.rawQuery, { + super.target = SearchTarget.signals, + }) : super(crossesBoundaries: false) { + final parts = rawQuery + .split(hierarchyPathSeparator) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + segments = parts.map((s) { + if (s == '**') { + return RegexSegment.globStar(); + } + final pattern = _globToRegex(s); + return RegexSegment(RegExp('^$pattern\$')); + }).toList(); + } + + @override + int get segmentCount => segments.length; + + @override + Set matchOccurrence(String occurrenceName, int stateIndex) { + if (stateIndex >= segments.length) { + return const {}; + } + final seg = segments[stateIndex]; + final results = {}; + if (seg.isGlobStar) { + // ** matches zero levels (skip) … + results + ..addAll(matchOccurrence(occurrenceName, stateIndex + 1)) + // … or consumes this node and stays at ** (one-or-more levels). + ..add(stateIndex); + } else if (seg.regex!.hasMatch(occurrenceName)) { + results.add(stateIndex + 1); + } + return results; + } + + @override + bool matchSignal(String signalName, int stateIndex) { + // Walk past any trailing **'s to find the signal-matching segment. + var i = stateIndex; + while (i < segments.length && segments[i].isGlobStar) { + i++; + } + if (i >= segments.length) { + return true; // all consumed + } + // The segment at i must be the last real regex. + if (!_allGlobStarAfter(i + 1)) { + return false; + } + return segments[i].regex!.hasMatch(signalName); + } + + @override + bool isComplete(int stateIndex) => + stateIndex >= segments.length || + segments.skip(stateIndex).every((s) => s.isGlobStar); + + /// Check if all segments from [fromIdx] onward are glob-stars. + bool _allGlobStarAfter(int fromIdx) => + segments.skip(fromIdx).every((s) => s.isGlobStar); + + /// Convert glob wildcards to regex equivalents. + static String _globToRegex(String segment) { + final buf = StringBuffer(); + for (var i = 0; i < segment.length; i++) { + final c = segment[i]; + if (c == '*') { + if (buf.toString().endsWith('.')) { + buf.write('*'); + } else { + buf.write('.*'); + } + } else if (c == '?') { + buf.write('.'); + } else { + buf.write(c); + } + } + return buf.toString(); + } +} + +/// A compiled regex segment for [RegexQuery]. +class RegexSegment { + /// The compiled regex, or null for glob-star segments. + final RegExp? regex; + + /// Whether this segment is a `**` glob-star. + final bool isGlobStar; + + /// Create a regex segment. + RegexSegment(this.regex) : isGlobStar = false; + + /// Create a glob-star segment (`**`). + RegexSegment.globStar() + : regex = null, + isGlobStar = true; +} diff --git a/packages/rohd_hierarchy/lib/src/signal_occurrence.dart b/packages/rohd_hierarchy/lib/src/signal_occurrence.dart new file mode 100644 index 000000000..3fb57772f --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/signal_occurrence.dart @@ -0,0 +1,274 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_occurrence.dart +// A signal in the hardware occurrence hierarchy. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; +import 'package:rohd_hierarchy/src/occurrence_address.dart'; + +/// Signals are the fundamental data carriers in hardware. A signal can be: +/// - An internal signal within an occurrence +/// - A port on an occurrence interface (has direction: input/output/inout) +/// +/// This is a structural model without waveform data. Path strings are +/// computed on demand from the parent occurrence reference — call [path] +/// with your desired separator. +class SignalOccurrence { + /// The name of the signal (bare name within its scope). + /// + /// Used for display, search, and local lookups within an occurrence. + /// Not guaranteed unique across the full hierarchy — use [path] for + /// unique keying. + final String name; + + /// The bit width of the signal. + final int width; + + /// Direction of the signal if it's a port. + /// Null for internal signals. + /// "input", "output", or "inout" for ports. + final String? direction; + + /// Current runtime value of the signal (if available). + /// Typically a hex or binary string representation. + final String? value; + + /// Whether this signal's value is computed/derivable (e.g. constant, + /// gate output, InlineSystemVerilog result) rather than directly tracked + /// by the waveform service. + final bool isComputed; + + /// Stable ordering index among ports in the parent occurrence. + /// + /// Set by the adapter that creates the signal. For ports (signals with + /// a [direction]), this records the deterministic position from the + /// original source (netlist JSON iteration order, ROHD module port + /// declaration order, etc.). Internal signals have `null`. + /// + /// [HierarchyOccurrence.buildAddresses] places ports before internal + /// signals when assigning [OccurrenceAddress] indices, so a port with + /// `portIndex == k` will receive signal address index `k`. + /// + /// Consumers that store connectivity by `(nodeId, portIndex)` tuples + /// (e.g. schematic hyperedges) rely on this value remaining stable + /// across incremental hierarchy expansion. + final int? portIndex; + + /// Type metadata from the netlist `logic_type` JSON field. + /// + /// For a **LogicStructure** (non-array), the format is: + /// ```json + /// {"typeName": "FloatingPoint", "fields": [ + /// {"name": "mantissa", "width": 4, "bits": [0,1,2,3]}, + /// {"name": "exponent", "width": 4, "bits": [4,5,6,7]}, + /// {"name": "sign", "width": 1, "bits": [8]} + /// ]} + /// ``` + /// + /// For a **LogicArray**, the format is: + /// ```json + /// {"width": 80, "arrayDims": [10], "elementWidth": 8} + /// ``` + /// + /// For a plain signal: `{"width": N}` or `null`. + /// + /// Nested structs have a recursive `"type"` key in their field entries. + Map? logicType; + + /// Hierarchical address for this signal. Assigned by + /// [HierarchyOccurrence.buildAddresses] to enable efficient navigation. + /// Format: [...occurrenceIndices, signalIndex] + OccurrenceAddress? get address => _address; + OccurrenceAddress? _address; + + /// Sets the address. Only for use by [HierarchyOccurrence.buildAddresses]. + @internal + set address(OccurrenceAddress? value) => _address = value; + + /// Parent occurrence containing this signal. Set by + /// [HierarchyOccurrence.buildAddresses]. + HierarchyOccurrence? get parent => _parent; + HierarchyOccurrence? _parent; + + /// Sets the parent. Only for use by [HierarchyOccurrence.buildAddresses]. + @internal + set parent(HierarchyOccurrence? value) => _parent = value; + + /// Creates a [SignalOccurrence] with the given properties. + SignalOccurrence({ + required this.name, + required this.width, + this.direction, + this.value, + this.isComputed = false, + this.portIndex, + this.logicType, + }); + + /// Whether this signal is a LogicStructure (has named sub-fields). + bool get isStruct => logicType != null && logicType!.containsKey('fields'); + + /// Whether this signal is a LogicArray (has indexed elements). + bool get isArray => logicType != null && logicType!.containsKey('arrayDims'); + + /// The struct type name (e.g. "FloatingPoint"), or null if not a struct. + String? get typeName => logicType?['typeName'] as String?; + + /// The struct field descriptors, or empty list if not a struct. + /// + /// Each field is `{"name": ..., "width": ..., "bits": [...]}` with an + /// optional `"type"` key for nested structs/arrays. + List> get structFields => + (logicType?['fields'] as List?)?.cast>() ?? + const []; + + /// Array dimensions (e.g. `[10]` for 1D, `[10, 2]` for 2D), or null. + List? get arrayDims => + (logicType?['arrayDims'] as List?)?.cast(); + + /// Element width for arrays, or null if not an array. + int? get arrayElementWidth => logicType?['elementWidth'] as int?; + + /// Returns the expected sub-field signal names derived from [logicType]. + /// + /// For structs, the synthesizer creates separate netnames for each field + /// following the Namer/Sanitizer conventions: + /// `Sanitizer.sanitizeSV(structureName)` → `{parentName}_{fieldName}` + /// + /// For example, signal `fp` with fields `mantissa`, `exponent`, `sign` + /// produces sub-field signal names: `fp_mantissa`, `fp_exponent`, `fp_sign`. + /// + /// These become separate [SignalOccurrence] entries in the same parent + /// module. Use `HierarchyOccurrence.findSubFieldSignals` to look + /// them up. + /// + /// Returns a list of `(expectedName, fieldLabel, width, startBit, + /// subLogicType)` for direct children. `expectedName` follows the + /// `{parentSignalName}_{fieldName}` convention. + /// `subLogicType` is non-null when the child is itself a sub-array + /// (remaining dimensions) and can be further expanded. + /// Empty if this is not a struct/array with known sub-fields. + List< + ({ + String expectedName, + String fieldLabel, + int width, + int startBit, + Map? subLogicType, + })> get subFieldDescriptors { + if (logicType == null) { + return const []; + } + return subFieldDescriptorsForType(logicType!, name); + } + + /// Compute sub-field descriptors for an arbitrary [logicType] map. + /// + /// [parentName] is used to derive expected signal names. + /// This is static so it can be called recursively for nested arrays + /// without needing a full [SignalOccurrence]. + static List< + ({ + String expectedName, + String fieldLabel, + int width, + int startBit, + Map? subLogicType, + })> subFieldDescriptorsForType( + Map logicType, + String parentName, + ) { + final fields = logicType['fields'] as List?; + if (fields != null) { + return fields.map((f) { + final field = f as Map; + final fieldName = field['name'] as String? ?? '?'; + final width = field['width'] as int? ?? 1; + final bits = field['bits'] as List?; + final startBit = bits != null && bits.isNotEmpty + ? (bits.cast().reduce((a, b) => a < b ? a : b)) + : 0; + // Naming convention: Sanitizer.sanitizeSV("$parentName.$fieldName") + // which produces "$parentName_$fieldName" + final expectedName = '${parentName}_$fieldName'; + return ( + expectedName: expectedName, + fieldLabel: fieldName, + width: width, + startBit: startBit, + subLogicType: field['type'] as Map?, + ); + }).toList(); + } + + final arrayDims = logicType['arrayDims'] as List?; + if (arrayDims != null && arrayDims.isNotEmpty) { + final leafWidth = (logicType['elementWidth'] as int?) ?? 1; + final outerDim = arrayDims.first as int; + // For multi-dimensional arrays, each outer element spans all + // remaining dimensions times the leaf element width. + final remainingDims = + arrayDims.length > 1 ? arrayDims.sublist(1).cast() : []; + final elementWidth = remainingDims.isEmpty + ? leafWidth + : remainingDims.fold(leafWidth, (acc, d) => acc * d); + + // Build sub-logicType for remaining dimensions (if any). + final subLogicType = remainingDims.isEmpty + ? null + : { + 'width': elementWidth, + 'arrayDims': remainingDims, + 'elementWidth': leafWidth, + }; + + return List.generate(outerDim, (i) { + // Naming convention: Sanitizer.sanitizeSV("$parentName[$i]") + // which produces "$parentName_${i}_" + final expectedName = '${parentName}_${i}_'; + return ( + expectedName: expectedName, + fieldLabel: '[$i]', + width: elementWidth, + startBit: i * elementWidth, + subLogicType: subLogicType, + ); + }); + } + + return const []; + } + + /// Compute the full hierarchical path for this signal. + /// + /// Joins the parent occurrence's path with this signal's [name] using + /// [separator]. Falls back to just [name] if parent is not yet set + /// (e.g. in test fixtures before `buildAddresses`). + String path({String separator = hierarchyPathSeparator}) { + if (_parent == null) { + return name; + } + return '${_parent!.path(separator: separator)}$separator$name'; + } + + /// Returns true if this signal is a port (has a direction). + bool get isPort => direction != null; + + /// Returns true if this is an input port. + bool get isInput => direction == 'input'; + + /// Returns true if this is an output port. + bool get isOutput => direction == 'output'; + + /// Returns true if this is a bidirectional port. + bool get isInout => direction == 'inout'; + + @override + String toString() => '$name (width=$width${isPort ? ', $direction' : ''})'; +} diff --git a/packages/rohd_hierarchy/lib/src/signal_search_result.dart b/packages/rohd_hierarchy/lib/src/signal_search_result.dart new file mode 100644 index 000000000..970855f74 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/signal_search_result.dart @@ -0,0 +1,49 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_search_result.dart +// Result of a signal search with enriched metadata. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +import 'package:rohd_hierarchy/src/hierarchy_search_result.dart'; +import 'package:rohd_hierarchy/src/signal_occurrence.dart'; + +/// Result of a signal search with enriched metadata. +/// +/// Contains the signal's full path, parsed path segments, and the full +/// [SignalOccurrence] object if available. This is the hierarchy-only portion +/// of search results; UI layers can use the pre-computed display helpers +/// directly without re-parsing paths. +@immutable +class SignalSearchResult extends HierarchySearchResult { + /// Alias for [id] — the signal's full hierarchical path. + String get signalId => id; + + /// The underlying [SignalOccurrence] from the hierarchy service (if + /// available). Contains width, direction, and other signal metadata. + final SignalOccurrence? signal; + + /// Creates a signal search result. + const SignalSearchResult({ + required String signalId, + required super.path, + this.signal, + }) : super(id: signalId); + + /// Occurrence names that need to be expanded to reveal this signal. + /// + /// These are the intermediate path segments between the top occurrence + /// and the signal name — i.e. everything except the first (top + /// occurrence) and last (signal name) segments. + /// + /// For `Top/sub1/sub2/clk` this returns `["sub1", "sub2"]`. + List get intermediateOccurrenceNames => + path.length > 2 ? path.sublist(1, path.length - 1) : const []; + + @override + String toString() => 'SignalSearchResult($id, width=${signal?.width ?? "?"})'; +} diff --git a/packages/rohd_hierarchy/pubspec.yaml b/packages/rohd_hierarchy/pubspec.yaml new file mode 100644 index 000000000..c75cd3d34 --- /dev/null +++ b/packages/rohd_hierarchy/pubspec.yaml @@ -0,0 +1,18 @@ +name: rohd_hierarchy +description: "Generic hierarchy data models for hardware module navigation - HierarchyNode, Port, and HierarchyService." +homepage: https://intel.github.io/rohd-website/ +repository: https://github.com/intel/rohd +version: 0.1.0 +issue_tracker: https://github.com/intel/rohd/issues + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + collection: ^1.15.0 + meta: ^1.9.0 + +dev_dependencies: + test: ^1.17.3 diff --git a/packages/rohd_hierarchy/test/adapter_search_parity_test.dart b/packages/rohd_hierarchy/test/adapter_search_parity_test.dart new file mode 100644 index 000000000..cde3a301a --- /dev/null +++ b/packages/rohd_hierarchy/test/adapter_search_parity_test.dart @@ -0,0 +1,285 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// adapter_search_parity_test.dart +// Baseline tests verifying that search produces identical results +// regardless of which adapter populated the HierarchyService. +// +// 2026 April +// Author: Desmond Kirkpatrick + +// This is the key contract: once a HierarchyService is built, callers +// cannot tell whether the data came from VCD (BaseHierarchyAdapter.fromTree), +// netlist JSON (NetlistHierarchyAdapter), or any other source. + +import 'dart:convert'; + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Concrete subclass that does NOT set root, so we can test the +/// StateError thrown by uninitialized access. +class _UnsetAdapter extends BaseHierarchyAdapter {} + +/// Resolve a pathname to a [SignalOccurrence] via +/// [OccurrenceAddress.tryFromPathname]. +SignalOccurrence? _resolve(HierarchyService svc, String path) { + final addr = OccurrenceAddress.tryFromPathname(path, svc.root); + if (addr == null) { + return null; + } + return svc.signalByAddress(addr); +} + +// ────────────────────────────────────────────────────────────────────── +// Build the SAME design via two different adapter paths +// ────────────────────────────────────────────────────────────────────── + +/// VCD-style: HierarchyNode tree with children/signals populated inline. +/// This is what `wellen` produces when loading a VCD/FST file. +BaseHierarchyAdapter _buildVcdAdapter() => BaseHierarchyAdapter.fromTree( + HierarchyOccurrence( + name: 'Abcd', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'resetn', width: 1), + SignalOccurrence(name: 'arvalid_s', width: 1), + ], + children: [ + HierarchyOccurrence( + name: 'lab', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'fromUpstream_request__st', width: 64), + ], + children: [ + HierarchyOccurrence( + name: 'cam', + signals: [ + SignalOccurrence(name: 'hit', width: 1), + SignalOccurrence(name: 'entry', width: 32), + ], + ), + ], + ), + ], + ), + ); + +/// Netlist JSON-style: flat-map adapter (like what DevTools/schematic viewer +/// builds from ROHD inspector JSON or netlist JSON). +/// Children and signals live in the adapter's flat maps, NOT inside +/// the HierarchyNode objects. +NetlistHierarchyAdapter _buildJsonAdapter() => + NetlistHierarchyAdapter.fromJson(jsonEncode({ + 'modules': { + 'Abcd': { + 'attributes': {'top': 1}, + 'ports': { + 'clk': { + 'direction': 'input', + 'bits': [1] + }, + 'resetn': { + 'direction': 'input', + 'bits': [2] + }, + 'arvalid_s': { + 'direction': 'input', + 'bits': [3] + }, + }, + 'netnames': {}, + 'cells': { + 'lab': { + 'type': 'Lab', + 'connections': {}, + }, + }, + }, + 'Lab': { + 'ports': { + 'clk': { + 'direction': 'input', + 'bits': [10] + }, + 'reset': { + 'direction': 'input', + 'bits': [11] + }, + 'fromUpstream_request__st': { + 'direction': 'input', + 'bits': List.generate(64, (i) => 100 + i) + }, + }, + 'netnames': {}, + 'cells': { + 'cam': { + 'type': 'Cam', + 'connections': {}, + }, + }, + }, + 'Cam': { + 'ports': { + 'hit': { + 'direction': 'input', + 'bits': [200] + }, + 'entry': { + 'direction': 'input', + 'bits': List.generate(32, (i) => 300 + i) + }, + }, + 'netnames': {}, + 'cells': {}, + }, + }, + })); + +void main() { + late HierarchyService vcdService; + late HierarchyService jsonService; + + setUp(() { + vcdService = _buildVcdAdapter(); + jsonService = _buildJsonAdapter(); + }); + + // ── The two services must be interchangeable for all search ops ── + // Case-insensitivity, dot separators, controller state, and + // search semantics are covered in address_conversion_test, + // hierarchy_search_controller_test, and regex_search_test. + // This file focuses exclusively on *parity* between adapters. + + group('Adapter search parity — both sources produce same results', () { + test('root name matches', () { + expect(vcdService.root.name, 'Abcd'); + expect(jsonService.root.name, 'Abcd'); + }); + + test('root.children returns same module names', () { + final vcdChildren = vcdService.root.children.map((c) => c.name).toSet(); + final jsonChildren = jsonService.root.children.map((c) => c.name).toSet(); + expect(vcdChildren, jsonChildren); + }); + + test('root.signals returns same signal names at root', () { + final vcdSigs = vcdService.root.signals.map((s) => s.name).toSet(); + final jsonSigs = jsonService.root.signals.map((s) => s.name).toSet(); + expect(vcdSigs, jsonSigs); + }); + + test('nested node signals() returns same signal names', () { + final vcdLab = vcdService.root.children.first; + final jsonLab = jsonService.root.children.first; + final vcdSigs = vcdLab.signals.map((s) => s.name).toSet(); + final jsonSigs = jsonLab.signals.map((s) => s.name).toSet(); + expect(vcdSigs, jsonSigs); + }); + + test('signalByAddress works on both — top level', () { + final vcdClk = _resolve(vcdService, 'Abcd/clk'); + final jsonClk = _resolve(jsonService, 'Abcd/clk'); + expect(vcdClk, isNotNull, reason: 'VCD: Abcd/clk'); + expect(jsonClk, isNotNull, reason: 'JSON: Abcd/clk'); + expect(vcdClk!.name, 'clk'); + expect(jsonClk!.name, 'clk'); + }); + + test('signalByAddress works on both — nested', () { + final vcdHit = _resolve(vcdService, 'Abcd/lab/cam/hit'); + final jsonHit = _resolve(jsonService, 'Abcd/lab/cam/hit'); + expect(vcdHit, isNotNull, reason: 'VCD: Abcd/lab/cam/hit'); + expect(jsonHit, isNotNull, reason: 'JSON: Abcd/lab/cam/hit'); + expect(vcdHit!.name, 'hit'); + expect(jsonHit!.name, 'hit'); + }); + + test('searchSignals plain query — same result names', () { + final vcdResults = + vcdService.searchSignals('clk').map((r) => r.name).toSet(); + final jsonResults = + jsonService.searchSignals('clk').map((r) => r.name).toSet(); + expect(vcdResults, isNotEmpty); + expect(vcdResults, jsonResults); + }); + + test('searchSignals glob query — same result names', () { + final vcdResults = + vcdService.searchSignals('**/clk').map((r) => r.name).toSet(); + final jsonResults = + jsonService.searchSignals('**/clk').map((r) => r.name).toSet(); + expect(vcdResults, isNotEmpty); + expect(vcdResults, jsonResults); + }); + + test('searchSignals path query — same result names', () { + final vcdResults = + vcdService.searchSignals('lab/clk').map((r) => r.name).toSet(); + final jsonResults = + jsonService.searchSignals('lab/clk').map((r) => r.name).toSet(); + expect(vcdResults, isNotEmpty); + expect(vcdResults, jsonResults); + }); + + test('searchModules — same module names', () { + final vcdNodes = vcdService + .searchOccurrences('lab') + .map((r) => r.occurrence.name) + .toSet(); + final jsonNodes = jsonService + .searchOccurrences('lab') + .map((r) => r.occurrence.name) + .toSet(); + expect(vcdNodes, isNotEmpty); + expect(vcdNodes, jsonNodes); + }); + + test('searchModules nested — same module names', () { + final vcdNodes = vcdService + .searchOccurrences('cam') + .map((r) => r.occurrence.name) + .toSet(); + final jsonNodes = jsonService + .searchOccurrences('cam') + .map((r) => r.occurrence.name) + .toSet(); + expect(vcdNodes, isNotEmpty); + expect(vcdNodes, jsonNodes); + }); + }); + + // ── Verify the external-hierarchy handoff works ── + // Individual search/address semantics are covered elsewhere. + // This group tests the adapter re-wrapping contract. + + group('External hierarchy flow (simulates DevTools → wave viewer)', () { + test('BaseHierarchyAdapter.fromTree produces identical search results', () { + final rewrapped = BaseHierarchyAdapter.fromTree(jsonService.root); + + final results = rewrapped.searchSignals('clk'); + expect(results, isNotEmpty); + expect( + results.map((r) => r.name).toSet(), + jsonService.searchSignals('clk').map((r) => r.name).toSet(), + ); + }); + + test('BaseHierarchyAdapter.fromTree preserves signalByAddress', () { + final rewrapped = BaseHierarchyAdapter.fromTree(jsonService.root); + + final hit = _resolve(rewrapped, 'Abcd/lab/cam/hit'); + expect(hit, isNotNull); + expect(hit!.name, 'hit'); + }); + }); + + group('BaseHierarchyAdapter.root', () { + test('throws StateError when root is not set', () { + final adapter = _UnsetAdapter(); + expect(() => adapter.root, throwsStateError); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/address_conversion_test.dart b/packages/rohd_hierarchy/test/address_conversion_test.dart new file mode 100644 index 000000000..2caabdef6 --- /dev/null +++ b/packages/rohd_hierarchy/test/address_conversion_test.dart @@ -0,0 +1,286 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// address_conversion_test.dart +// Tests for HierarchyService address ↔ pathname conversion methods. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('Address ↔ pathname conversion', () { + late HierarchyService service; + late HierarchyOccurrence root; + + // Build a test hierarchy: + // Top + // ├─ cpu (child 0) + // │ ├─ signals: clk, rst + // │ └─ alu (child 0 of cpu) + // │ └─ signals: a, b, out + // └─ mem (child 1) + // └─ signals: addr, data + + setUpAll(() { + final alu = HierarchyOccurrence( + name: 'alu', + signals: [ + SignalOccurrence( + name: 'a', + width: 1, + ), + SignalOccurrence( + name: 'b', + width: 1, + ), + SignalOccurrence( + name: 'out', + width: 1, + ), + ], + ); + + final cpu = HierarchyOccurrence( + name: 'cpu', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + ), + SignalOccurrence( + name: 'rst', + width: 1, + ), + ], + children: [alu], + ); + + final mem = HierarchyOccurrence( + name: 'mem', + signals: [ + SignalOccurrence( + name: 'addr', + width: 1, + ), + SignalOccurrence( + name: 'data', + width: 1, + ), + ], + ); + + root = HierarchyOccurrence( + name: 'Top', + children: [cpu, mem], + )..buildAddresses(); + + service = BaseHierarchyAdapter.fromTree(root); + }); + + group('pathnameToAddress', () { + test('root name resolves to root address', () { + final addr = service.pathnameToAddress('Top'); + expect(addr, isNotNull); + expect(addr!.path, equals([])); + }); + + test('module path resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu'); + expect(addr, isNotNull); + expect(addr!.path, equals([0])); + }); + + test('nested module path resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/alu'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0])); + }); + + test('second child module resolves correctly', () { + final addr = service.pathnameToAddress('Top/mem'); + expect(addr, isNotNull); + expect(addr!.path, equals([1])); + }); + + test('signal path resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/clk'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0])); // cpu[0], signal clk[0] + }); + + test('second signal resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/rst'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 1])); // cpu[0], signal rst[1] + }); + + test('nested signal resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/alu/out'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0, 2])); // cpu[0], alu[0], out[2] + }); + + test('dot-separated paths work too', () { + final addr = service.pathnameToAddress('Top.cpu.alu.b'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0, 1])); // cpu[0], alu[0], b[1] + }); + + test('non-existent path returns null', () { + expect(service.pathnameToAddress('Top/nonexistent'), isNull); + }); + + test('non-existent signal returns null', () { + expect(service.pathnameToAddress('Top/cpu/nonexistent'), isNull); + }); + + test('empty string returns root', () { + final addr = service.pathnameToAddress(''); + expect(addr, isNotNull); + expect(addr!.path, isEmpty); + }); + }); + + group('addressToPathname', () { + test('root address returns root name', () { + expect( + service.addressToPathname(OccurrenceAddress.root), + equals('Top'), + ); + }); + + test('module address resolves correctly', () { + expect( + service.addressToPathname(const OccurrenceAddress([0])), + equals('Top/cpu'), + ); + }); + + test('nested module address resolves correctly', () { + expect( + service.addressToPathname(const OccurrenceAddress([0, 0])), + equals('Top/cpu/alu'), + ); + }); + + test('signal address resolves with asSignal flag', () { + expect( + service.addressToPathname( + const OccurrenceAddress([0, 0]), + asSignal: true, + ), + equals('Top/cpu/clk'), + ); + }); + + test('nested signal address resolves with asSignal flag', () { + expect( + service.addressToPathname( + const OccurrenceAddress([0, 0, 2]), + asSignal: true, + ), + equals('Top/cpu/alu/out'), + ); + }); + + test('out-of-bounds child returns null', () { + expect( + service.addressToPathname(const OccurrenceAddress([5])), + isNull, + ); + }); + + test('out-of-bounds signal returns null', () { + expect( + service.addressToPathname( + const OccurrenceAddress([0, 99]), + asSignal: true, + ), + isNull, + ); + }); + }); + + group('nodeByAddress', () { + test('root address returns root', () { + final node = service.occurrenceByAddress(OccurrenceAddress.root); + expect(node?.name, equals('Top')); + }); + + test('child address returns correct child', () { + final node = service.occurrenceByAddress(const OccurrenceAddress([0])); + expect(node?.name, equals('cpu')); + }); + + test('nested address returns correct node', () { + final node = + service.occurrenceByAddress(const OccurrenceAddress([0, 0])); + expect(node?.name, equals('alu')); + }); + + test('out-of-bounds returns null', () { + expect( + service.occurrenceByAddress(const OccurrenceAddress([99])), + isNull, + ); + }); + }); + + group('signalByAddress', () { + test('signal address returns correct signal', () { + // cpu's first signal (clk) has address [0, 0] + final clkAddr = root.children[0].signals[0].address!; + final sig = service.signalByAddress(clkAddr); + expect(sig?.name, equals('clk')); + }); + + test('nested signal address returns correct signal', () { + // alu's third signal (out) has address [0, 0, 2] + final outAddr = root.children[0].children[0].signals[2].address!; + final sig = service.signalByAddress(outAddr); + expect(sig?.name, equals('out')); + }); + + test('root address returns null (not a signal)', () { + expect(service.signalByAddress(OccurrenceAddress.root), isNull); + }); + }); + + group('waveformIdToAddress', () { + test('dot-separated waveform ID resolves', () { + final addr = service.waveformIdToAddress('Top.cpu.alu.a'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0, 0])); // cpu[0], alu[0], a[0] + }); + }); + + group('round-trip', () { + test('pathname → address → pathname preserves module path', () { + const path = 'Top/cpu/alu'; + final addr = service.pathnameToAddress(path); + expect(addr, isNotNull); + final roundTripped = service.addressToPathname(addr!); + expect(roundTripped, equals(path)); + }); + + test('pathname → address → pathname preserves signal path', () { + const path = 'Top/cpu/alu/out'; + final addr = service.pathnameToAddress(path); + expect(addr, isNotNull); + final roundTripped = service.addressToPathname(addr!, asSignal: true); + expect(roundTripped, equals(path)); + }); + + test('address → pathname → address preserves module address', () { + const addr = OccurrenceAddress([0, 0]); + final path = service.addressToPathname(addr); + expect(path, isNotNull); + final roundTripped = service.pathnameToAddress(path!); + expect(roundTripped?.path, equals(addr.path)); + }); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/devtools_search_flow_test.dart b/packages/rohd_hierarchy/test/devtools_search_flow_test.dart new file mode 100644 index 000000000..ac5f11ad9 --- /dev/null +++ b/packages/rohd_hierarchy/test/devtools_search_flow_test.dart @@ -0,0 +1,214 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// devtools_search_flow_test.dart +// Tests that simulate the DevTools embedding flow with local signal IDs: +// HierarchyNode tree → BaseHierarchyAdapter.fromTree → search +// +// 2026 April +// Author: Desmond Kirkpatrick + +// The test verifies that search works correctly with local signal IDs +// (as opposed to VCD-style full-path IDs), catching any assumption +// mismatches in the search engine. + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Build the test hierarchy tree directly, matching the structure +/// that would be produced from ROHD inspector JSON. +/// Signals have local IDs and full qualified paths. +HierarchyOccurrence _buildTestHierarchy() { + final cam = HierarchyOccurrence( + name: 'cam', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'hit', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'entry', + width: 32, + direction: 'input', + ), + SignalOccurrence( + name: 'match_out', + width: 1, + direction: 'output', + ), + ], + ); + + final lab = HierarchyOccurrence( + name: 'lab', + children: [cam], + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'reset', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'fromUpstream_request__st', + width: 64, + direction: 'input', + ), + SignalOccurrence( + name: 'toUpstream_response__st', + width: 64, + direction: 'output', + ), + ], + ); + + final dmaEngine = HierarchyOccurrence( + name: 'engine', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'enable', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'data_in', + width: 64, + direction: 'input', + ), + SignalOccurrence( + name: 'data_out', + width: 64, + direction: 'output', + ), + SignalOccurrence( + name: 'done', + width: 1, + direction: 'output', + ), + ], + ); + + return HierarchyOccurrence( + name: 'Abcd', + children: [lab, dmaEngine], + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'resetn', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'araddr_s', + width: 32, + direction: 'input', + ), + SignalOccurrence( + name: 'rdata_s', + width: 32, + direction: 'output', + ), + ], + ); +} + +void main() { + late BaseHierarchyAdapter service; + + setUp(() { + final root = _buildTestHierarchy()..buildAddresses(); + service = BaseHierarchyAdapter.fromTree(root); + }); + + group( + 'DevTools flow — local signal IDs ' + '→ BaseHierarchyAdapter.fromTree → search', () { + // Basic search, address, glob, and controller behavior is covered by + // hierarchy_search_controller_test, regex_search_test, + // address_conversion_test, and module_search_test. + // + // This group focuses on what is unique to the DevTools local-ID flow: + // search correctness when SignalOccurrence.name is a local name (not a full + // path). + + test('search works with local signal IDs', () { + // Plain prefix search still finds signals by name + final results = service.searchSignals('clk'); + expect(results, isNotEmpty); + expect(results.map((r) => r.name), everyElement('clk')); + // Glob still works + final globResults = service.searchSignals('**/entry'); + expect(globResults, isNotEmpty); + expect(globResults.first.name, 'entry'); + }); + + test('signalByAddress resolves despite local IDs', () { + final addr = + OccurrenceAddress.tryFromPathname('Abcd/lab/cam/hit', service.root); + expect(addr, isNotNull); + final hit = service.signalByAddress(addr!); + expect(hit, isNotNull); + expect(hit!.name, 'hit'); + expect(hit.name, 'hit'); // local, not full path + expect(hit.path(), 'Abcd/lab/cam/hit'); + }); + + test('searchModules works with local-ID tree', () { + final results = service.searchOccurrences('cam'); + expect(results, isNotEmpty); + expect(results.first.occurrence.name, 'cam'); + }); + }); + + // ── SignalOccurrence ID format verification ── + + group('local signal ID format', () { + test('signals have local IDs (not full paths)', () { + final sigs = service.root.signals; + final clk = sigs.firstWhere((s) => s.name == 'clk'); + // The signal id is the local name, not the full path + expect(clk.name, 'clk'); + // But fullPath is the full qualified path + expect(clk.path(), 'Abcd/clk'); + }); + + test('local signal IDs do not break address resolution', () { + final addr = OccurrenceAddress.tryFromPathname( + 'Abcd/lab/cam/match_out', service.root); + expect(addr, isNotNull); + final result = service.signalByAddress(addr!); + expect(result, isNotNull); + expect(result!.name, 'match_out'); + expect(result.name, 'match_out'); // local name + }); + + test('search results carry the correct signal object', () { + final results = service.searchSignals('Abcd/rdata_s'); + expect(results, isNotEmpty); + final r = results.first; + expect(r.signal, isNotNull); + expect(r.signal!.name, 'rdata_s'); // local name + expect(r.signal!.path(), 'Abcd/rdata_s'); // full path + expect(r.signal!.width, 32); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart new file mode 100644 index 000000000..6a8fe173a --- /dev/null +++ b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart @@ -0,0 +1,719 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_integration_test.dart +// Integration tests using a real ROHD FilterBank netlist JSON fixture. +// Covers model getters, service methods, and adapter edge cases. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Load the slim FilterBank fixture and build a NetlistHierarchyAdapter. +NetlistHierarchyAdapter _loadFixture() { + final json = File('test/fixtures/filter_bank.json').readAsStringSync(); + return NetlistHierarchyAdapter.fromJson(json); +} + +void main() { + late NetlistHierarchyAdapter adapter; + late HierarchyService service; + + setUpAll(() { + adapter = _loadFixture(); + service = adapter; + service.root.buildAddresses(); + }); + + // ─────────────── NetlistHierarchyAdapter parsing ─────────────── + + group('NetlistHierarchyAdapter — FilterBank fixture', () { + test('top module is FilterBank', () { + expect(service.root.name, 'FilterBank'); + }); + + test('rootNameOverride replaces root node name', () { + final json = File('test/fixtures/filter_bank.json').readAsStringSync(); + final custom = NetlistHierarchyAdapter.fromJson( + json, + rootNameOverride: 'MyDesign', + ); + expect(custom.root.name, 'MyDesign'); + expect(custom.root.definition, 'FilterBank'); + }); + + test('child modules preserve definition separately from instance name', () { + final controller = service.root.children.firstWhere( + (child) => child.name == 'controller_1', + ); + + expect(controller.name, 'controller_1'); + expect(controller.definition, 'FilterController'); + }); + + test('root has expected ports as signals', () { + final portNames = service.root.signals.map((s) => s.name).toSet(); + expect(portNames, containsAll(['clk', 'reset', 'start', 'done'])); + }); + + test('has hierarchical children (ch0, ch1, controller)', () { + final childNames = service.root.children.map((c) => c.name).toSet(); + // ch0_1 and ch1_1 are FilterChannel instances; controller_1 is + // FilterController + expect(childNames, containsAll(['ch0_1', 'ch1_1', 'controller_1'])); + }); + + test('primitive cells are marked isPrimitive', () { + // array_slice cells in FilterBank are $slice — primitive + final sliceCells = service.root.children.where( + (c) => c.definition != null && c.definition!.startsWith(r'$'), + ); + expect(sliceCells, isNotEmpty); + for (final cell in sliceCells) { + expect( + cell.isPrimitive, + isTrue, + reason: '${cell.name} (${cell.definition}) should be primitive', + ); + } + }); + + test('primitive cells have port signals from port_directions', () { + final primitives = service.root.children.where((c) => c.isPrimitive); + for (final prim in primitives) { + expect( + prim.signals, + isNotEmpty, + reason: '${prim.name} should have port signals', + ); + // All signals on primitive cells should have a direction + for (final s in prim.signals) { + expect( + s.isPort, + isTrue, + reason: '${prim.name}/${s.name} should be a port', + ); + expect(s.direction, isNotEmpty); + } + } + }); + + test('netnames with hide_name=1 are excluded', () { + // FilterBank has controller_1_loadingPhase with hide_name=1 + final allSignalNames = service.root.depthFirstSignals().map( + (s) => s.name, + ); + expect(allSignalNames, isNot(contains('controller_1_loadingPhase'))); + }); + + test('netnames with computed attribute are included with isComputed', () { + // CoeffBank has const_0_2_h0 with computed=1 + // Navigate: FilterBank → ch0_1 → one of its children should have + // a CoeffBank with computed signals + bool foundComputed(HierarchyOccurrence node) { + for (final s in node.signals) { + if (s.isComputed) { + return true; + } + } + return node.children.any(foundComputed); + } + + expect( + foundComputed(service.root), + isTrue, + reason: 'Should have at least one computed signal', + ); + }); + + test(r'$-prefixed netnames are excluded', () { + // Any netname starting with $ should be filtered out + final allNames = service.root.depthFirstSignals().map((s) => s.name); + final dollarNames = allNames.where((n) => n.startsWith(r'$')); + expect( + dollarNames, + isEmpty, + reason: r'No $-prefixed netnames should appear', + ); + }); + }); + + // ─────────────── HierarchyNode model getters ─────────────── + + group('HierarchyNode model getters', () { + test('ports returns only signals with direction', () { + final ports = service.root.ports; + expect(ports, isNotEmpty); + for (final p in ports) { + expect(p.isPort, isTrue); + expect(p.direction, isNotEmpty); + } + }); + + test('inputs returns only input ports', () { + final inputs = service.root.inputs; + expect(inputs, isNotEmpty); + for (final s in inputs) { + expect(s.direction, 'input'); + } + expect(inputs.map((s) => s.name), contains('clk')); + }); + + test('outputs returns only output ports', () { + final outputs = service.root.outputs; + expect(outputs, isNotEmpty); + for (final s in outputs) { + expect(s.direction, 'output'); + } + expect(outputs.map((s) => s.name), contains('done')); + }); + + test(r'isPrimitiveType is true for $-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType(r'$mux'), isTrue); + expect(HierarchyOccurrence.isPrimitiveType(r'$and'), isTrue); + }); + + test(r'isPrimitiveType is false for non-$-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType('FilterBank'), isFalse); + }); + + test('isPrimitiveType is false for empty string', () { + expect(HierarchyOccurrence.isPrimitiveType(''), isFalse); + }); + + test('isPrimitiveCell reflects isPrimitive field and type', () { + // A node marked isPrimitive=true + final primCell = service.root.children.firstWhere((c) => c.isPrimitive); + expect(primCell.isPrimitiveCell, isTrue); + + // The root module is not primitive + expect(service.root.isPrimitiveCell, isFalse); + }); + + test('depthFirstSignals places root signals first', () { + final all = service.root.depthFirstSignals(); + expect(all, isNotEmpty); + + final rootSigs = service.root.signals; + for (var i = 0; i < rootSigs.length; i++) { + expect(all[i].name, rootSigs[i].name); + } + }); + + test('depthFirstSignals count equals recursive signal total', () { + final all = service.root.depthFirstSignals(); + int countSignals(HierarchyOccurrence n) => + n.signals.length + + n.children.fold(0, (sum, c) => sum + countSignals(c)); + expect(all.length, countSignals(service.root)); + }); + }); + + // ─────────────── SignalOccurrence model getters ─────────────── + + group('SignalOccurrence model getters', () { + test('isPort is true for Port instances', () { + final port = service.root.signals.first; + expect(port.isPort, isTrue); + }); + + test('input port has isInput true and isOutput/isInout false', () { + final clk = service.root.signals.firstWhere((s) => s.name == 'clk'); + expect(clk.isPort, isTrue); + expect(clk.isInput, isTrue); + expect(clk.isOutput, isFalse); + expect(clk.isInout, isFalse); + }); + + test('output port has isOutput true and isInput false', () { + final done = service.root.signals.firstWhere((s) => s.name == 'done'); + expect(done.isOutput, isTrue); + expect(done.isInput, isFalse); + }); + + test('isPort is false for non-Port signals (internal wires)', () { + // Internal signals (from netnames) are SignalOccurrence, not Port. + // The fixture includes visible non-port netnames like tapMatch0. + final allSigs = service.root.depthFirstSignals(); + final nonPorts = allSigs.where((s) => !s.isPort).toList(); + expect( + nonPorts, + isNotEmpty, + reason: 'Should have non-Port internal signals from netnames', + ); + }); + + test('SignalOccurrence.toString includes name and width', () { + final clk = service.root.signals.firstWhere((s) => s.name == 'clk'); + final str = clk.toString(); + expect(str, contains('clk')); + }); + }); + + // ─────────────── HierarchyService methods ─────────────── + + group('HierarchyService — search coverage', () { + test('searchNodes returns HierarchyNode objects', () { + final nodes = service.matchOccurrences('controller'); + expect(nodes, isNotEmpty); + for (final n in nodes) { + expect(n, isA()); + } + }); + + test('autocompletePaths returns children for partial path', () { + final suggestions = service.autocompletePaths('FilterBank/'); + expect(suggestions, isNotEmpty); + for (final s in suggestions) { + expect(s, startsWith('FilterBank/')); + } + }); + + test('autocompletePaths filters by prefix', () { + final suggestions = service.autocompletePaths('FilterBank/ch'); + expect(suggestions, isNotEmpty); + for (final s in suggestions) { + expect(s.toLowerCase(), contains('/ch')); + } + }); + + test('autocompletePaths with empty string returns root', () { + final suggestions = service.autocompletePaths(''); + // Should suggest root-level completions + expect(suggestions, isNotEmpty); + }); + + test('autocompletePaths appends / for nodes with children', () { + final suggestions = service.autocompletePaths('FilterBank/'); + final withSlash = suggestions.where((s) => s.endsWith('/')); + // At least ch0_1 and ch1_1 have children + expect(withSlash, isNotEmpty); + }); + + test('hasRegexChars is false for plain text', () { + expect(HierarchyService.hasRegexChars('clk'), isFalse); + }); + + test('hasRegexChars detects * glob', () { + expect(HierarchyService.hasRegexChars('c*'), isTrue); + }); + + test('hasRegexChars detects ? glob', () { + expect(HierarchyService.hasRegexChars('cl?'), isTrue); + }); + + test('hasRegexChars detects character class', () { + expect(HierarchyService.hasRegexChars('[a-z]'), isTrue); + }); + + test('hasRegexChars detects group alternation', () { + expect(HierarchyService.hasRegexChars('(a|b)'), isTrue); + }); + + test('hasRegexChars detects + quantifier', () { + expect(HierarchyService.hasRegexChars('a+'), isTrue); + }); + + test('longestCommonPrefix finds shared prefix', () { + expect( + HierarchyService.longestCommonPrefix([ + 'FilterBank/ch0', + 'FilterBank/ch1', + ]), + 'FilterBank/ch', + ); + }); + + test('longestCommonPrefix returns null for empty list', () { + expect(HierarchyService.longestCommonPrefix([]), isNull); + }); + + test('longestCommonPrefix returns null for no common prefix', () { + expect(HierarchyService.longestCommonPrefix(['abc', 'xyz']), isNull); + }); + + test('longestCommonPrefix is case-sensitive', () { + final prefix = HierarchyService.longestCommonPrefix([ + 'Filter/abc', + 'Filter/abd', + ]); + expect(prefix, 'Filter/ab'); + }); + }); + + // ─────────────── HierarchySearchController ─────────────── + + group('HierarchySearchController — additional coverage', () { + test('selectAt selects valid index', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + expect(ctrl.hasResults, isTrue); + + ctrl.selectAt(0); + expect(ctrl.selectedIndex, 0); + }); + + test('selectAt clamps high index to last result', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + expect(ctrl.hasResults, isTrue); + + ctrl.selectAt(999); + expect(ctrl.selectedIndex, ctrl.results.length - 1); + }); + + test('selectAt clamps negative index to zero', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + expect(ctrl.hasResults, isTrue); + + ctrl.selectAt(-5); + expect(ctrl.selectedIndex, 0); + }); + + test('selectAt on empty results is no-op', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..selectAt(3); + expect(ctrl.selectedIndex, 0); + expect(ctrl.hasResults, isFalse); + }); + + test('tabComplete expands to longest common prefix', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + if (ctrl.results.length > 1) { + final expansion = ctrl.tabComplete('clk'); + // Expansion should be longer than the query if results share a + // common prefix beyond 'clk' + if (expansion != null) { + expect(expansion.length, greaterThan(3)); + } + } + }); + + test('tabComplete returns null when no results', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('zzz_nonexistent'); + expect(ctrl.tabComplete('zzz_nonexistent'), isNull); + }); + + test('tabComplete returns null when prefix is not longer', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + // If there's a single result whose displayPath equals normalized + // query, tabComplete should return null or the path itself. + // With multiple results from different modules, the common prefix + // may not be longer. + final result = ctrl.tabComplete(ctrl.results.first.displayPath); + // Either null or the same length — shouldn't crash + expect(result, anyOf(isNull, isA())); + }); + }); + + // ─────────────── ModuleSearchResult getters ─────────────── + + group('ModuleSearchResult — additional getters', () { + test('isModule reflects non-primitive node', () { + final results = service.searchOccurrences('ch0'); + expect(results, isNotEmpty); + final r = results.first; + expect(r.isModule, isNotNull); + }); + + test('childCount reflects node.children.length', () { + final results = service.searchOccurrences('FilterBank'); + final fbResult = results.firstWhere( + (r) => r.path.length == 1, + orElse: () => results.first, + ); + expect(fbResult.childCount, greaterThan(0)); + }); + + test('toString includes module name', () { + final results = service.searchOccurrences('ch0'); + expect(results.first.toString(), contains('ch0')); + }); + }); + + // ─────────────── SignalSearchResult toString ─────────────── + + group('SignalSearchResult.toString', () { + test('toString includes signal name', () { + final results = service.searchSignals('clk'); + expect(results, isNotEmpty); + expect(results.first.toString(), contains('clk')); + }); + }); + + // ─────────────── BaseHierarchyAdapter edge case ─────────────── + // The real uninitialized-root StateError test lives in + // coverage_gaps_test.dart. Here we just verify fromTree works. + + group('BaseHierarchyAdapter — fromTree produces usable root', () { + test('fromTree immediately sets root', () { + final tree = HierarchyOccurrence(name: 'r'); + final svc = BaseHierarchyAdapter.fromTree(tree); + expect(svc.root.name, 'r'); + }); + }); + + // ─────────────── Multiple instantiation (dedup) ─────────────── + + group('Multiple instantiation — FilterChannel dedup', () { + test('ch0 and ch1 are separate node instances', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + expect(identical(ch0, ch1), isFalse); + }); + + test('ch0 and ch1 have identical signal structure', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + + expect(ch0.signals.length, ch1.signals.length); + + final ch0PortNames = ch0.signals.map((s) => s.name).toSet(); + final ch1PortNames = ch1.signals.map((s) => s.name).toSet(); + expect(ch0PortNames, ch1PortNames); + }); + + test('search finds signals in both channel instances', () { + // Both channels should have a clk port + final results = service.searchSignals('clk'); + final channelClks = results + .where( + (r) => r.signalId.contains('ch0_1') || r.signalId.contains('ch1_1'), + ) + .toList(); + // Should find clk in both ch0_1 and ch1_1 + expect( + channelClks.where((r) => r.signalId.contains('ch0_1')), + isNotEmpty, + ); + expect( + channelClks.where((r) => r.signalId.contains('ch1_1')), + isNotEmpty, + ); + }); + + test('addresses resolve independently for each instance', () { + final ch0Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch0_1/clk', + service.root, + ); + final ch1Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch1_1/clk', + service.root, + ); + + expect(ch0Addr, isNotNull); + expect(ch1Addr, isNotNull); + expect(ch0Addr, isNot(equals(ch1Addr))); + + final ch0Sig = service.signalByAddress(ch0Addr!); + final ch1Sig = service.signalByAddress(ch1Addr!); + expect(ch0Sig, isNotNull); + expect(ch1Sig, isNotNull); + expect(ch0Sig!.name, 'clk'); + expect(ch1Sig!.name, 'clk'); + }); + + test('both instances have internal (non-port) signals', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + + final ch0Internal = ch0.signals.where((s) => !s.isPort).toList(); + final ch1Internal = ch1.signals.where((s) => !s.isPort).toList(); + + expect( + ch0Internal, + isNotEmpty, + reason: 'ch0_1 should have internal signals from netnames', + ); + expect( + ch1Internal, + isNotEmpty, + reason: 'ch1_1 should have internal signals from netnames', + ); + }); + + test('both instances share the same internal signal names', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + + final ch0Names = + ch0.signals.where((s) => !s.isPort).map((s) => s.name).toSet(); + final ch1Names = + ch1.signals.where((s) => !s.isPort).map((s) => s.name).toSet(); + expect(ch0Names, ch1Names); + }); + + test('internal signals are addressable per-instance', () { + // validPipe exists as a netname in both FilterChannel definitions + final ch0Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch0_1/validPipe', + service.root, + ); + final ch1Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch1_1/validPipe', + service.root, + ); + + expect(ch0Addr, isNotNull, reason: 'ch0_1/validPipe should resolve'); + expect(ch1Addr, isNotNull, reason: 'ch1_1/validPipe should resolve'); + expect(ch0Addr, isNot(equals(ch1Addr))); + + final ch0Sig = service.signalByAddress(ch0Addr!); + final ch1Sig = service.signalByAddress(ch1Addr!); + expect(ch0Sig, isNotNull); + expect(ch1Sig, isNotNull); + expect(ch0Sig!.name, 'validPipe'); + expect(ch1Sig!.name, 'validPipe'); + expect(ch0Sig.isPort, isFalse); + }); + + test('search finds internal signals in both instances', () { + final results = service.searchSignals('validPipe'); + final inCh0 = results.where((r) => r.signalId.contains('ch0_1')); + final inCh1 = results.where((r) => r.signalId.contains('ch1_1')); + expect(inCh0, isNotEmpty, reason: 'validPipe should be found in ch0_1'); + expect(inCh1, isNotEmpty, reason: 'validPipe should be found in ch1_1'); + }); + + test('depthFirstSignals includes internal signals from both instances', () { + final all = service.root.depthFirstSignals(); + final vpSigs = all.where((s) => s.name == 'validPipe').toList(); + expect( + vpSigs.length, + greaterThanOrEqualTo(2), + reason: 'validPipe should appear in at least ch0 and ch1', + ); + }); + }); + + // ─────────────── InOut (bidirectional) port tests ─────────────── + + group('InOut port — dataBus', () { + test('root has dataBus as inout port', () { + final dataBus = service.root.signals + .where((s) => s.isPort) + .where((p) => p.name == 'dataBus') + .firstOrNull; + expect(dataBus, isNotNull, reason: 'FilterBank should have dataBus'); + expect(dataBus!.direction, 'inout'); + expect(dataBus.isInout, isTrue); + expect(dataBus.isInput, isFalse); + expect(dataBus.isOutput, isFalse); + }); + + test('inputs getter excludes inout ports', () { + final inputs = service.root.inputs; + final inoutInInputs = inputs.where((s) => s.direction == 'inout'); + expect( + inoutInInputs, + isEmpty, + reason: 'inputs should not include inout ports', + ); + }); + + test('outputs getter excludes inout ports', () { + final outputs = service.root.outputs; + final inoutInOutputs = outputs.where((s) => s.direction == 'inout'); + expect( + inoutInOutputs, + isEmpty, + reason: 'outputs should not include inout ports', + ); + }); + + test('inouts getter returns only inout ports', () { + final inouts = service.root.inouts; + expect(inouts, isNotEmpty, reason: 'inouts should include dataBus'); + expect(inouts.map((s) => s.name), contains('dataBus')); + for (final s in inouts) { + expect(s.direction, 'inout'); + } + }); + + test('ports getter includes inout ports', () { + final allPorts = service.root.ports; + final inouts = allPorts.where((p) => p.direction == 'inout').toList(); + expect(inouts, isNotEmpty, reason: 'ports should include inout ports'); + expect(inouts.first.name, 'dataBus'); + }); + + test('dataBus is addressable and resolvable', () { + final addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/dataBus', + service.root, + ); + expect(addr, isNotNull, reason: 'dataBus should be addressable'); + + final sig = service.signalByAddress(addr!); + expect(sig, isNotNull); + expect(sig!.name, 'dataBus'); + expect(sig.isInout, isTrue); + }); + + test('search finds dataBus inout port', () { + final results = service.searchSignals('dataBus'); + expect(results, isNotEmpty); + final dataBusResults = results.where( + (r) => r.signalId.contains('dataBus'), + ); + expect(dataBusResults, isNotEmpty); + }); + + test('SharedDataBus child also has dataBus inout', () { + final sharedBus = service.root.children + .where((c) => c.name == 'sharedBus_1') + .firstOrNull; + expect( + sharedBus, + isNotNull, + reason: 'sharedBus_1 cell should be present', + ); + final childDataBus = sharedBus!.signals + .where((s) => s.isPort) + .where((p) => p.name == 'dataBus') + .firstOrNull; + expect( + childDataBus, + isNotNull, + reason: 'SharedDataBus should have dataBus inout', + ); + expect(childDataBus!.isInout, isTrue); + }); + + test('depthFirstSignals includes inout ports', () { + final all = service.root.depthFirstSignals(); + final inouts = all.where((s) => s.isInout); + expect( + inouts, + isNotEmpty, + reason: 'depthFirstSignals should include inout ports', + ); + }); + + test('addressToPathname round-trips for inout signal', () { + final addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/dataBus', + service.root, + ); + expect(addr, isNotNull); + final pathname = service.addressToPathname(addr!, asSignal: true); + expect(pathname, 'FilterBank/dataBus'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/fixtures/filter_bank.json b/packages/rohd_hierarchy/test/fixtures/filter_bank.json new file mode 100644 index 000000000..494318612 --- /dev/null +++ b/packages/rohd_hierarchy/test/fixtures/filter_bank.json @@ -0,0 +1,1183 @@ +{ + "modules": { + "CoeffBank_T3_W16": { + "attributes": { + "src": "generated" + }, + "ports": { + "tapIndex": { + "direction": "input", + "bits": [ + 2, + 3 + ] + }, + "coeffArray": { + "direction": "input", + "bits": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51 + ] + }, + "coeffOut": { + "direction": "output", + "bits": [ + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67 + ] + } + }, + "netnames": { + "tapMatch0": { + "bits": [ + 68 + ], + "attributes": {} + }, + "tapMatch1": { + "bits": [ + 69 + ], + "attributes": {} + }, + "const_0_2_h0": { + "bits": [ + 103, + 104 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "mux_3": { + "type": "$mux", + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + } + }, + "equals_3": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "mux_0_1": { + "type": "$mux", + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + } + }, + "equals_0_1": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "mux_1_1": { + "type": "$mux", + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + } + } + } + }, + "MacUnit_W16": { + "attributes": { + "src": "generated" + }, + "ports": { + "sampleIn": { + "direction": "input", + "bits": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ] + }, + "coeffIn": { + "direction": "input", + "bits": [ + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33 + ] + }, + "accumIn": { + "direction": "input", + "bits": [ + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 50 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 51 + ] + }, + "enable": { + "direction": "input", + "bits": [ + 52 + ] + }, + "result": { + "direction": "output", + "bits": [ + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68 + ] + } + }, + "netnames": { + "sampleIn_stage2_i": { + "bits": [ + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84 + ], + "attributes": {} + }, + "sampleIn_stage0_o": { + "bits": [ + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100 + ], + "attributes": {} + } + }, + "cells": { + "comb_stage2_1": { + "type": "Combinational", + "port_directions": { + "_in0_sampleIn_stage2_i": "input", + "_in2_coeffIn_stage2_i": "input", + "_in4_accumIn_stage2_i": "input", + "_out1_sampleIn_stage2": "output", + "_out3_coeffIn_stage2": "output", + "_out5_accumIn_stage2": "output" + } + }, + "ff_sampleIn_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in4_sampleIn_stage0_o": "input", + "_in5_sampleIn_stage1_o": "input", + "_trigger0_clk": "input", + "_out6_sampleIn_stage1_i": "output", + "_out7_sampleIn_stage2_i": "output" + } + }, + "comb_stage0_1": { + "type": "Combinational", + "port_directions": { + "_in0_sampleIn_stage0_i": "input", + "_in2_coeffIn_stage0_i": "input", + "_in4_accumIn_stage0_i": "input", + "_in6_product": "input", + "_out1_sampleIn_stage0": "output", + "_out3_coeffIn_stage0": "output", + "_out5_accumIn_stage0": "output", + "_out7_sampleIn_stage0": "output" + } + }, + "multiply_1": { + "type": "$mul", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "ff_coeffIn_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in4_coeffIn_stage0_o": "input", + "_in5_coeffIn_stage1_o": "input", + "_trigger0_clk": "input", + "_out6_coeffIn_stage1_i": "output", + "_out7_coeffIn_stage2_i": "output" + } + } + } + }, + "FilterChannel_T3_W16": { + "attributes": { + "src": "generated" + }, + "ports": { + "sampleIn": { + "direction": "input", + "bits": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ] + }, + "validIn": { + "direction": "input", + "bits": [ + 18 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 19 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 20 + ] + }, + "enable": { + "direction": "input", + "bits": [ + 21 + ] + }, + "dataOut": { + "direction": "output", + "bits": [ + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37 + ] + }, + "validOut": { + "direction": "output", + "bits": [ + 38 + ] + } + }, + "netnames": { + "validPipe": { + "bits": [ + 39 + ], + "attributes": {} + }, + "outputReady": { + "bits": [ + 40 + ], + "attributes": {} + }, + "const_0_2_h0": { + "bits": [ + 420, + 421 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "combinational_2": { + "type": "Combinational", + "port_directions": { + "_in0_validPipe": "input", + "_in1_outputReg": "input", + "_out4_dataOut": "output", + "_out5_validOut": "output" + } + }, + "sequential_3": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_outputReady": "input", + "_trigger0_clk": "input", + "_out5_validPipe": "output" + } + }, + "and__3": { + "type": "$and", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "sequential_0_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in5_lastTap": "input", + "_in6_lastTapD1": "input", + "_in7_lastTapD2": "input", + "_in8_accumReg": "input", + "_trigger0_clk": "input", + "_out9_lastTapD1": "output", + "_out10_lastTapD2": "output", + "_out11_outputReg": "output" + } + }, + "sequential_1_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_lastTap": "input", + "_in7__tapCounter_add_const_1": "input", + "_trigger0_clk": "input", + "_out10_tapCounter": "output" + } + } + } + }, + "FilterController": { + "attributes": { + "src": "generated" + }, + "ports": { + "clk": { + "direction": "input", + "bits": [ + 2 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 3 + ] + }, + "start": { + "direction": "input", + "bits": [ + 4 + ] + }, + "inputValid": { + "direction": "input", + "bits": [ + 5 + ] + }, + "inputDone": { + "direction": "input", + "bits": [ + 6 + ] + }, + "filterEnable": { + "direction": "output", + "bits": [ + 7 + ] + }, + "loadingPhase": { + "direction": "output", + "bits": [ + 8 + ] + }, + "doneFlag": { + "direction": "output", + "bits": [ + 9 + ] + }, + "state": { + "direction": "output", + "bits": [ + 10, + 11, + 12 + ] + } + }, + "netnames": { + "currentState": { + "bits": [ + 13, + 14, + 15 + ], + "attributes": {} + }, + "isDraining": { + "bits": [ + 16 + ], + "attributes": {} + }, + "const_0_3_h3": { + "bits": [ + 94, + 95, + 96 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "combinational_1": { + "type": "Combinational", + "port_directions": { + "_in0_currentState": "input", + "_in1_FilterState_idle": "input", + "_in6_start": "input", + "_in9_FilterState_loading": "input", + "_in14_inputValid": "input", + "_in17_FilterState_running": "input", + "_in22_inputDone": "input", + "_in25_FilterState_draining": "input", + "_in30_drainDone": "input", + "_in33_FilterState_done": "input", + "_out42_filterEnable": "output", + "_out43_loadingPhase": "output", + "_out44_doneFlag": "output", + "_out45_nextState": "output" + } + }, + "swizzle_1": { + "type": "$buf", + "port_directions": { + "A": "input", + "Y": "output" + } + }, + "equals_2": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "sequential_2": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_isDraining": "input", + "_in4__drainCount_add_const_1": "input", + "_trigger0_clk": "input", + "_out7_drainCount": "output" + } + }, + "equals_0_1": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + } + } + }, + "FilterChannel_T3_W16_0": { + "attributes": { + "src": "generated" + }, + "ports": { + "sampleIn": { + "direction": "input", + "bits": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ] + }, + "validIn": { + "direction": "input", + "bits": [ + 18 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 19 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 20 + ] + }, + "enable": { + "direction": "input", + "bits": [ + 21 + ] + }, + "dataOut": { + "direction": "output", + "bits": [ + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37 + ] + }, + "validOut": { + "direction": "output", + "bits": [ + 38 + ] + } + }, + "netnames": { + "validPipe": { + "bits": [ + 39 + ], + "attributes": {} + }, + "outputReady": { + "bits": [ + 40 + ], + "attributes": {} + }, + "const_0_2_h0": { + "bits": [ + 420, + 421 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "combinational_2": { + "type": "Combinational", + "port_directions": { + "_in0_validPipe": "input", + "_in1_outputReg": "input", + "_out4_dataOut": "output", + "_out5_validOut": "output" + } + }, + "sequential_3": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_outputReady": "input", + "_trigger0_clk": "input", + "_out5_validPipe": "output" + } + }, + "and__3": { + "type": "$and", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "sequential_0_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in5_lastTap": "input", + "_in6_lastTapD1": "input", + "_in7_lastTapD2": "input", + "_in8_accumReg": "input", + "_trigger0_clk": "input", + "_out9_lastTapD1": "output", + "_out10_lastTapD2": "output", + "_out11_outputReg": "output" + } + }, + "sequential_1_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_lastTap": "input", + "_in7__tapCounter_add_const_1": "input", + "_trigger0_clk": "input", + "_out10_tapCounter": "output" + } + } + } + }, + "SharedDataBus": { + "attributes": { + "src": "generated" + }, + "ports": { + "writeEnable": { + "direction": "input", + "bits": [ + 502 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 503 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 504 + ] + }, + "storedValue": { + "direction": "output", + "bits": [ + 505, + 506, + 507, + 508, + 509, + 510, + 511, + 512, + 513, + 514, + 515, + 516, + 517, + 518, + 519, + 520 + ] + }, + "dataBus": { + "direction": "inout", + "bits": [ + 521, + 522, + 523, + 524, + 525, + 526, + 527, + 528, + 529, + 530, + 531, + 532, + 533, + 534, + 535, + 536 + ] + } + }, + "netnames": { + "latch": { + "bits": [ + 537, + 538, + 539, + 540, + 541, + 542, + 543, + 544, + 545, + 546, + 547, + 548, + 549, + 550, + 551, + 552 + ], + "attributes": {} + } + }, + "cells": {} + }, + "FilterBank": { + "attributes": { + "src": "generated", + "top": 1 + }, + "ports": { + "clk": { + "direction": "input", + "bits": [ + 2 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 3 + ] + }, + "start": { + "direction": "input", + "bits": [ + 4 + ] + }, + "samplesIn": { + "direction": "input", + "bits": [ + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36 + ] + }, + "validIn": { + "direction": "input", + "bits": [ + 37 + ] + }, + "inputDone": { + "direction": "input", + "bits": [ + 38 + ] + }, + "channelOut": { + "direction": "output", + "bits": [ + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70 + ] + }, + "validOut": { + "direction": "output", + "bits": [ + 71 + ] + }, + "done": { + "direction": "output", + "bits": [ + 72 + ] + }, + "state": { + "direction": "output", + "bits": [ + 73, + 74, + 75 + ] + }, + "dataBus": { + "direction": "inout", + "bits": [ + 600, + 601, + 602, + 603, + 604, + 605, + 606, + 607, + 608, + 609, + 610, + 611, + 612, + 613, + 614, + 615 + ] + } + }, + "netnames": { + "channelOut_0_": { + "bits": [ + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54 + ], + "attributes": {} + }, + "sample0_data": { + "bits": [ + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162 + ], + "attributes": {} + }, + "controller_1_loadingPhase": { + "bits": [ + 146 + ], + "hide_name": 1, + "attributes": {} + } + }, + "cells": { + "ch0_1": { + "type": "FilterChannel_T3_W16_0", + "port_directions": { + "sampleIn": "input", + "validIn": "input", + "clk": "input", + "reset": "input", + "enable": "input", + "dataOut": "output", + "validOut": "output" + } + }, + "controller_1": { + "type": "FilterController", + "port_directions": { + "clk": "input", + "reset": "input", + "start": "input", + "inputValid": "input", + "inputDone": "input", + "filterEnable": "output", + "loadingPhase": "output", + "doneFlag": "output", + "state": "output" + } + }, + "ch1_1": { + "type": "FilterChannel_T3_W16", + "port_directions": { + "sampleIn": "input", + "validIn": "input", + "clk": "input", + "reset": "input", + "enable": "input", + "dataOut": "output", + "validOut": "output" + } + }, + "array_slice_3": { + "type": "$slice", + "port_directions": { + "A": "input", + "Y": "output" + } + }, + "array_slice_4": { + "type": "$slice", + "port_directions": { + "A": "input", + "Y": "output" + } + }, + "sharedBus_1": { + "type": "SharedDataBus", + "port_directions": { + "writeEnable": "input", + "clk": "input", + "reset": "input", + "storedValue": "output", + "dataBus": "inout" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart b/packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart new file mode 100644 index 000000000..9c574df79 --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_path_vs_signal_id_test.dart +// Verifies that signal.path(separator:) and search result signalId +// work correctly with different separators. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('signal.path() with separator', () { + late BaseHierarchyAdapter adapter; + + setUp(() { + final root = HierarchyOccurrence( + name: 'abcd', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'arvalid_s', width: 1, direction: 'input'), + ], + children: [ + HierarchyOccurrence( + name: 'lab', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'data', width: 8, direction: 'output'), + ], + ), + ], + )..buildAddresses(); + adapter = BaseHierarchyAdapter.fromTree(root); + }); + + SignalOccurrence? resolve(String path) { + final addr = OccurrenceAddress.tryFromPathname(path, adapter.root); + return addr != null ? adapter.signalByAddress(addr) : null; + } + + test('resolves dot-separated IDs', () { + final s = resolve('abcd.clk'); + expect(s, isNotNull); + expect(s!.path(separator: '.'), 'abcd.clk'); + }); + + test('resolves slash-separated IDs', () { + final s = resolve('abcd/clk'); + expect(s, isNotNull); + expect(s!.path(), 'abcd/clk'); + }); + + test('resolves with exact case', () { + final s = resolve('abcd.clk'); + expect(s, isNotNull); + expect(s!.path(), 'abcd/clk'); + }); + + test('resolves nested dot-separated IDs', () { + final s = resolve('abcd.lab.data'); + expect(s, isNotNull); + expect(s!.path(separator: '.'), 'abcd.lab.data'); + }); + + test('resolves nested slash-separated IDs', () { + final s = resolve('abcd/lab/data'); + expect(s, isNotNull); + expect(s!.path(), 'abcd/lab/data'); + }); + + test('searchSignals returns result with resolved signal', () { + final results = adapter.searchSignals('clk'); + expect(results, isNotEmpty); + for (final r in results) { + expect(r.signal, isNotNull, + reason: 'SignalOccurrence should be resolved for "${r.signalId}"'); + } + final clkResult = + results.firstWhere((r) => r.path.last == 'clk' && r.path.length == 2); + expect(clkResult.signal!.path(), 'abcd/clk'); + expect(clkResult.signal!.path(separator: '.'), 'abcd.clk'); + }); + + test('searchSignals signalId is walker-built (slash) path', () { + final results = adapter.searchSignals('clk'); + expect(results, isNotEmpty); + final clkResult = + results.firstWhere((r) => r.path.last == 'clk' && r.path.length == 2); + expect(clkResult.signalId, 'abcd/clk'); + }); + + test('signal.path() matches signalId with default separator', () { + final results = adapter.searchSignals('clk'); + final result = results.first; + expect(result.signal, isNotNull); + expect(result.signal!.path(), result.signalId); + }); + + test('searchSignalsRegex returns result with signal resolved', () { + final results = adapter.searchSignalsRegex('**/clk'); + expect(results.length, greaterThanOrEqualTo(2)); + for (final r in results) { + expect(r.signal, isNotNull, + reason: 'SignalOccurrence should be resolved for "${r.signalId}"'); + expect(r.signal!.path(), contains('/')); + } + }); + }); + + group('ROHD slash-separated hierarchy', () { + late BaseHierarchyAdapter adapter; + + setUp(() { + final root = HierarchyOccurrence( + name: 'Top', + signals: [SignalOccurrence(name: 'clk', width: 1)], + children: [ + HierarchyOccurrence( + name: 'cpu', + signals: [SignalOccurrence(name: 'data_out', width: 8)], + ), + ], + )..buildAddresses(); + adapter = BaseHierarchyAdapter.fromTree(root); + }); + + test('ROHD signals: path() matches signalId (both slash)', () { + final results = adapter.searchSignals('clk'); + expect(results, isNotEmpty); + final r = results.first; + expect(r.signal!.path(), r.signalId); + }); + }); + + group('SignalOccurrence as port', () { + test('creates a port signal with defaults', () { + final p = SignalOccurrence(name: 'clk', width: 1, direction: 'input'); + expect(p.name, 'clk'); + expect(p.direction, 'input'); + expect(p.width, 1); + expect(p.isPort, isTrue); + expect(p.isInput, isTrue); + }); + + test('creates a port signal with explicit overrides', () { + final p = SignalOccurrence( + name: 'data', + direction: 'output', + width: 32, + isComputed: true, + ); + HierarchyOccurrence(name: 'Top', signals: [p]).buildAddresses(); + expect(p.name, 'data'); + expect(p.width, 32); + expect(p.direction, 'output'); + expect(p.path(), 'Top/data'); + expect(p.parent!.path(), 'Top'); + expect(p.isComputed, isTrue); + expect(p.isOutput, isTrue); + }); + }); + + group('SignalOccurrence.value', () { + test('value is null by default', () { + final s = SignalOccurrence(name: 'a', width: 1); + expect(s.value, isNull); + }); + + test('value stores the provided runtime value', () { + final s = SignalOccurrence(name: 'a', width: 8, value: 'ff'); + expect(s.value, 'ff'); + }); + }); + + group('SignalOccurrence.parent', () { + test('parent is null before buildAddresses', () { + final s = SignalOccurrence(name: 'a', width: 1); + expect(s.parent, isNull); + }); + + test('parent is set after buildAddresses', () { + final s = SignalOccurrence(name: 'a', width: 1); + HierarchyOccurrence( + name: 'Top', + children: [ + HierarchyOccurrence(name: 'sub', signals: [s]) + ], + ).buildAddresses(); + expect(s.parent!.path(), 'Top/sub'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/hierarchy_query_test.dart b/packages/rohd_hierarchy/test/hierarchy_query_test.dart new file mode 100644 index 000000000..7425aaa43 --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_query_test.dart @@ -0,0 +1,655 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_query_test.dart +// Tests for PrefixQuery and RegexQuery matching logic. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Build a test hierarchy: +/// +/// ```text +/// SoC +/// ├─ signals: [clk, reset, irq0, irq1] +/// ├─ cpu0 +/// │ ├─ signals: [clk, reset, pc] +/// │ ├─ alu +/// │ │ └─ signals: [a, b, result, carry_out, overflow] +/// │ ├─ regfile +/// │ │ └─ signals: [clk, reset, d0, d1, d2, d15, wr_en] +/// │ └─ decoder +/// │ └─ signals: [opcode, enable, mode] +/// ├─ cpu1 +/// │ ├─ signals: [clk, reset, pc] +/// │ ├─ alu +/// │ │ └─ signals: [a, b, result, carry_out, overflow] +/// │ └─ regfile +/// │ └─ signals: [clk, reset, d0, d1, d2, d15, wr_en] +/// ├─ mem_ctrl +/// │ ├─ signals: [clk, reset, addr, data_in, data_out, valid] +/// │ ├─ ch0 +/// │ │ └─ signals: [clk, addr, data, hit, miss] +/// │ ├─ ch1 +/// │ │ └─ signals: [clk, addr, data, hit, miss] +/// │ └─ ch2 +/// │ └─ signals: [clk, addr, data, hit, miss] +/// └─ io_mux +/// ├─ signals: [clk, sel, data_muxed, valid_muxed] +/// ├─ uart0 +/// │ └─ signals: [clk, tx, rx, baud_sel] +/// └─ uart1 +/// └─ signals: [clk, tx, rx, baud_sel] +/// ``` +HierarchyService buildTestHierarchy() { + HierarchyOccurrence mkAlu() => HierarchyOccurrence( + name: 'alu', + definition: 'ALU', + signals: [ + SignalOccurrence(name: 'a', width: 8), + SignalOccurrence(name: 'b', width: 8), + SignalOccurrence(name: 'result', width: 8), + SignalOccurrence(name: 'carry_out', width: 1), + SignalOccurrence(name: 'overflow', width: 1), + ], + ); + + HierarchyOccurrence mkRegfile() => HierarchyOccurrence( + name: 'regfile', + definition: 'RegFile', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'd0', width: 8), + SignalOccurrence(name: 'd1', width: 8), + SignalOccurrence(name: 'd2', width: 8), + SignalOccurrence(name: 'd15', width: 8), + SignalOccurrence(name: 'wr_en', width: 1), + ], + ); + + final decoder = HierarchyOccurrence( + name: 'decoder', + definition: 'Decoder', + signals: [ + SignalOccurrence(name: 'opcode', width: 4), + SignalOccurrence(name: 'enable', width: 1), + SignalOccurrence(name: 'mode', width: 2), + ], + ); + + final cpu0 = HierarchyOccurrence( + name: 'cpu0', + definition: 'CPU', + children: [mkAlu(), mkRegfile(), decoder], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'pc', width: 32), + ], + ); + + final cpu1 = HierarchyOccurrence( + name: 'cpu1', + definition: 'CPU', + children: [mkAlu(), mkRegfile()], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'pc', width: 32), + ], + ); + + HierarchyOccurrence mkCacheChannel(String name) => HierarchyOccurrence( + name: name, + definition: 'CacheChannel', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'addr', width: 16), + SignalOccurrence(name: 'data', width: 32), + SignalOccurrence(name: 'hit', width: 1), + SignalOccurrence(name: 'miss', width: 1), + ], + ); + + final memCtrl = HierarchyOccurrence( + name: 'mem_ctrl', + definition: 'MemController', + children: [ + mkCacheChannel('ch0'), + mkCacheChannel('ch1'), + mkCacheChannel('ch2') + ], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'addr', width: 16), + SignalOccurrence(name: 'data_in', width: 32), + SignalOccurrence(name: 'data_out', width: 32), + SignalOccurrence(name: 'valid', width: 1), + ], + ); + + HierarchyOccurrence mkUart(String name) => HierarchyOccurrence( + name: name, + definition: 'UART', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'tx', width: 1), + SignalOccurrence(name: 'rx', width: 1), + SignalOccurrence(name: 'baud_sel', width: 3), + ], + ); + + final ioMux = HierarchyOccurrence( + name: 'io_mux', + definition: 'IOMux', + children: [mkUart('uart0'), mkUart('uart1')], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'sel', width: 2), + SignalOccurrence(name: 'data_muxed', width: 8), + SignalOccurrence(name: 'valid_muxed', width: 1), + ], + ); + + final root = HierarchyOccurrence( + name: 'SoC', + definition: 'SoC', + children: [cpu0, cpu1, memCtrl, ioMux], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'irq0', width: 1), + SignalOccurrence(name: 'irq1', width: 1), + ], + ); + + return BaseHierarchyAdapter.fromTree(root); +} + +void main() { + late HierarchyService svc; + + setUpAll(() { + svc = buildTestHierarchy(); + }); + + // ═══════════════════════════════════════════════════════════════ + // PrefixQuery + // ═══════════════════════════════════════════════════════════════ + + group('PrefixQuery', () { + group('matchOccurrence', () { + test('matches occurrence name containing segment', () { + final q = PrefixQuery('cpu'); + // 'cpu0' contains 'cpu' + expect(q.matchOccurrence('cpu0', 0), equals({1})); + }); + + test('returns empty set when no match', () { + final q = PrefixQuery('mem'); + expect(q.matchOccurrence('cpu0', 0), isEmpty); + }); + + test('past end of segments returns current state', () { + final q = PrefixQuery('cpu'); + // stateIndex == segmentCount → already consumed + expect(q.matchOccurrence('anything', 1), equals({1})); + }); + + test('multi-segment: advances one segment at a time', () { + final q = PrefixQuery('cpu/alu'); + expect(q.matchOccurrence('cpu0', 0), equals({1})); + expect(q.matchOccurrence('alu', 1), equals({2})); + // 'regfile' doesn't match 'alu' + expect(q.matchOccurrence('regfile', 1), isEmpty); + }); + + test('dot separator treated as slash', () { + final q = PrefixQuery('cpu.alu'); + expect(q.segmentCount, equals(2)); + expect(q.matchOccurrence('cpu0', 0), equals({1})); + }); + }); + + group('matchSignal', () { + test('matches signal name with startsWith', () { + final q = PrefixQuery('cpu/clk'); + // At state 1 (after matching 'cpu'), 'clk' starts with 'clk' + expect(q.matchSignal('clk', 1), isTrue); + expect(q.matchSignal('clk_gated', 1), isTrue); + }); + + test('does not match signal for non-last segment', () { + final q = PrefixQuery('cpu/alu/res'); + // At state 1, there are still 2 segments left → only last matches + expect(q.matchSignal('result', 1), isFalse); + // At state 2, this is the last segment + expect(q.matchSignal('result', 2), isTrue); + }); + + test('past end matches any signal', () { + final q = PrefixQuery('cpu'); + expect(q.matchSignal('anything', 1), isTrue); + }); + }); + + group('isComplete', () { + test('complete when stateIndex >= segmentCount', () { + final q = PrefixQuery('cpu/alu'); + expect(q.isComplete(0), isFalse); + expect(q.isComplete(1), isFalse); + expect(q.isComplete(2), isTrue); + expect(q.isComplete(3), isTrue); + }); + }); + + group('isEmpty', () { + test('empty for blank query', () { + expect(PrefixQuery('').isEmpty, isTrue); + expect(PrefixQuery(' ').isEmpty, isTrue); + }); + + test('not empty for real query', () { + expect(PrefixQuery('clk').isEmpty, isFalse); + }); + }); + + group('target property', () { + test('defaults to signals', () { + expect(PrefixQuery('x').target, equals(SearchTarget.signals)); + }); + + test('can be set to occurrences', () { + final q = PrefixQuery('x', target: SearchTarget.occurrences); + expect(q.target, equals(SearchTarget.occurrences)); + }); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // RegexQuery + // ═══════════════════════════════════════════════════════════════ + + group('RegexQuery', () { + group('exact name matching', () { + test('matches exact occurrence name', () { + final q = RegexQuery('SoC/cpu0/alu'); + expect(q.matchOccurrence('SoC', 0), equals({1})); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('alu', 2), equals({3})); + }); + + test('does not match wrong name', () { + final q = RegexQuery('SoC/cpu0'); + expect(q.matchOccurrence('cpu1', 1), isEmpty); + }); + + test('case sensitive', () { + final q = RegexQuery('SoC/cpu0'); + expect(q.matchOccurrence('SoC', 0), equals({1})); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + }); + }); + + group('glob wildcard *', () { + test('star matches any characters', () { + final q = RegexQuery('SoC/cpu*'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('cpu1', 1), equals({2})); + expect(q.matchOccurrence('mem_ctrl', 1), isEmpty); + }); + + test('star at start', () { + final q = RegexQuery('SoC/*_ctrl'); + expect(q.matchOccurrence('mem_ctrl', 1), equals({2})); + expect(q.matchOccurrence('cpu0', 1), isEmpty); + }); + + test('star in middle', () { + final q = RegexQuery('SoC/io_*'); + expect(q.matchOccurrence('io_mux', 1), equals({2})); + expect(q.matchOccurrence('io_ctrl', 1), equals({2})); + expect(q.matchOccurrence('cpu0', 1), isEmpty); + }); + + test('standalone star matches anything', () { + final q = RegexQuery('SoC/*'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('mem_ctrl', 1), equals({2})); + expect(q.matchOccurrence('io_mux', 1), equals({2})); + }); + }); + + group('glob wildcard ?', () { + test('question mark matches one character', () { + final q = RegexQuery('SoC/cpu?'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('cpu1', 1), equals({2})); + // 'cpuXY' is two chars after 'cpu' → no match + expect(q.matchOccurrence('cpuXY', 1), isEmpty); + }); + }); + + group('glob-star ** (cross hierarchy boundaries)', () { + test('** matches zero levels', () { + final q = RegexQuery('SoC/**/alu'); + // ** at index 1 can match zero levels → try index 2 ('alu') + // directly against children of SoC + final states = q.matchOccurrence('alu', 1); + // Should include state 1 (stay at **) and possibly skip to 2 + expect(states, contains(1)); + }); + + test('** matches one or more levels', () { + final q = RegexQuery('SoC/**/clk'); + // ** stays at ** when consuming a node + expect(q.matchOccurrence('cpu0', 1), contains(1)); + expect(q.matchOccurrence('alu', 1), contains(1)); + }); + + test('** followed by exact segment', () { + final q = RegexQuery('SoC/**/alu'); + // At state 1 (**), 'alu' should match both staying and advancing + final states = q.matchOccurrence('alu', 1); + expect(states, contains(1)); // stay at ** + expect(states, contains(3)); // skip ** + match 'alu' → index 3 + }); + + test('isComplete with trailing **', () { + final q = RegexQuery('SoC/**'); + expect(q.isComplete(1), isTrue); // ** can match zero + expect(q.isComplete(2), isTrue); // past end + }); + }); + + group('character classes [...]', () { + test('matches character range', () { + final q = RegexQuery('SoC/**/d[0-9]+'); + // Signal matching + expect(q.matchSignal('d0', 2), isTrue); + expect(q.matchSignal('d1', 2), isTrue); + expect(q.matchSignal('d15', 2), isTrue); + expect(q.matchSignal('clk', 2), isFalse); + }); + + test('fixed character set', () { + final q = RegexQuery('SoC/mem_ctrl/ch[012]'); + expect(q.matchOccurrence('ch0', 2), equals({3})); + expect(q.matchOccurrence('ch1', 2), equals({3})); + expect(q.matchOccurrence('ch2', 2), equals({3})); + expect(q.matchOccurrence('ch3', 2), isEmpty); + }); + }); + + group('alternation (...|...)', () { + test('matches either alternative', () { + final q = RegexQuery('SoC/**/(clk|reset)'); + expect(q.matchSignal('clk', 2), isTrue); + expect(q.matchSignal('reset', 2), isTrue); + expect(q.matchSignal('data', 2), isFalse); + }); + + test('alternation on occurrences', () { + final q = RegexQuery('SoC/(cpu0|cpu1)'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('cpu1', 1), equals({2})); + expect(q.matchOccurrence('mem_ctrl', 1), isEmpty); + }); + }); + + group('regex quantifiers', () { + test('{n,m} repetition', () { + final q = RegexQuery('SoC/**/d[0-9]{1,2}'); + expect(q.matchSignal('d0', 2), isTrue); + expect(q.matchSignal('d15', 2), isTrue); + // 'd123' has 3 digits → no match (anchored) + expect(q.matchSignal('d123', 2), isFalse); + }); + + test('+ one or more', () { + final q = RegexQuery('SoC/**/irq[0-9]+'); + expect(q.matchSignal('irq0', 2), isTrue); + expect(q.matchSignal('irq1', 2), isTrue); + expect(q.matchSignal('irq', 2), isFalse); + }); + }); + + group('matchSignal', () { + test('exact signal name', () { + final q = RegexQuery('SoC/cpu0/clk'); + expect(q.matchSignal('clk', 2), isTrue); + expect(q.matchSignal('reset', 2), isFalse); + }); + + test('glob * on signal', () { + final q = RegexQuery('SoC/cpu0/alu/*'); + expect(q.matchSignal('a', 3), isTrue); + expect(q.matchSignal('result', 3), isTrue); + }); + + test('glob * prefix on signal', () { + final q = RegexQuery('SoC/**/carry_*'); + expect(q.matchSignal('carry_out', 2), isTrue); + expect(q.matchSignal('overflow', 2), isFalse); + }); + + test('** then signal matches all signals when past segments', () { + final q = RegexQuery('SoC/**'); + // At state 1 (**), isComplete is true → match all signals + expect(q.matchSignal('clk', 1), isTrue); + expect(q.matchSignal('anything', 1), isTrue); + }); + + test('signal does not match non-terminal segment', () { + // SoC/cpu0/alu/result — 'result' is segment index 3, last segment + final q = RegexQuery('SoC/cpu0/alu/result'); + // At state 2, there's still 'result' to match → not last-terminal + expect(q.matchSignal('result', 2), isFalse); + // At state 3 it is the last segment + expect(q.matchSignal('result', 3), isTrue); + }); + }); + + group('isComplete', () { + test('complete when past all segments', () { + final q = RegexQuery('SoC/cpu0'); + expect(q.isComplete(0), isFalse); + expect(q.isComplete(1), isFalse); + expect(q.isComplete(2), isTrue); + }); + + test('complete with trailing glob-stars', () { + final q = RegexQuery('SoC/**'); + expect(q.isComplete(0), isFalse); + expect(q.isComplete(1), isTrue); // ** matches zero + }); + + test('not complete with remaining regex segments', () { + final q = RegexQuery('SoC/**/alu'); + expect(q.isComplete(1), isFalse); // ** then 'alu' remains + }); + }); + + group('target property', () { + test('defaults to signals', () { + expect(RegexQuery('x').target, equals(SearchTarget.signals)); + }); + + test('can be set to both', () { + final q = RegexQuery('x', target: SearchTarget.both); + expect(q.target, equals(SearchTarget.both)); + }); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // Factory constructors on HierarchyQuery + // ═══════════════════════════════════════════════════════════════ + + group('HierarchyQuery factories', () { + test('.prefix creates PrefixQuery', () { + final q = HierarchyQuery.prefix('cpu/clk'); + expect(q, isA()); + expect(q.segmentCount, equals(2)); + }); + + test('.regex creates RegexQuery', () { + final q = HierarchyQuery.regex('SoC/**/clk'); + expect(q, isA()); + expect(q.segmentCount, equals(3)); + }); + + test('.prefix with target', () { + final q = HierarchyQuery.prefix('x', target: SearchTarget.occurrences); + expect(q.target, equals(SearchTarget.occurrences)); + }); + + test('.regex with target', () { + final q = HierarchyQuery.regex('x', target: SearchTarget.both); + expect(q.target, equals(SearchTarget.both)); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // Edge cases + // ═══════════════════════════════════════════════════════════════ + + group('Edge cases', () { + test('empty query is isEmpty', () { + expect(HierarchyQuery.prefix('').isEmpty, isTrue); + expect(HierarchyQuery.regex('').isEmpty, isTrue); + expect(HierarchyQuery.prefix(' ').isEmpty, isTrue); + expect(HierarchyQuery.regex(' ').isEmpty, isTrue); + }); + + test('PrefixQuery with only separators', () { + final q = PrefixQuery('///'); + expect(q.segmentCount, equals(0)); + expect(q.isEmpty, isFalse); // raw string isn't blank + expect(q.isComplete(0), isTrue); // no segments to match + }); + + test('RegexQuery single segment', () { + final q = RegexQuery('clk'); + expect(q.segmentCount, equals(1)); + expect(q.matchSignal('clk', 0), isTrue); + expect(q.matchSignal('reset', 0), isFalse); + }); + + test('RegexQuery multiple consecutive glob-stars', () { + final q = RegexQuery('SoC/**/**/clk'); + // Should still work — multiple **'s just redundantly match zero+ + expect(q.isComplete(1), isFalse); // **/** then clk + final states = q.matchOccurrence('cpu0', 1); + expect(states, contains(1)); // stay at first ** + }); + + test('RegexQuery with .* explicit regex', () { + final q = RegexQuery('SoC/**/.*mux.*'); + expect(q.matchOccurrence('io_mux', 2), equals({3})); + expect(q.matchSignal('data_muxed', 2), isTrue); + expect(q.matchSignal('valid_muxed', 2), isTrue); + expect(q.matchSignal('clk', 2), isFalse); + }); + + test('PrefixQuery crossesBoundaries is false', () { + expect(PrefixQuery('x').crossesBoundaries, isFalse); + }); + + test('RegexQuery crossesBoundaries is false (uses ** explicitly)', () { + expect(RegexQuery('SoC/**/clk').crossesBoundaries, isFalse); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // Integration: queries against the real hierarchy via + // HierarchyService (to verify the contract makes sense) + // ═══════════════════════════════════════════════════════════════ + + group('Integration with hierarchy (signal path search)', () { + test('PrefixQuery segments match existing search', () { + // Verify PrefixQuery produces the same segments as + // the existing searchSignalPaths logic. + final q = PrefixQuery('cpu/alu/res'); + final paths = svc.searchSignalPaths('cpu/alu/res'); + // Both cpu0 and cpu1 have ALU with 'result' + expect(paths.length, equals(2)); + for (final p in paths) { + expect(p, contains('result')); + } + // Verify the query matches the same way + expect(q.matchOccurrence('cpu0', 0), isNotEmpty); + expect(q.matchOccurrence('alu', 1), isNotEmpty); + expect(q.matchSignal('result', 2), isTrue); + }); + + test('RegexQuery glob matches existing regex search', () { + // SoC/**/clk should find clk at many levels + final paths = svc.searchSignalPathsRegex('SoC/**/clk'); + // clk exists at: SoC, cpu0, cpu1, cpu0/regfile, cpu1/regfile, + // mem_ctrl, ch0, ch1, ch2, io_mux, uart0, uart1 = 12 total + expect(paths.length, equals(12)); + for (final p in paths) { + expect(p, endsWith('/clk')); + } + }); + + test('RegexQuery character class matches indexed signals', () { + final paths = svc.searchSignalPathsRegex('SoC/**/d[0-9]+'); + // d0, d1, d2, d15 in cpu0/regfile and cpu1/regfile = 8 total + expect(paths.length, equals(8)); + for (final p in paths) { + expect(p, matches(RegExp(r'/d\d+$'))); + } + }); + + test('RegexQuery alternation matches specific signals', () { + final paths = svc.searchSignalPathsRegex('SoC/**/(tx|rx)'); + // tx and rx in uart0 and uart1 = 4 total + expect(paths.length, equals(4)); + }); + + test('RegexQuery ch[0-2] matches channel occurrences', () { + final paths = svc.searchOccurrencePathsRegex('SoC/mem_ctrl/ch[0-2]'); + expect(paths.length, equals(3)); + expect( + paths, + containsAll([ + 'SoC/mem_ctrl/ch0', + 'SoC/mem_ctrl/ch1', + 'SoC/mem_ctrl/ch2', + ])); + }); + + test('RegexQuery *_mux matches occurrence by suffix', () { + final paths = svc.searchOccurrencePathsRegex('SoC/*_mux'); + expect(paths.length, equals(1)); + expect(paths.first, equals('SoC/io_mux')); + }); + + test('RegexQuery .*mux.* matches signals containing mux', () { + final paths = svc.searchSignalPathsRegex('SoC/**/.*mux.*'); + expect(paths, contains('SoC/io_mux/data_muxed')); + expect(paths, contains('SoC/io_mux/valid_muxed')); + }); + + test('PrefixQuery finds irq signals at root', () { + final paths = svc.searchSignalPaths('SoC/irq'); + expect(paths.length, equals(2)); + expect(paths, contains('SoC/irq0')); + expect(paths, contains('SoC/irq1')); + }); + + test('RegexQuery baud_sel across both UARTs', () { + final paths = svc.searchSignalPathsRegex('SoC/**/baud_sel'); + expect(paths.length, equals(2)); + expect(paths, contains('SoC/io_mux/uart0/baud_sel')); + expect(paths, contains('SoC/io_mux/uart1/baud_sel')); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart b/packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart new file mode 100644 index 000000000..bd7801f2a --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart @@ -0,0 +1,505 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_search_controller_test.dart +// Tests for HierarchySearchController. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Minimal hierarchy for testing the controller with a real +/// HierarchyService. +HierarchyOccurrence _buildTestTree() => HierarchyOccurrence( + name: 'Top', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'rst', width: 1), + ], + children: [ + HierarchyOccurrence( + name: 'cpu', + signals: [ + SignalOccurrence(name: 'data_in', width: 8), + SignalOccurrence(name: 'data_out', width: 8), + ], + children: [ + HierarchyOccurrence( + name: 'alu', + signals: [ + SignalOccurrence(name: 'a', width: 16), + SignalOccurrence(name: 'b', width: 16), + SignalOccurrence(name: 'result', width: 16), + ], + ), + ], + ), + HierarchyOccurrence( + name: 'mem', + signals: [ + SignalOccurrence(name: 'addr', width: 32), + ], + ), + ], + ); + +void main() { + late BaseHierarchyAdapter hierarchy; + late HierarchySearchController signalCtrl; + late HierarchySearchController moduleCtrl; + + setUp(() { + hierarchy = BaseHierarchyAdapter.fromTree(_buildTestTree()); + signalCtrl = HierarchySearchController.forSignals(hierarchy); + moduleCtrl = HierarchySearchController.forOccurrences(hierarchy); + }); + + group('HierarchySearchController — signal search', () { + test('starts with empty state', () { + expect(signalCtrl.results, isEmpty); + expect(signalCtrl.selectedIndex, 0); + expect(signalCtrl.hasResults, isFalse); + expect(signalCtrl.counterText, isEmpty); + expect(signalCtrl.currentSelection, isNull); + }); + + test('updateQuery populates results', () { + signalCtrl.updateQuery('clk'); + expect(signalCtrl.hasResults, isTrue); + expect(signalCtrl.results.first.name, 'clk'); + expect(signalCtrl.selectedIndex, 0); + }); + + test('updateQuery with empty string clears results', () { + signalCtrl.updateQuery('clk'); + expect(signalCtrl.hasResults, isTrue); + + signalCtrl.updateQuery(''); + expect(signalCtrl.hasResults, isFalse); + expect(signalCtrl.selectedIndex, 0); + }); + + test('updateQuery resets selectedIndex', () { + signalCtrl + ..updateQuery('data') + ..selectNext(); // index 1 + expect(signalCtrl.selectedIndex, 1); + + signalCtrl.updateQuery('data'); // re-search + expect(signalCtrl.selectedIndex, 0); // reset + }); + + test('normalise converts dots to slashes', () { + signalCtrl.updateQuery('cpu.alu.a'); + expect(signalCtrl.hasResults, isTrue); + expect(signalCtrl.results.first.name, 'a'); + }); + + test('counterText is correct', () { + signalCtrl.updateQuery('data'); + expect(signalCtrl.counterText, '1/${signalCtrl.results.length}'); + + signalCtrl.selectNext(); + expect(signalCtrl.counterText, '2/${signalCtrl.results.length}'); + }); + + test('currentSelection returns the highlighted result', () { + signalCtrl.updateQuery('data'); + final first = signalCtrl.currentSelection; + expect(first, isNotNull); + expect(first!.name, 'data_in'); + + signalCtrl.selectNext(); + expect(signalCtrl.currentSelection!.name, 'data_out'); + }); + + test('selectNext wraps around', () { + signalCtrl.updateQuery('data'); + final count = signalCtrl.results.length; + expect(count, greaterThan(1)); + + for (var i = 0; i < count; i++) { + signalCtrl.selectNext(); + } + expect(signalCtrl.selectedIndex, 0); // wrapped + }); + + test('selectPrevious wraps around', () { + signalCtrl + ..updateQuery('data') + ..selectPrevious(); // wraps from 0 → last + expect(signalCtrl.selectedIndex, signalCtrl.results.length - 1); + }); + + test('selectNext/selectPrevious no-op when empty', () { + signalCtrl.selectNext(); + expect(signalCtrl.selectedIndex, 0); + signalCtrl.selectPrevious(); + expect(signalCtrl.selectedIndex, 0); + }); + + test('clear resets everything', () { + signalCtrl + ..updateQuery('data') + ..selectNext(); + expect(signalCtrl.hasResults, isTrue); + expect(signalCtrl.selectedIndex, greaterThan(0)); + + signalCtrl.clear(); + expect(signalCtrl.results, isEmpty); + expect(signalCtrl.selectedIndex, 0); + expect(signalCtrl.currentSelection, isNull); + }); + + test('no results for non-matching query', () { + signalCtrl.updateQuery('xyz_no_match'); + expect(signalCtrl.hasResults, isFalse); + expect(signalCtrl.counterText, isEmpty); + }); + + test('plain query uses prefix match, not substring', () { + // 'a' should match signals starting with 'a' (addr, a), + // but NOT signals that merely contain 'a' (data_in, data_out). + signalCtrl.updateQuery('a'); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, contains('a')); // Top/cpu/alu/a + expect(names, contains('addr')); // Top/mem/addr + expect(names, isNot(contains('data_in'))); // 'a' is not a prefix + expect(names, isNot(contains('data_out'))); + }); + + test('glob * pattern routes to regex search', () { + // 'cpu/*_out' should match signals ending in '_out' under cpu + // (single-segment globs like '*_out' only search root-level; + // use a path segment to target a child module). + signalCtrl.updateQuery('cpu/*_out'); + expect(signalCtrl.hasResults, isTrue); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, contains('data_out')); + expect(names, isNot(contains('data_in'))); + }); + + test('glob * at end matches prefix', () { + signalCtrl.updateQuery('cpu/data*'); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, containsAll(['data_in', 'data_out'])); + }); + + test('glob * at root matches top-level signals', () { + // Single-segment glob only searches root module signals. + signalCtrl.updateQuery('*st'); + expect(signalCtrl.hasResults, isTrue); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, contains('rst')); + }); + }); + + group('HierarchySearchController — module search', () { + test('finds modules by name', () { + moduleCtrl.updateQuery('cpu'); + expect(moduleCtrl.hasResults, isTrue); + expect(moduleCtrl.results.first.occurrence.name, 'cpu'); + }); + + test('finds nested modules', () { + moduleCtrl.updateQuery('alu'); + expect(moduleCtrl.hasResults, isTrue); + expect(moduleCtrl.results.first.occurrence.name, 'alu'); + }); + + test('counterText and selection work for modules', () { + moduleCtrl.updateQuery('m'); // matches 'mem', possibly others + expect(moduleCtrl.hasResults, isTrue); + expect(moduleCtrl.counterText, isNotEmpty); + expect(moduleCtrl.currentSelection, isNotNull); + }); + }); + + group('scrollOffsetToReveal', () { + test('returns null when item is visible', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 2, + itemHeight: 48, + viewportHeight: 300, + currentOffset: 0, + ); + // item at 96..144, viewport 0..300 → visible + expect(offset, isNull); + }); + + test('scrolls up when item is above viewport', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 0, + itemHeight: 48, + viewportHeight: 300, + currentOffset: 100, + ); + // item at 0..48, viewport starts at 100 → need to scroll to 0 + expect(offset, 0.0); + }); + + test('scrolls down when item is below viewport', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 10, + itemHeight: 48, + viewportHeight: 200, + currentOffset: 0, + ); + // item at 480..528, viewport 0..200 → scroll to 528-200 = 328 + expect(offset, 328.0); + }); + + test('returns null when item is at bottom edge', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 4, + itemHeight: 50, + viewportHeight: 250, + currentOffset: 0, + ); + // item at 200..250, viewport 0..250 → exactly visible + expect(offset, isNull); + }); + }); + + // ------------------------------------------------------------------ + // VCD-style dot-separated paths + // ------------------------------------------------------------------ + group('VCD dot-separated paths', () { + late BaseHierarchyAdapter vcdHierarchy; + + setUp(() { + // VCD/FST files produce dot-separated IDs like "testbench.childA.clk" + vcdHierarchy = BaseHierarchyAdapter.fromTree( + HierarchyOccurrence( + name: 'testbench', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'rst', width: 1), + ], + children: [ + HierarchyOccurrence( + name: 'childA', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'data', width: 8), + ], + children: [ + HierarchyOccurrence( + name: 'sub', + signals: [ + SignalOccurrence(name: 'out', width: 4), + ], + ), + ], + ), + HierarchyOccurrence( + name: 'childB', + signals: [ + SignalOccurrence(name: 'enable', width: 1), + ], + ), + ], + ), + ); + }); + + test('searchSignalPaths with slash query finds dot-separated signal', () { + // User types "childA/clk" — walker normalises to hierarchySeparator + final results = vcdHierarchy.searchSignalPaths('childA/clk'); + expect(results, isNotEmpty); + expect(results, contains('testbench/childA/clk')); + }); + + test('searchSignalPaths with slash query finds deep signal', () { + final results = vcdHierarchy.searchSignalPaths('childA/sub/out'); + expect(results, isNotEmpty); + expect(results, contains('testbench/childA/sub/out')); + }); + + test('searchSignals with slash query finds dot-separated signal', () { + final results = vcdHierarchy.searchSignals('childA/clk'); + expect(results, isNotEmpty); + expect(results.first.signal!.path(), 'testbench/childA/clk'); + }); + + test('searchModules with slash query finds dot-separated module', () { + final results = vcdHierarchy.searchOccurrences('childA'); + expect(results, isNotEmpty); + expect(results.first.occurrence.path(), 'testbench/childA'); + }); + + test('searchSignalPaths with dot query still works', () { + // Dots in query are treated as separators too + final results = vcdHierarchy.searchSignalPaths('childA.clk'); + expect(results, isNotEmpty); + expect(results, contains('testbench/childA/clk')); + }); + + test('searchSignals with glob on dot-separated paths', () { + // Glob wildcard should work across dot-separated IDs + final results = vcdHierarchy.searchSignals('**/clk'); + expect(results.length, greaterThanOrEqualTo(2)); + final ids = results.map((r) => r.signal!.path()).toSet(); + expect(ids, contains('testbench/clk')); + expect(ids, contains('testbench/childA/clk')); + }); + + test('searchSignals with single segment on dot-separated paths', () { + // Single segment search should use startsWith + final results = vcdHierarchy.searchSignals('ena'); + expect(results, isNotEmpty); + expect(results.first.signal!.path(), 'testbench/childB/enable'); + }); + + test('controller forSignals works with dot-separated hierarchy', () { + final ctrl = + HierarchySearchController.forSignals(vcdHierarchy) + ..updateQuery('childA/clk'); + expect(ctrl.results, isNotEmpty); + expect(ctrl.results.first.signal!.path(), 'testbench/childA/clk'); + }); + }); + + // ------------------------------------------------------------------ + // DevTools flow — hierarchy with local signal IDs + // ------------------------------------------------------------------ + group('DevTools flow — local signal IDs → BaseHierarchyAdapter.fromTree', () { + late BaseHierarchyAdapter rohdHierarchy; + late HierarchySearchController rohdSignalCtrl; + late HierarchySearchController rohdModuleCtrl; + + setUp(() { + // Build a tree with local signal IDs (not full paths) — this is the key + // difference from the VCD path where IDs are full paths. + final alu = HierarchyOccurrence( + name: 'alu', + signals: [ + SignalOccurrence( + name: 'a', + width: 16, + direction: 'input', + ), + SignalOccurrence( + name: 'b', + width: 16, + direction: 'input', + ), + SignalOccurrence( + name: 'result', + width: 16, + direction: 'output', + ), + ], + ); + final cpu = HierarchyOccurrence( + name: 'cpu', + children: [alu], + signals: [ + SignalOccurrence( + name: 'data_in', + width: 8, + direction: 'input', + ), + SignalOccurrence( + name: 'data_out', + width: 8, + direction: 'output', + ), + ], + ); + final mem = HierarchyOccurrence( + name: 'mem', + signals: [ + SignalOccurrence( + name: 'addr', + width: 32, + direction: 'input', + ), + ], + ); + final root = HierarchyOccurrence( + name: 'Top', + children: [cpu, mem], + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'rst', + width: 1, + direction: 'input', + ), + ], + )..buildAddresses(); + rohdHierarchy = BaseHierarchyAdapter.fromTree(root); + rohdSignalCtrl = HierarchySearchController.forSignals(rohdHierarchy); + rohdModuleCtrl = HierarchySearchController.forOccurrences(rohdHierarchy); + }); + + test('signal IDs are local (not full paths)', () { + final rootSigs = rohdHierarchy.root.signals; + final clk = rootSigs.firstWhere((s) => s.name == 'clk'); + expect(clk.name, 'clk'); + expect(clk.path(), 'Top/clk'); + }); + + test('updateQuery finds signals despite local IDs', () { + rohdSignalCtrl.updateQuery('clk'); + expect(rohdSignalCtrl.hasResults, isTrue); + expect(rohdSignalCtrl.results.first.name, 'clk'); + }); + + test('signalByAddress works with full path', () { + final addr = + OccurrenceAddress.tryFromPathname('Top/clk', rohdHierarchy.root); + final result = rohdHierarchy.signalByAddress(addr!); + expect(result, isNotNull); + expect(result!.name, 'clk'); + }); + + test('signalByAddress works with nested path', () { + final addr = OccurrenceAddress.tryFromPathname( + 'Top/cpu/alu/a', rohdHierarchy.root); + final result = rohdHierarchy.signalByAddress(addr!); + expect(result, isNotNull); + expect(result!.name, 'a'); + }); + + test('path-based search narrows to module', () { + rohdSignalCtrl.updateQuery('cpu/data'); + expect(rohdSignalCtrl.hasResults, isTrue); + final names = rohdSignalCtrl.results.map((r) => r.name).toSet(); + expect(names, containsAll(['data_in', 'data_out'])); + }); + + test('glob search works', () { + rohdSignalCtrl.updateQuery('**/a'); + expect(rohdSignalCtrl.hasResults, isTrue); + final names = rohdSignalCtrl.results.map((r) => r.name).toSet(); + expect(names, contains('a')); + }); + + test('module search works', () { + rohdModuleCtrl.updateQuery('alu'); + expect(rohdModuleCtrl.hasResults, isTrue); + expect(rohdModuleCtrl.results.first.occurrence.name, 'alu'); + }); + + test('search results match VCD-style tree results', () { + // The SAME queries should produce the same signal NAMES as + // the manually-built tree (VCD path), even though signal IDs differ. + rohdSignalCtrl.updateQuery('data'); + final rohdNames = rohdSignalCtrl.results.map((r) => r.name).toSet(); + + signalCtrl.updateQuery('data'); + final vcdNames = signalCtrl.results.map((r) => r.name).toSet(); + + expect(rohdNames, vcdNames, + reason: 'Same query should find same signals regardless of source'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/module_search_test.dart b/packages/rohd_hierarchy/test/module_search_test.dart new file mode 100644 index 000000000..9339492cf --- /dev/null +++ b/packages/rohd_hierarchy/test/module_search_test.dart @@ -0,0 +1,325 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_search_test.dart +// Tests for module tree search functionality using hierarchy API. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('Module Tree Search - HierarchyService', () { + late HierarchyOccurrence root; + late HierarchyService hierarchy; + + setUpAll(() { + // Create a test hierarchy + // Top + // CPU (2 children) + // ALU + // Decoder + // Memory + // ControlUnit + + final alu = HierarchyOccurrence( + name: 'ALU', + ); + + final decoder = HierarchyOccurrence( + name: 'Decoder', + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu, decoder], + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + ); + + final controlUnit = HierarchyOccurrence( + name: 'ControlUnit', + ); + + root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory, controlUnit], + ); + + // Use BaseHierarchyAdapter.fromTree to convert to HierarchyService + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('root node is accessible', () { + expect(hierarchy.root.name, equals('Top')); + expect(hierarchy.root.isPrimitive, isFalse); + }); + + test('children of root are accessible', () { + final children = hierarchy.root.children; + expect(children, isNotEmpty); + expect(children.length, equals(3)); + expect(children.any((c) => c.name == 'CPU'), isTrue); + }); + + test('searchNodePaths finds CPU module', () { + final results = hierarchy.searchOccurrencePaths('CPU'); + expect(results, isNotEmpty, + reason: 'Should find CPU module by simple name'); + expect(results.any((path) => path.contains('CPU')), isTrue); + }); + + test('searchNodePaths finds ALU with hierarchical query', () { + final results = hierarchy.searchOccurrencePaths('CPU/ALU'); + expect(results, isNotEmpty, + reason: 'Should find ALU with hierarchical path'); + expect(results.any((path) => path.contains('ALU')), isTrue); + }); + + test('searchNodePaths works with dot notation', () { + final results = hierarchy.searchOccurrencePaths('Top.CPU.ALU'); + expect(results, isNotEmpty, reason: 'Should find ALU with dot notation'); + expect(results.any((path) => path.contains('Top/CPU/ALU')), isTrue); + }); + + test('searchNodePaths limits results', () { + final results = hierarchy.searchOccurrencePaths('', limit: 2); + expect(results.length, lessThanOrEqualTo(2), + reason: 'Should respect limit parameter'); + }); + + test('searchModules returns ModuleSearchResult objects', () { + final results = hierarchy.searchOccurrences('Memory'); + expect(results, isNotEmpty); + expect(results.first, isA()); + expect(results.first.name, equals('Memory')); + expect(results.first.isModule, isTrue); + }); + + test('searchModules result contains full metadata', () { + final results = hierarchy.searchOccurrences('Decoder'); + expect(results, isNotEmpty); + final result = results.first; + expect(result.occurrenceId, contains('Decoder')); + expect(result.path, isNotEmpty); + expect(result.path.last, equals('Decoder')); + expect(result.occurrence, isNotNull); + }); + + test('searchNodePaths returns empty for non-matching query', () { + final results = hierarchy.searchOccurrencePaths('nonexistent'); + expect(results, isEmpty, + reason: 'Should return empty list for non-matching query'); + }); + + test('searchNodePaths returns empty for empty query', () { + final results = hierarchy.searchOccurrencePaths(''); + expect(results, isEmpty, + reason: 'Should return empty list for empty query'); + }); + + test('searchModules finds modules at different depths', () { + // Should find both Top and Top/CPU + final results = hierarchy.searchOccurrences('Top'); + expect(results.length, greaterThanOrEqualTo(1)); + expect(results.any((r) => r.name == 'Top'), isTrue); + }); + }); + + group('Module Search - Hierarchical Matching', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Create a deeper hierarchy to test matching + // Design + // ProcessingUnit + // DataPath + // Multiplier + // Adder + // Controller + // Memory + // RAM + // Cache + + final multiplier = HierarchyOccurrence( + name: 'Multiplier', + ); + + final adder = HierarchyOccurrence( + name: 'Adder', + ); + + final dataPath = HierarchyOccurrence( + name: 'DataPath', + children: [multiplier, adder], + ); + + final controller = HierarchyOccurrence( + name: 'Controller', + ); + + final processingUnit = HierarchyOccurrence( + name: 'ProcessingUnit', + children: [dataPath, controller], + ); + + final ram = HierarchyOccurrence( + name: 'RAM', + ); + + final cache = HierarchyOccurrence( + name: 'Cache', + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + children: [ram, cache], + ); + + final root = HierarchyOccurrence( + name: 'Design', + children: [processingUnit, memory], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('single segment matches at any level', () { + final results = hierarchy.searchOccurrencePaths('Multiplier'); + expect(results, isNotEmpty, + reason: 'Should find Multiplier even without full path'); + expect(results.any((r) => r.endsWith('Multiplier')), isTrue); + }); + + test('two segment path matches correctly', () { + final results = hierarchy.searchOccurrencePaths('DataPath/Multiplier'); + expect(results.any((r) => r.contains('DataPath/Multiplier')), isTrue, + reason: 'Should find Multiplier under DataPath'); + }); + + test('full hierarchical path matches precisely', () { + final results = + hierarchy.searchOccurrencePaths('ProcessingUnit/DataPath/Adder'); + expect(results.any((r) => r.contains('ProcessingUnit/DataPath/Adder')), + isTrue, + reason: 'Should find Adder with full hierarchical path'); + }); + + test('partial name matching works', () { + final results1 = hierarchy.searchOccurrencePaths('Path'); + expect(results1.any((r) => r.contains('DataPath')), isTrue, + reason: 'Should match partial "path" in DataPath'); + + final results2 = hierarchy.searchOccurrencePaths('Unit'); + expect(results2.any((r) => r.contains('ProcessingUnit')), isTrue, + reason: 'Should match partial "unit" in ProcessingUnit'); + }); + }); + + group('Module Search - Integration with Tree Filtering', () { + late HierarchyOccurrence root; + + setUpAll(() { + final alu = HierarchyOccurrence( + name: 'ALU', + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu], + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + ); + + root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory], + ); + }); + + test('hierarchical filtering shows root when descendant matches', () { + final matchesSearch = _filterNodeRecursive(root, 'alu'); + expect(matchesSearch, isTrue, + reason: 'Root should be shown because descendant matches'); + }); + + test('hierarchical filtering shows parent of matching child', () { + final cpuNode = root.children.first; + final cpuMatches = _filterNodeRecursive(cpuNode, 'alu'); + expect(cpuMatches, isTrue, + reason: 'CPU should be shown because child ALU matches'); + }); + + test('hierarchical filtering hides node without matching descendants', () { + final memoryNode = root.children.last; + final memoryMatches = _filterNodeRecursive(memoryNode, 'alu'); + expect(memoryMatches, isFalse, + reason: 'Memory should be hidden because no ALU descendant'); + }); + + test('path separator search shows root for hierarchical match', () { + final matchesSearch = _filterNodeRecursive(root, 'cpu/alu'); + expect(matchesSearch, isTrue, + reason: 'Root should be shown for hierarchical search'); + }); + + test('path separator search shows matching parent', () { + final cpuNode = root.children.first; + final cpuMatches = _filterNodeRecursive(cpuNode, 'cpu/alu'); + expect(cpuMatches, isTrue, + reason: 'CPU should be shown for hierarchical search'); + }); + + test('path separator search hides non-matching subtree', () { + final memoryNode = root.children.last; + final memoryMatches = _filterNodeRecursive(memoryNode, 'cpu/alu'); + expect(memoryMatches, isFalse, + reason: 'Memory should be hidden for non-matching path'); + }); + }); +} + +/// Helper function to simulate tree filtering with hierarchical search. +/// Matches query against node name using hierarchical logic. +bool _filterNodeRecursive(HierarchyOccurrence node, String query) { + final queryParts = query + .replaceAll('.', '/') + .toLowerCase() + .split('/') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + return _matchesHierarchicalQuery(node, queryParts, 0); +} + +bool _matchesHierarchicalQuery( + HierarchyOccurrence node, List queryParts, int queryIdx) { + if (queryIdx >= queryParts.length) { + return true; + } + + final currentQueryPart = queryParts[queryIdx].toLowerCase(); + final nodeName = node.name.toLowerCase(); + + final matched = nodeName.contains(currentQueryPart); + final nextQueryIdx = matched ? queryIdx + 1 : queryIdx; + + if (nextQueryIdx >= queryParts.length) { + return true; + } + + for (final child in node.children) { + if (_matchesHierarchicalQuery(child, queryParts, nextQueryIdx)) { + return true; + } + } + + return false; +} diff --git a/packages/rohd_hierarchy/test/occurrence_address_test.dart b/packages/rohd_hierarchy/test/occurrence_address_test.dart new file mode 100644 index 000000000..aafde9050 --- /dev/null +++ b/packages/rohd_hierarchy/test/occurrence_address_test.dart @@ -0,0 +1,335 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_address_test.dart +// Unit tests for OccurrenceAddress class. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('OccurrenceAddress', () { + test('child() appends module index', () { + final addr = OccurrenceAddress.root.child(0).child(2).child(4); + expect(addr.path, equals([0, 2, 4])); + }); + + test('signal() appends signal index', () { + final addr = const OccurrenceAddress([0, 1]).signal(5); + expect(addr.path, equals([0, 1, 5])); + }); + + test('equality and hashcode work correctly', () { + const addr1 = OccurrenceAddress([0, 2, 4]); + const addr2 = OccurrenceAddress([0, 2, 4]); + const addr3 = OccurrenceAddress([0, 2, 5]); + + expect(addr1, equals(addr2)); + expect(addr1.hashCode, equals(addr2.hashCode)); + expect(addr1, isNot(equals(addr3))); + expect(addr1.hashCode, isNot(equals(addr3.hashCode))); + }); + + test('toString() returns debug string', () { + expect(OccurrenceAddress.root.toString(), equals('[ROOT]')); + expect(const OccurrenceAddress([0, 2, 4]).toString(), equals('[0.2.4]')); + }); + + test('toDotString() returns dot-separated path', () { + expect(OccurrenceAddress.root.toDotString(), equals('')); + expect(const OccurrenceAddress([0]).toDotString(), equals('0')); + expect(const OccurrenceAddress([0, 2, 4]).toDotString(), equals('0.2.4')); + expect( + const OccurrenceAddress([10, 200]).toDotString(), equals('10.200')); + }); + + test('fromDotString() parses dot-separated path', () { + expect( + OccurrenceAddress.fromDotString(''), equals(OccurrenceAddress.root)); + expect(OccurrenceAddress.fromDotString('0'), + equals(const OccurrenceAddress([0]))); + expect(OccurrenceAddress.fromDotString('0.2.4'), + equals(const OccurrenceAddress([0, 2, 4]))); + expect(OccurrenceAddress.fromDotString('10.200'), + equals(const OccurrenceAddress([10, 200]))); + }); + + test('toDotString/fromDotString round-trip', () { + final testCases = [ + OccurrenceAddress.root, + const OccurrenceAddress([0]), + const OccurrenceAddress([5, 10, 15]), + const OccurrenceAddress([0, 0, 0]), + const OccurrenceAddress([255]), + const OccurrenceAddress([0, 1, 2, 3, 4, 5]), + ]; + for (final original in testCases) { + final dot = original.toDotString(); + final restored = OccurrenceAddress.fromDotString(dot); + expect(restored, equals(original), reason: 'Failed for $original'); + } + }); + }); + + group('OccurrenceAddress with HierarchyNode integration', () { + late HierarchyOccurrence root; + + setUp(() { + // Build a simple tree structure + final child0 = HierarchyOccurrence( + name: 'child_0', + signals: [ + SignalOccurrence( + name: 'sig0', + width: 1, + ), + SignalOccurrence( + name: 'sig1', + width: 8, + ), + ], + ); + + final grandchild = HierarchyOccurrence( + name: 'grandchild_0', + signals: [ + SignalOccurrence( + name: 'sig0', + width: 1, + ), + ], + ); + + final child1 = HierarchyOccurrence( + name: 'child_1', + signals: [ + SignalOccurrence( + name: 'sig0', + width: 4, + ), + ], + ); + + child0.children.add(grandchild); + + root = HierarchyOccurrence( + name: 'root', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + ), + ], + children: [child0, child1], + ) + // Build addresses for all nodes + ..buildAddresses(); + }); + + test('buildAddresses assigns address to root', () { + expect(root.address, equals(OccurrenceAddress.root)); + }); + + test('buildAddresses assigns addresses to all nodes', () { + expect(root.children[0].address, equals(const OccurrenceAddress([0]))); + expect(root.children[1].address, equals(const OccurrenceAddress([1]))); + expect(root.children[0].children[0].address, + equals(const OccurrenceAddress([0, 0]))); + }); + + test('buildAddresses assigns addresses to all signals', () { + // Root signals + expect(root.signals[0].address, equals(const OccurrenceAddress([0]))); + + // Child signals + expect(root.children[0].signals[0].address, + equals(const OccurrenceAddress([0, 0]))); + expect(root.children[0].signals[1].address, + equals(const OccurrenceAddress([0, 1]))); + + // Grandchild signals + expect(root.children[0].children[0].signals[0].address, + equals(const OccurrenceAddress([0, 0, 0]))); + }); + }); + + group('HierarchyOccurrence.parent', () { + test('parent is null for root', () { + final root = HierarchyOccurrence(name: 'Top')..buildAddresses(); + expect(root.parent, isNull); + }); + + test('parent is set for child nodes after buildAddresses', () { + final child = HierarchyOccurrence(name: 'sub'); + final root = HierarchyOccurrence(name: 'Top', children: [child]) + ..buildAddresses(); + expect(child.parent, same(root)); + expect(child.path(), 'Top/sub'); + }); + }); + + group('HierarchyOccurrence.definition', () { + test('type is null when not provided', () { + final n = HierarchyOccurrence(name: 'a'); + expect(n.definition, isNull); + }); + + test('type is stored when provided', () { + final n = HierarchyOccurrence(name: 'a', definition: 'Counter'); + expect(n.definition, 'Counter'); + }); + }); + + group('isPrimitive on nodes', () { + test('default isPrimitive is false', () { + final n = HierarchyOccurrence(name: 'sub'); + expect(n.isPrimitive, isFalse); + }); + }); + + group('buildAddresses ports-first ordering', () { + test('ports get lower signal indices than internal signals', () { + final root = HierarchyOccurrence( + name: 'Top', + signals: [ + SignalOccurrence(name: 'internal_a', width: 8), + SignalOccurrence( + name: 'clk', width: 1, direction: 'input', portIndex: 0), + SignalOccurrence(name: 'internal_b', width: 4), + SignalOccurrence( + name: 'out', width: 8, direction: 'output', portIndex: 1), + ], + )..buildAddresses(); + + final byName = {for (final s in root.signals) s.name: s}; + + // Ports should get indices 0 and 1 + expect(byName['clk']!.address, equals(const OccurrenceAddress([0]))); + expect(byName['out']!.address, equals(const OccurrenceAddress([1]))); + + // Internal signals get indices 2 and 3 + expect( + byName['internal_a']!.address, equals(const OccurrenceAddress([2]))); + expect( + byName['internal_b']!.address, equals(const OccurrenceAddress([3]))); + }); + + test('portIndex matches signal address index', () { + final root = HierarchyOccurrence( + name: 'Mod', + signals: [ + SignalOccurrence( + name: 'a', width: 1, direction: 'input', portIndex: 0), + SignalOccurrence( + name: 'b', width: 1, direction: 'input', portIndex: 1), + SignalOccurrence( + name: 'y', width: 1, direction: 'output', portIndex: 2), + SignalOccurrence(name: 'net0', width: 1), + ], + )..buildAddresses(); + + for (final s in root.signals) { + if (s.isPort) { + // portIndex should equal the last element of the address path + expect(s.address!.path.last, equals(s.portIndex), + reason: '${s.name}: portIndex=${s.portIndex} ' + 'but address index=${s.address!.path.last}'); + } + } + }); + + test('portCount returns correct count', () { + final occ = HierarchyOccurrence( + name: 'X', + signals: [ + SignalOccurrence( + name: 'a', width: 1, direction: 'input', portIndex: 0), + SignalOccurrence(name: 'b', width: 1), + SignalOccurrence( + name: 'c', width: 1, direction: 'output', portIndex: 1), + ], + ); + expect(occ.portCount, equals(2)); + }); + + test('all-ports occurrence: indices match list order', () { + final occ = HierarchyOccurrence( + name: 'Buf', + signals: [ + SignalOccurrence( + name: 'in', width: 8, direction: 'input', portIndex: 0), + SignalOccurrence( + name: 'out', width: 8, direction: 'output', portIndex: 1), + ], + )..buildAddresses(); + + expect(occ.signals[0].address, equals(const OccurrenceAddress([0]))); + expect(occ.signals[1].address, equals(const OccurrenceAddress([1]))); + }); + + test('all-internal occurrence: indices unchanged', () { + final occ = HierarchyOccurrence( + name: 'Internal', + signals: [ + SignalOccurrence(name: 'x', width: 1), + SignalOccurrence(name: 'y', width: 1), + ], + )..buildAddresses(); + + expect(occ.signals[0].address, equals(const OccurrenceAddress([0]))); + expect(occ.signals[1].address, equals(const OccurrenceAddress([1]))); + }); + + test('nested: ports-first ordering applies at every level', () { + final child = HierarchyOccurrence( + name: 'sub', + signals: [ + SignalOccurrence(name: 'net', width: 1), + SignalOccurrence( + name: 'p', width: 1, direction: 'input', portIndex: 0), + ], + ); + final root = HierarchyOccurrence( + name: 'Top', + children: [child], + signals: [ + SignalOccurrence(name: 'net_top', width: 1), + SignalOccurrence( + name: 'clk', width: 1, direction: 'input', portIndex: 0), + ], + )..buildAddresses(); + + // Root: clk (port) at 0, net_top (internal) at 1 + final rootByName = {for (final s in root.signals) s.name: s}; + expect(rootByName['clk']!.address!.path.last, equals(0)); + expect(rootByName['net_top']!.address!.path.last, equals(1)); + + // Child: p (port) at 0, net (internal) at 1 + final childByName = {for (final s in child.signals) s.name: s}; + expect(childByName['p']!.address!.path.last, equals(0)); + expect(childByName['net']!.address!.path.last, equals(1)); + }); + }); + + group('SignalOccurrence.portIndex', () { + test('portIndex is null for internal signals', () { + final s = SignalOccurrence(name: 'net', width: 1); + expect(s.portIndex, isNull); + expect(s.isPort, isFalse); + }); + + test('portIndex is set for port signals', () { + final s = SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + portIndex: 3, + ); + expect(s.portIndex, equals(3)); + expect(s.isPort, isTrue); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/regex_search_test.dart b/packages/rohd_hierarchy/test/regex_search_test.dart new file mode 100644 index 000000000..51f134f21 --- /dev/null +++ b/packages/rohd_hierarchy/test/regex_search_test.dart @@ -0,0 +1,546 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// regex_search_test.dart +// Tests for regex-based hierarchy search. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('Regex search - HierarchyService', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Build a test hierarchy: + // + // Top + // CPU + // ALU signals: [a, b, result, carry_out] + // Decoder signals: [opcode, enable] + // RegFile signals: [clk, reset, d0, d1, d2, d15] + // Memory + // Cache signals: [clk, addr, data, hit] + // DRAM signals: [clk, cas, ras] + // IO + // UART signals: [clk, tx, rx] + // signals (Top): [clk, reset] + + final alu = HierarchyOccurrence( + name: 'ALU', + signals: [ + SignalOccurrence(name: 'a', width: 8), + SignalOccurrence(name: 'b', width: 8), + SignalOccurrence(name: 'result', width: 8), + SignalOccurrence(name: 'carry_out', width: 1), + ], + ); + + final decoder = HierarchyOccurrence( + name: 'Decoder', + signals: [ + SignalOccurrence(name: 'opcode', width: 4), + SignalOccurrence(name: 'enable', width: 1), + ], + ); + + final regFile = HierarchyOccurrence( + name: 'RegFile', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'd0', width: 8), + SignalOccurrence(name: 'd1', width: 8), + SignalOccurrence(name: 'd2', width: 8), + SignalOccurrence(name: 'd15', width: 8), + ], + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu, decoder, regFile], + ); + + final cache = HierarchyOccurrence( + name: 'Cache', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'addr', width: 16), + SignalOccurrence(name: 'data', width: 32), + SignalOccurrence(name: 'hit', width: 1), + ], + ); + + final dram = HierarchyOccurrence( + name: 'DRAM', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'cas', width: 1), + SignalOccurrence(name: 'ras', width: 1), + ], + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + children: [cache, dram], + ); + + final uart = HierarchyOccurrence( + name: 'UART', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'tx', width: 1), + SignalOccurrence(name: 'rx', width: 1), + ], + ); + + final io = HierarchyOccurrence( + name: 'IO', + children: [uart], + ); + + final root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory, io], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'data_m', width: 8), + SignalOccurrence(name: 'addr_m', width: 16), + SignalOccurrence(name: 'flag_m', width: 1), + ], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + // ── Exact match ── + + test('exact path matches single signal', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/ALU/result'); + expect(results, contains('Top/CPU/ALU/result')); + expect(results.length, 1); + }); + + test('dot in regex pattern is treated as regex metachar, not separator', + () { + // In regex mode, `.` is NOT a hierarchy separator — only `/` is. + // `Top.CPU` is a single segment meaning "Top" + any char + "CPU". + final results = hierarchy.searchSignalPathsRegex('Top.CPU.ALU.result'); + // No match because the hierarchy root is "Top", not "Top.CPU.ALU" + expect(results, isEmpty); + }); + + // ── Wildcard at one level ── + + test('.* matches all signals in a module', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/ALU/.*'); + expect( + results, + containsAll([ + 'Top/CPU/ALU/a', + 'Top/CPU/ALU/b', + 'Top/CPU/ALU/result', + 'Top/CPU/ALU/carry_out', + ])); + expect(results.length, 4); + }); + + test('.* matches all children at a module level', () { + final results = hierarchy.searchSignalPathsRegex('Top/.*/clk'); + // Should match CPU/RegFile/clk but not deeper (** would be needed + // for that). .* represents any single-level child of Top. + // Top has children CPU, Memory, IO — none of them have clk directly + // (Top's own signals aren't "children"). Actually let's check: + // Top/.*/clk means: Top / (any child) / clk as signal + // That doesn't match because clk is in deeper modules. + // This should return empty for signals one level below Top. + expect(results, isEmpty); + }); + + test('.* matches modules at one level for signal search', () { + // Top/CPU/.*/clk — matches ALU, Decoder, RegFile; only RegFile has clk + final results = hierarchy.searchSignalPathsRegex('Top/CPU/.*/clk'); + expect(results, contains('Top/CPU/RegFile/clk')); + expect(results.length, 1); + }); + + // ── Glob-star ** ── + + test('** matches signals at any depth', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/clk'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/clk', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + 'Top/IO/UART/clk', + ])); + // Top's own clk is also accessible through ** matching zero levels + expect(results, contains('Top/clk')); + }); + + test('** at beginning matches everything', () { + final results = hierarchy.searchSignalPathsRegex('**/clk'); + // All clk signals anywhere + expect(results.length, greaterThanOrEqualTo(5)); + expect( + results, + containsAll([ + 'Top/clk', + 'Top/CPU/RegFile/clk', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + 'Top/IO/UART/clk', + ])); + }); + + test('** between levels matches across boundaries', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/**/d0'); + expect(results, contains('Top/CPU/RegFile/d0')); + expect(results.length, 1); + }); + + test('** with regex signal pattern', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/d[0-9]+'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/d0', + 'Top/CPU/RegFile/d1', + 'Top/CPU/RegFile/d2', + 'Top/CPU/RegFile/d15', + ])); + expect(results.length, 4); + }); + + // ── Regex character classes ── + + test('character class in signal name', () { + final results = + hierarchy.searchSignalPathsRegex('Top/CPU/RegFile/d[0-2]'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/d0', + 'Top/CPU/RegFile/d1', + 'Top/CPU/RegFile/d2', + ])); + expect(results, isNot(contains('Top/CPU/RegFile/d15'))); + }); + + // ── Alternation ── + + test('alternation in signal name', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/(?:clk|reset)'); + expect( + results, + containsAll([ + 'Top/clk', + 'Top/reset', + 'Top/CPU/RegFile/clk', + 'Top/CPU/RegFile/reset', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + 'Top/IO/UART/clk', + ])); + expect(results.length, 7); + }); + + test('alternation in module name', () { + final results = hierarchy.searchSignalPathsRegex('Top/(CPU|IO)/.*/clk'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/clk', + 'Top/IO/UART/clk', + ])); + }); + + // ── Module search ── + + test('searchOccurrencePathsRegex finds modules', () { + final results = hierarchy.searchOccurrencePathsRegex('Top/CPU/.*'); + expect( + results, + containsAll([ + 'Top/CPU/ALU', + 'Top/CPU/Decoder', + 'Top/CPU/RegFile', + ])); + }); + + test('searchOccurrencePathsRegex with **', () { + final results = hierarchy.searchOccurrencePathsRegex('Top/**/DRAM'); + expect(results, contains('Top/Memory/DRAM')); + }); + + // ── Enriched results ── + + test('searchSignalsRegex returns SignalSearchResult objects', () { + final results = hierarchy.searchSignalsRegex('Top/CPU/ALU/result'); + expect(results.length, 1); + // signalId uses the normalised hierarchySeparator ('/') format + // from the tree walker — findSignalById normalises both '.' and '/'. + expect(results.first.signalId, 'Top/CPU/ALU/result'); + expect(results.first.signal, isNotNull); + expect(results.first.signal!.name, 'result'); + }); + + test('searchSignalsRegex returns results with SignalOccurrence objects', + () { + final results = hierarchy.searchSignalsRegex('Top/**/carry_out'); + expect(results.length, 1); + expect(results.first.signal, isNotNull); + expect(results.first.signal!.name, 'carry_out'); + expect(results.first.signal!.width, 1); + }); + + test('searchOccurrencesRegex returns OccurrenceSearchResult objects', () { + final results = hierarchy.searchOccurrencesRegex('Top/**/Cache'); + expect(results.length, 1); + expect(results.first.occurrenceId, 'Top/Memory/Cache'); + }); + + // ── Limit ── + + test('limit controls maximum results', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/.+', limit: 3); + expect(results.length, 3); + }); + + // ── Glob-style wildcards ── + + test('glob * at start matches suffix pattern', () { + // User's scenario: "*m" should match signals ending in "m". + final results = hierarchy.searchSignalPathsRegex('Top/*_m'); + expect( + results, + containsAll([ + 'Top/data_m', + 'Top/addr_m', + 'Top/flag_m', + ])); + expect(results.length, 3); + }); + + test('glob * at end matches prefix pattern', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/RegFile/d*'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/d0', + 'Top/CPU/RegFile/d1', + 'Top/CPU/RegFile/d2', + 'Top/CPU/RegFile/d15', + ])); + expect(results.length, 4); + }); + + test('glob * in the middle matches infix pattern', () { + // *d*a* should match names containing 'd' followed eventually by 'a' + final results = + hierarchy.searchSignalPathsRegex('Top/Memory/Cache/*d*a*'); + expect(results, contains('Top/Memory/Cache/data')); + }); + + test('glob * matches all signals (like .*)', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/ALU/*'); + expect( + results, + containsAll([ + 'Top/CPU/ALU/a', + 'Top/CPU/ALU/b', + 'Top/CPU/ALU/result', + 'Top/CPU/ALU/carry_out', + ])); + expect(results.length, 4); + }); + + test('glob * in module level matches any child', () { + final results = hierarchy.searchSignalPathsRegex('Top/*/clk'); + // Top's immediate module-children are CPU, Memory, IO — none of + // them have a direct clk signal, so this is empty. + expect(results, isEmpty); + }); + + test('glob * combined with ** for deep search', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/*_m'); + expect( + results, + containsAll([ + 'Top/data_m', + 'Top/addr_m', + 'Top/flag_m', + ])); + expect(results.length, 3); + }); + + // ── Empty / no match ── + + test('empty pattern returns nothing', () { + expect(hierarchy.searchSignalPathsRegex(''), isEmpty); + expect(hierarchy.searchOccurrencePathsRegex(''), isEmpty); + }); + + test('non-matching pattern returns nothing', () { + expect(hierarchy.searchSignalPathsRegex('Top/NonExistent/foo'), isEmpty); + }); + + // ── ** at various positions ── + + test('trailing ** collects all signals below', () { + final results = hierarchy.searchSignalPathsRegex('Top/Memory/**'); + // Should collect all signals in Memory subtree + expect( + results, + containsAll([ + 'Top/Memory/Cache/clk', + 'Top/Memory/Cache/addr', + 'Top/Memory/Cache/data', + 'Top/Memory/Cache/hit', + 'Top/Memory/DRAM/clk', + 'Top/Memory/DRAM/cas', + 'Top/Memory/DRAM/ras', + ])); + expect(results.length, 7); + }); + + test('multiple ** segments work', () { + final results = + hierarchy.searchSignalPathsRegex('**/(CPU|Memory)/**/clk'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/clk', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + ])); + }); + }); + + group('searchOccurrences dispatches to regex', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Build hierarchy: + // Top + // CPU + // ALU + // Decoder + // MuxUnit + // Memory + // Cache + // DRAM + // IO + // UART + + final alu = HierarchyOccurrence( + name: 'ALU', + ); + + final decoder = HierarchyOccurrence( + name: 'Decoder', + ); + + final muxUnit = HierarchyOccurrence( + name: 'MuxUnit', + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu, decoder, muxUnit], + ); + + final cache = HierarchyOccurrence( + name: 'Cache', + ); + + final dram = HierarchyOccurrence( + name: 'DRAM', + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + children: [cache, dram], + ); + + final uart = HierarchyOccurrence( + name: 'UART', + ); + + final io = HierarchyOccurrence( + name: 'IO', + children: [uart], + ); + + final root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory, io], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('searchOccurrences with glob pattern finds modules', () { + // Pattern: *Mux* should find MuxUnit (auto-prepended with **/) + final results = hierarchy.searchOccurrences('*Mux*'); + expect(results, isNotEmpty, + reason: + 'searchOccurrences should dispatch to regex for glob patterns'); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with ** finds deep modules', () { + final results = hierarchy.searchOccurrences('**/*Mux*'); + expect(results, isNotEmpty); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with .* matches at one level', () { + // */.* matches any child one level below root + final results = hierarchy.searchOccurrences('*/.*/.*'); + expect(results.length, greaterThanOrEqualTo(3), + reason: 'Should match ALU, Decoder, MuxUnit, Cache, DRAM, UART'); + }); + + test('searchOccurrences with explicit path pattern', () { + // */CPU/.* matches children of CPU + final results = hierarchy.searchOccurrences('*/CPU/.*'); + expect(results.length, 3); + expect(results.any((r) => r.name == 'ALU'), isTrue); + expect(results.any((r) => r.name == 'Decoder'), isTrue); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with alternation', () { + final results = hierarchy.searchOccurrences('**/(ALU|DRAM)'); + expect(results.length, 2); + expect(results.any((r) => r.name == 'ALU'), isTrue); + expect(results.any((r) => r.name == 'DRAM'), isTrue); + }); + + test('searchOccurrences without regex uses plain matching', () { + // Plain query without glob chars uses substring matching + final results = hierarchy.searchOccurrences('Mux'); + expect(results, isNotEmpty); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with leading **/ is not double-prepended', () { + final results = hierarchy.searchOccurrences('**/UART'); + expect(results.length, 1); + expect(results.first.name, 'UART'); + }); + + test('searchOccurrences with leading */ is not double-prepended', () { + final results = hierarchy.searchOccurrences('*/CPU'); + expect(results.length, 1); + expect(results.first.name, 'CPU'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart b/packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart new file mode 100644 index 000000000..fa22a21b4 --- /dev/null +++ b/packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_signal_resolve_test.dart +// Tests for resolving ROHD dot-separated signal IDs. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + late HierarchyOccurrence root; + late BaseHierarchyAdapter adapter; + + setUpAll(() { + root = HierarchyOccurrence( + name: 'abcd', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'resetn', width: 1, direction: 'input'), + SignalOccurrence(name: 'arvalid_s', width: 1, direction: 'input'), + ], + children: [ + HierarchyOccurrence( + name: 'sub', + signals: [ + SignalOccurrence(name: 'data', width: 8, direction: 'output'), + ], + ), + ], + ); + + adapter = BaseHierarchyAdapter.fromTree(root); + root.buildAddresses(); + }); + + SignalOccurrence? resolve(String dotPath) { + final addr = OccurrenceAddress.tryFromPathname(dotPath, root); + if (addr == null) { + return null; + } + return adapter.signalByAddress(addr); + } + + group('findSignalById resolves ROHD dot-separated signal IDs', () { + test('resolves top-level clk', () { + final sig = resolve('abcd.clk'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/clk'); + }); + + test('resolves top-level resetn', () { + final sig = resolve('abcd.resetn'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/resetn'); + }); + + test('resolves top-level arvalid_s', () { + final sig = resolve('abcd.arvalid_s'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/arvalid_s'); + }); + + test('resolves nested sub.data', () { + final sig = resolve('abcd.sub.data'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/sub/data'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/signal_search_result_test.dart b/packages/rohd_hierarchy/test/signal_search_result_test.dart new file mode 100644 index 000000000..2669c859f --- /dev/null +++ b/packages/rohd_hierarchy/test/signal_search_result_test.dart @@ -0,0 +1,236 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_search_result_test.dart +// Tests for SignalSearchResult and ModuleSearchResult display helpers. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('SignalSearchResult display helpers', () { + test('displayPath strips top module', () { + const result = SignalSearchResult( + signalId: 'Top/counter/clk', + path: ['Top', 'counter', 'clk'], + ); + expect(result.displayPath, equals('counter/clk')); + }); + + test('displayPath for top-level signal', () { + const result = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + expect(result.displayPath, equals('clk')); + }); + + test('displayPath for single-segment path', () { + const result = SignalSearchResult( + signalId: 'clk', + path: ['clk'], + ); + expect(result.displayPath, equals('clk')); + }); + + test('displaySegments strips top module', () { + const result = SignalSearchResult( + signalId: 'Top/sub1/sub2/clk', + path: ['Top', 'sub1', 'sub2', 'clk'], + ); + expect(result.displaySegments, equals(['sub1', 'sub2', 'clk'])); + }); + + test('intermediateOccurrenceNames extracts middle segments', () { + const result = SignalSearchResult( + signalId: 'Top/sub1/sub2/clk', + path: ['Top', 'sub1', 'sub2', 'clk'], + ); + expect(result.intermediateOccurrenceNames, equals(['sub1', 'sub2'])); + }); + + test('intermediateOccurrenceNames empty for top-level signal', () { + const result = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + expect(result.intermediateOccurrenceNames, isEmpty); + }); + + test('intermediateOccurrenceNames empty for single-level nesting', () { + const result = SignalSearchResult( + signalId: 'Top/sub1/clk', + path: ['Top', 'sub1', 'clk'], + ); + // sub1 is both the containing block and an intermediate instance + expect(result.intermediateOccurrenceNames, equals(['sub1'])); + }); + + test('name returns last path segment', () { + const result = SignalSearchResult( + signalId: 'Top/counter/clk', + path: ['Top', 'counter', 'clk'], + ); + expect(result.name, equals('clk')); + }); + + test('equality based on signalId', () { + const a = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + const b = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + }); + + group('HierarchySearchResult.normalizeQuery', () { + test('converts dots to slashes', () { + expect( + HierarchySearchResult.normalizeQuery('top.cpu.clk'), + equals('top/cpu/clk'), + ); + }); + + test('preserves slashes', () { + expect( + HierarchySearchResult.normalizeQuery('top/cpu/clk'), + equals('top/cpu/clk'), + ); + }); + + test('handles mixed separators', () { + expect( + HierarchySearchResult.normalizeQuery('top.cpu/clk'), + equals('top/cpu/clk'), + ); + }); + + test('handles empty query', () { + expect(HierarchySearchResult.normalizeQuery(''), equals('')); + }); + }); + + group('ModuleSearchResult display helpers', () { + late HierarchyOccurrence aluNode; + + setUp(() { + aluNode = HierarchyOccurrence( + name: 'ALU', + ); + }); + + test('displayPath strips top module', () { + final result = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + expect(result.displayPath, equals('CPU/ALU')); + }); + + test('displaySegments strips top module', () { + final result = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + expect(result.displaySegments, equals(['CPU', 'ALU'])); + }); + + test('displayPath for single-segment path', () { + final topNode = HierarchyOccurrence( + name: 'Top', + ); + final result = OccurrenceSearchResult( + occurrenceId: 'Top', + path: const ['Top'], + occurrence: topNode, + ); + expect(result.displayPath, equals('Top')); + }); + + test('equality based on moduleId', () { + final a = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + final b = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + }); + + group('ModuleSearchResult.normalizeQuery', () { + test('converts dots to slashes', () { + expect( + HierarchySearchResult.normalizeQuery('top.cpu'), + equals('top/cpu'), + ); + }); + }); + + group('searchSignals integration with display helpers', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Build: Top -> counter (with clk, data[8] signals) + final counter = HierarchyOccurrence( + name: 'counter', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + ), + SignalOccurrence( + name: 'data', + width: 8, + ), + ], + ); + + final root = HierarchyOccurrence( + name: 'Top', + children: [counter], + signals: [ + SignalOccurrence( + name: 'reset', + width: 1, + direction: 'input', + ), + ], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('searchSignals returns enriched results', () { + final results = hierarchy.searchSignals('clk'); + expect(results, isNotEmpty); + final result = results.first; + expect(result.signalId, contains('clk')); + expect(result.displayPath, equals('counter/clk')); + expect(result.intermediateOccurrenceNames, equals(['counter'])); + }); + + test('searchSignals for top-level port', () { + final results = hierarchy.searchSignals('reset'); + expect(results, isNotEmpty); + final result = results.first; + expect(result.displayPath, equals('reset')); + expect(result.intermediateOccurrenceNames, isEmpty); + }); + }); +} diff --git a/rohd_devtools_extension/.vscode/launch.json b/rohd_devtools_extension/.vscode/launch.json index d80e58185..fae1b0be8 100644 --- a/rohd_devtools_extension/.vscode/launch.json +++ b/rohd_devtools_extension/.vscode/launch.json @@ -30,5 +30,16 @@ "--dart-define=use_simulated_environment=true" ], }, + { + "name": "Run: Web Standalone (port 9099)", + "request": "launch", + "type": "dart", + "program": "lib/main_standalone.dart", + "deviceId": "web-server", + "args": [ + "--web-port=9099", + "--web-hostname=0.0.0.0" + ] + } ] } \ No newline at end of file diff --git a/rohd_devtools_extension/.vscode/tasks.json b/rohd_devtools_extension/.vscode/tasks.json new file mode 100644 index 000000000..1d5530e98 --- /dev/null +++ b/rohd_devtools_extension/.vscode/tasks.json @@ -0,0 +1,137 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run: Web Standalone (Debug, port 9099)", + "type": "shell", + "command": "flutter run -d web-server --web-port=9099 --web-hostname=0.0.0.0 lib/main_standalone.dart", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching", + "endsPattern": "is being served at" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "detail": "Runs standalone web app in debug mode on port 9099" + }, + { + "label": "Run: Web Standalone (Release, port 9099)", + "type": "shell", + "command": "flutter run --release -d web-server --web-port=9099 --web-hostname=0.0.0.0 lib/main_standalone.dart", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching", + "endsPattern": "is being served at" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "detail": "Runs standalone web app in release mode on port 9099" + }, + { + "label": "Run: Linux Standalone (Debug)", + "type": "shell", + "command": "flutter run -d linux lib/main_standalone.dart", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching", + "endsPattern": "^Application finished" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "detail": "Runs standalone Linux app in debug mode" + }, + { + "label": "Run: Linux Standalone (Debug, software rendering)", + "type": "shell", + "command": "flutter run -d linux --enable-software-rendering lib/main_standalone.dart", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching", + "endsPattern": "^Application finished" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "detail": "Runs standalone Linux app in debug mode with software rendering" + }, + { + "label": "Run: Linux Standalone (Release)", + "type": "shell", + "command": "flutter run --release -d linux lib/main_standalone.dart", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching", + "endsPattern": "^Application finished" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "detail": "Runs standalone Linux app in release mode" + }, + { + "label": "Run: Linux Standalone (Release, software rendering)", + "type": "shell", + "command": "flutter run --release -d linux --enable-software-rendering lib/main_standalone.dart", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching", + "endsPattern": "^Application finished" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "detail": "Runs standalone Linux app in release mode with software rendering" + } + ] +} diff --git a/rohd_devtools_extension/LICENSE b/rohd_devtools_extension/LICENSE new file mode 100644 index 000000000..9e00cc822 --- /dev/null +++ b/rohd_devtools_extension/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (C) 2021-2026 Intel Corporation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rohd_devtools_extension/Makefile b/rohd_devtools_extension/Makefile new file mode 100644 index 000000000..19c7ac8ca --- /dev/null +++ b/rohd_devtools_extension/Makefile @@ -0,0 +1,23 @@ +# Minimal build entrypoints for the upstream standalone extension package. + +ROOT := $(shell pwd) + +.PHONY: all linux clean-linux help + +all: linux + +linux: + @flutter pub get + @flutter build linux --target=lib/main_standalone.dart + +clean-linux: + -rm -rf $(ROOT)/build/linux + -rm -rf $(ROOT)/linux/flutter/ephemeral + +help: + @echo "ROHD DevTools Extension" + @echo "" + @echo "Targets:" + @echo " all - Build the Linux standalone app (default)" + @echo " linux - Run flutter pub get and build Linux standalone" + @echo " clean-linux - Remove Linux build outputs" \ No newline at end of file diff --git a/rohd_devtools_extension/README.md b/rohd_devtools_extension/README.md index 9ead23c94..acb09c486 100644 --- a/rohd_devtools_extension/README.md +++ b/rohd_devtools_extension/README.md @@ -1,25 +1,83 @@ -# ROHD Devtool +# ROHD DevTools Extension -The ROHD Devtool provides debugging functionality for hardware designers. Initial proposals and discussions for the devtool can be found at . +The ROHD DevTools extension provides debugging support for ROHD hardware +designers. It connects to a running Dart VM, reads the ROHD module hierarchy, +and displays live signal information while the debugged program is paused. -How to Use the ROHD Devtool: +Initial proposals and discussions for the devtool can be found at +. -1. Set a breakpoint on your ROHD design. -2. When the breakpoint is hit, an URL will be outputted. -3. Run the dart devtools command on your terminal. -4. A webpage will open, and you can paste the URL into the webpage. -5. Look for the tab labeled 'ROHD'. +## Opening from Flutter DevTools -## Contributions +The normal user flow is through Flutter DevTools: + +1. Start debugging a ROHD program. +2. Stop at a breakpoint or otherwise pause the debugged program. +3. Use **Open DevTools in Browser** from VS Code. +4. In the browser DevTools page, open the **ROHD** tab. + +See the Flutter DevTools documentation for the surrounding DevTools workflow: +. + +When opened this way, the extension runs inside Flutter DevTools and uses VS +Code's Dart Tooling Daemon (DTD) integration to attach to and control the +debugged Dart VM. + +## Standalone Release Mode -We welcome contributions to the development of the ROHD Devtool. Please refer to our Contributing doc for guidance on how to get started. +The extension can also run as a standalone app. This is useful when you want to +connect directly to a Dart VM service URI, discover running VMs through a DTD +URI from the app's connection form, and select the specific debug VM to which to +attach. -## Running Tests on the Devtool +Run the release web standalone form: + +```sh +cd rohd_devtools_extension +flutter run --release -d web-server --web-port=9099 --web-hostname=0.0.0.0 lib/main_standalone.dart +``` + +Run the release Linux standalone form: + +```sh +cd rohd_devtools_extension +flutter run --release -d linux lib/main_standalone.dart +``` -The ROHD Devtool runs in an iframe, which means that the --platform chrome flag is required to ensure tests are run in the browser. +If the Linux build needs software rendering, use: -```cmd -flutter test --platform chrome Optional[test\modules\tree_structure\model_tree_card_test.dart] > test_output.txt +```sh +cd rohd_devtools_extension +flutter run --release -d linux --enable-software-rendering lib/main_standalone.dart ``` -This command will output the test results to a text file named `test_output.txt`. +The repository's `.vscode/tasks.json` contains development utilities for these +flows, including debug-mode variants. Those VS Code tasks are for extension +development only and may change or be removed; the commands above are the +release-mode forms to use directly. + +## Current Features + +The in-app help menu is the source of truth for the current feature set. The +main capability today is module-level inspection: + +- Select a block from the Module Tree. +- View that module's live port and internal `Logic` values in the Details pane. +- Search and filter the Module Tree and signal list. +- Refresh the module hierarchy from the connected VM. +- Export the signal details table as a PNG. + +## Contributions + +We welcome contributions to the development of the ROHD DevTools extension. +Please refer to the contributing documentation for guidance on how to get +started. + +## Running Tests + +The ROHD DevTools extension runs in an iframe when embedded in DevTools, so use +the Chrome platform for browser-based widget tests. + +```sh +flutter test --platform chrome test/ +``` diff --git a/rohd_devtools_extension/analysis_options.yaml b/rohd_devtools_extension/analysis_options.yaml index 0d2902135..f82d6cc51 100644 --- a/rohd_devtools_extension/analysis_options.yaml +++ b/rohd_devtools_extension/analysis_options.yaml @@ -1,28 +1,250 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +# Lints set up with some guidance from here: +# https://rydmike.com/blog_flutter_linting.html -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true +# keep up to date, matching https://dart.dev/tools/linter-rules/all +# some lints are not yet available, so disabled and marked with [not currently recognized] linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + # - always_specify_types + - always_use_package_imports + - annotate_overrides + - annotate_redeclares + # - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + # - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_dynamic_calls + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_futureor_void + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + # - document_ignores + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - invalid_runtime_check_with_js_interop_types + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - matching_super_parameters + - missing_code_block_language_in_doc_comment + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - omit_obvious_local_variable_types + # - omit_obvious_property_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_names + - package_prefixed_library_names + # 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 + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + # - prefer_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + # - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + # - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + # - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + # - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + # conflicts with omit_obvious_local_variable_types + # - specify_nonobvious_local_variable_types + # - specify_nonobvious_property_types + - strict_top_level_inference + # - switch_on_type + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + - unintended_html_in_doc_comment + # - unnecessary_async + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + # - unnecessary_final + - unnecessary_getters_setters + - unnecessary_ignore + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_library_name + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + # - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + # - unnecessary_unawaited + - unnecessary_underscores + # - unreachable_from_main + - unrelated_type_equality_checks + # - unsafe_variance + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_null_aware_elements + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - use_truncating_division + - valid_regexps + - void_checks \ No newline at end of file diff --git a/rohd_devtools_extension/assets/help/details_help.md b/rohd_devtools_extension/assets/help/details_help.md new file mode 100644 index 000000000..27689d8df --- /dev/null +++ b/rohd_devtools_extension/assets/help/details_help.md @@ -0,0 +1,28 @@ +# ℹ️ Module Details — Help + + + +Signal Details + Click module Select module to view signals + Signal list Shows ports and internal signals + +Signal Values + Value column Current signal value (hex/binary) + Width column Bit width of each signal + + + +## Signal Details + +| Action | Description | +| --- | --- | +| Click module (tree) | Select module and populate signal list | +| Signal list | Shows input ports, output ports, and internal signals | +| Value column | Displays the current value of each signal | +| Width column | Shows the bit width of each signal | + +## Export + +| Action | Description | +| --- | --- | +| 📷 Camera | Export signal table as PNG image | diff --git a/rohd_devtools_extension/assets/help/devtools_help.md b/rohd_devtools_extension/assets/help/devtools_help.md new file mode 100644 index 000000000..f7845a6bc --- /dev/null +++ b/rohd_devtools_extension/assets/help/devtools_help.md @@ -0,0 +1,34 @@ +# 🛠 ROHD DevTools — Help + + + +Module Tree (left panel) + Click node Select module + Click ▸ / ▾ Expand / collapse + 🔃 Refresh Reload hierarchy from VM + Type in search Filter modules by name + +Details (right panel) + Signal list Shows ports and internal signals + Search Filter signals by name + Filter Toggle input / output visibility + + + +## Module Tree (left panel) + +| Key | Description | +| --- | --- | +| Click module | Select module and show signals | +| Click ▸ / ▾ | Expand or collapse sub-modules | +| 🔃 Refresh | Reload hierarchy from the VM | +| Type in search | Filter modules by name | + +## Signal Details (right panel) + +| Key | Description | +| --- | --- | +| Signal list | Shows input ports, output ports, and internal signals | +| Search | Filter signals by name | +| Filter icon | Toggle input / output signal visibility | +| 📷 Export | Export signal details as PNG | diff --git a/rohd_devtools_extension/assets/icons/rohd_logo.png b/rohd_devtools_extension/assets/icons/rohd_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f1faecdde32ff3c2a4b01f5ca74a58c88c2726 GIT binary patch literal 5225 zcmZ`-cQoAFxBn_bMvXCQ5M}gU61|3)G3qFzchQ6BBu0qdOGGE41ks}=%7`JsB}DW- z*VTiB5cSRdy|><4?~k{><*e^MyPfa(?7h!935NRG)D)}~002-U;F>5x+xPEti;VC_ zEqaj?8d6tvJ#_$RN~64V01>`9o!}@v00`p)fXEjBa84jaZUI1`1OV*V1Asy<05JI$ zb{Q!VI>;Q6+M2-4zgKB@RThCl9)Qr(BHttN-QQ9cnWV7vN3r- zotS9Z4#^f7J@i53`ElB--)Bh*@_SlfdBnkZ%5RT} z1_dmm=FAhn9Yt0ih#)FI`?!uD-x6;46KfND6CfRtKrILLFZd5XrK<>Vs?!J_k-Teg zyG|qXef}`Vq(&6=SspX<-DC^B75hxxhZR2C8c`=2QOdsQETb~Kh$WL{Jwcdxbcqv~ zWBFK*?~5iCby_lw!>>fj9J8=h#~?SbO6sSAo(b3-Ys;aK2rCCm9sX=)o%FJN`;J~_ zon-h#CeMvAl_ln*+n0=8Mp5}m_L(ACJPDHxSBI%?21nFVLC+dYP#URpIdVv}XSRWD zP{z#7wvQYsST7w@^CR+JuVqa9g*(0DN3#rDp08nL%3hBxjyBqxJ5J#TpB7b>q%hUC z^PaITN4?Zr3CZ-miK($TqkMYng5YS4hp#ofk)>oPTvGy-DKk86=HZHWd&+!a7TT;4 zm2xI9Hqce68d<)OJp|zAk{c4;M|Z2A>NaNOM%l!wW;Z=Rkm&LOT-e}4OKbHF$Y}qA z0qFLpMkl|q@^GqzW$f^JFMU&%xxKP3C`)ibC!PgT+wq2b`6jXEN81*Sw@|9ZoL?5U zG8^-g?Wv9~-k_lRp{EK%$$+{kO>&lVxL^uTZxYfq?BhNd9U-Oie2AMDp8qJtvD<^s z%je@Y_Dtj<9GrRWssI*FgE~d=3~+XBhr=`7rI(WHR2dz{v@=E3CwoU>E=TGiz3E(t z^US<|_O3IwD0mTeD6Pq`HUP1LiUEG*_awmb#4Ori+cJV`lkTX<@^9q;q6$Hjsh>p* zE8+ve74#_51M;Dq|CgcKv0%!~7`Dq6Ulrf&urTmwk@u(72o(dyY&krX-Hc(y=Y7T7 z+4J?DX(mit>dx>Cl>&*l8us;B>1=nicVOK8E)09&>$=N7d`*=6M|~@%izBS>7%KSC z_@G58C8@A(0k|>AnJPdfNiBz>6B<3t;emCB7W|~Ew&3Z6roQbiQ7rR=S;}$x zmp47nW$|2}c;2*haca2!hk*jTEGWApAWfTw4fo^FA6$-J<_^#Se(+%94$UOtx@{6j zlM6ZOn^>ntdqb_QLSRuCOJ=lt?A^>e%i3}zX9VnfapC$9UlBwdC8R!mKKVVBInB4l zvFO96qbxA6f-rEJvFh&_Cc~SXOQPlJD;30=EoM{&B6xRy4T*~KZel(_ebO(}n+W|^ zLjx{Q&(1h0FAaZ0M4=TkU@g2JeXdVr+^p82J zdpa>7&$MPqpVBwbEp&>xpGQ~?r!zxWQt0EfA0BKCNhC%*CW<|zpj$y%NL!!tM$k75 zZs@JJC9(83g(wE?4b>Z}QZ}-Da(r;G`KGzzB9n9ll6h6LUEnR*f}<%L+Wy2IeQp7i zjrTN|zYWa@gPmo}e!Xa}^l8J1+^*d*sOV)JIk$sR&SL9k{ks?hSMK^&KS$$lD zp55o)5(3119NeGxUkX{*vGHR^4qGaH-s9L3=}pfdH3jMrfFu4iezbwzBYjn4f?WsP zHkR%dL+g32Q8`=+20p|OyppZaj!M!B7T}##NYe89z8>KQA~OQosD|~82I@@I5m9!D z7HR&zDR9k$lNomI5$}{-DmQuq9HrY$WQRCYf8}T5k08ahDMH3EWf0G~($UTMQJu$z zw&olqjAJ*4jNziP(TMkiBGM0z`eB8f)%CPo>OwwrU6_YY)Xk zvg0=}&zF-VknjF_O4lxP#iAFKVPl_~+kdIJH-C(q`iqcvE&Z^QCyvuQFgD&7=CW@{ zd{ooSyvmoSo7B*xoH=`T+_2g1l}{m68eIKkmnKy6E6*3gLcqT?cfZXm=-NOc(^o6F zhy6<`#VgTJn{q{*#+sCoR1;bW;hfc)Sr3*78XsqB+PesN&HFF-)-Cf~O)|TvsNJIzjzZVFqVIUKi5E+T z5>RV#QnQe3%jXz0=GjzJih`vUMPncb7fR&r)N#ylyd8cxf4s@idA~IgYVXLArs~>g z^*m}pEA>Iq=!CWN&Wmz@UK$2bw3PL&eg0QR`9J=nll^J~Q2QspaU}UY@d|<|B=yFa z_inqke1d&{kJ9_k+N{!?t~7q|2y#E?8zQ7qr+17W{+dDT7Pn8^7)~#=1Dgrq!e*dKDWQ#g;B}F0Gj*fkzO+1gC8CBRs<@Z>tjQ^*YF=de4#D;G z2n>&wMzr?T8Q!xL5+cgl+P8>6xpj!V`ej0*(SU%ff_g8JWmc(^CTrMBzQTnVt3 zhg^scdqmPV!U_JwvqOCr4RDWqG*k>OcM~;{#NZ7+UjmZGia7E9Dj8NF{33cP)vPC zJGuWjlRSoiJka(JUef9`744d{9D^Kx9om|8ob_zowC59MHdeEAR@y`1I&3i>gVQGD zLA0Sw@auGYl83lYH}|^{JT`)Dug{;+j@{#FrkoP_)!3U`^an!odj5wQ*S9dEogduN4q730S@{r&QA+!))Z0&-&_U%h)M z;*qg>8uDtEwK}!>%lD9m<$WMKT?!8J@46;ineqW+|q|O#ScMtAc9TF*C zIOrfYh`RJzl)Q+Ama5Jo_dISWPT+@>A8XDIO#aZ>rk12Cn%QL#AJZn`IDawZ8fTI6 z&;K5rJ6%oUu732V!|9k^Pz}=rhac*U)~ZL{P0X<`+t2)QTT*Mp_pgqUeIzRK+KFyC zPS!qx8gi-j!T__9!+_UY_r7`3^tJT~p~7EWuknMn&_Rsy-Tc+sQ5)P2*HF6RqlZcc zr2D3AAj(+9N(uRgACys@P~;@~HmaLkbDzQ6>lKgMKkV>=c5CEKU-9&6s2y zRh>ah#pv%>m|J6=4e=^~+Anx9CU6KN?P|4Rznk=$F{lQ+Zg1`m59jm1&Vb)=Fdx6` zb3Rudl{5^LcHf+4Cu-fZ=H1}@2T!2`&$B;!Be7y@n@sM}~gZ+=EqtccxmO?-A>biFVZOMCUknJ$xZki2inx zhB!3ZQzg`s22hU&4=A@V#l!NJ5N>i2276l@^)r2zY}<+ZyA-cL2C=UG_h-KnIxGW> zBP{UpskcK1Y591Wc|dNtsNgMb$+X_G!Efx(4e)9xVpOD=gc$S>?1aX=gjL^ic~|GKJ$R17_3%tfHW$T2P1=WS0gL{lPi+{ofjEc-Lf;Xy<&KJI3 zc5F*y6jk5Q3cZMdmc~FK@8}EWo>_O_v1St(B;^S(An2$dQ2XtY!U_WUD%k`c#if>7 zLCOKqN|Vvw4Tp942C`;p?~MtRD=f9+>%F0SC9dYl%Bd&^!NL1k#P19J0+sCehv}xj zTyqD#ohNCvnc;`b)(+Ha; zQR2;>(rc+$rxW-}d`vVafqN4QS>)4ryE`l@){^B0UVaR~-~`Cs!4-7YFRn}d(n7yI z;3ZHZCy%#YI?fDrp_AsgvF`cR8fe7E4%A*v0O8w|zFNak7^Gq=+~&R0O1gV=k;ef8 zF>|(MS}i7})VktBn#>wKX?e}X`*E(C6cYB?u(Mcm(VlMdsv;hk9A)_Lx4_I&neSXO z!B9GxSyDpIDl?sCyK51D*Y8Ugr_-M~3X zIrj7A0Eupzf6`EH%$t~yiZ&rMA(S1QR za)AxTUW=tr;83uXN8endzMDwFM zNo#Q`Up8m;33yC3%3M_}TmE_w7IHq6m{Upvj^K|X8gFJdeq(w|VkQzz!z`l%DHPTy zaAh-YVOP?DA36)YS~dH~z>k+T{?jQ$I*S~LKh6I$Tlf4I4QYkWR0*S1jlSn?c|DF8 zgEe2FSN3(E)5Bj1j?z#Qb|hl~wr4bSkPD|HzPah6*cTH1{AcYCxK{$dlVW-;28!8I zgA+`;yUxI=1N+fkIJcuiQ7Bze105H2_qvD3r}pZ{cV>7E6vOuxs^Y~a)akDlgh=g2 zNIpzYk>>KNY$aUpKRSAtzjfg1WaL+4r3acbDot#1H8K0dtIQUaTl=K5SIa*pV z=$AnBa37__L$3aavlnG*k7+v*CWUfnWe8hx+A0`-qc=v(>vC=IG3DmN(B_gA>DO+x z7?Yi(hbDmtkw?{$E?IB3CWlTK`+b}@9Dn5)UA=Vw! zkTgFa4gy5xUMCG+a}v_hAIHIJ>Ev3~^Pt5!Jd(LqBlsgl z?bnoDnr_qtA-KO>3AkiGvsFs=41Wqv(n21FogfZ9E;&K)HXSakGWTRN`iqPU2j_IC zzF``v>QIwBwSxSsB;Vyb4$Q0!UspQXJ5?Xa=LN!ap-|=25gK^mQKG z_x(|%^>K1x$}mO@1m|-hRF$Ew2QvvjKK~>WbLxTr9a@`62(`@pHIxy!o~eRqs3qFG zT#@>N39mXux^4n$<)SJ?K4PlRWt531ys8!+1?9|>;sxj6E4aC?iaG2C*_v#=Z6~6V zrRlf`W4#x4L;3h#g(UX7quAA7Ww(9AUFkRDaqn728!47VBs&moV_MuRTrApLSe>KW zDptCtww1CBTk+{p z>;pYayq7(7~!CdH$clgz}X?dMZxKb3!wp|B&4K8CGLw#NSR1VDBP1$kdYLY zkWi42*tqp9=KlzI`#8Hl3jO~DvQ2UF1OarIiFp9ZA(+$miO(Z|D?L*XC@#pY%ns4*gyd!Jd|Qil=M9G$Sf!cJ*Q@*%qM3Oo)#0%k=d!w1ET|M h1LPx-@KPNyV26n=;HSpDmjn|4LQ7w>Uc)}>zX0c)l=J`q literal 0 HcmV?d00001 diff --git a/rohd_devtools_extension/lib/main.dart b/rohd_devtools_extension/lib/main.dart index 34fbedef9..278c04212 100644 --- a/rohd_devtools_extension/lib/main.dart +++ b/rohd_devtools_extension/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // main.dart @@ -7,26 +7,122 @@ // 2025 January 28 // Author: Roberto Torres +import 'dart:js_interop'; + +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:devtools_extensions/api.dart'; import 'package:devtools_extensions/devtools_extensions.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/const/app_theme.dart'; import 'package:rohd_devtools_extension/rohd_devtools/view/rohd_devtools_page.dart'; - import 'package:rohd_devtools_extension/rohd_devtools_observer.dart'; +import 'package:web/web.dart' as web; void main() { + debugPrint('[main.dart] Starting ROHD DevTools Extension...'); + debugPrint('[main.dart] Platform: ${kIsWeb ? "Web" : "Native"}'); + + // Policy: preserve extension UI state across target-app restarts. + // DevTools emits forceReload when the debugged app restarts, but a full page + // reload would discard local selection and snapshot state that the extension + // can now recover through its own reconnect path. Intercept the wrapper event + // before DevToolsExtension handles it so reconnect is graceful instead of a + // hard reload. + _installMessageInterceptor(); + /// Initializing the [BlocObserver] created and calling runApp + debugPrint('[main.dart] Initializing BlocObserver...'); Bloc.observer = const RohdDevToolsObserver(); + debugPrint('[main.dart] Calling runApp...'); runApp(const RohdDevToolsApp()); + debugPrint('[main.dart] runApp called successfully'); +} + +/// Intercepts DevTools wrapper messages before the ExtensionManager +/// can process them. +/// +/// Policy note: this extension intentionally owns restart recovery instead of +/// delegating to the default DevTools page reload path. +/// +/// Blocks: +/// - `forceReload` – prevents the full page reload that would destroy local +/// extension state. Instead, the extension disconnects the stale VM service +/// and requests a fresh VM URI from DevTools so state can survive through a +/// controlled reconnect. +void _installMessageInterceptor() { + web.window.addEventListener( + 'message', + ((web.MessageEvent e) { + try { + final data = e.data.dartify(); + if (data is! Map) { + return; + } + final type = data['type']; + + final source = data['source'] ?? '?'; + debugPrint('[ROHD-MSG] type=$type source=$source ' + 'data=${data['data']}'); + + if (type == 'forceReload') { + debugPrint('[ROHD-MSG] BLOCKED forceReload — ' + 'triggering graceful reconnection'); + e.stopImmediatePropagation(); + + // After blocking the page reload, disconnect the stale VM + // service and ask DevTools for the current (restarted) URI. + // A short delay lets the DevTools wrapper finish its own + // transition before we re-request. + Future.delayed(const Duration(milliseconds: 300), () async { + try { + if (serviceManager.connectedState.value.connected) { + debugPrint('[ROHD-MSG] Disconnecting stale VM...'); + await serviceManager.manuallyDisconnect(); + } + debugPrint('[ROHD-MSG] Requesting fresh VM URI ' + 'from DevTools...'); + extensionManager.postMessageToDevTools(DevToolsExtensionEvent( + DevToolsExtensionEventType.vmServiceConnection)); + } on Object catch (err) { + debugPrint('[ROHD-MSG] Reconnection request ' + 'failed: $err'); + } + }); + return; + } + } on Object catch (_) {} + }).toJS); } +/// The main ROHD DevTools application. class RohdDevToolsApp extends StatelessWidget { + /// Creates the main ROHD DevTools application. const RohdDevToolsApp({super.key}); + @override Widget build(BuildContext context) { - return const DevToolsExtension( - child: RohdDevToolsPage(), - ); + debugPrint('[RohdDevToolsApp] Building app widget...'); + return DevToolsExtension( + // Reset IdeTheme scaling so extension renders at 1× size + // regardless of the IDE's editor.fontSize setting. + child: Builder(builder: (context) { + final current = ideTheme; + setGlobal( + IdeTheme, + IdeTheme( + backgroundColor: current.backgroundColor, + foregroundColor: current.foregroundColor, + embedMode: current.embedMode, + isDarkMode: current.isDarkMode)); + + final isDark = Theme.of(context).brightness == Brightness.dark; + final base = isDark ? buildDarkTheme() : buildLightTheme(); + + return Theme(data: base, child: const RohdDevToolsPage()); + })); } } diff --git a/rohd_devtools_extension/lib/main_standalone.dart b/rohd_devtools_extension/lib/main_standalone.dart new file mode 100644 index 000000000..f42e8c4f6 --- /dev/null +++ b/rohd_devtools_extension/lib/main_standalone.dart @@ -0,0 +1,52 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// main_standalone.dart +// Unified standalone entry point for both web and native (Linux/macOS/ +// Windows) builds. The platform-appropriate [VmConnectionStrategy] is +// selected via conditional imports in +// `rohd_devtools/services/platform_vm_connection_strategy.dart`. +// +// Run on web: flutter run -d web-server lib/main_standalone.dart +// Run on Linux: flutter run -d linux lib/main_standalone.dart +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/services.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/standalone_app_shell.dart'; + +/// Entry point for the standalone ROHD DevTools app. +void main(List args) { + _setupLogging(); + + final config = StandaloneAppConfig( + title: 'ROHD DevTools', + connectionStrategy: createPlatformVmConnectionStrategy(), + ); + + debugPrint( + '[main_standalone] Starting ROHD DevTools ' + '(${kIsWeb ? "Web" : "Native"})...', + ); + runApp(StandaloneRohdDevToolsApp(config: config)); +} + +void _setupLogging() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((record) { + final ts = record.time.toIso8601String(); + debugPrint( + '[$ts] [${record.loggerName}] ${record.level.name}: ${record.message}', + ); + if (record.error != null) { + debugPrint(' error: ${record.error}'); + } + if (record.stackTrace != null) { + debugPrint(' stack: ${record.stackTrace}'); + } + }); +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/const/app_theme.dart b/rohd_devtools_extension/lib/rohd_devtools/const/app_theme.dart new file mode 100644 index 000000000..2c6df9e89 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/const/app_theme.dart @@ -0,0 +1,193 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// app_theme.dart +// Centralized theme definitions for ROHD DevTools. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; + +const _fontFallback = ['Noto Color Emoji']; + +TextTheme _withFontFallback(TextTheme theme) => theme.apply( + fontFamilyFallback: _fontFallback, + ); + +/// Dark theme colors +class DarkThemeColors { + /// Colors matching VS Code dark theme. + static const scaffoldBackground = Color(0xFF1E1E1E); + + /// Card background color. + static const cardBackground = Color(0xFF252526); + + /// Panel background color. + static const panelBackground = Color(0xFF252526); + + /// Panel header color. + static const panelHeader = Color(0xFF333333); + + /// Divider color. + static const divider = Color(0xFF3C3C3C); + + /// Primary text color. + static const text = Colors.white; + + /// Secondary text color. + static const textSecondary = Colors.white70; + + /// AppBar background color. + static const appBarBackground = Color(0xFF252526); +} + +/// Light theme colors +class LightThemeColors { + /// Slightly darker than white + /// to reduce eye strain. + static const scaffoldBackground = Color(0xFFE8E8E8); + + /// Card background color. + static const cardBackground = Colors.white; + + /// Panel background color. + static const panelBackground = Color(0xFFFAFAFA); + + /// Panel header color. + static const panelHeader = Color(0xFFF5F5F5); + + /// Divider color. + static const divider = Colors.black26; + + /// Primary text color. + static const text = Colors.black87; + + /// Secondary text color. + static const textSecondary = Colors.black54; + + /// AppBar background color. + static const appBarBackground = Color(0xFFF5F5F5); +} + +/// AppBar themes +class AppBarThemes { + /// Dark theme AppBar - matches VS Code dark theme + static const dark = AppBarTheme( + backgroundColor: DarkThemeColors.appBarBackground, + foregroundColor: DarkThemeColors.text, + elevation: 0, + shadowColor: Colors.transparent, + ); + + /// Light theme AppBar + static const light = AppBarTheme( + backgroundColor: LightThemeColors.appBarBackground, + foregroundColor: LightThemeColors.text, + elevation: 0, + shadowColor: Colors.transparent, + ); +} + +/// Build dark theme data +ThemeData buildDarkTheme() => ThemeData.dark().copyWith( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF4A90A4), + brightness: Brightness.dark, + ), + scaffoldBackgroundColor: DarkThemeColors.scaffoldBackground, + cardColor: DarkThemeColors.cardBackground, + dividerColor: DarkThemeColors.divider, + cardTheme: const CardThemeData( + elevation: 0, + shadowColor: Colors.transparent, + color: DarkThemeColors.cardBackground, + ), + appBarTheme: AppBarThemes.dark, + popupMenuTheme: PopupMenuThemeData( + color: const Color(0xFF3C3C3C), + elevation: 8, + shadowColor: Colors.black54, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.white.withValues(alpha: 0.1)), + ), + textStyle: const TextStyle(color: Colors.white, fontSize: 13), + ), + dialogTheme: DialogThemeData( + backgroundColor: const Color(0xFF2D2D30).withValues(alpha: 0.90), + elevation: 16, + shadowColor: Colors.black54, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withValues(alpha: 0.08)), + ), + titleTextStyle: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + contentTextStyle: const TextStyle(color: Colors.white70, fontSize: 14), + ), + // Disable hover effects (workaround for Flutter #172079) + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + splashFactory: NoSplash.splashFactory, + textTheme: _withFontFallback(ThemeData.dark().textTheme), + primaryTextTheme: _withFontFallback(ThemeData.dark().primaryTextTheme), + ); + +/// Build light theme data +ThemeData buildLightTheme() => ThemeData.light().copyWith( + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4A90A4)), + scaffoldBackgroundColor: LightThemeColors.scaffoldBackground, + cardColor: LightThemeColors.cardBackground, + dividerColor: LightThemeColors.divider, + cardTheme: CardThemeData( + elevation: 2, + shadowColor: Colors.black.withValues(alpha: 0.2), + color: LightThemeColors.cardBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.black.withValues(alpha: 0.1)), + ), + ), + appBarTheme: AppBarThemes.light, + popupMenuTheme: PopupMenuThemeData( + color: Colors.white.withValues(alpha: 0.85), + elevation: 8, + shadowColor: Colors.black26, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Colors.black.withValues(alpha: 0.12)), + ), + textStyle: const TextStyle(color: Colors.black87, fontSize: 13), + ), + dialogTheme: DialogThemeData( + backgroundColor: Colors.white, + elevation: 16, + shadowColor: Colors.black26, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.black.withValues(alpha: 0.1)), + ), + titleTextStyle: const TextStyle( + color: Colors.black87, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + contentTextStyle: const TextStyle(color: Colors.black54, fontSize: 14), + ), + // Disable hover effects (workaround for Flutter #172079) + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + splashFactory: NoSplash.splashFactory, + textTheme: _withFontFallback(ThemeData.light().textTheme), + primaryTextTheme: _withFontFallback(ThemeData.light().primaryTextTheme), + ); diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/cubits.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/cubits.dart new file mode 100644 index 000000000..18c07803d --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/cubits.dart @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// cubits.dart +// Barrel file for rohd_devtools cubits. + +export 'details_tab_cubit.dart'; +export 'rohd_service_cubit.dart'; +export 'selected_module_cubit.dart'; +export 'signal_search_term_cubit.dart'; +export 'snapshot_cubit.dart'; +export 'theme_cubit.dart'; +export 'tree_search_term_cubit.dart'; diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/details_tab_cubit.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/details_tab_cubit.dart new file mode 100644 index 000000000..8ab1cabb0 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/details_tab_cubit.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// details_tab_cubit.dart +// Cubit for managing the selected tab in module details view. +// +// 2025 January 12 +// Author: Desmond Kirkpatrick + +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Enum representing the available tabs in the module details view. +enum DetailsTab { + /// Details tab showing module information. + details, + + /// Waveform tab showing signal waveforms. + waveform, + + /// Schematic tab showing module schematics. + schematic, +} + +/// Cubit for managing the selected tab state. +class DetailsTabCubit extends Cubit { + /// Initializes the cubit with the default tab as [DetailsTab.details]. + DetailsTabCubit() : super(DetailsTab.details); + + /// Sets the currently selected tab. + void selectTab(DetailsTab tab) => emit(tab); +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_cubit.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_cubit.dart index 2b8b70b79..6850dcd7b 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_cubit.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_cubit.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // rohd_service_cubit.dart @@ -7,28 +7,137 @@ // 2025 January 28 // Author: Roberto Torres +import 'dart:async'; + import 'package:devtools_app_shared/service.dart'; -import 'package:devtools_extensions/devtools_extensions.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:devtools_app_shared/utils.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/service_manager_bridge.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/signal_value_source.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/signal_value_source_binding.dart'; import 'package:rohd_devtools_extension/rohd_devtools/services/tree_service.dart'; +import 'package:vm_service/vm_service.dart' as vm; part 'rohd_service_state.dart'; +/// Cubit for managing ROHD service state. class RohdServiceCubit extends Cubit { + final bool _manageServiceManager; + ServiceManager? _localServiceManager; + + /// Completer used to signal teardown of the standalone VM service to the + /// local [ServiceManager]. + Completer? _localServiceClosedSignal; + + /// The TreeService instance for ROHD. TreeService? treeService; - RohdServiceCubit() : super(RohdServiceInitial()) { - evalModuleTree(); + /// Shared value source for live signal overlays, when available. + SignalValueSource? get signalValueSource => _signalValueSource; + SignalValueSource? _signalValueSource; + + /// The discovered ROHD isolate ID. + /// + /// Exposed so other consumers (e.g. waveform data source) can target the + /// same isolate that contains the ROHD inspector_service library. + String? get rohdIsolateId => _rohdIsolateId; + String? _rohdIsolateId; + + /// Listener for service connection state changes. + void Function()? _connectionListener; + + /// Constructor for RohdServiceCubit. + RohdServiceCubit({bool manageServiceManager = true}) + : _manageServiceManager = manageServiceManager, + super(RohdServiceInitial()) { + if (_manageServiceManager) { + _connectionListener = _onConnectionStateChanged; + serviceManager.connectedState.addListener(_connectionListener!); + if (serviceManager.connectedState.value.connected) { + unawaited(Future.microtask(evalModuleTree)); + } + } + } + + /// Configure a standalone VM service session without relying on the global + /// DevTools extension [serviceManager]. + Future configureStandaloneVmService( + vm.VmService vmService, String isolateId) async { + _rohdIsolateId = null; + _disposeSignalValueSource(); + + if (_localServiceManager != null && + _localServiceClosedSignal != null && + !_localServiceClosedSignal!.isCompleted) { + _localServiceClosedSignal!.complete(); + } + + _localServiceManager = ServiceManager(); + final localManager = _localServiceManager!; + _localServiceClosedSignal = Completer(); + + await localManager.vmServiceOpened(vmService, + onClosed: _localServiceClosedSignal!.future); + + final vmInfo = await vmService.getVM(); + final isolates = vmInfo.isolates ?? const []; + for (final ref in isolates) { + if (ref.id == isolateId) { + localManager.isolateManager.selectIsolate(ref); + break; + } + } + + treeService = null; + _rohdIsolateId = null; } + void _onConnectionStateChanged() { + final connected = serviceManager.connectedState.value.connected; + debugPrint('[RohdServiceCubit] Connection state changed: ' + 'connected=$connected'); + if (connected) { + // Reset tree service so we use the new connection + treeService = null; + unawaited(evalModuleTree()); + } else { + // VM disconnected — reset so tree page can tear down waveforms + // and other stale references. + treeService = null; + _disposeSignalValueSource(); + _rohdIsolateId = null; + emit(RohdServiceInitial()); + } + } + + @override + Future close() { + if (_connectionListener != null && _manageServiceManager) { + serviceManager.connectedState.removeListener(_connectionListener!); + _connectionListener = null; + } + if (_localServiceClosedSignal != null && + !_localServiceClosedSignal!.isCompleted) { + _localServiceClosedSignal!.complete(); + } + _disposeSignalValueSource(); + _localServiceManager = null; + return super.close(); + } + + /// Evaluate the module tree from the ROHD service. Future evalModuleTree() async { + debugPrint('[RohdServiceCubit] evalModuleTree called'); await _handleModuleTreeOperation( (treeService) => treeService.evalModuleTree()); } + /// Refresh the module tree from the ROHD service. Future refreshModuleTree() async { + debugPrint('[RohdServiceCubit] refreshModuleTree called'); await _handleModuleTreeOperation( (treeService) => treeService.refreshModuleTree()); } @@ -36,22 +145,102 @@ class RohdServiceCubit extends Cubit { Future _handleModuleTreeOperation( Future Function(TreeService) operation) async { try { + debugPrint( + '[RohdServiceCubit] _handleModuleTreeOperation - emitting loading'); emit(RohdServiceLoading()); - if (serviceManager.service == null) { - throw Exception('ServiceManager is not initialized'); + + final activeServiceManager = + _manageServiceManager ? serviceManager : _localServiceManager; + final activeService = activeServiceManager?.service; + + if (activeService == null) { + debugPrint('[RohdServiceCubit] ServiceManager is not initialized - ' + 'emitting loaded with null'); + // When not running in DevTools, just emit loaded with null tree + // This prevents constant error states and allows the UI to work + emit(const RohdServiceLoaded(null)); + return; + } + + debugPrint('[RohdServiceCubit] Creating TreeService...'); + if (treeService == null) { + // Find the isolate that actually has the ROHD library loaded. + // With `dart test`, the DevTools "selected" isolate is often the + // test-runner controller which doesn't import package:rohd. We + // need to scan all isolates to find the one with inspector_service. + final service = activeService; + ValueListenable? rohdIsolate; + + try { + final vmInfo = await service.getVM(); + final isolates = vmInfo.isolates ?? []; + debugPrint('[RohdServiceCubit] Scanning ${isolates.length} ' + 'isolate(s) for ROHD library...'); + + for (final isoRef in isolates) { + final id = isoRef.id; + if (id == null) { + continue; + } + try { + final iso = await service.getIsolate(id); + final libs = iso.libraries ?? []; + debugPrint('[RohdServiceCubit] Isolate ${isoRef.name} ' + '(${isoRef.id}): ${libs.length} libraries'); + final hasRohd = libs.any((lib) => + lib.uri == + 'package:rohd/src/diagnostics/inspector_service.dart'); + if (hasRohd) { + debugPrint('[RohdServiceCubit] → Found ROHD in ' + '${isoRef.name}'); + rohdIsolate = ValueNotifier(isoRef); + _rohdIsolateId = id; + break; + } + } on Exception catch (e) { + debugPrint('[RohdServiceCubit] Isolate ${isoRef.name} ' + 'scan error: $e'); + } + } + } on Exception catch (e) { + debugPrint('[RohdServiceCubit] VM scan failed: $e'); + } + + if (rohdIsolate == null) { + debugPrint('[RohdServiceCubit] ROHD isolate not found, ' + 'falling back to selected isolate'); + } + + treeService = TreeService( + EvalOnDartLibrary( + 'package:rohd/src/diagnostics/inspector_service.dart', service, + serviceManager: activeServiceManager!, isolate: rohdIsolate), + Disposable(), + vmService: service, + isolateId: _rohdIsolateId); + + _disposeSignalValueSource(); + _signalValueSource = createSignalValueSourceBinding( + treeService: treeService!, vmService: service); } - treeService ??= TreeService( - EvalOnDartLibrary( - 'package:rohd/src/diagnostics/inspector_service.dart', - serviceManager.service!, - serviceManager: serviceManager, - ), - Disposable(), - ); + + debugPrint('[RohdServiceCubit] Calling operation...'); final treeModel = await operation(treeService!); + + debugPrint('[RohdServiceCubit] Operation complete, emitting loaded'); emit(RohdServiceLoaded(treeModel)); - } catch (error, trace) { + } on Exception catch (error, trace) { + debugPrint('[RohdServiceCubit] Error: $error'); + // Reset treeService so next attempt re-scans for the ROHD isolate. + treeService = null; + _disposeSignalValueSource(); + _rohdIsolateId = null; emit(RohdServiceError(error.toString(), trace)); } } + + void _disposeSignalValueSource() { + unawaited(disposeSignalValueSourceBinding(_signalValueSource)); + _signalValueSource = null; + } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_state.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_state.dart index c6239e7c9..f31255819 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_state.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/rohd_service_state.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // rohd_service_state.dart @@ -9,30 +9,42 @@ part of 'rohd_service_cubit.dart'; +/// Base state for ROHD service loading and error handling. abstract class RohdServiceState extends Equatable { + /// Creates a ROHD service state. const RohdServiceState(); @override List get props => []; } +/// Initial state before any ROHD service activity occurs. class RohdServiceInitial extends RohdServiceState {} +/// State emitted while loading ROHD service data. class RohdServiceLoading extends RohdServiceState {} +/// State emitted after ROHD service data has been loaded. class RohdServiceLoaded extends RohdServiceState { + /// Loaded module tree data, if available. final TreeModel? treeModel; + /// Creates a loaded state with tree data. const RohdServiceLoaded(this.treeModel); @override List get props => [treeModel]; } +/// State emitted when ROHD service loading fails. class RohdServiceError extends RohdServiceState { + /// Error message. final String error; + + /// Stack trace associated with the failure. final StackTrace trace; + /// Creates an error state. const RohdServiceError(this.error, this.trace); @override diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_cubit.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_cubit.dart index 500d0661f..8df1df7f9 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_cubit.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_cubit.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // selected_module_cubit.dart @@ -7,15 +7,18 @@ // 2025 January 28 // Author: Roberto Torres -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; part 'selected_module_state.dart'; +/// Cubit that tracks which module is currently selected. class SelectedModuleCubit extends Cubit { + /// Creates the selected-module cubit. SelectedModuleCubit() : super(SelectedModuleInitial()); + /// Selects a module and emits the loaded state. void setModule(TreeModel module) { emit(SelectedModuleLoaded(module)); } diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_state.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_state.dart index 513c94758..8201a609d 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_state.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/selected_module_state.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // selected_module_state.dart @@ -9,18 +9,24 @@ part of 'selected_module_cubit.dart'; +/// Base state for the currently selected module. abstract class SelectedModuleState extends Equatable { + /// Creates a selected-module state. const SelectedModuleState(); @override List get props => []; } +/// State emitted when no module is selected. class SelectedModuleInitial extends SelectedModuleState {} +/// State emitted when a module has been selected. class SelectedModuleLoaded extends SelectedModuleState { + /// The currently selected module. final TreeModel module; + /// Creates a loaded state with the selected module. const SelectedModuleLoaded(this.module); @override diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/signal_search_term_cubit.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/signal_search_term_cubit.dart index 15a8edbb7..af3cbdc7f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/cubit/signal_search_term_cubit.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/signal_search_term_cubit.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // signal_search_term_cubit.dart @@ -9,9 +9,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +/// Cubit that stores the current signal-table search term. class SignalSearchTermCubit extends Cubit { + /// Creates the signal-search cubit with no initial term. SignalSearchTermCubit() : super(null); + /// Updates the search term. void setTerm(String term) { emit(term); } diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/snapshot_cubit.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/snapshot_cubit.dart new file mode 100644 index 000000000..88c77d6c8 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/snapshot_cubit.dart @@ -0,0 +1,233 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// snapshot_cubit.dart +// Cubit for managing signal value snapshots at a point in time. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/signal_value_source.dart'; + +/// Operating mode for the snapshot system. +enum SignalTrackingMode { + /// Camera mode — user manually takes a one-shot snapshot at the marker + /// time by pressing the snapshot button. + camera, + + /// Video mode — automatically tracks the latest signal values from the + /// ROHD debugger. Each time a value update arrives, a snapshot is taken. + video, +} + +/// Represents a single signal's value at the snapshot time. +class SignalSnapshot extends Equatable { + /// The signal ID or full path. + final String signalId; + + /// The display name of the signal. + final String name; + + /// The signal value at the snapshot time. + final String value; + + /// The bit width of the signal. + final int width; + + /// The direction of the signal (input/output/inout), null if internal. + final String? direction; + + /// Whether this value was computed extension-side. + final bool computed; + + /// Creates a [SignalSnapshot]. + const SignalSnapshot({ + required this.signalId, + required this.name, + required this.value, + required this.width, + this.direction, + this.computed = false, + }); + + @override + List get props => [ + signalId, + name, + value, + width, + direction, + computed, + ]; +} + +/// State for the [SnapshotCubit]. +sealed class SnapshotState extends Equatable { + /// Creates a [SnapshotState]. + const SnapshotState(); +} + +/// No snapshot has been taken yet. +class SnapshotInitial extends SnapshotState { + /// Creates a [SnapshotInitial]. + const SnapshotInitial(); + + @override + List get props => []; +} + +/// A snapshot is currently being fetched. +class SnapshotLoading extends SnapshotState { + /// The time being queried. + final int time; + + /// Creates a [SnapshotLoading]. + const SnapshotLoading(this.time); + + @override + List get props => [time]; +} + +/// A snapshot has been successfully fetched. +class SnapshotLoaded extends SnapshotState { + /// The time at which the snapshot was taken, in source-provided units. + final int time; + + /// Map of signal ID to [SignalSnapshot]. + final Map signals; + + /// Creates a [SnapshotLoaded]. + const SnapshotLoaded({required this.time, required this.signals}); + + @override + List get props => [time, signals]; + + /// Look up a signal's snapshot value by signal ID. + SignalSnapshot? getSignal(String signalId) => signals[signalId]; + + /// Look up a signal's snapshot value by signal name. + SignalSnapshot? getSignalByName(String name) { + for (final snapshot in signals.values) { + if (snapshot.name == name) { + return snapshot; + } + } + return null; + } +} + +/// An error occurred while fetching the snapshot. +class SnapshotError extends SnapshotState { + /// The error message. + final String message; + + /// Creates a [SnapshotError]. + const SnapshotError(this.message); + + @override + List get props => [message]; +} + +/// Cubit that manages signal value snapshots. +class SnapshotCubit extends Cubit { + /// Creates a snapshot cubit. + SnapshotCubit() : super(const SnapshotInitial()); + + /// The current operating mode. + SignalTrackingMode _mode = SignalTrackingMode.camera; + + /// The current operating mode. + SignalTrackingMode get mode => _mode; + + StreamSubscription? _liveUpdatesSub; + + /// Switch between camera and video mode. + void setMode(SignalTrackingMode mode) { + if (_mode == mode) { + return; + } + _mode = mode; + if (mode == SignalTrackingMode.camera) { + _stopVideoTracking(); + } + debugPrint('[SnapshotCubit] Mode changed to ${mode.name}'); + } + + /// Start video-mode tracking from a shared value source. + void startVideoTracking(SignalValueSource source) { + _stopVideoTracking(); + final liveUpdates = source.updates; + if (liveUpdates == null) { + debugPrint('[SnapshotCubit] Video tracking unavailable for source'); + return; + } + _liveUpdatesSub = liveUpdates.listen( + (event) { + if (_mode == SignalTrackingMode.video && + event.upToTime > 0 && + event.hasData) { + unawaited(takeSnapshot(source, event.upToTime)); + } + }, + onError: (Object e) { + debugPrint('[SnapshotCubit] Video tracking error: $e'); + }, + ); + } + + void _stopVideoTracking() { + unawaited(_liveUpdatesSub?.cancel()); + _liveUpdatesSub = null; + } + + /// Take a snapshot of all signal values at the given [time]. + Future takeSnapshot(SignalValueSource source, int time) async { + emit(SnapshotLoading(time)); + + try { + final rawData = await source.getSnapshot(time); + + if (rawData == null) { + emit(const SnapshotError('No snapshot data returned')); + return; + } + + final signals = {}; + for (final entry in rawData.entries) { + final signalId = entry.key; + final data = entry.value; + signals[signalId] = SignalSnapshot( + signalId: signalId, + name: (data['name'] as String?) ?? signalId, + value: (data['value'] as String?) ?? '?', + width: (data['width'] as int?) ?? 1, + direction: data['direction'] as String?, + computed: data['computed'] as bool? ?? false, + ); + } + + emit(SnapshotLoaded(time: time, signals: signals)); + } on Object catch (e) { + debugPrint('[SnapshotCubit] Error taking snapshot: $e'); + emit(SnapshotError('Failed to take snapshot: $e')); + } + } + + /// Clear the current snapshot, reset mode, and stop video tracking. + void clear() { + _stopVideoTracking(); + _mode = SignalTrackingMode.camera; + emit(const SnapshotInitial()); + } + + @override + Future close() { + _stopVideoTracking(); + return super.close(); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/theme_cubit.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/theme_cubit.dart new file mode 100644 index 000000000..34fdd868f --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/theme_cubit.dart @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// theme_cubit.dart +// Manages light/dark theme toggle for ROHD DevTools. + +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Enum for theme modes. +enum DevToolsThemeMode { + /// Light theme mode. + light, + + /// Dark theme mode. + dark, +} + +/// Cubit for managing DevTools theme state. +class DevToolsThemeCubit extends Cubit { + /// Constructor for [DevToolsThemeCubit]. + DevToolsThemeCubit() : super(DevToolsThemeMode.dark); + + /// Toggle between light and dark themes. + void toggleTheme() { + emit( + state == DevToolsThemeMode.dark + ? DevToolsThemeMode.light + : DevToolsThemeMode.dark, + ); + } + + /// Set a specific theme mode. + void setTheme(DevToolsThemeMode mode) { + emit(mode); + } + + /// Whether the current theme is dark. + bool get isDark => state == DevToolsThemeMode.dark; +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/cubit/tree_search_term_cubit.dart b/rohd_devtools_extension/lib/rohd_devtools/cubit/tree_search_term_cubit.dart index 0ce2c1933..133be9569 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/cubit/tree_search_term_cubit.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/cubit/tree_search_term_cubit.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // tree_search_term_cubit.dart @@ -9,9 +9,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +/// Cubit that stores the current tree search term. class TreeSearchTermCubit extends Cubit { + /// Creates the tree-search cubit with no initial term. TreeSearchTermCubit() : super(null); + /// Updates the search term. void setTerm(String term) { emit(term); } diff --git a/rohd_devtools_extension/lib/rohd_devtools/models/dtd_vm_service_info.dart b/rohd_devtools_extension/lib/rohd_devtools/models/dtd_vm_service_info.dart new file mode 100644 index 000000000..2fbbdf241 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/models/dtd_vm_service_info.dart @@ -0,0 +1,74 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// dtd_vm_service_info.dart +// Thin wrapper around the SDK's VmServiceInfo that adds UI state +// (autoReconnect, isAlive) for use in the connection form and +// auto-reconnect logic. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:dtd/dtd.dart'; + +/// Information about a discovered VM service, wrapping the SDK's +/// VmServiceInfo with additional UI-specific mutable state. +/// +/// Used by the VM connection form to display the list of available VMs +/// and by the auto-reconnect logic to match VMs by name. +class DtdVmServiceInfo { + /// The underlying SDK VM service info. + final VmServiceInfo info; + + /// Whether this VM service is currently reachable. + /// + /// Set to `false` by auto-rediscovery when the service is no longer + /// found via the DTD. Dead services are shown grayed-out in the list. + bool isAlive; + + /// Whether to automatically reconnect to this VM by name if it dies + /// and a new VM with the same name appears via DTD discovery. + bool autoReconnect; + + /// Creates a [DtdVmServiceInfo] wrapping the given [info]. + DtdVmServiceInfo({ + required this.info, + this.isAlive = true, + this.autoReconnect = false, + }); + + /// Creates a [DtdVmServiceInfo] from individual fields (convenience). + factory DtdVmServiceInfo.fromFields({ + required String uri, + String? name, + String? exposedUri, + bool isAlive = true, + bool autoReconnect = false, + }) => + DtdVmServiceInfo( + info: VmServiceInfo(uri: uri, exposedUri: exposedUri, name: name), + isAlive: isAlive, + autoReconnect: autoReconnect, + ); + + /// Human-readable name (may be null). + String? get name => info.name; + + /// Direct VM service URI. + String get uri => info.uri; + + /// Exposed/forwarded URI (preferred over [uri] when available). + String? get exposedUri => info.exposedUri; + + /// The URI to use for connection (prefers exposedUri). + String get connectionUri => exposedUri ?? uri; + + /// A compact display label. + String get displayLabel { + final label = name ?? 'VM Service'; + final uriLabel = connectionUri.length > 50 + ? '${connectionUri.substring(0, 50)}…' + : connectionUri; + return '$label — $uriLabel'; + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/models/signal_model.dart b/rohd_devtools_extension/lib/rohd_devtools/models/signal_model.dart index 3cfa0023f..a028d6068 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/models/signal_model.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/models/signal_model.dart @@ -1,40 +1,41 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // signal_model.dart -// Model of the signal to be tabulate on the detail table. +// Model of the signal shown in the details table. // // 2024 January 5 // Author: Yao Jing Quek +/// Model of a signal shown in the details table. class SignalModel { + /// Signal name. final String name; + + /// Signal direction label. final String direction; + + /// Signal value rendered as text. final String value; + + /// Signal bit width. final int width; - SignalModel({ - required this.name, - required this.direction, - required this.value, - required this.width, - }); + /// Creates a signal model. + SignalModel( + {required this.name, + required this.direction, + required this.value, + required this.width}); - factory SignalModel.fromMap(Map map) { - return SignalModel( + /// Builds a signal model from a map representation. + factory SignalModel.fromMap(Map map) => SignalModel( name: map['name'] as String, direction: map['direction'] as String, value: map['value'] as String, - width: map['width'] as int, - ); - } + width: map['width'] as int); - Map toMap() { - return { - 'name': name, - 'direction': direction, - 'value': value, - 'width': width, - }; - } + /// Converts the signal model to a JSON-compatible map. + Map toMap() => + {'name': name, 'direction': direction, 'value': value, 'width': width}; } diff --git a/rohd_devtools_extension/lib/rohd_devtools/models/tree_model.dart b/rohd_devtools_extension/lib/rohd_devtools/models/tree_model.dart index f6f60553b..4ef6820c4 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/models/tree_model.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/models/tree_model.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // tree_model.dart @@ -9,51 +9,83 @@ import 'package:rohd_devtools_extension/rohd_devtools/models/signal_model.dart'; +/// Hierarchical model of a ROHD module tree. class TreeModel { + /// Module name. final String name; + + /// Input signals for the module. final List inputs; + + /// Output signals for the module. final List outputs; + + /// Inout signals for the module. + final List inouts; + + /// Child submodules contained by this module. final List subModules; - TreeModel({ - required this.name, - required this.inputs, - required this.outputs, - required this.subModules, - }); + /// Creates a tree model for a module hierarchy node. + TreeModel( + {required this.name, + required this.inputs, + required this.outputs, + required this.subModules, + this.inouts = const []}); + /// Builds a tree model from a JSON map. factory TreeModel.fromJson(Map json) { - List inputSignalsList = []; - List outputSignalsList = []; + final inputSignalsList = []; + final outputSignalsList = []; + final inoutSignalsList = []; + final inputsJson = json['inputs'] as Map; + final outputsJson = json['outputs'] as Map; + final inoutsJson = json['inouts'] as Map? ?? {}; - for (var inputSignal in json['inputs'].entries) { - SignalModel signal = SignalModel.fromMap({ + for (final inputSignal in inputsJson.entries) { + final inputValue = inputSignal.value as Map; + final signal = SignalModel.fromMap({ 'name': inputSignal.key, 'direction': 'Input', - 'value': inputSignal.value['value'], - 'width': inputSignal.value['width'], + 'value': inputValue['value'], + 'width': inputValue['width'] }); inputSignalsList.add(signal); } - for (var outputSignal in json['outputs'].entries) { - SignalModel signal = SignalModel.fromMap({ + for (final outputSignal in outputsJson.entries) { + final outputValue = outputSignal.value as Map; + final signal = SignalModel.fromMap({ 'name': outputSignal.key, - 'direction': 'Input', - 'value': outputSignal.value['value'], - 'width': outputSignal.value['width'], + 'direction': 'Output', + 'value': outputValue['value'], + 'width': outputValue['width'] }); outputSignalsList.add(signal); } + for (final inoutSignal in inoutsJson.entries) { + final inoutValue = inoutSignal.value as Map; + final signal = SignalModel.fromMap({ + 'name': inoutSignal.key, + 'direction': 'Inout', + 'value': inoutValue['value'], + 'width': inoutValue['width'] + }); + + inoutSignalsList.add(signal); + } + return TreeModel( - name: json['name'], - inputs: inputSignalsList, - outputs: outputSignalsList, - subModules: (json["subModules"] as List) - .map((subModule) => TreeModel.fromJson(subModule)) - .toList(), - ); + name: json['name'] as String, + inputs: inputSignalsList, + outputs: outputSignalsList, + inouts: inoutSignalsList, + subModules: (json['subModules'] as List) + .map((subModule) => + TreeModel.fromJson(subModule as Map)) + .toList()); } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/rohd_devtools.dart b/rohd_devtools_extension/lib/rohd_devtools/rohd_devtools.dart index 3bc48ff96..712b5587f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/rohd_devtools.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/rohd_devtools.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // rohd_devtools.dart @@ -6,8 +6,11 @@ // 2025 January 28 // Author: Roberto Torres +export 'cubit/details_tab_cubit.dart'; export 'cubit/rohd_service_cubit.dart'; export 'cubit/selected_module_cubit.dart'; export 'cubit/signal_search_term_cubit.dart'; +export 'cubit/snapshot_cubit.dart'; +export 'cubit/theme_cubit.dart'; export 'cubit/tree_search_term_cubit.dart'; export 'view/view.dart'; diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/connection_state_machine.dart b/rohd_devtools_extension/lib/rohd_devtools/services/connection_state_machine.dart new file mode 100644 index 000000000..5f5f2789d --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/connection_state_machine.dart @@ -0,0 +1,618 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// connection_state_machine.dart +// State machine for managing the lifecycle of VM/DTD connections and +// the associated data (hierarchy, schematic, waveforms). +// +// 2026 March +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:vm_service/vm_service.dart'; + +// --------------------------------------------------------------------------- +// Connection phases +// --------------------------------------------------------------------------- + +/// The coarse-grained connection phase. +/// +/// ```text +/// disconnected ──connect──▶ connecting ──success──▶ connected +/// ▲ │ +/// │ vm dies / user +/// │ disconnects +/// └────────────────────────────────────────────────┘ +/// +/// connected ──pause──▶ paused ──resume──▶ connected +/// │ +/// connected ──vm dies──▶ vmDead ──reconnect──▶ connected +/// ``` +enum ConnectionPhase { + /// No VM connection — user is on the connection page. + disconnected, + + /// WebSocket handshake + isolate discovery in progress. + connecting, + + /// VM connection is live. Sub-state tracked by [DataLoadState]. + connected, + + /// User deliberately paused the connection (data preserved in UI). + paused, + + /// VM was detected as dead (polling or DTD event). + vmDead, +} + +// --------------------------------------------------------------------------- +// Data load states +// --------------------------------------------------------------------------- + +/// What data has been successfully loaded from the VM. +/// +/// These flags are orthogonal — hierarchy and waveforms load independently. +/// The state machine uses them to decide what still needs loading when a +/// debug pause event arrives. +class DataLoadState { + /// Whether the module tree hierarchy has been loaded. + bool hierarchyLoaded; + + /// Whether schematic JSON has been loaded. + bool schematicLoaded; + + /// Whether initial waveform data has been fetched. + bool waveformDataLoaded; + + /// Whether we've attempted hierarchy loading and it returned null + /// (the ROHD app may not have finished building ModuleTree yet). + bool hierarchyAttempted; + + /// Creates a data-load snapshot. + DataLoadState({ + this.hierarchyLoaded = false, + this.schematicLoaded = false, + this.waveformDataLoaded = false, + this.hierarchyAttempted = false, + }); + + /// True when all essential data is present. + bool get isFullyLoaded => hierarchyLoaded; + + /// True when no data has been loaded yet. + bool get isEmpty => + !hierarchyLoaded && !schematicLoaded && !waveformDataLoaded; + + /// Reset all flags (e.g. on full reconnect to a new VM process). + /// Resets all data-load flags. + void reset() { + hierarchyLoaded = false; + schematicLoaded = false; + waveformDataLoaded = false; + hierarchyAttempted = false; + } + + /// Copy constructor for snapshotting state. + /// Creates a copy of this data-load snapshot. + DataLoadState copy() => DataLoadState( + hierarchyLoaded: hierarchyLoaded, + schematicLoaded: schematicLoaded, + waveformDataLoaded: waveformDataLoaded, + hierarchyAttempted: hierarchyAttempted, + ); + + @override + + /// Returns a debug string summarizing the current data-load state. + String toString() => 'DataLoadState(' + 'hierarchy=${hierarchyLoaded ? "✓" : hierarchyAttempted ? "✗" : "–"}, ' + 'schematic=${schematicLoaded ? "✓" : "–"}, ' + 'wfData=${waveformDataLoaded ? "✓" : "–"})'; +} + +// --------------------------------------------------------------------------- +// Connection identity +// --------------------------------------------------------------------------- + +/// Identity of a VM connection for detecting same-process reconnects. +class VmIdentity { + /// The VM service URI. + final String uri; + + /// The isolate ID (unique per VM process). + final String isolateId; + + /// Human-readable VM name (from DTD discovery). + final String? vmName; + + /// Creates a VM identity. + const VmIdentity({required this.uri, required this.isolateId, this.vmName}); + + /// Whether [other] represents the same running VM process. + /// + /// The Dart VM assigns a new isolate ID for every process, so matching + /// IDs prove the same process is still alive. + /// Returns whether [other] refers to the same VM process. + bool isSameProcess(VmIdentity other) => isolateId == other.isolateId; + + @override + + /// Returns a debug string describing this VM identity. + String toString() => 'VmIdentity(uri=$uri, isolate=$isolateId, ' + 'name=$vmName)'; +} + +// --------------------------------------------------------------------------- +// Events / transitions +// --------------------------------------------------------------------------- + +/// Events that drive the state machine. +/// +/// Each event carries any data needed for the transition. +sealed class ConnectionEvent { + const ConnectionEvent(); +} + +/// User initiated a connection to a VM service URI. +class ConnectRequested extends ConnectionEvent { + /// The URI requested by the user. + final String uri; + + /// Creates a connect-request event. + const ConnectRequested(this.uri); +} + +/// VM connection succeeded. +class ConnectionEstablished extends ConnectionEvent { + /// The connected VM service. + final VmService vmService; + + /// Identity of the connected VM. + final VmIdentity identity; + + /// Creates a connection-established event. + const ConnectionEstablished(this.vmService, this.identity); +} + +/// VM connection or data loading failed. +class ConnectionFailed extends ConnectionEvent { + /// Error message describing the failure. + final String error; + + /// Creates a connection-failed event. + const ConnectionFailed(this.error); +} + +/// User deliberately disconnected. +class DisconnectRequested extends ConnectionEvent { + /// Creates a disconnect-request event. + const DisconnectRequested(); +} + +/// User paused the VM connection (data preserved). +class PauseRequested extends ConnectionEvent { + /// Creates a pause-request event. + const PauseRequested(); +} + +/// User resumed a paused VM connection. +class ResumeRequested extends ConnectionEvent { + /// Creates a resume-request event. + const ResumeRequested(); +} + +/// VM was detected as dead (via polling or DTD event). +class VmDied extends ConnectionEvent { + /// Creates a VM-died event. + const VmDied(); +} + +/// VM came back to life (liveness check recovered). +class VmRecovered extends ConnectionEvent { + /// Creates a VM-recovered event. + const VmRecovered(); +} + +/// A debug pause event was received from the VM (breakpoint, exception, etc). +class DebugPauseReceived extends ConnectionEvent { + /// Kind of debug pause event. + final String kind; + + /// Creates a debug-pause event. + const DebugPauseReceived(this.kind); +} + +/// Hierarchy data was loaded (or attempted and returned null). +class HierarchyLoadResult extends ConnectionEvent { + /// Whether the hierarchy load succeeded. + final bool success; + + /// Creates a hierarchy-load result event. + const HierarchyLoadResult({required this.success}); +} + +/// A DTD event signalled that a new VM registered. +class DtdVmRegistered extends ConnectionEvent { + /// VM service URI reported by DTD. + final String uri; + + /// Optional human-readable name. + final String? name; + + /// Creates a DTD VM registered event. + const DtdVmRegistered(this.uri, {this.name}); +} + +/// A DTD event signalled that a VM was unregistered. +class DtdVmUnregistered extends ConnectionEvent { + /// Creates a DTD VM unregistered event. + const DtdVmUnregistered(); +} + +/// User entered demo/loopback mode. +class DemoModeEntered extends ConnectionEvent { + /// Creates a demo-mode event. + const DemoModeEntered(); +} + +// --------------------------------------------------------------------------- +// State machine +// --------------------------------------------------------------------------- + +/// Callback signature for when the state machine wants the shell to load +/// hierarchy data. +typedef LoadHierarchyCallback = Future Function(); + +/// Callback signature for notifying the shell of state changes. +typedef StateChangeCallback = void Function( + ConnectionPhase phase, DataLoadState dataState); + +/// The connection state machine. +/// +/// Tracks the current [ConnectionPhase], [DataLoadState], and [VmIdentity]. +/// Emits [StateChangeCallback] whenever the state transitions so the UI +/// can update. +/// +/// ## Key design decisions +/// +/// 1. **No spinning on connect**: when the initial hierarchy load returns +/// null, we record `hierarchyAttempted = true` but do NOT retry in a +/// loop. Instead, when a [DebugPauseReceived] event arrives and +/// hierarchy is not yet loaded, we try again (exactly once per pause). +/// +/// 2. **Reconnect identity matching**: on reconnect, if the [VmIdentity] +/// has the same `isolateId` as before, we skip hierarchy/schematic +/// reload (the data is still valid). Only waveform data gets an +/// incremental pull. +/// +/// 3. **DTD events are authoritative**: when DTD says a VM died, we trust +/// it immediately (no additional liveness check). +class ConnectionStateMachine { + ConnectionPhase _phase = ConnectionPhase.disconnected; + final DataLoadState _dataState = DataLoadState(); + VmIdentity? _currentIdentity; + VmIdentity? _lastIdentity; + + /// Subscription to VM debug events for hierarchy-on-pause. + StreamSubscription? _debugEventSubscription; + + /// Debounce timer for debug pause events. + Timer? _pauseDebounceTimer; + static const _pauseDebounceDuration = Duration(milliseconds: 200); + + /// Whether a hierarchy load is currently in progress (prevents + /// concurrent loads from rapid breakpoints). + bool _hierarchyLoadInProgress = false; + + /// Callback invoked when the state machine needs hierarchy data loaded. + LoadHierarchyCallback? onLoadHierarchy; + + /// Callback invoked on every state transition. + StateChangeCallback? onStateChange; + + // ── Public getters ── + + /// Current connection phase. + ConnectionPhase get phase => _phase; + + /// Current data-load snapshot. + DataLoadState get dataState => _dataState; + + /// Identity of the currently connected VM, if any. + VmIdentity? get currentIdentity => _currentIdentity; + + /// Identity from the last successful connection, if any. + VmIdentity? get lastIdentity => _lastIdentity; + + /// Whether we're in a state where data loading makes sense. + bool get canLoadData => + _phase == ConnectionPhase.connected && _currentIdentity != null; + + /// Whether we should attempt hierarchy load on the next debug pause. + bool get shouldLoadHierarchyOnPause => + canLoadData && !_dataState.hierarchyLoaded; + + // ── State transitions ── + + /// Process an event and transition state accordingly. + void handleEvent(ConnectionEvent event) { + final oldPhase = _phase; + final oldDataSnapshot = _dataState.copy(); + + switch (event) { + case ConnectRequested(): + _onConnectRequested(event); + case ConnectionEstablished(): + _onConnectionEstablished(event); + case ConnectionFailed(): + _onConnectionFailed(event); + case DisconnectRequested(): + _onDisconnectRequested(); + case PauseRequested(): + _onPauseRequested(); + case ResumeRequested(): + _onResumeRequested(); + case VmDied(): + _onVmDied(); + case VmRecovered(): + _onVmRecovered(); + case DebugPauseReceived(): + _onDebugPause(event); + case HierarchyLoadResult(): + _onHierarchyLoadResult(event); + case DtdVmRegistered(): + _onDtdVmRegistered(event); + case DtdVmUnregistered(): + _onDtdVmUnregistered(); + case DemoModeEntered(): + _onDemoMode(); + } + + // Notify if anything changed + if (_phase != oldPhase || + _dataState.toString() != oldDataSnapshot.toString()) { + debugPrint('[CSM] ${oldPhase.name} → ${_phase.name} $_dataState'); + onStateChange?.call(_phase, _dataState); + } + } + + // ── Per-event handlers ── + + /// Handles a connect-request event. + void _onConnectRequested(ConnectRequested event) { + _phase = ConnectionPhase.connecting; + } + + /// Handles a successful VM connection. + void _onConnectionEstablished(ConnectionEstablished event) { + final isReconnectSameProcess = + _lastIdentity != null && _lastIdentity!.isSameProcess(event.identity); + + _currentIdentity = event.identity; + _phase = ConnectionPhase.connected; + + if (isReconnectSameProcess) { + // Same VM process — keep existing data, don't reload hierarchy. + debugPrint( + '[CSM] Reconnected to same process ' + '(${event.identity.isolateId}) — preserving data', + ); + } else { + // New process — reset data state so everything gets loaded fresh. + _dataState + ..reset() + ..hierarchyLoaded = false + ..schematicLoaded = false; + _hierarchyLoadInProgress = false; + _pauseDebounceTimer?.cancel(); + debugPrint( + '[CSM] Connected to new process ' + '(${event.identity.isolateId}) — data reset', + ); + } + } + + /// Handles a failed connection attempt. + void _onConnectionFailed(ConnectionFailed event) { + debugPrint('[CSM] Connection failed: ${event.error}'); + _phase = ConnectionPhase.disconnected; + } + + /// Handles a user-requested disconnect. + void _onDisconnectRequested() { + unawaited(_cancelDebugSubscription()); + _lastIdentity = _currentIdentity; + _currentIdentity = null; + _dataState.reset(); + _hierarchyLoadInProgress = false; + _phase = ConnectionPhase.disconnected; + } + + /// Handles a user-requested pause. + void _onPauseRequested() { + unawaited(_cancelDebugSubscription()); + _lastIdentity = _currentIdentity; + _currentIdentity = null; + // Data state is preserved — the UI keeps showing cached data. + _phase = ConnectionPhase.paused; + } + + /// Handles a user-requested resume. + void _onResumeRequested() { + // Phase transition happens when ConnectionEstablished arrives. + _phase = ConnectionPhase.connecting; + } + + /// Handles a VM death notification. + void _onVmDied() { + unawaited(_cancelDebugSubscription()); + _lastIdentity = _currentIdentity; + _hierarchyLoadInProgress = false; + // Data state preserved — UI stays. + _phase = ConnectionPhase.vmDead; + } + + /// Handles a VM recovery notification. + void _onVmRecovered() { + if (_phase == ConnectionPhase.vmDead) { + _phase = ConnectionPhase.connected; + } + } + + /// Handles a debug pause event from the VM. + void _onDebugPause(DebugPauseReceived event) { + if (_phase != ConnectionPhase.connected) { + debugPrint('[CSM] Debug pause ignored — phase is ${_phase.name}'); + return; + } + + debugPrint( + '[CSM] Debug pause (${event.kind}), data: $_dataState, ' + 'shouldLoad=$shouldLoadHierarchyOnPause, ' + 'inProgress=$_hierarchyLoadInProgress', + ); + + // If hierarchy hasn't been loaded yet, try now. + // This is the key behavior: instead of spinning/polling after connect, + // we wait for the first debug pause event and load then. + if (shouldLoadHierarchyOnPause && !_hierarchyLoadInProgress) { + _hierarchyLoadInProgress = true; + debugPrint('[CSM] Hierarchy not loaded — requesting load on pause'); + _scheduleHierarchyLoad(); + } + } + + /// Schedules a debounced hierarchy load. + void _scheduleHierarchyLoad() { + // Debounce: if multiple pause events fire rapidly, only the last + // one triggers a load. + _pauseDebounceTimer?.cancel(); + _pauseDebounceTimer = Timer(_pauseDebounceDuration, _doHierarchyLoad); + } + + /// Performs the actual hierarchy load. + Future _doHierarchyLoad() async { + if (onLoadHierarchy == null) { + _hierarchyLoadInProgress = false; + return; + } + try { + await onLoadHierarchy!(); + } on Exception catch (e) { + debugPrint('[CSM] Hierarchy load failed: $e'); + } finally { + _hierarchyLoadInProgress = false; + } + } + + /// Handles the result of a hierarchy load. + void _onHierarchyLoadResult(HierarchyLoadResult event) { + _dataState.hierarchyAttempted = true; + _dataState.hierarchyLoaded = event.success; + if (event.success) { + debugPrint('[CSM] Hierarchy loaded successfully'); + } else { + debugPrint( + '[CSM] Hierarchy load returned null — will retry on next ' + 'debug pause', + ); + } + } + + /// Handles a DTD VM registration event. + void _onDtdVmRegistered(DtdVmRegistered event) { + // Handled by the shell — the state machine just records the event + // for logging. + debugPrint( + '[CSM] DTD: VM registered at ${event.uri} ' + '(name=${event.name})', + ); + } + + /// Handles a DTD VM unregistration event. + void _onDtdVmUnregistered() { + debugPrint('[CSM] DTD: VM unregistered'); + _onVmDied(); + } + + /// Switches the state machine into demo mode. + void _onDemoMode() { + _currentIdentity = null; + _lastIdentity = null; + // Mark hierarchy as loaded since demo mode provides it synchronously. + _dataState + ..reset() + ..hierarchyLoaded = true + ..schematicLoaded = true; + _phase = ConnectionPhase.connected; + } + + // ── Debug event subscription management ── + + /// Subscribe to VM debug events on the given [vmService]. + /// + /// When a pause event arrives, the state machine checks if hierarchy + /// data is missing and triggers a load. This replaces the old + /// "retry loop with exponential backoff" approach. + /// Subscribes to VM debug events. + Future subscribeToDebugEvents(VmService vmService) async { + await _cancelDebugSubscription(); + // Note: we deliberately do NOT call vmService.streamListen(Debug) here. + // The Debug stream is subscribed by ServiceManager.vmServiceOpened (the + // owner of the connection's stream lifecycle). Calling streamListen + // here as well would race with ServiceManager and produce an + // unhandled `Stream already subscribed (103)` error, because + // ServiceManager issues its streamListen via `unawaited(...)` inside a + // try/catch that only catches synchronous throws. + _debugEventSubscription = vmService.onDebugEvent.listen((event) { + final kind = event.kind; + if (kind == EventKind.kPauseBreakpoint || + kind == EventKind.kPauseException || + kind == EventKind.kPauseInterrupted || + kind == EventKind.kPauseExit) { + handleEvent(DebugPauseReceived(kind ?? 'unknown')); + } + }); + debugPrint('[CSM] Subscribed to debug events'); + } + + /// Cancels the current debug event subscription. + Future _cancelDebugSubscription() async { + _pauseDebounceTimer?.cancel(); + await _debugEventSubscription?.cancel(); + _debugEventSubscription = null; + } + + // ── Convenience queries ── + + /// Whether a reconnect to [identity] should skip hierarchy reload. + /// + /// Returns true when the last known VM has the same isolate ID, + /// meaning the same process is still running and its data hasn't + /// changed. + /// Returns true when hierarchy reload can be skipped for [identity]. + bool shouldSkipHierarchyReload(VmIdentity identity) => + _lastIdentity != null && + _lastIdentity!.isSameProcess(identity) && + _dataState.hierarchyLoaded; + + /// Marks waveform data as loaded. + void markWaveformDataLoaded() { + _dataState.waveformDataLoaded = true; + onStateChange?.call(_phase, _dataState); + } + + /// Marks schematic data as loaded. + void markSchematicLoaded() { + _dataState.schematicLoaded = true; + onStateChange?.call(_phase, _dataState); + } + + /// Disposes timers and subscriptions used by the state machine. + Future dispose() async { + await _cancelDebugSubscription(); + _pauseDebounceTimer?.cancel(); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/io_vm_connection_strategy.dart b/rohd_devtools_extension/lib/rohd_devtools/services/io_vm_connection_strategy.dart new file mode 100644 index 000000000..c8f2c2d27 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/io_vm_connection_strategy.dart @@ -0,0 +1,145 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// io_vm_connection_strategy.dart +// VM connection strategy for native (Linux/macOS/Windows) platforms. +// Uses vm_service_io for WebSocket connection. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; +import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service_io.dart'; + +/// Simple logging class for VM service. +class _StdoutLog extends Log { + final Logger _logger = Logger('VMService'); + @override + void warning(String message) => _logger.warning(message); + + @override + void severe(String message) => _logger.severe(message); +} + +/// VM connection strategy for native platforms (Linux/macOS/Windows). +/// Uses vm_service_io's vmServiceConnectUri. +class IoVmConnectionStrategy extends VmConnectionStrategy { + @override + + /// Connects to a VM service on native platforms. + Future connect(String uri) async { + final normalizedUri = normalizeUri(uri); + + if (normalizedUri == null) { + throw Exception('Invalid URI format'); + } + + final vmService = await vmServiceConnectUri( + normalizedUri.toString(), + log: _StdoutLog(), + ).timeout( + const Duration(seconds: 10), + onTimeout: () => + throw TimeoutException('VM connection timed out after 10 s'), + ); + + final vm = await vmService.getVM().timeout( + const Duration(seconds: 5), + onTimeout: () => throw TimeoutException('getVM timed out after 5 s'), + ); + + // During a debugger restart the VM service endpoint becomes available + // before isolates are created, and the test isolate (which contains + // the ROHD inspector_service library) may lag behind the test-runner + // control isolate. Retry a few times with a short delay so we don't + // fall back to the slow polling reconnect path. + String? isolateId; + const maxRetries = 6; + const retryDelay = Duration(milliseconds: 500); + + for (var attempt = 1; attempt <= maxRetries; attempt++) { + final vmInfo = attempt == 1 + ? vm + : await vmService.getVM().timeout(const Duration(seconds: 3)); + final isolates = vmInfo.isolates ?? []; + + if (isolates.isEmpty) { + if (attempt < maxRetries) { + Logger('VMService').info( + 'No isolates yet (attempt $attempt/$maxRetries) — ' + 'waiting ${retryDelay.inMilliseconds} ms', + ); + await Future.delayed(retryDelay); + continue; + } + throw Exception( + 'No isolates found in the VM after $maxRetries ' + 'attempts (${retryDelay.inMilliseconds * maxRetries} ms)', + ); + } + + // Find the isolate that contains the ROHD inspector_service library. + for (final isolateRef in isolates) { + final id = isolateRef.id; + if (id == null) { + continue; + } + try { + final isolate = await vmService + .getIsolate(id) + .timeout(const Duration(milliseconds: 500)); + final libraries = isolate.libraries ?? []; + final hasRohd = libraries.any( + (lib) => + lib.uri != null && + lib.uri!.contains('rohd') && + lib.uri!.contains('inspector_service'), + ); + if (hasRohd) { + isolateId = id; + break; + } + } on Exception { + // Isolate not loaded yet or timed out — skip it + continue; + } + } + + if (isolateId != null) { + break; + } + + // Found isolates but none had ROHD — the test isolate may not + // have spawned yet. Retry unless this is the last attempt. + if (attempt < maxRetries) { + Logger('VMService').info( + 'ROHD isolate not found yet (attempt $attempt/$maxRetries, ' + '${isolates.length} isolate(s) seen) — retrying', + ); + await Future.delayed(retryDelay); + continue; + } + + // Last attempt — fall back to first isolate. + final fallback = isolates.first.id; + if (fallback == null) { + throw Exception('First isolate has no ID'); + } + isolateId = fallback; + Logger('VMService').info( + 'Isolate library scan incomplete after $maxRetries attempts — ' + 'using first isolate; evalModuleTree will verify', + ); + } + + return VmConnectionResult(vmService: vmService, isolateId: isolateId!); + } +} + +/// Returns an [IoVmConnectionStrategy]. Used by the conditional-import +/// dispatcher in `platform_vm_connection_strategy.dart`. +VmConnectionStrategy platformVmConnectionStrategy() => IoVmConnectionStrategy(); diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy.dart b/rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy.dart new file mode 100644 index 000000000..59b3ed478 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy.dart @@ -0,0 +1,21 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// platform_vm_connection_strategy.dart +// Conditional-import dispatcher that returns the correct +// [VmConnectionStrategy] for the current platform (IO vs. web). +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:rohd_devtools_extension/rohd_devtools/services/platform_vm_connection_strategy_stub.dart' + if (dart.library.io) 'package:rohd_devtools_extension/rohd_devtools/services/io_vm_connection_strategy.dart' + if (dart.library.js_interop) 'package:rohd_devtools_extension/rohd_devtools/services/web_vm_connection_strategy.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; + +/// Returns the platform-appropriate [VmConnectionStrategy]. +/// +/// On native (`dart:io`) platforms returns the IO strategy; +/// on web (`dart:js_interop`) platforms returns the web strategy. +VmConnectionStrategy createPlatformVmConnectionStrategy() => + platformVmConnectionStrategy(); diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy_stub.dart b/rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy_stub.dart new file mode 100644 index 000000000..8e56964d0 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/platform_vm_connection_strategy_stub.dart @@ -0,0 +1,22 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// platform_vm_connection_strategy_stub.dart +// Stub fallback for [createPlatformVmConnectionStrategy]. +// Real implementations live in [io_vm_connection_strategy.dart] and +// [web_vm_connection_strategy.dart] and are selected via conditional +// imports in [platform_vm_connection_strategy.dart]. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; + +/// Stub that throws when neither `dart:io` nor `dart:js_interop` is available. +/// Returns the platform VM connection strategy, or throws on unsupported +/// targets. +VmConnectionStrategy platformVmConnectionStrategy() { + throw UnsupportedError( + 'No VmConnectionStrategy available for the current platform.', + ); +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge.dart b/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge.dart new file mode 100644 index 000000000..8da322797 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge.dart @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// service_manager_bridge.dart +// Conditional bridge that exports the platform-specific ServiceManager. +// +// 2026 June +// Author: Desmond Kirkpatrick + +export 'service_manager_bridge_io.dart' + if (dart.library.js_interop) 'service_manager_bridge_web.dart'; diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_io.dart b/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_io.dart new file mode 100644 index 000000000..1c470c5db --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_io.dart @@ -0,0 +1,15 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// service_manager_bridge_io.dart +// Native implementation for exposing a local DevTools ServiceManager. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:devtools_app_shared/service.dart'; +import 'package:vm_service/vm_service.dart' as vm; + +/// Native fallback: keep an app-local ServiceManager instance. +final ServiceManager serviceManager = + ServiceManager(); diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_web.dart b/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_web.dart new file mode 100644 index 000000000..67a2683d9 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/service_manager_bridge_web.dart @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// service_manager_bridge_web.dart +// Web implementation that re-exports DevTools extension ServiceManager. +// +// 2026 June +// Author: Desmond Kirkpatrick + +export 'package:devtools_extensions/devtools_extensions.dart' + show serviceManager; diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/services.dart b/rohd_devtools_extension/lib/rohd_devtools/services/services.dart new file mode 100644 index 000000000..1445ae974 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/services.dart @@ -0,0 +1,16 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// services.dart +// Barrel file for rohd_devtools services. +// +// NOTE: io_vm_connection_strategy.dart and web_vm_connection_strategy.dart +// are excluded because they have platform-specific dependencies. + +export 'connection_state_machine.dart'; +export 'platform_vm_connection_strategy.dart'; +export 'service_manager_bridge.dart'; +export 'signal_service.dart'; +export 'signal_value_source.dart'; +export 'tree_service.dart'; +export 'vm_service_signal_value_source.dart'; diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/signal_service.dart b/rohd_devtools_extension/lib/rohd_devtools/services/signal_service.dart index ba4c5cc0d..d0844648f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/signal_service.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/signal_service.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // signal_service.dart @@ -9,14 +9,16 @@ import 'package:rohd_devtools_extension/rohd_devtools/models/signal_model.dart'; +/// Utility methods for signal filtering and lookup. abstract class SignalService { + /// Filters signals by case-insensitive name match. static List filterSignals( List signals, String searchTerm, ) { - List filteredSignals = []; + final filteredSignals = []; - for (var signal in signals) { + for (final signal in signals) { if (signal.name.toLowerCase().contains(searchTerm.toLowerCase())) { filteredSignals.add(signal); } diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source.dart b/rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source.dart new file mode 100644 index 000000000..5937a6824 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source.dart @@ -0,0 +1,42 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_source.dart +// Shared extension-side abstraction for fetching live signal values. +// +// 2026 June +// Author: Desmond Kirkpatrick + +/// Raw snapshot payload keyed by signal ID or full signal path. +typedef SignalSnapshotData = Map>; + +/// Minimal update event used by the shared snapshot overlay flow. +class SignalValueUpdateEvent { + /// Simulation time covered by the update, in source-provided units. + final int upToTime; + + /// Whether the update indicates value data is available. + final bool hasData; + + /// Human-readable update reason for diagnostics. + final String reason; + + /// Creates a [SignalValueUpdateEvent]. + const SignalValueUpdateEvent({ + required this.upToTime, + required this.hasData, + required this.reason, + }); +} + +/// Shared abstraction for value-only snapshot fetches and update ticks. +abstract interface class SignalValueSource { + /// Stream of live update ticks, if this source supports them. + Stream? get updates; + + /// Returns the current simulation time, if available. + Future getCurrentTime(); + + /// Fetch a value snapshot at [time]. + Future getSnapshot(int time); +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source_binding.dart b/rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source_binding.dart new file mode 100644 index 000000000..79e883bfc --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/signal_value_source_binding.dart @@ -0,0 +1,30 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_source_binding.dart +// Repo-specific binding for live signal value source creation. + +import 'package:rohd_devtools_extension/rohd_devtools/services/signal_value_source.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/tree_service.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/vm_service_signal_value_source.dart'; +import 'package:vm_service/vm_service.dart' as vm; + +/// Creates the repo-specific live signal value source for [treeService]. +SignalValueSource? createSignalValueSourceBinding({ + required TreeService treeService, + required vm.VmService vmService, +}) => + VmServiceSignalValueSource( + rohdControllerEval: treeService.rohdControllerEval, + evalDisposable: treeService.evalDisposable, + vmService: vmService, + ); + +/// Dispose any repo-specific live signal value source instance. +Future disposeSignalValueSourceBinding( + SignalValueSource? signalValueSource, +) async { + if (signalValueSource is VmServiceSignalValueSource) { + await signalValueSource.dispose(); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart index 578134c52..7701234b6 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // tree_service.dart @@ -10,39 +10,87 @@ import 'dart:convert'; import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/foundation.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; +import 'package:vm_service/vm_service.dart'; +/// Service helpers for evaluating and filtering the ROHD module tree. class TreeService { - final invokeFunc = 'ModuleTree.instance.hierarchyJSON'; + /// Primary expression for hierarchy JSON — available in all ROHD versions + /// that ship inspector_service.dart (i.e. main and later). + static const _primaryInvokeFunc = 'ModuleTree.instance.hierarchyJSON'; + + /// Fallback kept for any pre-inspector ROHD target. + static const _legacyInvokeFunc = 'ModuleTree.instance.hierarchyJSON'; + + /// Eval wrapper for accessing ROHD code in the target isolate. final EvalOnDartLibrary rohdControllerEval; + + /// Disposable token used to keep the eval alive. final Disposable evalDisposable; - TreeService(this.rohdControllerEval, this.evalDisposable); + /// Optional VM service for source-line lookups (cross-probe). + final VmService? vmService; + + /// Optional isolate ID used with [vmService]. + final String? isolateId; + + /// Creates a tree service around the given eval wrapper. + TreeService(this.rohdControllerEval, this.evalDisposable, + {this.vmService, this.isolateId}); + /// Evaluates the module tree from the ROHD service. Future evalModuleTree() async { - final treeInstance = await rohdControllerEval.evalInstance( - invokeFunc, - isAlive: evalDisposable, - ); + final payload = await _evalTreePayload(); + if (payload == null || payload.isEmpty) { + debugPrint('[TreeService] evalModuleTree failed: empty payload'); + return null; + } - final treeObj = jsonDecode(treeInstance.valueAsString ?? '') as Map; + final decoded = jsonDecode(payload); + if (decoded is! Map) { + debugPrint('[TreeService] evalModuleTree failed: unexpected payload type ' + '${decoded.runtimeType}'); + return null; + } - if (treeObj['status'] == 'fail') { - print('error'); + final treeObj = decoded; + if (treeObj['status'] == 'fail' || treeObj['status'] == 'unavailable') { + final message = + treeObj['message'] ?? treeObj['reason'] ?? treeObj['error']; + debugPrint('[TreeService] evalModuleTree failed: $message'); return null; - } else { - return TreeModel.fromJson(jsonDecode(treeInstance.valueAsString ?? "")); } + + return TreeModel.fromJson(treeObj); + } + + Future _evalTreePayload() async { + final expressions = [_primaryInvokeFunc, _legacyInvokeFunc]; + + for (final expression in expressions) { + try { + final treeInstance = await rohdControllerEval.evalInstance(expression, + isAlive: evalDisposable); + return treeInstance.valueAsString; + } on Exception catch (e) { + debugPrint('[TreeService] Eval failed for "$expression": $e'); + } + } + + return null; } + /// Returns whether the current module or any descendant matches the search. static bool isNodeOrDescendentMatching( TreeModel module, String? treeSearchTerm) { if (module.name.toLowerCase().contains(treeSearchTerm!.toLowerCase())) { return true; } - for (TreeModel childModule in module.subModules) { + for (final childModule in module.subModules) { if (isNodeOrDescendentMatching(childModule, treeSearchTerm)) { return true; } @@ -50,10 +98,12 @@ class TreeService { return false; } - Future refreshModuleTree() { - return rohdControllerEval - .evalInstance(invokeFunc, isAlive: evalDisposable) - .then((treeInstance) => - TreeModel.fromJson(jsonDecode(treeInstance.valueAsString ?? "{}"))); + /// Refreshes the module tree from the ROHD service. + Future refreshModuleTree() async { + final treeModel = await evalModuleTree(); + if (treeModel == null) { + throw StateError('Failed to refresh module tree.'); + } + return treeModel; } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart b/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart new file mode 100644 index 000000000..c1be04bfd --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart @@ -0,0 +1,382 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// vm_service_signal_value_source.dart +// VM-backed adapter for the shared signal value source interface. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; + +import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/signal_value_source.dart'; +import 'package:vm_service/vm_service.dart' as vm; + +/// VM-backed [SignalValueSource] that refreshes on debugger pause events. +class VmServiceSignalValueSource implements SignalValueSource { + static const _moduleTreeHierarchyExpression = + 'ModuleTree.instance.hierarchyJSON'; + + static const _currentTimeExpressions = [ + 'WaveformService.instance.currentTime', + ]; + + static const _currentTimeExtension = 'ext.rohd.currentTime'; + static const _snapshotExtension = 'ext.rohd.snapshotCompact'; + + /// Eval wrapper for ROHD-side expressions. + final EvalOnDartLibrary rohdControllerEval; + + /// Disposable token that keeps the eval alive. + final Disposable evalDisposable; + + /// VM service used for debug-event subscriptions. + final vm.VmService vmService; + + final StreamController _updatesController = + StreamController.broadcast(); + + StreamSubscription? _debugEventSubscription; + int _lastKnownTime = 0; + int _syntheticUpdateTime = 0; + + /// Creates a VM-backed signal value source. + VmServiceSignalValueSource({ + required this.rohdControllerEval, + required this.evalDisposable, + required this.vmService, + }) { + _debugEventSubscription = vmService.onDebugEvent.listen( + _handleDebugEvent, + onError: (Object e) { + debugPrint('[VmSignalValueSource] Debug stream error: $e'); + }, + ); + } + + @override + Stream get updates => _updatesController.stream; + + @override + Future getCurrentTime() async { + final extensionTime = await _readCurrentTimeFromExtension(); + if (extensionTime != null && extensionTime > 0) { + _rememberTime(extensionTime); + return extensionTime; + } + + for (final expression in _currentTimeExpressions) { + try { + final value = await rohdControllerEval.evalInstance( + expression, + isAlive: evalDisposable, + ); + final raw = value.valueAsString; + if (raw == null || raw.isEmpty) { + continue; + } + + final parsedInt = int.tryParse(raw); + if (parsedInt != null) { + _rememberTime(parsedInt); + return parsedInt; + } + + final decoded = jsonDecode(raw); + if (decoded is int) { + _rememberTime(decoded); + return decoded; + } + if (decoded is Map && decoded['currentTime'] is int) { + final currentTime = decoded['currentTime'] as int; + _rememberTime(currentTime); + return currentTime; + } + } on Exception catch (e) { + debugPrint( + '[VmSignalValueSource] Current time eval failed ' + 'for "$expression": $e', + ); + } + } + + return null; + } + + @override + Future getSnapshot(int time) async { + final extensionSnapshot = await _readSnapshotFromExtension(time); + if (extensionSnapshot != null) { + return extensionSnapshot; + } + + final snapshotExpressions = [ + _moduleTreeHierarchyExpression, + _moduleTreeSignalValuesExpression, + _waveformSnapshotExpression(time), + ]; + + for (final expression in snapshotExpressions) { + try { + final value = await rohdControllerEval.evalInstance( + expression, + isAlive: evalDisposable, + ); + final payload = value.valueAsString; + if (payload == null || payload.isEmpty) { + continue; + } + + final decoded = jsonDecode(payload); + if (decoded is! Map) { + continue; + } + + if (decoded['status'] == 'fail' || decoded['status'] == 'unavailable') { + debugPrint( + '[VmSignalValueSource] Snapshot unavailable: ' + '${decoded['message'] ?? decoded['reason'] ?? decoded['error']}', + ); + continue; + } + + if (expression == _moduleTreeHierarchyExpression) { + final hierarchySignals = _decodeHierarchySnapshot(decoded); + if (hierarchySignals != null) { + return hierarchySignals; + } + continue; + } + + final rawSignals = decoded['signals'] is Map + ? decoded['signals'] as Map + : decoded; + + final signals = >{}; + for (final entry in rawSignals.entries) { + final data = entry.value; + if (data is Map) { + signals[entry.key] = data; + } + } + + if (signals.isNotEmpty) { + return signals; + } + } on Exception catch (e) { + debugPrint( + '[VmSignalValueSource] Snapshot eval failed ' + 'for "$expression": $e', + ); + } + } + + return null; + } + + /// Dispose stream subscriptions owned by this source. + Future dispose() async { + await _debugEventSubscription?.cancel(); + _debugEventSubscription = null; + await _updatesController.close(); + } + + static String _waveformSnapshotExpression(int time) => + 'WaveformService.instance.getSnapshotCompactJSON($time)'; + + static const _moduleTreeSignalValuesExpression = + 'ModuleTree.instance.signalValuesJSON'; + + Future _readCurrentTimeFromExtension() async { + final response = await _callExtension(_currentTimeExtension); + if (response == null) { + return null; + } + + final currentTime = response['currentTime']; + if (currentTime is int) { + return currentTime; + } + return null; + } + + Future _readSnapshotFromExtension(int time) async { + final response = await _callExtension( + _snapshotExtension, + args: {'time': time.toString()}, + ); + if (response == null) { + return null; + } + + return _decodeSnapshotPayload(response); + } + + Future?> _callExtension( + String method, { + Map? args, + }) async { + try { + final response = await vmService.callServiceExtension(method, args: args); + return response.json; + } on Exception { + return null; + } + } + + SignalSnapshotData? _decodeSnapshotPayload(Map decoded) { + if (decoded['status'] == 'fail' || decoded['status'] == 'unavailable') { + debugPrint( + '[VmSignalValueSource] Snapshot unavailable: ' + '${decoded['message'] ?? decoded['reason'] ?? decoded['error']}', + ); + return null; + } + + final rawSignals = decoded['signals'] is Map + ? decoded['signals'] as Map + : decoded; + + final signals = >{}; + for (final entry in rawSignals.entries) { + final data = entry.value; + if (data is Map) { + signals[entry.key] = data; + } + } + + return signals.isEmpty ? null : signals; + } + + SignalSnapshotData? _decodeHierarchySnapshot(Map decoded) { + final signals = >{}; + _collectHierarchySignals(decoded, signals, parentPath: ''); + return signals.isEmpty ? null : signals; + } + + void _collectHierarchySignals( + Map moduleJson, + Map> signals, { + required String parentPath, + }) { + final moduleName = moduleJson['name'] as String?; + final modulePath = moduleName == null || moduleName.isEmpty + ? parentPath + : parentPath.isEmpty + ? moduleName + : '$parentPath.$moduleName'; + + _collectSignalGroup( + moduleJson['inputs'], + direction: 'Input', + modulePath: modulePath, + signals: signals, + ); + _collectSignalGroup( + moduleJson['outputs'], + direction: 'Output', + modulePath: modulePath, + signals: signals, + ); + _collectSignalGroup( + moduleJson['inouts'], + direction: 'Inout', + modulePath: modulePath, + signals: signals, + ); + + final subModules = moduleJson['subModules']; + if (subModules is! List) { + return; + } + + for (final child in subModules) { + if (child is Map) { + _collectHierarchySignals(child, signals, parentPath: modulePath); + } + } + } + + void _collectSignalGroup( + Object? groupJson, { + required String direction, + required String modulePath, + required Map> signals, + }) { + if (groupJson is! Map) { + return; + } + + for (final entry in groupJson.entries) { + final signalName = entry.key; + final data = entry.value; + if (data is! Map) { + continue; + } + + final signalPath = + modulePath.isEmpty ? signalName : '$modulePath.$signalName'; + signals[signalPath] = { + 'name': signalName, + 'value': data['value']?.toString() ?? '?', + 'width': _decodeWidth(data['width']), + 'direction': direction, + }; + } + } + + int _decodeWidth(Object? widthValue) { + if (widthValue is int) { + return widthValue; + } + if (widthValue is String) { + return int.tryParse(widthValue) ?? 1; + } + return 1; + } + + void _rememberTime(int time) { + if (time <= 0) { + return; + } + + _lastKnownTime = time; + if (_syntheticUpdateTime < time) { + _syntheticUpdateTime = time; + } + } + + int _nextUpdateTime() { + if (_lastKnownTime > _syntheticUpdateTime) { + _syntheticUpdateTime = _lastKnownTime; + } + + return ++_syntheticUpdateTime; + } + + void _handleDebugEvent(vm.Event event) { + final kind = event.kind; + + if (kind == null || !kind.startsWith('Pause')) { + return; + } + + unawaited(_emitPauseUpdate(kind)); + } + + Future _emitPauseUpdate(String reason) async { + if (_updatesController.isClosed) { + return; + } + + final time = _nextUpdateTime(); + + _updatesController.add( + SignalValueUpdateEvent(upToTime: time, hasData: true, reason: reason), + ); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/web_vm_connection_strategy.dart b/rohd_devtools_extension/lib/rohd_devtools/services/web_vm_connection_strategy.dart new file mode 100644 index 000000000..535aff769 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/services/web_vm_connection_strategy.dart @@ -0,0 +1,171 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// web_vm_connection_strategy.dart +// VM connection strategy for web platforms. +// Uses web_socket_channel for browser-compatible WebSocket connection. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:logging/logging.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; +import 'package:vm_service/vm_service.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +/// Simple logging class for VM service. +class _WebLog extends Log { + final Logger _logger = Logger('VMService.Web'); + @override + void warning(String message) => _logger.warning(message); + + @override + void severe(String message) => _logger.severe(message); +} + +/// VM connection strategy for web platforms (browser). +/// Uses web_socket_channel instead of dart:io WebSocket. +class WebVmConnectionStrategy extends VmConnectionStrategy { + void _logWebSocketError(Object error) { + Logger('VMService.Web').severe('WebSocket error: $error'); + } + + @override + Future connect(String uri) async { + final normalizedUri = normalizeUri(uri); + + if (normalizedUri == null) { + throw Exception('Invalid URI format'); + } + + final wsUrl = normalizedUri.toString(); + final channel = WebSocketChannel.connect(Uri.parse(wsUrl)); + final socketSink = channel.sink; + + // Wait for the connection to be established. + await channel.ready.timeout( + const Duration(seconds: 10), + onTimeout: () => + throw TimeoutException('WebSocket connection timed out after 10 s'), + ); + + final controller = StreamController(); + final streamClosedCompleter = Completer(); + + channel.stream.listen( + controller.add, + onDone: streamClosedCompleter.complete, + onError: _logWebSocketError, + ); + + final vmService = VmService( + controller.stream, + socketSink.add, + log: _WebLog(), + disposeHandler: () async { + await controller.close(); + await socketSink.close(); + }, + streamClosed: streamClosedCompleter.future, + wsUri: wsUrl, + ); + + final vm = await vmService.getVM().timeout( + const Duration(seconds: 5), + onTimeout: () => throw TimeoutException('getVM timed out after 5 s'), + ); + + // During a debugger restart the VM service endpoint becomes available + // before isolates are created, and the test isolate (which contains + // the ROHD inspector_service library) may lag behind the test-runner + // control isolate. Retry a few times with a short delay so we don't + // fall back to the slow polling reconnect path. + String? isolateId; + const maxRetries = 6; + const retryDelay = Duration(milliseconds: 500); + + for (var attempt = 1; attempt <= maxRetries; attempt++) { + final vmInfo = attempt == 1 + ? vm + : await vmService.getVM().timeout(const Duration(seconds: 3)); + final isolates = vmInfo.isolates ?? []; + + if (isolates.isEmpty) { + if (attempt < maxRetries) { + Logger('VMService.Web').info( + 'No isolates yet (attempt $attempt/$maxRetries) — ' + 'waiting ${retryDelay.inMilliseconds} ms', + ); + await Future.delayed(retryDelay); + continue; + } + throw Exception( + 'No isolates found in the VM after $maxRetries ' + 'attempts (${retryDelay.inMilliseconds * maxRetries} ms)', + ); + } + + // Find the isolate that contains the ROHD inspector_service library. + for (final isolateRef in isolates) { + final id = isolateRef.id; + if (id == null) { + continue; + } + try { + final isolate = await vmService + .getIsolate(id) + .timeout(const Duration(milliseconds: 500)); + final libraries = isolate.libraries ?? []; + final hasRohd = libraries.any( + (lib) => + lib.uri != null && + lib.uri!.contains('rohd') && + lib.uri!.contains('inspector_service'), + ); + if (hasRohd) { + isolateId = id; + break; + } + } on Exception { + // Isolate not loaded yet or timed out — skip it + continue; + } + } + + if (isolateId != null) { + break; + } + + // Found isolates but none had ROHD — the test isolate may not + // have spawned yet. Retry unless this is the last attempt. + if (attempt < maxRetries) { + Logger('VMService.Web').info( + 'ROHD isolate not found yet (attempt $attempt/$maxRetries, ' + '${isolates.length} isolate(s) seen) — retrying', + ); + await Future.delayed(retryDelay); + continue; + } + + // Last attempt — fall back to first isolate. + final fallback = isolates.first.id; + if (fallback == null) { + throw Exception('First isolate has no ID'); + } + isolateId = fallback; + Logger('VMService.Web').info( + 'Isolate library scan incomplete after $maxRetries attempts — ' + 'using first isolate; evalModuleTree will verify', + ); + } + + return VmConnectionResult(vmService: vmService, isolateId: isolateId!); + } +} + +/// Returns a [WebVmConnectionStrategy]. Used by the conditional-import +/// dispatcher in `platform_vm_connection_strategy.dart`. +VmConnectionStrategy platformVmConnectionStrategy() => + WebVmConnectionStrategy(); diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/details_help_button.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/details_help_button.dart new file mode 100644 index 000000000..6fe12d1e7 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/details_help_button.dart @@ -0,0 +1,40 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// details_help_button.dart +// Help button widget for the Details tab. +// +// Content is loaded from assets/help/details_help.md. +// Edit that markdown file to update hover tooltip and dialog content. +// +// 2026 March +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; + +/// A help button for the Details tab. +/// +/// Content is driven by `assets/help/details_help.md`. +/// Edit that file to update the hover tooltip and click-open dialog. +class DetailsHelpButton extends StatelessWidget { + /// Whether the current theme is dark mode. + final bool isDark; + + /// Create a [DetailsHelpButton]. + const DetailsHelpButton({required this.isDark, super.key}); + + @override + Widget build(BuildContext context) => MarkdownHelpButton( + assetPath: 'assets/help/details_help.md', + isDark: isDark, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(FlagProperty('isDark', value: isDark)); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/devtool_appbar.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/devtool_appbar.dart index 9138fc191..7c8d7e022 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/devtool_appbar.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/devtool_appbar.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // devtool_appbar.dart @@ -7,41 +7,77 @@ // 2024 January 5 // Author: Yao Jing Quek +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/cubit/cubits.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/devtools_help_button.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/platform_icon.dart'; +/// App bar used by the ROHD DevTools UI. class DevtoolAppBar extends StatelessWidget implements PreferredSizeWidget { - const DevtoolAppBar({ - super.key, - }); + /// Whether to render color emoji icons where available. + const DevtoolAppBar({super.key, this.hasColorEmoji = kIsWeb}); + + /// Whether the icon set should prefer color emoji glyphs. + final bool hasColorEmoji; @override + + /// Builds the app bar with help, license, and theme controls. Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final accentColor = Theme.of(context).colorScheme.primary; + return AppBar( - backgroundColor: Theme.of(context).colorScheme.onPrimary, - title: const Text('ROHD DevTool (Beta)'), - leading: const Icon(Icons.build), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - showLicensePage(context: context); - }, - child: const Text( - 'Licenses', - style: TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ], - ); + backgroundColor: Theme.of(context).colorScheme.onPrimary, + title: const Text('ROHD DevTool (Beta)'), + leading: Padding( + padding: const EdgeInsets.all(8), + child: + Image.asset('assets/icons/rohd_logo.png', fit: BoxFit.contain)), + actions: [ + // ── Help ── + DevToolsHelpButton(isDark: isDark), + + // ── Licenses ── + Padding( + padding: const EdgeInsets.only(right: 20), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + showLicensePage(context: context); + }, + child: const Text('Licenses', + style: TextStyle(fontWeight: FontWeight.bold))))), + + BlocBuilder( + builder: (context, themeMode) { + final isDark = themeMode == DevToolsThemeMode.dark; + return IconButton( + tooltip: + isDark ? 'Switch to light theme' : 'Switch to dark theme', + onPressed: () { + context.read().toggleTheme(); + }, + icon: platformIcon(isDark ? Icons.light_mode : Icons.dark_mode, + isDark ? '☀️' : '🌙', + size: 24, + color: accentColor, + hasColorEmoji: hasColorEmoji)); + }) + ]); } @override + + /// The preferred height of the app bar. Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(FlagProperty('hasColorEmoji', value: hasColorEmoji)); + } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/devtools_connection_host.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/devtools_connection_host.dart new file mode 100644 index 000000000..d6b80eac1 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/devtools_connection_host.dart @@ -0,0 +1,1568 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// devtools_connection_host.dart +// Abstract base class for DevTools app shells that manage VM/DTD connection +// lifecycle. Subclasses provide app-specific data loading and UI. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:dtd/dtd.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/models/dtd_vm_service_info.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/services.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; +import 'package:vm_service/vm_service.dart' hide Stack; + +/// Discover the VM services currently advertised by a DTD endpoint. +/// +/// If [onRegisteredServices] is provided, it is invoked with the set of +/// currently registered DTD client service names before VM service discovery +/// completes. +Future> discoverVmServicesViaDtd( + String dtdUri, { + void Function(Set serviceNames)? onRegisteredServices, +}) async { + debugPrint('[ConnectionHost] Connecting to DTD at: $dtdUri'); + final dtd = await DartToolingDaemon.connect(Uri.parse(dtdUri)); + + try { + if (onRegisteredServices != null) { + try { + final registered = await dtd.getRegisteredServices(); + final serviceNames = { + for (final svc in registered.clientServices) svc.name, + }; + onRegisteredServices(serviceNames); + debugPrint('[ConnectionHost] Registered services: $serviceNames'); + } on Exception catch (e) { + debugPrint('[ConnectionHost] getRegisteredServices failed: $e'); + } + } + + final response = await dtd.getVmServices(); + final services = response.vmServicesInfos; + + debugPrint('[ConnectionHost] Found ${services.length} VM service(s)'); + for (final svc in services) { + debugPrint( + '[ConnectionHost] ${svc.name ?? "(unnamed)"}: ' + 'uri=${svc.uri}, exposedUri=${svc.exposedUri}', + ); + } + + return services + .map( + (svc) => DiscoveredVmService( + name: svc.name, + uri: svc.uri, + exposedUri: svc.exposedUri, + ), + ) + .toList(); + } finally { + await dtd.close(); + } +} + +/// The resolved inputs for a user-driven connection attempt. +/// +/// Carries the cleaned URI strings plus either the chosen VM service URI or +/// a user-facing validation error. +class VmConnectionAttemptResolution { + /// The VM service URI to connect to, if resolution succeeded. + final String? vmServiceUri; + + /// The cleaned VM service URI derived from the user's input. + final String cleanedVmServiceUri; + + /// The cleaned DTD URI derived from the user's input. + final String cleanedDtdUri; + + /// A user-facing validation or discovery error, when resolution fails. + final String? error; + + /// Creates a resolved connection-attempt payload. + const VmConnectionAttemptResolution({ + required this.vmServiceUri, + required this.cleanedVmServiceUri, + required this.cleanedDtdUri, + this.error, + }); +} + +/// Resolve a connection attempt from raw VM service and DTD URI inputs. +/// +/// If the VM service URI is already valid, it is used directly. Otherwise the +/// DTD endpoint is queried via [discoverVmServices] and the first advertised VM +/// service is selected. +Future resolveVmConnectionAttempt({ + required String rawVmServiceUri, + required String rawDtdUri, + required Future> Function(String dtdUri) + discoverVmServices, +}) async { + final vmServiceUri = DevToolsConnectionHostState.cleanVmServiceUri( + rawVmServiceUri, + ); + var dtdUri = ''; + if (rawDtdUri.isNotEmpty) { + dtdUri = DevToolsConnectionHostState.cleanDtdUri(rawDtdUri); + } + + final hasVmUri = vmServiceUri.isNotEmpty && + vmServiceUri.startsWith('ws') && + !vmServiceUri.contains('xxxx'); + final hasDtdUri = dtdUri.isNotEmpty && dtdUri.startsWith('ws'); + + if (!hasVmUri && !hasDtdUri) { + return VmConnectionAttemptResolution( + vmServiceUri: null, + cleanedVmServiceUri: vmServiceUri, + cleanedDtdUri: dtdUri, + error: 'Please enter a VM Service URI or DTD URI', + ); + } + + if (hasVmUri) { + return VmConnectionAttemptResolution( + vmServiceUri: vmServiceUri, + cleanedVmServiceUri: vmServiceUri, + cleanedDtdUri: dtdUri, + ); + } + + final services = await discoverVmServices(dtdUri); + if (services.isEmpty) { + return VmConnectionAttemptResolution( + vmServiceUri: null, + cleanedVmServiceUri: vmServiceUri, + cleanedDtdUri: dtdUri, + error: 'No VM services found via DTD. Is your ROHD app running?', + ); + } + + return VmConnectionAttemptResolution( + vmServiceUri: services.first.connectionUri, + cleanedVmServiceUri: vmServiceUri, + cleanedDtdUri: dtdUri, + ); +} + +/// Whether a DTD VM lifecycle event refers to the VM currently tracked by the +/// host. +/// +/// Matches against either the direct URI or the exposed URI reported by DTD. +bool dtdEventMatchesTrackedVm({ + required String? trackedVmUri, + required String? eventUri, + required String? eventExposedUri, +}) { + bool matchesCandidate(String? candidate) { + if (candidate == null || candidate.isEmpty) { + return false; + } + if (trackedVmUri == null || trackedVmUri.isEmpty) { + return true; + } + return trackedVmUri.contains(candidate) || candidate.contains(trackedVmUri); + } + + if (trackedVmUri == null || trackedVmUri.isEmpty) { + return true; + } + + return matchesCandidate(eventUri) || matchesCandidate(eventExposedUri); +} + +/// Return the preferred VM service URI carried by a DTD lifecycle event. +/// +/// Prefers the externally connectable exposed URI when present, otherwise +/// falls back to the raw URI field. +String? preferredVmServiceUriFromDtdEvent(DTDEvent event) { + final eventUri = event.data[DtdParameters.uri]?.toString(); + final eventExposedUri = event.data[DtdParameters.exposedUri]?.toString(); + if (eventExposedUri != null && eventExposedUri.isNotEmpty) { + return eventExposedUri; + } + if (eventUri != null && eventUri.isNotEmpty) { + return eventUri; + } + return null; +} + +// --------------------------------------------------------------------------- +// VM Connection Strategy +// --------------------------------------------------------------------------- + +/// Abstract base for VM connection strategies. +/// Linux uses vm_service_io, Web uses package:web WebSocket. +abstract class VmConnectionStrategy { + /// Connect to VM service at the given URI. + /// Returns the VmService and isolateId for the main isolate. + Future connect(String uri); + + /// Normalize URI to websocket format. + Uri? normalizeUri(String value) { + try { + var uri = Uri.parse(value.trim()); + + if (uri.scheme == 'http') { + uri = uri.replace(scheme: 'ws'); + } else if (uri.scheme == 'https') { + uri = uri.replace(scheme: 'wss'); + } + + if (!uri.path.endsWith('/ws')) { + uri = uri.replace(path: '${uri.path}ws'); + } + + return uri; + } on Exception { + return null; + } + } +} + +/// Result of a VM connection attempt. +class VmConnectionResult { + /// The connected VM service. + final VmService vmService; + + /// The isolate ID of the main isolate. + final String isolateId; + + /// Constructor for [VmConnectionResult]. + VmConnectionResult({required this.vmService, required this.isolateId}); +} + +/// Describes whether an attach is brand-new, a same-VM restart, or a +/// teardown for disconnect. +enum VmConnectionTransitionKind { + /// A brand-new VM attachment with no preserved prior app state. + freshAttach, + + /// A reconnect to the same logical VM after a restart. + sameVmRestart, + + /// A teardown transition driven by explicit disconnect. + disconnect, +} + +/// Carries the previous VM identity into reconnect hooks so subclasses can +/// preserve app state only for same-VM restarts. +class VmConnectionTransition { + /// The host-classified reconnect kind for this attach. + final VmConnectionTransitionKind kind; + + /// The VM service URI that was attached before this transition. + final String? previousUri; + + /// The isolate ID that was attached before this transition. + final String? previousIsolateId; + + /// The last tracked VM name, when known. + final String? previousVmName; + + /// Creates a transition with the given classification and prior identity. + const VmConnectionTransition({ + required this.kind, + this.previousUri, + this.previousIsolateId, + this.previousVmName, + }); + + /// Creates a fresh attach transition. + const VmConnectionTransition.fresh() + : this(kind: VmConnectionTransitionKind.freshAttach); + + /// Creates a same-VM restart transition. + const VmConnectionTransition.sameVmRestart() + : this(kind: VmConnectionTransitionKind.sameVmRestart); + + /// Creates a disconnect transition. + const VmConnectionTransition.disconnect() + : this(kind: VmConnectionTransitionKind.disconnect); + + /// Whether the app should preserve widget state through this transition. + bool get preservesAppState => + kind == VmConnectionTransitionKind.sameVmRestart; + + /// Whether the old and new attachments represent the same logical VM. + bool get isSameLogicalVm => kind == VmConnectionTransitionKind.sameVmRestart; + + /// Returns a copy populated with the previous VM identity captured by the + /// host at reconnect time. + VmConnectionTransition withPrevious({ + String? previousUri, + String? previousIsolateId, + String? previousVmName, + }) => + VmConnectionTransition( + kind: kind, + previousUri: previousUri ?? this.previousUri, + previousIsolateId: previousIsolateId ?? this.previousIsolateId, + previousVmName: previousVmName ?? this.previousVmName, + ); +} + +class _ReconnectState { + final String? connectedVmName; + final bool autoReconnect; + + const _ReconnectState({ + required this.connectedVmName, + required this.autoReconnect, + }); +} + +// --------------------------------------------------------------------------- +// DevToolsConnectionHost base class +// --------------------------------------------------------------------------- + +/// Abstract base State that manages VM/DTD connection lifecycle. +/// +/// Subclasses (e.g. the ROHD DevTools page) extend this to get: +/// - VM connect / disconnect / pause / resume / lightweight reconnect +/// - Persistent DTD connection with VmServiceRegistered/Unregistered events +/// - DTD Service stream for extension availability (e.g. 'rohd' service) +/// - VM liveness polling with auto-reconnect by name +/// - ConnectionStateMachine integration +/// - Connection dialog management +/// +/// The subclass implements abstract hooks to react to these lifecycle events +/// and perform app-specific work (loading hierarchy, waveforms, etc.). +abstract class DevToolsConnectionHostState + extends State { + // ══════════════════════════════════════════════════════════════════════════ + // Configuration — override in subclass + // ══════════════════════════════════════════════════════════════════════════ + + /// The connection strategy (platform-specific VM service connection). + /// Return null if VM connection is not supported on this platform. + VmConnectionStrategy? get connectionStrategy; + + // ══════════════════════════════════════════════════════════════════════════ + // Connection state + // ══════════════════════════════════════════════════════════════════════════ + + /// Whether connected to a VM (true after successful handshake). + bool get isConnected => _isConnected; + + /// Sets whether the host is connected to a VM. + @protected + set isConnected(bool value) => _isConnected = value; + bool _isConnected = false; + + /// True while a VM connection handshake is in progress. + bool get isConnecting => _isConnecting; + + /// Sets whether a VM connection handshake is in progress. + @protected + set isConnecting(bool value) => _isConnecting = value; + bool _isConnecting = false; + + /// True when the VM service has been detected as dead. + bool get isVmDead => _isVmDead; + + /// Sets whether the host believes the VM is dead. + @protected + set isVmDead(bool value) => _isVmDead = value; + bool _isVmDead = false; + + /// True when the user deliberately paused the VM connection. + bool get isPaused => _isPaused; + + /// Sets whether the user deliberately paused the VM connection. + @protected + set isPaused(bool value) => _isPaused = value; + bool _isPaused = false; + + /// The active VM service instance (null when disconnected). + VmService? get vmService => _vmService; + + /// Sets the active VM service instance. + @protected + set vmService(VmService? value) => _vmService = value; + VmService? _vmService; + + /// URI of the last/current VM service connection. + String? get lastVmServiceUri => _lastVmServiceUri; + + /// Sets the URI of the last/current VM service connection. + @protected + set lastVmServiceUri(String? value) => _lastVmServiceUri = value; + String? _lastVmServiceUri; + + /// Isolate ID from the last successful connection. + String? get lastIsolateId => _lastIsolateId; + + /// Sets the last known isolate ID. + @protected + set lastIsolateId(String? value) => _lastIsolateId = value; + String? _lastIsolateId; + + /// Name of the connected VM (from DTD discovery). + String? get connectedVmName => _connectedVmName; + + /// Sets the name of the connected VM. + @protected + set connectedVmName(String? value) => _connectedVmName = value; + String? _connectedVmName; + + /// Whether auto-reconnect by name is enabled. + bool get autoReconnect => _autoReconnect; + + /// Sets whether auto-reconnect by name is enabled. + @protected + set autoReconnect(bool value) => _autoReconnect = value; + bool _autoReconnect = false; + + /// Whether a VM service is currently connected (shorthand). + bool get isVmConnected => _vmService != null; + + /// Monotonically increasing counter bumped on every full reconnect. + /// Used for widget keys so Flutter recreates stateful widgets. + int get connectionGeneration => _connectionGeneration; + int _connectionGeneration = 0; + + /// The connection state machine. + ConnectionStateMachine get connectionStateMachine => _csm; + final ConnectionStateMachine _csm = ConnectionStateMachine(); + + /// The persistent DTD connection (for VM lifecycle events + RPC). + DartToolingDaemon? get persistentDtd => _persistentDtd; + DartToolingDaemon? _persistentDtd; + + /// Remembered VM services across reconnects. + List? get rememberedServices => _rememberedServices; + + /// Sets the remembered VM services list. + @protected + set rememberedServices(List? value) => + _rememberedServices = value; + List? _rememberedServices; + + /// Services currently registered on DTD (populated by Service stream). + final Set _availableServices = {}; + + // ── Private connection state ── + + bool _autoReconnectInProgress = false; + int _vmLivenessFailCount = 0; + static const _vmDeadThreshold = 3; + Timer? _vmLivenessTimer; + StreamSubscription? _dtdEventSubscription; + StreamSubscription? _serviceStreamSubscription; + + // ── URI controllers (for connection dialog) ── + + /// Controller for the VM service URI field. + final TextEditingController vmServiceUriController = TextEditingController( + text: 'ws://127.0.0.1:8181/xxxx=/ws', + ); + + /// Controller for the DTD URI field. + final TextEditingController dtdUriController = TextEditingController(); + + /// Most recent connection error shown in the UI. + String? connectionError; + + // ══════════════════════════════════════════════════════════════════════════ + // Abstract hooks — subclass must implement + // ══════════════════════════════════════════════════════════════════════════ + + /// Called after a successful VM connection. + /// + /// [onBeforeVmConnected] runs first, while the previous VM identity is still + /// available for app-specific state preservation decisions. + /// + /// The subclass should create its data sources (tree, waveform, etc.) + /// using the provided [result] and [uri]. The VM service, isolate ID, + /// CSM, liveness timer, and DTD listener are already set up. + Future onVmConnected(VmConnectionResult result, String uri); + + /// Called after a new VM connection is established but before the host + /// updates its own connection identity. + /// + /// [transition] tells the subclass whether this is a fresh attach or a + /// same-VM restart, so app state can be preserved only when appropriate. + Future onBeforeVmConnected( + VmConnectionResult result, + String uri, { + required VmConnectionTransition transition, + }) async {} + + /// Tear down all state from a previous VM connection. + /// + /// Called during disconnect and before reconnect. The subclass should + /// dispose data sources, clear caches, reset cubits, etc. + /// Must be resilient (each step individually guarded). + Future tearDownOldConnection({ + required VmConnectionTransition transition, + }); + + /// Called when a full disconnect completes (before showing dialog). + /// + /// The subclass should clear any UI state and references that are + /// specific to the old connection. + void onVmDisconnected(); + + /// Called when the VM is detected as dead. + void onVmDead() {} + + /// Called when a dead VM recovers (liveness check succeeds). + void onVmRecovered() {} + + /// Called when the host pauses app-specific data fetching. + /// + /// Subclasses can pause waveform or polling work while keeping the VM + /// connection itself alive. + Future onVmPaused() async {} + + /// Called when the host resumes app-specific data fetching. + /// + /// Subclasses can resume waveform or polling work and backfill any + /// missed data while the VM connection remains alive. + Future onVmResumed() async {} + + /// Verify whether a lightweight reconnect is valid. + /// + /// Called with the new [result] after connecting to the same URI. + /// Return true if the isolate matches (same process) and a lightweight + /// swap is appropriate; return false to trigger a full reconnect. + bool onLightweightReconnectCheck(VmConnectionResult result) => + result.isolateId == _lastIsolateId; + + /// Called after a successful lightweight reconnect. + /// + /// The subclass should swap the VM service in existing transports + /// without tearing down tree/schematic/waveform state. + Future onLightweightReconnectSuccess( + VmConnectionResult result, + String uri, + ); + + /// Called when a DTD service becomes available. + /// + /// For example, when the 'rohd' extension service registers on DTD, + /// the subclass can enable source navigation. + void onServiceAvailable(String serviceName) {} + + /// Called when a DTD service becomes unavailable. + void onServiceUnavailable(String serviceName) {} + + @override + + /// Adds the host's public connection state to the diagnostics tree. + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(FlagProperty('isConnected', value: isConnected)) + ..add(FlagProperty('isConnecting', value: isConnecting)) + ..add(FlagProperty('isVmDead', value: isVmDead)) + ..add(FlagProperty('isPaused', value: isPaused)) + ..add(DiagnosticsProperty('vmService', vmService)) + ..add(StringProperty('lastVmServiceUri', lastVmServiceUri)) + ..add(StringProperty('lastIsolateId', lastIsolateId)) + ..add(StringProperty('connectedVmName', connectedVmName)) + ..add(FlagProperty('autoReconnect', value: autoReconnect)) + ..add(FlagProperty('isVmConnected', value: isVmConnected)) + ..add(IntProperty('connectionGeneration', connectionGeneration)) + ..add( + DiagnosticsProperty( + 'connectionStrategy', + connectionStrategy, + ), + ) + ..add( + DiagnosticsProperty( + 'connectionStateMachine', + connectionStateMachine, + ), + ) + ..add( + DiagnosticsProperty('persistentDtd', persistentDtd), + ) + ..add( + DiagnosticsProperty?>( + 'rememberedServices', + rememberedServices, + ), + ) + ..add( + DiagnosticsProperty( + 'vmServiceUriController', + vmServiceUriController, + ), + ) + ..add( + DiagnosticsProperty( + 'dtdUriController', + dtdUriController, + ), + ) + ..add(StringProperty('connectionError', connectionError)); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Lifecycle + // ══════════════════════════════════════════════════════════════════════════ + + @override + @mustCallSuper + void initState() { + super.initState(); + _csm.onLoadHierarchy = onCsmLoadHierarchy; + _csm.onStateChange = _onCsmStateChange; + } + + @override + @mustCallSuper + void dispose() { + _vmLivenessTimer?.cancel(); + stopDtdListener(); + unawaited(_csm.dispose()); + vmServiceUriController.dispose(); + dtdUriController.dispose(); + unawaited(_vmService?.dispose()); + super.dispose(); + } + + /// Override in subclass if the CSM's loadHierarchy callback should + /// trigger app-specific loading. Default is a no-op. + Future onCsmLoadHierarchy() async {} + + /// Called by the CSM on state changes. Override for additional behavior. + @protected + void _onCsmStateChange(ConnectionPhase phase, DataLoadState dataState) { + debugPrint('[ConnectionHost] CSM: ${phase.name} $dataState'); + } + + // ══════════════════════════════════════════════════════════════════════════ + // URI Cleaning Utilities + // ══════════════════════════════════════════════════════════════════════════ + + /// Clean a VM service URI by extracting the valid portion. + /// VM URIs start with 'ws:' and end with '=/ws'. + static String cleanVmServiceUri(String input) { + final trimmed = input.trim(); + var startIndex = trimmed.indexOf('ws:'); + if (startIndex < 0) { + startIndex = trimmed.indexOf('wss:'); + } + if (startIndex < 0) { + return trimmed; + } + + const endMarker = '=/ws'; + final endIndex = trimmed.indexOf(endMarker, startIndex); + if (endIndex < 0) { + return trimmed.substring(startIndex); + } + + return trimmed.substring(startIndex, endIndex + endMarker.length); + } + + /// Clean a DTD URI by extracting the valid portion. + /// DTD URIs start with 'ws:' and end with '='. + static String cleanDtdUri(String input) { + final trimmed = input.trim(); + var startIndex = trimmed.indexOf('ws:'); + if (startIndex < 0) { + startIndex = trimmed.indexOf('wss:'); + } + if (startIndex < 0) { + return trimmed; + } + + var searchFrom = startIndex; + while (true) { + final eqIndex = trimmed.indexOf('=', searchFrom); + if (eqIndex < 0) { + return trimmed.substring(startIndex); + } + + if (eqIndex + 3 < trimmed.length && + trimmed.substring(eqIndex, eqIndex + 4) == '=/ws') { + searchFrom = eqIndex + 1; + continue; + } + + return trimmed.substring(startIndex, eqIndex + 1); + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // Connection Actions (public API for subclass and UI) + // ══════════════════════════════════════════════════════════════════════════ + + /// Connect to a VM service at the given URI. + /// + /// Tears down any previous connection, establishes a new one, starts + /// liveness polling and DTD listener, then calls [onVmConnected]. + Future connectToVmService( + String vmServiceUri, { + VmConnectionTransition transition = const VmConnectionTransition.fresh(), + }) async { + debugPrint('[ConnectionHost] Starting connection to: $vmServiceUri'); + final strategy = connectionStrategy; + if (strategy == null) { + throw Exception('No connection strategy available'); + } + final previousUri = _lastVmServiceUri; + final previousIsolateId = _lastIsolateId; + final effectiveTransition = transition.withPrevious( + previousUri: previousUri, + previousIsolateId: previousIsolateId, + previousVmName: _connectedVmName, + ); + + _csm.handleEvent(ConnectRequested(vmServiceUri)); + + try { + await tearDownOldConnection(transition: effectiveTransition); + } on Exception catch (e) { + debugPrint( + '[ConnectionHost] tearDownOldConnection failed (non-fatal): $e', + ); + _connectionGeneration++; + } + + debugPrint('[ConnectionHost] Calling strategy.connect...'); + final result = await strategy.connect(vmServiceUri); + debugPrint('[ConnectionHost] Connected! isolateId: ${result.isolateId}'); + + await onBeforeVmConnected( + result, + vmServiceUri, + transition: effectiveTransition, + ); + + // Notify the state machine. + final identity = VmIdentity( + uri: vmServiceUri, + isolateId: result.isolateId, + vmName: _connectedVmName, + ); + _csm.handleEvent(ConnectionEstablished(result.vmService, identity)); + + setState(() { + _vmService = result.vmService; + _isConnected = true; + _isConnecting = false; + _isVmDead = false; + _isPaused = false; + _vmLivenessFailCount = 0; + _lastVmServiceUri = vmServiceUri; + _lastIsolateId = result.isolateId; + }); + + // Let the subclass set up its data sources. This is the path that + // calls ServiceManager.vmServiceOpened, which owns streamListen for + // the Debug/Isolate/etc streams. Subscribe to debug events only + // AFTER this has run so we never race ServiceManager. + await onVmConnected(result, vmServiceUri); + unawaited(_csm.subscribeToDebugEvents(result.vmService)); + + // Start VM liveness polling. + _vmLivenessTimer?.cancel(); + _vmLivenessTimer = Timer.periodic( + const Duration(seconds: 10), + (_) => unawaited(_checkVmLiveness()), + ); + debugPrint('[ConnectionHost] Started VM liveness polling (10 s)'); + + // Start persistent DTD listener. + unawaited(startDtdListener()); + } + + /// Disconnect from the current VM service. + /// + /// Tears down the connection, resets state, and calls [onVmDisconnected]. + Future disconnect() async { + _csm.handleEvent(const DisconnectRequested()); + _vmLivenessTimer?.cancel(); + _vmLivenessTimer = null; + stopDtdListener(); + await tearDownOldConnection( + transition: const VmConnectionTransition.disconnect().withPrevious( + previousUri: _lastVmServiceUri, + previousIsolateId: _lastIsolateId, + previousVmName: _connectedVmName, + ), + ); + + setState(() { + _vmService = null; + _isConnected = false; + _isConnecting = false; + _isVmDead = false; + _isPaused = false; + _vmLivenessFailCount = 0; + connectionError = null; + _lastVmServiceUri = null; + _lastIsolateId = null; + _connectedVmName = null; + _autoReconnect = false; + }); + + onVmDisconnected(); + } + + /// Pause waveform data fetches while keeping VM connection alive. + Future pauseVm() async { + if (!isVmConnected) { + return; + } + debugPrint('[ConnectionHost] Pausing (connection stays alive)'); + _csm.handleEvent(const PauseRequested()); + await onVmPaused(); + setState(() { + _isPaused = true; + }); + } + + /// Resume after a pause. + Future resumeVm() async { + if (!isVmConnected) { + debugPrint('[ConnectionHost] VM not connected — nothing to resume'); + return; + } + debugPrint('[ConnectionHost] Resuming'); + _csm.handleEvent(const ResumeRequested()); + setState(() { + _isPaused = false; + }); + await onVmResumed(); + } + + /// Attempt a lightweight reconnect to the same VM process. + /// + /// Returns true if successful (state preserved), false if the caller + /// should fall through to a full reconnect. + Future lightweightReconnect(String uri) async { + debugPrint('[ConnectionHost] Attempting lightweight reconnect to: $uri'); + final strategy = connectionStrategy; + if (strategy == null) { + return false; + } + + try { + final result = await strategy.connect(uri); + + if (!onLightweightReconnectCheck(result)) { + debugPrint( + '[ConnectionHost] Lightweight check failed — ' + 'need full reconnect', + ); + unawaited(result.vmService.dispose()); + return false; + } + + debugPrint( + '[ConnectionHost] Same process — swapping VM service in-place', + ); + + // Notify the state machine. + final identity = VmIdentity( + uri: uri, + isolateId: result.isolateId, + vmName: _connectedVmName, + ); + _csm.handleEvent(ConnectionEstablished(result.vmService, identity)); + + // Let the subclass swap the transport (this re-runs vmServiceOpened + // on the local ServiceManager, which owns streamListen). Subscribe + // to debug events only AFTER that to avoid racing ServiceManager. + await onLightweightReconnectSuccess(result, uri); + unawaited(_csm.subscribeToDebugEvents(result.vmService)); + + setState(() { + _vmService = result.vmService; + _isConnecting = false; + _isPaused = false; + _isVmDead = false; + _vmLivenessFailCount = 0; + _lastIsolateId = result.isolateId; + }); + + // Restart liveness timer. + _vmLivenessTimer?.cancel(); + _vmLivenessTimer = Timer.periodic( + const Duration(seconds: 10), + (_) => unawaited(_checkVmLiveness()), + ); + + // Restart DTD listener. + unawaited(startDtdListener()); + + debugPrint('[ConnectionHost] Lightweight reconnect succeeded'); + return true; + } on Exception catch (e) { + debugPrint('[ConnectionHost] Lightweight reconnect failed: $e'); + return false; + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // Connection Dialog + // ══════════════════════════════════════════════════════════════════════════ + + /// Show the VM connection dialog. + /// + /// Subclasses can override [buildConnectionDialogContent] to customize. + Future showConnectionDialog() async { + final strategy = connectionStrategy; + if (strategy == null) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('VM connection not available on this platform'), + ), + ); + } + return; + } + + await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => AlertDialog( + title: const Text('Connect to VM Service'), + content: SizedBox( + width: 400, + child: buildConnectionDialogContent(dialogContext), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Cancel'), + ), + ], + ), + ); + } + + /// Build the connection dialog content. + /// + /// Override in subclass to add demo-mode buttons, emoji detection, etc. + @protected + Widget buildConnectionDialogContent(BuildContext dialogContext) => + VmConnectionForm( + vmServiceUriController: vmServiceUriController, + dtdUriController: dtdUriController, + connectionError: connectionError, + onConnect: () async { + try { + await attemptConnection(); + if (mounted && dialogContext.mounted && _isConnected) { + Navigator.of(dialogContext).pop(); + } + } on Exception catch (e) { + setState(() { + connectionError = 'Connection failed: $e'; + }); + } + }, + onDemoMode: () { + Navigator.of(dialogContext).pop(); + onDemoModeRequested(); + }, + showDemoButton: true, + cleanVmServiceUri: cleanVmServiceUri, + cleanDtdUri: cleanDtdUri, + discoverVmServices: discoverVmServices, + initialDiscoveredServices: _rememberedServices + ?.map( + (s) => DiscoveredVmService( + name: s.name, + uri: s.uri, + exposedUri: s.exposedUri, + isAlive: s.isAlive, + autoReconnect: s.autoReconnect, + ), + ) + .toList(), + onServicesDiscovered: (services) { + _rememberedServices = services + .map( + (s) => DtdVmServiceInfo.fromFields( + name: s.name, + uri: s.uri, + exposedUri: s.exposedUri, + isAlive: s.isAlive, + autoReconnect: s.autoReconnect, + ), + ) + .toList(); + }, + ); + + /// Called when demo mode is selected from the connection dialog. + /// Override in subclass. + @protected + void onDemoModeRequested() {} + + /// Attempt connection using the current URI controller values. + /// + /// If only DTD URI is provided, discovers VMs and picks the first one. + Future attemptConnection() async { + final strategy = connectionStrategy; + if (strategy == null) { + setState(() { + connectionError = 'VM connection not available on this platform'; + }); + return; + } + + try { + final resolution = await resolveVmConnectionAttempt( + rawVmServiceUri: vmServiceUriController.text, + rawDtdUri: dtdUriController.text, + discoverVmServices: discoverVmServices, + ); + + if (resolution.cleanedVmServiceUri != vmServiceUriController.text && + resolution.cleanedVmServiceUri.isNotEmpty) { + vmServiceUriController.text = resolution.cleanedVmServiceUri; + } + if (resolution.cleanedDtdUri != dtdUriController.text && + resolution.cleanedDtdUri.isNotEmpty) { + dtdUriController.text = resolution.cleanedDtdUri; + } + if (resolution.error != null) { + setState(() { + connectionError = resolution.error; + }); + return; + } + final vmServiceUri = resolution.vmServiceUri!; + + setState(() { + connectionError = null; + }); + + setState(() { + _isConnecting = true; + }); + + // Capture VM name and auto-reconnect from discovery list. + final matchedService = + _rememberedServices?.cast().firstWhere( + (s) => s!.connectionUri == vmServiceUri, + orElse: () => null, + ); + _connectedVmName = matchedService?.name; + _autoReconnect = matchedService?.autoReconnect ?? false; + + await connectToVmService(vmServiceUri); + } on Exception catch (e) { + debugPrint( + '[ConnectionHost] attemptConnection failed: ' + '${e.runtimeType}: $e', + ); + if (mounted) { + setState(() { + _isConnected = false; + _isConnecting = false; + connectionError = 'Connection failed: $e'; + }); + } + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // DTD Discovery + // ══════════════════════════════════════════════════════════════════════════ + + /// Discover VM services from a DTD URI. + /// + /// Connects to DTD, calls getVmServices(), returns the list. + /// Also probes for registered services (new DTD 4.0 API). + Future> discoverVmServices(String dtdUri) async => + discoverVmServicesViaDtd( + dtdUri, + onRegisteredServices: (serviceNames) { + _availableServices + ..clear() + ..addAll(serviceNames); + }, + ); + + /// Check whether a named service is currently available on DTD. + bool isServiceAvailable(String serviceName) => + _availableServices.contains(serviceName); + + // ══════════════════════════════════════════════════════════════════════════ + // DTD Persistent Listener + // ══════════════════════════════════════════════════════════════════════════ + + /// Start the persistent DTD connection for VM lifecycle events. + @protected + Future startDtdListener() async { + final raw = dtdUriController.text; + if (raw.isEmpty) { + return; + } + + // Don't restart if already listening. + if (_persistentDtd != null && !_persistentDtd!.isClosed) { + return; + } + + try { + final dtd = await DartToolingDaemon.connect(Uri.parse(raw)); + _persistentDtd = dtd; + + // Notify subclass that DTD is available. + onDtdConnected(dtd); + + // Listen for VM service register/unregister events. + _dtdEventSubscription = dtd.onVmServiceUpdate().listen( + handleDtdVmEvent, + onError: (Object e) { + debugPrint('[ConnectionHost] DTD event stream error: $e'); + }, + onDone: () { + debugPrint('[ConnectionHost] DTD event stream closed'); + _persistentDtd = null; + _dtdEventSubscription = null; + onDtdDisconnected(); + }, + ); + + await dtd.streamListen(ConnectedAppServiceConstants.serviceName); + debugPrint('[ConnectionHost] Listening for VM lifecycle events'); + + // Subscribe to Service stream for extension availability. + try { + _serviceStreamSubscription = dtd + .onEvent(CoreDtdServiceConstants.servicesStreamId) + .listen(_handleServiceStreamEvent); + await dtd.streamListen(CoreDtdServiceConstants.servicesStreamId); + debugPrint('[ConnectionHost] Listening for Service stream events'); + } on Exception catch (e) { + debugPrint('[ConnectionHost] Service stream subscription failed: $e'); + } + + // Probe registered services on initial connect. + try { + final registered = await dtd.getRegisteredServices(); + _availableServices.clear(); + for (final svc in registered.clientServices) { + _availableServices.add(svc.name); + onServiceAvailable(svc.name); + } + } on Exception catch (e) { + debugPrint('[ConnectionHost] getRegisteredServices failed: $e'); + } + + // Use dtd.done as a backup death detector. + unawaited( + dtd.done.then((_) { + if (_persistentDtd == dtd) { + debugPrint('[ConnectionHost] dtd.done fired — DTD connection lost'); + _persistentDtd = null; + unawaited(_dtdEventSubscription?.cancel()); + _dtdEventSubscription = null; + unawaited(_serviceStreamSubscription?.cancel()); + _serviceStreamSubscription = null; + onDtdDisconnected(); + } + }), + ); + } on Exception catch (e) { + debugPrint('[ConnectionHost] Could not start DTD listener: $e'); + } + } + + /// Stop the persistent DTD listener. + @protected + void stopDtdListener() { + unawaited(_dtdEventSubscription?.cancel()); + _dtdEventSubscription = null; + unawaited(_serviceStreamSubscription?.cancel()); + _serviceStreamSubscription = null; + if (_persistentDtd != null && !_persistentDtd!.isClosed) { + unawaited(_persistentDtd!.close()); + } + _persistentDtd = null; + onDtdDisconnected(); + } + + /// Called when the persistent DTD connection is established. + /// Override to wire DTD to source navigation, etc. + void onDtdConnected(DartToolingDaemon dtd) {} + + /// Called when the persistent DTD connection is lost. + @protected + void onDtdDisconnected() {} + + /// Handle Service stream events (extension registered/unregistered). + void _handleServiceStreamEvent(DTDEvent event) { + final kind = event.kind; + // The service name is in event.data under 'service' or 'method'. + final serviceName = event.data['service']?.toString(); + if (serviceName == null || serviceName.isEmpty) { + return; + } + + if (kind == CoreDtdServiceConstants.serviceRegisteredKind) { + if (_availableServices.add(serviceName)) { + debugPrint('[ConnectionHost] Service available: $serviceName'); + onServiceAvailable(serviceName); + } + } else if (kind == CoreDtdServiceConstants.serviceUnregisteredKind) { + if (_availableServices.remove(serviceName)) { + debugPrint('[ConnectionHost] Service unavailable: $serviceName'); + onServiceUnavailable(serviceName); + } + } + } + + /// Handle DTD VM lifecycle events. + /// + /// When a VM service is unregistered, marks the connection as dead. + /// When a new VM with our name registers, triggers auto-reconnect. + @protected + Future handleDtdVmEvent(DTDEvent event) async { + debugPrint('[ConnectionHost] DTD event: ${event.kind} — ${event.data}'); + + // Ignore events while manually paused (except vmServiceRegistered). + if (_isPaused && + event.kind != ConnectedAppServiceConstants.vmServiceRegistered) { + debugPrint('[ConnectionHost] Ignoring event — VM is manually paused'); + return; + } + + // Ignore events while a connection is in progress. + if (_isConnecting) { + debugPrint('[ConnectionHost] Ignoring event — connection in progress'); + return; + } + + if (event.kind == ConnectedAppServiceConstants.vmServiceUnregistered) { + final eventUri = event.data[DtdParameters.uri]?.toString(); + final eventExposedUri = event.data[DtdParameters.exposedUri]?.toString(); + final ourUri = _lastVmServiceUri; + + if (!dtdEventMatchesTrackedVm( + trackedVmUri: ourUri, + eventUri: eventUri, + eventExposedUri: eventExposedUri, + )) { + debugPrint( + '[ConnectionHost] Ignoring unregister for different VM: ' + 'uri=$eventUri, exposedUri=$eventExposedUri (ours: $ourUri)', + ); + return; + } + + debugPrint( + '[ConnectionHost] Our VM service was unregistered — marking dead', + ); + _csm.handleEvent(const DtdVmUnregistered()); + _vmLivenessTimer?.cancel(); + _vmLivenessTimer = null; + + if (mounted) { + setState(() { + _isVmDead = true; + }); + onVmDead(); + } + } else if (event.kind == ConnectedAppServiceConstants.vmServiceRegistered) { + if (!_autoReconnect || !mounted) { + return; + } + + final eventName = event.data[DtdParameters.name]?.toString(); + + if (eventName == null || + eventName.isEmpty || + eventName != _connectedVmName) { + debugPrint( + '[ConnectionHost] vmServiceRegistered for "$eventName" — ' + 'not our target "$_connectedVmName", ignoring', + ); + return; + } + + final newUri = preferredVmServiceUriFromDtdEvent(event); + if (newUri == null || newUri.isEmpty) { + debugPrint( + '[ConnectionHost] vmServiceRegistered — no URI in event data', + ); + return; + } + + if (_isVmDead || _isPaused) { + _csm.handleEvent(DtdVmRegistered(newUri, name: eventName)); + debugPrint( + '[ConnectionHost] vmServiceRegistered for "$eventName" at ' + '$newUri — auto-reconnecting', + ); + unawaited(reconnectFromDtdEvent(newUri)); + } else if (_isConnected) { + debugPrint( + '[ConnectionHost] vmServiceRegistered for "$eventName" at ' + '$newUri — reconnecting (sameUri=${newUri == _lastVmServiceUri})', + ); + setState(() { + _isVmDead = true; + }); + _csm.handleEvent(DtdVmRegistered(newUri, name: eventName)); + unawaited(reconnectFromDtdEvent(newUri)); + } + } + } + + /// Reconnect driven by a DTD vmServiceRegistered event. + @protected + Future reconnectFromDtdEvent(String newUri) async { + if (_autoReconnectInProgress) { + debugPrint('[ConnectionHost] Already reconnecting — skipping'); + return; + } + _autoReconnectInProgress = true; + + final wasPaused = _isPaused; + if (wasPaused) { + debugPrint('[ConnectionHost] Clearing stale pause state'); + } + + try { + final sameUri = newUri == _lastVmServiceUri; + + if (sameUri) { + debugPrint('[ConnectionHost] Same URI — trying lightweight reconnect'); + stopDtdListener(); + final success = await lightweightReconnect(newUri); + if (success) { + debugPrint('[ConnectionHost] Lightweight reconnect succeeded'); + _autoReconnectInProgress = false; + return; + } + debugPrint('[ConnectionHost] Lightweight failed — full reconnect'); + } + + await performFullReconnect(newUri); + } on Exception catch (e) { + debugPrint('[ConnectionHost] DTD reconnect failed: $e'); + if (_autoReconnect && _isVmDead && mounted) { + unawaited(attemptAutoReconnect()); + } + } finally { + _autoReconnectInProgress = false; + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // VM Liveness Polling + // ══════════════════════════════════════════════════════════════════════════ + + /// Check if the VM service is alive (getVersion with timeout). + Future isVmServiceAlive() async { + final vm = _vmService; + if (vm == null) { + return false; + } + try { + await vm.getVersion().timeout(const Duration(seconds: 5)); + return true; + } on Exception { + return false; + } + } + + /// Periodic liveness probe. + Future _checkVmLiveness() async { + if (!isVmConnected || !mounted || _isPaused || _isConnecting) { + return; + } + final alive = await isVmServiceAlive(); + + if (!mounted || _isPaused || _isConnecting || !isVmConnected) { + return; + } + + if (alive) { + _vmLivenessFailCount = 0; + if (_isVmDead && mounted) { + debugPrint('[ConnectionHost] VM recovered — clearing dead flag'); + _csm.handleEvent(const VmRecovered()); + setState(() { + _isVmDead = false; + }); + onVmRecovered(); + } + } else { + _vmLivenessFailCount++; + debugPrint( + '[ConnectionHost] VM check failed ' + '($_vmLivenessFailCount/$_vmDeadThreshold)', + ); + if (_vmLivenessFailCount >= _vmDeadThreshold && !_isVmDead && mounted) { + debugPrint('[ConnectionHost] VM is dead'); + _csm.handleEvent(const VmDied()); + setState(() { + _isVmDead = true; + }); + onVmDead(); + if (_autoReconnect) { + unawaited(attemptAutoReconnect()); + } + } + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // Auto-Reconnect + // ══════════════════════════════════════════════════════════════════════════ + + /// Attempt to reconnect to a VM with the same name (exponential backoff). + Future attemptAutoReconnect() async { + if (_autoReconnectInProgress) { + debugPrint('[ConnectionHost] Already reconnecting — skipping'); + return; + } + _autoReconnectInProgress = true; + + final targetName = _connectedVmName; + final dtdUri = dtdUriController.text; + if (targetName == null || targetName.isEmpty || dtdUri.isEmpty) { + debugPrint('[ConnectionHost] No VM name or DTD URI — skipping'); + _autoReconnectInProgress = false; + return; + } + + debugPrint('[ConnectionHost] Will try to reconnect to "$targetName"'); + + const maxAttempts = 5; + var delay = const Duration(seconds: 2); + + for (var attempt = 1; attempt <= maxAttempts; attempt++) { + await Future.delayed(delay); + if (!mounted || !_isVmDead || !_autoReconnect) { + _autoReconnectInProgress = false; + return; + } + + debugPrint( + '[ConnectionHost] Auto-reconnect attempt ' + '$attempt/$maxAttempts', + ); + try { + final cleaned = cleanDtdUri(dtdUri); + final services = await discoverVmServices(cleaned); + final match = services.cast().firstWhere( + (s) => s!.name == targetName, + orElse: () => null, + ); + + if (match != null) { + final sameUri = match.connectionUri == _lastVmServiceUri; + debugPrint( + '[ConnectionHost] Found "$targetName" at ' + '${match.connectionUri} ' + '(${sameUri ? "same" : "different"} URI)', + ); + + _rememberedServices = services + .map( + (s) => DtdVmServiceInfo.fromFields( + name: s.name, + uri: s.uri, + exposedUri: s.exposedUri, + autoReconnect: s.connectionUri == match.connectionUri, + ), + ) + .toList(); + + if (sameUri) { + stopDtdListener(); + final success = await lightweightReconnect(match.connectionUri); + if (success) { + debugPrint( + '[ConnectionHost] Auto lightweight reconnect succeeded', + ); + _autoReconnectInProgress = false; + return; + } + } + + await performFullReconnect(match.connectionUri); + + if (isVmConnected && !_isVmDead) { + debugPrint('[ConnectionHost] Auto-reconnected to "$targetName"'); + _autoReconnectInProgress = false; + return; + } + } else { + debugPrint('[ConnectionHost] "$targetName" not found — will retry'); + } + } on Exception catch (e) { + debugPrint('[ConnectionHost] Attempt $attempt failed: $e'); + } + + delay *= 2; + } + + debugPrint('[ConnectionHost] Gave up after $maxAttempts attempts'); + _autoReconnectInProgress = false; + } + + /// Perform a full reconnect to [newUri], preserving same-VM restart + /// semantics for subclass hooks. + @protected + Future performFullReconnect(String newUri) async { + final reconnectState = _prepareForFullReconnect(newUri); + + await connectToVmService( + newUri, + transition: const VmConnectionTransition.sameVmRestart(), + ); + if (mounted) { + setState(() {}); + } + + _restoreReconnectPreferences(reconnectState); + } + + _ReconnectState _prepareForFullReconnect(String newUri) { + _vmLivenessTimer?.cancel(); + _vmLivenessTimer = null; + + vmServiceUriController.text = newUri; + + final reconnectState = _ReconnectState( + connectedVmName: _connectedVmName, + autoReconnect: _autoReconnect, + ); + + stopDtdListener(); + setState(() { + _isVmDead = false; + _isPaused = false; + _vmLivenessFailCount = 0; + }); + + return reconnectState; + } + + void _restoreReconnectPreferences(_ReconnectState reconnectState) { + _connectedVmName = reconnectState.connectedVmName; + _autoReconnect = reconnectState.autoReconnect; + } + + /// Increment the connection generation (triggers widget recreation). + @protected + void bumpConnectionGeneration() { + _connectionGeneration++; + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/devtools_help_button.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/devtools_help_button.dart new file mode 100644 index 000000000..4281686f6 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/devtools_help_button.dart @@ -0,0 +1,41 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// devtools_help_button.dart +// Help button widget for the ROHD DevTools app bar. +// +// Content is loaded from assets/help/devtools_help.md. +// Edit that markdown file to update hover tooltip and dialog content. +// +// 2026 March +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; + +/// A help button for the ROHD DevTools app bar. +/// +/// Content is driven by `assets/help/devtools_help.md`. +/// Edit that file to update the hover tooltip and click-open dialog. +class DevToolsHelpButton extends StatelessWidget { + /// Whether the current theme is dark mode. + final bool isDark; + + /// Create a [DevToolsHelpButton]. + const DevToolsHelpButton({required this.isDark, super.key}); + + @override + Widget build(BuildContext context) => MarkdownHelpButton( + assetPath: 'assets/help/devtools_help.md', + isDark: isDark, + labelIcon: kIsWeb ? const Icon(Icons.help_outline, size: 20) : null, + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(FlagProperty('isDark', value: isDark)); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart index 40f1e72de..9cf9e0741 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_card.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // module_tree_card.dart @@ -7,127 +7,125 @@ // 2024 January 5 // Author: Yao Jing Quek +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/cubit/selected_module_cubit.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/cubit/tree_search_term_cubit.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/cubit/cubits.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/services/tree_service.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/services.dart'; +/// Displays the module tree for the currently loaded ROHD model. class ModuleTreeCard extends StatefulWidget { + /// The root module to render as the tree. final TreeModel futureModuleTree; - const ModuleTreeCard({ - super.key, - required this.futureModuleTree, - }); + + /// Creates a module tree card for the provided module tree. + const ModuleTreeCard({required this.futureModuleTree, super.key}); @override + + /// Creates the mutable state for [ModuleTreeCard]. State createState() => _ModuleTreeCardState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty('futureModuleTree', futureModuleTree)); + } } class _ModuleTreeCardState extends State { + /// Creates the module tree card state. _ModuleTreeCardState(); @override - Widget build(BuildContext context) { - return genModuleTree( - moduleTree: widget.futureModuleTree, - ); - } + /// Builds the module tree widget. + Widget build(BuildContext context) => + genModuleTree(moduleTree: widget.futureModuleTree); + + /// Builds a tree node for [module], returning null if it is filtered out. TreeNode? buildNode(TreeModel module) { final treeSearchTerm = context.watch().state; - // If there's a search term, ensure that either this node or a descendant node matches it. + // If there's a search term, ensure that either this node or a + // descendant node matches it. if (treeSearchTerm != null && !TreeService.isNodeOrDescendentMatching(module, treeSearchTerm)) { return null; } // Build children recursively - List childrenNodes = buildChildrenNodes(module); + final childrenNodes = buildChildrenNodes(module); return TreeNode( - content: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - context.read().setModule(module); - }, - child: getNodeContent(module), - ), - ), - children: childrenNodes, - ); + content: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + context.read().setModule(module); + }, + child: getNodeContent(module))), + children: childrenNodes); } + /// Builds the visible text and icon for a tree node. Widget getNodeContent(TreeModel module) { final selectedModule = context.watch().state; + final colorScheme = Theme.of(context).colorScheme; // Check if the current module is the selected module - bool isSelected = selectedModule is SelectedModuleLoaded && + final isSelected = selectedModule is SelectedModuleLoaded && selectedModule.module == module; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( decoration: BoxDecoration( - color: - isSelected ? Colors.blue.withOpacity(0.2) : Colors.transparent, - borderRadius: BorderRadius.circular(4.0), - ), - padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - child: Row( - children: [ - const Icon(Icons.memory), - const SizedBox(width: 2.0), - Text( - module.name, + color: isSelected + ? Colors.blue.withValues(alpha: 0.2) + : Colors.transparent, + borderRadius: BorderRadius.circular(4)), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row(children: [ + Icon(Icons.memory, color: colorScheme.onSurface), + const SizedBox(width: 2), + Text(module.name, style: TextStyle( - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: isSelected ? Colors.blue : Colors.white, - ), - ), - ], - ), - ), - ], - ); + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected + ? colorScheme.primary + : colorScheme.onSurface)) + ])) + ]); } - List buildChildrenNodes( - TreeModel treeModule, - ) { - List childrenNodes = []; - List subModules = treeModule.subModules; + /// Builds child tree nodes for the given module. + List buildChildrenNodes(TreeModel treeModule) { + final childrenNodes = []; + final subModules = treeModule.subModules; if (subModules.isNotEmpty) { - for (var module in subModules) { - TreeNode? node = buildNode(module); + for (final module in subModules) { + final node = buildNode(module); if (node != null) { childrenNodes.add(node); } } } - return childrenNodes - .where((node) => node != null) - .toList() - .cast(); + return childrenNodes; } - TreeNode? buildTreeFromModule(TreeModel node) { - return buildNode(node); - } + /// Returns a tree node wrapper for the provided module. + TreeNode? buildTreeFromModule(TreeModel node) => buildNode(node); - Widget genModuleTree({ - required TreeModel moduleTree, - }) { - var root = buildNode(moduleTree); + /// Builds the full tree view widget for [moduleTree]. + Widget genModuleTree({required TreeModel moduleTree}) { + final root = buildNode(moduleTree); if (root != null) { return TreeView(nodes: [root]); - } else { - return const Text('No data'); } + return const Text('No data'); } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart index f84835e5e..d8e472006 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/module_tree_details_navbar.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // module_tree_details_navbar.dart @@ -7,39 +7,128 @@ // 2024 January 5 // Author: Yao Jing Quek +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/cubit/cubits.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/details_help_button.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/platform_icon.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/schematic_icon.dart'; +/// Navigation bar for switching between module detail views. class ModuleTreeDetailsNavbar extends StatelessWidget { - const ModuleTreeDetailsNavbar({ - super.key, - }); + /// Whether color emoji fonts are available on this platform. + final bool hasColorEmoji; + + /// Creates the details navigation bar. + const ModuleTreeDetailsNavbar({super.key, this.hasColorEmoji = kIsWeb}); + + @override + + /// Adds diagnostic properties for the nav bar. + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(FlagProperty('hasColorEmoji', + value: hasColorEmoji, ifFalse: 'using fallback emojis')); + } @override + + /// Builds the tab row and help button for module details. Widget build(BuildContext context) { - return BottomNavigationBar( - type: BottomNavigationBarType.fixed, - backgroundColor: const Color(0x1B1B1FEE), - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white.withOpacity(.60), - selectedFontSize: 10, - unselectedFontSize: 10, - onTap: (value) { - // Respond to item press. - }, - items: const [ - BottomNavigationBarItem( - label: 'Details', - icon: Icon(Icons.info), - ), - BottomNavigationBarItem( - label: 'Waveform', - icon: Icon(Icons.cable), - ), - BottomNavigationBarItem( - label: 'Schematic', - icon: Icon(Icons.developer_board), - ), - ], - ); + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + border: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor))), + child: BlocBuilder( + builder: (context, selectedTab) => Row(children: [ + _TabButton( + label: 'Details', + icon: platformIcon(Icons.info, 'ℹ️', + size: 18, hasColorEmoji: hasColorEmoji), + isSelected: selectedTab == DetailsTab.details, + onTap: () => context + .read() + .selectTab(DetailsTab.details)), + _TabButton( + label: 'Waveform', + icon: platformIcon(Icons.waves, '🌊', + size: 18, hasColorEmoji: hasColorEmoji), + isSelected: selectedTab == DetailsTab.waveform, + onTap: () => context + .read() + .selectTab(DetailsTab.waveform)), + _TabButton( + label: 'Schematic', + icon: const SchematicIcon(size: 18), + isSelected: selectedTab == DetailsTab.schematic, + onTap: () => context + .read() + .selectTab(DetailsTab.schematic)), + const Spacer(), + DetailsHelpButton(isDark: isDark) + ]))); + } +} + +class _TabButton extends StatelessWidget { + /// The tab text label. + final String label; + + /// Icon shown next to the label. + final Widget icon; + + /// Whether this tab is currently selected. + final bool isSelected; + + /// Callback invoked when the tab is tapped. + final VoidCallback onTap; + + const _TabButton( + {required this.label, + required this.icon, + required this.isSelected, + required this.onTap}); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('label', label)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(FlagProperty('isSelected', value: isSelected)) + ..add( + ObjectFlagProperty('onTap', onTap, ifNull: 'disabled')); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final selectedColor = colorScheme.primary; + final unselectedColor = colorScheme.onSurface.withValues(alpha: 0.6); + + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isSelected ? selectedColor : Colors.transparent, + width: 2))), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + icon, + const SizedBox(width: 8), + Text(label, + style: TextStyle( + fontSize: 13, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? selectedColor : unselectedColor)) + ]))); } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/platform_icon.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/platform_icon.dart new file mode 100644 index 000000000..821c515d9 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/platform_icon.dart @@ -0,0 +1,118 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// platform_icon.dart +// Provides platform-aware icon rendering with emoji fallback. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// A widget that renders either a Material Icon or emoji text based on +/// platform emoji font availability. +/// +/// On platforms with color emoji support, uses the provided emoji string. +/// On platforms without (or with `hasColorEmoji: false`), falls back to +/// the Material IconData. +class PlatformIcon extends StatelessWidget { + /// Material IconData to use as fallback on platforms without color emoji + final IconData nativeIcon; + + /// Emoji string to display if color emoji fonts are available + final String emoji; + + /// Size of the icon/emoji (defaults to 16) + final double? size; + + /// Color to apply to the icon/emoji + final Color? color; + + /// Whether color emoji fonts are available on this platform + /// (defaults to true - verify on native platforms) + final bool hasColorEmoji; + + /// Constructor for [PlatformIcon]. + const PlatformIcon( + this.nativeIcon, + this.emoji, { + this.size, + this.color, + this.hasColorEmoji = true, + super.key, + }); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('nativeIcon', nativeIcon)) + ..add(StringProperty('emoji', emoji)) + ..add(DoubleProperty('size', size)) + ..add(ColorProperty('color', color)) + ..add( + FlagProperty( + 'hasColorEmoji', + value: hasColorEmoji, + ifFalse: 'using fallback icons', + ), + ); + } + + @override + Widget build(BuildContext context) { + if (hasColorEmoji) { + return Text( + emoji, + style: TextStyle(fontSize: size ?? 16, color: color), + ); + } + return Icon(nativeIcon, size: size, color: color); + } +} + +/// Helper function for quick construction of PlatformIcon widgets. +/// +/// Returns a PlatformIcon widget that renders either emoji or Material icon +/// based on platform capabilities. +/// +/// Example: +/// ```dart +/// platformIcon(Icons.waves, '🔗', size: 24, hasColorEmoji: true) +/// ``` +Widget platformIcon( + IconData nativeIcon, + String emoji, { + double? size, + Color? color, + bool hasColorEmoji = true, +}) => + PlatformIcon( + nativeIcon, + emoji, + size: size, + color: color, + hasColorEmoji: hasColorEmoji, + ); + +/// Check whether a color emoji font (Noto Color Emoji) is installed on the +/// system. Returns true on web (always has emoji), or checks fc-list on Linux. +Future isEmojiFontInstalled() async { + if (kIsWeb) { + return true; // Web always has color emoji + } + + try { + final result = await Process.run('fc-list', []); + if (result.exitCode == 0) { + final out = result.stdout.toString().toLowerCase(); + return out.contains('noto color emoji'); + } + } on Exception { + // fc-list command not available or failed; assume no emoji font + } + return false; +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/schematic_icon.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/schematic_icon.dart new file mode 100644 index 000000000..b8a5c69df --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/schematic_icon.dart @@ -0,0 +1,125 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_icon.dart +// Custom icon: three colored blocks connected by orthogonal lines, +// resembling a small schematic / block diagram. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// A custom-painted icon showing three colored rectangles connected +/// by orthogonal (right-angle) wires — a miniature schematic diagram. +class SchematicIcon extends StatelessWidget { + /// Creates a schematic icon at the given [size]. + const SchematicIcon({super.key, this.size = 20, this.brightness}); + + /// Icon size in logical pixels (width = height). + final double size; + + /// Override brightness to force light/dark wire color. + /// If null, uses the ambient [Theme.of(context).brightness]. + final Brightness? brightness; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DoubleProperty('size', size)) + ..add(EnumProperty('brightness', brightness)); + } + + @override + + /// Builds the custom-painted schematic icon. + Widget build(BuildContext context) { + final effectiveBrightness = brightness ?? Theme.of(context).brightness; + return CustomPaint( + size: Size.square(size), + painter: _SchematicIconPainter(effectiveBrightness), + ); + } +} + +class _SchematicIconPainter extends CustomPainter { + _SchematicIconPainter(this.brightness); + + final Brightness brightness; + + @override + + /// Paints the schematic-style icon. + void paint(Canvas canvas, Size size) { + final s = size.width; + + final bw = s * 0.30; + final bh = s * 0.22; + final r = s * 0.04; + + final ax = s * 0.02; + final ay = s * 0.08; + final bx = s * 0.02; + final by = s * 0.62; + final cx = s * 0.64; + final cy = s * 0.38; + + final wireColor = + brightness == Brightness.dark ? Colors.white70 : Colors.black54; + final wirePaint = Paint() + ..color = wireColor + ..strokeWidth = s * 0.045 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + final jx = s * 0.52; + final aPortY = ay + bh / 2; + final bPortY = by + bh / 2; + final cPortY = cy + bh / 2; + + final wireA = Path() + ..moveTo(ax + bw, aPortY) + ..lineTo(jx, aPortY) + ..lineTo(jx, cPortY); + canvas.drawPath(wireA, wirePaint); + + final wireB = Path() + ..moveTo(bx + bw, bPortY) + ..lineTo(jx, bPortY) + ..lineTo(jx, cPortY); + canvas.drawPath(wireB, wirePaint); + + final wireC = Path() + ..moveTo(jx, cPortY) + ..lineTo(cx, cPortY); + canvas.drawPath(wireC, wirePaint); + + final dotPaint = Paint()..color = wireColor; + canvas.drawCircle(Offset(jx, cPortY), s * 0.04, dotPaint); + + const colorA = Color(0xFF4A90D9); + const colorB = Color(0xFF50B86C); + const colorC = Color(0xFFE8943A); + + void drawBlock(double x, double y, Color color) { + final rect = RRect.fromLTRBR(x, y, x + bw, y + bh, Radius.circular(r)); + final fill = Paint()..color = color; + canvas.drawRRect(rect, fill); + final border = Paint() + ..color = color.withAlpha(200) + ..style = PaintingStyle.stroke + ..strokeWidth = s * 0.02; + canvas.drawRRect(rect, border); + } + + drawBlock(ax, ay, colorA); + drawBlock(bx, by, colorB); + drawBlock(cx, cy, colorC); + } + + @override + bool shouldRepaint(_SchematicIconPainter old) => old.brightness != brightness; +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart index 0d3fdeb3a..c143059dc 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // signal_details_card.dart @@ -7,123 +7,175 @@ // 2024 January 5 // Author: Yao Jing Quek +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/cubit/snapshot_cubit.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; - -import 'package:rohd_devtools_extension/rohd_devtools/ui/signal_table_text_field.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/details_help_button.dart'; import 'package:rohd_devtools_extension/rohd_devtools/ui/signal_table.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/signal_table_text_field.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/simulation_time_display.dart'; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +/// Shows the selected module's signal details and search controls. class SignalDetailsCard extends StatefulWidget { + /// The module currently selected for inspection. final TreeModel? module; + /// Optional snapshot data to overlay signal values. + final SnapshotLoaded? snapshot; + + /// Display settings for simulation time values. + final SimulationTimeDisplay timeDisplay; + + /// Creates a signal details card for the selected module. const SignalDetailsCard({ - Key? key, + super.key, this.module, - }) : super(key: key); + this.snapshot, + this.timeDisplay = SimulationTimeDisplay.none, + }); @override + + /// Creates the mutable state for [SignalDetailsCard]. SignalDetailsCardState createState() => SignalDetailsCardState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('module', module)) + ..add(DiagnosticsProperty('snapshot', snapshot)) + ..add(DiagnosticsProperty( + 'timeDisplay', timeDisplay)); + } } +/// State for [SignalDetailsCard]. class SignalDetailsCardState extends State { + /// Search term used to filter signals. String? searchTerm; + + /// Whether input signals are shown. ValueNotifier inputSelected = ValueNotifier(true); + + /// Whether output signals are shown. ValueNotifier outputSelected = ValueNotifier(true); + + /// Whether inout signals are shown. + ValueNotifier inoutSelected = ValueNotifier(true); + + /// Notifies the widget tree to rebuild after filter changes. ValueNotifier notifier = ValueNotifier(0); - void toggleNotifier() { - notifier.value++; - } + /// Boundary used when exporting the signal details panel as PNG. + final GlobalKey _boundaryKey = GlobalKey(); + + /// Increments the rebuild notifier. + void toggleNotifier() => notifier.value++; void _showFilterDialog() { - showDialog( - context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return AlertDialog( - title: const Text('Filter Signals'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ + unawaited(showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setState) => AlertDialog( + title: const Text('Filter Signals'), + content: + Column(mainAxisSize: MainAxisSize.min, children: [ + CheckboxListTile( + title: const Text('Input'), + value: inputSelected.value, + onChanged: (value) { + setState(() { + inputSelected.value = value!; + }); + toggleNotifier(); + }), CheckboxListTile( - title: const Text('Input'), - value: inputSelected.value, - onChanged: (bool? value) { - setState(() { - inputSelected.value = value!; - }); - toggleNotifier(); - }, - ), + title: const Text('Output'), + value: outputSelected.value, + onChanged: (value) { + setState(() { + outputSelected.value = value!; + }); + toggleNotifier(); + }), CheckboxListTile( - title: const Text('Output'), - value: outputSelected.value, - onChanged: (bool? value) { - setState(() { - outputSelected.value = value!; - }); - toggleNotifier(); - }, - ), - ], - ), - ); - }, - ); - }, - ); + title: const Text('Inout'), + value: inoutSelected.value, + onChanged: (value) { + setState(() { + inoutSelected.value = value!; + }); + toggleNotifier(); + }) + ]))))); } @override + + /// Builds the signal details panel for the selected module. Widget build(BuildContext context) { if (widget.module == null) { return const Padding( - padding: EdgeInsets.only(top: 20.0), - child: Center(child: Text('No module selected')), - ); + padding: EdgeInsets.only(top: 20), + child: Center(child: Text('No module selected'))); } - return SizedBox( - height: MediaQuery.of(context).size.height / 1.4, - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - children: [ + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Stack(fit: StackFit.expand, children: [ + RepaintBoundary( + key: _boundaryKey, + child: SingleChildScrollView( + child: Column(children: [ Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ + padding: const EdgeInsets.all(8), + child: Row(children: [ SignalTableTextField( - labelText: 'Search Signals', - onChanged: (value) { - setState(() { - searchTerm = value; - }); - toggleNotifier(); - }, - ), + labelText: 'Search Signals', + onChanged: (value) { + setState(() { + searchTerm = value; + }); + toggleNotifier(); + }), IconButton( - icon: const Icon(Icons.filter_list), - onPressed: _showFilterDialog, - ), - ], - ), - ), + icon: const Icon(Icons.filter_list), + onPressed: _showFilterDialog), + DetailsHelpButton(isDark: isDark) + ])), ValueListenableBuilder( - valueListenable: notifier, - builder: (context, _, __) { - return SignalTable( - selectedModule: widget.module!, - searchTerm: searchTerm, - inputSelectedVal: inputSelected.value, - outputSelectedVal: outputSelected.value, - ); - }, - ), - ], - ), - ), - ); + valueListenable: notifier, + builder: (context, _, __) => SignalTable( + selectedModule: widget.module!, + searchTerm: searchTerm, + inputSelectedVal: inputSelected.value, + outputSelectedVal: outputSelected.value, + inoutSelectedVal: inoutSelected.value, + snapshot: widget.snapshot, + timeDisplay: widget.timeDisplay)) + ]))), + Positioned( + right: 8, + bottom: 8, + child: ExportPngButton( + onPressed: () => captureBoundaryToPng(context, + boundaryKey: _boundaryKey, filePrefix: 'signal_details'))) + ]); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('searchTerm', searchTerm)) + ..add(FlagProperty('inputSelected', value: inputSelected.value)) + ..add(FlagProperty('outputSelected', value: outputSelected.value)) + ..add(FlagProperty('inoutSelected', value: inoutSelected.value)) + ..add(IntProperty('notifier', notifier.value)); } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart index 8e97328d8..8c341c87f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart @@ -7,129 +7,162 @@ // 2024 January 5 // Author: Yao Jing Quek +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/cubit/snapshot_cubit.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/signal_model.dart'; import 'package:rohd_devtools_extension/rohd_devtools/models/tree_model.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/services/signal_service.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/services.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/simulation_time_display.dart'; +/// Displays the signals for a selected module in a table. class SignalTable extends StatefulWidget { + /// The module whose signals are shown in the table. final TreeModel selectedModule; + + /// Optional search term used to filter visible signals. final String? searchTerm; + + /// Whether input signals should be shown. final bool inputSelectedVal; + + /// Whether output signals should be shown. final bool outputSelectedVal; - const SignalTable({ - super.key, - required this.selectedModule, - required this.searchTerm, - required this.inputSelectedVal, - required this.outputSelectedVal, - }); + + /// Whether inout signals should be shown. + final bool inoutSelectedVal; + + /// Optional snapshot data to overlay signal values. + final SnapshotLoaded? snapshot; + + /// Display settings for simulation time values. + final SimulationTimeDisplay timeDisplay; + + /// Creates a signal table for the given module and filters. + const SignalTable( + {required this.selectedModule, + required this.searchTerm, + required this.inputSelectedVal, + required this.outputSelectedVal, + required this.inoutSelectedVal, + this.snapshot, + this.timeDisplay = SimulationTimeDisplay.none, + super.key}); @override - State createState() => _SignalTableState(); + + /// Creates the state object for [SignalTable]. + State createState() => _SignalTableState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('selectedModule', selectedModule)) + ..add(StringProperty('searchTerm', searchTerm)) + ..add(FlagProperty('inputSelectedVal', value: inputSelectedVal)) + ..add(FlagProperty('outputSelectedVal', value: outputSelectedVal)) + ..add(FlagProperty('inoutSelectedVal', value: inoutSelectedVal)) + ..add(DiagnosticsProperty('snapshot', snapshot)) + ..add(DiagnosticsProperty( + 'timeDisplay', timeDisplay)); + } } class _SignalTableState extends State { @override + + /// Builds the signal table and its rows. Widget build(BuildContext context) { - final tableHeaders = ['Name', 'Direction', 'Value', 'Width']; + final snapshotTime = widget.snapshot?.time; + final valueHeader = snapshotTime != null + ? 'Value (@ ${widget.timeDisplay.format(snapshotTime)})' + : 'Value'; + final tableHeaders = ['Name', 'Direction', valueHeader, 'Width']; return Table( - border: TableBorder.all(), - columnWidths: const { - 0: FlexColumnWidth(), - 1: FlexColumnWidth(), - 2: FlexColumnWidth(), - }, - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow( - children: List.generate( - tableHeaders.length, - (index) => _buildTableHeader(text: tableHeaders[index]), - ), - ), - ...generateSignalsRow( - widget.selectedModule, - widget.searchTerm, - widget.inputSelectedVal, - widget.outputSelectedVal, - ), - ], - ); + border: TableBorder.all(), + columnWidths: const { + 0: FlexColumnWidth(), + 1: FlexColumnWidth(), + 2: FlexColumnWidth() + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: List.generate(tableHeaders.length, + (index) => _buildTableHeader(text: tableHeaders[index]))), + ...generateSignalsRow(widget.selectedModule, + searchTerm: widget.searchTerm, + inputSelected: widget.inputSelectedVal, + outputSelected: widget.outputSelectedVal, + inoutSelected: widget.inoutSelectedVal) + ]); } - List generateSignalsRow( - TreeModel module, - String? searchTerm, - bool inputSelected, - bool outputSelected, - ) { - List rows = []; + /// Builds the rows for the signals that match the selected filters. + List generateSignalsRow(TreeModel module, + {required String? searchTerm, + required bool inputSelected, + required bool outputSelected, + required bool inoutSelected}) { + final rows = []; // Filter signals - List inputSignals = inputSelected + final inputSignals = inputSelected ? SignalService.filterSignals(module.inputs, searchTerm ?? '') - : []; - List outputSignals = outputSelected + : []; + final outputSignals = outputSelected ? SignalService.filterSignals(module.outputs, searchTerm ?? '') - : []; + : []; + final inoutSignals = inoutSelected + ? SignalService.filterSignals(module.inouts, searchTerm ?? '') + : []; // Add input from signal model list to row - for (var signal in inputSignals) { + for (final signal in inputSignals) { rows.add(_generateSignalRow(signal)); } // Add output from signal model list to row - for (var signal in outputSignals) { + for (final signal in outputSignals) { + rows.add(_generateSignalRow(signal)); + } + + for (final signal in inoutSignals) { rows.add(_generateSignalRow(signal)); } return rows; } - TableRow _generateSignalRow(SignalModel signal) { - return TableRow( - children: [ - SizedBox( - height: 32, - child: Center( - child: Text(signal.name), - ), - ), + TableRow _generateSignalRow(SignalModel signal) => + TableRow(children: [ + SizedBox(height: 32, child: Center(child: Text(signal.name))), + SizedBox(height: 32, child: Center(child: Text(signal.direction))), + SizedBox(height: 32, child: Center(child: Text(_lookupValue(signal)))), SizedBox( - height: 32, - child: Center( - child: Text(signal.direction), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.value), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.width.toString()), - ), - ), - ], - ); + height: 32, child: Center(child: Text(signal.width.toString()))) + ]); + + String _lookupValue(SignalModel signal) { + // Snapshot overlay is currently keyed by signal name because the upstream + // baseline does not yet thread a stable hierarchy-address identity through + // the details table path. This keeps live values working now and leaves a + // clear seam for the later hierarchy-address migration. + final snapshotData = widget.snapshot; + if (snapshotData != null) { + final ss = snapshotData.getSignalByName(signal.name); + if (ss != null) { + return ss.value; + } + } + return signal.value; } - Widget _buildTableHeader({required String text}) { - return SizedBox( + Widget _buildTableHeader({required String text}) => SizedBox( height: 32, child: Center( - child: Text( - text, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), - ), - ), - ); - } + child: Text(text, + style: + const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)))); } diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table_text_field.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table_text_field.dart index 4696ac39f..23a4e480b 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table_text_field.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table_text_field.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // signal_table_text_field.dart @@ -7,25 +7,133 @@ // 2024 January 5 // Author: Yao Jing Quek +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class SignalTableTextField extends StatelessWidget { +/// A text field widget for filtering signals in the signal table. +/// +/// Supports regex patterns (indicated by hint text). Includes a prefix +/// filter icon and a clear button that appears when text is entered. +class SignalTableTextField extends StatefulWidget { + /// The label text for the text field. final String labelText; + + /// Callback when the text field value changes. final ValueChanged onChanged; + /// Creates a [SignalTableTextField] with the given label and change callback. const SignalTableTextField({ - super.key, required this.labelText, required this.onChanged, + super.key, }); + @override + State createState() => _SignalTableTextFieldState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('labelText', labelText)) + ..add( + ObjectFlagProperty>.has('onChanged', onChanged), + ); + } +} + +class _SignalTableTextFieldState extends State { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _clear() { + _controller.clear(); + widget.onChanged(''); + } + @override Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Expanded( - child: TextField( - onChanged: onChanged, - decoration: InputDecoration( - labelText: labelText, + child: SizedBox( + height: 32, + child: TextField( + controller: _controller, + onChanged: widget.onChanged, + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white : Colors.black, + ), + decoration: InputDecoration( + hintText: '${widget.labelText} (regex supported)', + hintStyle: TextStyle( + fontSize: 12, + color: isDark ? Colors.white38 : Colors.black38, + ), + prefixIcon: Icon( + Icons.filter_list, + size: 16, + color: isDark ? Colors.white38 : Colors.black38, + ), + prefixIconConstraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + suffixIcon: ValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, _) { + if (value.text.isEmpty) { + return const SizedBox.shrink(); + } + return IconButton( + icon: Icon( + Icons.clear, + size: 16, + color: isDark ? Colors.white38 : Colors.black38, + ), + onPressed: _clear, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ); + }, + ), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 6, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: BorderSide( + color: isDark ? Colors.white24 : Colors.black12, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: BorderSide( + color: isDark ? Colors.white24 : Colors.black12, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + filled: true, + fillColor: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.03), + ), ), ), ); diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/simulation_time_display.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/simulation_time_display.dart new file mode 100644 index 000000000..757a833a5 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/simulation_time_display.dart @@ -0,0 +1,30 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// simulation_time_display.dart +// Display-level formatter for simulation time values. +// +// 2026 July +// Author: Desmond Kirkpatrick + +/// Display settings for simulation time values. +class SimulationTimeDisplay { + /// Default display settings when no unit is known. + static const none = SimulationTimeDisplay(); + + /// Optional unit suffix used when formatting simulation time. + final String? unit; + + /// Creates display settings for simulation time values. + const SimulationTimeDisplay({this.unit}); + + /// Formats [time] with the configured unit, if any. + String format(int time) { + final trimmedUnit = unit?.trim(); + if (trimmedUnit == null || trimmedUnit.isEmpty) { + return time.toString(); + } + + return '$time$trimmedUnit'; + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/standalone_app_shell.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/standalone_app_shell.dart new file mode 100644 index 000000000..62cbe2ed2 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/standalone_app_shell.dart @@ -0,0 +1,380 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// standalone_app_shell.dart +// Minimal standalone shell for early startup/connection porting. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/const/app_theme.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/cubit/cubits.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/models/dtd_vm_service_info.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/services/connection_state_machine.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/view/tree_structure_page.dart'; + +/// Configuration for the standalone ROHD DevTools app shell. +class StandaloneAppConfig { + /// Title shown in AppBar. + final String title; + + /// Strategy for connecting to VM service. + final VmConnectionStrategy? connectionStrategy; + + /// Constructor for [StandaloneAppConfig]. + const StandaloneAppConfig({ + this.title = 'ROHD DevTools (Standalone)', + this.connectionStrategy, + }); +} + +/// Standalone app entry point that wires up theming and the app shell. +class StandaloneRohdDevToolsApp extends StatelessWidget { + /// Configuration used by the standalone app shell. + final StandaloneAppConfig config; + + /// Creates the standalone ROHD DevTools app. + const StandaloneRohdDevToolsApp({ + super.key, + this.config = const StandaloneAppConfig(), + }); + + @override + + /// Builds the top-level app and injects theme state. + Widget build(BuildContext context) => BlocProvider( + create: (context) => DevToolsThemeCubit(), + child: BlocBuilder( + builder: (context, themeMode) { + final isDark = themeMode == DevToolsThemeMode.dark; + + return MaterialApp( + title: config.title, + debugShowCheckedModeBanner: false, + themeMode: isDark ? ThemeMode.dark : ThemeMode.light, + darkTheme: buildDarkTheme(), + theme: buildLightTheme(), + home: StandaloneDevToolsPage(config: config), + ); + }, + ), + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('config', config)); + } +} + +/// The main standalone page that manages connections and content. +class StandaloneDevToolsPage extends StatefulWidget { + /// Configuration for the standalone page. + final StandaloneAppConfig config; + + /// Creates the standalone DevTools page. + const StandaloneDevToolsPage({required this.config, super.key}); + + @override + + /// Creates the mutable state for [StandaloneDevToolsPage]. + State createState() => _StandaloneDevToolsPageState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('config', config)); + } +} + +class _StandaloneDevToolsPageState + extends DevToolsConnectionHostState { + late final RohdServiceCubit _rohdServiceCubit = RohdServiceCubit( + manageServiceManager: false, + ); + late final SnapshotCubit _snapshotCubit = SnapshotCubit(); + late final TreeSearchTermCubit _treeSearchTermCubit = TreeSearchTermCubit(); + late final SelectedModuleCubit _selectedModuleCubit = SelectedModuleCubit(); + late final SignalSearchTermCubit _signalSearchTermCubit = + SignalSearchTermCubit(); + + @override + + /// Returns the connection strategy requested by the widget config. + VmConnectionStrategy? get connectionStrategy => + widget.config.connectionStrategy; + + @override + + /// Initializes the connection dialog and supporting listeners. + void initState() { + super.initState(); + // Auto-pop the connection dialog after the first frame, once + // fonts have settled on web (so glyphs render correctly). + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && !isConnected) { + unawaited(_showConnectionDialogWhenReady()); + } + }); + } + + /// Wait for icon fonts to load on web before showing the dialog so + /// the form's glyphs (e.g. dropdown chevrons) render on the first + /// frame instead of as boxes. No-op on native platforms. + Future _showConnectionDialogWhenReady() async { + if (kIsWeb) { + final completer = Completer(); + void onFontsChanged() { + if (!completer.isCompleted) { + completer.complete(); + } + } + + PaintingBinding.instance.systemFonts.addListener(onFontsChanged); + await completer.future.timeout( + const Duration(milliseconds: 1500), + onTimeout: () {}, + ); + PaintingBinding.instance.systemFonts.removeListener(onFontsChanged); + + // Give CanvasKit one extra frame to rasterise the glyphs. + await Future.delayed(const Duration(milliseconds: 100)); + if (mounted) { + await WidgetsBinding.instance.endOfFrame; + } + } + if (!mounted || isConnected) { + return; + } + await showConnectionDialog(); + } + + @override + + /// Handles a successful VM connection by configuring the ROHD service. + Future onVmConnected(VmConnectionResult result, String uri) async { + await _rohdServiceCubit.configureStandaloneVmService( + result.vmService, + result.isolateId, + ); + await _loadHierarchyFromVm(); + } + + @override + Future onCsmLoadHierarchy() => _loadHierarchyFromVm(); + + @override + + /// Clears the standalone tree service when the connection is torn down. + Future tearDownOldConnection({ + required VmConnectionTransition transition, + }) async { + _rohdServiceCubit.treeService = null; + } + + @override + + /// Reopens the connection dialog after the VM disconnects. + void onVmDisconnected() { + // Re-pop the connection dialog so the user can reconnect. + if (mounted) { + unawaited(showConnectionDialog()); + } + } + + @override + + /// Reconfigures the ROHD service after a lightweight reconnect. + Future onLightweightReconnectSuccess( + VmConnectionResult result, + String uri, + ) async { + await _rohdServiceCubit.configureStandaloneVmService( + result.vmService, + result.isolateId, + ); + await _loadHierarchyFromVm(); + } + + Future _loadHierarchyFromVm() async { + await _rohdServiceCubit.evalModuleTree(); + + final state = _rohdServiceCubit.state; + final success = switch (state) { + RohdServiceLoaded(treeModel: _) => true, + _ => false, + }; + + connectionStateMachine.handleEvent(HierarchyLoadResult(success: success)); + } + + @override + + /// Releases cubits used by the standalone shell. + void dispose() { + unawaited(_rohdServiceCubit.close()); + unawaited(_snapshotCubit.close()); + unawaited(_treeSearchTermCubit.close()); + unawaited(_selectedModuleCubit.close()); + unawaited(_signalSearchTermCubit.close()); + super.dispose(); + } + + void _openConnectionDialog() => unawaited(showConnectionDialog()); + + void _disconnect() => unawaited(disconnect()); + + /// Override the base dialog content to wire dismiss-on-success and + /// the standalone shell's discovered-services memory. + @override + + /// Builds the standalone connection dialog content. + Widget buildConnectionDialogContent(BuildContext dialogContext) => + VmConnectionForm( + vmServiceUriController: vmServiceUriController, + dtdUriController: dtdUriController, + connectionError: connectionError, + onConnect: () async { + try { + await attemptConnection(); + if (mounted && dialogContext.mounted && isConnected) { + Navigator.of(dialogContext).pop(); + } + } on Exception catch (e) { + setState(() { + connectionError = 'Connection failed: $e'; + }); + } + }, + cleanVmServiceUri: DevToolsConnectionHostState.cleanVmServiceUri, + cleanDtdUri: DevToolsConnectionHostState.cleanDtdUri, + discoverVmServices: discoverVmServices, + hasColorEmoji: true, + initialDiscoveredServices: rememberedServices + ?.map( + (s) => DiscoveredVmService( + name: s.name, + uri: s.uri, + exposedUri: s.exposedUri, + isAlive: s.isAlive, + autoReconnect: s.autoReconnect, + ), + ) + .toList(), + onServicesDiscovered: (services) { + rememberedServices = services + .map( + (s) => DtdVmServiceInfo.fromFields( + name: s.name, + uri: s.uri, + exposedUri: s.exposedUri, + isAlive: s.isAlive, + autoReconnect: s.autoReconnect, + ), + ) + .toList(); + }, + ); + + /// Builds the empty state shown before any connection is established. + Widget _buildEmptyConnectionState() { + final isDark = Theme.of(context).brightness == Brightness.dark; + final secondaryTextColor = isDark ? Colors.white70 : Colors.black54; + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.cable_outlined, + size: 72, + color: Theme.of(context).disabledColor, + ), + const SizedBox(height: 16), + const Text( + 'Not connected', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 8), + Text( + 'Connect to a running ROHD application to begin.', + style: TextStyle(color: secondaryTextColor), + ), + const SizedBox(height: 24), + FilledButton.icon( + icon: const Icon(Icons.link), + label: const Text('Connect…'), + onPressed: () => unawaited(showConnectionDialog()), + ), + ], + ), + ); + } + + @override + + /// Builds the standalone shell, switching between connected and empty UI. + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final accentColor = Theme.of(context).colorScheme.primary; + return Scaffold( + appBar: AppBar( + title: Text(widget.config.title), + actions: [ + if (isConnected) ...[ + IconButton( + tooltip: 'Disconnect', + onPressed: _disconnect, + icon: const Icon(Icons.link_off), + ), + ] else + IconButton( + tooltip: 'Connect…', + onPressed: _openConnectionDialog, + icon: const Icon(Icons.link), + ), + BlocBuilder( + builder: (context, themeMode) { + final isDark = themeMode == DevToolsThemeMode.dark; + + return IconButton( + tooltip: + isDark ? 'Switch to light theme' : 'Switch to dark theme', + onPressed: () { + context.read().toggleTheme(); + }, + icon: platformIcon( + isDark ? Icons.light_mode : Icons.dark_mode, + isDark ? '☀️' : '🌙', + size: 24, + color: accentColor, + hasColorEmoji: kIsWeb, + ), + ); + }, + ), + DevToolsHelpButton(isDark: isDark), + ], + ), + body: !isConnected + ? _buildEmptyConnectionState() + : MultiBlocProvider( + providers: [ + BlocProvider.value(value: _rohdServiceCubit), + BlocProvider.value(value: _snapshotCubit), + BlocProvider.value(value: _treeSearchTermCubit), + BlocProvider.value(value: _selectedModuleCubit), + BlocProvider.value(value: _signalSearchTermCubit), + BlocProvider(create: (context) => DetailsTabCubit()), + ], + child: TreeStructurePage(screenSize: MediaQuery.of(context).size), + ), + ); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/ui.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/ui.dart new file mode 100644 index 000000000..7f5463121 --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/ui.dart @@ -0,0 +1,21 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// ui.dart +// Barrel file for rohd_devtools UI widgets. +// +// NOTE: standalone_app_shell.dart is excluded because it imports this barrel. + +export 'details_help_button.dart'; +export 'devtool_appbar.dart'; +export 'devtools_connection_host.dart'; +export 'devtools_help_button.dart'; +export 'module_tree_card.dart'; +export 'module_tree_details_navbar.dart'; +export 'platform_icon.dart'; +export 'schematic_icon.dart'; +export 'signal_details_card.dart'; +export 'signal_table.dart'; +export 'signal_table_text_field.dart'; +export 'simulation_time_display.dart'; +export 'vm_connection_form.dart'; diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/vm_connection_form.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/vm_connection_form.dart new file mode 100644 index 000000000..74848f14f --- /dev/null +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/vm_connection_form.dart @@ -0,0 +1,729 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// vm_connection_form.dart +// Reusable VM connection form widget for both initial screen and dialog. +// +// 2026 February +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/platform_icon.dart'; + +/// Describes a single VM service discovered via DTD. +class DiscoveredVmService with Diagnosticable { + /// Human-readable name (may be null). + final String? name; + + /// Direct VM service URI. + final String uri; + + /// Exposed/forwarded URI (preferred over [uri] when available). + final String? exposedUri; + + /// Whether this VM service is currently reachable. + /// + /// Set to `false` by auto-rediscovery when the service is no longer + /// found via the DTD. Dead services are shown grayed-out in the list. + bool isAlive; + + /// Whether to automatically reconnect to this VM by name if it dies + /// and a new VM with the same name appears via DTD discovery. + bool autoReconnect; + + /// The URI to use for connection (prefers exposedUri). + String get connectionUri => exposedUri ?? uri; + + /// Construction for [DiscoveredVmService]. + DiscoveredVmService({ + required this.uri, + this.name, + this.exposedUri, + this.isAlive = true, + this.autoReconnect = false, + }); + + /// A compact display label. + String get displayLabel { + final label = name ?? 'VM Service'; + final preview = connectionUri.length > 50 + ? '${connectionUri.substring(0, 50)}…' + : connectionUri; + return '$label — ' + '$preview'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('name', name)) + ..add(StringProperty('uri', uri)) + ..add(StringProperty('exposedUri', exposedUri)) + ..add(FlagProperty('isAlive', value: isAlive)) + ..add(FlagProperty('autoReconnect', value: autoReconnect)) + ..add(StringProperty('connectionUri', connectionUri)) + ..add(StringProperty('displayLabel', displayLabel)); + } +} + +/// Callback that discovers VM services from a DTD URI. +/// +/// Returns the list of services found, or throws on error. +typedef DiscoverVmServicesCallback = Future> Function( + String dtdUri); + +/// Reusable VM connection form that can be embedded in different contexts. +/// +/// This widget encapsulates the DTD URI discovery and VM Service URI input, +/// and can be used both as the initial connection screen and in dialogs. +/// +/// Layout (top to bottom): +/// 1. DTD URI field + Discover button +/// 2. Discovered VM list (when available) +/// 3. VM Service URI field (manual override) +/// 4. Connect button +class VmConnectionForm extends StatefulWidget { + /// Controller for VM Service URI + final TextEditingController vmServiceUriController; + + /// Controller for DTD URI + final TextEditingController dtdUriController; + + /// Current connection error message (if any) + final String? connectionError; + + /// Callback when Connect button is pressed + final VoidCallback onConnect; + + /// Callback when Demo mode button is pressed (optional) + final VoidCallback? onDemoMode; + + /// Whether to show the demo mode button and help text + final bool showDemoButton; + + /// Whether emoji colors are available (for platform icons) + final bool hasColorEmoji; + + /// Callback to clean VM Service URIs + final String Function(String) cleanVmServiceUri; + + /// Callback to clean DTD URIs + final String Function(String) cleanDtdUri; + + /// Callback that discovers VM services from a DTD URI. + final DiscoverVmServicesCallback? discoverVmServices; + + /// Previously discovered services to pre-populate the list. + /// + /// When returning to the connection screen after a VM death, the parent + /// passes the remembered list (with [DiscoveredVmService.isAlive] set + /// appropriately) so the user can see which VMs are still available. + final List? initialDiscoveredServices; + + /// Called whenever the form discovers (or re-discovers) VM services. + /// + /// The parent should save this list so it can be passed back as + /// [initialDiscoveredServices] if the connection screen is shown again. + final ValueChanged>? onServicesDiscovered; + + /// Construction for [VmConnectionForm]. + const VmConnectionForm({ + required this.vmServiceUriController, + required this.dtdUriController, + required this.onConnect, + required this.cleanVmServiceUri, + required this.cleanDtdUri, + this.connectionError, + this.onDemoMode, + this.showDemoButton = false, + this.hasColorEmoji = false, + this.discoverVmServices, + this.initialDiscoveredServices, + this.onServicesDiscovered, + super.key, + }); + + @override + + /// Creates the state object for the VM connection form. + State createState() => _VmConnectionFormState(); + + @override + + /// Adds diagnostic properties for the connection form. + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add( + DiagnosticsProperty( + 'vmServiceUriController', + vmServiceUriController, + ), + ) + ..add( + DiagnosticsProperty( + 'dtdUriController', + dtdUriController, + ), + ) + ..add(StringProperty('connectionError', connectionError)) + ..add( + ObjectFlagProperty( + 'onConnect', + onConnect, + ifNull: 'disabled', + ), + ) + ..add( + ObjectFlagProperty( + 'onDemoMode', + onDemoMode, + ifNull: 'disabled', + ), + ) + ..add(FlagProperty('showDemoButton', value: showDemoButton)) + ..add(FlagProperty('hasColorEmoji', value: hasColorEmoji)) + ..add( + DiagnosticsProperty( + 'cleanVmServiceUri', + cleanVmServiceUri, + ), + ) + ..add( + DiagnosticsProperty( + 'cleanDtdUri', + cleanDtdUri, + ), + ) + ..add( + ObjectFlagProperty( + 'discoverVmServices', + discoverVmServices, + ifNull: 'disabled', + ), + ) + ..add( + DiagnosticsProperty?>( + 'initialDiscoveredServices', + initialDiscoveredServices, + ), + ) + ..add( + ObjectFlagProperty>?>( + 'onServicesDiscovered', + onServicesDiscovered, + ifNull: 'disabled', + ), + ); + } +} + +class _VmConnectionFormState extends State { + List? _discoveredServices; + bool _isDiscovering = false; + bool _discoveryCancelled = false; + String? _discoveryError; + + static const _uriTextStyle = TextStyle( + fontFamily: 'monospace', + letterSpacing: 0, + ); + + TextStyle? get _nativeUriTextStyle => kIsWeb ? null : _uriTextStyle; + + void _connect() { + final raw = widget.vmServiceUriController.text; + final cleaned = widget.cleanVmServiceUri(raw); + if (cleaned.isNotEmpty && cleaned != raw) { + widget.vmServiceUriController.text = cleaned; + widget.vmServiceUriController.selection = + TextSelection.collapsed(offset: cleaned.length); + } + widget.onConnect(); + } + + @override + void initState() { + super.initState(); + // Pre-populate with remembered services (may include dead ones) + if (widget.initialDiscoveredServices != null) { + _discoveredServices = List.from( + widget.initialDiscoveredServices!, + ); + } else if (widget.dtdUriController.text.isNotEmpty && + widget.discoverVmServices != null) { + // DTD URI is already set (e.g. app reload) — auto-discover. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + unawaited(_discoverServices()); + } + }); + } + } + + Future _discoverServices() async { + final raw = widget.dtdUriController.text; + if (raw.isEmpty) { + return; + } + + final cleaned = widget.cleanDtdUri(raw); + if (!cleaned.startsWith('ws')) { + return; + } + + widget.dtdUriController.text = cleaned; + + if (widget.discoverVmServices == null) { + return; + } + + setState(() { + _isDiscovering = true; + _discoveryCancelled = false; + _discoveryError = null; + _discoveredServices = null; + }); + + try { + final services = await widget.discoverVmServices!(cleaned); + if (!mounted || _discoveryCancelled) { + return; + } + setState(() { + _isDiscovering = false; + _discoveredServices = services; + if (services.isEmpty) { + _discoveryError = 'No VM services found. Is your app running?'; + } else if (services.length == 1) { + // Auto-select the only service and enable auto-reconnect + widget.vmServiceUriController.text = services.first.connectionUri; + services.first.autoReconnect = true; + } + }); + // Notify parent so it can remember these across reconnects + widget.onServicesDiscovered?.call(services); + } on Exception catch (e) { + if (!mounted) { + return; + } + + // Determine the specific error to display better messages + final errorStr = e.toString().toLowerCase(); + final errorMessage = _getDiscoveryErrorMessage(errorStr, cleaned); + + setState(() { + _isDiscovering = false; + _discoveryError = errorMessage; + }); + } + } + + /// Generates a user-friendly error message based on the exception type. + /// + /// Distinguishes between DTD connection errors (invalid address) + /// and other errors, providing specific guidance for each case. + String _getDiscoveryErrorMessage(String errorStr, String dtdUri) { + // Check for WebSocket connection errors (invalid DTD address) + if (errorStr.contains('websocket') || + errorStr.contains('connection') || + errorStr.contains('failed to connect') || + errorStr.contains('refused')) { + return 'Failed to connect to DTD address: $dtdUri. ' + 'Please verify the URI is correct and the Dart Tooling Daemon ' + 'is running.'; + } + + // Check for socket timeouts or DNS resolution errors + if (errorStr.contains('timeout') || errorStr.contains('dns')) { + return 'Connection to DTD timed out or could not resolve ' + 'address: $dtdUri. ' + 'Please verify the DTD address is reachable.'; + } + + // Check for certificate/SSL errors + if (errorStr.contains('certificate') || errorStr.contains('ssl')) { + return 'SSL/certificate error connecting to DTD. ' + 'Make sure the DTD certificate is valid.'; + } + + // For other errors, provide a generic message + return 'Discovery failed. Please verify the DTD URI is correct ' + 'and check the console for more details.'; + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Title (only on full-screen layout) + if (widget.showDemoButton) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + platformIcon( + Icons.developer_board, + '🔧', + size: 32, + hasColorEmoji: widget.hasColorEmoji, + ), + const SizedBox(width: 12), + Text( + 'Connect to Dart VM', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + const SizedBox(height: 24), + ], + + // ── 1. DTD URI field ── + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: widget.dtdUriController, + style: _nativeUriTextStyle, + keyboardType: TextInputType.url, + autocorrect: false, + enableSuggestions: false, + smartDashesType: SmartDashesType.disabled, + smartQuotesType: SmartQuotesType.disabled, + decoration: InputDecoration( + labelText: 'DTD URI (auto-discover VMs)', + hintText: 'ws://127.0.0.1:xxxxx/xxxxx=', + hintStyle: _nativeUriTextStyle, + border: const OutlineInputBorder(), + prefixIcon: platformIcon( + Icons.cloud, + '☁️', + size: 20, + hasColorEmoji: widget.hasColorEmoji, + ), + ), + onSubmitted: (_) => _discoverServices(), + ), + ), + const SizedBox(width: 8), + if (_isDiscovering) ...[ + const SizedBox( + height: 56, + child: ElevatedButton( + onPressed: null, + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + height: 56, + child: TextButton( + onPressed: () { + setState(() { + _discoveryCancelled = true; + _isDiscovering = false; + }); + }, + child: const Text('Cancel'), + ), + ), + ] else + SizedBox( + height: 56, // match TextField height + child: ElevatedButton( + onPressed: _discoverServices, + child: const Text('Discover'), + ), + ), + ], + ), + const SizedBox(height: 8), + + // ── 2. Discovered VM list ── + if (_discoveredServices != null && + _discoveredServices!.isNotEmpty) ...[ + Builder( + builder: (context) { + final aliveCount = + _discoveredServices!.where((s) => s.isAlive).length; + final deadCount = _discoveredServices!.length - aliveCount; + final label = deadCount > 0 + ? '$aliveCount VM service(s) available ($deadCount ended):' + : '${_discoveredServices!.length} VM service(s) found:'; + return Text( + label, + style: TextStyle( + fontSize: 12, + color: isDark ? Colors.white70 : Colors.black54, + ), + ); + }, + ), + const SizedBox(height: 4), + Container( + constraints: const BoxConstraints(maxHeight: 160), + decoration: BoxDecoration( + border: Border.all( + color: isDark ? Colors.white24 : Colors.black12, + ), + borderRadius: BorderRadius.circular(8), + ), + child: ListView.separated( + shrinkWrap: true, + itemCount: _discoveredServices!.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final svc = _discoveredServices![index]; + final isDead = !svc.isAlive; + final isSelected = !isDead && + widget.vmServiceUriController.text == svc.connectionUri; + return ListTile( + dense: true, + selected: isSelected, + selectedTileColor: isDark + ? Colors.blue.shade900.withValues(alpha: 0.4) + : Colors.blue.shade50, + leading: platformIcon( + isDead ? Icons.cloud_off : Icons.memory, + isDead ? '🔌' : '🔌', + size: 18, + hasColorEmoji: widget.hasColorEmoji, + color: isDead ? Colors.grey : null, + ), + title: Text( + svc.name ?? 'VM Service ${index + 1}', + style: TextStyle( + fontSize: 13, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + color: isDead ? Colors.grey : null, + ), + ), + subtitle: Text( + isDead + ? '${svc.connectionUri} (ended)' + : svc.connectionUri, + style: TextStyle( + fontSize: 11, + fontFamily: 'monospace', + color: isDead ? Colors.grey : null, + ), + overflow: TextOverflow.ellipsis, + ), + trailing: isDead + ? null + : Tooltip( + message: 'Automatic Reconnect', + child: SizedBox( + width: 24, + height: 24, + child: Checkbox( + value: svc.autoReconnect, + onChanged: (value) { + setState(() { + svc.autoReconnect = value ?? false; + }); + widget.onServicesDiscovered?.call( + _discoveredServices!, + ); + }, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + ), + ), + onTap: () { + setState(() { + widget.vmServiceUriController.text = svc.connectionUri; + }); + // Alive VMs connect immediately; dead VMs just fill + // the URI field so the user can edit before connecting. + if (!isDead) { + widget.onConnect(); + } + }, + ); + }, + ), + ), + const SizedBox(height: 12), + ], + + // Discovery error + if (_discoveryError != null) ...[ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _discoveryError!, + style: const TextStyle(color: Colors.orange, fontSize: 12), + ), + ), + const SizedBox(height: 12), + ], + + // ── 3. VM Service URI field (manual / override) ── + TextField( + controller: widget.vmServiceUriController, + style: _nativeUriTextStyle, + keyboardType: TextInputType.url, + autocorrect: false, + enableSuggestions: false, + smartDashesType: SmartDashesType.disabled, + smartQuotesType: SmartQuotesType.disabled, + decoration: InputDecoration( + labelText: 'VM Service URI', + hintText: 'ws://127.0.0.1:8181/xxxx=/ws', + hintStyle: _nativeUriTextStyle, + border: const OutlineInputBorder(), + prefixIcon: platformIcon( + Icons.link, + '🔗', + size: 20, + hasColorEmoji: widget.hasColorEmoji, + ), + ), + onSubmitted: (_) => _connect(), + ), + const SizedBox(height: 16), + + // Connection error + if (widget.connectionError != null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + platformIcon( + Icons.error, + '❌', + color: Colors.red, + size: 20, + hasColorEmoji: widget.hasColorEmoji, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.connectionError!, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ), + ), + if (widget.connectionError != null) const SizedBox(height: 16), + + // ── 4. Connect button ── + ElevatedButton.icon( + onPressed: _connect, + icon: platformIcon( + Icons.power, + '⚡', + size: 20, + hasColorEmoji: widget.hasColorEmoji, + ), + label: const Text('Connect'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + + // Full-screen layout: demo mode button and help text + if (widget.showDemoButton && widget.onDemoMode != null) ...[ + const SizedBox(height: 16), + + // Divider + Row( + children: [ + const Expanded(child: Divider()), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'OR', + style: TextStyle( + color: isDark ? Colors.white54 : Colors.black54, + ), + ), + ), + const Expanded(child: Divider()), + ], + ), + const SizedBox(height: 16), + + // Demo mode button + OutlinedButton.icon( + onPressed: widget.onDemoMode, + icon: platformIcon( + Icons.play_arrow, + '▶️', + size: 20, + hasColorEmoji: widget.hasColorEmoji, + ), + label: const Text('Continue without Connection (Demo examples)'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + ), + const SizedBox(height: 24), + + // Help text + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'To connect to a running ROHD app:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: isDark ? Colors.white70 : Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + '1. Run your app with: dart run -- ' + 'observe your_app.dart\n' + '2. Copy the VM service URI from the console\n' + '3. Paste it above and click Connect', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: isDark ? Colors.white54 : Colors.black54, + ), + ), + ], + ), + ), + ], + ], + ), + ); + } +} diff --git a/rohd_devtools_extension/lib/rohd_devtools/view/rohd_devtools_page.dart b/rohd_devtools_extension/lib/rohd_devtools/view/rohd_devtools_page.dart index fd880f56b..e6c352864 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/view/rohd_devtools_page.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/view/rohd_devtools_page.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // rohd_devtools_page.dart @@ -7,60 +7,60 @@ // 2025 January 28 // Author: Roberto Torres -import 'package:devtools_app_shared/service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/const/app_theme.dart'; import 'package:rohd_devtools_extension/rohd_devtools/rohd_devtools.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/ui/devtool_appbar.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; +/// Main page for the embedded ROHD DevTools experience. class RohdDevToolsPage extends StatelessWidget { + /// Creates the DevTools page. const RohdDevToolsPage({super.key}); + @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => RohdServiceCubit(), - ), - BlocProvider( - create: (context) => TreeSearchTermCubit(), - ), - BlocProvider( - create: (context) => SelectedModuleCubit(), - ), - BlocProvider( - create: (context) => SignalSearchTermCubit(), - ), - ], - child: const RohdExtensionModule(), - ); - } + + /// Builds the themed DevTools page and its bloc providers. + Widget build(BuildContext context) => MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => DevToolsThemeCubit()), + BlocProvider(create: (context) => RohdServiceCubit()), + BlocProvider(create: (context) => TreeSearchTermCubit()), + BlocProvider(create: (context) => SelectedModuleCubit()), + BlocProvider(create: (context) => SignalSearchTermCubit()), + BlocProvider(create: (context) => DetailsTabCubit()), + BlocProvider(create: (context) => SnapshotCubit()) + ], + child: BlocBuilder( + builder: (context, themeMode) { + final theme = themeMode == DevToolsThemeMode.dark + ? buildDarkTheme() + : buildLightTheme(); + + return Theme(data: theme, child: const RohdExtensionModule()); + })); } +/// Extension module wrapper used by the DevTools host. class RohdExtensionModule extends StatefulWidget { + /// Creates the extension module. const RohdExtensionModule({super.key}); @override + + /// Creates the module state. State createState() => _RohdExtensionModuleState(); } class _RohdExtensionModuleState extends State { - late final EvalOnDartLibrary rohdControllerEval; - @override - void initState() { - super.initState(); - } - @override + /// Builds the module scaffold and tree view. Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; return Scaffold( - appBar: const DevtoolAppBar(), - body: TreeStructurePage( - screenSize: screenSize, - ), - ); + appBar: const DevtoolAppBar(), + body: TreeStructurePage(screenSize: screenSize)); } } diff --git a/rohd_devtools_extension/lib/rohd_devtools/view/tree_structure_page.dart b/rohd_devtools_extension/lib/rohd_devtools/view/tree_structure_page.dart index 91c57b1b7..83dca6df7 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/view/tree_structure_page.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/view/tree_structure_page.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // tree_structure_page.dart @@ -7,195 +7,232 @@ // 2024 January 5 // Author: Yao Jing Quek +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/cubit/rohd_service_cubit.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/cubit/tree_search_term_cubit.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/ui/signal_details_card.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/ui/module_tree_details_navbar.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/ui/module_tree_card.dart'; -import 'package:rohd_devtools_extension/rohd_devtools/cubit/selected_module_cubit.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/cubit/cubits.dart'; +import 'package:rohd_devtools_extension/rohd_devtools/ui/ui.dart'; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +/// Split-pane page showing the module tree and selected module details. class TreeStructurePage extends StatelessWidget { - TreeStructurePage({ - super.key, - required this.screenSize, - }); + /// Creates the tree structure page. + TreeStructurePage({required this.screenSize, super.key}); + /// Available size used to split the page into two panes. final Size screenSize; + /// Horizontal scroll controller for the tree pane. final ScrollController _horizontal = ScrollController(); + + /// Vertical scroll controller for the tree pane. final ScrollController _vertical = ScrollController(); + /// Boundary used when exporting the tree pane as PNG. + final GlobalKey _treeBoundaryKey = GlobalKey(); + @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - // Module Tree render here (Left Section) - SizedBox( - width: screenSize.width / 2, - height: screenSize.width / 2.6, - child: Card( - clipBehavior: Clip.antiAlias, + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('screenSize', screenSize)); + } + + @override + + /// Builds the split-pane tree structure page. + Widget build(BuildContext context) => MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + final snapshotCubit = context.read(); + + if (state is RohdServiceLoaded) { + final source = + context.read().signalValueSource; + if (source == null) { + return; + } + + if (snapshotCubit.mode != SignalTrackingMode.video) { + snapshotCubit.setMode(SignalTrackingMode.video); + } + + snapshotCubit.startVideoTracking(source); + } else if (state is RohdServiceInitial || + state is RohdServiceError) { + snapshotCubit.clear(); + } + }) + ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildTreePane(context), + _buildDetailsPane(context) + ])))); + + Widget _buildTreePane(BuildContext context) => SizedBox( + width: screenSize.width / 2, + child: Card( + clipBehavior: Clip.antiAlias, + child: Stack(children: [ + RepaintBoundary( + key: _treeBoundaryKey, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(10), - // Module Tree Menu Bar - child: Row( - children: [ - const Icon(Icons.account_tree), - const SizedBox(width: 10), - const Text('Module Tree'), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - width: 200, - child: TextField( - onChanged: (value) { - context - .read() - .setTerm(value); - }, - decoration: const InputDecoration( - labelText: "Search Tree", - ), - ), - ), - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => context - .read() - .evalModuleTree(), - ), - ], - ), - ), - ], - ), - ), - // expand the available column - Expanded( - child: Scrollbar( - thumbVisibility: true, - controller: _vertical, - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - controller: _vertical, - child: Row( - children: [ - Expanded( - child: Scrollbar( - thumbVisibility: true, - controller: _horizontal, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: _horizontal, - child: BlocBuilder( - builder: (context, state) { - if (state is RohdServiceLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (state is RohdServiceLoaded) { - final futureModuleTree = - state.treeModel; - if (futureModuleTree == null) { - return Expanded( - child: Container( - padding: - const EdgeInsets.all(20), - child: const Text( - 'Friendly Notice: Please make ' - 'sure that you use build() method ' - 'to build your module and put ' - 'the breakpoint at the ' - 'simulation time.', - style: - TextStyle(fontSize: 20), - textAlign: TextAlign.center, - ), - ), - ); - } else { - return ModuleTreeCard( - futureModuleTree: - futureModuleTree, - ); - } - } else if (state is RohdServiceError) { - return Center( - child: - Text('Error: ${state.error}'), - ); - } else { - return const Center( - child: Text('Unknown state'), - ); - } - }, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - - // Signal Table Right Section Module - SizedBox( - width: screenSize.width / 2, - height: screenSize.width / 2.6, - child: Card( - clipBehavior: Clip.antiAlias, - child: SingleChildScrollView( - child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const ModuleTreeDetailsNavbar(), - Padding( - padding: const EdgeInsets.only(left: 20, right: 20), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: BlocBuilder( - builder: (context, state) { - if (state is SelectedModuleLoaded) { - final selectedModule = state.module; - return SignalDetailsCard( - module: selectedModule, - ); - } else { - return const Center( - child: Text('No module selected'), - ); - } - }, - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ); + _buildTreeToolbar(context), + Expanded( + child: Scrollbar( + thumbVisibility: true, + controller: _vertical, + child: SingleChildScrollView( + controller: _vertical, + child: Row(children: [ + Expanded( + child: Scrollbar( + thumbVisibility: true, + controller: _horizontal, + child: SingleChildScrollView( + scrollDirection: + Axis.horizontal, + controller: _horizontal, + child: BlocBuilder< + RohdServiceCubit, + RohdServiceState>( + builder: (context, state) => + _buildTreeStateBody( + state))))) + ])))) + ])), + Positioned( + right: 8, + bottom: 8, + child: ExportPngButton( + onPressed: () => captureBoundaryToPng(context, + boundaryKey: _treeBoundaryKey, + filePrefix: 'module_tree'))) + ]))); + + Widget _buildTreeToolbar(BuildContext context) => Padding( + padding: const EdgeInsets.all(10), + child: Row(children: [ + const Icon(Icons.account_tree), + const SizedBox(width: 10), + const Text('Module Tree'), + Expanded( + child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + SizedBox( + width: 200, + child: TextField( + onChanged: (value) { + context.read().setTerm(value); + }, + decoration: const InputDecoration(labelText: 'Search Tree'))), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => + context.read().evalModuleTree()) + ])) + ])); + + Widget _buildTreeStateBody(RohdServiceState state) { + if (state is RohdServiceLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (state is RohdServiceLoaded) { + final futureModuleTree = state.treeModel; + if (futureModuleTree == null) { + return Container( + padding: const EdgeInsets.all(20), + child: const Text( + 'Friendly Notice: Please make sure that you use build() ' + 'method to build your module and put the breakpoint at ' + 'the simulation time.', + style: TextStyle(fontSize: 20), + textAlign: TextAlign.center)); + } + + return ModuleTreeCard(futureModuleTree: futureModuleTree); + } + + if (state is RohdServiceError) { + return Center(child: Text('Error: ${state.error}')); + } + + return const Center(child: Text('Unknown state')); + } + + Widget _buildDetailsPane(BuildContext context) => SizedBox( + width: screenSize.width / 2, + child: Card( + clipBehavior: Clip.antiAlias, + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + const ModuleTreeDetailsNavbar(), + Expanded( + child: BlocBuilder( + builder: (context, selectedTab) => + IndexedStack(index: selectedTab.index, children: [ + Padding( + padding: + const EdgeInsets.only(left: 20, right: 20), + child: BlocBuilder( + builder: (context, state) => + BlocBuilder( + builder: (context, snapshotState) { + if (state is SelectedModuleLoaded) { + return SignalDetailsCard( + module: state.module, + snapshot: snapshotState + is SnapshotLoaded + ? snapshotState + : null); + } + + return const Center( + child: Text('No module selected')); + }))), + _buildFeaturePlaceholderPane(context, + icon: platformIcon(Icons.waves, '🌊', + size: 36, + color: Theme.of(context).colorScheme.primary, + hasColorEmoji: kIsWeb), + title: 'Waveform', + message: 'Waveform content will be available ' + 'in a future release.'), + _buildFeaturePlaceholderPane(context, + icon: const SchematicIcon(size: 36), + title: 'Schematic', + message: 'Schematic content will be available ' + 'in a future release.') + ]))) + ]))); + + Widget _buildFeaturePlaceholderPane(BuildContext context, + {required Widget icon, required String title, required String message}) { + final colorScheme = Theme.of(context).colorScheme; + + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 360), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + icon, + const SizedBox(height: 12), + Text(title, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Text(message, + textAlign: TextAlign.center, + style: TextStyle( + color: colorScheme.onSurface.withValues(alpha: 0.72))) + ])))); } } diff --git a/rohd_devtools_extension/lib/rohd_devtools_observer.dart b/rohd_devtools_extension/lib/rohd_devtools_observer.dart index 42b14b167..780d97a4f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools_observer.dart +++ b/rohd_devtools_extension/lib/rohd_devtools_observer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // rohd_devtools_observer.dart @@ -11,9 +11,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// [BlocObserver] observe all state changes in the application. class RohdDevToolsObserver extends BlocObserver { + /// Creates the observer used by the app. const RohdDevToolsObserver(); @override + + /// Forwards bloc state changes to the default observer behavior. void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); } diff --git a/rohd_devtools_extension/linux/.gitignore b/rohd_devtools_extension/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/rohd_devtools_extension/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/rohd_devtools_extension/linux/CMakeLists.txt b/rohd_devtools_extension/linux/CMakeLists.txt new file mode 100644 index 000000000..5b5c5f30f --- /dev/null +++ b/rohd_devtools_extension/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "rohd_devtools_extension") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.rohd_devtools_extension") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the QuickJS bridge library from flutter_js plugin (if it exists). +# This is needed because the flutter_js plugin's CMakeLists.txt has an issue +# where it doesn't properly export the bundled libraries variable. +set(QUICKJS_BRIDGE_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/flutter/ephemeral/.plugin_symlinks/flutter_js/linux/shared/libquickjs_c_bridge_plugin.so") +if(EXISTS "${QUICKJS_BRIDGE_SOURCE}") + install(FILES "${QUICKJS_BRIDGE_SOURCE}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/rohd_devtools_extension/linux/flutter/CMakeLists.txt b/rohd_devtools_extension/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/rohd_devtools_extension/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/rohd_devtools_extension/linux/flutter/generated_plugin_registrant.cc b/rohd_devtools_extension/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..f6f23bfe9 --- /dev/null +++ b/rohd_devtools_extension/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/rohd_devtools_extension/linux/flutter/generated_plugin_registrant.h b/rohd_devtools_extension/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/rohd_devtools_extension/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/rohd_devtools_extension/linux/flutter/generated_plugins.cmake b/rohd_devtools_extension/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..f16b4c342 --- /dev/null +++ b/rohd_devtools_extension/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/rohd_devtools_extension/linux/runner/CMakeLists.txt b/rohd_devtools_extension/linux/runner/CMakeLists.txt new file mode 100644 index 000000000..e97dabc70 --- /dev/null +++ b/rohd_devtools_extension/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/rohd_devtools_extension/linux/runner/main.cc b/rohd_devtools_extension/linux/runner/main.cc new file mode 100644 index 000000000..e7c5c5437 --- /dev/null +++ b/rohd_devtools_extension/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/rohd_devtools_extension/linux/runner/my_application.cc b/rohd_devtools_extension/linux/runner/my_application.cc new file mode 100644 index 000000000..307532496 --- /dev/null +++ b/rohd_devtools_extension/linux/runner/my_application.cc @@ -0,0 +1,144 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView *view) +{ + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "rohd_devtools_extension"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "rohd_devtools_extension"); + } + + gtk_window_set_default_size(window, 2100, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/rohd_devtools_extension/linux/runner/my_application.h b/rohd_devtools_extension/linux/runner/my_application.h new file mode 100644 index 000000000..72271d5e4 --- /dev/null +++ b/rohd_devtools_extension/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/LICENSE b/rohd_devtools_extension/packages/rohd_devtools_widgets/LICENSE new file mode 100644 index 000000000..9e00cc822 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (C) 2021-2026 Intel Corporation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md new file mode 100644 index 000000000..90d3e3681 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md @@ -0,0 +1,13 @@ +# ROHD DevTools Widgets + +Shared Flutter widgets and utilities for ROHD DevTools debugger views. + +This package contains reusable UI pieces used across ROHD debugger tools such as schematic and waveform viewers. It is intended for common controls and presentation helpers that should stay consistent across DevTools packages, including shared menus, help system components, export helpers, and other supporting widgets. + +## Usage + +Add this package as a path dependency from a ROHD DevTools package and import the shared widgets you need: + +```dart +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +``` diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml new file mode 100644 index 000000000..572dd239d --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart new file mode 100644 index 000000000..452567fae --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart @@ -0,0 +1,41 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_devtools_widgets.dart +// Barrel file for the rohd_devtools_widgets package. +// Combines help_api, export_png, and overlay_api into one package. +// +// 2026 April +// Author: Desmond Kirkpatrick + +// Help +export 'src/markdown_help_button.dart'; + +// Overlay +export 'src/app_bar_overlay.dart'; + +// PNG export +export 'src/capture_boundary.dart'; +export 'src/export_button.dart'; +export 'src/export_toast.dart'; +export 'src/save_png_stub.dart' + if (dart.library.io) 'src/save_png_native.dart' + if (dart.library.js_interop) 'src/save_png_web.dart'; + +// Cross-probing +export 'src/cross_probe_service.dart'; +export 'src/cross_probe_button.dart'; +export 'src/cross_probe_menu.dart'; + +// Logic type utilities +export 'src/logic_type_utils.dart'; + +// Bit-field parsing, formatting, and dialog utilities +export 'src/bit_field_utils.dart'; + +// Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers +export 'src/bit_expansion_menu.dart'; + +// ROHD extension client +export 'src/rohd_extension_status.dart'; +export 'src/rohd_extension_client.dart'; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart new file mode 100644 index 000000000..ba210d5b2 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/app_bar_overlay.dart @@ -0,0 +1,168 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// app_bar_overlay.dart +// Auto-hiding overlay AppBar that slides in from the top edge. +// +// When [autoHide] is true, the bar slides out of view and reappears when +// the mouse enters a thin trigger zone along the top edge. When [autoHide] +// is false the bar behaves like a normal AppBar (always visible, pushes +// content down). +// +// Designed to be reusable across ROHD Wave Viewer, Schematic Viewer, etc. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; + +/// Wraps a [body] widget and an [appBar] widget, where the AppBar +/// auto-hides by sliding up when [autoHide] is true. +/// +/// When [autoHide] is false the layout is a simple Column (AppBar + body), +/// matching normal Scaffold behaviour. +class AppBarOverlay extends StatefulWidget { + /// The AppBar-like widget to show/hide. + final PreferredSizeWidget appBar; + + /// The main content below the AppBar. + final Widget body; + + /// When true, the AppBar auto-hides and slides in on mouse hover. + /// When false, the AppBar is always visible. + final bool autoHide; + + /// Height of the invisible trigger zone along the top edge (pixels). + final double triggerHeight; + + /// Opacity of the overlay AppBar when shown (0.0–1.0). + final double panelOpacity; + + /// Duration of the slide animation. + final Duration animationDuration; + + const AppBarOverlay({ + super.key, + required this.appBar, + required this.body, + this.autoHide = false, + this.triggerHeight = 12, + this.panelOpacity = 0.92, + this.animationDuration = const Duration(milliseconds: 200), + }); + + @override + State createState() => _AppBarOverlayState(); +} + +class _AppBarOverlayState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.animationDuration, + ); + _slideAnimation = Tween( + begin: const Offset(0, -1), // fully off-screen above + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + reverseCurve: Curves.easeInCubic, + )); + + // If not auto-hiding, snap open. + if (!widget.autoHide) { + _controller.value = 1.0; + } + } + + @override + void didUpdateWidget(covariant AppBarOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (!widget.autoHide && oldWidget.autoHide) { + // Switched from auto-hide → always visible: snap open. + _controller.forward(); + } else if (widget.autoHide && !oldWidget.autoHide) { + // Switched from always visible → auto-hide: hide immediately. + _controller.reverse(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _show() { + _controller.forward(); + } + + void _hide() { + if (!widget.autoHide) return; + _controller.reverse(); + } + + @override + Widget build(BuildContext context) { + // ── When not auto-hiding, simple column layout ── + if (!widget.autoHide) { + return Column( + children: [ + widget.appBar, + Expanded(child: widget.body), + ], + ); + } + + // ── Auto-hide mode: overlay with trigger zone ── + final appBarHeight = + widget.appBar.preferredSize.height + MediaQuery.of(context).padding.top; + + return Stack( + fit: StackFit.expand, + children: [ + // Body fills the entire area (no top inset — content goes edge-to-edge) + Positioned.fill(child: widget.body), + + // Trigger zone: thin invisible strip along the top edge + Positioned( + left: 0, + right: 0, + top: 0, + height: widget.triggerHeight, + child: MouseRegion( + onEnter: (_) => _show(), + opaque: false, // let clicks through when AppBar is hidden + child: const SizedBox.expand(), + ), + ), + + // Sliding overlay AppBar + Positioned( + left: 0, + right: 0, + top: 0, + height: appBarHeight, + child: SlideTransition( + position: _slideAnimation, + child: MouseRegion( + onEnter: (_) => _show(), + onExit: (_) => _hide(), + child: Opacity( + opacity: widget.panelOpacity, + child: widget.appBar, + ), + ), + ), + ), + ], + ); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart new file mode 100644 index 000000000..c01c55e31 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_expansion_menu.dart @@ -0,0 +1,139 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// bit_expansion_menu.dart +// Shared popup-menu items and dispatcher for the "Expand Bits" and +// "Define Bit Fields" actions. Used by all three surfaces that offer +// per-signal right-click menus: the waveform Signal-Selection overlay, +// the Selected-Signals panel, and the embedded Signal Details pane. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; + +import 'bit_field_utils.dart'; + +/// Popup-menu values used by the bit-expansion items. +/// +/// Callers may match against these string constants when handling a +/// `showMenu` result that includes bit-expansion items. +abstract final class BitExpansionMenuValues { + /// Menu item: "Expand Bits [N]" — expand each bit (or a chosen range) + /// as a synthesized 1-bit waveform. + static const String expandBits = 'expand_bits'; + + /// Menu item: "Define Bit Fields [N]..." — open a dialog that lets the + /// user name arbitrary bit ranges. + static const String defineFields = 'define_fields'; +} + +/// Result of a bit-expansion menu interaction after any follow-up dialog +/// has resolved. Returned by [resolveBitExpansionMenuValue]. +sealed class BitExpansionAction { + const BitExpansionAction(); +} + +/// User picked "Expand Bits" and (implicitly or via dialog) chose the bit +/// range `[bitEnd:bitStart]` to expand into single-bit synthesized +/// waveforms. +class BitExpandRangeAction extends BitExpansionAction { + /// Low bit (inclusive) of the range to expand. + final int bitStart; + + /// High bit (inclusive) of the range to expand. + final int bitEnd; + + const BitExpandRangeAction(this.bitStart, this.bitEnd); +} + +/// User picked "Define Bit Fields..." and entered a non-empty list of +/// named [BitFieldDef]s. +class BitDefineFieldsAction extends BitExpansionAction { + /// The user-defined bit fields. + final List fields; + + const BitDefineFieldsAction(this.fields); +} + +/// Build the standard pair of popup-menu items shown when right-clicking +/// a single multi-bit signal: +/// +/// - **Expand Bits [width]** +/// - **Define Bit Fields [width]...** +/// +/// Callers should typically append these items to their existing +/// `PopupMenuEntry` list only when the signal selection contains +/// exactly one signal whose width is > 1. +/// +/// The optional [includeDivider] inserts a [PopupMenuDivider] before the +/// items so they visually separate from preceding items. +List> buildBitExpansionMenuItems({ + required int width, + double fontSize = 13, + double itemHeight = 32, + bool includeDivider = false, +}) { + return >[ + if (includeDivider) const PopupMenuDivider(height: 8), + PopupMenuItem( + height: itemHeight, + value: BitExpansionMenuValues.expandBits, + child: Text('Expand Bits [$width]', style: TextStyle(fontSize: fontSize)), + ), + PopupMenuItem( + height: itemHeight, + value: BitExpansionMenuValues.defineFields, + child: Text( + 'Define Bit Fields [$width]...', + style: TextStyle(fontSize: fontSize), + ), + ), + ]; +} + +/// Translate a popup-menu [value] returned by `showMenu` into a +/// [BitExpansionAction], showing any follow-up dialog as needed. +/// +/// Returns: +/// * [BitExpandRangeAction] when [value] is +/// [BitExpansionMenuValues.expandBits]. If [width] is at or below +/// [BitFieldUtils.expandThreshold] the full range `(0, width-1)` is +/// returned immediately. Otherwise [showBitRangeDialog] is invoked +/// and `null` is returned if the user cancels. +/// * [BitDefineFieldsAction] when [value] is +/// [BitExpansionMenuValues.defineFields]. [showDefineBitFieldsDialog] +/// is invoked; `null` is returned if the user cancels or enters no +/// fields. +/// * `null` for any other value (callers should handle their own +/// non-bit-expansion menu items first). +Future resolveBitExpansionMenuValue( + BuildContext context, { + required String? value, + required String signalName, + required int width, +}) async { + if (value == BitExpansionMenuValues.expandBits) { + if (width <= BitFieldUtils.expandThreshold) { + return BitExpandRangeAction(0, width - 1); + } + final parsed = await showBitRangeDialog( + context, + signalName: signalName, + width: width, + ); + if (parsed == null) return null; + final (high, low) = parsed; + return BitExpandRangeAction(low, high); + } + if (value == BitExpansionMenuValues.defineFields) { + final fields = await showDefineBitFieldsDialog( + context, + signalName: signalName, + width: width, + ); + if (fields == null || fields.isEmpty) return null; + return BitDefineFieldsAction(fields); + } + return null; +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart new file mode 100644 index 000000000..339057750 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/bit_field_utils.dart @@ -0,0 +1,257 @@ +// Copyright (C) 2024-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// bit_field_utils.dart +// Shared bit-field parsing, formatting, and dialog utilities. +// +// 2025 May +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; + +/// A named bit-field definition within a bitvector signal. +class BitFieldDef { + /// Display name for this field (e.g. "exponent", "mantissa"). + final String name; + + /// High bit (inclusive, MSB of the field). + final int high; + + /// Low bit (inclusive, LSB of the field). + final int low; + + const BitFieldDef({ + required this.name, + required this.high, + required this.low, + }); + + /// Width of this field in bits. + int get width => high - low + 1; +} + +/// Shared utilities for parsing and formatting bit-field definitions. +abstract final class BitFieldUtils { + /// Number of elements/bits above which a confirmation pop-up is shown + /// before expanding an array, struct, or bitvector. + static const int expandThreshold = 8; + + /// Format a bit-range label from [startBit] and [width]. + /// + /// Returns e.g. `[7:4]` for startBit=4, width=4 or `[0]` for width=1. + static String formatBitRange(int startBit, int width) { + final highBit = startBit + width - 1; + return highBit == startBit ? '[$startBit]' : '[$highBit:$startBit]'; + } + + /// Parse a bit range string (`high:low`) or single bit index. + /// + /// Returns `(high, low)` clamped to `[0, maxBit]`, or `null` if invalid. + static (int, int)? parseBitRange(String input, int maxBit) { + if (input.contains(':')) { + final parts = input.split(':'); + if (parts.length != 2) return null; + final high = int.tryParse(parts[0].trim()); + final low = int.tryParse(parts[1].trim()); + if (high == null || low == null) return null; + final h = high.clamp(0, maxBit); + final l = low.clamp(0, maxBit); + return h >= l ? (h, l) : (l, h); + } + final bit = int.tryParse(input); + if (bit == null) return null; + final clamped = bit.clamp(0, maxBit); + return (clamped, clamped); + } + + /// Parse multi-line field definitions into [BitFieldDef] objects. + /// + /// Accepted formats per line: + /// - `name high:low` (e.g. `exponent 31:21`) + /// - `name high` (single bit, e.g. `sign 31`) + /// - `high:low` (unnamed, displayed as `[high:low]`) + /// - `bit` (unnamed single bit, displayed as `[bit]`) + static List parseBitFieldDefs(String input, int maxBit) { + final lines = input.split('\n'); + final fields = []; + for (final rawLine in lines) { + final line = rawLine.trim(); + if (line.isEmpty) continue; + + // Try: name high:low + final namedRange = RegExp(r'^(\w+)\s+(\d+):(\d+)$').firstMatch(line); + if (namedRange != null) { + final name = namedRange.group(1)!; + final a = int.parse(namedRange.group(2)!).clamp(0, maxBit); + final b = int.parse(namedRange.group(3)!).clamp(0, maxBit); + final high = a >= b ? a : b; + final low = a >= b ? b : a; + fields.add(BitFieldDef(name: name, high: high, low: low)); + continue; + } + + // Try: name bit (single bit) + final namedSingle = RegExp(r'^(\w+)\s+(\d+)$').firstMatch(line); + if (namedSingle != null) { + final name = namedSingle.group(1)!; + final bit = int.parse(namedSingle.group(2)!).clamp(0, maxBit); + fields.add(BitFieldDef(name: name, high: bit, low: bit)); + continue; + } + + // Try: high:low (unnamed) + final anonRange = RegExp(r'^(\d+):(\d+)$').firstMatch(line); + if (anonRange != null) { + final a = int.parse(anonRange.group(1)!).clamp(0, maxBit); + final b = int.parse(anonRange.group(2)!).clamp(0, maxBit); + final high = a >= b ? a : b; + final low = a >= b ? b : a; + fields.add(BitFieldDef(name: '[$high:$low]', high: high, low: low)); + continue; + } + + // Try: single number (unnamed single bit) + final anonSingle = RegExp(r'^(\d+)$').firstMatch(line); + if (anonSingle != null) { + final bit = int.parse(anonSingle.group(1)!).clamp(0, maxBit); + fields.add(BitFieldDef(name: '[$bit]', high: bit, low: bit)); + continue; + } + } + return fields; + } +} + +/// Show a dialog to select a bit range for a signal. +/// +/// Returns `(high, low)` or `null` if cancelled. +Future<(int, int)?> showBitRangeDialog( + BuildContext context, { + required String signalName, + required int width, +}) async { + final maxBit = width - 1; + final controller = TextEditingController(text: '$maxBit:0'); + controller.selection = TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ); + + final result = await showDialog( + context: context, + barrierColor: Colors.black26, + builder: (ctx) { + return AlertDialog( + title: Text( + '$signalName [$width bits]', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + labelText: 'Bit range (high:low) or single bit', + hintText: '$maxBit:0', + isDense: true, + ), + onSubmitted: (value) => Navigator.of(ctx).pop(value), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(controller.text), + child: const Text('OK'), + ), + ], + ); + }, + ); + + if (result == null || result.trim().isEmpty) return null; + return BitFieldUtils.parseBitRange(result.trim(), maxBit); +} + +/// Show a dialog to define named bit-field slices on a signal. +/// +/// Returns the parsed [BitFieldDef] list, or `null` if cancelled/empty. +Future?> showDefineBitFieldsDialog( + BuildContext context, { + required String signalName, + required int width, + List? existingDefs, +}) async { + final maxBit = width - 1; + + // Pre-fill with existing definitions if re-editing; append a trailing + // newline and place the cursor at the end so the user can immediately + // type additional fields without accidentally replacing existing ones. + final hasExisting = existingDefs != null && existingDefs.isNotEmpty; + final initialText = hasExisting + ? '${existingDefs.map((f) { + return f.high == f.low + ? '${f.name} ${f.high}' + : '${f.name} ${f.high}:${f.low}'; + }).join('\n')}\n' + : 'field0 $maxBit:0'; + + final controller = TextEditingController(text: initialText); + if (hasExisting) { + // Cursor at the end (after trailing newline) — ready for a new field. + controller.selection = TextSelection.collapsed( + offset: controller.text.length, + ); + } else { + // First time: select all default text for easy replacement. + controller.selection = TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ); + } + + final result = await showDialog( + context: context, + barrierColor: Colors.black26, + builder: (ctx) { + return AlertDialog( + title: Text( + '$signalName [$width bits] — Define Fields', + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + content: SizedBox( + width: 320, + child: TextField( + controller: controller, + autofocus: true, + maxLines: 8, + minLines: 3, + style: const TextStyle(fontFamily: 'monospace', fontSize: 13), + decoration: InputDecoration( + labelText: 'One field per line: name high:low', + hintText: 'exponent $maxBit:${maxBit - 10}\n' + 'mantissa ${maxBit - 11}:0', + isDense: true, + border: const OutlineInputBorder(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(controller.text), + child: const Text('OK'), + ), + ], + ); + }, + ); + + if (result == null || result.trim().isEmpty) return null; + final fields = BitFieldUtils.parseBitFieldDefs(result, maxBit); + return fields.isEmpty ? null : fields; +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/capture_boundary.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/capture_boundary.dart new file mode 100644 index 000000000..172f79d75 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/capture_boundary.dart @@ -0,0 +1,83 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// capture_boundary.dart +// One-call RepaintBoundary → PNG export with toast feedback. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'dart:typed_data' show Uint8List; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show RenderRepaintBoundary; + +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart' as export_png; + +/// Capture a [RepaintBoundary] identified by [boundaryKey], encode to PNG, +/// save/download, and show a toast. +/// +/// [filePrefix] is used as the first part of the file name +/// (e.g. `"schematic"` → `schematic_1713052800000.png`). +/// +/// When [saveFn] is provided it is used **instead** of the default platform +/// save/download. This allows callers (e.g. VS Code webview hosts) to route +/// the PNG bytes through a native Save dialog. [saveFn] receives the raw PNG +/// bytes and a suggested file name, and should return the saved path (or null +/// if no path feedback is available). +/// +/// [pixelRatio] controls the output resolution multiplier. Defaults to 2.0 +/// which works well in webview-constrained environments and keeps PNG sizes +/// manageable for postMessage serialisation. Callers that need print-quality +/// output (e.g. schematic exports) should pass a higher value explicitly. +/// +/// Returns `true` if the export succeeded. +Future captureBoundaryToPng( + BuildContext context, { + required GlobalKey boundaryKey, + String filePrefix = 'export', + double pixelRatio = 2.0, + Future Function(Uint8List pngBytes, String fileName)? saveFn, +}) async { + final boundary = + boundaryKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; + if (boundary == null) { + debugPrint('[ExportPng] No RepaintBoundary found'); + return false; + } + + final image = await boundary.toImage(pixelRatio: pixelRatio); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + image.dispose(); + + if (byteData == null) { + debugPrint('[ExportPng] Failed to encode PNG'); + return false; + } + + final pngBytes = byteData.buffer.asUint8List(); + final fileName = '${filePrefix}_${DateTime.now().millisecondsSinceEpoch}.png'; + + try { + final String? savedPath; + if (saveFn != null) { + savedPath = await saveFn(pngBytes, fileName); + } else { + savedPath = await export_png.savePngBytes(pngBytes, fileName); + } + final msg = + savedPath != null ? 'Saved: $savedPath' : 'Downloaded $fileName'; + debugPrint('[ExportPng] $msg'); + if (context.mounted) { + export_png.showExportToast(context, msg); + } + return true; + } on Object catch (e) { + debugPrint('[ExportPng] Export failed: $e'); + if (context.mounted) { + export_png.showExportToast(context, 'Export failed: $e'); + } + return false; + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart new file mode 100644 index 000000000..ea6148492 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_button.dart @@ -0,0 +1,48 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// cross_probe_button.dart +// Toolbar button for toggling cross-probing between viewers. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; +import 'cross_probe_service.dart'; + +/// A toolbar icon button for cross-probing signal selections between viewers. +/// +/// Displays a bidirectional arrows icon ([Icons.compare_arrows]). Tap to +/// toggle cross-probing on or off via [CrossProbeService.isActive]. +/// +/// When active the icon is rendered in the theme's primary colour; when +/// inactive it uses the theme's disabled colour. +class CrossProbeButton extends StatelessWidget { + /// The cross-probe service whose [CrossProbeService.isActive] state is + /// reflected by this button. + final CrossProbeService service; + + /// Creates a [CrossProbeButton] for the given [service]. + const CrossProbeButton({required this.service, super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: service.isActive, + builder: (context, active, _) { + final color = active + ? Theme.of(context).colorScheme.primary + : Theme.of(context).disabledColor; + return Tooltip( + message: active + ? 'Cross-probing active — tap to disable' + : 'Cross-probing disabled — tap to enable', + child: IconButton( + icon: Icon(Icons.compare_arrows, color: color), + onPressed: () => service.isActive.value = !service.isActive.value, + ), + ); + }, + ); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart new file mode 100644 index 000000000..b8134e4ad --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_menu.dart @@ -0,0 +1,253 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// cross_probe_menu.dart +// Shared helpers for building generalized "Go to Source" cross-probe +// context-menu items across all viewers (wave, schematic, details). +// +// Viewers consult an [AvailableSourceFormats] query (backed by the cached +// ROHD extension module info) to discover which source languages are +// navigable, then build menu items via [buildGotoSourceMenuItems]. A single +// [GoToSourceCallback] handles the selection for every format, and the +// secondary frame picker (when a signal resolves to multiple frames) works +// uniformly for all formats. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; + +import 'rohd_extension_status.dart'; + +/// Returns the source formats currently navigable for the active module. +/// +/// Must be synchronous (reads cached module info) so it can be consulted +/// while a popup menu is being built. +typedef AvailableSourceFormats = List Function(); + +/// Invoked when the user picks `Go to Source` for [signalPaths]. +typedef GoToSourceCallback = void Function( + RohdSourceFormat format, List signalPaths); + +/// Builds an icon for a source/output [format]. +typedef SourceFormatIconBuilder = Widget Function( + RohdSourceFormat format, { + double size, +}); + +/// Prefix used to encode source-navigation entries in a `String`-valued popup +/// menu (e.g. `'goto_source:rohd'`). Allows the shared items to coexist with +/// each viewer's other `String` menu values. +const String _gotoSourceValuePrefix = 'goto_source:'; + +/// Formats shown when source availability is unknown (module info not yet +/// loaded, the extension is unreachable, or the query errored). +const List kDefaultNavigableFormats = [ + RohdSourceFormat.rohd, + RohdSourceFormat.sv, +]; + +/// Encode a popup-menu value for navigating to [format]. +String gotoSourceMenuValue(RohdSourceFormat format) => + '$_gotoSourceValuePrefix${format.name}'; + +/// Decode a popup-menu value produced by [gotoSourceMenuValue]. +/// +/// Returns `null` when [value] is not a Go-to-Source entry, so callers can +/// fall through to handling their own menu values. +RohdSourceFormat? gotoSourceFormatFromValue(String? value) { + if (value == null || !value.startsWith(_gotoSourceValuePrefix)) { + return null; + } + final name = value.substring(_gotoSourceValuePrefix.length); + for (final f in RohdSourceFormat.values) { + if (f.name == name) { + return f; + } + } + return null; +} + +/// Short, menu-friendly name for [format] (e.g. `'ROHD'`, `'SV'`). +String gotoSourceShortName(RohdSourceFormat format) => switch (format) { + RohdSourceFormat.rohd => 'ROHD', + RohdSourceFormat.sv => 'SV', + RohdSourceFormat.sc => 'SystemC', + RohdSourceFormat.fst => 'Waveform', + }; + +/// Menu label for `Go to Source`, pluralized with [count]. +String gotoSourceMenuLabel(RohdSourceFormat format, {int count = 1}) { + final name = gotoSourceShortName(format); + return count <= 1 ? 'Go to $name Source' : 'Go to $name Source ($count)'; +} + +const _rohdIconAsset = 'assets/rohd_icon.png'; +const _systemVerilogIconAsset = 'assets/systemverilog_icon.png'; +const _systemCIconAsset = 'assets/systemc_icon.png'; + +/// App-bar-style icon for a source/output [format]. +Widget sourceFormatMenuIcon(RohdSourceFormat format, {double size = 18}) => + switch (format) { + RohdSourceFormat.rohd => _sourceFormatAssetIcon( + _rohdIconAsset, + semanticLabel: 'ROHD Source', + size: size, + ), + RohdSourceFormat.sv => _sourceFormatAssetIcon( + _systemVerilogIconAsset, + semanticLabel: 'SystemVerilog Source', + size: size, + ), + RohdSourceFormat.sc => _sourceFormatAssetIcon( + _systemCIconAsset, + semanticLabel: 'SystemC Source', + size: size, + ), + RohdSourceFormat.fst => Icon(Icons.timeline, size: size), + }; + +/// Backwards-compatible alias for [sourceFormatMenuIcon]. +Widget sourceFormatIcon(RohdSourceFormat format, {double size = 18}) => + sourceFormatMenuIcon(format, size: size); + +Widget _sourceFormatAssetIcon( + String asset, { + required String semanticLabel, + required double size, +}) => + Builder( + builder: (context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final image = Image.asset( + asset, + width: size, + height: size, + fit: BoxFit.contain, + filterQuality: FilterQuality.high, + semanticLabel: semanticLabel, + errorBuilder: (context, error, stackTrace) => Image.asset( + asset, + package: 'rohd_devtools_widgets', + width: size, + height: size, + fit: BoxFit.contain, + filterQuality: FilterQuality.high, + semanticLabel: semanticLabel, + errorBuilder: (context, error, stackTrace) => + Icon(Icons.code, size: size), + ), + ); + + if (!isDark) return image; + + return Container( + width: size + 4, + height: size + 4, + decoration: const BoxDecoration( + color: Color(0xFFE0E0E0), + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(2), + child: image, + ); + }, + ); + +/// Standard popup-menu row with a fixed-width prefix icon and ellipsized label. +Widget sourcePopupMenuRow({ + required Widget icon, + required String label, + TextStyle? textStyle, + double iconSlotWidth = 22, + double gap = 8, +}) => + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: iconSlotWidth, + child: Center(child: icon), + ), + SizedBox(width: gap), + Flexible( + child: Text(label, style: textStyle, overflow: TextOverflow.ellipsis), + ), + ], + ); + +/// Standard popup-menu item using the same fixed icon gutter as source rows. +PopupMenuItem buildRohdPopupMenuItem({ + required T value, + required Widget icon, + required String label, + double height = 32, + TextStyle? textStyle, + bool enabled = true, +}) => + PopupMenuItem( + value: value, + height: height, + enabled: enabled, + child: sourcePopupMenuRow( + icon: icon, + label: label, + textStyle: textStyle, + ), + ); + +/// Compact strip of source/output format icons for trace-picker menu rows. +Widget sourceFormatIconStrip({ + required Iterable formats, + SourceFormatIconBuilder iconBuilder = sourceFormatMenuIcon, + double size = 16, + double gap = 3, +}) { + final formatList = formats.toList(growable: false); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < formatList.length; i++) ...[ + if (i > 0) SizedBox(width: gap), + iconBuilder(formatList[i], size: size), + ], + ], + ); +} + +/// Resolve which navigable source formats to display for [info]. +/// +/// When [info] is `null`, the extension is unavailable, or the query errored, +/// availability is treated as *unknown* and [kDefaultNavigableFormats] is +/// returned so the actions stay available while metadata converges. +/// Otherwise the exact set of usable navigable formats is returned (which may +/// be empty when the module genuinely has no source). +List resolveNavigableFormats(RohdModuleInfo? info) { + if (info == null || !info.extensionAvailable || info.error != null) { + return kDefaultNavigableFormats; + } + return info.navigableSourceFormats; +} + +/// Build `Go to Source` popup-menu items for [formats]. +/// +/// Each item carries a value encoded by [gotoSourceMenuValue]; decode the +/// chosen value with [gotoSourceFormatFromValue] in the menu's result handler. +List> buildGotoSourceMenuItems({ + required List formats, + int count = 1, + double height = 32, + TextStyle? textStyle, + bool showIcons = true, + SourceFormatIconBuilder iconBuilder = sourceFormatMenuIcon, +}) => + [ + for (final format in formats) + buildRohdPopupMenuItem( + value: gotoSourceMenuValue(format), + height: height, + icon: showIcons ? iconBuilder(format) : const SizedBox.shrink(), + label: gotoSourceMenuLabel(format, count: count), + textStyle: textStyle, + ), + ]; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart new file mode 100644 index 000000000..694950740 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/cross_probe_service.dart @@ -0,0 +1,157 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// cross_probe_service.dart +// Interfaces and local implementations for cross-probing between viewers. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; + +/// Abstract interface for cross-probing signal selections between viewers. +/// +/// Cross-probing allows a user to select signals in one viewer (e.g. the +/// schematic) and have those signals automatically highlighted in all other +/// viewers (e.g. the waveform viewer). +abstract class CrossProbeService { + /// Whether cross-probing is currently active. + /// + /// When `false`, neither [send] broadcasts nor incoming messages from + /// the channel are delivered to [incomingSignals]. + ValueNotifier get isActive; + + /// The most recent incoming signal paths received from OTHER viewers. + /// + /// Updated whenever another viewer broadcasts a selection while + /// [isActive] is `true`. `null` until the first message arrives. + ValueNotifier?> get incomingSignals; + + /// Broadcast [signalPaths] from this viewer ([source]) to all others. + /// + /// [source] identifies the originating viewer (e.g. `'waveform'`, + /// `'schematic'`). [LocalCrossProbeService] uses this tag to filter + /// out its own broadcasts so it does not receive its own selections as + /// incoming signals. + /// + /// Does nothing when [isActive] is `false` or [signalPaths] is empty. + void send(List signalPaths, {required String source}); + + /// Release all resources held by this service. + void dispose(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Local (in-process) implementation +// ───────────────────────────────────────────────────────────────────────────── + +/// Shared in-process broadcast channel. +/// +/// Create a single [LocalCrossProbeChannel] and share it among all +/// [LocalCrossProbeService] instances that should cross-probe with each other. +/// This replaces the older `SignalSelectionBus` pattern. +class LocalCrossProbeChannel extends ChangeNotifier { + String? _lastSource; + List? _lastPaths; + + /// The source tag of the most recent broadcast. + String? get lastSource => _lastSource; + + /// The signal paths of the most recent broadcast. + List? get lastPaths => _lastPaths; + + /// Broadcast [signalPaths] from [source] to all registered listeners. + /// + /// Does nothing when [signalPaths] is empty. + void broadcast(List signalPaths, String source) { + if (signalPaths.isEmpty) return; + _lastSource = source; + _lastPaths = List.unmodifiable(signalPaths); + notifyListeners(); + } +} + +/// Per-viewer [CrossProbeService] backed by a shared [LocalCrossProbeChannel]. +/// +/// Create one [LocalCrossProbeService] per viewer, all sharing the same +/// [LocalCrossProbeChannel]. Each service filters out its own broadcasts +/// (matched by [source]) so viewers do not receive their own selections. +/// +/// ```dart +/// final channel = LocalCrossProbeChannel(); +/// final waveXp = LocalCrossProbeService(channel, source: 'waveform'); +/// final schemXp = LocalCrossProbeService(channel, source: 'schematic'); +/// +/// // Pass waveXp to the wave viewer and schemXp to the schematic viewer. +/// // dispose both services and the channel when done. +/// ``` +class LocalCrossProbeService implements CrossProbeService { + final LocalCrossProbeChannel _channel; + final String _source; + + @override + final ValueNotifier isActive = ValueNotifier(true); + + @override + final ValueNotifier?> incomingSignals = + ValueNotifier?>(null); + + /// Creates a [LocalCrossProbeService] backed by [channel]. + /// + /// [source] is the identifier used to filter self-broadcasts. Use a + /// stable, descriptive tag such as `'waveform'` or `'schematic'`. + LocalCrossProbeService( + LocalCrossProbeChannel channel, { + required String source, + }) : _channel = channel, + _source = source { + _channel.addListener(_onChannelMessage); + } + + void _onChannelMessage() { + if (!isActive.value) return; + final src = _channel.lastSource; + if (src == null || src == _source) return; // ignore own broadcasts + incomingSignals.value = _channel.lastPaths; + } + + @override + void send(List signalPaths, {required String source}) { + if (!isActive.value || signalPaths.isEmpty) return; + _channel.broadcast(signalPaths, source); + } + + @override + void dispose() { + _channel.removeListener(_onChannelMessage); + isActive.dispose(); + incomingSignals.dispose(); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Null (no-op) implementation +// ───────────────────────────────────────────────────────────────────────────── + +/// A no-op [CrossProbeService] for standalone or offline contexts where +/// cross-probing between viewers is not available. +/// +/// [isActive] is always `false`; [send] is a no-op; [incomingSignals] +/// never changes. +class NullCrossProbeService implements CrossProbeService { + @override + final ValueNotifier isActive = ValueNotifier(false); + + @override + final ValueNotifier?> incomingSignals = + ValueNotifier?>(null); + + @override + void send(List signalPaths, {required String source}) {} + + @override + void dispose() { + isActive.dispose(); + incomingSignals.dispose(); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart new file mode 100644 index 000000000..4c0dd1327 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_button.dart @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// export_button.dart +// Reusable camera-icon button for PNG export. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; + +/// Small camera-icon button for triggering PNG export. +/// +/// Designed to be placed in a [Positioned] overlay. Calls [onPressed] +/// when tapped. +class ExportPngButton extends StatelessWidget { + /// Called when the export button is tapped. + final VoidCallback onPressed; + + /// Tooltip text shown on hover. + final String tooltip; + + const ExportPngButton({ + super.key, + required this.onPressed, + this.tooltip = 'Export as PNG', + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Tooltip( + message: tooltip, + child: Material( + color: cs.surface.withAlpha(200), + shape: const CircleBorder(), + elevation: 2, + child: InkWell( + customBorder: const CircleBorder(), + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + Icons.camera_alt_outlined, + size: 20, + color: cs.onSurface, + ), + ), + ), + ), + ); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_toast.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_toast.dart new file mode 100644 index 000000000..e962a6dd0 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/export_toast.dart @@ -0,0 +1,48 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// export_toast.dart +// Overlay-based toast that works without a Scaffold ancestor. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// Show a brief floating toast at the bottom of the screen. +/// +/// Works without a [Scaffold] ancestor by inserting directly into the +/// root [Overlay]. Auto-removes after [duration]. +void showExportToast( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 3), +}) { + final overlay = Overlay.of(context, rootOverlay: true); + late OverlayEntry entry; + entry = OverlayEntry( + builder: (ctx) => Positioned( + bottom: 32, + left: 0, + right: 0, + child: Center( + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + color: Colors.grey.shade800, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Text( + message, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ), + ), + ), + ), + ); + overlay.insert(entry); + Timer(duration, entry.remove); +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart new file mode 100644 index 000000000..adf89e928 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/logic_type_utils.dart @@ -0,0 +1,408 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_type_utils.dart +// Utilities for expanding LogicStructure/LogicArray type metadata and +// extracting sub-field values via bit-slicing. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// A node in the expanded type tree, used for structured display. +class TypeFieldNode { + /// Field name (e.g. "mantissa", "[0]"). + final String name; + + /// Bit width of this field. + final int width; + + /// Extracted value string for this field, or null if unavailable. + final String? value; + + /// Child fields for nested structs/arrays. + final List children; + + /// The bit range within the parent: [startBit, endBit) (LSB-first). + final int startBit; + + /// Creates a type field node. + TypeFieldNode({ + required this.name, + required this.width, + this.value, + this.children = const [], + this.startBit = 0, + }); +} + +/// Expand a `logic_type` metadata map into a tree of [TypeFieldNode]s. +/// +/// If [parentBinaryValue] is provided (as a binary string, MSB-first), +/// sub-field values are extracted via bit-slicing. +/// +/// The `logic_type` format for structs: +/// ```json +/// {"typeName": "FloatingPoint", "fields": [ +/// {"name": "mantissa", "width": 4, "bits": [0,1,2,3]}, +/// {"name": "exponent", "width": 4, "bits": [4,5,6,7]}, +/// {"name": "sign", "width": 1, "bits": [8]} +/// ]} +/// ``` +/// +/// For arrays: +/// ```json +/// {"width": 80, "arrayDims": [10], "elementWidth": 8} +/// ``` +List expandLogicType( + Map? logicType, { + String? parentBinaryValue, +}) { + if (logicType == null) { + return const []; + } + + // Struct case + final fields = logicType['fields'] as List?; + if (fields != null) { + return _expandStructFields(fields, parentBinaryValue); + } + + // Array case + final arrayDims = logicType['arrayDims'] as List?; + if (arrayDims != null) { + final elementWidth = (logicType['elementWidth'] as int?) ?? 1; + final elementType = logicType['elementType'] as Map?; + return _expandArrayElements( + arrayDims.cast(), + elementWidth, + elementType, + parentBinaryValue, + ); + } + + return const []; +} + +/// Expand struct fields from the `fields` list. +List _expandStructFields( + List fields, + String? parentBinaryValue, +) { + // The `bits` arrays in struct metadata may use module-level absolute + // indices (e.g. [390..398] for a 9-bit signal). Normalize to signal- + // relative indices by subtracting the global minimum across all fields. + var baseOffset = 0; + if (parentBinaryValue != null) { + var minBit = 1 << 30; + for (final fieldRaw in fields) { + final field = fieldRaw as Map; + final bits = field['bits'] as List?; + if (bits != null && bits.isNotEmpty) { + for (final b in bits) { + final bInt = b as int; + if (bInt < minBit) { + minBit = bInt; + } + } + } + } + // Only apply offset if the bits exceed the binary value length, + // indicating module-level absolute indices. + if (minBit > 0 && minBit >= parentBinaryValue.length) { + baseOffset = minBit; + } + } + + final nodes = []; + for (final fieldRaw in fields) { + final field = fieldRaw as Map; + final name = field['name'] as String? ?? '?'; + final width = field['width'] as int? ?? 1; + final bits = field['bits'] as List?; + final nestedType = field['type'] as Map?; + + // Normalize bits to signal-relative indices. + final relativeBits = bits?.cast().map((b) => b - baseOffset).toList(); + + // Determine start bit from bits array (min value, relative). + final startBit = relativeBits != null && relativeBits.isNotEmpty + ? relativeBits.reduce((a, b) => a < b ? a : b) + : 0; + + // Extract value for this field. + String? fieldValue; + if (parentBinaryValue != null && + relativeBits != null && + relativeBits.isNotEmpty) { + fieldValue = _extractBitsFromBinary(parentBinaryValue, relativeBits); + } + + // Recursively expand nested types. + final children = nestedType != null + ? expandLogicType(nestedType, parentBinaryValue: fieldValue) + : const []; + + nodes.add( + TypeFieldNode( + name: name, + width: width, + value: fieldValue, + children: children, + startBit: startBit, + ), + ); + } + return nodes; +} + +/// Expand array elements. +List _expandArrayElements( + List dims, + int elementWidth, + Map? elementType, + String? parentBinaryValue, +) { + if (dims.isEmpty) { + return const []; + } + + final outerDim = dims.first; + final nodes = []; + + // The `elementWidth` from logicType is the LEAF element width. + // For multi-dimensional arrays the actual per-element width at this level + // is the product of remaining dimensions × leaf element width. + // Derive it from the parent binary length when available, or compute it. + final int stride; + if (parentBinaryValue != null && parentBinaryValue.isNotEmpty) { + stride = parentBinaryValue.length ~/ outerDim; + } else if (dims.length > 1) { + // Product of remaining dims × leaf element width. + var product = elementWidth; + for (var d = 1; d < dims.length; d++) { + product *= dims[d]; + } + stride = product; + } else { + stride = elementWidth; + } + + for (var i = 0; i < outerDim; i++) { + final startBit = i * stride; + + // Extract this element's value. + String? elementValue; + if (parentBinaryValue != null) { + elementValue = _extractContiguousBits( + parentBinaryValue, + startBit, + stride, + ); + } + + // For multi-dimensional arrays, recurse into inner dimensions. + List children; + if (dims.length > 1) { + // Only propagate elementType if it describes a leaf-level type (e.g. + // a struct). When elementType itself has 'arrayDims', it merely + // re-describes the intermediate structure already encoded in the + // parent's flat dims list — propagating it would incorrectly expand + // leaf elements with children that exceed their bit width. + final propagateElementType = + elementType != null && !elementType.containsKey('arrayDims'); + final innerType = { + 'arrayDims': dims.sublist(1), + 'elementWidth': elementWidth, + if (propagateElementType) 'elementType': elementType, + }; + children = expandLogicType(innerType, parentBinaryValue: elementValue); + } else if (elementType != null) { + children = expandLogicType(elementType, parentBinaryValue: elementValue); + } else { + children = const []; + } + + nodes.add( + TypeFieldNode( + name: '[$i]', + width: stride, + value: elementValue, + children: children, + startBit: startBit, + ), + ); + } + return nodes; +} + +/// Extract specified bit indices from a binary string (MSB-first format). +/// +/// The binary string is MSB-first: index 0 is the rightmost (LSB) bit. +/// The [bitIndices] are LSB-indexed (matching the netlist `bits` array). +String _extractBitsFromBinary(String binaryValue, List bitIndices) { + final totalWidth = binaryValue.length; + final result = StringBuffer(); + + // Sort indices descending to produce MSB-first output. + final sorted = List.from(bitIndices)..sort((a, b) => b.compareTo(a)); + + for (final idx in sorted) { + // Convert LSB index to MSB-first string position. + final pos = totalWidth - 1 - idx; + if (pos >= 0 && pos < totalWidth) { + result.write(binaryValue[pos]); + } else { + result.write('x'); + } + } + return result.toString(); +} + +/// Extract a contiguous bit range from a binary string (MSB-first format). +/// +/// [startBit] is the LSB index, [width] is the number of bits. +String _extractContiguousBits(String binaryValue, int startBit, int width) { + if (width <= 0) { + return ''; + } + + final totalWidth = binaryValue.length; + final endBit = startBit + width; // exclusive + if (startBit >= 0 && endBit <= totalWidth) { + return LogicValue.ofString(binaryValue) + .slice(endBit - 1, startBit) + .toString(includeWidth: false); + } + + final result = StringBuffer(); + + // Extract MSB-first. + for (var i = endBit - 1; i >= startBit; i--) { + final pos = totalWidth - 1 - i; + if (pos >= 0 && pos < totalWidth) { + result.write(binaryValue[pos]); + } else { + result.write('x'); + } + } + return result.toString(); +} + +/// Convert a hex value string (e.g. "0x1a3f" or "1a3f") to binary (MSB-first). +/// +/// Returns null if the input can't be parsed. +String? hexToBinary(String hexValue, int width) { + if (width <= 0) { + return ''; + } + + var cleaned = hexValue.trim().toLowerCase(); + if (cleaned.startsWith('0x')) { + cleaned = cleaned.substring(2); + } + if (cleaned.isEmpty) { + return null; + } + + final sourceWidth = cleaned.length * 4; + final parseWidth = sourceWidth > width ? sourceWidth : width; + try { + return LogicValue.ofRadixString("$parseWidth'h$cleaned") + .slice(width - 1, 0) + .toString(includeWidth: false); + } on Exception { + return null; + } +} + +/// Format a binary field value for display. +/// +/// Short values (<=4 bits) show as binary. Longer values show as hex. +/// Uses ROHD radixString style: width'hHEX. +String formatFieldValue(String? binaryValue, int width) { + if (binaryValue == null || binaryValue.isEmpty) { + return ''; + } + if (binaryValue.contains('x')) { + return "$width'hx"; + } + if (binaryValue.contains('z')) { + return "$width'hz"; + } + if (width <= 4) { + return "$width'b$binaryValue"; + } + // Convert to hex. + final bigInt = BigInt.tryParse(binaryValue, radix: 2); + if (bigInt == null) { + return binaryValue; + } + final hexDigits = (width + 3) ~/ 4; + final hex = bigInt.toRadixString(16).padLeft(hexDigits, '0'); + return "$width'h$hex"; +} + +/// Build a multi-line indented string showing struct/array fields with values. +/// +/// Used for schematic hover tooltips. +String formatTypeTooltip( + Map? logicType, { + String? parentBinaryValue, + String? signalName, + int maxDepth = 6, +}) { + if (logicType == null) { + return ''; + } + + final nodes = expandLogicType( + logicType, + parentBinaryValue: parentBinaryValue, + ); + if (nodes.isEmpty) { + return ''; + } + + final buf = StringBuffer(); + final typeName = logicType['typeName'] as String?; + if (signalName != null) { + buf.write(signalName); + if (typeName != null) { + buf.write(' ($typeName)'); + } + buf.writeln(); + } else if (typeName != null) { + buf.writeln(typeName); + } + + for (final node in nodes) { + _formatNode(buf, node, indent: 1, maxDepth: maxDepth); + } + return buf.toString().trimRight(); +} + +void _formatNode( + StringBuffer buf, + TypeFieldNode node, { + required int indent, + required int maxDepth, +}) { + final pad = ' ' * indent; + buf.write('$pad${node.name}'); + if (node.value != null) { + buf.write(': ${formatFieldValue(node.value, node.width)}'); + } else { + buf.write(' [${node.width}]'); + } + buf.writeln(); + + if (indent < maxDepth) { + for (final child in node.children) { + _formatNode(buf, child, indent: indent + 1, maxDepth: maxDepth); + } + } else if (node.children.isNotEmpty) { + buf.writeln('$pad ...'); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart new file mode 100644 index 000000000..713e6b0f1 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/markdown_help_button.dart @@ -0,0 +1,486 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// markdown_help_button.dart +// A generic help button driven by a markdown asset file. +// +// The markdown file contains two sections separated by : +// - Above the marker: plain-text tooltip shown on hover +// - Below the marker: markdown rendered in the click-open dialog +// +// The markdown file is also directly viewable in any markdown previewer +// (GitHub, VS Code, etc.) because both sections are valid markdown and +// the separator is an invisible HTML comment. +// +// Details section format: +// ## Heading → section heading +// | Key | Description | → key–description entry row (markdown table) +// Paragraphs → plain-text description +// +// 2026 March +// Author: Desmond Kirkpatrick + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; + +/// A help button that loads its content from a markdown asset file. +/// +/// The markdown file must contain a `` marker and a +/// `` marker. Text between those markers becomes the +/// hover tooltip; text after `` is rendered as the +/// click-open dialog body. +/// +/// The first line of the file (an `# H1` heading) is used as the dialog +/// title. Everything before `` is ignored at runtime +/// (it serves as the visible title when previewing the raw markdown). +/// +/// ### Markdown file layout +/// +/// ```markdown +/// # 🌳 My Tool — Help ← dialog title (H1) +/// +/// +/// +/// Short keybinding summary ← hover tooltip (plain text) +/// shown on mouse hover. +/// +/// +/// +/// ## Section ← dialog section heading +/// +/// | Key | Description | ← table header (required before rows) +/// |-----|-------------| +/// | F | Fit to canvas | ← key–description entry +/// +/// Any paragraph text. ← rendered as body text +/// ``` +class MarkdownHelpButton extends StatefulWidget { + /// Path to the markdown asset file (e.g. `assets/help/my_help.md`). + final String assetPath; + + /// Whether the current theme is dark mode. + final bool isDark; + + /// Optional override for the button label (defaults to `❓`). + final String label; + + /// Optional widget to use as the button icon instead of [label]. + /// + /// When non-null, this widget is displayed instead of `Text(label)`. + /// Use this on platforms where the emoji [label] would not render + /// (e.g. Linux without NotoColorEmoji), passing an `Icon(Icons.help_outline)` + /// or similar Material icon. + final Widget? labelIcon; + + /// Optional package name that owns the asset. + /// + /// When non-null the actual asset path becomes + /// `packages/$package/$assetPath`, which is how Flutter resolves assets + /// declared in dependency packages. + final String? package; + + /// Optional widget shown before the dialog title text. + /// + /// Use this to display a custom icon (e.g. a `CustomPaint` widget) + /// next to the dialog title instead of relying on emoji characters + /// that may not render on all platforms. + final Widget? titleIcon; + + /// Optional text substitutions applied to the markdown before parsing. + /// + /// Each key `K` replaces all occurrences of `{{K}}` in the raw markdown + /// with the corresponding value. For example: + /// ```dart + /// substitutions: {'VERSION': '1.2.3'} + /// ``` + /// will replace `{{VERSION}}` → `1.2.3` in the loaded asset. + final Map? substitutions; + + /// Create a [MarkdownHelpButton]. + const MarkdownHelpButton({ + required this.assetPath, + required this.isDark, + this.label = '❓', + this.labelIcon, + this.package, + this.titleIcon, + this.substitutions, + super.key, + }); + + @override + State createState() => _MarkdownHelpButtonState(); +} + +class _MarkdownHelpButtonState extends State { + /// Parsed help content, loaded once from the asset. + _HelpContent? _content; + + @override + void initState() { + super.initState(); + _loadContent(); + } + + @override + void didUpdateWidget(MarkdownHelpButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.assetPath != widget.assetPath || + oldWidget.package != widget.package) { + _loadContent(); + } + } + + Future _loadContent() async { + try { + String raw; + if (widget.package != null) { + // Try the package-qualified path first (works when embedded as a + // dependency in a host app), then fall back to the bare asset path + // (standalone mode). This order avoids a spurious 404 on the web + // when the bare path doesn't exist. + // Use catch-all because rootBundle.loadString throws FlutterError + // (an Error, not Exception) when the asset is missing. + try { + raw = await rootBundle + .loadString('packages/${widget.package}/${widget.assetPath}'); + // ignore: avoid_catches_without_on_clauses + } catch (_) { + raw = await rootBundle.loadString(widget.assetPath); + } + } else { + raw = await rootBundle.loadString(widget.assetPath); + } + // Apply substitutions before parsing. + final subs = widget.substitutions; + if (subs != null) { + for (final entry in subs.entries) { + raw = raw.replaceAll('{{${entry.key}}}', entry.value); + } + } + if (mounted) { + setState(() { + _content = _HelpContent.parse(raw); + }); + } + // ignore: avoid_catches_without_on_clauses + } catch (e) { + debugPrint('Failed to load help asset: $e'); + if (mounted) { + setState(() { + _content = _HelpContent.parse( + '# Help unavailable\n\n\n\n' + 'Help content could not be loaded.\n\n\n\n' + 'Error: $e', + ); + }); + } + } + } + + @override + Widget build(BuildContext context) { + final isDark = widget.isDark; + final tooltip = _content?.tooltip ?? 'Loading help…'; + + return Tooltip( + message: tooltip, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isDark ? Colors.white24 : Colors.black12, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.4 : 0.15), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + textStyle: TextStyle( + fontSize: 12, + fontFamily: 'monospace', + color: isDark ? Colors.white : Colors.black87, + height: 1.4, + ), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (_content != null) { + _showHelpDialog(context, _content!, + isDark: isDark, titleIcon: widget.titleIcon); + } + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: widget.labelIcon ?? + Text(widget.label, + style: const TextStyle(fontSize: 18, inherit: false)), + ), + ), + ), + ); + } + + /// Show the help dialog with parsed markdown content. + static void _showHelpDialog( + BuildContext context, + _HelpContent content, { + required bool isDark, + Widget? titleIcon, + }) { + final bgColor = isDark ? const Color(0xFF252526) : Colors.white; + final fgColor = isDark ? Colors.white : Colors.black87; + final headingColor = isDark ? Colors.blue[200]! : Colors.blue[800]!; + final keyColor = isDark ? Colors.amber[200]! : Colors.amber[900]!; + final dividerColor = isDark ? Colors.white24 : Colors.black12; + + final widgets = []; + for (final block in content.detailBlocks) { + if (block is _HeadingBlock) { + widgets.add(Padding( + padding: const EdgeInsets.only(top: 16, bottom: 4), + child: Text(block.text, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: headingColor, + )), + )); + } else if (block is _EntryBlock) { + widgets.add(Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: Text(block.key, + style: TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: keyColor, + )), + ), + Expanded( + child: Text(block.description, + style: TextStyle(fontSize: 13, color: fgColor)), + ), + ], + ), + )); + } else if (block is _ParagraphBlock) { + widgets.add(Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: + Text(block.text, style: TextStyle(fontSize: 13, color: fgColor)), + )); + } + } + + showDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: bgColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title row + Row( + children: [ + if (titleIcon != null) ...[ + titleIcon, + const SizedBox(width: 10), + ], + Expanded( + child: Text(content.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: fgColor, + )), + ), + IconButton( + icon: Icon(Icons.close, color: fgColor, size: 20), + onPressed: () => Navigator.of(ctx).pop(), + ), + ], + ), + Divider(color: dividerColor), + // Scrollable content + Flexible( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widgets, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +// --------------------------------------------------------------------------- +// Parsed help content model +// --------------------------------------------------------------------------- + +/// Parsed representation of a help markdown file. +class _HelpContent { + /// Dialog title (from the `# H1` heading). + final String title; + + /// Plain-text tooltip (between `` and ``). + final String tooltip; + + /// Parsed detail blocks (headings, entries, paragraphs). + final List<_DetailBlock> detailBlocks; + + _HelpContent({ + required this.title, + required this.tooltip, + required this.detailBlocks, + }); + + /// Parse a raw markdown string into [_HelpContent]. + factory _HelpContent.parse(String raw) { + const tooltipMarker = ''; + const detailsMarker = ''; + + final tooltipIdx = raw.indexOf(tooltipMarker); + final detailsIdx = raw.indexOf(detailsMarker); + + // Extract title from the first # heading. + String title = 'Help'; + final titleMatch = RegExp(r'^#\s+(.+)$', multiLine: true).firstMatch(raw); + if (titleMatch != null) { + title = titleMatch.group(1)!.trim(); + } + + // Extract tooltip text. + String tooltip = ''; + if (tooltipIdx >= 0 && detailsIdx > tooltipIdx) { + tooltip = + raw.substring(tooltipIdx + tooltipMarker.length, detailsIdx).trim(); + } + + // Parse detail blocks. + final detailBlocks = <_DetailBlock>[]; + if (detailsIdx >= 0) { + final detailsRaw = raw.substring(detailsIdx + detailsMarker.length); + detailBlocks.addAll(_parseDetails(detailsRaw)); + } + + return _HelpContent( + title: title, + tooltip: tooltip, + detailBlocks: detailBlocks, + ); + } + + /// Parse the details section into blocks. + static List<_DetailBlock> _parseDetails(String raw) { + final blocks = <_DetailBlock>[]; + final lines = raw.split('\n'); + + for (int i = 0; i < lines.length; i++) { + final line = lines[i]; + final trimmed = line.trim(); + + // Skip empty lines + if (trimmed.isEmpty) { + continue; + } + + // ## Heading + if (trimmed.startsWith('## ')) { + blocks.add(_HeadingBlock(trimmed.substring(3).trim())); + continue; + } + + // Table separator row (|---|---|) — skip + if (RegExp(r'^\|[\s\-:|]+\|$').hasMatch(trimmed)) { + continue; + } + + // Table header row (| Key | Description |) — skip + if (trimmed.startsWith('|') && + trimmed.endsWith('|') && + i + 1 < lines.length && + RegExp(r'^\|[\s\-:|]+\|$').hasMatch(lines[i + 1].trim())) { + continue; + } + + // Table data row (| key | description |) + if (trimmed.startsWith('|') && trimmed.endsWith('|')) { + final cells = trimmed + .substring(1, trimmed.length - 1) // strip outer pipes + .split('|') + .map((c) => c.trim()) + .toList(); + if (cells.length >= 2) { + blocks.add(_EntryBlock( + key: _stripInlineCode(cells[0]), + description: cells[1], + )); + continue; + } + } + + // Plain paragraph text (collect consecutive non-empty lines) + final para = StringBuffer(trimmed); + while (i + 1 < lines.length && lines[i + 1].trim().isNotEmpty) { + final next = lines[i + 1].trim(); + // Stop at headings, table rows, or markers + if (next.startsWith('## ') || + next.startsWith('|') || + next.startsWith(' - - - - - rohd_devtools_extension - diff --git a/rohd_devtools_extension/web/manifest.json b/rohd_devtools_extension/web/manifest.json deleted file mode 100644 index 672668b8a..000000000 --- a/rohd_devtools_extension/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "rohd_devtools_extension", - "short_name": "rohd_devtools_extension", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/rohd_extension/.gitignore b/rohd_extension/.gitignore new file mode 100644 index 000000000..2bbc3f814 --- /dev/null +++ b/rohd_extension/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +out/ +*.vsix \ No newline at end of file diff --git a/rohd_extension/.markdownlint.json b/rohd_extension/.markdownlint.json new file mode 100644 index 000000000..fe1bf1caa --- /dev/null +++ b/rohd_extension/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "MD013": { "tables": false, "code_blocks": false }, + "MD060": false +} diff --git a/rohd_extension/.nvmrc b/rohd_extension/.nvmrc new file mode 100644 index 000000000..cabf43b5d --- /dev/null +++ b/rohd_extension/.nvmrc @@ -0,0 +1 @@ +24 \ No newline at end of file diff --git a/rohd_extension/.vscodeignore b/rohd_extension/.vscodeignore new file mode 100644 index 000000000..45c19e85f --- /dev/null +++ b/rohd_extension/.vscodeignore @@ -0,0 +1,12 @@ +.gitignore +.markdownlint.json +.nvmrc +.vscodeignore +Makefile +dart/** +node_modules/@types/** +out/**/*.map +package-lock.json +src/** +tool/** +tsconfig.json \ No newline at end of file diff --git a/rohd_extension/LICENSE b/rohd_extension/LICENSE new file mode 100644 index 000000000..ceccd2b3e --- /dev/null +++ b/rohd_extension/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (C) 2021-2026 Intel Corporation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/rohd_extension/Makefile b/rohd_extension/Makefile new file mode 100644 index 000000000..1b10d4d97 --- /dev/null +++ b/rohd_extension/Makefile @@ -0,0 +1,107 @@ +# Makefile for the ROHD VS Code Extension (rohd_extension) +# +# Targets: +# compile - Compile TypeScript sources to out/ +# install - Compile and install to remote VS Code Server (default) +# install-local - Compile and install to local VS Code +# install-remote - Compile and install to remote VS Code Server +# clean - Remove compiled output and packaged .vsix files +# real-clean - clean + remove node_modules, lock files, .dart_tool +# help - Show this help + +ROOT := $(shell pwd) +TS_SOURCES := $(shell find $(ROOT)/src -name '*.ts' 2>/dev/null) + +# Read name/version from package.json +PKG_NAME := $(shell node -p "require('./package.json').name") +PKG_VERSION := $(shell node -p "require('./package.json').version") +PKG_PUBLISHER := $(shell node -p "require('./package.json').publisher") + +# VS Code Server extension directory (remote dev container) +REMOTE_EXT_DIR := $(HOME)/.vscode-server/extensions/$(PKG_PUBLISHER).$(PKG_NAME)-$(PKG_VERSION) +# Local VS Code extension directory +LOCAL_EXT_DIR := $(HOME)/.vscode/extensions/$(PKG_PUBLISHER).$(PKG_NAME)-$(PKG_VERSION) + +# Stamp file to track last successful compile +COMPILE_STAMP := out/.compile-stamp + +.PHONY: all help compile install install-local install-remote clean real-clean + +all: install + +help: + @echo "ROHD VS Code Extension - Build Targets" + @echo "" + @echo " compile - Compile TypeScript to out/" + @echo " install - Compile and install to remote server (default)" + @echo " install-local - Compile and install to local VS Code" + @echo " install-remote - Compile and install to remote VS Code Server" + @echo " clean - Remove compiled output and .vsix files" + @echo " real-clean - clean + node_modules, lock files, .dart_tool" + @echo "" + @echo "Extension: $(PKG_PUBLISHER).$(PKG_NAME) v$(PKG_VERSION)" + @echo "Remote: $(REMOTE_EXT_DIR)" + +# --------------------------------------------------------------------------- +# Dependencies +# --------------------------------------------------------------------------- + +# Install npm dependencies (including TypeScript) if missing or out of date +node_modules: package.json package-lock.json + @echo "Installing npm dependencies..." + @npm ci + @touch node_modules + +# --------------------------------------------------------------------------- +# Compile +# --------------------------------------------------------------------------- + +$(COMPILE_STAMP): $(TS_SOURCES) tsconfig.json package.json node_modules + @echo "Compiling TypeScript..." + @npx tsc -p ./ + @touch $(COMPILE_STAMP) + +compile: $(COMPILE_STAMP) + +# --------------------------------------------------------------------------- +# Install +# --------------------------------------------------------------------------- + +install-remote: compile + @if [ -d "$(REMOTE_EXT_DIR)" ]; then \ + echo "Installing to $(REMOTE_EXT_DIR)/out/..."; \ + cp out/*.js out/*.js.map "$(REMOTE_EXT_DIR)/out/"; \ + echo "Done. Reload the VS Code window to pick up changes."; \ + else \ + echo "ERROR: Extension not found at $(REMOTE_EXT_DIR)"; \ + echo "Install the extension first via VS Code, then use this target to update."; \ + exit 1; \ + fi + +install-local: compile + @if [ -d "$(LOCAL_EXT_DIR)" ]; then \ + echo "Installing to $(LOCAL_EXT_DIR)/out/..."; \ + cp out/*.js out/*.js.map "$(LOCAL_EXT_DIR)/out/"; \ + echo "Done. Reload the VS Code window to pick up changes."; \ + else \ + echo "ERROR: Extension not found at $(LOCAL_EXT_DIR)"; \ + echo "Install the extension first via VS Code, then use this target to update."; \ + exit 1; \ + fi + +install: install-remote + +# --------------------------------------------------------------------------- +# Clean +# --------------------------------------------------------------------------- + +clean: + @echo "Cleaning compiled output and .vsix files..." + @rm -rf out/ + @rm -f *.vsix + +real-clean: clean + @echo "Removing node_modules and Dart build artifacts..." + @rm -rf node_modules/ + @rm -rf dart/.dart_tool/ + @rm -f dart/pubspec.lock diff --git a/rohd_extension/README.md b/rohd_extension/README.md new file mode 100644 index 000000000..f93af8f63 --- /dev/null +++ b/rohd_extension/README.md @@ -0,0 +1,356 @@ +# ROHD VS Code Extension + +A VS Code extension for the [ROHD](https://github.com/intel/rohd) hardware +design framework. It provides context-aware Dart code snippets, cross-probe +source navigation from ROHD viewers (schematic, waveform), and automatic +port-forwarded URI display for the Dart Tooling Daemon (DTD) and VM Service. + +## Features + +- **Context-aware ROHD snippets** — conditional constructs (`If`, `Iff`, + `Else`, `Case`, `CaseZ`) only appear when the cursor is inside + a `Combinational` or `Sequential` block. Module-body patterns + (`Sequential`, `Combinational`, `Pipeline`, current-module `FSM`, etc.) + only appear inside a module body. +- **Cross-probe source navigation** — click a signal or module in an ROHD + viewer and jump directly to the corresponding Dart source (FLC — **F**ile, + **L**ine, **C**olumn — crossprobing). +- **Debug adapter tracking** — registers a `DebugAdapterTrackerFactory` for + Dart sessions to automatically capture DTD and VM Service URIs with + port-forwarding awareness. +- **DTD bridge** — registers `rohd.goToSource` / `rohd.resolveFrames` + services on the Dart Tooling Daemon so the DevTools extension can navigate + the editor remotely. + +On activation the extension prints: + +```text +════════════════════════════════════════════════════════════ +ROHD 0.1.0: Extension loaded for FLC crossprobing. + +DTD: + URI: ws://127.0.0.1:44123/token + Fwd: ws://localhost:58201/token ← only shown if port differs +VM: + URI: ws://127.0.0.1:40699/TOKEN=/ws + Fwd: ws://localhost:61969/TOKEN=/ws ← only shown if port differs +════════════════════════════════════════════════════════════ +``` + +## Snippets + +Snippets are registered for Dart files. Open a `.dart` file and make sure +VS Code shows **Dart** as the language mode in the lower-right status bar. + +VS Code does not always expand snippets from `prefix` by default. If +typing `mod` only accepts a normal Dart completion, use one of these +flows: + +1. Type `mod`, press **Ctrl+Space** to open suggestions, select + **ROHD: Create Module**, then press **Enter** or **Tab**. +2. Enable tab expansion in your VS Code settings: + + ```json + { + "editor.tabCompletion": "onlySnippets", + "editor.snippetSuggestions": "top" + } + ``` + + With that setting, `mod` expands the ROHD module snippet directly + when no higher-priority editor action is using Tab. + +The extension also has context-aware completion snippets, such as showing +`If`, `Case`, and conditional assignment only inside `Combinational` or +`Sequential` blocks. These are controlled by the ROHD setting +`rohd.enableCompletions`, which is enabled by default. If it was disabled, +enable it manually in Settings or add this to `settings.json`: + +```json +{ + "rohd.enableCompletions": true +} +``` + +After changing extension settings or installing a new VSIX, reload the VS Code +window with **Developer: Reload Window**. + +### Static snippets + +These snippets are contributed by VS Code's snippet system. Context-aware +completions below narrow the ROHD-specific options by cursor location. + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `mod`, `Module` | `class … extends Module { … }` | Module scaffold with `clk`, `reset`, `a`/`b` inputs, `depth`, `latchData`, `addInput`/`addOutput`, `definitionName`, and instance naming parameters | +| `sim`, `Simulator` | Clock, reset, `WaveDumper`, `Simulator.run()` | Simulation / testbench boilerplate | +| `fsmModule`, `FSMModule` | enum + `class extends Module` + `FiniteStateMachine` | Full standalone FSM module scaffold | +| `vf`, `tb`, `testbench` | `rohd_vf` testbench | Agent / Driver / Monitor / Sequencer template | + +### Context-aware — file scope + +These appear only at file/top level (not inside a function or class body). + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `FSM`, `fsm` | enum + `class extends Module` + `FiniteStateMachine` | Full FSM scaffold at file level from context-aware completions | +| `Module`, `mod` | `class extends Module { addInput/addOutput … }` | Module scaffold with `clk`, `reset`, `a`/`b` inputs, `depth`, `latchData`, `definitionName`, and instance naming parameters | +| `Interface`, `intf` | enum + `class extends Interface` + `clone()` | Classic Interface with direction enum, `setPorts`, and `clone()` | +| `PairInterface`, `pairintf` | `class extends PairInterface { … clone() }` | PairInterface with provider/consumer roles | + +### Context-aware — module body scope + +These appear when the cursor is inside a `class … extends Module` body. + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `FSM`, `fsm` | `FiniteStateMachine` for the current module | Inserts states and the FSM constructor call; inserts the enum before the enclosing class | +| `Pipeline`, `pipe` | `Pipeline(clk, stages: [(p) => […]])` | Pipelined datapath | +| `ReadyValidPipeline`, `rvpipe` | `ReadyValidPipeline(clk, stages: …, valid, ready)` | Pipeline with flow control | +| `Sequential`, `Seq`, `seq` | `Sequential(clk, [If(a, …)])` | `always_ff` block | +| `Combinational`, `Comb`, `comb` | `Combinational([…])` | `always_comb` block | +| `assign` | `out <= expr;` | Continuous assignment outside `_Always` blocks | + +### Context-aware — inside `Combinational` or `Sequential` + +These snippets only appear when the cursor is inside a `Combinational([…])` +or `Sequential(clk, […])` block, matching ROHD's requirement that +conditionals live inside an `_Always` block. + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `If` | `If(cond, then: […], orElse: […])` | Inline if/else (most common) | +| `ifthen` | `If(cond, then: […])` | Simple conditional guard | +| `ifnested`, `iforelse` | `If(a, then: …, orElse: [If(b, …)])` | Nested if / else-if / else chain | +| `If.block`, `ifblock` | `If.block([Iff(…), ElseIf(…), Else(…)])` | Flat if/else-if/else block chain | +| `Iff`, `iff` | `If.block([Iff(…), ElseIf(…), Else(…)])` | Complete if/elseif/else block using `Iff` as the first clause | +| `Else`, `else` | `Else([…])` | Final clause in `If.block` | +| `Case` | `Case(expr, [CaseItem(…)], …)` | `case` / `unique case` / `priority case` | +| `CaseZ`, `casez` | `CaseZ(expr, [CaseItem(…)])` | Don't-care matching with `z` syntax | +| `CaseItem`, `caseitem` | `CaseItem(value, […])` | Single arm inside `Case`/`CaseZ` | +| `assign` | `out < expr,` | Conditional assignment (inside `_Always`) | + +> **Note:** bare `Iff` (two f's) is *not* a standalone conditional. It is the +> first entry in an `If.block([…])` chain. The `Iff` snippet expands to the +> full `If.block` form so it can be inserted directly inside `Sequential` or +> `Combinational`. + +### Context-aware — `test/` directory + +These appear only in Dart files under a `test/` directory. + +| Prefix | Expands to | Description | +|--------|-----------|-------------| +| `test`, `Test` | `test('description', () async { … })` | Async package:test case | +| `group`, `Group` | `group('description', () { test(…) })` | Test group with an async test inside | +| `tearDown`, `resetTest` | `tearDown(() async { await Simulator.reset(); })` | Reset ROHD simulation state between tests | +| `rohdtest`, `simtest`, `testsim` | Clock/reset/DUT build/`Simulator.run()` scaffold | ROHD simulation test flow based on common ROHD-HCL tests | + +## FLC Cross-Probing + +FLC (**F**ile, **L**ine, **C**olumn) data maps every signal and submodule +in the generated output back to the Dart source location where it was +constructed. The extension uses FLC data to navigate from a schematic or +waveform viewer directly to the ROHD Dart source. + +### FLC JSON Format (v6) + +An `.flc.json` file uses a shared ROHD source file table plus a per-module +trie of source frames. Each trie leaf is a compact symbol string for a +signal or submodule instance: + +```json +{ + "version": 6, + "files": [ + "lib/src/my_module.dart", + "lib/src/modules/gates.dart" + ], + "modules": { + "Top": { + "outputFiles": { + "sv": ["Top.sv"], + "sc": ["Top.cpp"] + }, + "tree": [ + [ + "0:6:20", + ["0:15:9", "a@sv:2:19,8:7;sc:44:5"], + ["0:16:15", "b@sv:3:20~originalB"], + ["1:22:3", "*inner@sv:7:1"] + ] + ] + } + } +} +``` + +- **`version`** — v6 is the current format. The extension can still parse + v5; other explicit versions are rejected. +- **`files`** — array of ROHD source paths, indexed by the first number in + each trie frame. +- **`outputFiles`** — map from output language to generated file list, for + example `"sv": ["Top.sv"]` or `"sc": ["Top.cpp"]`. The first file for + each language is the canonical lookup target. +- **`tree`** — list of trie root nodes. Each node starts with a source frame + string, then contains child nodes and/or symbol strings that share that + source-frame prefix. +- **`"0:15:9"`** — ROHD source frame: file index 0, line 15, column 9. + The column is optional and defaults to 1. +- **`"a@sv:2:19,8:7;sc:44:5"`** — signal `a`, with two SystemVerilog + output positions and one SystemC output position. Output-language groups + are separated by semicolons; entries within one language are separated by + commas. The language tag appears on the first entry in the group, so + `sv:2:19,8:7` means `sv:2:19` and `sv:8:7`. +- **`"b@sv:3:20~originalB"`** — canonical signal name `b`, original source + name `originalB`. Lookups may use either name. +- **`"*inner@sv:7:1"`** — submodule instance `inner`. Instance symbols are + prefixed with `*`; signal symbols are not. + +Source frames accumulate along the trie path from outermost to innermost. +When the extension opens ROHD source frames, it presents them innermost first +so the construction site closest to the signal or instance is selected first. + +## Commands + +| Command | Title | +|---------|-------| +| `rohd.openSourceLocation` | Go to Source Location | +| `rohd.openSourceLocations` | Go to Source Locations (multi-frame) | +| `rohd.nextSourceLocation` | Next Source Frame | +| `rohd.prevSourceLocation` | Previous Source Frame | +| `rohd.connectDtd` | Connect to Dart Tooling Daemon | +| `rohd.showForwardedUris` | Show Forwarded DTD/VM URIs | + +## Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `rohd.enableCompletions` | `true` | Enable context-aware ROHD completions. Set `false` to disable the provider. | +| `rohd.dtdUri` | `""` | WebSocket URI of the Dart Tooling Daemon. Leave empty for auto-discovery. | + +## Prerequisites + +- **Node.js >= 22** for the pinned VSIX packaging tool (CI uses Node 24): + + ```bash + nvm install + nvm use + node --version + ``` + +- **npm** (comes with Node) + +## Build + +```bash +cd rohd_extension +npm ci +npm run compile # produces out/extension.js +``` + +## Local Installation + +### Package as VSIX + +```bash +cd rohd_extension +npm run package +``` + +This produces `rohd-0.1.0.vsix`. + +### Install + +```bash +code --install-extension rohd-0.1.0.vsix --force +``` + +Then reload the VS Code window (**Developer: Reload Window**). + +### One-liner (build + install) + +```bash +cd rohd_extension \ + && npm ci \ + && npm run package \ + && code --install-extension rohd-0.1.0.vsix --force \ + && echo "Done — reload the VS Code window to activate." +``` + +## Remote Installation (Dev Containers / SSH) + +Extensions that interact with the Dart debug adapter must be installed on the +**remote** side (inside the container or on the SSH host). + +### Option 1: devcontainer.json (recommended) + +Place the extension source in your repo and build it on container creation: + +```jsonc +// .devcontainer/devcontainer.json +{ + "postCreateCommand": "cd rohd_extension && npm ci && npm run package && code --install-extension rohd-0.1.0.vsix --force" +} +``` + +### Option 2: Pre-built VSIX + +Build the `.vsix` on your host or in CI, then install at container start: + +```jsonc +{ + "postStartCommand": "code --install-extension rohd_extension/rohd-0.1.0.vsix --force" +} +``` + +### Option 3: Install via CLI while connected + +```bash +code --install-extension rohd-0.1.0.vsix --force +``` + +The `code` CLI inside a remote session targets the VS Code Server +automatically. + +### Option 4: Install via VS Code UI + +1. Connect to the remote host / container. +2. Open Extensions (`Ctrl+Shift+X`). +3. Click `...` → **Install from VSIX...** and select the `.vsix` file. +4. Reload the window. + +### Option 5: Copy directly into `.vscode-server/extensions/` + +If the `code` CLI is not available (e.g. in a Dockerfile `RUN` step): + +```bash +mkdir -p ~/.vscode-server/extensions/rohd.rohd-0.1.0 +cp -r rohd_extension/{package.json,out,snippets,resources} \ + ~/.vscode-server/extensions/rohd.rohd-0.1.0/ +``` + +The directory name must follow the pattern `.-`. + +## File Structure + +```text +rohd_extension/ +├── package.json # Extension manifest +├── tsconfig.json # TypeScript configuration +├── src/ +│ ├── extension.ts # Entry point — activates all modules +│ ├── source_navigator.ts # Cross-probe → editor navigation +│ ├── dtd_bridge.ts # DTD JSON-RPC bridge +│ ├── debug_tracker.ts # Debug adapter tracker (DTD/VM URIs) +│ └── conditional_completions.ts # Context-aware conditional snippets +├── out/ # Compiled JS (generated) +├── snippets/ +│ └── rohd.json # ROHD Dart snippets +└── resources/ + └── rohd_icon.png # Extension icon +``` + +## License + +BSD-3-Clause — see the repository root LICENSE file. diff --git a/rohd_extension/dart/LICENSE b/rohd_extension/dart/LICENSE new file mode 100644 index 000000000..9e00cc822 --- /dev/null +++ b/rohd_extension/dart/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (C) 2021-2026 Intel Corporation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rohd_extension/dart/lib/dtd_service.dart b/rohd_extension/dart/lib/dtd_service.dart new file mode 100644 index 000000000..47a540b72 --- /dev/null +++ b/rohd_extension/dart/lib/dtd_service.dart @@ -0,0 +1,147 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// dtd_service.dart +// DTD service handler for receiving cross-probe source navigation +// requests from the ROHD DevTools extension. +// +// Registers a `rohd.goToSource` service on the Dart Tooling Daemon so +// that the DevTools extension can send resolved SourceFrame lists for +// navigation in the VS Code editor. +// +// 2026 April 27 +// Author: Desmond Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; + +import 'package:json_rpc_2/json_rpc_2.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import 'source_navigator.dart'; + +/// Callback invoked when the DTD service receives a goToSource request. +/// +/// The TS shell provides this callback to bridge DTD requests into +/// VS Code command execution. +typedef GoToSourceCallback = Future Function(List frames, + {int startIndex}); + +/// Manages the DTD connection and service registration for source +/// navigation. +class DtdService { + final GoToSourceCallback _onGoToSource; + Peer? _peer; + WebSocketChannel? _channel; + bool _disposed = false; + + /// Creates a DTD service that calls [onGoToSource] when a + /// `rohd.goToSource` request arrives. + DtdService({required GoToSourceCallback onGoToSource}) + : _onGoToSource = onGoToSource; + + /// Connect to the DTD at [uri] and register the `rohd.goToSource` + /// service. + /// + /// Returns `true` if connection and registration succeeded. + Future connect(String uri) async { + if (_disposed) return false; + + try { + _channel = WebSocketChannel.connect(Uri.parse(uri)); + await _channel!.ready; + _peer = Peer(_channel!.cast()); + + // Register the service method. + _peer!.registerMethod('rohd.goToSource', _handleGoToSource); + + // Start listening (non-blocking). + unawaited( + _peer!.listen().then((_) { + // Connection closed. + _peer = null; + }), + ); + + return true; + } on Exception catch (e) { + _peer = null; + _channel = null; + // ignore: avoid_print + print('[DtdService] Failed to connect to DTD at $uri: $e'); + return false; + } + } + + /// Whether the DTD connection is active. + bool get isConnected => _peer != null && !_peer!.isClosed; + + /// Disconnect from DTD and clean up. + Future dispose() async { + _disposed = true; + await _peer?.close(); + _peer = null; + await _channel?.sink.close(); + _channel = null; + } + + // --------------------------------------------------------------------------- + // RPC handler + // --------------------------------------------------------------------------- + + /// Handle an incoming `rohd.goToSource` request. + /// + /// Expected parameters: + /// ```json + /// { + /// "frames": [ + /// {"file": "lib/src/foo.dart", "line": 42, "col": 5, "type": "rohd"}, + /// {"file": "Foo.sv", "line": 10, "col": 1, "type": "sv"} + /// ], + /// "index": 0 // optional starting frame + /// } + /// ``` + Future> _handleGoToSource(Parameters params) async { + try { + final framesRaw = params['frames'].asList; + final startIndex = params['index'].asIntOr(0); + + final frames = framesRaw.map((raw) { + final map = raw as Map; + return SourceFrame.fromJson(map); + }).toList(); + + if (frames.isEmpty) { + return {'status': 'error', 'message': 'No frames provided'}; + } + + await _onGoToSource(frames, startIndex: startIndex); + return {'status': 'ok', 'navigated': frames.length}; + } on Exception catch (e) { + return {'status': 'error', 'message': e.toString()}; + } + } +} + +// --------------------------------------------------------------------------- +// Convenience: encode/decode frames for stdio-based communication +// --------------------------------------------------------------------------- + +/// Encode a goToSource request as a JSON string for stdio transport. +String encodeGoToSourceRequest(List frames, {int index = 0}) => + jsonEncode({ + 'method': 'rohd.goToSource', + 'frames': frames.map((f) => f.toJson()).toList(), + 'index': index, + }); + +/// Decode a goToSource request from a JSON string. +(List, int) decodeGoToSourceRequest(String json) { + final map = jsonDecode(json) as Map; + final framesRaw = map['frames'] as List; + final index = (map['index'] as int?) ?? 0; + final frames = framesRaw + .map((raw) => SourceFrame.fromJson(raw as Map)) + .toList(); + return (frames, index); +} diff --git a/rohd_extension/dart/lib/flc_data.dart b/rohd_extension/dart/lib/flc_data.dart new file mode 100644 index 000000000..010196836 --- /dev/null +++ b/rohd_extension/dart/lib/flc_data.dart @@ -0,0 +1,429 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// flc_data.dart +// FLC (File/Line/Column) data model for cross-probing from schematic +// signals to their ROHD Dart source locations. +// +// Parses trace data embedded in Yosys JSON module attributes under the +// `rohd.src_trace` key, as produced by `SourceTraceRegistry`. +// +// 2026 April +// Author: Desmond Kirkpatrick + +/// A single source location frame from an FLC trace. +class FlcFrame { + /// File path (package-relative, e.g. `lib/src/foo.dart`). + final String file; + + /// 1-based line number. + final int line; + + /// 1-based column number. + final int column; + + /// Frame type: `'rohd'` for ROHD Dart source, `'sv'` for SystemVerilog. + final String type; + + const FlcFrame({ + required this.file, + required this.line, + required this.column, + this.type = 'rohd', + }); + + @override + String toString() => '$file:$line:$column [$type]'; +} + +/// Trace entry for a single signal or instance — one or more stack frames +/// ordered innermost → outermost, plus output-language source locations. +class FlcEntry { + /// Stack frames for this signal/instance (ROHD Dart source). + final List frames; + + /// Output-language source locations (e.g. SystemVerilog, SystemC). + /// + /// Each frame's [FlcFrame.type] identifies the language (`'sv'`, `'sc'`, + /// etc.). Multiple frames per language are allowed (e.g. when a signal + /// appears in both a declaration and an assignment). + final List outputFrames; + + /// Original name before Namer disambiguation (e.g. `sum` before it + /// became `sum_0`). Null if the name was not renamed. + final String? origName; + + const FlcEntry({ + required this.frames, + this.outputFrames = const [], + this.origName, + }); + + /// First SystemVerilog output frame, or `null` if none. + /// + /// Convenience accessor for backward compatibility — equivalent to + /// `outputFrames.where((f) => f.type == 'sv').firstOrNull`. + FlcFrame? get svFrame => outputFrames.cast().firstWhere( + (f) => f!.type == 'sv', + orElse: () => null, + ); + + /// All frames: output-language frames first, then ROHD src frames. + List get allFrames => [...outputFrames, ...frames]; +} + +/// FLC data parsed from a v5 trie-based FLC hierarchy JSON file. +class FlcData { + /// Global file table (index → path). + final List files; + + /// Module name → signal name → FlcEntry. + final Map> _signals; + + /// Module name → instance name → FlcEntry. + final Map> _instances; + + FlcData._({ + required this.files, + required Map> signals, + required Map> instances, + }) : _signals = signals, + _instances = instances; + + /// Whether any FLC data was found. + bool get isEmpty => _signals.isEmpty && _instances.isEmpty; + + /// Parse FLC data from a v5/v6 trie-based hierarchy JSON. + /// + /// v5 and v6 share the trie structure; v6 adds multi-position support + /// (comma-separated entries per language) and list-per-language + /// outputFiles. Any other version is rejected and returns empty FLC data. + factory FlcData.fromJson(Map json) { + final version = json['version']; + if (version != null && version != 5 && version != 6) { + return FlcData._(files: [], signals: {}, instances: {}); + } + final files = + (json['files'] as List?)?.cast() ?? []; + final rawModules = json['modules']; + final modules = + rawModules is Map ? Map.from(rawModules) : null; + if (modules == null) { + return FlcData._(files: files, signals: {}, instances: {}); + } + + final signals = >{}; + final instances = >{}; + + for (final modEntry in modules.entries) { + final moduleName = modEntry.key; + final modMap = modEntry.value as Map?; + if (modMap == null) continue; + + final svFile = modMap['svFile'] as String?; + // Output files map. + // v5: {"sv": "Foo.sv", "sc": "Foo.h"} (string per language) + // v6: {"sv": ["Foo.sv"], "sc": ["Foo.h"]} (list per language) + // Falls back to legacy "svFile" field. For lookup we use the first + // file in each language list (the canonical output). + final rawOutputFiles = modMap['outputFiles']; + final outputFiles = { + if (svFile != null) 'sv': svFile, + }; + if (rawOutputFiles is Map) { + for (final e in rawOutputFiles.entries) { + final v = e.value; + if (v is String) { + outputFiles[e.key as String] = v; + } else if (v is List && v.isNotEmpty && v.first is String) { + outputFiles[e.key as String] = v.first as String; + } + } + } + final tree = modMap['tree'] as List?; + if (tree == null) continue; + + final modSignals = {}; + final modInstances = {}; + + /// Walk a trie node, collecting frames along the path. + /// Frames are accumulated outermost-first; we reverse at the leaf + /// to match the innermost-first convention of FlcEntry.frames. + void walkNode(List node, List path) { + if (node.isEmpty) return; + final frame = node[0] as String; + final currentPath = [...path, frame]; + + for (var i = 1; i < node.length; i++) { + final elem = node[i]; + if (elem is List) { + // Child trie node. + walkNode(elem.cast(), currentPath); + } else if (elem is String) { + // String-encoded leaf symbol. + final parsed = _parseSymbolString(elem); + final name = parsed.name; + final isInstance = parsed.isInstance; + final origName = parsed.origName; + + // Build ROHD source frames (reverse to innermost-first). + final rohdFrames = []; + for (final f in currentPath.reversed) { + final parts = f.split(':'); + if (parts.length < 2) continue; + final fi = int.tryParse(parts[0]); + if (fi == null || fi >= files.length) continue; + final line = int.tryParse(parts[1]) ?? 1; + final col = parts.length > 2 ? (int.tryParse(parts[2]) ?? 1) : 1; + rohdFrames.add( + FlcFrame(file: files[fi], line: line, column: col), + ); + } + + // Build output-language frames from parsed positions. + final outFrames = []; + for (final pos in parsed.outputPositions) { + final file = outputFiles[pos.type]; + if (file == null) continue; + outFrames.add( + FlcFrame( + file: file, + line: pos.line, + column: pos.column, + type: pos.type, + ), + ); + } + + if (rohdFrames.isNotEmpty || outFrames.isNotEmpty) { + final entry = FlcEntry( + frames: rohdFrames, + outputFrames: outFrames, + origName: origName, + ); + if (isInstance) { + modInstances[name] = entry; + } else { + modSignals[name] = entry; + } + } + } + } + } + + // tree is a list of root trie nodes. + for (final rootNode in tree) { + if (rootNode is List) { + walkNode(rootNode.cast(), []); + } + } + + if (modSignals.isNotEmpty) signals[moduleName] = modSignals; + if (modInstances.isNotEmpty) instances[moduleName] = modInstances; + } + + return FlcData._(files: files, signals: signals, instances: instances); + } + + /// Parse a v5 string-encoded symbol. + /// + /// Format: `[*]name[@positions][~origName]` + /// + /// Positions are semicolon-separated language groups; within each group + /// entries are comma-separated. Each entry is optionally prefixed with a + /// language tag (only the group's first entry carries the tag): + /// - `sv:L:C` — SystemVerilog at line L, column C + /// - `sc:L:C` — SystemC at line L, column C + /// - `L:C` — legacy shorthand, treated as `sv:L:C` + /// + /// Examples: + /// - `clk@2:13` (legacy single SV position) + /// - `clk@sv:2:13` (explicit SV position) + /// - `clk@sv:2:13;sc:10:5` (SV + SystemC) + /// - `clk@sv:2:13,5:7;sc:10:5` (v6: multi-entry within a language) + static _SymbolInfo _parseSymbolString(String s) { + final isInstance = s.startsWith('*'); + var rest = isInstance ? s.substring(1) : s; + + String? origName; + final tildeIdx = rest.indexOf('~'); + if (tildeIdx >= 0) { + origName = rest.substring(tildeIdx + 1); + rest = rest.substring(0, tildeIdx); + } + + final outputPositions = <_OutputPos>[]; + final atIdx = rest.indexOf('@'); + if (atIdx >= 0) { + final posStr = rest.substring(atIdx + 1); + rest = rest.substring(0, atIdx); + + for (final group in posStr.split(';')) { + if (group.isEmpty) continue; + // A group is `[lang:]entry(,entry)*` where each entry is `[F:]L:C`. + final entries = group.split(','); + String? groupLang; + for (var i = 0; i < entries.length; i++) { + var part = entries[i]; + if (part.isEmpty) continue; + // Only the first entry of a group may carry a language tag. + if (i == 0) { + final segments = part.split(':'); + final firstIsTag = + segments.length >= 3 && int.tryParse(segments[0]) == null; + if (firstIsTag) { + groupLang = segments[0]; + part = segments.sublist(1).join(':'); + } + } + final type = groupLang ?? 'sv'; + final segments = part.split(':'); + final lineStr = segments.length >= 2 + ? segments[segments.length - 2] + : segments[0]; + final colStr = + segments.length >= 2 ? segments[segments.length - 1] : null; + final line = int.tryParse(lineStr) ?? 1; + final column = colStr != null ? (int.tryParse(colStr) ?? 1) : 1; + outputPositions + .add(_OutputPos(type: type, line: line, column: column)); + } + } + } + + return _SymbolInfo( + name: rest, + isInstance: isInstance, + outputPositions: outputPositions, + origName: origName, + ); + } + + /// Create empty FLC data (no trace information available). + factory FlcData.empty() => FlcData._(files: [], signals: {}, instances: {}); + + /// Look up FLC frames for a signal in a given module. + /// + /// Returns null if no trace data exists for this signal. + /// Falls back to matching by [origName] if the canonical name isn't found. + List? lookupSignal(String moduleName, String signalName) => + lookupSignalEntry(moduleName, signalName)?.frames; + + /// Look up the full [FlcEntry] for a signal (includes SV frame if present). + FlcEntry? lookupSignalEntry(String moduleName, String signalName) { + final modSignals = _signals[moduleName]; + if (modSignals == null) return null; + + // Direct match. + final direct = modSignals[signalName]; + if (direct != null) return direct; + + // Fallback: search by origName. + for (final entry in modSignals.values) { + if (entry.origName != null && entry.origName == signalName) { + return entry; + } + } + return null; + } + + /// Look up FLC frames for an instance (submodule) in a given module. + List? lookupInstance(String moduleName, String instanceName) => + lookupInstanceEntry(moduleName, instanceName)?.frames; + + /// Look up the full [FlcEntry] for an instance (includes SV frame if present). + FlcEntry? lookupInstanceEntry(String moduleName, String instanceName) { + final modInstances = _instances[moduleName]; + if (modInstances == null) return null; + + final direct = modInstances[instanceName]; + if (direct != null) return direct; + + // Fallback: search by origName. + for (final entry in modInstances.values) { + if (entry.origName != null && entry.origName == instanceName) { + return entry; + } + } + return null; + } + + /// All module names that have FLC data. + Set get moduleNames => {..._signals.keys, ..._instances.keys}; + + /// Signal names recorded for [moduleName], or empty if none. + Set signalNamesFor(String moduleName) => + _signals[moduleName]?.keys.toSet() ?? {}; + + /// Instance names recorded for [moduleName], or empty if none. + Set instanceNamesFor(String moduleName) => + _instances[moduleName]?.keys.toSet() ?? {}; + + /// Reverse lookup: find all (moduleName, signalName, entry) tuples whose + /// ROHD source frames include the given [fileSuffix] and [line]. + /// + /// [fileSuffix] is matched against the end of each frame's file path + /// (e.g. `'serializer.dart'` matches `'lib/src/serialization/serializer.dart'`). + /// When [line] is non-null only frames on that exact line match. + List<({String module, String signal, FlcEntry entry})> lookupByRohdLine( + String fileSuffix, { + int? line, + }) { + final results = <({String module, String signal, FlcEntry entry})>[]; + for (final modEntry in _signals.entries) { + for (final sigEntry in modEntry.value.entries) { + for (final frame in sigEntry.value.frames) { + if (frame.file.endsWith(fileSuffix) && + (line == null || frame.line == line)) { + results.add(( + module: modEntry.key, + signal: sigEntry.key, + entry: sigEntry.value, + )); + break; // one match per signal is enough + } + } + } + } + for (final modEntry in _instances.entries) { + for (final instEntry in modEntry.value.entries) { + for (final frame in instEntry.value.frames) { + if (frame.file.endsWith(fileSuffix) && + (line == null || frame.line == line)) { + results.add(( + module: modEntry.key, + signal: instEntry.key, + entry: instEntry.value, + )); + break; + } + } + } + } + return results; + } +} + +/// Parsed v5 symbol string info. +class _SymbolInfo { + final String name; + final bool isInstance; + final List<_OutputPos> outputPositions; + final String? origName; + + const _SymbolInfo({ + required this.name, + required this.isInstance, + this.outputPositions = const [], + this.origName, + }); +} + +/// A parsed output-language position from a symbol string. +class _OutputPos { + final String type; // e.g. 'sv', 'sc' + final int line; + final int column; + + const _OutputPos({required this.type, required this.line, this.column = 1}); +} diff --git a/rohd_extension/dart/lib/rohd_source_navigator.dart b/rohd_extension/dart/lib/rohd_source_navigator.dart new file mode 100644 index 000000000..ea77ef85a --- /dev/null +++ b/rohd_extension/dart/lib/rohd_source_navigator.dart @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_source_navigator.dart +// Library exports for the ROHD source navigator Dart package. + +library; + +export 'dtd_service.dart'; +export 'flc_data.dart'; +export 'source_navigator.dart'; diff --git a/rohd_extension/dart/lib/source_navigator.dart b/rohd_extension/dart/lib/source_navigator.dart new file mode 100644 index 000000000..f6d96c972 --- /dev/null +++ b/rohd_extension/dart/lib/source_navigator.dart @@ -0,0 +1,217 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// source_navigator.dart +// Platform-independent source navigation logic — path normalisation, +// frame cycling, and candidate path generation. +// +// This is the Dart port of the core logic from source_navigator.ts. +// VS Code-specific APIs (editor, decorations, status bar) remain in the +// thin TypeScript shell. +// +// 2026 April 27 +// Author: Desmond Kirkpatrick + +/// A single source location frame. +class SourceFrame { + /// File path (package-relative, e.g. `lib/src/foo.dart`). + final String file; + + /// 1-based line number. + final int line; + + /// 1-based column number. + final int col; + + /// Optional description (e.g. function name from stack trace). + final String? desc; + + /// Frame type: `'sv'` for SystemVerilog, `'rohd'` for ROHD Dart source. + final String type; + + const SourceFrame({ + required this.file, + required this.line, + required this.col, + this.desc, + this.type = 'rohd', + }); + + /// Create from JSON map (as received over DTD). + factory SourceFrame.fromJson(Map json) => SourceFrame( + file: json['file'] as String, + line: json['line'] as int, + col: json['col'] as int, + desc: json['desc'] as String?, + type: (json['type'] as String?) ?? 'rohd', + ); + + /// Serialize to JSON for transmission. + Map toJson() => { + 'file': file, + 'line': line, + 'col': col, + if (desc != null) 'desc': desc, + 'type': type, + }; + + /// Short display name for status bar. + String get shortFile => file.split('/').last; + + /// Type tag for display: 'SV', 'ROHD', or 'Source'. + String get typeTag { + switch (type) { + case 'sv': + return 'SV'; + case 'rohd': + return 'ROHD'; + default: + return 'Source'; + } + } +} + +/// Manages frame cycling state for multi-frame source navigation. +class FrameCycler { + List _frames = []; + int _index = 0; + + /// The current list of frames. + List get frames => _frames; + + /// The current frame index. + int get index => _index; + + /// Whether there are multiple frames to cycle through. + bool get hasMultipleFrames => _frames.length > 1; + + /// Whether there are any frames at all. + bool get isEmpty => _frames.isEmpty; + + /// The current frame, or null if empty. + SourceFrame? get current => _frames.isEmpty ? null : _frames[_index]; + + /// Set frames for a single source location (no cycling). + void setSingle(SourceFrame frame) { + _frames = [frame]; + _index = 0; + } + + /// Set frames for multi-frame navigation. + void setMultiple(List frames, {int startIndex = 0}) { + _frames = frames; + _index = startIndex.clamp(0, frames.length - 1); + } + + /// Advance to the next frame (wrapping). + SourceFrame? next() { + if (_frames.isEmpty) return null; + _index = (_index + 1) % _frames.length; + return _frames[_index]; + } + + /// Go back to the previous frame (wrapping). + SourceFrame? prev() { + if (_frames.isEmpty) return null; + _index = (_index - 1 + _frames.length) % _frames.length; + return _frames[_index]; + } + + /// Clear all frames. + void clear() { + _frames = []; + _index = 0; + } + + /// Status bar text for the current frame. + /// + /// Format: `"TYPE 1/3: file.dart:42 desc"` + String get statusText { + if (_frames.isEmpty) return ''; + final f = _frames[_index]; + final desc = f.desc != null ? ' ${f.desc}' : ''; + return '${f.typeTag} ${_index + 1}/${_frames.length}: ' + '${f.shortFile}:${f.line}$desc'; + } + + /// Returns the first frame of each unique type (for opening + /// both ROHD and SV files simultaneously). + List firstOfEachType() { + final seen = {}; + final result = []; + for (final f in _frames) { + if (seen.add(f.type)) { + result.add(f); + } + } + return result; + } +} + +/// Normalize a file path by collapsing `.` and `..` segments. +/// +/// FLC paths from SourceTraceRegistry often contain `.dart_tool/../lib/...` +/// which needs collapsing before resolution. +String normalizePath(String filePath) { + final isAbsolute = filePath.startsWith('/'); + final parts = filePath.split('/'); + final resolved = []; + for (final part in parts) { + if (part == '.' || part.isEmpty) { + continue; + } else if (part == '..' && resolved.isNotEmpty && resolved.last != '..') { + resolved.removeLast(); + } else { + resolved.add(part); + } + } + final joined = resolved.join('/'); + return isAbsolute ? '/$joined' : joined; +} + +/// Generate candidate paths for a package-relative file path. +/// +/// Given a list of workspace root paths, produces candidates (in order): +/// 1. Normalized path relative to each workspace root +/// 2. Normalized path relative to parent directories (up to [parentLevels]) +/// 3. Absolute path (if applicable) +/// 4. Original un-normalized path (fallback) +/// +/// Returns relative candidate strings; the caller (TS shell) converts +/// them to URIs. +List resolveCandidatePaths( + String filePath, { + List workspaceRoots = const [], + int parentLevels = 4, +}) { + final normalized = normalizePath(filePath); + final candidates = []; + + for (final root in workspaceRoots) { + // Direct: workspace root + normalized path + candidates.add('$root/$normalized'); + + // Walk up parent directories. + var parent = root; + for (var i = 0; i < parentLevels; i++) { + final lastSlash = parent.lastIndexOf('/'); + if (lastSlash <= 0) break; + parent = parent.substring(0, lastSlash); + candidates.add('$parent/$normalized'); + } + } + + // Absolute path fallback. + if (normalized.startsWith('/')) { + candidates.add(normalized); + } + + // Try original un-normalized path if it differs. + if (normalized != filePath) { + for (final root in workspaceRoots) { + candidates.add('$root/$filePath'); + } + } + + return candidates; +} diff --git a/rohd_extension/dart/pubspec.yaml b/rohd_extension/dart/pubspec.yaml new file mode 100644 index 000000000..c7807621a --- /dev/null +++ b/rohd_extension/dart/pubspec.yaml @@ -0,0 +1,15 @@ +name: rohd_source_navigator +description: > + Dart implementation of the ROHD source navigator — path normalisation, + frame cycling, and DTD service for cross-probe source navigation. +version: 0.1.0 + +environment: + sdk: ^3.0.0 + +dependencies: + json_rpc_2: ^3.0.0 + web_socket_channel: ^3.0.0 + +dev_dependencies: + test: ^1.24.0 diff --git a/rohd_extension/dart/test/flc_data_test.dart b/rohd_extension/dart/test/flc_data_test.dart new file mode 100644 index 000000000..9c33e078f --- /dev/null +++ b/rohd_extension/dart/test/flc_data_test.dart @@ -0,0 +1,405 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// flc_data_test.dart +// Unit tests for FlcData model: v5 trie-based FLC JSON parsing and +// signal lookup. + +import 'package:rohd_source_navigator/flc_data.dart'; +import 'package:test/test.dart'; + +void main() { + group('FlcData.fromJson (v5 trie)', () { + test('parses single-frame signal', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart', 'lib/src/bar.dart'], + 'modules': { + 'TopModule': { + 'tree': [ + ['0:10:5', 'clk'], + [ + '0:20:3', + ['1:30:1', 'data'] + ], + ['1:50:7', '*sub0'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect(flc.isEmpty, isFalse); + expect(flc.files, ['lib/src/foo.dart', 'lib/src/bar.dart']); + expect(flc.moduleNames, contains('TopModule')); + + // Single-frame signal. + final clkFrames = flc.lookupSignal('TopModule', 'clk'); + expect(clkFrames, isNotNull); + expect(clkFrames!.length, 1); + expect(clkFrames[0].file, 'lib/src/foo.dart'); + expect(clkFrames[0].line, 10); + expect(clkFrames[0].column, 5); + + // Multi-frame signal (outermost frame first in trie, reversed to + // innermost-first in FlcEntry.frames). + final dataFrames = flc.lookupSignal('TopModule', 'data'); + expect(dataFrames, isNotNull); + expect(dataFrames!.length, 2); + // Innermost first after reversal. + expect(dataFrames[0].file, 'lib/src/bar.dart'); + expect(dataFrames[0].line, 30); + expect(dataFrames[1].file, 'lib/src/foo.dart'); + expect(dataFrames[1].line, 20); + + // Instance lookup. + final sub0Frames = flc.lookupInstance('TopModule', 'sub0'); + expect(sub0Frames, isNotNull); + expect(sub0Frames!.length, 1); + expect(sub0Frames[0].file, 'lib/src/bar.dart'); + expect(sub0Frames[0].line, 50); + }); + + test('parses signal with origName', () { + final json = { + 'version': 5, + 'files': ['lib/src/adder.dart'], + 'modules': { + 'Adder': { + 'tree': [ + ['0:42:5', 'sum_0~sum'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + + // Direct match by canonical name. + final directFrames = flc.lookupSignal('Adder', 'sum_0'); + expect(directFrames, isNotNull); + expect(directFrames![0].line, 42); + + // Fallback match by origName. + final origFrames = flc.lookupSignal('Adder', 'sum'); + expect(origFrames, isNotNull); + expect(origFrames![0].line, 42); + }); + + test('parses signal with SV position (legacy svFile)', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'FilterBank': { + 'svFile': 'FilterBank.sv', + 'tree': [ + ['0:868:11', 'clk@2:13'], + ['0:869:13', 'reset@3:13'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect(flc.isEmpty, isFalse); + + final clkEntry = flc.lookupSignalEntry('FilterBank', 'clk'); + expect(clkEntry, isNotNull); + + // SV frame via backward-compat getter. + expect(clkEntry!.svFrame, isNotNull); + expect(clkEntry.svFrame!.file, 'FilterBank.sv'); + expect(clkEntry.svFrame!.line, 2); + expect(clkEntry.svFrame!.column, 13); + expect(clkEntry.svFrame!.type, 'sv'); + + // outputFrames list. + expect(clkEntry.outputFrames.length, 1); + expect(clkEntry.outputFrames[0].type, 'sv'); + + // ROHD src frames. + expect(clkEntry.frames.length, 1); + expect(clkEntry.frames[0].file, 'lib/src/foo.dart'); + expect(clkEntry.frames[0].line, 868); + expect(clkEntry.frames[0].column, 11); + expect(clkEntry.frames[0].type, 'rohd'); + + // allFrames returns output frames first, then ROHD. + final allFrames = clkEntry.allFrames; + expect(allFrames.length, 2); + expect(allFrames[0].type, 'sv'); + expect(allFrames[1].type, 'rohd'); + }); + + test('parses signal with outputFiles map', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'FilterBank': { + 'outputFiles': {'sv': 'FilterBank.sv', 'sc': 'FilterBank.cpp'}, + 'tree': [ + ['0:868:11', 'clk@sv:2:13;sc:10:5'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final clkEntry = flc.lookupSignalEntry('FilterBank', 'clk'); + expect(clkEntry, isNotNull); + + // Two output frames. + expect(clkEntry!.outputFrames.length, 2); + expect(clkEntry.outputFrames[0].type, 'sv'); + expect(clkEntry.outputFrames[0].file, 'FilterBank.sv'); + expect(clkEntry.outputFrames[0].line, 2); + expect(clkEntry.outputFrames[0].column, 13); + expect(clkEntry.outputFrames[1].type, 'sc'); + expect(clkEntry.outputFrames[1].file, 'FilterBank.cpp'); + expect(clkEntry.outputFrames[1].line, 10); + expect(clkEntry.outputFrames[1].column, 5); + + // Backward-compat svFrame returns the first SV frame. + expect(clkEntry.svFrame, isNotNull); + expect(clkEntry.svFrame!.type, 'sv'); + expect(clkEntry.svFrame!.line, 2); + + // allFrames: output frames first (2), then ROHD (1). + expect(clkEntry.allFrames.length, 3); + }); + + test('parses multiple SV positions for same signal', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'Top': { + 'outputFiles': {'sv': 'Top.sv'}, + 'tree': [ + ['0:10:3', 'sig@sv:5:1;sv:20:3'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final entry = flc.lookupSignalEntry('Top', 'sig'); + expect(entry, isNotNull); + expect(entry!.outputFrames.length, 2); + expect(entry.outputFrames[0].line, 5); + expect(entry.outputFrames[1].line, 20); + // svFrame returns the first one. + expect(entry.svFrame!.line, 5); + }); + + test('sv frame is null when no svFile in module', () { + final json = { + 'version': 5, + 'files': ['lib/src/foo.dart'], + 'modules': { + 'Combinational': { + 'tree': [ + ['0:10:3', 'out@5:1'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final outEntry = flc.lookupSignalEntry('Combinational', 'out'); + expect(outEntry, isNotNull); + // No svFile/outputFiles -> output frames should be empty. + expect(outEntry!.svFrame, isNull); + expect(outEntry.outputFrames, isEmpty); + expect(outEntry.frames.length, 1); + expect(outEntry.frames[0].type, 'rohd'); + }); + + test('returns null for missing signals', () { + final json = { + 'version': 5, + 'files': ['lib/src/top.dart'], + 'modules': { + 'Top': { + 'tree': [ + ['0:1:1', 'a'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect(flc.lookupSignal('Top', 'nonexistent'), isNull); + expect(flc.lookupSignal('NonexistentModule', 'a'), isNull); + expect(flc.lookupInstance('Top', 'a'), isNull); + }); + + test('returns empty FlcData when modules is null', () { + final flc = FlcData.fromJson({'version': 5, 'files': []}); + expect(flc.isEmpty, isTrue); + expect(flc.files, isEmpty); + }); + + test('returns empty FlcData when modules is empty', () { + final flc = FlcData.fromJson({'version': 5, 'files': [], 'modules': {}}); + expect(flc.isEmpty, isTrue); + }); + + test('instance with SV position and origName', () { + final json = { + 'version': 5, + 'files': ['lib/src/top.dart'], + 'modules': { + 'Top': { + 'svFile': 'Top.sv', + 'tree': [ + ['0:42:3', '*sub0@20:5~origSub'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final entry = flc.lookupInstanceEntry('Top', 'sub0'); + expect(entry, isNotNull); + expect(entry!.frames.length, 1); + expect(entry.frames[0].line, 42); + expect(entry.svFrame, isNotNull); + expect(entry.svFrame!.line, 20); + expect(entry.outputFrames.length, 1); + expect(entry.outputFrames[0].type, 'sv'); + expect(entry.origName, 'origSub'); + + // Fallback by origName. + final byOrig = flc.lookupInstanceEntry('Top', 'origSub'); + expect(byOrig, isNotNull); + }); + }); + + group('FlcData.empty', () { + test('creates empty instance', () { + final flc = FlcData.empty(); + expect(flc.isEmpty, isTrue); + expect(flc.files, isEmpty); + expect(flc.moduleNames, isEmpty); + expect(flc.lookupSignal('any', 'thing'), isNull); + }); + }); + + group('FlcData multi-module', () { + test('handles multiple modules with shared files', () { + final json = { + 'version': 5, + 'files': ['lib/src/shared.dart', 'lib/src/b_only.dart'], + 'modules': { + 'ModA': { + 'tree': [ + ['0:10:1', 'a'], + ], + }, + 'ModB': { + 'tree': [ + ['1:20:1', 'b'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + expect( + flc.files, + containsAll(['lib/src/shared.dart', 'lib/src/b_only.dart']), + ); + + final aFrames = flc.lookupSignal('ModA', 'a'); + expect(aFrames, isNotNull); + expect(aFrames![0].file, 'lib/src/shared.dart'); + + final bFrames = flc.lookupSignal('ModB', 'b'); + expect(bFrames, isNotNull); + expect(bFrames![0].file, 'lib/src/b_only.dart'); + }); + }); + + group('FlcFrame edge cases', () { + test('handles frame with only file:line (no column)', () { + final json = { + 'version': 5, + 'files': ['lib/x.dart'], + 'modules': { + 'M': { + 'tree': [ + ['0:99', 's'], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final frames = flc.lookupSignal('M', 's'); + expect(frames, isNotNull); + expect(frames![0].line, 99); + expect(frames[0].column, 1); // defaults to 1 + }); + + test('skips malformed frame strings', () { + final json = { + 'version': 5, + 'files': ['lib/x.dart'], + 'modules': { + 'M': { + 'tree': [ + [ + 'bad', + ['0:10:5', 's'] + ], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + final frames = flc.lookupSignal('M', 's'); + expect(frames, isNotNull); + // 'bad' frame is skipped since file index parse fails. + expect(frames!.length, 1); + expect(frames[0].line, 10); + }); + + test('shared trie prefix produces correct frames', () { + final json = { + 'version': 5, + 'files': ['lib/src/top.dart', 'lib/src/inner.dart'], + 'modules': { + 'Top': { + 'tree': [ + [ + '0:100:1', // shared outer frame + ['1:10:5', 'sig1'], + ['1:20:3', 'sig2'], + ], + ], + }, + }, + }; + + final flc = FlcData.fromJson(json); + + // Both signals share the outer frame 0:100:1. + final sig1 = flc.lookupSignal('Top', 'sig1'); + expect(sig1, isNotNull); + expect(sig1!.length, 2); // inner + outer + // Innermost first after reversal. + expect(sig1[0].line, 10); + expect(sig1[1].line, 100); + + final sig2 = flc.lookupSignal('Top', 'sig2'); + expect(sig2, isNotNull); + expect(sig2!.length, 2); + expect(sig2[0].line, 20); + expect(sig2[1].line, 100); + }); + }); +} diff --git a/rohd_extension/package-lock.json b/rohd_extension/package-lock.json new file mode 100644 index 000000000..ec68a3030 --- /dev/null +++ b/rohd_extension/package-lock.json @@ -0,0 +1,3887 @@ +{ + "name": "rohd", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rohd", + "version": "0.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "ws": "^8.14.0" + }, + "devDependencies": { + "@types/node": "^20.2.5", + "@types/vscode": "^1.80.0", + "@types/ws": "^8.5.5", + "@vscode/vsce": "3.9.2", + "typescript": "^5.9.3" + }, + "engines": { + "vscode": "^1.80.0" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.17.1.tgz", + "integrity": "sha512-zBhRGzABKSI7hfWh5EaZmril5ybZ7imBN1qEZl5sDTaelr+l8SnPjZO50Q4dnKnm347YPIlBMSnXKZyh3Yu5DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.11.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.2.tgz", + "integrity": "sha512-yDhtBOGDCdK9ipQ9g3+wmlMEPnZx2pXaDicDd9jYyR1L+7lEbvEohTDmF5qejZDutZY3m9pWPxeYxzNC701A2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.4.1.tgz", + "integrity": "sha512-yqgoyOIMCH7TNaSLMBTP+4LUlbMMf1zgC8nzOFG95lmW82CmsAEtUT0J93e4BdqDcnX5qle/9X+yb7A8Mw9M0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.11.2", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.7.1" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.7.tgz", + "integrity": "sha512-JVUD8X2tfDMWjcjLs4yVxxVrS8yR5vnh386GAXT9Qj79nBxxXSaHFQZg5FweLmT8HlPQ3kii6noUB+Z9RN7DvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^13.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^10.2.2", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/rohd_extension/package.json b/rohd_extension/package.json new file mode 100644 index 000000000..bf397e864 --- /dev/null +++ b/rohd_extension/package.json @@ -0,0 +1,100 @@ +{ + "name": "rohd", + "displayName": "ROHD", + "description": "ROHD extension for VS Code — snippets, cross-probe source navigation, and language support for the ROHD hardware design framework.", + "version": "0.1.0", + "publisher": "rohd", + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "https://github.com/intel/rohd" + }, + "icon": "resources/rohd_icon.png", + "engines": { + "vscode": "^1.80.0" + }, + "keywords": [ + "rohd", + "hardware", + "rtl", + "hdl", + "cross-probe" + ], + "categories": [ + "Snippets", + "Other" + ], + "activationEvents": [ + "onCommand:rohd.openSourceLocation", + "onCommand:rohd.openSourceLocations", + "onDebugResolve:dart", + "onDebug:dart", + "onStartupFinished" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "rohd.openSourceLocation", + "title": "ROHD: Go to Source Location" + }, + { + "command": "rohd.openSourceLocations", + "title": "ROHD: Go to Source Locations (multi-frame)" + }, + { + "command": "rohd.nextSourceLocation", + "title": "ROHD: Next Source Frame" + }, + { + "command": "rohd.prevSourceLocation", + "title": "ROHD: Previous Source Frame" + }, + { + "command": "rohd.connectDtd", + "title": "ROHD: Connect to Dart Tooling Daemon" + }, + { + "command": "rohd.showForwardedUris", + "title": "ROHD: Show Forwarded DTD/VM URIs" + } + ], + "configuration": { + "title": "ROHD", + "properties": { + "rohd.enableCompletions": { + "type": "boolean", + "default": true, + "description": "Enable context-aware ROHD code completions (If, Case, Pipeline, Module, etc.). Set to false to disable them." + }, + "rohd.dtdUri": { + "type": "string", + "default": "", + "description": "WebSocket URI of the Dart Tooling Daemon (ws://...). Leave empty for auto-discovery." + } + } + }, + "snippets": [ + { + "language": "dart", + "path": "./snippets/rohd.json" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "package": "vsce package --allow-missing-repository" + }, + "dependencies": { + "ws": "^8.14.0" + }, + "devDependencies": { + "@types/node": "^20.2.5", + "@types/vscode": "^1.80.0", + "@types/ws": "^8.5.5", + "@vscode/vsce": "3.9.2", + "typescript": "^5.9.3" + } +} diff --git a/rohd_extension/resources/rohd_icon.png b/rohd_extension/resources/rohd_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f1faecdde32ff3c2a4b01f5ca74a58c88c2726 GIT binary patch literal 5225 zcmZ`-cQoAFxBn_bMvXCQ5M}gU61|3)G3qFzchQ6BBu0qdOGGE41ks}=%7`JsB}DW- z*VTiB5cSRdy|><4?~k{><*e^MyPfa(?7h!935NRG)D)}~002-U;F>5x+xPEti;VC_ zEqaj?8d6tvJ#_$RN~64V01>`9o!}@v00`p)fXEjBa84jaZUI1`1OV*V1Asy<05JI$ zb{Q!VI>;Q6+M2-4zgKB@RThCl9)Qr(BHttN-QQ9cnWV7vN3r- zotS9Z4#^f7J@i53`ElB--)Bh*@_SlfdBnkZ%5RT} z1_dmm=FAhn9Yt0ih#)FI`?!uD-x6;46KfND6CfRtKrILLFZd5XrK<>Vs?!J_k-Teg zyG|qXef}`Vq(&6=SspX<-DC^B75hxxhZR2C8c`=2QOdsQETb~Kh$WL{Jwcdxbcqv~ zWBFK*?~5iCby_lw!>>fj9J8=h#~?SbO6sSAo(b3-Ys;aK2rCCm9sX=)o%FJN`;J~_ zon-h#CeMvAl_ln*+n0=8Mp5}m_L(ACJPDHxSBI%?21nFVLC+dYP#URpIdVv}XSRWD zP{z#7wvQYsST7w@^CR+JuVqa9g*(0DN3#rDp08nL%3hBxjyBqxJ5J#TpB7b>q%hUC z^PaITN4?Zr3CZ-miK($TqkMYng5YS4hp#ofk)>oPTvGy-DKk86=HZHWd&+!a7TT;4 zm2xI9Hqce68d<)OJp|zAk{c4;M|Z2A>NaNOM%l!wW;Z=Rkm&LOT-e}4OKbHF$Y}qA z0qFLpMkl|q@^GqzW$f^JFMU&%xxKP3C`)ibC!PgT+wq2b`6jXEN81*Sw@|9ZoL?5U zG8^-g?Wv9~-k_lRp{EK%$$+{kO>&lVxL^uTZxYfq?BhNd9U-Oie2AMDp8qJtvD<^s z%je@Y_Dtj<9GrRWssI*FgE~d=3~+XBhr=`7rI(WHR2dz{v@=E3CwoU>E=TGiz3E(t z^US<|_O3IwD0mTeD6Pq`HUP1LiUEG*_awmb#4Ori+cJV`lkTX<@^9q;q6$Hjsh>p* zE8+ve74#_51M;Dq|CgcKv0%!~7`Dq6Ulrf&urTmwk@u(72o(dyY&krX-Hc(y=Y7T7 z+4J?DX(mit>dx>Cl>&*l8us;B>1=nicVOK8E)09&>$=N7d`*=6M|~@%izBS>7%KSC z_@G58C8@A(0k|>AnJPdfNiBz>6B<3t;emCB7W|~Ew&3Z6roQbiQ7rR=S;}$x zmp47nW$|2}c;2*haca2!hk*jTEGWApAWfTw4fo^FA6$-J<_^#Se(+%94$UOtx@{6j zlM6ZOn^>ntdqb_QLSRuCOJ=lt?A^>e%i3}zX9VnfapC$9UlBwdC8R!mKKVVBInB4l zvFO96qbxA6f-rEJvFh&_Cc~SXOQPlJD;30=EoM{&B6xRy4T*~KZel(_ebO(}n+W|^ zLjx{Q&(1h0FAaZ0M4=TkU@g2JeXdVr+^p82J zdpa>7&$MPqpVBwbEp&>xpGQ~?r!zxWQt0EfA0BKCNhC%*CW<|zpj$y%NL!!tM$k75 zZs@JJC9(83g(wE?4b>Z}QZ}-Da(r;G`KGzzB9n9ll6h6LUEnR*f}<%L+Wy2IeQp7i zjrTN|zYWa@gPmo}e!Xa}^l8J1+^*d*sOV)JIk$sR&SL9k{ks?hSMK^&KS$$lD zp55o)5(3119NeGxUkX{*vGHR^4qGaH-s9L3=}pfdH3jMrfFu4iezbwzBYjn4f?WsP zHkR%dL+g32Q8`=+20p|OyppZaj!M!B7T}##NYe89z8>KQA~OQosD|~82I@@I5m9!D z7HR&zDR9k$lNomI5$}{-DmQuq9HrY$WQRCYf8}T5k08ahDMH3EWf0G~($UTMQJu$z zw&olqjAJ*4jNziP(TMkiBGM0z`eB8f)%CPo>OwwrU6_YY)Xk zvg0=}&zF-VknjF_O4lxP#iAFKVPl_~+kdIJH-C(q`iqcvE&Z^QCyvuQFgD&7=CW@{ zd{ooSyvmoSo7B*xoH=`T+_2g1l}{m68eIKkmnKy6E6*3gLcqT?cfZXm=-NOc(^o6F zhy6<`#VgTJn{q{*#+sCoR1;bW;hfc)Sr3*78XsqB+PesN&HFF-)-Cf~O)|TvsNJIzjzZVFqVIUKi5E+T z5>RV#QnQe3%jXz0=GjzJih`vUMPncb7fR&r)N#ylyd8cxf4s@idA~IgYVXLArs~>g z^*m}pEA>Iq=!CWN&Wmz@UK$2bw3PL&eg0QR`9J=nll^J~Q2QspaU}UY@d|<|B=yFa z_inqke1d&{kJ9_k+N{!?t~7q|2y#E?8zQ7qr+17W{+dDT7Pn8^7)~#=1Dgrq!e*dKDWQ#g;B}F0Gj*fkzO+1gC8CBRs<@Z>tjQ^*YF=de4#D;G z2n>&wMzr?T8Q!xL5+cgl+P8>6xpj!V`ej0*(SU%ff_g8JWmc(^CTrMBzQTnVt3 zhg^scdqmPV!U_JwvqOCr4RDWqG*k>OcM~;{#NZ7+UjmZGia7E9Dj8NF{33cP)vPC zJGuWjlRSoiJka(JUef9`744d{9D^Kx9om|8ob_zowC59MHdeEAR@y`1I&3i>gVQGD zLA0Sw@auGYl83lYH}|^{JT`)Dug{;+j@{#FrkoP_)!3U`^an!odj5wQ*S9dEogduN4q730S@{r&QA+!))Z0&-&_U%h)M z;*qg>8uDtEwK}!>%lD9m<$WMKT?!8J@46;ineqW+|q|O#ScMtAc9TF*C zIOrfYh`RJzl)Q+Ama5Jo_dISWPT+@>A8XDIO#aZ>rk12Cn%QL#AJZn`IDawZ8fTI6 z&;K5rJ6%oUu732V!|9k^Pz}=rhac*U)~ZL{P0X<`+t2)QTT*Mp_pgqUeIzRK+KFyC zPS!qx8gi-j!T__9!+_UY_r7`3^tJT~p~7EWuknMn&_Rsy-Tc+sQ5)P2*HF6RqlZcc zr2D3AAj(+9N(uRgACys@P~;@~HmaLkbDzQ6>lKgMKkV>=c5CEKU-9&6s2y zRh>ah#pv%>m|J6=4e=^~+Anx9CU6KN?P|4Rznk=$F{lQ+Zg1`m59jm1&Vb)=Fdx6` zb3Rudl{5^LcHf+4Cu-fZ=H1}@2T!2`&$B;!Be7y@n@sM}~gZ+=EqtccxmO?-A>biFVZOMCUknJ$xZki2inx zhB!3ZQzg`s22hU&4=A@V#l!NJ5N>i2276l@^)r2zY}<+ZyA-cL2C=UG_h-KnIxGW> zBP{UpskcK1Y591Wc|dNtsNgMb$+X_G!Efx(4e)9xVpOD=gc$S>?1aX=gjL^ic~|GKJ$R17_3%tfHW$T2P1=WS0gL{lPi+{ofjEc-Lf;Xy<&KJI3 zc5F*y6jk5Q3cZMdmc~FK@8}EWo>_O_v1St(B;^S(An2$dQ2XtY!U_WUD%k`c#if>7 zLCOKqN|Vvw4Tp942C`;p?~MtRD=f9+>%F0SC9dYl%Bd&^!NL1k#P19J0+sCehv}xj zTyqD#ohNCvnc;`b)(+Ha; zQR2;>(rc+$rxW-}d`vVafqN4QS>)4ryE`l@){^B0UVaR~-~`Cs!4-7YFRn}d(n7yI z;3ZHZCy%#YI?fDrp_AsgvF`cR8fe7E4%A*v0O8w|zFNak7^Gq=+~&R0O1gV=k;ef8 zF>|(MS}i7})VktBn#>wKX?e}X`*E(C6cYB?u(Mcm(VlMdsv;hk9A)_Lx4_I&neSXO z!B9GxSyDpIDl?sCyK51D*Y8Ugr_-M~3X zIrj7A0Eupzf6`EH%$t~yiZ&rMA(S1QR za)AxTUW=tr;83uXN8endzMDwFM zNo#Q`Up8m;33yC3%3M_}TmE_w7IHq6m{Upvj^K|X8gFJdeq(w|VkQzz!z`l%DHPTy zaAh-YVOP?DA36)YS~dH~z>k+T{?jQ$I*S~LKh6I$Tlf4I4QYkWR0*S1jlSn?c|DF8 zgEe2FSN3(E)5Bj1j?z#Qb|hl~wr4bSkPD|HzPah6*cTH1{AcYCxK{$dlVW-;28!8I zgA+`;yUxI=1N+fkIJcuiQ7Bze105H2_qvD3r}pZ{cV>7E6vOuxs^Y~a)akDlgh=g2 zNIpzYk>>KNY$aUpKRSAtzjfg1WaL+4r3acbDot#1H8K0dtIQUaTl=K5SIa*pV z=$AnBa37__L$3aavlnG*k7+v*CWUfnWe8hx+A0`-qc=v(>vC=IG3DmN(B_gA>DO+x z7?Yi(hbDmtkw?{$E?IB3CWlTK`+b}@9Dn5)UA=Vw! zkTgFa4gy5xUMCG+a}v_hAIHIJ>Ev3~^Pt5!Jd(LqBlsgl z?bnoDnr_qtA-KO>3AkiGvsFs=41Wqv(n21FogfZ9E;&K)HXSakGWTRN`iqPU2j_IC zzF``v>QIwBwSxSsB;Vyb4$Q0!UspQXJ5?Xa=LN!ap-|=25gK^mQKG z_x(|%^>K1x$}mO@1m|-hRF$Ew2QvvjKK~>WbLxTr9a@`62(`@pHIxy!o~eRqs3qFG zT#@>N39mXux^4n$<)SJ?K4PlRWt531ys8!+1?9|>;sxj6E4aC?iaG2C*_v#=Z6~6V zrRlf`W4#x4L;3h#g(UX7quAA7Ww(9AUFkRDaqn728!47VBs&moV_MuRTrApLSe>KW zDptCtww1CBTk+{p z>;pYayq7(7~!CdH$clgz}X?dMZxKb3!wp|B&4K8CGLw#NSR1VDBP1$kdYLY zkWi42*tqp9=KlzI`#8Hl3jO~DvQ2UF1OarIiFp9ZA(+$miO(Z|D?L*XC@#pY%ns4*gyd!Jd|Qil=M9G$Sf!cJ*Q@*%qM3Oo)#0%k=d!w1ET|M h1LPx-@KPNyV26n=;HSpDmjn|4LQ7w>Uc)}>zX0c)l=J`q literal 0 HcmV?d00001 diff --git a/rohd_extension/snippets/rohd.json b/rohd_extension/snippets/rohd.json new file mode 100644 index 000000000..5c3c9ae73 --- /dev/null +++ b/rohd_extension/snippets/rohd.json @@ -0,0 +1,385 @@ +{ + "ROHD: Create Module": { + "prefix": ["mod", "module", "Mod", "Module"], + "body": [ + "/// A ROHD module with two data inputs and one output.", + "class ${1:MyModule} extends Module {", + "\t/// The output of this module.", + "\tLogic get ${2:out} => output('${2:out}');", + "", + "\t/// The clock input.", + "\tLogic get clk => input('clk');", + "", + "\t/// The reset input.", + "\tLogic get reset => input('reset');", + "", + "\t/// The configured data depth.", + "\tfinal int depth;", + "", + "\t/// Whether data should be latched.", + "\tfinal bool latchData;", + "", + "\t/// Constructs a ${1:MyModule}.", + "\t${1:MyModule}(", + "\t\tLogic ${3:clk},", + "\t\tLogic ${4:reset},", + "\t\tLogic ${5:a},", + "\t\tLogic ${6:b}, {", + "\t\tthis.depth = ${7:1},", + "\t\tthis.latchData = ${8:false},", + "\t\tString? definitionName,", + "\t\tsuper.name = '${9:my_module}',", + "\t\tsuper.reserveName,", + "\t\tsuper.reserveDefinitionName,", + "\t}) : super(definitionName: definitionName ?? '${1:MyModule}') {", + "\t\t// Register inputs and outputs of the module.", + "\t\t${3:clk} = addInput('clk', ${3:clk});", + "\t\t${4:reset} = addInput('reset', ${4:reset});", + "\t\t${5:a} = addInput('${5:a}', ${5:a}, width: ${5:a}.width);", + "\t\t${6:b} = addInput('${6:b}', ${6:b}, width: ${5:a}.width);", + "\t\tfinal ${2:out} = addOutput('${2:out}', width: ${5:a}.width);", + "", + "\t\t${2:out} <= ${5:a};", + "\t}", + "}" + ], + "description": "ROHD: Create Module with clk, reset, depth, latchData, definitionName, and instance naming parameters." + }, + "ROHD: Generate Simulation snippet.": { + "prefix": ["sim", "Simulator", "simulation"], + "body": [ + "final clk = SimpleClockGenerator(10).clk;", + "final reset = Logic(name: 'reset');", + "WaveDumper(module, outputPath: 'wavedumpername.vcd');", + "Simulator.setMaxSimTime(100);", + "unawaited(Simulator.run());", + "", + "// reset flow", + "reset.inject(0);", + "await clk.nextNegedge;", + "reset.inject(1);", + "await clk.nextNegedge;", + "await clk.nextNegedge;", + "await clk.nextNegedge;", + "reset.inject(0);", + "await clk.nextNegedge;", + "", + "// Stimulus and checking here", + "await clk.nextNegedge;", + "", + "Simulator.endSimulation();", + "await Simulator.simulationEnded;" + ], + "description": "Add simulation snippet. Normally used in performing testing." + }, + "ROHD: Finite State Machine Module (FSM)": { + "prefix": ["fsmModule", "FSMModule", "fsm_module"], + "body": [ + "// Change idle, active and done to your respective states", + "// Keep the enum at file scope, outside the Module class.", + "/// FSM states for SampleFSMModule.", + "enum FSMState {", + "\t/// Waiting for work.", + "\t${1:idle},", + "", + "\t/// Processing an active transaction.", + "\t${2:active},", + "", + "\t/// Completing the transaction.", + "\t${3:done},", + "}", + "", + "class SampleFSMModule extends Module {", + "\t// Modified Logics inputs based on your needs", + "\tSampleFSMModule(Logic clk, Logic reset, Logic a, Logic b)", + "\t\t\t: super(name: 'fsm_module_name') {", + "\t\tclk = addInput('clk', clk);", + "\t\treset = addInput(reset.name, reset);", + "\t\ta = addInput(a.name, a);\n", + "\t\t// The output of the fsm, modified this based on your needs", + "\t\tfinal c = addOutput('output_pin');\n", + "\t\t// Below is the example of the state transition, ", + "\t\t// modified according to your needs", + "\t\tfinal states = [", + "\t\t\t// IDLE", + "\t\t\tState(", + "\t\t\t\tFSMState.${1:idle},", + "\t\t\t\tevents: {", + "\t\t\t\t\tb: FSMState.${2:active},", + "\t\t\t\t},", + "\t\t\t\tactions: [", + "\t\t\t\t\tc < 0,", + "\t\t\t\t],", + "\t\t\t),", + "\t\t\t// ACTIVE", + "\t\t\tState(", + "\t\t\t\tFSMState.${2:active},", + "\t\t\t\tevents: {", + "\t\t\t\t\ta: FSMState.${3:done},", + "\t\t\t\t},", + "\t\t\t\tactions: [", + "\t\t\t\t\tc < 0,", + "\t\t\t\t],", + "\t\t\t),", + "\t\t\t// DONE", + "\t\t\tState(", + "\t\t\t\tFSMState.${3:done},", + "\t\t\t\tevents: {", + "\t\t\t\t\tConst(1): FSMState.${1:idle},", + "\t\t\t\t},", + "\t\t\t\tactions: [", + "\t\t\t\t\tc < 1,", + "\t\t\t\t],", + "\t\t\t)", + "\t\t];\n", + "\t\tFiniteStateMachine(clk, reset, FSMState.${1:idle}, states);", + "\t}", + "}" + ], + "description": "Full FSM Module scaffold. Use fsm inside an existing Module. The enum belongs at file scope, outside the class." + }, + "ROHD Testbench": { + "prefix": ["vf", "tb", "testbench"], + "body": [ + "import 'dart:async';", + "import 'dart:collection';", + "import 'package:logging/logging.dart';", + "import 'package:rohd/rohd.dart';", + "import 'package:rohd_vf/rohd_vf.dart';", + "", + "/// Main function entry point to execute this testbench.", + "Future main({Level loggerLevel = Level.FINER}) async {", + " // Set the logger level", + " Logger.root.level = loggerLevel;", + "", + " // Create the testbench", + " final tb = TopTB();", + "", + " // Build the DUT", + " await tb.dut.build();", + "", + " // Attach a waveform dumper to the DUT", + " WaveDumper(tb.dut);", + "", + " // Set a maximum simulation time so it doesn't run forever", + " Simulator.setMaxSimTime(300);", + "", + " // Create and start the test!", + " final test = DUTTest(tb.intf);", + " await test.start();", + "}", + "", + "class TopTB {", + " // TODO: Create an instance of the DUT (The Module you want to test)", + " late final ${1:dutModule} dut;", + "", + " // TODO: Build an instance of the interface for the DUT", + " final ${2:dutInterface} intf = ${2:dutInterface}();", + "", + " TopTB() {", + " // TODO(Optional): Initialized your pin here", + " // Example: intf.clk <= SimpleClockGenerator(10).clk;", + "", + " // Create the DUT, passing it our interface", + " dut = ${1:dutModule}(intf);", + " }", + "}", + "// A Test is like a top-level testing entity that contains the top testbench", + "// Env and kicks off Sequences. Only one Test should be running at a time. The", + "// Test also contains a central Random object to be used for randomization in", + "// a reproducible way.", + "class DUTTest extends Test {", + " // Interface of DUT", + " final ${2:dutInterface} intf;", + "", + " late final DUTEnv env;", + "", + " late final DUTSequencer _dutSequencer;", + "", + " DUTTest(this.intf, {String name = 'dutTest'}) : super(name) {", + " env = DUTEnv(intf, this);", + " _dutSequencer = env.agent.sequencer;", + " }", + "", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + " final obj = phase.raiseObjection('dut_test');", + "", + " logger.info('Running the test...');", + "", + " // TODO: Register your test action with Simulator here, you can", + " // change the Simulation time.", + " // Simulator.registerAction(1, () {", + " // Example: intf.reset.put(0);", + " // });", + "", + " // TODO: Add sequenceItem to the sequencer for initialization", + " // Example: _dutSequencer.add(DUTSeqItem(false));", + "", + " // TODO: Kick start the Sequencer with n number of DUTSequence repetition", + " // Example: await _dutSequencer.start(DUTSequence(5));", + "", + " logger.info('Done adding stimulus to the sequencer');", + "", + " obj.drop();", + " }", + "}", + "", + "class DUTEnv extends Env {", + " // Interface of DUT", + " final ${2:dutInterface} intf;", + "", + " /// The agent that communicates with the DUT.", + " late final DUTAgent agent;", + "", + "", + " DUTEnv(this.intf, Component parent, {String name = 'dutEnv'})", + " : super(name, parent) {", + " agent = DUTAgent(intf, this);", + " }", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + "", + " // TODO: You can add a listener to the output of the monitor for some logging", + " // Example:", + " // agent.valueMonitor.stream.listen((event) {", + " // logger.finer('');", + " // });", + " }", + "}", + "", + "/// An agent to bundle the sequencer, driver, and monitors for one DUT.", + "class DUTAgent extends Agent {", + " final ${2:dutInterface} intf;", + " late final DUTSequencer sequencer;", + " late final DUTDriver driver;", + " late final DUTValueMonitor valueMonitor;", + "", + " DUTAgent(this.intf, Component parent, {String name = 'dutAgent'})", + " : super(name, parent) {", + " sequencer = DUTSequencer(this);", + " driver = DUTDriver(intf, sequencer, this);", + " valueMonitor = DUTValueMonitor(intf, this);", + " }", + "}", + "", + "/// A basic [Sequencer] for the DUT.", + "class DUTSequencer extends Sequencer {", + " DUTSequencer(Component parent, {String name = 'dutSequencer'})", + " : super(name, parent);", + "}", + "", + "// A driver responisble for converting SequenceItem into signal transitions", + "// on a hardware interface.", + "class DUTDriver extends Driver {", + " final ${2:dutInterface} intf;", + "", + " final Queue _pendingItems = Queue();", + "", + " Objection? _driverObjection;", + "", + " DUTDriver(this.intf, DUTSequencer sequencer, Component parent,", + " {String name = 'dutDriver'})", + " : super(name, parent, sequencer: sequencer);", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + "", + " // Listen to new items coming from the sequencer, and add them to a queue", + " sequencer.stream.listen((newItem) {", + " _driverObjection ??= phase.raiseObjection('dut_driver')", + " ..dropped.then((value) => logger.fine('Driver objection dropped'));", + " _pendingItems.add(newItem);", + " });", + "", + " // Every clock negative edge, drive the next pending item if it exists", + " intf.clk.negedge.listen((args) {", + " if (_pendingItems.isNotEmpty) {", + " final nextItem = _pendingItems.removeFirst();", + " drive(nextItem);", + " if (_pendingItems.isEmpty) {", + " _driverObjection?.drop();", + " _driverObjection = null;", + " }", + " }", + " });", + " }", + "", + " // Translate a SequenceItem into pin wiggles", + " // TODO: Map sequence item to respective pin", + " void drive(DUTSeqItem? item) {", + " // Example:", + " // if (item == null) {", + " // intf.en.inject(0);", + " // } else {", + " // intf.en.inject(item.en);", + " // }", + " }", + "}", + "", + "/// A monitor is responsible for watching an interface and reporting out", + "/// interesting events onto an output stream.", + "class DUTValueMonitor extends Monitor {", + " final ${2:dutInterface} intf;", + "", + " DUTValueMonitor(this.intf, Component parent,", + " {String name = 'dutValueMonitor'})", + " : super(name, parent);", + "", + " @override", + " Future run(Phase phase) async {", + " unawaited(super.run(phase));", + " await intf.reset.nextNegedge;", + "", + " intf.clk.posedge.listen((event) {", + "", + " // TODO: Add the output pin that you want to monitor here", + " // Example: add(intf.val.value);", + " });", + " }", + "}", + "", + "", + "// A Sequence is a modular object which has instructions for how to send", + "// SequenceItems to a Sequencer. A typical use case would be sending a", + "// collection of SequenceItems in a specific order.", + "class DUTSequence extends Sequence {", + " final int numRepeat;", + "", + " DUTSequence(this.numRepeat, {String name = 'dutSequence'}) : super(name);", + "", + " @override", + " Future body(Sequencer sequencer) async {", + " final dutSequencer = sequencer as DUTSequencer;", + " for (var i = 0; i < numRepeat; i++) {", + " // TODO: Add sequenceItem to the Sequence that we want to send to test", + " // Example: dutSequencer", + " // ..add(DUTSeqItem(true))", + " // ..add(DUTSeqItem(false));", + " }", + " }", + "}", + "", + "// A SequenceItem represents a collection of information to transmit across", + "// an interface. A typical use case would be an object representing", + "// a transaction to be driven over a standardized hardware interface.", + "class DUTSeqItem extends SequenceItem {", + " // TODO: Add your sequence Item", + " // Example: final bool _enable;", + "", + " // TODO: Register your input variable to the constructor", + " // Example: DUTSeqItem(this._enable);", + "", + " // TODO: Create a getter for monitoring purposes", + " // int get en => _enable ? 1 : 0;", + "}", + "" + ], + "description": "Dart Testbench Template" + } +} \ No newline at end of file diff --git a/rohd_extension/src/conditional_completions.ts b/rohd_extension/src/conditional_completions.ts new file mode 100644 index 000000000..29c489df6 --- /dev/null +++ b/rohd_extension/src/conditional_completions.ts @@ -0,0 +1,1087 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * conditional_completions.ts + * Context-aware CompletionItemProvider for ROHD constructs. + * + * Four scopes: + * + * 1. FILE SCOPE — FSM (enum + class extends Module), Module scaffold + * only appear at file/top level, not inside a function like main(). + * + * 2. MODULE BODY — Pipeline, Sequential, Combinational appear when the + * cursor is inside a class that extends Module. + * + * 3. INSIDE _ALWAYS — If, If.block, Iff, Else, Case, CaseZ, + * CaseItem, and conditional assignment (<) only appear when the + * cursor is inside a Combinational or Sequential block. + * + * 4. TEST DIRECTORY — test(), group(), tearDown(), and ROHD simulation + * test scaffolds only appear for Dart files under a test/ directory. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; + +// --------------------------------------------------------------------------- +// Context detection +// --------------------------------------------------------------------------- + +/** + * Determine the ROHD context at the cursor position. + * + * Returns a set of active scopes: 'file', 'module', 'always', 'test'. + */ +function detectContext( + document: vscode.TextDocument, + position: vscode.Position, +): Set { + const textBefore = document.getText( + new vscode.Range(new vscode.Position(0, 0), position), + ); + + const scopes = new Set(); + + // --- Pass 1: find brace positions for classes and functions --- + + const classPattern = /class\s+\w+(?:\s+extends\s+[^\{]+)?[^\{]*\{/g; + let m: RegExpExecArray | null; + const classBraces = new Set(); + while ((m = classPattern.exec(textBefore)) !== null) { + classBraces.add(m.index + m[0].length - 1); + } + + const funcPattern = /(?:void|Future|int|bool|String|dynamic|var|final|async|static)\s+\w+\s*(?:<[^>]*>)?\s*\([^)]*\)\s*(?:async\s*)?\{/g; + const funcBraces = new Set(); + while ((m = funcPattern.exec(textBefore)) !== null) { + funcBraces.add(m.index + m[0].length - 1); + } + + // Also catch constructors: `ClassName(...) : super(...) {` or `ClassName(...) {` + const ctorPattern = /\b\w+\s*\([^)]*\)\s*(?::\s*super\([^)]*\)\s*)?\{/g; + while ((m = ctorPattern.exec(textBefore)) !== null) { + const bracePos = m.index + m[0].length - 1; + if (!classBraces.has(bracePos)) { + funcBraces.add(bracePos); + } + } + + // --- Pass 2: walk through text tracking brace depth + always detection --- + + let inString = false; + let stringChar = ''; + + interface BraceFrame { type: 'module' | 'function' | 'other' } + const braceStack: BraceFrame[] = []; + + let parenDepth = 0; + let alwaysParenDepth = -1; + + for (let i = 0; i < textBefore.length; i++) { + const ch = textBefore[i]; + + // String literals. + if (inString) { + if (ch === '\\') { i++; continue; } + if (ch === stringChar) { inString = false; } + continue; + } + if (ch === "'" || ch === '"') { + inString = true; + stringChar = ch; + continue; + } + + // Line comments. + if (ch === '/' && i + 1 < textBefore.length && textBefore[i + 1] === '/') { + i += 2; + while (i < textBefore.length && textBefore[i] !== '\n') { i++; } + continue; + } + + // Block comments. + if (ch === '/' && i + 1 < textBefore.length && textBefore[i + 1] === '*') { + i += 2; + while (i + 1 < textBefore.length && + !(textBefore[i] === '*' && textBefore[i + 1] === '/')) { i++; } + i++; + continue; + } + + // Braces. + if (ch === '{') { + let type: 'module' | 'function' | 'other' = 'other'; + if (classBraces.has(i)) { + type = 'module'; + } else if (funcBraces.has(i)) { + type = 'function'; + } + braceStack.push({ type }); + } else if (ch === '}') { + if (braceStack.length > 0) { + braceStack.pop(); + } + } + + // Parentheses — track Combinational/Sequential. + if (ch === '(') { + const lookback = textBefore.substring(Math.max(0, i - 30), i); + if (/(?:Combinational|Sequential)\s*$/.test(lookback)) { + alwaysParenDepth = parenDepth; + } + parenDepth++; + } else if (ch === ')') { + parenDepth--; + if (alwaysParenDepth >= 0 && parenDepth <= alwaysParenDepth) { + alwaysParenDepth = -1; + } + } + } + + // --- Determine scopes --- + + const insideModule = braceStack.some(f => f.type === 'module'); + const insideFunction = braceStack.some(f => f.type === 'function'); + const insideAlways = alwaysParenDepth >= 0 && insideFunction; + + if (insideAlways) { scopes.add('always'); } + if (insideModule) { scopes.add('module'); } + if (!insideFunction && !insideModule) { scopes.add('file'); } + if (/(^|[\\/])test[\\/]/.test(document.uri.fsPath)) { scopes.add('test'); } + + return scopes; +} + +interface EnclosingClassInfo { + className: string; + enumInsertionPosition: vscode.Position; +} + +function findEnclosingClassInfo( + document: vscode.TextDocument, + position: vscode.Position, +): EnclosingClassInfo | undefined { + const textBefore = document.getText( + new vscode.Range(new vscode.Position(0, 0), position), + ); + + const classPattern = /class\s+(\w+)(?:\s+extends\s+[^\{]+)?[^\{]*\{/g; + let match: RegExpExecArray | null; + const classBraces = new Map(); + while ((match = classPattern.exec(textBefore)) !== null) { + classBraces.set(match.index + match[0].length - 1, { + className: match[1], + classStart: match.index, + }); + } + + const funcPattern = /(?:void|Future|int|bool|String|dynamic|var|final|async|static)\s+\w+\s*(?:<[^>]*>)?\s*\([^)]*\)\s*(?:async\s*)?\{/g; + const funcBraces = new Set(); + while ((match = funcPattern.exec(textBefore)) !== null) { + funcBraces.add(match.index + match[0].length - 1); + } + + const ctorPattern = /\b\w+\s*\([^)]*\)\s*(?::\s*super\([^)]*\)\s*)?\{/g; + while ((match = ctorPattern.exec(textBefore)) !== null) { + const bracePos = match.index + match[0].length - 1; + if (!classBraces.has(bracePos)) { + funcBraces.add(bracePos); + } + } + + interface BraceFrame { + type: 'class' | 'function' | 'other'; + className?: string; + classStart?: number; + } + + const braceStack: BraceFrame[] = []; + let inString = false; + let stringChar = ''; + + for (let i = 0; i < textBefore.length; i++) { + const ch = textBefore[i]; + + if (inString) { + if (ch === '\\') { i++; continue; } + if (ch === stringChar) { inString = false; } + continue; + } + if (ch === "'" || ch === '"') { + inString = true; + stringChar = ch; + continue; + } + + if (ch === '/' && i + 1 < textBefore.length && textBefore[i + 1] === '/') { + i += 2; + while (i < textBefore.length && textBefore[i] !== '\n') { i++; } + continue; + } + + if (ch === '/' && i + 1 < textBefore.length && textBefore[i + 1] === '*') { + i += 2; + while (i + 1 < textBefore.length && + !(textBefore[i] === '*' && textBefore[i + 1] === '/')) { i++; } + i++; + continue; + } + + if (ch === '{') { + const classInfo = classBraces.get(i); + if (classInfo !== undefined) { + braceStack.push({ type: 'class', ...classInfo }); + } else if (funcBraces.has(i)) { + braceStack.push({ type: 'function' }); + } else { + braceStack.push({ type: 'other' }); + } + } else if (ch === '}') { + braceStack.pop(); + } + } + + const enclosingClass = [...braceStack].reverse() + .find(frame => frame.type === 'class' && + frame.className !== undefined && frame.classStart !== undefined); + if (enclosingClass?.className === undefined || enclosingClass.classStart === undefined) { + return undefined; + } + + let insertionLine = document.positionAt(enclosingClass.classStart).line; + while (insertionLine > 0 && document.lineAt(insertionLine - 1).text.trim().startsWith('@')) { + insertionLine--; + } + while (insertionLine > 0 && document.lineAt(insertionLine - 1).text.trim().startsWith('///')) { + insertionLine--; + } + + if (insertionLine > 0 && document.lineAt(insertionLine - 1).text.trim().endsWith('*/')) { + insertionLine--; + while (insertionLine > 0 && !document.lineAt(insertionLine).text.trim().startsWith('/**')) { + insertionLine--; + } + } + + return { + className: enclosingClass.className, + enumInsertionPosition: new vscode.Position(insertionLine, 0), + }; +} + +// --------------------------------------------------------------------------- +// Snippet definitions +// --------------------------------------------------------------------------- + +interface SnippetDef { + label: string; + prefixes: string[]; + body: string; + detail: string; + documentation: string; + sortOrder: string; +} + +// ---- ALWAYS scope ------------------------------------------------------- + +const ALWAYS_SNIPPETS: SnippetDef[] = [ + { + label: 'If (then/orElse)', + prefixes: ['If', 'if'], + body: [ + 'If(', + '\t${1:a}.gt(Const(0, width: ${1:a}.width)),', + '\tthen: [', + '\t\t${2:out} < ${1:a},', + '\t],', + '\torElse: [', + '\t\t${2:out} < ${3:b},', + '\t],', + '),', + ].join('\n'), + detail: 'If(cond, then: [...], orElse: [...])', + documentation: 'Inline conditional. Maps to if/else in SystemVerilog.', + sortOrder: '0a', + }, + { + label: 'If (then only)', + prefixes: ['If', 'if', 'ifthen'], + body: [ + 'If(', + '\t${1:a}.gt(Const(0, width: ${1:a}.width)),', + '\tthen: [', + '\t\t${2:out} < ${1:a},', + '\t],', + '),', + ].join('\n'), + detail: 'If(cond, then: [...])', + documentation: 'Simple conditional guard — no else branch.', + sortOrder: '0b', + }, + { + label: 'If nested (then/orElse chain)', + prefixes: ['If', 'if', 'ifnested', 'iforelse'], + body: [ + 'If(', + '\t${1:a}.gt(Const(0, width: ${1:a}.width)),', + '\tthen: [', + '\t\t${4:out} < ${1:a},', + '\t],', + '\torElse: [', + '\t\tIf(', + '\t\t\t${2:b}.gt(Const(0, width: ${2:b}.width)),', + '\t\t\tthen: [', + '\t\t\t\t${4:out} < ${2:b},', + '\t\t\t],', + '\t\t\torElse: [', + '\t\t\t\t${4:out} < Const(0, width: ${1:a}.width),', + '\t\t\t],', + '\t\t),', + '\t],', + '),', + ].join('\n'), + detail: 'If(a, ..., orElse: [If(b, ...)])', + documentation: 'Nested if / else-if / else chain using orElse nesting.', + sortOrder: '0c', + }, + { + label: 'If.block (Iff/ElseIf/Else)', + prefixes: ['If.block', 'ifblock', 'IfBlock'], + body: [ + 'If.block([', + '\tIff(${1:a}.gt(Const(0, width: ${1:a}.width)), [', + '\t\t${4:out} < ${1:a},', + '\t]),', + '\tElseIf(${2:b}.gt(Const(0, width: ${2:b}.width)), [', + '\t\t${4:out} < ${2:b},', + '\t]),', + '\tElse([', + '\t\t${4:out} < Const(0, width: ${1:a}.width),', + '\t]),', + ']),', + ].join('\n'), + detail: 'If.block([Iff(...), ElseIf(...), Else(...)])', + documentation: 'Flat if/else-if/else chain. First entry must be Iff (two f\'s).', + sortOrder: '0d', + }, + { + label: 'If.block (Iff/Else only)', + prefixes: ['If.block', 'ifblock', 'IfBlock'], + body: [ + 'If.block([', + '\tIff(${1:a}.gt(Const(0, width: ${1:a}.width)), [', + '\t\t${2:out} < ${1:a},', + '\t]),', + '\tElse([', + '\t\t${2:out} < Const(0, width: ${1:a}.width),', + '\t]),', + ']),', + ].join('\n'), + detail: 'If.block([Iff(...), Else(...)])', + documentation: 'Simple if/else using block style.', + sortOrder: '0e', + }, + { + label: 'If.block (from Iff)', + prefixes: ['Iff', 'iff'], + body: [ + 'If.block([', + '\tIff(${1:a}.gt(Const(0, width: ${1:a}.width)), [', + '\t\t${4:out} < ${1:a},', + '\t]),', + '\tElseIf(${2:b}.gt(Const(0, width: ${2:b}.width)), [', + '\t\t${4:out} < ${2:b},', + '\t]),', + '\tElse([', + '\t\t${4:out} < Const(0, width: ${1:a}.width),', + '\t]),', + ']),', + ].join('\n'), + detail: 'If.block([Iff(...), ElseIf(...), Else(...)])', + documentation: 'Complete if/elseif/else block. Bare Iff is only valid inside If.block, so this prefix expands to the full valid construct.', + sortOrder: '1a', + }, + { + label: 'Else', + prefixes: ['Else', 'else'], + body: 'Else([\n\t${1:out} < Const(0, width: ${2:a}.width),\n]),', + detail: 'Else([...])', + documentation: 'Final clause in an If.block chain.', + sortOrder: '1c', + }, + { + label: 'Case', + prefixes: ['Case', 'case'], + body: 'Case(${1:a}.gt(Const(0, width: ${1:a}.width)), [\n\tCaseItem(Const(0), [\n\t\t${5:out} < Const(0, width: ${1:a}.width),\n\t]),\n\tCaseItem(Const(1), [\n\t\t${5:out} < ${1:a},\n\t]),\n], defaultItem: [\n\t${5:out} < ${2:b},\n], conditionalType: ConditionalType.${4|none,unique,priority|}\n),', + detail: 'Case(expr, [CaseItem(...)], ...)', + documentation: 'Case statement — maps to case/unique case/priority case in SystemVerilog.', + sortOrder: '2a', + }, + { + label: 'CaseZ', + prefixes: ['CaseZ', 'caseZ', 'casez'], + body: 'CaseZ(${1:a}, [\n\tCaseItem(Const(LogicValue.ofString(\'${2:z1}\')), [\n\t\t${4:out} < ${1:a},\n\t]),\n\tCaseItem(Const(LogicValue.ofString(\'${3:10}\')), [\n\t\t${4:out} < ${5:b},\n\t]),\n], defaultItem: [\n\t${4:out} < Const(0, width: ${1:a}.width),\n], conditionalType: ConditionalType.${8|none,unique,priority|}\n),', + detail: 'CaseZ(expr, [CaseItem(...)])', + documentation: 'Like Case but with \'z\' don\'t-care matching.', + sortOrder: '2b', + }, + { + label: 'CaseItem', + prefixes: ['CaseItem', 'caseitem'], + body: 'CaseItem(${1:value}, [\n\t${2:out} < ${3:result},\n]),', + detail: 'CaseItem(value, [...])', + documentation: 'A single arm inside a Case or CaseZ.', + sortOrder: '2c', + }, + { + label: 'Conditional assign (<)', + prefixes: ['assign'], + body: '${1:out} < ${2:value},', + detail: 'out < value', + documentation: 'Conditional assignment using < operator.', + sortOrder: '3a', + }, +]; + +// ---- MODULE scope ------------------------------------------------------- + +const MODULE_SNIPPETS: SnippetDef[] = [ + { + label: 'Finite State Machine fallback (inside Module)', + prefixes: ['FSMCurrent', 'fsmCurrent'], + body: [ + '// Put this enum at file scope, outside this Module class:', + '// /// FSM states for this module.', + '// enum ${1:MyState} {', + '// /// Waiting for work.', + '// ${2:idle},', + '//', + '// /// Processing an active transaction.', + '// ${3:active},', + '//', + '// /// Completing the transaction.', + '// ${4:done},', + '// }', + 'final ${6:states} = [', + '\t// IDLE', + '\tState(', + '\t\t${1:MyState}.${2:idle},', + '\t\tevents: {', + '\t\t\t${7:a}.gt(Const(0, width: ${7:a}.width)): ${1:MyState}.${3:active},', + '\t\t},', + '\t\tactions: [', + '\t\t\t${8:out} < Const(0, width: ${7:a}.width),', + '\t\t],', + '\t),', + '\t// ACTIVE', + '\tState(', + '\t\t${1:MyState}.${3:active},', + '\t\tevents: {', + '\t\t\t${9:b}.gt(Const(0, width: ${9:b}.width)): ${1:MyState}.${4:done},', + '\t\t},', + '\t\tactions: [', + '\t\t\t${8:out} < ${7:a},', + '\t\t],', + '\t),', + '\t// DONE', + '\tState(', + '\t\t${1:MyState}.${4:done},', + '\t\tevents: {', + '\t\tConst(1): ${1:MyState}.${2:idle},', + '\t\t},', + '\t\tactions: [', + '\t\t\t${8:out} < ${9:b},', + '\t\t],', + '\t),', + '];', + 'FiniteStateMachine(${10:clk}, ${11:reset}, ${1:MyState}.${2:idle}, ${6:states});', + ].join('\n'), + detail: 'states + FiniteStateMachine for an existing Module', + documentation: 'Fallback FSM logic for use inside an existing Module. Prefer the context-aware fsm completion, which inserts the enum before the enclosing class.', + sortOrder: '0a', + }, + { + label: 'Pipeline', + prefixes: ['Pipeline', 'pipeline', 'PIpeline', 'Pipe', 'pipe'], + body: [ + 'final ${1:pipeline} = Pipeline(', + '\t${2:clk},', + '\tstages: [', + '\t\t(p) => [p.get(${3:a}) < p.get(${3:a}) + Const(1, width: ${3:a}.width)],', + '\t\t(p) => [p.get(${3:a}) < p.get(${3:a}) + Const(1, width: ${3:a}.width)],', + '\t],', + '\treset: ${4:reset},', + ');', + '${5:out} <= ${1:pipeline}.get(${3:a});', + ].join('\n'), + detail: 'Pipeline(clk, stages: [(p) => [...], ...])', + documentation: 'Pipelined logic — each stage is a `List Function(PipelineStageInfo p)`. Use `p.get(signal)` to access pipelined values. Stages use the same conditional syntax as Combinational.', + sortOrder: '0b', + }, + { + label: 'ReadyValidPipeline', + prefixes: ['ReadyValidPipeline', 'readyvalidpipeline', 'rvpipe', 'rvpipeline'], + body: [ + 'final ${3:validIn} = Logic(name: \'${3:validIn}\');', + 'final ${4:readyOut} = Logic(name: \'${4:readyOut}\');', + 'final ${8:validOut} = Logic(name: \'${8:validOut}\');', + 'final ${9:readyIn} = Logic(name: \'${9:readyIn}\');', + '', + 'final ${1:rvPipeline} = ReadyValidPipeline(', + '\t${2:clk},', + '\t${3:validIn},', + '\t${4:readyOut},', + '\tstages: [', + '\t\t(p) => [p.get(${5:a}) < p.get(${5:a}) + Const(1, width: ${5:a}.width)],', + '\t\t(p) => [p.get(${5:a}) < p.get(${5:a}) + Const(1, width: ${5:a}.width)],', + '\t],', + '\treset: ${6:reset},', + ');', + '${7:out} <= ${1:rvPipeline}.get(${5:a});', + '${8:validOut} <= ${1:rvPipeline}.validPipeOut;', + '${9:readyIn} <= ${1:rvPipeline}.readyPipeIn;', + ].join('\n'), + detail: 'ReadyValidPipeline(clk, stages: [...], valid, ready)', + documentation: 'Pipeline with ready/valid flow control. Same stage syntax as Pipeline, plus validPipeIn and readyPipeOut for backpressure.', + sortOrder: '0c', + }, + { + label: 'Sequential', + prefixes: ['Sequential', 'sequential', 'Seq', 'seq'], + body: 'Sequential(${1:clk}, [\n\tIf(${2:a}.gt(Const(0, width: ${2:a}.width)), then: [\n\t\t${4:out} < ${2:a},\n\t], orElse: [\n\t\t${4:out} < ${3:b},\n\t]),\n]);', + detail: 'Sequential(clk, [...])', + documentation: 'always_ff block triggered on clock edge.', + sortOrder: '0d', + }, + { + label: 'Combinational', + prefixes: ['Combinational', 'combinational', 'Comb', 'comb'], + body: 'Combinational([\n\t${1:out} < ${2:expression},\n]);', + detail: 'Combinational([...])', + documentation: 'always_comb block.', + sortOrder: '0e', + }, + { + label: 'Continuous assign (<=)', + prefixes: ['assign'], + body: '${1:out} <= ${2:a};', + detail: 'out <= a', + documentation: 'Continuous assignment outside Combinational/Sequential.', + sortOrder: '0f', + }, +]; + +// ---- FILE scope --------------------------------------------------------- + +const FILE_SNIPPETS: SnippetDef[] = [ + { + label: 'Finite State Machine (FSM)', + prefixes: ['FSM', 'fsm'], + body: [ + '/// FSM states for ${5:MyFSMModule}.', + 'enum ${1:MyState} {', + '\t/// Waiting for work.', + '\t${2:idle},', + '', + '\t/// Processing an active transaction.', + '\t${3:active},', + '', + '\t/// Completing the transaction.', + '\t${4:done},', + '}', + '', + 'class ${5:MyFSMModule} extends Module {', + '\t${5:MyFSMModule}(Logic clk, Logic reset, Logic ${6:input})', + '\t\t\t: super(name: \'${7:fsm_module}\') {', + '\t\tclk = addInput(\'clk\', clk);', + '\t\treset = addInput(\'reset\', reset);', + '\t\t${6:input} = addInput(\'${6:input}\', ${6:input});', + '', + '\t\tfinal ${8:output} = addOutput(\'${8:output}\');', + '', + '\t\tfinal states = [', + '\t\t\tState(${1:MyState}.${2:idle}, events: {', + '\t\t\t\t${6:input}: ${1:MyState}.${3:active},', + '\t\t\t}, actions: [', + '\t\t\t\t${8:output} < 0,', + '\t\t\t]),', + '\t\t\tState(${1:MyState}.${3:active}, events: {', + '\t\t\t\t${6:input}: ${1:MyState}.${4:done},', + '\t\t\t}, actions: [', + '\t\t\t\t${8:output} < 1,', + '\t\t\t]),', + '\t\t\tState(${1:MyState}.${4:done}, events: {', + '\t\t\t\tConst(1): ${1:MyState}.${2:idle},', + '\t\t\t}, actions: [', + '\t\t\t\t${8:output} < 0,', + '\t\t\t]),', + '\t\t];', + '', + '\t\tFiniteStateMachine(clk, reset, ${1:MyState}.${2:idle}, states);', + '\t}', + '}', + ].join('\n'), + detail: 'enum + class extends Module with FiniteStateMachine', + documentation: 'FSM scaffold — generates the enum and Module class at file scope.', + sortOrder: '0a', + }, + { + label: 'Module', + prefixes: ['Module', 'module', 'mod'], + body: [ + '/// A ROHD module with two data inputs and one output.', + 'class ${1:MyModule} extends Module {', + '\t/// The output of this module.', + '\tLogic get ${2:out} => output(\'${2:out}\');', + '', + '\t/// The clock input.', + '\tLogic get clk => input(\'clk\');', + '', + '\t/// The reset input.', + '\tLogic get reset => input(\'reset\');', + '', + '\t/// The configured data depth.', + '\tfinal int depth;', + '', + '\t/// Whether data should be latched.', + '\tfinal bool latchData;', + '', + '\t/// Constructs a ${1:MyModule}.', + '\t${1:MyModule}(', + '\t\tLogic ${3:clk},', + '\t\tLogic ${4:reset},', + '\t\tLogic ${5:a},', + '\t\tLogic ${6:b}, {', + '\t\tthis.depth = ${7:1},', + '\t\tthis.latchData = ${8:false},', + '\t\tString? definitionName,', + '\t\tsuper.name = \'${9:my_module}\',', + '\t\tsuper.reserveName,', + '\t\tsuper.reserveDefinitionName,', + '\t}) : super(definitionName: definitionName ?? \'${1:MyModule}\') {', + '\t\t// Register inputs and outputs of the module.', + '\t\t${3:clk} = addInput(\'clk\', ${3:clk});', + '\t\t${4:reset} = addInput(\'reset\', ${4:reset});', + '\t\t${5:a} = addInput(\'${5:a}\', ${5:a}, width: ${5:a}.width);', + '\t\t${6:b} = addInput(\'${6:b}\', ${6:b}, width: ${5:a}.width);', + '\t\tfinal ${2:out} = addOutput(\'${2:out}\', width: ${5:a}.width);', + '', + '\t\t${2:out} <= ${5:a};', + '\t}', + '}', + ].join('\n'), + detail: 'class MyModule extends Module { ... }', + documentation: 'ROHD Module scaffold with clk, reset, depth, latchData, addInput/addOutput, definitionName, and instance naming parameters.', + sortOrder: '0b', + }, + { + label: 'Interface (enum + setPorts + clone)', + prefixes: ['Interface', 'interface', 'intf'], + body: [ + '/// Port directions for ${4:MyInterface}.', + 'enum ${1:MyDirection} {', + '\t/// Ports entering the interface owner.', + '\t${2:inward},', + '', + '\t/// Ports leaving the interface owner.', + '\t${3:outward},', + '}', + '', + 'class ${4:MyInterface} extends Interface<${1:MyDirection}> {', + '\tLogic get ${5:dataIn} => port(\'${5:dataIn}\');', + '\tLogic get ${6:dataOut} => port(\'${6:dataOut}\');', + '\tLogic get ${7:clk} => port(\'${7:clk}\');', + '', + '\tfinal int ${8:width};', + '\t${4:MyInterface}({this.${8:width} = 8}) {', + '\t\tsetPorts([', + '\t\t\tLogic.port(\'${5:dataIn}\', ${8:width}),', + '\t\t\tLogic.port(\'${7:clk}\'),', + '\t\t], [', + '\t\t\t${1:MyDirection}.${2:inward},', + '\t\t]);', + '', + '\t\tsetPorts([', + '\t\t\tLogic.port(\'${6:dataOut}\', ${8:width}),', + '\t\t], [', + '\t\t\t${1:MyDirection}.${3:outward},', + '\t\t]);', + '\t}', + '', + '\t@override', + '\t${4:MyInterface} clone() => ${4:MyInterface}(${8:width}: ${8:width});', + '}', + ].join('\n'), + detail: 'enum + class extends Interface with clone()', + documentation: 'Classic ROHD Interface with direction enum, setPorts grouping, port getters, and clone(). Use with Module.addInterfacePorts(intf, inputTags: {...}, outputTags: {...}).', + sortOrder: '0c', + }, + { + label: 'PairInterface (provider/consumer)', + prefixes: ['PairInterface', 'pairinterface', 'pairintf'], + body: [ + 'class ${1:MyPairInterface} extends PairInterface {', + '\tLogic get ${2:clk} => port(\'${2:clk}\');', + '\tLogic get ${3:req} => port(\'${3:req}\');', + '\tLogic get ${4:rsp} => port(\'${4:rsp}\');', + '', + '\t${1:MyPairInterface}()', + '\t\t\t: super(', + '\t\t\t\t\tportsFromProvider: [Logic.port(\'${3:req}\')],', + '\t\t\t\t\tportsFromConsumer: [Logic.port(\'${4:rsp}\')],', + '\t\t\t\t\tsharedInputPorts: [Logic.port(\'${2:clk}\')],', + '\t\t\t\t);', + '', + '\t@override', + '\t${1:MyPairInterface} clone() => ${1:MyPairInterface}();', + '}', + ].join('\n'), + detail: 'class extends PairInterface { ... clone() }', + documentation: 'PairInterface with provider/consumer roles and shared inputs. Use with Module.addPairInterfacePorts(intf, PairRole.provider) or PairRole.consumer.', + sortOrder: '0d', + }, +]; + +// ---- TEST directory scope ----------------------------------------------- + +const TEST_SNIPPETS: SnippetDef[] = [ + { + label: 'test', + prefixes: ['test', 'Test'], + body: [ + 'test(\'${1:description}\', () async {', + '\texpect(${2:actual}, equals(${3:expected}));', + '});', + ].join('\n'), + detail: 'test(\'description\', () async { ... })', + documentation: 'Async package:test test case, common in ROHD and ROHD-HCL tests.', + sortOrder: '0a', + }, + { + label: 'group', + prefixes: ['group', 'Group'], + body: [ + 'group(\'${1:description}\', () {', + '\ttest(\'${2:case}\', () async {', + '\t\texpect(${3:actual}, equals(${4:expected}));', + '\t});', + '});', + ].join('\n'), + detail: 'group(\'description\', () { test(...) })', + documentation: 'Package:test group with an async test inside.', + sortOrder: '0b', + }, + { + label: 'ROHD tearDown reset', + prefixes: ['tearDown', 'teardown', 'resetTest'], + body: [ + 'tearDown(() async {', + '\tawait Simulator.reset();', + '});', + ].join('\n'), + detail: 'tearDown(() async { await Simulator.reset(); })', + documentation: 'Reset the ROHD simulator between tests.', + sortOrder: '0c', + }, + { + label: 'ROHD simulation test', + prefixes: ['rohdtest', 'simtest', 'testsim'], + body: [ + 'test(\'${1:module behavior}\', () async {', + '\tfinal ${2:clk} = SimpleClockGenerator(10).clk;', + '\tfinal ${3:reset} = Logic()..put(0);', + '', + '\tfinal ${4:dut} = ${5:MyModule}(${2:clk}, ${3:reset});', + '\tawait ${4:dut}.build();', + '', + '\tunawaited(Simulator.run());', + '', + '\t${3:reset}.put(1);', + '\tawait ${2:clk}.nextNegedge;', + '\tawait ${2:clk}.nextNegedge;', + '\t${3:reset}.put(0);', + '\tawait ${2:clk}.nextNegedge;', + '', + '\texpect(${4:dut}.${6:out}.value.toInt(), equals(${7:0}));', + '', + '\tawait Simulator.endSimulation();', + '});', + ].join('\n'), + detail: 'test(...) with SimpleClockGenerator, reset, build, Simulator.run()', + documentation: 'ROHD simulation test scaffold based on common ROHD-HCL tests. Requires dart:async, rohd, and package:test imports.', + sortOrder: '0d', + }, +]; + +// --------------------------------------------------------------------------- +// Build completion items +// --------------------------------------------------------------------------- + +function buildItems( + snippets: SnippetDef[], + range?: vscode.Range, + includeSnippet?: (snippet: SnippetDef) => boolean, +): vscode.CompletionItem[] { + const items: vscode.CompletionItem[] = []; + for (const snippet of snippets) { + if (includeSnippet !== undefined && !includeSnippet(snippet)) { + continue; + } + + for (const prefix of snippet.prefixes) { + const item = new vscode.CompletionItem( + `ROHD: ${snippet.label}`, + vscode.CompletionItemKind.Snippet, + ); + item.detail = `${snippet.label} — ${snippet.detail}`; + item.documentation = new vscode.MarkdownString(snippet.documentation); + item.filterText = prefix; + item.insertText = new vscode.SnippetString(snippet.body); + item.range = range; + item.sortText = `!${snippet.sortOrder}${prefix}`; + item.preselect = true; + items.push(item); + } + } + return items; +} + +const alwaysItems = buildItems(ALWAYS_SNIPPETS); +const moduleItems = buildItems(MODULE_SNIPPETS); +const fileItems = buildItems(FILE_SNIPPETS); +const testItems = buildItems(TEST_SNIPPETS); + +const COMPLETION_TRIGGER_CHARACTERS = Array.from(new Set([ + ...ALWAYS_SNIPPETS, + ...MODULE_SNIPPETS, + ...FILE_SNIPPETS, + ...TEST_SNIPPETS, +].flatMap(snippet => snippet.prefixes.map(prefix => prefix[0])))).concat('.'); + +function buildPipelineTypedItems( + range: vscode.Range, + readyValid: boolean, + typedPrefix: string, +): vscode.CompletionItem[] { + const snippet = MODULE_SNIPPETS.find(candidate => + candidate.label === (readyValid ? 'ReadyValidPipeline' : 'Pipeline'), + ); + if (snippet === undefined) { + return []; + } + + const item = new vscode.CompletionItem( + readyValid ? 'ROHD ReadyValidPipeline' : 'ROHD Pipeline', + vscode.CompletionItemKind.Snippet, + ); + item.detail = `ROHD ${snippet.label} — ${snippet.detail}`; + item.documentation = new vscode.MarkdownString(snippet.documentation); + item.filterText = typedPrefix; + item.insertText = new vscode.SnippetString(snippet.body); + item.range = range; + item.sortText = '0000_rohd_pipeline'; + item.preselect = true; + return [item]; +} + +function buildFsmInModuleItems( + enclosingClass: EnclosingClassInfo, +): vscode.CompletionItem[] { + const enumName = `${enclosingClass.className}State`; + const enumText = [ + `/// FSM states for ${enclosingClass.className}.`, + `enum ${enumName} {`, + ' /// Waiting for work.', + ' idle,', + '', + ' /// Processing an active transaction.', + ' active,', + '', + ' /// Completing the transaction.', + ' done,', + '}', + '', + ].join('\n'); + const body = [ + 'final ${2:states} = [', + '\t// IDLE', + '\tState(', + `\t\t${enumName}.idle,`, + '\t\tevents: {', + `\t\t\t${'${3:a}'}.gt(Const(0, width: ${'${3:a}'}.width)): ${enumName}.active,`, + '\t\t},', + '\t\tactions: [', + `\t\t\t${'${4:out}'} < Const(0, width: ${'${3:a}'}.width),`, + '\t\t],', + '\t),', + '\t// ACTIVE', + '\tState(', + `\t\t${enumName}.active,`, + '\t\tevents: {', + `\t\t\t${'${5:b}'}.gt(Const(0, width: ${'${5:b}'}.width)): ${enumName}.done,`, + '\t\t},', + '\t\tactions: [', + `\t\t\t${'${4:out}'} < ${'${3:a}'},`, + '\t\t],', + '\t),', + '\t// DONE', + '\tState(', + `\t\t${enumName}.done,`, + '\t\tevents: {', + `\t\t\tConst(1): ${enumName}.idle,`, + '\t\t},', + '\t\tactions: [', + `\t\t\t${'${4:out}'} < ${'${5:b}'},`, + '\t\t],', + '\t),', + '];', + `FiniteStateMachine(${'${6:clk}'}, ${'${7:reset}'}, ${enumName}.idle, ${'${2:states}'});`, + ].join('\n'); + + return ['FSM', 'fsm'].map(prefix => { + const item = new vscode.CompletionItem( + 'ROHD: Finite State Machine (inside Module)', + vscode.CompletionItemKind.Snippet, + ); + item.detail = 'Finite State Machine (inside Module) — states + FiniteStateMachine for an existing Module'; + item.documentation = new vscode.MarkdownString( + 'FSM logic for use inside an existing Module. The enum is inserted before the enclosing class declaration.', + ); + item.filterText = prefix; + item.insertText = new vscode.SnippetString(body); + item.additionalTextEdits = [ + vscode.TextEdit.insert(enclosingClass.enumInsertionPosition, enumText), + ]; + item.sortText = `!0a${prefix}`; + item.preselect = true; + return item; + }); +} + +function ifBlockTypedPrefixRange( + document: vscode.TextDocument, + position: vscode.Position, +): vscode.Range | undefined { + const linePrefix = document.lineAt(position.line).text.substring(0, position.character); + const match = /(?:If|if)\.(?:block)?$/.exec(linePrefix); + if (match === null) { + return undefined; + } + + return new vscode.Range( + new vscode.Position(position.line, position.character - match[0].length), + position, + ); +} + +function iffTypedPrefixRange( + document: vscode.TextDocument, + position: vscode.Position, +): vscode.Range | undefined { + const linePrefix = document.lineAt(position.line).text.substring(0, position.character); + const match = /(?:Iff|iff)$/.exec(linePrefix); + if (match === null) { + return undefined; + } + + return new vscode.Range( + new vscode.Position(position.line, position.character - match[0].length), + position, + ); +} + +function pipelineTypedPrefix( + document: vscode.TextDocument, + position: vscode.Position, +): { range: vscode.Range; readyValid: boolean; typedPrefix: string } | undefined { + const linePrefix = document.lineAt(position.line).text.substring(0, position.character); + const match = /(?:ReadyValidPipeline|readyvalidpipeline|rvpipeline|rvpipe|PIpeline|Pipeline|pipeline|Pipe|pipe)$/.exec(linePrefix); + if (match === null) { + return undefined; + } + + return { + range: new vscode.Range( + new vscode.Position(position.line, position.character - match[0].length), + position, + ), + readyValid: /^(?:ReadyValidPipeline|readyvalidpipeline|rvpipeline|rvpipe)$/.test(match[0]), + typedPrefix: match[0], + }; +} + +// --------------------------------------------------------------------------- +// Completion provider +// --------------------------------------------------------------------------- + +class RohdContextCompletionProvider + implements vscode.CompletionItemProvider +{ + provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + _token: vscode.CancellationToken, + _context: vscode.CompletionContext, + ): vscode.CompletionItem[] | undefined { + const scopes = detectContext(document, position); + const items: vscode.CompletionItem[] = []; + + const ifBlockRange = ifBlockTypedPrefixRange(document, position); + if (ifBlockRange !== undefined && scopes.has('always')) { + return buildItems( + ALWAYS_SNIPPETS, + ifBlockRange, + snippet => snippet.prefixes.some(prefix => + prefix === 'If.block' || prefix === 'ifblock' || prefix === 'IfBlock', + ), + ); + } + + const iffRange = iffTypedPrefixRange(document, position); + if (iffRange !== undefined && scopes.has('always')) { + return buildItems( + ALWAYS_SNIPPETS, + iffRange, + snippet => snippet.label === 'If.block (from Iff)', + ); + } + + const pipelinePrefix = pipelineTypedPrefix(document, position); + if (pipelinePrefix !== undefined && scopes.has('module')) { + return buildPipelineTypedItems( + pipelinePrefix.range, + pipelinePrefix.readyValid, + pipelinePrefix.typedPrefix, + ); + } + + if (scopes.has('always')) { items.push(...alwaysItems); } + if (scopes.has('module')) { + const enclosingClass = findEnclosingClassInfo(document, position); + if (enclosingClass !== undefined) { + items.push(...buildFsmInModuleItems(enclosingClass)); + } + items.push(...moduleItems.filter(item => item.label !== 'ROHD: Finite State Machine (inside Module)')); + } + if (scopes.has('file')) { items.push(...fileItems); } + if (scopes.has('test')) { items.push(...testItems); } + + return items.length > 0 ? items : undefined; + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +export function activate(_context: vscode.ExtensionContext): vscode.Disposable { + const provider = new RohdContextCompletionProvider(); + + return vscode.languages.registerCompletionItemProvider( + { language: 'dart', scheme: 'file' }, + provider, + ...COMPLETION_TRIGGER_CHARACTERS, + ); +} diff --git a/rohd_extension/src/debug_tracker.ts b/rohd_extension/src/debug_tracker.ts new file mode 100644 index 000000000..ae556d9e8 --- /dev/null +++ b/rohd_extension/src/debug_tracker.ts @@ -0,0 +1,384 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * debug_tracker.ts + * Debug Adapter Tracker for the ROHD VS Code extension. + * + * Registers with VS Code's debug infrastructure via + * `registerDebugAdapterTrackerFactory` to automatically intercept Dart + * debug sessions. Extracts the VM Service URI from DAP messages and + * obtains the DTD URI from the Dart extension API — no manual + * configuration required. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; +import * as dtdBridge from './dtd_bridge'; + +const output = vscode.window.createOutputChannel('ROHD Debug Tracker'); + +// --------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------- + +/** Per-session tracking data. */ +interface SessionInfo { + vmServiceUri?: string; + vmServiceForwardedUri?: string; + dtdUri?: string; + dtdForwardedUri?: string; +} + +const sessions = new Map(); + +/** The DTD URI obtained from the Dart extension API. */ +let dartExtDtdUri: string | undefined; +let dartExtDtdForwardedUri: string | undefined; + +// --------------------------------------------------------------------------- +// URI helpers (shared logic with uri_forwarder.ts) +// --------------------------------------------------------------------------- + +async function resolveForwardedUri( + rawWsUri: string, +): Promise { + try { + const httpUri = rawWsUri + .replace(/^ws:\/\//, 'http://') + .replace(/^wss:\/\//, 'https://'); + + const parsed = vscode.Uri.parse(httpUri); + const resolved = await vscode.env.asExternalUri(parsed); + + const scheme = resolved.scheme === 'https' ? 'wss' : 'ws'; + const forwarded = `${scheme}://${resolved.authority}${parsed.path}`; + + if (resolved.authority !== parsed.authority) { + return forwarded; + } + return undefined; + } catch { + return undefined; + } +} + +function normalizeWsUri(uri: string, ensureWsSuffix: boolean): string { + let u = uri; + if (u.startsWith('http://')) { + u = u.replace('http://', 'ws://'); + } else if (u.startsWith('https://')) { + u = u.replace('https://', 'wss://'); + } + if (ensureWsSuffix && !u.endsWith('/ws')) { + u = u.replace(/\/?$/, '/ws'); + } + return u; +} + +// --------------------------------------------------------------------------- +// Dart extension DTD API +// --------------------------------------------------------------------------- + +interface DartExtensionApi { + dtdUri?: string; + onDtdUriChanged?: ( + listener: (uri: string | undefined) => void, + thisArgs?: unknown, + disposables?: vscode.Disposable[], + ) => vscode.Disposable; +} + +function watchDtdFromDartExtension(context: vscode.ExtensionContext): void { + const dartExt = vscode.extensions.getExtension('dart-code.dart-code'); + if (!dartExt) { + output.appendLine('[DTD] Dart extension not found — will rely on DAP events only.'); + return; + } + + async function processDtd(rawUri: string): Promise { + const wsUri = normalizeWsUri(rawUri, false); + dartExtDtdUri = wsUri; + output.appendLine(`[DTD] From Dart extension API: ${wsUri}`); + + const forwarded = await resolveForwardedUri(wsUri); + dartExtDtdForwardedUri = forwarded; + if (forwarded) { + output.appendLine(`[DTD] Forwarded: ${forwarded}`); + } + + // Feed the original (non-forwarded) DTD URI to the bridge. + // The bridge runs on the same host as the DTD daemon, so it uses + // the local port. The forwarded port is only for remote clients. + dtdBridge.connectIfNeeded(wsUri).catch((err) => { + output.appendLine(`[DTD] Bridge connect failed: ${err}`); + }); + } + + function handleApi(api: DartExtensionApi): void { + if (api.dtdUri) { + processDtd(api.dtdUri).catch((err) => { + output.appendLine(`[DTD] Error processing DTD URI: ${err}`); + }); + } + if (api.onDtdUriChanged) { + const disposable = api.onDtdUriChanged((uri) => { + if (uri) { + processDtd(uri).catch((err) => { + output.appendLine(`[DTD] Error on DTD URI change: ${err}`); + }); + } + }); + if (disposable) { + context.subscriptions.push(disposable); + } + } + } + + if (dartExt.isActive) { + handleApi(dartExt.exports as DartExtensionApi); + } else { + dartExt.activate().then( + (api) => handleApi(api as DartExtensionApi), + (err) => output.appendLine(`[DTD] Dart extension activation failed: ${err}`), + ); + } +} + +// --------------------------------------------------------------------------- +// Banner output +// --------------------------------------------------------------------------- + +const EXTENSION_VERSION = '0.1.0'; + +async function printSessionBanner( + session: vscode.DebugSession, + info: SessionInfo, +): Promise { + const lines: string[] = []; + + lines.push(`ROHD ${EXTENSION_VERSION}: Extension loaded for FLC crossprobing.`); + lines.push(''); + + // DTD: prefer session-specific, fall back to Dart extension API + const dtdOrig = info.dtdUri ?? dartExtDtdUri; + const dtdFwd = info.dtdForwardedUri ?? dartExtDtdForwardedUri; + if (dtdOrig) { + lines.push('DTD:'); + lines.push(` URI: ${dtdOrig}`); + if (dtdFwd) { + lines.push(` Fwd: ${dtdFwd}`); + } + } + + if (info.vmServiceUri) { + lines.push('VM:'); + lines.push(` URI: ${info.vmServiceUri}`); + if (info.vmServiceForwardedUri) { + lines.push(` Fwd: ${info.vmServiceForwardedUri}`); + } + } + + const banner = '═'.repeat(60); + const block = [banner, ...lines, banner].join('\n'); + + // Debug console + vscode.debug.activeDebugConsole.appendLine(''); + vscode.debug.activeDebugConsole.appendLine(block); + + // Output channel (persists across sessions) + output.appendLine(''); + output.appendLine(block); + + // Popup with DTD URI for quick copy + const popupUri = dtdFwd ?? dtdOrig; + if (popupUri) { + const action = await vscode.window.showInformationMessage( + `ROHD DTD: ${popupUri}`, + 'Copy', + ); + if (action === 'Copy') { + await vscode.env.clipboard.writeText(popupUri); + vscode.window.showInformationMessage('DTD URI copied to clipboard.'); + } + } +} + +// --------------------------------------------------------------------------- +// Debug Adapter Tracker +// --------------------------------------------------------------------------- + +class RohdDebugAdapterTracker implements vscode.DebugAdapterTracker { + private readonly session: vscode.DebugSession; + + constructor(session: vscode.DebugSession) { + this.session = session; + } + + onWillStartSession(): void { + output.appendLine( + `[Tracker] Debug session starting: "${this.session.name}" (${this.session.id})`, + ); + sessions.set(this.session.id, {}); + } + + onDidSendMessage(message: unknown): void { + // The Dart debug adapter sends an "event" message with + // event === "dart.debuggerUris" containing the VM Service URI. + const msg = message as Record; + if (msg.type !== 'event') { return; } + + if (msg.event === 'dart.debuggerUris') { + const body = msg.body as Record | undefined; + const vmUri = body?.vmServiceUri as string | undefined; + if (vmUri) { + this.processVmServiceUri(vmUri); + } + } + } + + onWillStopSession(): void { + output.appendLine( + `[Tracker] Debug session ending: "${this.session.name}" (${this.session.id})`, + ); + sessions.delete(this.session.id); + } + + onError(error: Error): void { + output.appendLine(`[Tracker] Error in session "${this.session.name}": ${error.message}`); + } + + private async processVmServiceUri(rawUri: string): Promise { + const vmUri = normalizeWsUri(rawUri, true); + const info = sessions.get(this.session.id) ?? {}; + info.vmServiceUri = vmUri; + + output.appendLine(`[Tracker] VM Service URI: ${vmUri}`); + + const forwarded = await resolveForwardedUri(vmUri); + info.vmServiceForwardedUri = forwarded; + if (forwarded) { + output.appendLine(`[Tracker] VM Service Forwarded: ${forwarded}`); + } + + // Copy DTD info from Dart extension API into session info + if (dartExtDtdUri) { + info.dtdUri = dartExtDtdUri; + info.dtdForwardedUri = dartExtDtdForwardedUri; + + // Ensure the DTD bridge is connected now that we have a session. + // Use the original (non-forwarded) URI — bridge is local. + dtdBridge.connectIfNeeded(dartExtDtdUri).catch((err) => { + output.appendLine(`[Tracker] Bridge connect failed: ${err}`); + }); + } + + sessions.set(this.session.id, info); + await printSessionBanner(this.session, info); + } +} + +class RohdDebugAdapterTrackerFactory implements vscode.DebugAdapterTrackerFactory { + createDebugAdapterTracker( + session: vscode.DebugSession, + ): vscode.ProviderResult { + return new RohdDebugAdapterTracker(session); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** Get session info for the active debug session. */ +export function getActiveSessionInfo(): SessionInfo | undefined { + const session = vscode.debug.activeDebugSession; + if (!session) { return undefined; } + return sessions.get(session.id); +} + +/** Get the DTD URI (forwarded preferred, original fallback). */ +export function getDtdUri(): string | undefined { + const info = getActiveSessionInfo(); + return info?.dtdForwardedUri ?? info?.dtdUri ?? dartExtDtdForwardedUri ?? dartExtDtdUri; +} + +/** Get the VM Service URI (forwarded preferred, original fallback). */ +export function getVmServiceUri(): string | undefined { + const info = getActiveSessionInfo(); + return info?.vmServiceForwardedUri ?? info?.vmServiceUri; +} + +export function activate(context: vscode.ExtensionContext): void { + output.appendLine('[Debug Tracker] Activating...'); + + // Watch for DTD URI from the Dart extension API + watchDtdFromDartExtension(context); + + // Register the tracker factory for Dart debug sessions + context.subscriptions.push( + vscode.debug.registerDebugAdapterTrackerFactory( + 'dart', + new RohdDebugAdapterTrackerFactory(), + ), + ); + + // Also register for generic debug types in case Dart sessions + // use a different type identifier + context.subscriptions.push( + vscode.debug.registerDebugAdapterTrackerFactory( + '*', + { + createDebugAdapterTracker(session: vscode.DebugSession) { + // Only track Dart-related sessions + if (session.type === 'dart' || session.type === 'flutter') { + // Already tracked by the 'dart' factory above; skip duplication. + return undefined; + } + return undefined; + }, + }, + ), + ); + + // Command to show forwarded URIs for the active session + context.subscriptions.push( + vscode.commands.registerCommand('rohd.showForwardedUris', () => { + const info = getActiveSessionInfo(); + if (!info) { + vscode.window.showWarningMessage('No active Dart debug session.'); + return; + } + + const lines: string[] = []; + if (info.dtdUri) { + lines.push(`DTD: ${info.dtdUri}`); + if (info.dtdForwardedUri) { lines.push(`DTD Fwd: ${info.dtdForwardedUri}`); } + } else if (dartExtDtdUri) { + lines.push(`DTD: ${dartExtDtdUri}`); + if (dartExtDtdForwardedUri) { lines.push(`DTD Fwd: ${dartExtDtdForwardedUri}`); } + } + if (info.vmServiceUri) { + lines.push(`VM: ${info.vmServiceUri}`); + if (info.vmServiceForwardedUri) { lines.push(`VM Fwd: ${info.vmServiceForwardedUri}`); } + } + + if (lines.length === 0) { + vscode.window.showWarningMessage('URIs not yet available. Start a debug session first.'); + return; + } + + output.appendLine(lines.join('\n')); + output.show(true); + }), + ); + + output.appendLine('[Debug Tracker] Activated — tracking Dart debug sessions.'); +} + +export function deactivate(): void { + sessions.clear(); + dartExtDtdUri = undefined; + dartExtDtdForwardedUri = undefined; +} diff --git a/rohd_extension/src/dtd_bridge.ts b/rohd_extension/src/dtd_bridge.ts new file mode 100644 index 000000000..52f70bbef --- /dev/null +++ b/rohd_extension/src/dtd_bridge.ts @@ -0,0 +1,439 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * dtd_bridge.ts + * DTD (Dart Tooling Daemon) bridge for receiving cross-probe source + * navigation requests from the ROHD DevTools extension. + * + * Registers a `rohd.goToSource` service method on the DTD so that the + * DevTools extension can send resolved SourceFrame lists for navigation. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; +import { openSourceLocations, resolveFrames } from './source_navigator'; +import * as flcService from './flc_service'; + +const output = vscode.window.createOutputChannel('ROHD DTD Bridge'); + +// --------------------------------------------------------------------------- +// Minimal JSON-RPC 2.0 server over WebSocket +// --------------------------------------------------------------------------- + +let ws: import('ws').WebSocket | undefined; +let nextId = 1; +const pendingRequests = new Map void; + reject: (error: Error) => void; +}>(); + +type RpcHandler = (params: Record) => Promise; +const methods = new Map(); + +const rohdDtdBridgeCapabilities = { + rohdDtdBridge: 1, + owner: 'rohd-vscode-extension', +}; + +function dtdResult(result: Record): Record { + return { type: 'Success', ...result }; +} + +function sendJsonRpc(data: Record): void { + if (ws?.readyState === 1 /* OPEN */) { + ws.send(JSON.stringify(data)); + } +} + +function handleMessage(raw: string): void { + let msg: Record; + try { + msg = JSON.parse(raw); + } catch { + return; + } + + // Response to a request we sent. + if ('id' in msg && ('result' in msg || 'error' in msg)) { + const id = msg.id as number; + const pending = pendingRequests.get(id); + if (pending) { + pendingRequests.delete(id); + if ('error' in msg) { + pending.reject(new Error(JSON.stringify(msg.error))); + } else { + pending.resolve(msg.result); + } + } + return; + } + + // Incoming request or notification. + if ('method' in msg) { + const method = msg.method as string; + const params = (msg.params ?? {}) as Record; + const handler = methods.get(method); + + if (handler && 'id' in msg) { + // Request — send response. + handler(params) + .then(result => sendJsonRpc({ jsonrpc: '2.0', id: msg.id, result })) + .catch(err => + sendJsonRpc({ + jsonrpc: '2.0', + id: msg.id, + error: { code: -32000, message: String(err) }, + }), + ); + } else if (handler) { + // Notification — fire and forget. + handler(params).catch(() => {}); + } + } +} + +/** Send a JSON-RPC request and wait for the response. */ +async function rpcRequest( + method: string, + params?: Record, +): Promise { + return new Promise((resolve, reject) => { + const id = nextId++; + pendingRequests.set(id, { resolve, reject }); + sendJsonRpc({ jsonrpc: '2.0', id, method, params: params ?? {} }); + + // Timeout after 10 seconds. + setTimeout(() => { + if (pendingRequests.has(id)) { + pendingRequests.delete(id); + reject(new Error(`RPC timeout: ${method}`)); + } + }, 10000); + }); +} + +// --------------------------------------------------------------------------- +// Service registration +// --------------------------------------------------------------------------- + +/** + * Register the `rohd.goToSource` handler. + * + * When the DevTools extension calls this service via DTD, the handler + * parses the SourceFrame list and delegates to the existing + * `openSourceLocations` command. + */ +function registerGoToSourceHandler(): void { + methods.set('rohd.goToSource', async (params) => { + const framesRaw = params.frames; + if (!Array.isArray(framesRaw) || framesRaw.length === 0) { + return dtdResult({ status: 'error', message: 'No frames provided' }); + } + + const index = typeof params.index === 'number' ? params.index : 0; + + output.appendLine( + `[DTD] goToSource: ${framesRaw.length} frame(s), index=${index}`, + ); + + // Delegate to the existing source navigator. + await openSourceLocations({ frames: framesRaw, index }); + return dtdResult({ status: 'ok', navigated: framesRaw.length }); + }); + + methods.set('rohd.resolveFrames', async (params) => { + const framesRaw = params.frames; + if (!Array.isArray(framesRaw) || framesRaw.length === 0) { + return dtdResult({ status: 'error', message: 'No frames provided' }); + } + + output.appendLine( + `[DTD] resolveFrames: ${framesRaw.length} frame(s)`, + ); + + const enriched = await resolveFrames(framesRaw); + return dtdResult({ status: 'ok', frames: enriched }); + }); + + // Query which source formats are available for a module. + // params: { flcPath: string, module: string | null } + methods.set('rohd.queryModule', async (params) => { + const flcPath = params.flcPath as string | undefined; + const moduleName = (params.module as string | null) ?? null; + + if (!flcPath) { + return dtdResult({ status: 'error', message: 'flcPath is required' }); + } + + output.appendLine(`[DTD] queryModule: module=${moduleName ?? '(any)'}, flcPath=${flcPath}`); + const info = flcService.queryModule(flcPath, moduleName); + return dtdResult({ status: 'ok', ...info }); + }); + + // Look up signal source frames from a persisted .flc.json file. + // params: { flcPath: string, module: string | null, signal: string, format?: string } + methods.set('rohd.lookupSignal', async (params) => { + const flcPath = params.flcPath as string | undefined; + const moduleName = (params.module as string | null) ?? null; + const signalName = params.signal as string | undefined; + const format = params.format as string | undefined; + + if (!flcPath || !signalName) { + return dtdResult({ status: 'error', message: 'flcPath and signal are required' }); + } + + output.appendLine( + `[DTD] lookupSignal: signal=${signalName}, module=${moduleName ?? '(any)'}, format=${format ?? 'all'}`, + ); + const frames = flcService.lookupSignal(flcPath, moduleName, signalName, format); + return dtdResult({ status: 'ok', frames }); + }); +} + +// --------------------------------------------------------------------------- +// DTD connection lifecycle +// --------------------------------------------------------------------------- + +let reconnectTimer: ReturnType | undefined; +let reconnectAttempts = 0; +let dtdUri: string | undefined; + +// In-flight connection guard. `connectToDtd` is async (it awaits the dynamic +// `ws` import before assigning `ws`), so two near-simultaneous callers — e.g. +// activation-time discovery and the debug tracker — can both pass the +// `isConnected()` check (which is only OPEN, never CONNECTING) and open a +// second socket to the same URI. The second socket then fails registration +// with "Service already registered by another client." This flag is set +// synchronously at the top of `connectToDtd` to close that window. +let connecting = false; +let connectingUri: string | undefined; + +/** + * Connect to the DTD and register services. + * + * @param uri WebSocket URI of the Dart Tooling Daemon. + */ +async function connectToDtd(uri: string): Promise { + // Synchronous dedupe: ignore a second connect to a URI we are already + // connecting to or connected to. + if (connecting && connectingUri === uri) { + output.appendLine(`[DTD] Connect already in progress for ${uri}; skipping`); + return false; + } + if (isConnected() && dtdUri === uri) { + return true; + } + + connecting = true; + connectingUri = uri; + dtdUri = uri; + + try { + // Dynamic import — ws is a Node.js dependency. + const WebSocket = (await import('ws')).default; + + ws = new WebSocket(uri); + + return new Promise((resolve) => { + ws!.on('open', () => { + output.appendLine(`[DTD] Connected to ${uri}`); + reconnectAttempts = 0; + connecting = false; + connectingUri = undefined; + + registerGoToSourceHandler(); + + // Register ourselves as a service on the DTD. + rpcRequest('registerService', { + service: 'rohd', + method: 'goToSource', + capabilities: rohdDtdBridgeCapabilities, + }).then(() => { + output.appendLine('[DTD] Registered rohd.goToSource service'); + }).catch((err) => { + output.appendLine(`[DTD] Service registration note: ${err}`); + }); + + rpcRequest('registerService', { + service: 'rohd', + method: 'resolveFrames', + capabilities: rohdDtdBridgeCapabilities, + }).then(() => { + output.appendLine('[DTD] Registered rohd.resolveFrames service'); + }).catch((err) => { + output.appendLine(`[DTD] resolveFrames registration note: ${err}`); + }); + + rpcRequest('registerService', { + service: 'rohd', + method: 'queryModule', + capabilities: rohdDtdBridgeCapabilities, + }).then(() => { + output.appendLine('[DTD] Registered rohd.queryModule service'); + }).catch((err) => { + output.appendLine(`[DTD] queryModule registration note: ${err}`); + }); + + rpcRequest('registerService', { + service: 'rohd', + method: 'lookupSignal', + capabilities: rohdDtdBridgeCapabilities, + }).then(() => { + output.appendLine('[DTD] Registered rohd.lookupSignal service'); + }).catch((err) => { + output.appendLine(`[DTD] lookupSignal registration note: ${err}`); + }); + + resolve(true); + }); + + ws!.on('message', (data: Buffer | string) => { + handleMessage(data.toString()); + }); + + ws!.on('close', () => { + output.appendLine('[DTD] Connection closed'); + ws = undefined; + scheduleReconnect(); + }); + + ws!.on('error', (err: Error) => { + output.appendLine(`[DTD] Connection error: ${err.message}`); + ws = undefined; + connecting = false; + connectingUri = undefined; + resolve(false); + }); + }); + } catch (err) { + output.appendLine(`[DTD] Failed to connect: ${err}`); + connecting = false; + connectingUri = undefined; + return false; + } +} + +function scheduleReconnect(): void { + if (!dtdUri || reconnectTimer) return; + + // Exponential backoff: 2s, 4s, 8s, 16s, 32s max. + const delay = Math.min(2000 * 2 ** reconnectAttempts, 32000); + reconnectAttempts++; + + output.appendLine(`[DTD] Reconnecting in ${delay / 1000}s...`); + reconnectTimer = setTimeout(async () => { + reconnectTimer = undefined; + if (dtdUri) { + await connectToDtd(dtdUri); + } + }, delay); +} + +// --------------------------------------------------------------------------- +// DTD URI discovery +// --------------------------------------------------------------------------- + +/** + * Try to discover the DTD URI from known sources. + * + * Order of precedence: + * 1. `DART_TOOLING_DAEMON_URI` environment variable + * 2. VS Code setting `rohd.dtdUri` (for manual override) + * + * Note: The Dart extension API DTD URI is discovered asynchronously by + * debug_tracker.ts and fed to connectIfNeeded() when available. + */ +function discoverDtdUri(): string | undefined { + // Environment variable (set by IDE or debug launcher). + const envUri = process.env.DART_TOOLING_DAEMON_URI; + if (envUri) { + output.appendLine(`[DTD] URI from env: ${envUri}`); + return envUri; + } + + // VS Code setting. + const config = vscode.workspace.getConfiguration('rohd'); + const configUri = config.get('dtdUri'); + if (configUri) { + output.appendLine(`[DTD] URI from setting: ${configUri}`); + return configUri; + } + + output.appendLine('[DTD] No DTD URI discovered (debug_tracker will push later)'); + return undefined; +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** Whether the DTD bridge is connected. */ +export function isConnected(): boolean { + return ws?.readyState === 1; +} + +/** + * Connect to the DTD if not already connected. + * Called by debug_tracker when the DTD URI is discovered from the Dart + * extension API (which resolves after activation). + */ +export async function connectIfNeeded(uri: string): Promise { + if (isConnected()) { + return; // Already connected — nothing to do. + } + if (connecting && connectingUri === uri) { + return; // A connection to this URI is already in flight. + } + output.appendLine(`[DTD] Connecting via debug tracker: ${uri}`); + await connectToDtd(uri); +} + +/** + * Activate the DTD bridge. + * + * Discovers the DTD URI and connects. If the URI is not available + * at activation time, the bridge remains dormant and can be connected + * later via the `rohd.connectDtd` command. + */ +export async function activate(context: vscode.ExtensionContext): Promise { + // Register a command for manual DTD connection. + context.subscriptions.push( + vscode.commands.registerCommand('rohd.connectDtd', async () => { + const uri = await vscode.window.showInputBox({ + prompt: 'Enter the Dart Tooling Daemon WebSocket URI', + placeHolder: 'ws://127.0.0.1:...', + value: dtdUri ?? '', + }); + if (uri) { + await connectToDtd(uri); + } + }), + ); + + // Try automatic discovery. + const uri = discoverDtdUri(); + if (uri) { + await connectToDtd(uri); + } +} + +/** Clean up the DTD bridge. */ +export async function dispose(): Promise { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = undefined; + } + dtdUri = undefined; + connecting = false; + connectingUri = undefined; + + if (ws) { + ws.close(); + ws = undefined; + } + + pendingRequests.clear(); + methods.clear(); +} diff --git a/rohd_extension/src/extension.ts b/rohd_extension/src/extension.ts new file mode 100644 index 000000000..53a762890 --- /dev/null +++ b/rohd_extension/src/extension.ts @@ -0,0 +1,113 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * extension.ts + * Main entry point for the ROHD VS Code extension. + * + * Provides: + * - ROHD Dart code snippets (carried forward from v0.0.5) + * - Cross-probe source navigation commands for ROHD viewer extensions + * + * Original snippets by: Yao Jing Quek + * Cross-probe navigation by: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; +import * as sourceNavigator from './source_navigator'; +import * as dtdBridge from './dtd_bridge'; +import * as debugTracker from './debug_tracker'; +import * as conditionalCompletions from './conditional_completions'; +import * as flcService from './flc_service'; + +export function activate(context: vscode.ExtensionContext): void { + console.log('ROHD extension is now active (v0.1.0)'); + + // Initialise the FLC service with the extension path so it can locate + // the compiled flc_lookup binary. + flcService.initialize(context.extensionPath); + + // Register cross-probe → editor navigation commands. + // These are invoked by ROHD viewer extensions (schematic, wave) via + // vscode.commands.executeCommand('rohd.openSourceLocation', ...). + sourceNavigator.registerCommands(context); + + // ── FLC commands — viewer extensions delegate here ────────────────────── + + // rohd.queryModule: resolve available source formats for a module. + // Args: { flcPath: string, module: string | null } + // Returns: ModuleInfo (extensionAvailable, module, formats, ...) + context.subscriptions.push( + vscode.commands.registerCommand( + 'rohd.queryModule', + (args: { flcPath: string; module: string | null }) => + flcService.queryModule(args.flcPath, args.module), + ), + ); + + // rohd.lookupSignal: resolve source frames for a signal. + // Args: { flcPath: string, module: string | null, signal: string, format?: string } + // Returns: SourceFrame[] + context.subscriptions.push( + vscode.commands.registerCommand( + 'rohd.lookupSignal', + (args: { flcPath: string; module: string | null; signal: string; format?: string }) => + flcService.lookupSignal(args.flcPath, args.module, args.signal, args.format), + ), + ); + + // rohd.resolveFlcPath: find the .flc.json sidecar for a document path. + // Args: { documentFsPath: string } + // Returns: string | null + context.subscriptions.push( + vscode.commands.registerCommand( + 'rohd.resolveFlcPath', + (args: { documentFsPath: string }) => + flcService.resolveFlcPath(args.documentFsPath), + ), + ); + + // Activate the DTD bridge for receiving cross-probe requests from + // the ROHD DevTools extension running in a remote iframe. + dtdBridge.activate(context); + + // Register with the debug adapter to automatically intercept Dart + // debug sessions, capture VM Service + DTD URIs, and print the + // consolidated banner. Replaces the old passive uri_forwarder. + debugTracker.activate(context); + + // Register context-aware ROHD completions if the user has opted in. + activateCompletionsIfEnabled(context); + + // Re-check when the setting changes at runtime. + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('rohd.enableCompletions')) { + activateCompletionsIfEnabled(context); + } + }), + ); +} + +let completionsDisposable: vscode.Disposable | undefined; + +async function activateCompletionsIfEnabled( + context: vscode.ExtensionContext, +): Promise { + const config = vscode.workspace.getConfiguration('rohd'); + const enabled = config.get('enableCompletions', true); + + if (enabled && !completionsDisposable) { + completionsDisposable = conditionalCompletions.activate(context); + context.subscriptions.push(completionsDisposable); + } else if (!enabled && completionsDisposable) { + completionsDisposable.dispose(); + completionsDisposable = undefined; + } +} + +export function deactivate(): void { + debugTracker.deactivate(); + dtdBridge.dispose(); + sourceNavigator.dispose(); +} diff --git a/rohd_extension/src/flc_service.ts b/rohd_extension/src/flc_service.ts new file mode 100644 index 000000000..ff46245ea --- /dev/null +++ b/rohd_extension/src/flc_service.ts @@ -0,0 +1,515 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * flc_service.ts + * Centralised FLC (File-Location Cache) service for rohd_extension. + * + * Owns: + * - Resolving the .flc.json sidecar path from any document path + * - Querying available source formats (rohd, sv, …) for a module + * - Looking up signal/instance source frames from v5/v6 FLC JSON + * + * All FLC parsing logic that was previously duplicated across + * rohd-schematic-viewer/extension.js and rohd-wave-viewer/extension.ts + * now lives here. Those extensions delegate via VS Code commands: + * rohd.queryModule – returns ModuleInfo + * rohd.lookupSignal – returns SourceFrame[] + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +const output = vscode.window.createOutputChannel('ROHD FLC'); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface FormatInfo { + available: boolean; + fileFound: boolean; + path: string | null; +} + +export interface ModuleFormats { + rohd?: FormatInfo; + sv?: FormatInfo; + [key: string]: FormatInfo | undefined; +} + +export interface ModuleInfo { + extensionAvailable: boolean; + module: string | null; + formats: ModuleFormats; + error?: string; + fstLoading: boolean; +} + +export interface SourceFrame { + file: string; + line: number; + col: number; + desc?: string; + type: string; +} + +interface FlcOutputPos { + type: string; + line: number; + col: number; +} + +interface FlcSymbolInfo { + name: string; + isInstance: boolean; + outputPositions: FlcOutputPos[]; + origName: string | null; +} + +// --------------------------------------------------------------------------- +// Initialisation +// --------------------------------------------------------------------------- + +let _extensionPath: string | undefined; + +/** Must be called from extension activate() before any other API. */ +export function initialize(extensionPath: string): void { + _extensionPath = extensionPath; + output.appendLine('[FlcService] Initialised. extensionPath=' + extensionPath); +} + +// --------------------------------------------------------------------------- +// Sidecar resolution +// --------------------------------------------------------------------------- + +/** + * Resolve the .flc.json sidecar for any document path. + * + * Conventions supported: + * Foo.rohd.json → Foo.flc.json (schematic viewer) + * Foo.vcd → Foo.flc.json (wave viewer) + * Foo.fst → Foo.flc.json + * Foo.ghw → Foo.flc.json + * + * Returns the absolute path if the sidecar exists, otherwise null. + */ +export function resolveFlcPath(documentFsPath: string): string | null { + const dir = path.dirname(documentFsPath); + const base = path.basename(documentFsPath); + + // .rohd.json → .flc.json + const fromRohdJson = base.replace(/\.rohd\.json$/i, '.flc.json'); + if (fromRohdJson !== base) { + const p = path.join(dir, fromRohdJson); + return fs.existsSync(p) ? p : null; + } + + // .vcd / .fst / .ghw → .flc.json + const fromWave = base.replace(/\.(vcd|fst|ghw)$/i, '.flc.json'); + if (fromWave !== base) { + const p = path.join(dir, fromWave); + return fs.existsSync(p) ? p : null; + } + + return null; +} + +// --------------------------------------------------------------------------- +// Module format query +// --------------------------------------------------------------------------- + +/** + * Return which source formats are available for [moduleName] in [flcPath]. + * + * Reads and parses the FLC JSON directly (no subprocess needed for metadata). + * Performs case-insensitive module name matching. + */ +export function queryModule(flcPath: string, moduleName: string | null): ModuleInfo { + if (!fs.existsSync(flcPath)) { + return { + extensionAvailable: true, + module: moduleName, + formats: {}, + error: 'FLC file not found: ' + flcPath, + fstLoading: false, + }; + } + + try { + const raw = fs.readFileSync(flcPath, 'utf8'); + const flcJson = JSON.parse(raw) as Record; + const modules = (flcJson['modules'] ?? {}) as Record; + const docDir = path.dirname(flcPath); + + // `files` entries (ROHD Dart sources) are stored relative to the package + // root (e.g. `.dart_tool/../lib/src/...`), NOT relative to the directory + // that holds the `.flc.json` (which is typically a `build/` output dir). + // Resolve against `packageRoot` when present, then fall back to `docDir`. + const packageRoot = + typeof flcJson['packageRoot'] === 'string' + ? (flcJson['packageRoot'] as string) + : null; + const resolveSourcePath = (relPath: string): string => { + const bases = packageRoot ? [packageRoot, docDir] : [docDir]; + for (const base of bases) { + const candidate = path.resolve(base, relPath); + if (fs.existsSync(candidate)) { + return candidate; + } + } + // None existed — return the best-guess canonical path (packageRoot-based + // when available) so the reported path is meaningful. + return path.resolve(bases[0], relPath); + }; + + // Accept exact match or case-insensitive prefix match. + let modData: Record | null = null; + if (moduleName && modules[moduleName]) { + modData = modules[moduleName] as Record; + } else if (moduleName) { + const lc = moduleName.toLowerCase(); + for (const [k, v] of Object.entries(modules)) { + if (k.toLowerCase() === lc || k.toLowerCase().startsWith(lc + '_')) { + modData = v as Record; + break; + } + } + } + + const formats: ModuleFormats = {}; + + if (modData) { + // ROHD Dart source: trie tree non-empty + at least one global .dart file. + const tree = modData['tree']; + const hasRohdTree = Array.isArray(tree) && tree.length > 0; + const globalFiles = (flcJson['files'] ?? []) as string[]; + const hasRohd = hasRohdTree && globalFiles.length > 0; + + if (hasRohd) { + const rohdFile = globalFiles.find(f => f.endsWith('.dart')); + const rohdPath = rohdFile ? resolveSourcePath(rohdFile) : null; + formats['rohd'] = { + available: true, + fileFound: rohdPath ? fs.existsSync(rohdPath) : false, + path: rohdPath, + }; + } + + // Output-language files (sv, sc, …) from outputFiles map. + // v6: Record (list per language). + // v5 legacy: Record. + const rawOutputFiles = + (modData['outputFiles'] ?? {}) as Record; + const outputFiles: Record = {}; + for (const [lang, val] of Object.entries(rawOutputFiles)) { + if (Array.isArray(val)) { + if (val.length > 0 && typeof val[0] === 'string') { + outputFiles[lang] = val[0] as string; + } + } else if (typeof val === 'string') { + outputFiles[lang] = val; + } + } + // Legacy single svFile field. + if (!outputFiles['sv'] && modData['svFile']) { + outputFiles['sv'] = modData['svFile'] as string; + } + for (const [lang, relPath] of Object.entries(outputFiles)) { + const absPath = path.resolve(docDir, relPath); + formats[lang] = { + available: true, + fileFound: fs.existsSync(absPath), + path: absPath, + }; + } + } else { + output.appendLine( + '[FlcService] queryModule: "' + moduleName + + '" not found. Available: ' + Object.keys(modules).slice(0, 10).join(', '), + ); + } + + return { + extensionAvailable: true, + module: moduleName, + formats, + fstLoading: false, + }; + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + output.appendLine('[FlcService] queryModule error: ' + msg); + return { + extensionAvailable: true, + module: moduleName, + formats: {}, + error: 'Error reading FLC: ' + msg, + fstLoading: false, + }; + } +} + +// --------------------------------------------------------------------------- +// Signal lookup +// --------------------------------------------------------------------------- + +/** + * Look up source frames for [signalName] in [moduleName]. + * + * When [format] is provided, only frames of that type ('rohd', 'sv', 'sc', ...) + * are returned. Pass null/undefined to return all formats. + */ +export function lookupSignal( + flcPath: string, + moduleName: string | null, + signalName: string, + format?: string, +): SourceFrame[] { + if (!fs.existsSync(flcPath)) { + output.appendLine('[FlcService] lookupSignal: FLC not found: ' + flcPath); + return []; + } + + output.appendLine( + '[FlcService] lookupSignal: ' + flcPath + + ' module=' + (moduleName ?? '(any)') + + ' signal=' + signalName + + ' format=' + (format ?? 'all'), + ); + + try { + const raw = fs.readFileSync(flcPath, 'utf8'); + const flcJson = JSON.parse(raw) as Record; + const frames = lookupSignalInJson(flcJson, path.dirname(flcPath), moduleName, signalName); + const filtered = format ? frames.filter(f => f.type === format) : frames; + output.appendLine( + '[FlcService] lookupSignal: ' + filtered.length + ' frame(s)' + + (format ? ' after ' + format + ' filter' : ''), + ); + if (filtered.length === 0 && frames.length > 0 && format) { + output.appendLine( + '[FlcService] lookupSignal: available frame types=' + + Array.from(new Set(frames.map(f => f.type))).join(', '), + ); + } + return filtered; + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + output.appendLine('[FlcService] lookupSignal failed: ' + msg); + return []; + } +} + +function lookupSignalInJson( + flcJson: Record, + flcDir: string, + moduleName: string | null, + signalName: string, +): SourceFrame[] { + const modules = asRecord(flcJson['modules']); + if (!modules) { return []; } + + const moduleNames = moduleName ? matchingModuleNames(modules, moduleName) : Object.keys(modules); + for (const modName of moduleNames) { + const modData = asRecord(modules[modName]); + if (!modData) { continue; } + const frames = lookupSignalInModule(flcJson, flcDir, modData, signalName); + if (frames.length > 0) { + return frames; + } + } + + if (moduleName) { + output.appendLine( + '[FlcService] lookupSignal: no match for ' + moduleName + '/' + signalName + + '. Available modules: ' + Object.keys(modules).slice(0, 10).join(', '), + ); + } + return []; +} + +function lookupSignalInModule( + flcJson: Record, + flcDir: string, + modData: Record, + signalName: string, +): SourceFrame[] { + const tree = modData['tree']; + if (!Array.isArray(tree)) { return []; } + + const files = Array.isArray(flcJson['files']) + ? (flcJson['files'] as unknown[]).filter((f): f is string => typeof f === 'string') + : []; + const outputFiles = getOutputFiles(modData); + + let origNameMatch: SourceFrame[] = []; + + const walkNode = (node: unknown[], pathFrames: string[]): SourceFrame[] => { + if (node.length === 0 || typeof node[0] !== 'string') { return []; } + const currentPath = [...pathFrames, node[0] as string]; + + for (let i = 1; i < node.length; i++) { + const elem = node[i]; + if (Array.isArray(elem)) { + const found = walkNode(elem, currentPath); + if (found.length > 0) { return found; } + } else if (typeof elem === 'string') { + const parsed = parseSymbolString(elem); + const frames = entryToFrames(parsed, currentPath, files, outputFiles, flcDir, signalName); + if (parsed.name === signalName) { + return frames; + } + if (parsed.origName === signalName && origNameMatch.length === 0) { + origNameMatch = frames; + } + } + } + + return []; + }; + + for (const rootNode of tree) { + if (!Array.isArray(rootNode)) { continue; } + const found = walkNode(rootNode, []); + if (found.length > 0) { return found; } + } + return origNameMatch; +} + +function entryToFrames( + symbol: FlcSymbolInfo, + pathFrames: string[], + files: string[], + outputFiles: Record, + flcDir: string, + signalName: string, +): SourceFrame[] { + const frames: SourceFrame[] = []; + + for (const frame of [...pathFrames].reverse()) { + const parts = frame.split(':'); + if (parts.length < 2) { continue; } + const fileIndex = Number.parseInt(parts[0], 10); + if (!Number.isInteger(fileIndex) || fileIndex < 0 || fileIndex >= files.length) { continue; } + frames.push({ + file: files[fileIndex], + line: Number.parseInt(parts[1], 10) || 1, + col: parts.length > 2 ? (Number.parseInt(parts[2], 10) || 1) : 1, + desc: signalName + ' [ROHD]', + type: 'rohd', + }); + } + + for (const pos of symbol.outputPositions) { + const outputFile = outputFiles[pos.type]; + if (!outputFile) { continue; } + frames.push({ + file: path.resolve(flcDir, outputFile), + line: pos.line, + col: pos.col, + desc: signalName + ' [' + pos.type.toUpperCase() + ']', + type: pos.type, + }); + } + + return frames; +} + +function getOutputFiles(modData: Record): Record { + const outputFiles: Record = {}; + const svFile = modData['svFile']; + if (typeof svFile === 'string') { + outputFiles['sv'] = svFile; + } + + const rawOutputFiles = asRecord(modData['outputFiles']); + if (!rawOutputFiles) { return outputFiles; } + + for (const [lang, val] of Object.entries(rawOutputFiles)) { + if (typeof val === 'string') { + outputFiles[lang] = val; + } else if (Array.isArray(val)) { + const first = val.find((item): item is string => typeof item === 'string'); + if (first) { + outputFiles[lang] = first; + } + } + } + return outputFiles; +} + +function parseSymbolString(symbol: string): FlcSymbolInfo { + const isInstance = symbol.startsWith('*'); + let rest = isInstance ? symbol.substring(1) : symbol; + + let origName: string | null = null; + const tildeIdx = rest.indexOf('~'); + if (tildeIdx >= 0) { + origName = rest.substring(tildeIdx + 1); + rest = rest.substring(0, tildeIdx); + } + + const outputPositions: FlcOutputPos[] = []; + const atIdx = rest.indexOf('@'); + if (atIdx >= 0) { + const positions = rest.substring(atIdx + 1); + rest = rest.substring(0, atIdx); + outputPositions.push(...parseOutputPositions(positions)); + } + + return { name: rest, isInstance, outputPositions, origName }; +} + +function parseOutputPositions(positions: string): FlcOutputPos[] { + const result: FlcOutputPos[] = []; + for (const group of positions.split(';')) { + if (!group) { continue; } + const entries = group.split(','); + let groupLang: string | null = null; + for (let i = 0; i < entries.length; i++) { + let part = entries[i]; + if (!part) { continue; } + if (i === 0) { + const segments = part.split(':'); + const firstIsTag = segments.length >= 3 && Number.isNaN(Number.parseInt(segments[0], 10)); + if (firstIsTag) { + groupLang = segments[0]; + part = segments.slice(1).join(':'); + } + } + const type = groupLang ?? 'sv'; + const segments = part.split(':'); + const lineText = segments.length >= 2 ? segments[segments.length - 2] : segments[0]; + const colText = segments.length >= 2 ? segments[segments.length - 1] : undefined; + result.push({ + type, + line: Number.parseInt(lineText, 10) || 1, + col: colText ? (Number.parseInt(colText, 10) || 1) : 1, + }); + } + } + return result; +} + +function matchingModuleNames(modules: Record, moduleName: string): string[] { + if (Object.prototype.hasOwnProperty.call(modules, moduleName)) { + return [moduleName]; + } + + const lower = moduleName.toLowerCase(); + const matches = Object.keys(modules).filter((name) => { + const candidate = name.toLowerCase(); + return candidate === lower || candidate.startsWith(lower + '_'); + }); + return matches; +} + +function asRecord(value: unknown): Record | null { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return value as Record; + } + return null; +} diff --git a/rohd_extension/src/source_navigator.ts b/rohd_extension/src/source_navigator.ts new file mode 100644 index 000000000..dc727e774 --- /dev/null +++ b/rohd_extension/src/source_navigator.ts @@ -0,0 +1,482 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * source_navigator.ts + * Cross-probe → VS Code editor navigation module. + * + * Receives source location requests from ROHD viewer extensions (schematic, + * wave) and navigates the VS Code editor to the corresponding file/line/col. + * Supports multi-frame stack traces with cycling. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; + +const output = vscode.window.createOutputChannel('ROHD Source Navigator'); + +/** A single source location frame. */ +export interface SourceFrame { + /** File path (package-relative, e.g. `lib/src/foo.dart`). */ + file: string; + /** 1-based line number. */ + line: number; + /** 1-based column number. */ + col: number; + /** Optional description (e.g. function name from stack trace). */ + desc?: string; + /** Frame type: 'sv' for SystemVerilog, 'rohd' for ROHD Dart source. */ + type?: string; +} + +// --------------------------------------------------------------------------- +// Frame cycling state +// --------------------------------------------------------------------------- + +let currentFrames: SourceFrame[] = []; +let currentFrameIndex = 0; +let statusBarItem: vscode.StatusBarItem | undefined; +let statusBarTimeout: ReturnType | undefined; + +// Highlight decoration for the target symbol (yellow flash). +const highlightDecoration = vscode.window.createTextEditorDecorationType({ + backgroundColor: 'rgba(255, 213, 79, 0.35)', +}); +let highlightTimeout: ReturnType | undefined; + +// --------------------------------------------------------------------------- +// Public API — called via vscode.commands.executeCommand() +// --------------------------------------------------------------------------- + +/** + * Navigate to a single source location. + * + * @param args `{ file: string, line: number, col: number }` + */ +export async function openSourceLocation( + args: { file: string; line: number; col: number }, +): Promise { + if (!args || !args.file) { + vscode.window.showWarningMessage('ROHD: No source location provided.'); + return; + } + // Clear any previous frame cycling state. + currentFrames = [{ file: args.file, line: args.line, col: args.col }]; + currentFrameIndex = 0; + hideStatusBar(); + await navigateToFrame(currentFrames[0]); +} + +/** + * Navigate to the first of multiple source location frames and enable + * cycling through them. + * + * @param args `{ frames: SourceFrame[], index?: number }` + */ +export async function openSourceLocations( + args: { frames: SourceFrame[]; index?: number }, +): Promise { + if (!args || !args.frames || args.frames.length === 0) { + vscode.window.showWarningMessage('ROHD: No source locations provided.'); + return; + } + currentFrames = args.frames; + currentFrameIndex = args.index ?? 0; + if (currentFrameIndex < 0 || currentFrameIndex >= currentFrames.length) { + currentFrameIndex = 0; + } + updateStatusBar(); + + // Navigate to the primary frame. + await navigateToFrame(currentFrames[currentFrameIndex]); + + // Also open the first frame of each other type (e.g. SV alongside ROHD) + // so both files are visible simultaneously. + const openedTypes = new Set([currentFrames[currentFrameIndex].type || 'rohd']); + for (const frame of currentFrames) { + const t = frame.type || 'rohd'; + if (!openedTypes.has(t)) { + openedTypes.add(t); + await navigateToFrame(frame, true); + } + } +} + +/** Advance to the next frame in the current frame list. */ +export async function nextSourceLocation(): Promise { + if (currentFrames.length === 0) { return; } + currentFrameIndex = (currentFrameIndex + 1) % currentFrames.length; + updateStatusBar(); + await navigateToFrame(currentFrames[currentFrameIndex]); +} + +/** Go back to the previous frame in the current frame list. */ +export async function prevSourceLocation(): Promise { + if (currentFrames.length === 0) { return; } + currentFrameIndex = + (currentFrameIndex - 1 + currentFrames.length) % currentFrames.length; + updateStatusBar(); + await navigateToFrame(currentFrames[currentFrameIndex]); +} + +// --------------------------------------------------------------------------- +// Navigation implementation +// --------------------------------------------------------------------------- + +/** + * Open (or reuse) an editor tab for the given frame and scroll to the + * target line. Tries multiple candidate paths until one succeeds. + */ +async function navigateToFrame(frame: SourceFrame, preserveFocus = false): Promise { + const candidates = resolveCandidates(frame.file); + if (candidates.length === 0) { + vscode.window.showWarningMessage( + `ROHD: Could not resolve file: ${frame.file}`, + ); + return; + } + + // 0-based position from 1-based FLC data. + const line = Math.max(0, frame.line - 1); + const col = Math.max(0, frame.col - 1); + const pos = new vscode.Position(line, col); + const range = new vscode.Range(pos, pos); + + for (const uri of candidates) { + try { + const doc = await vscode.workspace.openTextDocument(uri); + const docUriStr = doc.uri.toString(); + + // Reuse an existing editor tab for this document if one is already + // open, instead of always opening a new split. Compare against + // the document's canonical URI (not the candidate URI which may + // contain unresolved `..` segments). + let viewColumn = vscode.ViewColumn.Beside; + for (const tab of vscode.window.tabGroups.all.flatMap(g => g.tabs)) { + const tabInput = tab.input; + if (tabInput instanceof vscode.TabInputText && + tabInput.uri.toString() === docUriStr) { + viewColumn = tab.group.viewColumn; + break; + } + } + + const editor = await vscode.window.showTextDocument(doc, { + viewColumn, + preserveFocus, + selection: range, + }); + + editor.revealRange( + new vscode.Range(pos, pos), + vscode.TextEditorRevealType.InCenterIfOutsideViewport, + ); + + // Flash-highlight the symbol at the target position. + flashHighlight(editor, line, col); + + output.appendLine( + `Navigated to ${frame.file}:${frame.line}:${frame.col} (resolved: ${uri.fsPath})`, + ); + return; // Success — stop trying candidates. + } catch { + // This candidate didn't work; try the next one. + continue; + } + } + + // All candidates failed. + vscode.window.showWarningMessage( + `ROHD: Could not find file: ${frame.file}`, + ); + output.appendLine( + `Failed to resolve ${frame.file} (tried ${candidates.length} candidates)`, + ); +} + +/** + * Normalize a file path by collapsing `.` and `..` segments. + * + * FLC paths from SourceTraceRegistry often contain `.dart_tool/../lib/...` + * which needs collapsing before resolution. + */ +function normalizePath(filePath: string): string { + const isAbsolute = filePath.startsWith('/'); + const parts = filePath.split('/'); + const resolved: string[] = []; + for (const part of parts) { + if (part === '.' || part === '') { + continue; + } else if (part === '..' && resolved.length > 0 && resolved[resolved.length - 1] !== '..') { + resolved.pop(); + } else { + resolved.push(part); + } + } + const joined = resolved.join('/'); + return isAbsolute ? '/' + joined : joined; +} + +/** + * Generate candidate URIs for a package-relative file path. + * + * Tries (in order): + * 1. Normalized path relative to each workspace folder + * 2. Normalized path relative to parent directories of each workspace + * folder (up to 4 levels — covers typical layouts where the ROHD + * package root is above the extension subdirectory) + * 3. As an absolute path + */ +function resolveCandidates(filePath: string): vscode.Uri[] { + const normalized = normalizePath(filePath); + const candidates: vscode.Uri[] = []; + const folders = vscode.workspace.workspaceFolders; + + if (folders) { + for (const folder of folders) { + // Direct: workspace root + normalized path + candidates.push(vscode.Uri.joinPath(folder.uri, normalized)); + + // SV files are often generated into a build/ directory. + candidates.push(vscode.Uri.joinPath(folder.uri, 'build', normalized)); + + // Walk up parent directories (the ROHD package root may be above + // the workspace folder, e.g. merged/ vs merged/rohd_devtools_extension/rohd-schematic-viewer/) + let parent = folder.uri; + for (let i = 0; i < 4; i++) { + parent = vscode.Uri.joinPath(parent, '..'); + candidates.push(vscode.Uri.joinPath(parent, normalized)); + candidates.push(vscode.Uri.joinPath(parent, 'build', normalized)); + } + } + } + + // Absolute path fallback. + if (normalized.startsWith('/')) { + candidates.push(vscode.Uri.file(normalized)); + } + + // Also try the original (un-normalized) path in case it's already correct. + if (normalized !== filePath && folders) { + for (const folder of folders) { + candidates.push(vscode.Uri.joinPath(folder.uri, filePath)); + } + } + + return candidates; +} + +// --------------------------------------------------------------------------- +// Status bar +// --------------------------------------------------------------------------- + +function updateStatusBar(): void { + if (currentFrames.length <= 1) { + hideStatusBar(); + return; + } + if (!statusBarItem) { + statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100, + ); + statusBarItem.command = 'rohd.nextSourceLocation'; + statusBarItem.tooltip = 'Click to cycle through source frames'; + } + const frame = currentFrames[currentFrameIndex]; + const shortFile = frame.file.split('/').pop() ?? frame.file; + const typeTag = frame.type === 'sv' ? 'SV' : frame.type === 'rohd' ? 'ROHD' : 'Source'; + const desc = frame.desc ? ` ${frame.desc}` : ''; + statusBarItem.text = `$(source-control) ${typeTag} ${currentFrameIndex + 1}/${currentFrames.length}: ${shortFile}:${frame.line}${desc}`; + statusBarItem.show(); + + // Auto-hide after 15 seconds. + if (statusBarTimeout) { clearTimeout(statusBarTimeout); } + statusBarTimeout = setTimeout(() => hideStatusBar(), 15000); +} + +function hideStatusBar(): void { + statusBarItem?.hide(); + if (statusBarTimeout) { + clearTimeout(statusBarTimeout); + statusBarTimeout = undefined; + } +} + +// --------------------------------------------------------------------------- +// Highlight flash +// --------------------------------------------------------------------------- + +function flashHighlight(editor: vscode.TextEditor, line: number, col: number): void { + if (highlightTimeout) { clearTimeout(highlightTimeout); } + + const doc = editor.document; + const pos = new vscode.Position(line, col); + + // Try to get the word range at the column position (symbol highlight). + const wordRange = doc.getWordRangeAtPosition(pos); + + // Use the word range if found, otherwise highlight from column to end of line. + const highlightRange = wordRange + ?? new vscode.Range(pos, doc.lineAt(line).range.end); + + editor.setDecorations(highlightDecoration, [highlightRange]); + + // Remove highlight after 60 seconds. + highlightTimeout = setTimeout(() => { + editor.setDecorations(highlightDecoration, []); + }, 60000); +} + +// --------------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------------- + +/** Register all commands. Call from extension activate(). */ +export function registerCommands(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.commands.registerCommand('rohd.openSourceLocation', openSourceLocation), + vscode.commands.registerCommand('rohd.openSourceLocations', openSourceLocations), + vscode.commands.registerCommand('rohd.nextSourceLocation', nextSourceLocation), + vscode.commands.registerCommand('rohd.prevSourceLocation', prevSourceLocation), + ); + output.appendLine('ROHD Source Navigator commands registered.'); +} + +/** Clean up. Call from extension deactivate(). */ +export function dispose(): void { + hideStatusBar(); + statusBarItem?.dispose(); + highlightDecoration.dispose(); +} + +// --------------------------------------------------------------------------- +// Frame enrichment — resolve enclosing method names via Document Symbols +// --------------------------------------------------------------------------- + +/** A frame enriched with its enclosing method/class names. */ +export interface EnrichedFrame extends SourceFrame { + /** Enclosing method/function name (e.g. `"build"`). */ + methodName?: string; + /** Enclosing class name (e.g. `"Serializer"`). */ + className?: string; + /** Human-readable label: `"Serializer.build() — serializer.dart:55"`. */ + label?: string; +} + +/** + * Resolve the enclosing method name for a source location using the + * VS Code Document Symbol Provider (backed by the Dart language server). + */ +async function resolveEnclosingSymbol( + uri: vscode.Uri, + line: number, + col: number, +): Promise<{ methodName?: string; className?: string }> { + try { + const symbols = await vscode.commands.executeCommand( + 'vscode.executeDocumentSymbolProvider', + uri, + ); + if (!symbols || symbols.length === 0) { + return {}; + } + + const pos = new vscode.Position(Math.max(0, line - 1), Math.max(0, col - 1)); + let methodName: string | undefined; + let className: string | undefined; + + // Walk the symbol tree depth-first, tracking the innermost containing + // function/method and its parent class. + function walk(syms: vscode.DocumentSymbol[], parentClass?: string): void { + for (const sym of syms) { + if (!sym.range.contains(pos)) { continue; } + + if (sym.kind === vscode.SymbolKind.Class || + sym.kind === vscode.SymbolKind.Enum) { + className = sym.name; + walk(sym.children, sym.name); + } else if ( + sym.kind === vscode.SymbolKind.Method || + sym.kind === vscode.SymbolKind.Function || + sym.kind === vscode.SymbolKind.Constructor + ) { + methodName = sym.name; + if (parentClass) { className = parentClass; } + // Continue into children in case there's a nested function. + walk(sym.children, parentClass); + } else { + walk(sym.children, parentClass); + } + } + } + + walk(symbols); + return { methodName, className }; + } catch { + return {}; + } +} + +/** + * Enrich a list of frames with enclosing method/class names. + * + * Opens each document (lazily — no visible editor tab) to query the + * Document Symbol Provider. Returns an enriched copy of each frame. + */ +export async function resolveFrames( + frames: SourceFrame[], +): Promise { + const enriched: EnrichedFrame[] = []; + + for (const frame of frames) { + const candidates = resolveCandidates(frame.file); + let methodName: string | undefined; + let className: string | undefined; + let resolved = false; + + for (const uri of candidates) { + try { + // Open the document without showing it — this triggers the + // language server to analyse it if not already cached. + await vscode.workspace.openTextDocument(uri); + const result = await resolveEnclosingSymbol(uri, frame.line, frame.col); + methodName = result.methodName; + className = result.className; + resolved = true; + break; + } catch { + continue; + } + } + + // Build human-readable label. + const shortFile = frame.file.split('/').pop() ?? frame.file; + let label: string; + if (className && methodName) { + label = `${className}.${methodName}() — ${shortFile}:${frame.line}`; + } else if (methodName) { + label = `${methodName}() — ${shortFile}:${frame.line}`; + } else if (className) { + label = `${className} — ${shortFile}:${frame.line}`; + } else { + label = `${shortFile}:${frame.line}`; + } + + enriched.push({ + ...frame, + methodName, + className, + label, + }); + + if (!resolved) { + output.appendLine( + `[resolveFrames] Could not resolve symbols for ${frame.file}:${frame.line}`, + ); + } + } + + return enriched; +} diff --git a/rohd_extension/src/uri_forwarder.ts b/rohd_extension/src/uri_forwarder.ts new file mode 100644 index 000000000..62336f32d --- /dev/null +++ b/rohd_extension/src/uri_forwarder.ts @@ -0,0 +1,295 @@ +/* --------------------------------------------------------------------------- + * Copyright (C) 2026 Intel Corporation. + * SPDX-License-Identifier: BSD-3-Clause + * + * uri_forwarder.ts + * Resolves and displays DTD and VM Service URIs with port-forwarding + * awareness. When running inside a dev container or remote session, + * VS Code's `asExternalUri` may map container-internal ports to + * host-visible ports. This module detects that and shows both the + * original and forwarded URIs so they can be pasted into the ROHD + * DevTools GUI. + * + * Author: Desmond Kirkpatrick + * --------------------------------------------------------------------------- */ + +import * as vscode from 'vscode'; + +const EXTENSION_VERSION = '0.1.0'; + +let outputChannel: vscode.OutputChannel; + +/** Map of session id → last known VM Service URI (container-internal). */ +const sessionVmUris = new Map(); + +/** Latest original DTD URI (container-internal). */ +let originalDtdUri: string | undefined; + +/** Latest forwarded DTD URI, undefined if no forwarding occurred. */ +let forwardedDtdUri: string | undefined; + +// --------------------------------------------------------------------------- +// URI helpers +// --------------------------------------------------------------------------- + +/** + * Given a raw WS URI, resolve via `vscode.env.asExternalUri` to get the + * port-forwarded equivalent. Returns the forwarded URI string, or + * `undefined` if no forwarding occurred (i.e. authority is unchanged). + */ +async function resolveForwardedUri( + rawWsUri: string, +): Promise { + try { + const httpUri = rawWsUri + .replace(/^ws:\/\//, 'http://') + .replace(/^wss:\/\//, 'https://'); + + const parsed = vscode.Uri.parse(httpUri); + const resolved = await vscode.env.asExternalUri(parsed); + + const scheme = resolved.scheme === 'https' ? 'wss' : 'ws'; + const forwarded = `${scheme}://${resolved.authority}${parsed.path}`; + + if (resolved.authority !== parsed.authority) { + return forwarded; + } + return undefined; + } catch { + return undefined; + } +} + +/** Normalize a URI to ws:// scheme; optionally ensure a /ws suffix. */ +function normalizeWsUri(uri: string, ensureWsSuffix: boolean): string { + let u = uri; + if (u.startsWith('http://')) { + u = u.replace('http://', 'ws://'); + } else if (u.startsWith('https://')) { + u = u.replace('https://', 'wss://'); + } + if (ensureWsSuffix && !u.endsWith('/ws')) { + u = u.replace(/\/?$/, '/ws'); + } + return u; +} + +// --------------------------------------------------------------------------- +// Dart extension API typings (subset) +// --------------------------------------------------------------------------- + +interface DartExtensionApi { + dtdUri?: string; + onDtdUriChanged?: ( + listener: (uri: string | undefined) => void, + thisArgs?: unknown, + disposables?: vscode.Disposable[], + ) => vscode.Disposable; +} + +// --------------------------------------------------------------------------- +// DTD URI handling +// --------------------------------------------------------------------------- + +async function processDtdUri(rawUri: string): Promise { + const wsUri = normalizeWsUri(rawUri, false); + originalDtdUri = wsUri; + outputChannel.appendLine(`[DTD] Original: ${wsUri}`); + + const forwarded = await resolveForwardedUri(wsUri); + forwardedDtdUri = forwarded; + if (forwarded) { + outputChannel.appendLine(`[DTD] Forwarded: ${forwarded}`); + } +} + +function watchDtdUri(context: vscode.ExtensionContext): void { + const dartExt = vscode.extensions.getExtension('dart-code.dart-code'); + if (!dartExt) { + outputChannel.appendLine( + '[DTD] Dart extension not found — DTD URI detection disabled.', + ); + return; + } + + function handleApi(api: DartExtensionApi): void { + if (api.dtdUri) { + processDtdUri(api.dtdUri).catch((err) => { + outputChannel.appendLine(`[DTD] Error: ${err}`); + }); + } + if (api.onDtdUriChanged) { + const disposable = api.onDtdUriChanged((uri) => { + if (uri) { + processDtdUri(uri).catch((err) => { + outputChannel.appendLine(`[DTD] Error: ${err}`); + }); + } + }); + if (disposable) { + context.subscriptions.push(disposable); + } + } + } + + if (dartExt.isActive) { + handleApi(dartExt.exports as DartExtensionApi); + } else { + dartExt.activate().then( + (api) => handleApi(api as DartExtensionApi), + (err) => + outputChannel.appendLine( + `[DTD] Dart extension activation failed: ${err}`, + ), + ); + } +} + +// --------------------------------------------------------------------------- +// Consolidated output +// --------------------------------------------------------------------------- + +/** + * Print a formatted block with the ROHD version, FLC info, and the + * DTD / VM Service URIs (original + forwarded when port differs). + */ +async function printConsolidatedBlock( + vmOriginal: string, + vmForwarded: string | undefined, + session: vscode.DebugSession, +): Promise { + const lines: string[] = []; + + lines.push(`ROHD ${EXTENSION_VERSION}: Extension loaded for FLC crossprobing.`); + lines.push(''); + + if (originalDtdUri) { + lines.push('DTD:'); + lines.push(` URI: ${originalDtdUri}`); + if (forwardedDtdUri) { + lines.push(` Fwd: ${forwardedDtdUri}`); + } + } + + lines.push('VM:'); + lines.push(` URI: ${vmOriginal}`); + if (vmForwarded) { + lines.push(` Fwd: ${vmForwarded}`); + } + + const banner = '═'.repeat(60); + const block = [banner, ...lines, banner].join('\n'); + + // Debug console + vscode.debug.activeDebugConsole.appendLine(''); + vscode.debug.activeDebugConsole.appendLine(block); + + // Output channel (persists across sessions) + outputChannel.appendLine(''); + outputChannel.appendLine(block); + + // Show the most useful forwarded URI in a popup for quick copy + const popupUri = forwardedDtdUri ?? originalDtdUri; + if (popupUri) { + const action = await vscode.window.showInformationMessage( + `DTD: ${popupUri}`, + 'Copy', + ); + if (action === 'Copy') { + await vscode.env.clipboard.writeText(popupUri); + vscode.window.showInformationMessage('DTD URI copied to clipboard.'); + } + } +} + +// --------------------------------------------------------------------------- +// VM Service URI handling +// --------------------------------------------------------------------------- + +async function processVmServiceUri( + rawUri: string, + session: vscode.DebugSession, +): Promise { + const vmUri = normalizeWsUri(rawUri, true); + sessionVmUris.set(session.id, vmUri); + + outputChannel.appendLine(`[VM] Original: ${vmUri}`); + + const forwarded = await resolveForwardedUri(vmUri); + await printConsolidatedBlock(vmUri, forwarded, session); +} + +// --------------------------------------------------------------------------- +// Public API — called from extension.ts +// --------------------------------------------------------------------------- + +export function activate(context: vscode.ExtensionContext): void { + outputChannel = vscode.window.createOutputChannel('ROHD URI Forwarder'); + + // Start watching for the DTD URI early — it resolves before the + // debug session starts so it will be ready when we print. + watchDtdUri(context); + + // Listen for the DAP custom event "dart.debuggerUris" + context.subscriptions.push( + vscode.debug.onDidReceiveDebugSessionCustomEvent((e) => { + if (e.event !== 'dart.debuggerUris') { + return; + } + + const uri = e.body?.vmServiceUri as string | undefined; + if (!uri) { + outputChannel.appendLine( + '[URI Forwarder] dart.debuggerUris event received but no vmServiceUri in body.', + ); + return; + } + + outputChannel.appendLine( + `[URI Forwarder] Received dart.debuggerUris for session "${e.session.name}".`, + ); + + processVmServiceUri(uri, e.session).catch((err) => { + outputChannel.appendLine(`[URI Forwarder] Error: ${err}`); + }); + }), + ); + + // Clean up stored URIs when sessions end + context.subscriptions.push( + vscode.debug.onDidTerminateDebugSession((session) => { + sessionVmUris.delete(session.id); + }), + ); + + // Manual command: re-resolve and re-print for the active session + context.subscriptions.push( + vscode.commands.registerCommand('rohd.showForwardedUris', async () => { + const session = vscode.debug.activeDebugSession; + if (!session) { + vscode.window.showWarningMessage('No active debug session.'); + return; + } + + const uri = sessionVmUris.get(session.id); + if (!uri) { + vscode.window.showWarningMessage( + 'VM Service URI not yet available for this session. ' + + 'It will be displayed automatically once the Dart VM starts.', + ); + return; + } + + await processVmServiceUri(uri, session); + }), + ); + + outputChannel.appendLine('[URI Forwarder] Activated.'); +} + +export function deactivate(): void { + sessionVmUris.clear(); + originalDtdUri = undefined; + forwardedDtdUri = undefined; + outputChannel?.dispose(); +} diff --git a/rohd_extension/tool/install.sh b/rohd_extension/tool/install.sh new file mode 100755 index 000000000..bb29d2479 --- /dev/null +++ b/rohd_extension/tool/install.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# install.sh — Build and install the ROHD VS Code extension. +# +# Usage: +# ./tool/install.sh # build + install +# ./tool/install.sh --skip-build # install existing .vsix only +# +# Requires: node >= 22, npm, code CLI +# --------------------------------------------------------------------------- +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +VERSION=$(node -p "require('$EXT_DIR/package.json').version") +VSIX="$EXT_DIR/rohd-${VERSION}.vsix" + +# Ensure Node >= 22 is available. +node_major=0 +if command -v node &>/dev/null; then + node_version=$(node --version) + node_major=${node_version#v} + node_major=${node_major%%.*} +fi +if (( node_major < 22 )) && [[ -d "$HOME/.nvm/versions/node" ]]; then + NODE_DIR=$(ls -d "$HOME/.nvm/versions/node"/v2* 2>/dev/null | sort -V | tail -1) + if [[ -n "$NODE_DIR" ]]; then + export PATH="$NODE_DIR/bin:$PATH" + echo "Using node from $NODE_DIR" + fi +fi + +node_ver=$(node --version 2>/dev/null || echo "none") +echo "Node: $node_ver" +node_major=${node_ver#v} +node_major=${node_major%%.*} +if [[ ! "$node_major" =~ ^[0-9]+$ ]] || (( node_major < 22 )); then + echo "ERROR: Node.js >= 22 is required to package the extension." >&2 + exit 1 +fi + +# ── Build ── +if [[ "${1:-}" != "--skip-build" ]]; then + echo "── Installing npm dependencies ──" + cd "$EXT_DIR" + npm ci --no-audit --no-fund + + echo "── Compiling TypeScript ──" + npx tsc + + echo "── Packaging VSIX ──" + rm -f "$EXT_DIR"/rohd-*.vsix + npm run package +fi + +# ── Install ── +if [[ ! -f "$VSIX" ]]; then + echo "ERROR: $VSIX not found. Run without --skip-build first." >&2 + exit 1 +fi + +echo "── Installing $VSIX ──" +code --install-extension "$VSIX" --force + +echo "" +echo "Done — ROHD extension v${VERSION} installed." +echo "Reload the VS Code window to activate." diff --git a/rohd_extension/tsconfig.json b/rohd_extension/tsconfig.json new file mode 100644 index 000000000..ec2434dd0 --- /dev/null +++ b/rohd_extension/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "out"] +} diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 3a4c6b74e..a287cba6f 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // array_collapsing_test.dart @@ -65,6 +65,842 @@ class ArrayWithShuffledAssignment extends Module { } } +/// Partially assigns three elements of an internal array from an input array. +/// +/// When reversed is false, the assignments are contiguous and same-offset, +/// so they can collapse into a range-to-range assignment. When reversed is +/// true, the assignments are intentionally not same-offset and must remain +/// expanded. +class PartialArrayRangeAssignment extends Module { + PartialArrayRangeAssignment({bool reversed = false}) + : super(name: 'partial_array_range_assignment') { + final src = addInputArray('src', LogicArray([6], 1), dimensions: [6]); + final dst = LogicArray([6], 1, name: 'dst'); + + for (var dstIndex = 2; dstIndex <= 4; dstIndex++) { + final srcIndex = reversed ? 6 - dstIndex : dstIndex; + dst.elements[dstIndex] <= src.elements[srcIndex]; + } + + addOutput('y', width: 6) <= dst.elements.rswizzle(); + } +} + +/// Partially assigns a range through an intermediate array. The two range +/// assignments should compose so the intermediate array can be pruned. +class ChainedPartialArrayRangeAssignment extends Module { + ChainedPartialArrayRangeAssignment({ + bool exposeIntermediate = false, + Naming? intermediateNaming = Naming.mergeable, + }) : super(name: 'chained_partial_array_range_assignment') { + final src = addInputArray('src', LogicArray([6], 1), dimensions: [6]); + final intermediate = + LogicArray([6], 1, name: 'intermediate', naming: intermediateNaming); + final dst = LogicArray([6], 1, name: 'dst'); + + for (var index = 2; index <= 4; index++) { + intermediate.elements[index] <= src.elements[index]; + dst.elements[index] <= intermediate.elements[index]; + } + + addOutput('y', width: 6) <= dst.elements.rswizzle(); + if (exposeIntermediate) { + addOutput('z', width: 6) <= intermediate.elements.rswizzle(); + } + } +} + +/// Drives every leaf of a small multidimensional array from literal constants. +/// Constant sources are not range bases, so range collapsing must not try to +/// treat these as array-to-array range assignments. +class ConstantLeafArrayAssignment extends Module { + ConstantLeafArrayAssignment({ + List values = const [0, 0, 0, 0], + super.name = 'constant_leaf_array_assignment', + }) { + final banana = LogicArray([2, 2], 1, name: 'banana'); + + for (var index = 0; index < banana.leafElements.length; index++) { + banana.leafElements[index] <= Const(values[index]); + } + + addOutput('y', width: banana.width) <= banana.leafElements.rswizzle(); + } +} + +/// Partially assigns a range through two intermediate arrays. The range +/// composition pass should iterate until both intermediates are gone. +class ThreeDeepChainedPartialArrayRangeAssignment extends Module { + ThreeDeepChainedPartialArrayRangeAssignment() + : super(name: 'three_deep_chained_partial_array_range_assignment') { + final src = addInputArray('src', LogicArray([6], 1), dimensions: [6]); + final intermediate0 = + LogicArray([6], 1, name: 'intermediate0', naming: Naming.mergeable); + final intermediate1 = + LogicArray([6], 1, name: 'intermediate1', naming: Naming.mergeable); + final dst = LogicArray([6], 1, name: 'dst'); + + for (var index = 2; index <= 4; index++) { + intermediate0.elements[index] <= src.elements[index]; + intermediate1.elements[index] <= intermediate0.elements[index]; + dst.elements[index] <= intermediate1.elements[index]; + } + + addOutput('y', width: 6) <= dst.elements.rswizzle(); + } +} + +/// Passes a partial packed-array range through many mergeable intermediates. +class LongChainedPartialArrayRangeAssignment extends Module { + /// The number of intermediate range stages. + final int length; + + LongChainedPartialArrayRangeAssignment({this.length = 256}) + : super(name: 'long_chained_partial_array_range_assignment') { + final src = addInputArray('src', LogicArray([6], 1), dimensions: [6]); + var previous = src; + for (var stage = 0; stage < length; stage++) { + final intermediate = LogicArray( + [6], + 1, + name: 'intermediate$stage', + naming: Naming.mergeable, + ); + for (var index = 2; index <= 4; index++) { + intermediate.elements[index] <= previous.elements[index]; + } + previous = intermediate; + } + + final dst = LogicArray([6], 1, name: 'dst'); + for (var index = 2; index <= 4; index++) { + dst.elements[index] <= previous.elements[index]; + } + addOutput('y', width: 6) <= dst.elements.rswizzle(); + } +} + +/// Partially assigns an offset source range through an intermediate, then reads +/// only a subrange of the intermediate. +class ChainedSubrangeArrayRangeAssignment extends Module { + ChainedSubrangeArrayRangeAssignment() + : super(name: 'chained_subrange_array_range_assignment') { + final src = addInputArray('src', LogicArray([8], 1), dimensions: [8]); + final intermediate = + LogicArray([8], 1, name: 'intermediate', naming: Naming.mergeable); + final dst = LogicArray([8], 1, name: 'dst'); + + for (var index = 1; index <= 5; index++) { + intermediate.elements[index] <= src.elements[index + 2]; + } + for (var index = 2; index <= 3; index++) { + dst.elements[index] <= intermediate.elements[index + 1]; + } + + addOutput('y', width: 8) <= dst.elements.rswizzle(); + } +} + +/// Partially assigns array elements from bits of a flat input bus. +class PartialBusToArrayRangeAssignment extends Module { + PartialBusToArrayRangeAssignment({ + bool reversed = false, + int numUnpackedDimensions = 0, + }) : super(name: 'partial_bus_to_array_range_assignment') { + final src = addInput('src', Logic(width: 8), width: 8); + final dst = LogicArray([8], 1, + name: 'dst', numUnpackedDimensions: numUnpackedDimensions); + + for (var dstIndex = 2; dstIndex <= 5; dstIndex++) { + final srcIndex = reversed ? 7 - dstIndex : dstIndex; + dst.elements[dstIndex] <= src[srcIndex]; + } + + addOutput('y', width: 8) <= dst.leafElements.rswizzle(); + } +} + +/// Assigns a full array range into a flat bus through [Logic.assignSubset]. +class ArrayToBusAssignSubsetRangeAssignment extends Module { + ArrayToBusAssignSubsetRangeAssignment({bool partial = false}) + : super(name: 'array_to_bus_assign_subset_range_assignment') { + final src = addInputArray('src', LogicArray([8], 1), dimensions: [8]); + final dst = Logic(width: 8, name: 'dst'); + + final start = partial ? 2 : 0; + final end = partial ? 5 : 7; + for (var index = start; index <= end; index++) { + dst.assignSubset([src.elements[index]], start: index); + } + + addOutput('y', width: 8) <= dst; + } +} + +/// Assigns a flat bus range into another flat bus through a temporary slice. +class BusSliceTemporaryToAssignSubsetRangeAssignment extends Module { + BusSliceTemporaryToAssignSubsetRangeAssignment({ + bool receiverIsOutput = false, + bool driveLowBits = true, + }) : super(name: 'bus_slice_temporary_to_assign_subset_range_assignment') { + final src = addInput('src', Logic(width: 16), width: 16); + final dst = receiverIsOutput + ? addOutput('y', width: 8) + : Logic(width: 8, name: 'dst'); + final srcSlice = + Logic(width: 4, name: 'src_slice', naming: Naming.mergeable); + + if (driveLowBits) { + for (var index = 0; index < 4; index++) { + dst.assignSubset([src[index]], start: index); + } + } + srcSlice <= src.getRange(11, 15); + for (var index = 0; index < 4; index++) { + dst.assignSubset([srcSlice[index]], start: index + 4); + } + + if (!receiverIsOutput) { + addOutput('y', width: 8) <= dst; + } + } +} + +/// Uses the same selected bus bits both for [Logic.assignSubset] and for other +/// submodule inputs. Range collapse must not delete the bit-select helpers +/// needed by those other consumers. +class BusSubsetBitsWithExtraConsumers extends Module { + BusSubsetBitsWithExtraConsumers() + : super(name: 'bus_subset_bits_with_extra_consumers') { + final src = addInput('src', Logic(width: 8), width: 8); + final dst = Logic(width: 4, name: 'dst'); + final inverted = []; + + for (var index = 0; index < 4; index++) { + final selected = src[index + 2]; + dst.assignSubset([selected], start: index); + inverted.add(InverterMod(selected).o); + } + + addOutput('y', width: 4) <= dst; + addOutput('z', width: 4) <= inverted.rswizzle(); + } +} + +/// Assigns two sparse contiguous bus ranges into a flat bus through +/// [Logic.assignSubset]. +class SparseBusRunsToAssignSubsetRangeAssignment extends Module { + SparseBusRunsToAssignSubsetRangeAssignment() + : super(name: 'sparse_bus_runs_to_assign_subset_range_assignment') { + final srcA = addInput('srcA', Logic(width: 32), width: 32); + final srcB = addInput('srcB', Logic(width: 16), width: 16); + final dst = Logic(width: 64, name: 'dst'); + + for (var index = 0; index < 12; index++) { + dst.assignSubset([srcA[index + 4]], start: index + 20); + } + for (var index = 0; index < 12; index++) { + dst.assignSubset([srcB[index]], start: index + 44); + } + + addOutput('y', width: 64) <= dst; + } +} + +/// Builds a packed bus from a live lower range and a constant-backed upper +/// range, then passes the whole bus to a child input. +class TiedRangeToAssignSubsetRangeAssignment extends Module { + TiedRangeToAssignSubsetRangeAssignment({ + Naming tieNaming = Naming.mergeable, + Naming busNaming = Naming.mergeable, + }) : super(name: 'tied_range_to_assign_subset_range_assignment') { + final source = addInput('source', Logic(width: 24), width: 24); + final tie = Logic(width: 8, name: 'tie', naming: tieNaming); + final bus = Logic(width: 32, name: 'bus', naming: busNaming); + + tie <= Const(0, width: 8); + bus + ..assignSubset(source.elements) + ..assignSubset(tie.elements, start: 24); + + final consumer = WholeBusChild(bus, n: 32); + addOutput('y', width: 32) <= consumer.output('mirror'); + } +} + +/// Produces the live range for [TiedSiblingRangeToAssignSubsetAssignment]. +class TiedRangeProducer extends Module { + TiedRangeProducer() : super(name: 'tied_range_producer') { + final seed = addInput('seed', Logic(width: 24), width: 24); + addOutput('result', width: 24) <= seed; + } +} + +/// Builds a packed bus from a sibling output and a constant-backed upper +/// range, then passes the whole bus to another sibling input. +class TiedSiblingRangeToAssignSubsetAssignment extends Module { + TiedSiblingRangeToAssignSubsetAssignment() + : super(name: 'tied_sibling_range_to_assign_subset_assignment') { + final seed = addInput('seed', Logic(width: 24), width: 24); + final tie = Logic( + width: 8, + name: 'tie', + naming: Naming.mergeable, + ); + final bus = Logic( + width: 32, + name: 'bus', + naming: Naming.mergeable, + ); + + final producer = TiedRangeProducer(); + producer.inputSource('seed') <= seed; + + tie <= Const(0, width: 8); + bus + ..assignSubset(producer.output('result').elements) + ..assignSubset(tie.elements, start: 24); + + final consumer = WholeBusChild(bus, n: 32); + addOutput('y', width: 32) <= consumer.output('mirror'); + } +} + +/// Late-drives a child input source from a sibling output and a constant-backed +/// upper range. +class TiedSiblingRangeToLateInputSource extends Module { + TiedSiblingRangeToLateInputSource() + : super(name: 'tied_sibling_range_to_late_input_source') { + final seed = addInput('seed', Logic(width: 24), width: 24); + final tie = Const(0, width: 8).named( + 'tie', + naming: Naming.mergeable, + ); + final consumer = WholeBusChild(Logic(width: 32), n: 32); + final producer = TiedRangeProducer(); + + producer.inputSource('seed') <= seed; + consumer.inputSource('data') + ..assignSubset(producer.output('result').elements) + ..assignSubset(tie.elements, start: 24); + + addOutput('y', width: 32) <= consumer.output('mirror'); + } +} + +/// Produces independently mapped scalar outputs for +/// [ScalarSiblingOutputsWithNamedTieTop]. +class ScalarRangeProducer extends Module { + ScalarRangeProducer(this.liveWidth) : super(name: 'scalar_range_producer') { + final seed = addInput('seed', Logic(width: liveWidth), width: liveWidth); + for (var index = 0; index < liveWidth; index++) { + addOutput('bit$index') <= seed[index]; + } + } + + final int liveWidth; +} + +/// Late-drives a child input from scalar sibling outputs followed by a named +/// constant tie-off. +class ScalarSiblingOutputsWithNamedTieTop extends Module { + ScalarSiblingOutputsWithNamedTieTop() + : super(name: 'scalar_sibling_outputs_with_named_tie_top') { + final seed = addInput('seed', Logic(width: liveWidth), width: liveWidth); + final tie = Const(0, width: tieWidth).named( + 'tie', + naming: Naming.mergeable, + ); + final consumer = WholeBusChild(Logic(width: busWidth), n: busWidth); + final producer = ScalarRangeProducer(liveWidth); + + producer.inputSource('seed') <= seed; + for (var index = 0; index < liveWidth; index++) { + consumer.inputSource('data').assignSubset( + [producer.output('bit$index')], + start: index, + ); + } + consumer.inputSource('data').assignSubset(tie.elements, start: liveWidth); + + addOutput('y', width: busWidth) <= consumer.output('mirror'); + } + + static const busWidth = 8; + static const tieWidth = 3; + static const liveWidth = busWidth - tieWidth; +} + +/// Builds a packed bus with a named constant range between live ranges and a +/// scalar sibling output mapped to the top bit. +class InteriorNamedTieWithMappedOutputTop extends Module { + InteriorNamedTieWithMappedOutputTop() + : super(name: 'interior_named_tie_with_mapped_output_top') { + final low = addInput('low', Logic(width: 4), width: 4); + final high = addInput('high', Logic(width: 8), width: 8); + final seed = addInput('seed', Logic()); + final tie = Const(0, width: 3).named( + 'tie', + naming: Naming.mergeable, + ); + final bus = Logic(width: 16, name: 'bus'); + final producer = SiblingBitProducer(); + + producer.inputSource('seed') <= seed; + final aliasedResult = producer.output('result')[0]; + bus + ..assignSubset(low.elements) + ..assignSubset(tie.elements, start: 4) + ..assignSubset(high.elements, start: 7) + ..assignSubset([aliasedResult], start: 15); + + final consumer = WholeBusChild(bus, n: 16); + final secondConsumer = WholeBusChild(bus, n: 16); + addOutput('y', width: 16) <= consumer.output('mirror'); + addOutput('z', width: 16) <= secondConsumer.output('mirror'); + } +} + +/// Mixes repeated nonzero constant ranges with an interior sibling output. +/// The child can be connected through an explicit bus or late-driven directly, +/// and the sibling output can optionally have an additional consumer. +class RepeatedConstantsAndSiblingOutputTop extends Module { + RepeatedConstantsAndSiblingOutputTop({ + required bool lateInput, + required bool fanout, + }) : super(name: 'repeated_constants_and_sibling_output_top') { + final source = addInput('source', Logic(width: 2), width: 2); + final seed = addInput('seed', Logic()); + final tie2 = Const(2, width: 2).named( + 'tie2', + naming: Naming.mergeable, + ); + final tie3 = Const(5, width: 3).named( + 'tie3', + naming: Naming.mergeable, + ); + final producer = SiblingBitProducer(); + producer.inputSource('seed') <= seed; + final result = producer.output('result'); + + late final Logic target; + late final WholeBusChild consumer; + if (lateInput) { + consumer = WholeBusChild(Logic(width: 12), n: 12); + target = consumer.inputSource('data'); + } else { + target = Logic( + width: 12, + name: 'bus', + naming: Naming.mergeable, + ); + consumer = WholeBusChild(target, n: 12); + } + + target + ..assignSubset(tie2.elements) + ..assignSubset(source.elements, start: 2) + ..assignSubset([result], start: 4) + ..assignSubset(tie3.elements, start: 5) + ..assignSubset(source.elements, start: 8) + ..assignSubset(tie2.elements, start: 10); + + addOutput('y', width: 12) <= consumer.output('mirror'); + if (fanout) { + addOutput('fanout') <= result; + } + } +} + +/// Mixes unknown and floating constant ranges with live bits in one packed +/// child input. +class InvalidConstantsToAssignSubsetTop extends Module { + InvalidConstantsToAssignSubsetTop() + : super(name: 'invalid_constants_to_assign_subset_top') { + final source = addInput('source', Logic(width: 2), width: 2); + final unknown = Const(LogicValue.filled(2, LogicValue.x)).named( + 'unknown', + naming: Naming.mergeable, + ); + final floating = Const(LogicValue.filled(2, LogicValue.z)).named( + 'floating', + naming: Naming.mergeable, + ); + final bus = Logic( + width: 6, + name: 'bus', + naming: Naming.mergeable, + ) + ..assignSubset(source.elements) + ..assignSubset(unknown.elements, start: 2) + ..assignSubset(floating.elements, start: 4); + + final consumer = WholeBusChild(bus, n: 6); + addOutput('y', width: 6) <= consumer.output('mirror'); + } +} + +/// Assigns contiguous direct and temporary-sliced bus ranges from an internal +/// source into a flat bus through [Logic.assignSubset]. +class InternalBusRunsToAssignSubsetRangeAssignment extends Module { + InternalBusRunsToAssignSubsetRangeAssignment({bool computedSource = false}) + : super(name: 'internal_bus_runs_to_assign_subset_range_assignment') { + final src = addInput('src', Logic(width: 32), width: 32); + final srcStage = + Logic(width: 32, name: 'srcStage', naming: Naming.mergeable); + final srcLow = Logic(width: 8, name: 'srcLow', naming: Naming.mergeable); + final srcHigh = Logic(width: 16, name: 'srcHigh', naming: Naming.mergeable); + final dst = Logic(width: 64, name: 'dst'); + + srcStage <= (computedSource ? ~src : src); + for (var index = 0; index < 8; index++) { + dst.assignSubset([srcStage[index]], start: index + 13); + } + srcLow <= srcStage.getRange(8, 16); + for (var index = 0; index < 8; index++) { + dst.assignSubset([srcLow[index]], start: index + 21); + } + srcHigh <= srcStage.getRange(16, 32); + for (var index = 0; index < 16; index++) { + dst.assignSubset([srcHigh[index]], start: index + 29); + } + + addOutput('y', width: 64) <= dst; + } +} + +/// Partially assigns a contiguous run from a temporary slice while also using +/// one bit of that slice elsewhere. The run should collapse, but the slice +/// helper must remain live for the extra consumer. +class PartialSliceWithExtraConsumer extends Module { + PartialSliceWithExtraConsumer() + : super(name: 'partial_slice_with_extra_consumer') { + final src = addInput('src', Logic(width: 16), width: 16); + final enable = addInput('enable', Logic()); + final slice = Logic(width: 8, name: 'slice', naming: Naming.mergeable); + final dst = Logic(width: 12, name: 'dst'); + + slice <= src.getRange(4, 12); + for (var index = 2; index <= 5; index++) { + dst.assignSubset([slice[index]], start: index + 3); + } + + addOutput('y', width: 12) <= dst; + addOutput('z') <= enable & ~slice[3]; + } +} + +/// Assigns a temporary flat bus slice into wide array elements. +class WideTemporarySliceToArrayWords extends Module { + WideTemporarySliceToArrayWords({bool extraConsumers = false}) + : super(name: 'wide_temporary_slice_to_array_words') { + final src = addInput('src', Logic(width: 128), width: 128); + final srcSlice = + Logic(width: 64, name: 'src_slice', naming: Naming.mergeable); + final dst = addOutputArray('y', dimensions: [4], elementWidth: 16); + final inverted = []; + + srcSlice <= src.getRange(32, 96); + final words = [ + srcSlice.getRange(0, 16), + srcSlice.getRange(16, 32), + srcSlice.getRange(32, 48), + srcSlice.getRange(48, 64), + ]; + dst.elements[1] <= words[1]; + dst.elements[0] <= words[0]; + dst.elements[2] <= words[2]; + dst.elements[3] <= words[3]; + + if (extraConsumers) { + for (final word in words) { + inverted.add(InverterMod(word, width: 16).o); + } + addOutput('z', width: 64) <= inverted.rswizzle(); + } + } +} + +/// Uses a manually-created array with a subset-like name. +class ManualSubsetNamedArrayRangeAssignment extends Module { + ManualSubsetNamedArrayRangeAssignment() + : super(name: 'manual_subset_named_array_range_assignment') { + final src = addInputArray('src', LogicArray([6], 1), dimensions: [6]); + final intermediate = + LogicArray([6], 1, name: 'manual_subset', naming: Naming.unnamed); + + for (var index = 2; index <= 4; index++) { + intermediate.elements[index] <= src.elements[index]; + } + + addOutput('y', width: 6) <= intermediate.elements.rswizzle(); + } +} + +/// Partially assigns a packed inner dimension of a two-dimensional array. +class PartialInnerArrayRangeAssignment extends Module { + PartialInnerArrayRangeAssignment({int numUnpackedDimensions = 0}) + : super(name: 'partial_inner_array_range_assignment') { + final src = addInputArray( + 'src', + LogicArray([2, 4], 1, numUnpackedDimensions: numUnpackedDimensions), + dimensions: [2, 4], + numUnpackedDimensions: numUnpackedDimensions, + ); + final dst = LogicArray([2, 4], 1, + name: 'dst', numUnpackedDimensions: numUnpackedDimensions); + + final srcRow = src.elements[1] as LogicArray; + final dstRow = dst.elements[1] as LogicArray; + for (var index = 1; index <= 3; index++) { + dstRow.elements[index] <= srcRow.elements[index]; + } + + addOutput('y', width: 8) <= dst.leafElements.rswizzle(); + } +} + +/// Partially assigns an unpacked one-dimensional array, which must not be +/// collapsed into a packed slice. +class PartialUnpackedArrayRangeAssignment extends Module { + PartialUnpackedArrayRangeAssignment() + : super(name: 'partial_unpacked_array_range_assignment') { + final src = addInputArray( + 'src', + LogicArray([6], 1, numUnpackedDimensions: 1), + dimensions: [6], + numUnpackedDimensions: 1, + ); + final dst = LogicArray([6], 1, name: 'dst', numUnpackedDimensions: 1); + + for (var index = 2; index <= 4; index++) { + dst.elements[index] <= src.elements[index]; + } + + addOutput('y', width: 6) <= dst.leafElements.rswizzle(); + } +} + +/// Partially assigns multi-bit array elements, which must not be collapsed by +/// the one-bit range assignment optimization. +class PartialWideArrayRangeAssignment extends Module { + PartialWideArrayRangeAssignment() + : super(name: 'partial_wide_array_range_assignment') { + final src = addInputArray( + 'src', + LogicArray([4], 2), + dimensions: [4], + elementWidth: 2, + ); + final dst = LogicArray([4], 2, name: 'dst'); + + for (var index = 1; index <= 2; index++) { + dst.elements[index] <= src.elements[index]; + } + + addOutput('y', width: 8) <= dst.leafElements.rswizzle(); + } +} + +/// Partially connects net array elements; range collapse must leave these for +/// the net connection flow instead of emitting procedural assignments. +class PartialNetArrayRangeAssignment extends Module { + PartialNetArrayRangeAssignment() + : super(name: 'partial_net_array_range_assignment') { + final src = addInOutArray( + 'src', + LogicArray.net([6], 1), + dimensions: [6], + ); + final mirror = addInOut('mirror', LogicNet(width: 6), width: 6); + final dst = LogicArray.net([6], 1, name: 'dst'); + + for (var index = 2; index <= 4; index++) { + dst.elements[index] <= src.elements[index]; + } + + mirror <= dst.leafElements.rswizzle(); + } +} + +/// Partially connects a flat net bus through bit selections; this is outside +/// array range collapse and should stay in the net connection flow. +class PartialLogicNetRangeAssignment extends Module { + PartialLogicNetRangeAssignment() + : super(name: 'partial_logic_net_range_assignment') { + final src = addInOut('src', LogicNet(width: 6), width: 6); + final mirror = addInOut('mirror', LogicNet(width: 6), width: 6); + final dst = LogicNet(width: 6, name: 'dst'); + + for (var index = 2; index <= 4; index++) { + dst.slice(index, index) <= src.slice(index, index); + } + + mirror <= dst; + } +} + +/// Inverts a bus of the given `width`. +class InverterMod extends Module { + Logic get o => output('o'); + InverterMod(Logic i, {int width = 1}) { + i = addInput('i', i, width: width); + addOutput('o', width: width) <= ~i; + } +} + +/// Bidirectionally connects two nets of the given `width`. +class NetPassthrough extends Module { + NetPassthrough(Logic x, Logic y, {int width = 1}) { + x = addInOut('x', x, width: width); + y = addInOut('y', y, width: width); + x <= y; + } +} + +/// A flat input bus blasted into an array of `dimensions`/`elementWidth`, each +/// leaf element feeding an [InverterMod] (computing `~a`). The intermediate +/// array should disappear and the submodule ports should reference the input +/// bits directly. When `reversed`, leaves are consumed in reverse order. +class ArrayElementFanout extends Module { + ArrayElementFanout( + Logic a, { + List dimensions = const [4], + int elementWidth = 1, + bool reversed = false, + }) { + final total = dimensions.reduce((x, y) => x * y) * elementWidth; + a = addInput('a', a, width: total); + final arr = LogicArray(dimensions, elementWidth, + name: 'arr', naming: Naming.mergeable); + arr <= a; + + final leaves = arr.leafElements; + final results = []; + for (var i = 0; i < leaves.length; i++) { + final srcIdx = reversed ? leaves.length - 1 - i : i; + results.add(InverterMod(leaves[srcIdx], width: elementWidth).o); + } + + addOutput('y', width: total) <= results.rswizzle(); + } +} + +/// Net version of [ArrayElementFanout]: a flat inout bus blasted into a net +/// array whose leaves bidirectionally connect (via [NetPassthrough]) to an +/// output bus. The intermediate array and its `net_connect`s should disappear. +class NetArrayElementFanout extends Module { + NetArrayElementFanout( + LogicNet a, + LogicNet b, { + List dimensions = const [4], + int elementWidth = 1, + }) { + final total = dimensions.reduce((x, y) => x * y) * elementWidth; + a = addInOut('a', a, width: total); + b = addInOut('b', b, width: total); + final arr = LogicArray.net(dimensions, elementWidth, + name: 'arr', naming: Naming.mergeable); + arr <= a; + + final leaves = arr.leafElements; + for (var i = 0; i < leaves.length; i++) { + NetPassthrough( + leaves[i], b.getRange(i * elementWidth, (i + 1) * elementWidth), + width: elementWidth); + } + } +} + +/// An array where only some leaf elements are driven (the first and last are +/// left undriven), with the whole array reconstructed via a swizzle. This must +/// NOT be inlined away (partial inlining would change `x`/`z` behavior of the +/// undriven bits). +class PartiallyDrivenArray extends Module { + PartiallyDrivenArray(Logic a, {List dimensions = const [4]}) { + final total = dimensions.reduce((x, y) => x * y); + a = addInput('a', a, width: total - 2); + final arr = + LogicArray(dimensions, 1, name: 'arr', naming: Naming.mergeable); + + final leaves = arr.leafElements; + // drive everything except the first and last leaf + for (var i = 1; i < leaves.length - 1; i++) { + leaves[i] <= a[i - 1]; + } + + addOutput('y', width: total) <= leaves.rswizzle(); + } +} + +/// The array's leaves feed submodules, but the whole array is ALSO consumed as +/// an aggregate (assigned to an output array). Because of the aggregate use, +/// the elements must NOT be inlined and the array must remain declared. +class ArrayElementsWithAggregateUse extends Module { + ArrayElementsWithAggregateUse(Logic a) { + a = addInput('a', a, width: 4); + final arr = LogicArray([4], 1, name: 'arr', naming: Naming.mergeable); + arr <= a; + + final results = []; + for (final leaf in arr.leafElements) { + results.add(InverterMod(leaf).o); + } + addOutput('y', width: 4) <= results.rswizzle(); + + // whole-array (aggregate) use, which blocks element inlining + addOutputArray('arrCopy', dimensions: [4]) <= arr; + } +} + +/// An array provided as an input *port* whose leaves feed submodules. Port +/// array elements are not clearable, so the port must remain; generation must +/// still be correct. +class ArrayPortElementsToSubmodules extends Module { + ArrayPortElementsToSubmodules(LogicArray a) { + a = addInputArray('a', a, + dimensions: a.dimensions, elementWidth: a.elementWidth); + final results = []; + for (final leaf in a.leafElements) { + results.add(InverterMod(leaf, width: a.elementWidth).o); + } + addOutput('y', width: a.width) <= results.rswizzle(); + } +} + +/// A struct with a [LogicArray] field, provided as a port, whose array leaves +/// feed submodules. Struct-port array elements must NOT be inlined. +class StructWithArrayField extends LogicStructure { + final LogicArray arr; + final Logic flag; + + factory StructWithArrayField({String name = 'swaf'}) => + StructWithArrayField._( + LogicArray([4], 1, name: 'arr'), + Logic(name: 'flag'), + name: name, + ); + + StructWithArrayField._(this.arr, this.flag, {super.name}) + : super([arr, flag]); + + @override + StructWithArrayField clone({String? name}) => + StructWithArrayField(name: name ?? this.name); +} + +/// Feeds the leaves of a struct-port array field into submodules. +class StructArrayFieldToSubmodules extends Module { + StructArrayFieldToSubmodules(StructWithArrayField s) { + s = StructWithArrayField()..gets(addInput('s', s, width: s.width)); + final results = []; + for (final leaf in s.arr.leafElements) { + results.add(InverterMod(leaf).o); + } + addOutput('y', width: s.arr.width) <= results.rswizzle(); + } +} + class ArrayModuleWithNetIntermediates extends Module { ArrayModuleWithNetIntermediates(LogicArray a, LogicArray b) { a = addInOutArray('a', a, @@ -72,59 +908,2328 @@ class ArrayModuleWithNetIntermediates extends Module { elementWidth: a.elementWidth, numUnpackedDimensions: a.numUnpackedDimensions); - final intermediate = LogicArray.net( - a.dimensions, - a.elementWidth, - name: 'intermediate', - naming: Naming.reserved, - ); + final intermediate = LogicArray.net( + a.dimensions, + a.elementWidth, + name: 'intermediate', + naming: Naming.reserved, + ); + + b = addInOutArray('b', b, + dimensions: a.dimensions, + elementWidth: a.elementWidth, + numUnpackedDimensions: a.numUnpackedDimensions); + + intermediate <= a; + b <= intermediate; + } +} + +/// Child with a single array input port whose whole value is inverted to `y`. +class ArrayPortInvChild extends Module { + Logic get y => output('y'); + ArrayPortInvChild(LogicArray a, {int n = 4, int elementWidth = 1}) + : super(name: 'arr_inv_child') { + a = addInputArray('a', a, dimensions: [n], elementWidth: elementWidth); + addOutput('y', width: n * elementWidth) <= ~a.elements.rswizzle(); + } +} + +/// Observes the only element of a packed array input. +class SingleElementArrayConsumer extends Module { + SingleElementArrayConsumer() : super(name: 'single_element_array_consumer') { + final data = addInputArray( + 'data', + LogicArray([1], 8), + dimensions: [1], + elementWidth: 8, + ); + addOutput('observed', width: 8) <= data.elements.single; + } +} + +/// Late-drives a one-element packed array child input with a flat constant. +class ConstantToSingleElementArrayInputTop extends Module { + ConstantToSingleElementArrayInputTop({int value = 0}) + : super(name: 'constant_to_single_element_array_input_top') { + final consumer = SingleElementArrayConsumer(); + consumer.inputSource('data') <= Const(value, width: 8); + addOutput('y', width: 8) <= consumer.output('observed'); + } +} + +/// Parent feeding `n` individual signals (each `elementWidth` wide) into a +/// single child array port, element-by-element through a mergeable intermediate +/// array. `perm` optionally reorders which signal drives which element. +/// +/// The intermediate array (and all of its per-element assignments) should be +/// collapsed into a single inline concatenation on the child port. +class IndividualSignalsToArrayPort extends Module { + Logic get y => output('y'); + IndividualSignalsToArrayPort(List sigs, + {int elementWidth = 1, List? perm}) { + final n = sigs.length; + final ins = [ + for (var i = 0; i < n; i++) + addInput('sig$i', sigs[i], width: elementWidth) + ]; + final arr = + LogicArray([n], elementWidth, name: 'arr', naming: Naming.mergeable); + final child = ArrayPortInvChild(arr, n: n, elementWidth: elementWidth); + for (var i = 0; i < n; i++) { + arr.elements[i] <= ins[perm == null ? i : perm[i]]; + } + addOutput('y', width: n * elementWidth) <= child.y; + } +} + +/// Like [IndividualSignalsToArrayPort], but each array element is connected +/// through a mergeable intermediate before aggregate connection inlining runs. +class MergedSourcesToArrayPort extends Module { + Logic get y => output('y'); + MergedSourcesToArrayPort(List sigs, {int elementWidth = 1}) { + final n = sigs.length; + final ins = [ + for (var i = 0; i < n; i++) + addInput('sig$i', sigs[i], width: elementWidth) + ]; + final arr = + LogicArray([n], elementWidth, name: 'arr', naming: Naming.mergeable); + final child = ArrayPortInvChild(arr, n: n, elementWidth: elementWidth); + for (var i = 0; i < n; i++) { + final intermediate = Logic( + width: elementWidth, + name: 'intermediate$i', + naming: Naming.mergeable, + ); + arr.elements[i] <= intermediate; + intermediate <= ins[i]; + } + addOutput('y', width: n * elementWidth) <= child.y; + } +} + +/// Parent feeding ranged slices from one source bus into a single child array +/// port through a mergeable intermediate array. +class RangeSourcesToArrayPort extends Module { + Logic get y => output('y'); + RangeSourcesToArrayPort() { + const n = 4; + const elementWidth = 16; + final src = addInput('src', Logic(width: n * elementWidth), + width: n * elementWidth); + final srcSlice = Logic( + width: n * elementWidth, + name: 'srcSlice', + naming: Naming.mergeable, + ); + final arr = + LogicArray([n], elementWidth, name: 'arr', naming: Naming.mergeable); + final child = ArrayPortInvChild(arr, elementWidth: elementWidth); + + srcSlice <= src; + for (var i = 0; i < n; i++) { + arr.elements[i] <= + srcSlice.getRange(i * elementWidth, (i + 1) * elementWidth); + } + + addOutput('y', width: n * elementWidth) <= child.y; + } +} + +/// Child with a single inout net array port bidirectionally mirrored to `b`. +class ArrayPortNetChild extends Module { + ArrayPortNetChild(LogicArray a, LogicNet b, {int n = 4}) + : super(name: 'arr_net_child') { + a = addInOutArray('a', a, dimensions: [n]); + b = addInOut('b', b, width: n); + b <= a.elements.rswizzle(); + } +} + +/// Net version of [IndividualSignalsToArrayPort]: `n` individual inout nets are +/// connected element-by-element to a single child inout array port through a +/// mergeable intermediate net array. The intermediate array and its +/// `net_connect`s should be collapsed into a single inline concatenation. +class IndividualNetsToArrayPort extends Module { + IndividualNetsToArrayPort(List sigs, LogicNet out, + {List? perm}) { + final n = sigs.length; + final ins = [for (var i = 0; i < n; i++) addInOut('sig$i', sigs[i])]; + final o = addInOut('out', out, width: n); + final arr = LogicArray.net([n], 1, name: 'arr', naming: Naming.mergeable); + ArrayPortNetChild(arr, o, n: n); + for (var i = 0; i < n; i++) { + arr.elements[i] <= ins[perm == null ? i : perm[i]]; + } + } +} + +/// Builds a mergeable array from individual signals and uses it as a whole +/// twice (two child array ports), so the single-aggregate-use restriction +/// prevents collapsing and the array stays declared. +class MultiUseAggregate extends Module { + Logic get y => output('y'); + Logic get z => output('z'); + MultiUseAggregate(List sigs) { + final n = sigs.length; + final ins = [for (var i = 0; i < n; i++) addInput('sig$i', sigs[i])]; + final arr = LogicArray([n], 1, name: 'arr', naming: Naming.mergeable); + for (var i = 0; i < n; i++) { + arr.elements[i] <= ins[i]; + } + addOutput('y', width: n) <= ArrayPortInvChild(arr, n: n).y; + addOutput('z', width: n) <= ArrayPortInvChild(arr, n: n).y; + } +} + +/// Child with a single array output port whose whole value is the inverted +/// input bus (`a = ~x`). +class ArrayOutChild extends Module { + LogicArray get a => output('a') as LogicArray; + ArrayOutChild(Logic x, {int n = 4}) : super(name: 'array_out_child') { + x = addInput('x', x, width: n); + addOutputArray('a', dimensions: [n]) <= (~x).elements.rswizzle(); + } +} + +/// The opposite shape of the originally-reported issue (logic direction): a +/// submodule's array *output* port whose individual elements each drive a +/// separate single (scalar) output wire. This must generate correct +/// SystemVerilog. +class ArrayPortToIndividualSignals extends Module { + ArrayPortToIndividualSignals(Logic x, {int n = 4}) { + x = addInput('x', x, width: n); + final child = ArrayOutChild(x, n: n); + for (var i = 0; i < n; i++) { + addOutput('y$i') <= child.a.elements[i]; + } + } +} + +/// The opposite shape of the originally-reported issue (net direction): a +/// submodule's inout array port whose individual elements each connect to a +/// separate single (scalar) net. The intermediate array and its +/// `net_connect`s should collapse into a single inline concatenation. +class ArrayPortToIndividualNets extends Module { + ArrayPortToIndividualNets(List sigs, LogicNet b) { + final n = sigs.length; + final outs = [for (var i = 0; i < n; i++) addInOut('y$i', sigs[i])]; + b = addInOut('b', b, width: n); + final arr = LogicArray.net([n], 1, name: 'arr', naming: Naming.mergeable); + ArrayPortNetChild(arr, b, n: n); + for (var i = 0; i < n; i++) { + outs[i] <= arr.elements[i]; + } + } +} + +/// An intermediate mergeable array whose elements are all driven by elements of +/// one common parent array (here, reversed), then passed to a child array port. +/// The common-parent guard must leave this to the other collapsing mechanisms +/// rather than fabricating a redundant concatenation. +class RearrangeOneArray extends Module { + Logic get y => output('y'); + RearrangeOneArray(LogicArray src, {int n = 4}) { + src = addInputArray('src', src, dimensions: [n]); + final arr = LogicArray([n], 1, name: 'arr', naming: Naming.mergeable); + final child = ArrayPortInvChild(arr, n: n); + for (var i = 0; i < n; i++) { + arr.elements[i] <= src.elements[n - 1 - i]; + } + addOutput('y', width: n) <= child.y; + } +} + +/// A child whose array input port `a` is declared *expressionless*, so the +/// aggregate connection inlining must NOT inline a concatenation into it; the +/// intermediate array and its per-element assignments must remain. +class ExpressionlessArrayPortChild extends Module with SystemVerilog { + Logic get y => output('y'); + ExpressionlessArrayPortChild(LogicArray a, {int n = 4}) + : super(name: 'expressionless_child') { + a = addInputArray('a', a, dimensions: [n]); + addOutput('y', width: n) <= a.elements.rswizzle(); + } + + @override + List get expressionlessInputs => const ['a']; + + @override + String? definitionVerilog(String definitionType) => null; +} + +/// Feeds individual signals through a mergeable array into the expressionless +/// array port of [ExpressionlessArrayPortChild]. +class IndividualSignalsToExpressionlessPort extends Module { + Logic get y => output('y'); + IndividualSignalsToExpressionlessPort(List sigs) { + final n = sigs.length; + final ins = [for (var i = 0; i < n; i++) addInput('sig$i', sigs[i])]; + final arr = LogicArray([n], 1, name: 'arr', naming: Naming.mergeable); + final child = ExpressionlessArrayPortChild(arr, n: n); + for (var i = 0; i < n; i++) { + arr.elements[i] <= ins[i]; + } + addOutput('y', width: n) <= child.y; + } +} + +/// A child whose single inout net array port `data` is bidirectionally mirrored +/// to `mirror` (`mirror = data.rswizzle()`). +class WholeNetBusChild extends Module { + WholeNetBusChild(LogicNet data, LogicNet mirror, {int n = 8}) + : super(name: 'whole_net_bus_child') { + data = addInOut('data', data, width: n); + mirror = addInOut('mirror', mirror, width: n); + mirror <= data; + } +} + +/// Case A (flat whole-bus): a flat net bus `bus` whose every bit is tied +/// bit-by-bit to an individual net, and which is then passed *as a whole* to a +/// child inout port. The bus and all of its per-bit `net_connect`s should +/// collapse into a single inline concatenation of those nets on the child port. +/// +/// `busNaming` controls whether the intermediate bus is collapsible. +class WholeNetBusToPort extends Module { + WholeNetBusToPort(List nets, LogicNet mirror, + {Naming busNaming = Naming.mergeable}) + : super(name: 'whole_net_bus_to_port') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: busNaming); + for (var i = 0; i < n; i++) { + bus.slice(i, i) <= netPorts[i]; + } + WholeNetBusChild(bus, mirror, n: n); + } +} + +/// Like [WholeNetBusToPort], but one bus slice also feeds an inline gate +/// expression. Collapsing the whole bus must not leave that expression reading +/// an undriven subset helper. +class WholeNetBusToPortWithInlineSubsetConsumer extends Module { + WholeNetBusToPortWithInlineSubsetConsumer( + List nets, LogicNet mirror) + : super(name: 'whole_net_bus_to_port_with_inline_subset_consumer') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + final enable = addInput('enable', Logic()); + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus'); + for (var i = 0; i < n; i++) { + bus.slice(i, i) <= netPorts[i]; + } + + addOutput('z') <= enable & ~bus.slice(0, 0); + WholeNetBusChild(bus, mirror, n: n); + } +} + +/// Reads every bit of a net bus through subset helpers, while the whole bus is +/// also consumed by a child. These read-only helpers must not be mistaken for +/// bit definers and removed out from under the inline expression. +class WholeNetBusToPortWithReadOnlyInlineSubsetConsumer extends Module { + WholeNetBusToPortWithReadOnlyInlineSubsetConsumer(LogicNet mirror, + {int n = 4}) + : super( + name: + 'whole_net_bus_to_port_with_read_only_inline_subset_consumer') { + final enable = addInput('enable', Logic()); + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus'); + final guarded = []; + + for (var i = 0; i < n; i++) { + final selected = bus.slice(i, i); + guarded.add(enable & ~selected); + } + + addOutput('z', width: n) <= guarded.rswizzle(); + WholeNetBusChild(bus, mirror, n: n); + } +} + +/// Reproduces the current naming-order issue where temporary [BusSubset] +/// instances that will be collapsed still claim basenames before surviving +/// signals can use them. +class WholeNetBusCollapseNamingCollision extends Module { + WholeNetBusCollapseNamingCollision() + : super(name: 'whole_net_bus_collapse_naming_collision') { + final netPorts = [ + for (var i = 0; i < 2; i++) addInOut('net$i', LogicNet()), + ]; + final mirror = addInOut('mirror', LogicNet(width: 2), width: 2); + final bus = LogicNet(width: 2, name: 'bus'); + for (var i = 0; i < 2; i++) { + bus.slice(i, i) <= netPorts[i]; + } + WholeNetBusChild(bus, mirror, n: 2); + + final sig = addInput('sig', Logic()); + final busNamedSignal = Logic(name: 'bussubset'); + busNamedSignal <= sig; + addOutput('busSubsetOut') <= busNamedSignal; + } +} + +/// A child whose inout net *array* port `data` is bidirectionally mirrored to +/// `mirror` (`mirror = data.elements.rswizzle()`). +class ArrayNetBusChild extends Module { + ArrayNetBusChild(LogicNet bus, LogicNet mirror, {int n = 8}) + : super(name: 'array_net_bus_child') { + final data = addInOutArray('data', bus, dimensions: [n]); + mirror = addInOut('mirror', mirror, width: n); + mirror <= data.elements.rswizzle(); + } +} + +/// Case B (bit-wise pass-through into array port): a flat net bus `bus` whose +/// every bit is tied to an individual net, then passed to a child inout *array* +/// port. Because the bus bit-selects feed the array elements through +/// pass-through `BusSubset`s, the bus and all of its `net_connect`s should be +/// traced away and collapsed into a single inline concatenation of those nets +/// on the child port. +class BitwiseNetBusToArrayPort extends Module { + BitwiseNetBusToArrayPort(List nets, LogicNet mirror, + {Naming busNaming = Naming.mergeable}) + : super(name: 'bitwise_net_bus_to_array_port') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: busNaming); + for (var i = 0; i < n; i++) { + bus.slice(i, i) <= netPorts[i]; + } + ArrayNetBusChild(bus, mirror, n: n); + } +} + +/// Like [BitwiseNetBusToArrayPort], but the same subset helper that ties one +/// net into the bus also feeds an inline gate expression. +class BitwiseNetBusToArrayPortWithInlineSubsetConsumer extends Module { + BitwiseNetBusToArrayPortWithInlineSubsetConsumer( + List nets, LogicNet mirror) + : super( + name: 'bitwise_net_bus_to_array_port_with_inline_subset_consumer') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + final enable = addInput('enable', Logic()); + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus'); + for (var i = 0; i < n; i++) { + final selected = bus.slice(i, i); + selected <= netPorts[i]; + if (i == 0) { + addOutput('z') <= enable & ~selected; + } + } + ArrayNetBusChild(bus, mirror, n: n); + } +} + +/// A flat net bus passed as a whole to a child port, but also read by a second +/// consumer (another child). The extra whole use must prevent the bus from +/// collapsing. +class WholeNetBusMultiUse extends Module { + WholeNetBusMultiUse(List nets, LogicNet mirror1, LogicNet mirror2) + : super(name: 'whole_net_bus_multi_use') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + mirror1 = addInOut('mirror1', mirror1, width: n); + mirror2 = addInOut('mirror2', mirror2, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: Naming.mergeable); + for (var i = 0; i < n; i++) { + bus.slice(i, i) <= netPorts[i]; + } + WholeNetBusChild(bus, mirror1, n: n); + WholeNetBusChild(bus, mirror2, n: n); + } +} + +/// A flat net bus whose lower bits are tied to individual nets, but whose +/// top bit is tied to *another bit of itself* (`bus[n-1] <= bus[0]`). That +/// self-connection puts a second `BusSubset` definer on bit 0, so the bus must +/// *not* be collapsed (the "each bit driven exactly once" guard must reject +/// it). Sends the bus either as a whole (`toArray` false) or into an array +/// port (`toArray` true). +class SelfBitNetBus extends Module { + SelfBitNetBus(List nets, LogicNet mirror, {bool toArray = false}) + : super(name: 'self_bit_net_bus') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: Naming.mergeable); + for (var i = 0; i < n - 1; i++) { + bus.slice(i, i) <= netPorts[i]; + } + // top bit follows bit 0 of the same bus + bus.slice(n - 1, n - 1) <= bus.slice(0, 0); + if (toArray) { + ArrayNetBusChild(bus, mirror, n: n); + } else { + WholeNetBusChild(bus, mirror, n: n); + } + } +} + +/// A pure self-loop net bus: `bus[1] <= bus[0]` with no other drivers, passed +/// as a whole to a child port. Both bits merge into a single standalone net, +/// so the bus may safely collapse into `{M, M}` where `M` is that merged net +/// (it is *not* a slice of the deleted bus, so there is no dangling +/// self-reference). +class PureSelfLoopNetBus extends Module { + PureSelfLoopNetBus(LogicNet mirror, {bool toArray = false}) + : super(name: 'pure_self_loop_net_bus') { + mirror = addInOut('mirror', mirror, width: 2); + final bus = LogicNet(width: 2, name: 'bus', naming: Naming.mergeable); + bus.slice(1, 1) <= bus.slice(0, 0); + if (toArray) { + ArrayNetBusChild(bus, mirror, n: 2); + } else { + WholeNetBusChild(bus, mirror, n: 2); + } + } +} + +/// Models the `connectPorts(top.bit_i, child.netPort[i])` receiver scenario: +/// each external bit net is tied into one bit of a child's whole net bus via +/// [Logic.assignSubset]. `assignSubset` introduces an intermediate `*_subset` +/// net array whose elements are pure pass-throughs; those elements must be +/// forwarded directly into the child connection so that no per-bit +/// `net_connect` remains. +class AssignSubsetReceiver extends Module { + AssignSubsetReceiver(List nets, LogicNet mirror, + {Naming busNaming = Naming.mergeable}) + : super(name: 'assign_subset_receiver') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: busNaming); + WholeNetBusChild(bus, mirror, n: n); + for (var i = 0; i < n; i++) { + bus.assignSubset([netPorts[i]], start: i); + } + } +} + +/// Like [AssignSubsetReceiver] but assigns the external bits in non-monotonic +/// order, exercising that the forwarding preserves per-bit positions. +class AssignSubsetReceiverScrambled extends Module { + AssignSubsetReceiverScrambled(List nets, LogicNet mirror) + : super(name: 'assign_subset_receiver_scrambled') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: Naming.mergeable); + WholeNetBusChild(bus, mirror, n: n); + // assign in a scrambled order; each still targets its own bit + for (final i in [3, 0, 2, 1].where((i) => i < n)) { + bus.assignSubset([netPorts[i]], start: i); + } + // cover any remaining bits in order (for n != 4) + for (var i = 4; i < n; i++) { + bus.assignSubset([netPorts[i]], start: i); + } + } +} + +/// The driver-direction counterpart of [AssignSubsetReceiver]: each external +/// bit net *receives* a one-bit slice of a child's whole net bus via +/// `assignSubset`. The per-bit `*_subset` pass-throughs must still be +/// forwarded away so the whole connection collapses with no per-bit +/// `net_connect`. +class AssignSubsetDriver extends Module { + AssignSubsetDriver(List nets, LogicNet mirror, + {Naming busNaming = Naming.mergeable}) + : super(name: 'assign_subset_driver') { + final n = nets.length; + final netPorts = [ + for (var i = 0; i < n; i++) addInOut('net$i', nets[i]), + ]; + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: busNaming); + WholeNetBusChild(bus, mirror, n: n); + for (var i = 0; i < n; i++) { + netPorts[i].assignSubset([bus.slice(i, i)]); + } + } +} + +/// A non-net child mirroring a whole input bus `data` to output `mirror`. +class WholeBusChild extends Module { + WholeBusChild(Logic data, {int n = 4}) : super(name: 'whole_bus_child') { + data = addInput('data', data, width: n); + addOutput('mirror', width: n) <= data; + } +} + +/// A non-net child mirroring an input *array* bus `data` to output `mirror`. +class ArrayBusChild extends Module { + ArrayBusChild(Logic data, {int n = 4}) : super(name: 'array_bus_child') { + final d = addInputArray('data', data, dimensions: [n]); + addOutput('mirror', width: n) <= d.elements.rswizzle(); + } +} + +/// How individual bits are tied into the intermediate bus in the kitchen-sink +/// collapse modules. +enum TieMechanism { + /// `bus.slice(i, i) <= bit[i]` (nets only — non-net slices are read-only). + directSlice, + + /// `bus.assignSubset([bit[i]], start: i)` (the receiver direction). + subsetReceiver, + + /// `bit[i].assignSubset([bus.slice(i, i)])` (the driver direction; nets + /// only). + subsetDriver, +} + +/// A configuration of independent toggles describing one collapse scenario, so +/// a single parameterized module can exercise every combination of them. +class CollapseConfig { + /// Whether the signals/bus are nets (`LogicNet`) or plain `Logic`. + final bool isNet; + + /// How the individual bits are tied into the bus. + final TieMechanism mechanism; + + /// Whether the bus is consumed via a child *array* port (vs a whole port). + final bool toArray; + + /// Whether the bits are tied in a non-monotonic order. + final bool scrambled; + + /// Whether the bus may collapse (`Naming.mergeable`) or must be preserved + /// (`Naming.renameable`). + final bool collapsibleBus; + + /// Whether only the low half of the bus is driven (undriven bits stay `z`). + final bool partial; + + /// Whether a second consumer reads the whole bus (which blocks collapse). + final bool multiUse; + + const CollapseConfig({ + required this.isNet, + required this.mechanism, + required this.toArray, + required this.scrambled, + required this.collapsibleBus, + required this.partial, + required this.multiUse, + }); + + /// The bus width used by the kitchen-sink modules. + static const width = 4; + + /// The number of bits actually tied into the bus (half when [partial]). + int get driven => partial ? width ~/ 2 : width; + + /// The intermediate bus and any `*_subset` arrays should fully dissolve into + /// a single inline concatenation only when the bus is collapsible, drives a + /// *whole* (non-array) child port, and there is no partial drive or extra + /// use. Array ports are bit-blasted into per-element connections, so the + /// bus survives as a named intermediate there. + bool get fullyCollapses => + collapsibleBus && !partial && !multiUse && !toArray; + + /// Whether generated `*_subset` pass-through arrays can be forwarded away. + /// + /// Driver-direction forwarding traces through and removes the intermediate + /// bus, so a preserved bus also preserves those subset arrays. + bool get noSubset => + !partial && + !multiUse && + (mechanism != TieMechanism.subsetDriver || collapsibleBus); + + String get description => [ + if (isNet) 'net' else 'logic', + mechanism.name, + if (toArray) 'arrayPort' else 'wholePort', + if (scrambled) 'scrambled' else 'inOrder', + if (collapsibleBus) 'mergeableBus' else 'renameableBus', + if (partial) 'partial' else 'full', + if (multiUse) 'multiUse' else 'singleUse', + ].join('/'); +} + +/// A deterministic order (possibly non-monotonic) in which to tie the [k] bits. +List _tieOrder(int k, {required bool scrambled}) { + if (!scrambled) { + return [for (var i = 0; i < k; i++) i]; + } + return [ + ...[3, 0, 2, 1].where((i) => i < k), + for (var i = 4; i < k; i++) i, + ]; +} + +/// The net (`LogicNet`) kitchen-sink: ties `k` external nets into an +/// intermediate bus via [CollapseConfig.mechanism], then mirrors the bus to +/// one (or two, when [CollapseConfig.multiUse]) child(ren). Because nets are +/// bidirectional, driving the external nets propagates through to every mirror +/// regardless of which `assignSubset` direction is used. +class NetKitchenSink extends Module { + final CollapseConfig config; + NetKitchenSink(this.config, List nets, LogicNet mirror1, + [LogicNet? mirror2]) + : super(name: 'net_kitchen_sink') { + const n = CollapseConfig.width; + final k = config.driven; + final netPorts = [ + for (var i = 0; i < k; i++) addInOut('net$i', nets[i]), + ]; + final bus = LogicNet( + width: n, + name: 'bus', + naming: config.collapsibleBus ? Naming.mergeable : Naming.renameable, + ); + for (final i in _tieOrder(k, scrambled: config.scrambled)) { + switch (config.mechanism) { + case TieMechanism.directSlice: + bus.slice(i, i) <= netPorts[i]; + case TieMechanism.subsetReceiver: + bus.assignSubset([netPorts[i]], start: i); + case TieMechanism.subsetDriver: + netPorts[i].assignSubset([bus.slice(i, i)]); + } + } + _consume(bus, addInOut('mirror1', mirror1, width: n), n); + if (config.multiUse) { + _consume(bus, addInOut('mirror2', mirror2!, width: n), n); + } + } + + void _consume(LogicNet bus, LogicNet mirror, int n) { + if (config.toArray) { + ArrayNetBusChild(bus, mirror, n: n); + } else { + WholeNetBusChild(bus, mirror, n: n); + } + } +} + +/// The non-net (`Logic`) kitchen-sink: ties external input bits into an +/// intermediate bus via `assignSubset` (the only valid non-net mechanism), +/// then mirrors the bus to one (or two, when [CollapseConfig.multiUse]) +/// child(ren) whose outputs are surfaced on `y` (and `y2`). +class LogicKitchenSink extends Module { + final CollapseConfig config; + LogicKitchenSink(this.config, List bits) + : super(name: 'logic_kitchen_sink') { + const n = CollapseConfig.width; + final ins = [for (var i = 0; i < n; i++) addInput('b$i', bits[i])]; + final bus = Logic( + width: n, + name: 'bus', + naming: config.collapsibleBus ? Naming.mergeable : Naming.renameable, + ); + for (final i in _tieOrder(n, scrambled: config.scrambled)) { + bus.assignSubset([ins[i]], start: i); + } + addOutput('y', width: n) <= _consume(bus, n); + if (config.multiUse) { + addOutput('y2', width: n) <= _consume(bus, n); + } + } + + Logic _consume(Logic bus, int n) => config.toArray + ? (ArrayBusChild(bus, n: n).output('mirror')) + : (WholeBusChild(bus, n: n).output('mirror')); +} + +/// A non-net child whose input `i` is inverted onto output `o`. +class LogicInvChild extends Module { + LogicInvChild(Logic i, {int n = 4}) : super(name: 'logic_inv_child') { + i = addInput('i', i, width: n); + addOutput('o', width: n) <= ~i; + } +} + +/// A child whose input source can be driven after construction. +class LateSubsetInputChild extends Module { + LateSubsetInputChild({super.name = 'late_subset_input_child'}) { + addInput('data', Logic(width: 8), width: 8); + addOutput('out') <= input('data')[0]; + } +} + +/// Drives a child input source late using [Logic.assignSubset]. +class LateSubsetInputTop extends Module { + LateSubsetInputTop() : super(name: 'late_subset_input_top') { + final source = addInput('source', Logic(width: 8), width: 8); + final child = LateSubsetInputChild(); + + child.inputSource('data').assignSubset(source.elements); + + addOutput('y') <= child.output('out'); + } +} + +/// Drives a child input source late from a slice of a wider source. +class LateSlicedSubsetInputTop extends Module { + LateSlicedSubsetInputTop() : super(name: 'late_sliced_subset_input_top') { + final source = addInput('source', Logic(width: 16), width: 16); + final child = LateSubsetInputChild(); + + child.inputSource('data').assignSubset([ + for (var index = 0; index < 8; index++) source[index + 4], + ]); + + addOutput('y') <= child.output('out'); + } +} + +/// A child that forwards its input to an output for sibling connection tests. +class SiblingSubsetProducer extends Module { + SiblingSubsetProducer({super.name = 'sibling_subset_producer'}) { + final seed = addInput('seed', Logic(width: 4), width: 4); + addOutput('result', width: 4) <= seed; + } +} + +/// A child that observes a bit inside a late-driven input source. +class SiblingSubsetConsumer extends Module { + SiblingSubsetConsumer({super.name = 'sibling_subset_consumer'}) { + addInput('data', Logic(width: 8), width: 8); + addOutput('observed') <= input('data')[2]; + } +} + +/// Width-matched sibling consumer for full-width subset mapping tests. +class SiblingFullSubsetConsumer extends Module { + SiblingFullSubsetConsumer({super.name = 'sibling_full_subset_consumer'}) { + addInput('data', Logic(width: 4), width: 4); + addOutput('observed') <= input('data')[0]; + } +} + +/// Drives one sibling's input source from another sibling's output subset. +class SiblingOutputToInputSubsetTop extends Module { + SiblingOutputToInputSubsetTop() + : super(name: 'sibling_output_to_input_subset_top') { + final source = addInput('source', Logic(width: 4), width: 4); + final producer = SiblingSubsetProducer(); + producer.inputSource('seed') <= source; + + final consumer = SiblingSubsetConsumer(); + consumer.inputSource('data').assignSubset( + producer.output('result').elements, + start: 2, + ); + + addOutput('y') <= consumer.output('observed'); + } +} + +/// Drives one sibling's full input source from another sibling's full output. +class SiblingFullOutputToInputSubsetTop extends Module { + SiblingFullOutputToInputSubsetTop() + : super(name: 'sibling_full_output_to_input_subset_top') { + final source = addInput('source', Logic(width: 4), width: 4); + final producer = SiblingSubsetProducer(); + producer.inputSource('seed') <= source; + + final consumer = SiblingFullSubsetConsumer(); + consumer.inputSource('data').assignSubset( + producer.output('result').elements, + ); + + addOutput('y') <= consumer.output('observed'); + } +} + +/// Produces one bit used by [SiblingOutputWithRangeAssignmentsTop]. +class SiblingBitProducer extends Module { + SiblingBitProducer() : super(name: 'sibling_bit_producer') { + final seed = addInput('seed', Logic()); + addOutput('result') <= seed; + } +} + +/// Observes the upper bit of a whole input bus. +class SiblingUpperBitConsumer extends Module { + SiblingUpperBitConsumer() : super(name: 'sibling_upper_bit_consumer') { + final data = addInput('data', Logic(width: 8), width: 8); + addOutput('observed') <= data[7]; + } +} + +/// Builds a bus from a contiguous input range and one sibling output, then +/// passes the whole bus to another sibling input. +class SiblingOutputWithRangeAssignmentsTop extends Module { + SiblingOutputWithRangeAssignmentsTop() + : super(name: 'sibling_output_with_range_assignments_top') { + final source = addInput('source', Logic(width: 7), width: 7); + final seed = addInput('seed', Logic()); + final bus = Logic(width: 8, name: 'bus'); + + final producer = SiblingBitProducer(); + producer.inputSource('seed') <= seed; + + bus + ..assignSubset(source.elements) + ..assignSubset([producer.output('result')], start: 7); + + final consumer = SiblingUpperBitConsumer(); + consumer.inputSource('data') <= bus; + + addOutput('y') <= consumer.output('observed'); + } +} + +/// Mirrors a whole input bus for mixed-source connection tests. +class SiblingBusConsumer extends Module { + SiblingBusConsumer() : super(name: 'sibling_bus_consumer') { + final data = addInput('data', Logic(width: 8), width: 8); + addOutput('observed', width: 8) <= data; + } +} + +/// Inserts one sibling output at [outputIndex], with input ranges on either +/// side where space permits. +class IndexedSiblingOutputWithRangeAssignmentsTop extends Module { + IndexedSiblingOutputWithRangeAssignmentsTop(this.outputIndex) + : assert( + outputIndex >= 0 && outputIndex < 8, + 'Output index must fit within the eight-bit bus.', + ), + super(name: 'indexed_sibling_output_with_range_assignments_top') { + final source = addInput('source', Logic(width: 7), width: 7); + final seed = addInput('seed', Logic()); + final bus = Logic(width: 8, name: 'bus'); + + final producer = SiblingBitProducer(); + producer.inputSource('seed') <= seed; + + if (outputIndex > 0) { + bus.assignSubset(source.elements.sublist(0, outputIndex)); + } + bus.assignSubset([producer.output('result')], start: outputIndex); + if (outputIndex < 7) { + bus.assignSubset( + source.elements.sublist(outputIndex), + start: outputIndex + 1, + ); + } + + final consumer = SiblingBusConsumer(); + consumer.inputSource('data') <= bus; + addOutput('y', width: 8) <= consumer.output('observed'); + } + + final int outputIndex; +} + +/// Inserts two independent sibling outputs among packed input ranges. +class MultipleSiblingOutputsWithRangeAssignmentsTop extends Module { + MultipleSiblingOutputsWithRangeAssignmentsTop() + : super(name: 'multiple_sibling_outputs_with_range_assignments_top') { + final sourceLow = addInput('sourceLow', Logic(width: 2), width: 2); + final sourceHigh = addInput('sourceHigh', Logic(width: 4), width: 4); + final seed0 = addInput('seed0', Logic()); + final seed1 = addInput('seed1', Logic()); + final bus = Logic( + width: 8, + name: 'bus', + naming: Naming.mergeable, + ); + + final producer0 = SiblingBitProducer(); + final producer1 = SiblingBitProducer(); + producer0.inputSource('seed') <= seed0; + producer1.inputSource('seed') <= seed1; + + bus + ..assignSubset(sourceLow.elements) + ..assignSubset([producer0.output('result')], start: 2) + ..assignSubset(sourceHigh.elements, start: 3) + ..assignSubset([producer1.output('result')], start: 7); + + final consumer = SiblingBusConsumer(); + consumer.inputSource('data') <= bus; + addOutput('y', width: 8) <= consumer.output('observed'); + } +} + +/// Uses one sibling output both inside a packed bus and as a separate output. +class FanoutSiblingOutputWithRangeAssignmentsTop extends Module { + FanoutSiblingOutputWithRangeAssignmentsTop() + : super(name: 'fanout_sibling_output_with_range_assignments_top') { + final source = addInput('source', Logic(width: 7), width: 7); + final seed = addInput('seed', Logic()); + final bus = Logic( + width: 8, + name: 'bus', + naming: Naming.mergeable, + ); + + final producer = SiblingBitProducer(); + producer.inputSource('seed') <= seed; + final result = producer.output('result'); + + bus + ..assignSubset(source.elements) + ..assignSubset([result], start: 7); + + final consumer = SiblingBusConsumer(); + consumer.inputSource('data') <= bus; + addOutput('y', width: 8) <= consumer.output('observed'); + addOutput('fanout') <= result; + } +} + +/// Inserts a four-bit sibling output between two packed input ranges. +class WideSiblingOutputWithRangeAssignmentsTop extends Module { + WideSiblingOutputWithRangeAssignmentsTop() + : super(name: 'wide_sibling_output_with_range_assignments_top') { + final source = addInput('source', Logic(width: 4), width: 4); + final seed = addInput('seed', Logic(width: 4), width: 4); + final bus = Logic(width: 8, name: 'bus'); + + final producer = SiblingSubsetProducer(); + producer.inputSource('seed') <= seed; + + bus + ..assignSubset(source.elements.sublist(0, 2)) + ..assignSubset(producer.output('result').elements, start: 2) + ..assignSubset(source.elements.sublist(2), start: 6); + + final consumer = SiblingBusConsumer(); + consumer.inputSource('data') <= bus; + addOutput('y', width: 8) <= consumer.output('observed'); + } +} + +/// Places a wide sibling output between repeated nonzero constant ranges and +/// also exposes that output separately. +class WideSiblingOutputWithConstantsAndFanoutTop extends Module { + WideSiblingOutputWithConstantsAndFanoutTop() + : super(name: 'wide_sibling_output_with_constants_and_fanout_top') { + final seed = addInput('seed', Logic(width: 4), width: 4); + final tie = Const(2, width: 2).named( + 'tie', + naming: Naming.mergeable, + ); + final bus = Logic( + width: 8, + name: 'bus', + naming: Naming.mergeable, + ); + final producer = SiblingSubsetProducer(); + producer.inputSource('seed') <= seed; + final result = producer.output('result'); + + bus + ..assignSubset(tie.elements) + ..assignSubset(result.elements, start: 2) + ..assignSubset(tie.elements, start: 6); + + final consumer = SiblingBusConsumer(); + consumer.inputSource('data') <= bus; + addOutput('y', width: 8) <= consumer.output('observed'); + addOutput('fanout', width: 4) <= result; + } +} + +/// Array-output sibling variant of [SiblingSubsetProducer]. +class SiblingArraySubsetProducer extends Module { + SiblingArraySubsetProducer({super.name = 'sibling_array_subset_producer'}) { + final seed = addInput('seed', Logic(width: 4), width: 4); + final result = addOutputArray('result', dimensions: [4]); + + for (var index = 0; index < 4; index++) { + result.elements[index] <= seed[index]; + } + } +} + +/// Array-input sibling variant of [SiblingSubsetConsumer]. +class SiblingArraySubsetConsumer extends Module { + SiblingArraySubsetConsumer({super.name = 'sibling_array_subset_consumer'}) { + final data = addInputArray('data', LogicArray([8], 1), dimensions: [8]); + addOutput('observed') <= data.elements[2]; + } +} + +/// Drives one sibling's input array source from another sibling's output array. +class SiblingArrayOutputToInputSubsetTop extends Module { + SiblingArrayOutputToInputSubsetTop() + : super(name: 'sibling_array_output_to_input_subset_top') { + final source = addInput('source', Logic(width: 4), width: 4); + final producer = SiblingArraySubsetProducer(); + producer.inputSource('seed') <= source; + + final consumer = SiblingArraySubsetConsumer(); + (consumer.inputSource('data') as LogicArray).assignSubset( + (producer.output('result') as LogicArray).elements, + start: 2, + ); + + addOutput('y') <= consumer.output('observed'); + } +} + +/// Small structure used for sibling subset mapping regressions. +class SiblingSubsetStruct extends LogicStructure { + final Logic low; + final Logic high; + + factory SiblingSubsetStruct({String name = 'sibling_subset_struct'}) => + SiblingSubsetStruct._( + Logic(name: 'low'), + Logic(name: 'high'), + name: name, + ); + + SiblingSubsetStruct._(this.low, this.high, {super.name}) : super([low, high]); + + @override + SiblingSubsetStruct clone({String? name}) => + SiblingSubsetStruct(name: name ?? this.name); +} + +/// Structure-output sibling variant of [SiblingSubsetProducer]. +class SiblingStructSubsetProducer extends Module { + SiblingStructSubsetProducer({super.name = 'sibling_struct_subset_producer'}) { + final seed = addInput('seed', Logic(width: 2), width: 2); + final result = addTypedOutput('result', SiblingSubsetStruct.new); + + result.low <= seed[0]; + result.high <= seed[1]; + } +} + +/// Structure-input sibling variant of [SiblingSubsetConsumer]. +class SiblingStructSubsetConsumer extends Module { + SiblingStructSubsetConsumer({super.name = 'sibling_struct_subset_consumer'}) { + final data = addTypedInput('data', SiblingSubsetStruct()); + addOutput('observed') <= data.high; + } +} + +/// Drives one sibling's input structure source from another sibling's output +/// structure. +class SiblingStructOutputToInputSubsetTop extends Module { + SiblingStructOutputToInputSubsetTop() + : super(name: 'sibling_struct_output_to_input_subset_top') { + final source = addInput('source', Logic(width: 2), width: 2); + final producer = SiblingStructSubsetProducer(); + producer.inputSource('seed') <= source; + + final consumer = SiblingStructSubsetConsumer(); + (consumer.inputSource('data') as SiblingSubsetStruct).assignSubset( + (producer.output('result') as SiblingSubsetStruct).elements, + ); + + addOutput('y') <= consumer.output('observed'); + } +} + +/// Inout sibling variant that exposes a net bus through a source mapping. +class SiblingInOutSubsetProducer extends Module { + SiblingInOutSubsetProducer({super.name = 'sibling_inout_subset_producer'}) { + addInOut('link', LogicNet(width: 4), width: 4); + } +} + +/// Inout sibling variant that observes a bit from an inout source mapping. +class SiblingInOutSubsetConsumer extends Module { + SiblingInOutSubsetConsumer({super.name = 'sibling_inout_subset_consumer'}) { + final data = addInOut('data', LogicNet(width: 8), width: 8); + addOutput('observed') <= data.slice(2, 2); + } +} + +/// Drives one sibling's inout source subset from another sibling's inout +/// source. +class SiblingInOutToInOutSubsetTop extends Module { + SiblingInOutToInOutSubsetTop() + : super(name: 'sibling_inout_to_inout_subset_top') { + final source = addInOut('source', LogicNet(width: 4), width: 4); + final producer = SiblingInOutSubsetProducer(); + producer.inOutSource('link') <= source; + + final consumer = SiblingInOutSubsetConsumer(); + consumer.inOutSource('data').assignSubset([ + for (var index = 0; index < 4; index++) + producer.inOutSource('link').slice(index, index), + ], start: 2); + + addOutput('y') <= consumer.output('observed'); + } +} + +/// Producer with scalar, array, structure, and inout ports for mixed boundary +/// mapping regressions. +class SiblingBoundaryProductProducer extends Module { + SiblingBoundaryProductProducer( + {super.name = 'sibling_boundary_product_producer'}) { + final seed = addInput('seed', Logic(width: 4), width: 4); + addOutput('wide', width: 4) <= seed; + + final arr = addOutputArray('arr', dimensions: [4]); + for (var index = 0; index < 4; index++) { + arr.elements[index] <= seed[index]; + } + + final pair = addTypedOutput('pair', SiblingSubsetStruct.new); + pair.low <= seed[0]; + pair.high <= seed[1]; + + addInOut('link', LogicNet(width: 4), width: 4); + } +} + +/// Consumer with mixed port shapes that observes one bit from each mapping. +class SiblingBoundaryProductConsumer extends Module { + SiblingBoundaryProductConsumer( + {super.name = 'sibling_boundary_product_consumer'}) { + addInput('wide_from_scalar', Logic(width: 10), width: 10); + final arrayFromScalar = addInputArray( + 'array_from_scalar', + LogicArray([10], 1), + dimensions: [10], + ); + final structFromArray = addTypedInput( + 'struct_from_array', + SiblingSubsetStruct(), + ); + addInput('wide_from_struct', Logic(width: 8), width: 8); + final netFromNet = addInOut('net_from_net', LogicNet(width: 10), width: 10); + + addOutput('scalar_bit') <= input('wide_from_scalar')[5]; + addOutput('array_bit') <= arrayFromScalar.elements[6]; + addOutput('struct_bit') <= structFromArray.high; + addOutput('wide_struct_bit') <= input('wide_from_struct')[4]; + addOutput('net_bit') <= netFromNet.slice(6, 6); + } +} + +/// Mixed sibling boundary fixture that exercises several source/destination +/// shape combinations in one pruning/collapse pass. +class SiblingBoundaryProductTop extends Module { + SiblingBoundaryProductTop() : super(name: 'sibling_boundary_product_top') { + final source = addInput('source', Logic(width: 4), width: 4); + final netSource = addInOut('net_source', LogicNet(width: 4), width: 4); + + final producer = SiblingBoundaryProductProducer(); + producer.inputSource('seed') <= source; + producer.inOutSource('link') <= netSource; + + final consumer = SiblingBoundaryProductConsumer(); + consumer.inputSource('wide_from_scalar').assignSubset( + producer.output('wide').elements, + start: 4, + ); + (consumer.inputSource('array_from_scalar') as LogicArray).assignSubset( + producer.output('wide').elements, + start: 4, + ); + (consumer.inputSource('struct_from_array') as SiblingSubsetStruct) + .assignSubset( + (producer.output('arr') as LogicArray).elements.sublist(1, 3), + ); + consumer.inputSource('wide_from_struct').assignSubset( + (producer.output('pair') as SiblingSubsetStruct).elements, + start: 3, + ); + consumer.inOutSource('net_from_net').assignSubset([ + for (var index = 0; index < 4; index++) + producer.inOutSource('link').slice(index, index), + ], start: 5); + + for (final outputName in [ + 'scalar_bit', + 'array_bit', + 'struct_bit', + 'wide_struct_bit', + 'net_bit', + ]) { + addOutput(outputName) <= consumer.output(outputName); + } + } +} + +/// Non-net (regular [Logic]) driver-direction `assignSubset`: each external bit +/// drives one bit of `sig` via `assignSubset`, and `sig` feeds a child input. +/// The intermediate `*_subset` array must be forwarded straight into the child +/// connection with no surviving `assign`. +class AssignSubsetLogicDriver extends Module { + AssignSubsetLogicDriver(List bits, {int n = 4}) + : super(name: 'assign_subset_logic_driver') { + final ins = [for (var i = 0; i < n; i++) addInput('b$i', bits[i])]; + final sig = Logic(width: n, name: 'sig', naming: Naming.mergeable); + final child = LogicInvChild(sig, n: n); + addOutput('y', width: n) <= child.output('o'); + for (var i = 0; i < n; i++) { + sig.assignSubset([ins[i]], start: i); + } + } +} + +/// Partial `assignSubset`: only the low half of the child bus is driven; the +/// high half stays undriven (`z`). Because not every element is a +/// pass-through, the intermediate `*_subset` array must be conservatively +/// preserved (no collapse) so the undriven high bits remain `z`. +class AssignSubsetPartial extends Module { + AssignSubsetPartial(List nets, LogicNet mirror, {int n = 4}) + : super(name: 'assign_subset_partial') { + final lo = n ~/ 2; + final netPorts = [ + for (var i = 0; i < lo; i++) addInOut('net$i', nets[i]), + ]; + mirror = addInOut('mirror', mirror, width: n); + final bus = LogicNet(width: n, name: 'bus', naming: Naming.mergeable); + WholeNetBusChild(bus, mirror, n: n); + for (var i = 0; i < lo; i++) { + bus.assignSubset([netPorts[i]], start: i); + } + } +} + +/// Returns the body of the last (top-level) module declaration in [sv], +/// avoiding false matches inside `endmodule`. +String _topModuleBody(String sv) { + final matches = RegExp(r'(?:^|\n)module ').allMatches(sv).toList(); + return sv.substring(matches.last.start); +} + +LogicValue _expectedPartialArrayRangeValue(int pattern, + {required bool reversed}) => + _expectedSparseValue(6, pattern, (dstIndex) { + if (dstIndex < 2 || dstIndex > 4) { + return null; + } + return reversed ? 6 - dstIndex : dstIndex; + }); + +LogicValue _expectedSparseValue( + int width, + int pattern, + int? Function(int dstIndex) srcIndexFor, +) { + final bits = []; + for (var dstIndex = width - 1; dstIndex >= 0; dstIndex--) { + final srcIndex = srcIndexFor(dstIndex); + bits.add(srcIndex == null ? 'z' : '${(pattern >> srcIndex) & 1}'); + } + return LogicValue.ofString(bits.join()); +} + +LogicValue _expectedWideTemporarySlice(int pattern) => + LogicValue.ofInt(pattern, 96).getRange(32, 96); + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('simple 1d collapse', () async { + final mod = SimpleLAPassthrough(LogicArray([4], 1)); + await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, contains('assign laOut = laIn;')); + }); + + test('array collapse for cross-module connection', () async { + final mod = ArrayTopMod(Logic()); + await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, contains(RegExp(r'ArraySubModIn.*\.inp\(inp\)'))); + expect(sv, contains(RegExp(r'ArraySubModOut.*\.arrOut\(inp\)'))); + }); + + test('array nets with intermediate collapse', () async { + final mod = ArrayModuleWithNetIntermediates( + LogicArray([3, 3], 1), LogicArray([3, 3], 1)); + await mod.build(); + + final sv = mod.generateSynth(); + expect(sv, + contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); + expect(sv, + contains('net_connect #(.WIDTH(9)) net_connect_0 (b, intermediate);')); + + final vectors = [ + Vector({'a': 0}, {'b': 0}), + Vector({'a': 123}, {'b': 123}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('partial array assignments collapse into range assignment', () async { + final mod = PartialArrayRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[4:2] = src[4:2];')); + expect(topBody, isNot(contains('assign dst[2] = src[2];'))); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('chained partial array range assignments collapse through intermediate', + () async { + final mod = ChainedPartialArrayRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[4:2] = src[4:2];')); + expect(topBody, isNot(contains('intermediate'))); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('chained range assignment composes contained subrange offsets', + () async { + final mod = ChainedSubrangeArrayRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[3:2] = src[6:5];')); + expect(topBody, isNot(contains('intermediate'))); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue( + 8, + pattern, + (dstIndex) => dstIndex >= 2 && dstIndex <= 3 ? dstIndex + 3 : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('three-deep chained range assignments collapse iteratively', () async { + final mod = ThreeDeepChainedPartialArrayRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[4:2] = src[4:2];')); + expect(topBody, isNot(contains('intermediate0'))); + expect(topBody, isNot(contains('intermediate1'))); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('long chained range assignments collapse without global rescans', + () async { + final mod = LongChainedPartialArrayRangeAssignment(); + await mod.build(); + final topBody = _topModuleBody(mod.generateSynth()); + + expect(topBody, contains('assign dst[4:2] = src[4:2];')); + expect(topBody, isNot(contains('intermediate'))); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('multi-use chained range intermediate stays expanded', () async { + final mod = ChainedPartialArrayRangeAssignment(exposeIntermediate: true); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); + expect(topBody, contains('assign dst[4:2] = intermediate[4:2];')); + expect(topBody, contains('assign intermediate[4:2] = src[4:2];')); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + 'z': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('renameable chained range intermediate stays expanded', () async { + final mod = ChainedPartialArrayRangeAssignment(intermediateNaming: null); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); + expect(topBody, contains('assign dst[4:2] = intermediate[4:2];')); + expect(topBody, contains('assign intermediate[4:2] = src[4:2];')); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('partial bus-to-array assignments collapse into range assignment', + () async { + final mod = PartialBusToArrayRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[5:2] = src[5:2];')); + expect(topBody, isNot(contains('bussubset'))); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue( + 8, + pattern, + (dstIndex) => dstIndex >= 2 && dstIndex <= 5 ? dstIndex : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('full array-to-bus assignSubset has no subset intermediate', () async { + final mod = ArrayToBusAssignSubsetRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains('assign dst[0]'))); + expect(topBody, contains('assign dst = src[7:0];')); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({'src': pattern}, {'y': pattern}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('partial array-to-bus assignSubset collapses into range assignment', + () async { + final mod = ArrayToBusAssignSubsetRangeAssignment(partial: true); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[5:2] = src[5:2];')); + expect(topBody, isNot(contains('_subset'))); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue( + 8, + pattern, + (dstIndex) => dstIndex >= 2 && dstIndex <= 5 ? dstIndex : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + for (final config in [ + (receiverIsOutput: false, driveLowBits: true, dstName: 'dst'), + (receiverIsOutput: true, driveLowBits: true, dstName: 'y'), + (receiverIsOutput: false, driveLowBits: false, dstName: 'dst'), + (receiverIsOutput: true, driveLowBits: false, dstName: 'y'), + ]) { + test( + 'bus slice temporary feeding assignSubset collapses into ranges ' + '(receiver is ${config.receiverIsOutput ? 'output' : 'internal'}, ' + '${config.driveLowBits ? 'full' : 'partial'} coverage)', () async { + final mod = BusSliceTemporaryToAssignSubsetRangeAssignment( + receiverIsOutput: config.receiverIsOutput, + driveLowBits: config.driveLowBits, + ); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + if (config.driveLowBits) { + expect(topBody, contains('assign ${config.dstName}[3:0] = src[3:0];')); + } else { + expect(topBody, isNot(contains('assign ${config.dstName}[3:0] ='))); + } + expect(topBody, contains('assign ${config.dstName}[7:4] = src[14:11];')); + expect(topBody, isNot(contains('src_slice'))); + expect(topBody, isNot(contains('_subset'))); + + final vectors = [ + for (final pattern in [0x0000, 0x1234, 0x5AA5, 0xFFFF]) + Vector({ + 'src': pattern + }, { + 'y': config.driveLowBits + ? LogicValue.ofInt( + (pattern & 0xF) | (((pattern >> 11) & 0xF) << 4), + 8, + ) + : _expectedSparseValue( + 8, + pattern, + (dstIndex) => dstIndex >= 4 ? dstIndex + 7 : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('bus subset helpers with extra consumers are preserved', () async { + final mod = BusSubsetBitsWithExtraConsumers(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[3:0] = src[5:2];')); + expect(topBody, contains(RegExp(r'\.i\([^)]*src'))); + + final vectors = [ + for (final pattern in [0x00, 0x3C, 0xA5, 0xFF]) + Vector({ + 'src': pattern, + }, { + 'y': (pattern >> 2) & 0xF, + 'z': (~((pattern >> 2) & 0xF)) & 0xF, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('partial slice helper with extra consumer is preserved', () async { + final mod = PartialSliceWithExtraConsumer(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[8:5] = src[9:6];')); + expect(topBody, contains('assign slice = src[11:4];')); + expect(topBody, contains('slice[3]')); + expect(topBody, isNot(contains(RegExp(r'assign dst\[[0-9]+\]')))); + + final vectors = [ + for (final pattern in [0x0000, 0x0080, 0x03c0, 0xffff]) + for (final enable in [0, 1]) + Vector({ + 'src': pattern, + 'enable': enable, + }, { + 'y': _expectedSparseValue( + 12, + pattern, + (dstIndex) => + dstIndex >= 5 && dstIndex <= 8 ? dstIndex + 1 : null, + ), + 'z': enable == 1 ? (~((pattern >> 7) & 1)) & 1 : 0, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sparse bus runs feeding assignSubset collapse independently', () async { + final mod = SparseBusRunsToAssignSubsetRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[31:20] = srcA[15:4];')); + expect(topBody, contains('assign dst[55:44] = srcB[11:0];')); + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains(RegExp(r'assign dst\[[0-9]+\]')))); + + LogicValue expectedValue(int srcA, int srcB) => _expectedSparseValue( + 64, + srcA, + (dstIndex) => dstIndex >= 20 && dstIndex <= 31 ? dstIndex - 16 : null, + ).withSet( + 44, + LogicValue.ofInt(srcB & 0xfff, 12), + ); + + final vectors = [ + for (final pattern in [0x00000000, 0x12345678, 0x89abcdef]) + Vector({ + 'srcA': pattern, + 'srcB': pattern >> 4, + }, { + 'y': expectedValue(pattern, pattern >> 4), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('constant-backed upper range remains tied off after collapse', () async { + final mod = TiedRangeToAssignSubsetRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.data(({')); + expect(topBody, contains("8'h0")); + expect(topBody, contains('source')); + expect(topBody, isNot(contains('logic [31:0] bus;'))); + expect(topBody, isNot(contains('logic [7:0] tie;'))); + expect(topBody, isNot(contains(RegExp(r'\bbus_subset\b')))); + + final vectors = [ + for (final pattern in [0x000000, 0x123456, 0xabcdef, 0xffffff]) + Vector({'source': pattern}, {'y': pattern}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + for (final tieNaming in [Naming.renameable, Naming.reserved]) { + test('${tieNaming.name} constant-backed range signal is preserved', + () async { + final mod = TiedRangeToAssignSubsetRangeAssignment( + tieNaming: tieNaming, + ); + await mod.build(); + final topBody = _topModuleBody(mod.generateSynth()); + + expect(topBody, contains('logic [7:0] tie;')); + expect(topBody, contains("assign tie = 8'h0;")); + + final vectors = [ + for (final pattern in [0x000000, 0x123456, 0xffffff]) + Vector({'source': pattern}, {'y': pattern}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('renameable packed range destination is preserved', () async { + final mod = TiedRangeToAssignSubsetRangeAssignment( + busNaming: Naming.renameable, + ); + await mod.build(); + final topBody = _topModuleBody(mod.generateSynth()); + + expect(topBody, contains('logic [31:0] bus;')); + expect(topBody, contains('.data(bus)')); + + final vectors = [ + for (final pattern in [0x000000, 0x123456, 0xffffff]) + Vector({'source': pattern}, {'y': pattern}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('constant-backed range concatenates with sibling output', () async { + final mod = TiedSiblingRangeToAssignSubsetAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.data(({')); + expect(topBody, contains("8'h0")); + expect(topBody, isNot(contains('.result()'))); + expect(topBody, isNot(contains('logic [31:0] bus;'))); + expect(topBody, isNot(contains('logic [7:0] tie;'))); + expect(topBody, isNot(contains(RegExp(r'\bbus_subset\b')))); + + final vectors = [ + for (final pattern in [0x000000, 0x123456, 0xabcdef, 0xffffff]) + Vector({'seed': pattern}, {'y': pattern}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('constant-backed range concatenates into late child input', () async { + final mod = TiedSiblingRangeToLateInputSource(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.data(({')); + expect(topBody, contains("8'h0")); + expect(topBody, isNot(contains('.result()'))); + expect(topBody, isNot(contains('logic [7:0] tie;'))); + expect(topBody, isNot(contains(RegExp(r'\bdata_subset\b')))); + + final vectors = [ + for (final pattern in [0x000000, 0x123456, 0xabcdef, 0xffffff]) + Vector({'seed': pattern}, {'y': pattern}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('named constant subsets survive scalar output collapse', () async { + final mod = ScalarSiblingOutputsWithNamedTieTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.data(({')); + expect(topBody, contains(RegExp(r"\d+'h0"))); + expect(topBody, isNot(contains('.bit0()'))); + expect(topBody, isNot(contains('.bit4()'))); + expect(topBody, isNot(contains('logic [7:0] data;'))); + expect(topBody, isNot(contains(RegExp(r'\bdata_subset\b')))); + + const liveWidth = ScalarSiblingOutputsWithNamedTieTop.liveWidth; + final maxValue = (BigInt.one << liveWidth) - BigInt.one; + final vectors = [ + for (final pattern in [ + BigInt.zero, + BigInt.one, + BigInt.from(0x5a5a5a) & maxValue, + maxValue, + ]) + Vector({'seed': pattern}, {'y': pattern}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('interior named constant range survives mapped sibling output', + () async { + final mod = InteriorNamedTieWithMappedOutputTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains("assign bus[6:4] = 3'h0;")); + expect(topBody, contains('.result(bus[15])')); + expect(topBody, isNot(contains('logic [2:0] tie;'))); + expect(topBody, isNot(contains(RegExp(r'\bbus_subset\b')))); + + final vectors = [ + for (final low in [0x0, 0x5, 0xf]) + for (final high in [0x00, 0x5a, 0xff]) + for (final seed in [0, 1]) + Vector({ + 'low': low, + 'high': high, + 'seed': seed, + }, { + 'y': low | (high << 7) | (seed << 15), + 'z': low | (high << 7) | (seed << 15), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + for (final lateInput in [false, true]) { + for (final fanout in [false, true]) { + test( + 'repeated constants survive ${lateInput ? 'late input' : 'bus'} ' + 'with sibling output${fanout ? ' fanout' : ''}', () async { + final mod = RepeatedConstantsAndSiblingOutputTop( + lateInput: lateInput, + fanout: fanout, + ); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.data(({')); + expect(topBody, contains("2'h2")); + expect(topBody, contains("3'h5")); + expect(topBody, isNot(contains('.result()'))); + expect(topBody, isNot(contains('logic [11:0] bus;'))); + expect(topBody, isNot(contains('logic [1:0] tie2;'))); + expect(topBody, isNot(contains('logic [2:0] tie3;'))); + expect(topBody, isNot(contains('_subset'))); + + final vectors = [ + for (final source in [0, 1, 2, 3]) + for (final seed in [0, 1]) + Vector({ + 'source': source, + 'seed': seed, + }, { + 'y': 2 | + (source << 2) | + (seed << 4) | + (5 << 5) | + (source << 8) | + (2 << 10), + if (fanout) 'fanout': seed, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + } + + test('unknown and floating constant ranges preserve four-state values', + () async { + final mod = InvalidConstantsToAssignSubsetTop(); + await mod.build(); + final topBody = _topModuleBody(mod.generateSynth()); + + expect(topBody, contains("2'bxx")); + expect(topBody, isNot(contains("2'bzz"))); + expect(topBody, contains('logic [5:0] data;')); + expect(topBody, contains('.data(data)')); + expect(topBody, isNot(contains('.data()'))); + + final vectors = [ + for (final source in [0, 1, 2, 3]) + Vector({ + 'source': source, + }, { + 'y': LogicValue.ofString( + 'zzxx${source.toRadixString(2).padLeft(2, '0')}', + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('internal bus runs feeding assignSubset collapse through slices', + () async { + final mod = InternalBusRunsToAssignSubsetRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[44:13] = src[31:0];')); + expect(topBody, isNot(contains('srcStage'))); + expect(topBody, isNot(contains('srcLow'))); + expect(topBody, isNot(contains('srcHigh'))); + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains(RegExp(r'assign dst\[[0-9]+\]')))); + + final vectors = [ + for (final pattern in [0x00000000, 0x12345678, 0x89abcdef]) + Vector({ + 'src': pattern, + }, { + 'y': _expectedSparseValue( + 64, + pattern, + (dstIndex) => + dstIndex >= 13 && dstIndex <= 44 ? dstIndex - 13 : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test( + 'computed internal bus runs feeding assignSubset collapse through ' + 'slices', () async { + final mod = InternalBusRunsToAssignSubsetRangeAssignment( + computedSource: true, + ); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[44:13] = srcStage[31:0];')); + expect(topBody, isNot(contains('srcLow'))); + expect(topBody, isNot(contains('srcHigh'))); + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains(RegExp(r'assign dst\[[0-9]+\]')))); + + final vectors = [ + for (final pattern in [0x00000000, 0x12345678, 0x89abcdef]) + Vector({ + 'src': pattern, + }, { + 'y': _expectedSparseValue( + 64, + ~pattern, + (dstIndex) => + dstIndex >= 13 && dstIndex <= 44 ? dstIndex - 13 : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('wide temporary bus slice feeding array words eliminates temporary', + () async { + final mod = WideTemporarySliceToArrayWords(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign y[0][15:0] = src[47:32];')); + expect(topBody, contains('assign y[1][15:0] = src[63:48];')); + expect(topBody, contains('assign y[2][15:0] = src[79:64];')); + expect(topBody, contains('assign y[3][15:0] = src[95:80];')); + expect(topBody, isNot(contains('src_slice'))); + + final vectors = [ + for (final pattern in [ + 0, + 0x123456789abc, + 0xffffffffffff, + ]) + Vector({ + 'src': pattern, + }, { + 'y': _expectedWideTemporarySlice(pattern), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('wide temporary slice helpers with extra consumers are preserved', + () async { + final mod = WideTemporarySliceToArrayWords(extraConsumers: true); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign y[0][15:0] = src[47:32];')); + expect(topBody, contains('assign y[3][15:0] = src[95:80];')); + expect(topBody, contains(RegExp(r'\.i\([^)]*src_slice'))); + + final vectors = [ + for (final pattern in [ + 0, + 0x123456789abc, + 0xffffffffffff, + ]) + Vector({ + 'src': pattern, + }, { + 'y': _expectedWideTemporarySlice(pattern), + 'z': ~_expectedWideTemporarySlice(pattern), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('subset-like manual array name does not trigger generated subset fold', + () async { + final mod = ManualSubsetNamedArrayRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('manual_subset')); + expect(topBody, contains('assign manual_subset[4:2] = src[4:2];')); + expect(topBody, contains('assign y = manual_subset[5:0];')); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('reordered bus-to-array assignments stay expanded', () async { + final mod = PartialBusToArrayRangeAssignment(reversed: true); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('assign dst[5:2] = src[5:2];'))); + expect(topBody, contains('assign dst[2] = src[5];')); + expect(topBody, contains('assign dst[3] = src[4];')); + expect(topBody, contains('assign dst[4] = src[3];')); + expect(topBody, contains('assign dst[5] = src[2];')); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue( + 8, + pattern, + (dstIndex) => dstIndex >= 2 && dstIndex <= 5 ? 7 - dstIndex : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('bus-to-unpacked-array assignments stay expanded', () async { + final mod = PartialBusToArrayRangeAssignment(numUnpackedDimensions: 1); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('assign dst[5:2] = src[5:2];'))); + expect(topBody, contains('assign dst[2] = src[2];')); + expect(topBody, contains('assign dst[3] = src[3];')); + expect(topBody, contains('assign dst[4] = src[4];')); + expect(topBody, contains('assign dst[5] = src[5];')); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue( + 8, + pattern, + (dstIndex) => dstIndex >= 2 && dstIndex <= 5 ? dstIndex : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + }); + + test('non-contiguous partial array assignments stay expanded', () async { + final mod = PartialArrayRangeAssignment(reversed: true); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); - b = addInOutArray('b', b, - dimensions: a.dimensions, - elementWidth: a.elementWidth, - numUnpackedDimensions: a.numUnpackedDimensions); + expect(topBody, isNot(contains('assign dst[4:2]'))); + expect(topBody, contains('assign dst[2] = src[4];')); + expect(topBody, contains('assign dst[3] = src[3];')); + expect(topBody, contains('assign dst[4] = src[2];')); - intermediate <= a; - b <= intermediate; - } -} + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: true), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); -void main() { - tearDown(() async { - await Simulator.reset(); + test('packed multidimensional partial assignments collapse inner range', + () async { + final mod = PartialInnerArrayRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('assign dst[1][3:1] = src[1][3:1];')); + expect(topBody, isNot(contains('assign dst[1][1] = src[1][1];'))); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue(8, pattern, (dstIndex) { + final outerIndex = dstIndex ~/ 4; + final innerIndex = dstIndex % 4; + return outerIndex == 1 && innerIndex >= 1 ? dstIndex : null; + }), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); }); - test('simple 1d collapse', () async { - final mod = SimpleLAPassthrough(LogicArray([4], 1)); + test('unpacked outer dimension still collapses inner packed range', () async { + final mod = PartialInnerArrayRangeAssignment(numUnpackedDimensions: 1); await mod.build(); final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); - expect(sv, contains('assign laOut = laIn;')); + expect(topBody, contains('assign dst[1][3:1] = src[1][3:1];')); + expect(topBody, isNot(contains(RegExp(r'assign dst\[[0-9]+:[0-9]+\]')))); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue(8, pattern, (dstIndex) { + final outerIndex = dstIndex ~/ 4; + final innerIndex = dstIndex % 4; + return outerIndex == 1 && innerIndex >= 1 ? dstIndex : null; + }), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); }); - test('array collapse for cross-module connection', () async { - final mod = ArrayTopMod(Logic()); + test('unpacked one-dimensional partial assignments stay expanded', () async { + final mod = PartialUnpackedArrayRangeAssignment(); await mod.build(); final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); - expect(sv, contains(RegExp(r'ArraySubModIn.*\.inp\(inp\)'))); - expect(sv, contains(RegExp(r'ArraySubModOut.*\.arrOut\(inp\)'))); + expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); + expect(topBody, contains('assign dst[2] = src[2];')); + expect(topBody, contains('assign dst[3] = src[3];')); + expect(topBody, contains('assign dst[4] = src[4];')); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'y': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); }); - test('array nets with intermediate collapse', () async { - final mod = ArrayModuleWithNetIntermediates( - LogicArray([3, 3], 1), LogicArray([3, 3], 1)); + test('wide element partial array assignments stay expanded', () async { + final mod = PartialWideArrayRangeAssignment(); await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('assign dst[2:1] = src[2:1];'))); + expect(topBody, contains('assign dst[1] = src[1];')); + expect(topBody, contains('assign dst[2] = src[2];')); + + final vectors = [ + for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) + Vector({ + 'src': pattern + }, { + 'y': _expectedSparseValue( + 8, + pattern, + (dstIndex) => dstIndex >= 2 && dstIndex <= 5 ? dstIndex : null, + ), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + test('net array partial assignments stay in net connection flow', () async { + final mod = PartialNetArrayRangeAssignment(); + await mod.build(); final sv = mod.generateSynth(); - expect(sv, - contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); - expect(sv, - contains('net_connect #(.WIDTH(9)) net_connect_0 (b, intermediate);')); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); + expect(topBody, contains('net_connect')); + expect(topBody, contains(RegExp(r'net_connect.*\(dst\[2\], src\[2\]\)'))); final vectors = [ - Vector({'a': 0}, {'b': 0}), - Vector({'a': 123}, {'b': 123}), + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'mirror': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('flat LogicNet partial assignments stay in net connection flow', + () async { + final mod = PartialLogicNetRangeAssignment(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); + expect(topBody, contains('net_connect')); + + final vectors = [ + for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) + Vector({ + 'src': pattern + }, { + 'mirror': _expectedPartialArrayRangeValue(pattern, reversed: false), + }) ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); @@ -182,4 +3287,1403 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); }); + + for (final cfg in [ + (name: 'all zero', values: [0, 0, 0, 0]), + (name: 'mixed', values: [0, 0, 1, 0]), + ]) { + test( + 'constant leaf assignments into multidimensional array synthesize ' + '(${cfg.name})', () async { + final mod = ConstantLeafArrayAssignment( + values: cfg.values, + name: 'constant_leaf_array_assignment_${cfg.name.replaceAll(' ', '_')}', + ); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + for (final row in [0, 1]) { + for (final column in [0, 1]) { + expect(topBody, contains('banana[$row][$column]')); + } + } + expect(topBody, contains("1'h0")); + if (cfg.values.contains(1)) { + expect(topBody, contains("1'h1")); + } + + final vectors = [ + Vector({}, {'y': LogicValue.ofString(cfg.values.reversed.join())}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + group('array element inlining', () { + /// Expected `~a` result for the [ArrayElementFanout] configurations, where + /// leaves are inverted and optionally consumed in [reversed] order. + LogicValue expectedInverted(LogicValue a, int leafCount, int elementWidth, + {required bool reversed}) { + final leaves = [ + for (var i = 0; i < leafCount; i++) + a.getRange(i * elementWidth, (i + 1) * elementWidth) + ]; + return [ + for (var i = 0; i < leafCount; i++) + ~leaves[reversed ? leafCount - 1 - i : i] + ].rswizzle(); + } + + final fanoutConfigs = <({ + String name, + List dimensions, + int elementWidth, + bool reversed, + })>[ + (name: '1d', dimensions: [4], elementWidth: 1, reversed: false), + (name: '1d reversed', dimensions: [4], elementWidth: 1, reversed: true), + ( + name: '1d wide elements', dimensions: [3], elementWidth: 4, // + reversed: false + ), + (name: '2d', dimensions: [2, 2], elementWidth: 1, reversed: false), + (name: '3d', dimensions: [2, 2, 2], elementWidth: 1, reversed: false), + ( + name: '2d wide elements', dimensions: [2, 2], elementWidth: 3, // + reversed: false + ), + ]; + + for (final cfg in fanoutConfigs) { + test('logic elements inline and drop the array (${cfg.name})', () async { + final leafCount = cfg.dimensions.reduce((x, y) => x * y); + final total = leafCount * cfg.elementWidth; + + final mod = ArrayElementFanout(Logic(width: total), + dimensions: cfg.dimensions, + elementWidth: cfg.elementWidth, + reversed: cfg.reversed); + await mod.build(); + final sv = mod.generateSynth(); + + // the intermediate array (and every declaration of it) must be gone + expect(sv, isNot(contains('arr'))); + + final vectors = [ + for (final value in [0, 0xA, (1 << total) - 1]) + Vector({ + 'a': value + }, { + 'y': expectedInverted( + LogicValue.ofInt(value, total), leafCount, cfg.elementWidth, + reversed: cfg.reversed) + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + final netConfigs = + <({String name, List dimensions, int elementWidth})>[ + (name: '1d', dimensions: [4], elementWidth: 1), + (name: '2d', dimensions: [2, 2], elementWidth: 1), + (name: '2d wide elements', dimensions: [2, 2], elementWidth: 2), + ]; + + for (final cfg in netConfigs) { + test('net elements inline without net_connect (${cfg.name})', () async { + final total = cfg.dimensions.reduce((x, y) => x * y) * cfg.elementWidth; + + final mod = NetArrayElementFanout( + LogicNet(width: total), LogicNet(width: total), + dimensions: cfg.dimensions, elementWidth: cfg.elementWidth); + await mod.build(); + final sv = mod.generateSynth(); + + // the intermediate array and its net_connects must be gone + expect(sv, isNot(contains('arr'))); + expect(sv, isNot(contains('net_connect (arr'))); + + final vectors = [ + for (final value in [0, 0xA, (1 << total) - 1]) + Vector({'a': value}, {'b': value}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + for (final dimensions in [ + [4], + [2, 2], + ]) { + test('partially-driven array is not inlined ($dimensions)', () async { + final total = dimensions.reduce((x, y) => x * y); + final mod = PartiallyDrivenArray(Logic(width: total - 2), + dimensions: dimensions); + await mod.build(); + final sv = mod.generateSynth(); + + // the array must remain declared since undriven bits must stay `z` + expect(sv, contains('arr')); + + final vectors = [ + Vector({'a': bin('01')}, {'y': LogicValue.ofString('z01z')}), + Vector({'a': bin('10')}, {'y': LogicValue.ofString('z10z')}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('aggregate-used array is not inlined', () async { + final mod = ArrayElementsWithAggregateUse(Logic(width: 4)); + await mod.build(); + final sv = mod.generateSynth(); + + // the array stays (aggregate use), so elements are not inlined into ports + expect(sv, contains('arr')); + expect(sv, isNot(contains('.i((a['))); + + final vectors = [ + Vector({'a': bin('0000')}, {'y': bin('1111'), 'arrCopy': bin('0000')}), + Vector({'a': bin('1010')}, {'y': bin('0101'), 'arrCopy': bin('1010')}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('input-array port elements are not inlined away', () async { + final mod = ArrayPortElementsToSubmodules(LogicArray([2, 2], 2)); + await mod.build(); + final sv = mod.generateSynth(); + + // the array port must remain declared + expect(sv, contains('a')); + + final vectors = [ + Vector({'a': 0}, {'y': LogicValue.ofInt(~0, 8)}), + Vector({'a': 0xA5}, {'y': LogicValue.ofInt(~0xA5, 8)}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('struct-port array field elements are not inlined', () async { + final mod = StructArrayFieldToSubmodules(StructWithArrayField()); + await mod.build(); + + final vectors = [ + // s = {flag, arr[4]}; arr is the low 4 bits, y = ~arr + Vector({'s': 0x00}, {'y': 0xF}), + Vector({'s': 0x14}, {'y': 0xB}), + Vector({'s': 0x1F}, {'y': 0x0}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + }); + + group('aggregate connection inlining', () { + test('constant stays connected to single-element packed array input', + () async { + final mod = ConstantToSingleElementArrayInputTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains(".data((8'h0))")); + expect(topBody, isNot(contains('logic [0:0][7:0] data;'))); + + final vectors = [ + Vector({}, {'y': 0}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('nonzero constant stays connected to single-element packed array', + () async { + final mod = ConstantToSingleElementArrayInputTop(value: 0xa5); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains(".data((8'ha5))")); + expect(topBody, isNot(contains('logic [0:0][7:0] data;'))); + + final vectors = [ + Vector({}, {'y': 0xa5}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + final logicConfigs = + <({String name, int n, int elementWidth, List? perm})>[ + (name: '1d in order', n: 4, elementWidth: 1, perm: null), + (name: '1d out of order', n: 4, elementWidth: 1, perm: [2, 3, 0, 1]), + (name: 'wide elements', n: 3, elementWidth: 4, perm: null), + ( + name: 'wide elements out of order', + n: 3, + elementWidth: 4, + perm: [2, 0, 1] + ), + ]; + + for (final cfg in logicConfigs) { + test('individual signals collapse into array port (${cfg.name})', + () async { + final mod = IndividualSignalsToArrayPort( + List.generate(cfg.n, (_) => Logic(width: cfg.elementWidth)), + elementWidth: cfg.elementWidth, + perm: cfg.perm); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the intermediate array (and every per-element assignment) is gone, + // replaced by a single inline concatenation on the child port + expect(topBody, isNot(contains('assign'))); + expect(topBody, contains('.a(({')); + + final total = cfg.n * cfg.elementWidth; + // element i is driven by signal perm[i] (or i), and the child + // inverts each element; element 0 is the least-significant chunk + LogicValue expected(int Function(int) sigVal) => [ + for (var i = 0; i < cfg.n; i++) + ~LogicValue.ofInt(sigVal(cfg.perm == null ? i : cfg.perm![i]), + cfg.elementWidth) + ].rswizzle(); + + final vectors = [ + for (final pattern in [0, 1, 2]) + Vector({ + for (var i = 0; i < cfg.n; i++) + 'sig$i': (pattern * 7 + i * 3) & ((1 << cfg.elementWidth) - 1) + }, { + 'y': expected( + (s) => (pattern * 7 + s * 3) & ((1 << cfg.elementWidth) - 1)) + }) + ]; + expect(total, lessThanOrEqualTo(32)); + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('merged element sources collapse to the real source', () async { + const n = 4; + final mod = MergedSourcesToArrayPort(List.generate(n, (_) => Logic())); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('intermediate'))); + expect(topBody, isNot(contains('assign'))); + expect(topBody, contains('.a(({')); + for (var i = 0; i < n; i++) { + expect(topBody, contains('sig$i')); + } + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF]) + Vector({ + for (var i = 0; i < n; i++) 'sig$i': (pattern >> i) & 1 + }, { + 'y': LogicValue.ofInt(~pattern, n), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('ranged element sources are not collapsed into whole-source concat', + () async { + final mod = RangeSourcesToArrayPort(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains(RegExp(r'\.a\(\(\{\s*src,')))); + expect( + topBody, + contains(RegExp( + r'assign [A-Za-z_][A-Za-z0-9_$]*\[0\]\[15:0\] = src\[15:0\];')), + ); + expect( + topBody, + contains(RegExp( + r'assign [A-Za-z_][A-Za-z0-9_$]*\[3\]\[15:0\] = src\[63:48\];')), + ); + + final patterns = [ + LogicValue.filled(64, LogicValue.zero), + LogicValue.ofInt(0x123456789abc, 64), + LogicValue.filled(64, LogicValue.one), + ]; + final vectors = [ + for (final pattern in patterns) + Vector({ + 'src': pattern, + }, { + 'y': ~pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + final netConfigs = <({String name, int n, List? perm})>[ + (name: '1d in order', n: 4, perm: null), + (name: '1d out of order', n: 4, perm: [2, 3, 0, 1]), + ]; + + for (final cfg in netConfigs) { + test('individual nets collapse into array port (${cfg.name})', () async { + final mod = IndividualNetsToArrayPort( + List.generate(cfg.n, (_) => LogicNet()), LogicNet(width: cfg.n), + perm: cfg.perm); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the intermediate array and its net_connects are gone, replaced by a + // single inline concatenation on the child port + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, contains('.a(({')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF]) + Vector({ + for (var i = 0; i < cfg.n; i++) 'sig$i': (pattern >> i) & 1 + }, { + // out bit i mirrors element i, driven by signal perm[i] (or i) + 'out': [ + for (var i = 0; i < cfg.n; i++) + LogicValue.ofInt( + (pattern >> (cfg.perm == null ? i : cfg.perm![i])) & 1, 1) + ].rswizzle() + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('multiple aggregate uses are not collapsed', () async { + // when the array is used as a whole more than once, the single-use + // restriction prevents collapsing and the array stays declared + final mod = MultiUseAggregate(List.generate(4, (_) => Logic())); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // with two whole-array uses, the array stays declared and its per-element + // assignments remain (no inline concatenation on the ports) + expect(topBody, contains('assign')); + expect(topBody, isNot(contains('.a(({'))); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF]) + Vector({ + for (var i = 0; i < 4; i++) 'sig$i': (pattern >> i) & 1 + }, { + 'y': (~pattern) & 0xF, + 'z': (~pattern) & 0xF, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + // The original issue was about *individual signals -> array port*; these + // exercise the opposite shape (*array port -> individual signals*) to + // confirm both directions generate correct SystemVerilog. + test('array output port distributed to individual signals (logic)', + () async { + final mod = ArrayPortToIndividualSignals(Logic(width: 4)); + await mod.build(); + + final vectors = [ + for (final x in [0x0, 0xA, 0x5, 0xF]) + Vector({ + 'x': x + }, { + for (var i = 0; i < 4; i++) 'y$i': (~x >> i) & 1, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('array port elements collapse into individual nets (net)', () async { + final mod = ArrayPortToIndividualNets( + List.generate(4, (_) => LogicNet()), LogicNet(width: 4)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the intermediate array and its net_connects collapse into a single + // inline concatenation on the child port + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, contains('.a(({')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF]) + Vector({ + for (var i = 0; i < 4; i++) 'y$i': (pattern >> i) & 1 + }, { + // child mirrors element i to b bit i, and element i == y$i + 'b': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('re-arrangement of one array is left to other mechanisms', () async { + // every element source shares one common parent array, so the + // common-parent guard skips this (no fabricated concatenation); the + // result must still be correct. + final mod = RearrangeOneArray(LogicArray([4], 1)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // this pass did not fabricate a consolidating concatenation on the port + expect(topBody, isNot(contains('.a(({'))); + + final vectors = [ + for (final src in [0x0, 0xA, 0x5, 0xF]) + Vector({ + 'src': src + }, { + // arr element i = src element (3 - i); child inverts the swizzle + 'y': [ + for (var i = 0; i < 4; i++) + ~LogicValue.ofInt((src >> (3 - i)) & 1, 1) + ].rswizzle() + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('expressionless array port is not inlined into', () async { + // the child's array port is declared expressionless, so no concatenation + // may be inlined into it; the array and its assignments must remain. + final mod = IndividualSignalsToExpressionlessPort( + List.generate(4, (_) => Logic())); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // no inline concatenation on the expressionless port; per-element + // assignments to the intermediate array remain + expect(topBody, isNot(contains('.a(({'))); + expect(topBody, contains('assign')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF]) + Vector({ + for (var i = 0; i < 4; i++) 'sig$i': (pattern >> i) & 1 + }, { + 'y': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + }); + + group('flat net bus collapsing', () { + test('cleared bus subsets do not claim better surviving basenames', + () async { + final mod = WholeNetBusCollapseNamingCollision(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('bussubset ('))); + expect(topBody, contains('logic bussubset;')); + expect(topBody, isNot(contains('bussubset_'))); + expect(topBody, contains('.data(({')); + }, + skip: 'Known issue: BusSubset instances that are later cleared by ' + 'whole-net-bus collapse can still claim names before surviving ' + 'signals.'); + + // Case A: a flat net bus tied bit-by-bit to individual nets and passed as a + // whole to a child inout port collapses into a single inline concatenation + // of those nets. + for (final busNaming in [Naming.mergeable, Naming.renameable]) { + test('whole net bus to port respects naming ($busNaming)', () async { + const n = 8; + final mod = WholeNetBusToPort( + List.generate(n, (_) => LogicNet()), LogicNet(width: n), + busNaming: busNaming); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + if (busNaming == Naming.mergeable) { + // The bus and its per-bit net_connects are gone, replaced by a + // single inline concatenation on the child port. + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, isNot(contains('wire [7:0] bus'))); + expect(topBody, contains('.data(({')); + } else { + expect(topBody, contains('net_connect')); + expect(topBody, contains('wire [7:0] bus')); + expect(topBody, contains('.data(bus)')); + } + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xFF, 0x3C]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('whole net bus collapse preserves inline subset consumers', () async { + const n = 4; + final mod = WholeNetBusToPortWithInlineSubsetConsumer( + List.generate(n, (_) => LogicNet()), LogicNet(width: n)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.data')); + expect(topBody, contains('enable &')); + + final vectors = [ + for (final enable in [0, 1]) + for (final pattern in [0x0, 0x5, 0xA, 0xF]) + Vector({ + 'enable': enable, + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1, + }, { + 'mirror': pattern, + 'z': enable == 0 ? 0 : (~pattern) & 1, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('whole net bus collapse ignores read-only subset consumers', () async { + const n = 4; + final mod = + WholeNetBusToPortWithReadOnlyInlineSubsetConsumer(LogicNet(width: n)); + await mod.build(); + final topBody = _topModuleBody(mod.generateSynth()); + + expect(topBody, contains('wire [3:0] bus')); + expect(topBody, contains(RegExp('net_connect.*_subset_0_0_bus'))); + expect(topBody, contains('enable &')); + }); + + test('reserved-named whole net bus is not collapsed', () async { + const n = 8; + final mod = WholeNetBusToPort( + List.generate(n, (_) => LogicNet()), LogicNet(width: n), + busNaming: Naming.reserved); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // a reserved name must be preserved, so the bus and its net_connects stay + expect(topBody, contains('bus')); + expect(topBody, contains('net_connect')); + expect(topBody, isNot(contains('.data(({'))); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0xFF]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('multiply-used whole net bus is not collapsed', () async { + const n = 8; + final mod = WholeNetBusMultiUse(List.generate(n, (_) => LogicNet()), + LogicNet(width: n), LogicNet(width: n)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // used as a whole twice, so the single-use restriction keeps the bus + expect(topBody, contains('bus')); + expect(topBody, contains('net_connect')); + expect(topBody, isNot(contains('.data(({'))); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0xFF]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror1': pattern, + 'mirror2': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + // Case B: a flat net bus tied bit-by-bit to individual nets and passed to a + // child inout *array* port traces through the pass-through bus and + // collapses into a single inline concatenation of those nets. + for (final busNaming in [Naming.mergeable, Naming.renameable]) { + test('bitwise net bus into array port respects naming ($busNaming)', + () async { + const n = 8; + final mod = BitwiseNetBusToArrayPort( + List.generate(n, (_) => LogicNet()), LogicNet(width: n), + busNaming: busNaming); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + if (busNaming == Naming.mergeable) { + // The bus and its net_connects are traced away and replaced by a + // single inline concatenation of those nets on the child array port. + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, isNot(contains('wire [7:0] bus'))); + expect(topBody, contains('.data(({')); + } else { + expect(topBody, contains('net_connect')); + expect(topBody, contains('wire [7:0] bus')); + expect(topBody, isNot(contains('.data(({'))); + } + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xFF, 0x3C]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('bitwise net bus collapse preserves inline subset consumers', + () async { + const n = 4; + final mod = BitwiseNetBusToArrayPortWithInlineSubsetConsumer( + List.generate(n, (_) => LogicNet()), LogicNet(width: n)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.data')); + expect(topBody, contains('enable &')); + + final vectors = [ + for (final enable in [0, 1]) + for (final pattern in [0x0, 0x5, 0xA, 0xF]) + Vector({ + 'enable': enable, + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1, + }, { + 'mirror': pattern, + 'z': enable == 0 ? 0 : (~pattern) & 1, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('reserved-named bitwise net bus into array port is not collapsed', + () async { + const n = 8; + final mod = BitwiseNetBusToArrayPort( + List.generate(n, (_) => LogicNet()), LogicNet(width: n), + busNaming: Naming.reserved); + await mod.build(); + final sv = mod.generateSynth(); + + // a reserved bus name must be preserved, so it is not traced away + expect(sv, contains('bus')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0xFF]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + // Corner case: a bit connected to *another bit of the same bus*. When that + // bit is also externally driven, it gets a second `BusSubset` definer, so + // the "each bit driven exactly once" guard must keep the bus intact. + for (final toArray in [false, true]) { + test('self-bit-connected net bus is not collapsed (toArray=$toArray)', + () async { + const n = 8; + final mod = SelfBitNetBus( + List.generate(n, (_) => LogicNet()), LogicNet(width: n), + toArray: toArray); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the self-connection leaves a per-bit net_connect structure intact + expect(topBody, contains('net_connect')); + expect(topBody, isNot(contains('.data(({'))); + + final vectors = [ + for (final pattern in [0x0, 0x2A, 0x55, 0x7F]) + Vector({ + // only the lower n-1 bits are externally driven + for (var i = 0; i < n - 1; i++) 'net$i': (pattern >> i) & 1 + }, { + // bits 0..n-2 mirror their nets; the top bit follows bit 0 + 'mirror': + (pattern & ((1 << (n - 1)) - 1)) | ((pattern & 1) << (n - 1)), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + // Corner case: a pure self-loop (bit1 <= bit0, no other drivers) merges + // both bits into a single standalone net and may safely collapse without + // producing a dangling reference to the deleted bus. + for (final toArray in [false, true]) { + test('pure self-loop net bus collapses safely (toArray=$toArray)', + () async { + final mod = PureSelfLoopNetBus(LogicNet(width: 2), toArray: toArray); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the bus collapses into an inline concatenation of the merged net, and + // the swizzle must not reference a now-deleted bus slice + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, isNot(contains('wire [1:0] bus'))); + expect(topBody, contains('.data(({')); + }); + } + }); + + group('assignSubset pass-through forwarding', () { + // The `connectPorts(top.bit_i, child.netPort[i])` receiver scenario: + // `assignSubset` ties each external bit net into one bit of a child's net + // bus. The intermediate `*_subset` net array must be forwarded away so the + // whole connection becomes a single inline concatenation with no per-bit + // `net_connect`. + test('per-bit assignSubset into whole net bus collapses', () async { + const n = 4; + final mod = AssignSubsetReceiver( + List.generate(n, (_) => LogicNet()), LogicNet(width: n)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // no intermediate subset array, and no per-bit net_connects remain + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, contains('.data(({')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF, 0x3]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('scrambled-order assignSubset preserves per-bit positions', () async { + const n = 4; + final mod = AssignSubsetReceiverScrambled( + List.generate(n, (_) => LogicNet()), LogicNet(width: n)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, contains('.data(({')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF, 0x6]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('renameable subset target is still forwarded but bus is preserved', + () async { + const n = 4; + final mod = AssignSubsetReceiver( + List.generate(n, (_) => LogicNet()), LogicNet(width: n), + busNaming: Naming.renameable); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the named bus is preserved, but the per-bit `*_subset` pass-through and + // its per-bit net_connects are still forwarded away (a single net_connect + // carries the whole inline concatenation onto the bus) + expect(topBody, isNot(contains('_subset'))); + expect(topBody, contains('bus')); + expect(topBody, contains('({')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0xF]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('driver-direction assignSubset into whole net bus collapses', + () async { + const n = 4; + final mod = AssignSubsetDriver( + List.generate(n, (_) => LogicNet()), LogicNet(width: n)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the per-bit `*_subset` pass-throughs and per-bit `net_connect`s are + // forwarded away so the whole connection is a single inline concatenation + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains('net_connect'))); + expect(topBody, contains('.data(({')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF, 0x3]) + Vector({ + for (var i = 0; i < n; i++) 'net$i': (pattern >> i) & 1 + }, { + 'mirror': pattern, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('non-net assignSubset driver forwards into child input', () async { + const n = 4; + final mod = AssignSubsetLogicDriver(List.generate(n, (_) => Logic())); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // the intermediate `sig_subset` array is forwarded straight into the + // child input with no surviving per-bit `assign` + expect(topBody, isNot(contains('_subset'))); + expect(topBody, isNot(contains('assign'))); + expect(topBody, contains('.i(({')); + + final vectors = [ + for (final pattern in [0x0, 0xA, 0x5, 0xF, 0x6]) + Vector({ + for (var i = 0; i < n; i++) 'b$i': (pattern >> i) & 1 + }, { + 'y': (~pattern) & 0xF, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('assignSubset into late child input source maps instance input', + () async { + final mod = LateSubsetInputTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.data()'))); + expect(topBody, contains('.data(source)')); + + final vectors = [ + for (final pattern in [0x00, 0x01, 0x02, 0xff]) + Vector({'source': pattern}, {'y': pattern & 1}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('assignSubset slice into late child input source keeps mapping', + () async { + final mod = LateSlicedSubsetInputTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.data()'))); + expect(topBody, contains('assign')); + expect(topBody, contains('source[11:4]')); + + final vectors = [ + for (final pattern in [0x0000, 0x0010, 0x00f0, 0xffff]) + Vector({'source': pattern}, {'y': (pattern >> 4) & 1}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling output can drive subset of sibling input source', () async { + final mod = SiblingOutputToInputSubsetTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.data()'))); + expect(topBody, isNot(contains('.result()'))); + + final vectors = [ + for (final pattern in [0x0, 0x1, 0x2, 0xf]) + Vector({'source': pattern}, {'y': pattern & 1}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling full output can drive sibling full input source', () async { + final mod = SiblingFullOutputToInputSubsetTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.data()'))); + expect(topBody, isNot(contains('.result()'))); + + final vectors = [ + for (final pattern in [0x0, 0x1, 0x2, 0xf]) + Vector({'source': pattern}, {'y': pattern & 1}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling output stays connected beside range assignments', () async { + final mod = SiblingOutputWithRangeAssignmentsTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.result(bus[7])')); + expect(topBody, contains('.data(bus)')); + expect(topBody, isNot(contains('_subset'))); + + final vectors = [ + for (final pattern in [0x00, 0x01, 0x04, 0x7f]) + for (final bit in [0, 1]) + Vector({ + 'source': pattern, + 'seed': bit, + }, { + 'y': bit, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + for (final outputIndex in [0, 3]) { + test('sibling output at bit $outputIndex stays directly connected', + () async { + final mod = IndexedSiblingOutputWithRangeAssignmentsTop(outputIndex); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, contains('.result(bus[$outputIndex])')); + expect(topBody, contains('.data(bus)')); + expect(topBody, isNot(contains('_subset'))); + + final vectors = [ + for (final pattern in [0x00, 0x01, 0x35, 0x7f]) + for (final bit in [0, 1]) + Vector({ + 'source': pattern, + 'seed': bit, + }, { + 'y': (pattern & ((1 << outputIndex) - 1)) | + (bit << outputIndex) | + ((pattern >> outputIndex) << (outputIndex + 1)), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + + test('multiple sibling outputs stay connected in packed concat', () async { + final mod = MultipleSiblingOutputsWithRangeAssignmentsTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.result()'))); + expect(topBody, contains('.data(({')); + expect(topBody, isNot(contains('logic [7:0] bus;'))); + expect(topBody, isNot(contains('_subset'))); + + final vectors = [ + for (final pattern in [0x00, 0x01, 0x15, 0x3f]) + for (final bit0 in [0, 1]) + for (final bit1 in [0, 1]) + Vector({ + 'sourceLow': pattern & 0x3, + 'sourceHigh': pattern >> 2, + 'seed0': bit0, + 'seed1': bit1, + }, { + 'y': (pattern & 0x3) | + (bit0 << 2) | + ((pattern >> 2) << 3) | + (bit1 << 7), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling output fanout stays connected after range collapse', + () async { + final mod = FanoutSiblingOutputWithRangeAssignmentsTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.result()'))); + expect(topBody, contains('.data(({')); + expect(topBody, isNot(contains('logic [7:0] bus;'))); + expect(topBody, isNot(contains('_subset'))); + + final vectors = [ + for (final pattern in [0x00, 0x01, 0x35, 0x7f]) + for (final bit in [0, 1]) + Vector({ + 'source': pattern, + 'seed': bit, + }, { + 'y': pattern | (bit << 7), + 'fanout': bit, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('wide sibling output stays connected after range collapse', () async { + final mod = WideSiblingOutputWithRangeAssignmentsTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.result()'))); + expect(topBody, contains('.data(bus)')); + expect(topBody, contains('bus[5:2]')); + expect(topBody, isNot(contains(RegExp(r'\bbus_subset\b')))); + + final vectors = [ + for (final source in [0x0, 0x1, 0xa, 0xf]) + for (final seed in [0x0, 0x3, 0xc, 0xf]) + Vector({ + 'source': source, + 'seed': seed, + }, { + 'y': (source & 0x3) | (seed << 2) | ((source >> 2) << 6), + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('wide sibling output keeps fanout between constant ranges', () async { + final mod = WideSiblingOutputWithConstantsAndFanoutTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.result()'))); + expect(topBody, contains('.data(({')); + expect(RegExp("2'h2").allMatches(topBody), hasLength(greaterThan(1))); + expect(topBody, isNot(contains('logic [7:0] bus;'))); + expect(topBody, isNot(contains('logic [1:0] tie;'))); + expect(topBody, isNot(contains(RegExp(r'\bbus_subset\b')))); + + final vectors = [ + for (final seed in [0x0, 0x1, 0x5, 0xa, 0xf]) + Vector({ + 'seed': seed, + }, { + 'y': 2 | (seed << 2) | (2 << 6), + 'fanout': seed, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling output array can drive subset of sibling input array source', + () async { + final mod = SiblingArrayOutputToInputSubsetTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.data()'))); + expect(topBody, isNot(contains('.result()'))); + + final vectors = [ + for (final pattern in [0x0, 0x1, 0x2, 0xf]) + Vector({'source': pattern}, {'y': pattern & 1}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling output structure can drive sibling input structure source', + () async { + final mod = SiblingStructOutputToInputSubsetTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.data()'))); + expect(topBody, isNot(contains('.result()'))); + + final vectors = [ + for (final pattern in [0x0, 0x1, 0x2, 0x3]) + Vector({'source': pattern}, {'y': (pattern >> 1) & 1}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling inout can drive subset of sibling inout source', () async { + final mod = SiblingInOutToInOutSubsetTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + expect(topBody, isNot(contains('.data()'))); + expect(topBody, isNot(contains('.link()'))); + + final vectors = [ + for (final pattern in [0x0, 0x1, 0x2, 0xf]) + Vector({'source': pattern}, {'y': pattern & 1}) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('sibling boundary kitchen sink keeps mixed source mappings', () async { + final mod = SiblingBoundaryProductTop(); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + for (final portName in [ + 'wide', + 'arr', + 'pair', + 'link', + 'wide_from_scalar', + 'array_from_scalar', + 'struct_from_array', + 'wide_from_struct', + 'net_from_net', + ]) { + expect(topBody, isNot(contains('.$portName()'))); + } + + final vectors = [ + for (final pattern in [0x0, 0x1, 0x2, 0x4, 0xf]) + for (final netPattern in [0x0, 0x2, 0xf]) + Vector({ + 'source': pattern, + 'net_source': netPattern, + }, { + 'scalar_bit': (pattern >> 1) & 1, + 'array_bit': (pattern >> 2) & 1, + 'struct_bit': (pattern >> 2) & 1, + 'wide_struct_bit': (pattern >> 1) & 1, + 'net_bit': (netPattern >> 1) & 1, + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('partial assignSubset is conservatively preserved (undriven stays z)', + () async { + const n = 4; + final mod = AssignSubsetPartial( + List.generate(n ~/ 2, (_) => LogicNet()), LogicNet(width: n)); + await mod.build(); + final sv = mod.generateSynth(); + final topBody = _topModuleBody(sv); + + // not every element is a pass-through, so the subset array is preserved + expect(topBody, contains('_subset')); + expect(topBody, contains('bus_subset[3')); + expect(topBody, contains('net_connect')); + + final vectors = [ + for (final pattern in [0x0, 0x1, 0x2, 0x3]) + Vector({ + for (var i = 0; i < n ~/ 2; i++) 'net$i': (pattern >> i) & 1 + }, { + // high half is undriven => z, low half mirrors the driven bits + 'mirror': 'zz${(pattern >> 1) & 1}${pattern & 1}', + }) + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + }); + + group('kitchen-sink collapse combinations', () { + // Enumerate every valid combination of the independent collapse toggles for + // both nets and non-nets. For every combination we always check functional + // correctness (functional + iverilog), and for the combinations whose + // structure we can confidently predict we also assert the generated + // SystemVerilog looks the way it should. The cost of a silent corner-case + // bug here is high, so coverage is intentionally exhaustive. + final configs = []; + for (final isNet in [true, false]) { + final mechanisms = + isNet ? TieMechanism.values : const [TieMechanism.subsetReceiver]; + for (final mechanism in mechanisms) { + for (final toArray in [false, true]) { + for (final scrambled in [false, true]) { + for (final collapsibleBus in [true, false]) { + // partial drive only has well-defined `z` semantics for nets + for (final partial in isNet ? [false, true] : [false]) { + for (final multiUse in [false, true]) { + configs.add(CollapseConfig( + isNet: isNet, + mechanism: mechanism, + toArray: toArray, + scrambled: scrambled, + collapsibleBus: collapsibleBus, + partial: partial, + multiUse: multiUse, + )); + } + } + } + } + } + } + } + + for (final config in configs) { + test(config.description, () async { + const n = CollapseConfig.width; + final k = config.driven; + final reason = config.description; + + final Module mod; + final List vectors; + if (config.isNet) { + final netMod = NetKitchenSink( + config, + List.generate(k, (_) => LogicNet()), + LogicNet(width: n), + config.multiUse ? LogicNet(width: n) : null, + ); + mod = netMod; + String expected(int pattern) => config.partial + ? ('z' * (n - k)) + pattern.toRadixString(2).padLeft(k, '0') + : LogicValue.ofInt(pattern, n).toString(includeWidth: false); + final patterns = + config.partial ? [0x0, 0x1, 0x2, 0x3] : [0x0, 0xA, 0x5, 0xF, 0x6]; + vectors = [ + for (final pattern in patterns) + Vector({ + for (var i = 0; i < k; i++) 'net$i': (pattern >> i) & 1, + }, { + 'mirror1': expected(pattern), + if (config.multiUse) 'mirror2': expected(pattern), + }), + ]; + } else { + final logicMod = + LogicKitchenSink(config, List.generate(n, (_) => Logic())); + mod = logicMod; + final patterns = [0x0, 0xA, 0x5, 0xF, 0x6]; + vectors = [ + for (final pattern in patterns) + Vector({ + for (var i = 0; i < n; i++) 'b$i': (pattern >> i) & 1, + }, { + 'y': pattern, + if (config.multiUse) 'y2': pattern, + }), + ]; + } + + await mod.build(); + final topBody = _topModuleBody(mod.generateSynth()); + + // --- structural expectations (only where confidently predictable) --- + if (config.noSubset) { + expect(topBody, isNot(contains('_subset')), + reason: 'subset pass-throughs should be forwarded away: $reason'); + } + + if (config.fullyCollapses) { + expect(topBody, isNot(contains(RegExp(r'\bbus\b'))), + reason: 'mergeable bus should dissolve entirely: $reason'); + expect(topBody, contains('.data(({'), + reason: 'an inline concatenation should drive the child: ' + '$reason'); + if (config.isNet) { + expect(topBody, isNot(contains('net_connect')), + reason: 'no per-bit net_connect should remain: $reason'); + } + } + + // --- functional correctness (always) --- + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + } + }); } diff --git a/test/benchmark_test.dart b/test/benchmark_test.dart index 4a54fcfe3..a65020166 100644 --- a/test/benchmark_test.dart +++ b/test/benchmark_test.dart @@ -20,7 +20,7 @@ import '../benchmark/ssa_driver_search_benchmark.dart'; import '../benchmark/wave_dump_benchmark.dart'; void main() { - group('benchmark', () { + group('benchmark', tags: 'benchmark', () { test('pipeline', () async { await PipelineBenchmark().measure(); }); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..8128eea2d 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // collapse_test.dart @@ -38,6 +38,15 @@ class CollapseTestModule extends Module { } } +class CombinationalLoopCollapseModule extends Module { + CombinationalLoopCollapseModule(Logic a) { + a = addInput('a', a); + final loop = Logic(); + loop <= loop | a; + addOutput('out') <= loop.eq(a); + } +} + void main() { tearDown(() async { await Simulator.reset(); @@ -64,4 +73,13 @@ void main() { expect(sv, contains(RegExp('internal.*=.*~z'))); }); + + test('combinational loop does not recurse while collapsing', () async { + final mod = CombinationalLoopCollapseModule(Logic()); + await mod.build(); + + final sv = mod.generateSynth(); + expect(sv, contains(' | a')); + expect(sv, contains(' == a')); + }); } diff --git a/test/const_radix_test.dart b/test/const_radix_test.dart new file mode 100644 index 000000000..4e9beb4eb --- /dev/null +++ b/test/const_radix_test.dart @@ -0,0 +1,164 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// const_radix_test.dart +// Tests for preferred radix formatting of constants. +// +// 2026 July 14 +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// Exposes constants in each supported radix through generated outputs. +class _ConstRadixModule extends Module { + _ConstRadixModule() : super(name: 'constRadix') { + addOutput('autoHex', width: 8) <= Const(42, width: 8); + addOutput('binaryValue', width: 8) <= + Const(42, width: 8, preferredRadix: 2); + addOutput('octalValue', width: 8) <= Const(42, width: 8, preferredRadix: 8); + addOutput('decimalValue', width: 8) <= + Const(42, width: 8, preferredRadix: 10); + addOutput('hexValue', width: 8) <= Const(42, width: 8, preferredRadix: 16); + addOutput('invalidValue', width: 4) <= + Const( + LogicValue.ofString('10xz'), + preferredRadix: 16, + ); + } +} + +/// Requires its input connection to be a signal rather than an expression. +class _ExpressionlessRadixSub extends Module with SystemVerilog { + @override + List get expressionlessInputs => const ['in']; + + _ExpressionlessRadixSub(Logic input) : super(name: 'expressionlessRadix') { + input = addInput('in', input, width: input.width); + addOutput('out', width: input.width) <= input; + } +} + +/// Drives an expressionless submodule input with a radix-preferred constant. +class _ExpressionlessRadixTop extends Module { + _ExpressionlessRadixTop() : super(name: 'expressionlessRadixTop') { + final sub = _ExpressionlessRadixSub( + Const(42, width: 8, preferredRadix: 10), + ); + addOutput('out', width: 8) <= sub.output('out'); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('Const preferred radix', () { + test('normalizes names using the displayed radix', () { + expect(Const(42, width: 8).name, 'const_42'); + expect( + Const(42, width: 8, preferredRadix: 2).name, + 'const_0b101010', + ); + expect( + Const(42, width: 8, preferredRadix: 8).name, + 'const_0o52', + ); + expect( + Const(42, width: 8, preferredRadix: 10).name, + 'const_42', + ); + expect( + Const(42, width: 8, preferredRadix: 16).name, + 'const_0x2a', + ); + }); + + test('derives names from the normalized stored value', () { + expect( + Const(-1, width: 8, preferredRadix: 16).name, + 'const_0xff', + ); + expect( + Const(BigInt.from(42), width: 8, preferredRadix: 8).name, + 'const_0o52', + ); + expect( + Const( + LogicValue.ofInt(42, 8), + preferredRadix: 10, + ).name, + 'const_42', + ); + expect( + Const(1, width: 8, fill: true, preferredRadix: 16).name, + 'const_0xff', + ); + expect( + Const( + LogicValue.ofString('10xz'), + preferredRadix: 16, + ).name, + 'const_0b10xz', + ); + }); + + test('rejects unsupported radices', () { + for (final radix in [3, 4, 12]) { + expect( + () => Const(1, preferredRadix: radix), + throwsA(isA()), + ); + } + }); + + test('clone preserves the preference and normalized name', () { + final original = Const(42, width: 8, preferredRadix: 2); + final clone = original.clone(); + + expect(clone.preferredRadix, 2); + expect(clone.name, original.name); + expect(clone.value, original.value); + }); + + test('toString remains a Logic diagnostic', () { + expect( + Const(42, width: 8, preferredRadix: 10).toString(), + 'Logic(8): const_42', + ); + }); + + test('normalized names compose into expression names', () { + final result = + Logic(name: 'a', width: 8) + Const(42, width: 8, preferredRadix: 16); + + expect(result.name, contains('const_0x2a')); + }); + + test('controls generated SystemVerilog literals', () async { + final module = _ConstRadixModule(); + await module.build(); + + final systemVerilog = module.generateSynth(); + + expect(systemVerilog, contains("assign autoHex = 8'h2a;")); + expect(systemVerilog, contains("assign binaryValue = 8'b101010;")); + expect(systemVerilog, contains("assign octalValue = 8'o52;")); + expect(systemVerilog, contains("assign decimalValue = 8'd42;")); + expect(systemVerilog, contains("assign hexValue = 8'h2a;")); + expect(systemVerilog, contains("assign invalidValue = 4'b10xz;")); + }); + + test('preserves the preference for expressionless inputs', () async { + final module = _ExpressionlessRadixTop(); + await module.build(); + + final systemVerilog = module.generateSynth(); + + expect(systemVerilog, contains("assign in = 8'd42;")); + expect(systemVerilog, contains('.in(in)')); + expect(systemVerilog, isNot(contains(".in(8'd42)"))); + }); + }); +} diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..b2c47c21e 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2024 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // gate_test.dart @@ -68,7 +68,8 @@ class Absolute extends Module { class ShiftTestModule extends Module { dynamic constant; // int or BigInt - ShiftTestModule(Logic a, Logic b, {this.constant = 3}) + ShiftTestModule(Logic a, Logic b, + {this.constant = 3, bool useExplicitConstShiftModules = false}) : super(name: 'shifttestmodule') { a = addInput('a', a, width: a.width); b = addInput('b', b, width: b.width); @@ -84,9 +85,15 @@ class ShiftTestModule extends Module { aRshiftB <= a >>> b; aLshiftB <= a << b; aArshiftB <= a >> b; - aRshiftConst <= a >>> constant; - aLshiftConst <= a << constant; - aArshiftConst <= a >> constant; + if (useExplicitConstShiftModules) { + aRshiftConst <= RShift(a, constant).out; + aLshiftConst <= LShift(a, constant).out; + aArshiftConst <= ARShift(a, constant).out; + } else { + aRshiftConst <= a >>> constant; + aLshiftConst <= a << constant; + aArshiftConst <= a >> constant; + } } } @@ -112,6 +119,20 @@ class IndexGateTestModule extends Module { } } +/// A module with a signal driven by a [Const]. +class ConstAliasModule extends Module { + /// The constant source protected from mutation. + late final Const constant = Const(0); + + /// A named signal driven by [constant]. + late final Logic alias = constant.named('alias'); + + /// Creates a module whose output is driven by [alias]. + ConstAliasModule() : super(name: 'constaliasmodule') { + addOutput('out') <= alias; + } +} + void main() { tearDown(() async { await Simulator.reset(); @@ -352,6 +373,197 @@ void main() { }); }); + group('constant gate optimizations', () { + test('Const cannot be mutated', () async { + final module = ConstAliasModule(); + final constant = module.constant; + final alias = module.alias; + + expect(() => constant.put(1), throwsA(isA())); + expect(() => constant.inject(1), throwsA(isA())); + expect( + () => alias.put(1), + throwsA(isA().having( + (exception) => exception.message, + 'message', + allOf( + contains('"${alias.name}" cannot be updated'), + contains('is driven by immutable signal'), + contains('A `Const` value cannot be modified'), + )))); + expect(() => alias.inject(1), throwsA(isA())); + expect(constant.value, LogicValue.zero); + expect(alias.value, LogicValue.zero); + + await module.build(); + expect(module.generateSynth(), contains("1'h0")); + }); + + test('bitwise NOT folds Const inputs', () { + final result = ~Const(LogicValue.ofString('01xz')); + + expect(result, isA()); + expect(result.value, LogicValue.ofString('10xx')); + + final normalResult = ~(Logic()..put(1)); + expect(normalResult, isNot(isA())); + expect(normalResult.value, LogicValue.zero); + }); + + test('bitwise AND folds only four-state-safe cases', () { + final signal = Logic(width: 4)..put(LogicValue.ofString('10z1')); + final zeros = Const(0, width: 4); + final ones = Const(0xf, width: 4); + + expect(signal & zeros, same(zeros)); + expect(zeros & signal, same(zeros)); + + final identityResult = signal & ones; + expect(identityResult, isNot(same(signal))); + expect(identityResult.value, LogicValue.ofString('10x1')); + + final folded = Const(LogicValue.ofString('11x1')) & + Const(LogicValue.ofString('1011')); + expect(folded, isA()); + expect(folded.value, LogicValue.ofString('10x1')); + + expect(() => Const(0, width: 2) & Logic(), + throwsA(isA())); + }); + + test('bitwise OR folds only four-state-safe cases', () { + final signal = Logic(width: 4)..put(LogicValue.ofString('01z0')); + final zeros = Const(0, width: 4); + final ones = Const(0xf, width: 4); + + expect(signal | ones, same(ones)); + expect(ones | signal, same(ones)); + + final identityResult = signal | zeros; + expect(identityResult, isNot(same(signal))); + expect(identityResult.value, LogicValue.ofString('01x0')); + + final folded = Const(LogicValue.ofString('10x0')) | + Const(LogicValue.ofString('0101')); + expect(folded, isA()); + expect(folded.value, LogicValue.ofString('11x1')); + + expect(() => Const(0, width: 2) | Logic(), + throwsA(isA())); + }); + + test('bitwise XOR folds only when both inputs are Const', () { + final folded = Const(LogicValue.ofString('10x0')) ^ + Const(LogicValue.ofString('0101')); + expect(folded, isA()); + expect(folded.value, LogicValue.ofString('11x1')); + + final signal = Logic(width: 4)..put(LogicValue.ofString('01z0')); + final identityResult = Const(0, width: 4) ^ signal; + expect(identityResult, isNot(same(signal))); + expect(identityResult.value, LogicValue.ofString('01x0')); + }); + + test('reductions and comparisons fold Const inputs', () { + final value = Const(0xa, width: 4); + + expect(value.and(), isA()); + expect(value.and().value, LogicValue.zero); + expect(value.or().value, LogicValue.one); + expect(value.xor().value, LogicValue.zero); + + final equal = value.eq(Const(0xa, width: 4)); + final notEqual = value.neq(0xb); + final notEqualConst = value.neq(Const(0xb, width: 4)); + expect(equal, isA()); + expect(equal.value, LogicValue.one); + expect(notEqual, isA()); + expect(notEqual.value, LogicValue.one); + expect(notEqualConst, isA()); + expect(notEqualConst.value, LogicValue.one); + + final unknown = Const(LogicValue.z).eq(Const(LogicValue.z)); + expect(unknown, isA()); + expect(unknown.value, LogicValue.x); + }); + + test('comparisons with mutable Logic retain comparison modules', () { + final value = Const(0xa, width: 4); + final mutable = Logic(width: 4)..put(0xa); + + final equal = value.eq(mutable); + final notEqual = value.neq(mutable); + + expect(equal.parentModule, isA()); + expect(notEqual.parentModule, isA()); + expect(equal.value, LogicValue.one); + expect(notEqual.value, LogicValue.zero); + + mutable.put(0xb); + expect(equal.value, LogicValue.zero); + expect(notEqual.value, LogicValue.one); + }); + + test('shifts by constant zero return the original signal', () { + final signal = Logic(width: 4)..put(LogicValue.ofString('10z1')); + + expect(signal << 0, same(signal)); + expect(signal >> Const(0), same(signal)); + expect(signal >>> BigInt.zero, same(signal)); + + final constant = Const(bin('1010'), width: 4); + expect(constant << 0, same(constant)); + expect(constant >> BigInt.zero, same(constant)); + expect(constant >>> Const(0), same(constant)); + + final left = constant << 1; + final right = constant >>> 1; + final arithmeticRight = constant >> 1; + expect(left, isA()); + expect(left.value, LogicValue.ofString('0100')); + expect(right.value, LogicValue.ofString('0101')); + expect(arithmeticRight.value, LogicValue.ofString('1101')); + }); + + test('Const shifts by mutable Logic retain shift modules', () { + final constant = Const(bin('1010'), width: 4); + final amount = Logic(width: 2)..put(1); + + final left = constant << amount; + final right = constant >>> amount; + final arithmeticRight = constant >> amount; + + expect(left.parentModule, isA()); + expect(right.parentModule, isA()); + expect(arithmeticRight.parentModule, isA()); + expect(left.value, LogicValue.ofString('0100')); + expect(right.value, LogicValue.ofString('0101')); + expect(arithmeticRight.value, LogicValue.ofString('1101')); + + amount.put(2); + expect(left.value, LogicValue.ofString('1000')); + expect(right.value, LogicValue.ofString('0010')); + expect(arithmeticRight.value, LogicValue.ofString('1110')); + }); + + test('mux bypasses only valid constant controls', () { + final d0 = Logic(width: 4); + final d1 = Logic(width: 4); + + expect(mux(Const(0), d1, d0), same(d0)); + expect(mux(Const(1), d1, d0), same(d1)); + + final invalidResult = mux(Const(LogicValue.z), d1, d0); + expect(invalidResult, isNot(anyOf(same(d0), same(d1)))); + expect(invalidResult.value, LogicValue.filled(4, LogicValue.x)); + + expect(() => mux(Const(0, width: 2), d1, d0), + throwsA(isA())); + expect(() => mux(Const(0), Logic(width: 2), d0), + throwsA(isA())); + }); + }); + group('simcompare', () { test('NotGate single bit', () async { final gtm = GateTestModule(Logic(), Logic()); @@ -534,8 +746,12 @@ void main() { }); test('shift by const zero', () async { - final gtm = - ShiftTestModule(Logic(width: 3), Logic(width: 8), constant: 0); + final gtm = ShiftTestModule( + Logic(width: 3), + Logic(width: 8), + constant: 0, + useExplicitConstShiftModules: true, + ); await gtm.build(); final sv = gtm.generateSynth(); diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..2bac39cae 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_array_test.dart diff --git a/test/logic_test.dart b/test/logic_test.dart index 5e21fb39f..cd3169cab 100644 --- a/test/logic_test.dart +++ b/test/logic_test.dart @@ -15,4 +15,26 @@ void main() { final logic = Logic(); expect(logic.packed, logic); }); + + test('getRange on filled constants returns a constant', () { + for (final fillValue in [ + LogicValue.zero, + LogicValue.one, + LogicValue.x, + LogicValue.z, + ]) { + final range = Const(LogicValue.filled(8, fillValue)).getRange(2, 5); + + expect(range, isA()); + expect(range.value, LogicValue.filled(3, fillValue)); + expect(range.parentModule, isNull); + } + }); + + test('getRange on mixed constants still uses BusSubset', () { + final range = Const(LogicValue.ofString('10101010')).getRange(2, 5); + + expect(range, isNot(isA())); + expect(range.parentModule, isA()); + }); } diff --git a/test/logic_value_test.dart b/test/logic_value_test.dart index d3753df8d..0622439b4 100644 --- a/test/logic_value_test.dart +++ b/test/logic_value_test.dart @@ -2088,6 +2088,8 @@ void main() { test('radixString binary expansion', () { final lv = LogicValue.ofRadixString("12'b10z111011z00"); expect(lv.toRadixString(radix: 16), equals("12'h<10z1>d<1z00>")); + expect(lv.toRadixString(radix: 16, includeWidth: false), + equals('<10z1>d<1z00>')); for (final i in [2, 4, 8, 16]) { expect( LogicValue.ofRadixString(lv.toRadixString(radix: i)), equals(lv)); @@ -2097,13 +2099,20 @@ void main() { test('radixString leading zero', () { final lv = LogicValue.ofRadixString("10'b00_0010_0111"); expect(lv.toRadixString(), equals("10'b10_0111")); + expect(lv.toRadixString(sepChar: ''), equals("10'b100111")); + expect( + lv.toRadixString(includeWidth: false, sepChar: ''), equals('100111')); expect(lv.toRadixString(leadingZeros: true), equals("10'b00_0010_0111")); expect(lv.toRadixString(radix: 4), equals("10'q213")); + expect(lv.toRadixString(radix: 4, includeWidth: false), equals('213')); expect(lv.toRadixString(radix: 8), equals("10'o47")); + expect(lv.toRadixString(radix: 8, includeWidth: false), equals('47')); expect(lv.toRadixString(radix: 10), equals("10'd39")); + expect(lv.toRadixString(radix: 10, includeWidth: false), equals('39')); expect( lv.toRadixString(radix: 10, leadingZeros: true), equals("10'd0039")); expect(lv.toRadixString(radix: 16), equals("10'h27")); + expect(lv.toRadixString(radix: 16, includeWidth: false), equals('27')); expect( lv.toRadixString(radix: 16, leadingZeros: true), equals("10'h027")); for (final i in [2, 4, 8, 10, 16]) { diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index 2d5fccdc0..73e8c451e 100644 --- a/test/net_bus_test.dart +++ b/test/net_bus_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // net_bus_test.dart @@ -601,7 +601,7 @@ void main() { sv, contains('net_connect_0' ' (_original__swizzled, ' - '({bus[1][1],bus[1][0],bus[0][3],bus[0][2]}));')); + '({bus[1][1:0],bus[0][3:2]}));')); } final vectors = [ @@ -631,7 +631,7 @@ void main() { sv, contains('net_connect_0' ' (_original__swizzled, ' - '({bus[1][1],bus[1][0],bus[0][3],bus[0][2]}));')); + '({bus[1][1:0],bus[0][3:2]}));')); } final vectors = [ @@ -788,7 +788,7 @@ void main() { sv, contains('assign _swizzled = ' '{({({in0[1][1],in0[1][0]}),({in0[0][1],in0[0][0]})}),' - '({in1[3],in1[2],in1[1],in1[0]})};')); + '(in1[3:0])};')); }); test('net array 2', () async { @@ -805,7 +805,7 @@ void main() { expect( sv, contains('net_connect (swizzled,' - ' ({({in0[3],in0[2],in0[1],in0[0]}),in1[0]}));')); + ' ({(in0[3:0]),in1[0]}));')); }); test('net array 3', () async { @@ -825,7 +825,7 @@ void main() { contains('net_connect (swizzled, ' '({({({in0[1][1],in0[1][0]}),' '({in0[0][1],in0[0][0]})}),' - '({in1[3],in1[2],in1[1],in1[0]}),in2[0]}));')); + '(in1[3:0]),in2[0]}));')); }); test('net and non-net', () async { @@ -931,7 +931,7 @@ void main() { 'net_connect #(.WIDTH(16)) net_connect (swizzled, ' '({({({in0[1][1],in0[1][0]}),' '({in0[0][1],in0[0][0]})}),' - '({in1[3],in1[2],in1[1],in1[0]}),in2[0]}));')); + '(in1[3:0]),in2[0]}));')); } } diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 7453d0cfe..71eba12d0 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -267,6 +267,21 @@ class NestedInternalArrayToChildModule extends Module { } } +/// Parent whose 2D LogicArray.net rows are driven by independent child array +/// outputs before feeding a child array input port. +class NestedNetArrayRowsToChildModule extends Module { + NestedNetArrayRowsToChildModule() : super(name: 'nestednetarrayrowstochild') { + final lower = ArrayOutputChildModule(); + final upper = ArrayOutputChildModule(); + final values = LogicArray.net([2, 4], 8, name: 'values'); + + values.elements[0] <= lower.values; + values.elements[1] <= upper.values; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + /// Simple two-field structure used to demonstrate netlist struct unpack/pack /// cells. class NetlistPairStruct extends LogicStructure { @@ -275,10 +290,7 @@ class NetlistPairStruct extends LogicStructure { Logic get high => elements[1]; NetlistPairStruct({super.name = 'pair'}) - : super([ - Logic(name: 'low', width: 4), - Logic(name: 'high', width: 4), - ]); + : super([Logic(name: 'low', width: 4), Logic(name: 'high', width: 4)]); @override NetlistPairStruct clone({String? name}) => NetlistPairStruct(name: name); @@ -559,6 +571,68 @@ bool _hasCellType(Map json, String cellType) { }); } +({List undrivenInputs, Map> driversByBit}) + _connectivityReport(Map moduleDef) { + final ports = _ports(moduleDef); + final cells = _cells(moduleDef); + final producedBits = {}; + final driversByBit = >{}; + + void addDriver(int bit, String driver) { + producedBits.add(bit); + (driversByBit[bit] ??= []).add(driver); + } + + for (final entry in ports.entries) { + final port = entry.value as Map; + final direction = port['direction'] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + for (final bit in (port['bits'] as List).whereType()) { + addDriver(bit, 'port ${entry.key}'); + } + } + + for (final entry in cells.entries) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map? ?? {}; + final connections = cell['connections'] as Map? ?? {}; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] != 'output' && + directions[portEntry.key] != 'inout') { + continue; + } + for (final bit in (portEntry.value as List).whereType()) { + addDriver(bit, 'cell ${entry.key}.${portEntry.key}'); + } + } + } + + final undrivenInputs = []; + for (final entry in cells.entries) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map? ?? {}; + final connections = cell['connections'] as Map? ?? {}; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] != 'input') { + continue; + } + final undrivenBits = (portEntry.value as List) + .whereType() + .where((bit) => !producedBits.contains(bit)) + .toList(); + if (undrivenBits.isNotEmpty) { + undrivenInputs.add( + '${entry.key}.${portEntry.key}: ${undrivenBits.take(8).join(', ')}', + ); + } + } + } + + return (undrivenInputs: undrivenInputs, driversByBit: driversByBit); +} + // ──────────────────────────────────────────────────────────────────── // Tests // ──────────────────────────────────────────────────────────────────── @@ -1530,6 +1604,53 @@ void main() { expect(concatOutputBits.intersection(concatInputConsumers), isNotEmpty); }); + test( + 'nested LogicArray.net aggregate ports have connected concat inputs', + () async { + for (final options in [ + const NetlistOptions(enableDCE: false), + const NetlistOptions( + collapseTransparentClusters: true, + enableDCE: false, + ), + ]) { + final json = await _synthToMap( + NestedNetArrayRowsToChildModule(), + options: options, + ); + final moduleDef = _modules(json) + .values + .cast>() + .reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final nestedArrayConcats = cells.entries.where((entry) { + final cell = entry.value as Map; + return entry.key.startsWith('array_concat') && + cell['type'] == r'$concat'; + }); + final report = _connectivityReport(moduleDef); + final multipleDrivers = report.driversByBit.entries + .where((entry) => entry.value.length > 1) + .toList(); + + expect(nestedArrayConcats, isNotEmpty, reason: cells.keys.join(', ')); + expect( + report.undrivenInputs, + isEmpty, + reason: report.undrivenInputs.join('\n'), + ); + expect( + multipleDrivers, + isEmpty, + reason: multipleDrivers.take(8).join('\n'), + ); + } + }, + ); + test('struct input fields get explicit unpack cell', () async { final module = StructInputConsumerModule(NetlistPairStruct()); final json = await _synthToMap(module); diff --git a/test/pair_interface_hier_test.dart b/test/pair_interface_hier_test.dart index e665b7102..c6bb7ad96 100644 --- a/test/pair_interface_hier_test.dart +++ b/test/pair_interface_hier_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // pair_interface_hier_test.dart diff --git a/test/pair_interface_hier_w_modify_test.dart b/test/pair_interface_hier_w_modify_test.dart index 47c65318c..11ad499e8 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_w_modify_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // pair_interface_hier_w_modify_test.dart diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 0167335f3..46afdbef0 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // pair_interface_test.dart diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 97c15f648..8ee28bb70 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // provider_consumer_test.dart @@ -178,37 +178,41 @@ void main() { final sv = mod.generateSynth(); - expect(sv, contains(''' -module Provider ( -input logic clk, -input logic reset, -input logic wd0_ready_req, -input logic wd1_ready_req, -input logic [31:0] rd_data_rsp, -input logic rd_valid_rsp, -output logic [31:0] wd0_data_req, -output logic wd0_valid_req, -output logic [31:0] wd1_data_req, -output logic wd1_valid_req, -output logic rd_ready_rsp -); -''')); - - expect(sv, contains(''' -module Consumer ( -input logic clk, -input logic reset, -input logic [31:0] wd0_data_req, -input logic wd0_valid_req, -input logic [31:0] wd1_data_req, -input logic wd1_valid_req, -input logic rd_ready_rsp, -output logic wd0_ready_req, -output logic wd1_ready_req, -output logic [31:0] rd_data_rsp, -output logic rd_valid_rsp -); -''')); + expect( + sv, + contains([ + 'module Provider (', + 'input logic clk,', + 'input logic reset,', + 'input logic wd0_ready_req,', + 'input logic wd1_ready_req,', + 'input logic [31:0] rd_data_rsp,', + 'input logic rd_valid_rsp,', + 'output logic [31:0] wd0_data_req,', + 'output logic wd0_valid_req,', + 'output logic [31:0] wd1_data_req,', + 'output logic wd1_valid_req,', + 'output logic rd_ready_rsp', + ');', + ].join('\n'))); + + expect( + sv, + contains([ + 'module Consumer (', + 'input logic clk,', + 'input logic reset,', + 'input logic [31:0] wd0_data_req,', + 'input logic wd0_valid_req,', + 'input logic [31:0] wd1_data_req,', + 'input logic wd1_valid_req,', + 'input logic rd_ready_rsp,', + 'output logic wd0_ready_req,', + 'output logic wd1_ready_req,', + 'output logic [31:0] rd_data_rsp,', + 'output logic rd_valid_rsp', + ');', + ].join('\n'))); await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index d34c8e374..14963665c 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // provider_consumer_w_modify_test.dart @@ -148,37 +148,41 @@ void main() { final sv = mod.generateSynth(); - expect(sv, contains(''' -module Provider ( -input logic clk, -input logic reset, -input logic wd0_ready_req, -input logic wd1_ready_req, -input logic [31:0] rd_data_rsp, -input logic rd_valid_rsp, -output logic [31:0] wd0_data_req, -output logic wd0_valid_req, -output logic [31:0] wd1_data_req, -output logic wd1_valid_req, -output logic rd_ready_rsp -); -''')); - - expect(sv, contains(''' -module Consumer ( -input logic clk, -input logic reset, -input logic [31:0] wd0_data_req, -input logic wd0_valid_req, -input logic [31:0] wd1_data_req, -input logic wd1_valid_req, -input logic rd_ready_rsp, -output logic wd0_ready_req, -output logic wd1_ready_req, -output logic [31:0] rd_data_rsp, -output logic rd_valid_rsp -); -''')); + expect( + sv, + contains([ + 'module Provider (', + 'input logic clk,', + 'input logic reset,', + 'input logic wd0_ready_req,', + 'input logic wd1_ready_req,', + 'input logic [31:0] rd_data_rsp,', + 'input logic rd_valid_rsp,', + 'output logic [31:0] wd0_data_req,', + 'output logic wd0_valid_req,', + 'output logic [31:0] wd1_data_req,', + 'output logic wd1_valid_req,', + 'output logic rd_ready_rsp', + ');', + ].join('\n'))); + + expect( + sv, + contains([ + 'module Consumer (', + 'input logic clk,', + 'input logic reset,', + 'input logic [31:0] wd0_data_req,', + 'input logic wd0_valid_req,', + 'input logic [31:0] wd1_data_req,', + 'input logic wd1_valid_req,', + 'input logic rd_ready_rsp,', + 'output logic wd0_ready_req,', + 'output logic wd1_ready_req,', + 'output logic [31:0] rd_data_rsp,', + 'output logic rd_valid_rsp', + ');', + ].join('\n'))); await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); diff --git a/test/replication_test.dart b/test/replication_test.dart index 79296a395..892dbf6c7 100644 --- a/test/replication_test.dart +++ b/test/replication_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // replication_test.dart @@ -21,6 +21,13 @@ class ReplicationOpModule extends Module { } } +class SignExtendModule extends Module { + SignExtendModule(Logic a, int newWidth) { + a = addInput('a', a, width: a.width); + addOutput('b', width: newWidth) <= a.signExtend(newWidth); + } +} + void main() { group('Logic', () { tearDown(Simulator.reset); @@ -52,6 +59,27 @@ void main() { ], 1, originalWidth: 4); }); + test('multiply by 1 returns the original signal', () { + final a = Logic(width: 4); + expect(identical(a.replicate(1), a), isTrue); + }); + + test('multiply by 1 generates no replication in SystemVerilog', () async { + final mod = ReplicationOpModule(Logic(width: 4), 1); + await mod.build(); + final sv = mod.generateSynth(); + expect(sv, contains('assign b = a;')); + expect(sv, isNot(contains('{1{'))); + }); + + test('signExtend of a 1-bit signal by 1 generates no replication', + () async { + final mod = SignExtendModule(Logic(), 1); + await mod.build(); + final sv = mod.generateSynth(); + expect(sv, isNot(contains('{1{'))); + }); + test('multiply by 2 replicates the input signal twice', () async { await replicateVectors([ Vector({'a': 0}, {'b': 0}), diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..029c510f7 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -496,7 +496,7 @@ class ModWithPartialArrayAssignment extends Module { class ModWithConstInlineUnaryOp extends Module { ModWithConstInlineUnaryOp() { - addOutput('b', width: 8) <= ~Const(0, width: 8); + addOutput('b', width: 8) <= NotGate(Const(0, width: 8)).out; } } @@ -682,6 +682,9 @@ void main() { test('const unary inline op', () async { final mod = ModWithConstInlineUnaryOp(); await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, contains("~8'h0"), reason: sv); final vectors = [ Vector({}, {'b': 0xff}), @@ -885,13 +888,16 @@ void main() { expect(sv, isNot(contains('replicate'))); expect(sv, isNot(contains('subset'))); - expect(sv, contains(''' -module ModWithUselessWireMods ( -input logic [7:0] a, -input logic [7:0] b -); - -endmodule : ModWithUselessWireMods''')); + expect( + sv, + contains([ + 'module ModWithUselessWireMods (', + 'input logic [7:0] a,', + 'input logic [7:0] b', + ');', + '', + 'endmodule : ModWithUselessWireMods', + ].join('\n'))); }); test('partial array assignment sv', () async { diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index 6e1fc0949..d6ccf9d4f 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2025 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // swizzle_test.dart @@ -9,6 +9,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:rohd/src/utilities/sv_cleaner.dart'; import 'package:test/test.dart'; class SwizzlyModule extends Module { @@ -113,6 +114,70 @@ class LargeWidthSwizzle extends Module { } } +class SwizzleAdjacentBitSlices extends Module { + SwizzleAdjacentBitSlices() { + final a = addInput('a', Logic(width: 8), width: 8); + addOutput('out', width: 6) <= + [a[7], a[6], a[5], a[2], a[1], a[0]].swizzle(); + } +} + +class SwizzleAllAdjacentBitSlices extends Module { + SwizzleAllAdjacentBitSlices() { + final a = addInput('a', Logic(width: 8), width: 8); + addOutput('out', width: 3) <= [a[7], a[6], a[5]].swizzle(); + } +} + +class SwizzleAscendingBitSlices extends Module { + SwizzleAscendingBitSlices() { + final a = addInput('a', Logic(width: 8), width: 8); + addOutput('out', width: 3) <= [a[0], a[1], a[2]].swizzle(); + } +} + +class SwizzleNestedAdjacentBitSlices extends Module { + SwizzleNestedAdjacentBitSlices() { + final a = addInput('a', Logic(width: 8), width: 8); + final inner = Swizzle([a[7], a[6], a[5]]).out; + addOutput('out', width: 5) <= [inner, a[4], a[3]].swizzle(); + } +} + +class SwizzleAdjacentRanges extends Module { + SwizzleAdjacentRanges() { + final a = addInput('a', Logic(width: 8), width: 8); + addOutput('out', width: 6) <= [a.slice(5, 2), a.slice(1, 0)].swizzle(); + } +} + +class SwizzlePackedArrayElementBits extends Module { + SwizzlePackedArrayElementBits(LogicArray arr) { + final inArr = addInputArray('arr', arr, dimensions: [2, 2]); + final upper = inArr.elements[1] as LogicArray; + final lower = inArr.elements[0] as LogicArray; + addOutput('out', width: 4) <= + [ + upper.elements[1], + upper.elements[0], + lower.elements[1], + lower.elements[0], + ].swizzle(); + } +} + +class SwizzleUnpackedArrayElements extends Module { + SwizzleUnpackedArrayElements(LogicArray arr) { + final inArr = addInputArray( + 'arr', + arr, + dimensions: [4], + numUnpackedDimensions: 1, + ); + addOutput('out', width: 4) <= inArr.elements.reversed.toList().swizzle(); + } +} + void main() { tearDown(() async { await Simulator.reset(); @@ -319,6 +384,146 @@ void main() { }); }); + group('SystemVerilog slice collapsing', () { + test('collapses descending contiguous bit selects', () async { + final mod = SwizzleAdjacentBitSlices(); + await mod.build(); + + final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + + expect(sv, contains('assign out = {a[7:5],a[2:0]};')); + expect(sv, isNot(contains('a[7],a[6]'))); + + final vectors = [ + for (final value in [0x00, 0xe5, 0x3c, 0xff]) + Vector({ + 'a': value + }, { + 'out': (((value >> 5) & 0x7) << 3) | (value & 0x7), + }), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + final simResult = SimCompare.iverilogVector(mod, vectors); + expect(simResult, equals(true)); + }); + + test('omits braces when the whole swizzle collapses to one slice', + () async { + final mod = SwizzleAllAdjacentBitSlices(); + await mod.build(); + + final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + + expect(sv, contains('assign out = a[7:5];')); + expect(sv, isNot(contains('assign out = {a[7:5]};'))); + + final vectors = [ + for (final value in [0x00, 0xe0, 0xa0, 0xff]) + Vector({'a': value}, {'out': (value >> 5) & 0x7}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + final simResult = SimCompare.iverilogVector(mod, vectors); + expect(simResult, equals(true)); + }); + + test('does not collapse ascending bit selects', () async { + final mod = SwizzleAscendingBitSlices(); + await mod.build(); + + final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + + expect(sv, isNot(contains('a[2:0]'))); + expect(sv, contains('a[0]')); + expect(sv, contains('a[1]')); + expect(sv, contains('a[2]')); + + final vectors = [ + for (final value in [0x00, 0x05, 0x06, 0xff]) + Vector({ + 'a': value + }, { + 'out': ((value & 0x1) << 2) | + (((value >> 1) & 0x1) << 1) | + ((value >> 2) & 0x1), + }), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + final simResult = SimCompare.iverilogVector(mod, vectors); + expect(simResult, equals(true)); + }); + + test('collapses nested swizzle internals but not across the nested range', + () async { + final mod = SwizzleNestedAdjacentBitSlices(); + await mod.build(); + + final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + + expect(sv, contains('assign out = {(a[7:5]),a[4:3]};')); + expect(sv, isNot(contains('a[7:3]'))); + + final vectors = [ + for (final value in [0x00, 0xf8, 0xa8, 0xff]) + Vector({'a': value}, {'out': (value >> 3) & 0x1f}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + final simResult = SimCompare.iverilogVector(mod, vectors); + expect(simResult, equals(true)); + }); + + test('does not re-collapse adjacent range operands', () async { + final mod = SwizzleAdjacentRanges(); + await mod.build(); + + final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + + expect(sv, contains('assign out = {(a[5:2]),(a[1:0])};')); + expect(sv, isNot(contains('a[5:0]'))); + + final vectors = [ + for (final value in [0x00, 0x15, 0x2a, 0xff]) + Vector({'a': value}, {'out': value & 0x3f}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + final simResult = SimCompare.iverilogVector(mod, vectors); + expect(simResult, equals(true)); + }); + + test('collapses packed array element bit selects within each element', + () async { + final mod = SwizzlePackedArrayElementBits(LogicArray([2, 2], 1)); + await mod.build(); + + final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + + expect(sv, contains('assign out = {arr[1][1:0],arr[0][1:0]};')); + expect(sv, isNot(contains('arr[1][1],arr[1][0]'))); + + final vectors = [ + for (final value in [0x0, 0x5, 0xa, 0xf]) + Vector({'arr': value}, {'out': value}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + final simResult = SimCompare.iverilogVector(mod, vectors); + expect(simResult, equals(true)); + }); + + test('does not collapse unpacked array element selects', () async { + final mod = SwizzleUnpackedArrayElements( + LogicArray([4], 1, numUnpackedDimensions: 1), + ); + await mod.build(); + + final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + + expect(sv, isNot(contains('arr[3:0]'))); + expect(sv, contains('arr[3]')); + expect(sv, contains('arr[2]')); + expect(sv, contains('arr[1]')); + expect(sv, contains('arr[0]')); + }); + }); + test('annotated elements of swizzle in generated sv', () async { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); diff --git a/test/synth_builder_test.dart b/test/synth_builder_test.dart index 9b1831621..3cda6989b 100644 --- a/test/synth_builder_test.dart +++ b/test/synth_builder_test.dart @@ -48,44 +48,6 @@ class BModule extends Module { } } -class _WarningSynthesizer extends Synthesizer { - @override - bool generatesDefinition(Module module) => true; - - @override - SynthesisResult synthesize( - Module module, - String Function(Module module) getInstanceTypeOfModule, - ) => - _WarningSynthesisResult( - module, - getInstanceTypeOfModule, - warnings: ['warning for ${module.name}'], - ); -} - -class _WarningSynthesisResult extends SynthesisResult { - const _WarningSynthesisResult( - super.module, - super.getInstanceTypeOfModule, { - super.warnings, - }); - - @override - int get matchHashCode => module.definitionName.hashCode; - - @override - bool matchesImplementation(SynthesisResult other) => - other is _WarningSynthesisResult && - other.module.definitionName == module.definitionName; - - @override - String toFileContents() => ''; - - @override - List toSynthFileContents() => const []; -} - void main() { tearDown(() async { await Simulator.reset(); @@ -141,14 +103,5 @@ void main() { expect(synthResults.where((e) => e.module is AModule).length, 2); expect(synthResults.where((e) => e.module is BModule).length, 1); }); - - test('collects warnings from synthesis results', () async { - final mod = AModule(Logic(width: 4)); - await mod.build(); - - final synthBuilder = SynthBuilder(mod, _WarningSynthesizer()); - - expect(synthBuilder.warnings, ['warning for amodule']); - }); }); } diff --git a/test/systemverilog_port_types_test.dart b/test/systemverilog_port_types_test.dart new file mode 100644 index 000000000..80ca66505 --- /dev/null +++ b/test/systemverilog_port_types_test.dart @@ -0,0 +1,168 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_port_types_test.dart +// Tests for SystemVerilog port object and data types. +// +// 2026 July +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +class _PortStructure extends LogicStructure { + final bool asNet; + + factory _PortStructure({String? name, bool asNet = false}) => + _PortStructure._( + (asNet ? LogicNet.new : Logic.new)(width: 2, name: 'first'), + (asNet ? LogicNet.new : Logic.new)(width: 6, name: 'second'), + name: name, + asNet: asNet, + ); + + _PortStructure._(Logic first, Logic second, + {required String? name, required this.asNet}) + : super([first, second], name: name ?? 'portStructure'); + + @override + _PortStructure clone({String? name}) => + _PortStructure(name: name ?? this.name, asNet: asNet); +} + +class _PortTypesModule extends Module { + _PortTypesModule({bool includeUnpackedInOut = true}) { + final scalarIn = addInput('scalarIn', Logic(width: 8), width: 8); + addOutput('scalarOut', width: 8) <= scalarIn; + addInOut('scalarInOut', LogicNet(width: 8), width: 8); + + final structureIn = addTypedInput('structureIn', _PortStructure()); + addTypedOutput('structureOut', structureIn.clone) <= structureIn; + addTypedInOut('structureInOut', _PortStructure(asNet: true)); + + final packedArrayIn = addTypedInput('packedArrayIn', LogicArray([2, 3], 4)); + addTypedOutput('packedArrayOut', packedArrayIn.clone) <= packedArrayIn; + addTypedInOut('packedArrayInOut', LogicArray.net([2, 3], 4)); + + final unpackedArrayIn = addTypedInput( + 'unpackedArrayIn', LogicArray([2, 3], 4, numUnpackedDimensions: 1)); + addTypedOutput('unpackedArrayOut', unpackedArrayIn.clone) <= + unpackedArrayIn; + if (includeUnpackedInOut) { + addTypedInOut('unpackedArrayInOut', + LogicArray.net([2, 3], 4, numUnpackedDimensions: 1)); + } + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + final testCases = [ + ( + name: 'historical port types by default', + configuration: const SystemVerilogSynthesizerConfiguration(), + inputPrefix: 'input logic', + outputPrefix: 'output logic', + inOutPrefix: 'inout wire', + ), + ( + name: 'explicit object and data types', + configuration: const SystemVerilogSynthesizerConfiguration( + inputPortType: SystemVerilogPortTypeConfiguration(), + outputPortType: SystemVerilogPortTypeConfiguration(), + inOutPortType: SystemVerilogPortTypeConfiguration(), + ), + inputPrefix: 'input wire logic', + outputPrefix: 'output var logic', + inOutPrefix: 'inout wire logic', + ), + ( + name: 'implicit object and data types', + configuration: const SystemVerilogSynthesizerConfiguration( + inputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.implicit, + ), + outputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.implicit, + ), + inOutPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.implicit, + ), + ), + inputPrefix: 'input', + outputPrefix: 'output', + inOutPrefix: 'inout', + ), + ( + name: 'independently configured port directions', + configuration: const SystemVerilogSynthesizerConfiguration( + inputPortType: SystemVerilogPortTypeConfiguration( + dataType: SystemVerilogPortType.implicit, + ), + outputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.implicit, + ), + inOutPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + ), + ), + inputPrefix: 'input wire', + outputPrefix: 'output', + inOutPrefix: 'inout logic', + ), + ]; + + for (final testCase in testCases) { + test(testCase.name, () async { + final module = _PortTypesModule(); + await module.build(); + + final sv = module.generateSynth(configuration: testCase.configuration); + + final declarations = { + testCase.inputPrefix: [ + '[7:0] scalarIn', + '[7:0] structureIn', + '[1:0][2:0][3:0] packedArrayIn', + '[2:0][3:0] unpackedArrayIn [1:0]', + ], + testCase.outputPrefix: [ + '[7:0] scalarOut', + '[7:0] structureOut', + '[1:0][2:0][3:0] packedArrayOut', + '[2:0][3:0] unpackedArrayOut [1:0]', + ], + testCase.inOutPrefix: [ + '[7:0] scalarInOut', + '[7:0] structureInOut', + '[1:0][2:0][3:0] packedArrayInOut', + '[2:0][3:0] unpackedArrayInOut [1:0]', + ], + }; + + for (final MapEntry(key: prefix, value: suffixes) + in declarations.entries) { + for (final suffix in suffixes) { + expect(sv, contains('$prefix $suffix')); + } + } + + final iverilogModule = _PortTypesModule(includeUnpackedInOut: false); + await iverilogModule.build(); + SimCompare.checkIverilogVector( + iverilogModule, + [], + buildOnly: true, + synthesizerConfiguration: testCase.configuration, + ); + }); + } +} diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..d2fdabdd8 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // typed_port_test.dart diff --git a/tool/generate_coverage.sh b/tool/generate_coverage.sh index 7717c71ae..ddaebabb4 100755 --- a/tool/generate_coverage.sh +++ b/tool/generate_coverage.sh @@ -34,8 +34,8 @@ done # Remove old coverage data rm -rf coverage -# Run tests with coverage -dart test --coverage=coverage || true +# Run tests with line and branch coverage +dart test --coverage=coverage --branch-coverage || true # Check if coverage was generated if [ ! -d "coverage" ]; then @@ -51,6 +51,25 @@ dart run coverage:format_coverage \ --packages=.dart_tool/package_config.json \ --report-on=lib +# package:coverage emits branch details as BRDA records without BRF/BRH totals, +# which genhtml 2.x does not summarize. Calculate the branch rate directly. +BRANCH_SUMMARY=$(awk -F'[:,]' ' + /^BRDA:/ { + found++ + if ($5 ~ /^[1-9][0-9]*$/) { + hit++ + } + } + END { + if (found == 0) { + print "Error: no branch coverage data found" > "/dev/stderr" + exit 1 + } + printf "%.1f%% (%d of %d branches)", 100 * hit / found, hit, found + } +' coverage/lcov.info) +echo "Branch coverage: $BRANCH_SUMMARY" + # Install lcov if needed if ! command -v lcov &> /dev/null; then if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then @@ -59,8 +78,8 @@ if ! command -v lcov &> /dev/null; then fi fi -# Generate HTML report -genhtml -o coverage/html coverage/lcov.info --branch-coverage +# Generate HTML line coverage report +genhtml -o coverage/html coverage/lcov.info printf '\n%s\n\n' "Open coverage/html/index.html to review code coverage results." # Extract coverage percentage diff --git a/tool/gh_actions/devtool/build_web.sh b/tool/gh_actions/devtool/build_web.sh deleted file mode 100755 index f11186c63..000000000 --- a/tool/gh_actions/devtool/build_web.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2024 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# -# build_web.sh -# Build DevTool static web. -# -# 2024 January 03 -# Author: Yao Jing Quek - -set -euo pipefail - -cd rohd_devtools_extension - -flutter pub get - -dart run devtools_extensions build_and_copy --source=. --dest=../extension/devtools \ No newline at end of file diff --git a/tool/gh_actions/devtool/install_devtools.sh b/tool/gh_actions/devtool/install_devtools.sh new file mode 100755 index 000000000..6174a7c53 --- /dev/null +++ b/tool/gh_actions/devtool/install_devtools.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# install_devtools.sh +# Build the ROHD DevTools extension web artifact: +# extension/devtools/ – DevTools extension (iframe in Chrome DevTools) +# +# Usage (from repo root): +# bash tool/gh_actions/devtool/install_devtools.sh +# +# 2024 January 03 +# Author: Yao Jing Quek + +set -euo pipefail + +DEST="../extension/devtools" + +# ═══════════════════════════════════════════════════════════════════════ +# Build starts here +# ═══════════════════════════════════════════════════════════════════════ + +cd rohd_devtools_extension + +flutter pub get + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " Building DevTools extension..." +echo "════════════════════════════════════════════════════════════" + +flutter build web --pwa-strategy=none --release --no-tree-shake-icons + +if [ ! -f build/web/canvaskit/canvaskit.js ] || [ ! -f build/web/canvaskit/canvaskit.wasm ]; then + echo " Expected CanvasKit artifacts were not generated." + exit 1 +fi + +chmod 0755 build/web/canvaskit/canvaskit.js build/web/canvaskit/canvaskit.wasm + +# DevTools server serves the extension iframe from $DEST/build/. +rm -rf "$DEST/build" +mkdir -p "$DEST/build" +cp -R build/web/. "$DEST/build/" +rm -f "$DEST/build/manifest.json" "$DEST/build/flutter_service_worker.js" + +# Ensure config.yaml exists at $DEST/ (build_and_copy does not generate it). +if [ ! -f "$DEST/config.yaml" ]; then + echo " Creating config.yaml..." + cat > "$DEST/config.yaml" << 'CFGEOF' +name: rohd +issueTracker: https://github.com/intel/rohd/issues +version: 0.0.1 +materialIconCodePoint: '0xe1c5' +requiresConnection: false +CFGEOF +fi + +# Inject a redirect as the very first |' "$DEST/build/index.html" + +echo " Extension deployed to $DEST/ (web assets in $DEST/build/)" diff --git a/tool/gh_actions/devtool/test_devtools_install.sh b/tool/gh_actions/devtool/test_devtools_install.sh new file mode 100755 index 000000000..c919b100b --- /dev/null +++ b/tool/gh_actions/devtool/test_devtools_install.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# test_devtools_install.sh +# Smoke-test DevTools discovery of the installed ROHD DevTools extension. +# +# Usage (from repo root, after install_devtools.sh): +# bash tool/gh_actions/devtool/test_devtools_install.sh [package-root|extension-dir|github-tree-url] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +TARGET="${1:-extension/devtools}" +CLEANUP_DIR="" + +cleanup() { + if [[ -n "$CLEANUP_DIR" ]]; then + rm -rf "$CLEANUP_DIR" + fi +} +trap cleanup EXIT + +fail() { + echo " $*" >&2 + exit 1 +} + +echo "" +echo "════════════════════════════════════════════════════════════" +echo " Testing DevTools extension installation..." +echo "════════════════════════════════════════════════════════════" + +if [[ "$TARGET" =~ ^https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)(/(.*))?$ ]]; then + OWNER="${BASH_REMATCH[1]}" + REPO="${BASH_REMATCH[2]}" + BRANCH="${BASH_REMATCH[3]}" + TREE_PATH="${BASH_REMATCH[5]:-}" + CLEANUP_DIR="$(mktemp -d)" + ARCHIVE="$CLEANUP_DIR/$REPO-$BRANCH.zip" + + curl -fsSL "https://github.com/$OWNER/$REPO/archive/refs/heads/$BRANCH.zip" -o "$ARCHIVE" + unzip -q "$ARCHIVE" -d "$CLEANUP_DIR" + TARGET="$CLEANUP_DIR/$REPO-$BRANCH" + if [[ -n "$TREE_PATH" ]]; then + TARGET="$TARGET/$TREE_PATH" + fi +elif [[ "$TARGET" =~ ^https?:// ]]; then + fail "Unsupported URL. Expected a GitHub tree URL like https://github.com/intel/rohd/tree/artifacts" +elif [[ "$TARGET" != /* ]]; then + TARGET="$(pwd)/$TARGET" +fi + +if [[ ! -d "$TARGET" ]]; then + fail "Expected target directory not found: $TARGET" +fi + +(cd "$REPO_ROOT/rohd_devtools_extension" && dart run tool/test_devtools_install.dart "$TARGET") + +echo " DevTools extension installation smoke test passed." \ No newline at end of file diff --git a/tool/run_checks.sh b/tool/run_checks.sh index 5933cee30..6a0ea0080 100755 --- a/tool/run_checks.sh +++ b/tool/run_checks.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2022-2023 Intel Corporation +# Copyright (C) 2022-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # run_checks.sh @@ -53,7 +53,7 @@ if which iverilog; then echo 'Icarus Verilog found!' else declare -r exit_code=${?} - declare -r iverilog_recommended_version='11' + declare -r iverilog_recommended_version='12' echo 'Icarus Verilog not found: please install Icarus Verilog'\ "(iverilog; recommended version: ${iverilog_recommended_version})!" exit ${exit_code} From 5f7999adc1f65bc44a507f7403ed4019b60af4e5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 4 Aug 2026 15:34:27 -0700 Subject: [PATCH 65/77] false positive on validation fixed --- .../netlist/netlist_validation.dart | 24 +++++--- test/netlist_synthesizer_test.dart | 60 +++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index 85809678c..c6e7c5cac 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -12,6 +12,18 @@ import 'package:meta/meta.dart'; /// Graph queries and structural checks for an emitted module netlist. @internal class NetlistValidation { + static const _nonDrivingAliasTypes = { + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + static const _transparentTypes = { + r'$buf', + ..._nonDrivingAliasTypes, + }; + /// Prevents construction of this static utility class. NetlistValidation._(); @@ -70,13 +82,6 @@ class NetlistValidation { cellDirection: 'input', ); final driversByBit = _driversByBit(ports, cells); - const transparentTypes = { - r'$buf', - r'$slice', - r'$concat', - r'$struct_unpack', - r'$struct_pack', - }; for (final entry in driversByBit.entries) { if (entry.value.length <= 1) { @@ -129,7 +134,7 @@ class NetlistValidation { for (final entry in cells.entries) { final cell = entry.value; final type = cell['type'] as String? ?? ''; - if (transparentTypes.contains(type) || type == r'$const') { + if (_transparentTypes.contains(type) || type == r'$const') { continue; } final outputBits = _cellBits(cell, 'output'); @@ -189,6 +194,9 @@ class NetlistValidation { continue; } final type = entry.value['type'] as String? ?? 'unknown'; + if (_nonDrivingAliasTypes.contains(type)) { + continue; + } for (final port in connections.entries) { final direction = directions[port.key] as String?; if (direction != 'output' && direction != 'inout') { diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 71eba12d0..0f2361723 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2073,6 +2073,66 @@ void main() { ); }); + test('validation ignores structural aliases but counts buffers as drivers', + () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + }; + final concatAlias = { + 'concat': { + 'type': r'$concat', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [1], + }, + }, + }; + + expect( + NetlistValidation.validate( + ports, + concatAlias, + 'StructuralAlias', + printWarnings: false, + ), + isNot(contains(contains('multiple drivers'))), + ); + + final buffers = { + 'firstBuffer': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [2], + }, + }, + 'secondBuffer': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [2], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + buffers, + 'BufferedShort', + throwOnMultipleDrivers: true, + printWarnings: false, + ), + throwsStateError, + ); + }); + test('validation can skip unconnected output warnings', () { final ports = { 'a': { From 1cb81d984c1138fc44dbb1ad2df7ff5bbf69f400 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 7 Aug 2026 17:24:39 -0700 Subject: [PATCH 66/77] handle inouts properly --- .../netlist/netlist_validation.dart | 3 +- test/netlist_synthesizer_test.dart | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index c6e7c5cac..362d30d6f 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -199,7 +199,8 @@ class NetlistValidation { } for (final port in connections.entries) { final direction = directions[port.key] as String?; - if (direction != 'output' && direction != 'inout') { + final isTriStateOutput = type == r'$tribuf' && direction == 'inout'; + if (direction != 'output' && !isTriStateOutput) { continue; } for (final bit in (port.value as List?) ?? const []) { diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 0f2361723..d5a1bba85 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2133,6 +2133,35 @@ void main() { ); }); + test('validation accepts inout module boundaries', () { + final ports = >{ + 'dataBus': { + 'direction': 'inout', + 'bits': [1], + }, + }; + final cells = >{ + 'SharedDataBus': { + 'type': 'SharedDataBus', + 'port_directions': {'dataBus': 'inout'}, + 'connections': { + 'dataBus': [1], + }, + }, + }; + + expect( + NetlistValidation.validate( + ports, + cells, + 'FilterBank', + throwOnMultipleDrivers: true, + printWarnings: false, + ), + isEmpty, + ); + }); + test('validation can skip unconnected output warnings', () { final ports = { 'a': { From 9f0081bee26fe1596fd959591d3f4dcbc26b81dc Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 19 Aug 2026 15:07:09 -0700 Subject: [PATCH 67/77] Hid generateNetlist until we have services available. Moved from Options to Configuration nomenclature. Dependency on rohd_hierarchy so we can use Occurrence classes Leverage Namer and eliminate residual naming functionality. Remove warnings and move to just throw on error. --- lib/src/module.dart | 27 +-- lib/src/synthesizers/netlist/netlist.dart | 2 +- .../netlist/netlist_module_translation.dart | 4 +- .../netlist/netlist_synthesizer.dart | 214 ++++++++++++++---- ...=> netlist_synthesizer_configuration.dart} | 34 ++- .../synthesizers/netlist/netlist_utils.dart | 4 +- .../netlist/netlist_validation.dart | 150 +++++------- lib/src/synthesizers/synthesis_result.dart | 2 +- lib/src/synthesizers/synthesizers.dart | 3 +- .../utilities/synth_array_concat.dart | 13 +- .../utilities/synth_array_slice.dart | 13 +- .../utilities/synth_module_stop_policy.dart | 12 +- .../utilities/synth_operation_namer.dart | 143 ------------ .../utilities/synth_structure_concat.dart | 13 +- .../utilities/synth_structure_slice.dart | 13 +- lib/src/synthesizers/utilities/utilities.dart | 3 +- pubspec.yaml | 5 + test/netlist_synthesizer_test.dart | 192 ++++++---------- test/netlist_test.dart | 36 +-- test/synth_name_parity_test.dart | 14 ++ 20 files changed, 386 insertions(+), 511 deletions(-) rename lib/src/synthesizers/netlist/{netlist_options.dart => netlist_synthesizer_configuration.dart} (84%) delete mode 100644 lib/src/synthesizers/utilities/synth_operation_namer.dart diff --git a/lib/src/module.dart b/lib/src/module.dart index 2a6511b85..99e1b1539 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -927,6 +927,11 @@ abstract class Module { return outPort; } + bool _hasConsts(LogicStructure structure) => structure.elements.any( + (element) => + element is Const || + (element is LogicStructure && _hasConsts(element))); + /// Checks that the [logic] meets type requirements for `Typed` [Logic]s and /// returns a potentially modified [logic] to use. LogicType _validateType(LogicType logic, @@ -940,7 +945,7 @@ abstract class Module { throw PortTypeException(logic, exceptionMessage); } - if (logic is Const || (logic is LogicStructure && logic.hasConsts)) { + if (logic is Const || (logic is LogicStructure && _hasConsts(logic))) { if (LogicType == Logic) { // we're ok, can just convert to Logic final newLogic = @@ -1161,24 +1166,4 @@ abstract class Module { SystemVerilogSynthesizer(configuration: configuration), ).getSynthFileContents().join('\n\n////////////////////\n\n'); } - - /// Testing-only convenience for a synthesized netlist JSON representation of - /// this [Module]. Use [NetlistSynthesizer] directly outside tests. - @visibleForTesting - String generateNetlist( - {NetlistOptions options = const NetlistOptions(), String? packageRoot}) { - if (!_hasBuilt) { - throw ModuleNotBuiltException(this); - } - - return NetlistSynthesizer(options: options) - .synthesizeToJson(this, packageRoot: packageRoot); - } -} - -extension on LogicStructure { - /// Indicates that a [LogicStructure] has a [Const] element within it or - /// within one of its [elements]. - bool get hasConsts => - elements.any((e) => e is Const || (e is LogicStructure && e.hasConsts)); } diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart index c9c819b17..0e86e506f 100644 --- a/lib/src/synthesizers/netlist/netlist.dart +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -7,5 +7,5 @@ // 2026 February 11 // Author: Desmond Kirkpatrick -export 'netlist_options.dart'; export 'netlist_synthesizer.dart'; +export 'netlist_synthesizer_configuration.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 1dac184d8..9612788cb 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -485,14 +485,14 @@ class NetlistModuleTranslation { synthLogic is SynthLogicArrayElement) { bits = [ for (final bit in bits) - bit is int ? (arraySliceOldToNew[bit] ?? bit) : bit, + if (bit is int) arraySliceOldToNew[bit] ?? bit else bit, ]; } if (arrayConcatOldToNew.isNotEmpty && synthLogic is SynthLogicArrayElement) { bits = [ for (final bit in bits) - bit is int ? (arrayConcatOldToNew[bit] ?? bit) : bit, + if (bit is int) arrayConcatOldToNew[bit] ?? bit else bit, ]; } bits = resolveAggregateBits(bits); diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index c24f5932a..6e1be170c 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -18,6 +18,7 @@ import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; /// A simple [Synthesizer] that produces netlist-compatible JSON. /// @@ -31,19 +32,26 @@ import 'package:rohd/src/utilities/sanitizer.dart'; /// /// Usage: /// ```dart -/// const options = NetlistOptions(collapseTransparentClusters: true); -/// final synth = NetlistSynthesizer(options: options); +/// const configuration = NetlistSynthesizerConfiguration( +/// collapseTransparentClusters: true, +/// ); +/// final synth = NetlistSynthesizer(configuration: configuration); /// final builder = SynthBuilder(topModule, synth); /// final json = synth.synthesizeToJson(topModule); /// ``` class NetlistSynthesizer extends Synthesizer { - /// The current format version for netlist JSON produced by this synthesizer. - static const String formatVersion = '0.0.5'; + /// The version of the ROHD extensions to the Yosys JSON netlist format. + /// + /// Consumers of ROHD-generated netlists must reject an unsupported version. + /// This version changes when ROHD adds or changes fields that affect how a + /// consumer interprets the netlist. + static const String formatVersion = '0.0.1'; - /// The configuration options controlling netlist synthesis. + /// The configuration controlling netlist synthesis. /// - /// See [NetlistOptions] for documentation on individual fields. - final NetlistOptions options; + /// See [NetlistSynthesizerConfiguration] for documentation on individual + /// fields. + final NetlistSynthesizerConfiguration configuration; final SynthModuleStopPolicy _moduleStopPolicy; @@ -58,14 +66,15 @@ class NetlistSynthesizer extends Synthesizer { /// Creates a [NetlistSynthesizer]. /// - /// All synthesis parameters are bundled in [options]; see - /// [NetlistOptions] for documentation on each field. - NetlistSynthesizer({this.options = const NetlistOptions()}) - : _moduleStopPolicy = options.moduleStopPolicy ?? + /// All synthesis parameters are bundled in [configuration]; see + /// [NetlistSynthesizerConfiguration] for documentation on each field. + NetlistSynthesizer({ + this.configuration = const NetlistSynthesizerConfiguration(), + }) : _moduleStopPolicy = configuration.moduleStopPolicy ?? SynthModuleStopPolicy.netlist( - leafModuleTypes: options.leafModuleTypes), + leafModuleTypes: configuration.leafModuleTypes), _netlistCellMapper = - options.netlistCellMapper ?? NetlistCellMapper.withDefaults(); + configuration.netlistCellMapper ?? NetlistCellMapper.withDefaults(); @override bool generatesDefinition(Module module) => @@ -492,7 +501,7 @@ class NetlistSynthesizer extends Synthesizer { for (final cellEntry in cells.entries) { if (!cellEntry.key.startsWith( - SynthOperationNamer.arraySliceOperationName, + SynthArraySlice.operationName, )) { continue; } @@ -507,12 +516,13 @@ class NetlistSynthesizer extends Synthesizer { final oldBits = (portEntry.value as List).cast(); conns[portEntry.key] = [ for (final b in oldBits) - b is int - ? arraySliceOldToNew.putIfAbsent( - b, - translation.allocateWireId, - ) - : b, + if (b is int) + arraySliceOldToNew.putIfAbsent( + b, + translation.allocateWireId, + ) + else + b, ]; } } @@ -522,7 +532,7 @@ class NetlistSynthesizer extends Synthesizer { if (arraySliceOldToNew.isNotEmpty) { for (final cellEntry in cells.entries) { if (cellEntry.key.startsWith( - SynthOperationNamer.arraySliceOperationName, + SynthArraySlice.operationName, )) { continue; // skip the slice cells themselves } @@ -536,7 +546,8 @@ class NetlistSynthesizer extends Synthesizer { } final bits = (portEntry.value as List).cast(); final newBits = [ - for (final b in bits) b is int ? (arraySliceOldToNew[b] ?? b) : b, + for (final b in bits) + if (b is int) arraySliceOldToNew[b] ?? b else b, ]; if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { conns[portEntry.key] = newBits; @@ -556,7 +567,7 @@ class NetlistSynthesizer extends Synthesizer { } // Unconditionally remove struct_slice cells — they are duplicated by // $struct_unpack cells which carry field names. - if (cellKey.startsWith(SynthOperationNamer.structureSliceOperationName)) { + if (cellKey.startsWith(SynthStructureSlice.operationName)) { return true; } final params = cell['parameters'] as Map?; @@ -735,10 +746,11 @@ class NetlistSynthesizer extends Synthesizer { final structLayout = dstLogic is LogicStructure ? SynthStructureLayout(dstLogic) : null; final cellName = dstLogic != null - ? SynthOperationNamer.instanceName( - operationName: SynthOperationNamer.structureConcatOperationName, - destination: dstLogic) - : SynthOperationNamer.structureConcatOperationName; + ? _synthesizedCellName( + operationName: SynthStructureConcat.operationName, + destination: dstLogic, + ) + : SynthStructureConcat.operationName; // Build port_directions and connections. final portDirs = {}; @@ -787,9 +799,10 @@ class NetlistSynthesizer extends Synthesizer { } translation - ..processCellCleanup(enableDce: options.enableDCE) + ..processCellCleanup(enableDce: configuration.enableDeadCellElimination) ..processConstants( - applyAlias: applyAlias, pruneFloating: options.enableDCE); + applyAlias: applyAlias, + pruneFloating: configuration.enableDeadCellElimination); // -- Break shared wire IDs for array_concat cells -------------------- // After aliasing, concat Y can share wire IDs with the independently @@ -810,8 +823,7 @@ class NetlistSynthesizer extends Synthesizer { ]; for (final cellEntry in cells.entries) { - if (!cellEntry.key - .startsWith(SynthOperationNamer.arrayConcatOperationName)) { + if (!cellEntry.key.startsWith(SynthArrayConcat.operationName)) { continue; } if (cellEntry.key.startsWith('array_concat_output_')) { @@ -833,7 +845,8 @@ class NetlistSynthesizer extends Synthesizer { continue; } final newBits = [ - for (final b in oldBits) b is int ? translation.allocateWireId() : b, + for (final b in oldBits) + if (b is int) translation.allocateWireId() else b, ]; conns[portEntry.key] = newBits; arrayConcatReplacements @@ -930,8 +943,8 @@ class NetlistSynthesizer extends Synthesizer { applyAlias: applyAlias, arraySliceOldToNew: arraySliceOldToNew, arrayConcatOldToNew: arrayConcatOldToNew, - pruneUndriven: options.enableDCE, - drivenBits: options.enableDCE + pruneUndriven: configuration.enableDeadCellElimination, + drivenBits: configuration.enableDeadCellElimination ? NetlistValidation.connectedBits(ports, cells, portDirections: const {'input', 'inout'}, cellDirection: 'output') @@ -939,19 +952,10 @@ class NetlistSynthesizer extends Synthesizer { final netnames = translation.netnames; // -- Structural validation ------------------------------------------- - // Always catch netlist shorts, even when assertions are disabled. - final warnings = NetlistValidation.validate(ports, cells, module.name, - netnames: netnames, - throwOnMultipleDrivers: true, - printWarnings: false, - checkUnconnectedOutputs: options.validateUnconnectedOutputs); + NetlistValidation.validate(ports, cells, module.name, netnames: netnames); return NetlistSynthesisResult(module, getInstanceTypeOfModule, - ports: ports, - cells: cells, - netnames: netnames, - attributes: attr, - warnings: warnings); + ports: ports, cells: cells, netnames: netnames, attributes: attr); } /// Apply all post-processing passes to the modules map. @@ -961,7 +965,7 @@ class NetlistSynthesizer extends Synthesizer { /// **Flow 2** (incremental full via `moduleNetlistJson`). /// Also used internally by [buildModulesMap] / [synthesizeToJson]. void applyPostProcessingPasses(Map> modules) { - if (options.collapseTransparentClusters) { + if (configuration.collapseTransparentClusters) { NetlistPasses.collapseConcatOfAdjacentSlices(modules); NetlistPasses.removeTrivialConcatAliases(modules); NetlistPasses.applyTransparentClustering(modules); @@ -979,7 +983,7 @@ class NetlistSynthesizer extends Synthesizer { Map> buildModulesMap( SynthBuilder synth, Module top, {bool? slimMode}) { - final effectiveSlimMode = slimMode ?? options.slimMode; + final effectiveSlimMode = slimMode ?? configuration.slimMode; final swEntries = Stopwatch()..start(); final modules = NetlistPasses.collectModuleEntries(synth.synthesisResults, topModule: top, includeCellConnections: !effectiveSlimMode); @@ -1000,7 +1004,7 @@ class NetlistSynthesizer extends Synthesizer { swCollect.stop(); final swCompress = Stopwatch()..start(); - if (options.compressBitRanges) { + if (configuration.compressBitRanges) { _compressModulesMap(modules); } swCompress.stop(); @@ -1012,7 +1016,7 @@ class NetlistSynthesizer extends Synthesizer { }; final swEncode = Stopwatch()..start(); - final encoder = options.compactJson + final encoder = configuration.compactJson ? const JsonEncoder() : const JsonEncoder.withIndent(' '); final result = encoder.convert(combined); @@ -1114,3 +1118,115 @@ class NetlistSynthesizer extends Synthesizer { return generateCombinedJson(sb, top, slimMode: slimMode); } } + +String _synthesizedCellName({ + required String operationName, + required Logic destination, +}) => + '${Sanitizer.sanitizeSV(operationName)}_' + '${_destinationSuffix(destination)}'; + +String _destinationSuffix(Logic destination) { + final module = destination.parentModule; + if (module == null) { + throw SynthException( + 'Cannot derive a netlist cell key for ${destination.name}: ' + 'the destination has no parent module.', + ); + } + + final rootLocation = _logicLocationInModule(module, destination); + final elementPath = _logicElementPathIndices(destination).path; + final parts = [ + ..._modulePathIndices(module).path, + ...rootLocation.path, + ...elementPath, + ]; + + return parts.isEmpty ? '0' : parts.map((part) => part.toString()).join('_'); +} + +OccurrenceAddress _modulePathIndices(Module module) { + final parent = module.parent; + if (parent == null) { + return const OccurrenceAddress([0]); + } + + final siblings = parent.subModules.toList(); + final index = siblings.indexWhere( + (submodule) => identical(submodule, module), + ); + return OccurrenceAddress([ + ..._modulePathIndices(parent).path, + if (index < 0) 0 else index, + ]); +} + +OccurrenceAddress _logicElementPathIndices(Logic destination) { + final elementPath = []; + var root = destination; + while (root.parentStructure != null) { + final parent = root.parentStructure!; + final index = parent.elements.indexWhere( + (element) => identical(element, root), + ); + elementPath.insert(0, index < 0 ? root.arrayIndex ?? 0 : index); + root = parent; + } + + final module = root.parentModule; + if (module == null) { + throw SynthException( + 'Cannot derive a netlist cell key for ${destination.name}: ' + 'the logic root has no parent module.', + ); + } + + return OccurrenceAddress(elementPath); +} + +OccurrenceAddress _logicLocationInModule(Module module, Logic root) { + final signalIndex = _rootSignalIndexInModule(module, root); + return OccurrenceAddress([signalIndex]); +} + +int _rootSignalIndexInModule(Module module, Logic root) { + final inputIndex = _identityIndex(module.inputs.values, root); + if (inputIndex != null) { + return inputIndex; + } + + final outputIndex = _identityIndex(module.outputs.values, root); + if (outputIndex != null) { + return module.inputs.length + outputIndex; + } + + final inOutIndex = _identityIndex(module.inOuts.values, root); + if (inOutIndex != null) { + return module.inputs.length + module.outputs.length + inOutIndex; + } + + final internalIndex = _identityIndex(module.internalSignals, root); + if (internalIndex != null) { + return module.inputs.length + + module.outputs.length + + module.inOuts.length + + internalIndex; + } + + throw SynthException( + 'Cannot derive a netlist cell key for ${root.name}: ' + 'the logic root is not registered with module ${module.name}.', + ); +} + +int? _identityIndex(Iterable logics, Logic target) { + var index = 0; + for (final logic in logics) { + if (identical(logic, target)) { + return index; + } + index++; + } + return null; +} diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart similarity index 84% rename from lib/src/synthesizers/netlist/netlist_options.dart rename to lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart index 750e69e93..95932f441 100644 --- a/lib/src/synthesizers/netlist/netlist_options.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -1,7 +1,7 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// netlist_options.dart +// netlist_synthesizer_configuration.dart // Configuration for netlist synthesis. // // 2026 March 12 @@ -10,11 +10,12 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +export '../utilities/synth_module_stop_policy.dart'; -/// Configuration options for netlist synthesis. +/// Configuration for netlist synthesis. /// /// The netlist synthesizer serves two main consumer flows, both configured -/// through these options: +/// through this configuration: /// /// **Flow 1 — Slim JSON** (`NetlistService.slimJson`): /// Batch synthesis of the entire design, producing a lightweight @@ -37,12 +38,9 @@ import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; /// /// Example usage: /// ```dart -/// const options = NetlistOptions( -/// collapseTransparentClusters: true, -/// ); -/// final synth = NetlistSynthesizer(options: options); +/// final synth = NetlistSynthesizer(); /// ``` -class NetlistOptions { +class NetlistSynthesizerConfiguration { /// The policy used to decide which modules stop hierarchy traversal and are /// emitted as cells in their parent instead of as separate module /// definitions. When `null`, [SynthModuleStopPolicy.netlist] is used. @@ -69,16 +67,14 @@ class NetlistOptions { /// bits back to their ultimate source bits, and replaces every /// multi-cell cluster with a direct `$buf`. This subsumes all of /// the individual collapse passes above. + @internal final bool collapseTransparentClusters; /// When `true`, dead-cell elimination is performed after aliasing to /// remove cells whose inputs are entirely undriven or whose outputs /// are entirely unconsumed. - final bool enableDCE; - - /// When `true`, validation reports debug warnings for cells whose output - /// bits are not consumed. Multiple-driver validation always runs. - final bool validateUnconnectedOutputs; + @internal + final bool enableDeadCellElimination; /// When `true`, the synthesizer produces "slim" output: cell connection maps /// are not copied into the emitted JSON projection. Netnames and ports are @@ -100,17 +96,13 @@ class NetlistOptions { /// indentation. final bool compactJson; - /// Creates a [NetlistOptions] with the given configuration. - /// - /// All parameters have sensible defaults matching the current - /// netlist synthesizer behaviour. - const NetlistOptions({ + /// Creates a configuration for netlist synthesis. + const NetlistSynthesizerConfiguration({ this.moduleStopPolicy, this.leafModuleTypes = const [FlipFlop], this.netlistCellMapper, - this.collapseTransparentClusters = false, - this.enableDCE = true, - this.validateUnconnectedOutputs = true, + @visibleForTesting this.collapseTransparentClusters = false, + @visibleForTesting this.enableDeadCellElimination = true, this.slimMode = false, this.compressBitRanges = false, this.compactJson = false, diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index ffcd08e40..7ff34ae78 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -164,8 +164,8 @@ abstract class NetlistUtils { // Choose a name for the aggregate port. final rootName = tryGetSynthLogicName(rootSL) ?? 'agg_${minBit}_$maxBit'; - // Replace individual ports with the aggregate. The bypassed - // BusSubset cells are left in place; the post-synthesis DCE pass + // Replace individual ports with the aggregate. The bypassed BusSubset + // cells are left in place; the post-synthesis Dead Cell Elimination pass // will remove them if their outputs are no longer consumed. for (final (portName, _, _, _) in ports) { connections.remove(portName); diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index 362d30d6f..f2c8a6b87 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -8,6 +8,7 @@ // Author: Desmond Kirkpatrick import 'package:meta/meta.dart'; +import 'package:rohd/src/exceptions/synth_exception.dart'; /// Graph queries and structural checks for an emitted module netlist. @internal @@ -19,11 +20,6 @@ class NetlistValidation { r'$struct_pack', }; - static const _transparentTypes = { - r'$buf', - ..._nonDrivingAliasTypes, - }; - /// Prevents construction of this static utility class. NetlistValidation._(); @@ -51,48 +47,27 @@ class NetlistValidation { }), }; - /// Reports disconnected cells, floating constants, and shorted wires. - static List validate( + /// Throws [NetlistValidationException] if the netlist has structural errors. + static void validate( Map> ports, Map> cells, String moduleName, { Map? netnames, - bool throwOnMultipleDrivers = false, - bool printWarnings = true, - bool checkUnconnectedOutputs = true, }) { - final warnings = []; - final multipleDriverWarnings = []; + final issues = []; - void report(String message, {bool multipleDriver = false}) { - warnings.add(message); - if (multipleDriver) { - multipleDriverWarnings.add(message); - } - if (printWarnings) { - // ignore: avoid_print - print(message); - } - } - - final consumedBits = connectedBits( - ports, - cells, - portDirections: const {'output', 'inout'}, - cellDirection: 'input', - ); final driversByBit = _driversByBit(ports, cells); for (final entry in driversByBit.entries) { if (entry.value.length <= 1) { continue; } - report( - '[netlist-validate] WARNING: $moduleName: ' + issues.add(NetlistValidationIssue( 'wire bit ${entry.key} has multiple drivers: ' '${entry.value.join(', ')}', - multipleDriver: true, - ); + wireBit: entry.key, + drivers: entry.value, + )); } if (netnames != null) { @@ -113,55 +88,18 @@ class NetlistValidation { if (aggregateDrivers.length <= 1) { continue; } - report( - '[netlist-validate] WARNING: $moduleName: ' + issues.add(NetlistValidationIssue( 'aggregate net "${entry.key}" is reached from multiple drivers: ' '${aggregateDrivers.join(', ')}', - multipleDriver: true, - ); + netname: entry.key, + drivers: aggregateDrivers.toList(), + )); } } - if (throwOnMultipleDrivers && multipleDriverWarnings.isNotEmpty) { - throw StateError( - 'Netlist validation failed for $moduleName: ' - '${multipleDriverWarnings.length} multiple-driver wire bit(s) found.\n' - '${multipleDriverWarnings.join('\n')}', - ); - } - - if (checkUnconnectedOutputs) { - for (final entry in cells.entries) { - final cell = entry.value; - final type = cell['type'] as String? ?? ''; - if (_transparentTypes.contains(type) || type == r'$const') { - continue; - } - final outputBits = _cellBits(cell, 'output'); - if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - report( - '[netlist-validate] WARNING: $moduleName: ' - 'cell "${entry.key}" (type: $type) has no consumed outputs ' - '— fully disconnected logic gate', - ); - } - } - - for (final entry in cells.entries.where( - (entry) => entry.value['type'] == r'$const', - )) { - final outputBits = _cellBits(entry.value, 'output'); - if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { - report( - '[netlist-validate] WARNING: $moduleName: ' - r'$const cell "${entry.key}" drives wires consumed by nothing ' - '— floating constant', - ); - } - } + if (issues.isNotEmpty) { + throw NetlistValidationException(moduleName, issues); } - - return warnings; } /// Collects the port and cell output drivers for each integer bit ID. @@ -213,18 +151,52 @@ class NetlistValidation { return drivers; } +} - /// Returns integer bits connected to ports with the requested [direction]. - static List _cellBits(Map cell, String direction) { - final connections = cell['connections'] as Map?; - final directions = cell['port_directions'] as Map?; - if (connections == null || directions == null) { - return const []; - } - return connections.entries - .where((port) => directions[port.key] == direction) - .expand((port) => (port.value as List?) ?? const []) - .whereType() - .toList(); - } +/// A structural netlist validation failure. +@internal +class NetlistValidationException extends SynthException { + /// The module containing the structural errors. + final String moduleName; + + /// The structural errors found in [moduleName]. + final List issues; + + /// Creates a validation exception for [moduleName]. + NetlistValidationException( + this.moduleName, Iterable issues) + : issues = List.unmodifiable(issues), + super('Netlist validation failed for $moduleName.'); + + @override + String toString() => 'Netlist validation failed for $moduleName: ' + '${issues.length} issue(s) found.\n' + '${issues.join('\n')}'; +} + +/// A structural problem found while validating an emitted netlist. +@internal +class NetlistValidationIssue { + /// A human-readable explanation of the structural problem. + final String description; + + /// The affected wire bit, when the problem concerns one bit. + final int? wireBit; + + /// The affected aggregate net name, when applicable. + final String? netname; + + /// The drivers involved in the problem, when applicable. + final List drivers; + + /// Creates a structural validation issue. + NetlistValidationIssue( + this.description, { + this.wireBit, + this.netname, + Iterable drivers = const [], + }) : drivers = List.unmodifiable(drivers); + + @override + String toString() => description; } diff --git a/lib/src/synthesizers/synthesis_result.dart b/lib/src/synthesizers/synthesis_result.dart index 4154832a2..6a8634e24 100644 --- a/lib/src/synthesizers/synthesis_result.dart +++ b/lib/src/synthesizers/synthesis_result.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesis_result.dart diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index a2b5831be..da5d76586 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'netlist/netlist.dart'; @@ -7,4 +7,3 @@ export 'synth_file_contents.dart'; export 'synthesis_result.dart'; export 'synthesizer.dart'; export 'systemverilog/systemverilog.dart'; -export 'utilities/synth_module_stop_policy.dart'; diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index e0c64aa71..168ec0e01 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -9,30 +9,27 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [Swizzle] used by synthesis backends to explicitly assemble a /// [LogicArray] from its elements. @internal class SynthArrayConcat extends Swizzle { + /// The canonical base name for synthesized array concat operations. + static const String operationName = 'array_concat'; + final LogicArray _destination; /// Creates a synthesis array concatenation from [signals]. SynthArrayConcat(super.signals, {required LogicArray destination}) : _destination = destination, - super( - name: SynthOperationNamer.instanceName( - operationName: SynthOperationNamer.arrayConcatOperationName, - destination: destination, - ), - ); + super(name: operationName); @override bool get hasBuilt => true; @override Object get instanceNameKey => ( - operationName: SynthOperationNamer.arrayConcatOperationName, + operationName: operationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart index b1b13e5ce..ec4446826 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -9,12 +9,14 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [BusSubset] used by synthesis backends to explicitly extract a /// [LogicArray] element from its packed parent representation. @internal class SynthArraySlice extends BusSubset { + /// The canonical base name for synthesized array slice operations. + static const String operationName = 'array_slice'; + final Logic _destination; /// Creates a synthesis array slice over the selected indices of [bus]. @@ -24,19 +26,14 @@ class SynthArraySlice extends BusSubset { super.endIndex, { required Logic destination, }) : _destination = destination, - super( - name: SynthOperationNamer.instanceName( - operationName: SynthOperationNamer.arraySliceOperationName, - destination: destination, - ), - ); + super(name: operationName); @override bool get hasBuilt => true; @override Object get instanceNameKey => ( - operationName: SynthOperationNamer.arraySliceOperationName, + operationName: operationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart index 08fb7799f..317565cce 100644 --- a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -40,15 +40,9 @@ class SynthModuleStopPolicy { /// Creates the default SystemVerilog stopping policy. factory SynthModuleStopPolicy.systemVerilog() => SynthModuleStopPolicy( leafPredicates: [ - (module) { - // ignore: deprecated_member_use_from_same_package - if (module is CustomSystemVerilog) { - return true; - } - - return module is SystemVerilog && - module.generatedDefinitionType == DefinitionGenerationType.none; - }, + (module) => + module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none, ], ); diff --git a/lib/src/synthesizers/utilities/synth_operation_namer.dart b/lib/src/synthesizers/utilities/synth_operation_namer.dart deleted file mode 100644 index 145f5b619..000000000 --- a/lib/src/synthesizers/utilities/synth_operation_namer.dart +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (C) 2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// synth_operation_namer.dart -// Stable naming for synthesis-created helper operations. -// -// 2026 July 14 -// Author: Desmond Kirkpatrick - -import 'package:meta/meta.dart'; -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/sanitizer.dart'; - -/// Stable names for synthesis-created helper operations. -@internal -class SynthOperationNamer { - /// Canonical base name for synthesis-created array slice operations. - /// - /// Netlist synthesis uses these when a [LogicArray] port or submodule output - /// must be decomposed into element wires, such as a child reading - /// `values.elements[0]` from an aggregate array input port. - static const String arraySliceOperationName = 'array_slice'; - - /// Canonical base name for synthesis-created array concat operations. - /// - /// Netlist synthesis uses these when independently driven [LogicArray] - /// elements must be reassembled for an aggregate consumer, such as - /// `values.elements[0] <= a` and `values.elements[1] <= b` feeding a child - /// array input port. - static const String arrayConcatOperationName = 'array_concat'; - - /// Canonical base name for synthesis-created structure slice operations. - /// - /// Synthesis uses these internally when a [LogicStructure] aggregate must - /// expose field wires. The netlist projection can replace them with - /// `$struct_unpack` cells so field names are preserved. - static const String structureSliceOperationName = 'struct_slice'; - - /// Canonical base name for synthesis-created structure concat operations. - /// - /// Netlist synthesis uses these when independently driven [LogicStructure] - /// fields must be packed back into an aggregate struct output or submodule - /// input. The emitted netlist projection represents this as `$struct_pack`. - static const String structureConcatOperationName = 'struct_concat'; - - SynthOperationNamer._(); - - /// Returns the canonical instance name for a synthesis-created operation - /// that targets [destination]. - /// - /// The numeric suffix is derived from [destination]'s structural position, - /// not from the order in which a backend asks for names. This keeps helper - /// operation names stable across output formats that traverse a module in - /// different orders. - static String instanceName({ - required String operationName, - required Logic destination, - }) => - '${Sanitizer.sanitizeSV(operationName)}_' - '${_destinationSuffix(destination)}'; - - static String _destinationSuffix(Logic destination) { - final parts = [ - ..._modulePathIndices(destination.parentModule), - ..._logicLocationIndices(destination), - ]; - - return parts.isEmpty ? '0' : parts.join('_'); - } - - static List _modulePathIndices(Module? module) { - if (module == null) { - return const [0]; - } - - final parent = module.parent; - if (parent == null) { - return const [0]; - } - - final siblings = parent.subModules.toList(); - final index = siblings.indexWhere( - (submodule) => identical(submodule, module), - ); - return [..._modulePathIndices(parent), if (index < 0) 0 else index]; - } - - static List _logicLocationIndices(Logic destination) { - final elementPath = []; - var root = destination; - while (root.parentStructure != null) { - final parent = root.parentStructure!; - final index = parent.elements.indexWhere( - (element) => identical(element, root), - ); - elementPath.insert(0, index < 0 ? root.arrayIndex ?? 0 : index); - root = parent; - } - - final module = root.parentModule; - if (module == null) { - return [0, ...elementPath]; - } - - final location = _logicLocationInModule(module, root); - return [...location, ...elementPath]; - } - - static List _logicLocationInModule(Module module, Logic root) { - final inputIndex = _identityIndex(module.inputs.values, root); - if (inputIndex >= 0) { - return [0, inputIndex]; - } - - final outputIndex = _identityIndex(module.outputs.values, root); - if (outputIndex >= 0) { - return [1, outputIndex]; - } - - final inOutIndex = _identityIndex(module.inOuts.values, root); - if (inOutIndex >= 0) { - return [2, inOutIndex]; - } - - final internalIndex = _identityIndex(module.internalSignals, root); - if (internalIndex >= 0) { - return [3, internalIndex]; - } - - return const [4, 0]; - } - - static int _identityIndex(Iterable logics, Logic target) { - var index = 0; - for (final logic in logics) { - if (identical(logic, target)) { - return index; - } - index++; - } - return -1; - } -} diff --git a/lib/src/synthesizers/utilities/synth_structure_concat.dart b/lib/src/synthesizers/utilities/synth_structure_concat.dart index d78d8721c..dfc23408a 100644 --- a/lib/src/synthesizers/utilities/synth_structure_concat.dart +++ b/lib/src/synthesizers/utilities/synth_structure_concat.dart @@ -9,30 +9,27 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [Swizzle] used by synthesis backends to explicitly assemble a /// [LogicStructure] from its leaf elements. @internal class SynthStructureConcat extends Swizzle { + /// The canonical base name for synthesized structure concat operations. + static const String operationName = 'struct_concat'; + final LogicStructure _destination; /// Creates a synthesis structure concatenation from [signals]. SynthStructureConcat(super.signals, {required LogicStructure destination}) : _destination = destination, - super( - name: SynthOperationNamer.instanceName( - operationName: SynthOperationNamer.structureConcatOperationName, - destination: destination, - ), - ); + super(name: operationName); @override bool get hasBuilt => true; @override Object get instanceNameKey => ( - operationName: SynthOperationNamer.structureConcatOperationName, + operationName: operationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/synth_structure_slice.dart b/lib/src/synthesizers/utilities/synth_structure_slice.dart index d33b6f80f..d3c7dfc18 100644 --- a/lib/src/synthesizers/utilities/synth_structure_slice.dart +++ b/lib/src/synthesizers/utilities/synth_structure_slice.dart @@ -9,12 +9,14 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; /// A [BusSubset] used by synthesis backends to explicitly extract a /// [LogicStructure] leaf from its packed parent representation. @internal class SynthStructureSlice extends BusSubset { + /// The canonical base name for synthesized structure slice operations. + static const String operationName = 'struct_slice'; + final Logic _destination; /// Creates a synthesis structure slice over the selected indices of [bus]. @@ -24,19 +26,14 @@ class SynthStructureSlice extends BusSubset { super.endIndex, { required Logic destination, }) : _destination = destination, - super( - name: SynthOperationNamer.instanceName( - operationName: SynthOperationNamer.structureSliceOperationName, - destination: destination, - ), - ); + super(name: operationName); @override bool get hasBuilt => true; @override Object get instanceNameKey => ( - operationName: SynthOperationNamer.structureSliceOperationName, + operationName: operationName, destination: _destination, ); } diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index 1387bfb50..1a02d1952 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'synth_array_concat.dart'; @@ -7,7 +7,6 @@ export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; export 'synth_module_stop_policy.dart'; -export 'synth_operation_namer.dart'; export 'synth_structure_concat.dart'; export 'synth_structure_layout.dart'; export 'synth_structure_slice.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 42949a933..9df237505 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,9 +13,14 @@ dependencies: collection: ^1.15.0 logging: ^1.0.1 meta: ^1.9.0 + rohd_hierarchy: ^0.1.0 test: ^1.17.3 dev_dependencies: benchmark_harness: ^2.2.0 coverage: ^1.10.0 yaml: ^3.1.1 + +dependency_overrides: + rohd_hierarchy: + path: packages/rohd_hierarchy diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index d5a1bba85..c7a3f1280 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -13,7 +13,7 @@ import 'dart:convert'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; -import 'package:rohd/src/synthesizers/utilities/synth_operation_namer.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_structure_concat.dart'; import 'package:test/test.dart'; import '../example/example.dart'; @@ -519,10 +519,12 @@ FilterBank _buildFilterBank() { /// Build a module and synthesize to a parsed JSON map. Future> _synthToMap( Module mod, { - NetlistOptions options = const NetlistOptions(), + NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), }) async { await mod.build(); - final synth = SynthBuilder(mod, NetlistSynthesizer(options: options)); + final synth = + SynthBuilder(mod, NetlistSynthesizer(configuration: configuration)); final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson(mod); return jsonDecode(json) as Map; } @@ -969,9 +971,9 @@ void main() { }); }); - // ── Group 4: NetlistOptions permutations ───────────────────────── + // ── Group 4: NetlistSynthesizerConfiguration permutations ────────────────── - group('NetlistOptions', () { + group('NetlistSynthesizerConfiguration', () { late Module filterBank; setUp(() async { @@ -980,19 +982,23 @@ void main() { await filterBank.build(); }); - test('default options produce valid netlist', () async { + test('default configuration produce valid netlist', () { final synth = SynthBuilder(filterBank, NetlistSynthesizer()); final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( filterBank, ); final parsed = jsonDecode(json) as Map; + expect(parsed['creator'], equals('NetlistSynthesizer (rohd)')); + expect(parsed['version'], equals(NetlistSynthesizer.formatVersion)); expect(_modules(parsed), isNotEmpty); }); - test('slimMode omits connections', () async { + test('slimMode omits connections', () { final synth = SynthBuilder( filterBank, - NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(slimMode: true)), ); final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( filterBank, @@ -1022,7 +1028,7 @@ void main() { await module.build(); final translator = NetlistSynthesizer( - options: const NetlistOptions(slimMode: true), + configuration: const NetlistSynthesizerConfiguration(slimMode: true), ); final slim = translator.synthesizeToJson(module); final expanded = translator.synthesizeToJson(module, slimMode: false); @@ -1042,9 +1048,10 @@ void main() { test( 'filter bank can stop traversal at an opaque custom SV module', - () async { + () { final synthesizer = NetlistSynthesizer( - options: const NetlistOptions(leafModuleTypes: [FlipFlop, MacUnit]), + configuration: const NetlistSynthesizerConfiguration( + leafModuleTypes: [FlipFlop, MacUnit]), ); final json = jsonDecode(synthesizer.synthesizeToJson(filterBank)) as Map; @@ -1079,10 +1086,12 @@ void main() { }, ); - test('DCE disabled still produces valid netlist', () async { + test('DCE disabled still produces valid netlist', () { final synth = SynthBuilder( filterBank, - NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false)), ); final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( filterBank, @@ -1091,10 +1100,12 @@ void main() { expect(_modules(parsed), isNotEmpty); }); - test('all optimizations disabled produces valid netlist', () async { + test('all optimizations disabled produces valid netlist', () { final synth = SynthBuilder( filterBank, - NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false)), ); final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( filterBank, @@ -1115,7 +1126,9 @@ void main() { await fb2.build(); final slimSynth = SynthBuilder( fb2, - NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(slimMode: true)), ); final slimJson = (slimSynth.synthesizer as NetlistSynthesizer).synthesizeToJson(fb2); @@ -1441,7 +1454,8 @@ void main() { final childDefinitionName = module.subModules.single.definitionName; final synthesizer = NetlistSynthesizer( - options: const NetlistOptions(leafModuleTypes: [AddModule]), + configuration: const NetlistSynthesizerConfiguration( + leafModuleTypes: [AddModule]), ); final json = jsonDecode(synthesizer.synthesizeToJson(module)) @@ -1509,13 +1523,7 @@ void main() { }); test('array concat outputs use fresh wire IDs', () async { - final warnings = []; - final json = await runZoned( - () => _synthToMap(InternalArrayToChildModule()), - zoneSpecification: ZoneSpecification( - print: (_, __, ___, line) => warnings.add(line), - ), - ); + final json = await _synthToMap(InternalArrayToChildModule()); final moduleDef = _modules(json)['InternalArrayToChildModule'] as Map; final cells = _cells(moduleDef); @@ -1523,10 +1531,6 @@ void main() { (entry) => entry.key.startsWith('array_concat'), ); - expect( - warnings.where((warning) => warning.contains('multiple drivers')), - isEmpty, - ); expect(arrayConcats, isNotEmpty); for (final arrayConcat in arrayConcats) { final connections = (arrayConcat.value @@ -1607,16 +1611,17 @@ void main() { test( 'nested LogicArray.net aggregate ports have connected concat inputs', () async { - for (final options in [ - const NetlistOptions(enableDCE: false), - const NetlistOptions( + for (final configuration in [ + const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false), + const NetlistSynthesizerConfiguration( collapseTransparentClusters: true, - enableDCE: false, + enableDeadCellElimination: false, ), ]) { final json = await _synthToMap( NestedNetArrayRowsToChildModule(), - options: options, + configuration: configuration, ); final moduleDef = _modules(json) .values @@ -1687,7 +1692,7 @@ void main() { final structPacks = cells.entries.where((entry) { final cell = entry.value as Map; return entry.key.startsWith( - SynthOperationNamer.structureConcatOperationName, + SynthStructureConcat.operationName, ) && cell['type'] == r'$struct_pack'; }).toList(); @@ -1753,17 +1758,16 @@ void main() { cells, 'struct_module', netnames: netnames, - throwOnMultipleDrivers: true, - printWarnings: false, ), - throwsStateError, + throwsA(isA()), ); }); test('optimized netlist removes concat aliases of named vectors', () async { final json = await _synthToMap( NestedInternalArrayToChildModule(), - options: const NetlistOptions(collapseTransparentClusters: true), + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), ); for (final moduleDef in _modules( @@ -1955,7 +1959,8 @@ void main() { final fbNoDce = _buildFilterBank(); final jsonNoDce = await _synthToMap( fbNoDce, - options: const NetlistOptions(enableDCE: false), + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false), ); final dceCells = countCells(jsonDce); @@ -1988,7 +1993,8 @@ void main() { final fbNoDce = _buildFilterBank(); final jsonNoDce = await _synthToMap( fbNoDce, - options: const NetlistOptions(enableDCE: false), + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false), ); expect( @@ -2001,18 +2007,18 @@ void main() { // ── Group 10: Post-processing option combinations ────────────────── - group('post-processing options', () { + group('post-processing configuration', () { test('collapseTransparentClusters produces valid netlist', () async { final fb = _buildFilterBank(); final json = await _synthToMap( fb, - options: const NetlistOptions(collapseTransparentClusters: true), + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), ); expect(_modules(json), isNotEmpty); }); - test('validation warns on multiple drivers', () { - final warnings = []; + test('validation reports multiple drivers with their locations', () { final ports = { 'a': { 'direction': 'input', @@ -2034,42 +2040,24 @@ void main() { }, }; - runZoned( - () => NetlistValidation.validate(ports, cells, 'ShortedModule'), - zoneSpecification: ZoneSpecification( - print: (_, __, ___, line) => warnings.add(line), - ), - ); - - expect(warnings, contains(contains('wire bit 1 has multiple drivers'))); - warnings.clear(); - - final capturedWarnings = runZoned( - () => NetlistValidation.validate( - ports, - cells, - 'ShortedModule', - printWarnings: false, - ), - zoneSpecification: ZoneSpecification( - print: (_, __, ___, line) => warnings.add(line), - ), - ); - - expect( - capturedWarnings, - contains(contains('wire bit 1 has multiple drivers')), - ); - expect(warnings, isEmpty); expect( () => NetlistValidation.validate( ports, cells, 'ShortedModule', - throwOnMultipleDrivers: true, - printWarnings: false, ), - throwsStateError, + throwsA( + isA() + .having( + (error) => error.moduleName, 'module name', 'ShortedModule') + .having((error) => error.issues, 'issues', hasLength(1)) + .having((error) => error.issues.single.wireBit, 'wire bit', 1) + .having( + (error) => error.issues.single.drivers, + 'drivers', + containsAll(['port a (input)', r'cell driver.Y ($buf)']), + ), + ), ); }); @@ -2093,13 +2081,8 @@ void main() { }; expect( - NetlistValidation.validate( - ports, - concatAlias, - 'StructuralAlias', - printWarnings: false, - ), - isNot(contains(contains('multiple drivers'))), + () => NetlistValidation.validate(ports, concatAlias, 'StructuralAlias'), + returnsNormally, ); final buffers = { @@ -2126,10 +2109,8 @@ void main() { ports, buffers, 'BufferedShort', - throwOnMultipleDrivers: true, - printWarnings: false, ), - throwsStateError, + throwsA(isA()), ); }); @@ -2151,18 +2132,12 @@ void main() { }; expect( - NetlistValidation.validate( - ports, - cells, - 'FilterBank', - throwOnMultipleDrivers: true, - printWarnings: false, - ), - isEmpty, + () => NetlistValidation.validate(ports, cells, 'FilterBank'), + returnsNormally, ); }); - test('validation can skip unconnected output warnings', () { + test('validation allows disconnected cell outputs', () { final ports = { 'a': { 'direction': 'input', @@ -2186,29 +2161,12 @@ void main() { }; expect( - NetlistValidation.validate( - ports, - cells, - 'UnusedOutputModule', - printWarnings: false, - ), - contains(contains('unusedAnd')), - ); - - expect( - NetlistValidation.validate( - ports, - cells, - 'UnusedOutputModule', - printWarnings: false, - checkUnconnectedOutputs: false, - ), - isEmpty, + () => NetlistValidation.validate(ports, cells, 'UnusedOutputModule'), + returnsNormally, ); }); test('validation allows cells to drive inout ports', () { - final warnings = []; final ports = { 'bus': { 'direction': 'inout', @@ -2227,21 +2185,11 @@ void main() { }, }; - runZoned( - () => NetlistValidation.validate(ports, cells, 'InOutModule'), - zoneSpecification: ZoneSpecification( - print: (_, __, ___, line) => warnings.add(line), - ), - ); - - expect(warnings, isEmpty); expect( () => NetlistValidation.validate( ports, cells, 'InOutModule', - throwOnMultipleDrivers: true, - printWarnings: false, ), returnsNormally, ); diff --git a/test/netlist_test.dart b/test/netlist_test.dart index a3b00b434..899d45851 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -549,15 +549,14 @@ void main() { }); }); - // ── NetlistOptions ─────────────────────────────────────────────────── - - group('NetlistOptions', () { + // ── NetlistSynthesizerConfiguration ────────────────────────────────── + group('NetlistSynthesizerConfiguration', () { test('slimMode omits cell connections', () async { final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); await mod.build(); final slimSynth = NetlistSynthesizer( - options: const NetlistOptions(slimMode: true), + configuration: const NetlistSynthesizerConfiguration(slimMode: true), ); final json = slimSynth.synthesizeToJson(mod); final decoded = jsonDecode(json) as Map; @@ -595,7 +594,8 @@ void main() { final collapsedTop = _topModuleFromJson( mod, NetlistSynthesizer( - options: const NetlistOptions(collapseTransparentClusters: true), + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), ).synthesizeToJson(mod), ); expect(_cellCount(collapsedTop, r'$slice'), equals(1)); @@ -616,7 +616,8 @@ void main() { final collapsedTop = _topModuleFromJson( mod, NetlistSynthesizer( - options: const NetlistOptions(collapseTransparentClusters: true), + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), ).synthesizeToJson(mod), ); expect(_cellCount(collapsedTop, r'$slice'), equals(0)); @@ -720,7 +721,8 @@ void main() { await module.build(); final synthesizer = NetlistSynthesizer( - options: const NetlistOptions(compressBitRanges: true), + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), ); final builder = SynthBuilder(module, synthesizer); final result = builder.synthesisResults @@ -748,7 +750,8 @@ void main() { await mod.build(); final synthCompressed = NetlistSynthesizer( - options: const NetlistOptions(compressBitRanges: true), + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), ); final jsonCompressed = synthCompressed.synthesizeToJson(mod); @@ -779,7 +782,8 @@ void main() { await mod.build(); final synth = NetlistSynthesizer( - options: const NetlistOptions(compressBitRanges: true), + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), ); final json = synth.synthesizeToJson(mod); final decoded = jsonDecode(json) as Map; @@ -794,7 +798,7 @@ void main() { await mod.build(); final synthCompact = NetlistSynthesizer( - options: const NetlistOptions(compactJson: true), + configuration: const NetlistSynthesizerConfiguration(compactJson: true), ); final jsonCompact = synthCompact.synthesizeToJson(mod); @@ -814,13 +818,13 @@ void main() { ); }); - test('both options together produce smallest output', () async { + test('both configuration together produce smallest output', () async { final a = Logic(name: 'a', width: 8); final mod = _AdderModule(a, Logic(name: 'b', width: 8)); await mod.build(); final synthBoth = NetlistSynthesizer( - options: const NetlistOptions( + configuration: const NetlistSynthesizerConfiguration( compressBitRanges: true, compactJson: true, ), @@ -828,12 +832,13 @@ void main() { final jsonBoth = synthBoth.synthesizeToJson(mod); final synthCompressOnly = NetlistSynthesizer( - options: const NetlistOptions(compressBitRanges: true), + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), ); final jsonCompressOnly = synthCompressOnly.synthesizeToJson(mod); final synthCompactOnly = NetlistSynthesizer( - options: const NetlistOptions(compactJson: true), + configuration: const NetlistSynthesizerConfiguration(compactJson: true), ); final jsonCompactOnly = synthCompactOnly.synthesizeToJson(mod); @@ -854,7 +859,8 @@ void main() { as Map)['modules'] as Map; final synthCompressed = NetlistSynthesizer( - options: const NetlistOptions(compressBitRanges: true), + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), ); final jsonCompressed = synthCompressed.synthesizeToJson(mod); final compressedModules = (jsonDecode(jsonCompressed) diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index ea7022aad..d4f20fe47 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -14,6 +14,20 @@ import 'package:test/test.dart'; import '../example/filter_bank.dart'; +extension _NetlistTestModule on Module { + String generateNetlist( + {NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), + String? packageRoot}) { + if (!hasBuilt) { + throw ModuleNotBuiltException(this); + } + + return NetlistSynthesizer(configuration: configuration) + .synthesizeToJson(this, packageRoot: packageRoot); + } +} + class _Counter extends Module { _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { en = addInput('en', en); From 37216100183e9243c200ee5ef9c41b0a414f4a11 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 19 Aug 2026 15:15:34 -0700 Subject: [PATCH 68/77] point to local rohd_hierarchy package, not pub --- pubspec.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9df237505..d80f48bf9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,14 +13,11 @@ dependencies: collection: ^1.15.0 logging: ^1.0.1 meta: ^1.9.0 - rohd_hierarchy: ^0.1.0 + rohd_hierarchy: + path: packages/rohd_hierarchy test: ^1.17.3 dev_dependencies: benchmark_harness: ^2.2.0 coverage: ^1.10.0 yaml: ^3.1.1 - -dependency_overrides: - rohd_hierarchy: - path: packages/rohd_hierarchy From 39c6a8409f8b00cf9c9ec0d8299efb08c1767f16 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 19 Aug 2026 15:39:42 -0700 Subject: [PATCH 69/77] use publish_to: none for local package --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index d80f48bf9..58758deae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,6 +5,7 @@ homepage: https://intel.github.io/rohd-website repository: https://github.com/intel/rohd issue_tracker: https://github.com/intel/rohd/issues documentation: https://intel.github.io/rohd-website/docs/sample-example/ +publish_to: none environment: sdk: '>=3.0.0 <4.0.0' From 043837dba5bc3dce1ac33972a1c914b955ce60c1 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 20 Aug 2026 10:13:10 -0700 Subject: [PATCH 70/77] compressBitRanges is an optimization test feature --- .../netlist/netlist_synthesizer_configuration.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart index 95932f441..744a25b4e 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -89,6 +89,7 @@ class NetlistSynthesizerConfiguration { /// This is backward-compatible: Yosys-format arrays already mix /// integers with constant strings `"0"` and `"1"`. Parsers can /// detect range strings by the presence of `:`. + @internal final bool compressBitRanges; /// When `true`, the JSON output uses no indentation (compact form). @@ -104,7 +105,7 @@ class NetlistSynthesizerConfiguration { @visibleForTesting this.collapseTransparentClusters = false, @visibleForTesting this.enableDeadCellElimination = true, this.slimMode = false, - this.compressBitRanges = false, + @visibleForTesting this.compressBitRanges = false, this.compactJson = false, }); } From 8a1a1eda330345226f8ba4f496be7ce3c68ab2c4 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 20 Aug 2026 16:54:56 -0700 Subject: [PATCH 71/77] fixes for full gate library, synth cell naming --- lib/src/modules/conditionals/flop.dart | 4 + .../netlist/netlist_cell_mapper.dart | 206 +- .../netlist/netlist_module_translation.dart | 141 +- .../netlist/netlist_synthesis_result.dart | 1 - .../netlist/netlist_synthesizer.dart | 17 - lib/src/synthesizers/synth_builder.dart | 7 - lib/src/synthesizers/synthesis_result.dart | 9 +- .../utilities/synth_structure_layout.dart | 59 +- test/fixtures/gate_catalog.rohd.json | 4779 +++++++++++++++++ test/fixtures/gate_catalog_module.dart | 207 + test/gate_catalog_test.dart | 286 + test/netlist_synthesizer_test.dart | 276 +- test/synth_structure_layout_test.dart | 20 + tool/generate_gate_catalog.dart | 67 + 14 files changed, 5985 insertions(+), 94 deletions(-) create mode 100644 test/fixtures/gate_catalog.rohd.json create mode 100644 test/fixtures/gate_catalog_module.dart create mode 100644 test/gate_catalog_test.dart create mode 100644 tool/generate_gate_catalog.dart diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index cd9aa8750..c8b932ec2 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -92,6 +92,10 @@ class FlipFlop extends Module with SystemVerilog { /// reset. If no `reset` is provided, this will have no effect. final bool asyncReset; + /// The constant reset value, or `null` when reset is absent or data-driven. + LogicValue? get constantResetValue => + _reset == null || _resetValuePort != null ? null : _resetValueConst; + /// Constructs a flip flop which is positive edge triggered on [clk]. /// /// When optional [en] is provided, an additional input will be created for diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart index 618fc595a..9e2496a08 100644 --- a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -178,6 +178,69 @@ class NetlistCellMapper { ); } + /// Maps a shift gate to a Yosys binary shift cell. + static NetlistCellMapping? shiftABY( + NetlistCellContext ctx, + String cellType, { + required bool aSigned, + }) { + final a = ctx.findInput('_in'); + final b = ctx.findInput('_shiftAmount'); + final y = ctx.firstOutput; + if (a == null || b == null || y == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', y: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': aSigned ? 1 : 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(y), + }, + ); + } + + /// Map a two-input gate with ports A, B, Y (e.g. `$pow`, `$div`, `$mod`), + /// including the standard Yosys `A_SIGNED`/`B_SIGNED` parameters. + /// + /// Unlike [binaryABY], this always emits the full standard parameter set + /// (`A_SIGNED`, `A_WIDTH`, `B_SIGNED`, `B_WIDTH`, `Y_WIDTH`) so the result + /// is directly consumable by standard Yosys tooling that expects these + /// arithmetic cells to be fully specified. + static NetlistCellMapping? binaryABYSigned( + NetlistCellContext ctx, + String cellType, { + required String inAPrefix, + required String inBPrefix, + bool aSigned = false, + bool bSigned = false, + }) { + final a = ctx.findInput(inAPrefix); + final b = ctx.findInput(inBPrefix); + final out = ctx.firstOutput; + if (a == null || b == null || out == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': aSigned ? 1 : 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': bSigned ? 1 : 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + // ══════════════════════════════════════════════════════════════════════ // Built-in handler registration // ══════════════════════════════════════════════════════════════════════ @@ -353,9 +416,10 @@ class NetlistCellMapper { }, ); }) - // ── FlipFlop → $dff ─────────────────────────────────────────────── + // ── FlipFlop → Yosys register cells ─────────────────────────────── ..register((ctx) { - if (ctx.module is! FlipFlop) { + final flipFlop = ctx.module; + if (flipFlop is! FlipFlop) { return null; } final clk = ctx.findInput('_clk') ?? ctx.findInput('clk'); @@ -366,38 +430,74 @@ class NetlistCellMapper { if (clk == null || d == null || q == null) { return null; } + final hasEnable = en != null && ctx.rawConns.containsKey(en); + final hasReset = rst != null && ctx.rawConns.containsKey(rst); + final rstVal = + ctx.findInput('_resetValue') ?? ctx.findInput('resetValue'); + final hasDynamicResetValue = + hasReset && rstVal != null && ctx.rawConns.containsKey(rstVal); + + String cellType; + if (!hasReset) { + cellType = hasEnable ? r'$dffe' : r'$dff'; + } else if (flipFlop.asyncReset) { + cellType = hasDynamicResetValue + ? (hasEnable ? r'$aldffe' : r'$aldff') + : (hasEnable ? r'$adffe' : r'$adff'); + } else if (!hasDynamicResetValue) { + cellType = hasEnable ? r'$sdffe' : r'$sdff'; + } else { + // Dynamic synchronous reset values are lowered to standard mux cells + // by NetlistModuleTranslation. + return null; + } + final pd = { - '_clk': 'input', - '_d': 'input', - '_q': 'output', + 'CLK': 'input', + 'D': 'input', + 'Q': 'output', }; final cn = >{ - '_clk': ctx.rawConns[clk] ?? [], - '_d': ctx.rawConns[d] ?? [], - '_q': ctx.rawConns[q] ?? [], + 'CLK': ctx.rawConns[clk] ?? [], + 'D': ctx.rawConns[d] ?? [], + 'Q': ctx.rawConns[q] ?? [], }; - if (en != null && ctx.rawConns.containsKey(en)) { - pd['_en'] = 'input'; - cn['_en'] = ctx.rawConns[en] ?? []; + if (hasEnable) { + pd['EN'] = 'input'; + cn['EN'] = ctx.rawConns[en] ?? []; } - if (rst != null && ctx.rawConns.containsKey(rst)) { - pd['_reset'] = 'input'; - cn['_reset'] = ctx.rawConns[rst] ?? []; + if (hasReset) { + final resetPort = flipFlop.asyncReset + ? (hasDynamicResetValue ? 'ALOAD' : 'ARST') + : 'SRST'; + pd[resetPort] = 'input'; + cn[resetPort] = ctx.rawConns[rst] ?? []; } - final rstVal = - ctx.findInput('_resetValue') ?? ctx.findInput('resetValue'); - if (rstVal != null && ctx.rawConns.containsKey(rstVal)) { - pd['_resetValue'] = 'input'; - cn['_resetValue'] = ctx.rawConns[rstVal] ?? []; + if (hasDynamicResetValue) { + pd['AD'] = 'input'; + cn['AD'] = ctx.rawConns[rstVal] ?? []; } + + final parameters = { + 'WIDTH': ctx.width(d), + 'CLK_POLARITY': 1, + if (hasEnable) 'EN_POLARITY': 1, + if (hasReset && flipFlop.asyncReset) + (hasDynamicResetValue ? 'ALOAD_POLARITY' : 'ARST_POLARITY'): 1, + if (hasReset && !flipFlop.asyncReset) 'SRST_POLARITY': 1, + if (hasReset && !hasDynamicResetValue) + if (flipFlop.asyncReset) + 'ARST_VALUE': + flipFlop.constantResetValue!.toString(includeWidth: false) + else + 'SRST_VALUE': + flipFlop.constantResetValue!.toString(includeWidth: false), + }; return ( - cellType: r'$dff', + cellType: cellType, portDirs: pd, connections: cn, - parameters: { - 'WIDTH': ctx.width(d), - 'CLK_POLARITY': 1, - }, + parameters: parameters, ); }); @@ -437,24 +537,63 @@ class NetlistCellMapper { (ctx, type) => binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), ), + ( + const {LShift: r'$shl', RShift: r'$shr'}, + (ctx, type) => shiftABY(ctx, type, aSigned: false), + ), + ( + const {ARShift: r'$sshr'}, + (ctx, type) => shiftABY(ctx, type, aSigned: true), + ), ( const { - LShift: r'$shl', - RShift: r'$shr', - ARShift: r'$shiftx', + Power: r'$pow', + Divide: r'$div', + Modulo: r'$mod', }, - (ctx, type) => binaryABY( - ctx, - type, - inAPrefix: '_in', - inBPrefix: '_shiftAmount', - ), + (ctx, type) => + binaryABYSigned(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), ), ]; for (final (typeMap, handler) in gateRegistrations) { registerByTypeMap(typeMap, handler); } + // ── IndexGate → $shiftx ───────────────────────────────────────────── + // + // `$shiftx` extracts `Y_WIDTH` bits of `A` starting at bit offset `B`, + // producing `x` when the offset is out of range. This matches + // [IndexGate]'s bit-select semantics (`original[index]`, `Y_WIDTH == 1`) + // exactly, including its out-of-range-selects-`x` behavior. + register((ctx) { + if (ctx.module is! IndexGate) { + return null; + } + final inputNames = ctx.module.inputs.keys.toList(); + if (inputNames.length != 2) { + return null; + } + final a = inputNames[0]; + final b = inputNames[1]; + final y = ctx.firstOutput; + if (y == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', y: 'Y'}); + return ( + cellType: r'$shiftx', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(y), + }, + ); + }); + // ── TriStateBuffer → $tribuf ────────────────────────────────────── register((ctx) { if (ctx.module is! TriStateBuffer) { @@ -465,6 +604,7 @@ class NetlistCellMapper { final enName = tsb.inputs.keys.last; // enable final outName = tsb.inOuts.keys.first; // inout output final r = ctx.remap({inName: 'A', enName: 'EN', outName: 'Y'}); + r.portDirs['Y'] = 'output'; return ( cellType: r'$tribuf', portDirs: r.portDirs, diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 9612788cb..0ac8cef11 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -271,6 +271,7 @@ class NetlistModuleTranslation { } final submodule = instance.module; + final cellKey = instance.name; final isLeaf = !_generatesDefinition(submodule); final defaultCellType = isLeaf ? submodule.definitionName @@ -292,9 +293,19 @@ class NetlistModuleTranslation { final mapped = isLeaf ? _netlistCellMapper.map(submodule, rawPortDirs, rawConnections) : null; + if (mapped == null && + submodule is FlipFlop && + _emitDynamicSynchronousResetFlipFlop( + cellKey, + submodule, + rawPortDirs, + rawConnections, + )) { + emittedCellKeys[instance] = cellKey; + continue; + } final cellPortDirs = mapped?.portDirs ?? rawPortDirs; final cellConnections = mapped?.connections ?? rawConnections; - final cellKey = instance.name; emittedCellKeys[instance] = cellKey; if (submodule is Combinational || submodule is Sequential) { @@ -373,6 +384,134 @@ class NetlistModuleTranslation { .forEach(cells.remove); } + /// Lowers a flip-flop with a dynamic synchronous reset value to Yosys cells. + /// + /// `$sdff` requires a constant reset value. The reset mux precedes the + /// enable, and the enable is ORed with reset so reset retains priority. + bool _emitDynamicSynchronousResetFlipFlop( + String cellKey, + FlipFlop flipFlop, + Map rawPortDirs, + Map> rawConnections, + ) { + if (flipFlop.asyncReset) { + return false; + } + + String? findInput(String unpreferredName, String name) { + for (final entry in rawPortDirs.entries) { + if (entry.value == 'input' && + (entry.key.startsWith(unpreferredName) || entry.key == name)) { + return entry.key; + } + } + return null; + } + + final clk = findInput('_clk', 'clk'); + final d = findInput('_d', 'd'); + final en = findInput('_en', 'en'); + final reset = findInput('_reset', 'reset'); + final resetValue = findInput('_resetValue', 'resetValue'); + final q = rawPortDirs.entries + .where((entry) => entry.value == 'output') + .map((entry) => entry.key) + .firstOrNull; + if (clk == null || + d == null || + reset == null || + resetValue == null || + q == null) { + return false; + } + + final dBits = rawConnections[d] ?? const []; + final resetValueBits = rawConnections[resetValue] ?? const []; + final resetBits = rawConnections[reset] ?? const []; + final clkBits = rawConnections[clk] ?? const []; + final qBits = rawConnections[q] ?? const []; + if (dBits.isEmpty || + dBits.length != resetValueBits.length || + resetBits.length != 1 || + clkBits.length != 1 || + qBits.length != dBits.length) { + return false; + } + + final resetMuxOutput = + List.generate(dBits.length, (_) => allocateWireId()); + cells['${cellKey}_reset_mux'] = { + 'hide_name': 0, + 'type': r'$mux', + 'parameters': {'WIDTH': dBits.length}, + 'attributes': {}, + 'port_directions': { + 'A': 'input', + 'B': 'input', + 'S': 'input', + 'Y': 'output' + }, + 'connections': { + 'A': dBits, + 'B': resetValueBits, + 'S': resetBits, + 'Y': resetMuxOutput, + }, + }; + + final hasEnable = en != null && rawConnections.containsKey(en); + final dffConnections = >{ + 'CLK': clkBits, + 'D': resetMuxOutput, + 'Q': qBits, + }; + final dffDirections = { + 'CLK': 'input', + 'D': 'input', + 'Q': 'output', + }; + final dffParameters = { + 'WIDTH': dBits.length, + 'CLK_POLARITY': 1, + }; + if (hasEnable) { + final enableBits = rawConnections[en] ?? const []; + if (enableBits.length != 1) { + return false; + } + final effectiveEnable = [allocateWireId()]; + cells['${cellKey}_reset_enable'] = { + 'hide_name': 0, + 'type': r'$or', + 'parameters': { + 'A_WIDTH': 1, + 'B_WIDTH': 1, + 'Y_WIDTH': 1, + }, + 'attributes': {}, + 'port_directions': {'A': 'input', 'B': 'input', 'Y': 'output'}, + 'connections': { + 'A': enableBits, + 'B': resetBits, + 'Y': effectiveEnable, + }, + }; + dffConnections['EN'] = effectiveEnable; + dffDirections['EN'] = 'input'; + dffParameters['EN_POLARITY'] = 1; + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': hasEnable ? r'$dffe' : r'$dff', + 'parameters': dffParameters, + 'attributes': {}, + 'port_directions': dffDirections, + 'connections': dffConnections, + }; + return true; + } + /// Emits port and internal netnames, fills unnamed connection coverage, /// and optionally removes names for undriven wires. void processNetnames({ diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart index 49c424608..ddf6855c0 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -39,7 +39,6 @@ class NetlistSynthesisResult extends SynthesisResult { required this.cells, required this.netnames, this.attributes = const {}, - super.warnings, }); /// Builds the JSON representation for this single module entry. diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 6e1be170c..24e3e0fd9 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -1138,7 +1138,6 @@ String _destinationSuffix(Logic destination) { final rootLocation = _logicLocationInModule(module, destination); final elementPath = _logicElementPathIndices(destination).path; final parts = [ - ..._modulePathIndices(module).path, ...rootLocation.path, ...elementPath, ]; @@ -1146,22 +1145,6 @@ String _destinationSuffix(Logic destination) { return parts.isEmpty ? '0' : parts.map((part) => part.toString()).join('_'); } -OccurrenceAddress _modulePathIndices(Module module) { - final parent = module.parent; - if (parent == null) { - return const OccurrenceAddress([0]); - } - - final siblings = parent.subModules.toList(); - final index = siblings.indexWhere( - (submodule) => identical(submodule, module), - ); - return OccurrenceAddress([ - ..._modulePathIndices(parent).path, - if (index < 0) 0 else index, - ]); -} - OccurrenceAddress _logicElementPathIndices(Logic destination) { final elementPath = []; var root = destination; diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index bb05d4abc..f9d0a0d08 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -38,13 +38,6 @@ class SynthBuilder { Set get synthesisResults => UnmodifiableSetView(_synthesisResults); - /// Non-fatal warnings reported by generated [SynthesisResult]s. - List get warnings => UnmodifiableListView( - _synthesisResults - .expand((synthesisResult) => synthesisResult.warnings) - .toList(growable: false), - ); - /// [Uniquifier] for instance type names. final Uniquifier _instanceTypeUniquifier = Uniquifier(); diff --git a/lib/src/synthesizers/synthesis_result.dart b/lib/src/synthesizers/synthesis_result.dart index 6a8634e24..b1b34e9b9 100644 --- a/lib/src/synthesizers/synthesis_result.dart +++ b/lib/src/synthesizers/synthesis_result.dart @@ -23,16 +23,9 @@ abstract class SynthesisResult { /// The name of the definition type for this module instance. String get instanceTypeName => getInstanceTypeOfModule(module); - /// Non-fatal warnings reported while producing this synthesis result. - final List warnings; - /// Represents a constant computed synthesis result for [module] given /// the provided type mapping in [getInstanceTypeOfModule]. - const SynthesisResult( - this.module, - this.getInstanceTypeOfModule, { - this.warnings = const [], - }); + const SynthesisResult(this.module, this.getInstanceTypeOfModule); /// Whether two implementations are identical or not /// diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart index c3715cd82..db972c929 100644 --- a/lib/src/synthesizers/utilities/synth_structure_layout.dart +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -9,26 +9,32 @@ import 'package:rohd/rohd.dart'; +/// An exclusive-end bit range within a packed [LogicStructure]. +typedef SynthStructureBitRange = ({int start, int end}); + +typedef _SynthStructureRange = ({ + int start, + int end, + String name, + String path, + String fieldPath, + int indexInParent, +}); + /// Provides bit ranges and field names for a packed [LogicStructure]. class SynthStructureLayout { - final List< - ({ - int start, - int end, - String name, - String path, - int indexInParent, - })> _ranges = []; + final List<_SynthStructureRange> _ranges = []; /// Creates a layout with elements ordered from least to most significant. SynthStructureLayout(LogicStructure structure) { - _addStructure(structure, 0, ''); + _addStructure(structure, 0, '', ''); } void _addStructure( LogicStructure structure, int baseOffset, String parentPath, + String parentFieldPath, ) { var offset = baseOffset; for (var index = 0; index < structure.elements.length; index++) { @@ -36,20 +42,37 @@ class SynthStructureLayout { final end = offset + element.width; final path = parentPath.isEmpty ? element.name : '${parentPath}_${element.name}'; + final fieldPath = parentFieldPath.isEmpty + ? element.name + : '$parentFieldPath.${element.name}'; _ranges.add(( start: offset, end: end, name: element.name, path: path, + fieldPath: fieldPath, indexInParent: index, )); if (element is LogicStructure && element is! LogicArray) { - _addStructure(element, offset, path); + _addStructure(element, offset, path, fieldPath); } offset = end; } } + /// Returns the exclusive-end bit range for a dot-separated [fieldPath]. + /// + /// For example, `a.b` returns the range for the nested `b` field in `a`. + /// Returns `null` when [fieldPath] does not identify a field. + SynthStructureBitRange? bitRangeForPath(String fieldPath) { + for (final range in _ranges) { + if (range.fieldPath == fieldPath) { + return (start: range.start, end: range.end); + } + } + return null; + } + /// Returns the best field name containing [bitOffset]. /// /// When [anonymousUnpreferred] is true, an unpreferred leaf with no named @@ -59,20 +82,8 @@ class SynthStructureLayout { required String fallbackName, bool anonymousUnpreferred = false, }) { - ({ - int start, - int end, - String name, - String path, - int indexInParent, - })? bestNamed; - ({ - int start, - int end, - String name, - String path, - int indexInParent, - })? narrowest; + _SynthStructureRange? bestNamed; + _SynthStructureRange? narrowest; for (final range in _ranges) { if (bitOffset < range.start || bitOffset >= range.end) { diff --git a/test/fixtures/gate_catalog.rohd.json b/test/fixtures/gate_catalog.rohd.json new file mode 100644 index 000000000..3e52e4ce1 --- /dev/null +++ b/test/fixtures/gate_catalog.rohd.json @@ -0,0 +1,4779 @@ +{ + "creator": "NetlistSynthesizer (rohd)", + "version": "0.0.1", + "modules": { + "GateCatalog": { + "attributes": { + "src": "generated", + "top": 1 + }, + "ports": { + "clk": { + "direction": "input", + "bits": [ + 2 + ], + "logic_type": { + "width": 1 + } + }, + "en": { + "direction": "input", + "bits": [ + 3 + ], + "logic_type": { + "width": 1 + } + }, + "reset": { + "direction": "input", + "bits": [ + 4 + ], + "logic_type": { + "width": 1 + } + }, + "muxSel": { + "direction": "input", + "bits": [ + 5 + ], + "logic_type": { + "width": 1 + } + }, + "enableTri": { + "direction": "input", + "bits": [ + 6 + ], + "logic_type": { + "width": 1 + } + }, + "a4": { + "direction": "input", + "bits": [ + 7, + 8, + 9, + 10 + ], + "logic_type": { + "width": 4 + } + }, + "b4": { + "direction": "input", + "bits": [ + 11, + 12, + 13, + 14 + ], + "logic_type": { + "width": 4 + } + }, + "a8": { + "direction": "input", + "bits": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "logic_type": { + "width": 8 + } + }, + "b8": { + "direction": "input", + "bits": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "logic_type": { + "width": 8 + } + }, + "d4": { + "direction": "input", + "bits": [ + 31, + 32, + 33, + 34 + ], + "logic_type": { + "width": 4 + } + }, + "shamt4": { + "direction": "input", + "bits": [ + 35, + 36, + 37, + 38 + ], + "logic_type": { + "width": 4 + } + }, + "idx3": { + "direction": "input", + "bits": [ + 39, + 40, + 41 + ], + "logic_type": { + "width": 3 + } + }, + "idx5": { + "direction": "input", + "bits": [ + 42, + 43, + 44, + 45, + 46 + ], + "logic_type": { + "width": 5 + } + }, + "resetValueDyn4": { + "direction": "input", + "bits": [ + 47, + 48, + 49, + 50 + ], + "logic_type": { + "width": 4 + } + }, + "not_out": { + "direction": "output", + "bits": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ], + "logic_type": { + "width": 8 + } + }, + "and_ll_out": { + "direction": "output", + "bits": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ], + "logic_type": { + "width": 8 + } + }, + "and_lc_out": { + "direction": "output", + "bits": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ], + "logic_type": { + "width": 8 + } + }, + "or_ll_out": { + "direction": "output", + "bits": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ], + "logic_type": { + "width": 8 + } + }, + "or_lc_out": { + "direction": "output", + "bits": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ], + "logic_type": { + "width": 8 + } + }, + "xor_ll_out": { + "direction": "output", + "bits": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ], + "logic_type": { + "width": 8 + } + }, + "xor_lc_out": { + "direction": "output", + "bits": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ], + "logic_type": { + "width": 8 + } + }, + "reduce_and_out": { + "direction": "output", + "bits": [ + 107 + ], + "logic_type": { + "width": 1 + } + }, + "reduce_or_out": { + "direction": "output", + "bits": [ + 108 + ], + "logic_type": { + "width": 1 + } + }, + "reduce_xor_out": { + "direction": "output", + "bits": [ + 109 + ], + "logic_type": { + "width": 1 + } + }, + "add_ll_sum": { + "direction": "output", + "bits": [ + 110, + 111, + 112, + 113 + ], + "logic_type": { + "width": 4 + } + }, + "add_ll_carry": { + "direction": "output", + "bits": [ + 114 + ], + "logic_type": { + "width": 1 + } + }, + "add_lc_sum": { + "direction": "output", + "bits": [ + 115, + 116, + 117, + 118 + ], + "logic_type": { + "width": 4 + } + }, + "add_lc_carry": { + "direction": "output", + "bits": [ + 119 + ], + "logic_type": { + "width": 1 + } + }, + "sub_ll_out": { + "direction": "output", + "bits": [ + 120, + 121, + 122, + 123 + ], + "logic_type": { + "width": 4 + } + }, + "sub_lc_out": { + "direction": "output", + "bits": [ + 124, + 125, + 126, + 127 + ], + "logic_type": { + "width": 4 + } + }, + "mul_ll_out": { + "direction": "output", + "bits": [ + 128, + 129, + 130, + 131 + ], + "logic_type": { + "width": 4 + } + }, + "mul_lc_out": { + "direction": "output", + "bits": [ + 132, + 133, + 134, + 135 + ], + "logic_type": { + "width": 4 + } + }, + "div_ll_out": { + "direction": "output", + "bits": [ + 136, + 137, + 138, + 139 + ], + "logic_type": { + "width": 4 + } + }, + "div_lc_out": { + "direction": "output", + "bits": [ + 140, + 141, + 142, + 143 + ], + "logic_type": { + "width": 4 + } + }, + "mod_ll_out": { + "direction": "output", + "bits": [ + 144, + 145, + 146, + 147 + ], + "logic_type": { + "width": 4 + } + }, + "mod_lc_out": { + "direction": "output", + "bits": [ + 148, + 149, + 150, + 151 + ], + "logic_type": { + "width": 4 + } + }, + "pow_ll_out": { + "direction": "output", + "bits": [ + 152, + 153, + 154, + 155 + ], + "logic_type": { + "width": 4 + } + }, + "pow_lc_out": { + "direction": "output", + "bits": [ + 156, + 157, + 158, + 159 + ], + "logic_type": { + "width": 4 + } + }, + "eq_ll_out": { + "direction": "output", + "bits": [ + 160 + ], + "logic_type": { + "width": 1 + } + }, + "eq_lc_out": { + "direction": "output", + "bits": [ + 161 + ], + "logic_type": { + "width": 1 + } + }, + "neq_ll_out": { + "direction": "output", + "bits": [ + 162 + ], + "logic_type": { + "width": 1 + } + }, + "neq_lc_out": { + "direction": "output", + "bits": [ + 163 + ], + "logic_type": { + "width": 1 + } + }, + "lt_ll_out": { + "direction": "output", + "bits": [ + 164 + ], + "logic_type": { + "width": 1 + } + }, + "lt_lc_out": { + "direction": "output", + "bits": [ + 165 + ], + "logic_type": { + "width": 1 + } + }, + "gt_ll_out": { + "direction": "output", + "bits": [ + 166 + ], + "logic_type": { + "width": 1 + } + }, + "gt_lc_out": { + "direction": "output", + "bits": [ + 167 + ], + "logic_type": { + "width": 1 + } + }, + "le_ll_out": { + "direction": "output", + "bits": [ + 168 + ], + "logic_type": { + "width": 1 + } + }, + "le_lc_out": { + "direction": "output", + "bits": [ + 169 + ], + "logic_type": { + "width": 1 + } + }, + "ge_ll_out": { + "direction": "output", + "bits": [ + 170 + ], + "logic_type": { + "width": 1 + } + }, + "ge_lc_out": { + "direction": "output", + "bits": [ + 171 + ], + "logic_type": { + "width": 1 + } + }, + "lshift_ll_out": { + "direction": "output", + "bits": [ + 172, + 173, + 174, + 175 + ], + "logic_type": { + "width": 4 + } + }, + "lshift_lc_out": { + "direction": "output", + "bits": [ + 176, + 177, + 178, + 179 + ], + "logic_type": { + "width": 4 + } + }, + "rshift_ll_out": { + "direction": "output", + "bits": [ + 180, + 181, + 182, + 183 + ], + "logic_type": { + "width": 4 + } + }, + "rshift_lc_out": { + "direction": "output", + "bits": [ + 184, + 185, + 186, + 187 + ], + "logic_type": { + "width": 4 + } + }, + "arshift_ll_out": { + "direction": "output", + "bits": [ + 188, + 189, + 190, + 191 + ], + "logic_type": { + "width": 4 + } + }, + "arshift_lc_out": { + "direction": "output", + "bits": [ + 192, + 193, + 194, + 195 + ], + "logic_type": { + "width": 4 + } + }, + "mux_class_out": { + "direction": "output", + "bits": [ + 196, + 197, + 198, + 199 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_dynamic_out": { + "direction": "output", + "bits": [ + 200, + 201, + 202, + 203 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_const1_out": { + "direction": "output", + "bits": [ + 407, + 408, + 409, + 410 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_const0_out": { + "direction": "output", + "bits": [ + 411, + 412, + 413, + 414 + ], + "logic_type": { + "width": 4 + } + }, + "index_natural_out": { + "direction": "output", + "bits": [ + 212 + ], + "logic_type": { + "width": 1 + } + }, + "index_oversized_out": { + "direction": "output", + "bits": [ + 213 + ], + "logic_type": { + "width": 1 + } + }, + "replicate_x3_out": { + "direction": "output", + "bits": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ], + "logic_type": { + "width": 12 + } + }, + "replicate_x5_out": { + "direction": "output", + "bits": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ], + "logic_type": { + "width": 20 + } + }, + "slice_out": { + "direction": "output", + "bits": [ + 246, + 247, + 248, + 249 + ], + "logic_type": { + "width": 4 + } + }, + "swizzle_out": { + "direction": "output", + "bits": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ], + "logic_type": { + "width": 8 + } + }, + "tribuf_readback_out": { + "direction": "output", + "bits": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ], + "logic_type": { + "width": 8 + } + }, + "q_dff": { + "direction": "output", + "bits": [ + 266, + 267, + 268, + 269 + ], + "logic_type": { + "width": 4 + } + }, + "q_dffe": { + "direction": "output", + "bits": [ + 270, + 271, + 272, + 273 + ], + "logic_type": { + "width": 4 + } + }, + "q_sdff": { + "direction": "output", + "bits": [ + 274, + 275, + 276, + 277 + ], + "logic_type": { + "width": 4 + } + }, + "q_sdffe": { + "direction": "output", + "bits": [ + 278, + 279, + 280, + 281 + ], + "logic_type": { + "width": 4 + } + }, + "q_adff": { + "direction": "output", + "bits": [ + 282, + 283, + 284, + 285 + ], + "logic_type": { + "width": 4 + } + }, + "q_adffe": { + "direction": "output", + "bits": [ + 286, + 287, + 288, + 289 + ], + "logic_type": { + "width": 4 + } + }, + "q_aldff": { + "direction": "output", + "bits": [ + 290, + 291, + 292, + 293 + ], + "logic_type": { + "width": 4 + } + }, + "q_aldffe": { + "direction": "output", + "bits": [ + 294, + 295, + 296, + 297 + ], + "logic_type": { + "width": 4 + } + }, + "q_dynsync_noen": { + "direction": "output", + "bits": [ + 298, + 299, + 300, + 301 + ], + "logic_type": { + "width": 4 + } + }, + "q_dynsync_en": { + "direction": "output", + "bits": [ + 302, + 303, + 304, + 305 + ], + "logic_type": { + "width": 4 + } + }, + "bus": { + "direction": "inout", + "bits": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "logic_type": { + "width": 8 + } + } + }, + "cells": { + "not_": { + "hide_name": 0, + "type": "$not", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ] + } + }, + "and_": { + "hide_name": 0, + "type": "$and", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ] + } + }, + "and__0": { + "hide_name": 0, + "type": "$and", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ], + "Y": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ] + } + }, + "or_": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ] + } + }, + "or__0": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ], + "Y": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ] + } + }, + "xor_": { + "hide_name": 0, + "type": "$xor", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ] + } + }, + "xor__0": { + "hide_name": 0, + "type": "$xor", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ], + "Y": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ] + } + }, + "uand": { + "hide_name": 0, + "type": "$reduce_and", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 107 + ] + } + }, + "uor": { + "hide_name": 0, + "type": "$reduce_or", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 108 + ] + } + }, + "uxor": { + "hide_name": 0, + "type": "$reduce_xor", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 109 + ] + } + }, + "add": { + "hide_name": 0, + "type": "$add", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output", + "CO": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 110, + 111, + 112, + 113 + ], + "CO": [ + 114 + ] + } + }, + "add_0": { + "hide_name": 0, + "type": "$add", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output", + "CO": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 338, + 339, + 340, + 341 + ], + "Y": [ + 115, + 116, + 117, + 118 + ], + "CO": [ + 119 + ] + } + }, + "subtract": { + "hide_name": 0, + "type": "$sub", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 120, + 121, + 122, + 123 + ] + } + }, + "subtract_0": { + "hide_name": 0, + "type": "$sub", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 342, + 343, + 344, + 345 + ], + "Y": [ + 124, + 125, + 126, + 127 + ] + } + }, + "multiply": { + "hide_name": 0, + "type": "$mul", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 128, + 129, + 130, + 131 + ] + } + }, + "multiply_0": { + "hide_name": 0, + "type": "$mul", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 346, + 347, + 348, + 349 + ], + "Y": [ + 132, + 133, + 134, + 135 + ] + } + }, + "divide": { + "hide_name": 0, + "type": "$div", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 136, + 137, + 138, + 139 + ] + } + }, + "divide_0": { + "hide_name": 0, + "type": "$div", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 350, + 351, + 352, + 353 + ], + "Y": [ + 140, + 141, + 142, + 143 + ] + } + }, + "modulo": { + "hide_name": 0, + "type": "$mod", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 144, + 145, + 146, + 147 + ] + } + }, + "modulo_0": { + "hide_name": 0, + "type": "$mod", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 354, + 355, + 356, + 357 + ], + "Y": [ + 148, + 149, + 150, + 151 + ] + } + }, + "power": { + "hide_name": 0, + "type": "$pow", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 152, + 153, + 154, + 155 + ] + } + }, + "power_0": { + "hide_name": 0, + "type": "$pow", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 358, + 359, + 360, + 361 + ], + "Y": [ + 156, + 157, + 158, + 159 + ] + } + }, + "equals": { + "hide_name": 0, + "type": "$eq", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 160 + ] + } + }, + "equals_0": { + "hide_name": 0, + "type": "$eq", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 362, + 363, + 364, + 365 + ], + "Y": [ + 161 + ] + } + }, + "notEquals": { + "hide_name": 0, + "type": "$ne", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 162 + ] + } + }, + "notEquals_0": { + "hide_name": 0, + "type": "$ne", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 366, + 367, + 368, + 369 + ], + "Y": [ + 163 + ] + } + }, + "lessthan": { + "hide_name": 0, + "type": "$lt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 164 + ] + } + }, + "lessthan_0": { + "hide_name": 0, + "type": "$lt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 370, + 371, + 372, + 373 + ], + "Y": [ + 165 + ] + } + }, + "greaterThan": { + "hide_name": 0, + "type": "$gt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 166 + ] + } + }, + "greaterThan_0": { + "hide_name": 0, + "type": "$gt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 374, + 375, + 376, + 377 + ], + "Y": [ + 167 + ] + } + }, + "lessThanOrEqual": { + "hide_name": 0, + "type": "$le", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 168 + ] + } + }, + "lessThanOrEqual_0": { + "hide_name": 0, + "type": "$le", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 378, + 379, + 380, + 381 + ], + "Y": [ + 169 + ] + } + }, + "greaterThanOrEqual": { + "hide_name": 0, + "type": "$ge", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 170 + ] + } + }, + "greaterThanOrEqual_0": { + "hide_name": 0, + "type": "$ge", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 382, + 383, + 384, + 385 + ], + "Y": [ + 171 + ] + } + }, + "lshift": { + "hide_name": 0, + "type": "$shl", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 172, + 173, + 174, + 175 + ] + } + }, + "lshift_0": { + "hide_name": 0, + "type": "$shl", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 403, + 404, + 405, + 406 + ], + "Y": [ + 176, + 177, + 178, + 179 + ] + } + }, + "rshift": { + "hide_name": 0, + "type": "$shr", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 180, + 181, + 182, + 183 + ] + } + }, + "rshift_0": { + "hide_name": 0, + "type": "$shr", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 2, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 390, + 391 + ], + "Y": [ + 184, + 185, + 186, + 187 + ] + } + }, + "arshift": { + "hide_name": 0, + "type": "$sshr", + "parameters": { + "A_SIGNED": 1, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 188, + 189, + 190, + 191 + ] + } + }, + "arshift_0": { + "hide_name": 0, + "type": "$sshr", + "parameters": { + "A_SIGNED": 1, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 2, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 392, + 393 + ], + "Y": [ + 192, + 193, + 194, + 195 + ] + } + }, + "mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "S": [ + 5 + ], + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 196, + 197, + 198, + 199 + ] + } + }, + "mux_0": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "S": [ + 5 + ], + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 200, + 201, + 202, + 203 + ] + } + }, + "unnamed_module": { + "hide_name": 0, + "type": "$shiftx", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 8, + "B_SIGNED": 0, + "B_WIDTH": 3, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 39, + 40, + 41 + ], + "Y": [ + 212 + ] + } + }, + "unnamed_module_0": { + "hide_name": 0, + "type": "$shiftx", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 8, + "B_SIGNED": 0, + "B_WIDTH": 5, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 42, + 43, + 44, + 45, + 46 + ], + "Y": [ + 213 + ] + } + }, + "unnamed_module_1": { + "hide_name": 0, + "type": "ReplicationOp", + "parameters": {}, + "attributes": {}, + "port_directions": { + "_a4": "input", + "_replicated_a4": "output" + }, + "connections": { + "_a4": [ + 7, + 8, + 9, + 10 + ], + "_replicated_a4": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ] + } + }, + "unnamed_module_2": { + "hide_name": 0, + "type": "ReplicationOp", + "parameters": {}, + "attributes": {}, + "port_directions": { + "_a4": "input", + "_replicated_a4": "output" + }, + "connections": { + "_a4": [ + 7, + 8, + 9, + 10 + ], + "_replicated_a4": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ] + } + }, + "bussubset": { + "hide_name": 0, + "type": "$slice", + "parameters": { + "OFFSET": 2, + "A_WIDTH": 8, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 246, + 247, + 248, + 249 + ] + } + }, + "swizzle": { + "hide_name": 0, + "type": "$concat", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ] + } + }, + "flipflop": { + "hide_name": 0, + "type": "$dff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 266, + 267, + 268, + 269 + ] + } + }, + "flipflop_0": { + "hide_name": 0, + "type": "$dffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 270, + 271, + 272, + 273 + ], + "EN": [ + 3 + ] + } + }, + "flipflop_1": { + "hide_name": 0, + "type": "$sdff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "SRST_POLARITY": 1, + "SRST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "SRST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 274, + 275, + 276, + 277 + ], + "SRST": [ + 4 + ] + } + }, + "flipflop_2": { + "hide_name": 0, + "type": "$sdffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "SRST_POLARITY": 1, + "SRST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "SRST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 278, + 279, + 280, + 281 + ], + "EN": [ + 3 + ], + "SRST": [ + 4 + ] + } + }, + "flipflop_3": { + "hide_name": 0, + "type": "$adff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "ARST_POLARITY": 1, + "ARST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "ARST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 282, + 283, + 284, + 285 + ], + "ARST": [ + 4 + ] + } + }, + "flipflop_4": { + "hide_name": 0, + "type": "$adffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "ARST_POLARITY": 1, + "ARST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "ARST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 286, + 287, + 288, + 289 + ], + "EN": [ + 3 + ], + "ARST": [ + 4 + ] + } + }, + "flipflop_5": { + "hide_name": 0, + "type": "$aldff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "ALOAD_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "ALOAD": "input", + "AD": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 290, + 291, + 292, + 293 + ], + "ALOAD": [ + 4 + ], + "AD": [ + 47, + 48, + 49, + 50 + ] + } + }, + "flipflop_6": { + "hide_name": 0, + "type": "$aldffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "ALOAD_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "ALOAD": "input", + "AD": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 294, + 295, + 296, + 297 + ], + "EN": [ + 3 + ], + "ALOAD": [ + 4 + ], + "AD": [ + 47, + 48, + 49, + 50 + ] + } + }, + "flipflop_7_reset_mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "S": "input", + "Y": "output" + }, + "connections": { + "A": [ + 31, + 32, + 33, + 34 + ], + "B": [ + 47, + 48, + 49, + 50 + ], + "S": [ + 4 + ], + "Y": [ + 394, + 395, + 396, + 397 + ] + } + }, + "flipflop_7": { + "hide_name": 0, + "type": "$dff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 394, + 395, + 396, + 397 + ], + "Q": [ + 298, + 299, + 300, + 301 + ] + } + }, + "flipflop_8_reset_mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "S": "input", + "Y": "output" + }, + "connections": { + "A": [ + 31, + 32, + 33, + 34 + ], + "B": [ + 47, + 48, + 49, + 50 + ], + "S": [ + 4 + ], + "Y": [ + 398, + 399, + 400, + 401 + ] + } + }, + "flipflop_8_reset_enable": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 1, + "B_WIDTH": 1, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 3 + ], + "B": [ + 4 + ], + "Y": [ + 402 + ] + } + }, + "flipflop_8": { + "hide_name": 0, + "type": "$dffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 398, + 399, + 400, + 401 + ], + "Q": [ + 302, + 303, + 304, + 305 + ], + "EN": [ + 402 + ] + } + }, + "tsb": { + "hide_name": 0, + "type": "$tribuf", + "parameters": { + "WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "EN": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "EN": [ + 6 + ], + "Y": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ] + } + }, + "passthrough_buf_0": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 407, + 408, + 409, + 410 + ] + } + }, + "passthrough_buf_1": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 411, + 412, + 413, + 414 + ] + } + }, + "passthrough_buf_2": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "Y": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ] + } + }, + "const_0_8_haa": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'haa": "output" + }, + "connections": { + "8'haa": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ] + } + }, + "const_1_8_h55": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'h55": "output" + }, + "connections": { + "8'h55": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ] + } + }, + "const_2_8_hf": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'hf": "output" + }, + "connections": { + "8'hf": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ] + } + }, + "const_3_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 338, + 339, + 340, + 341 + ] + } + }, + "const_4_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 342, + 343, + 344, + 345 + ] + } + }, + "const_5_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 346, + 347, + 348, + 349 + ] + } + }, + "const_6_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 350, + 351, + 352, + 353 + ] + } + }, + "const_7_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 354, + 355, + 356, + 357 + ] + } + }, + "const_8_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 358, + 359, + 360, + 361 + ] + } + }, + "const_9_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 362, + 363, + 364, + 365 + ] + } + }, + "const_10_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 366, + 367, + 368, + 369 + ] + } + }, + "const_11_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 370, + 371, + 372, + 373 + ] + } + }, + "const_12_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 374, + 375, + 376, + 377 + ] + } + }, + "const_13_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 378, + 379, + 380, + 381 + ] + } + }, + "const_14_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 382, + 383, + 384, + 385 + ] + } + }, + "const_15_4_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h2": "output" + }, + "connections": { + "4'h2": [ + 403, + 404, + 405, + 406 + ] + } + }, + "const_16_2_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "2'h2": "output" + }, + "connections": { + "2'h2": [ + 390, + 391 + ] + } + }, + "const_17_2_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "2'h2": "output" + }, + "connections": { + "2'h2": [ + 392, + 393 + ] + } + } + }, + "netnames": { + "clk": { + "bits": [ + 2 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "en": { + "bits": [ + 3 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reset": { + "bits": [ + 4 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "muxSel": { + "bits": [ + 5 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "enableTri": { + "bits": [ + 6 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "a4": { + "bits": [ + 7, + 8, + 9, + 10 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "b4": { + "bits": [ + 11, + 12, + 13, + 14 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "a8": { + "bits": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "b8": { + "bits": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "d4": { + "bits": [ + 31, + 32, + 33, + 34 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "shamt4": { + "bits": [ + 35, + 36, + 37, + 38 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "idx3": { + "bits": [ + 39, + 40, + 41 + ], + "logic_type": { + "width": 3 + }, + "attributes": {} + }, + "idx5": { + "bits": [ + 42, + 43, + 44, + 45, + 46 + ], + "logic_type": { + "width": 5 + }, + "attributes": {} + }, + "resetValueDyn4": { + "bits": [ + 47, + 48, + 49, + 50 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "not_out": { + "bits": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "and_ll_out": { + "bits": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "and_lc_out": { + "bits": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "or_ll_out": { + "bits": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "or_lc_out": { + "bits": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "xor_ll_out": { + "bits": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "xor_lc_out": { + "bits": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "reduce_and_out": { + "bits": [ + 107 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reduce_or_out": { + "bits": [ + 108 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reduce_xor_out": { + "bits": [ + 109 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "add_ll_sum": { + "bits": [ + 110, + 111, + 112, + 113 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "add_ll_carry": { + "bits": [ + 114 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "add_lc_sum": { + "bits": [ + 115, + 116, + 117, + 118 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "add_lc_carry": { + "bits": [ + 119 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "sub_ll_out": { + "bits": [ + 120, + 121, + 122, + 123 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "sub_lc_out": { + "bits": [ + 124, + 125, + 126, + 127 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mul_ll_out": { + "bits": [ + 128, + 129, + 130, + 131 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mul_lc_out": { + "bits": [ + 132, + 133, + 134, + 135 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "div_ll_out": { + "bits": [ + 136, + 137, + 138, + 139 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "div_lc_out": { + "bits": [ + 140, + 141, + 142, + 143 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mod_ll_out": { + "bits": [ + 144, + 145, + 146, + 147 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mod_lc_out": { + "bits": [ + 148, + 149, + 150, + 151 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "pow_ll_out": { + "bits": [ + 152, + 153, + 154, + 155 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "pow_lc_out": { + "bits": [ + 156, + 157, + 158, + 159 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "eq_ll_out": { + "bits": [ + 160 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "eq_lc_out": { + "bits": [ + 161 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "neq_ll_out": { + "bits": [ + 162 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "neq_lc_out": { + "bits": [ + 163 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lt_ll_out": { + "bits": [ + 164 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lt_lc_out": { + "bits": [ + 165 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "gt_ll_out": { + "bits": [ + 166 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "gt_lc_out": { + "bits": [ + 167 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "le_ll_out": { + "bits": [ + 168 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "le_lc_out": { + "bits": [ + 169 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "ge_ll_out": { + "bits": [ + 170 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "ge_lc_out": { + "bits": [ + 171 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lshift_ll_out": { + "bits": [ + 172, + 173, + 174, + 175 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "lshift_lc_out": { + "bits": [ + 176, + 177, + 178, + 179 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "rshift_ll_out": { + "bits": [ + 180, + 181, + 182, + 183 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "rshift_lc_out": { + "bits": [ + 184, + 185, + 186, + 187 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "arshift_ll_out": { + "bits": [ + 188, + 189, + 190, + 191 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "arshift_lc_out": { + "bits": [ + 192, + 193, + 194, + 195 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_class_out": { + "bits": [ + 196, + 197, + 198, + 199 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_dynamic_out": { + "bits": [ + 200, + 201, + 202, + 203 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_const1_out": { + "bits": [ + 407, + 408, + 409, + 410 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_const0_out": { + "bits": [ + 411, + 412, + 413, + 414 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "index_natural_out": { + "bits": [ + 212 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "index_oversized_out": { + "bits": [ + 213 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "replicate_x3_out": { + "bits": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ], + "logic_type": { + "width": 12 + }, + "attributes": {} + }, + "replicate_x5_out": { + "bits": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ], + "logic_type": { + "width": 20 + }, + "attributes": {} + }, + "slice_out": { + "bits": [ + 246, + 247, + 248, + 249 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "swizzle_out": { + "bits": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "tribuf_readback_out": { + "bits": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "q_dff": { + "bits": [ + 266, + 267, + 268, + 269 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dffe": { + "bits": [ + 270, + 271, + 272, + 273 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_sdff": { + "bits": [ + 274, + 275, + 276, + 277 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_sdffe": { + "bits": [ + 278, + 279, + 280, + 281 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_adff": { + "bits": [ + 282, + 283, + 284, + 285 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_adffe": { + "bits": [ + 286, + 287, + 288, + 289 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_aldff": { + "bits": [ + 290, + 291, + 292, + 293 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_aldffe": { + "bits": [ + 294, + 295, + 296, + 297 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dynsync_noen": { + "bits": [ + 298, + 299, + 300, + 301 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dynsync_en": { + "bits": [ + 302, + 303, + 304, + 305 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "bus": { + "bits": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "const_0_8_haa": { + "bits": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ], + "attributes": { + "computed": 1 + } + }, + "const_1_8_h55": { + "bits": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ], + "attributes": { + "computed": 1 + } + }, + "const_2_8_hf": { + "bits": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ], + "attributes": { + "computed": 1 + } + }, + "const_3_4_h5": { + "bits": [ + 338, + 339, + 340, + 341 + ], + "attributes": { + "computed": 1 + } + }, + "const_4_4_h3": { + "bits": [ + 342, + 343, + 344, + 345 + ], + "attributes": { + "computed": 1 + } + }, + "const_5_4_h3": { + "bits": [ + 346, + 347, + 348, + 349 + ], + "attributes": { + "computed": 1 + } + }, + "const_6_4_h3": { + "bits": [ + 350, + 351, + 352, + 353 + ], + "attributes": { + "computed": 1 + } + }, + "const_7_4_h3": { + "bits": [ + 354, + 355, + 356, + 357 + ], + "attributes": { + "computed": 1 + } + }, + "const_8_4_h3": { + "bits": [ + 358, + 359, + 360, + 361 + ], + "attributes": { + "computed": 1 + } + }, + "const_9_4_h5": { + "bits": [ + 362, + 363, + 364, + 365 + ], + "attributes": { + "computed": 1 + } + }, + "const_10_4_h5": { + "bits": [ + 366, + 367, + 368, + 369 + ], + "attributes": { + "computed": 1 + } + }, + "const_11_4_h5": { + "bits": [ + 370, + 371, + 372, + 373 + ], + "attributes": { + "computed": 1 + } + }, + "const_12_4_h5": { + "bits": [ + 374, + 375, + 376, + 377 + ], + "attributes": { + "computed": 1 + } + }, + "const_13_4_h5": { + "bits": [ + 378, + 379, + 380, + 381 + ], + "attributes": { + "computed": 1 + } + }, + "const_14_4_h5": { + "bits": [ + 382, + 383, + 384, + 385 + ], + "attributes": { + "computed": 1 + } + }, + "const_15_4_h2": { + "bits": [ + 403, + 404, + 405, + 406 + ], + "attributes": { + "computed": 1 + } + }, + "const_16_2_h2": { + "bits": [ + 390, + 391 + ], + "attributes": { + "computed": 1 + } + }, + "const_17_2_h2": { + "bits": [ + 392, + 393 + ], + "attributes": { + "computed": 1 + } + }, + "flipflop_7_reset_mux_Y": { + "bits": [ + 394, + 395, + 396, + 397 + ], + "hide_name": 1, + "attributes": {} + }, + "flipflop_8_reset_mux_Y": { + "bits": [ + 398, + 399, + 400, + 401 + ], + "hide_name": 1, + "attributes": {} + }, + "flipflop_8_reset_enable_Y": { + "bits": [ + 402 + ], + "hide_name": 1, + "attributes": {} + } + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/gate_catalog_module.dart b/test/fixtures/gate_catalog_module.dart new file mode 100644 index 000000000..ea602f44d --- /dev/null +++ b/test/fixtures/gate_catalog_module.dart @@ -0,0 +1,207 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gate_catalog_module.dart +// A single ROHD module that instantiates every public gate API in +// `lib/src/modules/gates.dart` (plus a few closely related primitives: +// FlipFlop variants, TriStateBuffer, BusSubset, and Swizzle) so that the +// netlist synthesizer's cell-mapper coverage can be captured in one +// deterministic, checked-in JSON asset. +// +// Every instantiated gate's output (or, for multi-output gates, every +// output) is wired directly to a uniquely named top-level output port. This +// guarantees dead-cell elimination cannot prune any of the cells this file +// is meant to exercise. +// +// See `test/gate_catalog_test.dart` for the test that verifies the checked-in +// `test/fixtures/gate_catalog.rohd.json` asset still matches what this module +// produces, and `tool/generate_gate_catalog.dart` for the script that +// (re)generates that asset. +// +// 2026 August 20 +// Author: Copilot <223556219+Copilot@users.noreply.github.com> + +import 'package:rohd/rohd.dart'; + +/// A gate-catalog top-level module. +/// +/// Instantiates one (or a small number of representative variants) of every +/// gate [Module] and top-level gate-building function exposed by +/// `lib/src/modules/gates.dart`, along with [FlipFlop] (in all mapper- +/// supported configurations), [TriStateBuffer], [BusSubset], and [Swizzle]. +/// +/// All inputs are plain free (unconnected) top-level signals; this module is +/// intended purely for structural (netlist) synthesis, not simulation. +class GateCatalog extends Module { + /// Creates the gate catalog module. + /// + /// All inputs are supplied by the caller so that construction is fully + /// deterministic and repeatable byte-for-byte across runs. + GateCatalog({ + required Logic clk, + required Logic en, + required Logic reset, + required Logic muxSel, + required Logic enableTri, + required Logic a4, + required Logic b4, + required Logic a8, + required Logic b8, + required Logic d4, + required Logic shamt4, + required Logic idx3, + required Logic idx5, + required Logic resetValueDyn4, + required LogicNet busNet, + }) : super(name: 'gate_catalog', definitionName: 'GateCatalog') { + clk = addInput('clk', clk); + en = addInput('en', en); + reset = addInput('reset', reset); + muxSel = addInput('muxSel', muxSel); + enableTri = addInput('enableTri', enableTri); + a4 = addInput('a4', a4, width: 4); + b4 = addInput('b4', b4, width: 4); + a8 = addInput('a8', a8, width: 8); + b8 = addInput('b8', b8, width: 8); + d4 = addInput('d4', d4, width: 4); + shamt4 = addInput('shamt4', shamt4, width: 4); + idx3 = addInput('idx3', idx3, width: 3); + idx5 = addInput('idx5', idx5, width: 5); + resetValueDyn4 = addInput('resetValueDyn4', resetValueDyn4, width: 4); + final bus = addInOut('bus', busNet, width: 8); + + // ── NotGate → $not ─────────────────────────────────────────────── + addOutput('not_out', width: 8) <= ~a8; + + // ── And2Gate → $and (logic/logic and logic/const variants) ─────── + addOutput('and_ll_out', width: 8) <= a8 & b8; + addOutput('and_lc_out', width: 8) <= + And2Gate(a8, Const(0xaa, width: 8)).out; + + // ── Or2Gate → $or (logic/logic and logic/const variants) ───────── + addOutput('or_ll_out', width: 8) <= a8 | b8; + addOutput('or_lc_out', width: 8) <= Or2Gate(a8, Const(0x55, width: 8)).out; + + // ── Xor2Gate → $xor (logic/logic and logic/const variants) ─────── + addOutput('xor_ll_out', width: 8) <= a8 ^ b8; + addOutput('xor_lc_out', width: 8) <= + Xor2Gate(a8, Const(0x0f, width: 8)).out; + + // ── Unary reductions → $reduce_and / $reduce_or / $reduce_xor ───── + addOutput('reduce_and_out') <= a8.and(); + addOutput('reduce_or_out') <= a8.or(); + addOutput('reduce_xor_out') <= a8.xor(); + + // ── Add → $add (logic/logic and logic/const variants) ──────────── + final addLl = Add(a4, b4); + addOutput('add_ll_sum', width: 4) <= addLl.sum; + addOutput('add_ll_carry') <= addLl.carry; + final addLc = Add(a4, 5); + addOutput('add_lc_sum', width: 4) <= addLc.sum; + addOutput('add_lc_carry') <= addLc.carry; + + // ── Subtract → $sub (logic/logic and logic/const variants) ─────── + addOutput('sub_ll_out', width: 4) <= a4 - b4; + addOutput('sub_lc_out', width: 4) <= a4 - 3; + + // ── Multiply → $mul (logic/logic and logic/const variants) ─────── + addOutput('mul_ll_out', width: 4) <= a4 * b4; + addOutput('mul_lc_out', width: 4) <= a4 * 3; + + // ── Divide → $div (logic/logic and logic/const variants) ───────── + addOutput('div_ll_out', width: 4) <= a4 / b4; + addOutput('div_lc_out', width: 4) <= a4 / 3; + + // ── Modulo → $mod (logic/logic and logic/const variants) ───────── + addOutput('mod_ll_out', width: 4) <= a4 % b4; + addOutput('mod_lc_out', width: 4) <= a4 % 3; + + // ── Power → $pow (logic/logic and logic/const variants) ────────── + addOutput('pow_ll_out', width: 4) <= a4.pow(b4); + addOutput('pow_lc_out', width: 4) <= a4.pow(3); + + // ── Comparisons → $eq/$ne/$lt/$gt/$le/$ge ───────────────────────── + addOutput('eq_ll_out') <= a4.eq(b4); + addOutput('eq_lc_out') <= a4.eq(5); + addOutput('neq_ll_out') <= a4.neq(b4); + addOutput('neq_lc_out') <= a4.neq(5); + addOutput('lt_ll_out') <= a4.lt(b4); + addOutput('lt_lc_out') <= a4.lt(5); + addOutput('gt_ll_out') <= (a4 > b4); + addOutput('gt_lc_out') <= (a4 > 5); + addOutput('le_ll_out') <= a4.lte(b4); + addOutput('le_lc_out') <= a4.lte(5); + addOutput('ge_ll_out') <= (a4 >= b4); + addOutput('ge_lc_out') <= (a4 >= 5); + + // ── Shifts → $shl / $shr / $sshr (dynamic and constant amounts) ── + addOutput('lshift_ll_out', width: 4) <= LShift(a4, shamt4).out; + addOutput('lshift_lc_out', width: 4) <= LShift(a4, 2).out; + addOutput('rshift_ll_out', width: 4) <= RShift(a4, shamt4).out; + addOutput('rshift_lc_out', width: 4) <= RShift(a4, 2).out; + addOutput('arshift_ll_out', width: 4) <= ARShift(a4, shamt4).out; + addOutput('arshift_lc_out', width: 4) <= ARShift(a4, 2).out; + + // ── Mux / mux() ──────────────────────────────────────────────── + // + // Dynamic control ⇒ a real `$mux` cell is instantiated. + addOutput('mux_class_out', width: 4) <= Mux(muxSel, a4, b4).out; + addOutput('mux_fn_dynamic_out', width: 4) <= mux(muxSel, a4, b4); + + // Constant, valid control ⇒ `mux()` folds to the selected input + // directly at *build* time: no `$mux` cell is instantiated for these two + // outputs at all. This documents/validates the function-level constant + // fold described by `mux()`'s doc comment. These outputs are wired + // directly to `a4`/`b4` (via a `$buf`-shaped netlist alias, if any) with + // no arithmetic/select cell in between. + addOutput('mux_fn_const1_out', width: 4) <= mux(Const(1, width: 1), a4, b4); + addOutput('mux_fn_const0_out', width: 4) <= mux(Const(0, width: 1), a4, b4); + + // ── IndexGate → $shiftx (natural and oversized index widths) ───── + addOutput('index_natural_out') <= a8[idx3]; + addOutput('index_oversized_out') <= a8[idx5]; + + // ── ReplicationOp (retained as an explicit, unmapped cell; no + // standard Yosys `$concat`/`$pos`-style cell models replication of a + // single dynamic operand cleanly, so it is intentionally left visible + // as its own `ReplicationOp`-typed cell rather than force-mapped) ──── + addOutput('replicate_x3_out', width: 12) <= a4.replicate(3); + addOutput('replicate_x5_out', width: 20) <= a4.replicate(5); + + // ── BusSubset → $slice ──────────────────────────────────────────── + addOutput('slice_out', width: 4) <= a8.getRange(2, 6); + + // ── Swizzle → $concat ───────────────────────────────────────────── + addOutput('swizzle_out', width: 8) <= [a4, b4].swizzle(); + + // ── TriStateBuffer → $tribuf ─────────────────────────────────────── + TriStateBuffer(a8, enable: enableTri, name: 'tsb').out.gets(bus); + addOutput('tribuf_readback_out', width: 8) <= bus; + + // ── FlipFlop variants → $dff/$dffe/$sdff/$sdffe/$adff/$adffe/ + // $aldff/$aldffe, plus dynamic-synchronous-reset lowering ──────── + addOutput('q_dff', width: 4) <= flop(clk, d4); + addOutput('q_dffe', width: 4) <= flop(clk, d4, en: en); + addOutput('q_sdff', width: 4) <= flop(clk, d4, reset: reset, resetValue: 9); + addOutput('q_sdffe', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: 9); + addOutput('q_adff', width: 4) <= + flop(clk, d4, reset: reset, resetValue: 9, asyncReset: true); + addOutput('q_adffe', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: 9, asyncReset: true); + addOutput('q_aldff', width: 4) <= + flop(clk, d4, + reset: reset, resetValue: resetValueDyn4, asyncReset: true); + addOutput('q_aldffe', width: 4) <= + flop(clk, d4, + en: en, reset: reset, resetValue: resetValueDyn4, asyncReset: true); + // Dynamic synchronous reset value: `$sdff`/`$sdffe` require a *constant* + // reset value, so the netlist translator lowers these to a `$mux` + // (selecting the reset value) feeding a plain `$dff`/`$dffe` (the enable + // ORed with reset so reset retains priority when enabled). + addOutput('q_dynsync_noen', width: 4) <= + flop(clk, d4, reset: reset, resetValue: resetValueDyn4); + addOutput('q_dynsync_en', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: resetValueDyn4); + } +} diff --git a/test/gate_catalog_test.dart b/test/gate_catalog_test.dart new file mode 100644 index 000000000..fa050f501 --- /dev/null +++ b/test/gate_catalog_test.dart @@ -0,0 +1,286 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gate_catalog_test.dart +// Verifies that the checked-in gate-catalog netlist asset +// (`test/fixtures/gate_catalog.rohd.json`) is byte-for-byte reproducible from +// `GateCatalog` (see `test/fixtures/gate_catalog_module.dart`), and spot +// checks coverage of every gate API this catalog is meant to exercise. +// +// If this test fails only because of an intentional change to gate lowering +// or the netlist cell mapper, regenerate the asset with: +// dart run tool/generate_gate_catalog.dart +// and review the diff before committing it. +// +// 2026 August 20 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import 'fixtures/gate_catalog_module.dart'; + +/// Builds a fresh [GateCatalog] with deterministic, freshly-allocated input +/// signals. +GateCatalog _buildCatalog() => GateCatalog( + clk: Logic(name: 'clk'), + en: Logic(name: 'en'), + reset: Logic(name: 'reset'), + muxSel: Logic(name: 'muxSel'), + enableTri: Logic(name: 'enableTri'), + a4: Logic(name: 'a4', width: 4), + b4: Logic(name: 'b4', width: 4), + a8: Logic(name: 'a8', width: 8), + b8: Logic(name: 'b8', width: 8), + d4: Logic(name: 'd4', width: 4), + shamt4: Logic(name: 'shamt4', width: 4), + idx3: Logic(name: 'idx3', width: 3), + idx5: Logic(name: 'idx5', width: 5), + resetValueDyn4: Logic(name: 'resetValueDyn4', width: 4), + busNet: LogicNet(name: 'busNet', width: 8), + ); + +/// Synthesizes [GateCatalog] to combined netlist JSON using the default +/// [NetlistSynthesizerConfiguration] (the same defaults a typical consumer +/// would use). +Future _synthesizeCatalogJson() async { + final catalog = _buildCatalog(); + await catalog.build(); + final synth = NetlistSynthesizer(); + return synth.synthesizeToJson(catalog); +} + +/// Path (relative to the package root, where `dart test` runs) to the +/// checked-in fixture asset. +const _fixturePath = 'test/fixtures/gate_catalog.rohd.json'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('GateCatalog netlist JSON matches checked-in fixture byte-for-byte', + () async { + final generated = await _synthesizeCatalogJson(); + final fixtureFile = File(_fixturePath); + + expect(fixtureFile.existsSync(), isTrue, + reason: 'Missing fixture at $_fixturePath. Generate it with: ' + 'dart run tool/generate_gate_catalog.dart'); + + final checkedIn = fixtureFile.readAsStringSync(); + expect( + generated, + equals(checkedIn), + reason: 'Generated gate-catalog netlist JSON no longer matches the ' + 'checked-in fixture. If this change is intentional, regenerate ' + 'the asset with `dart run tool/generate_gate_catalog.dart` and ' + 'review/commit the diff.', + ); + }); + + test('GateCatalog netlist JSON is deterministic across repeated synthesis', + () async { + final first = await _synthesizeCatalogJson(); + await Simulator.reset(); + final second = await _synthesizeCatalogJson(); + expect(second, equals(first)); + }); + + group('gate catalog coverage', () { + late Map json; + late Map moduleDef; + late Map cells; + + setUpAll(() async { + final text = await _synthesizeCatalogJson(); + json = jsonDecode(text) as Map; + final modules = json['modules'] as Map; + moduleDef = modules['GateCatalog'] as Map; + cells = moduleDef['cells'] as Map; + }); + + List> cellsOfType(String type) => cells.values + .cast>() + .where((c) => c['type'] == type) + .toList(); + + test('every standard Yosys arithmetic/logic/compare/shift cell exists', () { + const expectedTypes = { + r'$not', + r'$and', + r'$or', + r'$xor', + r'$reduce_and', + r'$reduce_or', + r'$reduce_xor', + r'$add', + r'$sub', + r'$mul', + r'$div', + r'$mod', + r'$pow', + r'$eq', + r'$ne', + r'$lt', + r'$gt', + r'$le', + r'$ge', + r'$shl', + r'$shr', + r'$sshr', + r'$mux', + r'$shiftx', + r'$slice', + r'$concat', + r'$tribuf', + r'$dff', + r'$dffe', + r'$sdff', + r'$sdffe', + r'$adff', + r'$adffe', + r'$aldff', + r'$aldffe', + }; + for (final type in expectedTypes) { + expect(cellsOfType(type), isNotEmpty, reason: 'missing $type cell'); + } + }); + + test('Power/Divide/Modulo cells have full standard A/B/Y parameters', () { + for (final type in [r'$pow', r'$div', r'$mod']) { + final matches = cellsOfType(type); + expect(matches, isNotEmpty, reason: type); + for (final cell in matches) { + expect( + cell['parameters'], + equals({ + 'A_SIGNED': 0, + 'A_WIDTH': 4, + 'B_SIGNED': 0, + 'B_WIDTH': 4, + 'Y_WIDTH': 4, + }), + reason: type, + ); + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + reason: type, + ); + } + } + }); + + test(r'IndexGate cells map to $shiftx with full standard parameters', () { + final matches = cellsOfType(r'$shiftx'); + // One "natural" 3-bit index and one "oversized" 5-bit index. + expect(matches, hasLength(2)); + final bWidths = + matches.map((c) => (c['parameters'] as Map)['B_WIDTH']).toSet(); + expect(bWidths, equals({3, 5})); + for (final cell in matches) { + final params = cell['parameters'] as Map; + expect(params['A_SIGNED'], 0); + expect(params['B_SIGNED'], 0); + expect(params['A_WIDTH'], 8); + expect(params['Y_WIDTH'], 1); + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + ); + } + }); + + test('ReplicationOp is retained as an explicit, visible (unmapped) cell', + () { + final replicationCells = cellsOfType('ReplicationOp'); + expect(replicationCells, hasLength(2)); + // Confirm it is not force-mapped to any standard Yosys cell type + // (e.g. `$concat`): its cell `type` field is the raw ROHD + // `definitionName`, not a `$`-prefixed standard primitive. + for (final cell in replicationCells) { + expect(cell['type'], isNot(startsWith(r'$'))); + } + final outputWidths = replicationCells.map((c) { + final connections = + (c['connections'] as Map).values.cast>(); + return connections.map((l) => l.length).reduce((a, b) => a > b ? a : b); + }).toSet(); + expect(outputWidths, equals({12, 20})); + }); + + test(r'constant-control mux() folds away at build time (no extra $mux)', + () { + // Two dynamic-control mux instantiations (Mux class + mux() function) + // produce two explicit `$mux` cells; the two dynamic-synchronous-reset + // flip-flops (see below) each lower to an additional `$mux` (selecting + // the reset value) for four `$mux` cells total. The two + // constant-control mux() calls fold away entirely at build time and + // contribute no additional `$mux` cells. + expect(cellsOfType(r'$mux'), hasLength(4)); + + final ports = moduleDef['ports'] as Map; + final const1Bits = + (ports['mux_fn_const1_out'] as Map)['bits'] as List; + final const0Bits = + (ports['mux_fn_const0_out'] as Map)['bits'] as List; + final aBits = (ports['a4'] as Map)['bits'] as List; + final bBits = (ports['b4'] as Map)['bits'] as List; + + // Find the (non-$mux) driver of a port's bits: since `mux()` folded + // away, the only thing between the port and its source signal is a + // plain `$buf` passthrough (emitted whenever an output port aliases an + // input directly), never a `$mux`. + // + // Note: `List`'s `==` is identity-based, not element-wise, so bit + // lists must be compared with an explicit element-wise check. + bool sameBits(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + Map driverOf(List outputBits) => + cells.values.cast>().singleWhere((c) { + final y = (c['connections'] as Map)['Y']; + return y is List && sameBits(y, outputBits); + }); + + final const1Driver = driverOf(const1Bits); + final const0Driver = driverOf(const0Bits); + expect(const1Driver['type'], r'$buf'); + expect(const0Driver['type'], r'$buf'); + + // mux(Const(1), a4, b4) folds to a4; mux(Const(0), a4, b4) folds to b4. + expect((const1Driver['connections'] as Map)['A'], equals(aBits)); + expect((const0Driver['connections'] as Map)['A'], equals(bBits)); + }); + + test(r'dynamic synchronous reset flip-flops lower to $mux + $dff/$dffe', + () { + // 8 explicit register cells (dff/dffe/sdff/sdffe/adff/adffe/aldff/ + // aldffe) + 2 lowered dynamic-sync-reset flops (1 dff-shaped, 1 + // dffe-shaped) = 9 $dff-family cells total (dffe used twice: q_dffe + // and the lowered en-variant). + expect(cellsOfType(r'$dff'), hasLength(2)); // q_dff, q_dynsync_noen + expect(cellsOfType(r'$dffe'), hasLength(2)); // q_dffe, q_dynsync_en + expect(cellsOfType(r'$sdff'), hasLength(1)); + expect(cellsOfType(r'$sdffe'), hasLength(1)); + expect(cellsOfType(r'$adff'), hasLength(1)); + expect(cellsOfType(r'$adffe'), hasLength(1)); + expect(cellsOfType(r'$aldff'), hasLength(1)); + expect(cellsOfType(r'$aldffe'), hasLength(1)); + }); + }); +} diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index c7a3f1280..a73a77bc9 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -5,7 +5,7 @@ // Comprehensive tests for the netlist synthesizer. // // 2026 April 13 -// Author: Auto-generated +// Author: Desmond Kirkpatrick import 'dart:async'; import 'dart:convert'; @@ -87,6 +87,40 @@ class FlopModule extends Module { } } +/// Exercises flip-flops with optional control signals. +class ControlledFlopModule extends Module { + ControlledFlopModule( + Logic clk, + Logic d, { + Logic? en, + Logic? reset, + Logic? resetValue, + int? constantResetValue, + bool asyncReset = false, + }) : super(name: 'controlledflop') { + clk = addInput('clk', clk); + d = addInput('d', d, width: d.width); + if (en != null) { + en = addInput('en', en); + } + if (reset != null) { + reset = addInput('reset', reset); + } + if (resetValue != null) { + resetValue = addInput('resetValue', resetValue, width: d.width); + } + addOutput('q', width: d.width) <= + flop( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue ?? constantResetValue, + asyncReset: asyncReset, + ); + } +} + /// Exercises Add. class AddModule extends Module { Logic get sum => output('sum'); @@ -321,6 +355,17 @@ class StructOutputProducerModule extends Module { } } +/// Instantiates identical structure-packing children at distinct parent paths. +class StructOutputProducerDedupTop extends Module { + StructOutputProducerDedupTop() : super(name: 'structoutputproducerdeduptop') { + final first = StructOutputProducerModule(); + final second = StructOutputProducerModule(); + + addOutput('first', width: 8) <= first.output('pair'); + addOutput('second', width: 8) <= second.output('pair'); + } +} + /// Exercises arithmetic right shift (ARShift). class ARShiftModule extends Module { Logic get y => output('y'); @@ -680,6 +725,166 @@ void main() { expect(_hasCellType(json, r'$dff'), isTrue); }); + test('FlipFlop controls map to standard Yosys register cells', () async { + final clk = SimpleClockGenerator(10).clk; + final d = Logic(width: 4); + final en = Logic(); + final reset = Logic(); + final resetValue = Logic(width: 4); + final cases = <( + ControlledFlopModule module, + String type, + Set ports, + Map parameters, + )>[ + ( + ControlledFlopModule(clk, d, en: en), + r'$dffe', + {'CLK', 'D', 'EN', 'Q'}, + {'WIDTH': 4, 'CLK_POLARITY': 1, 'EN_POLARITY': 1}, + ), + ( + ControlledFlopModule(clk, d, reset: reset, constantResetValue: 9), + r'$sdff', + {'CLK', 'D', 'SRST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'SRST_POLARITY': 1, + 'SRST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + constantResetValue: 9, + ), + r'$sdffe', + {'CLK', 'D', 'EN', 'SRST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'SRST_POLARITY': 1, + 'SRST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + reset: reset, + constantResetValue: 9, + asyncReset: true, + ), + r'$adff', + {'CLK', 'D', 'ARST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'ARST_POLARITY': 1, + 'ARST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + constantResetValue: 9, + asyncReset: true, + ), + r'$adffe', + {'CLK', 'D', 'EN', 'ARST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'ARST_POLARITY': 1, + 'ARST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + reset: reset, + resetValue: resetValue, + asyncReset: true, + ), + r'$aldff', + {'CLK', 'D', 'ALOAD', 'AD', 'Q'}, + {'WIDTH': 4, 'CLK_POLARITY': 1, 'ALOAD_POLARITY': 1}, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: true, + ), + r'$aldffe', + {'CLK', 'D', 'EN', 'ALOAD', 'AD', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'ALOAD_POLARITY': 1, + }, + ), + ]; + + for (final testCase in cases) { + final (module, type, ports, parameters) = testCase; + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (cell) => cell['type'] == type, + ); + expect( + (cell['port_directions'] as Map).keys.toSet(), + equals(ports), + reason: type, + ); + expect( + cell['parameters'], + equals(parameters), + reason: type, + ); + } + }); + + test('FlipFlop dynamic synchronous reset is lowered to standard cells', + () async { + final module = ControlledFlopModule( + SimpleClockGenerator(10).clk, + Logic(width: 4), + en: Logic(), + reset: Logic(), + resetValue: Logic(width: 4), + ); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef).values.cast>(); + final dff = cells.singleWhere((cell) => cell['type'] == r'$dffe'); + + expect(_hasCellType(json, r'$mux'), isTrue); + expect(_hasCellType(json, r'$or'), isTrue); + expect( + (dff['port_directions'] as Map).keys.toSet(), + equals({'CLK', 'D', 'EN', 'Q'}), + ); + }); + test(r'Add maps to $add cell', () async { final json = await _synthToMap( AddModule(Logic(width: 8), Logic(width: 8)), @@ -769,11 +974,47 @@ void main() { expect(_hasCellType(json, r'$shr'), isTrue); }); - test(r'ARShift maps to $shiftx cell', () async { + test(r'ARShift maps to $sshr cell', () async { final json = await _synthToMap( ARShiftModule(Logic(width: 8), Logic(width: 8)), ); - expect(_hasCellType(json, r'$shiftx'), isTrue); + expect(_hasCellType(json, r'$sshr'), isTrue); + }); + + test('shift cells use standard ports and signedness parameters', () async { + final cases = <(Module Function() moduleGen, String type, int aSigned)>[ + (() => ShiftModule(Logic(width: 8), Logic(width: 8)), r'$shl', 0), + (() => ShiftModule(Logic(width: 8), Logic(width: 8)), r'$shr', 0), + (() => ARShiftModule(Logic(width: 8), Logic(width: 8)), r'$sshr', 1), + ]; + + for (final (moduleGen, type, aSigned) in cases) { + final module = moduleGen(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (cell) => cell['type'] == type, + ); + + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + reason: type, + ); + expect( + cell['parameters'], + equals({ + 'A_SIGNED': aSigned, + 'A_WIDTH': 8, + 'B_SIGNED': 0, + 'B_WIDTH': 8, + 'Y_WIDTH': 8, + }), + reason: type, + ); + } }); test(r'AndUnary maps to $reduce_and cell', () async { @@ -797,6 +1038,16 @@ void main() { TriBufModule(busNet, Logic(width: 8), Logic()), ); expect(_hasCellType(json, r'$tribuf'), isTrue); + final tribuf = _modules(json) + .values + .cast>() + .expand((moduleDef) => _cells(moduleDef).values) + .cast>() + .singleWhere((cell) => cell['type'] == r'$tribuf'); + expect( + tribuf['port_directions'], + equals({'A': 'input', 'EN': 'input', 'Y': 'output'}), + ); }); }); @@ -969,6 +1220,25 @@ void main() { reason: 'Different-width AddModules should NOT be deduplicated', ); }); + + test('structure-pack children at different paths are deduplicated', + () async { + final json = await _synthToMap(StructOutputProducerDedupTop()); + final structProducerDefs = _modules(json) + .keys + .where( + (definitionName) => + definitionName.startsWith('StructOutputProducerModule'), + ) + .toList(); + + expect( + structProducerDefs, + hasLength(1), + reason: + 'The structure-pack cell keys must be local to each child module.', + ); + }); }); // ── Group 4: NetlistSynthesizerConfiguration permutations ────────────────── diff --git a/test/synth_structure_layout_test.dart b/test/synth_structure_layout_test.dart index d25714c92..9491598ff 100644 --- a/test/synth_structure_layout_test.dart +++ b/test/synth_structure_layout_test.dart @@ -42,6 +42,26 @@ void main() { expect(layout.fieldNameAt(3, fallbackName: 'fallback'), 'payload_1'); }); + test('returns bit ranges for nested field paths', () { + final nested = LogicStructure([ + Logic(name: 'b', width: 2), + LogicStructure([ + Logic(name: 'd', width: 3), + ], name: 'c'), + ], name: 'a'); + final structure = LogicStructure([ + Logic(name: 'prefix'), + nested, + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.bitRangeForPath('a'), (start: 1, end: 6)); + expect(layout.bitRangeForPath('a.b'), (start: 1, end: 3)); + expect(layout.bitRangeForPath('a.c'), (start: 3, end: 6)); + expect(layout.bitRangeForPath('a.c.d'), (start: 3, end: 6)); + expect(layout.bitRangeForPath('a.missing'), isNull); + }); + test('supports unpack-specific anonymous field names', () { final fieldName = Naming.unpreferredName('field'); final structure = LogicStructure([Logic(name: fieldName)]); diff --git a/tool/generate_gate_catalog.dart b/tool/generate_gate_catalog.dart new file mode 100644 index 000000000..87adb8e5b --- /dev/null +++ b/tool/generate_gate_catalog.dart @@ -0,0 +1,67 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// generate_gate_catalog.dart +// Regenerates the checked-in gate-catalog netlist asset +// (`test/fixtures/gate_catalog.rohd.json`) from `GateCatalog` (see +// `test/fixtures/gate_catalog_module.dart`) using the default +// [NetlistSynthesizerConfiguration]. +// +// Usage: +// dart run tool/generate_gate_catalog.dart +// +// After regenerating, review the diff to `test/fixtures/gate_catalog.rohd.json` +// before committing it, and re-run `dart test test/gate_catalog_test.dart` to +// confirm the fixture, determinism, and coverage checks all pass. +// +// 2026 August 20 +// Author: Copilot <223556219+Copilot@users.noreply.github.com> + +import 'dart:io'; + +import 'package:rohd/rohd.dart'; + +import '../test/fixtures/gate_catalog_module.dart'; + +/// Relative (to the package root) output path for the generated fixture. +const _fixturePath = 'test/fixtures/gate_catalog.rohd.json'; + +/// Builds a fresh [GateCatalog] with deterministic, freshly-allocated input +/// signals. +/// +/// This must stay in sync with `_buildCatalog()` in +/// `test/gate_catalog_test.dart` so that the fixture this tool generates is +/// exactly what that test's byte-for-byte comparison expects. +GateCatalog _buildCatalog() => GateCatalog( + clk: Logic(name: 'clk'), + en: Logic(name: 'en'), + reset: Logic(name: 'reset'), + muxSel: Logic(name: 'muxSel'), + enableTri: Logic(name: 'enableTri'), + a4: Logic(name: 'a4', width: 4), + b4: Logic(name: 'b4', width: 4), + a8: Logic(name: 'a8', width: 8), + b8: Logic(name: 'b8', width: 8), + d4: Logic(name: 'd4', width: 4), + shamt4: Logic(name: 'shamt4', width: 4), + idx3: Logic(name: 'idx3', width: 3), + idx5: Logic(name: 'idx5', width: 5), + resetValueDyn4: Logic(name: 'resetValueDyn4', width: 4), + busNet: LogicNet(name: 'busNet', width: 8), + ); + +Future main() async { + final catalog = _buildCatalog(); + await catalog.build(); + + final synth = NetlistSynthesizer(); + final json = synth.synthesizeToJson(catalog); + + final fixtureFile = File(_fixturePath); + fixtureFile.parent.createSync(recursive: true); + fixtureFile.writeAsStringSync(json); + + stdout.writeln('Wrote $_fixturePath (${json.length} bytes).'); + + await Simulator.reset(); +} From 2a2f6149d0c04bcc520a158b7ab2254157fae7a5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 20 Aug 2026 16:57:05 -0700 Subject: [PATCH 72/77] fix authorship --- test/fixtures/gate_catalog_module.dart | 2 +- tool/generate_gate_catalog.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/gate_catalog_module.dart b/test/fixtures/gate_catalog_module.dart index ea602f44d..ccc6fe95e 100644 --- a/test/fixtures/gate_catalog_module.dart +++ b/test/fixtures/gate_catalog_module.dart @@ -19,7 +19,7 @@ // (re)generates that asset. // // 2026 August 20 -// Author: Copilot <223556219+Copilot@users.noreply.github.com> +// Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; diff --git a/tool/generate_gate_catalog.dart b/tool/generate_gate_catalog.dart index 87adb8e5b..120a9cb54 100644 --- a/tool/generate_gate_catalog.dart +++ b/tool/generate_gate_catalog.dart @@ -15,7 +15,7 @@ // confirm the fixture, determinism, and coverage checks all pass. // // 2026 August 20 -// Author: Copilot <223556219+Copilot@users.noreply.github.com> +// Author: Desmond Kirkpatrick import 'dart:io'; From a195eac61eb608450854bb6f820d153a12bb0188 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 20 Aug 2026 17:23:58 -0700 Subject: [PATCH 73/77] gate catalog can be vm only --- test/gate_catalog_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/gate_catalog_test.dart b/test/gate_catalog_test.dart index fa050f501..965decb45 100644 --- a/test/gate_catalog_test.dart +++ b/test/gate_catalog_test.dart @@ -15,6 +15,9 @@ // 2026 August 20 // Author: Desmond Kirkpatrick +@TestOn('vm') +library; + import 'dart:convert'; import 'dart:io'; From d4e23ef81c86a8af92d02a1e1359de51b276cf40 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 24 Aug 2026 09:10:43 -0700 Subject: [PATCH 74/77] feedback fixes and prepare for merge --- example/filter_bank/filter_bank.dart | 44 ++++- example/filter_bank/filter_sample.dart | 4 +- example/filter_bank/mac_unit.dart | 2 + lib/src/module.dart | 7 +- lib/src/signals/logic_structure.dart | 6 +- .../netlist/netlist_cell_mapper.dart | 22 ++- .../netlist/netlist_module_translation.dart | 43 ++--- .../netlist/netlist_synthesis_result.dart | 44 ++++- .../netlist/netlist_synthesizer.dart | 101 +----------- .../netlist_synthesizer_configuration.dart | 17 +- .../synthesizers/netlist/netlist_utils.dart | 89 ++++++++++ .../utilities/synth_module_stop_policy.dart | 18 +-- packages/rohd_hierarchy/pubspec.yaml | 2 - pubspec.yaml | 3 - test/logic_structure_test.dart | 13 ++ test/mac_unit_test.dart | 82 ++++++++++ test/netlist_synthesizer_test.dart | 101 +++++++++++- test/netlist_test.dart | 152 ++++++++++++++++++ test/synth_name_parity_test.dart | 10 +- 19 files changed, 586 insertions(+), 174 deletions(-) create mode 100644 test/mac_unit_test.dart diff --git a/example/filter_bank/filter_bank.dart b/example/filter_bank/filter_bank.dart index 48e7fff48..f0e973472 100644 --- a/example/filter_bank/filter_bank.dart +++ b/example/filter_bank/filter_bank.dart @@ -119,9 +119,49 @@ class FilterBank extends Module { super.name = 'FilterBank', String? definitionName, }) : super(definitionName: definitionName ?? 'FilterBank') { + if (numChannels <= 0) { + throw ArgumentError.value( + numChannels, + 'numChannels', + 'must be greater than zero', + ); + } + if (numTaps <= 0) { + throw ArgumentError.value( + numTaps, + 'numTaps', + 'must be greater than zero', + ); + } + if (dataWidth <= 0) { + throw ArgumentError.value( + dataWidth, + 'dataWidth', + 'must be greater than zero', + ); + } + if (samples.length != numChannels) { + throw ArgumentError.value( + samples.length, + 'samples', + 'must have $numChannels entries (one per channel)', + ); + } if (coefficients.length != numChannels) { - throw Exception( - 'coefficients must have $numChannels entries (one per channel).'); + throw ArgumentError.value( + coefficients.length, + 'coefficients', + 'must have $numChannels entries (one per channel)', + ); + } + for (var ch = 0; ch < numChannels; ch++) { + if (coefficients[ch].length != numTaps) { + throw ArgumentError.value( + coefficients[ch].length, + 'coefficients[$ch]', + 'must have $numTaps entries (one per tap)', + ); + } } // ── Register ports ── diff --git a/example/filter_bank/filter_sample.dart b/example/filter_bank/filter_sample.dart index 28d40dc89..290e9dc76 100644 --- a/example/filter_bank/filter_sample.dart +++ b/example/filter_bank/filter_sample.dart @@ -11,8 +11,8 @@ import 'package:rohd/rohd.dart'; /// A structured signal bundling a data sample with metadata. /// -/// Packs three fields — [data], and [valid] — into a single -/// bus that can be driven and sampled as a unit. Used throughout the +/// Packs two fields — [data] and [valid] — into a single bus that can be +/// driven and sampled as a unit. Used throughout the /// filter bank to carry tagged samples between modules. class FilterSample extends LogicStructure { /// The sample data word. diff --git a/example/filter_bank/mac_unit.dart b/example/filter_bank/mac_unit.dart index 3bf43603a..0e63c6f59 100644 --- a/example/filter_bank/mac_unit.dart +++ b/example/filter_bank/mac_unit.dart @@ -60,11 +60,13 @@ class MacUnit extends Module { reset = addInput('reset', reset); enable = addInput('enable', enable); final result = addOutput('result', width: dataWidth); + final stall = (~enable).named('stall', naming: Naming.mergeable); // A 2-stage pipeline: multiply, then accumulate final pipe = Pipeline( clk, reset: reset, + stalls: [stall, stall], stages: [ // Stage 0: multiply (p) => [ diff --git a/lib/src/module.dart b/lib/src/module.dart index 99e1b1539..4a6a9e07f 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -927,11 +927,6 @@ abstract class Module { return outPort; } - bool _hasConsts(LogicStructure structure) => structure.elements.any( - (element) => - element is Const || - (element is LogicStructure && _hasConsts(element))); - /// Checks that the [logic] meets type requirements for `Typed` [Logic]s and /// returns a potentially modified [logic] to use. LogicType _validateType(LogicType logic, @@ -945,7 +940,7 @@ abstract class Module { throw PortTypeException(logic, exceptionMessage); } - if (logic is Const || (logic is LogicStructure && _hasConsts(logic))) { + if (logic is Const || (logic is LogicStructure && logic.hasConsts)) { if (LogicType == Logic) { // we're ok, can just convert to Logic final newLogic = diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index ef463b9bb..0ee7b17dd 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_structure.dart @@ -640,6 +640,10 @@ class LogicStructure implements Logic { elements.any((e) => e.isNet || (e is LogicStructure && e.hasNets)) || isNet; + /// Indicates whether this structure contains a [Const] at any depth. + bool get hasConsts => _hasConsts; + late final bool _hasConsts = leafElements.any((element) => element is Const); + @override Iterable get srcConnections => { for (final element in elements) ...element.srcConnections diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart index 9e2496a08..37fe922e4 100644 --- a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -36,7 +36,15 @@ class NetlistCellContext { final Map> rawConns; /// Creates a [NetlistCellContext]. - const NetlistCellContext(this.module, this.rawPortDirs, this.rawConns); + NetlistCellContext( + this.module, + Map rawPortDirs, + Map> rawConns, + ) : rawPortDirs = Map.unmodifiable(rawPortDirs), + rawConns = Map>.unmodifiable({ + for (final entry in rawConns.entries) + entry.key: List.unmodifiable(entry.value), + }); // ── Shared helper methods ─────────────────────────────────────────── @@ -395,16 +403,16 @@ class NetlistCellMapper { if (in0 == null || in1 == null || sumName.isEmpty) { return null; } + final sumBits = ctx.rawConns[sumName] ?? []; + final carryBits = carryName.isEmpty + ? const [] + : ctx.rawConns[carryName] ?? []; final pd = {'A': 'input', 'B': 'input', 'Y': 'output'}; final cn = >{ 'A': ctx.rawConns[in0] ?? [], 'B': ctx.rawConns[in1] ?? [], - 'Y': ctx.rawConns[sumName] ?? [], + 'Y': [...sumBits, ...carryBits], }; - if (carryName.isNotEmpty) { - pd['CO'] = 'output'; - cn['CO'] = ctx.rawConns[carryName] ?? []; - } return ( cellType: r'$add', portDirs: pd, @@ -412,7 +420,7 @@ class NetlistCellMapper { parameters: { 'A_WIDTH': ctx.width(in0), 'B_WIDTH': ctx.width(in1), - 'Y_WIDTH': ctx.width(sumName), + 'Y_WIDTH': sumBits.length + carryBits.length, }, ); }) diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 0ac8cef11..cd81babe4 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -20,7 +20,7 @@ import 'package:rohd/src/utilities/sanitizer.dart'; @internal class NetlistModuleTranslation { /// The module being translated. - final Module module; + final Module _module; /// The synthesis definition for this module level, when one can be built. final NetlistSynthModuleDefinition? synthDef; @@ -33,10 +33,9 @@ class NetlistModuleTranslation { /// /// Starts at 2 so consumers never confuse wire IDs 0 or 1 with the /// Yosys-JSON constant bit strings `"0"` and `"1"`. - int nextId = 2; + int _nextId = 2; - /// Wire identifiers allocated for each synthesis logic. - final Map> synthLogicIds = {}; + final Map> _synthLogicIds = {}; /// Emitted module ports. final Map> ports = {}; @@ -47,16 +46,16 @@ class NetlistModuleTranslation { /// Emitted netnames. final Map netnames = {}; - /// Constants consumed only by procedural cells, which need no driver cell. - final Set blockedConstSynthLogics = {}; + final Set _blockedConstSynthLogics = {}; /// Creates translation state for one [module]. NetlistModuleTranslation( - this.module, { + Module module, { required NetlistCellMapper netlistCellMapper, required bool Function(Module module) generatesDefinition, required String Function(Module module) getInstanceTypeOfModule, - }) : _netlistCellMapper = netlistCellMapper, + }) : _module = module, + _netlistCellMapper = netlistCellMapper, _generatesDefinition = generatesDefinition, _getInstanceTypeOfModule = getInstanceTypeOfModule, synthDef = module is SystemVerilog && @@ -65,12 +64,12 @@ class NetlistModuleTranslation { : NetlistSynthModuleDefinition(module); /// Allocates the next wire identifier. - int allocateWireId() => nextId++; + int allocateWireId() => _nextId++; /// Allocates or returns the wire identifiers for [synthLogic]. List getIds(SynthLogic synthLogic) { final resolved = synthLogic.isConstant ? synthLogic : synthLogic.resolved; - return synthLogicIds.putIfAbsent( + return _synthLogicIds.putIfAbsent( resolved, () => List.generate(resolved.width, (_) => allocateWireId()), ); @@ -79,9 +78,9 @@ class NetlistModuleTranslation { /// Emits input, output, and inout ports in canonical allocation order. void processPorts() { final portGroups = [ - ('input', synthDef?.inputs, module.inputs), - ('output', synthDef?.outputs, module.outputs), - ('inout', synthDef?.inOuts, module.inOuts), + ('input', synthDef?.inputs, _module.inputs), + ('output', synthDef?.outputs, _module.outputs), + ('inout', synthDef?.inOuts, _module.inOuts), ]; for (final (direction, synthLogics, modulePorts) in portGroups) { if (synthLogics != null) { @@ -187,7 +186,11 @@ class NetlistModuleTranslation { concatConnections['Y'] = outputIds.cast(); concatDirections['Y'] = 'output'; - cells['array_concat_output_$concatName'] = { + final cellName = NetlistUtils.synthesizedCellName( + operationName: 'array_concat_output', + destination: array, + ); + cells[cellName] = { 'hide_name': 0, 'type': r'$concat', 'parameters': { @@ -250,7 +253,7 @@ class NetlistModuleTranslation { if (definition == null) { return; } - module.internalSignals + _module.internalSignals .map((signal) => definition.logicToSynthMap[signal]) .whereType() .where((synthLogic) => !synthLogic.isConstant) @@ -522,7 +525,7 @@ class NetlistModuleTranslation { required Set drivenBits, }) { final emittedNames = {}; - final isInlineSystemVerilog = module is InlineSystemVerilog; + final isInlineSystemVerilog = _module is InlineSystemVerilog; void addNetname( String name, @@ -611,7 +614,7 @@ class NetlistModuleTranslation { } if (synthDef != null) { - for (final entry in synthLogicIds.entries.where( + for (final entry in _synthLogicIds.entries.where( (entry) => !entry.key.isConstant && !entry.key.declarationCleared, )) { final synthLogic = entry.key; @@ -801,9 +804,9 @@ class NetlistModuleTranslation { }) { var constantIndex = 0; final emittedConstantWires = {}; - for (final entry in synthLogicIds.entries + for (final entry in _synthLogicIds.entries .where((entry) => entry.key.isConstant) - .where((entry) => !blockedConstSynthLogics.contains(entry.key)) + .where((entry) => !_blockedConstSynthLogics.contains(entry.key)) .where((entry) => entry.value.isNotEmpty)) { final constant = NetlistUtils.constValueFromSynthLogic(entry.key); if (constant == null) { @@ -871,7 +874,7 @@ class NetlistModuleTranslation { instance.inputMapping[port.key] ?? instance.inOutMapping[port.key]; if (synthLogic != null && NetlistUtils.isConstantSynthLogic(synthLogic)) { portsToRemove.add(port.key); - blockedConstSynthLogics.add(synthLogic.resolved); + _blockedConstSynthLogics.add(synthLogic.resolved); } } for (final portName in portsToRemove) { diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart index ddf6855c0..4e312cbb1 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -35,11 +35,14 @@ class NetlistSynthesisResult extends SynthesisResult { NetlistSynthesisResult( super.module, super.getInstanceTypeOfModule, { - required this.ports, - required this.cells, - required this.netnames, - this.attributes = const {}, - }); + required Map> ports, + required Map> cells, + required Map netnames, + Map attributes = const {}, + }) : ports = _freezeNestedMap(ports), + cells = _freezeNestedMap(cells), + netnames = _freezeObjectMap(netnames), + attributes = _freezeObjectMap(attributes); /// Builds the JSON representation for this single module entry. String _buildJson() { @@ -86,3 +89,34 @@ class NetlistSynthesisResult extends SynthesisResult { ]; } } + +Map> _freezeNestedMap( + Map> source, +) => + Map.unmodifiable({ + for (final entry in source.entries) + entry.key: _freezeObjectMap(entry.value), + }); + +Map _freezeObjectMap(Map source) => + Map.unmodifiable({ + for (final entry in source.entries) entry.key: _freezeObject(entry.value), + }); + +Object? _freezeObject(Object? value) { + if (value is Map) { + return _freezeObjectMap(value); + } + if (value is Map) { + return Map.unmodifiable({ + for (final entry in value.entries) entry.key: _freezeObject(entry.value), + }); + } + if (value is List) { + return List.unmodifiable(value.map(_freezeObject)); + } + if (value is Set) { + return Set.unmodifiable(value.map(_freezeObject)); + } + return value; +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 24e3e0fd9..e7bb04475 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -15,10 +15,10 @@ import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd_hierarchy/rohd_hierarchy.dart'; /// A simple [Synthesizer] that produces netlist-compatible JSON. /// @@ -72,7 +72,7 @@ class NetlistSynthesizer extends Synthesizer { this.configuration = const NetlistSynthesizerConfiguration(), }) : _moduleStopPolicy = configuration.moduleStopPolicy ?? SynthModuleStopPolicy.netlist( - leafModuleTypes: configuration.leafModuleTypes), + leafModulePredicate: configuration.leafModulePredicate), _netlistCellMapper = configuration.netlistCellMapper ?? NetlistCellMapper.withDefaults(); @@ -746,7 +746,7 @@ class NetlistSynthesizer extends Synthesizer { final structLayout = dstLogic is LogicStructure ? SynthStructureLayout(dstLogic) : null; final cellName = dstLogic != null - ? _synthesizedCellName( + ? NetlistUtils.synthesizedCellName( operationName: SynthStructureConcat.operationName, destination: dstLogic, ) @@ -1118,98 +1118,3 @@ class NetlistSynthesizer extends Synthesizer { return generateCombinedJson(sb, top, slimMode: slimMode); } } - -String _synthesizedCellName({ - required String operationName, - required Logic destination, -}) => - '${Sanitizer.sanitizeSV(operationName)}_' - '${_destinationSuffix(destination)}'; - -String _destinationSuffix(Logic destination) { - final module = destination.parentModule; - if (module == null) { - throw SynthException( - 'Cannot derive a netlist cell key for ${destination.name}: ' - 'the destination has no parent module.', - ); - } - - final rootLocation = _logicLocationInModule(module, destination); - final elementPath = _logicElementPathIndices(destination).path; - final parts = [ - ...rootLocation.path, - ...elementPath, - ]; - - return parts.isEmpty ? '0' : parts.map((part) => part.toString()).join('_'); -} - -OccurrenceAddress _logicElementPathIndices(Logic destination) { - final elementPath = []; - var root = destination; - while (root.parentStructure != null) { - final parent = root.parentStructure!; - final index = parent.elements.indexWhere( - (element) => identical(element, root), - ); - elementPath.insert(0, index < 0 ? root.arrayIndex ?? 0 : index); - root = parent; - } - - final module = root.parentModule; - if (module == null) { - throw SynthException( - 'Cannot derive a netlist cell key for ${destination.name}: ' - 'the logic root has no parent module.', - ); - } - - return OccurrenceAddress(elementPath); -} - -OccurrenceAddress _logicLocationInModule(Module module, Logic root) { - final signalIndex = _rootSignalIndexInModule(module, root); - return OccurrenceAddress([signalIndex]); -} - -int _rootSignalIndexInModule(Module module, Logic root) { - final inputIndex = _identityIndex(module.inputs.values, root); - if (inputIndex != null) { - return inputIndex; - } - - final outputIndex = _identityIndex(module.outputs.values, root); - if (outputIndex != null) { - return module.inputs.length + outputIndex; - } - - final inOutIndex = _identityIndex(module.inOuts.values, root); - if (inOutIndex != null) { - return module.inputs.length + module.outputs.length + inOutIndex; - } - - final internalIndex = _identityIndex(module.internalSignals, root); - if (internalIndex != null) { - return module.inputs.length + - module.outputs.length + - module.inOuts.length + - internalIndex; - } - - throw SynthException( - 'Cannot derive a netlist cell key for ${root.name}: ' - 'the logic root is not registered with module ${module.name}.', - ); -} - -int? _identityIndex(Iterable logics, Logic target) { - var index = 0; - for (final logic in logics) { - if (identical(logic, target)) { - return index; - } - index++; - } - return null; -} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart index 744a25b4e..6d60ad4f8 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -46,14 +46,15 @@ class NetlistSynthesizerConfiguration { /// definitions. When `null`, [SynthModuleStopPolicy.netlist] is used. /// /// When this is provided, it owns the complete stopping policy and - /// [leafModuleTypes] is ignored. + /// [leafModulePredicate] is ignored. final SynthModuleStopPolicy? moduleStopPolicy; - /// Exact [Module.runtimeType]s that should stop netlist hierarchy traversal - /// and be emitted as cells in their parent. Defaults to [FlipFlop], which - /// contains internal sequential submodules but should be emitted as a `$dff` - /// netlist cell. - final List leafModuleTypes; + /// Determines which modules should stop netlist hierarchy traversal and be + /// emitted as cells in their parent. + /// + /// Defaults to matching [FlipFlop] and its subclasses, which contain internal + /// sequential submodules but should be emitted as `$dff` netlist cells. + final SynthModuleLeafPredicate leafModulePredicate; /// The netlist-internal mapper used to convert selected leaf modules to /// Yosys primitive cell types. When `null`, each synthesizer creates its own @@ -100,7 +101,7 @@ class NetlistSynthesizerConfiguration { /// Creates a configuration for netlist synthesis. const NetlistSynthesizerConfiguration({ this.moduleStopPolicy, - this.leafModuleTypes = const [FlipFlop], + this.leafModulePredicate = _isFlipFlop, this.netlistCellMapper, @visibleForTesting this.collapseTransparentClusters = false, @visibleForTesting this.enableDeadCellElimination = true, @@ -109,3 +110,5 @@ class NetlistSynthesizerConfiguration { this.compactJson = false, }); } + +bool _isFlipFlop(Module module) => module is FlipFlop; diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 7ff34ae78..6e7daa3d6 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -10,12 +10,101 @@ 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'; /// Shared utility functions for netlist synthesis and post-processing passes. /// /// All methods are static. @internal abstract class NetlistUtils { + /// Returns a deterministic cell name for an operation producing + /// [destination]. + static String synthesizedCellName({ + required String operationName, + required Logic destination, + }) => + '${Sanitizer.sanitizeSV(operationName)}_' + '${_destinationSuffix(destination)}'; + + static String _destinationSuffix(Logic destination) { + final module = destination.parentModule; + if (module == null) { + throw SynthException( + 'Cannot derive a netlist cell key for ${destination.name}: ' + 'the destination has no parent module.', + ); + } + + final parts = [ + _rootSignalIndexInModule(module, _rootLogic(destination)), + ..._logicElementPathIndices(destination), + ]; + return parts.map((part) => part.toString()).join('_'); + } + + static Logic _rootLogic(Logic destination) { + var root = destination; + while (root.parentStructure != null) { + root = root.parentStructure!; + } + return root; + } + + static List _logicElementPathIndices(Logic destination) { + final elementPath = []; + var current = destination; + while (current.parentStructure != null) { + final parent = current.parentStructure!; + final index = parent.elements.indexWhere( + (element) => identical(element, current), + ); + elementPath.insert(0, index < 0 ? current.arrayIndex ?? 0 : index); + current = parent; + } + return elementPath; + } + + static int _rootSignalIndexInModule(Module module, Logic root) { + final inputIndex = _identityIndex(module.inputs.values, root); + if (inputIndex != null) { + return inputIndex; + } + + final outputIndex = _identityIndex(module.outputs.values, root); + if (outputIndex != null) { + return module.inputs.length + outputIndex; + } + + final inOutIndex = _identityIndex(module.inOuts.values, root); + if (inOutIndex != null) { + return module.inputs.length + module.outputs.length + inOutIndex; + } + + final internalIndex = _identityIndex(module.internalSignals, root); + if (internalIndex != null) { + return module.inputs.length + + module.outputs.length + + module.inOuts.length + + internalIndex; + } + + throw SynthException( + 'Cannot derive a netlist cell key for ${root.name}: ' + 'the logic root is not registered with module ${module.name}.', + ); + } + + static int? _identityIndex(Iterable logics, Logic target) { + var index = 0; + for (final logic in logics) { + if (identical(logic, target)) { + return index; + } + index++; + } + return null; + } + /// Find the port name in [portMap] that corresponds to [sl]. static String? portNameForSynthLogic( SynthLogic sl, diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart index 317565cce..9447f2764 100644 --- a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -19,22 +19,19 @@ typedef SynthModuleDefinitionPredicate = bool Function(Module module); /// Shared hierarchy stopping policy for synthesis backends. /// -/// A synthesizer configures this with backend-specific leaf module types, -/// predicates, and a default definition rule, then queries [isLeaf] or -/// [generatesDefinition] while walking a module hierarchy. +/// A synthesizer configures this with backend-specific leaf predicates and a +/// default definition rule, then queries [isLeaf] or [generatesDefinition] +/// while walking a module hierarchy. class SynthModuleStopPolicy { - final Set _leafModuleTypes; final List _leafPredicates; final SynthModuleDefinitionPredicate _generatesDefinitionByDefault; /// Creates a module stopping policy. SynthModuleStopPolicy({ SynthModuleDefinitionPredicate? generatesDefinitionByDefault, - Iterable leafModuleTypes = const [], Iterable leafPredicates = const [], }) : _generatesDefinitionByDefault = generatesDefinitionByDefault ?? ((_) => true), - _leafModuleTypes = Set.unmodifiable(leafModuleTypes), _leafPredicates = List.unmodifiable(leafPredicates); /// Creates the default SystemVerilog stopping policy. @@ -48,22 +45,21 @@ class SynthModuleStopPolicy { /// Creates the default netlist stopping policy. factory SynthModuleStopPolicy.netlist({ - Iterable leafModuleTypes = const [FlipFlop], - Iterable leafPredicates = const [], + SynthModuleLeafPredicate leafModulePredicate = _isFlipFlop, }) => SynthModuleStopPolicy( generatesDefinitionByDefault: (module) => module.subModules.isNotEmpty, - leafModuleTypes: leafModuleTypes, - leafPredicates: leafPredicates, + leafPredicates: [leafModulePredicate], ); /// Returns `true` when [module] should be treated as a leaf cell in its /// parent instead of receiving its own generated definition. bool isLeaf(Module module) => !_generatesDefinitionByDefault(module) || - _leafModuleTypes.contains(module.runtimeType) || _leafPredicates.any((predicate) => predicate(module)); /// Returns `true` when [module] should receive its own generated definition. bool generatesDefinition(Module module) => !isLeaf(module); } + +bool _isFlipFlop(Module module) => module is FlipFlop; diff --git a/packages/rohd_hierarchy/pubspec.yaml b/packages/rohd_hierarchy/pubspec.yaml index c75cd3d34..8a3351519 100644 --- a/packages/rohd_hierarchy/pubspec.yaml +++ b/packages/rohd_hierarchy/pubspec.yaml @@ -5,8 +5,6 @@ repository: https://github.com/intel/rohd version: 0.1.0 issue_tracker: https://github.com/intel/rohd/issues -publish_to: none - environment: sdk: '>=3.0.0 <4.0.0' diff --git a/pubspec.yaml b/pubspec.yaml index 58758deae..42949a933 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,6 @@ homepage: https://intel.github.io/rohd-website repository: https://github.com/intel/rohd issue_tracker: https://github.com/intel/rohd/issues documentation: https://intel.github.io/rohd-website/docs/sample-example/ -publish_to: none environment: sdk: '>=3.0.0 <4.0.0' @@ -14,8 +13,6 @@ dependencies: collection: ^1.15.0 logging: ^1.0.1 meta: ^1.9.0 - rohd_hierarchy: - path: packages/rohd_hierarchy test: ^1.17.3 dev_dependencies: diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index a1de7e79a..695942bbe 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -191,6 +191,19 @@ void main() { expect(s.name, 'structure'); }); + test('hasConsts detects constants at any depth', () { + final withoutConsts = LogicStructure([Logic()]); + final withDirectConst = LogicStructure([Logic(), Const(0)]); + final withNestedConst = LogicStructure([ + Logic(), + LogicStructure([Logic(), Const(1)]), + ]); + + expect(withoutConsts.hasConsts, isFalse); + expect(withDirectConst.hasConsts, isTrue); + expect(withNestedConst.hasConsts, isTrue); + }); + test('sub logic in two structures throws exception', () { final s = LogicStructure([ Logic(), diff --git a/test/mac_unit_test.dart b/test/mac_unit_test.dart new file mode 100644 index 000000000..1dcda6026 --- /dev/null +++ b/test/mac_unit_test.dart @@ -0,0 +1,82 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// mac_unit_test.dart +// Tests for the filter-bank multiply-accumulate example. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank/mac_unit.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('disabled pipeline holds its result and intermediate stages', () async { + const dataWidth = 8; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final enable = Logic(); + final sample = Logic(width: dataWidth); + final coefficient = Logic(width: dataWidth); + final accumulator = Logic(width: dataWidth); + final dut = MacUnit( + sample, + coefficient, + accumulator, + clk, + reset, + enable, + dataWidth: dataWidth, + ); + await dut.build(); + + reset.inject(1); + enable.inject(0); + sample.inject(0); + coefficient.inject(0); + accumulator.inject(0); + Simulator.setMaxSimTime(200); + unawaited(Simulator.run()); + + await clk.nextPosedge; + reset.inject(0); + enable.inject(1); + sample.inject(3); + coefficient.inject(4); + accumulator.inject(5); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect(dut.result.value.toInt(), 17); + + enable.inject(0); + sample.inject(7); + coefficient.inject(8); + accumulator.inject(9); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect( + dut.result.value.toInt(), + 17, + reason: 'Both pipeline stages must hold while enable is low.', + ); + + enable.inject(1); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect(dut.result.value.toInt(), 65); + + await Simulator.endSimulation(); + }); +} diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index a73a77bc9..8486814e2 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -87,6 +87,11 @@ class FlopModule extends Module { } } +/// A custom [FlipFlop] used to verify inheritance-aware leaf matching. +class CustomFlipFlop extends FlipFlop { + CustomFlipFlop(super.clk, super.d); +} + /// Exercises flip-flops with optional control signals. class ControlledFlopModule extends Module { ControlledFlopModule( @@ -131,6 +136,18 @@ class AddModule extends Module { } } +/// Exercises both the sum and carry outputs of [Add]. +class AddWithCarryModule extends Module { + AddWithCarryModule(Logic a, Logic b, {int width = 8}) + : super(name: 'addwithcarry') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final add = Add(a, b); + addOutput('sum', width: width) <= add.sum; + addOutput('carry') <= add.carry; + } +} + /// Wraps an [AddModule] so stop-policy tests can choose whether the child /// receives its own definition or is emitted as a netlist cell. class AddWrapperModule extends Module { @@ -247,6 +264,28 @@ class ArrayOutputChildModule extends Module { } } +/// Provides multiple array outputs to verify synthesized concat cell names. +class MultipleArrayOutputModule extends Module { + MultipleArrayOutputModule() + : super( + name: 'multiplearrayoutput', + definitionName: 'MultipleArrayOutputModule', + ) { + final dataA = addInput('dataA', Logic(width: 8), width: 8); + final dataB = addInput('dataB', Logic(width: 8), width: 8); + final first = addOutputArray('first', dimensions: [2], elementWidth: 8); + final second = addOutputArray('second', dimensions: [2], elementWidth: 8); + for (final element in first.elements) { + element <= dataA; + } + for (final element in second.elements) { + element <= dataB; + } + final child = NotModule(dataA[0]); + addOutput('childOut') <= child.y; + } +} + /// Parent whose internal LogicArray elements independently feed a child array /// input port. class InternalArrayToChildModule extends Module { @@ -892,6 +931,26 @@ void main() { expect(_hasCellType(json, r'$add'), isTrue); }); + test(r'Add maps carry into the high bit of standard $add Y', () async { + final json = await _synthToMap( + AddWithCarryModule(Logic(width: 8), Logic(width: 8)), + ); + final addCell = _modules(json) + .values + .cast>() + .expand((definition) => _cells(definition).values) + .cast>() + .singleWhere((cell) => cell['type'] == r'$add'); + final directions = addCell['port_directions'] as Map; + final connections = addCell['connections'] as Map; + final parameters = addCell['parameters'] as Map; + + expect(directions.keys.toSet(), equals({'A', 'B', 'Y'})); + expect(connections.keys.toSet(), equals({'A', 'B', 'Y'})); + expect(connections['Y'], hasLength(9)); + expect(parameters['Y_WIDTH'], 9); + }); + test(r'Subtract maps to $sub cell', () async { final json = await _synthToMap( SubModule(Logic(width: 8), Logic(width: 8)), @@ -1320,8 +1379,10 @@ void main() { 'filter bank can stop traversal at an opaque custom SV module', () { final synthesizer = NetlistSynthesizer( - configuration: const NetlistSynthesizerConfiguration( - leafModuleTypes: [FlipFlop, MacUnit]), + configuration: NetlistSynthesizerConfiguration( + leafModulePredicate: (module) => + module is FlipFlop || module is MacUnit, + ), ); final json = jsonDecode(synthesizer.synthesizeToJson(filterBank)) as Map; @@ -1717,15 +1778,16 @@ void main() { }); test( - 'leaf module type option controls which modules stop traversal', + 'leaf module predicate controls which modules stop traversal', () async { final module = AddWrapperModule(); await module.build(); final childDefinitionName = module.subModules.single.definitionName; final synthesizer = NetlistSynthesizer( - configuration: const NetlistSynthesizerConfiguration( - leafModuleTypes: [AddModule]), + configuration: NetlistSynthesizerConfiguration( + leafModulePredicate: (module) => module is AddModule, + ), ); final json = jsonDecode(synthesizer.synthesizeToJson(module)) @@ -1746,6 +1808,15 @@ void main() { }, ); + test('default leaf predicate matches FlipFlop subclasses', () { + const configuration = NetlistSynthesizerConfiguration(); + + expect( + configuration.leafModulePredicate(CustomFlipFlop(Logic(), Logic())), + isTrue, + ); + }); + test('repeated translation of the same module is identical', () async { final module = LogicArrayExample( LogicArray([4], 8, name: 'arrayA'), @@ -1816,6 +1887,26 @@ void main() { } }); + test('array concat output names use unique destination addresses', + () async { + final module = MultipleArrayOutputModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final arrayConcatNames = _cells(moduleDef) + .entries + .where( + (entry) => + (entry.value as Map)['type'] == r'$concat', + ) + .map((entry) => entry.key) + .where((name) => name.startsWith('array_concat_output_')) + .toList(); + + expect(arrayConcatNames, hasLength(2)); + expect(arrayConcatNames.toSet(), hasLength(2)); + }); + test('regrouped array output elements get explicit concat', () async { final module = RegroupedArrayOutputToChildModule(); final json = await _synthToMap(module); diff --git a/test/netlist_test.dart b/test/netlist_test.dart index 899d45851..5efb405a8 100644 --- a/test/netlist_test.dart +++ b/test/netlist_test.dart @@ -182,6 +182,133 @@ void main() { await Simulator.reset(); }); + group('FilterBank argument validation', () { + FilterBank construct({ + int numChannels = 2, + int numTaps = 3, + int dataWidth = 16, + List? samples, + List> coefficients = const [ + [1, 2, 1], + [1, -2, 1], + ], + }) => + FilterBank( + Logic(), + Logic(), + Logic(), + samples ?? + List.generate( + numChannels, + (ch) => FilterSample( + dataWidth: dataWidth, + name: 'sample$ch', + ), + ), + Logic(), + numChannels: numChannels, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients, + ); + + test('rejects an empty channel count', () { + expect( + () => construct( + numChannels: 0, + samples: const [], + coefficients: const [], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'numChannels', + ), + ), + ); + }); + + test('rejects an empty tap count', () { + expect( + () => construct( + numTaps: 0, + coefficients: const [[], []], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'numTaps', + ), + ), + ); + }); + + test('rejects a non-positive data width', () { + expect( + () => construct( + dataWidth: 0, + samples: [ + FilterSample(name: 'sample0'), + FilterSample(name: 'sample1'), + ], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'dataWidth', + ), + ), + ); + }); + + test('rejects a sample count that differs from the channel count', () { + expect( + () => construct(samples: [FilterSample(name: 'sample0')]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'samples', + ), + ), + ); + }); + + test('rejects a coefficient count that differs from the channel count', () { + expect( + () => construct(coefficients: const [ + [1, 2, 1], + ]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'coefficients', + ), + ), + ); + }); + + test('rejects a coefficient row with the wrong tap count', () { + expect( + () => construct(coefficients: const [ + [1, 2, 1], + [1, -2], + ]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'coefficients[1]', + ), + ), + ); + }); + }); + // ── Example smoke tests ─────────────────────────────────────────────── // // Each example is synthesized once, verifying that the netlist is @@ -495,6 +622,31 @@ void main() { .firstWhere((r) => r.module == mod); expect(result.netnames, isNotEmpty); }); + + test('result maps and nested values are unmodifiable', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults + .whereType() + .firstWhere((result) => result.module == mod); + final firstCell = result.cells.values.first; + final connections = firstCell['connections']! as Map; + + expect( + () => result.cells['replacement'] = {}, + throwsUnsupportedError, + ); + expect( + () => firstCell['type'] = r'$replacement', + throwsUnsupportedError, + ); + expect( + () => connections['replacement'] = [], + throwsUnsupportedError, + ); + }); }); // ── collectModuleEntries ────────────────────────────────────────────── diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index d4f20fe47..dc10da6e1 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause // // synth_name_parity_test.dart -// Tests that verify canonicalNameOf works consistently across +// Tests that verify signalNameOfBest works consistently across // different synthesis paths (SV and netlist). // // 2026 April 14 @@ -186,7 +186,7 @@ void main() { await Simulator.reset(); }); - group('canonicalNameOf after netlist synthesis', () { + group('signalNameOfBest after netlist synthesis', () { test('counter — returns names after netlist synthesis', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); @@ -228,8 +228,8 @@ void main() { }); }); - group('canonicalNameOf after SV synthesis', () { - test('counter — returns canonical name after SV synth', () async { + group('signalNameOfBest after SV synthesis', () { + test('counter — returns best signal name after SV synth', () async { final mod = _Counter(Logic(), Logic()); await mod.build(); @@ -242,7 +242,7 @@ void main() { group('cross-synthesizer parity', () { test( - 'counter — SV and netlist produce identical canonicalNameOf', + 'counter — SV and netlist produce identical signalNameOfBest', () async { final modNetlist = _Counter(Logic(), Logic()); await modNetlist.build(); From cf62d8ae07676116bbacda409d2e714bab692f10 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 24 Aug 2026 09:31:31 -0700 Subject: [PATCH 75/77] exhaustive gate fixture changed. --- test/fixtures/gate_catalog.rohd.json | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/fixtures/gate_catalog.rohd.json b/test/fixtures/gate_catalog.rohd.json index 3e52e4ce1..ffa54ef77 100644 --- a/test/fixtures/gate_catalog.rohd.json +++ b/test/fixtures/gate_catalog.rohd.json @@ -1353,14 +1353,13 @@ "parameters": { "A_WIDTH": 4, "B_WIDTH": 4, - "Y_WIDTH": 4 + "Y_WIDTH": 5 }, "attributes": {}, "port_directions": { "A": "input", "B": "input", - "Y": "output", - "CO": "output" + "Y": "output" }, "connections": { "A": [ @@ -1379,9 +1378,7 @@ 110, 111, 112, - 113 - ], - "CO": [ + 113, 114 ] } @@ -1392,14 +1389,13 @@ "parameters": { "A_WIDTH": 4, "B_WIDTH": 4, - "Y_WIDTH": 4 + "Y_WIDTH": 5 }, "attributes": {}, "port_directions": { "A": "input", "B": "input", - "Y": "output", - "CO": "output" + "Y": "output" }, "connections": { "A": [ @@ -1418,9 +1414,7 @@ 115, 116, 117, - 118 - ], - "CO": [ + 118, 119 ] } From c6e7860a5c0d492644abfc77eb4eaf966b071ac4 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 24 Aug 2026 11:29:53 -0700 Subject: [PATCH 76/77] performance optimizations as well as review changes related to leaf cells, and using enum for port dirs --- lib/src/modules/conditionals/flop.dart | 2 +- .../synthesizers/netlist/netlist_cell.dart | 53 ++++ .../netlist/netlist_cell_mapper.dart | 52 ++-- .../netlist/netlist_module_translation.dart | 262 +++++++++--------- .../synthesizers/netlist/netlist_passes.dart | 22 +- .../netlist/netlist_port_direction.dart | 27 ++ .../netlist/netlist_synthesizer.dart | 43 +-- .../synthesizers/netlist/netlist_utils.dart | 174 ++++++------ tool/generate_gate_catalog.dart | 2 + 9 files changed, 373 insertions(+), 264 deletions(-) create mode 100644 lib/src/synthesizers/netlist/netlist_cell.dart create mode 100644 lib/src/synthesizers/netlist/netlist_port_direction.dart diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index c8b932ec2..4930d6c96 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // flop.dart diff --git a/lib/src/synthesizers/netlist/netlist_cell.dart b/lib/src/synthesizers/netlist/netlist_cell.dart new file mode 100644 index 000000000..2f920288c --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_cell.dart @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_cell.dart +// Typed representation of a serialized netlist cell. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; + +/// A cell in a synthesized netlist. +@internal +class NetlistCell { + /// Whether consumers should hide this cell's name. + final int hideName; + + /// The Yosys cell type or module definition name. + final String type; + + /// Parameters configuring the cell. + final Map parameters; + + /// Attributes attached to the cell. + final Map attributes; + + /// Directions of the cell's ports. + final Map portDirections; + + /// Bits connected to each cell port. + final Map> connections; + + /// Creates a netlist cell. + const NetlistCell({ + required this.type, + required this.portDirections, + required this.connections, + this.parameters = const {}, + this.attributes = const {}, + this.hideName = 0, + }); + + /// Serializes this cell to the Yosys-compatible JSON structure. + Map toJson() => { + 'hide_name': hideName, + 'type': type, + 'parameters': parameters, + 'attributes': attributes, + 'port_directions': serializePortDirections(portDirections), + 'connections': connections, + }; +} diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart index 37fe922e4..eb3a16742 100644 --- a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -9,12 +9,13 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; /// The result of mapping a netlist cell module to a Yosys-style cell. @internal typedef NetlistCellMapping = ({ String cellType, - Map portDirs, + Map portDirs, Map> connections, Map parameters, }); @@ -29,8 +30,8 @@ class NetlistCellContext { /// The ROHD [Module] being mapped. final Module module; - /// Raw ROHD port-direction map (`{'portName': 'input'|'output'|'inout'}`). - final Map rawPortDirs; + /// Raw ROHD port-direction map. + final Map rawPortDirs; /// Raw ROHD connection map (`{'portName': [wireId, ...]}`). final Map> rawConns; @@ -38,9 +39,10 @@ class NetlistCellContext { /// Creates a [NetlistCellContext]. NetlistCellContext( this.module, - Map rawPortDirs, + Map rawPortDirs, Map> rawConns, - ) : rawPortDirs = Map.unmodifiable(rawPortDirs), + ) : rawPortDirs = + Map.unmodifiable(rawPortDirs), rawConns = Map>.unmodifiable({ for (final entry in rawConns.entries) entry.key: List.unmodifiable(entry.value), @@ -71,15 +73,19 @@ class NetlistCellContext { /// Build new port-direction and connection maps from a /// `{rohdPortName: yosysPortName}` mapping. - ({Map portDirs, Map> connections}) remap( + ({ + Map portDirs, + Map> connections, + }) remap( Map nameMap, ) { - final pd = {}; + final pd = {}; final cn = >{}; for (final e in nameMap.entries) { final rohdName = e.key; final netlistPortName = e.value; - pd[netlistPortName] = rawPortDirs[rohdName] ?? 'output'; + pd[netlistPortName] = + rawPortDirs[rohdName] ?? NetlistPortDirection.output; cn[netlistPortName] = rawConns[rohdName] ?? []; } return (portDirs: pd, connections: cn); @@ -124,7 +130,7 @@ class NetlistCellMapper { /// Returns `null` if no registered handler matches. NetlistCellMapping? map( Module module, - Map rawPortDirs, + Map rawPortDirs, Map> rawConns, ) { final ctx = NetlistCellContext(module, rawPortDirs, rawConns); @@ -332,7 +338,7 @@ class NetlistCellMapper { } // N-input concat: per-input range labels, output is Y. - final pd = {}; + final pd = {}; final cn = >{}; final params = {}; var bitOffset = 0; @@ -341,13 +347,13 @@ class NetlistCellMapper { final w = ctx.width(ik); final label = w == 1 ? '[$bitOffset]' : '[${bitOffset + w - 1}:$bitOffset]'; - pd[label] = 'input'; + pd[label] = NetlistPortDirection.input; cn[label] = ctx.rawConns[ik] ?? []; params['IN${i}_WIDTH'] = w; bitOffset += w; } if (outName != null) { - pd['Y'] = 'output'; + pd['Y'] = NetlistPortDirection.output; cn['Y'] = ctx.rawConns[outName] ?? []; } return ( @@ -407,7 +413,11 @@ class NetlistCellMapper { final carryBits = carryName.isEmpty ? const [] : ctx.rawConns[carryName] ?? []; - final pd = {'A': 'input', 'B': 'input', 'Y': 'output'}; + final pd = { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }; final cn = >{ 'A': ctx.rawConns[in0] ?? [], 'B': ctx.rawConns[in1] ?? [], @@ -460,10 +470,10 @@ class NetlistCellMapper { return null; } - final pd = { - 'CLK': 'input', - 'D': 'input', - 'Q': 'output', + final pd = { + 'CLK': NetlistPortDirection.input, + 'D': NetlistPortDirection.input, + 'Q': NetlistPortDirection.output, }; final cn = >{ 'CLK': ctx.rawConns[clk] ?? [], @@ -471,18 +481,18 @@ class NetlistCellMapper { 'Q': ctx.rawConns[q] ?? [], }; if (hasEnable) { - pd['EN'] = 'input'; + pd['EN'] = NetlistPortDirection.input; cn['EN'] = ctx.rawConns[en] ?? []; } if (hasReset) { final resetPort = flipFlop.asyncReset ? (hasDynamicResetValue ? 'ALOAD' : 'ARST') : 'SRST'; - pd[resetPort] = 'input'; + pd[resetPort] = NetlistPortDirection.input; cn[resetPort] = ctx.rawConns[rst] ?? []; } if (hasDynamicResetValue) { - pd['AD'] = 'input'; + pd['AD'] = NetlistPortDirection.input; cn['AD'] = ctx.rawConns[rstVal] ?? []; } @@ -612,7 +622,7 @@ class NetlistCellMapper { final enName = tsb.inputs.keys.last; // enable final outName = tsb.inOuts.keys.first; // inout output final r = ctx.remap({inName: 'A', enName: 'EN', outName: 'Y'}); - r.portDirs['Y'] = 'output'; + r.portDirs['Y'] = NetlistPortDirection.output; return ( cellType: r'$tribuf', portDirs: r.portDirs, diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index cd81babe4..eead0033a 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -9,7 +9,9 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synth_module_definition.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; @@ -48,6 +50,15 @@ class NetlistModuleTranslation { final Set _blockedConstSynthLogics = {}; + late final ({ + Set arrayConcatOutputs, + Set directSubmoduleOutputs, + }) _submoduleOutputDrivers = _indexSubmoduleOutputDrivers(); + + late final NetlistAlwaysBlockPortCollapseIndex? + _alwaysBlockPortCollapseIndex = + synthDef == null ? null : NetlistAlwaysBlockPortCollapseIndex(synthDef!); + /// Creates translation state for one [module]. NetlistModuleTranslation( Module module, { @@ -78,29 +89,29 @@ class NetlistModuleTranslation { /// Emits input, output, and inout ports in canonical allocation order. void processPorts() { final portGroups = [ - ('input', synthDef?.inputs, _module.inputs), - ('output', synthDef?.outputs, _module.outputs), - ('inout', synthDef?.inOuts, _module.inOuts), + (NetlistPortDirection.input, synthDef?.inputs, _module.inputs), + (NetlistPortDirection.output, synthDef?.outputs, _module.outputs), + (NetlistPortDirection.inout, synthDef?.inOuts, _module.inOuts), ]; for (final (direction, synthLogics, modulePorts) in portGroups) { if (synthLogics != null) { + final portNames = + NetlistUtils.portNamesForSynthLogics(synthLogics, modulePorts); for (final synthLogic in synthLogics) { - final portName = NetlistUtils.portNameForSynthLogic( - synthLogic, - modulePorts, - ); + final portName = portNames[synthLogic]; if (portName != null) { final portLogic = modulePorts[portName]; - final emitOutputArrayConcat = direction == 'output' && - portLogic is LogicArray && - !_hasExistingOutputArrayConcat(synthLogic) && - !_hasDirectSubmoduleOutputDriver(synthLogic); + final emitOutputArrayConcat = + direction == NetlistPortDirection.output && + portLogic is LogicArray && + !_hasExistingOutputArrayConcat(synthLogic) && + !_hasDirectSubmoduleOutputDriver(synthLogic); final originalIds = getIds(synthLogic); final ids = emitOutputArrayConcat ? List.generate(synthLogic.width, (_) => allocateWireId()) : originalIds; ports[portName] = { - 'direction': direction, + 'direction': direction.name, 'bits': ids, if (portLogic != null) 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), @@ -117,7 +128,7 @@ class NetlistModuleTranslation { (_) => allocateWireId(), ); ports[entry.key] = { - 'direction': direction, + 'direction': direction.name, 'bits': ids, 'logic_type': NetlistUtils.buildLogicType(entry.value, ids), }; @@ -147,7 +158,7 @@ class NetlistModuleTranslation { } final concatConnections = >{}; - final concatDirections = {}; + final concatDirections = {}; var lowerIndex = 0; for (final (index, element) in array.elements.indexed) { @@ -175,7 +186,8 @@ class NetlistModuleTranslation { final upperIndex = lowerIndex + elementIds.length - 1; concatConnections['[$upperIndex:$lowerIndex]'] = elementIds.cast(); - concatDirections['[$upperIndex:$lowerIndex]'] = 'input'; + concatDirections['[$upperIndex:$lowerIndex]'] = + NetlistPortDirection.input; lowerIndex = upperIndex + 1; } @@ -184,67 +196,61 @@ class NetlistModuleTranslation { } concatConnections['Y'] = outputIds.cast(); - concatDirections['Y'] = 'output'; + concatDirections['Y'] = NetlistPortDirection.output; final cellName = NetlistUtils.synthesizedCellName( operationName: 'array_concat_output', destination: array, ); - cells[cellName] = { - 'hide_name': 0, - 'type': r'$concat', - 'parameters': { + cells[cellName] = NetlistCell( + type: r'$concat', + parameters: { for (var index = 0; index < array.elements.length; index++) 'IN${index}_WIDTH': array.elements[index].width, }, - 'attributes': {}, - 'port_directions': concatDirections, - 'connections': concatConnections, - }; + portDirections: concatDirections, + connections: concatConnections, + ).toJson(); return true; } /// Checks whether [synthLogic] is already driven by an output concat cell. - bool _hasExistingOutputArrayConcat(SynthLogic synthLogic) { - final definition = synthDef; - if (definition == null) { - return false; - } - - for (final instance in definition.subModuleInstantiations) { - if (instance.module is! SynthArrayConcat) { - continue; - } - if (instance.outputMapping.values.any( - (outputLogic) => outputLogic.resolved == synthLogic.resolved, - )) { - return true; - } - } - - return false; - } + bool _hasExistingOutputArrayConcat(SynthLogic synthLogic) => + _submoduleOutputDrivers.arrayConcatOutputs.contains(synthLogic.resolved); /// Checks whether [synthLogic] is driven directly by a non-concat submodule. - bool _hasDirectSubmoduleOutputDriver(SynthLogic synthLogic) { + bool _hasDirectSubmoduleOutputDriver(SynthLogic synthLogic) => + _submoduleOutputDrivers.directSubmoduleOutputs + .contains(synthLogic.resolved); + + ({ + Set arrayConcatOutputs, + Set directSubmoduleOutputs, + }) _indexSubmoduleOutputDrivers() { final definition = synthDef; if (definition == null) { - return false; + return ( + arrayConcatOutputs: {}, + directSubmoduleOutputs: {}, + ); } + final arrayConcatOutputs = {}; + final directSubmoduleOutputs = {}; for (final instance in definition.subModuleInstantiations) { - if (instance.module is SynthArrayConcat) { - continue; - } - if (instance.outputMapping.values.any( - (outputLogic) => outputLogic.resolved == synthLogic.resolved, - )) { - return true; - } + (instance.module is SynthArrayConcat + ? arrayConcatOutputs + : directSubmoduleOutputs) + .addAll( + instance.outputMapping.values.map((output) => output.resolved), + ); } - return false; + return ( + arrayConcatOutputs: arrayConcatOutputs, + directSubmoduleOutputs: directSubmoduleOutputs, + ); } /// Preallocates internal wires in [Module.internalSignals] order. @@ -279,13 +285,13 @@ class NetlistModuleTranslation { final defaultCellType = isLeaf ? submodule.definitionName : _getInstanceTypeOfModule(submodule); - final rawPortDirs = {}; + final rawPortDirs = {}; final rawConnections = >{}; for (final (direction, mapping) in [ - ('input', instance.inputMapping), - ('output', instance.outputMapping), - ('inout', instance.inOutMapping), + (NetlistPortDirection.input, instance.inputMapping), + (NetlistPortDirection.output, instance.outputMapping), + (NetlistPortDirection.inout, instance.inOutMapping), ]) { for (final entry in mapping.entries) { rawPortDirs[entry.key] = direction; @@ -313,7 +319,7 @@ class NetlistModuleTranslation { if (submodule is Combinational || submodule is Sequential) { NetlistUtils.collapseAlwaysBlockPorts( - definition, + _alwaysBlockPortCollapseIndex!, instance, cellPortDirs, cellConnections, @@ -327,7 +333,8 @@ class NetlistModuleTranslation { for (final portEntry in submodule.inputs.entries) { final portName = portEntry.key; final port = portEntry.value; - if (port is! LogicArray || cellPortDirs[portName] != 'input') { + if (port is! LogicArray || + cellPortDirs[portName] != NetlistPortDirection.input) { continue; } final bits = cellConnections[portName]; @@ -336,7 +343,7 @@ class NetlistModuleTranslation { } final concatConnections = >{}; - final concatDirections = {}; + final concatDirections = {}; var lowerIndex = 0; for (final element in port.elements) { final upperIndex = lowerIndex + element.width - 1; @@ -345,7 +352,7 @@ class NetlistModuleTranslation { lowerIndex, upperIndex + 1, ); - concatDirections[concatPort] = 'input'; + concatDirections[concatPort] = NetlistPortDirection.input; lowerIndex = upperIndex + 1; } @@ -353,31 +360,27 @@ class NetlistModuleTranslation { for (var i = 0; i < bits.length; i++) allocateWireId(), ]; concatConnections['Y'] = concatOutput; - concatDirections['Y'] = 'output'; + concatDirections['Y'] = NetlistPortDirection.output; cellConnections[portName] = concatOutput; - cells['array_concat_${cellKey}_$portName'] = { - 'hide_name': 0, - 'type': r'$concat', - 'parameters': { + cells['array_concat_${cellKey}_$portName'] = NetlistCell( + type: r'$concat', + parameters: { for (var index = 0; index < port.elements.length; index++) 'IN${index}_WIDTH': port.elements[index].width, }, - 'attributes': {}, - 'port_directions': concatDirections, - 'connections': concatConnections, - }; + portDirections: concatDirections, + connections: concatConnections, + ).toJson(); } } - cells[cellKey] = { - 'hide_name': 0, - 'type': mapped?.cellType ?? defaultCellType, - 'parameters': mapped?.parameters ?? {}, - 'attributes': {}, - 'port_directions': cellPortDirs, - 'connections': cellConnections, - }; + cells[cellKey] = NetlistCell( + type: mapped?.cellType ?? defaultCellType, + parameters: mapped?.parameters ?? const {}, + portDirections: cellPortDirs, + connections: cellConnections, + ).toJson(); } definition.subModuleInstantiations @@ -394,7 +397,7 @@ class NetlistModuleTranslation { bool _emitDynamicSynchronousResetFlipFlop( String cellKey, FlipFlop flipFlop, - Map rawPortDirs, + Map rawPortDirs, Map> rawConnections, ) { if (flipFlop.asyncReset) { @@ -403,7 +406,7 @@ class NetlistModuleTranslation { String? findInput(String unpreferredName, String name) { for (final entry in rawPortDirs.entries) { - if (entry.value == 'input' && + if (entry.value == NetlistPortDirection.input && (entry.key.startsWith(unpreferredName) || entry.key == name)) { return entry.key; } @@ -417,7 +420,7 @@ class NetlistModuleTranslation { final reset = findInput('_reset', 'reset'); final resetValue = findInput('_resetValue', 'resetValue'); final q = rawPortDirs.entries - .where((entry) => entry.value == 'output') + .where((entry) => entry.value == NetlistPortDirection.output) .map((entry) => entry.key) .firstOrNull; if (clk == null || @@ -443,24 +446,22 @@ class NetlistModuleTranslation { final resetMuxOutput = List.generate(dBits.length, (_) => allocateWireId()); - cells['${cellKey}_reset_mux'] = { - 'hide_name': 0, - 'type': r'$mux', - 'parameters': {'WIDTH': dBits.length}, - 'attributes': {}, - 'port_directions': { - 'A': 'input', - 'B': 'input', - 'S': 'input', - 'Y': 'output' + cells['${cellKey}_reset_mux'] = NetlistCell( + type: r'$mux', + parameters: {'WIDTH': dBits.length}, + portDirections: { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'S': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, }, - 'connections': { + connections: { 'A': dBits, 'B': resetValueBits, 'S': resetBits, 'Y': resetMuxOutput, }, - }; + ).toJson(); final hasEnable = en != null && rawConnections.containsKey(en); final dffConnections = >{ @@ -468,10 +469,10 @@ class NetlistModuleTranslation { 'D': resetMuxOutput, 'Q': qBits, }; - final dffDirections = { - 'CLK': 'input', - 'D': 'input', - 'Q': 'output', + final dffDirections = { + 'CLK': NetlistPortDirection.input, + 'D': NetlistPortDirection.input, + 'Q': NetlistPortDirection.output, }; final dffParameters = { 'WIDTH': dBits.length, @@ -483,35 +484,35 @@ class NetlistModuleTranslation { return false; } final effectiveEnable = [allocateWireId()]; - cells['${cellKey}_reset_enable'] = { - 'hide_name': 0, - 'type': r'$or', - 'parameters': { + cells['${cellKey}_reset_enable'] = NetlistCell( + type: r'$or', + parameters: { 'A_WIDTH': 1, 'B_WIDTH': 1, 'Y_WIDTH': 1, }, - 'attributes': {}, - 'port_directions': {'A': 'input', 'B': 'input', 'Y': 'output'}, - 'connections': { + portDirections: { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: { 'A': enableBits, 'B': resetBits, 'Y': effectiveEnable, }, - }; + ).toJson(); dffConnections['EN'] = effectiveEnable; - dffDirections['EN'] = 'input'; + dffDirections['EN'] = NetlistPortDirection.input; dffParameters['EN_POLARITY'] = 1; } - cells[cellKey] = { - 'hide_name': 0, - 'type': hasEnable ? r'$dffe' : r'$dff', - 'parameters': dffParameters, - 'attributes': {}, - 'port_directions': dffDirections, - 'connections': dffConnections, - }; + cells[cellKey] = NetlistCell( + type: hasEnable ? r'$dffe' : r'$dff', + parameters: dffParameters, + portDirections: dffDirections, + connections: dffConnections, + ).toJson(); return true; } @@ -556,7 +557,7 @@ class NetlistModuleTranslation { } final aggregateConstructors = - <({List inputBits, List outputBits})>[]; + inputBits, List outputBits})>>{}; for (final cellEntry in cells.entries) { final cell = cellEntry.value; final cellType = cell['type'] as String?; @@ -588,7 +589,9 @@ class NetlistModuleTranslation { } if (inputBits.length == outputBits.length) { - aggregateConstructors.add(( + aggregateConstructors + .putIfAbsent(Object.hashAll(inputBits), () => []) + .add(( inputBits: inputBits, outputBits: outputBits, )); @@ -596,7 +599,11 @@ class NetlistModuleTranslation { } List resolveAggregateBits(List bits) { - for (final constructorBits in aggregateConstructors) { + final candidates = aggregateConstructors[Object.hashAll(bits)]; + if (candidates == null) { + return bits; + } + for (final constructorBits in candidates) { if (bits.length == constructorBits.inputBits.length) { var matches = true; for (var index = 0; index < bits.length; index++) { @@ -827,14 +834,13 @@ class NetlistModuleTranslation { final valuePart = NetlistUtils.constValuePart(constant); final cellName = 'const_${constantIndex}_$valuePart'; final valueLiteral = valuePart.replaceFirst('_', "'"); - cells[cellName] = { - 'hide_name': 0, - 'type': r'$const', - 'parameters': {}, - 'attributes': {}, - 'port_directions': {valueLiteral: 'output'}, - 'connections': >{valueLiteral: resolvedIds}, - }; + cells[cellName] = NetlistCell( + type: r'$const', + portDirections: { + valueLiteral: NetlistPortDirection.output, + }, + connections: >{valueLiteral: resolvedIds}, + ).toJson(); constantIndex++; } @@ -865,7 +871,7 @@ class NetlistModuleTranslation { /// Removes procedural constant ports and records their constants as blocked. void _filterProceduralConstants( SynthSubModuleInstantiation instance, - Map portDirections, + Map portDirections, Map> connections, ) { final portsToRemove = []; @@ -886,7 +892,7 @@ class NetlistModuleTranslation { /// Renames procedural ports to match their resolved synth logic names. void _renameProceduralPorts( SynthSubModuleInstantiation instance, - Map portDirections, + Map portDirections, Map> connections, ) { final renames = {}; diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart index ff0af154a..4f22c5d0b 100644 --- a/lib/src/synthesizers/netlist/netlist_passes.dart +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -9,6 +9,8 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; @@ -575,21 +577,25 @@ class NetlistPasses { continue; } - cells[concatEntry.key] = { - 'hide_name': concat['hide_name'] ?? 0, - 'type': r'$slice', - 'parameters': { + cells[concatEntry.key] = NetlistCell( + hideName: concat['hide_name'] as int? ?? 0, + type: r'$slice', + parameters: { 'OFFSET': startOffset, 'A_WIDTH': sourceWidth, 'Y_WIDTH': combinedWidth, }, - 'attributes': concat['attributes'] ?? {}, - 'port_directions': {'A': 'input', 'Y': 'output'}, - 'connections': >{ + attributes: (concat['attributes'] as Map?)?.cast() ?? + const {}, + portDirections: const { + 'A': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: >{ 'A': sourceBits, 'Y': outputBits, }, - }; + ).toJson(); for (final sliceRef in inputSliceRefs) { if (!_sliceOutputConsumedOutside( diff --git a/lib/src/synthesizers/netlist/netlist_port_direction.dart b/lib/src/synthesizers/netlist/netlist_port_direction.dart new file mode 100644 index 000000000..610ab774e --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_port_direction.dart @@ -0,0 +1,27 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_port_direction.dart +// Type-safe netlist port directions and JSON serialization. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +/// A port direction while constructing a netlist. +@internal +enum NetlistPortDirection { + input, + output, + inout, +} + +/// Converts typed [directions] to the strings required by Yosys JSON. +@internal +Map serializePortDirections( + Map directions, +) => + { + for (final entry in directions.entries) entry.key: entry.value.name, + }; diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index e7bb04475..db921fb26 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -11,9 +11,11 @@ import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; @@ -645,7 +647,9 @@ class NetlistSynthesizer extends Synthesizer { : null; // Build port_directions and connections with one output per field. - final portDirs = {'A': 'input'}; + final portDirs = { + 'A': NetlistPortDirection.input, + }; final conns = >{'A': resolvedParentBits}; for (var i = 0; i < nonTrivialFields.length; i++) { @@ -658,7 +662,7 @@ class NetlistSynthesizer extends Synthesizer { if (portDirs.containsKey(portName)) { portName = '${fieldName}_$i'; } - portDirs[portName] = 'output'; + portDirs[portName] = NetlistPortDirection.output; conns[portName] = f.resolvedElemBits; } @@ -676,14 +680,12 @@ class NetlistSynthesizer extends Synthesizer { params['FIELD_${i}_WIDTH'] = f.width; } - cells['struct_unpack_${suIdx}_$structName'] = { - 'hide_name': 0, - 'type': r'$struct_unpack', - 'parameters': params, - 'attributes': {}, - 'port_directions': portDirs, - 'connections': conns - }; + cells['struct_unpack_${suIdx}_$structName'] = NetlistCell( + type: r'$struct_unpack', + parameters: params, + portDirections: portDirs, + connections: conns, + ).toJson(); suIdx++; } } @@ -753,7 +755,7 @@ class NetlistSynthesizer extends Synthesizer { : SynthStructureConcat.operationName; // Build port_directions and connections. - final portDirs = {}; + final portDirs = {}; final conns = >{}; for (var i = 0; i < nonTrivialFields.length; i++) { @@ -765,12 +767,12 @@ class NetlistSynthesizer extends Synthesizer { if (portDirs.containsKey(portName)) { portName = '${fieldName}_$i'; } - portDirs[portName] = 'input'; + portDirs[portName] = NetlistPortDirection.input; conns[portName] = f.resolvedSrcBits; } // Output port Y: full destination bus. - portDirs['Y'] = 'output'; + portDirs['Y'] = NetlistPortDirection.output; conns['Y'] = resolvedDstBits; // Parameters list field metadata for the schematic viewer. @@ -787,14 +789,12 @@ class NetlistSynthesizer extends Synthesizer { params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; } - cells['${cellName}_$structName'] = { - 'hide_name': 0, - 'type': r'$struct_pack', - 'parameters': params, - 'attributes': {}, - 'port_directions': portDirs, - 'connections': conns - }; + cells['${cellName}_$structName'] = NetlistCell( + type: r'$struct_pack', + parameters: params, + portDirections: portDirs, + connections: conns, + ).toJson(); } } @@ -1113,6 +1113,7 @@ class NetlistSynthesizer extends Synthesizer { /// The [packageRoot] parameter is accepted for API compatibility with /// downstream trace-enabled branches. [slimMode] overrides the configured /// output mode for this call, allowing expansion after a slim request. + @visibleForTesting String synthesizeToJson(Module top, {String? packageRoot, bool? slimMode}) { final sb = SynthBuilder(top, this); return generateCombinedJson(sb, top, slimMode: slimMode); diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 6e7daa3d6..638afbd45 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -9,9 +9,66 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; +typedef _BusSubsetCollapseInfo = ( + BusSubset, + SynthLogic, + SynthSubModuleInstantiation, +); + +typedef _SwizzleCollapseInfo = ( + String, + int, + int, + SynthLogic, + SynthSubModuleInstantiation, +); + +/// Reusable indexes for collapsing procedural-cell ports. +@internal +class NetlistAlwaysBlockPortCollapseIndex { + final Module _module; + final Map _busSubsets = {}; + final Map _swizzles = {}; + + /// Indexes aggregate-producing submodules in [synthDef]. + NetlistAlwaysBlockPortCollapseIndex(SynthModuleDefinition synthDef) + : _module = synthDef.module { + for (final instance in synthDef.subModuleInstantiations) { + final module = instance.module; + if (module is BusSubset) { + final output = instance.outputMapping.values.firstOrNull; + final input = instance.inputMapping.values.firstOrNull; + if (output != null && input != null) { + _busSubsets[output.resolved] = (module, input.resolved, instance); + } + } else if (module is Swizzle) { + final output = instance.outputMapping.values.firstOrNull; + if (output == null) { + continue; + } + + var offset = 0; + for (final input in instance.inputMapping.entries) { + final resolvedInput = input.value.resolved; + _swizzles[resolvedInput] = ( + input.key, + offset, + resolvedInput.width, + output.resolved, + instance, + ); + offset += resolvedInput.width; + } + } + } + } +} + /// Shared utility functions for netlist synthesis and post-processing passes. /// /// All methods are static. @@ -105,17 +162,26 @@ abstract class NetlistUtils { return null; } - /// Find the port name in [portMap] that corresponds to [sl]. - static String? portNameForSynthLogic( - SynthLogic sl, + /// Indexes [synthLogics] by their corresponding name in [portMap]. + static Map portNamesForSynthLogics( + Iterable synthLogics, Map portMap, ) { - for (final e in portMap.entries) { - if (sl.logics.contains(e.value)) { - return e.key; + final namesByLogic = Map.identity() + ..addEntries( + portMap.entries.map((entry) => MapEntry(entry.value, entry.key)), + ); + final portNames = Map.identity(); + for (final synthLogic in synthLogics) { + for (final logic in synthLogic.logics) { + final portName = namesByLogic[logic]; + if (portName != null) { + portNames[synthLogic] = portName; + break; + } } } - return null; + return portNames; } /// Safely retrieve the name from a [SynthLogic], returning null if @@ -129,14 +195,15 @@ abstract class NetlistUtils { List aBits, List yBits, ) => - { - 'hide_name': 0, - 'type': r'$buf', - 'parameters': {'WIDTH': width}, - 'attributes': {}, - 'port_directions': {'A': 'input', 'Y': 'output'}, - 'connections': >{'A': aBits, 'Y': yBits}, - }; + NetlistCell( + type: r'$buf', + parameters: {'WIDTH': width}, + portDirections: { + 'A': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: >{'A': aBits, 'Y': yBits}, + ).toJson(); /// Collapses bit-slice ports of a Combinational/Sequential cell into /// aggregate ports. @@ -154,38 +221,14 @@ abstract class NetlistUtils { /// the inputs of the same Swizzle submodule are collapsed into a single /// aggregate port connected to the Swizzle's output wire IDs. static void collapseAlwaysBlockPorts( - SynthModuleDefinition synthDef, + NetlistAlwaysBlockPortCollapseIndex index, SynthSubModuleInstantiation instance, - Map portDirs, + Map portDirs, Map> connections, List Function(SynthLogic) getIds, ) { // ── Input-side collapsing (BusSubset → Combinational) ────────────── - // Build reverse lookup: resolved BusSubset output SynthLogic → - // (BusSubset module, resolved root input SynthLogic, - // SynthSubModuleInstantiation). - final busSubsetLookup = - {}; - for (final bsInst in synthDef.subModuleInstantiations) { - if (bsInst.module is! BusSubset) { - continue; - } - final bsMod = bsInst.module as BusSubset; - - // BusSubset has input 'original' and output 'subset' - final outputSL = bsInst.outputMapping.values.firstOrNull; - final inputSL = bsInst.inputMapping.values.firstOrNull; - if (outputSL == null || inputSL == null) { - continue; - } - - final resolvedOutput = outputSL.resolved; - final resolvedInput = inputSL.resolved; - - busSubsetLookup[resolvedOutput] = (bsMod, resolvedInput, bsInst); - } - // Group input ports by root signal, also tracking the BusSubset // instantiations that produced each port. final inputGroups = {}; - for (final szInst in synthDef.subModuleInstantiations) { - if (szInst.module is! Swizzle) { - continue; - } - final outputSL = szInst.outputMapping.values.firstOrNull; - if (outputSL == null) { - continue; - } - final resolvedOutput = outputSL.resolved; - - // Swizzle inputs are in0, in1, ... with bit-0 first. - var offset = 0; - for (final inEntry in szInst.inputMapping.entries) { - final resolvedInput = inEntry.value.resolved; - final w = resolvedInput.width; - swizzleLookup[resolvedInput] = ( - inEntry.key, - offset, - w, - resolvedOutput, - szInst, - ); - offset += w; - } - } - // Group output ports by Swizzle output signal. final outputGroups = []).add(( @@ -344,14 +349,13 @@ abstract class NetlistUtils { // with a single aggregate that uses the downstream Swizzle's bit // IDs, which would orphan the module-level port bits (they would // no longer be driven by any cell). - final parentModule = synthDef.module; final hasModulePort = entry.value.any((member) { final sl = instance.outputMapping[member.$1]; if (sl == null) { return false; } final resolved = sl.resolved; - return resolved.isPort(parentModule); + return resolved.isPort(index._module); }); if (hasModulePort) { continue; @@ -394,7 +398,7 @@ abstract class NetlistUtils { portDirs.remove(portName); } connections[outName] = aggBits; - portDirs[outName] = 'output'; + portDirs[outName] = NetlistPortDirection.output; } } diff --git a/tool/generate_gate_catalog.dart b/tool/generate_gate_catalog.dart index 120a9cb54..41b0a9c89 100644 --- a/tool/generate_gate_catalog.dart +++ b/tool/generate_gate_catalog.dart @@ -55,6 +55,8 @@ Future main() async { await catalog.build(); final synth = NetlistSynthesizer(); + // We will migrate to a new public API in a future PR + // ignore: invalid_use_of_visible_for_testing_member final json = synth.synthesizeToJson(catalog); final fixtureFile = File(_fixturePath); From 15e4ed57db069550d19d23beaf56fa7f2f6aff6e Mon Sep 17 00:00:00 2001 From: Rebase Agent Date: Sun, 23 Aug 2026 07:25:36 -0700 Subject: [PATCH 77/77] Stack feature/signal-value-format-registry after netlist_pre Exact cumulative tree from original branch 7412bd260ddd235b6005d54ae7dadba372fad0b3 in the netlist-first merge order. --- packages/rohd_hierarchy/analysis_options.yaml | 9 + .../lib/src/hierarchy_models.dart | 1 + .../lib/src/occurrence_trie.dart | 108 ++++++++ .../test/occurrence_trie_test.dart | 54 ++++ packages/rohd_waveform/analysis_options.yaml | 9 + .../packages/rohd_devtools_widgets/README.md | 42 +++ .../lib/rohd_devtools_widgets.dart | 3 + .../lib/src/signal_value_format_registry.dart | 239 ++++++++++++++++++ .../rohd_devtools_widgets/pubspec.yaml | 2 + .../signal_value_format_registry_test.dart | 173 +++++++++++++ ...sted_array_struct_port_synthesis_test.dart | 3 + 11 files changed, 643 insertions(+) create mode 100644 packages/rohd_hierarchy/lib/src/occurrence_trie.dart create mode 100644 packages/rohd_hierarchy/test/occurrence_trie_test.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart diff --git a/packages/rohd_hierarchy/analysis_options.yaml b/packages/rohd_hierarchy/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_hierarchy/analysis_options.yaml +++ b/packages/rohd_hierarchy/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart index 2f7cb3f76..dc79dbb6c 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -12,5 +12,6 @@ export 'hierarchy_occurrence.dart'; export 'hierarchy_search_result.dart'; export 'occurrence_address.dart'; export 'occurrence_search_result.dart'; +export 'occurrence_trie.dart'; export 'signal_occurrence.dart'; export 'signal_search_result.dart'; diff --git a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart new file mode 100644 index 000000000..31ea2d647 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -0,0 +1,108 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie.dart +// Compact storage for values keyed by hierarchy occurrence addresses. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/occurrence_address.dart'; + +/// A prefix-sharing map from [OccurrenceAddress] values to values of type [T]. +/// +/// Common address prefixes are stored once, making this more compact than a +/// conventional map when many values belong to the same hierarchy subtree. +class OccurrenceTrie { + final _OccurrenceTrieNode _root = _OccurrenceTrieNode(); + + /// Whether this trie contains no values. + bool get isEmpty => _root.isEmpty; + + /// The value stored at [address], if any. + T? operator [](OccurrenceAddress address) { + var node = _root; + for (final index in _validatedPath(address)) { + final child = node.children[index]; + if (child == null) { + return null; + } + node = child; + } + return node.value; + } + + /// Associates [value] with [address]. + /// + /// Returns the value previously stored at [address], if any. + T? set(OccurrenceAddress address, T value) { + var node = _root; + for (final index in _validatedPath(address)) { + node = node.children.putIfAbsent(index, _OccurrenceTrieNode.new); + } + final previous = node.value; + node.value = value; + return previous; + } + + /// Removes and returns the value stored at [address], if any. + T? remove(OccurrenceAddress address) { + final path = _validatedPath(address); + final nodes = <_OccurrenceTrieNode>[_root]; + var node = _root; + for (final index in path) { + final child = node.children[index]; + if (child == null) { + return null; + } + nodes.add(child); + node = child; + } + + final previous = node.value; + if (previous == null) { + return null; + } + node.value = null; + for (var index = path.length - 1; index >= 0; index--) { + final child = nodes[index + 1]; + if (!child.isEmpty) { + break; + } + nodes[index].children.remove(path[index]); + } + return previous; + } + + /// Removes every value from this trie. + void clear() { + _root + ..value = null + ..children.clear(); + } + + static List _validatedPath(OccurrenceAddress address) { + if (address.path.isEmpty) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must not be empty.', + ); + } + if (address.path.any((index) => index < 0)) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must contain non-negative indices.', + ); + } + return address.path; + } +} + +class _OccurrenceTrieNode { + final Map> children = {}; + T? value; + + bool get isEmpty => value == null && children.isEmpty; +} diff --git a/packages/rohd_hierarchy/test/occurrence_trie_test.dart b/packages/rohd_hierarchy/test/occurrence_trie_test.dart new file mode 100644 index 000000000..ef9e7d4bc --- /dev/null +++ b/packages/rohd_hierarchy/test/occurrence_trie_test.dart @@ -0,0 +1,54 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie_test.dart +// Tests for compact occurrence-address trie storage. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + test('stores values with shared occurrence-address prefixes', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + + expect(trie.set(first, 'first'), isNull); + expect(trie.set(second, 'second'), isNull); + + expect(trie[first], 'first'); + expect(trie[second], 'second'); + expect(trie[const OccurrenceAddress([0, 2, 6])], isNull); + }); + + test('prunes an address branch after removing its final value', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + trie + ..set(first, 'first') + ..set(second, 'second'); + + expect(trie.remove(first), 'first'); + expect(trie[first], isNull); + expect(trie[second], 'second'); + expect(trie.remove(second), 'second'); + expect(trie.isEmpty, isTrue); + }); + + test('rejects an address that cannot identify a signal', () { + final trie = OccurrenceTrie(); + + expect( + () => trie.set(OccurrenceAddress.root, 'root'), + throwsArgumentError, + ); + expect( + () => trie[const OccurrenceAddress([0, -1])], + throwsArgumentError, + ); + }); +} diff --git a/packages/rohd_waveform/analysis_options.yaml b/packages/rohd_waveform/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_waveform/analysis_options.yaml +++ b/packages/rohd_waveform/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md index e40b328bb..fa99b412a 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md @@ -26,6 +26,48 @@ across DevTools packages. - ROHD extension client/status abstractions: `RohdExtensionClient`, `NullExtensionClient`, `RohdModuleInfo`, and `RohdFormatInfo`. +## Widgets & Utilities + +### UI Controls & Buttons + +- **`MarkdownHelpButton`** — A help button that displays Markdown content from an asset file in a dialog. Supports tooltip text and rich formatting. + +- **`ExportPngButton`** — A camera icon button for triggering PNG export functionality. Includes customizable tooltip text. + +- **`CrossProbeButton`** — A toolbar button for toggling cross-probing between viewers. Shows a bidirectional arrows icon that reflects the active/inactive state. + +### Overlays & Layout + +- **`AppBarOverlay`** — An auto-hiding AppBar that slides in from the top edge when the mouse approaches. When disabled, behaves like a standard AppBar. + +### Export & Capture + +- **`CaptureBoundary`** — Utility for capturing a `RepaintBoundary` as PNG, saving/downloading, and showing user feedback via toast notifications. + +- **`ExportToast`** — Toast notification widget for export feedback and status messages. + +### Cross-Probing + +- **`CrossProbeService`** — Service for managing cross-probe state between multiple viewers/debuggers. Handles bidirectional signal selection synchronization. + +- **`CrossProbeMenu`** — Shared context menu integration for cross-probing actions across different ROHD DevTools surfaces. + +### Signal & Bit Field Utilities + +- **`LogicTypeUtils`** — Utilities for working with ROHD logic types and formatting logic values for display. + +- **`BitFieldUtils`** — Utilities for parsing, validating, and formatting bit field ranges and named bit fields. + +- **`BitExpansionMenu`** — Shared popup menu items for "Expand Bits" and "Define Bit Fields" actions used across signal selection overlays and panels. + +- **`SignalValueFormatRegistry`** — Shared registry for signal display-format preferences, allowing consistent formatting across multiple viewers. + +### Extension Integration + +- **`RohdExtensionClient`** — Abstract interface for querying the ROHD VS Code extension. Supports multiple implementations (DevTools, VS Code webview, offline mode). + +- **`RohdExtensionStatus`** — Status information and connection state for the ROHD extension. + ## Usage Add this package as a path dependency from a ROHD DevTools package and import the shared widgets you need: diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart index 452567fae..ca31b9ace 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart @@ -36,6 +36,9 @@ export 'src/bit_field_utils.dart'; // Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers export 'src/bit_expansion_menu.dart'; +// Shared signal display-format preferences and value formatting +export 'src/signal_value_format_registry.dart'; + // ROHD extension client export 'src/rohd_extension_status.dart'; export 'src/rohd_extension_client.dart'; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart new file mode 100644 index 000000000..bf5fdcad7 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -0,0 +1,239 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry.dart +// Shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:rohd/rohd.dart' + show LogicValue, LogicValueConstructionException; +import 'package:rohd_hierarchy/rohd_hierarchy.dart' + show OccurrenceAddress, OccurrenceTrie; + +/// The available display formats for signal values. +enum SignalValueFormat { + /// The source waveform representation. + waveform, + + /// A binary representation. + binary, + + /// A hexadecimal representation. + hexadecimal, + + /// An unsigned decimal representation. + unsignedDecimal, + + /// A two's-complement signed decimal representation. + signedDecimal, + + /// An octal representation. + octal, + + /// An ASCII representation. + ascii, +} + +/// A format preference for one signal occurrence address. +class SignalValueFormatPreference { + /// Creates a preference for [address] using [format]. + SignalValueFormatPreference( + this.address, + this.format, + ); + + /// The occurrence address, including the signal index. + final OccurrenceAddress address; + + /// The selected display format. + final SignalValueFormat format; +} + +/// Shared display-format preferences keyed by occurrence address. +/// +/// Viewer packages publish [SignalValueFormat] values; embedded surfaces use +/// the same values without depending on viewer-local format enums. +class SignalValueFormatRegistry { + SignalValueFormatRegistry._(); + + static final _formatTrie = OccurrenceTrie(); + + /// Notifies listeners whenever occurrence-format preferences change. + static final changes = ValueNotifier(0); + + /// Replaces all occurrence-format preferences with [preferences]. + static void update(Iterable preferences) { + _formatTrie.clear(); + for (final preference in preferences) { + _formatTrie.set(preference.address, preference.format); + } + _notifyListeners(); + } + + /// Removes all occurrence-format preferences. + static void clear() { + if (_formatTrie.isEmpty) { + return; + } + _formatTrie.clear(); + _notifyListeners(); + } + + /// Sets [format] for each signal occurrence in [addresses]. + static void setFormatFor( + Iterable addresses, + SignalValueFormat format, + ) { + var changed = false; + for (final address in addresses) { + changed = (_formatTrie.set(address, format) != format) || changed; + } + if (changed) { + _notifyListeners(); + } + } + + /// Converts a serialized format name to its corresponding enum value. + /// + /// Returns `null` when [value] is not a known format name. + static SignalValueFormat? formatFromString(String value) { + for (final format in SignalValueFormat.values) { + if (format.name == value) { + return format; + } + } + return null; + } + + /// Converts [format] to its serialized format name. + static String formatToString(SignalValueFormat format) => format.name; + + /// Returns the requested format for [address], or the waveform default. + static SignalValueFormat formatFor(OccurrenceAddress address) { + return formatForAny([address]); + } + + /// Returns the first registered format matching [addresses]. + static SignalValueFormat formatForAny( + Iterable addresses, { + SignalValueFormat fallback = SignalValueFormat.waveform, + }) { + for (final address in addresses) { + if (address == null) { + continue; + } + final format = _formatTrie[address]; + if (format != null) { + return format; + } + } + return fallback; + } + + static void _notifyListeners() => changes.value++; + + static bool _containsUnknownDigits(String value) { + final lower = value.toLowerCase(); + final apostrophe = lower.indexOf("'"); + final digits = apostrophe > 0 && apostrophe + 2 <= lower.length + ? lower.substring(apostrophe + 2) + : lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + return digits.contains('x') || digits.contains('z'); + } + + /// Formats a ROHD radix literal according to [format]. + static String formatValue( + String value, + SignalValueFormat format, + int width, + ) { + final waveformValue = _waveformValue(value, width); + if (waveformValue == null) { + return _canonicalWaveformValue(value, width); + } + final (logicValue, canonical) = waveformValue; + if (format == SignalValueFormat.waveform || + _containsUnknownDigits(canonical)) { + return canonical; + } + return switch (format) { + SignalValueFormat.binary => logicValue.toRadixString( + leadingZeros: true, + includeWidth: false, + sepChar: '', + ), + SignalValueFormat.hexadecimal => + logicValue.toRadixString(radix: 16, sepChar: ''), + SignalValueFormat.unsignedDecimal => + logicValue.toRadixString(radix: 10, includeWidth: false, sepChar: ''), + SignalValueFormat.signedDecimal => + logicValue.toBigInt().toSigned(logicValue.width).toString(), + SignalValueFormat.octal => + '0o${logicValue.toRadixString(radix: 8, includeWidth: false, sepChar: '')}', + SignalValueFormat.ascii => String.fromCharCodes( + List.generate( + ((logicValue.width + 7) ~/ 8).clamp(1, 32), + (index) { + final shift = + (((logicValue.width + 7) ~/ 8).clamp(1, 32) - index - 1) * 8; + final code = + ((logicValue.toBigInt() >> shift) & BigInt.from(0xff)) + .toInt(); + return code >= 0x20 && code <= 0x7e ? code : 0x2e; + }, + ), + ), + SignalValueFormat.waveform => canonical, + }; + } + + static (LogicValue, String)? _waveformValue(String value, int width) { + final trimmed = value.trim().replaceAll('\u0000', ''); + if (trimmed.isEmpty || _containsUnknownDigits(trimmed)) { + return null; + } + final lower = trimmed.toLowerCase(); + final displayWidth = width > 0 ? width : 1; + final isRadixLiteral = RegExp(r"^\d+'[bqodh]").hasMatch(lower); + final digits = lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + final radix = lower.startsWith('0x') + ? 'h' + : lower.startsWith('0b') + ? 'b' + : isRadixLiteral + ? lower[lower.indexOf("'") + 1] + : digits.codeUnits.every( + (codeUnit) => codeUnit == 0x30 || codeUnit == 0x31, + ) + ? 'b' + : digits.codeUnits.any( + (codeUnit) => + (codeUnit >= 0x61 && codeUnit <= 0x66) || + (codeUnit >= 0x41 && codeUnit <= 0x46), + ) + ? 'h' + : 'd'; + final radixLiteral = isRadixLiteral ? lower : "$displayWidth'$radix$digits"; + try { + final logicValue = LogicValue.ofRadixString(radixLiteral); + return ( + logicValue, + isRadixLiteral ? trimmed : logicValue.toString(), + ); + } on LogicValueConstructionException { + return null; + } + } + + static String _canonicalWaveformValue(String value, int width) { + final waveformValue = _waveformValue(value, width); + return waveformValue?.$2 ?? value.trim().replaceAll('\u0000', ''); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml index dcb1b7a95..3f24d6f92 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: flutter: {sdk: flutter} rohd: ^0.6.9 + rohd_hierarchy: + path: ../../../packages/rohd_hierarchy web: ^1.0.0 dev_dependencies: flutter_test: {sdk: flutter} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart new file mode 100644 index 000000000..3b0eec1a7 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -0,0 +1,173 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry_test.dart +// Tests for shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter_test/flutter_test.dart'; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +void main() { + tearDown(SignalValueFormatRegistry.clear); + + test('formats bare binary and hexadecimal waveform values', () { + expect( + SignalValueFormatRegistry.formatValue( + '0000', + SignalValueFormat.waveform, + 4, + ), + "4'h0", + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.signedDecimal, + 8, + ), + '-1', + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + 'ff', + SignalValueFormat.unsignedDecimal, + 8, + ), + '255', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x0', + SignalValueFormat.unsignedDecimal, + 4, + ), + '0', + ); + }); + + test('uses ROHD radix literals for typed format conversions', () { + expect( + SignalValueFormatRegistry.formatValue( + "8'd255", + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + '1010', + SignalValueFormat.octal, + 4, + ), + '0o12', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x4142', + SignalValueFormat.ascii, + 16, + ), + 'AB', + ); + }); + + test('looks up an occurrence address from the format trie', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.signedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.signedDecimal, + ); + }); + + test('looks up a fallback occurrence address', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.unsignedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatForAny([ + const OccurrenceAddress([7, 8, 9]), + const OccurrenceAddress([0, 2, 4]), + ]), + SignalValueFormat.unsignedDecimal, + ); + }); + + test('stores shared address prefixes once in the format trie', () { + SignalValueFormatRegistry.update([ + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 4]), + SignalValueFormat.unsignedDecimal, + ), + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 5]), + SignalValueFormat.signedDecimal, + ), + ]); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.unsignedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 5])), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 6])), + SignalValueFormat.waveform, + ); + }); + + test('rejects an invalid signal occurrence address', () { + expect( + () => SignalValueFormatRegistry.setFormatFor( + const [OccurrenceAddress([])], + SignalValueFormat.unsignedDecimal, + ), + throwsArgumentError, + ); + expect( + () => SignalValueFormatRegistry.formatFor( + const OccurrenceAddress([0, -1]), + ), + throwsArgumentError, + ); + }); + + test('converts between serialized names and format enum values', () { + expect( + SignalValueFormatRegistry.formatFromString('signedDecimal'), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatToString( + SignalValueFormat.signedDecimal, + ), + 'signedDecimal', + ); + expect(SignalValueFormatRegistry.formatFromString('unknown'), isNull); + }); +} diff --git a/test/nested_array_struct_port_synthesis_test.dart b/test/nested_array_struct_port_synthesis_test.dart index bd3bc8f6d..7c1ff5ee0 100644 --- a/test/nested_array_struct_port_synthesis_test.dart +++ b/test/nested_array_struct_port_synthesis_test.dart @@ -7,6 +7,9 @@ // 2026 August 18 // Author: Max Korbel +@TestOn('vm') +library; + import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart';