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/counter.dart b/doc/tutorials/chapter_9/rohd_vf_example/lib/counter.dart index 53205b5ef..d37508a51 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/lib/counter.dart +++ b/doc/tutorials/chapter_9/rohd_vf_example/lib/counter.dart @@ -11,19 +11,17 @@ class MyCounterInterface extends Interface { final int width; MyCounterInterface({this.width = 8}) { - setPorts( - [Logic.port('en'), Logic.port('reset')], [CounterDirection.inward]); + setPorts([Port('en'), Port('reset')], [CounterDirection.inward]); setPorts([ - Logic.port('val', width), + Port('val', width), ], [ CounterDirection.outward ]); - setPorts([Logic.port('clk')], [CounterDirection.misc]); + setPorts([Port('clk')], [CounterDirection.misc]); } - @override MyCounterInterface clone() => MyCounterInterface(width: width); } @@ -38,9 +36,10 @@ class MyCounter extends Module { late final MyCounterInterface counterintf; MyCounter(MyCounterInterface intf) : super(name: 'counter') { - counterintf = addInterfacePorts(counterintf, - inputTags: {CounterDirection.inward, CounterDirection.misc}, - outputTags: {CounterDirection.outward}); + counterintf = MyCounterInterface(width: intf.width) + ..connectIO(this, intf, + inputTags: {CounterDirection.inward, CounterDirection.misc}, + outputTags: {CounterDirection.outward}); _buildLogic(); } 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..f84de84ab 100644 --- a/example/example.dart +++ b/example/example.dart @@ -11,35 +11,16 @@ // allow `print` messages (disable lint): // ignore_for_file: avoid_print -// Import necessary dart packages for this file. +// Import necessary dart pacakges for this file. import 'dart:async'; // Import the ROHD package. import 'package:rohd/rohd.dart'; -// Define a class Counter that extends ROHD's abstract Module class. -class Counter extends Module { - // For convenience, map interesting outputs to short variable names for - // consumers of this module. - Logic get val => output('val'); - - // This counter supports any width, determined at run-time. - final int width; - - Counter(Logic en, Logic reset, Logic clk, - {this.width = 8, super.name = 'counter'}) { - // Register inputs and outputs of the module in the constructor. - // Module logic must consume registered inputs and output to registered - // outputs. - en = addInput('en', en); - reset = addInput('reset', reset); - clk = addInput('clk', clk); - addOutput('val', width: width); - - // We can use the `flop` function to automate creation of a `Sequential`. - val <= flop(clk, reset: reset, en: en, val + 1); - } -} +// Re-export the Counter module from the library examples so that +// existing tests that `import 'example/example.dart'` still see it. +import 'package:rohd/src/examples/oven_fsm_modules.dart' show Counter; +export 'package:rohd/src/examples/oven_fsm_modules.dart' show Counter; // Let's simulate with this counter a little, generate a waveform, and take a // look at generated SystemVerilog. @@ -61,7 +42,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,14 +51,15 @@ 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, // just for this example to make it easier to see before we look at waves. if (!noPrint) { - counter.val.changed - .listen((e) => print('@${Simulator.time}: Value changed: $e')); + counter.val.changed.listen( + (e) => print('@${Simulator.time}: Value changed: $e'), + ); } // Start off with a disabled counter and asserting reset at the start. @@ -115,7 +97,9 @@ Future main({bool noPrint = false}) async { // We can take a look at the waves now. if (!noPrint) { - print('To view waves, check out waves.vcd with a waveform viewer' - ' (e.g. `gtkwave waves.vcd`).'); + print( + 'To view waves, check out waves.vcd with a waveform viewer' + ' (e.g. `gtkwave waves.vcd`).', + ); } } diff --git a/example/filter_bank.dart b/example/filter_bank.dart new file mode 100644 index 000000000..0b3803c4c --- /dev/null +++ b/example/filter_bank.dart @@ -0,0 +1,118 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank.dart +// A polyphase FIR filter bank design example exercising: +// - Deep hierarchy with shared sub-module definitions +// - Interface (FilterDataInterface) +// - LogicStructure (FilterSample) +// - LogicArray (coefficient storage) +// - Pipeline (pipelined MAC accumulation) +// - FiniteStateMachine (FilterController) +// +// The filter bank has two channels that share an identical MacUnit definition. +// A controller FSM sequences: idle → loading → running → draining → done. +// +// 2026 March 26 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; + +// Import module definitions. +import 'package:rohd/src/examples/filter_bank_modules.dart'; + +// Re-export so downstream consumers (e.g. devtools loopback) can use. +export 'package:rohd/src/examples/filter_bank_modules.dart'; + +// ────────────────────────────────────────────────────────────────── +// Standalone simulation entry point +// ────────────────────────────────────────────────────────────────── + +Future main({bool noPrint = false}) async { + const dataWidth = 16; + const numTaps = 3; + + // Low-pass-ish coefficients (scaled integers) + const coeffs0 = [1, 2, 1]; // channel 0: symmetric LPF kernel + const coeffs1 = [1, -2, 1]; // channel 1: high-pass kernel + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [coeffs0, coeffs1], + ); + + // Before we can simulate or generate code, we need to build it. + await dut.build(); + + // Set a maximum time for the simulation so it doesn't keep running forever. + Simulator.setMaxSimTime(500); + + // Attach a waveform dumper so we can see what happens. + if (!noPrint) { + WaveformService(dut, outputPath: 'filter_bank.vcd'); + } + + // Kick off the simulation. + unawaited(Simulator.run()); + + // ── Reset ── + reset.inject(1); + start.inject(0); + samples[0].data.inject(0); + samples[0].valid.inject(0); + samples[1].data.inject(0); + samples[1].valid.inject(0); + inputDone.inject(0); + + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + + // ── Start filtering ── + await clk.nextPosedge; + start.inject(1); + await clk.nextPosedge; + start.inject(0); + samples[0].valid.inject(1); + samples[1].valid.inject(1); + + // ── Feed sample stream: impulse response test ── + // Send a single '1' followed by zeros to get the impulse response + samples[0].data.inject(1); + samples[1].data.inject(1); + await clk.nextPosedge; + + for (var i = 0; i < 8; i++) { + samples[0].data.inject(0); + samples[1].data.inject(0); + await clk.nextPosedge; + } + + // ── Signal end of input ── + samples[0].valid.inject(0); + samples[1].valid.inject(0); + inputDone.inject(1); + await clk.nextPosedge; + inputDone.inject(0); + + // ── Wait for drain ── + for (var i = 0; i < 15; i++) { + await clk.nextPosedge; + } + + await Simulator.endSimulation(); +} diff --git a/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..a68a575bb 100644 --- a/example/oven_fsm.dart +++ b/example/oven_fsm.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // oven_fsm.dart @@ -14,175 +14,12 @@ import 'dart:async'; import 'package:rohd/rohd.dart'; -// Import the counter module implement in example.dart. -import './example.dart'; +// Import module definitions (Counter, OvenModule, enums). +import 'package:rohd/src/examples/oven_fsm_modules.dart'; -// Enumerated type named `OvenState` with four possible states: -// `standby`, `cooking`,`paused`, and `completed`. -enum OvenState { standby, cooking, paused, completed } - -// One-hot encoded `Button` using dart enhanced enums. -// Represent start, pause, and resume as integer value 0, 1, -// and 2 respectively. -enum Button { - start(value: 0), - pause(value: 1), - resume(value: 2); - - const Button({required this.value}); - - final int value; -} - -// One-hot encoded `LEDLight` using dart enhanced enums. -// Represent yellow, blue, red, and green as integer value 0, 1, -// 2, and 3 respectively. -enum LEDLight { - yellow(value: 0), - blue(value: 1), - red(value: 2), - green(value: 3); - - const LEDLight({required this.value}); - - final int value; -} - -// Define a class OvenModule that extends ROHD's abstract Module class. -class OvenModule extends Module { - // A private variable with type FiniteStateMachine `_oven`. - // - // Use `late` to indicate that the value will not be null - // and will be assign in the later section. - late FiniteStateMachine _oven; - - // We can expose an LED light output as a getter to retrieve it value. - Logic get led => output('led'); - - // This oven module receives a `button` and a `reset` input from runtime. - OvenModule(Logic button, Logic reset, Logic clk) : super(name: 'OvenModule') { - // Register inputs and outputs of the module in the constructor. - // Module logic must consume registered inputs and output to registered - // outputs. `led` output also added as the output port. - button = addInput('button', button, width: button.width); - reset = addInput('reset', reset); - clk = addInput('clk', clk); - final led = addOutput('led', width: button.width); - - // Register local signals, `counterReset` and `en` - // for Counter module. - final counterReset = Logic(name: 'counter_reset'); - final en = Logic(name: 'counter_en'); - - // An internal counter module that will be used to time the cooking state. - // Receive `en`, `counterReset` and `clk` as input. - final counter = Counter(en, counterReset, clk, name: 'counter_module'); - - // A list of `OvenState` that describe the FSM. Note that - // `OvenState` consists of identifier, events and actions. We - // can think of `identifier` as the state name, `events` is a map of event - // that trigger next state. `actions` is the behaviour of current state, - // like what is the actions need to be shown separate current state with - // other state. Represented as List of conditionals to be executed. - final states = [ - // identifier: standby state, represent by `OvenState.standby`. - State(OvenState.standby, - // events: - // When the button `start` is pressed during standby state, - // OvenState will changed to `OvenState.cooking` state. - events: { - Logic(name: 'button_start') - ..gets(button - .eq(Const(Button.start.value, width: button.width))): - OvenState.cooking, - }, - // actions: - // During the standby state, `led` is change to blue; timer's - // `counterReset` is set to 1 (Reset the timer); - // timer's `en` is set to 0 (Disable value update). - actions: [ - led < LEDLight.blue.value, - counterReset < 1, - en < 0, - ]), - - // identifier: cooking state, represent by `OvenState.cooking`. - State(OvenState.cooking, - // events: - // When the button `paused` is pressed during cooking state, - // OvenState will changed to `OvenState.paused` state. - // - // When the button `counter` time is elapsed during cooking state, - // OvenState will changed to `OvenState.completed` state. - events: { - Logic(name: 'button_pause') - ..gets(button - .eq(Const(Button.pause.value, width: button.width))): - OvenState.paused, - Logic(name: 'counter_time_complete')..gets(counter.val.eq(4)): - OvenState.completed - }, - // actions: - // During the cooking state, `led` is change to yellow; timer's - // `counterReset` is set to 0 (Do not reset); - // timer's `en` is set to 1 (Enable value update). - actions: [ - led < LEDLight.yellow.value, - counterReset < 0, - en < 1, - ]), - - // identifier: paused state, represent by `OvenState.paused`. - State(OvenState.paused, - // events: - // When the button `resume` is pressed during paused state, - // OvenState will changed to `OvenState.cooking` state. - events: { - Logic(name: 'button_resume') - ..gets(button - .eq(Const(Button.resume.value, width: button.width))): - OvenState.cooking - }, - // actions: - // During the paused state, `led` is change to red; timer's - // `counterReset` is set to 0 (Do not reset); - // timer's `en` is set to 0 (Disable value update). - actions: [ - led < LEDLight.red.value, - counterReset < 0, - en < 0, - ]), - - // identifier: completed state, represent by `OvenState.completed`. - State(OvenState.completed, - // events: - // When the button `start` is pressed during completed state, - // OvenState will changed to `OvenState.cooking` state. - events: { - Logic(name: 'button_start') - ..gets(button - .eq(Const(Button.start.value, width: button.width))): - OvenState.cooking - }, - // actions: - // During the start state, `led` is change to green; timer's - // `counterReset` is set to 1 (Reset value); - // timer's `en` is set to 0 (Disable value update). - actions: [ - led < LEDLight.green.value, - counterReset < 1, - en < 0, - ]) - ]; - - // Assign the _oven FiniteStateMachine object to private variable declared. - _oven = - FiniteStateMachine(clk, reset, OvenState.standby, states); - } - - // An oven FiniteStateMachine that represent in getter. - FiniteStateMachine get ovenStateMachine => _oven; -} +// Re-export module definitions so test files that import this file +// get access to OvenModule, OvenState, Button, LEDLight, etc. +export 'package:rohd/src/examples/oven_fsm_modules.dart' hide Counter; /// A helper function to wait for a number of cycles. Future waitCycles(Logic clk, int numCycles) async { @@ -225,7 +62,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..35501694c 100644 --- a/example/tree.dart +++ b/example/tree.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // tree.dart @@ -13,6 +13,13 @@ import 'package:rohd/rohd.dart'; +// Import module definition. +import 'package:rohd/src/examples/tree_modules.dart'; + +// Re-export module definition so test files that import this file +// get access to TreeOfTwoInputModules. +export 'package:rohd/src/examples/tree_modules.dart'; + /// The below example demonstrates some aspects of the power of ROHD where /// writing equivalent design code in SystemVerilog can be challenging or /// impossible. The example is a port from an example used by Chisel. @@ -35,36 +42,6 @@ import 'package:rohd/rohd.dart'; /// number of inputs and different logic without any explicit /// parameterization. -class TreeOfTwoInputModules extends Module { - final Logic Function(Logic a, Logic b) _op; - final List _seq = []; - Logic get out => output('out'); - - TreeOfTwoInputModules(List seq, this._op) - : super(name: 'tree_of_two_input_modules') { - if (seq.isEmpty) { - throw Exception("Don't use TreeOfTwoInputModules with an empty sequence"); - } - - for (var i = 0; i < seq.length; i++) { - _seq.add(addInput('seq$i', seq[i], width: seq[i].width)); - } - addOutput('out', width: seq[0].width); - - if (_seq.length == 1) { - out <= _seq[0]; - } else { - final a = TreeOfTwoInputModules( - _seq.getRange(0, _seq.length ~/ 2).toList(), _op) - .out; - final b = TreeOfTwoInputModules( - _seq.getRange(_seq.length ~/ 2, _seq.length).toList(), _op) - .out; - out <= _op(a, b); - } - } -} - Future main({bool noPrint = false}) async { // You could instantiate this module with some code such as: final tree = TreeOfTwoInputModules( @@ -85,7 +62,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..5db2cfa8e 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,10 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/inspector_service.dart'; +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 +16,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..6114a2374 --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,72 @@ +// 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'; + +/// 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..3e06605b8 --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,426 @@ +// 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/examples/filter_bank_modules.dart b/lib/src/examples/filter_bank_modules.dart new file mode 100644 index 000000000..10ec1d329 --- /dev/null +++ b/lib/src/examples/filter_bank_modules.dart @@ -0,0 +1,937 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_modules.dart +// Module class definitions for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick +// +// Architecture: each FilterChannel uses a single MacUnit that is +// time-multiplexed across taps. A tap counter sequences CoeffBank +// and a delay-line mux so the MAC accumulates one tap per clock cycle. +// After numTaps cycles the accumulated result is latched as the output +// sample and the accumulator resets for the next input sample. +// +// ROHD features exercised: +// - LogicStructure (FilterSample) +// - Interface (FilterDataInterface) +// - LogicArray (CoeffBank coefficient ROM, delay line) +// - Pipeline (MacUnit multiply-accumulate) +// - FiniteStateMachine (FilterController) +// - Multiple instantiation (two FilterChannels share one definition) +// +// Separated from filter_bank.dart so these classes can be imported +// in web-targeted code (no dart:io dependency). +// +// 2026 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +// ────────────────────────────────────────────────────────────────── +// LogicStructure: a typed sample word carrying data + valid + channel +// ────────────────────────────────────────────────────────────────── + +/// A structured signal bundling a data sample with metadata. +/// +/// Packs three fields — [data], and [valid] — into a single +/// bus that can be driven and sampled as a unit. Used throughout the +/// [FilterBank] to carry tagged samples between modules. +class FilterSample extends LogicStructure { + /// The sample data word. + late final Logic data; + + /// Whether this sample is valid. + late final Logic valid; + + /// Creates a [FilterSample] with the given [dataWidth] (default 16) + /// and optional [name]. + FilterSample({int dataWidth = 16, String? name}) + : super( + [ + Logic(name: 'data', width: dataWidth), + Logic(name: 'valid'), + ], + name: name ?? 'filter_sample', + ) { + data = elements[0]; + valid = elements[1]; + } + + // Private constructor for clone to share element structure. + FilterSample._clone(super.elements, {required super.name}) { + data = elements[0]; + valid = elements[1]; + } + + @override + + /// Returns a structural clone of this sample, preserving element names. + FilterSample clone({String? name}) => FilterSample._clone( + elements.map((e) => e.clone(name: e.name)), + name: name ?? this.name, + ); +} + +// ────────────────────────────────────────────────────────────────── +// Interface: tagged port bundle for filter data I/O +// ────────────────────────────────────────────────────────────────── + +/// Tags for grouping port directions in [FilterDataInterface]. +enum FilterPortTag { + /// Ports carrying data into the filter (`sampleIn`, `validIn`). + inputPorts, + + /// Ports carrying data out of the filter (`dataOut`, `validOut`). + outputPorts, +} + +/// An interface carrying sample data and control into/out of filter modules. +/// +/// Groups ports by [FilterPortTag] so that [connectIO] can wire +/// inputs and outputs in a single call. +class FilterDataInterface extends Interface { + /// Input sample data bus. + Logic get sampleIn => port('sampleIn'); + + /// Input valid strobe. + Logic get validIn => port('validIn'); + + /// Output filtered data bus. + Logic get dataOut => port('dataOut'); + + /// Output valid strobe. + Logic get validOut => port('validOut'); + + /// The data width used by this interface. + final int _dataWidth; + + /// Creates a [FilterDataInterface] with the given [dataWidth] + /// (default 16 bits). + FilterDataInterface({int dataWidth = 16}) : _dataWidth = dataWidth { + setPorts([ + Logic.port('sampleIn', dataWidth), + Logic.port('validIn'), + ], [ + FilterPortTag.inputPorts + ]); + + setPorts([ + Logic.port('dataOut', dataWidth), + Logic.port('validOut'), + ], [ + FilterPortTag.outputPorts + ]); + } + + @override + + /// Returns a new interface with the same data width. + FilterDataInterface clone() => FilterDataInterface(dataWidth: _dataWidth); +} + +// ────────────────────────────────────────────────────────────────── +// CoeffBank: stores FIR tap coefficients in a LogicArray +// ────────────────────────────────────────────────────────────────── + +/// A coefficient storage module backed by a [LogicArray] input port. +/// +/// Accepts a [LogicArray] of per-tap coefficients via [addInputArray] +/// and a tap index, then mux-selects the corresponding coefficient. +class CoeffBank extends Module { + /// The coefficient value at the selected index. + Logic get coeffOut => output('coeffOut'); + + /// The per-tap coefficient array (registered input port). + @protected + LogicArray get coeffArray => input('coeffArray') as LogicArray; + + /// The tap index input. + @protected + Logic get tapIndex => input('tapIndex'); + + /// Number of taps. + final int numTaps; + + /// Data width. + final int dataWidth; + + /// Creates a [CoeffBank] with [numTaps] taps at [dataWidth] bits. + /// + /// [coefficients] is a [LogicArray] with one element per tap — + /// registered as an input port via [addInputArray]. + /// [tapIndex] selects the active coefficient. + CoeffBank(Logic tapIndex, LogicArray coefficients, + {required this.numTaps, + required this.dataWidth, + super.name = 'CoeffBank'}) + : super(definitionName: 'CoeffBank_T${numTaps}_W$dataWidth') { + // Register ports + tapIndex = addInput('tapIndex', tapIndex, width: tapIndex.width); + final coeffArray = addInputArray('coeffArray', coefficients, + dimensions: [numTaps], elementWidth: dataWidth); + final coeffOut = addOutput('coeffOut', width: dataWidth); + + // Mux-chain ROM: priority-select coefficient by tap index. + Logic selected = Const(0, width: dataWidth); + for (var i = numTaps - 1; i >= 0; i--) { + selected = mux( + tapIndex.eq(Const(i, width: tapIndex.width)).named('tapMatch$i'), + coeffArray.elements[i], + selected, + ); + } + coeffOut <= selected; + } +} + +// ────────────────────────────────────────────────────────────────── +// MacUnit: a single multiply-accumulate pipeline stage +// ────────────────────────────────────────────────────────────────── + +/// A pipelined multiply-accumulate unit. +/// +/// Pipeline stage 0: multiply sample × coefficient +/// Pipeline stage 1: add product to running accumulator +class MacUnit extends Module { + /// Accumulated result. + Logic get result => output('result'); + + /// Sample data input. + @protected + Logic get sampleInPin => input('sampleIn'); + + /// Coefficient input. + @protected + Logic get coeffInPin => input('coeffIn'); + + /// Accumulator input. + @protected + Logic get accumInPin => input('accumIn'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Data width. + final int dataWidth; + + /// Creates a [MacUnit] that multiplies [sampleIn] by [coeffIn] in + /// stage 0 and adds the product to [accumIn] in stage 1. + /// + /// [clk], [reset], and [enable] control the pipeline registers. + MacUnit(Logic sampleIn, Logic coeffIn, Logic accumIn, Logic clk, Logic reset, + Logic enable, + {required this.dataWidth, super.name = 'MacUnit'}) + : super(definitionName: 'MacUnit_W$dataWidth') { + sampleIn = addInput('sampleIn', sampleIn, width: dataWidth); + coeffIn = addInput('coeffIn', coeffIn, width: dataWidth); + accumIn = addInput('accumIn', accumIn, width: dataWidth); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + final result = addOutput('result', width: dataWidth); + + // A 2-stage pipeline: multiply, then accumulate + final pipe = Pipeline( + clk, + reset: reset, + stages: [ + // Stage 0: multiply + (p) => [ + // Product = sample * coefficient (truncated to dataWidth) + p.get(sampleIn) < + (p.get(sampleIn) * p.get(coeffIn)).named('product'), + ], + // Stage 1: accumulate + (p) => [ + p.get(sampleIn) < + (p.get(sampleIn) + p.get(accumIn)).named('macSum'), + ], + ], + signals: [sampleIn, coeffIn, accumIn], + ); + + result <= pipe.get(sampleIn); + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterChannel: one polyphase FIR channel with time-multiplexed MAC +// ────────────────────────────────────────────────────────────────── + +/// A single polyphase FIR filter channel with [numTaps] taps. +/// +/// Uses a [FilterDataInterface] for its sample I/O ports. +/// +/// Architecture: +/// - A delay line (shift register) captures incoming samples. +/// - A tap counter cycles 0 … numTaps-1 each sample period. +/// - [CoeffBank] provides the coefficient for the current tap. +/// - A mux selects the delay-line sample for the current tap. +/// - A single [MacUnit] multiplies the selected sample by the +/// coefficient and adds it to a running accumulator. +/// - After all taps are processed the accumulator is latched as +/// the output and the accumulator resets for the next sample. +class FilterChannel extends Module { + /// The data interface for this channel (internal use only). + @protected + late final FilterDataInterface intf; + + /// Filtered output. + Logic get dataOut => intf.dataOut; + + /// Output valid. + Logic get validOut => intf.validOut; + + /// Number of FIR taps in this channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Creates a [FilterChannel] with [numTaps] taps at [dataWidth] bits. + /// + /// [srcIntf] provides the sample/valid input ports. [coefficients] + /// supplies per-tap constant coefficients. + FilterChannel( + FilterDataInterface srcIntf, + Logic clk, + Logic reset, + Logic enable, { + required this.numTaps, + required this.dataWidth, + required List coefficients, + super.name = 'FilterChannel', + }) : super(definitionName: 'FilterChannel_T${numTaps}_W$dataWidth') { + // Connect the Interface — creates module input/output ports + intf = FilterDataInterface(dataWidth: dataWidth) + ..connectIO(this, srcIntf, + inputTags: [FilterPortTag.inputPorts], + outputTags: [FilterPortTag.outputPorts]); + + final sampleIn = intf.sampleIn; + final validIn = intf.validIn; + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + + final tapIdxWidth = _bitsFor(numTaps); + + // ── Delay line (shift register via explicit flop bank + gates) ── + // AND gate: shift enable = enable & validIn & tapCounter==0 + // Samples shift in only when starting a new accumulation cycle. + final tapCounter = Logic(width: tapIdxWidth, name: 'tapCounter'); + final atFirstTap = + tapCounter.eq(Const(0, width: tapIdxWidth)).named('atFirstTap'); + final shiftEn = Logic(name: 'shiftEn'); + shiftEn <= (enable & validIn).named('enableAndValid') & atFirstTap; + + // LogicArray-backed delay line: one element per tap register. + final delayLine = LogicArray([numTaps], dataWidth, name: 'delayLine'); + for (var i = 0; i < numTaps; i++) { + final tapInput = (i == 0) ? sampleIn : delayLine.elements[i - 1]; + // Mux: hold current value or shift in new sample + final tapNext = Logic(width: dataWidth, name: 'nextTap$i'); + tapNext <= mux(shiftEn, tapInput, delayLine.elements[i]); + // Flop: register the next-state value + delayLine.elements[i] <= flop(clk, reset: reset, tapNext); + } + + // ── Coefficient bank — driven by tapCounter ── + // Build a LogicArray of constants from the coefficient list and + // pass it as an input port to CoeffBank (demonstrates addInputArray + // on a sub-module). + final coeffArray = LogicArray([numTaps], dataWidth, name: 'coeffArray'); + for (var i = 0; i < numTaps; i++) { + coeffArray.elements[i] <= Const(coefficients[i], width: dataWidth); + } + + final coeffBank = CoeffBank( + tapCounter, + coeffArray, + numTaps: numTaps, + dataWidth: dataWidth, + name: 'coeffBank', + ); + + // ── Delay-line mux — select sample for current tap ── + var selectedSample = delayLine.elements[0]; + for (var i = 1; i < numTaps; i++) { + final tapSelect = + tapCounter.eq(Const(i, width: tapIdxWidth)).named('tapSelect$i'); + selectedSample = mux(tapSelect, delayLine.elements[i], selectedSample) + .named('tapMux$i'); + } + + // ── Running accumulator (feedback register) ── + final accumReg = Logic(width: dataWidth, name: 'accumReg'); + // Reset accumulator at the start of each new sample (tap 0). + // Combinational block: equivalent to `always_comb` in SystemVerilog. + final accumFeedback = Logic(width: dataWidth, name: 'accumFeedback'); + Combinational([ + If(atFirstTap, then: [ + accumFeedback < Const(0, width: dataWidth), + ], orElse: [ + accumFeedback < accumReg, + ]), + ]); + + // ── Single MAC unit — time-multiplexed across taps ── + final mac = MacUnit( + selectedSample, + coeffBank.coeffOut, + accumFeedback, + clk, + reset, + enable, + dataWidth: dataWidth, + name: 'mac', + ); + + // Register the MAC result for accumulator feedback. + accumReg <= flop(clk, reset: reset, mac.result); + + // ── Tap counter: cycles 0 … numTaps-1 while enabled ── + // Sequential block: equivalent to `always_ff @(posedge clk)` in SV. + // When enabled, the counter increments and wraps at numTaps-1. + // When disabled, it resets to 0. + final lastTap = + tapCounter.eq(Const(numTaps - 1, width: tapIdxWidth)).named('lastTap'); + Sequential(clk, reset: reset, [ + If(enable, then: [ + If(lastTap, then: [ + tapCounter < Const(0, width: tapIdxWidth), + ], orElse: [ + tapCounter < tapCounter + Const(1, width: tapIdxWidth), + ]), + ], orElse: [ + tapCounter < Const(0, width: tapIdxWidth), + ]), + ]); + + // ── Output latch: capture accumulator when all taps processed ── + // The MAC pipeline has 2 stages, so the result is ready 2 cycles + // after the last tap enters. A 2-stage shift register of lastTap + // creates the latch strobe. + final lastTapD1 = Logic(name: 'lastTapD1'); + final lastTapD2 = Logic(name: 'lastTapD2'); + final outputReg = Logic(width: dataWidth, name: 'outputReg'); + + // Sequential block with If: latch strobe delay and output register. + Sequential(clk, reset: reset, [ + lastTapD1 < lastTap, + lastTapD2 < lastTapD1, + If(lastTapD2, then: [ + outputReg < accumReg, + ]), + ]); + + // ── Valid pipeline: track whether we have a valid output ── + // validIn is high during data injection. After the MAC pipeline + // latency (numTaps + 2 cycles), outputs become valid. + final validPipe = Logic(name: 'validPipe'); + final outputReady = (lastTapD2 & enable).named('outputReady'); + + // Sequential block: register the valid strobe and hold it. + Sequential(clk, reset: reset, [ + If(enable, then: [ + validPipe < outputReady, + ]), + ]); + + // Combinational block: gate the output to zero when not valid. + final dataOut = intf.dataOut; + final validOut = intf.validOut; + Combinational([ + If(validPipe, then: [ + dataOut < outputReg, + ], orElse: [ + dataOut < Const(0, width: dataWidth), + ]), + validOut < validPipe, + ]); + } + + /// Minimum bits needed to represent [n] values. + static int _bitsFor(int n) { + if (n <= 1) { + return 1; + } + var bits = 0; + var v = n - 1; + while (v > 0) { + bits++; + v >>= 1; + } + return bits; + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterController: FSM sequencing the filter bank +// ────────────────────────────────────────────────────────────────── + +/// States for the [FilterController] finite state machine. +enum FilterState { + /// Waiting for the start signal. + idle, + + /// Accepting initial samples into the delay line. + loading, + + /// Normal filtering operation. + running, + + /// Flushing the pipeline after the input stream ends. + draining, + + /// Processing complete. + done, +} + +/// Controls the filter bank operation via a [FiniteStateMachine]. +/// +/// - idle: waiting for start signal +/// - loading: accepting initial samples into delay line +/// - running: normal filtering +/// - draining: flushing pipeline after input stream ends +/// - done: processing complete +class FilterController extends Module { + /// Encoded FSM state (3 bits). + Logic get state => output('state'); + + /// High while the filter channels should be processing. + Logic get filterEnable => output('filterEnable'); + + /// High during the initial sample-loading phase. + Logic get loadingPhase => output('loadingPhase'); + + /// Asserted when the filter bank has finished processing. + Logic get doneFlag => output('doneFlag'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input valid. + @protected + Logic get inputValidPin => input('inputValid'); + + /// Input done. + @protected + Logic get inputDonePin => input('inputDone'); + + late final FiniteStateMachine _fsm; + + /// Returns the FSM's current state index for a given [FilterState]. + int? getStateIndex(FilterState s) => _fsm.getStateIndex(s); + + /// Creates a [FilterController] that sequences the filter bank. + /// + /// After [start] is asserted the FSM moves through loading → running + /// → draining (for [drainCycles] cycles) → done. + FilterController( + Logic clk, Logic reset, Logic start, Logic inputValid, Logic inputDone, + {required int drainCycles, super.name = 'FilterController'}) + : super(definitionName: 'FilterController') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputValid = addInput('inputValid', inputValid); + inputDone = addInput('inputDone', inputDone); + + final filterEnable = addOutput('filterEnable'); + final loadingPhase = addOutput('loadingPhase'); + final doneFlag = addOutput('doneFlag'); + final state = addOutput('state', width: 3); + + // Drain counter + final drainCount = Logic(width: 8, name: 'drainCount'); + final drainDone = + drainCount.eq(Const(drainCycles, width: 8)).named('drainDone'); + + _fsm = FiniteStateMachine( + clk, + reset, + FilterState.idle, + [ + State( + FilterState.idle, + events: { + start: FilterState.loading, + }, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.loading, + events: { + inputValid: FilterState.running, + }, + actions: [ + filterEnable < 1, + loadingPhase < 1, + doneFlag < 0, + ], + ), + State( + FilterState.running, + events: { + inputDone: FilterState.draining, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.draining, + events: { + drainDone: FilterState.done, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.done, + events: {}, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 1, + ], + ), + ], + ); + + state <= _fsm.currentState.zeroExtend(state.width); + + // Drain counter: Sequential block increments while draining, + // resets to zero otherwise. + final drainIdx = _fsm.getStateIndex(FilterState.draining)!; + final isDraining = Logic(name: 'isDraining'); + isDraining <= _fsm.currentState.eq(Const(drainIdx, width: _fsm.stateWidth)); + + Sequential(clk, reset: reset, [ + If(isDraining, then: [ + drainCount < drainCount + Const(1, width: 8), + ], orElse: [ + drainCount < Const(0, width: 8), + ]), + ]); + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterBank: top-level 2-channel polyphase FIR filter +// ────────────────────────────────────────────────────────────────── + +/// A 2-channel polyphase FIR filter bank. +/// +/// Hierarchy: +/// ```text +/// FilterBank (top) +/// ├── FilterController (FSM) +/// ├── FilterChannel 'ch0' +/// │ ├── CoeffBank (coefficient ROM via LogicArray + mux chain) +/// │ └── MacUnit 'mac' (pipelined multiply-accumulate) +/// └── FilterChannel 'ch1' +/// ├── CoeffBank +/// └── MacUnit 'mac' +/// ``` +/// +/// Each channel time-multiplexes a single MacUnit across all taps, +/// sequenced by a tap counter that drives the CoeffBank tap index +/// and a delay-line sample mux. +/// +/// Uses: +/// - [FilterDataInterface] for I/O port bundles +/// - [FilterSample] LogicStructure for structured sample signals +/// - [LogicArray] in CoeffBank for coefficient storage +/// - [Pipeline] in MacUnit for pipelined MAC +/// - [FiniteStateMachine] in FilterController for sequencing +/// - Multiple instantiation: two [FilterChannel]s share one definition +/// - [LogicNet] / [addInOut] for bidirectional shared data bus + +// ────────────────────────────────────────────────────────────────── +// SharedDataBus: bidirectional port for coefficient/status I/O +// ────────────────────────────────────────────────────────────────── + +/// A module with a bidirectional data bus for loading/reading data. +/// +/// In real hardware, a shared data bus is common for: +/// - Loading filter coefficients from external memory +/// - Reading diagnostic status or filter output snapshots +/// +/// Direction is controlled by `writeEnable`: when high, the module's +/// internal [TriStateBuffer] drives `storedValue` onto `dataBus`; +/// when low, the external driver owns the bus and the module latches +/// the incoming value into a register. +/// +/// Exercises `addInOut` / `LogicNet` / [TriStateBuffer] / inout port +/// direction through the full ROHD stack: synthesis, hierarchy, +/// waveform capture, and DevTools rendering. +class SharedDataBus extends Module { + /// The bidirectional data bus port. + Logic get dataBus => inOut('dataBus'); + + /// The stored value (latched when the bus is driven externally). + Logic get storedValue => output('storedValue'); + + /// Write-enable input. + @protected + Logic get writeEnablePin => input('writeEnable'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Data width in bits. + final int dataWidth; + + /// Creates a [SharedDataBus] with a [dataWidth]-bit bidirectional port. + /// + /// [dataBusNet] is the external [LogicNet] to connect. + /// [writeEnable] controls bus direction: 1 = module drives bus, + /// 0 = external drives bus (module reads). + /// [clk] and [reset] provide synchronous storage. + SharedDataBus( + LogicNet dataBusNet, + Logic writeEnable, + Logic clk, + Logic reset, { + required this.dataWidth, + super.name = 'SharedDataBus', + }) : super(definitionName: 'SharedDataBus') { + final bus = addInOut('dataBus', dataBusNet, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + final storedValue = addOutput('storedValue', width: dataWidth); + + // Latch the bus value on clock edge when the external side is driving. + storedValue <= + flop( + clk, + bus, + reset: reset, + en: ~writeEnable, + resetValue: Const(0, width: dataWidth), + ); + + // Drive the latched value back onto the bus when writeEnable is high. + // TriStateBuffer drives its out (a LogicNet) with storedValue when + // enabled; otherwise it outputs high-Z. Joining out↔bus makes the + // two nets share the same wire. + TriStateBuffer(storedValue, enable: writeEnable, name: 'busDriver') + .out + .gets(bus); + } +} + +/// The top-level polyphase FIR filter bank. +class FilterBank extends Module { + /// Per-channel filtered outputs as a [LogicArray]. + /// + /// `channelOut.elements[i]` is the filtered output of channel `i`. + LogicArray get channelOut => output('channelOut') as LogicArray; + + /// Channel 0 filtered output (convenience getter). + Logic get out0 => channelOut.elements[0]; + + /// Channel 1 filtered output (convenience getter). + Logic get out1 => channelOut.elements[1]; + + /// Output valid (aligned with filtered outputs). + Logic get validOut => output('validOut'); + + /// Done signal from the controller FSM. + Logic get done => output('done'); + + /// Controller state (for debug visibility). + Logic get state => output('state'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input [FilterSample] port for channel [ch]. + @protected + FilterSample samplePin(int ch) => input('sample$ch') as FilterSample; + + /// Input-done strobe. + @protected + Logic get inputDonePin => input('inputDone'); + + /// Number of FIR taps per channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Number of filter channels. + final int numChannels; + + /// Creates a [FilterBank] with [numChannels] channels (default 2). + /// + /// Each channel has [numTaps] FIR taps at [dataWidth] bits. + /// [coefficients] is a list of per-channel coefficient lists — + /// `coefficients[i]` supplies the tap weights for channel `i`. + /// [samples] is a [LogicArray] with one element per channel. + /// [inputDone] when the input stream is complete. + /// + /// Optionally pass [dataBus] (a `LogicNet`) and [writeEnable] to + /// attach a bidirectional shared data bus via [SharedDataBus]. + /// The bus latches external data when [writeEnable] is low and + /// drives `storedValue` output. + FilterBank( + Logic clk, + Logic reset, + Logic start, + List samples, + Logic inputDone, { + required this.numTaps, + required this.dataWidth, + required List> coefficients, + this.numChannels = 2, + LogicNet? dataBus, + Logic? writeEnable, + super.name = 'FilterBank', + String? definitionName, + }) : super(definitionName: definitionName ?? 'FilterBank') { + if (coefficients.length != numChannels) { + throw Exception( + 'coefficients must have $numChannels entries (one per channel).'); + } + + // ── Register ports ── + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputDone = addInput('inputDone', inputDone); + + // One typed FilterSample input port per channel. + final inPorts = []; + for (var ch = 0; ch < numChannels; ch++) { + inPorts.add(addTypedInput('sample$ch', samples[ch])); + } + + final channelOut = addTypedOutput( + 'channelOut', + ({name = 'channelOut'}) => + LogicArray([numChannels], dataWidth, name: name)); + final validOut = addOutput('validOut'); + final done = addOutput('done'); + final state = addOutput('state', width: 3); + + // ── Controller FSM ── + // Drain cycles: numTaps cycles per accumulation + pipeline depth (2) + 1 + final controller = FilterController( + clk, + reset, + start, + inPorts[0].valid, // valid is shared across channels + inputDone, + drainCycles: numTaps + 3, + name: 'controller', + ); + + final filterEnable = controller.filterEnable; + + // ── Per-channel filter instantiation ── + final srcIntfs = []; + for (var ch = 0; ch < numChannels; ch++) { + final srcIntf = FilterDataInterface(dataWidth: dataWidth); + srcIntf.sampleIn <= inPorts[ch].data; + srcIntf.validIn <= inPorts[ch].valid; + + FilterChannel( + srcIntf, + clk, + reset, + filterEnable, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients[ch], + name: 'ch$ch', + ); + + srcIntfs.add(srcIntf); + } + + // ── Connect outputs ── + for (var ch = 0; ch < numChannels; ch++) { + channelOut.elements[ch] <= srcIntfs[ch].dataOut; + } + validOut <= srcIntfs[0].validOut; + done <= controller.doneFlag; + state <= controller.state; + + // ── Optional shared data bus (inOut port) ── + if (dataBus != null && writeEnable != null) { + final busPort = addInOut('dataBus', dataBus, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + final storedValue = addOutput('storedValue', width: dataWidth); + + final sharedBus = SharedDataBus( + LogicNet(name: 'busNet', width: dataWidth)..gets(busPort), + writeEnable, + clk, + reset, + dataWidth: dataWidth, + ); + storedValue <= sharedBus.storedValue; + } + } +} diff --git a/lib/src/examples/oven_fsm_modules.dart b/lib/src/examples/oven_fsm_modules.dart new file mode 100644 index 000000000..b1f18a3f5 --- /dev/null +++ b/lib/src/examples/oven_fsm_modules.dart @@ -0,0 +1,211 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// oven_fsm_modules.dart +// Web-safe module class definitions for the Oven FSM example. +// +// Extracted from example/oven_fsm.dart and example/example.dart so these +// classes can be imported in web-targeted code (no dart:io dependency). +// +// 2026 April +// Original authors: Yao Jing Quek, Max Korbel + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +// ────────────────────────────────────────────────────────────────── +// Counter (from example/example.dart) +// ────────────────────────────────────────────────────────────────── + +/// A simple 8-bit counter with enable and synchronous reset. +class Counter extends Module { + /// The current counter value. + Logic get val => output('val'); + + /// The enable input. + @protected + Logic get en => input('en'); + + /// The reset input. + @protected + Logic get resetPin => input('reset'); + + /// The clock input. + @protected + Logic get clkPin => input('clk'); + + /// Bit width of the counter (default 8). + final int width; + + /// Creates a [Counter] of [width] bits driven by [clk]. + /// + /// Increments on each rising edge when [en] is high. + /// [reset] synchronously clears the count to zero. + Counter( + Logic en, + Logic reset, + Logic clk, { + this.width = 8, + super.name = 'counter', + }) : super(definitionName: 'Counter_W$width') { + en = addInput('en', en); + reset = addInput('reset', reset); + clk = addInput('clk', clk); + addOutput('val', width: width); + + val <= flop(clk, reset: reset, en: en, val + 1); + } +} + +// ────────────────────────────────────────────────────────────────── +// Oven FSM enums +// ────────────────────────────────────────────────────────────────── + +/// Oven states: standby → cooking → paused → completed. +enum OvenState { + /// Waiting for the start button. + standby, + + /// Actively cooking (timer running). + cooking, + + /// Cooking paused (timer held). + paused, + + /// Cooking finished (timer expired). + completed, +} + +/// One-hot encoded button inputs. +enum Button { + /// Start or restart cooking. + start(value: 0), + + /// Pause cooking. + pause(value: 1), + + /// Resume from pause. + resume(value: 2); + + /// Creates a button with the given encoded [value]. + const Button({required this.value}); + + /// The encoded value for this button. + final int value; +} + +/// One-hot encoded LED output colors. +enum LEDLight { + /// Yellow — cooking in progress. + yellow(value: 0), + + /// Blue — standby. + blue(value: 1), + + /// Red — paused. + red(value: 2), + + /// Green — cooking complete. + green(value: 3); + + /// Creates an LED color with the given encoded [value]. + const LEDLight({required this.value}); + + /// The encoded value for this LED color. + final int value; +} + +// ────────────────────────────────────────────────────────────────── +// OvenModule +// ────────────────────────────────────────────────────────────────── + +/// A microwave oven FSM with 4 states and an internal timer counter. +/// +/// Inputs: +/// - `button` (2-bit): start / pause / resume +/// - `reset`: active-high synchronous reset +/// - `clk`: clock +/// +/// Outputs: +/// - `led` (2-bit): blue (standby), yellow (cooking), +/// red (paused), green (completed) +class OvenModule extends Module { + late final FiniteStateMachine _oven; + + /// The LED output encoding the current state. + Logic get led => output('led'); + + /// The button input. + @protected + Logic get button => input('button'); + + /// The reset input. + @protected + Logic get resetPin => input('reset'); + + /// The clock input. + @protected + Logic get clkPin => input('clk'); + + /// Creates an [OvenModule] controlled by [button] with [clk] and [reset]. + OvenModule(Logic button, Logic reset, Logic clk) + : super(name: 'oven', definitionName: 'OvenModule') { + button = addInput('button', button, width: button.width); + reset = addInput('reset', reset); + clk = addInput('clk', clk); + final led = addOutput('led', width: button.width); + + final counterReset = Logic(name: 'counter_reset'); + final en = Logic(name: 'counter_en'); + + final counter = Counter(en, counterReset, clk, name: 'counter_module'); + + final states = [ + State(OvenState.standby, events: { + Logic(name: 'button_start') + ..gets(button.eq(Const(Button.start.value, width: button.width))): + OvenState.cooking, + }, actions: [ + led < LEDLight.blue.value, + counterReset < 1, + en < 0, + ]), + State(OvenState.cooking, events: { + Logic(name: 'button_pause') + ..gets(button.eq(Const(Button.pause.value, width: button.width))): + OvenState.paused, + Logic(name: 'counter_time_complete')..gets(counter.val.eq(4)): + OvenState.completed, + }, actions: [ + led < LEDLight.yellow.value, + counterReset < 0, + en < 1, + ]), + State(OvenState.paused, events: { + Logic(name: 'button_resume') + ..gets( + button.eq(Const(Button.resume.value, width: button.width))): + OvenState.cooking, + }, actions: [ + led < LEDLight.red.value, + counterReset < 0, + en < 0, + ]), + State(OvenState.completed, events: { + Logic(name: 'button_start') + ..gets(button.eq(Const(Button.start.value, width: button.width))): + OvenState.cooking, + }, actions: [ + led < LEDLight.green.value, + counterReset < 1, + en < 0, + ]), + ]; + + _oven = + FiniteStateMachine(clk, reset, OvenState.standby, states); + } + + /// The internal [FiniteStateMachine] driving the oven states. + FiniteStateMachine get ovenStateMachine => _oven; +} diff --git a/lib/src/examples/tree_modules.dart b/lib/src/examples/tree_modules.dart new file mode 100644 index 000000000..96fc7d283 --- /dev/null +++ b/lib/src/examples/tree_modules.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// tree_modules.dart +// Web-safe module class definition for the Tree of Two-Input Modules example. +// +// Extracted from example/tree.dart so it can be imported in web-targeted code. +// +// 2026 April +// Original author: Max Korbel + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +// ────────────────────────────────────────────────────────────────── +// TreeOfTwoInputModules +// ────────────────────────────────────────────────────────────────── + +/// A logarithmic-height tree of arbitrary two-input/one-output modules. +/// +/// Recursively instantiates itself, splitting the input list in half at each +/// level. The operation [op] is applied to combine pairs of results. +class TreeOfTwoInputModules extends Module { + /// The combining operation (internal use only). + @protected + final Logic Function(Logic a, Logic b) op; + + final List _seq = []; + + /// The combined output of the tree. + Logic get out => output('out'); + + /// Creates a tree that reduces [seq] using [op]. + /// + /// Recursively splits [seq] in half until single elements remain, + /// then combines them pair-wise with the supplied operation. + TreeOfTwoInputModules(List seq, this.op) + : super( + name: 'tree_of_two_input_modules', + definitionName: 'TreeMax_N${seq.length}', + ) { + if (seq.isEmpty) { + throw Exception("Don't use TreeOfTwoInputModules with an empty sequence"); + } + + for (var i = 0; i < seq.length; i++) { + _seq.add(addInput('seq$i', seq[i], width: seq[i].width)); + } + addOutput('out', width: seq[0].width); + + if (_seq.length == 1) { + out <= _seq[0]; + } else { + final a = + TreeOfTwoInputModules(_seq.getRange(0, _seq.length ~/ 2).toList(), op) + .out; + final b = TreeOfTwoInputModules( + _seq.getRange(_seq.length ~/ 2, _seq.length).toList(), op) + .out; + out <= op(a, b); + } + } +} 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..981b03d85 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 = {}; @@ -106,7 +115,7 @@ abstract class Module { ..._inputs.values, ..._outputs.values, ..._inOuts.values, - ...internalSignals, + ...internalSignals ]); /// Accesses the [Logic] associated with this [Module]s [input] port @@ -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; @@ -288,8 +309,11 @@ abstract class Module { /// /// The hierarchy is built "bottom-up", so leaf-level [Module]s are built /// before the [Module]s which contain them. + /// + /// If [netlistOptions] is provided, a [NetlistService] is automatically + /// created and registered after the module hierarchy is constructed. @mustCallSuper - Future build() async { + Future build({NetlistOptions? netlistOptions}) async { if (hasBuilt) { throw Exception( 'This Module has already been built, and can only be built once.'); @@ -317,17 +341,21 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; + + // Optionally synthesize a netlist and register the service. + if (netlistOptions != null) { + NetlistService(this, options: netlistOptions); + } } /// Confirms that the post-[build] hierarchy is valid. /// /// - No module exists in two separate hierarchies. /// - No module is a submodule of itself. - void _checkValidHierarchy({ - required Map> visited, - List hierarchy = const [], - }) { + void _checkValidHierarchy( + {required Map> visited, + List hierarchy = const []}) { final newHierarchy = [...hierarchy, this]; if (hierarchy.contains(this)) { @@ -862,22 +890,18 @@ abstract class Module { /// /// Performs validation on overall width matching for [source], but not on /// [dimensions], [elementWidth], or [numUnpackedDimensions]. - LogicArray addInputArray( - String name, - Logic source, { - List dimensions = const [1], - int elementWidth = 1, - int numUnpackedDimensions = 0, - }) { + LogicArray addInputArray(String name, Logic source, + {List dimensions = const [1], + int elementWidth = 1, + int numUnpackedDimensions = 0}) { _checkForSafePortName(name); final inArr = LogicArray( - name: name, - dimensions, - elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.reserved, - ) + name: name, + dimensions, + elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.reserved) ..gets(source) ..setAllParentModule(this); @@ -987,21 +1011,19 @@ abstract class Module { /// named [name]. /// /// This is very similar to [addOutput], except for [LogicArray]s. - LogicArray addOutputArray( - String name, { - List dimensions = const [1], - int elementWidth = 1, - int numUnpackedDimensions = 0, - }) { + LogicArray addOutputArray(String name, + {List dimensions = const [1], + int elementWidth = 1, + int numUnpackedDimensions = 0}) { _checkForSafePortName(name); final outArr = LogicArray( - name: name, - dimensions, - elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.reserved, - )..setAllParentModule(this); + name: name, + dimensions, + elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.reserved) + ..setAllParentModule(this); _outputs[name] = outArr; @@ -1016,13 +1038,10 @@ abstract class Module { /// /// Performs validation on overall width matching for [source], but not on /// [dimensions], [elementWidth], or [numUnpackedDimensions]. - LogicArray addInOutArray( - String name, - Logic source, { - List dimensions = const [1], - int elementWidth = 1, - int numUnpackedDimensions = 0, - }) { + LogicArray addInOutArray(String name, Logic source, + {List dimensions = const [1], + int elementWidth = 1, + int numUnpackedDimensions = 0}) { _checkForSafePortName(name); // make sure we register all the _inOutDrivers properly @@ -1041,12 +1060,11 @@ abstract class Module { } final inOutArr = LogicArray.net( - name: name, - dimensions, - elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.reserved, - ) + name: name, + dimensions, + elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.reserved) ..gets(source) ..setAllParentModule(this); @@ -1113,23 +1131,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/signals/logic.dart b/lib/src/signals/logic.dart index 4c5f99e5e..b77c689c4 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -245,15 +245,8 @@ class Logic { /// /// The [naming] and [name], if unspecified, are chosen based on the rules in /// [Naming.chooseNaming] and [Naming.chooseName], respectively. - Logic({ - String? name, - int width = 1, - Naming? naming, - }) : this._( - name: name, - width: width, - naming: naming, - ); + Logic({String? name, int width = 1, Naming? naming}) + : this._(name: name, width: width, naming: naming); /// A cloning utility for [clone] and [named]. Logic _clone({String? name, Naming? naming}) => @@ -290,12 +283,8 @@ class Logic { /// An internal constructor for [Logic] which additional provides access to /// setting the [wire]. - Logic._({ - String? name, - int width = 1, - Naming? naming, - _Wire? wire, - }) : naming = Naming.chooseNaming(name, naming), + Logic._({String? name, int width = 1, Naming? naming, _Wire? wire}) + : naming = Naming.chooseNaming(name, naming), name = Naming.chooseName(name, naming), _wire = wire ?? _Wire(width: width) { if (width < 0) { @@ -314,13 +303,12 @@ class Logic { } return Logic( - name: name, - width: width, + name: name, + width: width, - // make port names mergeable so we don't duplicate the ports - // when calling connectIO - naming: Naming.mergeable, - ); + // make port names mergeable so we don't duplicate the ports + // when calling connectIO + naming: Naming.mergeable); } @override @@ -422,10 +410,7 @@ class Logic { // tell all downstream signals to update to the new wire as well final Iterable toUpdateWire; if (this is LogicNet) { - toUpdateWire = [ - ...dstConnections, - ...srcConnections, - ].where( + toUpdateWire = [...dstConnections, ...srcConnections].where( (connection) => connection._wire != _wire && connection is LogicNet); } else { toUpdateWire = dstConnections.where((element) => element is! LogicNet); @@ -482,10 +467,7 @@ class Logic { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); if (shamt != null) { - return [ - this[-1].replicate(shamt), - getRange(shamt), - ].swizzle(); + return [this[-1].replicate(shamt), getRange(shamt)].swizzle(); } } @@ -502,10 +484,7 @@ class Logic { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); if (shamt != null) { - return [ - getRange(0, -shamt), - Const(0, width: shamt), - ].swizzle(); + return [getRange(0, -shamt), Const(0, width: shamt)].swizzle(); } } @@ -522,10 +501,7 @@ class Logic { // many SV simulators don't support shifting of nets, so default this final shamt = _constShiftAmount(other); if (shamt != null) { - return [ - Const(0, width: shamt), - getRange(shamt), - ].swizzle(); + return [Const(0, width: shamt), getRange(shamt)].swizzle(); } } @@ -831,10 +807,7 @@ class Logic { throw Exception( 'New width $newWidth must be greater than or equal to width $width.'); } - return [ - Const(0, width: newWidth - width), - this, - ].swizzle(); + return [Const(0, width: newWidth - width), this].swizzle(); } /// Calculates the absolute value of a signal, assuming that the @@ -859,10 +832,7 @@ class Logic { if (width == 1) { return replicate(newWidth); } else if (newWidth > width) { - return [ - this[-1].replicate(newWidth - width), - this, - ].swizzle(); + return [this[-1].replicate(newWidth - width), this].swizzle(); } else if (newWidth == width) { return this; } @@ -896,7 +866,7 @@ class Logic { return [ getRange(startIndex + update.width, width), update, - getRange(0, startIndex), + getRange(0, startIndex) ].swizzle(); } @@ -953,18 +923,16 @@ class Logic { width: busList.first.width, naming: Naming.mergeable); - Combinational( - [ - Case( - this, - [ - for (var i = 0; i < busList.length; i++) - CaseItem(Const(i, width: width), [selected < busList[i]]) - ], - conditionalType: ConditionalType.unique, - defaultItem: [selected < (defaultValue ?? 0)]) - ], - ); + Combinational([ + Case( + this, + [ + for (var i = 0; i < busList.length; i++) + CaseItem(Const(i, width: width), [selected < busList[i]]) + ], + conditionalType: ConditionalType.unique, + defaultItem: [selected < (defaultValue ?? 0)]) + ]); return selected; } @@ -992,12 +960,8 @@ class Logic { } if (_subsetDriver == null) { - _subsetDriver = (isNet ? LogicArray.net : LogicArray.new)( - [width], - 1, - name: '${name}_subset', - naming: Naming.unnamed, - ); + _subsetDriver = (isNet ? LogicArray.net : LogicArray.new)([width], 1, + name: '${name}_subset', naming: Naming.unnamed); this <= _subsetDriver!; } diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index ef463b9bb..009cf326b 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -391,10 +391,8 @@ class LogicStructure implements Logic { newElement <= element.withSet( max(startIndex - index, 0), - update.getRange( - max(index - startIndex, 0), - min(index - startIndex + elementWidth, update.width), - )); + update.getRange(max(index - startIndex, 0), + min(index - startIndex + elementWidth, update.width))); } else { newElement <= element; } diff --git a/lib/src/synthesizers/netlist/leaf_cell_mapper.dart b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart new file mode 100644 index 000000000..606747dee --- /dev/null +++ b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart @@ -0,0 +1,486 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_cell_mapper.dart +// Maps ROHD leaf modules to Yosys-primitive cell representations. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// The result of mapping a leaf ROHD module to a Yosys-style cell. +typedef LeafCellMapping = ({ + String cellType, + Map portDirs, + Map> connections, + Map parameters, +}); + +/// Context provided to each leaf-cell mapping handler. +/// +/// Contains the module instance plus the raw ROHD port directions and +/// connections built by the synthesizer, so handlers can remap them to +/// Yosys-primitive port names. +class LeafCellContext { + /// The ROHD [Module] being mapped. + final Module module; + + /// Raw ROHD port-direction map (`{'portName': 'input'|'output'|'inout'}`). + final Map rawPortDirs; + + /// Raw ROHD connection map (`{'portName': [wireId, ...]}`). + final Map> rawConns; + + /// Creates a [LeafCellContext]. + const LeafCellContext(this.module, this.rawPortDirs, this.rawConns); + + // ── Shared helper methods ─────────────────────────────────────────── + + /// Find the first input port name matching [prefix]. + String? findInput(String prefix) { + for (final k in module.inputs.keys) { + if (k.startsWith(prefix)) { + return k; + } + } + return null; + } + + /// The first output port name, or `null` if there are none. + String? get firstOutput => + module.outputs.keys.isEmpty ? null : module.outputs.keys.first; + + /// The first input port name, or `null` if there are none. + String? get firstInput => + module.inputs.keys.isEmpty ? null : module.inputs.keys.first; + + /// Width (number of wire IDs) for a given ROHD port name. + int width(String portName) => rawConns[portName]?.length ?? 0; + + /// Build new port-direction and connection maps from a + /// `{rohdPortName: yosysPortName}` mapping. + ({ + Map portDirs, + Map> connections, + }) remap(Map nameMap) { + final pd = {}; + final cn = >{}; + for (final e in nameMap.entries) { + final rohdName = e.key; + final netlistPortName = e.value; + pd[netlistPortName] = rawPortDirs[rohdName] ?? 'output'; + cn[netlistPortName] = rawConns[rohdName] ?? []; + } + return (portDirs: pd, connections: cn); + } +} + +/// Signature for a leaf-cell mapping handler. +/// +/// Returns a [LeafCellMapping] if the handler recognises the module, +/// or `null` to let the next handler try. +typedef LeafCellHandler = LeafCellMapping? Function(LeafCellContext ctx); + +/// Maps ROHD leaf [Module]s to Yosys-primitive cell representations. +/// +/// Handlers are registered via [register] and tried in registration order. +/// A singleton instance with all built-in ROHD types pre-registered is +/// available via [LeafCellMapper.defaultMapper]. +/// +/// ```dart +/// final mapper = LeafCellMapper.defaultMapper; +/// final result = mapper.map(sub, rawPortDirs, rawConns); +/// ``` +class LeafCellMapper { + /// Ordered list of registered handlers. + final _handlers = []; + + /// Creates an empty [LeafCellMapper] with no registered handlers. + LeafCellMapper(); + + /// The default mapper with all built-in ROHD leaf types registered. + static final defaultMapper = LeafCellMapper._withDefaults(); + + /// Register a mapping [handler]. + /// + /// Handlers are tried in registration order; the first non-null result + /// wins. Register more-specific handlers before less-specific ones. + void register(LeafCellHandler handler) { + _handlers.add(handler); + } + + /// Try to map [module] to a Yosys-primitive cell. + /// + /// Returns `null` if no registered handler matches. + LeafCellMapping? map( + Module module, + Map rawPortDirs, + Map> rawConns, + ) { + final ctx = LeafCellContext(module, rawPortDirs, rawConns); + for (final handler in _handlers) { + final result = handler(ctx); + if (result != null) { + return result; + } + } + return null; + } + + // ══════════════════════════════════════════════════════════════════════ + // Reusable mapping patterns + // ══════════════════════════════════════════════════════════════════════ + + /// Map a single-input, single-output gate (e.g. `$not`, `$reduce_and`). + static LeafCellMapping? unaryAY( + LeafCellContext ctx, + String cellType, + ) { + final inN = ctx.firstInput; + final out = ctx.firstOutput; + if (inN == null || out == null) { + return null; + } + final r = ctx.remap({inN: 'A', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(inN), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + /// Map a two-input gate with ports A, B, Y (e.g. `$and`, `$eq`, `$shl`). + static LeafCellMapping? binaryABY( + LeafCellContext ctx, + String cellType, { + required String inAPrefix, + required String inBPrefix, + }) { + final a = ctx.findInput(inAPrefix); + final b = ctx.findInput(inBPrefix); + final out = ctx.firstOutput; + if (a == null || b == null || out == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(a), + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + // ══════════════════════════════════════════════════════════════════════ + // Built-in handler registration + // ══════════════════════════════════════════════════════════════════════ + + /// Creates a [LeafCellMapper] with built-in handlers for common ROHD leaf + /// types. + factory LeafCellMapper._withDefaults() { + final m = LeafCellMapper(); + + // Helper to reduce boilerplate for type-map-based handlers. + void registerByTypeMap( + Map typeMap, + LeafCellMapping? Function(LeafCellContext ctx, String cellType) handler, + ) { + m.register((ctx) { + final cellType = typeMap[ctx.module.runtimeType]; + return cellType == null ? null : handler(ctx, cellType); + }); + } + + m + // ── BusSubset → $slice ──────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! BusSubset) { + return null; + } + final sub = ctx.module as BusSubset; + final inName = sub.inputs.keys.first; + final outName = sub.outputs.keys.first; + final r = ctx.remap({inName: 'A', outName: 'Y'}); + return ( + cellType: r'$slice', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'OFFSET': sub.startIndex, + 'A_WIDTH': ctx.width(inName), + 'Y_WIDTH': ctx.width(outName), + }, + ); + }) + + // ── Swizzle → $concat ───────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Swizzle) { + return null; + } + final outName = ctx.firstOutput; + final inputKeys = ctx.module.inputs.keys.toList(); + + // Filter out zero-width inputs (degenerate concat operands). + final nonZeroKeys = inputKeys.where((k) => ctx.width(k) > 0).toList(); + + if (nonZeroKeys.length == 2 && outName != null) { + final r = ctx + .remap({nonZeroKeys[0]: 'A', nonZeroKeys[1]: 'B', outName: 'Y'}); + return ( + cellType: r'$concat', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(nonZeroKeys[0]), + 'B_WIDTH': ctx.width(nonZeroKeys[1]), + }, + ); + } + + // Single non-zero input ⇒ emit as $buf. + if (nonZeroKeys.length == 1 && outName != null) { + final r = ctx.remap({nonZeroKeys[0]: 'A', outName: 'Y'}); + return ( + cellType: r'$buf', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'WIDTH': ctx.width(nonZeroKeys[0]), + }, + ); + } + + if (nonZeroKeys.isEmpty) { + return null; + } + + // N-input concat: per-input range labels, output is Y. + final pd = {}; + final cn = >{}; + final params = {}; + var bitOffset = 0; + for (var i = 0; i < nonZeroKeys.length; i++) { + final ik = nonZeroKeys[i]; + final w = ctx.width(ik); + final label = + w == 1 ? '[$bitOffset]' : '[${bitOffset + w - 1}:$bitOffset]'; + pd[label] = 'input'; + cn[label] = ctx.rawConns[ik] ?? []; + params['IN${i}_WIDTH'] = w; + bitOffset += w; + } + if (outName != null) { + pd['Y'] = 'output'; + cn['Y'] = ctx.rawConns[outName] ?? []; + } + return ( + cellType: r'$concat', + portDirs: pd, + connections: cn, + parameters: params, + ); + }) + + // ── NOT gate ────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! NotGate) { + return null; + } + return unaryAY(ctx, r'$not'); + }) + + // ── Mux ─────────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Mux) { + return null; + } + final ctrl = ctx.findInput('_control') ?? ctx.findInput('control'); + final d0 = ctx.findInput('_d0') ?? ctx.findInput('d0'); + final d1 = ctx.findInput('_d1') ?? ctx.findInput('d1'); + final out = ctx.firstOutput; + if (ctrl == null || d0 == null || d1 == null || out == null) { + return null; + } + // Yosys: S=select, A=d0 (when S=0), B=d1 (when S=1). + final r = ctx.remap({ctrl: 'S', d0: 'A', d1: 'B', out: 'Y'}); + return ( + cellType: r'$mux', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'WIDTH': ctx.width(d0), + }, + ); + }) + + // ── Add ─────────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Add) { + return null; + } + final in0 = ctx.findInput('_in0') ?? ctx.findInput('in0'); + final in1 = ctx.findInput('_in1') ?? ctx.findInput('in1'); + final sumName = ctx.module.outputs.keys + .firstWhere((k) => !k.contains('carry'), orElse: () => ''); + final carryName = ctx.module.outputs.keys + .firstWhere((k) => k.contains('carry'), orElse: () => ''); + if (in0 == null || in1 == null || sumName.isEmpty) { + return null; + } + final pd = { + 'A': 'input', + 'B': 'input', + 'Y': 'output', + }; + final cn = >{ + 'A': ctx.rawConns[in0] ?? [], + 'B': ctx.rawConns[in1] ?? [], + 'Y': ctx.rawConns[sumName] ?? [], + }; + if (carryName.isNotEmpty) { + pd['CO'] = 'output'; + cn['CO'] = ctx.rawConns[carryName] ?? []; + } + return ( + cellType: r'$add', + portDirs: pd, + connections: cn, + parameters: { + 'A_WIDTH': ctx.width(in0), + 'B_WIDTH': ctx.width(in1), + 'Y_WIDTH': ctx.width(sumName), + }, + ); + }) + + // ── FlipFlop → $dff ─────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! FlipFlop) { + return null; + } + final clk = ctx.findInput('_clk') ?? ctx.findInput('clk'); + final d = ctx.findInput('_d') ?? ctx.findInput('d'); + final en = ctx.findInput('_en') ?? ctx.findInput('en'); + final rst = ctx.findInput('_reset') ?? ctx.findInput('reset'); + final q = ctx.firstOutput; + if (clk == null || d == null || q == null) { + return null; + } + final pd = { + '_clk': 'input', + '_d': 'input', + '_q': 'output', + }; + final cn = >{ + '_clk': ctx.rawConns[clk] ?? [], + '_d': ctx.rawConns[d] ?? [], + '_q': ctx.rawConns[q] ?? [], + }; + if (en != null && ctx.rawConns.containsKey(en)) { + pd['_en'] = 'input'; + cn['_en'] = ctx.rawConns[en] ?? []; + } + if (rst != null && ctx.rawConns.containsKey(rst)) { + pd['_reset'] = 'input'; + cn['_reset'] = ctx.rawConns[rst] ?? []; + } + final rstVal = + ctx.findInput('_resetValue') ?? ctx.findInput('resetValue'); + if (rstVal != null && ctx.rawConns.containsKey(rstVal)) { + pd['_resetValue'] = 'input'; + cn['_resetValue'] = ctx.rawConns[rstVal] ?? []; + } + return ( + cellType: r'$dff', + portDirs: pd, + connections: cn, + parameters: { + 'WIDTH': ctx.width(d), + 'CLK_POLARITY': 1, + }, + ); + }); + + // ── Type-map-based gates ─────────────────────────────────────────── + final gateRegistrations = <( + Map, + LeafCellMapping? Function(LeafCellContext, String), + )>[ + ( + const { + And2Gate: r'$and', + Or2Gate: r'$or', + Xor2Gate: r'$xor', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + AndUnary: r'$reduce_and', + OrUnary: r'$reduce_or', + XorUnary: r'$reduce_xor', + }, + unaryAY, + ), + ( + const { + Multiply: r'$mul', + Subtract: r'$sub', + Equals: r'$eq', + NotEquals: r'$ne', + LessThan: r'$lt', + GreaterThan: r'$gt', + LessThanOrEqual: r'$le', + GreaterThanOrEqual: r'$ge', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + LShift: r'$shl', + RShift: r'$shr', + ARShift: r'$shiftx', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in', inBPrefix: '_shiftAmount'), + ), + ]; + for (final (typeMap, handler) in gateRegistrations) { + registerByTypeMap(typeMap, handler); + } + + // ── TriStateBuffer → $tribuf ────────────────────────────────────── + m.register((ctx) { + if (ctx.module is! TriStateBuffer) { + return null; + } + final tsb = ctx.module as TriStateBuffer; + final inName = tsb.inputs.keys.first; // data input + final enName = tsb.inputs.keys.last; // enable + final outName = tsb.inOuts.keys.first; // inout output + final r = ctx.remap({inName: 'A', enName: 'EN', outName: 'Y'}); + return ( + cellType: r'$tribuf', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'WIDTH': ctx.width(inName), + }, + ); + }); + + return m; + } +} diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart new file mode 100644 index 000000000..8d335e812 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -0,0 +1,16 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist.dart +// Barrel file for netlist synthesis library. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +export 'leaf_cell_mapper.dart'; +export 'netlist_options.dart'; +export 'netlist_passes.dart'; +export 'netlist_service.dart'; +export 'netlist_synthesis_result.dart'; +export 'netlist_synthesizer.dart'; +export 'netlist_utils.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_options.dart new file mode 100644 index 000000000..a92be2217 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_options.dart @@ -0,0 +1,97 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_options.dart +// Configuration for netlist synthesis. +// +// 2026 March 12 +// Author: Desmond Kirkpatrick + +import 'package:rohd/src/synthesizers/netlist/leaf_cell_mapper.dart'; + +/// Configuration options for netlist synthesis. +/// +/// The netlist synthesizer serves two main consumer flows, both configured +/// through these options: +/// +/// **Flow 1 — Slim JSON** (`NetlistService.slimJson`): +/// Batch synthesis of the entire design, producing a lightweight +/// representation with ports, signals, and cell stubs but **no cell +/// connections**. Used for the initial DevTools hierarchy load. +/// +/// **Flow 2 — Full JSON, incremental** (`NetlistService.moduleJson`): +/// Returns the complete netlist (with cell connections) for a single +/// module definition on demand. Results are cached; the first call +/// may trigger a lazy `SynthBuilder` run on the requested subtree. +/// +/// Both flows run the identical pipeline: `SynthBuilder` → +/// `collectModuleEntries` → `applyPostProcessingPasses`. Flow 1 +/// then strips cell connections from the cached data; Flow 2 returns +/// it verbatim. This guarantees cell keys and wire IDs are stable +/// across both flows. +/// +/// Bundles all parameters that control netlist generation into a single +/// object, making it easier to pass through call chains and to store +/// for incremental synthesis. +/// +/// Example usage: +/// ```dart +/// const options = NetlistOptions( +/// collapseTransparentClusters: true, +/// ); +/// final synth = NetlistSynthesizer(options: options); +/// ``` +class NetlistOptions { + /// The leaf-cell mapper used to convert ROHD leaf modules to Yosys + /// primitive cell types. When `null`, [LeafCellMapper.defaultMapper] + /// is used. + final LeafCellMapper? leafCellMapper; + + /// When `true`, a single unified pass finds connected components of + /// all transparent cells (`$buf`, `$slice`, `$concat`, + /// `$struct_unpack`, `$struct_pack`), traces each cluster's output + /// bits back to their ultimate source bits, and replaces every + /// multi-cell cluster with a direct `$buf`. This subsumes all of + /// the individual collapse passes above. + final bool collapseTransparentClusters; + + /// When `true`, dead-cell elimination is performed after aliasing to + /// remove cells whose inputs are entirely undriven or whose outputs + /// are entirely unconsumed. + final bool enableDCE; + + /// When `true`, the synthesizer produces "slim" output: the full + /// synthesis pipeline runs (including all post-processing passes), + /// but cell connection maps are stripped from the result. + /// Netnames and ports are still emitted with full wire-ID fidelity, + /// so a subsequent full-mode synthesis of the same module will + /// produce compatible wire IDs. + final bool slimMode; + + /// When `true`, contiguous ascending runs of ≥3 integer bit IDs in + /// `bits` arrays and cell `connections` arrays are replaced with + /// `"start:end"` range strings (e.g. `[52, 53, 54, 55]` → `["52:55"]`). + /// + /// This is backward-compatible: Yosys-format arrays already mix + /// integers with constant strings `"0"` and `"1"`. Parsers can + /// detect range strings by the presence of `:`. + final bool compressBitRanges; + + /// When `true`, the JSON output uses no indentation (compact form). + /// When `false` (default), the JSON is pretty-printed with two-space + /// indentation. + final bool compactJson; + + /// Creates a [NetlistOptions] with the given configuration. + /// + /// All parameters have sensible defaults matching the current + /// netlist synthesizer behaviour. + const NetlistOptions({ + this.leafCellMapper, + this.collapseTransparentClusters = false, + this.enableDCE = true, + this.slimMode = false, + this.compressBitRanges = false, + this.compactJson = false, + }); +} diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart new file mode 100644 index 000000000..f5e498740 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -0,0 +1,356 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_passes.dart +// Post-processing optimization passes for netlist synthesis. +// +// These passes operate on the modules map (definition name → module data) +// produced by [NetlistSynthesizer.synthesize]. They simplify the netlist +// by grouping struct conversions, collapsing redundant cells, and inserting +// buffer cells for cleaner schematic rendering. +// +// 2025 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Post-processing optimization passes for netlist synthesis. +/// +/// All methods are static — no instances are created. +class NetlistPasses { + NetlistPasses._(); + + /// Collects a combined modules map from [SynthesisResult]s suitable for + /// JSON emission. + static Map> collectModuleEntries( + Iterable results, { + Module? topModule, + }) { + final allModules = >{}; + for (final result in results) { + if (result is NetlistSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = Map.from(result.attributes); + if (topModule != null && result.module == topModule) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }; + } + } + return allModules; + } + + // ════════════════════════════════════════════════════════════════════ + // Unified transparent-cell clustering + // ════════════════════════════════════════════════════════════════════ + + /// Transparent cell types that only reshuffle / rename bits. + static const _transparentTypes = { + r'$buf', + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + /// Unified transparent-cell clustering pass. + /// + /// **Phase 1 — Cluster identification:** + /// Builds an undirected graph over transparent cells (two cells are + /// neighbours when one's output wire feeds the other's input) and + /// finds connected components via BFS. + /// + /// **Phase 2 — Cluster collapse:** + /// For every multi-cell component, traces each externally-consumed + /// output bit backward through the component's bit-level mapping + /// until reaching an external source bit, then replaces the entire + /// component with a single `$buf` wired from traced sources to + /// destinations. + static void applyTransparentClustering( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + + // ── Gather transparent cells ── + + final tCells = { + for (final e in cells.entries) + if (_transparentTypes.contains(e.value['type'] as String?)) e.key, + }; + if (tCells.isEmpty) { + continue; + } + + // ── Wire maps ── + + final wireConsumers = >{}; + + for (final e in cells.entries) { + final dirs = e.value['port_directions'] as Map? ?? {}; + final conns = e.value['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) == 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is int) { + (wireConsumers[b] ??= {}).add(e.key); + } + } + } + } + + // Bits consumed by module output / inout ports. + final portOutBits = {}; + for (final pv in ports.values) { + final pm = pv as Map; + final dir = pm['direction'] as String?; + if (dir == 'output' || dir == 'inout') { + for (final b in pm['bits'] as List) { + if (b is int) { + portOutBits.add(b); + } + } + } + } + + // ── Phase 1: connected components ── + + final adj = >{for (final tc in tCells) tc: {}}; + + for (final tc in tCells) { + final dirs = + cells[tc]!['port_directions'] as Map? ?? {}; + final conns = cells[tc]!['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + for (final c in wireConsumers[b] ?? const {}) { + if (c != tc && tCells.contains(c)) { + adj[tc]!.add(c); + adj[c]!.add(tc); + } + } + } + } + } + + final visited = {}; + final components = >[]; + + for (final tc in tCells) { + if (!visited.add(tc)) { + continue; + } + final comp = {tc}; + final stack = [tc]; + while (stack.isNotEmpty) { + final cur = stack.removeLast(); + for (final nb in adj[cur]!) { + if (visited.add(nb)) { + comp.add(nb); + stack.add(nb); + } + } + } + if (comp.length >= 2) { + components.add(comp); + } + } + + if (components.isEmpty) { + continue; + } + + // ── Phase 2: trace & replace ── + + final cellsToRemove = {}; + final cellsToAdd = >{}; + + for (final comp in components) { + // Build output-bit → input-bit map for the whole cluster. + final bitMap = {}; + for (final cn in comp) { + _mapCellBits(cells[cn]!, bitMap); + } + + // External output bits: produced by the cluster but consumed + // by something outside it (another cell or module output port). + final extOut = []; + for (final cn in comp) { + final dirs = + cells[cn]!['port_directions'] as Map? ?? {}; + final conns = + cells[cn]!['connections'] as Map? ?? {}; + for (final pe in conns.entries) { + if ((dirs[pe.key] as String?) != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + if (portOutBits.contains(b) || + (wireConsumers[b]?.any((c) => !comp.contains(c)) ?? false)) { + extOut.add(b); + } + } + } + } + + if (extOut.isEmpty) { + // Fully dead cluster — remove. + cellsToRemove.addAll(comp); + continue; + } + + // Trace each external output back through the cluster to an + // external source bit. + final aList = []; + final yList = []; + var ok = true; + + for (final ob in extOut) { + Object cur = ob; + final seen = {}; + while (cur is int && bitMap.containsKey(cur)) { + if (!seen.add(cur)) { + ok = false; + break; + } + cur = bitMap[cur]!; + } + if (!ok) { + break; + } + aList.add(cur); + yList.add(ob); + } + + if (!ok) { + continue; + } + + cellsToAdd['cluster_buf_${comp.first}'] = NetlistUtils.makeBufCell( + aList.length, + aList, + yList, + ); + cellsToRemove.addAll(comp); + } + + cellsToRemove.forEach(cells.remove); + cells.addAll(cellsToAdd); + } + } + + /// Populates [bitMap] with output-wire-bit → input-wire-bit entries + /// for a single transparent cell. + static void _mapCellBits(Map cell, Map bitMap) { + final type = cell['type']! as String; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final params = cell['parameters'] as Map? ?? {}; + + switch (type) { + case r'$buf': + _mapPairwise(conns['A'] as List, conns['Y'] as List, bitMap); + + case r'$slice': + final a = conns['A'] as List; + final y = conns['Y'] as List; + final off = params['OFFSET'] as int? ?? 0; + for (var i = 0; i < y.length; i++) { + if (y[i] is int && (off + i) < a.length) { + bitMap[y[i] as int] = a[off + i] as Object; + } + } + + case r'$concat': + final y = conns['Y'] as List; + // Input ports are in connection-map order; their bits + // concatenate to form Y (first port at LSB). + final inBits = [ + for (final pe in conns.entries) + if ((dirs[pe.key] as String?) != 'output') + ...(pe.value as List).cast(), + ]; + _mapPairwise(inBits, y, bitMap); + + case r'$struct_unpack': + final a = conns['A'] as List; + final fc = params['FIELD_COUNT'] as int? ?? 0; + for (var f = 0; f < fc; f++) { + final fn = params['FIELD_${f}_NAME'] as String?; + final fo = params['FIELD_${f}_OFFSET'] as int? ?? 0; + if (fn == null) { + continue; + } + final fb = conns[fn] as List?; + if (fb == null) { + continue; + } + for (var i = 0; i < fb.length; i++) { + if (fb[i] is int && (fo + i) < a.length) { + bitMap[fb[i] as int] = a[fo + i] as Object; + } + } + } + + case r'$struct_pack': + final y = conns['Y'] as List; + final fc = params['FIELD_COUNT'] as int? ?? 0; + final src = List.filled(y.length, null); + for (var f = 0; f < fc; f++) { + final fn = params['FIELD_${f}_NAME'] as String?; + final fo = params['FIELD_${f}_OFFSET'] as int? ?? 0; + if (fn == null) { + continue; + } + final fb = conns[fn] as List?; + if (fb == null) { + continue; + } + for (var i = 0; i < fb.length; i++) { + if ((fo + i) < src.length) { + src[fo + i] = fb[i]; + } + } + } + for (var i = 0; i < y.length; i++) { + if (y[i] is int && src[i] != null) { + bitMap[y[i] as int] = src[i]!; + } + } + } + } + + /// Maps `Y[i]` → `A[i]` for identity-shaped cells. + static void _mapPairwise( + List a, + List y, + Map bitMap, + ) { + for (var i = 0; i < y.length && i < a.length; i++) { + if (y[i] is int) { + bitMap[y[i] as int] = a[i] as Object; + } + } + } +} diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart new file mode 100644 index 000000000..c98bc6553 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -0,0 +1,312 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_service.dart +// Service wrapper for netlist synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; + +/// A service that wraps netlist (Yosys JSON) synthesis of a [Module] +/// hierarchy. +/// +/// Provides access to the full hierarchy JSON and per-module JSON with +/// lazy caching, and optionally registers with [ModuleServices] for +/// DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final netlist = NetlistService(dut); +/// +/// // Full hierarchy JSON: +/// print(netlist.json); +/// +/// // Single module (lazy, cached): +/// print(netlist.moduleJson('FilterChannel')); +/// ``` +class NetlistService extends OutputService { + /// The current format version for netlist JSON produced by this service. + static const String formatVersion = '0.0.5'; + + /// The most recently registered [NetlistService], or `null`. + static NetlistService? current; + + /// The top-level [Module] being synthesized. + @override + final Module module; + + /// The default location written by [write], or `null`. + @override + final String? outputPath; + + /// Whether [write] emits multiple files. Netlist output is a single JSON + /// document, so this is always `false`. + @override + bool get multiFile => false; + + /// The [NetlistSynthesizer] used for synthesis. + late final NetlistSynthesizer synthesizer; + + /// The underlying [SynthBuilder]. + late final SynthBuilder synthBuilder; + + /// The combined JSON string for the full hierarchy. + late final String _fullJson; + + /// Cached per-module JSON, keyed by definition name. + final Map _moduleJsonCache = {}; + + /// The parsed modules map from the combined JSON. + late final Map _modulesMap; + + /// The package root directory used for FLC trace injection. + /// + /// When non-null, downstream trace-enabled branches use this path to embed + /// `rohd.src_trace` attributes in the netlist JSON. + late final String? packageRoot; + + /// Creates a netlist service for a built [module]. + /// + /// Uses [options] for netlist synthesis configuration and optionally + /// [register]s this instance with [ModuleServices] for DevTools lookup. + NetlistService( + this.module, { + NetlistOptions options = const NetlistOptions(), + String? packageRoot, + bool register = true, + this.outputPath, + }) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating NetlistService. ' + 'Call build() first.', + ); + } + + final effectiveRoot = packageRoot; + synthesizer = NetlistSynthesizer(options: options); + this.packageRoot = effectiveRoot; + synthBuilder = SynthBuilder(module, synthesizer); + _fullJson = synthesizer.synthesizeToJson( + module, + packageRoot: effectiveRoot, + ); + + final decoded = jsonDecode(_fullJson) as Map; + _modulesMap = + (decoded['modules'] as Map?) ?? {}; + _loadedVersion = decoded['version'] as String?; + + if (outputPath != null) { + write(); + } + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + /// The format version found in the loaded JSON, or `null` if absent. + String? _loadedVersion; + + /// The format version string from the loaded netlist JSON. + /// + /// Returns the `version` field from the JSON if present, otherwise + /// returns [formatVersion] (assumes current format). + String get version => _loadedVersion ?? formatVersion; + + /// Checks whether [version] is compatible with the current + /// [formatVersion]. + /// + /// Compatible means the major version matches. Returns `true` if + /// the loaded JSON can be consumed by this version of the service. + static bool isCompatibleVersion(String version) { + final current = formatVersion.split('.'); + final other = version.split('.'); + if (other.length < 2 || current.length < 2) { + return false; + } + // Major and minor must match for compatibility. + return current[0] == other[0] && current[1] == other[1]; + } + + /// Whether the loaded netlist JSON is compatible with the current format. + bool get isCompatible => isCompatibleVersion(version); + + /// Returns the full netlist hierarchy as a JSON string. + String get json => _fullJson; + + /// Writes the full netlist [json] to [path], or to [outputPath] when [path] + /// is omitted. + @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.', + ); + } + File(target) + ..parent.createSync(recursive: true) + ..writeAsStringSync(_fullJson); + } + + /// Returns a JSON-serialisable summary of the netlist synthesis. + /// + /// Contains the netlist format version and the list of module definition + /// names. For the full netlist document, use [json]. + @override + Map toJson() => { + 'creator': 'ROHD netlist synthesizer', + 'version': version, + 'modules': moduleNames.toList(), + }; + + /// Returns the netlist JSON for a single module [definitionName]. + /// + /// The returned JSON is keyed by definition name: + /// `{"DefinitionName": { ports, cells, netnames }}`. + /// This matches the format expected by the DevTools schematic viewer + /// for incremental module fetches. + /// + /// If the module is not found, returns a JSON error object. + String moduleJson(String definitionName) => + _moduleJsonCache.putIfAbsent(definitionName, () { + final modData = _modulesMap[definitionName]; + if (modData == null) { + return jsonEncode({ + 'status': 'not_found', + 'reason': 'module "$definitionName" not in netlist', + }); + } + return jsonEncode({ + 'creator': 'ROHD netlist synthesizer', + 'version': formatVersion, + 'modules': {definitionName: modData}, + }); + }); + + /// Returns the set of module definition names in the netlist. + Set get moduleNames => _modulesMap.keys.toSet(); + + /// Read-only access to the parsed modules map. + /// + /// Each key is a definition name and each value is the Yosys-style + /// module descriptor containing `ports`, `cells`, and `netnames`. + Map get synthesizedModules => + Map.unmodifiable(_modulesMap); + + /// Cached slim JSON (lazy). + String? _slimJsonCache; + + /// Returns a slim netlist JSON string — same structure as [toJson] but + /// with cell `connections` stripped. + /// + /// The slim representation preserves ports, cells (type + port_directions + /// + port_widths), and netnames so the DevTools extension can render the + /// hierarchy and signal tree without the full connectivity payload. + /// Full per-module connectivity is fetched on demand via [moduleJson]. + String get slimJson => _slimJsonCache ??= _buildSlimJson(); + + String _buildSlimJson() { + final slimModules = {}; + for (final entry in _modulesMap.entries) { + final mod = entry.value as Map; + final cells = mod['cells'] as Map? ?? {}; + final slimCells = {}; + for (final cellEntry in cells.entries) { + final cell = cellEntry.value as Map; + // Compute per-port widths from connections (bit-array lengths). + final conns = cell['connections'] as Map?; + final portWidths = {}; + if (conns != null) { + for (final c in conns.entries) { + final bits = c.value; + if (bits is List) { + portWidths[c.key] = bits.length; + } + } + } + slimCells[cellEntry.key] = { + 'hide_name': cell['hide_name'] ?? 0, + 'type': cell['type'], + 'parameters': cell['parameters'] ?? {}, + 'attributes': cell['attributes'] ?? {}, + 'port_directions': cell['port_directions'] ?? {}, + if (portWidths.isNotEmpty) 'port_widths': portWidths, + // connections intentionally omitted → slim + }; + } + + // Determine which module-level ports have internal connectivity. + final ports = mod['ports'] as Map? ?? {}; + final slimPorts = {}; + final cellConnectedBits = {}; + for (final cellEntry in cells.values) { + final cell = cellEntry as Map; + final conns = cell['connections'] as Map?; + if (conns == null) { + continue; + } + for (final bits in conns.values) { + if (bits is List) { + for (final b in bits) { + if (b is int) { + cellConnectedBits.add(b); + } + } + } + } + } + for (final portEntry in ports.entries) { + final portData = portEntry.value as Map; + final bits = portData['bits'] as List?; + var connected = false; + if (bits != null) { + for (final b in bits) { + if (b is int && cellConnectedBits.contains(b)) { + connected = true; + break; + } + } + } + slimPorts[portEntry.key] = { + ...portData, + if (connected) 'connected': true, + }; + } + + final netnames = mod['netnames'] as Map? ?? {}; + + slimModules[entry.key] = { + 'attributes': { + ...(mod['attributes'] as Map? ?? {}), + 'original_signal_count': netnames.length, + 'original_cell_count': slimCells.length, + }, + 'ports': slimPorts, + 'cells': slimCells, + 'netnames': netnames, + }; + } + + final rootName = module.hasBuilt ? module.uniqueInstanceName : module.name; + + return jsonEncode({ + 'netlist': { + 'creator': 'ROHD NetlistService (slim)', + 'version': formatVersion, + 'rootInstanceName': rootName, + 'modules': slimModules, + }, + }); + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart new file mode 100644 index 000000000..bccca7f2d --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -0,0 +1,85 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesis_result.dart +// A simple SynthesisResult that holds netlist data for one module. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; + +/// A [SynthesisResult] that holds the netlist representation of a single +/// module level: its ports, cells, and netnames. +class NetlistSynthesisResult extends SynthesisResult { + /// The ports map: name → {direction, bits}. + final Map> ports; + + /// The cells map: instance name → cell data. + final Map> cells; + + /// The netnames map: net name → {bits, attributes}. + final Map netnames; + + /// Attributes for this module (e.g., top marker). + final Map attributes; + + /// Cached JSON string for comparison and output. + late final String _cachedJson = _buildJson(); + + /// Creates a [NetlistSynthesisResult] for [module]. + NetlistSynthesisResult( + super.module, + super.getInstanceTypeOfModule, { + required this.ports, + required this.cells, + required this.netnames, + this.attributes = const {}, + }); + + String _buildJson() { + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + return const JsonEncoder().convert(moduleEntry); + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is NetlistSynthesisResult && _cachedJson == other._cachedJson; + + @override + int get matchHashCode => _cachedJson.hashCode; + + @override + @Deprecated('Use `toSynthFileContents()` instead.') + String toFileContents() => toSynthFileContents().first.contents; + + @override + List toSynthFileContents() { + final typeName = instanceTypeName; + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + final contents = const JsonEncoder.withIndent(' ').convert({ + 'creator': 'NetlistSynthesizer (rohd)', + 'version': NetlistService.formatVersion, + 'modules': {typeName: moduleEntry}, + }); + return [ + SynthFileContents( + name: '$typeName.rohd.json', + description: 'netlist for $typeName', + contents: contents, + ), + ]; + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart new file mode 100644 index 000000000..c1a2b6e18 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -0,0 +1,2064 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer.dart +// A netlist synthesizer built on [SynthModuleDefinition]. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// +/// Skips SystemVerilog-specific processing (chain collapsing, net connects, +/// inOut inline replacement) since netlist represents all sub-modules as +/// cells rather than inline assignment expressions. +class _NetlistSynthModuleDefinition extends SynthModuleDefinition { + _NetlistSynthModuleDefinition(Module module) : super(module) { + // Create explicit $slice cells for LogicArray input ports so the + // netlist shows select gates for element extraction rather than + // flat bit aliasing. + module.inputs.values + .whereType() + .forEach(_subsetReceiveArrayPort); + + // Same for LogicArray outputs on submodules (received into this scope). + final subModuleOutputArrays = module.subModules + .expand((sub) => sub.outputs.values) + .whereType() + .toSet() + ..forEach(_subsetReceiveArrayPort); + + // Create explicit $concat cells for internal LogicArrays whose elements + // are driven independently (e.g. by constants) and then consumed by + // submodule input ports. This parallels what _subsetReceiveArrayPort does + // on the decomposition side. + // + // Skip arrays that were merged with a port array's SynthLogic — those + // are already structurally decomposed by the $slice cells created above + // and reassembling them would create a circular driver on the port bus. + // Also skip submodule output arrays that already received $slice cells. + final portArrays = { + ...module.inputs.values.whereType(), + ...module.outputs.values.whereType(), + ...module.inOuts.values.whereType(), + }; + final excludedArrays = { + ...portArrays, + ...subModuleOutputArrays, + }; + // For multi-dimensional arrays, also exclude nested sub-arrays. + // E.g. LogicArray([10,2],8) has 10 children each being LogicArray([2],8). + // Without this, the sub-arrays get spurious $concat cells creating + // multi-driver conflicts on the parent port's bits. + void addNestedArrays(LogicArray arr) { + for (final elem in arr.elements) { + if (elem is LogicArray) { + excludedArrays.add(elem); + addNestedArrays(elem); + } + } + } + + {...portArrays, ...subModuleOutputArrays} + .forEach(addNestedArrays); + final portArraySynthLogics = {}; + for (final pa in excludedArrays) { + final sl = logicToSynthMap[pa]; + if (sl != null) { + portArraySynthLogics.add(sl.replacement ?? sl); + } + } + module.internalSignals.whereType().where((sig) { + if (excludedArrays.contains(sig)) { + return false; + } + final sl = logicToSynthMap[sig]; + if (sl == null) { + return false; + } + final resolved = sl.replacement ?? sl; + return !portArraySynthLogics.contains(resolved); + }).forEach(_concatAssembleArray); + } + + /// Creates explicit `$slice` cells for each element of a [LogicArray] port. + /// + /// Each element gets a [_BusSubsetForArraySlice] that extracts its bit range + /// from the packed parent bus. This produces explicit select gates in the + /// netlist, making array decomposition visible and traceable. + void _subsetReceiveArrayPort(LogicArray port) { + final portSynth = getSynthLogic(port)!; + + var idx = 0; + for (final element in port.elements) { + final elemSynth = getSynthLogic(element)!; + internalSignals.add(elemSynth); + + final subsetMod = _BusSubsetForArraySlice( + Logic(width: port.width, name: 'DUMMY'), + idx, + idx + element.width - 1, + ); + + getSynthSubModuleInstantiation(subsetMod) + ..setOutputMapping(subsetMod.subset.name, elemSynth) + ..setInputMapping(subsetMod.original.name, portSynth) + + // Pick a name now — this may be called after _pickNames() has run. + ..pickName(module); + + idx += element.width; + } + } + + /// Creates an explicit `$concat` cell that assembles a [LogicArray]'s + /// elements into the full packed array bus. + /// + /// This is the assembly counterpart to [_subsetReceiveArrayPort]: when + /// individual array elements are driven independently (e.g. by constants), + /// this makes the concatenation explicit as a visible gate in the netlist. + void _concatAssembleArray(LogicArray array) { + final arraySynth = getSynthLogic(array)!; + + // Build dummy signals matching each element's width. + final dummyElements = []; + for (final element in array.elements) { + dummyElements.add(Logic(width: element.width, name: 'DUMMY')); + } + + // Pass reversed dummies so that Swizzle's internal reversal cancels out, + // leaving in0 aligned with element[0] (LSB) and inN with element[N]. + final concatMod = _SwizzleForArrayConcat(dummyElements.reversed.toList()); + + final ssmi = getSynthSubModuleInstantiation(concatMod) + // Map the concat output to the full array. + ..setOutputMapping(concatMod.out.name, arraySynth); + + // Map each element input. + // Because we reversed dummies above, in0 corresponds to element[0], + // in1 to element[1], etc. + for (var i = 0; i < array.elements.length; i++) { + final elemSynth = getSynthLogic(array.elements[i])!; + internalSignals.add(elemSynth); + final inputName = concatMod.inputs.keys.elementAt(i); + ssmi.setInputMapping(inputName, elemSynth); + } + + // Pick a name now — this may be called after _pickNames() has run. + ssmi.pickName(module); + } + + @override + void process() { + // No SV-specific transformations -- we want every sub-module to remain + // as a cell in the JSON. + } +} + +/// A simple [Synthesizer] that produces netlist-compatible JSON. +/// +/// Leverages [SynthModuleDefinition] for signal tracing, naming, and +/// constant resolution, then maps the resulting [SynthLogic]s to integer +/// wire-bit IDs for netlist JSON output. +/// +/// Leaf modules (those with no sub-modules, or special cases like [FlipFlop]) +/// do *not* get their own module definition -- they appear only as cells +/// inside their parent. +/// +/// Usage: +/// ```dart +/// const options = NetlistOptions(collapseTransparentClusters: true); +/// final synth = NetlistSynthesizer(options: options); +/// final builder = SynthBuilder(topModule, synth); +/// final json = synth.synthesizeToJson(topModule); +/// ``` +class NetlistSynthesizer extends Synthesizer { + /// The configuration options controlling netlist synthesis. + /// + /// See [NetlistOptions] for documentation on individual fields. + final NetlistOptions options; + + /// Convenience accessor for the leaf-cell mapper. + LeafCellMapper get leafCellMapper => + options.leafCellMapper ?? LeafCellMapper.defaultMapper; + + /// Creates a [NetlistSynthesizer]. + /// + /// All synthesis parameters are bundled in [options]; see + /// [NetlistOptions] for documentation on each field. + NetlistSynthesizer({this.options = const NetlistOptions()}); + + @override + bool generatesDefinition(Module module) => + // Only modules with sub-modules generate their own module definition. + // Leaf modules (no children) become cells inside their parent. + // FlipFlop has internal Sequential sub-modules but should be emitted as + // a flat Yosys $dff primitive, not as a hierarchical module. + module is! FlipFlop && module.subModules.isNotEmpty; + + @override + SynthesisResult synthesize( + Module module, + String Function(Module module) getInstanceTypeOfModule, { + SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults, + }) { + final isTop = module.parent == null; + final attr = {'src': 'generated'}; + if (isTop) { + attr['top'] = 1; + } + + // -- Build SynthModuleDefinition ------------------------------------ + // This does all signal tracing, naming, constant handling, + // assignment collapsing, and unused signal pruning. + final canBuildSynthDef = !(module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none); + final synthDef = + canBuildSynthDef ? _NetlistSynthModuleDefinition(module) : null; + + // -- Wire-ID allocation --------------------------------------------- + // Start wire IDs at 2 to avoid collision with Yosys constant string + // bits "0" and "1". JavaScript viewers coerce object keys to strings, + // so integer wire ID 0 becomes "0", clashing with the constant-bit + // string "0". + var nextId = 2; + + // Map from SynthLogic -> assigned wire-bit IDs. + final synthLogicIds = >{}; + + /// Allocate or retrieve wire IDs for a [SynthLogic]. + /// For constants, do NOT follow the replacement chain to ensure each + /// constant usage gets its own separate driver cell in netlist. + List getIds(SynthLogic sl) { + var resolved = sl; + // For non-constants, follow replacement chain to resolve merged logics. + // For constants, keep them separate to create distinct const drivers. + if (!sl.isConstant) { + resolved = NetlistUtils.resolveReplacement(resolved); + } + final ids = synthLogicIds.putIfAbsent( + resolved, () => List.generate(resolved.width, (_) => nextId++)); + return ids; + } + + // -- Ports ----------------------------------------------------------- + final ports = >{}; + + final portGroups = [ + ('input', synthDef?.inputs, module.inputs), + ('output', synthDef?.outputs, module.outputs), + ('inout', synthDef?.inOuts, module.inOuts), + ]; + for (final (direction, synthLogics, modulePorts) in portGroups) { + if (synthLogics != null) { + for (final sl in synthLogics) { + final ids = getIds(sl); + final portName = NetlistUtils.portNameForSynthLogic(sl, modulePorts); + if (portName != null) { + final portLogic = modulePorts[portName]; + ports[portName] = { + 'direction': direction, + 'bits': ids, + if (portLogic != null) + 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), + }; + } + } + } else { + for (final entry in modulePorts.entries) { + final ids = List.generate(entry.value.width, (_) => nextId++); + ports[entry.key] = { + 'direction': direction, + 'bits': ids, + 'logic_type': NetlistUtils.buildLogicType(entry.value, ids), + }; + } + } + } + + // -- Pre-allocate IDs for internal signals in Module order ----------- + // This ensures that internals get IDs in the same order as + // Module.internalSignals, matching WaveformService._collectSignals. + // Signals already allocated during the port phase are skipped by + // putIfAbsent. Synthesis-generated wires get IDs later (during cell + // emission), so they are naturally appended after internals. + // + // Three-tier ordering guarantee: + // Tier 0 (ports): inputs → outputs → inOuts [above] + // Tier 1 (internals): module.internalSignals [here] + // Tier 2 (synth): cell emission wires [below] + if (synthDef != null) { + module.internalSignals + .map((sig) => synthDef.logicToSynthMap[sig]) + .whereType() + .where((sl) => !sl.isConstant) + .forEach(getIds); + } + + // -- Cell emission --------------------------------------------------- + final cells = >{}; + + // Track constant SynthLogics consumed exclusively by + // Combinational/Sequential so we can suppress their driver cells. + final blockedConstSynthLogics = {}; + + // Track emitted cell keys per instance for purging later. + final emittedCellKeys = {}; + + if (synthDef != null) { + for (final instance in synthDef.subModuleInstantiations) { + if (!instance.needsInstantiation) { + continue; + } + + final sub = instance.module; + + final isLeaf = !generatesDefinition(sub); + final defaultCellType = + isLeaf ? sub.definitionName : getInstanceTypeOfModule(sub); + + // Build port directions and connections from instance mappings. + final rawPortDirs = {}; + final rawConnections = >{}; + + for (final (dir, mapping) in [ + ('input', instance.inputMapping), + ('output', instance.outputMapping), + ('inout', instance.inOutMapping), + ]) { + for (final e in mapping.entries) { + rawPortDirs[e.key] = dir; + final ids = getIds(e.value); + rawConnections[e.key] = ids.cast(); + } + } + + // Map leaf cells to Yosys primitive types where possible. + final mapped = isLeaf + ? leafCellMapper.map(sub, rawPortDirs, rawConnections) + : null; + + final cellPortDirs = mapped?.portDirs ?? rawPortDirs; + final cellConns = mapped?.connections ?? rawConnections; + + // Use the SSMI's uniquified name as cell key to avoid + // collisions between identically-named modules (e.g. multiple + // struct_slice instances that share the same Module.name). + final cellKey = instance.name; + emittedCellKeys[instance] = cellKey; + + // -- Collapse bit-slice ports on Combinational / Sequential ---- + if (sub is Combinational || sub is Sequential) { + NetlistUtils.collapseAlwaysBlockPorts( + synthDef, + instance, + cellPortDirs, + cellConns, + getIds, + ); + } + + // -- Filter constant inputs from Combinational / Sequential ---- + if (sub is Combinational || sub is Sequential) { + final portsToRemove = []; + for (final pe in cellConns.entries) { + final portName = pe.key; + final synthLogic = instance.inputMapping[portName] ?? + instance.inOutMapping[portName]; + if (synthLogic != null && + NetlistUtils.isConstantSynthLogic(synthLogic)) { + portsToRemove.add(portName); + blockedConstSynthLogics.add(synthLogic.replacement ?? synthLogic); + } + } + for (final p in portsToRemove) { + cellConns.remove(p); + cellPortDirs.remove(p); + } + } + + // -- Rename Seq/Comb ports to Namer wire names ----------------- + // The port names from _Always.addInput/addOutput are internal + // (e.g. `_out`, `_enable`). Replace them with the Namer's + // resolved wire name so they match SystemVerilog and WaveDumper. + if (sub is Combinational || sub is Sequential) { + final renames = {}; + for (final portName in cellConns.keys.toList()) { + final sl = instance.inputMapping[portName] ?? + instance.outputMapping[portName] ?? + instance.inOutMapping[portName]; + if (sl == null) { + continue; // aggregated port, already renamed + } + final resolved = NetlistUtils.resolveReplacement(sl); + final namerName = NetlistUtils.tryGetSynthLogicName(resolved); + if (namerName != null && namerName != portName) { + renames[portName] = namerName; + } + } + for (final entry in renames.entries) { + final bits = cellConns.remove(entry.key)!; + final dir = cellPortDirs.remove(entry.key)!; + var newName = entry.value; + // Avoid collision with existing port names. + if (cellConns.containsKey(newName)) { + newName = '${entry.value}_${entry.key}'; + } + cellConns[newName] = bits; + cellPortDirs[newName] = dir; + } + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': mapped?.cellType ?? defaultCellType, + 'parameters': mapped?.parameters ?? {}, + 'attributes': {}, + 'port_directions': cellPortDirs, + 'connections': cellConns, + }; + } + } + + // -- Remove cells that were cleared by collapseAlwaysBlockPorts ------ + // Because the iteration order may process a Swizzle/BusSubset cell + // BEFORE the Combinational/Sequential that clears it, we need to purge + // stale cells after all collapsing has been applied. + if (synthDef != null) { + synthDef.subModuleInstantiations + .where((i) => !i.needsInstantiation) + .map((i) => emittedCellKeys[i]) + .whereType() + .forEach(cells.remove); + } + + // -- Wire-ID aliasing from remaining assignments ------------------- + // SynthModuleDefinition._collapseAssignments may leave assignments + // between non-mergeable SynthLogics (e.g., reserved port + + // renameable internal signal). In SV synthesis these become + // `assign` statements. In netlist we need the two sides to + // share wire IDs so that the netlist is properly connected. + // + // Similarly, PartialSynthAssignments for output struct ports tell + // us which leaf-field IDs should compose the port's bits, and + // input-struct BusSubsets (which may be pruned) tell us which + // leaf-field IDs should be carved from the port's bits. + final idAlias = {}; + + // Pending $struct_field cells collected during Step 3. + // Each entry records a single field extraction from a parent struct. + // The `parentLogic` and `fullParentIds` fields are used to group + // entries from the same LogicStructure into a single multi-port + // `$struct_unpack` cell. + final structFieldCells = <({ + List parentIds, + List elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds, + })>[]; + + // Pending $struct_compose cells: for output struct ports, instead of + // aliasing port bits to leaf bits (which causes "shorting"), we + // collect composition operations and emit explicit cells later. + // Each entry records: field (src) → port sub-range [lower:upper]. + final structComposeCells = <({ + List srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + })>[]; + + // Track struct ports (both output ports of the current module AND + // sub-module input struct ports) so Step 3 can skip $struct_field + // collection for them ($struct_pack handles these instead). + final outputStructPortLogics = {}; + + if (synthDef != null) { + // 1. Non-partial assignments: src drives dst → dst IDs become + // src IDs (the driver's IDs are canonical). + for (final assignment + in synthDef.assignments.where((a) => a is! PartialSynthAssignment)) { + final srcIds = getIds(assignment.src); + final dstIds = getIds(assignment.dst); + final len = + srcIds.length < dstIds.length ? srcIds.length : dstIds.length; + for (var i = 0; i < len; i++) { + if (dstIds[i] != srcIds[i]) { + idAlias[dstIds[i]] = srcIds[i]; + } + } + } + + // 2. Partial assignments (output / sub-module struct ports): + // src → dst[lower:upper]. The port-slice IDs become the + // leaf's IDs so that the port is composed from its fields. + // + // For struct ports (both output ports of the current module + // AND sub-module input struct ports), we keep distinct port + // and field IDs and instead collect pending $struct_pack + // cells. This avoids "shorting" where field wires are + // aliased directly to port bits, which creates multi-driver + // conflicts with $struct_unpack cells emitted in Step 3. + // + // For non-struct sub-module input ports, we alias as before. + + /// Recursively add [struct] and all its nested [LogicStructure] + /// descendants (excluding [LogicArray]) to [set]. + void addStructAndDescendants(LogicStructure struct, Set set) { + set.add(struct); + for (final elem in struct.elements) { + if (elem is LogicStructure && elem is! LogicArray) { + addStructAndDescendants(elem, set); + } + } + } + + for (final pa + in synthDef.assignments.whereType()) { + final srcIds = getIds(pa.src); + final dstIds = getIds(pa.dst); + + // Detect: is pa.dst an output struct port of the current module? + final isCurrentModuleOutputPort = + pa.dst.isPort(module) && pa.dst.logics.any((l) => l.isOutput); + + // Detect: is pa.dst a sub-module input struct port? + // (LogicStructure but not LogicArray, and not an output of the + // current module.) + final isSubModuleInputStructPort = !isCurrentModuleOutputPort && + pa.dst.logics.any((l) => l is LogicStructure && l is! LogicArray); + + if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { + // Record as pending compose cell instead of aliasing. + structComposeCells.add(( + srcIds: srcIds, + dstIds: dstIds, + dstLowerIndex: pa.dstLowerIndex, + dstUpperIndex: pa.dstUpperIndex, + srcSynthLogic: pa.src, + dstSynthLogic: pa.dst, + )); + // Track the Logic (and nested structs) so Step 3 skips + // $struct_unpack for them. + for (final l in pa.dst.logics) { + if (l is LogicStructure && l is! LogicArray) { + addStructAndDescendants(l, outputStructPortLogics); + } + } + } else { + // Non-struct sub-module input port: alias as before. + for (var i = 0; i < srcIds.length; i++) { + final dstIdx = pa.dstLowerIndex + i; + if (dstIdx < dstIds.length && dstIds[dstIdx] != srcIds[i]) { + idAlias[dstIds[dstIdx]] = srcIds[i]; + } + } + } + } + + // 3. LogicStructure and LogicArray: child IDs → parent-slice IDs. + // + // LogicArray elements alias their IDs to matching parent bits + // so array connectivity works. + // + // Non-array LogicStructure elements are NOT aliased. Instead, + // their parent→element mappings are collected in + // [structFieldCells] and emitted as explicit $struct_field + // cells after alias resolution. This preserves element signals + // (e.g. "a_mantissa") as distinct named wires visible in the + // schematic, rather than collapsing them into parent bit ranges. + // + // For arrays with explicit $slice/$concat cells (from + // _BusSubsetForArraySlice / _SwizzleForArrayConcat), aliasing + // is skipped entirely — the cells provide the structural link. + // + // Applied to ALL instances (ports AND internal signals) since + // internal arrays/structs (e.g. constant-driven coefficients) + // also need child→parent aliasing. + // + // - LogicStructure (non-array): walks leafElements (recursive) + // - LogicArray: walks elements (direct children only, since + // each element is already a flat bitvector). + // For input array ports that have _BusSubsetForArraySlice + // cells, we skip aliasing so the $slice cells provide the + // structural connection (see _subsetReceiveArrayPort). + // + // When a child ID was already aliased (e.g. by step 1 to a + // constant driver), we also redirect that prior target to the + // parent ID so the transitive chain resolves correctly: + // constId → childId → parentId. + void aliasChildToParent(int childId, int parentId) { + if (childId == parentId) { + return; + } + // If childId already aliases somewhere (e.g. constId → childId + // was set in step 1 as childId → constId), redirect that old + // target to parentId as well, so constId → parentId. + final existing = idAlias[childId]; + if (existing != null && existing != parentId) { + idAlias[existing] = parentId; + } + idAlias[childId] = parentId; + } + + // Collect LogicArray ports that have explicit array_slice or + // array_concat submodules so we can skip aliasing them (the + // $slice/$concat cells provide the structural link). + final arraysWithExplicitCells = {}; + for (final inst in synthDef.subModuleInstantiations) { + if (inst.module is _BusSubsetForArraySlice) { + // The input of the BusSubset is the array port. + for (final inputSL in inst.inputMapping.values) { + final logic = synthDef.logicToSynthMap.entries + .where( + (e) => e.value == inputSL || e.value.replacement == inputSL) + .map((e) => e.key) + .firstOrNull; + if (logic != null && logic is LogicArray) { + arraysWithExplicitCells.add(logic); + } + // Also check the resolved replacement chain. + final resolved = NetlistUtils.resolveReplacement(inputSL); + final logic2 = synthDef.logicToSynthMap.entries + .where((e) => e.value == resolved) + .map((e) => e.key) + .firstOrNull; + if (logic2 != null && logic2 is LogicArray) { + arraysWithExplicitCells.add(logic2); + } + } + } + if (inst.module is _SwizzleForArrayConcat) { + // The output of the Swizzle is the array signal. + for (final outputSL in inst.outputMapping.values) { + final logic = synthDef.logicToSynthMap.entries + .where((e) => + e.value == outputSL || e.value.replacement == outputSL) + .map((e) => e.key) + .firstOrNull; + if (logic != null && logic is LogicArray) { + arraysWithExplicitCells.add(logic); + } + } + } + } + + for (final entry in synthDef.logicToSynthMap.entries) { + final logic = entry.key; + if (logic is! LogicStructure) { + continue; + } + final parentSL = entry.value; + final parentIds = getIds(parentSL); + + if (logic is LogicArray) { + // Skip aliasing for arrays that have explicit $slice/$concat cells. + if (arraysWithExplicitCells.contains(logic)) { + continue; + } + // Array: alias each element's IDs to matching parent slice. + var idx = 0; + for (final element in logic.elements) { + final elemSL = synthDef.logicToSynthMap[element]; + if (elemSL != null) { + final elemIds = getIds(elemSL); + for (var i = 0; + i < elemIds.length && idx + i < parentIds.length; + i++) { + aliasChildToParent(elemIds[i], parentIds[idx + i]); + } + } + idx += element.width; + } + } else { + // Struct: collect element→parent mappings for $struct_field + // cell emission instead of aliasing. This preserves named + // field signals as distinct wires connected through explicit + // cells, making them visible in the schematic and evaluable + // by the netlist evaluator. + // + // Skip output struct ports of the current module — those are + // handled by $struct_compose cells (from Step 2). + if (outputStructPortLogics.contains(logic)) { + continue; + } + var idx = 0; + for (final elem in logic.elements) { + final elemSL = synthDef.logicToSynthMap[elem]; + if (elemSL != null) { + final elemIds = getIds(elemSL); + final sliceLen = elemIds.length < parentIds.length - idx + ? elemIds.length + : parentIds.length - idx; + if (sliceLen > 0) { + structFieldCells.add(( + parentIds: parentIds.sublist(idx, idx + sliceLen), + elemIds: elemIds.sublist(0, sliceLen), + offset: idx, + width: sliceLen, + elemLogic: elem, + parentLogic: logic, + fullParentIds: parentIds, + )); + } + } else if (elem is LogicStructure && elem is! LogicArray) { + // Nested InterfaceStructure: the intermediate struct + // itself has no SynthLogic, but its leaf elements do + // (created by _subsetReceiveStructPort). Walk leaf + // elements and emit struct field entries for each, + // using the top-level parent as the parent Logic. + var leafIdx = idx; + for (final leaf in elem.leafElements) { + final leafSL = synthDef.logicToSynthMap[leaf]; + if (leafSL != null) { + final leafIds = getIds(leafSL); + final sliceLen = leafIds.length < parentIds.length - leafIdx + ? leafIds.length + : parentIds.length - leafIdx; + if (sliceLen > 0) { + structFieldCells.add(( + parentIds: parentIds.sublist(leafIdx, leafIdx + sliceLen), + elemIds: leafIds.sublist(0, sliceLen), + offset: leafIdx, + width: sliceLen, + elemLogic: leaf, + parentLogic: logic, + fullParentIds: parentIds, + )); + } + } + leafIdx += leaf.width; + } + } + idx += elem.width; + } + } + } + } + + // Transitively resolve an alias chain to its canonical ID. + // Uses a visited set to detect cycles created by conflicting + // child→parent and assignment aliasing directions. + int resolveAlias(int id) { + var resolved = id; + final visited = {}; + while (idAlias.containsKey(resolved)) { + if (!visited.add(resolved)) { + // Cycle detected — break the cycle by removing this entry. + idAlias.remove(resolved); + break; + } + resolved = idAlias[resolved]!; + } + return resolved; + } + + // Apply aliases to a list of bit IDs / string constants. + List applyAlias(List bits) => idAlias.isEmpty + ? bits + : bits.map((b) => b is int ? resolveAlias(b) : b).toList(); + + // -- Break shared wire IDs for array_slice cells ----------------------- + // (Populated inside the alias block below; declared here so netnames + // can reference it later.) + final arraySliceOldToNew = {}; + + // Alias port bits. + if (idAlias.isNotEmpty) { + for (final p in ports.values) { + p['bits'] = applyAlias((p['bits']! as List).cast()); + } + // Alias cell connections. + for (final c in cells.values) { + final conns = c['connections']! as Map; + for (final key in conns.keys.toList()) { + conns[key] = applyAlias((conns[key] as List).cast()); + } + } + + // After aliasing, the slice output Y bits share the same wire IDs + // as the corresponding sub-range of input A (because LogicArray + // elements share the parent's bit storage). This makes the slice + // trivial and it would be elided below. + // + // To preserve the structural decomposition in the schematic, we + // allocate fresh wire IDs for each array_slice Y output, then + // redirect all other cells that consume those IDs as inputs to + // read from the fresh IDs instead. The slice input A keeps the + // original parent-array IDs, so the data flow becomes: + // parent (original IDs) → slice A → slice Y (fresh IDs) → consumer + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith('array_slice')) { + continue; + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'output') { + continue; + } + final oldBits = (portEntry.value as List).cast(); + conns[portEntry.key] = [ + for (final b in oldBits) + b is int ? arraySliceOldToNew.putIfAbsent(b, () => nextId++) : b, + ]; + } + } + + // Redirect other cells: any input port bit that matches an old ID + // gets replaced with the corresponding fresh ID. + if (arraySliceOldToNew.isNotEmpty) { + for (final cellEntry in cells.entries) { + if (cellEntry.key.startsWith('array_slice')) { + continue; // skip the slice cells themselves + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'input') { + continue; + } + final bits = (portEntry.value as List).cast(); + final newBits = [ + for (final b in bits) b is int ? (arraySliceOldToNew[b] ?? b) : b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + + // -- Elide trivial $slice cells ---------------------------------- + // Also elide struct_slice cells (`_BusSubsetForStructSlice` + // instances from `_subsetReceiveStructPort`) because the new + // `$struct_unpack` cells emitted below supersede them with + // better-named field-level connections. + cells.removeWhere((cellKey, cell) { + if (cell['type'] != r'$slice') { + return false; + } + // Unconditionally remove struct_slice cells — they are + // duplicated by $struct_unpack cells which carry field names. + if (cellKey.startsWith('struct_slice')) { + return true; + } + final params = cell['parameters'] as Map?; + final offset = params?['OFFSET']; + if (offset is! int) { + return false; + } + final conns = cell['connections']! as Map; + final aBits = conns['A'] as List?; + final yBits = conns['Y'] as List?; + if (aBits == null || yBits == null) { + return false; + } + return yBits.indexed.every((e) => + offset + e.$1 < aBits.length && e.$2 == aBits[offset + e.$1]); + }); + } + + // -- Emit $struct_unpack cells for LogicStructure elements ---------- + // Group per-field entries by their parent LogicStructure and emit a + // single multi-port cell per group. Each group has: + // • input port A: the full parent bus (packed bitvector) + // • one output port per non-trivial field: bits for that field + // This replaces the old per-field $struct_field cells. + if (synthDef != null && structFieldCells.isNotEmpty) { + // Group by parent Logic identity. + final groups = parentIds, + List elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds, + })>>{}; + for (final sf in structFieldCells) { + (groups[sf.parentLogic] ??= []).add(sf); + } + + var suIdx = 0; + for (final entry in groups.entries) { + final parentLogic = entry.key; + final fields = entry.value; + final fullParentIds = fields.first.fullParentIds; + final resolvedParentBits = applyAlias(fullParentIds.cast()); + + // Filter out trivial fields (input slice == output after aliasing). + final nonTrivialFields = fields + .map((sf) { + final resolvedElemBits = applyAlias(sf.elemIds.cast()); + return ( + resolvedElemBits: resolvedElemBits, + offset: sf.offset, + width: sf.width, + elemLogic: sf.elemLogic, + ); + }) + .where((f) => !f.resolvedElemBits.indexed.every((e) { + final (i, bit) = e; + return f.offset + i < resolvedParentBits.length && + bit == resolvedParentBits[f.offset + i]; + })) + .toList(); + + if (nonTrivialFields.isEmpty) { + continue; + } + + // Derive struct name for the cell key. + final structName = Sanitizer.sanitizeSV(parentLogic.name); + + // Build element range table for the parent struct so we can + // derive proper field names even when the leaf Logic objects + // have unpreferred names like `_swizzled`. + // Same strategy as $struct_pack: walk the hierarchy collecting + // (start, end, name, path, indexInParent) and look up the + // narrowest non-unpreferred range for each field offset. + final suElementRanges = <({ + int start, + int end, + String name, + String path, + int indexInParent, + })>[]; + if (parentLogic is LogicStructure) { + void walkStruct( + LogicStructure struct, int baseOffset, String parentPath) { + var offset = baseOffset; + for (var idx = 0; idx < struct.elements.length; idx++) { + final elem = struct.elements[idx]; + final elemEnd = offset + elem.width; + final elemPath = + parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; + suElementRanges.add(( + start: offset, + end: elemEnd, + name: elem.name, + path: elemPath, + indexInParent: idx, + )); + if (elem is LogicStructure && elem is! LogicArray) { + walkStruct(elem, offset, elemPath); + } + offset = elemEnd; + } + } + + walkStruct(parentLogic, 0, ''); + } + + String suFieldNameFor(int fieldOffset, String fallbackName) { + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestNamed; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestAny; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? narrowest; + + for (final r in suElementRanges) { + if (fieldOffset >= r.start && fieldOffset < r.end) { + final span = r.end - r.start; + if (narrowest == null || + span < (narrowest.end - narrowest.start)) { + narrowest = r; + } + if (bestAny == null || span < (bestAny.end - bestAny.start)) { + bestAny = r; + } + if (!Naming.isUnpreferred(r.name)) { + if (bestNamed == null || + span < (bestNamed.end - bestNamed.start)) { + bestNamed = r; + } + } + } + } + + if (bestNamed != null) { + if (narrowest != null && + (narrowest.end - narrowest.start) < + (bestNamed.end - bestNamed.start)) { + final bestNamedPrefix = bestNamed.path; + if (narrowest.path.length > bestNamedPrefix.length && + narrowest.path.startsWith(bestNamedPrefix)) { + final suffix = + narrowest.path.substring(bestNamedPrefix.length + 1); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + // All matching elements have unpreferred names — use the + // narrowest element's positional index as discriminator. + if (narrowest != null && Naming.isUnpreferred(narrowest.name)) { + return 'anonymous_${narrowest.indexInParent}'; + } + return bestAny?.name ?? fallbackName; + } + + // Build port_directions and connections with one output per field. + final portDirs = {'A': 'input'}; + final conns = >{'A': resolvedParentBits}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = suFieldNameFor(f.offset, f.elemLogic.name); + // Disambiguate duplicate field names with index suffix. + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = 'output'; + conns[portName] = f.resolvedElemBits; + } + + // Parameters list field metadata for the schematic viewer. + final params = { + 'STRUCT_NAME': parentLogic.name, + 'FIELD_COUNT': nonTrivialFields.length, + }; + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + params['FIELD_${i}_NAME'] = + suFieldNameFor(f.offset, f.elemLogic.name); + params['FIELD_${i}_OFFSET'] = f.offset; + params['FIELD_${i}_WIDTH'] = f.width; + } + + cells['struct_unpack_${suIdx}_$structName'] = { + 'hide_name': 0, + 'type': r'$struct_unpack', + 'parameters': params, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': conns, + }; + suIdx++; + } + } + + // -- Emit $struct_pack cells for output struct ports ------------------ + // Group compose entries by destination port and emit a single + // multi-port cell per group. Each group has: + // • one input port per non-trivial field + // • output port Y: the full packed output bus + // This replaces the old per-field $struct_compose cells. + if (structComposeCells.isNotEmpty) { + // Group by destination SynthLogic identity. + final composeGroups = srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + })>>{}; + for (final sc in structComposeCells) { + (composeGroups[sc.dstSynthLogic] ??= []).add(sc); + } + + var spIdx = 0; + for (final entry in composeGroups.entries) { + final dstSynthLogic = entry.key; + final fields = entry.value; + final resolvedDstBits = applyAlias(fields.first.dstIds.cast()); + + // Filter out trivial fields. + final nonTrivialFields = fields + .map((sc) { + final resolvedSrcBits = applyAlias(sc.srcIds.cast()); + final yBits = resolvedDstBits.sublist( + sc.dstLowerIndex, sc.dstUpperIndex + 1); + return ( + resolvedSrcBits: resolvedSrcBits, + yBits: yBits, + dstLowerIndex: sc.dstLowerIndex, + dstUpperIndex: sc.dstUpperIndex, + srcSynthLogic: sc.srcSynthLogic, + ); + }) + .where((f) => !f.resolvedSrcBits + .take(f.yBits.length) + .indexed + .every((e) => e.$2 == f.yBits[e.$1])) + .toList(); + + if (nonTrivialFields.isEmpty) { + continue; + } + + // Derive struct name from the destination Logic. + final dstLogic = dstSynthLogic.logics.firstOrNull; + final structName = dstLogic != null + ? Sanitizer.sanitizeSV(dstLogic.name) + : 'struct_$spIdx'; + + // Build a lookup from bit offset to the best struct element + // name, so that field names come from the struct definition + // (e.g. "data", "last", "poison") rather than the source + // signal name (which may be an internal like "_swizzled"). + // + // Elements pack LSB-first via `rswizzle`, so element[0] + // starts at offset 0, element[1] at element[0].width, etc. + // + // We collect (start, end, name, path, parentElementIndex) + // ranges for every element at every nesting level. The + // `path` carries the chain of parent struct names so we can + // produce qualified names like "mmu_info_mmuSid". When + // leaf names are unpreferred, `parentElementIndex` provides + // a fallback discriminator like "mmu_info_0". + final dstElementRanges = <({ + int start, + int end, + String name, + String path, + int indexInParent, + })>[]; + if (dstLogic is LogicStructure) { + void walkStruct( + LogicStructure struct, int baseOffset, String parentPath) { + var offset = baseOffset; + for (var idx = 0; idx < struct.elements.length; idx++) { + final elem = struct.elements[idx]; + final elemEnd = offset + elem.width; + final elemPath = + parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; + dstElementRanges.add(( + start: offset, + end: elemEnd, + name: elem.name, + path: elemPath, + indexInParent: idx, + )); + if (elem is LogicStructure && elem is! LogicArray) { + walkStruct(elem, offset, elemPath); + } + offset = elemEnd; + } + } + + walkStruct(dstLogic, 0, ''); + } + + /// Look up the field name for a compose entry by finding the + /// best struct element whose range contains [dstLowerIndex]. + /// + /// Strategy (deepest-first): + /// 1. Find the narrowest element with a non-unpreferred name. + /// 2. If a narrower unpreferred leaf exists under a named + /// parent, try to qualify with the leaf's proper name + /// (e.g. `mmu_info_mmuSid`). + /// 3. If the leaf name is also unpreferred, fall back to the + /// parent name qualified by the leaf's positional index + /// (e.g. `mmu_info_0`, `mmu_info_1`). + /// 4. Falls back to the resolved source SynthLogic name. + String fieldNameFor( + int dstLowerIndex, + SynthLogic srcSynthLogic, + ) { + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestNamed; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestAny; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? narrowest; + + for (final r in dstElementRanges) { + if (dstLowerIndex >= r.start && dstLowerIndex < r.end) { + final span = r.end - r.start; + if (narrowest == null || + span < (narrowest.end - narrowest.start)) { + narrowest = r; + } + if (bestAny == null || span < (bestAny.end - bestAny.start)) { + bestAny = r; + } + if (!Naming.isUnpreferred(r.name)) { + if (bestNamed == null || + span < (bestNamed.end - bestNamed.start)) { + bestNamed = r; + } + } + } + } + + if (bestNamed != null) { + // Check if there's a narrower child element under + // bestNamed that we can use to discriminate. + if (narrowest != null && + (narrowest.end - narrowest.start) < + (bestNamed.end - bestNamed.start)) { + final bestNamedPrefix = bestNamed.path; + // Try using the child's proper name as qualifier. + if (narrowest.path.length > bestNamedPrefix.length && + narrowest.path.startsWith(bestNamedPrefix)) { + final suffix = + narrowest.path.substring(bestNamedPrefix.length + 1); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + // Child has unpreferred name — use positional index. + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + return bestAny?.name ?? + NetlistUtils.resolveReplacement(srcSynthLogic).name; + } + + // Build port_directions and connections. + final portDirs = {}; + final conns = >{}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = fieldNameFor(f.dstLowerIndex, f.srcSynthLogic); + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = 'input'; + conns[portName] = f.resolvedSrcBits; + } + + // Output port Y: full destination bus. + portDirs['Y'] = 'output'; + conns['Y'] = resolvedDstBits; + + // Parameters list field metadata for the schematic viewer. + final params = { + 'STRUCT_NAME': dstLogic?.name ?? 'struct', + 'FIELD_COUNT': nonTrivialFields.length, + }; + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + params['FIELD_${i}_NAME'] = + fieldNameFor(f.dstLowerIndex, f.srcSynthLogic); + params['FIELD_${i}_OFFSET'] = f.dstLowerIndex; + params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; + } + + cells['struct_pack_${spIdx}_$structName'] = { + 'hide_name': 0, + 'type': r'$struct_pack', + 'parameters': params, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': conns, + }; + spIdx++; + } + } + + // -- Passthrough buffer insertion ------------------------------------ + // When a signal passes directly from an input port to an output port, + // they share the same wire IDs after aliasing. This causes the signal + // to appear routed *around* the module in the netlist rather than + // *through* it. Insert a `$buf` cell to break the wire-ID sharing, + // giving the output port fresh IDs driven by the buffer. + { + final inputBitIds = ports.values + .where((p) => p['direction'] == 'input' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType() + .toSet(); + + // Check each output port for overlap with input bits. + var bufIdx = 0; + for (final p + in ports.entries.where((p) => p.value['direction'] == 'output')) { + final outBits = (p.value['bits']! as List).cast(); + if (!outBits.any((b) => b is int && inputBitIds.contains(b))) { + continue; + } + + // Allocate fresh wire IDs for the output side of the buffer. + final freshBits = + List.generate(outBits.length, (_) => nextId++); + + // Insert a $buf cell: input = original (shared) IDs, + // output = fresh IDs. + cells['passthrough_buf_$bufIdx'] = + NetlistUtils.makeBufCell(outBits.length, outBits, freshBits); + + // Update the output port to use the fresh IDs. + p.value['bits'] = freshBits; + bufIdx++; + } + } + + // -- Dead-cell elimination (DCE) ------------------------------------- + // After aliasing and elision, some cells may have inputs whose wire + // IDs are not driven by any cell output or module input port. This + // typically happens when a LogicStructure's `packed` representation + // creates a Swizzle chain whose inputs reference sub-module-internal + // signals that are not accessible from the synthesised module's + // scope. Iteratively remove such dead cells using both forward + // (all-inputs-undriven) and backward (all-outputs-unconsumed) DCE. + if (options.enableDCE) { + var dceChanged = true; + while (dceChanged) { + dceChanged = false; + + // Build set of driven wire IDs (from input/inout ports and cell + // outputs). + final drivenIds = { + ...ports.values + .where( + (p) => p['direction'] == 'input' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + // Build set of consumed wire IDs (from output/inout ports and + // cell inputs). + final consumedIds = { + ...ports.values + .where((p) => + p['direction'] == 'output' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + // Forward DCE: remove cells whose inputs are ALL undriven. + cells + ..removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + final inputPorts = + conns.entries.where((pe) => pdirs[pe.key] == 'input'); + if (inputPorts.isEmpty) { + return false; + } + final allUndriven = !inputPorts + .expand((pe) => pe.value as List) + .any((b) => (b is int && drivenIds.contains(b)) || b is String); + if (allUndriven) { + dceChanged = true; + return true; + } + return false; + }) + + // Backward DCE: remove cells whose outputs are ALL unconsumed. + // Preserve non-leaf cells (user module instances) — their type + // does not start with '$' (Yosys primitive convention). Users + // expect to see all instantiated modules in the schematic even + // when outputs are unconnected. + ..removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + final cellType = cell['type'] as String? ?? ''; + if (!cellType.startsWith(r'$')) { + return false; + } + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + final outputPorts = + conns.entries.where((pe) => pdirs[pe.key] == 'output'); + if (outputPorts.isEmpty) { + return false; + } + final allUnconsumed = !outputPorts + .expand((pe) => pe.value as List) + .whereType() + .any(consumedIds.contains); + if (allUnconsumed) { + dceChanged = true; + return true; + } + return false; + }); + } + } + + // -- Constant driver cells ------------------------------------------- + // Generated AFTER the aliasing pass so that constants discovered + // during aliasing (via getIds(assignment.src)) are included. + // Constant IDs may have been redirected by step 3 (struct/array + // child→parent aliasing), so apply alias resolution to their + // connection bits. + { + var constIdx = 0; + final emittedConstWires = {}; + for (final entry in synthLogicIds.entries + .where((e) => e.key.isConstant) + .where((e) => !blockedConstSynthLogics.contains(e.key)) + .where((e) => e.value.isNotEmpty)) { + final sl = entry.key; + final constValue = NetlistUtils.constValueFromSynthLogic(sl); + if (constValue == null) { + continue; + } + final ids = entry.value; + + // Resolve aliases and skip if these wires are already driven + // by a previously emitted $const cell (can happen when aliasing + // merges two SynthLogic constants onto the same wire IDs). + final resolvedIds = applyAlias(ids.cast()); + final firstWire = + resolvedIds.firstWhere((b) => b is int, orElse: () => -1); + if (firstWire is int && firstWire >= 0) { + if (emittedConstWires.contains(firstWire)) { + continue; + } + emittedConstWires.addAll(resolvedIds.whereType()); + } + + final valuePart = NetlistUtils.constValuePart(constValue); + final cellName = 'const_${constIdx}_$valuePart'; + final valueLiteral = valuePart.replaceFirst('_', "'"); + + cells[cellName] = { + 'hide_name': 0, + 'type': r'$const', + 'parameters': {}, + 'attributes': {}, + 'port_directions': {valueLiteral: 'output'}, + 'connections': >{ + valueLiteral: resolvedIds, + }, + }; + constIdx++; + } + } + + // -- Remove floating $const cells ------------------------------------ + // The $const cells were emitted after the main DCE pass, so they + // may reference wire IDs that no cell input or output port consumes. + if (options.enableDCE) { + final consumedByInputs = { + ...ports.values + .where( + (p) => p['direction'] == 'output' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + cells.removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + if (cell['type'] != r'$const') { + return false; + } + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return !conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType() + .any(consumedByInputs.contains); + }); + } + + // -- Break shared wire IDs for array_concat cells -------------------- + // After aliasing, the concat inputs share the same wire IDs as the + // concat Y output (because LogicArray elements share the parent's + // bit storage). This makes the concat transparent -- constants + // appear to drive the parent array directly. + // + // To fix: allocate fresh wire IDs for each concat input port, + // then redirect all other cells whose outputs used those old IDs + // to drive the fresh IDs instead. The concat Y output keeps the + // original parent-array IDs, so the data flow becomes: + // const → fresh_IDs → concat input → concat Y (= parent IDs) + final arrayConcatOldToNew = {}; + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith('array_concat')) { + continue; + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'input') { + continue; + } + final oldBits = (portEntry.value as List).cast(); + conns[portEntry.key] = [ + for (final b in oldBits) + b is int ? arrayConcatOldToNew.putIfAbsent(b, () => nextId++) : b, + ]; + } + } + + // Redirect other cells: any output port bit that matches an old ID + // gets replaced with the corresponding fresh ID. + if (arrayConcatOldToNew.isNotEmpty) { + for (final cellEntry in cells.entries) { + if (cellEntry.key.startsWith('array_concat')) { + continue; // skip the concat cells themselves + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'output') { + continue; + } + final bits = (portEntry.value as List).cast(); + final newBits = [ + for (final b in bits) b is int ? (arrayConcatOldToNew[b] ?? b) : b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + + // -- Netnames -------------------------------------------------------- + final netnames = {}; + final emittedNames = {}; + + // InlineSystemVerilog modules are pure combinational — all their + // signals are derivable from the gate netlist. + final isInlineSV = module is InlineSystemVerilog; + + void addNetname(String name, List bits, + {bool hideName = false, + bool computed = false, + Map? logicType}) { + if (emittedNames.contains(name)) { + return; + } + emittedNames.add(name); + netnames[name] = { + 'bits': bits, + if (hideName) 'hide_name': 1, + if (logicType != null) 'logic_type': logicType, + 'attributes': { + if (computed || isInlineSV) 'computed': 1, + }, + }; + } + + // Port nets (already aliased above). + for (final p in ports.entries) { + addNetname( + Sanitizer.sanitizeSV(p.key), + (p.value['bits']! as List).cast(), + logicType: p.value['logic_type'] as Map?, + ); + } + + // Named signals from SynthModuleDefinition. + if (synthDef != null) { + for (final entry in synthLogicIds.entries + .where((e) => !e.key.isConstant && !e.key.declarationCleared)) { + final sl = entry.key; + final name = NetlistUtils.tryGetSynthLogicName(sl); + if (name != null) { + var bits = applyAlias(entry.value.cast()); + // For element signals whose IDs were remapped by the + // array_slice fresh-ID pass, apply that mapping so the + // element netname matches the slice output (fresh) IDs. + if (arraySliceOldToNew.isNotEmpty && sl is SynthLogicArrayElement) { + bits = bits + .map((b) => b is int ? (arraySliceOldToNew[b] ?? b) : b) + .toList(); + } + // For element signals whose IDs were remapped by the + // array_concat fresh-ID pass, apply that mapping so the + // element netname matches the concat input (fresh) IDs. + if (arrayConcatOldToNew.isNotEmpty && sl is SynthLogicArrayElement) { + bits = bits + .map((b) => b is int ? (arrayConcatOldToNew[b] ?? b) : b) + .toList(); + } + final typeLogic = NetlistUtils.typeLogicFromSynthLogic(sl); + addNetname( + Sanitizer.sanitizeSV(name), + bits, + logicType: typeLogic != null + ? NetlistUtils.buildLogicType(typeLogic, bits) + : null, + ); + } + } + } + + // Constant netnames for non-blocked constants (already aliased via + // cell connections above). + for (final cellEntry + in cells.entries.where((e) => e.value['type'] == r'$const')) { + final conns = + cellEntry.value['connections'] as Map>?; + if (conns != null && conns.isNotEmpty) { + addNetname(cellEntry.key, conns.values.first, computed: true); + } + } + + // -- Ensure every bit ID in cell connections has a netname ------------ + { + final coveredIds = netnames.values + .expand( + (nn) => ((nn! as Map)['bits'] as List?) ?? []) + .whereType() + .toSet(); + + for (final cellEntry in cells.entries) { + final cellName = cellEntry.key; + final conns = + cellEntry.value['connections'] as Map? ?? {}; + for (final connEntry in conns.entries) { + final portName = connEntry.key; + final bits = connEntry.value as List; + final missingBits = []; + for (final b in bits) { + if (b is int && !coveredIds.contains(b)) { + missingBits.add(b); + coveredIds.add(b); + } + } + if (missingBits.isNotEmpty) { + addNetname( + Sanitizer.sanitizeSV('${cellName}_$portName'), missingBits, + hideName: true); + } + } + } + } + + // -- Remove orphaned netnames after DCE -------------------------------- + // DCE may remove cells that drove certain named signals. Remove + // netnames whose integer bits are ALL undriven (not output of any + // remaining cell, not a module input/inout port bit). This prevents + // dead signals from appearing in the schematic viewer. + if (options.enableDCE) { + final drivenBits = { + ...ports.values + .where( + (p) => p['direction'] == 'input' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = + cell['connections'] as Map? ?? const {}; + final pdirs = + cell['port_directions'] as Map? ?? const {}; + return conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + netnames.removeWhere((name, nnRaw) { + final nn = nnRaw as Map?; + if (nn == null) { + return false; + } + final bits = nn['bits'] as List?; + if (bits == null) { + return false; + } + final intBits = bits.whereType(); + if (intBits.isEmpty) { + return false; + } + return !intBits.any(drivenBits.contains); + }); + } + + // -- Slim: strip cell connections ------------------------------------ + // The full pipeline ran identically, so the cell set (keys, ordering) + // is canonical. Now drop the connection maps to reduce the output + // size. This is the ONLY difference between slim and full output. + if (options.slimMode) { + for (final cell in cells.values) { + cell.remove('connections'); + } + } + + // -- Structural validation ------------------------------------------- + // Debug-mode checks to catch netlist bugs early. These verify that + // every signal has a driver and every cell is connected. + assert(() { + _validateNetlist(ports, cells, netnames, module.name); + return true; + }(), 'Netlist structural validation failed for ${module.name}'); + + return NetlistSynthesisResult( + module, + getInstanceTypeOfModule, + ports: ports, + cells: cells, + netnames: netnames, + attributes: attr, + ); + } + + /// Validates structural integrity of the netlist. + /// + /// Checks: + /// 1. No non-transparent cell is fully disconnected — every logic gate + /// must have at least one output bit consumed by another cell's + /// input or a module output/inout port. + /// 2. No `$const` cell drives wire IDs that nothing consumes. + /// + /// These fire only under `assert()` (i.e. `--enable-asserts`) so they + /// don't affect production runs. + static void _validateNetlist( + Map> ports, + Map> cells, + Map netnames, + String moduleName, + ) { + // Collect all wire IDs consumed by cell input ports or module + // output/inout ports. + final consumedBits = { + ...ports.values + .where((p) => p['direction'] == 'output' || p['direction'] == 'inout') + .expand((p) => (p['bits'] as List?) ?? []) + .whereType(), + ...cells.values.expand((c) { + final conns = c['connections'] as Map? ?? {}; + final pdirs = c['port_directions'] as Map? ?? {}; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => (pe.value as List?) ?? []) + .whereType(); + }), + }; + + const transparentTypes = { + r'$buf', + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + // Check 1: no logic gate is fully disconnected. + for (final entry in cells.entries) { + final cell = entry.value; + final type = cell['type'] as String? ?? ''; + if (transparentTypes.contains(type) || type == r'$const') { + continue; + } + + final conns = cell['connections'] as Map?; + final pdirs = cell['port_directions'] as Map?; + if (conns == null || pdirs == null) { + continue; + } + + final outputBits = conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => (pe.value as List?) ?? []) + .whereType() + .toList(); + + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + // ignore: avoid_print + print('[netlist-validate] WARNING: $moduleName: ' + 'cell "${entry.key}" (type: $type) has no consumed outputs ' + '— fully disconnected logic gate'); + } + } + + // Check 2: no $const cell drives unconsumed wires. + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$const') { + continue; + } + + final conns = cell['connections'] as Map?; + final pdirs = cell['port_directions'] as Map?; + if (conns == null || pdirs == null) { + continue; + } + + final outputBits = conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => (pe.value as List?) ?? []) + .whereType() + .toList(); + + if (outputBits.isNotEmpty && !outputBits.any(consumedBits.contains)) { + // ignore: avoid_print + print('[netlist-validate] WARNING: $moduleName: ' + r'$const cell "${entry.key}" drives wires consumed by nothing ' + '— floating constant'); + } + } + } + + /// Apply all post-processing passes to the modules map. + /// + /// This is the canonical pass ordering used by both netlist flows: + /// **Flow 1** (slim batch via `_synthesizeSlimModules`) and + /// **Flow 2** (incremental full via `moduleNetlistJson`). + /// Also used internally by [buildModulesMap] / [synthesizeToJson]. + void applyPostProcessingPasses( + Map> modules, + ) { + if (options.collapseTransparentClusters) { + NetlistPasses.applyTransparentClustering(modules); + } + } + + /// Build the processed modules map from a [SynthBuilder]'s results. + /// + /// Returns the intermediate module map (definition name → module data) + /// after all post-processing passes have been applied. This allows + /// callers to retain per-module results for incremental serving while + /// avoiding redundant re-synthesis. + Map> buildModulesMap( + SynthBuilder synth, Module top) { + final swEntries = Stopwatch()..start(); + final modules = NetlistPasses.collectModuleEntries( + synth.synthesisResults, + topModule: top, + ); + swEntries.stop(); + + final swPasses = Stopwatch()..start(); + applyPostProcessingPasses(modules); + swPasses.stop(); + + return modules; + } + + /// Generate the combined netlist JSON from a [SynthBuilder]'s results. + String generateCombinedJson(SynthBuilder synth, Module top) { + final swCollect = Stopwatch()..start(); + final modules = buildModulesMap(synth, top); + swCollect.stop(); + + final swCompress = Stopwatch()..start(); + if (options.compressBitRanges) { + _compressModulesMap(modules); + } + swCompress.stop(); + + final combined = { + 'creator': 'NetlistSynthesizer (rohd)', + 'version': NetlistService.formatVersion, + 'modules': modules, + }; + + final swEncode = Stopwatch()..start(); + final encoder = options.compactJson + ? const JsonEncoder() + : const JsonEncoder.withIndent(' '); + final result = encoder.convert(combined); + swEncode.stop(); + + return result; + } + + /// Compresses a list of bit IDs by replacing contiguous ascending runs of + /// 3 or more integers with `"start:end"` range strings. + static List _compressBits(List bits) { + final result = []; + final pending = []; + + void flushPending() { + if (pending.isEmpty) { + return; + } + var i = 0; + while (i < pending.length) { + var j = i; + while (j + 1 < pending.length && pending[j + 1] == pending[j] + 1) { + j++; + } + final runLen = j - i + 1; + if (runLen >= 3) { + result.add('${pending[i]}:${pending[j]}'); + } else { + for (var k = i; k <= j; k++) { + result.add(pending[k]); + } + } + i = j + 1; + } + pending.clear(); + } + + for (final element in bits) { + if (element is int) { + pending.add(element); + } else { + flushPending(); + result.add(element); + } + } + flushPending(); + return result; + } + + /// Applies [_compressBits] to all `bits` arrays and cell `connections` + /// arrays in a modules map. + static void _compressModulesMap( + Map> modules, + ) { + for (final moduleDef in modules.values) { + final ports = moduleDef['ports'] as Map>?; + if (ports != null) { + for (final port in ports.values) { + final bits = port['bits']; + if (bits is List) { + port['bits'] = _compressBits(bits.cast()); + } + } + } + + final cells = moduleDef['cells'] as Map>?; + if (cells != null) { + for (final cell in cells.values) { + final conns = cell['connections'] as Map>?; + if (conns != null) { + for (final key in conns.keys.toList()) { + conns[key] = _compressBits(conns[key]!); + } + } + } + } + + final netnames = moduleDef['netnames'] as Map?; + if (netnames != null) { + for (final entry in netnames.values) { + if (entry is Map) { + final bits = entry['bits']; + if (bits is List) { + entry['bits'] = _compressBits(bits.cast()); + } + } + } + } + } + } + + /// Convenience: synthesize [top] into a combined netlist JSON string. + /// + /// Builds a [SynthBuilder] internally and returns the full JSON. + /// + /// The [packageRoot] parameter is accepted for API compatibility with + /// downstream trace-enabled branches. + String synthesizeToJson(Module top, {String? packageRoot}) { + final sb = SynthBuilder(top, this); + return generateCombinedJson(sb, top); + } +} + +/// A version of [BusSubset] that creates explicit `$slice` cells for +/// [LogicArray] element extraction in the netlist. +/// +/// When a [LogicArray] port is decomposed into its elements, each element +/// gets its own [_BusSubsetForArraySlice] so the netlist shows explicit +/// select gates rather than flat bit aliasing. +class _BusSubsetForArraySlice extends BusSubset { + _BusSubsetForArraySlice( + super.bus, + super.startIndex, + super.endIndex, + ) : super(name: 'array_slice'); + + @override + bool get hasBuilt => true; +} + +/// A version of [Swizzle] that creates explicit `$concat` cells for +/// [LogicArray] element assembly in the netlist. +/// +/// When a [LogicArray]'s elements are driven independently (e.g. by +/// constants), this creates a visible concat gate in the netlist that +/// assembles the element signals into the full packed array bus. +class _SwizzleForArrayConcat extends Swizzle { + _SwizzleForArrayConcat(super.signals) : super(name: 'array_concat'); + + @override + bool get hasBuilt => true; +} diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart new file mode 100644 index 000000000..1906e2e6d --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -0,0 +1,448 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_utils.dart +// Shared utility functions for netlist synthesis and post-processing passes. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Shared utility functions for netlist synthesis and post-processing passes. +/// +/// All methods are static — no instances are created. +class NetlistUtils { + NetlistUtils._(); + + /// Find the port name in [portMap] that corresponds to [sl]. + static String? portNameForSynthLogic( + SynthLogic sl, Map portMap) { + for (final e in portMap.entries) { + if (sl.logics.contains(e.value)) { + return e.key; + } + } + return null; + } + + /// Safely retrieve the name from a [SynthLogic], returning null if + /// retrieval fails (e.g. name not yet picked, or the SynthLogic has + /// been replaced). + static String? tryGetSynthLogicName(SynthLogic sl) { + try { + return sl.name; + // ignore: avoid_catches_without_on_clauses + } catch (_) { + return null; + } + } + + /// Resolves [sl] to the end of its replacement chain. + static SynthLogic resolveReplacement(SynthLogic sl) { + var r = sl; + while (r.replacement != null) { + r = r.replacement!; + } + return r; + } + + /// Create a `$buf` cell map. + static Map makeBufCell( + int width, + List aBits, + List yBits, + ) => + { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': width}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{'A': aBits, 'Y': yBits}, + }; + + /// Collapses bit-slice ports of a Combinational/Sequential cell into + /// aggregate ports. + /// + /// **Input side**: When a Combinational references individual struct fields, + /// each field creates a BusSubset in the parent scope, and each slice + /// becomes a separate input port. This method detects groups of input + /// ports whose SynthLogics are outputs of BusSubset submodule + /// instantiations that slice the same root signal. For each group + /// forming a contiguous bit range, the N individual ports are replaced + /// with a single aggregate port connected to the corresponding sub-range + /// of the root signal's wire IDs. + /// + /// **Output side**: Similarly, Combinational output ports that feed into + /// the inputs of the same Swizzle submodule are collapsed into a single + /// aggregate port connected to the Swizzle's output wire IDs. + static void collapseAlwaysBlockPorts( + SynthModuleDefinition synthDef, + SynthSubModuleInstantiation instance, + Map portDirs, + Map> connections, + List Function(SynthLogic) getIds, + ) { + // ── Input-side collapsing (BusSubset → Combinational) ────────────── + + // Build reverse lookup: resolved BusSubset output SynthLogic → + // (BusSubset module, resolved root input SynthLogic, + // SynthSubModuleInstantiation). + final busSubsetLookup = + {}; + for (final bsInst in synthDef.subModuleInstantiations) { + if (bsInst.module is! BusSubset) { + continue; + } + final bsMod = bsInst.module as BusSubset; + + // BusSubset has input 'original' and output 'subset' + final outputSL = bsInst.outputMapping.values.firstOrNull; + final inputSL = bsInst.inputMapping.values.firstOrNull; + if (outputSL == null || inputSL == null) { + continue; + } + + final resolvedOutput = resolveReplacement(outputSL); + final resolvedInput = resolveReplacement(inputSL); + + busSubsetLookup[resolvedOutput] = (bsMod, resolvedInput, bsInst); + } + + // Group input ports by root signal, also tracking the BusSubset + // instantiations that produced each port. + final inputGroups = >{}; + + for (final e in instance.inputMapping.entries) { + final portName = e.key; + if (!connections.containsKey(portName)) { + continue; // already filtered + } + + final resolved = resolveReplacement(e.value); + final info = busSubsetLookup[resolved]; + if (info != null) { + final (bsMod, rootSL, bsInst) = info; + final width = bsMod.endIndex - bsMod.startIndex + 1; + inputGroups + .putIfAbsent(rootSL, () => []) + .add((portName, bsMod.startIndex, width, bsInst)); + } + } + + // Collapse each group with > 1 contiguous member. + for (final entry in inputGroups.entries) { + if (entry.value.length <= 1) { + continue; + } + + final rootSL = entry.key; + final ports = entry.value..sort((a, b) => a.$2.compareTo(b.$2)); + + // Verify contiguous non-overlapping coverage. + var expectedBit = ports.first.$2; + var contiguous = true; + for (final (_, startIdx, width, _) in ports) { + if (startIdx != expectedBit) { + contiguous = false; + break; + } + expectedBit += width; + } + if (!contiguous) { + continue; + } + + final minBit = ports.first.$2; + final maxBit = ports.last.$2 + ports.last.$3 - 1; + + // Get the root signal's full wire IDs and extract the sub-range. + final rootIds = getIds(rootSL); + if (maxBit >= rootIds.length) { + continue; // safety check + } + final aggBits = rootIds.sublist(minBit, maxBit + 1).cast(); + + // Choose a name for the aggregate port. + final rootName = tryGetSynthLogicName(rootSL) ?? 'agg_${minBit}_$maxBit'; + + // Replace individual ports with the aggregate. The bypassed + // BusSubset cells are left in place; the post-synthesis DCE pass + // will remove them if their outputs are no longer consumed. + for (final (portName, _, _, _) in ports) { + connections.remove(portName); + portDirs.remove(portName); + } + connections[rootName] = aggBits; + portDirs[rootName] = 'input'; + } + + // ── Output-side collapsing (Combinational → Swizzle) ─────────────── + + // Build reverse lookup: resolved Swizzle input SynthLogic → + // (Swizzle port name, bit offset within the Swizzle output, + // port width, resolved Swizzle output SynthLogic, + // SynthSubModuleInstantiation). + final swizzleLookup = {}; + for (final szInst in synthDef.subModuleInstantiations) { + if (szInst.module is! Swizzle) { + continue; + } + final outputSL = szInst.outputMapping.values.firstOrNull; + if (outputSL == null) { + continue; + } + final resolvedOutput = resolveReplacement(outputSL); + + // Swizzle inputs are in0, in1, ... with bit-0 first. + var offset = 0; + for (final inEntry in szInst.inputMapping.entries) { + final resolvedInput = resolveReplacement(inEntry.value); + final w = resolvedInput.width; + swizzleLookup[resolvedInput] = + (inEntry.key, offset, w, resolvedOutput, szInst); + offset += w; + } + } + + // Group output ports by Swizzle output signal. + final outputGroups = >{}; + + for (final e in instance.outputMapping.entries) { + final portName = e.key; + if (!connections.containsKey(portName)) { + continue; + } + + final resolved = resolveReplacement(e.value); + final info = swizzleLookup[resolved]; + if (info != null) { + final (_, offset, width, swizzleOutputSL, szInst) = info; + outputGroups + .putIfAbsent(swizzleOutputSL, () => []) + .add((portName, offset, width, szInst)); + } + } + + // Collapse each group with > 1 contiguous member. + for (final entry in outputGroups.entries) { + if (entry.value.length <= 1) { + continue; + } + + // Skip collapsing when any member's SynthLogic is a port of the + // parent module. Collapsing replaces the individual output ports + // with a single aggregate that uses the downstream Swizzle's bit + // IDs, which would orphan the module-level port bits (they would + // no longer be driven by any cell). + final parentModule = synthDef.module; + final hasModulePort = entry.value.any((member) { + final sl = instance.outputMapping[member.$1]; + if (sl == null) { + return false; + } + final resolved = resolveReplacement(sl); + return resolved.isPort(parentModule); + }); + if (hasModulePort) { + continue; + } + + final swizOutSL = entry.key; + final ports = entry.value..sort((a, b) => a.$2.compareTo(b.$2)); + + // Verify contiguous. + var expectedBit = ports.first.$2; + var contiguous = true; + for (final (_, offset, width, _) in ports) { + if (offset != expectedBit) { + contiguous = false; + break; + } + expectedBit += width; + } + if (!contiguous) { + continue; + } + + final minBit = ports.first.$2; + final maxBit = ports.last.$2 + ports.last.$3 - 1; + + final outIds = getIds(swizOutSL); + if (maxBit >= outIds.length) { + continue; + } + final aggBits = outIds.sublist(minBit, maxBit + 1).cast(); + + final outName = + tryGetSynthLogicName(swizOutSL) ?? 'agg_out_${minBit}_$maxBit'; + + // Replace individual ports with the aggregate. The bypassed + // Swizzle cells are left in place; the post-synthesis DCE pass + // will remove them if their outputs are no longer consumed. + for (final (portName, _, _, _) in ports) { + connections.remove(portName); + portDirs.remove(portName); + } + connections[outName] = aggBits; + portDirs[outName] = 'output'; + } + } + + /// Builds a JSON-serializable type descriptor for [logic]. + /// + /// Returns: + /// - For a plain [Logic] or [LogicArray]: `{'width': N}` (bitvector is the + /// default) + /// - For a [LogicStructure] (non-array): `{'typeName': className, 'fields': + /// [field, ...]}` where each field is `{'name': fieldName, 'width': W}` for + /// leaf fields or `{'name': fieldName, 'type': {...}}` for nested + /// [LogicStructure]s. + /// + /// Fields are listed in LSB-to-MSB order (matching ROHD's element ordering + /// via `rswizzle`: `elements[0]` occupies the lowest bits). + /// + /// When [bits] is provided, each field entry also includes a `'bits'` key + /// containing the slice of [bits] that belongs to that field. This allows + /// consumers to identify which net IDs map to which field even when the + /// signal is only partially connected (where computing offsets from the flat + /// top-level `bits` array would be ambiguous). + static Map buildLogicType(Logic logic, + [List? bits]) { + if (logic is LogicArray) { + final result = { + 'width': logic.width, + 'arrayDims': logic.dimensions, + 'elementWidth': logic.elementWidth, + }; + // If the leaf elements are LogicStructures (array of structs), + // include the element type metadata for recursive expansion. + if (logic.elements.isNotEmpty) { + final first = logic.elements.first; + if (first is LogicStructure && first is! LogicArray) { + result['elementType'] = buildLogicType(first); + } else if (first is LogicArray) { + // Nested array — encode inner dimensions via recursive call. + result['elementType'] = buildLogicType(first); + } + } + return result; + } else if (logic is LogicStructure) { + var offset = 0; + final fields = logic.elements.map((e) { + final fieldBits = bits?.sublist(offset, offset + e.width); + offset += e.width; + if (e is LogicStructure && e is! LogicArray) { + return { + 'name': e.name, + if (fieldBits != null) 'bits': fieldBits, + 'type': buildLogicType(e, fieldBits), + }; + } else if (e is LogicArray) { + return { + 'name': e.name, + 'width': e.width, + if (fieldBits != null) 'bits': fieldBits, + 'type': buildLogicType(e, fieldBits), + }; + } else { + return { + 'name': e.name, + 'width': e.width, + if (fieldBits != null) 'bits': fieldBits, + }; + } + }).toList(); + return { + 'typeName': logic.runtimeType.toString(), + 'fields': fields, + }; + } else { + return {'width': logic.width}; + } + } + + /// Returns the most type-specific [Logic] from [sl]'s [Logic] list for + /// use in [buildLogicType]. + /// + /// Prefers a [LogicStructure] (non-array) over a plain [Logic], since it + /// carries richer field metadata. + static Logic? typeLogicFromSynthLogic(SynthLogic sl) { + final logics = sl.logics; + return logics + .whereType() + .where((l) => l is! LogicArray) + .firstOrNull ?? + logics.firstOrNull; + } + + /// Check if a SynthLogic is a constant (following replacement chain). + static bool isConstantSynthLogic(SynthLogic sl) => + resolveReplacement(sl).isConstant; + + /// Extract the Const value from a constant SynthLogic. + static Const? constValueFromSynthLogic(SynthLogic sl) { + final resolved = resolveReplacement(sl); + for (final logic in resolved.logics) { + if (logic is Const) { + return logic; + } + } + return null; + } + + /// Value portion of a constant name: `_h` or `_b`. + static String constValuePart(Const c) { + final bitChars = []; + var hasXZ = false; + for (var i = c.width - 1; i >= 0; i--) { + final v = c.value[i]; + switch (v) { + case LogicValue.zero: + bitChars.add('0'); + case LogicValue.one: + bitChars.add('1'); + case LogicValue.x: + bitChars.add('x'); + hasXZ = true; + case LogicValue.z: + bitChars.add('z'); + hasXZ = true; + } + } + if (hasXZ) { + return '${c.width}_b${bitChars.join()}'; + } + var value = BigInt.zero; + for (var i = c.width - 1; i >= 0; i--) { + value = value << 1; + if (c.value[i] == LogicValue.one) { + value = value | BigInt.one; + } + } + return '${c.width}_h${value.toRadixString(16)}'; + } +} diff --git a/lib/src/synthesizers/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/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..da5d76586 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,6 +1,7 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'netlist/netlist.dart'; export 'synth_builder.dart'; export 'synth_file_contents.dart'; export 'synthesis_result.dart'; diff --git a/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_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 72471eef1..0d60d62e6 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -101,10 +101,10 @@ class SystemVerilogSynthesisResult extends SynthesisResult { @override List toSynthFileContents() => List.unmodifiable([ SynthFileContents( - name: instanceTypeName, - description: 'SystemVerilog module definition for $instanceTypeName', - contents: _toVerilog(), - ) + name: instanceTypeName, + description: + 'SystemVerilog module definition for $instanceTypeName', + contents: _toVerilog()) ]); /// Representation of all input port declarations in generated SV. @@ -189,14 +189,14 @@ class SystemVerilogSynthesisResult extends SynthesisResult { [ _verilogInternalSignals(), _verilogAssignments(), // order matters! - _verilogSubModuleInstantiations(getInstanceTypeOfModule), + _verilogSubModuleInstantiations(getInstanceTypeOfModule) ].where((element) => element.isNotEmpty).join('\n'); /// The representation of all port declarations. String _verilogPorts() => [ ..._verilogInputs(), ..._verilogOutputs(), - ..._verilogInOuts(), + ..._verilogInOuts() ].join(',\n'); String? _verilogParameters(Module module) { @@ -211,7 +211,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { defParams .map((p) => 'parameter ${p.type} ${p.name} = ${p.defaultValue}') .join(',\n'), - ')', + ')' ].join('\n'); } @@ -222,11 +222,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { String _toVerilog() { final verilogModuleName = getInstanceTypeOfModule(module); return [ - [ - 'module $verilogModuleName', - _parameterString, - '(', - ].nonNulls.join(' '), + ['module $verilogModuleName', _parameterString, '('].nonNulls.join(' '), _portsString, ');', _moduleContentsString, diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..d261e91ad 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 @@ -50,11 +50,7 @@ class SystemVerilogSynthesizer extends Synthesizer { bool forceStandardInstantiation = false}) { if (!forceStandardInstantiation) { if (module is SystemVerilog) { - return module.instantiationVerilog( - instanceType, - instanceName, - ports, - ) ?? + return module.instantiationVerilog(instanceType, instanceName, ports) ?? instantiationVerilogFor( module: module, instanceType: instanceType, @@ -65,13 +61,12 @@ class SystemVerilogSynthesizer extends Synthesizer { // ignore: deprecated_member_use_from_same_package else if (module is CustomSystemVerilog) { return module.instantiationVerilog( - instanceType, - instanceName, - Map.fromEntries(ports.entries - .where((element) => module.inputs.containsKey(element.key))), - Map.fromEntries(ports.entries - .where((element) => module.outputs.containsKey(element.key))), - ); + instanceType, + instanceName, + Map.fromEntries(ports.entries + .where((element) => module.inputs.containsKey(element.key))), + Map.fromEntries(ports.entries + .where((element) => module.outputs.containsKey(element.key)))); } } @@ -127,13 +122,12 @@ class SystemVerilogSynthesizer extends Synthesizer { Map? parameters, bool forceStandardInstantiation = false}) => instantiationVerilogFor( - module: module, - instanceType: instanceType, - instanceName: instanceName, - ports: {...inputs, ...outputs, ...inOuts}, - parameters: parameters, - forceStandardInstantiation: forceStandardInstantiation, - ); + module: module, + instanceType: instanceType, + instanceName: instanceName, + ports: {...inputs, ...outputs, ...inOuts}, + parameters: parameters, + forceStandardInstantiation: forceStandardInstantiation); @override SynthesisResult synthesize( 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..bc6af3376 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. @@ -110,10 +117,6 @@ class SynthModuleDefinition { @override String toString() => "module name: '${module.name}'"; - /// Used to uniquify any identifiers, including signal names - /// and module instances. - final Uniquifier _synthInstantiationNameUniquifier; - /// Indicates whether [logic] has a corresponding present [SynthLogic] in /// this definition. @internal @@ -132,9 +135,7 @@ class SynthModuleDefinition { /// Either accesses a previously created [SynthLogic] corresponding to /// [logic], or else creates a new one and adds it to the [logicToSynthMap]. - SynthLogic? getSynthLogic( - Logic? logic, - ) { + SynthLogic? getSynthLogic(Logic? logic) { if (logic == null) { return null; } else if (!(logic.parentModule == module || @@ -244,8 +245,14 @@ class SynthModuleDefinition { for (final leafElement in port.leafElements) { final leafSynth = getSynthLogic(leafElement)!; internalSignals.add(leafSynth); - assignments.add(PartialSynthAssignment(leafSynth, portSynth, - dstUpperIndex: idx + leafElement.width - 1, dstLowerIndex: idx)); + assignments.add( + PartialSynthAssignment( + leafSynth, + portSynth, + dstUpperIndex: idx + leafElement.width - 1, + dstLowerIndex: idx, + ), + ); idx += leafElement.width; } } @@ -266,9 +273,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 +299,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 +343,9 @@ class SynthModuleDefinition { // find any named signals sitting around that don't do anything // this is not necessary for functionality, just nice naming inclusion logicsToTraverse.addAll( - module.internalSignals - .where((element) => element.naming != Naming.unnamed), + module.internalSignals.where( + (element) => element.naming != Naming.unnamed, + ), ); // make sure floating modules are included @@ -374,9 +378,10 @@ class SynthModuleDefinition { final receiver = logicsToTraverse[i]; assert( - receiver.parentModule != null, - 'Any signal traced by this should have been detected by build,' - ' but $receiver was not.'); + receiver.parentModule != null, + 'Any signal traced by this should have been detected by build,' + ' but $receiver was not.', + ); if (receiver.parentModule != module && !module.subModules.contains(receiver.parentModule)) { @@ -399,10 +404,12 @@ class SynthModuleDefinition { if (receiver is LogicNet) { // only for the leaves, that's why only `LogicNet` and not array/struct - logicsToTraverse.addAll([ - ...receiver.srcConnections, - ...receiver.dstConnections - ].where((element) => element.parentModule == module)); + logicsToTraverse.addAll( + [ + ...receiver.srcConnections, + ...receiver.dstConnections, + ].where((element) => element.parentModule == module), + ); for (final srcConnection in receiver.srcConnections) { if (srcConnection.parentModule == module || @@ -410,10 +417,7 @@ class SynthModuleDefinition { srcConnection.parentModule!.parent == module)) { final netSynthDriver = getSynthLogic(srcConnection)!; - assignments.add(SynthAssignment( - netSynthDriver, - synthReceiver, - )); + assignments.add(SynthAssignment(netSynthDriver, synthReceiver)); } } } @@ -442,10 +446,11 @@ class SynthModuleDefinition { inOuts.add(synthReceiver); } else { assert( - !inputs.contains(synthReceiver) && - !outputs.contains(synthReceiver) && - !inOuts.contains(synthReceiver), - 'Internal signals should not be ports also.'); + !inputs.contains(synthReceiver) && + !outputs.contains(synthReceiver) && + !inOuts.contains(synthReceiver), + 'Internal signals should not be ports also.', + ); internalSignals.add(synthReceiver); } @@ -456,8 +461,9 @@ class SynthModuleDefinition { if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setInOutMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setInOutMapping(receiver.name, synthReceiver); } logicsToTraverse.addAll(subModule.inOuts.values); @@ -465,14 +471,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 +511,9 @@ class SynthModuleDefinition { // array elements are not named ports, just contained in array if (synthReceiver is! SynthLogicArrayElement && !synthReceiver.isStructPortElement()) { - getSynthSubModuleInstantiation(subModule) - .setInputMapping(receiver.name, synthReceiver); + getSynthSubModuleInstantiation( + subModule, + ).setInputMapping(receiver.name, synthReceiver); } } } @@ -513,6 +522,7 @@ class SynthModuleDefinition { _collapseArrays(); _collapseAssignments(); _assignSubmodulePortMapping(); + _pruneUnused(); process(); _pickNames(); @@ -576,8 +586,9 @@ class SynthModuleDefinition { final logics = internalSignal.logics; if (internalSignal.isArray) { - if (logics.any((logicArray) => - logicArray.elements.any(logicHasPresentSynthLogic))) { + if (logics.any( + (logicArray) => logicArray.elements.any(logicHasPresentSynthLogic), + )) { // if it's an array, can only remove if all elements are removed reducedInternalSignals.add(internalSignal); } else { @@ -589,26 +600,31 @@ class SynthModuleDefinition { continue; } - final isCustomSvModPort = logics.any((logic) => - logic.isPort && - isSubmoduleAndPresent(logic.parentModule) && - ((logic.parentModule! is SystemVerilog && - !(logic.parentModule! as SystemVerilog) - .acceptsEmptyPortConnections) || - // ignore: deprecated_member_use_from_same_package - logic.parentModule! is CustomSystemVerilog)); + final isCustomSvModPort = logics.any( + (logic) => + logic.isPort && + isSubmoduleAndPresent(logic.parentModule) && + ((logic.parentModule! is SystemVerilog && + !(logic.parentModule! as SystemVerilog) + .acceptsEmptyPortConnections) || + // ignore: deprecated_member_use_from_same_package + logic.parentModule! is CustomSystemVerilog), + ); if (!isCustomSvModPort) { if (internalSignal.isNet) { final anyInternalConnections = [ ...internalSignal.srcConnections, - ...internalSignal.dstConnections + ...internalSignal.dstConnections, ] - .where((e) => - (e.parentModule == module || - ( // in case of sub-module output driving a net - e.parentModule?.parent == module && e.isOutput)) && - logicHasPresentSynthLogic(e)) + .where( + (e) => + (e.parentModule == module || + ( // in case of sub-module output driving a net + e.parentModule?.parent == module && + e.isOutput)) && + logicHasPresentSynthLogic(e), + ) .isNotEmpty; if (anyInternalConnections) { @@ -619,9 +635,11 @@ class SynthModuleDefinition { final connectedSubModules = logics .map((e) => e.parentModule) .nonNulls - .where((e) => - e != module && - getSynthSubModuleInstantiation(e).needsInstantiation) + .where( + (e) => + e != module && + getSynthSubModuleInstantiation(e).needsInstantiation, + ) .toSet(); if (connectedSubModules.length > 1) { @@ -632,13 +650,15 @@ class SynthModuleDefinition { // If the signal appears in multiple inout port mappings on the // same (single) connected submodule, it's a loopback and needs // a wire declaration so both ports can reference it by name. - final hasInOutLoopback = connectedSubModules.any((m) => - getSynthSubModuleInstantiation(m) - .inOutMapping - .values - .where((v) => v == internalSignal) - .length > - 1); + final hasInOutLoopback = connectedSubModules.any( + (m) => + getSynthSubModuleInstantiation(m) + .inOutMapping + .values + .where((v) => v == internalSignal) + .length > + 1, + ); if (hasInOutLoopback) { reducedInternalSignals.add(internalSignal); @@ -696,39 +716,44 @@ class SynthModuleDefinition { continue; } - for (final subModuleInstantiation - in subModuleInstantiations.where((e) => e.needsInstantiation)) { + for (final subModuleInstantiation in subModuleInstantiations.where( + (e) => e.needsInstantiation, + )) { final subModule = subModuleInstantiation.module; if (subModule is SystemVerilog && subModule.isWiresOnly) { final inputs = { ...subModuleInstantiation.inputMapping, - ...subModuleInstantiation.inOutMapping + ...subModuleInstantiation.inOutMapping, }; final outputs = { ...subModuleInstantiation.outputMapping, - ...subModuleInstantiation.inOutMapping + ...subModuleInstantiation.inOutMapping, }; // if all the inputs or all the outputs are not used, we can remove // the module - final allOutputsUnused = outputs.values.every((output) => - output.declarationCleared || - (output.isClearable && - !output.isStructPortElement() && - !output.hasDstConnectionsPresent())); + final allOutputsUnused = outputs.values.every( + (output) => + output.declarationCleared || + (output.isClearable && + !output.isStructPortElement() && + !output.hasDstConnectionsPresent()), + ); if (allOutputsUnused) { subModuleInstantiation.clearInstantiation(); changed = true; continue; } - final allInputsUnused = inputs.values.every((input) => - input.declarationCleared || - (input.isClearable && - !input.isStructPortElement() && - !input.hasSrcConnectionsPresent())); + final allInputsUnused = inputs.values.every( + (input) => + input.declarationCleared || + (input.isClearable && + !input.isStructPortElement() && + !input.hasSrcConnectionsPresent()), + ); if (allInputsUnused) { subModuleInstantiation.clearInstantiation(); changed = true; @@ -746,70 +771,87 @@ 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 get the unsuffixed name + // when there are collisions. This matches production ROHD priority: + // 1. Ports (reserved by _initNamespace, claimed via signalName) + // 2. Reserved submodule instances + // 3. Reserved internal signals + // 4. Non-reserved submodule instances + // 5. Non-reserved internal signals for (final input in inputs) { - input.pickName(_synthInstantiationNameUniquifier); + input.pickName(); } for (final output in outputs) { - output.pickName(_synthInstantiationNameUniquifier); + output.pickName(); } for (final inOut in inOuts) { - inOut.pickName(_synthInstantiationNameUniquifier); + inOut.pickName(); } - // pick names of *reserved* submodule instances - final nonReservedSubmodules = []; + // Reserved submodule instances first (they assert their exact name). for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { - submodule.pickName(_synthInstantiationNameUniquifier); - 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); + signal.pickName(); } else { nonReservedSignals.add(signal); } } - // then submodule instances - for (final submodule in nonReservedSubmodules - .where((element) => element.needsInstantiation)) { - submodule.pickName(_synthInstantiationNameUniquifier); + // Then non-reserved submodule instances. + for (final submodule in subModuleInstantiations) { + if (!submodule.module.reserveName && submodule.needsInstantiation) { + submodule.pickName(module); + } } - // then the rest of the internal signals + // Then the rest of the internal signals. for (final signal in nonReservedSignals) { - signal.pickName(_synthInstantiationNameUniquifier); + signal.pickName(); } } @@ -852,9 +894,10 @@ class SynthModuleDefinition { for (final MapEntry(key: (srcArray, dstArray), value: arrAssignments) in groupedAssignments.entries) { assert( - srcArray.logics.first.elements.length == - dstArray.logics.first.elements.length, - 'should be equal lengths of elements in both arrays by now'); + srcArray.logics.first.elements.length == + dstArray.logics.first.elements.length, + 'should be equal lengths of elements in both arrays by now', + ); // first requirement is that all elements have been assigned var shouldMerge = @@ -909,8 +952,10 @@ class SynthModuleDefinition { assignments.where((a) => !a.src.isConstant && !a.dst.isConstant), assignments.where((a) => a.src.isConstant || a.dst.isConstant), ])) { - assert(assignment is! PartialSynthAssignment, - 'Partial assignments should have been removed before this.'); + assert( + assignment is! PartialSynthAssignment, + 'Partial assignments should have been removed before this.', + ); final dst = assignment.dst; final src = assignment.src; @@ -922,8 +967,10 @@ class SynthModuleDefinition { continue; } - assert(dst != src, - 'No circular assignment allowed between $dst and $src.'); + assert( + dst != src, + 'No circular assignment allowed between $dst and $src.', + ); final mergeResults = SynthLogic.tryMerge(dst, src); @@ -963,14 +1010,18 @@ class SynthModuleDefinition { /// Performs updates to this definition after merging away a signal as part of /// [_collapseAssignments]. - void _applyAssignmentMergeUpdates( - {required SynthLogic mergedAway, required SynthLogic kept}) { + void _applyAssignmentMergeUpdates({ + required SynthLogic mergedAway, + required SynthLogic kept, + }) { final foundInternal = internalSignals.remove(mergedAway); if (!foundInternal) { final foundKept = internalSignals.remove(kept); - assert(foundKept, - 'One of the two should be internal since we cant merge ports.'); + assert( + foundKept, + 'One of the two should be internal since we cant merge ports.', + ); if (inputs.contains(mergedAway)) { inputs @@ -994,8 +1045,8 @@ class SynthModuleDefinition { // should all be the same synth, and arrays only merge with arrays final keptElement = getSynthLogic(keptElementLogic)!; final mergedAwayElement = getSynthLogic( - (mergedAway.logics.first as LogicArray) - .elements[keptElementIndex])!; + (mergedAway.logics.first as LogicArray).elements[keptElementIndex], + )!; if (keptElement == mergedAwayElement) { continue; @@ -1004,7 +1055,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..cc553a24c 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,14 +25,15 @@ class SynthSubModuleInstantiation { String get name => _name!; /// Selects a name for this module instance. Must be called exactly once. - void pickName(Uniquifier uniquifier) { + /// + /// Names are allocated from [parentModule]'s [Namer]'s shared namespace + /// via [Namer.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]. 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..6c7359416 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -228,12 +228,12 @@ 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 = [ Vector({'oneBit': 0}, {'result': 0}), - Vector({'oneBit': 1}, {'result': 1}), + Vector({'oneBit': 1}, {'result': 1}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -384,7 +384,7 @@ void main() { final mod = ConstBusModule(0xabcd, subset: true); await mod.build(); final vectors = [ - Vector({}, {'const_subset': 0xcd}), + Vector({}, {'const_subset': 0xcd}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -395,13 +395,13 @@ void main() { final mod = ConstBusModule(0xabcd, subset: false); await mod.build(); final vectors = [ - Vector({}, {'const_subset': 0xabcd}), + Vector({}, {'const_subset': 0xabcd}) ]; 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); }); }); @@ -413,7 +413,7 @@ void main() { Vector({'a': 0xff}, {'a_bar': 0}), Vector({'a': 0}, {'a_bar': 0xff}), Vector({'a': 0x55}, {'a_bar': 0xaa}), - Vector({'a': 1}, {'a_bar': 0xfe}), + Vector({'a': 1}, {'a_bar': 0xfe}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -428,7 +428,7 @@ void main() { Vector({'a': 0, 'b': 1}, {'a_and_b': 0}), Vector({'a': 1, 'b': 0}, {'a_and_b': 0}), Vector({'a': 1, 'b': 1}, {'a_and_b': 1}), - Vector({'a': 0xff, 'b': 0xaa}, {'a_and_b': 0xaa}), + Vector({'a': 0xff, 'b': 0xaa}, {'a_and_b': 0xaa}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); @@ -445,7 +445,7 @@ void main() { Vector({'a': bin('11101111')}, {'a_operator_indexing3': 0}), Vector({'a': bin('11111110')}, {'a_operator_neg_indexing1': 0}), Vector({'a': bin('10000000')}, {'a_operator_neg_indexing2': 1}), - Vector({'a': bin('10111111')}, {'a_operator_neg_indexing3': 0}), + Vector({'a': bin('10111111')}, {'a_operator_neg_indexing3': 0}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); @@ -532,7 +532,7 @@ void main() { final vectors = [ Vector({'a': 0}, {'a_reversed': 0}), Vector({'a': 0xff}, {'a_reversed': 0xff}), - Vector({'a': 0xf5}, {'a_reversed': 0xaf}), + Vector({'a': 0xf5}, {'a_reversed': 0xaf}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -577,7 +577,7 @@ void main() { // Test set 4 Vector({'a': 0}, {'a_neg_range4': 0}), Vector({'a': 0xaf}, {'a_neg_range4': 5}), - Vector({'a': bin('11000101')}, {'a_neg_range4': bin('110')}), + Vector({'a': bin('11000101')}, {'a_neg_range4': bin('110')}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -592,7 +592,7 @@ void main() { Vector({'a': 0xff, 'b': 0xff}, {'a_b_joined': 0xffff}), Vector({'a': 0xff, 'b': 0}, {'a_b_joined': 0xff}), Vector({'a': 0, 'b': 0xff}, {'a_b_joined': 0xff00}), - Vector({'a': 0xaa, 'b': 0x55}, {'a_b_joined': 0x55aa}), + Vector({'a': 0xaa, 'b': 0x55}, {'a_b_joined': 0x55aa}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -605,7 +605,7 @@ void main() { final vectors = [ Vector({'a': 0}, {'a1': 0}), Vector({'a': 0xff}, {'a1': 1}), - Vector({'a': 0xf5}, {'a1': 0}), + Vector({'a': 0xf5}, {'a1': 0}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -620,7 +620,7 @@ void main() { Vector({'a': 0, 'b': 1}, {'a_plus_b': 1}), Vector({'a': 1, 'b': 0}, {'a_plus_b': 1}), Vector({'a': 1, 'b': 1}, {'a_plus_b': 2}), - Vector({'a': 6, 'b': 7}, {'a_plus_b': 13}), + Vector({'a': 6, 'b': 7}, {'a_plus_b': 13}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -631,7 +631,7 @@ void main() { final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); await gtm.build(); final vectors = [ - Vector({'a': 1, 'b': 1}, {'expression_bit_select': 2}), + Vector({'a': 1, 'b': 1}, {'expression_bit_select': 2}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -661,7 +661,7 @@ void main() { await gtm.build(); final vectors = [ Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 4, 'defaultValue': 5}, - {'selectFromValue': 5, 'selectIndexValue': 5}), + {'selectFromValue': 5, 'selectIndexValue': 5}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..8cb8497d0 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -48,7 +48,7 @@ void main() { await mod.build(); final vectors = [ Vector({'a': 1, 'b': 1}, {'c': 1, 'd': 1, 'e': 1, 'f': 1}), - Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 0, 'e': 0, 'f': 0}), + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 0, 'e': 0, 'f': 0}) ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); @@ -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..848167f37 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -28,21 +28,13 @@ class TestModule extends Module { final clk = testingAsyncReset ? Const(0) : SimpleClockGenerator(10).clk; reset = addInput('reset', reset); final states = [ - State(MyStates.state1, events: { - a: MyStates.state2, - ~a: MyStates.state3 - }, actions: [ - b < c, - ]), + State(MyStates.state1, + events: {a: MyStates.state2, ~a: MyStates.state3}, actions: [b < c]), State(MyStates.state2, conditionalType: ConditionalType.priority, events: {}, - actions: [ - b < 1, - ]), - State(MyStates.state3, events: {}, actions: [ - b < ~c, - ]), + actions: [b < 1]), + State(MyStates.state3, events: {}, actions: [b < ~c]) ]; final fsm = FiniteStateMachine( @@ -63,16 +55,14 @@ class DefaultStateFsmMod extends Module { final b = addOutput('b', width: 8); _fsm = FiniteStateMachine(clk, reset, MyStates.state1, [ - State( - MyStates.state1, - events: {Const(0): MyStates.state2}, - actions: [b < 1], - defaultNextState: MyStates.state3, - ), + State(MyStates.state1, + events: {Const(0): MyStates.state2}, + actions: [b < 1], + defaultNextState: MyStates.state3), State(MyStates.state2, events: {}, actions: [b < 2]), State(MyStates.state3, events: {}, actions: [b < 3], defaultNextState: MyStates.state4), - State(MyStates.state4, events: {}, actions: [b < 4]), + State(MyStates.state4, events: {}, actions: [b < 4]) ]); } } @@ -119,48 +109,32 @@ class TrafficTestModule extends Module { final states = >[ State(LightStates.northFlowing, events: { - TrafficPresence.isEastActive(traffic): LightStates.northSlowing, + TrafficPresence.isEastActive(traffic): LightStates.northSlowing }, actions: [ - northLight < LightColor.green.value, + northLight < LightColor.green.value ]), - State( - LightStates.northSlowing, - events: {}, - defaultNextState: LightStates.eastFlowing, - actions: [ - northLight < LightColor.yellow.value, - ], - ), - State( - LightStates.eastFlowing, - events: { - TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing, - }, - actions: [ - eastLight < LightColor.green.value, - ], - ), - State( - LightStates.eastSlowing, - events: {}, - defaultNextState: LightStates.northFlowing, - actions: [ - eastLight < LightColor.yellow.value, - ], - ), + State(LightStates.northSlowing, + events: {}, + defaultNextState: LightStates.eastFlowing, + actions: [northLight < LightColor.yellow.value]), + State(LightStates.eastFlowing, events: { + TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing + }, actions: [ + eastLight < LightColor.green.value + ]), + State(LightStates.eastSlowing, + events: {}, + defaultNextState: LightStates.northFlowing, + actions: [eastLight < LightColor.yellow.value]) ]; final fsm = FiniteStateMachine( - clk, - reset, - LightStates.northFlowing, - states, - setupActions: [ - // by default, lights should be red - northLight < LightColor.red.value, - eastLight < LightColor.red.value, - ], - ); + clk, reset, LightStates.northFlowing, states, + setupActions: [ + // by default, lights should be red + northLight < LightColor.red.value, + eastLight < LightColor.red.value + ]); if (!kIsWeb) { fsm.generateDiagram(outputPath: _trafficFSMPath); @@ -183,7 +157,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 +166,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 +175,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')); }); @@ -230,17 +204,15 @@ void main() { FiniteStateMachine(Logic(), Logic(), MyStates.state1, [ State(MyStates.state1, events: {}, actions: []), State(MyStates.state2, events: {}, actions: []), - State(MyStates.state2, events: {}, actions: []), + State(MyStates.state2, events: {}, actions: []) ]), throwsA(isA())); }); test('missing reset state throws exception', () { expect( - () => - FiniteStateMachine(Logic(), Logic(), MyStates.state1, [ - State(MyStates.state2, events: {}, actions: []), - ]), + () => FiniteStateMachine(Logic(), Logic(), MyStates.state1, + [State(MyStates.state2, events: {}, actions: [])]), throwsA(isA())); }); }); @@ -251,7 +223,7 @@ void main() { State(MyStates.state4, events: {}, actions: []), State(MyStates.state1, events: {}, actions: []), State(MyStates.state3, events: {}, actions: []), - State(MyStates.state2, events: {}, actions: []), + State(MyStates.state2, events: {}, actions: []) ]).getStateIndex(MyStates.state2), 3); }); @@ -266,7 +238,7 @@ void main() { Vector({'reset': 1, 'a': 0, 'c': 0}, {}), Vector({'reset': 0}, {'b': 0}), Vector({}, {'b': 1}), - Vector({'c': 1}, {'b': 0}), + Vector({'c': 1}, {'b': 0}) ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); @@ -282,7 +254,7 @@ void main() { final vectors = [ Vector({'reset': 0, 'a': 0, 'c': 0}, {}), - Vector({'reset': 1}, {'b': 0}), + Vector({'reset': 1}, {'b': 0}) ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); @@ -300,7 +272,7 @@ void main() { Vector({'reset': 0}, {'b': 1}), Vector({'reset': 0}, {'b': 3}), Vector({'reset': 0}, {'b': 4}), - Vector({'reset': 0}, {'b': 4}), + Vector({'reset': 0}, {'b': 4}) ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..8b9afda1e 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -220,11 +220,7 @@ void main() { final d0 = Logic(width: 8)..put(LogicValue.ofInt(2, 8)); final d1 = Logic(width: 8)..put(LogicValue.ofInt(3, 8)); final result = cases( - control, - { - d0: LogicValue.ofInt(2, 8), - d1: LogicValue.ofInt(3, 8), - }, + control, {d0: LogicValue.ofInt(2, 8), d1: LogicValue.ofInt(3, 8)}, width: 8); control.put(2); @@ -241,14 +237,7 @@ void main() { const d0 = 2; const d1 = 3; - final result = cases( - control, - { - d0: 2, - d1: 3, - }, - width: 4, - defaultValue: 3); + final result = cases(control, {d0: 2, d1: 3}, width: 4, defaultValue: 3); expect(result.value, LogicValue.ofInt(2, 4)); }); @@ -257,10 +246,7 @@ void main() { final control = Logic(); final d0 = Logic()..put(LogicValue.zero); final d1 = Logic()..put(LogicValue.one); - final result = cases(control, { - d0: LogicValue.zero, - d1: LogicValue.one, - }); + final result = cases(control, {d0: LogicValue.zero, d1: LogicValue.one}); control.put(0); expect(result.value, LogicValue.zero); @@ -273,14 +259,7 @@ void main() { final control = Logic(width: 4); const d0 = 1; const d1 = 2; - final result = cases( - control, - { - d0: 1, - d1: 2, - }, - width: 4, - defaultValue: 3); + final result = cases(control, {d0: 1, d1: 2}, width: 4, defaultValue: 3); control.put(LogicValue.zero); expect(result.value, LogicValue.ofInt(3, 4)); @@ -358,7 +337,7 @@ void main() { await gtm.build(); final vectors = [ Vector({'a': 1}, {'a_bar': 0}), - Vector({'a': 0}, {'a_bar': 1}), + Vector({'a': 0}, {'a_bar': 1}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -371,7 +350,7 @@ void main() { Vector({'a': bin('0000')}, {'a_and': 0}), Vector({'a': bin('1010')}, {'a_and': 0}), Vector({'a': bin('1111')}, {'a_and': 1}), - Vector({'a': bin('0001')}, {'a_and': 0}), + Vector({'a': bin('0001')}, {'a_and': 0}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -385,7 +364,7 @@ void main() { Vector({'a': bin('0000')}, {'a_or': 0}), Vector({'a': bin('1010')}, {'a_or': 1}), Vector({'a': bin('1111')}, {'a_or': 1}), - Vector({'a': bin('0001')}, {'a_or': 1}), + Vector({'a': bin('0001')}, {'a_or': 1}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -399,7 +378,7 @@ void main() { Vector({'a': bin('0000')}, {'a_xor': 0}), Vector({'a': bin('1010')}, {'a_xor': 0}), Vector({'a': bin('1111')}, {'a_xor': 0}), - Vector({'a': bin('0001')}, {'a_xor': 1}), + Vector({'a': bin('0001')}, {'a_xor': 1}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -412,7 +391,7 @@ void main() { final vectors = [ Vector({'control': 1, 'd0': 0, 'd1': 1}, {'y': 1}), Vector({'control': 0, 'd0': 0, 'd1': 1}, {'y': 0}), - Vector({'control': 0, 'd0': 1, 'd1': 1}, {'y': 1}), + Vector({'control': 0, 'd0': 1, 'd1': 1}, {'y': 1}) ]; await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); @@ -426,7 +405,7 @@ void main() { Vector({'a': bin('1111')}, {'y': bin('0001')}), Vector({'a': bin('0110')}, {'y': bin('0110')}), Vector({'a': bin('0010')}, {'y': bin('0010')}), - Vector({'a': bin('1011')}, {'y': bin('0101')}), + Vector({'a': bin('1011')}, {'y': bin('0101')}) ]; await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); @@ -441,7 +420,7 @@ void main() { Vector({'control': 0, 'd0': 18, 'd1': 7}, {'y': 18}), Vector({'control': 0, 'd0': 3, 'd1': 6}, {'y': 3}), Vector({'control': 0, 'd0': 10, 'd1': LogicValue.z}, {'y': 10}), - Vector({'control': 1, 'd0': LogicValue.z, 'd1': 6}, {'y': 6}), + Vector({'control': 1, 'd0': LogicValue.z, 'd1': 6}, {'y': 6}) ]; final vector2 = [ @@ -450,7 +429,7 @@ void main() { Vector( {'control': LogicValue.z, 'd0': 10, 'd1': 6}, {'y': LogicValue.x}), Vector( - {'control': 0, 'd0': LogicValue.z, 'd1': 10}, {'y': LogicValue.x}), + {'control': 0, 'd0': LogicValue.z, 'd1': 10}, {'y': LogicValue.x}) ]; await SimCompare.checkFunctionalVector(mod, vector1 + vector2); @@ -466,7 +445,7 @@ void main() { Vector({'a': bin('010'), 'b': 0}, {'a_lshift_b': bin('010')}), Vector({'a': bin('010'), 'b': 1}, {'a_lshift_b': bin('100')}), Vector({'a': bin('010'), 'b': 2}, {'a_lshift_b': bin('000')}), - Vector({'a': bin('010'), 'b': 6}, {'a_lshift_b': bin('000')}), + Vector({'a': bin('010'), 'b': 6}, {'a_lshift_b': bin('000')}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -479,7 +458,7 @@ void main() { Vector({'a': bin('010'), 'b': 0}, {'a_rshift_b': bin('010')}), Vector({'a': bin('010'), 'b': 1}, {'a_rshift_b': bin('001')}), Vector({'a': bin('010'), 'b': 2}, {'a_rshift_b': bin('000')}), - Vector({'a': bin('010'), 'b': 6}, {'a_rshift_b': bin('000')}), + Vector({'a': bin('010'), 'b': 6}, {'a_rshift_b': bin('000')}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -494,7 +473,7 @@ void main() { Vector({'a': bin('010'), 'b': 2}, {'a_arshift_b': bin('000')}), Vector({'a': bin('010'), 'b': 6}, {'a_arshift_b': bin('000')}), Vector({'a': bin('110'), 'b': 0}, {'a_arshift_b': bin('110')}), - Vector({'a': bin('110'), 'b': 6}, {'a_arshift_b': bin('111')}), + Vector({'a': bin('110'), 'b': 6}, {'a_arshift_b': bin('111')}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -505,7 +484,7 @@ void main() { ShiftTestModule(Logic(width: 3), Logic(width: 8), constant: 1); await gtm.build(); final vectors = [ - Vector({'a': bin('010')}, {'a_lshift_const': bin('100')}), + Vector({'a': bin('010')}, {'a_lshift_const': bin('100')}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -516,7 +495,7 @@ void main() { ShiftTestModule(Logic(width: 3), Logic(width: 8), constant: 1); await gtm.build(); final vectors = [ - Vector({'a': bin('010')}, {'a_rshift_const': bin('001')}), + Vector({'a': bin('010')}, {'a_rshift_const': bin('001')}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -527,7 +506,7 @@ void main() { ShiftTestModule(Logic(width: 3), Logic(width: 8), constant: 1); await gtm.build(); final vectors = [ - Vector({'a': bin('010')}, {'a_arshift_const': bin('001')}), + Vector({'a': bin('010')}, {'a_arshift_const': bin('001')}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -537,7 +516,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"))); @@ -548,7 +527,7 @@ void main() { 'a_arshift_const': bin('010'), 'a_lshift_const': bin('010'), 'a_rshift_const': bin('010') - }), + }) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -568,8 +547,8 @@ void main() { 'a_arshift_const': BigInt.one << 30, 'a_lshift_b': BigInt.one << 170, 'a_rshift_b': BigInt.one << 30, - 'a_arshift_b': BigInt.one << 30, - }), + 'a_arshift_b': BigInt.one << 30 + }) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -589,8 +568,8 @@ void main() { 'a_arshift_const': BigInt.one << 30, 'a_lshift_b': BigInt.one << 170, 'a_rshift_b': BigInt.one << 30, - 'a_arshift_b': BigInt.one << 30, - }), + 'a_arshift_b': BigInt.one << 30 + }) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -610,8 +589,8 @@ void main() { 'a_arshift_const': BigInt.one << 10, 'a_lshift_b': BigInt.one << 30, 'a_rshift_b': BigInt.one << 10, - 'a_arshift_b': BigInt.one << 10, - }), + 'a_arshift_b': BigInt.one << 10 + }) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -631,8 +610,8 @@ void main() { 'a_arshift_const': LogicValue.filled(200, LogicValue.one), 'a_lshift_b': 0, 'a_rshift_b': 0, - 'a_arshift_b': LogicValue.filled(200, LogicValue.one), - }), + 'a_arshift_b': LogicValue.filled(200, LogicValue.one) + }) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -652,8 +631,8 @@ void main() { 'a_arshift_const': LogicValue.filled(40, LogicValue.zero), 'a_lshift_b': 0, 'a_rshift_b': 0, - 'a_arshift_b': LogicValue.filled(40, LogicValue.zero), - }), + 'a_arshift_b': LogicValue.filled(40, LogicValue.zero) + }) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -673,8 +652,8 @@ void main() { 'a_arshift_const': LogicValue.filled(20, LogicValue.zero), 'a_lshift_b': 0, 'a_rshift_b': 0, - 'a_arshift_b': LogicValue.filled(20, LogicValue.zero), - }), + 'a_arshift_b': LogicValue.filled(20, LogicValue.zero) + }) ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); @@ -688,7 +667,7 @@ void main() { Vector({'a': 0, 'b': 0}, {'a_and_b': 0}), Vector({'a': 0, 'b': 1}, {'a_and_b': 0}), Vector({'a': 1, 'b': 0}, {'a_and_b': 0}), - Vector({'a': 1, 'b': 1}, {'a_and_b': 1}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -702,7 +681,7 @@ void main() { Vector({'a': 0, 'b': 0}, {'a_or_b': 0}), Vector({'a': 0, 'b': 1}, {'a_or_b': 1}), Vector({'a': 1, 'b': 0}, {'a_or_b': 1}), - Vector({'a': 1, 'b': 1}, {'a_or_b': 1}), + Vector({'a': 1, 'b': 1}, {'a_or_b': 1}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); @@ -716,7 +695,7 @@ void main() { Vector({'a': 0, 'b': 0}, {'a_xor_b': 0}), Vector({'a': 0, 'b': 1}, {'a_xor_b': 1}), Vector({'a': 1, 'b': 0}, {'a_xor_b': 1}), - Vector({'a': 1, 'b': 1}, {'a_xor_b': 0}), + Vector({'a': 1, 'b': 1}, {'a_xor_b': 0}) ]; await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); diff --git a/test/incremental_expansion_test.dart b/test/incremental_expansion_test.dart new file mode 100644 index 000000000..8d779dcd7 --- /dev/null +++ b/test/incremental_expansion_test.dart @@ -0,0 +1,115 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// incremental_expansion_test.dart +// Tests for the incremental expansion protocol: +// - original_signal_count / original_cell_count attributes in slim JSON +// - HierarchyNode.extendSignals / extendChildren + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart' as ex; +import '../example/filter_bank.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + // ─────── original_signal_count / original_cell_count ──────────────── + + group('original_signal_count / original_cell_count', () { + test('Counter slim JSON has counts in attributes', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = ex.Counter(en, reset, clk); + await counter.build(); + final netSvc = NetlistService(counter); + + final slimStr = netSvc.slimJson; + final unified = jsonDecode(slimStr) as Map; + final netlist = unified['netlist'] as Map; + final modules = netlist['modules'] as Map; + + for (final entry in modules.entries) { + final mod = entry.value as Map; + final attrs = mod['attributes'] as Map; + expect( + attrs.containsKey('original_signal_count'), + isTrue, + reason: '${entry.key} missing original_signal_count', + ); + expect( + attrs.containsKey('original_cell_count'), + isTrue, + reason: '${entry.key} missing original_cell_count', + ); + + final sigCount = attrs['original_signal_count'] as int; + final cellCount = attrs['original_cell_count'] as int; + final netnames = mod['netnames'] as Map? ?? {}; + final cells = mod['cells'] as Map? ?? {}; + + // Counts must match the actual number of entries in slim JSON. + expect( + sigCount, + equals(netnames.length), + reason: '${entry.key}: original_signal_count mismatch', + ); + expect( + cellCount, + equals(cells.length), + reason: '${entry.key}: original_cell_count mismatch', + ); + } + }); + + test('FilterBank slim JSON has counts in attributes', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + final filterBank = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: 4, + dataWidth: 16, + coefficients: [List.filled(4, 1), List.filled(4, 1)], + ); + await filterBank.build(); + final netSvc = NetlistService(filterBank); + + final slimStr = netSvc.slimJson; + final unified = jsonDecode(slimStr) as Map; + final netlist = unified['netlist'] as Map; + final modules = netlist['modules'] as Map; + + // At least the root module should have counts. + expect(modules.isNotEmpty, isTrue); + for (final entry in modules.entries) { + final mod = entry.value as Map; + final attrs = mod['attributes'] as Map; + expect( + attrs['original_signal_count'], + isA(), + reason: '${entry.key}: original_signal_count not int', + ); + expect( + attrs['original_cell_count'], + isA(), + reason: '${entry.key}: original_cell_count not int', + ); + } + }); + }); +} 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/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..9a9f5580d 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -18,28 +18,22 @@ import 'package:test/test.dart'; class SimpleLAPassthrough extends Module { LogicArray get laOut => output('laOut') as LogicArray; - SimpleLAPassthrough( - LogicArray laIn, { - List? dimOverride, - int? elemWidthOverride, - int? numUnpackedOverride, - super.name = 'simple_la_passthrough', - }) { - laIn = addInputArray( - 'laIn', - laIn, - dimensions: dimOverride ?? laIn.dimensions, - elementWidth: elemWidthOverride ?? laIn.elementWidth, - numUnpackedDimensions: numUnpackedOverride ?? laIn.numUnpackedDimensions, - ); + SimpleLAPassthrough(LogicArray laIn, + {List? dimOverride, + int? elemWidthOverride, + int? numUnpackedOverride, + super.name = 'simple_la_passthrough'}) { + laIn = addInputArray('laIn', laIn, + dimensions: dimOverride ?? laIn.dimensions, + elementWidth: elemWidthOverride ?? laIn.elementWidth, + numUnpackedDimensions: + numUnpackedOverride ?? laIn.numUnpackedDimensions); - addOutputArray( - 'laOut', - dimensions: dimOverride ?? laIn.dimensions, - elementWidth: elemWidthOverride ?? laIn.elementWidth, - numUnpackedDimensions: - numUnpackedOverride ?? laIn.numUnpackedDimensions, - ) <= + addOutputArray('laOut', + dimensions: dimOverride ?? laIn.dimensions, + elementWidth: elemWidthOverride ?? laIn.elementWidth, + numUnpackedDimensions: + numUnpackedOverride ?? laIn.numUnpackedDimensions) <= laIn; } } @@ -49,36 +43,27 @@ class RangeAndSliceArrModule extends Module implements SimpleLAPassthrough { LogicArray get laOut => output('laOut') as LogicArray; RangeAndSliceArrModule(LogicArray laIn) { - laIn = addInputArray( - 'laIn', - laIn, - dimensions: [3, 3, 3], - elementWidth: 8, - ); + laIn = addInputArray('laIn', laIn, dimensions: [3, 3, 3], elementWidth: 8); - addOutputArray( - 'laOut', - dimensions: laIn.dimensions, - elementWidth: laIn.elementWidth, - numUnpackedDimensions: laIn.numUnpackedDimensions, - ); + addOutputArray('laOut', + dimensions: laIn.dimensions, + elementWidth: laIn.elementWidth, + numUnpackedDimensions: laIn.numUnpackedDimensions); laOut.elements[0] <= - [ - laIn.elements[0].getRange(16), - laIn.elements[0].getRange(0, 16), - ].swizzle(); + [laIn.elements[0].getRange(16), laIn.elements[0].getRange(0, 16)] + .swizzle(); laOut.elements[1] <= [ laIn.elements[1].slice(16, 3 * 3 * 8 - 1).reversed, - laIn.elements[1].slice(15, 0), + laIn.elements[1].slice(15, 0) ].swizzle(); laOut.elements[2] <= [ laIn.elements[2].slice(-1, 0).getRange(3 * 3 * 8 ~/ 2), - laIn.elements[2].getRange(-3 * 3 * 8).getRange(0, 3 * 3 * 8 ~/ 2), + laIn.elements[2].getRange(-3 * 3 * 8).getRange(0, 3 * 3 * 8 ~/ 2) ].swizzle(); } } @@ -88,19 +73,12 @@ class WithSetArrayModule extends Module implements SimpleLAPassthrough { LogicArray get laOut => output('laOut') as LogicArray; WithSetArrayModule(LogicArray laIn) { - laIn = addInputArray( - 'laIn', - laIn, - dimensions: [2, 2], - elementWidth: 8, - ); + laIn = addInputArray('laIn', laIn, dimensions: [2, 2], elementWidth: 8); - addOutputArray( - 'laOut', - dimensions: laIn.dimensions, - elementWidth: laIn.elementWidth, - numUnpackedDimensions: laIn.numUnpackedDimensions, - ); + addOutputArray('laOut', + dimensions: laIn.dimensions, + elementWidth: laIn.elementWidth, + numUnpackedDimensions: laIn.numUnpackedDimensions); laOut <= laIn.withSet(8, laIn.elements[0].elements[1]); } @@ -111,19 +89,12 @@ class WithSetArrayOffsetModule extends Module implements SimpleLAPassthrough { LogicArray get laOut => output('laOut') as LogicArray; WithSetArrayOffsetModule(LogicArray laIn) { - laIn = addInputArray( - 'laIn', - laIn, - dimensions: [2, 2], - elementWidth: 8, - ); + laIn = addInputArray('laIn', laIn, dimensions: [2, 2], elementWidth: 8); - addOutputArray( - 'laOut', - dimensions: laIn.dimensions, - elementWidth: laIn.elementWidth, - numUnpackedDimensions: laIn.numUnpackedDimensions, - ); + addOutputArray('laOut', + dimensions: laIn.dimensions, + elementWidth: laIn.elementWidth, + numUnpackedDimensions: laIn.numUnpackedDimensions); laOut <= laIn.withSet(3 + 16, laIn.elements[1].getRange(3, 3 + 9)); } @@ -139,11 +110,10 @@ class LAPassthroughIntf extends Interface { Logic get laIn => port('laIn'); Logic get laOut => port('laOut'); - LAPassthroughIntf({ - required this.dimensions, - required this.elementWidth, - required this.numUnpackedDimensions, - }) { + LAPassthroughIntf( + {required this.dimensions, + required this.elementWidth, + required this.numUnpackedDimensions}) { setPorts([ LogicArray.port('laIn', dimensions, elementWidth, numUnpackedDimensions) ], [ @@ -159,25 +129,21 @@ class LAPassthroughIntf extends Interface { LAPassthroughIntf.clone(LAPassthroughIntf other) : this( - dimensions: other.dimensions, - elementWidth: other.elementWidth, - numUnpackedDimensions: other.numUnpackedDimensions, - ); + dimensions: other.dimensions, + elementWidth: other.elementWidth, + numUnpackedDimensions: other.numUnpackedDimensions); @override LAPassthroughIntf clone() => LAPassthroughIntf( - dimensions: dimensions, - elementWidth: elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - ); + dimensions: dimensions, + elementWidth: elementWidth, + numUnpackedDimensions: numUnpackedDimensions); } class LAPassthroughWithIntf extends Module implements SimpleLAPassthrough { @override LogicArray get laOut => output('laOut') as LogicArray; - LAPassthroughWithIntf( - LAPassthroughIntf intf, - ) { + LAPassthroughWithIntf(LAPassthroughIntf intf) { intf = LAPassthroughIntf.clone(intf) ..connectIO(this, intf, inputTags: {LADir.laIn}, outputTags: {LADir.laOut}); @@ -189,26 +155,19 @@ class LAPassthroughWithIntf extends Module implements SimpleLAPassthrough { class SimpleLAPassthroughLogic extends Module implements SimpleLAPassthrough { @override LogicArray get laOut => output('laOut') as LogicArray; - SimpleLAPassthroughLogic( - Logic laIn, { - required List dimensions, - required int elementWidth, - required int numUnpackedDimensions, - }) { - laIn = addInputArray( - 'laIn', - laIn, - dimensions: dimensions, - elementWidth: elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - ); + SimpleLAPassthroughLogic(Logic laIn, + {required List dimensions, + required int elementWidth, + required int numUnpackedDimensions}) { + laIn = addInputArray('laIn', laIn, + dimensions: dimensions, + elementWidth: elementWidth, + numUnpackedDimensions: numUnpackedDimensions); - addOutputArray( - 'laOut', - dimensions: dimensions, - elementWidth: elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - ) <= + addOutputArray('laOut', + dimensions: dimensions, + elementWidth: elementWidth, + numUnpackedDimensions: numUnpackedDimensions) <= laIn; } } @@ -412,26 +371,21 @@ class ConstantAssignmentArrayModule extends Module { class CondAssignArray extends Module implements SimpleLAPassthrough { @override LogicArray get laOut => output('laOut') as LogicArray; - CondAssignArray( - LogicArray laIn, { - List? dimOverride, - int? elemWidthOverride, - int? numUnpackedOverride, - }) { - laIn = addInputArray( - 'laIn', - laIn, - dimensions: dimOverride ?? laIn.dimensions, - elementWidth: elemWidthOverride ?? laIn.elementWidth, - numUnpackedDimensions: numUnpackedOverride ?? laIn.numUnpackedDimensions, - ); + CondAssignArray(LogicArray laIn, + {List? dimOverride, + int? elemWidthOverride, + int? numUnpackedOverride}) { + laIn = addInputArray('laIn', laIn, + dimensions: dimOverride ?? laIn.dimensions, + elementWidth: elemWidthOverride ?? laIn.elementWidth, + numUnpackedDimensions: + numUnpackedOverride ?? laIn.numUnpackedDimensions); - final laOut = addOutputArray( - 'laOut', - dimensions: dimOverride ?? laIn.dimensions, - elementWidth: elemWidthOverride ?? laIn.elementWidth, - numUnpackedDimensions: numUnpackedOverride ?? laIn.numUnpackedDimensions, - ); + final laOut = addOutputArray('laOut', + dimensions: dimOverride ?? laIn.dimensions, + elementWidth: elemWidthOverride ?? laIn.elementWidth, + numUnpackedDimensions: + numUnpackedOverride ?? laIn.numUnpackedDimensions); Combinational([laOut < laIn]); } @@ -440,39 +394,33 @@ class CondAssignArray extends Module implements SimpleLAPassthrough { class CondCompArray extends Module implements SimpleLAPassthrough { @override LogicArray get laOut => output('laOut') as LogicArray; - CondCompArray( - LogicArray laIn, { - List? dimOverride, - int? elemWidthOverride, - int? numUnpackedOverride, - }) : assert(laIn.dimensions.length == 1, 'test assumes 1x1 array'), + CondCompArray(LogicArray laIn, + {List? dimOverride, + int? elemWidthOverride, + int? numUnpackedOverride}) + : assert(laIn.dimensions.length == 1, 'test assumes 1x1 array'), assert(laIn.width == 1, 'test assumes 1x1 array') { - laIn = addInputArray( - 'laIn', - laIn, - dimensions: dimOverride ?? laIn.dimensions, - elementWidth: elemWidthOverride ?? laIn.elementWidth, - numUnpackedDimensions: numUnpackedOverride ?? laIn.numUnpackedDimensions, - ); + laIn = addInputArray('laIn', laIn, + dimensions: dimOverride ?? laIn.dimensions, + elementWidth: elemWidthOverride ?? laIn.elementWidth, + numUnpackedDimensions: + numUnpackedOverride ?? laIn.numUnpackedDimensions); - final laOut = addOutputArray( - 'laOut', - dimensions: dimOverride ?? laIn.dimensions, - elementWidth: elemWidthOverride ?? laIn.elementWidth, - numUnpackedDimensions: numUnpackedOverride ?? laIn.numUnpackedDimensions, - ); + final laOut = addOutputArray('laOut', + dimensions: dimOverride ?? laIn.dimensions, + elementWidth: elemWidthOverride ?? laIn.elementWidth, + numUnpackedDimensions: + numUnpackedOverride ?? laIn.numUnpackedDimensions); Combinational([ - If( - laIn, - then: [laOut < laIn], - orElse: [ - Case(laIn, [ - CaseItem(Const(0), [laOut < laIn]), - CaseItem(Const(1), [laOut < ~laIn]), - ]) - ], - ), + If(laIn, then: [ + laOut < laIn + ], orElse: [ + Case(laIn, [ + CaseItem(Const(0), [laOut < laIn]), + CaseItem(Const(1), [laOut < ~laIn]) + ]) + ]) ]); } } @@ -509,9 +457,7 @@ class AssignSubsetModule extends Module { } class ParentModuleWithPackingArrayOutput extends Module { - ParentModuleWithPackingArrayOutput( - Logic x, - ) { + ParentModuleWithPackingArrayOutput(Logic x) { x = addInput('x', x); ChildModuleWithPackingArrayOutput(x).myOut.packed.xor() ^ x; } @@ -721,8 +667,8 @@ void main() { }, { 'outArr': '000000000011110011000011', 'p1out': '110011000011', - 'partialOut': '0011', - }), + 'partialOut': '0011' + }) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -751,7 +697,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 +746,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 +764,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\]')), @@ -838,30 +784,24 @@ void main() { test('3 dimensions with interface', () async { final mod = LAPassthroughWithIntf(LAPassthroughIntf( - dimensions: [3, 2, 3], - elementWidth: 8, - numUnpackedDimensions: 0, - )); + dimensions: [3, 2, 3], elementWidth: 8, numUnpackedDimensions: 0)); 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')); }); test('3 dimensions with interface and unpacked', () async { final mod = LAPassthroughWithIntf(LAPassthroughIntf( - dimensions: [3, 2, 3], - elementWidth: 8, - numUnpackedDimensions: 1, - )); + dimensions: [3, 2, 3], elementWidth: 8, numUnpackedDimensions: 1)); 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 +868,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); }); }); @@ -937,12 +877,8 @@ void main() { test('array param mismatch', () async { final i = LogicArray([3, 2], 8, numUnpackedDimensions: 1); final o = LogicArray([3, 2], 8, numUnpackedDimensions: 1); - final mod = SimpleLAPassthrough( - i, - dimOverride: [1, 3], - elemWidthOverride: 16, - numUnpackedOverride: 0, - ); + final mod = SimpleLAPassthrough(i, + dimOverride: [1, 3], elemWidthOverride: 16, numUnpackedOverride: 0); o <= mod.laOut; await testArrayPassthrough(mod); }); @@ -950,12 +886,8 @@ void main() { test('logic into array', () async { final i = Logic(width: 3 * 2 * 8); final o = Logic(width: 3 * 2 * 8); - final mod = SimpleLAPassthroughLogic( - i, - dimensions: [1, 3], - elementWidth: 16, - numUnpackedDimensions: 0, - ); + final mod = SimpleLAPassthroughLogic(i, + dimensions: [1, 3], elementWidth: 16, numUnpackedDimensions: 0); o <= mod.laOut; await testArrayPassthrough(mod); }); @@ -981,7 +913,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 +924,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 +933,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 +975,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( @@ -1143,7 +1076,7 @@ void main() { }, { 'outputLogicArray': LogicValue.ofString(('z' * 3 * 2) + ('101' * 5) + ('z' * 3 * 3)) - }), + }) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -1174,13 +1107,8 @@ void main() { group('array clone', () { for (final isNet in [true, false]) { test('isNet = $isNet', () { - final la = (isNet ? LogicArray.net : LogicArray.new)( - [3, 2, 4], - 8, - numUnpackedDimensions: 1, - name: 'myarray', - naming: Naming.reserved, - ); + final la = (isNet ? LogicArray.net : LogicArray.new)([3, 2, 4], 8, + numUnpackedDimensions: 1, name: 'myarray', naming: Naming.reserved); final clone = la.clone(); expect(la.dimensions, clone.dimensions); expect(la.elementWidth, clone.elementWidth); 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..8a8e46d51 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -18,11 +18,8 @@ class MyStruct extends LogicStructure { final Logic ready; final Logic valid; - factory MyStruct({String name = 'myStruct'}) => MyStruct._( - Logic(name: 'ready'), - Logic(name: 'valid'), - name: name, - ); + factory MyStruct({String name = 'myStruct'}) => + MyStruct._(Logic(name: 'ready'), Logic(name: 'valid'), name: name); MyStruct._(this.ready, this.valid, {required String name}) : super([ready, valid], name: name); @@ -156,7 +153,7 @@ class VariousNamingStruct extends LogicStructure { Logic(name: 'reserved_$name', naming: Naming.reserved), Logic(name: 'mergeable', naming: Naming.mergeable), Logic(name: 'unnamed', naming: Naming.unnamed), - MyStruct(name: 'my_sub_struct'), + MyStruct(name: 'my_sub_struct') ]); @override @@ -225,13 +222,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 +236,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 +245,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 +254,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 +263,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')); @@ -275,11 +272,10 @@ void main() { test('array port and simple port with _num name conflict', () async { final mod = NameCollisionArrayTop( - // mark as renameable so that it doesnt get pruned away (no name needed) - portANaming: Naming.renameable, - ); + // mark as renameable so that it doesnt get pruned away (no name needed) + portANaming: Naming.renameable); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -295,7 +291,7 @@ void main() { () async { final mod = NameCollisionArrayTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -312,7 +308,7 @@ void main() { await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('_wow_______')); }); @@ -321,7 +317,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;')); @@ -357,11 +353,10 @@ void main() { for (final newNaming in [...Naming.values, null]) { final selectedName = newName ?? originalName; final selectedNaming = Naming.chooseCloneNaming( - originalName: originalName, - newName: newName, - originalNaming: originalNaming, - newNaming: newNaming, - ); + originalName: originalName, + newName: newName, + originalNaming: originalNaming, + newNaming: newNaming); final reason = 'original: ($originalName, ${originalNaming.name}),' ' new: ($newName, ${newNaming?.name})' @@ -508,12 +503,10 @@ void main() { test('structure', () { final a = logic_structure_test.MyFancyStruct(); - final b = a.named( - 'b', + final b = a.named('b', - // naming should have no effect - naming: Naming.reserved, - ); + // naming should have no effect + naming: Naming.reserved); expect(b.name, 'b'); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index fdc522e96..98d865f0b 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -18,11 +18,9 @@ class ReadyValidStruct extends LogicStructure { final Logic ready; final Logic valid; - factory ReadyValidStruct({String name = 'readyValid'}) => ReadyValidStruct._( - Logic(name: 'ready'), - Logic(name: 'valid'), - name: name, - ); + factory ReadyValidStruct({String name = 'readyValid'}) => + ReadyValidStruct._(Logic(name: 'ready'), Logic(name: 'valid'), + name: name); ReadyValidStruct._(this.ready, this.valid, {required String name}) : super([ready, valid], name: name); @@ -36,11 +34,8 @@ class MyStruct extends LogicStructure { final Logic ready; final Logic valid; - factory MyStruct({String name = 'myStruct'}) => MyStruct._( - Logic(name: 'ready'), - Logic(name: 'valid'), - name: name, - ); + factory MyStruct({String name = 'myStruct'}) => + MyStruct._(Logic(name: 'ready'), Logic(name: 'valid'), name: name); MyStruct._(this.ready, this.valid, {required super.name}) : super([ready, valid]); @@ -55,12 +50,9 @@ class MyFancyStruct extends LogicStructure { final LogicStructure subStruct; factory MyFancyStruct({int busWidth = 12, String name = 'myFancyStruct'}) => - MyFancyStruct._( - LogicArray([3, 3], 8, name: 'arr'), - Logic(name: 'bus', width: busWidth), - MyStruct(), - name: name, - ); + MyFancyStruct._(LogicArray([3, 3], 8, name: 'arr'), + Logic(name: 'bus', width: busWidth), MyStruct(), + name: name); MyFancyStruct._(this.arr, this.bus, this.subStruct, {super.name = 'myFancyStruct'}) @@ -175,7 +167,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!'); @@ -183,18 +175,13 @@ void main() { group('LogicStructure construction', () { test('simple construction', () { - final s = LogicStructure([ - Logic(), - Logic(), - ], name: 'structure'); + final s = LogicStructure([Logic(), Logic()], name: 'structure'); expect(s.name, 'structure'); }); test('sub logic in two structures throws exception', () { - final s = LogicStructure([ - Logic(), - ], name: 'structure'); + final s = LogicStructure([Logic()], name: 'structure'); expect(() => LogicStructure([s.elements.first]), throwsA(isA())); @@ -203,9 +190,7 @@ void main() { test('sub structure in two structures throws exception', () { final subS = LogicStructure([Logic()]); - LogicStructure([ - subS, - ], name: 'structure'); + LogicStructure([subS], name: 'structure'); expect(() => LogicStructure([subS]), throwsA(isA())); @@ -251,7 +236,7 @@ void main() { final vectors = [ Vector({'ready': 0}, {'valid': 0}), - Vector({'ready': 1}, {'valid': 1}), + Vector({'ready': 1}, {'valid': 1}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -266,7 +251,7 @@ void main() { final vectors = [ Vector({'sIn': 0}, {'sOut': 0}), Vector({'sIn': LogicValue.ofString('10')}, - {'sOut': LogicValue.ofString('10')}), + {'sOut': LogicValue.ofString('10')}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -288,7 +273,7 @@ void main() { Vector({'sIn': 0}, {'sOut': LogicValue.filled(struct.width, LogicValue.one)}), Vector({'sIn': LogicValue.ofString('10' * (struct.width ~/ 2))}, - {'sOut': LogicValue.ofString('01' * (struct.width ~/ 2))}), + {'sOut': LogicValue.ofString('01' * (struct.width ~/ 2))}) ]; await SimCompare.checkFunctionalVector(mod, vectors); 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..fb01e4d29 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 @@ -24,16 +24,12 @@ class TopModule extends Module { } class SpeciallyNamedModule extends Module { - SpeciallyNamedModule( - Logic a, - bool reserveDefName, - bool reserveInstanceName, { - super.name = 'specialInstanceName', - super.definitionName = 'specialName', - }) : super( - reserveName: reserveInstanceName, - reserveDefinitionName: reserveDefName, - ) { + SpeciallyNamedModule(Logic a, bool reserveDefName, bool reserveInstanceName, + {super.name = 'specialInstanceName', + super.definitionName = 'specialName'}) + : super( + reserveName: reserveInstanceName, + reserveDefinitionName: reserveDefName) { addInput('a', a, width: a.width); } } @@ -41,20 +37,19 @@ class SpeciallyNamedModule extends Module { class RenameableModule extends Module { final String inputPortName; final String outputPortName; - RenameableModule( - Logic inputPort, { - this.outputPortName = 'outputPort', - String internalSignalName = 'internalSignal', - String reservedInternalSignalName = 'reservedInternalSignal', - String internalModuleInstanceName = 'internalModuleInstanceName', - String reservedInternalModuleInstanceName = - 'reservedInternalModuleInstanceName', - String internalModuleDefinitionName = 'internalModuleDefinitionName', - super.definitionName = 'moduleDefinitionName', - super.name = 'moduleInstanceName', - super.reserveDefinitionName = true, - super.reserveName = true, - }) : inputPortName = inputPort.name { + RenameableModule(Logic inputPort, + {this.outputPortName = 'outputPort', + String internalSignalName = 'internalSignal', + String reservedInternalSignalName = 'reservedInternalSignal', + String internalModuleInstanceName = 'internalModuleInstanceName', + String reservedInternalModuleInstanceName = + 'reservedInternalModuleInstanceName', + String internalModuleDefinitionName = 'internalModuleDefinitionName', + super.definitionName = 'moduleDefinitionName', + super.name = 'moduleInstanceName', + super.reserveDefinitionName = true, + super.reserveName = true}) + : inputPortName = inputPort.name { inputPort = addInput(inputPort.name, inputPort); final outputPort = addOutput(outputPortName); @@ -64,21 +59,13 @@ class RenameableModule extends Module { Combinational([internalSignal < ~inputPort]); Combinational([outputPort < internalSignal]); - SpeciallyNamedModule( - ~internalSignal, - true, - false, - name: internalModuleInstanceName, - definitionName: internalModuleDefinitionName, - ); - - SpeciallyNamedModule( - ~reservedInternalSignal, - true, - true, - name: reservedInternalModuleInstanceName, - definitionName: internalModuleDefinitionName, - ); + SpeciallyNamedModule(~internalSignal, true, false, + name: internalModuleInstanceName, + definitionName: internalModuleDefinitionName); + + SpeciallyNamedModule(~reservedInternalSignal, true, true, + name: reservedInternalModuleInstanceName, + definitionName: internalModuleDefinitionName); } } @@ -105,7 +92,7 @@ void main() { final vectors = [ Vector({mod.inputPortName: 0}, {mod.outputPortName: 1}), - Vector({mod.inputPortName: 1}, {mod.outputPortName: 0}), + Vector({mod.inputPortName: 1}, {mod.outputPortName: 0}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -115,19 +102,17 @@ void main() { } Future runTestGen(Map names) async => - runTest(RenameableModule( - Logic(name: names[NameType.inputPort]), - outputPortName: names[NameType.outputPort]!, - internalSignalName: names[NameType.internalSignal]!, - reservedInternalSignalName: names[NameType.reservedInternalSignal]!, - internalModuleInstanceName: names[NameType.internalModuleInstance]!, - reservedInternalModuleInstanceName: - names[NameType.reservedInternalModuleInstance]!, - internalModuleDefinitionName: - names[NameType.internalModuleDefinition]!, - definitionName: names[NameType.topDefinitionName], - name: names[NameType.topName]!, - )); + runTest(RenameableModule(Logic(name: names[NameType.inputPort]), + outputPortName: names[NameType.outputPort]!, + internalSignalName: names[NameType.internalSignal]!, + reservedInternalSignalName: names[NameType.reservedInternalSignal]!, + internalModuleInstanceName: names[NameType.internalModuleInstance]!, + reservedInternalModuleInstanceName: + names[NameType.reservedInternalModuleInstance]!, + internalModuleDefinitionName: + names[NameType.internalModuleDefinition]!, + definitionName: names[NameType.topDefinitionName], + name: names[NameType.topName]!)); for (var i = 0; i < NameType.values.length; i++) { for (var j = i + 1; j < NameType.values.length; j++) { @@ -136,17 +121,17 @@ 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, - NameType.topDefinitionName, - }, + {NameType.internalModuleDefinition, NameType.topDefinitionName}, { NameType.inputPort, NameType.outputPort, NameType.reservedInternalSignal, - NameType.reservedInternalModuleInstance, - }, + NameType.reservedInternalModuleInstance + } ]; var expectFail = false; @@ -191,20 +176,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 +197,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..71af40d48 --- /dev/null +++ b/test/naming_consistency_test.dart @@ -0,0 +1,310 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_consistency_test.dart +// Validates that both the SystemVerilog synthesizer and a base +// SynthModuleDefinition (used by the netlist synthesizer) produce +// consistent signal names via the shared Module.namer. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Helper modules ────────────────────────────────────────────────── + +/// A simple module with ports, internal wires, and a sub-module. +class _Inner extends Module { + _Inner(Logic a, Logic b) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + addOutput('y', width: a.width) <= a & b; + } +} + +class _Outer extends Module { + _Outer(Logic a, Logic b) : super(name: 'outer') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final inner = _Inner(a, b); + addOutput('y', width: a.width) <= inner.output('y'); + } +} + +/// A module with a constant assignment (exercises const naming). +class _ConstModule extends Module { + _ConstModule(Logic a) : super(name: 'constmod') { + a = addInput('a', a, width: 8); + final c = Const(0x42, width: 8).named('myConst', naming: Naming.mergeable); + addOutput('y', width: 8) <= a + c; + } +} + +/// A module with Naming.renameable and Naming.mergeable signals. +class _MixedNaming extends Module { + _MixedNaming(Logic a) : super(name: 'mixednaming') { + a = addInput('a', a, width: 8); + final r = Logic(name: 'renamed', width: 8, naming: Naming.renameable) + ..gets(a); + final m = Logic(name: 'merged', width: 8, naming: Naming.mergeable) + ..gets(r); + addOutput('y', width: 8) <= m; + } +} + +/// A module with a FlipFlop sub-module. +class _FlopOuter extends Module { + _FlopOuter(Logic clk, Logic d) : super(name: 'flopouter') { + clk = addInput('clk', clk); + d = addInput('d', d, width: 8); + addOutput('q', width: 8) <= flop(clk, d); + } +} + +/// Builds [SynthModuleDefinition]s from both bases and collects a +/// Logic→name mapping for all present SynthLogics. +/// +/// Returns maps from Logic to its resolved signal name. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + // Skip SynthLogics whose name was never picked (replaced/pruned). + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked — skip + } + } + return names; +} + +void main() { + group('naming consistency', () { + test('SV and base SynthModuleDefinition agree on port names', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // SV synthesizer path + final svDef = SystemVerilogSynthModuleDefinition(mod); + + // Base path (same as netlist synthesizer uses) + // Since namer is late final, the second constructor reuses + // the same naming state — names must be consistent. + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + // Every Logic present in both must have the same name. + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name} ' + '(${logic.runtimeType}, naming=${logic.naming})', + ); + } + } + + // Port names specifically must match. + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + expect( + svNames[port], + isNotNull, + reason: 'SV def should have port ${port.name}', + ); + expect( + baseNames[port], + isNotNull, + reason: 'Base def should have port ${port.name}', + ); + expect( + svNames[port], + baseNames[port], + reason: 'Port name must match for ${port.name}', + ); + } + }); + + test('constant naming is consistent', () async { + final mod = _ConstModule(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); + } + } + }); + + test('mixed naming (renameable + mergeable) is consistent', () async { + final mod = _MixedNaming(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); + } + } + }); + + test('flop module naming is consistent', () async { + final mod = _FlopOuter(Logic(), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect( + baseNames[logic], + svNames[logic], + reason: 'Name mismatch for ${logic.name}', + ); + } + } + }); + + test('namer is shared across multiple SynthModuleDefinitions', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // Build one def, then build another — same namer instance. + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = _collectNames(def1); + final names2 = _collectNames(def2); + + for (final logic in names1.keys) { + if (names2.containsKey(logic)) { + expect( + names2[logic], + names1[logic], + reason: 'Shared namer should produce same name for ' + '${logic.name}', + ); + } + } + }); + + test('Namer.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.', + ); + }, + ); + }); +} diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart new file mode 100644 index 000000000..2c596847c --- /dev/null +++ b/test/naming_namespace_test.dart @@ -0,0 +1,129 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_namespace_test.dart +// Tests for constant naming via nameOfBest and shared instance/signal +// namespace uniquification. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// A simple submodule whose instance name can collide with a signal name. +class _Inner extends Module { + _Inner(Logic a, {super.name = 'inner'}) { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// Top module that has a signal named the same as a submodule instance. +class _InstanceSignalCollision extends Module { + _InstanceSignalCollision({String instanceName = 'inner'}) + : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // Create a signal whose name matches the submodule instance name. + final sig = Logic(name: instanceName); + sig <= ~a; + + final sub = _Inner(sig, name: instanceName); + o <= sub.output('b'); + } +} + +/// Top module with two submodule instances that have the same name. +class _DuplicateInstances extends Module { + _DuplicateInstances() : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + final sub1 = _Inner(a, name: 'blk'); + final sub2 = _Inner(sub1.output('b'), name: 'blk'); + o <= sub2.output('b'); + } +} + +/// Module that uses a constant in a connection chain, exercising constant +/// naming through nameOfBest. +class _ConstantNamingModule extends Module { + _ConstantNamingModule() : super(name: 'const_mod') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // A constant "1" drives one input of the AND gate. + o <= a & Const(1); + } +} + +/// Module with a mux where one input is a constant, exercising the +/// constNameDisallowed path — the mux output cannot use the constant's +/// literal as its name because it also carries non-constant values. +class _ConstNameDisallowedModule extends Module { + _ConstNameDisallowedModule() : super(name: 'const_disallow') { + final a = addInput('a', Logic()); + final sel = addInput('sel', Logic()); + final o = addOutput('o'); + + // mux output can be the constant OR a, so the constant name is disallowed. + o <= mux(sel, Const(1), a); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('constant naming via nameOfBest', () { + test('constant value appears as literal in SV output', () async { + final dut = _ConstantNamingModule(); + await dut.build(); + final sv = 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('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/netlist_example_test.dart b/test/netlist_example_test.dart new file mode 100644 index 000000000..53e807d8b --- /dev/null +++ b/test/netlist_example_test.dart @@ -0,0 +1,298 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_example_test.dart +// Convert examples to netlist JSON and check the produced output. + +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +void main() { + // Detect whether running in JS (dart2js) environment. In JS many + // `dart:io` APIs are unsupported; when running tests with + // `--platform node` we skip filesystem and loader assertions. + const isJS = identical(0, 0.0); + + // Helper used by the tests to synthesize `top` and optionally write the + // produced JSON to `outPath` when running on VM. Returns the decoded + // modules map from the Yosys-format JSON. + Future> convertTestWriteNetlist( + Module top, + String outPath, + ) async { + final synth = SynthBuilder(top, NetlistSynthesizer()); + final jsonStr = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + top, + ); + if (!isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(jsonStr); + } + final decoded = jsonDecode(jsonStr) as Map; + return decoded['modules'] as Map; + } + + test('Netlist dump for example Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + SvService(counter, register: false).synthOutput; + + final modules = await convertTestWriteNetlist( + counter, + 'build/Counter.rohd.json', + ); + + expect( + modules, + isNotEmpty, + reason: 'Counter netlist should have module definitions', + ); + // The top module should have cells (sub-module instances or gates) + final topMod = modules[counter.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'Counter should have cells'); + }); + + group('SynthBuilder netlist generation for examples', () { + test('SynthBuilder netlist for Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + + final modules = await convertTestWriteNetlist( + counter, + 'build/Counter.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'Counter synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = FirFilter( + en, + resetB, + clk, + inputVal, + [ + 0, + 0, + 0, + 1, + ], + bitWidth: 8); + await fir.build(); + + final synth = SynthBuilder(fir, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + fir, + 'build/FirFilter.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'FirFilter synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = LogicArrayExample( + arrayA, + id, + selectIndexValue, + selectFromValue, + ); + await la.build(); + + final synth = SynthBuilder(la, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + la, + 'build/LogicArrayExample.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'LogicArrayExample synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + final synth = SynthBuilder(oven, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + oven, + 'build/OvenModule.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'OvenModule synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + final synth = SynthBuilder(tree, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser (pure Dart or JS). + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + tree, + ); + expect( + json, + isNotEmpty, + reason: 'TreeOfTwoInputModules should produce non-empty JSON', + ); + if (!isJS) { + final file = File('build/TreeOfTwoInputModules.synth.rohd.json'); + await file.create(recursive: true); + await file.writeAsString(json); + } + }); + }); + + test('Netlist dump for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await fir.build(); + + const outPath = 'build/FirFilter.rohd.json'; + final modules = await convertTestWriteNetlist(fir, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'FirFilter netlist should have module definitions', + ); + }); + + test('Netlist dump for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = LogicArrayExample(arrayA, id, selectIndexValue, selectFromValue); + await la.build(); + + const outPath = 'build/LogicArrayExample.rohd.json'; + final modules = await convertTestWriteNetlist(la, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'LogicArrayExample netlist should have module definitions', + ); + }); + + test('Netlist dump for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + const outPath = 'build/OvenModule.rohd.json'; + final modules = await convertTestWriteNetlist(oven, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'OvenModule netlist should have module definitions', + ); + }); + + test('Netlist dump for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser. + const outPath = 'build/TreeOfTwoInputModules.rohd.json'; + final synth = SynthBuilder(tree, NetlistSynthesizer()); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + tree, + ); + expect( + json, + isNotEmpty, + reason: 'TreeOfTwoInputModules should produce non-empty JSON', + ); + if (!isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(json); + expect(file.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + } + }); +} diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart new file mode 100644 index 000000000..d7ad0620c --- /dev/null +++ b/test/netlist_synthesizer_test.dart @@ -0,0 +1,1428 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer_test.dart +// Comprehensive tests for the netlist synthesizer covering leaf cell +// mapping, structural validation, options permutations, and real +// example designs. +// +// 2026 April 13 +// Author: Auto-generated + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +// ──────────────────────────────────────────────────────────────────── +// Tiny helper modules for targeted gate-level tests +// ──────────────────────────────────────────────────────────────────── + +/// Exercises And2Gate. +class AndModule extends Module { + Logic get y => output('y'); + AndModule(Logic a, Logic b) : super(name: 'andmod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a & b; + } +} + +/// Exercises Or2Gate. +class OrModule extends Module { + Logic get y => output('y'); + OrModule(Logic a, Logic b) : super(name: 'ormod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a | b; + } +} + +/// Exercises Xor2Gate. +class XorModule extends Module { + Logic get y => output('y'); + XorModule(Logic a, Logic b) : super(name: 'xormod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a ^ b; + } +} + +/// Exercises NotGate. +class NotModule extends Module { + Logic get y => output('y'); + NotModule(Logic a) : super(name: 'notmod') { + a = addInput('a', a); + addOutput('y') <= ~a; + } +} + +/// Exercises Mux. +class MuxModule extends Module { + Logic get y => output('y'); + MuxModule(Logic sel, Logic a, Logic b, {int width = 8}) : super(name: 'mux') { + sel = addInput('sel', sel); + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y', width: width) <= mux(sel, a, b); + } +} + +/// Exercises FlipFlop. +class FlopModule extends Module { + Logic get q => output('q'); + FlopModule(Logic clk, Logic d, {int width = 8}) : super(name: 'flopmod') { + clk = addInput('clk', clk); + d = addInput('d', d, width: width); + addOutput('q', width: width) <= flop(clk, d); + } +} + +/// Exercises Add. +class AddModule extends Module { + Logic get sum => output('sum'); + AddModule(Logic a, Logic b, {int width = 8}) : super(name: 'addmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('sum', width: width) <= a + b; + } +} + +/// Exercises Multiply. +class MulModule extends Module { + Logic get prod => output('prod'); + MulModule(Logic a, Logic b, {int width = 8}) : super(name: 'mulmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('prod', width: width) <= a * b; + } +} + +/// Exercises BusSubset ($slice). +class SliceModule extends Module { + Logic get y => output('y'); + SliceModule(Logic a) : super(name: 'slicemod') { + a = addInput('a', a, width: 8); + addOutput('y', width: 4) <= a.getRange(2, 6); + } +} + +/// Exercises comparison operators. +class CompareModule extends Module { + Logic get lt => output('lt'); + Logic get gt => output('gt'); + Logic get eq => output('eq'); + CompareModule(Logic a, Logic b, {int width = 8}) : super(name: 'cmpmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('lt') <= LessThan(a, b).out; + addOutput('gt') <= GreaterThan(a, b).out; + addOutput('eq') <= a.eq(b); + } +} + +/// Exercises shift operations. +class ShiftModule extends Module { + Logic get shl => output('shl'); + Logic get shr => output('shr'); + ShiftModule(Logic a, Logic amt, {int width = 8}) : super(name: 'shiftmod') { + a = addInput('a', a, width: width); + amt = addInput('amt', amt, width: width); + addOutput('shl', width: width) <= a << amt; + addOutput('shr', width: width) <= a >>> amt; + } +} + +/// Exercises Xor2Gate. +class XorGateModule extends Module { + Logic get y => output('y'); + XorGateModule(Logic a, Logic b) : super(name: 'xormod2') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a ^ b; + } +} + +/// Exercises Subtract. +class SubModule extends Module { + Logic get diff => output('diff'); + SubModule(Logic a, Logic b, {int width = 8}) : super(name: 'submod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('diff', width: width) <= a - b; + } +} + +/// Exercises Swizzle ($concat). +class SwizzleModule extends Module { + Logic get y => output('y'); + SwizzleModule(Logic a, Logic b, {int width = 4}) : super(name: 'swizmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y', width: width * 2) <= [a, b].swizzle(); + } +} + +/// Exercises arithmetic right shift (ARShift). +class ARShiftModule extends Module { + Logic get y => output('y'); + ARShiftModule(Logic a, Logic amt, {int width = 8}) + : super(name: 'arshiftmod') { + a = addInput('a', a, width: width); + amt = addInput('amt', amt, width: width); + addOutput('y', width: width) <= a >> amt; + } +} + +/// Exercises unary reduction ops. +class ReduceModule extends Module { + Logic get andR => output('andR'); + Logic get orR => output('orR'); + Logic get xorR => output('xorR'); + ReduceModule(Logic a, {int width = 8}) : super(name: 'reducemod') { + a = addInput('a', a, width: width); + addOutput('andR') <= a.and(); + addOutput('orR') <= a.or(); + addOutput('xorR') <= a.xor(); + } +} + +/// Exercises individual comparison ops for cell-type checking. +class LtModule extends Module { + Logic get y => output('y'); + LtModule(Logic a, Logic b, {int width = 8}) : super(name: 'ltmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.lt(b); + } +} + +class GtModule extends Module { + Logic get y => output('y'); + GtModule(Logic a, Logic b, {int width = 8}) : super(name: 'gtmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.gt(b); + } +} + +class EqModule extends Module { + Logic get y => output('y'); + EqModule(Logic a, Logic b, {int width = 8}) : super(name: 'eqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.eq(b); + } +} + +class NeqModule extends Module { + Logic get y => output('y'); + NeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'neqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.neq(b); + } +} + +class LeqModule extends Module { + Logic get y => output('y'); + LeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'leqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.lte(b); + } +} + +class GeqModule extends Module { + Logic get y => output('y'); + GeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'geqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.gte(b); + } +} + +/// Exercises TriStateBuffer. +class TriBufModule extends Module { + Logic get bus => inOut('bus'); + TriBufModule(LogicNet busNet, Logic data, Logic en) + : super(name: 'tribufmod') { + final bus = addInOut('bus', busNet, width: data.width); + data = addInput('data', data, width: data.width); + en = addInput('en', en); + TriStateBuffer(data, enable: en, name: 'tsb').out.gets(bus); + } +} + +/// Exercises Combinational with If. +class CombIfModule extends Module { + Logic get y => output('y'); + CombIfModule(Logic sel, Logic a, Logic b, {int width = 8}) + : super(name: 'combif') { + sel = addInput('sel', sel); + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final y = addOutput('y', width: width); + Combinational([ + If(sel, then: [y < a], orElse: [y < b]), + ]); + } +} + +/// Exercises Sequential with If. +class SeqIfModule extends Module { + Logic get q => output('q'); + SeqIfModule(Logic clk, Logic en, Logic d, {int width = 8}) + : super(name: 'seqif') { + clk = addInput('clk', clk); + en = addInput('en', en); + d = addInput('d', d, width: width); + final q = addOutput('q', width: width); + Sequential(clk, [ + If(en, then: [q < d]), + ]); + } +} + +/// Module with multiple instances of the same sub-module (dedup test). +class DedupTop extends Module { + Logic get y0 => output('y0'); + Logic get y1 => output('y1'); + DedupTop(Logic a, Logic b, {int width = 8}) + : super(name: 'deduptop', definitionName: 'DedupTop') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y0', width: width) <= AddModule(a, b, width: width).sum; + addOutput('y1', width: width) <= AddModule(a, b, width: width).sum; + } +} + +/// Module with different-width instances (no dedup). +class NoDedupTop extends Module { + Logic get y0 => output('y0'); + Logic get y1 => output('y1'); + NoDedupTop(Logic a4, Logic b4, Logic a8, Logic b8) + : super(name: 'nodeduptop', definitionName: 'NoDedupTop') { + a4 = addInput('a4', a4, width: 4); + b4 = addInput('b4', b4, width: 4); + a8 = addInput('a8', a8, width: 8); + b8 = addInput('b8', b8, width: 8); + addOutput('y0', width: 4) <= AddModule(a4, b4, width: 4).sum; + addOutput('y1', width: 8) <= AddModule(a8, b8).sum; + } +} + +/// A module with a named constant (Logic..gets(Const)) used inside a +/// Combinational block — exercises the named-constant fix. +class _NamedConstModule extends Module { + _NamedConstModule(Logic clk, Logic reset) : super(name: 'namedConstMod') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + final dataIn = addInput('dataIn', Logic(width: 8), width: 8); + final result = addOutput('result', width: 8); + + // Named constant driven by Const — this is the pattern from + // _dynamicInputToLogic in SummationBase. + final myConst = Logic(name: 'myConst', width: 8)..gets(Const(0, width: 8)); + + Combinational([ + result < mux(dataIn.or(), dataIn, myConst), + ]); + } +} + +// ──────────────────────────────────────────────────────────────────── +// Helpers +// ──────────────────────────────────────────────────────────────────── + +/// Build a FilterBank module for testing (not yet built). +FilterBank _buildFilterBank() { + const dataWidth = 16; + const numTaps = 3; + const coeffs0 = [1, 2, 1]; + const coeffs1 = [1, -2, 1]; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [coeffs0, coeffs1], + ); +} + +/// Build a module and synthesize to a parsed JSON map. +Future> _synthToMap( + Module mod, { + NetlistOptions options = const NetlistOptions(), +}) async { + await mod.build(); + final synth = SynthBuilder(mod, NetlistSynthesizer(options: options)); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + mod, + ); + return jsonDecode(json) as Map; +} + +/// Extract the `modules` map from a synthesized JSON map. +Map _modules(Map json) => + json['modules'] as Map; + +/// Get cells map from a module definition. +Map _cells(Map moduleDef) => + moduleDef['cells'] as Map? ?? {}; + +/// Get ports map from a module definition. +Map _ports(Map moduleDef) => + moduleDef['ports'] as Map? ?? {}; + +/// Get netnames map from a module definition. +Map _netnames(Map moduleDef) => + moduleDef['netnames'] as Map? ?? {}; + +/// Check that a module definition has a port with given name and direction. +void _expectPort( + Map moduleDef, + String portName, + String direction, +) { + final ports = _ports(moduleDef); + expect(ports, contains(portName), reason: 'Expected port "$portName"'); + final port = ports[portName] as Map; + expect( + port['direction'], + equals(direction), + reason: 'Port "$portName" should be "$direction"', + ); +} + +/// Returns true if any cell in any module definition has the given type. +bool _hasCellType(Map json, String cellType) { + final mod = _modules(json); + return mod.values.any((m) { + final def = m as Map; + return _cells(def).values.any((c) { + final cell = c as Map; + return (cell['type'] as String) == cellType; + }); + }); +} + +// ──────────────────────────────────────────────────────────────────── +// Tests +// ──────────────────────────────────────────────────────────────────── + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + // ── Group 1: Leaf cell mapper — individual gate mappings ─────────── + + group('leaf cell mapping', () { + test(r'And2Gate maps to $and cell', () async { + final json = await _synthToMap(AndModule(Logic(), Logic())); + expect(_hasCellType(json, r'$and'), isTrue); + }); + + test(r'Or2Gate maps to $or cell', () async { + final json = await _synthToMap(OrModule(Logic(), Logic())); + expect(_hasCellType(json, r'$or'), isTrue); + }); + + test(r'Xor2Gate maps to $xor cell', () async { + final json = await _synthToMap(XorGateModule(Logic(), Logic())); + expect(_hasCellType(json, r'$xor'), isTrue); + }); + + test(r'NotGate maps to $not cell', () async { + final json = await _synthToMap(NotModule(Logic())); + expect(_hasCellType(json, r'$not'), isTrue); + }); + + test(r'Mux maps to $mux cell', () async { + final json = await _synthToMap( + MuxModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$mux'), isTrue); + }); + + test(r'FlipFlop maps to $dff cell', () async { + final clk = SimpleClockGenerator(10).clk; + final json = await _synthToMap(FlopModule(clk, Logic(width: 8))); + expect(_hasCellType(json, r'$dff'), isTrue); + }); + + test(r'Add maps to $add cell', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$add'), isTrue); + }); + + test(r'Subtract maps to $sub cell', () async { + final json = await _synthToMap( + SubModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$sub'), isTrue); + }); + + test(r'Multiply maps to $mul cell', () async { + final json = await _synthToMap( + MulModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$mul'), isTrue); + }); + + test(r'BusSubset maps to $slice cell', () async { + final json = await _synthToMap(SliceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$slice'), isTrue); + }); + + test(r'Swizzle maps to $concat cell', () async { + final json = await _synthToMap( + SwizzleModule(Logic(width: 4), Logic(width: 4)), + ); + expect(_hasCellType(json, r'$concat'), isTrue); + }); + + test(r'LessThan maps to $lt cell', () async { + final json = await _synthToMap( + LtModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$lt'), isTrue); + }); + + test(r'GreaterThan maps to $gt cell', () async { + final json = await _synthToMap( + GtModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$gt'), isTrue); + }); + + test(r'Equals maps to $eq cell', () async { + final json = await _synthToMap( + EqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$eq'), isTrue); + }); + + test(r'NotEquals maps to $ne cell', () async { + final json = await _synthToMap( + NeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$ne'), isTrue); + }); + + test(r'LessThanOrEqual maps to $le cell', () async { + final json = await _synthToMap( + LeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$le'), isTrue); + }); + + test(r'GreaterThanOrEqual maps to $ge cell', () async { + final json = await _synthToMap( + GeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$ge'), isTrue); + }); + + test(r'LShift maps to $shl cell', () async { + final json = await _synthToMap( + ShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shl'), isTrue); + }); + + test(r'RShift maps to $shr cell', () async { + final json = await _synthToMap( + ShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shr'), isTrue); + }); + + test(r'ARShift maps to $shiftx cell', () async { + final json = await _synthToMap( + ARShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shiftx'), isTrue); + }); + + test(r'AndUnary maps to $reduce_and cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_and'), isTrue); + }); + + test(r'OrUnary maps to $reduce_or cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_or'), isTrue); + }); + + test(r'XorUnary maps to $reduce_xor cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_xor'), isTrue); + }); + + test(r'TriStateBuffer maps to $tribuf cell', () async { + final busNet = LogicNet(width: 8); + final json = await _synthToMap( + TriBufModule(busNet, Logic(width: 8), Logic()), + ); + expect(_hasCellType(json, r'$tribuf'), isTrue); + }); + }); + + // ── Group 2: Structural content validation ───────────────────────── + + group('structural validation', () { + test('ports have correct direction', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + // Find the top-level or AddModule definition + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + final ports = _ports(d); + for (final port in ports.entries) { + final p = port.value as Map; + expect( + ['input', 'output', 'inout'].contains(p['direction']), + isTrue, + reason: 'Port ${port.key} should have valid direction', + ); + // Each port should have bits + expect( + p['bits'], + isNotNull, + reason: 'Port ${port.key} should have bits array', + ); + } + } + }); + + test('cells have type and connections', () async { + final json = await _synthToMap( + MuxModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + expect(c['type'], isNotNull, reason: 'Every cell should have a type'); + expect( + c['connections'], + isNotNull, + reason: 'Every cell should have connections', + ); + } + } + }); + + test('netnames have bits arrays', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + for (final nn in _netnames(d).values) { + final n = nn as Map; + expect( + n['bits'], + isA>(), + reason: 'Each netname should have a bits list', + ); + } + } + }); + + test('inOut ports have direction inout', () async { + final busNet = LogicNet(width: 8); + final json = await _synthToMap( + TriBufModule(busNet, Logic(width: 8), Logic()), + ); + final mod = _modules(json); + // Find the TriBufModule definition + final tribufDef = mod.values.firstWhere((m) { + final d = m as Map; + return _ports(d).values.any((p) { + final port = p as Map; + return port['direction'] == 'inout'; + }); + }, orElse: () => {}) as Map; + expect( + tribufDef, + isNotEmpty, + reason: 'Should have a module with inout ports', + ); + }); + + test('Combinational If produces Combinational cell', () async { + final json = await _synthToMap( + CombIfModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + // Combinational blocks become Combinational cell type + expect( + _hasCellType(json, 'Combinational'), + isTrue, + reason: 'Combinational If should produce a Combinational cell', + ); + }); + + test('Sequential If produces dff cells', () async { + final clk = SimpleClockGenerator(10).clk; + final json = await _synthToMap( + SeqIfModule(clk, Logic(), Logic(width: 8)), + ); + final mod = _modules(json); + final hasSeq = mod.values.any((m) { + final def = m as Map; + final cells = _cells(def); + return cells.values.any((c) { + final cell = c as Map; + return (cell['type'] as String).contains('Sequential'); + }); + }); + expect( + hasSeq, + isTrue, + reason: 'Sequential If should contain Sequential cells', + ); + }); + }); + + // ── Group 3: Module deduplication ────────────────────────────────── + + group('deduplication', () { + test('identical sub-modules are deduplicated', () async { + final json = await _synthToMap( + DedupTop(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + // AddModule should appear only once as a definition + final addDefs = mod.keys.where((k) => k.contains('Add')).toList(); + expect( + addDefs.length, + equals(1), + reason: 'Two identical AddModules should produce one definition', + ); + // But should be instantiated twice in the top-level cells + final topDef = mod.entries + .firstWhere((e) => e.key.contains('DedupTop')) + .value as Map; + final addCells = _cells(topDef).values.where((c) { + final cell = c as Map; + return (cell['type'] as String).contains('Add'); + }).toList(); + expect( + addCells.length, + equals(2), + reason: 'Top module should instantiate AddModule twice', + ); + }); + + test('different-width sub-modules are not deduplicated', () async { + final json = await _synthToMap( + NoDedupTop( + Logic(width: 4), + Logic(width: 4), + Logic(width: 8), + Logic(width: 8), + ), + ); + final mod = _modules(json); + // Should have two distinct AddModule definitions (different widths) + final addDefs = mod.keys.where((k) => k.contains('Add')).toList(); + expect( + addDefs.length, + greaterThanOrEqualTo(2), + reason: 'Different-width AddModules should NOT be deduplicated', + ); + }); + }); + + // ── Group 4: NetlistOptions permutations ───────────────────────── + + group('NetlistOptions', () { + late Module filterBank; + + setUp(() async { + await Simulator.reset(); + filterBank = _buildFilterBank(); + await filterBank.build(); + }); + + test('default options produce valid netlist', () async { + final synth = SynthBuilder(filterBank, NetlistSynthesizer()); + final json = (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('slimMode omits connections', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + ); + final json = (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + final mod = _modules(parsed); + expect(mod, isNotEmpty); + // In slim mode, cells should exist but connections should be empty + for (final def in mod.values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + final conns = c['connections'] as Map?; + if (conns != null) { + expect( + conns, + isEmpty, + reason: 'Slim mode cells should have empty connections', + ); + } + } + } + }); + + test('DCE disabled still produces valid netlist', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + ); + final json = (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('all optimizations disabled produces valid netlist', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + ); + final json = (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('slim and full produce same module definitions', () async { + final fullSynth = SynthBuilder(filterBank, NetlistSynthesizer()); + final fullJson = (fullSynth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final fullParsed = jsonDecode(fullJson) as Map; + + // Rebuild for slim + await Simulator.reset(); + final fb2 = _buildFilterBank(); + await fb2.build(); + final slimSynth = SynthBuilder( + fb2, + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + ); + final slimJson = + (slimSynth.synthesizer as NetlistSynthesizer).synthesizeToJson(fb2); + final slimParsed = jsonDecode(slimJson) as Map; + + // Same module definition names + expect( + _modules(slimParsed).keys.toSet(), + equals(_modules(fullParsed).keys.toSet()), + reason: 'Slim and full should have identical module definition names', + ); + }); + }); + + // ── Group 5: Example designs — structural checks ─────────────────── + + group('example designs', () { + test('Counter netlist has FlipFlop and FSM-related cells', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = Counter(en, reset, clk); + final json = await _synthToMap(counter); + final mod = _modules(json); + + expect( + mod, + isNotEmpty, + reason: 'Counter should produce module definitions', + ); + // Should have a Counter definition + expect(mod.keys.any((k) => k.contains('Counter')), isTrue); + }); + + test('FirFilter netlist has pipeline and multiplier cells', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + final fir = FirFilter( + en, + resetB, + clk, + inputVal, + [ + 0, + 0, + 0, + 1, + ], + bitWidth: 8); + final json = await _synthToMap(fir); + final mod = _modules(json); + + expect( + mod, + isNotEmpty, + reason: 'FirFilter should produce module definitions', + ); + }); + + test('OvenModule netlist has FSM states', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final oven = OvenModule(button, reset, clk); + final json = await _synthToMap(oven); + final mod = _modules(json); + + expect(mod, isNotEmpty); + // Should have OvenModule definition + expect( + mod.keys.any((k) => k.contains('Oven') || k.contains('oven')), + isTrue, + ); + }); + + test('LogicArrayExample netlist has array-related cells', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + final la = LogicArrayExample( + arrayA, + id, + selectIndexValue, + selectFromValue, + ); + final json = await _synthToMap(la); + final mod = _modules(json); + + expect(mod, isNotEmpty); + }); + + test('TreeOfTwoInputModules netlist has recursive hierarchy', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + final synth = SynthBuilder(tree, NetlistSynthesizer()); + final json = + (synth.synthesizer as NetlistSynthesizer).synthesizeToJson(tree); + expect(json, isNotEmpty); + final parsed = jsonDecode(json) as Map; + final mod = _modules(parsed); + expect(mod, isNotEmpty, reason: 'Tree should have module definitions'); + }); + }); + + // ── Group 6: FilterBank deep structural checks ───────────────────── + + group('FilterBank netlist structure', () { + late Map json; + + setUpAll(() async { + final fb = _buildFilterBank(); + json = await _synthToMap(fb); + }); + + test('contains expected module definitions', () { + final mod = _modules(json); + final defNames = mod.keys.toSet(); + + // FilterBank, FilterChannel, CoeffBank, MacUnit, FilterController + // should all appear (possibly with parameterized suffixes) + expect( + defNames.any((k) => k.contains('FilterBank')), + isTrue, + reason: 'Should have FilterBank definition', + ); + expect( + defNames.any((k) => k.contains('FilterChannel')), + isTrue, + reason: 'Should have FilterChannel definition', + ); + expect( + defNames.any((k) => k.contains('CoeffBank')), + isTrue, + reason: 'Should have CoeffBank definition', + ); + expect( + defNames.any((k) => k.contains('MacUnit')), + isTrue, + reason: 'Should have MacUnit definition', + ); + expect( + defNames.any((k) => k.contains('FilterController')), + isTrue, + reason: 'Should have FilterController definition', + ); + }); + + test('FilterBank has array ports', () { + final mod = _modules(json); + final fbDef = mod.entries + .firstWhere((e) => e.key.contains('FilterBank')) + .value as Map; + final ports = _ports(fbDef); + + // Should have sample0/sample1 and channelOut as array ports + expect( + ports.keys.any( + (k) => k.contains('sample') || k.contains('channelOut'), + ), + isTrue, + reason: 'FilterBank should have array port signals', + ); + }); + + test('FilterBank top instantiates two FilterChannels', () { + final mod = _modules(json); + final fbDef = mod.entries + .firstWhere((e) => e.key.contains('FilterBank')) + .value as Map; + final cells = _cells(fbDef); + + final channelCells = cells.entries.where((e) { + final cell = e.value as Map; + return (cell['type'] as String).contains('FilterChannel'); + }).toList(); + + expect( + channelCells.length, + equals(2), + reason: 'FilterBank should instantiate 2 FilterChannels', + ); + }); + + test( + 'FilterChannels with different coefficients get separate definitions', + () { + final mod = _modules(json); + final channelDefs = + mod.keys.where((k) => k.contains('FilterChannel')).toList(); + + expect( + channelDefs.length, + equals(2), + reason: 'Two FilterChannels with different coefficients ' + 'should produce distinct definitions', + ); + }, + ); + + test('MacUnit definition contains Pipeline-generated cells', () { + final mod = _modules(json); + final macDef = mod.entries + .firstWhere((e) => e.key.contains('MacUnit')) + .value as Map; + final cells = _cells(macDef); + + // Pipeline generates Sequential cells for stage registers + final hasSeq = cells.values.any((c) { + final cell = c as Map; + final type = cell['type'] as String; + return type.contains('Sequential'); + }); + expect( + hasSeq, + isTrue, + reason: 'MacUnit Pipeline should produce Sequential cells', + ); + }); + + test('CoeffBank has coeffArray input port', () { + final mod = _modules(json); + final coeffDef = mod.entries + .firstWhere((e) => e.key.contains('CoeffBank')) + .value as Map; + final ports = _ports(coeffDef); + + // Should have coeffArray-related port names + expect( + ports.keys.any((k) => k.contains('coeffArray')), + isTrue, + reason: 'CoeffBank should have coeffArray port', + ); + + // tapIndex should be input + expect( + ports.keys.any((k) => k.contains('tapIndex')), + isTrue, + reason: 'CoeffBank should have tapIndex port', + ); + }); + + test('FilterController has FSM state output', () { + final mod = _modules(json); + final ctrlDef = mod.entries + .firstWhere((e) => e.key.contains('FilterController')) + .value as Map; + final ports = _ports(ctrlDef); + + _expectPort(ctrlDef, 'state', 'output'); + _expectPort(ctrlDef, 'filterEnable', 'output'); + _expectPort(ctrlDef, 'doneFlag', 'output'); + expect(ports.keys.any((k) => k.contains('clk')), isTrue); + expect(ports.keys.any((k) => k.contains('reset')), isTrue); + }); + + test('all module definitions have valid JSON structure', () { + final mod = _modules(json); + for (final entry in mod.entries) { + final defName = entry.key; + final def = entry.value as Map; + + // Every definition must have ports and cells + expect( + def.containsKey('ports'), + isTrue, + reason: '$defName should have ports', + ); + expect( + def.containsKey('cells'), + isTrue, + reason: '$defName should have cells', + ); + + // All ports must have direction and bits + for (final port in _ports(def).entries) { + final p = port.value as Map; + expect( + p.containsKey('direction'), + isTrue, + reason: '$defName.${port.key} should have direction', + ); + expect( + p.containsKey('bits'), + isTrue, + reason: '$defName.${port.key} should have bits', + ); + } + + // All cells must have type + for (final cell in _cells(def).entries) { + final c = cell.value as Map; + expect( + c.containsKey('type'), + isTrue, + reason: '$defName cell ${cell.key} should have type', + ); + } + } + }); + }); + + // ── Group 7: Design API path ─────────────────────────────────────── + + group('Design API path', () { + test('build with netlistOptions enables NetlistService', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = Counter(en, reset, clk); + + await counter.build(); + final netSvc = NetlistService(counter); + + final fullJson = netSvc.json; + expect(fullJson, isNotNull); + + final parsed = jsonDecode(fullJson) as Map; + expect(parsed.containsKey('modules'), isTrue); + }); + + test('moduleJson returns per-module data', () async { + final fb = _buildFilterBank(); + await fb.build(); + final netSvc = NetlistService(fb); + + // Fetch FilterBank definition specifically + final fbJson = netSvc.moduleJson(fb.definitionName); + final parsed = jsonDecode(fbJson) as Map; + final modules = parsed['modules'] as Map; + expect(modules.containsKey(fb.definitionName), isTrue); + }); + + test('slimJson produces slim output', () async { + final fb = _buildFilterBank(); + await fb.build(); + final netSvc = NetlistService(fb); + + final slimJson = netSvc.slimJson; + + final parsed = jsonDecode(slimJson) as Map; + expect(parsed.containsKey('netlist'), isTrue); + final netlist = parsed['netlist'] as Map; + final modules = netlist['modules'] as Map; + expect(modules, isNotEmpty); + }); + + test('NetlistService is an OutputService and registers itself', () async { + final fb = _buildFilterBank(); + await fb.build(); + final netSvc = NetlistService(fb); + + expect(netSvc, isA()); + expect(ModuleServices.instance.lookup(), same(netSvc)); + expect(NetlistService.current, same(netSvc)); + + final summary = netSvc.toJson(); + expect(summary['version'], equals(netSvc.version)); + expect(summary['modules'], isList); + + ModuleServices.instance.reset(); + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('register false keeps NetlistService out of the registry', () async { + final fb = _buildFilterBank(); + await fb.build(); + ModuleServices.instance.reset(); + NetlistService(fb, register: false); + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('write() emits the full netlist JSON to a file', () async { + final fb = _buildFilterBank(); + await fb.build(); + final netSvc = NetlistService(fb, register: false); + + final dir = Directory.systemTemp.createTempSync('rohd_netlist_'); + try { + final path = '${dir.path}/netlist.json'; + netSvc.write(path); + expect(File(path).readAsStringSync(), equals(netSvc.json)); + } finally { + dir.deleteSync(recursive: true); + } + }); + }); + + // ── Group 8: Wire ID and structural invariants ───────────────────── + + group('wire ID and structural invariants', () { + test('all wire IDs are >= 2 (0 and 1 reserved for constants)', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final entry in mod.entries) { + final def = entry.value as Map; + // Check ports + for (final port in _ports(def).entries) { + final p = port.value as Map; + final bits = p['bits'] as List; + for (final bit in bits) { + if (bit is int) { + expect( + bit, + greaterThanOrEqualTo(2), + reason: 'Wire ID ${port.key} bit $bit should be >= 2', + ); + } + } + } + } + }); + + test(r'FilterBank contains $const cells for constant drivers', () async { + final json = await _synthToMap(_buildFilterBank()); + expect( + _hasCellType(json, r'$const'), + isTrue, + reason: r'FilterBank should have $const cells for constant values', + ); + }); + + test('passthrough buffers prevent input-output wire sharing', () async { + // A module whose output directly comes from an input should get a + // $buf for wire-ID isolation. + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + // Verify input and output port bits don't overlap in any definition + for (final entry in mod.entries) { + final def = entry.value as Map; + final ports = _ports(def); + final inputBits = {}; + final outputBits = {}; + for (final port in ports.entries) { + final p = port.value as Map; + final bits = (p['bits'] as List).whereType().toSet(); + final dir = p['direction'] as String; + if (dir == 'input') { + inputBits.addAll(bits); + } else if (dir == 'output') { + outputBits.addAll(bits); + } + } + expect( + inputBits.intersection(outputBits), + isEmpty, + reason: '${entry.key}: input and output ports should not share wire ' + 'IDs (passthrough buffer should break sharing)', + ); + } + }); + }); + + // ── Group 9: DCE (dead-cell elimination) verification ────────────── + + group('dead-cell elimination', () { + test('DCE enabled produces fewer cells than DCE disabled', () async { + final fbDce = _buildFilterBank(); + final jsonDce = await _synthToMap(fbDce); + int countCells(Map j) { + var total = 0; + for (final def in _modules(j).values) { + total += _cells(def as Map).length; + } + return total; + } + + final fbNoDce = _buildFilterBank(); + final jsonNoDce = await _synthToMap( + fbNoDce, + options: const NetlistOptions(enableDCE: false), + ); + + final dceCells = countCells(jsonDce); + final noDceCells = countCells(jsonNoDce); + expect( + dceCells, + lessThanOrEqualTo(noDceCells), + reason: 'DCE should remove at least as many cells as no-DCE', + ); + }); + + test(r'DCE removes floating $const cells', () async { + // With DCE disabled, there may be more $const cells + final fbDce = _buildFilterBank(); + final jsonDce = await _synthToMap(fbDce); + int countConstCells(Map j) { + var total = 0; + for (final def in _modules(j).values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + if ((c['type'] as String) == r'$const') { + total++; + } + } + } + return total; + } + + final fbNoDce = _buildFilterBank(); + final jsonNoDce = await _synthToMap( + fbNoDce, + options: const NetlistOptions(enableDCE: false), + ); + + expect( + countConstCells(jsonDce), + lessThanOrEqualTo(countConstCells(jsonNoDce)), + reason: r'DCE should not produce more $const cells than no-DCE', + ); + }); + }); + + // ── Group 10: Post-processing option combinations ────────────────── + + group('post-processing options', () { + test('collapseTransparentClusters produces valid netlist', () async { + final fb = _buildFilterBank(); + final json = await _synthToMap( + fb, + options: const NetlistOptions(collapseTransparentClusters: true), + ); + expect(_modules(json), isNotEmpty); + }); + }); + + // ── Group 11: Named constant signals ───────────────────────────── + + group('named constant signals', () { + test(r'Logic..gets(Const) produces $const cell and netname', () async { + final mod = _NamedConstModule(Logic(name: 'clk'), Logic(name: 'reset')); + final json = await _synthToMap(mod); + final mods = _modules(json); + + // Find the module definition for _NamedConstModule. + final modDef = mods.values.firstWhere( + (m) { + final def = m as Map; + return (def['cells'] as Map?)?.isNotEmpty ?? false; + }, + orElse: () => mods.values.first, + ) as Map; + + final netnames = _netnames(modDef); + final cells = _cells(modDef); + + // The signal 'myConst' should appear as a netname. + expect( + netnames.keys.any((n) => n.contains('myConst')), + isTrue, + reason: "Logic('myConst')..gets(Const(0)) should produce a netname", + ); + + // There should be a $const cell driving it. + expect( + cells.values.any( + (c) => (c as Map)['type'] == r'$const', + ), + isTrue, + reason: r'Named constant should have a $const driver cell', + ); + + // The netname bits should be integer wire IDs (not string literals). + final constNetname = + netnames.entries.firstWhere((e) => e.key.contains('myConst')); + final bits = (constNetname.value as Map)['bits'] as List; + expect( + bits.every((b) => b is int), + isTrue, + reason: 'Named constant netname should have integer wire IDs ' + r'(driven by a $const cell)', + ); + }); + }); +} diff --git a/test/netlist_test.dart b/test/netlist_test.dart new file mode 100644 index 000000000..a2c451034 --- /dev/null +++ b/test/netlist_test.dart @@ -0,0 +1,704 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_test.dart +// Tests for the netlist synthesizer: JSON structure, SynthBuilder, +// NetlistSynthesisResult, collectModuleEntries, NetlistOptions, +// and example-based smoke tests. +// +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +// --------------------------------------------------------------------------- +// Simple test modules (self-contained, no example imports needed) +// --------------------------------------------------------------------------- + +/// A trivial module that inverts a single-bit input. +class _InverterModule extends Module { + Logic get out => output('out'); + + _InverterModule(Logic inp) : super(name: 'inverter') { + inp = addInput('inp', inp); + final out = addOutput('out'); + out <= ~inp; + } +} + +/// A module that instantiates two sub-modules: an inverter and an AND gate. +class _CompositeModule extends Module { + Logic get out => output('out'); + + _CompositeModule(Logic a, Logic b) : super(name: 'composite') { + a = addInput('a', a); + b = addInput('b', b); + final out = addOutput('out'); + + final invA = _InverterModule(a); + out <= (_InverterModule(invA.out).out & b); + } +} + +/// A simple adder module with a configurable width. +class _AdderModule extends Module { + Logic get sum => output('sum'); + + _AdderModule(Logic a, Logic b, {int width = 8}) : super(name: 'adder') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final sum = addOutput('sum', width: width); + sum <= a + b; + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Detect whether running in JS (dart2js) environment. +const _isJS = identical(0, 0.0); + +/// Synthesize [top] and optionally write the produced JSON to [outPath]. +/// Returns the decoded modules map from the Yosys-format JSON. +Future> _synthesizeAndWrite( + Module top, + String outPath, +) async { + final synth = SynthBuilder(top, NetlistSynthesizer()); + final jsonStr = + (synth.synthesizer as NetlistSynthesizer).synthesizeToJson(top); + if (!_isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(jsonStr); + } + final decoded = jsonDecode(jsonStr) as Map; + return decoded['modules'] as Map; +} + +/// Build a FilterBank with default test parameters. +FilterBank _buildFilterBank({ + int dataWidth = 16, + int numTaps = 3, + List> coefficients = const [ + [1, 2, 1], + [1, -2, 1], + ], +}) { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(coefficients.length, + (ch) => FilterSample(dataWidth: dataWidth, name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients, + ); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // ── Example smoke tests ─────────────────────────────────────────────── + // + // Each example is synthesized once, verifying that the netlist is + // non-empty and (on VM) that the JSON file is written successfully. + + group('Example netlist smoke tests', () { + test('Counter', () async { + final counter = Counter(Logic(name: 'en'), Logic(name: 'reset'), + SimpleClockGenerator(10).clk); + await counter.build(); + + final modules = + await _synthesizeAndWrite(counter, 'build/Counter.rohd.json'); + expect(modules, isNotEmpty); + + final topMod = modules[counter.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'Counter should have cells'); + }); + + test('FIR filter', () async { + final fir = FirFilter( + Logic(name: 'en'), + Logic(name: 'resetB'), + SimpleClockGenerator(10).clk, + Logic(name: 'inputVal', width: 8), + [0, 0, 0, 1], + bitWidth: 8, + ); + await fir.build(); + + final modules = + await _synthesizeAndWrite(fir, 'build/FirFilter.rohd.json'); + expect(modules, isNotEmpty); + if (!_isJS) { + expect(File('build/FirFilter.rohd.json').existsSync(), isTrue); + } + }); + + test('LogicArray', () async { + final la = LogicArrayExample( + LogicArray([4], 8, name: 'arrayA'), + Logic(name: 'id', width: 3), + Logic(name: 'selectIndexValue', width: 8), + Logic(name: 'selectFromValue', width: 8), + ); + await la.build(); + + final modules = + await _synthesizeAndWrite(la, 'build/LogicArrayExample.rohd.json'); + expect(modules, isNotEmpty); + }); + + test('OvenModule', () async { + final oven = OvenModule( + Logic(name: 'button', width: 2), + Logic(name: 'reset'), + SimpleClockGenerator(10).clk, + ); + await oven.build(); + + final modules = + await _synthesizeAndWrite(oven, 'build/OvenModule.rohd.json'); + expect(modules, isNotEmpty); + }); + + test('TreeOfTwoInputModules', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser. + final json = NetlistSynthesizer().synthesizeToJson(tree); + expect(json, isNotEmpty); + if (!_isJS) { + final file = File('build/TreeOfTwoInputModules.rohd.json'); + await file.create(recursive: true); + await file.writeAsString(json); + } + }); + + test('FilterBank', () async { + final fb = _buildFilterBank(); + await fb.build(); + + final modules = + await _synthesizeAndWrite(fb, 'build/FilterBank.smoke.rohd.json'); + expect(modules, isNotEmpty); + expect(modules.length, greaterThan(1), + reason: 'FilterBank should have sub-module definitions'); + }); + }); + + // ── JSON structure ──────────────────────────────────────────────────── + + group('JSON structure', () { + test('synthesizeToJson returns valid JSON with modules key', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + expect(json, isNotEmpty); + final decoded = jsonDecode(json) as Map; + expect(decoded, contains('modules')); + }); + + test('top module is present with correct ports and top attribute', + () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + expect(modules, contains(mod.definitionName)); + + final topMod = modules[mod.definitionName] as Map; + + // Port directions + final ports = topMod['ports'] as Map; + expect(ports, contains('inp')); + expect(ports, contains('out')); + expect((ports['inp'] as Map)['direction'], equals('input')); + expect((ports['out'] as Map)['direction'], equals('output')); + + // Top attribute + final attrs = topMod['attributes'] as Map?; + expect(attrs, isNotNull); + expect(attrs!['top'], equals(1)); + }); + + test('port bit widths match module interface', () async { + const width = 16; + final mod = _AdderModule( + Logic(name: 'a', width: width), Logic(name: 'b', width: width), + width: width); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + final topMod = modules[mod.definitionName] as Map; + final ports = topMod['ports'] as Map; + + expect((ports['a'] as Map)['bits'], hasLength(width)); + expect((ports['b'] as Map)['bits'], hasLength(width)); + expect((ports['sum'] as Map)['bits'], hasLength(width)); + }); + + test('cells have connections in default mode', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + final topMod = modules[mod.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + + final hasConnections = cells.values.any((cell) { + final c = cell as Map; + final conns = c['connections'] as Map?; + return conns != null && conns.isNotEmpty; + }); + expect(hasConnections, isTrue); + }); + + test('generateCombinedJson and synthesizeToJson produce same module keys', + () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + + final fromCombined = synthesizer.generateCombinedJson(synth, mod); + final fromConvenience = NetlistSynthesizer().synthesizeToJson(mod); + + final combinedModules = + (jsonDecode(fromCombined) as Map)['modules'] as Map; + final convenienceModules = + (jsonDecode(fromConvenience) as Map)['modules'] as Map; + expect(combinedModules.keys.toSet(), + equals(convenienceModules.keys.toSet())); + }); + }); + + // ── SynthBuilder ────────────────────────────────────────────────────── + + group('SynthBuilder', () { + test('synthesisResults are NetlistSynthesisResult instances', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + expect(synth.synthesisResults, isNotEmpty); + for (final result in synth.synthesisResults) { + expect(result, isA()); + } + }); + + test('composite module includes sub-module definitions', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + final names = + synth.synthesisResults.map((r) => r.instanceTypeName).toSet(); + expect(names, contains(mod.definitionName)); + expect(synth.synthesisResults.length, greaterThan(1)); + }); + + test('toSynthFileContents produces valid JSON per definition', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final fileContents = + SynthBuilder(mod, NetlistSynthesizer()).getSynthFileContents(); + expect(fileContents, isNotEmpty); + for (final fc in fileContents) { + expect(fc.name, isNotEmpty); + expect(jsonDecode(fc.contents), isA>()); + } + }); + }); + + // ── NetlistSynthesisResult maps ─────────────────────────────────────── + + group('NetlistSynthesisResult maps', () { + test('ports map has direction and bits for each port', () async { + final mod = + _AdderModule(Logic(name: 'a', width: 8), Logic(name: 'b', width: 8)); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults + .whereType() + .firstWhere((r) => r.module == mod); + + for (final portName in ['a', 'b', 'sum']) { + expect(result.ports, contains(portName)); + final port = result.ports[portName]!; + expect(port, contains('direction')); + expect(port, contains('bits')); + } + }); + + test('netnames map is populated', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults + .whereType() + .firstWhere((r) => r.module == mod); + expect(result.netnames, isNotEmpty); + }); + }); + + // ── collectModuleEntries ────────────────────────────────────────────── + + group('collectModuleEntries', () { + test('gathers results with correct structure and top attribute', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + final modulesMap = NetlistPasses.collectModuleEntries( + synth.synthesisResults, + topModule: mod); + + expect(modulesMap, contains(mod.definitionName)); + expect(modulesMap.length, greaterThan(1)); + + // Top attribute + final topAttrs = modulesMap[mod.definitionName]!['attributes']! + as Map; + expect(topAttrs['top'], equals(1)); + + // Every entry has the expected sections + for (final entry in modulesMap.values) { + expect(entry, contains('ports')); + expect(entry, contains('cells')); + expect(entry, contains('netnames')); + } + }); + }); + + // ── buildModulesMap ─────────────────────────────────────────────────── + + group('buildModulesMap', () { + test('returns map with all definitions and expected sections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + final modulesMap = synthesizer.buildModulesMap(synth, mod); + + expect(modulesMap, contains(mod.definitionName)); + expect(modulesMap.length, greaterThan(1)); + for (final modEntry in modulesMap.entries) { + final data = modEntry.value; + expect(data, contains('ports'), reason: modEntry.key); + expect(data, contains('cells'), reason: modEntry.key); + expect(data, contains('netnames'), reason: modEntry.key); + } + }); + }); + + // ── NetlistOptions ─────────────────────────────────────────────────── + + group('NetlistOptions', () { + test('slimMode omits cell connections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final slimSynth = + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)); + final json = slimSynth.synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + + for (final modEntry in modules.values) { + final data = modEntry as Map; + final cells = data['cells'] as Map? ?? {}; + for (final cell in cells.values) { + final c = cell as Map; + final conns = c['connections'] as Map?; + if (conns != null) { + expect(conns, isEmpty, reason: 'slim mode should omit connections'); + } + } + } + }); + }); + + // ── FilterBank (multi-channel, dedup, loopback) ─────────────────────── + + group('FilterBank netlist', () { + test('produces valid netlist with multiple module definitions', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final modules = + await _synthesizeAndWrite(mod, 'build/FilterBank.rohd.json'); + expect(modules, isNotEmpty); + expect(modules.length, greaterThan(1), + reason: 'FilterBank should have sub-module definitions'); + + // Top module should have cells + final topMod = modules[mod.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'FilterBank should have cells'); + }); + + test('FilterChannel definitions are deduplicated', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final json = NetlistSynthesizer().synthesizeToJson(mod); + final parsed = jsonDecode(json) as Map; + final modules = parsed['modules'] as Map; + final channelDefs = + modules.keys.where((k) => k.contains('FilterChannel')).toList(); + // Two channels with different coefficients should produce + // separate definitions (not fully deduplicated). + expect(channelDefs, isNotEmpty, + reason: 'FilterChannel definitions should be present'); + }); + + test('all module entries have ports, cells, and netnames', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + final modulesMap = synthesizer.buildModulesMap(synth, mod); + + for (final entry in modulesMap.entries) { + final data = entry.value; + expect(data, contains('ports'), reason: '${entry.key} missing ports'); + expect(data, contains('cells'), reason: '${entry.key} missing cells'); + expect(data, contains('netnames'), + reason: '${entry.key} missing netnames'); + } + }); + + test('ports have correct directions on sub-modules', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + + for (final result + in synth.synthesisResults.whereType()) { + for (final port in result.ports.entries) { + final dir = port.value['direction']! as String; + expect(['input', 'output', 'inout'], contains(dir), + reason: '${result.instanceTypeName}.${port.key} ' + 'has invalid direction'); + } + } + }); + }); + + // ----------------------------------------------------------------------- + // Bit-range compression & compact JSON + // ----------------------------------------------------------------------- + group('Bit-range compression', () { + test('compressBitRanges option produces range strings in JSON', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthCompressed = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressed = synthCompressed.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = synthNormal.synthesizeToJson(mod); + + // Compressed should be shorter. + expect(jsonCompressed.length, lessThan(jsonNormal.length)); + + // Both should parse as valid JSON with the same module keys. + final decodedCompressed = jsonDecode(jsonCompressed) as Map; + final decodedNormal = jsonDecode(jsonNormal) as Map; + expect( + (decodedCompressed['modules'] as Map).keys.toSet(), + equals((decodedNormal['modules'] as Map).keys.toSet()), + ); + + // Compressed JSON should contain range strings like "2:9". + expect(jsonCompressed, contains(RegExp(r'"\d+:\d+"'))); + // Normal JSON should NOT contain range strings. + expect(jsonNormal, isNot(contains(RegExp(r'"\d+:\d+"')))); + }); + + test('compressed ranges preserve constant bit strings', () async { + // Use a module that produces constant "0"/"1" bits in the netlist. + final a = Logic(name: 'a'); + final mod = _InverterModule(a); + await mod.build(); + + final synth = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final json = synth.synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + + // Should still be valid JSON. + expect(decoded['modules'], isNotNull); + }); + + test('compactJson option removes indentation', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthCompact = NetlistSynthesizer( + options: const NetlistOptions(compactJson: true), + ); + final jsonCompact = synthCompact.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = synthNormal.synthesizeToJson(mod); + + // Compact should be shorter. + expect(jsonCompact.length, lessThan(jsonNormal.length)); + // Compact should have no leading whitespace lines. + expect(jsonCompact, isNot(contains('\n '))); + // Both should be valid JSON with the same module keys. + final decodedCompact = jsonDecode(jsonCompact) as Map; + final decodedNormal = jsonDecode(jsonNormal) as Map; + expect( + (decodedCompact['modules'] as Map).keys.toSet(), + equals((decodedNormal['modules'] as Map).keys.toSet()), + ); + }); + + test('both options together produce smallest output', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthBoth = NetlistSynthesizer( + options: const NetlistOptions( + compressBitRanges: true, + compactJson: true, + ), + ); + final jsonBoth = synthBoth.synthesizeToJson(mod); + + final synthCompressOnly = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressOnly = synthCompressOnly.synthesizeToJson(mod); + + final synthCompactOnly = NetlistSynthesizer( + options: const NetlistOptions(compactJson: true), + ); + final jsonCompactOnly = synthCompactOnly.synthesizeToJson(mod); + + expect(jsonBoth.length, lessThan(jsonCompressOnly.length)); + expect(jsonBoth.length, lessThan(jsonCompactOnly.length)); + }); + + test( + 'compressed FilterBank round-trips: range strings expand to ' + 'same bit IDs as uncompressed', () async { + final mod = _buildFilterBank(); + await mod.build(); + + // Generate both compressed and uncompressed. + final synthNormal = NetlistSynthesizer(); + final jsonNormal = synthNormal.synthesizeToJson(mod); + final normalModules = (jsonDecode(jsonNormal) + as Map)['modules'] as Map; + + final synthCompressed = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressed = synthCompressed.synthesizeToJson(mod); + final compressedModules = (jsonDecode(jsonCompressed) + as Map)['modules'] as Map; + + // Compressed should be smaller. + expect(jsonCompressed.length, lessThan(jsonNormal.length)); + + // Same module keys. + expect(compressedModules.keys.toSet(), normalModules.keys.toSet()); + + // Verify compressed JSON contains range strings. + expect(jsonCompressed, contains(RegExp(r'"\d+:\d+"'))); + + // For each module, expand compressed port bits and compare to normal. + for (final modName in normalModules.keys) { + final normalPorts = (normalModules[modName] + as Map)['ports'] as Map?; + final compPorts = (compressedModules[modName] + as Map)['ports'] as Map?; + if (normalPorts == null || compPorts == null) { + continue; + } + + for (final portName in normalPorts.keys) { + final normalBits = + (normalPorts[portName] as Map)['bits'] as List; + final compBits = + (compPorts[portName] as Map)['bits'] as List; + + // Expand any range strings in the compressed bits. + final expanded = []; + for (final b in compBits) { + if (b is String && b.contains(':')) { + final parts = b.split(':'); + final start = int.parse(parts[0]); + final end = int.parse(parts[1]); + for (var i = start; i <= end; i++) { + expanded.add(i); + } + } else { + expanded.add(b); + } + } + + expect(expanded, normalBits, + reason: 'round-trip failed for $modName.$portName'); + } + } + }); + }); +} diff --git a/test/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..773989964 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -37,11 +37,8 @@ class RequestInterface extends PairInterface { final int numWd; RequestInterface({this.numWd = 2}) { for (var wd = 0; wd < numWd; wd++) { - writeDatas.add(addSubInterface( - 'write_data$wd', - DataInterface(), - uniquify: withPrefix('wd$wd'), - )); + writeDatas.add(addSubInterface('write_data$wd', DataInterface(), + uniquify: withPrefix('wd$wd'))); } } @@ -52,12 +49,8 @@ class RequestInterface extends PairInterface { class ResponseInterface extends PairInterface { late final DataInterface readData; ResponseInterface() : super() { - readData = addSubInterface( - 'read_data', - DataInterface(), - reverse: true, - uniquify: withPrefix('rd'), - ); + readData = addSubInterface('read_data', DataInterface(), + reverse: true, uniquify: withPrefix('rd')); } @override ResponseInterface clone() => ResponseInterface(); @@ -67,16 +60,10 @@ class PCInterface extends PairInterface { late final RequestInterface req; late final ResponseInterface rsp; PCInterface() { - req = addSubInterface( - 'req', - RequestInterface(), - uniquify: withSuffix('req'), - ); - rsp = addSubInterface( - 'rsp', - ResponseInterface(), - uniquify: withSuffix('rsp'), - ); + req = + addSubInterface('req', RequestInterface(), uniquify: withSuffix('req')); + rsp = addSubInterface('rsp', ResponseInterface(), + uniquify: withSuffix('rsp')); } @override PCInterface clone() => PCInterface(); @@ -88,26 +75,18 @@ class Provider extends Module { clk = addInput('clk', clk); reset = addInput('reset', reset); reqIntf = reqIntf.clone() - ..pairConnectIO( - this, - reqIntf, - PairRole.provider, - uniquify: withSuffix('req'), - ); + ..pairConnectIO(this, reqIntf, PairRole.provider, + uniquify: withSuffix('req')); rspIntf = rspIntf.clone() - ..pairConnectIO( - this, - rspIntf, - PairRole.provider, - uniquify: withSuffix('rsp'), - ); + ..pairConnectIO(this, rspIntf, PairRole.provider, + uniquify: withSuffix('rsp')); reqIntf.writeDatas[0].valid <= Const(1); reqIntf.writeDatas[1].valid <= Const(1); Sequential(clk, reset: reset, [ reqIntf.writeDatas[0].data.incr(val: 2), - reqIntf.writeDatas[1].data.incr(), + reqIntf.writeDatas[1].data.incr() ]); } } @@ -118,19 +97,11 @@ class Consumer extends Module { clk = addInput('clk', clk); reset = addInput('reset', reset); reqIntf = reqIntf.clone() - ..pairConnectIO( - this, - reqIntf, - PairRole.consumer, - uniquify: withSuffix('req'), - ); + ..pairConnectIO(this, reqIntf, PairRole.consumer, + uniquify: withSuffix('req')); rspIntf = rspIntf.clone() - ..pairConnectIO( - this, - rspIntf, - PairRole.consumer, - uniquify: withSuffix('rsp'), - ); + ..pairConnectIO(this, rspIntf, PairRole.consumer, + uniquify: withSuffix('rsp')); rspIntf.readData.valid <= Const(1); @@ -138,7 +109,7 @@ class Consumer extends Module { rspIntf.readData.data < reqIntf.writeDatas.map((e) => e.data).reduce((a, b) => a + b), reqIntf.writeDatas[0].ready < 1, - reqIntf.writeDatas[1].ready < 1, + reqIntf.writeDatas[1].ready < 1 ]); } } @@ -173,10 +144,10 @@ void main() { Vector({}, {}), Vector({}, {'rsp_data': 3}), Vector({}, {'rsp_data': 6}), - Vector({}, {'rsp_data': 9}), + 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..9648b3f50 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -25,10 +25,8 @@ class DataInterface extends PairInterface { : super( portsFromProvider: [Logic.port('data', 32), Logic.port('valid')], portsFromConsumer: [Logic.port('ready')], - modify: (original) => [ - if (prefix != null) prefix, - original, - ].join('_')); + modify: (original) => + [if (prefix != null) prefix, original].join('_')); @override DataInterface clone() => DataInterface(prefix: prefix); } @@ -87,7 +85,7 @@ class Provider extends Module { Sequential(clk, reset: reset, [ reqIntf.writeDatas[0].data.incr(val: 2), - reqIntf.writeDatas[1].data.incr(), + reqIntf.writeDatas[1].data.incr() ]); } } @@ -108,7 +106,7 @@ class Consumer extends Module { rspIntf.readData.data < reqIntf.writeDatas.map((e) => e.data).reduce((a, b) => a + b), reqIntf.writeDatas[0].ready < 1, - reqIntf.writeDatas[1].ready < 1, + reqIntf.writeDatas[1].ready < 1 ]); } } @@ -143,10 +141,10 @@ void main() { Vector({}, {}), Vector({}, {'rsp_data': 3}), Vector({}, {'rsp_data': 6}), - Vector({}, {'rsp_data': 9}), + 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..c18bd3fb2 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -40,9 +40,7 @@ class DelaySignal extends Module { Sequential(clk, [ If.block([ Iff(en, zList), - Else([ - out < 0, - ]) + Else([out < 0]) ]) ]); } @@ -76,23 +74,16 @@ class ShorthandSeqModule extends Module { final clk = SimpleClockGenerator(10).clk; - Sequential( - clk, - [ - piOut.incr(), - pdOut.decr(), - maOut.mulAssign(2), - daOut.divAssign(2), - ], - reset: reset, - resetValues: { - piOut: initialVal, - pdOut: initialVal, - maOut: initialVal, - daOut: initialVal, - if (useArrays && doubleResetError) daOut.elements[0]: initialVal, - }, - ); + Sequential(clk, + [piOut.incr(), pdOut.decr(), maOut.mulAssign(2), daOut.divAssign(2)], + reset: reset, + resetValues: { + piOut: initialVal, + pdOut: initialVal, + maOut: initialVal, + daOut: initialVal, + if (useArrays && doubleResetError) daOut.elements[0]: initialVal + }); } } @@ -108,20 +99,10 @@ class SeqResetValTypes extends Module { final d = addOutput('d', width: 8); // none Sequential( - clk, - reset: reset, - resetValues: { - a: 7, - b: LogicValue.ofInt(12, 8), - c: i, - }, - [ - a < 8, - b < LogicValue.ofInt(13, 8), - c < i + 1, - d < 4, - ], - ); + clk, + reset: reset, + resetValues: {a: 7, b: LogicValue.ofInt(12, 8), c: i}, + [a < 8, b < LogicValue.ofInt(13, 8), c < i + 1, d < 4]); } } @@ -131,11 +112,7 @@ class NegedgeTriggeredSeq extends Module { final b = addOutput('b'); final clk = SimpleClockGenerator(10).clk; - Sequential.multi( - [], - negedgeTriggers: [~clk], - [b < a], - ); + Sequential.multi([], negedgeTriggers: [~clk], [b < a]); } } @@ -145,12 +122,7 @@ class BothTriggeredSeq extends Module { final b = addOutput('b', width: 8); final clk = SimpleClockGenerator(10).clk; - Sequential.multi( - [clk], - reset: reset, - negedgeTriggers: [clk], - [b < b + 1], - ); + Sequential.multi([clk], reset: reset, negedgeTriggers: [clk], [b < b + 1]); } } @@ -160,10 +132,7 @@ void main() { }); test('simple pipeline', () async { - final dut = DelaySignal( - Logic(), - Logic(width: 4), - ); + final dut = DelaySignal(Logic(), Logic(width: 4)); await dut.build(); final vectors = [ @@ -178,7 +147,7 @@ void main() { Vector({'inputVal': 8, 'en': 1}, {'out': 0}), Vector({'inputVal': 9, 'en': 1}, {'out': 3}), Vector({}, {'out': 4}), - Vector({}, {'out': 5}), + Vector({}, {'out': 5}) ]; await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); @@ -198,7 +167,7 @@ void main() { Vector( {'reset': 0}, {'piOut': 16, 'pdOut': 16, 'maOut': 16, 'daOut': 16}), Vector( - {'reset': 0}, {'piOut': 17, 'pdOut': 15, 'maOut': 32, 'daOut': 8}), + {'reset': 0}, {'piOut': 17, 'pdOut': 15, 'maOut': 32, 'daOut': 8}) ]; // await SimCompare.checkFunctionalVector(mod, vectors); @@ -220,17 +189,14 @@ void main() { }); test('reset and value types', () async { - final dut = SeqResetValTypes( - Logic(), - Logic(width: 8), - ); + final dut = SeqResetValTypes(Logic(), Logic(width: 8)); await dut.build(); final vectors = [ Vector({'reset': 1, 'i': 17}, {}), Vector({'reset': 1}, {'a': 7, 'b': 12, 'c': 17, 'd': 0}), Vector({'reset': 0}, {'a': 7, 'b': 12, 'c': 17, 'd': 0}), - Vector({'reset': 0}, {'a': 8, 'b': 13, 'c': 18, 'd': 4}), + Vector({'reset': 0}, {'a': 8, 'b': 13, 'c': 18, 'd': 4}) ]; await SimCompare.checkFunctionalVector(dut, vectors); @@ -241,13 +207,13 @@ 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 = [ Vector({'a': 0}, {}), Vector({'a': 1}, {'b': 0}), - Vector({'a': 0}, {'b': 1}), + Vector({'a': 0}, {'b': 1}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -264,7 +230,7 @@ void main() { Vector({'reset': 0}, {'b': 0}), Vector({}, {'b': 2}), Vector({}, {'b': 4}), - Vector({}, {'b': 6}), + Vector({}, {'b': 6}) ]; await SimCompare.checkFunctionalVector(mod, vectors); diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart new file mode 100644 index 000000000..c5c69dd87 --- /dev/null +++ b/test/signal_registry_test.dart @@ -0,0 +1,366 @@ +// 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/examples/filter_bank_modules.dart'; +import 'package:rohd/src/utilities/namer.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.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('filter_bank hierarchy', () { + test('submodule canonical names work independently', () async { + const dataWidth = 16; + const numTaps = 3; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + + expect(dut.namer.signalNameOfBest([dut.input('clk')]), equals('clk')); + expect(dut.namer.signalNameOfBest([dut.output('done')]), equals('done')); + + for (final sub in dut.subModules) { + for (final entry in sub.inputs.entries) { + final name = sub.namer.signalNameOfBest([entry.value]); + expect(name, isNotEmpty); + } + } + }); + }); + + 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/slim_connected_port_test.dart b/test/slim_connected_port_test.dart new file mode 100644 index 000000000..be809519d --- /dev/null +++ b/test/slim_connected_port_test.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class SimpleModule extends Module { + SimpleModule(Logic a) : super(name: 'SimpleTest') { + a = addInput('a', a, width: 8); + final b = addOutput('b', width: 8); + b <= ~a; + addOutput('unused_port', width: 4); // not connected internally + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + test('slim ports carry connected attribute', () async { + final a = Logic(width: 8, name: 'a'); + final mod = SimpleModule(a); + await mod.build(); + final netSvc = NetlistService(mod); + + final slim = netSvc.slimJson; + + final parsed = json.decode(slim) as Map; + final netlist = parsed['netlist'] as Map; + final modules = netlist['modules'] as Map; + + // Find the SimpleTest module + // The module name may be the type name or uniquified; find it + final simpleTestKey = modules.keys.firstWhere( + (k) => k.contains('SimpleTest'), + orElse: () => modules.keys.first, + ); + final simpleTest = modules[simpleTestKey] as Map; + + final ports = simpleTest['ports'] as Map; + + // Port 'a' is connected internally (feeds ~a) + final portA = ports['a'] as Map; + expect( + portA['connected'], + isTrue, + reason: 'Port a should be marked connected', + ); + + // Port 'b' is connected internally (output of ~a) + final portB = ports['b'] as Map; + expect( + portB['connected'], + isTrue, + reason: 'Port b should be marked connected', + ); + + // Port 'unused_port' is NOT connected internally + final portUnused = ports['unused_port'] as Map; + expect( + portUnused.containsKey('connected'), + isFalse, + reason: 'unused_port should not have connected attribute', + ); + }); +} diff --git a/test/slim_full_canonical_test.dart b/test/slim_full_canonical_test.dart new file mode 100644 index 000000000..c10bbff93 --- /dev/null +++ b/test/slim_full_canonical_test.dart @@ -0,0 +1,171 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// slim_full_canonical_test.dart +// Validates that slim and full synthesis produce identical cell sets. + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + test('slim and full produce identical cell keys for FilterBank', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: 3, + dataWidth: 16, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + final netSvc = NetlistService(dut); + + // 1. Get slim JSON + final slimJsonStr = netSvc.slimJson; + + final slimUnified = jsonDecode(slimJsonStr) as Map; + final slimNetlist = slimUnified['netlist'] as Map; + final slimModules = slimNetlist['modules'] as Map; + + expect(slimModules, isNotEmpty, reason: 'No slim modules found'); + + // 2. For each slim module, fetch full and compare cell keys + var modulesTested = 0; + final mismatches = []; + + for (final moduleKey in slimModules.keys) { + final slimMod = slimModules[moduleKey] as Map; + final slimCells = slimMod['cells'] as Map? ?? {}; + + // Fetch full data + final fullJsonStr = netSvc.moduleJson(moduleKey); + final fullJson = jsonDecode(fullJsonStr) as Map; + if (fullJson.containsKey('status')) { + mismatches.add('$moduleKey: full fetch returned not_found'); + continue; + } + + // moduleJson returns {creator, version, modules: {name: modData}}. + final modulesMap = + (fullJson['modules'] as Map?) ?? fullJson; + final fullMod = modulesMap[moduleKey] as Map?; + if (fullMod == null) { + // Try the first key in case the definition name differs. + final firstKey = modulesMap.keys.first; + final altMod = modulesMap[firstKey] as Map?; + if (altMod == null) { + mismatches.add('$moduleKey: no module data in full response'); + continue; + } + _compareCells(moduleKey, slimCells, altMod, mismatches); + } else { + _compareCells(moduleKey, slimCells, fullMod, mismatches); + } + modulesTested++; + } + + // Report + if (mismatches.isNotEmpty) { + fail( + 'Cell key mismatches found in $modulesTested modules:\n' + '${mismatches.join('\n')}', + ); + } + + // Sanity: we tested a reasonable number of modules + expect(modulesTested, greaterThan(0), reason: 'No modules were tested'); + }); +} + +void _compareCells( + String moduleKey, + Map slimCells, + Map fullMod, + List mismatches, +) { + final fullCells = fullMod['cells'] as Map? ?? {}; + + final slimKeys = slimCells.keys.toList(); + final fullKeys = fullCells.keys.toList(); + + if (slimKeys.length != fullKeys.length) { + mismatches.add( + '$moduleKey: cell count differs — ' + 'slim=${slimKeys.length}, full=${fullKeys.length}', + ); + // Show which keys differ + final slimOnly = slimKeys.toSet().difference(fullKeys.toSet()); + final fullOnly = fullKeys.toSet().difference(slimKeys.toSet()); + if (slimOnly.isNotEmpty) { + mismatches.add(' slim-only: $slimOnly'); + } + if (fullOnly.isNotEmpty) { + mismatches.add(' full-only: $fullOnly'); + } + return; + } + + // Check ordering matches + for (var i = 0; i < slimKeys.length; i++) { + if (slimKeys[i] != fullKeys[i]) { + mismatches.add( + '$moduleKey: cell key ordering differs at index $i — ' + 'slim="${slimKeys[i]}", full="${fullKeys[i]}"', + ); + return; + } + } + + // Check cell types match + for (final key in slimKeys) { + final slimCell = slimCells[key] as Map; + final fullCell = fullCells[key] as Map; + final slimType = slimCell['type'] as String?; + final fullType = fullCell['type'] as String?; + if (slimType != fullType) { + mismatches.add( + '$moduleKey: cell "$key" type differs — ' + 'slim="$slimType", full="$fullType"', + ); + } + } + + // Verify slim cells DON'T have connections + for (final key in slimKeys) { + final slimCell = slimCells[key] as Map; + if (slimCell.containsKey('connections')) { + mismatches.add( + '$moduleKey: slim cell "$key" has connections ' + '(should be stripped)', + ); + } + } + + // Verify full cells DO have connections + for (final key in fullKeys) { + final fullCell = fullCells[key] as Map; + if (!fullCell.containsKey('connections')) { + mismatches.add('$moduleKey: full cell "$key" missing connections'); + } + } +} diff --git a/test/slim_incremental_equivalence_test.dart b/test/slim_incremental_equivalence_test.dart new file mode 100644 index 000000000..3aa4e298b --- /dev/null +++ b/test/slim_incremental_equivalence_test.dart @@ -0,0 +1,301 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// slim_incremental_equivalence_test.dart +// Validates that assembling full data from slim + per-module fetches +// produces the same result as pulling the full netlist in one shot. + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart' as ex; +import '../example/filter_bank.dart'; +import '../example/fir_filter.dart'; +import '../example/oven_fsm.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + /// Builds [module] with netlist synthesis, then verifies that + /// reassembling the full netlist from slim + per-module fetches + /// produces a result equivalent to [toFullJson]. + /// + /// For each module definition in the slim netlist: + /// 1. Cell keys, types, and ordering must match the full netlist. + /// 2. Port definitions (direction, bit indices) must match. + /// 3. Fetching full data via [moduleJson] adds connections + /// that exactly match those in the full netlist. + Future validateSlimIncrementalEquivalence(Module module) async { + await module.build(); + final netSvc = NetlistService(module); + + // ── Pull full netlist in one shot ───────────────────────────── + final fullJsonStr = netSvc.json; + final fullNetlist = jsonDecode(fullJsonStr) as Map; + final fullModules = fullNetlist['modules'] as Map; + + // ── Pull slim netlist ───────────────────────────────────────── + final slimJsonStr = netSvc.slimJson; + final slimUnified = jsonDecode(slimJsonStr) as Map; + final slimNetlist = slimUnified['netlist'] as Map; + final slimModules = slimNetlist['modules'] as Map; + + // ── Same set of module definition keys ──────────────────────── + expect( + slimModules.keys.toSet(), + equals(fullModules.keys.toSet()), + reason: 'Slim and full should have identical module keys', + ); + + // ── Per-module comparison ───────────────────────────────────── + final errors = []; + + for (final moduleKey in fullModules.keys) { + final fullMod = fullModules[moduleKey] as Map; + final slimMod = slimModules[moduleKey] as Map; + + final fullCells = fullMod['cells'] as Map? ?? {}; + final slimCells = slimMod['cells'] as Map? ?? {}; + + // ── Cell keys and ordering ────────────────────────────────── + final fullCellKeys = fullCells.keys.toList(); + final slimCellKeys = slimCells.keys.toList(); + if (!_listsEqual(fullCellKeys, slimCellKeys)) { + errors.add( + '$moduleKey: cell keys differ — ' + 'full=$fullCellKeys, slim=$slimCellKeys', + ); + continue; // Skip deeper checks for this module + } + + // ── Cell types match ──────────────────────────────────────── + for (final cellKey in fullCellKeys) { + final fullCell = fullCells[cellKey] as Map; + final slimCell = slimCells[cellKey] as Map; + if (fullCell['type'] != slimCell['type']) { + errors.add( + '$moduleKey.$cellKey: type mismatch — ' + 'full="${fullCell['type']}", slim="${slimCell['type']}"', + ); + } + } + + // ── Port definitions match ────────────────────────────────── + final fullPorts = fullMod['ports'] as Map? ?? {}; + final slimPorts = slimMod['ports'] as Map? ?? {}; + if (!_listsEqual(fullPorts.keys.toList(), slimPorts.keys.toList())) { + errors.add( + '$moduleKey: port keys differ — ' + 'full=${fullPorts.keys.toList()}, ' + 'slim=${slimPorts.keys.toList()}', + ); + } else { + for (final portKey in fullPorts.keys) { + final fullPort = fullPorts[portKey] as Map; + final slimPort = slimPorts[portKey] as Map; + if (fullPort['direction'] != slimPort['direction']) { + errors.add('$moduleKey port $portKey: direction mismatch'); + } + final fullBits = fullPort['bits'] as List?; + final slimBits = slimPort['bits'] as List?; + if (!_listsEqual(fullBits ?? [], slimBits ?? [])) { + errors.add( + '$moduleKey port $portKey: bits mismatch — ' + 'full=$fullBits, slim=$slimBits', + ); + } + } + } + + // ── Slim cells must NOT have connections ──────────────────── + for (final cellKey in slimCellKeys) { + final slimCell = slimCells[cellKey] as Map; + if (slimCell.containsKey('connections')) { + errors.add( + '$moduleKey.$cellKey: slim cell has connections ' + '(should be stripped)', + ); + } + } + + // ── Full cells must have connections ──────────────────────── + for (final cellKey in fullCellKeys) { + final fullCell = fullCells[cellKey] as Map; + if (!fullCell.containsKey('connections')) { + errors.add('$moduleKey.$cellKey: full cell missing connections'); + } + } + + // ── Fetch full data via moduleJson ───────────────────────── + // This is the incremental-loading contract: for EVERY module + // in the slim netlist, fetching full data must recover the + // exact connections present in the one-shot full netlist. + final fetchedStr = netSvc.moduleJson(moduleKey); + final fetchedJson = jsonDecode(fetchedStr) as Map; + if (fetchedJson.containsKey('status')) { + errors.add('$moduleKey: moduleJson returned not_found'); + continue; + } + + // The fetched result is {"creator":..., "modules": {key: data}}. + final fetchedModules = + fetchedJson['modules'] as Map? ?? fetchedJson; + final fetchedMod = (fetchedModules[moduleKey] ?? + fetchedModules.values.first) as Map; + + final fetchedCells = fetchedMod['cells'] as Map? ?? {}; + + // ── Fetched cell keys must match full ─────────────────────── + if (!_listsEqual(fetchedCells.keys.toList(), fullCellKeys)) { + errors.add( + '$moduleKey: fetched cell keys differ from full — ' + 'fetched=${fetchedCells.keys.toList()}, ' + 'full=$fullCellKeys', + ); + continue; + } + + // ── Fetched connections must match full exactly ───────────── + for (final cellKey in fullCellKeys) { + final fullCell = fullCells[cellKey] as Map; + final fetchedCell = fetchedCells[cellKey] as Map; + + final fullConns = + fullCell['connections'] as Map? ?? {}; + final fetchedConns = + fetchedCell['connections'] as Map? ?? {}; + + if (!_connectionsEqual(fullConns, fetchedConns)) { + errors.add( + '$moduleKey.$cellKey: connections mismatch — ' + 'full=$fullConns, fetched=$fetchedConns', + ); + } + } + + // ── Fetched ports must match full ─────────────────────────── + final fetchedPorts = fetchedMod['ports'] as Map? ?? {}; + for (final portKey in fullPorts.keys) { + final fullPort = fullPorts[portKey] as Map; + final fetchedPort = fetchedPorts[portKey] as Map?; + if (fetchedPort == null) { + errors.add('$moduleKey port $portKey: missing in fetched data'); + continue; + } + if (fullPort['direction'] != fetchedPort['direction']) { + errors.add( + '$moduleKey port $portKey: direction mismatch ' + 'in fetched data', + ); + } + final fullBits = fullPort['bits'] as List?; + final fetchedBits = fetchedPort['bits'] as List?; + if (!_listsEqual(fullBits ?? [], fetchedBits ?? [])) { + errors.add( + '$moduleKey port $portKey: bits mismatch — ' + 'full=$fullBits, fetched=$fetchedBits', + ); + } + } + } + + // ── Report ──────────────────────────────────────────────────── + if (errors.isNotEmpty) { + fail('Slim incremental equivalence errors:\n${errors.join('\n')}'); + } + } + + test('Counter: slim + incremental fetch == full', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = ex.Counter(en, reset, clk); + await validateSlimIncrementalEquivalence(counter); + }); + + test('FIR filter: slim + incremental fetch == full', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await validateSlimIncrementalEquivalence(fir); + }); + + test('OvenModule: slim + incremental fetch == full', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final oven = OvenModule(button, reset, clk); + await validateSlimIncrementalEquivalence(oven); + }); + + test('FilterBank: slim + incremental fetch == full', () async { + const dataWidth = 16; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: 3, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await validateSlimIncrementalEquivalence(dut); + }); +} + +/// Deep-compare two lists element by element. +bool _listsEqual(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +/// Compare two connection maps: {portName: [bit indices]}. +/// +/// All bit indices are numeric IDs; order within each port's list matters +/// because it encodes the wire mapping. +bool _connectionsEqual(Map a, Map b) { + if (a.length != b.length) { + return false; + } + for (final key in a.keys) { + if (!b.containsKey(key)) { + return false; + } + final aBits = a[key] as List?; + final bBits = b[key] as List?; + if (aBits == null && bBits == null) { + continue; + } + if (aBits == null || bBits == null) { + return false; + } + if (!_listsEqual(aBits, bBits)) { + return false; + } + } + return true; +} diff --git a/test/struct_port_pruning_test.dart b/test/struct_port_pruning_test.dart new file mode 100644 index 000000000..55a1bdb82 --- /dev/null +++ b/test/struct_port_pruning_test.dart @@ -0,0 +1,143 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// struct_port_pruning_test.dart +// Verifies that struct port elements on submodules are not incorrectly +// pruned during SV synthesis. Exercises the `submoduleOutputSynths` / +// `submoduleInputSynths` fix in `_pruneUnused`. +// +// 2026 April 17 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +// ── Struct definition ────────────────────────────────────────── + +class PairStruct extends LogicStructure { + PairStruct({Logic? a, Logic? b, super.name = 'pair'}) + : super([a ?? Logic(name: 'a'), b ?? Logic(name: 'b')]); + + @override + PairStruct clone({String? name}) => PairStruct(name: name); +} + +// ── Leaf submodule with a struct output port ─────────────────── + +class StructProducer extends Module { + Logic get out => PairStruct()..gets(output('out')); + + StructProducer(Logic x, Logic y) : super(name: 'struct_producer') { + x = addInput('x', x); + y = addInput('y', y); + + final s = PairStruct(a: x, b: y); + addOutput('out', width: s.width) <= s; + } +} + +// ── Leaf submodule with a struct input port ──────────────────── + +class StructConsumer extends Module { + Logic get sum => output('sum'); + + StructConsumer(Logic pair) : super(name: 'struct_consumer') { + pair = addInput('pair', pair, width: pair.width); + + final s = PairStruct()..gets(pair); + addOutput('sum') <= s.elements[0] ^ s.elements[1]; + } +} + +// ── Top module: struct output from submodule → struct input ─── + +class StructPipeTop extends Module { + Logic get result => output('result'); + + StructPipeTop(Logic x, Logic y) : super(name: 'struct_pipe_top') { + x = addInput('x', x); + y = addInput('y', y); + + final producer = StructProducer(x, y); + final consumer = StructConsumer(producer.out); + + addOutput('result') <= consumer.sum; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('struct port pruning', () { + test('SV output retains struct element signals from submodule', () async { + final dut = StructPipeTop(Logic(), Logic()); + await dut.build(); + + final svStr = SvService(dut, register: false).synthOutput; + + // The struct_producer submodule should appear in the SV. + expect( + svStr, + contains('struct_producer'), + reason: 'Submodule with struct output should not be pruned', + ); + + // The struct_consumer submodule should appear in the SV. + expect( + svStr, + contains('struct_consumer'), + reason: 'Submodule with struct input should not be pruned', + ); + + // The output port 'out' of struct_producer (width 2) must have a + // connection in the parent — it should not be pruned away. + expect( + svStr, + contains('.out('), + reason: 'Struct output port connection should not be pruned', + ); + + // The input port 'pair' of struct_consumer must be connected. + expect( + svStr, + contains('.pair('), + reason: 'Struct input port connection should not be pruned', + ); + }); + + test('struct element signals survive SV synthesis for producer', () async { + final dut = StructProducer(Logic(), Logic()); + await dut.build(); + + final svStr = SvService(dut, register: false).synthOutput; + + // Inside StructProducer, the struct elements (a, b from PairStruct) + // drive the output via struct_slice decomposition. They must not + // be pruned. + expect(svStr, contains('out'), reason: 'Output port should appear in SV'); + expect( + svStr, + contains('input'), + reason: 'Input ports should appear in SV', + ); + }); + + test('struct element signals survive SV synthesis for consumer', () async { + final dut = StructConsumer(Logic(width: 2)); + await dut.build(); + + final svStr = SvService(dut, register: false).synthOutput; + + // Inside StructConsumer, the struct elements are extracted from the + // packed input. The XOR of elements drives the output. + expect(svStr, contains('sum'), reason: 'Output port should appear in SV'); + expect( + svStr, + contains('pair'), + reason: 'Input struct port should appear in SV', + ); + }); + }); +} diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..c1281de6e 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -121,7 +121,7 @@ class SimpleStruct extends LogicStructure { (asNet ? LogicNet.new : Logic.new)( name: 'field4', width: 4, naming: elementNaming), (asNet ? LogicNet.new : Logic.new)( - name: 'field8', width: 8, naming: elementNaming), + name: 'field8', width: 8, naming: elementNaming) ]); @override @@ -154,20 +154,20 @@ class TopWithUnusedSubModPorts extends Module { late final LogicArray outArrTopC; late final Logic outStructTopC; - TopWithUnusedSubModPorts({ - required Logic topIn, - required LogicNet topIo, - required LogicNet outTopIoA, - required LogicNet outTopIoB, - required LogicNet outTopIoC, - required LogicArray topArrIn, - required SimpleStruct topStructIn, - required LogicArray topArrNetIn, - required SimpleStruct topStructNetIn, - required LogicArray outTopIoArrA, - required SimpleStruct outTopIoStructA, - required Naming internalNaming, - }) : super(name: 'TopWithUnusedSubModPorts') { + TopWithUnusedSubModPorts( + {required Logic topIn, + required LogicNet topIo, + required LogicNet outTopIoA, + required LogicNet outTopIoB, + required LogicNet outTopIoC, + required LogicArray topArrIn, + required SimpleStruct topStructIn, + required LogicArray topArrNetIn, + required SimpleStruct topStructNetIn, + required LogicArray outTopIoArrA, + required SimpleStruct outTopIoStructA, + required Naming internalNaming}) + : super(name: 'TopWithUnusedSubModPorts') { // Connectivity description: // ^ outTopA // | between @@ -217,23 +217,22 @@ class TopWithUnusedSubModPorts extends Module { : null); final subModA = SubModWithSomePortsUsed( - fromIn: topIn, - fromIo: topIo, - fromArrIn: topArrIn, - fromStructIn: topStructIn, - fromArrNetIn: topArrNetIn, - fromStructNetIn: topStructNetIn, - inpNotUsed: inpNotUsed, - ioNotUsed: ioNotUsedA, - arrInNotUsed: arrInNotUsed, - structInNotUsed: structInNotUsed, - arrNetInNotUsed: arrNetInNotUsed, - structNetInNotUsed: structNetInNotUsed, - outIoTo: outTopIoA, - outIoArrTo: outTopIoArrA, - outIoStructTo: outTopIoStructA, - name: 'subModA', - ); + fromIn: topIn, + fromIo: topIo, + fromArrIn: topArrIn, + fromStructIn: topStructIn, + fromArrNetIn: topArrNetIn, + fromStructNetIn: topStructNetIn, + inpNotUsed: inpNotUsed, + ioNotUsed: ioNotUsedA, + arrInNotUsed: arrInNotUsed, + structInNotUsed: structInNotUsed, + arrNetInNotUsed: arrNetInNotUsed, + structNetInNotUsed: structNetInNotUsed, + outIoTo: outTopIoA, + outIoArrTo: outTopIoArrA, + outIoStructTo: outTopIoStructA, + name: 'subModA'); outTopA = addOutput('outTopA', width: topIn.width)..gets(subModA.outTo); outArrTopA = addOutputArray('outArrTopA', @@ -243,25 +242,24 @@ class TopWithUnusedSubModPorts extends Module { ..gets(subModA.outStructTo); final subModB = SubModWithSomePortsUsed( - fromIn: subModA.outTo, - fromIo: betweenAtoBNet, - fromArrIn: subModA.outArrTo.elements[0] as LogicArray, - fromStructIn: subModA.outStructTo.elements[0], - fromArrNetIn: topArrNetIn, - fromStructNetIn: topStructNetIn, - inpNotUsed: inpNotUsed, - ioNotUsed: LogicNet( - name: 'ioNotUsedB', - naming: internalNaming), // don't multiply connect IO - arrInNotUsed: arrInNotUsed, - structInNotUsed: structInNotUsed, - arrNetInNotUsed: arrNetInNotUsed.clone(), - structNetInNotUsed: structNetInNotUsed.clone(), - outIoTo: outTopIoB, - outIoArrTo: betweenAtoBArrNet, - outIoStructTo: betweenAtoBStructNet, - name: 'subModB', - ); + fromIn: subModA.outTo, + fromIo: betweenAtoBNet, + fromArrIn: subModA.outArrTo.elements[0] as LogicArray, + fromStructIn: subModA.outStructTo.elements[0], + fromArrNetIn: topArrNetIn, + fromStructNetIn: topStructNetIn, + inpNotUsed: inpNotUsed, + ioNotUsed: LogicNet( + name: 'ioNotUsedB', + naming: internalNaming), // don't multiply connect IO + arrInNotUsed: arrInNotUsed, + structInNotUsed: structInNotUsed, + arrNetInNotUsed: arrNetInNotUsed.clone(), + structNetInNotUsed: structNetInNotUsed.clone(), + outIoTo: outTopIoB, + outIoArrTo: betweenAtoBArrNet, + outIoStructTo: betweenAtoBStructNet, + name: 'subModB'); outTopB = addOutput('outTopB', width: topIn.width)..gets(subModB.outTo); outArrTopB = addOutputArray('outArrTopB', @@ -272,32 +270,32 @@ class TopWithUnusedSubModPorts extends Module { ..gets(subModB.outStructTo); final subModC = SubModWithSomePortsUsed( - fromIn: subModA.outTo, - fromIo: betweenAtoBNet, - fromArrIn: LogicArray( - [2, ...subModA.outArrTo.dimensions], subModA.outArrTo.elementWidth) - ..elements[0].gets(subModA.outArrTo) - ..elements[1].gets(Const(3, width: subModA.outArrTo.width)), - fromStructIn: LogicStructure([ - SimpleStruct(elementNaming: internalNaming)..gets(subModA.outStructTo), - SimpleStruct(elementNaming: internalNaming) - ..gets(Const(3, width: subModA.outStructTo.width)) - ]), - fromArrNetIn: topArrNetIn, - fromStructNetIn: topStructNetIn, - inpNotUsed: inpNotUsed, - ioNotUsed: LogicNet( - name: 'ioNotUsedC', - naming: internalNaming), // don't multiply connect IO - arrInNotUsed: arrInNotUsed, - structInNotUsed: structInNotUsed, - arrNetInNotUsed: arrNetInNotUsed.clone(), - structNetInNotUsed: structNetInNotUsed.clone(), - outIoTo: outTopIoC, - outIoArrTo: betweenAtoBArrNet, - outIoStructTo: betweenAtoBStructNet, - name: 'subModC', - ); + fromIn: subModA.outTo, + fromIo: betweenAtoBNet, + fromArrIn: LogicArray( + [2, ...subModA.outArrTo.dimensions], subModA.outArrTo.elementWidth) + ..elements[0].gets(subModA.outArrTo) + ..elements[1].gets(Const(3, width: subModA.outArrTo.width)), + fromStructIn: LogicStructure([ + SimpleStruct(elementNaming: internalNaming) + ..gets(subModA.outStructTo), + SimpleStruct(elementNaming: internalNaming) + ..gets(Const(3, width: subModA.outStructTo.width)) + ]), + fromArrNetIn: topArrNetIn, + fromStructNetIn: topStructNetIn, + inpNotUsed: inpNotUsed, + ioNotUsed: LogicNet( + name: 'ioNotUsedC', + naming: internalNaming), // don't multiply connect IO + arrInNotUsed: arrInNotUsed, + structInNotUsed: structInNotUsed, + arrNetInNotUsed: arrNetInNotUsed.clone(), + structNetInNotUsed: structNetInNotUsed.clone(), + outIoTo: outTopIoC, + outIoArrTo: betweenAtoBArrNet, + outIoStructTo: betweenAtoBStructNet, + name: 'subModC'); outTopC = addOutput('outTopC', width: topIn.width)..gets(subModC.outTo); outArrTopC = addOutputArray('outArrTopC', @@ -628,21 +626,19 @@ class _StructLeafNamingStruct extends LogicStructure { @override _StructLeafNamingStruct clone({String? name}) => _StructLeafNamingStruct( - a: a.clone(), - b: b.clone(), // key: element.clone() → Naming.mergeable - name: name ?? this.name, - ); + a: a.clone(), + b: b.clone(), // key: element.clone() → Naming.mergeable + name: name ?? this.name); } class _StructLeafNamingModule extends Module { _StructLeafNamingModule() { final inp = addTypedInput( - 'inp', - _StructLeafNamingStruct( - a: Logic(name: 'a', width: 4), - b: Logic(width: 4), // unnamed → auto '_s' - ), - ); + 'inp', + _StructLeafNamingStruct( + a: Logic(name: 'a', width: 4), + b: Logic(width: 4), // unnamed → auto '_s' + )); addOutput('out', width: 4) <= inp.b ^ inp.a; } } @@ -684,7 +680,7 @@ void main() { await mod.build(); final vectors = [ - Vector({}, {'b': 0xff}), + Vector({}, {'b': 0xff}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -698,7 +694,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;")); @@ -707,8 +703,8 @@ void main() { final vectors = [ Vector({}, { 'outApple': 'zzzzzzzzz00zzzzz', - 'outBanana': 'zzzzzzzzz00zzzzz', - }), + 'outBanana': 'zzzzzzzzz00zzzzz' + }) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -719,17 +715,14 @@ 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)")); // simcompare to make sure simulation works as expected final vectors = [ - Vector({}, { - 'outApple': 0, - 'outBanana': 0, - }), + Vector({}, {'outApple': 0, 'outBanana': 0}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -751,7 +744,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 +761,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 +787,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 +796,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 +805,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 +819,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 +839,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,14 +854,14 @@ 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!')); final vectors = [ Vector({'a': 1}, {'b': 1}), - Vector({'a': 0}, {'b': 0}), + Vector({'a': 0}, {'b': 0}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -879,7 +872,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 +890,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;')); @@ -905,7 +898,7 @@ endmodule : ModWithUselessWireMods''')); final vectors = [ Vector({'a': 42}, {'b': 42}), - Vector({'a': 255}, {'b': 255}), + Vector({'a': 255}, {'b': 255}) ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); @@ -915,19 +908,18 @@ endmodule : ModWithUselessWireMods''')); for (final naming in [Naming.renameable, Naming.mergeable]) { test('with naming $naming', () async { final mod = TopWithUnusedSubModPorts( - topIn: Logic(), - topIo: LogicNet(width: 2), - topArrIn: LogicArray([4, 3], 2), - topStructIn: SimpleStruct(elementNaming: naming), - topArrNetIn: LogicArray.net([2, 2], 3), - topStructNetIn: SimpleStruct(elementNaming: naming, asNet: true), - internalNaming: naming, - outTopIoA: LogicNet(width: 2), - outTopIoB: LogicNet(width: 2), - outTopIoC: LogicNet(width: 2), - outTopIoArrA: LogicArray.net([2, 2], 3), - outTopIoStructA: SimpleStruct(elementNaming: naming, asNet: true), - ); + topIn: Logic(), + topIo: LogicNet(width: 2), + topArrIn: LogicArray([4, 3], 2), + topStructIn: SimpleStruct(elementNaming: naming), + topArrNetIn: LogicArray.net([2, 2], 3), + topStructNetIn: SimpleStruct(elementNaming: naming, asNet: true), + internalNaming: naming, + outTopIoA: LogicNet(width: 2), + outTopIoB: LogicNet(width: 2), + outTopIoC: LogicNet(width: 2), + outTopIoArrA: LogicArray.net([2, 2], 3), + outTopIoStructA: SimpleStruct(elementNaming: naming, asNet: true)); await mod.build(); final topSv = SynthBuilder(mod, SystemVerilogSynthesizer()) @@ -1002,7 +994,7 @@ endmodule : ModWithUselessWireMods''')); 'topStructIn': LogicValue.of('110011110011'), 'topIo': '10', 'topArrNetIn': LogicValue.of('110011').replicate(2), - 'topStructNetIn': LogicValue.of('101011101010'), + 'topStructNetIn': LogicValue.of('101011101010') }, { 'outTopA': 1, 'outTopB': 1, @@ -1016,16 +1008,13 @@ endmodule : ModWithUselessWireMods''')); 'outStructTopA': LogicValue.of('110011110011'), 'outStructTopB': LogicValue.of('0011'), 'outStructTopC': [ - LogicValue.ofInt( - 3, - 12, - ), + LogicValue.ofInt(3, 12), LogicValue.of('110011110011') ].swizzle(), 'outTopIoA': '10', 'outTopIoArrA': LogicValue.of('110011').replicate(2), 'outTopIoStructA': LogicValue.of('101011101010') - }), + }) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -1041,13 +1030,13 @@ endmodule : ModWithUselessWireMods''')); final mod = OutToInOutTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign myNet = myOut;')); final vectors = [ Vector({'clk': 0}, {'clkB': 1}), - Vector({'clk': 1}, {'clkB': 0}), + Vector({'clk': 1}, {'clkB': 0}) ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); @@ -1057,7 +1046,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 +1056,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/synth_name_parity_test.dart b/test/synth_name_parity_test.dart new file mode 100644 index 000000000..19be30013 --- /dev/null +++ b/test/synth_name_parity_test.dart @@ -0,0 +1,125 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_name_parity_test.dart +// Tests that verify canonicalNameOf works consistently across +// different synthesis paths (SV and netlist). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; + +class _Counter extends Module { + _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi( + [SimpleClockGenerator(10).clk, reset], + [ + If( + reset, + then: [val < 0], + orElse: [ + If(en, then: [val < nextVal]), + ], + ), + ], + ); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + group('canonicalNameOf after netlist synthesis', () { + test('counter — returns names after netlist synthesis', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + NetlistService(mod); + + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + expect(mod.namer.signalNameOfBest([mod.output('val')]), equals('val')); + }); + + test('filter_bank — returns names for sub-module signals', () async { + const dataWidth = 16; + const numTaps = 3; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + NetlistService(dut); + + expect(dut.namer.signalNameOfBest([dut.input('clk')]), equals('clk')); + expect(dut.namer.signalNameOfBest([dut.input('reset')]), equals('reset')); + expect(dut.namer.signalNameOfBest([dut.output('done')]), equals('done')); + }); + }); + + group('canonicalNameOf after SV synthesis', () { + test('counter — returns canonical name after SV synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + SvService(mod, register: false).synthOutput; + + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + }); + }); + + group('cross-synthesizer parity', () { + test( + 'counter — SV and netlist produce identical canonicalNameOf', + () async { + final modNetlist = _Counter(Logic(), Logic()); + await modNetlist.build(); + NetlistService(modNetlist); + await Simulator.reset(); + + final modSv = _Counter(Logic(), Logic()); + await modSv.build(); + SvService(modSv, register: false).synthOutput; + + // Both paths use the same Namer, so names must match. + final enNetlist = modNetlist.namer.signalNameOfBest([ + modNetlist.input('en'), + ]); + final enSv = modSv.namer.signalNameOfBest([modSv.input('en')]); + + expect( + enSv, + equals(enNetlist), + reason: 'SV and netlist should produce identical canonical names', + ); + }, + ); + }); +} diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..c8fb66397 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -19,13 +19,12 @@ class MyStruct extends LogicStructure { final bool asNet; factory MyStruct({String? name, bool asNet = false}) => MyStruct._( - (asNet ? LogicNet.new : Logic.new)( - name: 'ready', naming: Naming.mergeable), - (asNet ? LogicNet.new : Logic.new)( - name: 'valid', naming: Naming.mergeable), - name: name, - asNet: asNet, - ); + (asNet ? LogicNet.new : Logic.new)( + name: 'ready', naming: Naming.mergeable), + (asNet ? LogicNet.new : Logic.new)( + name: 'valid', naming: Naming.mergeable), + name: name, + asNet: asNet); MyStruct._(this.ready, this.valid, {required String? name, required this.asNet}) @@ -178,10 +177,7 @@ class ParentModuleWithStructsContainingPorts extends Module { ParentModuleWithStructsContainingPorts(Logic x) { x = addInput('x', x); - final child = ChildModuleForStructsOfPorts( - x, - LogicNet(name: 'y'), - ); + final child = ChildModuleForStructsOfPorts(x, LogicNet(name: 'y')); LogicStructure(name: 'BADNAMEO2', [child.out2]) ^ x; } @@ -204,9 +200,7 @@ class ChildModuleForStructsOfPorts extends Module { } class ParentModuleWithPackingSubModuleOutput extends Module { - ParentModuleWithPackingSubModuleOutput( - Logic x, - ) { + ParentModuleWithPackingSubModuleOutput(Logic x) { x = addInput('x', x); ChildModuleWithPackingOutput(x).myOut.packed.xor() ^ x; } @@ -229,7 +223,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'))); @@ -238,7 +232,7 @@ void main() { final vectors = [ Vector({'a1': 0, 'a2': 1}, {'b1': 1, 'b2': 0}), - Vector({'a1': 1, 'a2': 0}, {'b1': 0, 'b2': 1}), + Vector({'a1': 1, 'a2': 0}, {'b1': 0, 'b2': 1}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -251,7 +245,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')); @@ -266,7 +260,7 @@ void main() { Vector({'inp': '00'}, {'out': '01'}), Vector({'inp': '10'}, {'out': '01'}), Vector({'inp': '01'}, {'out': '01'}), - Vector({'inp': '11'}, {'out': '10'}), + Vector({'inp': '11'}, {'out': '10'}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -277,7 +271,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( @@ -308,16 +302,13 @@ void main() { .addTypedOutput( 'p', logic.clone as LogicType Function({String name})), 'inOut': (LogicType logic) => - DummyModule().addTypedInOut('p', logic), + DummyModule().addTypedInOut('p', logic) }; for (final MapEntry(key: portType, value: creator) in typedPortCreators.entries) { test('$portType with const', () { - expect( - () => creator(Const(1)), - throwsA(isA()), - ); + expect(() => creator(Const(1)), throwsA(isA())); }); test('$portType with const but param as Logic', () { @@ -332,10 +323,8 @@ void main() { }); test('$portType with struct containing const', () { - expect( - () => creator(StructWithConst()), - throwsA(isA()), - ); + expect(() => creator(StructWithConst()), + throwsA(isA())); }); test('$portType with struct containing const but param as Logic', () { @@ -357,7 +346,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'))); @@ -372,7 +361,7 @@ void main() { final vectors = [ Vector({'a1': 0, 'a2': 1}, {'b1': 1, 'b2': 0}), - Vector({'a1': 1, 'a2': 0}, {'b1': 0, 'b2': 1}), + Vector({'a1': 1, 'a2': 0}, {'b1': 0, 'b2': 1}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -384,7 +373,7 @@ void main() { final vectorsReversed = [ Vector({'b1': 1, 'b2': 0}, {'a1': 0, 'a2': 1}), - Vector({'b1': 0, 'b2': 1}, {'a1': 1, 'a2': 0}), + Vector({'b1': 0, 'b2': 1}, {'a1': 1, 'a2': 0}) ]; await SimCompare.checkFunctionalVector(mod, vectorsReversed); @@ -396,17 +385,15 @@ void main() { final typedPortCreators = { 'input': (Logic logic) => DummyModule().addTypedInput('p', logic), 'output': (Logic logic) => DummyModule().addTypedOutput('p', logic.clone), - 'inOut': (Logic logic) => DummyModule().addTypedInOut('p', logic), + 'inOut': (Logic logic) => DummyModule().addTypedInOut('p', logic) }; group('struct with partial nets fails', () { for (final MapEntry(key: portType, value: creator) in typedPortCreators.entries) { test(portType, () { - expect( - () => creator(LogicStructure([Logic(), LogicNet()])), - throwsA(isA()), - ); + expect(() => creator(LogicStructure([Logic(), LogicNet()])), + throwsA(isA())); }); } }); @@ -461,13 +448,13 @@ void main() { LogicArray.net([4], 1), LogicStructure([LogicNet(width: 2)]) ]) - ), + ) ]; final modMakers = [ (name: 'basic', maker: MatcherModule.new, invert: true), (name: 'wrapper', maker: MatcherModuleWrapper.new, invert: true), - (name: 'pass through', maker: MatcherPassThrough.new, invert: false), + (name: 'pass through', maker: MatcherPassThrough.new, invert: false) ]; for (final modMaker in modMakers) { @@ -486,8 +473,7 @@ void main() { {'anyIn': 0xa5}, {'anyOut': modMaker.invert ? 0x5a : 0xa5}), Vector( {'anyIn': 0xff}, {'anyOut': modMaker.invert ? 0x00 : 0xff}), - Vector( - {'anyIn': 0x13}, {'anyOut': modMaker.invert ? 0xec : 0x13}), + Vector({'anyIn': 0x13}, {'anyOut': modMaker.invert ? 0xec : 0x13}) ]; await SimCompare.checkFunctionalVector(mod, vectors); @@ -502,14 +488,14 @@ 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')); final vectors = [ Vector({'inStruct': 0}, {'outStruct': 0}), - Vector({'inStruct': 1}, {'outStruct': 1}), + Vector({'inStruct': 1}, {'outStruct': 1}) ]; await SimCompare.checkFunctionalVector(mod, vectors); diff --git a/test/wave_dumper_test.dart b/test/wave_dumper_test.dart index 07aafc8c8..bfca9c14d 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]. @@ -227,10 +227,8 @@ void main() { final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - expect( - VcdParser.confirmValue(vcdContents, 'a', 99, LogicValue.one), - equals(true), - ); + expect(VcdParser.confirmValue(vcdContents, 'a', 99, LogicValue.one), + equals(true)); deleteTemporaryDump(dumpName); }); @@ -241,7 +239,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-----