diff --git a/README.md b/README.md index a996ccccb..969d8046a 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ You can also open this repository in a GitHub Codespace to run the example in yo - Easy **IP integration** and **interfaces**; using an IP is as easy as an import. Reduces tedious, redundant, and error prone aspects of integration - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer -- Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform capture service** to .vcd file format +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption via **SvService** - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/benchmark/many_submodules_benchmark.dart b/benchmark/many_submodules_benchmark.dart index 763261a4c..277170143 100644 --- a/benchmark/many_submodules_benchmark.dart +++ b/benchmark/many_submodules_benchmark.dart @@ -33,7 +33,7 @@ class ManySubmodulesBenchmark extends AsyncBenchmarkBase { Future run() async { final dut = ManySubmodulesModule(Logic(), numSubModules: 10000); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; } } diff --git a/benchmark/wave_dump_benchmark.dart b/benchmark/wave_dump_benchmark.dart index 777b42eb0..1adfbdbc1 100644 --- a/benchmark/wave_dump_benchmark.dart +++ b/benchmark/wave_dump_benchmark.dart @@ -56,7 +56,7 @@ class WaveDumpBenchmark extends AsyncBenchmarkBase { _mod = _ModuleToDump(Logic(), _clk); await _mod.build(); - WaveDumper(_mod, outputPath: _vcdTemporaryPath); + WaveformService(_mod, outputPath: _vcdTemporaryPath); await Simulator.run(); diff --git a/doc/tutorials/chapter_2/helper.dart b/doc/tutorials/chapter_2/helper.dart index ecd0b5d98..af1dc6eaf 100644 --- a/doc/tutorials/chapter_2/helper.dart +++ b/doc/tutorials/chapter_2/helper.dart @@ -13,7 +13,8 @@ import 'package:rohd/rohd.dart'; Future displaySystemVerilog(Module mod) async { await mod.build(); - print('\nYour System Verilog Equivalent Code: \n ${mod.generateSynth()}'); + print('\nYour System Verilog Equivalent Code: \n' + '${SvService(mod).synthOutput}'); } class LogicInitialization extends Module { diff --git a/doc/tutorials/chapter_3/answers/exercise_sv.dart b/doc/tutorials/chapter_3/answers/exercise_sv.dart index b4ead1f2c..fce71a6fe 100644 --- a/doc/tutorials/chapter_3/answers/exercise_sv.dart +++ b/doc/tutorials/chapter_3/answers/exercise_sv.dart @@ -33,7 +33,7 @@ void main() async { await fSub.build(); // ignore: avoid_print - print(fSub.generateSynth()); + print(SvService(fSub).synthOutput); test('should return 0 when a and b equal 1', () async { a.put(1); diff --git a/doc/tutorials/chapter_3/full_adder.dart b/doc/tutorials/chapter_3/full_adder.dart index 5e798a69d..27f82896c 100644 --- a/doc/tutorials/chapter_3/full_adder.dart +++ b/doc/tutorials/chapter_3/full_adder.dart @@ -77,5 +77,5 @@ void main() async { final mod = FullAdderModule(a, b, cIn, faOps); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); } diff --git a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart index b324d4201..a3c7c8bf2 100644 --- a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart @@ -10,7 +10,7 @@ void main() async { final mod = NBitAdder(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 255 when both inputs are added', () { a.put(127); diff --git a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart index c4d3137db..a2716e6f8 100644 --- a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart @@ -9,7 +9,7 @@ void main() async { final mod = NBitSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 5 when a is 25 and b is 20', () { a.put(25); diff --git a/doc/tutorials/chapter_4/basic_generation_sv.dart b/doc/tutorials/chapter_4/basic_generation_sv.dart index 4c3ff86c0..8e23637b2 100644 --- a/doc/tutorials/chapter_4/basic_generation_sv.dart +++ b/doc/tutorials/chapter_4/basic_generation_sv.dart @@ -78,7 +78,7 @@ void main() async { await nbitAdder.build(); - print(nbitAdder.generateSynth()); + print(SvService(nbitAdder).synthOutput); test('should return 10 when both inputs are 5.', () async { a.put(5); diff --git a/doc/tutorials/chapter_5/answers/full_adder.dart b/doc/tutorials/chapter_5/answers/full_adder.dart index b7b54d5ed..f994a9387 100644 --- a/doc/tutorials/chapter_5/answers/full_adder.dart +++ b/doc/tutorials/chapter_5/answers/full_adder.dart @@ -49,7 +49,7 @@ void main() async { final mod = FullAdder(a: a, b: b, carryIn: cIn); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return true if result sum similar to truth table.', () async { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/full_subtractor.dart b/doc/tutorials/chapter_5/answers/full_subtractor.dart index 162b2d72c..bf4e8acf0 100644 --- a/doc/tutorials/chapter_5/answers/full_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/full_subtractor.dart @@ -44,7 +44,7 @@ Future main() async { await diff.build(); - print(diff.generateSynth()); + print(SvService(diff).synthOutput); test('should return true if results matched truth table', () async { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart index 2f12ab6fe..224999e5e 100644 --- a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart @@ -36,7 +36,7 @@ Future main() async { final mod = NBitFullSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 1 when a is 8 and b is 7.', () { a.put(8); diff --git a/doc/tutorials/chapter_5/n_bit_adder.dart b/doc/tutorials/chapter_5/n_bit_adder.dart index 8a4a61944..9eb0c1a63 100644 --- a/doc/tutorials/chapter_5/n_bit_adder.dart +++ b/doc/tutorials/chapter_5/n_bit_adder.dart @@ -79,7 +79,7 @@ void main() async { await nbitAdder.build(); - // print(nbitAdder.generateSynth()); + // print(SvService(nbitAdder).synthOutput); test('should return 20 when A and B perform add.', () async { a.put(15); diff --git a/doc/tutorials/chapter_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'); diff --git a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart index 15395d7ec..bd63338bf 100644 --- a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart +++ b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart @@ -44,7 +44,7 @@ Future main() async { final dff = DFlipFlop(data, reset, clk); await dff.build(); - print(dff.generateSynth()); + print(SvService(dff).synthOutput); data.inject(1); reset.inject(1); @@ -60,7 +60,7 @@ Future main() async { unawaited(Simulator.run()); - WaveDumper(dff, + WaveformService(dff, outputPath: 'doc/tutorials/chapter_7/answers/d_flip_flop.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_7/shift_register.dart b/doc/tutorials/chapter_7/shift_register.dart index 1dc98c4f1..2324a860b 100644 --- a/doc/tutorials/chapter_7/shift_register.dart +++ b/doc/tutorials/chapter_7/shift_register.dart @@ -69,7 +69,7 @@ void main() async { // kick-off the simulator, but we don't want to wait unawaited(Simulator.run()); - WaveDumper(shiftReg, + WaveformService(shiftReg, outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart index d8315156d..2cbc89916 100644 --- a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart +++ b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart @@ -139,7 +139,7 @@ void main() async { await tb.build(); - print(tb.generateSynth()); + print(SvService(tb).synthOutput); testInterface.cs.inject(0); testInterface.sdi.inject(0); @@ -163,7 +163,7 @@ void main() async { Simulator.setMaxSimTime(100); unawaited(Simulator.run()); - WaveDumper(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); + WaveformService(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); await drive(LogicValue.ofString('01010101')); } diff --git a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart index 6912f1313..26d18cd16 100644 --- a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart +++ b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart @@ -49,13 +49,13 @@ Future main(List args) async { final toyCap = ToyCapsuleFSM(clk, reset, dispenseBtn, coin); await toyCap.build(); - print(toyCap.generateSynth()); + print(SvService(toyCap).synthOutput); toyCap.toyCapsuleStateMachine.generateDiagram(); reset.inject(1); - WaveDumper(toyCap, outputPath: 'toyCapsuleFSM.vcd'); + WaveformService(toyCap, outputPath: 'toyCapsuleFSM.vcd'); Simulator.setMaxSimTime(100); Simulator.registerAction(25, () { diff --git a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart index ef93701b3..2a0350698 100644 --- a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart +++ b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart @@ -34,14 +34,14 @@ void main(List args) async { final pipe = Pipeline4Stages(clk, reset, a); await pipe.build(); - // print(pipe.generateSynth()); + // print(SvService(pipe).synthOutput); a.inject(5); reset.inject(1); Simulator.registerAction(10, () => reset.put(0)); - WaveDumper(pipe, outputPath: 'answer_1.vcd'); + WaveformService(pipe, outputPath: 'answer_1.vcd'); Simulator.registerAction(50, () async { // stage 4 / result: 30 + (30 * 3) = 120 diff --git a/doc/tutorials/chapter_8/carry_save_multiplier.dart b/doc/tutorials/chapter_8/carry_save_multiplier.dart index 82b9521da..783e54f6f 100644 --- a/doc/tutorials/chapter_8/carry_save_multiplier.dart +++ b/doc/tutorials/chapter_8/carry_save_multiplier.dart @@ -108,7 +108,7 @@ void main() async { reset.inject(1); // Attach a waveform dumper so we can see what happens. - WaveDumper(csm, outputPath: 'csm.vcd'); + WaveformService(csm, outputPath: 'csm.vcd'); Simulator.registerAction(10, () { reset.inject(0); diff --git a/doc/tutorials/chapter_8/counter_interface.dart b/doc/tutorials/chapter_8/counter_interface.dart index 41a49eb0e..6d5261620 100644 --- a/doc/tutorials/chapter_8/counter_interface.dart +++ b/doc/tutorials/chapter_8/counter_interface.dart @@ -63,9 +63,9 @@ Future main() async { await counter.build(); - print(counter.generateSynth()); + print(SvService(counter).synthOutput); - WaveDumper(counter, + WaveformService(counter, outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); Simulator.registerAction(25, () { intf.en.put(1); diff --git a/doc/tutorials/chapter_8/oven_fsm.dart b/doc/tutorials/chapter_8/oven_fsm.dart index 172d83006..01f653198 100644 --- a/doc/tutorials/chapter_8/oven_fsm.dart +++ b/doc/tutorials/chapter_8/oven_fsm.dart @@ -192,7 +192,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); + WaveformService(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); } if (!noPrint) { diff --git a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart index 4b1ef9c34..77d2a5a32 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart +++ b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart @@ -315,7 +315,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + WaveformService(tb.counter); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml b/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml index e763ab748..27d99489a 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml +++ b/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml @@ -8,10 +8,14 @@ environment: # Add regular dependencies here. dependencies: - rohd: ^0.4.2 - rohd_vf: ^0.4.1 + rohd: ^0.6.0 + rohd_vf: ^0.6.0 logging: ^1.0.1 +dependency_overrides: + rohd: + path: ../../../../ + dev_dependencies: lints: ^2.0.0 test: ^1.21.0 diff --git a/example/example.dart b/example/example.dart index 2ddbfc738..d2c2ee81d 100644 --- a/example/example.dart +++ b/example/example.dart @@ -61,7 +61,7 @@ Future main({bool noPrint = false}) async { // Let's see what this module looks like as SystemVerilog, so we can pass it // to other tools. - final systemVerilogCode = counter.generateSynth(); + final systemVerilogCode = SvService(counter).synthOutput; if (!noPrint) { print(systemVerilogCode); } @@ -70,7 +70,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(counter); + WaveformService(counter); } // Let's also print a message every time the value on the counter changes, diff --git a/example/fir_filter.dart b/example/fir_filter.dart index 1f17f0d3d..d16168552 100644 --- a/example/fir_filter.dart +++ b/example/fir_filter.dart @@ -96,7 +96,7 @@ Future main({bool noPrint = false}) async { await firFilter.build(); // Generate SystemVerilog code. - final systemVerilogCode = firFilter.generateSynth(); + final systemVerilogCode = SvService(firFilter).synthOutput; if (!noPrint) { // Print SystemVerilog code to console. print(systemVerilogCode); @@ -108,7 +108,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper. if (!noPrint) { - WaveDumper(firFilter); + WaveformService(firFilter); } // Let's set the initial setting. diff --git a/example/logic_array.dart b/example/logic_array.dart index f772c4929..0b149717e 100644 --- a/example/logic_array.dart +++ b/example/logic_array.dart @@ -58,14 +58,14 @@ Future main({bool noPrint = false}) async { // Build the module await logicArrayExample.build(); - final systemVerilogCode = logicArrayExample.generateSynth(); + final systemVerilogCode = SvService(logicArrayExample).synthOutput; if (!noPrint) { print(systemVerilogCode); } // Simulate the module if (!noPrint) { - WaveDumper(logicArrayExample); + WaveformService(logicArrayExample); } // Set the input values diff --git a/example/oven_fsm.dart b/example/oven_fsm.dart index 2788baa55..50c30197e 100644 --- a/example/oven_fsm.dart +++ b/example/oven_fsm.dart @@ -225,7 +225,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'oven.vcd'); + WaveformService(oven, outputPath: 'oven.vcd'); } // Kick off the simulation. diff --git a/example/tree.dart b/example/tree.dart index f5c30a979..12eb86a63 100644 --- a/example/tree.dart +++ b/example/tree.dart @@ -85,7 +85,7 @@ Future main({bool noPrint = false}) async { // Below will generate an output of the ROHD-generated SystemVerilog: await tree.build(); - final generatedSystemVerilog = tree.generateSynth(); + final generatedSystemVerilog = SvService(tree).synthOutput; if (!noPrint) { print(generatedSystemVerilog); } diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..a8a6082fd 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,9 @@ // Copyright (C) 2021-2023 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/module_service.dart'; +export 'src/diagnostics/module_services.dart'; +export 'src/diagnostics/waveform_service.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; @@ -12,6 +15,7 @@ export 'src/signals/signals.dart'; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; +export 'src/synthesizers/systemverilog/sv_service.dart'; export 'src/utilities/naming.dart'; export 'src/values/values.dart'; export 'src/wave_dumper.dart'; diff --git a/lib/src/diagnostics/module_service.dart b/lib/src/diagnostics/module_service.dart new file mode 100644 index 000000000..6e72e1819 --- /dev/null +++ b/lib/src/diagnostics/module_service.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_service.dart +// Common base types shared by all module-scoped services. +// +// 2026 June 23 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// The common contract implemented by every module-scoped service that +/// registers with [ModuleServices]. +/// +/// A service wraps some derived view of a built [Module] (synthesis output, +/// netlist, source trace, waveform, etc.) and exposes a JSON-serialisable +/// summary via [toJson]. Concrete services additionally expose their own +/// format-specific accessors; consumers reach them through +/// [ModuleServices.lookup] or the service's own `current` accessor rather than +/// through getters on the registry. +abstract interface class ModuleService { + /// The top-level [Module] this service operates on. + Module get module; + + /// A JSON-serialisable summary of this service. + Map toJson(); +} + +/// A [ModuleService] that emits output to one or more files. +/// +/// Establishes the common output convention shared by synthesis, netlist, +/// trace, and waveform services: +/// - [outputPath] — the default file or directory written by [write]. +/// - [multiFile] — whether [write] emits one file per module definition +/// (a directory) or a single combined file. +/// - [write] — performs the write, honouring [multiFile]. +abstract class OutputService implements ModuleService { + /// The default location written by [write]. + /// + /// Interpreted as a directory when [multiFile] is `true`, otherwise as a + /// single file path. May be `null` when no default has been configured, in + /// which case a path must be passed to [write]. + String? get outputPath; + + /// Whether [write] emits one file per module definition (`true`) or a single + /// combined file (`false`). + bool get multiFile; + + /// Writes this service's output to [path], or to [outputPath] when [path] is + /// omitted. + void write([String? path]); +} + +/// An [OutputService] that generates source-code text, keyed per module +/// definition. +/// +/// Shared by the language code-generation services (e.g. SystemVerilog and +/// SystemC), which all produce a combined single-file [output] as well as +/// per-definition contents. +abstract class CodegenService extends OutputService { + /// The combined single-file generated output (including any header). + String get output; + + /// The generated output keyed by module definition name + /// ([Module.definitionName]). + Map get contentsByDefinitionName; + + /// The generated output for a single module [definitionName], or `null` when + /// that definition was not generated. + String? moduleOutput(String definitionName) => + contentsByDefinitionName[definitionName]; +} diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..98f2341f7 --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,73 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services.dart +// Slim, type-keyed registry of module-scoped services for DevTools and other +// inspection tools. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; + +/// A slim, type-keyed registry of [ModuleService]s. +/// +/// Services register themselves here on construction (keyed by their concrete +/// type) and are retrieved with [lookup]. The registry intentionally exposes +/// no per-format accessors: each service owns its own JSON and output methods, +/// reached through [lookup] or the service's own static `current` accessor. +/// +/// The registry references no specific service type, so it is identical across +/// all feature branches that contribute services. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJSON] — set by [Module.build]. +class ModuleServices { + ModuleServices._(); + + /// The singleton instance. + static final ModuleServices instance = ModuleServices._(); + + // ─── Hierarchy (auto-registered by Module.build) ────────────── + + /// The most recently built top-level [Module]. + /// + /// Set automatically at the end of [Module.build]. + Module? rootModule; + + /// Returns the module hierarchy as a JSON string. + /// + /// DevTools evaluates this via `EvalOnDartLibrary` to display the module + /// hierarchy. Richer design views (e.g. a slim netlist) are composed by the + /// DevTools client from the relevant registered service. + String get hierarchyJSON { + ModuleTree.rootModuleInstance = rootModule; + return ModuleTree.instance.hierarchyJSON; + } + + // ─── Type-keyed service registry ────────────────────────────── + + final Map _services = {}; + + /// Registers [service] under the type argument [T]. + /// + /// Replaces any previously registered service of the same type. + void register(T service) { + _services[T] = service; + } + + /// Returns the registered service of type [T], or `null` if none. + T? lookup() => _services[T] as T?; + + /// Removes the registered service of type [T], if any. + void unregister() { + _services.remove(T); + } + + /// Resets all services. Intended for test teardown. + void reset() { + rootModule = null; + _services.clear(); + } +} diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart new file mode 100644 index 000000000..33eb83d6e --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,428 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_service.dart +// Base waveform service: file output with filtering, timescale, and +// flush/overwrite control. Designed to be subclassed by the DevTools +// streaming variant. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:collection'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +// ─── Supporting types ──────────────────────────────────────────────────────── + +/// The output format for waveform capture. +enum WaveOutputFormat { + /// Value Change Dump — the classic text-based waveform format. + vcd, + + /// Fast Signal Trace — a compact binary format. + /// + /// Requires an FST writer to be available; see the DevTools subclass for + /// a fully FST-backed implementation. + fst, +} + +/// Policy applied when the output file already exists at construction time. +enum OverwritePolicy { + /// Silently overwrite any existing file. + overwrite, + + /// Throw a [FileSystemException] if the file already exists. + failIfExists, +} + +// ─── Service ───────────────────────────────────────────────────────────────── + +/// A waveform capture service that writes signal changes to a file. +/// +/// This is the base class for waveform capture. It handles: +/// - Signal collection (with optional [signalFilter]) +/// - VCD file output with configurable [timescale] +/// - Selective recording via [startTime] / [stopTime] +/// - Periodic buffer flushing and [overwritePolicy] +/// - Optional registration with [ModuleServices] +/// +/// **Subclassing for DevTools streaming:** +/// +/// Override the protected hooks below to intercept the simulation event loop +/// without re-implementing the file-writing logic: +/// +/// - [onSignalCollected] — called once per tracked signal at startup; use +/// it to register signals in a VM-service index. +/// - [onValueChange] — called for every value-change event within the +/// [startTime]/[stopTime] window; use it to feed an in-memory store for +/// streaming. +/// - [onTimestampCapture] — called once per simulation timestamp that +/// contains at least one change; the full changed-signal set is passed. +/// - [onSimulationEnd] — called after the final timestamp is written and +/// the file is closed; use it to finalise any streaming buffers. +/// +/// Example subclass skeleton: +/// ```dart +/// class DevToolsWaveformService extends WaveformService { +/// DevToolsWaveformService(super.module, {super.outputPath}); +/// +/// @override +/// void onSignalCollected(Logic signal) { +/// super.onSignalCollected(signal); +/// _registerWithVmService(signal); +/// } +/// +/// @override +/// void onValueChange(Logic signal, int timestamp) { +/// super.onValueChange(signal, timestamp); +/// _recordInMemory(signal, timestamp); +/// } +/// } +/// ``` +class WaveformService implements ModuleService { + /// The most recently registered [WaveformService], or `null`. + static WaveformService? current; + + /// The top-level [Module] being captured. + @override + final Module module; + + /// Path of the output waveform file. + /// + /// The parent directory is created if necessary. + final String outputPath; + + /// Output format. + final WaveOutputFormat format; + + /// Optional predicate that determines whether a given [Logic] signal is + /// captured. + /// + /// When `null`, all non-[Const] signals in the hierarchy are captured, + /// matching the legacy waveform dumper behaviour. + final bool Function(Logic signal)? signalFilter; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Simulation time at which recording begins. + /// + /// Signals are still collected before this time so they appear in the scope + /// definition, but value-change events are suppressed until [startTime] is + /// reached. `null` means "from the very start". + final int? startTime; + + /// Simulation time at which recording ends. + /// + /// Value-change events after this time are suppressed. `null` means "until + /// end of simulation". + final int? stopTime; + + /// Number of characters accumulated in the write buffer before it is flushed + /// to disk. + final int flushBufferSize; + + /// What to do when the output file already exists. + final OverwritePolicy overwritePolicy; + + /// Whether to register this service with [ModuleServices] for inspection. + final bool register; + + /// Whether to enable DevTools streaming. + /// + /// The base [WaveformService] stores this flag but takes no action on it. + /// The DevTools subclass uses it to conditionally register extensions. + final bool enableDevToolsStreaming; + + // ─── Internal file-writing state ───────────────────────────── + + /// The output file. + late final File _outputFile; + + /// Sink writing into [_outputFile]. + late final IOSink _outFileSink; + + /// Write buffer; flushed when it exceeds [flushBufferSize]. + final StringBuffer _fileBuffer = StringBuffer(); + + /// Counter for assigning compact signal markers in the VCD. + int _signalMarkerIdx = 0; + + /// Maps each captured [Logic] to its VCD marker string. + final Map _signalToMarkerMap = {}; + + /// Signals that changed during the current simulation timestamp. + final Set _changedThisTimestamp = HashSet(); + + /// The timestamp currently being accumulated. + int _currentDumpingTimestamp = Simulator.time; + + // ─── Constructor ───────────────────────────────────────────── + + /// Creates a [WaveformService] for [module]. + /// + /// [module] must be built before construction. + /// + /// Use the optional constructor parameters to configure format, path, + /// filtering, timescale, start/stop times, flush size, and overwrite policy. + WaveformService( + this.module, { + this.outputPath = 'waves.vcd', + this.format = WaveOutputFormat.vcd, + this.signalFilter, + this.timescale = '1ps', + this.startTime, + this.stopTime, + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + this.register = true, + this.enableDevToolsStreaming = false, + }) { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating WaveformService. ' + 'Call build() first.'); + } + + if (overwritePolicy == OverwritePolicy.failIfExists) { + final f = File(outputPath); + if (f.existsSync()) { + throw FileSystemException( + 'Waveform output file already exists and overwritePolicy is ' + 'failIfExists.', + outputPath); + } + } + + _outputFile = File(outputPath)..createSync(recursive: true); + _outFileSink = _outputFile.openWrite(); + + _collectSignals(); + _writeHeader(); + _writeScope(); + + Simulator.preTick.listen((_) { + if (Simulator.time != _currentDumpingTimestamp) { + if (_changedThisTimestamp.isNotEmpty) { + _captureTimestamp(_currentDumpingTimestamp); + } + _currentDumpingTimestamp = Simulator.time; + } + }); + + Simulator.registerEndOfSimulationAction(() async { + _captureTimestamp(Simulator.time); + await _terminate(); + onSimulationEnd(); + }); + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + // ─── Extensibility hooks ────────────────────────────────────── + + /// Called once for each [Logic] signal that passes + /// [signalFilter] during initial signal collection. + /// + /// Override in a subclass to register signals with an in-memory store, + /// VM service index, or FST handle map. Always call `super` first. + @protected + void onSignalCollected(Logic signal) {} + + /// Called for every value-change event on [signal] at [timestamp]. + /// + /// Only called within the [startTime] / [stopTime] window. + /// + /// Override in a subclass to feed an in-memory waveform store or + /// streaming buffer. Always call `super` first. + @protected + void onValueChange(Logic signal, int timestamp) {} + + /// Called once per simulation timestamp that contains at least one change, + /// after all value-change events for that timestamp have been processed. + /// + /// [changed] is the set of signals that changed at [timestamp]. + /// + /// Override in a subclass to flush incremental streaming payloads. + /// Always call `super` first. + @protected + void onTimestampCapture(int timestamp, Set changed) {} + + /// Called after the final timestamp has been written and the file is closed. + /// + /// Override in a subclass to finalise any streaming buffers or emit + /// end-of-simulation notifications. + @protected + void onSimulationEnd() {} + + // ─── Internal signal collection ────────────────────────────── + + void _collectSignals() { + final modulesToParse = [module]; + for (var i = 0; i < modulesToParse.length; i++) { + final m = modulesToParse[i]; + for (final sig in m.signals) { + if (sig is Const) { + continue; + } + if (signalFilter != null && !signalFilter!(sig)) { + continue; + } + + _signalToMarkerMap[sig] = 's${_signalMarkerIdx++}'; + onSignalCollected(sig); + + sig.changed.listen((_) { + _changedThisTimestamp.add(sig); + }); + } + + for (final subm in m.subModules) { + if (subm is InlineSystemVerilog) { + continue; + } + modulesToParse.add(subm); + } + } + } + + // ─── VCD output helpers ─────────────────────────────────────── + + void _writeHeader() { + final header = ''' +\$date + ${Timestamper.stamp()} +\$end +\$version + ROHD v${Config.version} +\$end +\$comment + Generated by ROHD - www.github.com/intel/rohd +\$end +\$timescale $timescale \$end +'''; + _writeToBuffer(header); + } + + void _writeScope() { + var scopeString = _computeScopeString(module); + scopeString += '\$enddefinitions \$end\n'; + scopeString += '\$dumpvars\n'; + _writeToBuffer(scopeString); + _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + _writeToBuffer('\$end\n'); + } + + String _computeScopeString(Module m, {int indent = 0}) { + final moduleSignalUniquifier = Uniquifier(); + final padding = List.filled(indent, ' ').join(); + var scopeString = '$padding\$scope module ${m.uniqueInstanceName} \$end\n'; + final innerScopeString = StringBuffer(); + + for (final sig in m.signals) { + if (!_signalToMarkerMap.containsKey(sig)) { + continue; + } + final width = sig.width; + final marker = _signalToMarkerMap[sig]; + var signalName = Sanitizer.sanitizeSV(sig.name); + signalName = moduleSignalUniquifier.getUniqueName( + initialName: signalName, reserved: sig.isPort); + innerScopeString + .write(' $padding\$var wire $width $marker $signalName \$end\n'); + } + for (final subModule in m.subModules) { + innerScopeString + .write(_computeScopeString(subModule, indent: indent + 1)); + } + if (innerScopeString.isEmpty) { + return ''; + } + + scopeString += innerScopeString.toString(); + scopeString += '$padding\$upscope \$end\n'; + return scopeString; + } + + bool _isInRecordingWindow(int timestamp) { + if (startTime != null && timestamp < startTime!) { + return false; + } + if (stopTime != null && timestamp > stopTime!) { + return false; + } + return true; + } + + void _captureTimestamp(int timestamp) { + if (!_isInRecordingWindow(timestamp)) { + _changedThisTimestamp.clear(); + return; + } + + _writeToBuffer('#$timestamp\n'); + + final snapshot = Set.of(_changedThisTimestamp); + for (final sig in snapshot) { + _writeSignalValueUpdate(sig); + onValueChange(sig, timestamp); + } + _changedThisTimestamp.clear(); + + onTimestampCapture(timestamp, snapshot); + } + + void _writeSignalValueUpdate(Logic signal) { + final binaryValue = signal.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); + final updateValue = signal.width > 1 + ? 'b$binaryValue ' + : signal.value.toString(includeWidth: false); + final marker = _signalToMarkerMap[signal]; + _writeToBuffer('$updateValue$marker\n'); + } + + // ─── Buffered I/O ───────────────────────────────────────────── + + void _writeToBuffer(String contents) { + _fileBuffer.write(contents); + if (_fileBuffer.length > flushBufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + _outFileSink.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } + + Future _terminate() async { + _flushBuffer(); + await _outFileSink.flush(); + await _outFileSink.close(); + } + + // ─── Inspection ─────────────────────────────────────────────── + + /// Returns a JSON-serialisable summary of this service. + @override + Map toJson() => { + 'outputPath': outputPath, + 'format': format.name, + 'signalCount': _signalToMarkerMap.length, + 'timescale': timescale, + if (startTime != null) 'startTime': startTime!, + if (stopTime != null) 'stopTime': stopTime!, + }; +} diff --git a/lib/src/exceptions/logic/put_exception.dart b/lib/src/exceptions/logic/put_exception.dart index 36d8f8015..96bd51602 100644 --- a/lib/src/exceptions/logic/put_exception.dart +++ b/lib/src/exceptions/logic/put_exception.dart @@ -9,10 +9,11 @@ import 'package:rohd/rohd.dart'; -/// An exception that thrown when a [Logic] signal fails to `put`. +/// An exception that thrown when a [Logic] signal fails to [Logic.put]. class PutException extends RohdException { - /// Creates an exception for when a `put` fails on a `Logic` with [context] as - /// to where the + /// Creates an exception for when a [Logic.put] fails on a [Logic] with + /// [context] as to where the failure occurred and [message] describing the + /// failure. PutException(String context, String message) : super('Failed to put value on signal ($context): $message'); } diff --git a/lib/src/module.dart b/lib/src/module.dart index 92fc410e0..d5cf5edb5 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,13 +11,10 @@ import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; - import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; -import 'package:rohd/src/diagnostics/inspector_service.dart'; -import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a synthesizable hardware entity with clearly defined interface @@ -52,6 +49,18 @@ abstract class Module { /// An internal mapping of input names to their sources to this [Module]. late final Map _inputSources = {}; + // ─── Central naming (Namer) ───────────────────────────────────── + + /// Central namer that owns both the signal and instance namespaces. + /// Initialized lazily on first access (after build). + @internal + late final Namer namer = _createNamer(); + + Namer _createNamer() { + assert(hasBuilt, 'Module must be built before canonical names are bound.'); + return Namer.forModule(this); + } + /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; @@ -202,6 +211,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; @@ -317,7 +338,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. @@ -1113,23 +1134,16 @@ abstract class Module { /// /// Currently returns one long file in SystemVerilog, but in the future /// may have other output formats, languages, files, etc. + /// + /// For richer access to per-module file contents, named maps, and individual + /// file writing, see [SvService] (and [SvService.synthOutput] for the + /// equivalent one-shot string). String generateSynth() { if (!_hasBuilt) { throw ModuleNotBuiltException(this); } - final synthHeader = ''' -/** - * Generated by ROHD - www.github.com/intel/rohd - * Generation time: ${Timestamper.stamp()} - * ROHD Version: ${Config.version} - */ - -'''; - return synthHeader + - SynthBuilder(this, SystemVerilogSynthesizer()) - .getSynthFileContents() - .join('\n\n////////////////////\n\n'); + return SvService(this, register: false).synthOutput; } } diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 54e312ab3..f9d0a0d08 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_builder.dart diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index b70c9338e..687bbab03 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesizer.dart @@ -6,7 +6,6 @@ // // 2021 August 26 // Author: Max Korbel -// import 'package:rohd/rohd.dart'; diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart new file mode 100644 index 000000000..97cf0c45e --- /dev/null +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sv_service.dart +// Service wrapper for SystemVerilog synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; + +/// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. +/// +/// Provides access to the generated SV file contents and per-module +/// synthesis results, and optionally registers with [ModuleServices] +/// for DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final sv = SvService(dut); +/// +/// // Write individual .sv files: +/// sv.writeFiles('build/'); +/// +/// // Or get the concatenated output (like generateSynth): +/// print(sv.allContents); +/// ``` +class SvService extends CodegenService { + /// The separator inserted between module definitions in the + /// concatenated single-file output from [allContents]. + /// + /// Matches the format historically produced by `Module.generateSynth()`. + static const moduleSeparator = '\n\n////////////////////\n\n'; + + /// The most recently registered [SvService], or `null`. + static SvService? current; + + /// The top-level [Module] being synthesized. + @override + final Module module; + + /// The default location written by [write]. + /// + /// A directory when [multiFile] is `true`, otherwise a single file path. + @override + final String? outputPath; + + /// Whether [write] emits one `.sv` file per module definition (`true`) or a + /// single concatenated file (`false`). + @override + final bool multiFile; + + /// The underlying [SynthBuilder] that drove synthesis. + late final SynthBuilder synthBuilder; + + /// The generated file contents (one per unique module definition). + late final List fileContents; + + /// Creates an [SvService] for [module]. + /// + /// [module] must already be built. + /// + /// If [outputPath] is provided, output is written immediately: a directory + /// of per-module files when [multiFile] is `true`, otherwise the + /// concatenated SV output (with header) to that single file. + SvService(this.module, + {bool register = true, this.outputPath, this.multiFile = false}) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating SvService. ' + 'Call build() first.', + ); + } + + synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); + fileContents = synthBuilder.getSynthFileContents(); + + if (outputPath != null) { + write(); + } + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + /// All [SynthesisResult]s produced by synthesis. + Set get synthesisResults => synthBuilder.synthesisResults; + + /// Returns the concatenated SystemVerilog module definitions as a single + /// string, without the generation header. + /// + /// For the full output with header (matching `Module.generateSynth()`), + /// use [synthOutput]. + String get allContents => + fileContents.map((fc) => fc.contents).join(moduleSeparator); + + /// The ROHD generation header prepended to single-file output. + String get synthHeader => ''' +/** + * Generated by ROHD - www.github.com/intel/rohd + * Generation time: ${Timestamper.stamp()} + * ROHD Version: ${Config.version} + */ + +'''; + + /// Returns the full single-file SystemVerilog output with header, + /// identical to `Module.generateSynth()`. + /// + /// Computed once and cached so the timestamped header is stable for the + /// lifetime of this service. + late final String synthOutput = synthHeader + allContents; + + /// The combined single-file generated output (alias for [synthOutput]). + @override + String get output => synthOutput; + + /// Returns a map from module definition name to its SV file contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + + /// Returns a map from module definition name + /// ([Module.definitionName]) to its SV file contents. + /// + /// This uses the original definition name (not uniquified), matching + /// the keys used by FLC trace data. + @override + Map get contentsByDefinitionName { + final result = {}; + for (final sr in synthesisResults) { + final defName = sr.module.definitionName; + final instanceName = sr.instanceTypeName; + // Find the file content matching this instance type name. + final fc = fileContents.firstWhereOrNull((f) => f.name == instanceName); + if (fc != null) { + result[defName] = fc.contents; + } + } + return result; + } + + /// Writes each module's SV to a separate file in [directory]. + /// + /// Files are named `.sv`. + void writeFiles(String directory) { + final dir = Directory(directory)..createSync(recursive: true); + for (final fc in fileContents) { + File('${dir.path}/${fc.name}.sv').writeAsStringSync(fc.contents); + } + } + + /// Writes the SV output to [path], or to [outputPath] when [path] is omitted. + /// + /// When [multiFile] is `true`, writes one `.sv` file per module definition + /// into the target directory (see [writeFiles]); otherwise writes the + /// concatenated [synthOutput] to the target file. + @override + void write([String? path]) { + final target = path ?? outputPath; + if (target == null) { + throw ArgumentError( + 'No output path provided: pass a path to write() or set outputPath.', + ); + } + if (multiFile) { + writeFiles(target); + } else { + File(target) + ..parent.createSync(recursive: true) + ..writeAsStringSync(synthOutput); + } + } + + /// Returns a JSON-serialisable summary of the SV synthesis. + /// + /// Contains the list of generated module definition names. + @override + Map toJson() => { + 'modules': [for (final fc in fileContents) fc.name], + }; +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog.dart b/lib/src/synthesizers/systemverilog/systemverilog.dart index 281b05df9..e5f772e44 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,5 +1,6 @@ // Copyright (C) 2021-2024 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'sv_service.dart'; export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 3c82c4a58..4d15ff5fa 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -18,8 +18,8 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { @override void process() { - _replaceNetConnections(); - _collapseChainableModules(); + _buildNetConnectsForNaming(pickName: true); + _collapseMarkedChainableModules(); _replaceInOutConnectionInlineableModules(); } @@ -30,16 +30,20 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// Creates a new [_NetConnect] module to synthesize assignment between two /// [LogicNet]s. SystemVerilogSynthSubModuleInstantiation _addNetConnect( - SynthLogic dst, SynthLogic src) { + SynthLogic dst, + SynthLogic src, { + bool pickName = false, + }) { // 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) - // map inouts to the appropriate `_SynthLogic`s ..setInOutMapping(_NetConnect.n0Name, dst) ..setInOutMapping(_NetConnect.n1Name, src); @@ -47,19 +51,25 @@ 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) { 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); + _addNetConnect(assignment.dst, assignment.src, pickName: pickName); } else { reducedAssignments.add(assignment); } @@ -73,8 +83,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,97 +92,10 @@ 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) { - (subModuleInstantiation.module as InlineSystemVerilog).resultSignalName; - + for (final subModuleInstantiation in chainableModulesToCollapse + .cast()) { // inlineable modules have only 1 result signal final resultSynthLogic = subModuleInstantiation.inlineResultLogic!; @@ -189,8 +112,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { for (final subModuleInstantiation in subModuleInstantiations) { subModuleInstantiation as SystemVerilogSynthSubModuleInstantiation; - subModuleInstantiation.synthLogicToInlineableSynthSubmoduleMap = - synthLogicToInlineableSynthSubmoduleMap; + subModuleInstantiation.synthLogicToInlineableSynthSubmoduleMap = { + ...?subModuleInstantiation.synthLogicToInlineableSynthSubmoduleMap, + ...synthLogicToInlineableSynthSubmoduleMap, + }; } } @@ -199,11 +124,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 @@ -225,8 +151,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; @@ -260,19 +189,21 @@ class _NetConnect extends Module with SystemVerilog { _NetConnect(LogicNet n0, LogicNet n1) : assert(n0.width == n1.width, 'Widths must be equal.'), width = n0.width, - super( - definitionName: _definitionName, - name: _definitionName, - ) { + 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' diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..062647ac3 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synthesizer.dart diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index c3026a0d5..8fcbc014a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -11,8 +11,8 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a logic signal in the generated code within a module. @internal @@ -212,92 +212,25 @@ class SynthLogic { /// The name of this, if it has been picked. String? _name; - /// Picks a [name]. + /// Picks a [name] using the module's signal namer. /// /// Must be called exactly once. - void pickName(Uniquifier uniquifier) { + void pickName() { assert(_name == null, 'Should only pick a name once.'); - _name = _findName(uniquifier); + _name = _findName(); } /// Finds the best name from the collection of [Logic]s. - String _findName(Uniquifier uniquifier) { - // check for const - if (_constLogic != null) { - if (!_constNameDisallowed) { - return _constLogic!.value.toString(); - } else { - assert( - logics.length > 1, - 'If there is a constant, but the const name is not allowed, ' - 'there needs to be another option', - ); - } - } - - // check for reserved - if (_reservedLogic != null) { - return uniquifier.getUniqueName( - initialName: _reservedLogic!.name, - reserved: true, - ); - } - - // check for renameable - if (_renameableLogic != null) { - return uniquifier.getUniqueName( - initialName: _renameableLogic!.preferredSynthName, - ); - } - - // pick a preferred, available, mergeable name, if one exists - final unpreferredMergeableLogics = []; - final uniquifiableMergeableLogics = []; - for (final mergeableLogic in _mergeableLogics) { - if (Naming.isUnpreferred(mergeableLogic.preferredSynthName)) { - unpreferredMergeableLogics.add(mergeableLogic); - } else if (!uniquifier.isAvailable(mergeableLogic.preferredSynthName)) { - uniquifiableMergeableLogics.add(mergeableLogic); - } else { - return uniquifier.getUniqueName( - initialName: mergeableLogic.preferredSynthName, - ); - } - } - - // uniquify a preferred, mergeable name, if one exists - if (uniquifiableMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: uniquifiableMergeableLogics.first.preferredSynthName, - ); - } - - // pick an available unpreferred mergeable name, if one exists, otherwise - // uniquify an unpreferred mergeable name - if (unpreferredMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: unpreferredMergeableLogics - .firstWhereOrNull( - (element) => - uniquifier.isAvailable(element.preferredSynthName), - ) - ?.preferredSynthName ?? - unpreferredMergeableLogics.first.preferredSynthName, + /// + /// Delegates to signal namer which handles constant value naming, priority + /// selection, and uniquification via the module's shared namespace. + String _findName() => + parentSynthModuleDefinition.module.namer.signalNameOfBest( + logics, + constValue: _constLogic, + constNameDisallowed: _constNameDisallowed, ); - } - - // pick anything (unnamed) and uniquify as necessary (considering preferred) - // no need to prefer an available one here, since it's all unnamed - return uniquifier.getUniqueName( - initialName: _unnamedLogics - .firstWhereOrNull( - (element) => !Naming.isUnpreferred(element.preferredSynthName), - ) - ?.preferredSynthName ?? - _unnamedLogics.first.preferredSynthName, - ); - } /// Creates an instance to represent [initialLogic] and any that merge /// into it. @@ -404,7 +337,7 @@ class SynthLogic { @override String toString() => '${_name == null ? 'null' : '"$name"'}, ' - 'logics contained: ${logics.map((e) => e.preferredSynthName).toList()}'; + 'logics contained: ${logics.map(Namer.baseName).toList()}'; /// Provides a definition for a range in SV from a width. static String _widthToRangeDef(int width, {bool forceRange = false}) { @@ -551,17 +484,3 @@ class SynthLogicArrayElement extends SynthLogic { ' parentArray=($parentArray), element ${logic.arrayIndex}, logic: $logic' ' logics contained: ${logics.map((e) => e.name).toList()}'; } - -extension on Logic { - /// Returns the preferred name for this [Logic] while generating in the synth - /// stack. - String get preferredSynthName => naming == Naming.reserved - // if reserved, keep the exact name - ? name - : isArrayMember - // arrays nicely name their elements already - ? name - // sanitize to remove any `.` in struct names - // the base `name` will be returned if not a structure. - : Sanitizer.sanitizeSV(structureName); -} diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 37ebfb323..f1f7e890e 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -14,22 +14,29 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; +import 'package:rohd/src/utilities/namer.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. class _BusSubsetForStructSlice extends BusSubset { + 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. @@ -75,6 +82,17 @@ class SynthModuleDefinition { Iterable get subModuleInstantiations => moduleToSubModuleInstantiationMap.values; + /// Chainable inline modules that should claim names after emitted objects. + @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 = {}; + /// Indicates that [m] is a submodule used within this definition. /// /// This is only valid to call after all the submodules have been detected. @@ -110,31 +128,17 @@ 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 bool logicHasPresentSynthLogic(Logic logic) { final synthLogic = logicToSynthMap[logic]; - if (synthLogic == null) { - return false; - } else if (synthLogic.declarationCleared) { - return false; - } else if (synthLogic.isStructPortElement()) { - return true; - } else { - return true; - } + return synthLogic != null && !synthLogic.declarationCleared; } /// Either accesses a previously created [SynthLogic] corresponding to /// [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 || @@ -244,8 +248,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; } } @@ -266,9 +276,12 @@ class SynthModuleDefinition { // this is DISCONNECTED, just a module used for synthesizing final subsetMod = _BusSubsetForStructSlice( (port.isNet ? LogicNet.new : Logic.new)( - width: port.width, name: 'DUMMY'), + width: port.width, + name: 'DUMMY', + ), idx, idx + leafElement.width - 1, + destination: leafElement, ); final ssmi = getSynthSubModuleInstantiation(subsetMod); @@ -289,19 +302,12 @@ 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( - !(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) @@ -340,8 +346,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 @@ -374,9 +381,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)) { @@ -399,10 +407,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 || @@ -410,10 +420,7 @@ class SynthModuleDefinition { srcConnection.parentModule!.parent == module)) { final netSynthDriver = getSynthLogic(srcConnection)!; - assignments.add(SynthAssignment( - netSynthDriver, - synthReceiver, - )); + assignments.add(SynthAssignment(netSynthDriver, synthReceiver)); } } } @@ -442,10 +449,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); } @@ -456,8 +464,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); @@ -465,14 +474,16 @@ class SynthModuleDefinition { final receiverIsSubModuleOutput = receiver.isOutput && (receiver.parentModule?.parent == module); + if (receiverIsSubModuleOutput) { final subModule = receiver.parentModule!; // 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 @@ -503,8 +514,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); } } } @@ -513,9 +525,125 @@ class SynthModuleDefinition { _collapseArrays(); _collapseAssignments(); _assignSubmodulePortMapping(); + _pruneUnused(); - process(); + + // 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 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()); + _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; + final expressionlessInputs = subModule is SystemVerilog + ? subModule.expressionlessInputs + // ignore: deprecated_member_use_from_same_package + : subModule is CustomSystemVerilog + ? subModule.expressionlessInputs + : const []; + + if (expressionlessInputs.isNotEmpty) { + singleUseSignals.removeAll( + 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, @@ -576,8 +704,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 { @@ -589,26 +718,31 @@ class SynthModuleDefinition { continue; } - final isCustomSvModPort = logics.any((logic) => - logic.isPort && - isSubmoduleAndPresent(logic.parentModule) && - ((logic.parentModule! is SystemVerilog && - !(logic.parentModule! as SystemVerilog) - .acceptsEmptyPortConnections) || - // ignore: deprecated_member_use_from_same_package - logic.parentModule! is CustomSystemVerilog)); + final isCustomSvModPort = logics.any( + (logic) => + logic.isPort && + isSubmoduleAndPresent(logic.parentModule) && + ((logic.parentModule! is SystemVerilog && + !(logic.parentModule! as SystemVerilog) + .acceptsEmptyPortConnections) || + // ignore: deprecated_member_use_from_same_package + logic.parentModule! is CustomSystemVerilog), + ); if (!isCustomSvModPort) { if (internalSignal.isNet) { final anyInternalConnections = [ ...internalSignal.srcConnections, - ...internalSignal.dstConnections + ...internalSignal.dstConnections, ] - .where((e) => - (e.parentModule == module || - ( // in case of sub-module output driving a net - e.parentModule?.parent == module && e.isOutput)) && - logicHasPresentSynthLogic(e)) + .where( + (e) => + (e.parentModule == module || + ( // in case of sub-module output driving a net + e.parentModule?.parent == module && + e.isOutput)) && + logicHasPresentSynthLogic(e), + ) .isNotEmpty; if (anyInternalConnections) { @@ -619,9 +753,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) { @@ -632,13 +768,15 @@ class SynthModuleDefinition { // If the signal appears in multiple inout port mappings on the // same (single) connected submodule, it's a loopback and needs // a wire declaration so both ports can reference it by name. - final hasInOutLoopback = connectedSubModules.any((m) => - getSynthSubModuleInstantiation(m) - .inOutMapping - .values - .where((v) => v == internalSignal) - .length > - 1); + final hasInOutLoopback = connectedSubModules.any( + (m) => + getSynthSubModuleInstantiation(m) + .inOutMapping + .values + .where((v) => v == internalSignal) + .length > + 1, + ); if (hasInOutLoopback) { reducedInternalSignals.add(internalSignal); @@ -696,39 +834,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; @@ -746,73 +889,142 @@ class SynthModuleDefinition { for (final inputName in submoduleInstantiation.module.inputs.keys) { final orig = submoduleInstantiation.inputMapping[inputName]!; submoduleInstantiation.setInputMapping( - inputName, orig.replacement ?? orig, - replace: true); + inputName, + orig.replacement ?? orig, + replace: true, + ); } for (final outputName in submoduleInstantiation.module.outputs.keys) { final orig = submoduleInstantiation.outputMapping[outputName]!; submoduleInstantiation.setOutputMapping( - outputName, orig.replacement ?? orig, - replace: true); + outputName, + orig.replacement ?? orig, + replace: true, + ); } for (final inOutName in submoduleInstantiation.module.inOuts.keys) { final orig = submoduleInstantiation.inOutMapping[inOutName]!; submoduleInstantiation.setInOutMapping( - inOutName, orig.replacement ?? orig, - replace: true); + inOutName, + orig.replacement ?? orig, + replace: true, + ); } } } /// Picks names of signals and sub-modules. + /// + /// Signal names are 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: + // 1. Ports (reserved by _initNamespace, claimed via signalName) + // 2. Reserved submodule instances + // 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(_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); - assert(submodule.module.name == submodule.name, - 'Expect reserved names to retain their name.'); - } else { - nonReservedSubmodules.add(submodule); + submodule.pickName(module); + assert( + submodule.module.name == submodule.name, + 'Expect reserved names to retain their name.', + ); } } - // then *reserved* internal signals get priority + // Reserved internal signals next. final nonReservedSignals = []; - for (final signal in internalSignals) { - if (signal.isReserved) { - signal.pickName(_synthInstantiationNameUniquifier); + 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 submodule instances - for (final submodule in nonReservedSubmodules - .where((element) => element.needsInstantiation)) { - submodule.pickName(_synthInstantiationNameUniquifier); + // Then non-reserved submodule instances with strong name claims. + final weakSubmodules = []; + for (final submodule in subModuleInstantiations) { + 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 with strong name claims. + for (final signal in _signalsInModuleOrder(nonReservedSignals)) { + signal.pickName(); } - // then the rest of the internal signals - for (final signal in nonReservedSignals) { - signal.pickName(_synthInstantiationNameUniquifier); + // 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) { + 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() { @@ -852,9 +1064,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 = @@ -909,8 +1122,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 +1137,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 +1180,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 +1215,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 +1225,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 80a415a09..cb6ee9f89 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_sub_module_instantiation.dart @@ -11,7 +11,7 @@ import 'dart:collection'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; +import 'package:rohd/src/utilities/namer.dart'; /// Represents an instantiation of a module within another module. class SynthSubModuleInstantiation { @@ -25,66 +25,88 @@ class SynthSubModuleInstantiation { String get name => _name!; /// Selects a name for this module instance. Must be called exactly once. - void pickName(Uniquifier uniquifier) { + /// + /// Names are allocated from [parentModule]'s [Namer]'s shared namespace + /// via [Namer.instanceNameOf], which memoizes by [Module.instanceNameKey] so + /// the same instance receives an identical canonical name across repeated + /// synthesis passes. + void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = uniquifier.getUniqueName( - initialName: module.uniqueInstanceName, - reserved: module.reserveName, - nullStarter: 'm', - ); + _name = parentModule.namer.instanceNameOf(module); } /// 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( + 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.'); + (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( - (replace && _inOutMapping.containsKey(name)) || - !_inOutMapping.containsKey(name), - 'A mapping already exists to this output: $name.'); + 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.', + ); _inOutMapping[name] = synthLogic; } diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart new file mode 100644 index 000000000..70b0a6e47 --- /dev/null +++ b/lib/src/utilities/namer.dart @@ -0,0 +1,244 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// namer.dart +// Central collision-free naming for signals and instances within a module. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +/// Central namer that manages collision-free names for both signals and +/// submodule instances within a single module scope. +/// +/// All identifiers (signals and instances) share a single namespace, +/// ensuring no name collisions in the generated SystemVerilog. +/// +/// Port names are reserved at construction time. Internal signal names +/// are assigned lazily on the first [signalNameOfBest] call. Instance names +/// are assigned lazily on the first [instanceNameOf] call. +@internal +class Namer { + // ─── Shared namespace ─────────────────────────────────────────── + + final Uniquifier _uniquifier; + + /// Cache of resolved names for internal (non-port) signals only. + /// Port names are returned directly from [_portLogics] and never cached here. + final Map _signalNames = {}; + + /// Cache of resolved instance names, keyed by [Module.instanceNameKey]. + /// + /// 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 = {}; + + /// The set of port [Logic] objects, for O(1) port membership tests. + final Set _portLogics; + + // ─── Construction ─────────────────────────────────────────────── + + Namer._({required Uniquifier uniquifier, required Set portLogics}) + : _uniquifier = uniquifier, + _portLogics = portLogics; + + /// Creates a [Namer] for the given [module]'s ports. + /// + /// Port names are reserved in the shared namespace. Port names are + /// guaranteed sanitary by [Module]'s `_checkForSafePortName`. + factory Namer.forModule(Module module) { + final portLogics = { + ...module.inputs.values, + ...module.outputs.values, + ...module.inOuts.values, + }; + + final uniquifier = Uniquifier(); + for (final logic in portLogics) { + uniquifier.getUniqueName(initialName: logic.name, reserved: true); + } + + return Namer._(uniquifier: uniquifier, portLogics: portLogics); + } + + // ─── Name availability / allocation ───────────────────────────── + + /// Returns `true` if [name] has not yet been claimed in the namespace. + bool isAvailable(String name) => _uniquifier.isAvailable(name); + + // ─── 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 = _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]. + /// + /// The first call for a given [logic] allocates a collision-free name + /// via the underlying [Uniquifier]. Subsequent calls return the cached + /// result in O(1). + String _signalNameOf(Logic logic) { + final cached = _signalNames[logic]; + if (cached != null) { + return cached; + } + + if (_portLogics.contains(logic)) { + return logic.name; + } + + String base; + final isReservedInternal = logic.naming == Naming.reserved && !logic.isPort; + if (logic.naming == Naming.reserved || logic.isArrayMember) { + base = logic.name; + } else { + base = Sanitizer.sanitizeSV(logic.structureName); + } + + final name = _uniquifier.getUniqueName( + initialName: base, + reserved: isReservedInternal, + ); + _signalNames[logic] = name; + return name; + } + + /// The base name that would be used for [logic] before uniquification. + static String baseName(Logic logic) => + (logic.naming == Naming.reserved || logic.isArrayMember) + ? logic.name + : Sanitizer.sanitizeSV(logic.structureName); + + /// Chooses the best name from a pool of merged [Logic] signals. + /// + /// When [constValue] is provided and [constNameDisallowed] is `false`, + /// the constant's value string is used directly as the name (no + /// uniquification). When [constNameDisallowed] is `true`, the constant + /// is excluded from the candidate pool and the normal priority applies. + /// + /// Priority (after constant handling): + /// 1. Port of this module (always wins — its name is already reserved). + /// 2. Reserved internal signal (exact name, throws on collision). + /// 3. Renameable signal. + /// 4. Preferred-available mergeable (base name not yet taken). + /// 5. Preferred-uniquifiable mergeable. + /// 6. Available-unpreferred mergeable. + /// 7. First unpreferred mergeable. + /// 8. Unnamed (prefer non-unpreferred base name). + /// + /// The winning name is allocated once and cached for the chosen [Logic]. + /// All other non-port [Logic]s in [candidates] are also cached to the + /// same name. + String signalNameOfBest( + Iterable candidates, { + Const? constValue, + bool constNameDisallowed = false, + }) { + if (constValue != null && !constNameDisallowed) { + return constValue.value.toString(); + } + + Logic? port; + Logic? reserved; + Logic? renameable; + final preferredMergeable = []; + final unpreferredMergeable = []; + final unnamed = []; + + for (final logic in candidates) { + if (_portLogics.contains(logic)) { + port = logic; + } else if (logic.isPort) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else if (logic.naming == Naming.reserved) { + reserved = logic; + } else if (logic.naming == Naming.renameable) { + renameable = logic; + } else if (logic.naming == Naming.mergeable) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else { + unnamed.add(logic); + } + } + + if (port != null) { + return _nameAndCacheAll(port, candidates); + } + + if (reserved != null) { + return _nameAndCacheAll(reserved, candidates); + } + + if (renameable != null) { + return _nameAndCacheAll(renameable, candidates); + } + + if (preferredMergeable.isNotEmpty) { + final best = preferredMergeable.firstWhereOrNull( + (e) => isAvailable(baseName(e)), + ) ?? + preferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + if (unpreferredMergeable.isNotEmpty) { + final best = unpreferredMergeable.firstWhereOrNull( + (e) => isAvailable(baseName(e)), + ) ?? + unpreferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + if (unnamed.isNotEmpty) { + final best = + unnamed.firstWhereOrNull((e) => !Naming.isUnpreferred(baseName(e))) ?? + unnamed.first; + return _nameAndCacheAll(best, candidates); + } + + throw StateError('No Logic candidates to name.'); + } + + /// Names [chosen] 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); + for (final logic in all) { + if (!identical(logic, chosen) && !_portLogics.contains(logic)) { + _signalNames[logic] = name; + } + } + return name; + } +} diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..b2e67a6a8 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -104,10 +104,7 @@ class Vector { final outputPort = module.tryInOut(outputName) ?? module.output(outputName); final expected = expectedOutput.value; - final expectedValue = LogicValue.of( - expected, - width: outputPort.width, - ); + final expectedValue = LogicValue.of(expected, width: outputPort.width); final inputStimulus = inputValues.toString(); if (outputPort is LogicArray) { @@ -125,12 +122,8 @@ class Vector { } final checks = checksList.join('\n'); - final tbVerilog = [ - assignments, - '#$_offset', - checks, - '#${_period - _offset}', - ].join('\n'); + final tbVerilog = + [assignments, '#$_offset', checks, '#${_period - _offset}'].join('\n'); return tbVerilog; } } @@ -202,12 +195,10 @@ abstract class SimCompare { throw NonSupportedTypeException(value); } } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { - Simulator.throwException(err as Exception, stackTrace); - }, - )); + }).catchError(test: (error) => error is Exception, + (Object err, StackTrace stackTrace) { + Simulator.throwException(err as Exception, stackTrace); + })); } }); timestamp += Vector._period; @@ -224,23 +215,20 @@ abstract class SimCompare { RegExp(r'sorry: constant selects in always_\* processes' ' are not currently supported'), RegExp('warning: always_comb process has no sensitivities'), - RegExp('finish called at'), + RegExp('finish called at') ]; /// Executes [vectors] against the Icarus Verilog simulator and checks /// that it passes. - static void checkIverilogVector( - Module module, - List vectors, { - String? moduleName, - bool dontDeleteTmpFiles = false, - bool dumpWaves = false, - List iverilogExtraArgs = const [], - bool allowWarnings = false, - bool maskKnownWarnings = true, - bool enableChecking = true, - bool buildOnly = false, - }) { + static void checkIverilogVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool enableChecking = true, + bool buildOnly = false}) { final result = iverilogVector(module, vectors, moduleName: moduleName, dontDeleteTmpFiles: dontDeleteTmpFiles, @@ -255,17 +243,14 @@ abstract class SimCompare { } /// Executes [vectors] against the Icarus Verilog simulator. - static bool iverilogVector( - Module module, - List vectors, { - String? moduleName, - bool dontDeleteTmpFiles = false, - bool dumpWaves = false, - List iverilogExtraArgs = const [], - bool allowWarnings = false, - bool maskKnownWarnings = true, - bool buildOnly = false, - }) { + static bool iverilogVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool buildOnly = false}) { if (kIsWeb) { // if running in web mode, then we can't run icarus verilog return true; @@ -307,7 +292,7 @@ abstract class SimCompare { final topModule = moduleName ?? module.definitionName; final allSignals = { for (final v in vectors) ...v.inputValues.keys, - for (final v in vectors) ...v.expectedOutputValues.keys, + for (final v in vectors) ...v.expectedOutputValues.keys }; late final tbWireUniquifier = Uniquifier(); @@ -335,7 +320,7 @@ abstract class SimCompare { final sigDecl = signalDeclaration(logicName, adjust: toTbWireName, signalTypeOverride: 'wire'); return '$sigDecl; assign $wireName = $logicName;'; - }), + }) ].join('\n'); final moduleConnections = @@ -370,7 +355,7 @@ abstract class SimCompare { stimulus, r'$finish;', // so the test doesn't run forever if there's a clock gen 'end', - 'endmodule', + 'endmodule' ].join('\n'); Directory(dir).createSync(recursive: true); @@ -397,11 +382,7 @@ abstract class SimCompare { } return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), caseSensitive: false)); } diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 3a37e55ea..9948d47fb 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -17,11 +17,14 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// A waveform dumper for simulations. /// -/// Outputs to vcd format at [outputPath]. [module] must be built prior to +/// Deprecated: use [WaveformService] instead. +/// +/// Outputs to VCD format at [outputPath]. [module] must be built prior to /// attaching the [WaveDumper]. /// /// The waves will only dump to the file periodically and then once the /// simulation has completed. +@Deprecated('Use WaveformService instead') class WaveDumper { /// The [Module] being dumped. final Module module; @@ -58,6 +61,8 @@ class WaveDumper { /// Attaches a [WaveDumper] to record all signal changes in a simulation of /// [module] in a VCD file at [outputPath]. + /// @Deprecated: use [WaveformService] instead. + @Deprecated('Use WaveformService instead') WaveDumper(this.module, {this.outputPath = 'waves.vcd'}) : _outputFile = File(outputPath)..createSync(recursive: true) { if (!module.hasBuilt) { diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 3a4c6b74e..e3c1f1c78 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -97,7 +97,7 @@ void main() { test('simple 1d collapse', () async { final mod = SimpleLAPassthrough(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign laOut = laIn;')); }); @@ -105,7 +105,7 @@ void main() { test('array collapse for cross-module connection', () async { final mod = ArrayTopMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(RegExp(r'ArraySubModIn.*\.inp\(inp\)'))); expect(sv, contains(RegExp(r'ArraySubModOut.*\.arrOut\(inp\)'))); @@ -116,7 +116,7 @@ void main() { LogicArray([3, 3], 1), LogicArray([3, 3], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -134,7 +134,7 @@ void main() { final mod = ArrayWithShuffledAssignment(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).allContents; expect(sv, contains('assign b[0] = a[3];')); expect(sv, contains('assign b[3] = a[0];')); @@ -151,7 +151,7 @@ void main() { LogicArray([3, 3], 1, numUnpackedDimensions: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -168,7 +168,7 @@ void main() { final mod = ArrayModule(LogicArray([4, 4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign d = c[0];')); expect(sv, contains('assign b = a;')); diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..7fbd7f44b 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -228,7 +228,7 @@ void main() { final mod = SingleBitBusSubsetMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign result = oneBit')); final vectors = [ @@ -401,7 +401,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains("assign const_subset = 16'habcd;"), true); }); }); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..70d6ff73d 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -57,7 +57,7 @@ void main() { test('collapse pretty', () async { final mod = CollapseTestModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure e=a&b&c is in there, to prove there was some inlining expect(sv, contains(RegExp('e.*=.*a.*&.*b.*&.*c'))); diff --git a/test/config_test.dart b/test/config_test.dart index 28cd2e7d8..1730ab77a 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -46,7 +46,7 @@ void main() async { final mod = SimpleModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(version)); }); diff --git a/test/counter_wintf_test.dart b/test/counter_wintf_test.dart index 7889369cc..91986aea2 100644 --- a/test/counter_wintf_test.dart +++ b/test/counter_wintf_test.dart @@ -145,7 +145,7 @@ void main() { test('interface ports dont get doubled up', () async { final mod = Counter(CounterInterface(8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(!sv.contains('en_0'), true); }); diff --git a/test/external_test.dart b/test/external_test.dart index 09ac84c87..3ba33c649 100644 --- a/test/external_test.dart +++ b/test/external_test.dart @@ -31,7 +31,7 @@ void main() { test('instantiate', () async { final mod = TopModule(Logic(width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure we instantiate the external module properly expect( diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..b4e879403 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -183,7 +183,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("b = 1'h0;")); }); @@ -192,7 +192,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('priority case')); }); @@ -201,7 +201,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('MyStates_state1 : begin')); }); diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..4fabc3074 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -537,7 +537,7 @@ void main() { final gtm = ShiftTestModule(Logic(width: 3), Logic(width: 8), constant: 0); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = SvService(gtm).synthOutput; expect(sv, isNot(contains("0'h0"))); diff --git a/test/inout_loopback_test.dart b/test/inout_loopback_test.dart index 0c3b5b343..b2fe711b2 100644 --- a/test/inout_loopback_test.dart +++ b/test/inout_loopback_test.dart @@ -237,7 +237,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The outer module should NOT contain an internal net_connect // for the loopback — the submodule ports should just be wired to the @@ -268,7 +268,7 @@ void main() { final mod = SimpleOuterLoopback(LogicNet(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; final outerModuleSv = _extractModuleSv(sv, 'simpleOuter'); expect(outerModuleSv, isNot(contains('net_connect')), @@ -284,7 +284,7 @@ void main() { final mod = LoopbackPairTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Check for net_connect in the top module final topModuleSv = _extractModuleSv(sv, 'LoopbackPairTop'); @@ -309,7 +309,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The inner module SHOULD have a net_connect (connecting ioA <= ioB). final innerModuleSv = _extractModuleSv(sv, 'innerConnected'); @@ -347,7 +347,7 @@ void main() { final mod = OuterClkLoopback(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The outer module should NOT have a net_connect — the loopback net // is only used as port connections in the inner instantiation. diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart new file mode 100644 index 000000000..68f0e9a80 --- /dev/null +++ b/test/instance_signal_name_collision_test.dart @@ -0,0 +1,90 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// instance_signal_name_collision_test.dart +// Tests that submodule instance names and signal names share a single +// namespace, so a collision between them results in uniquification. +// +// 2026 April 18 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Minimal repro modules ──────────────────────────────────────────────────── + +/// Leaf module whose default instance name is "inner". +class _Inner extends Module { + _Inner(Logic a) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + addOutput('y', width: a.width) <= a; + } +} + +/// Parent module that: +/// • instantiates [_Inner] (default instance name: "inner") +/// • names an internal wire "inner" as well +/// +/// Because both identifiers live in a single shared namespace, one of them +/// will be suffixed to avoid collision. +class _CollidingParent extends Module { + _CollidingParent(Logic a) : super(name: 'colliding_parent') { + a = addInput('a', a, width: a.width); + + // Internal wire explicitly named "inner". + final inner = Logic(name: 'inner', width: a.width, naming: Naming.reserved) + ..gets(a); + + // Submodule whose uniqueInstanceName will also be "inner". + final sub = _Inner(inner); + + addOutput('y', width: a.width) <= sub.output('y'); + } +} + +// ── Test ───────────────────────────────────────────────────────────────────── + +void main() { + group('instance / signal name collision (shared namespace)', () { + late _CollidingParent mod; + late SynthModuleDefinition def; + + setUpAll(() async { + mod = _CollidingParent(Logic(width: 8)); + await mod.build(); + def = SynthModuleDefinition(mod); + }); + + test('internal signal named "inner" retains its exact name', () { + // The reserved signal should keep its exact name. + final sl = def.internalSignals.cast().firstWhere( + (s) => s!.logics.any((l) => l.name == 'inner'), + orElse: () => null, + ); + expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); + expect( + sl!.name, + 'inner', + reason: 'Reserved signal "inner" must keep its exact name', + ); + }); + + test( + 'submodule instance is uniquified because signal ' + '"inner" already claimed the name', () { + final inst = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .cast() + .firstWhere((s) => s!.module.name == 'inner', orElse: () => null); + expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); + // The instance should be suffixed since the signal took "inner" first. + expect( + inst!.name, + isNot('inner'), + reason: 'Instance should be uniquified when signal already ' + 'claims "inner"', + ); + }); + }); +} diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..f6d9ca784 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -751,7 +751,7 @@ void main() { ]; if (checkNoSwizzle) { - expect(mod.generateSynth().contains('swizzle'), false, + expect(SvService(mod).synthOutput.contains('swizzle'), false, reason: 'Expected no swizzles but found one.'); } @@ -800,7 +800,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains(RegExp(r'\[7:0\]\s*laIn\s*\[2:0\]')), true); expect(sv.contains(RegExp(r'\[7:0\]\s*laOut\s*\[2:0\]')), true); }); @@ -818,7 +818,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv.contains(RegExp( r'\[2:0\]\s*\[1:0\]\s*\[7:0\]\s*laIn\s*\[4:0\]\s*\[3:0\]')), @@ -846,7 +846,7 @@ void main() { await testArrayPassthrough(mod); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [2:0][1:0][2:0][7:0] laIn')); expect(sv, contains('output logic [2:0][1:0][2:0][7:0] laOut')); }); @@ -861,7 +861,7 @@ void main() { await testArrayPassthrough(mod, noSvSim: true); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [1:0][2:0][7:0] laIn [2:0]')); expect(sv, contains('output logic [1:0][2:0][7:0] laOut [2:0]')); }); @@ -928,7 +928,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('logic [2:0][3:0][7:0] intermediate [1:0]'), true); }); }); @@ -981,7 +981,7 @@ void main() { test('3d', () async { final mod = SimpleArraysAndHierarchy(LogicArray([2], 8)); await testArrayPassthrough(mod); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('SimpleLAPassthrough simple_la_passthrough')); }); @@ -992,7 +992,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - expect(mod.generateSynth(), contains('SimpleLAPassthrough')); + expect(SvService(mod).synthOutput, contains('SimpleLAPassthrough')); }); }); @@ -1001,7 +1001,7 @@ void main() { final mod = FancyArraysAndHierarchy(LogicArray([4, 3, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure the 4th one is there (since we expect 4) expect(sv, contains('SimpleLAPassthrough simple_la_passthrough_2')); @@ -1043,7 +1043,8 @@ void main() { final mod = WithSetArrayOffsetModule(LogicArray([2, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); // make sure we're reassigning both times it overlaps! expect( diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 1a5fb1d98..b2cc6c95d 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -30,7 +30,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('intermediate')); }); @@ -45,7 +45,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // no intermediate expect(sv.contains('intermediate'), isFalse); @@ -58,7 +58,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // just the ports expect('logic'.allMatches(sv).length, 3); @@ -71,7 +71,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // just the ports expect('logic'.allMatches(sv).length, 3); @@ -90,7 +90,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // held one sticks expect(sv, contains('intermediate_1 = in1')); @@ -108,7 +108,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -124,7 +124,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -145,7 +145,7 @@ void main() { out1 <= intermediate | intermediate2; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -167,7 +167,7 @@ void main() { out1 <= ~intermediatePost; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('goodname')); }); @@ -220,7 +220,7 @@ void main() { out1 <= ~prev; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains(expectedName), reason: 'Amongst ${l.map((e) => e.name).toList()},' @@ -235,7 +235,7 @@ void main() { intermediate <= in1; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('intermediate')); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 8ce9d5f40..457d3ed52 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -225,13 +225,13 @@ void main() { final mod = LogicWithInternalSignalModule(Logic()); await mod.build(); - expect(mod.generateSynth(), contains('shouldExist')); + expect(SvService(mod).synthOutput, contains('shouldExist')); }); test('unconnected port does not duplicate internal signal', () async { final pMod = ParentMod(Logic(), Logic()); await pMod.build(); - final sv = pMod.generateSynth(); + final sv = SvService(pMod).synthOutput; expect(RegExp('logic a[,;\n]').allMatches(sv).length, 2); }); @@ -239,7 +239,7 @@ void main() { test('assigns and gates', () async { final mod = SensitiveNaming(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('e = a & d')); expect(sv, contains('b = a')); expect(sv, contains('d = c')); @@ -248,7 +248,7 @@ void main() { test('bus subset', () async { final mod = BusSubsetNaming(Logic(width: 32)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('c = b[3]')); }); }); @@ -257,7 +257,7 @@ void main() { test('unconnected floating', () async { final mod = DrivenOutputModule(null); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // shouldn't add a Z in there if left floating expect(!sv.contains('z'), true); @@ -266,7 +266,7 @@ void main() { test('driven to z', () async { final mod = DrivenOutputModule(Const('z')); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // should add a Z if it's explicitly added expect(sv, contains('z')); @@ -279,7 +279,7 @@ void main() { portANaming: Naming.renameable, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -295,7 +295,7 @@ void main() { () async { final mod = NameCollisionArrayTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -312,7 +312,7 @@ void main() { await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('_wow_______')); }); @@ -321,7 +321,7 @@ void main() { final mod = StructElementNamingModule(VariousNamingStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign outp[0] = outp_renameable;')); expect(sv, contains('assign outp[1] = reserved_outp;')); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index fdc522e96..85c75468f 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -175,7 +175,7 @@ void main() { final mod = StructModuleWithInstrumentation(Const(0, width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('swizzle'), isFalse, reason: 'Should not pack from instrumentation!'); diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..041ce7ed6 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -91,7 +91,7 @@ void main() { final mod = AddWithCarryMod(Logic(width: 8), Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign {carry, sum} = a + b')); }); @@ -119,7 +119,7 @@ void main() { final gtm = MathTestModule(Logic(width: 8), Logic(width: 8)); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = SvService(gtm).synthOutput; final lines = sv.split('\n'); // ensure we never lshift by a constant directly diff --git a/test/module_merging_test.dart b/test/module_merging_test.dart index 5a5590ce9..bc14a4228 100644 --- a/test/module_merging_test.dart +++ b/test/module_merging_test.dart @@ -91,7 +91,7 @@ void main() async { () async { final dut = TrunkWithLeaves(Logic(), Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect('module ComplicatedLeaf'.allMatches(sv).length, 1); }); @@ -99,7 +99,7 @@ void main() async { test('different reserved definition name modules stay separate', () async { final dut = ParentOfDifferentModuleDefNames(Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('module def1')); expect(sv, contains('module def2')); diff --git a/test/module_services_test.dart b/test/module_services_test.dart new file mode 100644 index 000000000..246263191 --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,201 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Unit tests for ModuleServices, the service base types, and SvService. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class SimpleModule extends Module { + SimpleModule(Logic a) : super(name: 'simple') { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// A minimal [ModuleService] used to exercise the type-keyed registry. +class FakeService implements ModuleService { + FakeService(this.module); + + @override + final Module module; + + @override + Map toJson() => {'kind': 'fake'}; +} + +void main() { + tearDown(ModuleServices.instance.reset); + + group('ModuleServices registry', () { + test('rootModule is set after build', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, equals(mod)); + }); + + test('hierarchyJSON returns valid JSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final json = ModuleServices.instance.hierarchyJSON; + expect(() => jsonDecode(json), returnsNormally); + }); + + test('register and lookup round-trips a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final fake = FakeService(mod); + ModuleServices.instance.register(fake); + expect(ModuleServices.instance.lookup(), same(fake)); + }); + + test('lookup returns null when no service registered', () { + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('unregister removes a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.register(FakeService(mod)); + ModuleServices.instance.unregister(); + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('reset clears rootModule and all services', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.register(FakeService(mod)); + expect(ModuleServices.instance.rootModule, isNotNull); + + ModuleServices.instance.reset(); + expect(ModuleServices.instance.rootModule, isNull); + expect(ModuleServices.instance.lookup(), isNull); + }); + }); + + group('SvService', () { + test('registers with ModuleServices and sets current', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(ModuleServices.instance.lookup(), same(sv)); + expect(SvService.current, same(sv)); + }); + + test('is a CodegenService', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(SvService(mod), isA()); + }); + + test('allContents is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.allContents, isNotEmpty); + }); + + test('output equals synthOutput', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.output, equals(sv.synthOutput)); + }); + + test('contentsByName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByName, isNotEmpty); + }); + + test('contentsByDefinitionName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByDefinitionName, isNotEmpty); + expect(sv.contentsByDefinitionName.containsKey('SimpleModule'), isTrue); + }); + + test('moduleOutput returns the definition contents', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.moduleOutput('SimpleModule'), isNotNull); + expect(sv.moduleOutput('DoesNotExist'), isNull); + }); + + test('toJson lists generated modules', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.toJson()['modules'], isList); + }); + + test('writeFiles creates SV files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + sv.writeFiles(dir.path); + final files = dir.listSync().whereType().toList(); + expect(files, isNotEmpty); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('write() emits a single file', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod, register: false); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + final path = '${dir.path}/out.sv'; + sv.write(path); + expect(File(path).readAsStringSync(), equals(sv.synthOutput)); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('write() with multiFile emits a directory of files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + // Construction with outputPath writes immediately. + SvService(mod, register: false, outputPath: dir.path, multiFile: true); + final files = dir.listSync().whereType().toList(); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('register false does not register', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.reset(); + SvService(mod, register: false); + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + expect(() => SvService(mod), throwsException); + }); + }); +} diff --git a/test/module_test.dart b/test/module_test.dart index 08502d6f8..ff6714e01 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -303,8 +303,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,(a ? 1'h0 : 1'h1)}")); @@ -320,8 +320,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,a}")); @@ -336,7 +336,8 @@ void main() { TopStructInoutWrap(LogicNet(), LogicNet(), LogicNet(width: 2)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); expect( sv, @@ -352,7 +353,7 @@ void main() { expect( mod.internalSignals.firstWhereOrNull((e) => e.name == 't0'), isNotNull); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign a_concat[0] = t0;')); }); @@ -363,7 +364,7 @@ void main() { expect(mod.internalSignals.firstWhereOrNull((e) => e.name == 'unconnected'), isNotNull); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign a_arr[1] = unconnected;')); }); diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 52470dc8c..779ae13ed 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -54,7 +54,7 @@ void main() { .isNotEmpty, 'Should find a z two levels deep'); - final synth = ftm.generateSynth(); + final synth = SvService(ftm).synthOutput; // "z = 1" means it correctly traversed down from inputs assert(synth.contains('z = 1'), diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index b7642bb24..fb20a6106 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -35,7 +35,7 @@ void main() { final mod = TopModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('Passthrough')); }); diff --git a/test/name_test.dart b/test/name_test.dart index 2742c0ec8..c41573f92 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -1,7 +1,7 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// definition_name_test.dart +// name_test.dart // Tests for definition names (including reserving them) of Modules. // // 2022 March 7 @@ -136,6 +136,9 @@ void main() { final nameTypes = [nameType1, nameType2]; // skip ones that actually *should* cause a failure + // + // Note: SystemVerilog does not allow using the same identifier for a + // signal and an instance. final shouldConflict = [ { NameType.internalModuleDefinition, @@ -191,20 +194,20 @@ void main() { test('respected with no conflicts', () async { final mod = SpeciallyNamedModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module specialName (')); }); test('uniquified with conflicts', () async { final mod = TopModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module specialName (')); expect(sv, contains('module specialName_0 (')); }); test('reserved throws exception with conflicts', () async { final mod = TopModule(Logic(), true, false); await mod.build(); - expect(mod.generateSynth, throwsException); + expect(() => SvService(mod).synthOutput, throwsException); }); }); @@ -212,7 +215,7 @@ void main() { test('uniquified with conflicts', () async { final mod = TopModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('specialInstanceName(')); expect(sv, contains('specialInstanceName_0(')); diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart new file mode 100644 index 000000000..0ae049180 --- /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 = SvService(mod).synthOutput; + + // 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..150a9b751 --- /dev/null +++ b/test/naming_consistency_test.dart @@ -0,0 +1,356 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_consistency_test.dart +// Validates that both the SystemVerilog synthesizer and a base +// SynthModuleDefinition (used by the netlist synthesizer) produce +// consistent signal names via the shared Module.namer. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Helper modules ────────────────────────────────────────────────── + +/// A simple module with ports, internal wires, and a sub-module. +class _Inner extends Module { + _Inner(Logic a, Logic b) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + addOutput('y', width: a.width) <= a & b; + } +} + +class _Outer extends Module { + _Outer(Logic a, Logic b) : super(name: 'outer') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final inner = _Inner(a, b); + addOutput('y', width: a.width) <= inner.output('y'); + } +} + +/// A module with a constant assignment (exercises const naming). +class _ConstModule extends Module { + _ConstModule(Logic a) : super(name: 'constmod') { + a = addInput('a', a, width: 8); + final c = Const(0x42, width: 8).named('myConst', naming: Naming.mergeable); + addOutput('y', width: 8) <= a + c; + } +} + +/// A module with Naming.renameable and Naming.mergeable signals. +class _MixedNaming extends Module { + _MixedNaming(Logic a) : super(name: 'mixednaming') { + a = addInput('a', a, width: 8); + final r = Logic(name: 'renamed', width: 8, naming: Naming.renameable) + ..gets(a); + final m = Logic(name: 'merged', width: 8, naming: Naming.mergeable) + ..gets(r); + addOutput('y', width: 8) <= m; + } +} + +/// A module with a FlipFlop sub-module. +class _FlopOuter extends Module { + _FlopOuter(Logic clk, Logic d) : super(name: 'flopouter') { + clk = addInput('clk', clk); + d = addInput('d', d, width: 8); + addOutput('q', width: 8) <= flop(clk, d); + } +} + +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. +/// +/// Returns maps from Logic to its resolved signal name. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + // Skip SynthLogics whose name was never picked (replaced/pruned). + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked — skip + } + } + return names; +} + +void main() { + group('naming consistency', () { + test('SV and base SynthModuleDefinition agree on port names', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // SV synthesizer path + final svDef = SystemVerilogSynthModuleDefinition(mod); + + // Base path (same as netlist synthesizer uses) + // Since namer is late final, the second constructor reuses + // the same naming state — names must be consistent. + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + // Every Logic present in both must have the same name. + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name} ' + '(${logic.runtimeType}, naming=${logic.naming})', + ); + } + } + + // Port names specifically must match. + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + expect( + svNames[port], + isNotNull, + reason: 'SV def should have port ${port.name}', + ); + expect( + baseNames[port], + isNotNull, + reason: 'Base def should have port ${port.name}', + ); + expect( + svNames[port], + baseNames[port], + reason: 'Port name must match for ${port.name}', + ); + } + }); + + test('constant naming is consistent', () async { + final mod = _ConstModule(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); + } + } + }); + + test('mixed naming (renameable + mergeable) is consistent', () async { + final mod = _MixedNaming(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); + } + } + }); + + test('flop module naming is consistent', () async { + final mod = _FlopOuter(Logic(), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); + } + } + }); + + test('namer is shared across multiple SynthModuleDefinitions', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // Build one def, then build another — same namer instance. + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = _collectNames(def1); + final names2 = _collectNames(def2); + + for (final logic in names1.keys) { + if (names2.containsKey(logic)) { + expect( + names2[logic], + names1[logic], + reason: 'Shared namer should produce same name for ' + '${logic.name}', + ); + } + } + }); + + test('Namer.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.signalNameOfBest uses Namer directly + 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}', + ); + } + }); + + 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')); + }, + ); + }); +} diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart new file mode 100644 index 000000000..326782b38 --- /dev/null +++ b/test/naming_namespace_test.dart @@ -0,0 +1,169 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_namespace_test.dart +// Tests for constant naming via nameOfBest and shared instance/signal +// namespace uniquification. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// A simple submodule whose instance name can collide with a signal name. +class _Inner extends Module { + _Inner(Logic a, {super.name = 'inner'}) { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// Top module that has a signal named the same as a submodule instance. +class _InstanceSignalCollision extends Module { + _InstanceSignalCollision({String instanceName = 'inner'}) + : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // Create a signal whose name matches the submodule instance name. + final sig = Logic(name: instanceName); + sig <= ~a; + + final sub = _Inner(sig, name: instanceName); + o <= sub.output('b'); + } +} + +/// Top module with two submodule instances that have the same name. +class _DuplicateInstances extends Module { + _DuplicateInstances() : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + final sub1 = _Inner(a, name: 'blk'); + final sub2 = _Inner(sub1.output('b'), name: 'blk'); + o <= sub2.output('b'); + } +} + +/// Module that uses a constant in a connection chain, exercising constant +/// naming through nameOfBest. +class _ConstantNamingModule extends Module { + _ConstantNamingModule() : super(name: 'const_mod') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // A constant "1" drives one input of the AND gate. + o <= a & Const(1); + } +} + +/// Module with a mux where one input is a constant, exercising the +/// constNameDisallowed path — the mux output cannot use the constant's +/// literal as its name because it also carries non-constant values. +class _ConstNameDisallowedModule extends Module { + _ConstNameDisallowedModule() : super(name: 'const_disallow') { + final a = addInput('a', Logic()); + final sel = addInput('sel', Logic()); + final o = addOutput('o'); + + // mux output can be the constant OR a, so the constant name is disallowed. + o <= mux(sel, Const(1), a); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('constant naming via nameOfBest', () { + test('constant value appears as literal in SV output', () async { + final dut = _ConstantNamingModule(); + await dut.build(); + final sv = SvService(dut).synthOutput; + + // 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 = SvService(dut).synthOutput; + + // The output assignment should NOT use the raw constant literal + // as a wire name; a proper signal name should be used instead. + // The constant still appears as a literal in the mux expression. + expect(sv, contains("1'h1")); + // The output 'o' should be assigned from something. + expect(sv, contains('o')); + }); + }); + + group('shared instance and signal namespace', () { + test( + 'signal and instance with same name get uniquified ' + 'in the shared namespace', () async { + final dut = _InstanceSignalCollision(); + await dut.build(); + final sv = SvService(dut).synthOutput; + + // With a single shared namespace, one of the two "inner" identifiers + // must be suffixed to avoid collision. + 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(); + final sv = SvService(dut).synthOutput; + + // Two instances named 'blk' — one should be 'blk', the other 'blk_0'. + expect(sv, contains('blk')); + expect(sv, contains(RegExp(r'blk_\d'))); + }); + }); +} diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index 2d5fccdc0..9bcd680a2 100644 --- a/test/net_bus_test.dart +++ b/test/net_bus_test.dart @@ -255,7 +255,8 @@ void main() { final mod = NicePortPassingTop(LogicNet(width: 8), LogicNet(width: 8)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); expect(sv.contains('net_connect'), isFalse); expect(sv, @@ -314,7 +315,7 @@ void main() { final dut = DoubleNetPassthrough(LogicNet(width: 8), LogicNet(width: 8)); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect( sv, @@ -455,7 +456,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, contains( @@ -517,7 +518,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -590,7 +591,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); if (netTypeName == LogicNet) { expect( sv, @@ -620,7 +621,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); if (netTypeName == LogicNet) { expect( sv, @@ -750,8 +751,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect(sv, contains('net_connect (swizzled, ({in0[0],in1[0]}));')); }); @@ -764,8 +765,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -781,8 +782,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -799,8 +800,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -817,8 +818,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -835,8 +836,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect(sv, contains('assign _in1 = in0;')); expect( @@ -852,8 +853,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -942,7 +943,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); checkSV(sv); final vectors = [ @@ -962,7 +963,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); checkSV(sv); final vectors = [ @@ -1204,8 +1205,8 @@ void main() { final mod = ReplicateMod(LogicNet(width: 4), 2); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -1225,8 +1226,8 @@ void main() { final mod = ReplicateMod(LogicNet(width: 4), 2); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, diff --git a/test/net_test.dart b/test/net_test.dart index c8d15b7d7..be2c51d1f 100644 --- a/test/net_test.dart +++ b/test/net_test.dart @@ -461,7 +461,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('intermediate1')); expect(sv, contains('intermediate2')); expect(sv, contains('intermediate3')); @@ -504,7 +504,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -515,7 +515,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -526,7 +526,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(' submod'.allMatches(sv).length, 2); }); }); @@ -611,7 +611,7 @@ void main() { isNotNull); } - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // test that " _b;" is not present (indication that a leftover internal // signal was there) @@ -631,7 +631,7 @@ void main() { final mod = NetArrayTopMod(Logic(width: 8), NetArrayIntf()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // print(sv); expect(sv, contains('wire [1:0][1:0][7:0] bd3')); }); @@ -677,7 +677,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign c = _a_and_b;')); expect(sv, contains('assign d = _aIntermediate_or_bIntermediate;')); diff --git a/test/pair_interface_hier_test.dart b/test/pair_interface_hier_test.dart index e665b7102..71c7cbc92 100644 --- a/test/pair_interface_hier_test.dart +++ b/test/pair_interface_hier_test.dart @@ -91,7 +91,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_hier_w_modify_test.dart b/test/pair_interface_hier_w_modify_test.dart index 47c65318c..fe6255c8c 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_w_modify_test.dart @@ -93,7 +93,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 0167335f3..536576d1b 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -192,7 +192,7 @@ void main() { await mod.build(); // Make sure the "modify" went through: - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic simple_clk')); }); diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 97c15f648..8e5f17177 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -176,7 +176,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' module Provider ( diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index d34c8e374..87b8495aa 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -146,7 +146,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' module Provider ( diff --git a/test/sequential_test.dart b/test/sequential_test.dart index ade256cf3..324777f0a 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -241,7 +241,7 @@ void main() { final mod = NegedgeTriggeredSeq(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('always_ff @(negedge')); final vectors = [ diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart new file mode 100644 index 000000000..2a2ded5ef --- /dev/null +++ b/test/signal_registry_test.dart @@ -0,0 +1,335 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_registry_test.dart +// Tests for Module canonical naming (Namer). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; +import 'package:test/test.dart'; + +// ──────────────────────────────────────────────────────────────── +// Simple test modules +// ──────────────────────────────────────────────────────────────── + +class _GateMod extends Module { + _GateMod(Logic a, Logic b) : super(name: 'gatetestmodule') { + a = addInput('a', a); + b = addInput('b', b); + final aBar = addOutput('a_bar'); + final aAndB = addOutput('a_and_b'); + aBar <= ~a; + aAndB <= a & b; + } +} + +class _Counter extends Module { + _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi( + [SimpleClockGenerator(10).clk, reset], + [ + If( + reset, + then: [val < 0], + orElse: [ + If(en, then: [val < nextVal]), + ], + ), + ], + ); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('signalName basics', () { + test('returns port names after build', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.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.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 signalNameOfBest after synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + for (final entry in mod.inputs.entries) { + expect( + mod.namer.signalNameOfBest([entry.value]), + isNotNull, + reason: 'signalNameOfBest should work for input ${entry.key}', + ); + } + for (final entry in mod.outputs.entries) { + expect( + mod.namer.signalNameOfBest([entry.value]), + isNotNull, + reason: 'signalNameOfBest should work for output ${entry.key}', + ); + } + }); + }); + + group('single-signal allocation', () { + test('avoids collision with existing names', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + 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.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'); + }); + }); + + group('sparse storage', () { + test('identity names not stored in renames', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOfBest([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.signalNameOfBest([sig]), + }; + } + + final names1 = await buildAndGetNames(); + await Simulator.reset(); + final names2 = await buildAndGetNames(); + + expect(names1, equals(names2)); + }); + }); + + group('isAvailable', () { + test('port names are not available', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.isAvailable('a'), isFalse); + expect(mod.namer.isAvailable('b'), isFalse); + expect(mod.namer.isAvailable('a_bar'), isFalse); + expect(mod.namer.isAvailable('a_and_b'), isFalse); + }); + + test('unallocated names are available', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.isAvailable('xyz'), isTrue); + expect(mod.namer.isAvailable('new_signal'), isTrue); + }); + + test('allocated names become unavailable', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + final name = mod.namer.signalNameOfBest([ + Logic(name: 'wire', naming: Naming.renameable), + ]); + expect(mod.namer.isAvailable(name), isFalse); + }); + }); + + group('reserved single-signal allocation', () { + test('reserved signal claims exact name', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + 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); + }); + + test('reserved collision throws', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect( + () => mod.namer.signalNameOfBest([ + Logic(name: 'a', naming: Naming.reserved), + ]), + 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.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())); + }); + + 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); + }); + }); +} diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..dc9e68ddd 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -698,7 +698,7 @@ void main() { final mod = TieOffSubsetTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("assign banana_tieoff = 2'h0;")); expect(sv, contains("assign apple_tieoff = 2'h0;")); @@ -719,7 +719,7 @@ void main() { final mod = TieOffPortTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("assign banana = 1'h0;")); expect(sv, contains(".apple(1'h0)")); @@ -751,7 +751,7 @@ void main() { test('input, output, and internal signals are sorted', () async { final mod = AlphabeticalModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -768,7 +768,7 @@ void main() { () async { final mod = AlphabeticalWidthsModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -794,7 +794,7 @@ void main() { final mod = AlphabeticalSubmodulePorts(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; checkPortConnectionOrder(sv, ['l', 'a', 'w', 'm', 'x', 'b']); }); @@ -803,7 +803,7 @@ void main() { final mod = TopWithExpressions(Logic(), Logic(width: 5)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('.a((a | (b[2])))')); }); @@ -812,7 +812,7 @@ void main() { final mod = ModuleWithFloatingSignals(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // only expect 1 assignment to xylophone expect('assign'.allMatches(sv).length, 1); @@ -826,8 +826,8 @@ void main() { final mod = TopCustomSvWrap(Logic(), Logic(), useOld: useOld, banExpressions: banExpressions); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (banExpressions) { expect(sv, contains('assign my_fancy_new_signal <= ^fer_swizzle;')); @@ -846,7 +846,7 @@ void main() { final mod = ModuleWithCustomDefinitionEmptyPorts(Logic(), acceptsEmptyPortConnections: acceptsEmptyPortConnections); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; if (acceptsEmptyPortConnections) { expect(sv, contains('.b()')); @@ -861,7 +861,7 @@ void main() { test('custom definition', () async { final mod = TopWithCustomDef(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module CustomDefinitionModule (')); expect(sv, contains('// this is a custom definition!')); @@ -879,7 +879,7 @@ void main() { final mod = ModWithUselessWireMods(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('swizzle'))); expect(sv, isNot(contains('replicate'))); @@ -897,7 +897,7 @@ endmodule : ModWithUselessWireMods''')); test('partial array assignment sv', () async { final mod = ModWithPartialArrayAssignment(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign b = aArr[0];')); expect(sv, contains('assign aArr[0] = a;')); @@ -1041,7 +1041,7 @@ endmodule : ModWithUselessWireMods''')); final mod = OutToInOutTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign myNet = myOut;')); @@ -1057,7 +1057,7 @@ endmodule : ModWithUselessWireMods''')); () async { final mod = _StructLeafNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('_in0')), reason: 'Struct leaf from unnamed Logic() should use its ' @@ -1067,7 +1067,7 @@ endmodule : ModWithUselessWireMods''')); test('const merge not blocked by constNameDisallowed', () async { final mod = _ConstNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; final constAssignments = RegExp(r"assign \w+ = 8'h0;").allMatches(sv).length; diff --git a/test/sv_param_passthrough_test.dart b/test/sv_param_passthrough_test.dart index e7b0876dd..041eb8ce5 100644 --- a/test/sv_param_passthrough_test.dart +++ b/test/sv_param_passthrough_test.dart @@ -162,7 +162,7 @@ void main() { () async { final mod = TopForEmptyParams(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('#'), isFalse); }); } diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index 6e1fc0949..691497eab 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -123,7 +123,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('/*')); expect(sv, contains('*/')); @@ -146,7 +146,7 @@ void main() { final mod = SingleElementSwizzle(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Single element should not have braces or bit range annotations // Look for bit range annotations specifically (/* number */) @@ -171,7 +171,7 @@ void main() { final mod = AllSingleBitSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have bit range annotations for single bits expect(sv, contains('/*')); @@ -202,7 +202,7 @@ void main() { final mod = NestedSwizzle(Logic(width: 4), Logic(width: 3)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should contain annotations for both inner and outer swizzles expect(sv, contains('/*')); @@ -222,7 +222,7 @@ void main() { final mod = InlinedSwizzle(Logic(width: 4), Logic(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have annotations even when swizzle is part of larger expression expect(sv, contains('/*')); @@ -242,7 +242,7 @@ void main() { final mod = VariedWidthSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have aligned bit range annotations expect(sv, contains('/*')); @@ -280,7 +280,7 @@ void main() { // Create a module with indices requiring different digit widths final largeModule = LargeWidthSwizzle(); await largeModule.build(); - final sv = largeModule.generateSynth(); + final sv = SvService(largeModule).synthOutput; // Should have properly aligned annotations despite different digit counts expect(sv, contains('/*')); @@ -323,7 +323,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' assign b = { diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..8f27bd0fb 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -229,7 +229,7 @@ void main() { final mod = SimpleStructModuleContainer(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('internal_struct'))); @@ -251,7 +251,7 @@ void main() { expect(mod.anyOut, isA()); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [3:0][1:0] anyIn')); expect(sv, contains('output logic [3:0][1:0] anyOut')); @@ -277,7 +277,7 @@ void main() { final mod = ParentModuleWithStructsContainingPorts(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // if naming is wrong, these names will appear in the SV in ports expect( @@ -357,7 +357,7 @@ void main() { SimpleStructModuleContainer(LogicNet(), LogicNet(), asNet: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('internal_struct'))); @@ -502,7 +502,7 @@ void main() { final mod = ModuleWithOneBitStructPort(OneBitStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // no slicing on single-bit signals expect(sv, contains('assign outStruct = outStruct_oneBit')); diff --git a/test/wave_dumper_test.dart b/test/wave_dumper_test.dart index 07aafc8c8..a94d06620 100644 --- a/test/wave_dumper_test.dart +++ b/test/wave_dumper_test.dart @@ -40,11 +40,11 @@ const tempDumpDir = 'tmp_test'; /// Gets the path of the VCD file based on a name. String temporaryDumpPath(String name) => '$tempDumpDir/temp_dump_$name.vcd'; -/// Attaches a [WaveDumper] to [module] to VCD with [name]. +/// Attaches a [WaveformService] to [module] to VCD with [name]. void createTemporaryDump(Module module, String name) { Directory(tempDumpDir).createSync(recursive: true); final tmpDumpFile = temporaryDumpPath(name); - WaveDumper(module, outputPath: tmpDumpFile); + WaveformService(module, outputPath: tmpDumpFile); } /// Deletes the temporary VCD file associated with [name]. @@ -241,7 +241,8 @@ void main() { const dir1Path = '$tempDumpDir/dir1'; - final waveDumper = WaveDumper(mod, outputPath: '$dir1Path/dir2/waves.vcd'); + final waveDumper = + WaveformService(mod, outputPath: '$dir1Path/dir2/waves.vcd'); expect(File(waveDumper.outputPath).existsSync(), equals(true)); diff --git a/tool/gh_codespaces/install_dart.sh b/tool/gh_codespaces/install_dart.sh index abbe39a0c..d0bfdfe91 100755 --- a/tool/gh_codespaces/install_dart.sh +++ b/tool/gh_codespaces/install_dart.sh @@ -8,24 +8,93 @@ # # 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 -# Add Dart repository key. +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' -declare -r input_pubkey_file='tool/gh_codespaces/pubkeys/dart.pub' -declare -r output_pubkey_file='/usr/share/keyrings/dart.gpg' +sudo apt-get update +sudo apt-get install -y wget gpg apt-transport-https -sudo gpg --output ${output_pubkey_file} --dearmor ${input_pubkey_file} +sudo mkdir -p /usr/share/keyrings # Add Dart repository. -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' +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}" -echo "deb [signed-by=${output_pubkey_file}] ${dart_repository_url} stable main" | sudo tee ${dart_repository_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. 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 index 0366239cb..839f8a235 100644 --- a/tool/gh_codespaces/pubkeys/dart.pub +++ b/tool/gh_codespaces/pubkeys/dart.pub @@ -1,35 +1,4 @@ -----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 @@ -262,6 +231,75 @@ pU5M3j2F1RFKRr95+HZT/NXNeGbFvsdKmvP4ELtDAuYVMgYR8GqjI5yP/ccVMsi/ mhT+cUxO/F7+7nixw1Go637Jqr/NF5kjjrBD8EiGy8QrGm6uBR3NGad0BnMWKa2Y oYKF1m3Fs/evBkcymR+hSwFzkXm6WSOb8hzJIayFa6kAc7uSKyR5iG00p/neibbq M1aUAQDBwV7g9wPmcdRIjJS2MtK1JXHZCR1gVKb+EObct6RJOVw8s58ES5O9wGZm -bVtIZ+JHTbuH+tg0EoRNcCbz -=JIbr +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-----