diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index f2ed2cf65..c72e1a47e 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -121,4 +121,3 @@ jobs: - name: Test DevTools Installation run: tool/gh_actions/devtool/test_devtools_install.sh extension/devtools - diff --git a/CHANGELOG.md b/CHANGELOG.md index 6294c1c42..454bde779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## Next Release + +- Added the `ModuleService` API for module-scoped generation, capture, and + inspection services. `ModuleServices` registers and looks up services for a + built module hierarchy, and `hierarchyJson` exposes its hierarchy as JSON. +- Added `ArtifactProducingService` and `ModuleServiceArtifact` for + transport-neutral output. Artifact-producing services default + `outputDirectory` to the current directory and `outputBaseName` to the + module definition name, and expose named, media-typed byte streams without + requiring filesystem output. +- Added `SystemVerilogService` for configured SystemVerilog synthesis, + in-memory source output, artifact inspection, and explicit directory writes. + Added `WaveformService` for in-memory waveform capture with optional file + writing through `writeToFile`. +- Added legacy-compatible `Module.dumpSystemVerilog` and `Module.dumpWaves` + convenience methods. `dumpSystemVerilog()` provides simple in-memory output + or writes an optional `outputPath`; `dumpWaves()` provides standard VCD + capture as the replacement for `WaveDumper`. `WaveDumper` and `generateSynth` + are deprecated in favor of these `Module` methods. + ## 0.6.10 - Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bb9116ce..1c10e0bfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Anyone interested in participating in ROHD is more than welcome to help! ## Code of Conduct -ROHD adopts the [Contributor Covenant](https://www.contributor-covenant.org/) v2.1 for the code of conduct. It can be accessed [here](CODE_OF_CONDUCT.md). +ROHD adopts the [Contributor Covenant](https://www.contributor-covenant.org/) v2.1 for the [Code of Conduct](CODE_OF_CONDUCT.md). ## Getting Help @@ -124,7 +124,7 @@ Please include the SPDX tag near the top of any new files you create: Here is an example of a recommended file header template: ```dart -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // example.dart diff --git a/benchmark/many_submodules_benchmark.dart b/benchmark/many_submodules_benchmark.dart index 763261a4c..5f7a6be9a 100644 --- a/benchmark/many_submodules_benchmark.dart +++ b/benchmark/many_submodules_benchmark.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // 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(); + dut.dumpSystemVerilog().output; } } diff --git a/benchmark/wave_dump_benchmark.dart b/benchmark/wave_dump_benchmark.dart index 777b42eb0..002e59f48 100644 --- a/benchmark/wave_dump_benchmark.dart +++ b/benchmark/wave_dump_benchmark.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // wave_dump_benchmark.dart @@ -56,7 +56,7 @@ class WaveDumpBenchmark extends AsyncBenchmarkBase { _mod = _ModuleToDump(Logic(), _clk); await _mod.build(); - WaveDumper(_mod, outputPath: _vcdTemporaryPath); + _mod.dumpWaves(outputPath: _vcdTemporaryPath); await Simulator.run(); diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 000000000..f150a85f7 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,3 @@ +tags: + benchmark: + timeout: 2x \ No newline at end of file diff --git a/doc/tutorials/chapter_1/01_setup_installation.md b/doc/tutorials/chapter_1/01_setup_installation.md index 4c5ce37e0..87ef1ecf1 100644 --- a/doc/tutorials/chapter_1/01_setup_installation.md +++ b/doc/tutorials/chapter_1/01_setup_installation.md @@ -162,7 +162,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 = counter.dumpSystemVerilog().output; if (!noPrint) { print(systemVerilogCode); } @@ -175,7 +175,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(counter); + counter.dumpWaves(); } // Drop reset at time 25. diff --git a/doc/tutorials/chapter_2/helper.dart b/doc/tutorials/chapter_2/helper.dart index 387a57fe3..a56a39af5 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 ' + '${mod.dumpSystemVerilog().output}'); } 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 5f8675a37..c6605e685 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 - tutorial - print(fSub.generateSynth()); + print(fSub.dumpSystemVerilog().output); test('should return 0 when a and b equal 1', () { a.put(1); diff --git a/doc/tutorials/chapter_3/full_adder.dart b/doc/tutorials/chapter_3/full_adder.dart index 08ac2cbc4..a39882b0f 100644 --- a/doc/tutorials/chapter_3/full_adder.dart +++ b/doc/tutorials/chapter_3/full_adder.dart @@ -76,5 +76,5 @@ void main() async { final mod = FullAdderModule(a, b, cIn, faOps); await mod.build(); - print(mod.generateSynth()); + print(mod.dumpSystemVerilog().output); } diff --git a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart index be347b040..c221d3507 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(mod.dumpSystemVerilog().output); 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 9c062a1d9..f1ec6d02f 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(mod.dumpSystemVerilog().output); 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 55eb4ff07..9e25deb97 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(nbitAdder.dumpSystemVerilog().output); test('should return 10 when both inputs are 5.', () { a.put(5); diff --git a/doc/tutorials/chapter_5/00_basic_modules.md b/doc/tutorials/chapter_5/00_basic_modules.md index 65ab303fc..c2207c748 100644 --- a/doc/tutorials/chapter_5/00_basic_modules.md +++ b/doc/tutorials/chapter_5/00_basic_modules.md @@ -95,7 +95,7 @@ Do note that the `build()` method returns a `Future`, not just `void`. Thi ## Converting ROHD Module to System Verilog RTL -Next, we can see how extending your `Logic` to `Module` enables the generation of system Verilog code. Building on the previous example, we've made some slight modifications by adding `simModule.build()` and `simModule.generateSynth()`. +Next, we can see how extending your `Logic` to `Module` enables the generation of system Verilog code. Building on the previous example, we've made some slight modifications by adding `simModule.build()` and `simModule.dumpSystemVerilog().output`. ```dart void main() async { @@ -106,7 +106,7 @@ void main() async { await simModule.build(); // Print out system verilog code - print(simModule.generateSynth()); + print(simModule.dumpSystemVerilog().output); test('should return input value.', () => expect(simModule.out.value.toInt(), equals(1))); @@ -114,7 +114,8 @@ void main() async { // Add this to test on generate system verilog code test( 'should generate system verilog code.', - () => expect(simModule.generateSynth(), contains('module SimpleModule('))); + () => expect(simModule.dumpSystemVerilog().output, + contains('module SimpleModule('))); } ``` diff --git a/doc/tutorials/chapter_5/answers/full_adder.dart b/doc/tutorials/chapter_5/answers/full_adder.dart index 3d932bef9..b14326d5b 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(mod.dumpSystemVerilog().output); test('should return true if result sum similar to truth table.', () { 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 1e260272c..cf93b7d76 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(diff.dumpSystemVerilog().output); test('should return true if results matched truth table', () { 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 7fed1c9d3..8387d2648 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(mod.dumpSystemVerilog().output); 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 6d8d32413..4001fc5b9 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(nbitAdder.dumpSystemVerilog().output); test('should return 20 when A and B perform add.', () { a.put(15); diff --git a/doc/tutorials/chapter_7/00_sequential_logic.md b/doc/tutorials/chapter_7/00_sequential_logic.md index ecb2daa25..3eca09077 100644 --- a/doc/tutorials/chapter_7/00_sequential_logic.md +++ b/doc/tutorials/chapter_7/00_sequential_logic.md @@ -5,7 +5,7 @@ - [Shift Register](#shift-register) - [ROHD Simulator](#rohd-simulator) - [Unit Test in Sequential Logic](#unit-test-in-sequential-logic) -- [Wave Dumper](#wave-dumper) +- [Waveform Dumping](#waveform-dumping) - [Exercise](#exercise) ## Learning Outcome @@ -44,7 +44,7 @@ class ShiftRegister extends Module { void main() async { final shiftReg = ShiftRegister(); await shiftReg.build(); - print(shiftReg.generateSynth()); + print(shiftReg.dumpSystemVerilog().output); } ``` @@ -196,7 +196,7 @@ void main() async { final shiftReg = ShiftRegister(clk, reset, sin); await shiftReg.build(); - print(shiftReg.generateSynth()); + print(shiftReg.dumpSystemVerilog().output); // Inject 1 to reset and 0 to shift in reset.inject(1); @@ -217,9 +217,9 @@ void main() async { } ``` -## Wave Dumper +## Waveform Dumping -Let also add `WaveDumper` to view the waveform of the Simulation results. +Let also call `dumpWaves` to view the waveform of the Simulation results. ```dart void main() async { @@ -242,8 +242,8 @@ void main() async { // Run the simulator but don't wait for it unawaited(Simulator.run()); - // Output the simulation waveform using WaveDumper - WaveDumper(shiftReg, + // Output the simulation waveform + shiftReg.dumpWaves( outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); } ``` @@ -252,7 +252,7 @@ Now, let print the flop before the first clock Positive edge. We can just call t ```dart ... -WaveDumper(shiftReg, +shiftReg.dumpWaves( outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); printFlop('Before'); ``` 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 0e81c46fd..c695906d5 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(dff.dumpSystemVerilog().output); data.inject(1); reset.inject(1); @@ -60,7 +60,7 @@ Future main() async { unawaited(Simulator.run()); - WaveDumper(dff, + dff.dumpWaves( 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 046cc05c0..24ced0ee9 100644 --- a/doc/tutorials/chapter_7/shift_register.dart +++ b/doc/tutorials/chapter_7/shift_register.dart @@ -69,7 +69,7 @@ void main() { // kick-off the simulator, but we don't want to wait unawaited(Simulator.run()); - WaveDumper(shiftReg, + shiftReg.dumpWaves( outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_8/01_interface.md b/doc/tutorials/chapter_8/01_interface.md index ccd44fd62..c8fa9be14 100644 --- a/doc/tutorials/chapter_8/01_interface.md +++ b/doc/tutorials/chapter_8/01_interface.md @@ -168,9 +168,9 @@ Future main() async { counterInterface.en.inject(0); counterInterface.reset.inject(1); - print(counter.generateSynth()); + print(counter.dumpSystemVerilog().output); - WaveDumper(counter, + counter.dumpWaves( outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); Simulator.registerAction(25, () { counterInterface.en.put(1); diff --git a/doc/tutorials/chapter_8/02_finite_state_machine.md b/doc/tutorials/chapter_8/02_finite_state_machine.md index dead94976..5a01d088d 100644 --- a/doc/tutorials/chapter_8/02_finite_state_machine.md +++ b/doc/tutorials/chapter_8/02_finite_state_machine.md @@ -268,11 +268,11 @@ await oven.build(); reset.inject(1); ``` -Let also attach a `WaveDumper` to preview what is the waveform and what happened during the Simulation. +Let also call `dumpWaves` to preview the waveform and what happened during the Simulation. ```dart if (!noPrint) { - WaveDumper(oven, outputPath: 'oven.vcd'); + oven.dumpWaves(outputPath: 'oven.vcd'); } ``` diff --git a/doc/tutorials/chapter_8/03_pipeline.md b/doc/tutorials/chapter_8/03_pipeline.md index 09ec7bffc..5788a8099 100644 --- a/doc/tutorials/chapter_8/03_pipeline.md +++ b/doc/tutorials/chapter_8/03_pipeline.md @@ -282,7 +282,7 @@ void main() async { reset.inject(1); // Attach a waveform dumper so we can see what happens. - WaveDumper(csm, outputPath: 'csm.vcd'); + csm.dumpWaves(outputPath: 'csm.vcd'); Simulator.registerAction(10, () { reset.inject(0); diff --git a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart index 1c67fe3b7..03d292cde 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(tb.dumpSystemVerilog().output); 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'); + peri.dumpWaves(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 9eacf69aa..fbc9c1360 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(toyCap.dumpSystemVerilog().output); toyCap.toyCapsuleStateMachine.generateDiagram(); reset.inject(1); - WaveDumper(toyCap, outputPath: 'toyCapsuleFSM.vcd'); + toyCap.dumpWaves(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 ae810afeb..0547c1882 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) { final pipe = Pipeline4Stages(clk, reset, a); await pipe.build(); - // print(pipe.generateSynth()); + // print(pipe.dumpSystemVerilog().output); a.inject(5); reset.inject(1); Simulator.registerAction(10, () => reset.put(0)); - WaveDumper(pipe, outputPath: 'answer_1.vcd'); + pipe.dumpWaves(outputPath: 'answer_1.vcd'); Simulator.registerAction(50, () { // 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 2063f1aaa..fdcbc42aa 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'); + csm.dumpWaves(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 b2dc142ca..34fbe5c11 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(counter.dumpSystemVerilog().output); - WaveDumper(counter, + counter.dumpWaves( 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 0fcd8002a..844baa8ec 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'); + oven.dumpWaves(outputPath: 'doc/tutorials/chapter_8/oven.vcd'); } if (!noPrint) { diff --git a/doc/tutorials/chapter_9/rohd_vf.md b/doc/tutorials/chapter_9/rohd_vf.md index 7d6035930..ceb5c58d1 100644 --- a/doc/tutorials/chapter_9/rohd_vf.md +++ b/doc/tutorials/chapter_9/rohd_vf.md @@ -435,7 +435,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + tb.counter.dumpWaves(); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); 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..782a06e45 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); + tb.counter.dumpWaves(); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index e4e625952..2fc7aea8c 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -1,13 +1,14 @@ --- title: "Generating Outputs" permalink: /docs/generation/ -last_modified_at: 2023-11-13 +last_modified_at: 2026-08-19 toc: true --- Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. -The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: +The simplest way to write SystemVerilog is with `dumpSystemVerilog` on +`Module`: ```dart void main() async { @@ -16,24 +17,40 @@ void main() async { // remember that `build` returns a `Future`, hence the `await` here await myModule.build(); - final generatedSv = myModule.generateSynth(); - - // you can print it out... - print(generatedSv); - - // or write it to a file - File('myHardware.sv').writeAsStringSync(generatedSv); + myModule.dumpSystemVerilog(outputPath: 'myHardware.sv'); } ``` -The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. + `dumpSystemVerilog` writes one file containing the SystemVerilog `module` + definitions for the top-level module and all recursive submodules. To write + one `.sv` file per module definition instead, pass a directory and set + `multiFile` to `true`: + + ```dart + myModule.dumpSystemVerilog( + outputPath: 'build/systemverilog', + multiFile: true, + ); + ``` + + For generated text without writing a file, use `dumpSystemVerilog` without an + `outputPath`: + + ```dart + final generatedSv = myModule.dumpSystemVerilog().output; + ``` + + The dump methods preserve the legacy one-shot output workflow. For the service + API, artifact streams, or explicit output configuration, use + `SystemVerilogService` directly. ## Controlling port types Generated ports default to `input logic`, `output logic`, and `inout wire`, preserving the traditional ROHD declarations. Use a `SystemVerilogSynthesizerConfiguration` to independently control whether object types, such as `wire` and `var`, and data types, such as `logic`, are explicit for each port direction: ```dart -final generatedSv = myModule.generateSynth( +myModule.dumpSystemVerilog( + outputPath: 'myHardware.sv', configuration: const SystemVerilogSynthesizerConfiguration( inputPortType: SystemVerilogPortTypeConfiguration( objectType: SystemVerilogPortType.explicit, @@ -51,7 +68,8 @@ final generatedSv = myModule.generateSynth( ); ``` -The same configuration can be passed directly to `SystemVerilogSynthesizer` when using `SynthBuilder`. +The same configuration can be passed directly to `SystemVerilogSynthesizer` +when using `SynthBuilder`. ## Controlling naming @@ -79,6 +97,113 @@ Internal signals, unlike ports, don't need to always have the same exact name as The `Naming.unpreferredName` function will modify a signal name to indicate to downstream flows that the name is preferably omitted from the output, but preferable to an unnamed signal. This is generally most useful for things like output ports of `InlineSystemVerilog` modules. -## More advanced generation +## Services API + +`ModuleService` is the shared API for module-scoped generation, capture, and +inspection. Services can register with `ModuleServices` for lookup by DevTools +and other consumers. `ArtifactProducingService` implementations expose named +artifacts as media-typed byte streams, so consumers do not need to require a +local output file. + +### Registering and discovering services + +Services such as `SystemVerilogService` and `WaveformService` register +themselves by default when constructed. `ModuleServices` keeps the most recently +registered service of each concrete type. This allows DevTools, application +code, and other services to discover an optional capability without requiring +the creator to pass the service instance to every consumer: + +```dart +final systemVerilog = SystemVerilogService(myModule); + +final registeredSystemVerilog = + ModuleServices.instance.lookup(); + +assert(identical(systemVerilog, registeredSystemVerilog)); +assert(identical(systemVerilog, SystemVerilogService.current)); +``` + +Constructing another `SystemVerilogService` replaces the previous +`SystemVerilogService` in the registry. Other service types remain registered. +Use `unregister()` to remove one service type or `reset()` to clear the +registry. + +Registration also enables services to collaborate without directly depending +on how the application created them. For example, a generation or capture +service can look up an optional tracing service and include source file, line, +and column information when tracing is available. Because a service may consult +the registry during construction or generation, register supporting services +before constructing the services that consume them. + +For one-shot work that should not change globally discoverable service state, +set `register` to `false`: + +```dart +final oneShotSystemVerilog = SystemVerilogService( + myModule, + register: false, +); +``` -Under the hood of `generateSynth`, it's actually using a [`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) which accepts a `Module` and a `Synthesizer` (usually a `SystemVerilogSynthesizer`) as arguments. This `SynthBuilder` can provide a collection of `String` file contents via `getFileContents`, or you can ask for the full set of `synthesisResults`, which contains `SynthesisResult`s which can each be converted `toSynthFileContents` but also has context about the `module` it refers to, the `instanceTypeName`, etc. With these APIs, you can easily generate named files, add file headers, ignore generation of some modules, generate file lists for other tools, etc. The `SynthBuilder.multi` constructor makes it convenient to generate outputs for multiple independent hierarchies. +The `Module.dumpSystemVerilog()` and `Module.dumpWaves()` convenience methods +use the normal registration defaults. Construct the corresponding service +directly with `register: false` when this side effect is not desired. + +`SystemVerilogService` is the direct synthesis service. Its `outputDirectory` +defaults to the current directory and its `outputBaseName` defaults to the top +module's `definitionName`. The service generates output in memory; call +`writeOutputs` only when files are required: + +```dart +final service = SystemVerilogService( + myModule, + outputDirectory: 'build/netlist', + outputBaseName: 'accelerator', + configuration: const SystemVerilogSynthesizerConfiguration(), +); + +// Use the generated SystemVerilog directly. +final generatedSv = service.output; + +// Or inspect a transport-neutral artifact stream. +final artifact = service.artifacts.single; +final bytes = await artifact.openRead().expand((chunk) => chunk).toList(); + +// Write build/netlist/accelerator.sv. +service.writeOutputs(); +``` + +With `multiFile: true`, `SystemVerilogService` writes one `.sv` file per +generated module definition. For custom synthesis flows, +[`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) +accepts a `Module` and a `Synthesizer` (usually a +`SystemVerilogSynthesizer`). + +## Capturing waveforms + +Use `dumpWaves` for the legacy-compatible common case of writing all simulation +signals to a VCD file: + +```dart +myModule.dumpWaves(outputPath: 'waves.vcd'); +``` + +`WaveformService` records VCD data in memory by default and exposes it as a +`ModuleServiceArtifact`. Its output directory and basename use the same +defaults as other artifact-producing services. Set `writeToFile` when capture +should also create a VCD file: + +```dart +final waveform = WaveformService( + myModule, + outputDirectory: 'build/waves', + outputBaseName: 'interesting-signals', + writeToFile: true, + timescale: '1ns', + startTime: 100, + stopTime: 1000, + signalFilter: (signal) => signal.name.startsWith('debug_'), +); + +final vcdArtifact = waveform.artifacts.single; +``` diff --git a/doc/user_guide/_get-started/03-development-recommendations.md b/doc/user_guide/_get-started/03-development-recommendations.md index d224e54df..6ffb5ab7b 100644 --- a/doc/user_guide/_get-started/03-development-recommendations.md +++ b/doc/user_guide/_get-started/03-development-recommendations.md @@ -10,9 +10,9 @@ toc: true - The [ROHD Cosimulation](https://github.com/intel/rohd-cosim) package allows you to cosimulate the ROHD simulator with a variety of SystemVerilog simulators. - The [ROHD Hardware Component Library](https://github.com/intel/rohd-vf) provides a set of reusable and configurable components for design and verification. - Visual Studio Code (vscode) is a great, free IDE with excellent support for Dart. It works well on all platforms, including native Windows or Windows Subsystem for Linux (WSL) which allows you to run a native Linux kernel (e.g. Ubuntu) within Windows. You can also use vscode to develop on a remote machine with the Remote SSH extension. - - vscode: + - vscode: - WSL: - - Remote SSH: + - Remote SSH: - Dart extension for vscode: Head over to the [user guide]({{ site.baseurl }}{% link _docs/A01-sample-example.md %}) to learn more about how to use ROHD. diff --git a/example/example.dart b/example/example.dart index 74abf8e4b..8691d20f4 100644 --- a/example/example.dart +++ b/example/example.dart @@ -61,7 +61,7 @@ Future main({bool noPrint = false}) async { // Let's see what this module looks like as SystemVerilog, so we can pass it // to other tools. - final systemVerilogCode = counter.generateSynth(); + final systemVerilogCode = counter.dumpSystemVerilog().output; if (!noPrint) { print(systemVerilogCode); } @@ -70,7 +70,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(counter); + counter.dumpWaves(); } // Let's also print a message every time the value on the counter changes, diff --git a/example/filter_bank.dart b/example/filter_bank.dart new file mode 100644 index 000000000..155e629d5 --- /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 'filter_bank/filter_bank_modules.dart'; + +// Re-export so downstream consumers (e.g. devtools loopback) can use. +export 'filter_bank/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) { + dut.dumpWaves(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/filter_bank/coeff_bank.dart b/example/filter_bank/coeff_bank.dart new file mode 100644 index 000000000..da7523f6d --- /dev/null +++ b/example/filter_bank/coeff_bank.dart @@ -0,0 +1,62 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// coeff_bank.dart +// Coefficient storage module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A coefficient storage module backed by a [LogicArray] input port. +/// +/// Accepts a [LogicArray] of per-tap coefficients via [addInputArray] +/// and a tap index, then mux-selects the corresponding coefficient. +class CoeffBank extends Module { + /// The coefficient value at the selected index. + Logic get coeffOut => output('coeffOut'); + + /// The per-tap coefficient array (registered input port). + @protected + LogicArray get coeffArray => input('coeffArray') as LogicArray; + + /// The tap index input. + @protected + Logic get tapIndex => input('tapIndex'); + + /// Number of taps. + final int numTaps; + + /// Data width. + final int dataWidth; + + /// Creates a [CoeffBank] with [numTaps] taps at [dataWidth] bits. + /// + /// [coefficients] is a [LogicArray] with one element per tap — + /// registered as an input port via [addInputArray]. + /// [tapIndex] selects the active coefficient. + CoeffBank(Logic tapIndex, LogicArray coefficients, + {required this.numTaps, + required this.dataWidth, + super.name = 'CoeffBank'}) + : super(definitionName: 'CoeffBank_T${numTaps}_W$dataWidth') { + // Register ports + tapIndex = addInput('tapIndex', tapIndex, width: tapIndex.width); + final coeffArray = addInputArray('coeffArray', coefficients, + dimensions: [numTaps], elementWidth: dataWidth); + final coeffOut = addOutput('coeffOut', width: dataWidth); + + // Mux-chain ROM: priority-select coefficient by tap index. + Logic selected = Const(0, width: dataWidth); + for (var i = numTaps - 1; i >= 0; i--) { + selected = mux( + tapIndex.eq(Const(i, width: tapIndex.width)).named('tapMatch$i'), + coeffArray.elements[i], + selected, + ); + } + coeffOut <= selected; + } +} diff --git a/example/filter_bank/filter_bank.dart b/example/filter_bank/filter_bank.dart new file mode 100644 index 000000000..f0e973472 --- /dev/null +++ b/example/filter_bank/filter_bank.dart @@ -0,0 +1,246 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank.dart +// Top-level polyphase FIR filter bank module for the example library. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +import 'filter_channel.dart'; +import 'filter_controller.dart'; +import 'filter_data_interface.dart'; +import 'filter_sample.dart'; +import 'shared_data_bus.dart'; + +/// A 2-channel polyphase FIR filter bank. +/// +/// Hierarchy: +/// ```text +/// FilterBank (top) +/// ├── FilterController (FSM) +/// ├── FilterChannel 'ch0' +/// │ ├── CoeffBank (coefficient ROM via LogicArray + mux chain) +/// │ └── MacUnit 'mac' (pipelined multiply-accumulate) +/// └── FilterChannel 'ch1' +/// ├── CoeffBank +/// └── MacUnit 'mac' +/// ``` +/// +/// Each channel time-multiplexes a single MacUnit across all taps, +/// sequenced by a tap counter that drives the CoeffBank tap index +/// and a delay-line sample mux. +/// +/// Uses: +/// - [FilterDataInterface] for I/O port bundles +/// - [FilterSample] LogicStructure for structured sample signals +/// - [LogicArray] in CoeffBank for coefficient storage +/// - [Pipeline] in MacUnit for pipelined MAC +/// - [FiniteStateMachine] in FilterController for sequencing +/// - Multiple instantiation: two [FilterChannel]s share one definition +/// - [LogicNet] / [addInOut] for bidirectional shared data bus +class FilterBank extends Module { + /// Per-channel filtered outputs as a [LogicArray]. + /// + /// `channelOut.elements[i]` is the filtered output of channel `i`. + LogicArray get channelOut => output('channelOut') as LogicArray; + + /// Channel 0 filtered output (convenience getter). + Logic get out0 => channelOut.elements[0]; + + /// Channel 1 filtered output (convenience getter). + Logic get out1 => channelOut.elements[1]; + + /// Output valid (aligned with filtered outputs). + Logic get validOut => output('validOut'); + + /// Done signal from the controller FSM. + Logic get done => output('done'); + + /// Controller state (for debug visibility). + Logic get state => output('state'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input [FilterSample] port for channel [ch]. + @protected + FilterSample samplePin(int ch) => input('sample$ch') as FilterSample; + + /// Input-done strobe. + @protected + Logic get inputDonePin => input('inputDone'); + + /// Number of FIR taps per channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Number of filter channels. + final int numChannels; + + /// Creates a [FilterBank] with [numChannels] channels (default 2). + /// + /// Each channel has [numTaps] FIR taps at [dataWidth] bits. + /// [coefficients] is a list of per-channel coefficient lists — + /// `coefficients[i]` supplies the tap weights for channel `i`. + /// [samples] is a [LogicArray] with one element per channel. + /// [inputDone] when the input stream is complete. + /// + /// Optionally pass [dataBus] (a `LogicNet`) and [writeEnable] to + /// attach a bidirectional shared data bus via [SharedDataBus]. + /// The bus latches external data when [writeEnable] is low and + /// drives `storedValue` output. + FilterBank( + Logic clk, + Logic reset, + Logic start, + List samples, + Logic inputDone, { + required this.numTaps, + required this.dataWidth, + required List> coefficients, + this.numChannels = 2, + LogicNet? dataBus, + Logic? writeEnable, + super.name = 'FilterBank', + String? definitionName, + }) : super(definitionName: definitionName ?? 'FilterBank') { + if (numChannels <= 0) { + throw ArgumentError.value( + numChannels, + 'numChannels', + 'must be greater than zero', + ); + } + if (numTaps <= 0) { + throw ArgumentError.value( + numTaps, + 'numTaps', + 'must be greater than zero', + ); + } + if (dataWidth <= 0) { + throw ArgumentError.value( + dataWidth, + 'dataWidth', + 'must be greater than zero', + ); + } + if (samples.length != numChannels) { + throw ArgumentError.value( + samples.length, + 'samples', + 'must have $numChannels entries (one per channel)', + ); + } + if (coefficients.length != numChannels) { + throw ArgumentError.value( + coefficients.length, + 'coefficients', + 'must have $numChannels entries (one per channel)', + ); + } + for (var ch = 0; ch < numChannels; ch++) { + if (coefficients[ch].length != numTaps) { + throw ArgumentError.value( + coefficients[ch].length, + 'coefficients[$ch]', + 'must have $numTaps entries (one per tap)', + ); + } + } + + // ── Register ports ── + 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/example/filter_bank/filter_bank_modules.dart b/example/filter_bank/filter_bank_modules.dart new file mode 100644 index 000000000..5341784d8 --- /dev/null +++ b/example/filter_bank/filter_bank_modules.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_modules.dart +// Barrel file for the polyphase FIR filter bank example modules. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +export 'coeff_bank.dart'; +export 'filter_bank.dart'; +export 'filter_channel.dart'; +export 'filter_controller.dart'; +export 'filter_data_interface.dart'; +export 'filter_sample.dart'; +export 'mac_unit.dart'; +export 'shared_data_bus.dart'; diff --git a/example/filter_bank/filter_channel.dart b/example/filter_bank/filter_channel.dart new file mode 100644 index 000000000..317c9f934 --- /dev/null +++ b/example/filter_bank/filter_channel.dart @@ -0,0 +1,235 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_channel.dart +// Single FIR channel module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +import 'coeff_bank.dart'; +import 'filter_data_interface.dart'; +import 'mac_unit.dart'; + +/// A single polyphase FIR filter channel with [numTaps] taps. +/// +/// Uses a [FilterDataInterface] for its sample I/O ports. +/// +/// Architecture: +/// - A delay line (shift register) captures incoming samples. +/// - A tap counter cycles 0 … numTaps-1 each sample period. +/// - [CoeffBank] provides the coefficient for the current tap. +/// - A mux selects the delay-line sample for the current tap. +/// - A single [MacUnit] multiplies the selected sample by the +/// coefficient and adds it to a running accumulator. +/// - After all taps are processed the accumulator is latched as +/// the output and the accumulator resets for the next sample. +class FilterChannel extends Module { + /// The data interface for this channel (internal use only). + @protected + late final FilterDataInterface intf; + + /// Filtered output. + Logic get dataOut => intf.dataOut; + + /// Output valid. + Logic get validOut => intf.validOut; + + /// Number of FIR taps in this channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Creates a [FilterChannel] with [numTaps] taps at [dataWidth] bits. + /// + /// [srcIntf] provides the sample/valid input ports. [coefficients] + /// supplies per-tap constant coefficients. + FilterChannel( + FilterDataInterface srcIntf, + Logic clk, + Logic reset, + Logic enable, { + required this.numTaps, + required this.dataWidth, + required List coefficients, + super.name = 'FilterChannel', + }) : super(definitionName: 'FilterChannel_T${numTaps}_W$dataWidth') { + // Connect the Interface — creates module input/output ports + intf = FilterDataInterface(dataWidth: dataWidth) + ..connectIO(this, srcIntf, + inputTags: [FilterPortTag.inputPorts], + outputTags: [FilterPortTag.outputPorts]); + + final sampleIn = intf.sampleIn; + final validIn = intf.validIn; + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + + final tapIdxWidth = _bitsFor(numTaps); + + // ── Delay line (shift register via explicit flop bank + gates) ── + // AND gate: shift enable = enable & validIn & tapCounter==0 + // Samples shift in only when starting a new accumulation cycle. + final tapCounter = Logic(width: tapIdxWidth, name: 'tapCounter'); + final atFirstTap = + tapCounter.eq(Const(0, width: tapIdxWidth)).named('atFirstTap'); + final shiftEn = Logic(name: 'shiftEn'); + shiftEn <= (enable & validIn).named('enableAndValid') & atFirstTap; + + // LogicArray-backed delay line: one element per tap register. + final delayLine = LogicArray([numTaps], dataWidth, name: 'delayLine'); + for (var i = 0; i < numTaps; i++) { + final tapInput = (i == 0) ? sampleIn : delayLine.elements[i - 1]; + // Mux: hold current value or shift in new sample + final tapNext = Logic(width: dataWidth, name: 'nextTap$i'); + tapNext <= mux(shiftEn, tapInput, delayLine.elements[i]); + // Flop: register the next-state value + delayLine.elements[i] <= flop(clk, reset: reset, tapNext); + } + + // ── Coefficient bank — driven by tapCounter ── + // Build a LogicArray of constants from the coefficient list and + // pass it as an input port to CoeffBank (demonstrates addInputArray + // on a sub-module). + final coeffArray = LogicArray([numTaps], dataWidth, name: 'coeffArray'); + for (var i = 0; i < numTaps; i++) { + coeffArray.elements[i] <= Const(coefficients[i], width: dataWidth); + } + + final coeffBank = CoeffBank( + tapCounter, + coeffArray, + numTaps: numTaps, + dataWidth: dataWidth, + name: 'coeffBank', + ); + + // ── Delay-line mux — select sample for current tap ── + var selectedSample = delayLine.elements[0]; + for (var i = 1; i < numTaps; i++) { + final tapSelect = + tapCounter.eq(Const(i, width: tapIdxWidth)).named('tapSelect$i'); + selectedSample = mux(tapSelect, delayLine.elements[i], selectedSample) + .named('tapMux$i'); + } + + // ── Running accumulator (feedback register) ── + final accumReg = Logic(width: dataWidth, name: 'accumReg'); + // Reset accumulator at the start of each new sample (tap 0). + // Combinational block: equivalent to `always_comb` in SystemVerilog. + final accumFeedback = Logic(width: dataWidth, name: 'accumFeedback'); + Combinational([ + If(atFirstTap, then: [ + accumFeedback < Const(0, width: dataWidth), + ], orElse: [ + accumFeedback < accumReg, + ]), + ]); + + // ── Single MAC unit — time-multiplexed across taps ── + final mac = MacUnit( + selectedSample, + coeffBank.coeffOut, + accumFeedback, + clk, + reset, + enable, + dataWidth: dataWidth, + name: 'mac', + ); + + // Register the MAC result for accumulator feedback. + accumReg <= flop(clk, reset: reset, mac.result); + + // ── Tap counter: cycles 0 … numTaps-1 while enabled ── + // Sequential block: equivalent to `always_ff @(posedge clk)` in SV. + // When enabled, the counter increments and wraps at numTaps-1. + // When disabled, it resets to 0. + final lastTap = + tapCounter.eq(Const(numTaps - 1, width: tapIdxWidth)).named('lastTap'); + Sequential(clk, reset: reset, [ + If(enable, then: [ + If(lastTap, then: [ + tapCounter < Const(0, width: tapIdxWidth), + ], orElse: [ + tapCounter < tapCounter + Const(1, width: tapIdxWidth), + ]), + ], orElse: [ + tapCounter < Const(0, width: tapIdxWidth), + ]), + ]); + + // ── Output latch: capture accumulator when all taps processed ── + // The MAC pipeline has 2 stages, so the result is ready 2 cycles + // after the last tap enters. A 2-stage shift register of lastTap + // creates the latch strobe. + final lastTapD1 = Logic(name: 'lastTapD1'); + final lastTapD2 = Logic(name: 'lastTapD2'); + final outputReg = Logic(width: dataWidth, name: 'outputReg'); + + // Sequential block with If: latch strobe delay and output register. + Sequential(clk, reset: reset, [ + lastTapD1 < lastTap, + lastTapD2 < lastTapD1, + If(lastTapD2, then: [ + outputReg < accumReg, + ]), + ]); + + // ── Valid pipeline: track whether we have a valid output ── + // validIn is high during data injection. After the MAC pipeline + // latency (numTaps + 2 cycles), outputs become valid. + final validPipe = Logic(name: 'validPipe'); + final outputReady = (lastTapD2 & enable).named('outputReady'); + + // Sequential block: register the valid strobe and hold it. + Sequential(clk, reset: reset, [ + If(enable, then: [ + validPipe < outputReady, + ]), + ]); + + // Combinational block: gate the output to zero when not valid. + final dataOut = intf.dataOut; + final validOut = intf.validOut; + Combinational([ + If(validPipe, then: [ + dataOut < outputReg, + ], orElse: [ + dataOut < Const(0, width: dataWidth), + ]), + validOut < validPipe, + ]); + } + + /// Minimum bits needed to represent [n] values. + static int _bitsFor(int n) { + if (n <= 1) { + return 1; + } + var bits = 0; + var v = n - 1; + while (v > 0) { + bits++; + v >>= 1; + } + return bits; + } +} diff --git a/example/filter_bank/filter_controller.dart b/example/filter_bank/filter_controller.dart new file mode 100644 index 000000000..cfc730ef4 --- /dev/null +++ b/example/filter_bank/filter_controller.dart @@ -0,0 +1,177 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_controller.dart +// FSM controller module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// States for the [FilterController] finite state machine. +enum FilterState { + /// Waiting for the start signal. + idle, + + /// Accepting initial samples into the delay line. + loading, + + /// Normal filtering operation. + running, + + /// Flushing the pipeline after the input stream ends. + draining, + + /// Processing complete. + done, +} + +/// Controls the filter bank operation via a [FiniteStateMachine]. +/// +/// - idle: waiting for start signal +/// - loading: accepting initial samples into delay line +/// - running: normal filtering +/// - draining: flushing pipeline after input stream ends +/// - done: processing complete +class FilterController extends Module { + /// Encoded FSM state (3 bits). + Logic get state => output('state'); + + /// High while the filter channels should be processing. + Logic get filterEnable => output('filterEnable'); + + /// High during the initial sample-loading phase. + Logic get loadingPhase => output('loadingPhase'); + + /// Asserted when the filter bank has finished processing. + Logic get doneFlag => output('doneFlag'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input valid. + @protected + Logic get inputValidPin => input('inputValid'); + + /// Input done. + @protected + Logic get inputDonePin => input('inputDone'); + + late final FiniteStateMachine _fsm; + + /// Returns the FSM's current state index for a given [FilterState]. + int? getStateIndex(FilterState s) => _fsm.getStateIndex(s); + + /// Creates a [FilterController] that sequences the filter bank. + /// + /// After [start] is asserted the FSM moves through loading → running + /// → draining (for [drainCycles] cycles) → done. + FilterController( + Logic clk, Logic reset, Logic start, Logic inputValid, Logic inputDone, + {required int drainCycles, super.name = 'FilterController'}) + : super(definitionName: 'FilterController') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputValid = addInput('inputValid', inputValid); + inputDone = addInput('inputDone', inputDone); + + final filterEnable = addOutput('filterEnable'); + final loadingPhase = addOutput('loadingPhase'); + final doneFlag = addOutput('doneFlag'); + final state = addOutput('state', width: 3); + + // Drain counter + final drainCount = Logic(width: 8, name: 'drainCount'); + final drainDone = + drainCount.eq(Const(drainCycles, width: 8)).named('drainDone'); + + _fsm = FiniteStateMachine( + clk, + reset, + FilterState.idle, + [ + State( + FilterState.idle, + events: { + start: FilterState.loading, + }, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.loading, + events: { + inputValid: FilterState.running, + }, + actions: [ + filterEnable < 1, + loadingPhase < 1, + doneFlag < 0, + ], + ), + State( + FilterState.running, + events: { + inputDone: FilterState.draining, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.draining, + events: { + drainDone: FilterState.done, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.done, + events: {}, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 1, + ], + ), + ], + ); + + state <= _fsm.currentState.zeroExtend(state.width); + + // Drain counter: Sequential block increments while draining, + // resets to zero otherwise. + final drainIdx = _fsm.getStateIndex(FilterState.draining)!; + final isDraining = Logic(name: 'isDraining'); + isDraining <= _fsm.currentState.eq(Const(drainIdx, width: _fsm.stateWidth)); + + Sequential(clk, reset: reset, [ + If(isDraining, then: [ + drainCount < drainCount + Const(1, width: 8), + ], orElse: [ + drainCount < Const(0, width: 8), + ]), + ]); + } +} diff --git a/example/filter_bank/filter_data_interface.dart b/example/filter_bank/filter_data_interface.dart new file mode 100644 index 000000000..06faf7dba --- /dev/null +++ b/example/filter_bank/filter_data_interface.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_data_interface.dart +// Interface definition for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Tags for grouping port directions in [FilterDataInterface]. +enum FilterPortTag { + /// Ports carrying data into the filter (`sampleIn`, `validIn`). + inputPorts, + + /// Ports carrying data out of the filter (`dataOut`, `validOut`). + outputPorts, +} + +/// An interface carrying sample data and control into/out of filter modules. +/// +/// Groups ports by [FilterPortTag] so that [connectIO] can wire +/// inputs and outputs in a single call. +class FilterDataInterface extends Interface { + /// Input sample data bus. + Logic get sampleIn => port('sampleIn'); + + /// Input valid strobe. + Logic get validIn => port('validIn'); + + /// Output filtered data bus. + Logic get dataOut => port('dataOut'); + + /// Output valid strobe. + Logic get validOut => port('validOut'); + + /// The data width used by this interface. + final int _dataWidth; + + /// Creates a [FilterDataInterface] with the given [dataWidth] + /// (default 16 bits). + FilterDataInterface({int dataWidth = 16}) : _dataWidth = dataWidth { + setPorts([ + Logic.port('sampleIn', dataWidth), + Logic.port('validIn'), + ], [ + FilterPortTag.inputPorts + ]); + + setPorts([ + Logic.port('dataOut', dataWidth), + Logic.port('validOut'), + ], [ + FilterPortTag.outputPorts + ]); + } + + @override + + /// Returns a new interface with the same data width. + FilterDataInterface clone() => FilterDataInterface(dataWidth: _dataWidth); +} diff --git a/example/filter_bank/filter_sample.dart b/example/filter_bank/filter_sample.dart new file mode 100644 index 000000000..290e9dc76 --- /dev/null +++ b/example/filter_bank/filter_sample.dart @@ -0,0 +1,51 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_sample.dart +// LogicStructure sample word for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// A structured signal bundling a data sample with metadata. +/// +/// Packs two fields — [data] and [valid] — into a single bus that can be +/// driven and sampled as a unit. Used throughout the +/// filter bank to carry tagged samples between modules. +class FilterSample extends LogicStructure { + /// The sample data word. + late final Logic data; + + /// Whether this sample is valid. + late final Logic valid; + + /// Creates a [FilterSample] with the given [dataWidth] (default 16) + /// and optional [name]. + FilterSample({int dataWidth = 16, String? name}) + : super( + [ + Logic(name: 'data', width: dataWidth), + Logic(name: 'valid'), + ], + name: name ?? 'filter_sample', + ) { + data = elements[0]; + valid = elements[1]; + } + + // Private constructor for clone to share element structure. + FilterSample._clone(super.elements, {required super.name}) { + data = elements[0]; + valid = elements[1]; + } + + @override + + /// Returns a structural clone of this sample, preserving element names. + FilterSample clone({String? name}) => FilterSample._clone( + elements.map((e) => e.clone(name: e.name)), + name: name ?? this.name, + ); +} diff --git a/example/filter_bank/mac_unit.dart b/example/filter_bank/mac_unit.dart new file mode 100644 index 000000000..0e63c6f59 --- /dev/null +++ b/example/filter_bank/mac_unit.dart @@ -0,0 +1,88 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// mac_unit.dart +// Multiply-accumulate module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A pipelined multiply-accumulate unit. +/// +/// Pipeline stage 0: multiply sample × coefficient +/// Pipeline stage 1: add product to running accumulator +class MacUnit extends Module { + /// Accumulated result. + Logic get result => output('result'); + + /// Sample data input. + @protected + Logic get sampleInPin => input('sampleIn'); + + /// Coefficient input. + @protected + Logic get coeffInPin => input('coeffIn'); + + /// Accumulator input. + @protected + Logic get accumInPin => input('accumIn'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Data width. + final int dataWidth; + + /// Creates a [MacUnit] that multiplies [sampleIn] by [coeffIn] in + /// stage 0 and adds the product to [accumIn] in stage 1. + /// + /// [clk], [reset], and [enable] control the pipeline registers. + MacUnit(Logic sampleIn, Logic coeffIn, Logic accumIn, Logic clk, Logic reset, + Logic enable, + {required this.dataWidth, super.name = 'MacUnit'}) + : super(definitionName: 'MacUnit_W$dataWidth') { + sampleIn = addInput('sampleIn', sampleIn, width: dataWidth); + coeffIn = addInput('coeffIn', coeffIn, width: dataWidth); + accumIn = addInput('accumIn', accumIn, width: dataWidth); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + final result = addOutput('result', width: dataWidth); + final stall = (~enable).named('stall', naming: Naming.mergeable); + + // A 2-stage pipeline: multiply, then accumulate + final pipe = Pipeline( + clk, + reset: reset, + stalls: [stall, stall], + stages: [ + // Stage 0: multiply + (p) => [ + // Product = sample * coefficient (truncated to dataWidth) + p.get(sampleIn) < + (p.get(sampleIn) * p.get(coeffIn)).named('product'), + ], + // Stage 1: accumulate + (p) => [ + p.get(sampleIn) < + (p.get(sampleIn) + p.get(accumIn)).named('macSum'), + ], + ], + signals: [sampleIn, coeffIn, accumIn], + ); + + result <= pipe.get(sampleIn); + } +} diff --git a/example/filter_bank/shared_data_bus.dart b/example/filter_bank/shared_data_bus.dart new file mode 100644 index 000000000..1d86462d5 --- /dev/null +++ b/example/filter_bank/shared_data_bus.dart @@ -0,0 +1,88 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// shared_data_bus.dart +// Bidirectional data bus module for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A module with a bidirectional data bus for loading/reading data. +/// +/// In real hardware, a shared data bus is common for: +/// - Loading filter coefficients from external memory +/// - Reading diagnostic status or filter output snapshots +/// +/// Direction is controlled by `writeEnable`: when high, the module's +/// internal [TriStateBuffer] drives `storedValue` onto `dataBus`; +/// when low, the external driver owns the bus and the module latches +/// the incoming value into a register. +/// +/// Exercises `addInOut` / `LogicNet` / [TriStateBuffer] / inout port +/// direction through the full ROHD stack: synthesis, hierarchy, +/// waveform capture, and DevTools rendering. +class SharedDataBus extends Module { + /// The bidirectional data bus port. + Logic get dataBus => inOut('dataBus'); + + /// The stored value (latched when the bus is driven externally). + Logic get storedValue => output('storedValue'); + + /// Write-enable input. + @protected + Logic get writeEnablePin => input('writeEnable'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Data width in bits. + final int dataWidth; + + /// Creates a [SharedDataBus] with a [dataWidth]-bit bidirectional port. + /// + /// [dataBusNet] is the external [LogicNet] to connect. + /// [writeEnable] controls bus direction: 1 = module drives bus, + /// 0 = external drives bus (module reads). + /// [clk] and [reset] provide synchronous storage. + SharedDataBus( + LogicNet dataBusNet, + Logic writeEnable, + Logic clk, + Logic reset, { + required this.dataWidth, + super.name = 'SharedDataBus', + }) : super(definitionName: 'SharedDataBus') { + final bus = addInOut('dataBus', dataBusNet, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + final storedValue = addOutput('storedValue', width: dataWidth); + + // Latch the bus value on clock edge when the external side is driving. + storedValue <= + flop( + clk, + bus, + reset: reset, + en: ~writeEnable, + resetValue: Const(0, width: dataWidth), + ); + + // Drive the latched value back onto the bus when writeEnable is high. + // TriStateBuffer drives its out (a LogicNet) with storedValue when + // enabled; otherwise it outputs high-Z. Joining out↔bus makes the + // two nets share the same wire. + TriStateBuffer(storedValue, enable: writeEnable, name: 'busDriver') + .out + .gets(bus); + } +} diff --git a/example/fir_filter.dart b/example/fir_filter.dart index 571bbd1ed..61bab07e9 100644 --- a/example/fir_filter.dart +++ b/example/fir_filter.dart @@ -1,3 +1,4 @@ +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // fir_filter.dart @@ -96,7 +97,7 @@ Future main({bool noPrint = false}) async { await firFilter.build(); // Generate SystemVerilog code. - final systemVerilogCode = firFilter.generateSynth(); + final systemVerilogCode = firFilter.dumpSystemVerilog().output; if (!noPrint) { // Print SystemVerilog code to console. print(systemVerilogCode); @@ -108,7 +109,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper. if (!noPrint) { - WaveDumper(firFilter); + firFilter.dumpWaves(); } // Let's set the initial setting. diff --git a/example/logic_array.dart b/example/logic_array.dart index 2cde075ce..dbe9a447e 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 = logicArrayExample.dumpSystemVerilog().output; if (!noPrint) { print(systemVerilogCode); } // Simulate the module if (!noPrint) { - WaveDumper(logicArrayExample); + logicArrayExample.dumpWaves(); } // Set the input values diff --git a/example/oven_fsm.dart b/example/oven_fsm.dart index 144ef14d9..44023fcbf 100644 --- a/example/oven_fsm.dart +++ b/example/oven_fsm.dart @@ -225,7 +225,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'oven.vcd'); + oven.dumpWaves(outputPath: 'oven.vcd'); } // Kick off the simulation. diff --git a/example/tree.dart b/example/tree.dart index 395be6a10..004031667 100644 --- a/example/tree.dart +++ b/example/tree.dart @@ -85,7 +85,7 @@ Future main({bool noPrint = false}) async { // Below will generate an output of the ROHD-generated SystemVerilog: await tree.build(); - final generatedSystemVerilog = tree.generateSynth(); + final generatedSystemVerilog = tree.dumpSystemVerilog().output; if (!noPrint) { print(generatedSystemVerilog); } diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..2248ac384 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,9 +1,18 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +// +// rohd.dart +// Main public API exports for the ROHD framework. +// +// 2026 July +// Author: ROHD Contributors +export 'src/diagnostics/diagnostics.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; +export 'src/fst/fst_types.dart'; +export 'src/fst/fst_writer.dart'; export 'src/interfaces/interfaces.dart'; export 'src/module.dart'; export 'src/modules/modules.dart'; @@ -12,6 +21,7 @@ export 'src/signals/signals.dart'; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; +export 'src/synthesizers/systemverilog/system_verilog_service.dart'; export 'src/utilities/naming.dart'; export 'src/values/values.dart'; export 'src/wave_dumper.dart'; diff --git a/lib/src/collections/iterable_removable_queue.dart b/lib/src/collections/iterable_removable_queue.dart index 06b7981e7..c6042da5d 100644 --- a/lib/src/collections/iterable_removable_queue.dart +++ b/lib/src/collections/iterable_removable_queue.dart @@ -58,7 +58,7 @@ class IterableRemovableQueue { return; } - if (_first == _last && _removeWhere!(_first!.item)) { + if (_first == _last && _removeWhere(_first!.item)) { // if size is 1 and its removable, then we can just clear the queue and be // done with it clear(); @@ -77,7 +77,7 @@ class IterableRemovableQueue { } while (_patrol != null) { - if (_removeWhere!(_patrol!.item)) { + if (_removeWhere(_patrol!.item)) { assert(size > 0, 'Should not be removing if size is already 0.'); if (_patrol == _first && _first == _last) { @@ -118,7 +118,7 @@ class IterableRemovableQueue { /// Also may remove items from the queue if they are indicated by /// [_removeWhere]. void add(T item) { - if (_removeWhere != null && _removeWhere!(item)) { + if (_removeWhere != null && _removeWhere(item)) { // If the item should be removed, we don't add it. return; } @@ -184,7 +184,7 @@ class IterableRemovableQueue { var element = _first; _IterableRemovableElement? previous; while (element != null) { - if (_removeWhere != null && _removeWhere!(element.item)) { + if (_removeWhere != null && _removeWhere(element.item)) { assert(size > 0, 'Should not be removing if size is already 0.'); previous?.next = element.next; diff --git a/lib/src/diagnostics/diagnostics.dart b/lib/src/diagnostics/diagnostics.dart new file mode 100644 index 000000000..150721f21 --- /dev/null +++ b/lib/src/diagnostics/diagnostics.dart @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// diagnostics.dart +// Barrel export for the diagnostics library. +// +// 2026 July 16 +// Author: Desmond Kirkpatrick + +export 'module_service.dart'; +export 'module_services.dart'; +export 'waveform_service.dart'; +export 'waveform_writer.dart'; diff --git a/lib/src/diagnostics/inspector_service.dart b/lib/src/diagnostics/inspector_service.dart index ef4d6ea10..3a8e50f26 100644 --- a/lib/src/diagnostics/inspector_service.dart +++ b/lib/src/diagnostics/inspector_service.dart @@ -8,6 +8,7 @@ // Author: Yao Jing Quek import 'dart:convert'; +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; extension _LogicDevToolUtils on Logic { @@ -74,8 +75,8 @@ extension _ModuleDevToolUtils on Module { /// `ModuleTree` implements the Singleton design pattern /// to ensure there is only one instance of it during runtime. /// -/// This class is used to maintain a tree-like structure -/// for managing modules in an application. +/// This class preserves the legacy DevTools inspector entry point for the +/// built module hierarchy. class ModuleTree { /// Private constructor used to initialize the Singleton instance. ModuleTree._(); @@ -86,8 +87,22 @@ class ModuleTree { static ModuleTree get instance => _instance; static final _instance = ModuleTree._(); - /// Stores the root Module instance. - static Module? rootModuleInstance; + Module? _rootModule; + + /// The root [Module] registered for hierarchy inspection. + @internal + Module? get rootModule => _rootModule; + + /// Sets the root [Module] used to produce downstream hierarchy JSON. + /// + /// This is kept as an internal setter instead of a writable field so callers + /// make the bridge explicit: [ModuleServices] is the public service registry, + /// while [ModuleTree] owns the legacy DevTools hierarchy JSON surface + /// consumed by downstream hierarchy adapters. + @internal + set rootModule(Module? module) { + _rootModule = module; + } /// Returns the `hierarchyString` as JSON. /// @@ -95,10 +110,12 @@ class ModuleTree { /// /// Returns: string representing hierarchical structure of modules in JSON /// format. - String get hierarchyJSON => - rootModuleInstance?.buildModuleTreeJsonSchema(rootModuleInstance!) ?? - json.encode({ - 'status': 'fail', - 'reason': 'module not yet build', - }); + String get hierarchyJson { + final rootModule = _rootModule; + return rootModule?.buildModuleTreeJsonSchema(rootModule) ?? + json.encode({ + 'status': 'fail', + 'reason': 'module not yet build', + }); + } } diff --git a/lib/src/diagnostics/module_service.dart b/lib/src/diagnostics/module_service.dart new file mode 100644 index 000000000..1a7d4e52d --- /dev/null +++ b/lib/src/diagnostics/module_service.dart @@ -0,0 +1,96 @@ +// 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 'dart:async'; + +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 named artifact produced by an [ArtifactProducingService]. +/// +/// Artifacts expose their content as bytes so services can retain data in +/// memory, generate it lazily, or stream it without requiring a filesystem. +class ModuleServiceArtifact { + /// Creates an artifact with [fileName], [mediaType], and byte [openRead]. + const ModuleServiceArtifact({ + required this.fileName, + required this.mediaType, + required Stream> Function() openRead, + }) : _openRead = openRead; + + /// The artifact filename, including its format-specific extension. + final String fileName; + + /// The IANA-style media type of the artifact. + final String mediaType; + + final Stream> Function() _openRead; + + /// Opens a new stream of the artifact's bytes. + Stream> openRead() => _openRead(); +} + +/// A [ModuleService] that produces named output artifacts. +/// +/// The output location is always a directory. [outputBaseName] defaults to the +/// module definition name, while each concrete service configuration determines +/// artifact extensions and layouts. +abstract class ArtifactProducingService implements ModuleService { + /// Creates an artifact-producing service for [module]. + ArtifactProducingService( + this.module, { + this.outputDirectory = '.', + String? outputBaseName, + }) : outputBaseName = outputBaseName ?? module.definitionName; + + /// The top-level [Module] this service operates on. + @override + final Module module; + + /// Directory receiving filesystem artifacts when this service writes them. + final String outputDirectory; + + /// Filename stem used for primary artifacts. + final String outputBaseName; + + /// The artifacts this service can provide. + Iterable get artifacts; +} + +/// An [ArtifactProducingService] that generates source-code text. +/// +/// Shared by language code-generation services, which all produce a combined +/// single-file [output]. +abstract class CodeGenService extends ArtifactProducingService { + /// Creates a code-generation service for [module]. + CodeGenService( + super.module, { + super.outputDirectory, + super.outputBaseName, + }); + + /// The combined single-file generated output (including any header). + String get output; +} diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..86696770e --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,79 @@ +// 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:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; + +/// A slim, type-keyed registry of [ModuleService]s. +/// +/// Services register themselves here on construction (keyed by their concrete +/// type) and are retrieved with [lookup]. The registry intentionally exposes +/// no per-format accessors: each service owns its own JSON and output methods, +/// reached through [lookup] or the service's own static `current` accessor. +/// +/// The registry references no specific service type, so it is identical across +/// all feature branches that contribute services. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJson] — set by [Module.build]. +class ModuleServices { + ModuleServices._(); + + /// The singleton instance. + static final ModuleServices instance = ModuleServices._(); + + // ─── Hierarchy (auto-registered by Module.build) ────────────── + + /// The most recently built top-level [Module]. + /// + /// Set automatically at the end of [Module.build]. + Module? get rootModule => ModuleTree.instance.rootModule; + + /// Sets the most recently built top-level [Module]. + /// + /// Intended for internal use by [Module.build] and test reset paths. + @internal + set rootModule(Module? module) { + ModuleTree.instance.rootModule = module; + } + + /// 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.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/output_file_writer.dart b/lib/src/diagnostics/output_file_writer.dart new file mode 100644 index 000000000..c4053fe7e --- /dev/null +++ b/lib/src/diagnostics/output_file_writer.dart @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// output_file_writer.dart +// Platform-neutral output file writer stub. +// +// 2026 July 5 +// Author: Desmond Kirkpatrick + +/// Writes [contents] to [path] on platforms that support file IO. +void writeOutputTextFile(String path, String contents) { + throw UnsupportedError('File output is not supported on this platform.'); +} diff --git a/lib/src/diagnostics/output_file_writer_io.dart b/lib/src/diagnostics/output_file_writer_io.dart new file mode 100644 index 000000000..6529fe6f3 --- /dev/null +++ b/lib/src/diagnostics/output_file_writer_io.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// output_file_writer_io.dart +// Native output file writer. +// +// 2026 July 5 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +/// Writes [contents] to [path], creating parent directories as needed. +void writeOutputTextFile(String path, String contents) { + File(path) + ..parent.createSync(recursive: true) + ..writeAsStringSync(contents); +} diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart new file mode 100644 index 000000000..5373472a6 --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,334 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_service.dart +// Base waveform service: capture module signal changes to waveform writers. +// +// 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/sanitizer.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +/// A waveform capture service that writes signal changes to a file. +/// +/// Selects the output backend via [format]; each format is emitted by a +/// dedicated [WaveformWriter] implementation ([VcdWaveformWriter] for +/// [WaveOutputFormat.vcd], [FstWaveformWriter] for [WaveOutputFormat.fst]). +class WaveformService extends ArtifactProducingService { + /// The most recently registered [WaveformService], or `null`. + static WaveformService? current; + + /// Exact output filename override. + /// + /// Prefer [outputBaseName] for new service code. This override exists for + /// compatibility with legacy APIs that accepted an arbitrary output path. + final String? outputFileName; + + /// Path of the output waveform file. + /// + /// Derived from [outputDirectory], [outputBaseName], [outputFileName], + /// and [format]. + String get outputPath => '$outputDirectory${Platform.pathSeparator}' + '${outputFileName ?? '$outputBaseName.${format.fileExtension}'}'; + + /// Output format. + final WaveOutputFormat format; + + /// Optional predicate that determines whether a given [Logic] signal is + /// captured. + final bool Function(Logic signal)? signalFilter; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Simulation time at which recording begins. + final int? startTime; + + /// Simulation time at which recording ends. + final int? stopTime; + + /// Number of characters accumulated in the VCD 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; + + /// The FST writer configuration (only used when [format] is + /// [WaveOutputFormat.fst]). + final FstWriterConfig? fstConfig; + + late final WaveformWriter _writer; + + /// Maps each captured [Logic] to its writer-specific signal handle. + final Map _signalHandles = {}; + + /// Signals that changed during the current simulation timestamp. + final Set _changedThisTimestamp = HashSet(); + + /// The timestamp currently being accumulated. + int _currentDumpingTimestamp = Simulator.time; + + /// Creates a [WaveformService] for [module]. + /// + /// [module] must be built before construction. [outputDirectory] defaults to + /// the current directory and [outputBaseName] defaults to + /// [Module.definitionName]; the on-disk file is + /// `/.`. Pass + /// [outputFileName] to override the filename explicitly. + WaveformService( + Module module, { + super.outputDirectory, + super.outputBaseName, + this.outputFileName, + this.format = WaveOutputFormat.vcd, + this.signalFilter, + this.timescale = '1ps', + this.startTime, + this.stopTime, + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + this.register = true, + this.fstConfig, + }) : super(module) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating WaveformService. ' + 'Call build() first.', + ); + } + + _writer = _createWriter(); + _collectSignals(module); + _writer.finishDeclarations( + _signalHandles.entries.map( + (entry) => WaveformInitialValue(entry.value, _binaryValue(entry.key)), + ), + timestamp: Simulator.time, + ); + + 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); + } + } + + /// Legacy factory that accepts a single `outputPath` argument. + /// + /// Splits [outputPath] into an [outputDirectory] and [outputFileName] and + /// delegates to the main constructor. Provided so that pre-services-API + /// callers of the form `WaveformService(module, outputPath: '/tmp/foo.vcd')` + /// still compile. + factory WaveformService.fromOutputPath( + Module module, { + required String outputPath, + WaveOutputFormat format = WaveOutputFormat.vcd, + bool Function(Logic signal)? signalFilter, + String timescale = '1ps', + int? startTime, + int? stopTime, + int flushBufferSize = 100000, + OverwritePolicy overwritePolicy = OverwritePolicy.overwrite, + bool register = true, + FstWriterConfig? fstConfig, + }) { + final normalized = outputPath.replaceAll(r'\', '/'); + final sep = normalized.lastIndexOf('/'); + final directory = switch (sep) { + -1 => '.', + 0 => '/', + _ => normalized.substring(0, sep), + }; + final filename = normalized.substring(sep + 1); + return WaveformService( + module, + outputDirectory: directory, + outputFileName: filename, + format: format, + signalFilter: signalFilter, + timescale: timescale, + startTime: startTime, + stopTime: stopTime, + flushBufferSize: flushBufferSize, + overwritePolicy: overwritePolicy, + register: register, + fstConfig: fstConfig, + ); + } + + /// The concrete output writer used by this service. + @protected + WaveformWriter get writer => _writer; + + /// Called once for each [Logic] signal that passes [signalFilter]. + @protected + void onSignalCollected(Logic signal) {} + + /// Called for every value-change event on [signal] at [timestamp]. + @protected + void onValueChange(Logic signal, int timestamp) {} + + /// Called once per simulation timestamp that contains at least one change. + @protected + void onTimestampCapture(int timestamp, Set changed) {} + + /// Called after the final timestamp has been written and the file is closed. + @protected + void onSimulationEnd() {} + + WaveformWriter _createWriter() { + switch (format) { + case WaveOutputFormat.vcd: + return VcdWaveformWriter( + outputPath, + timescale: timescale, + flushBufferSize: flushBufferSize, + overwritePolicy: overwritePolicy, + ); + case WaveOutputFormat.fst: + return FstWaveformWriter( + outputPath, + config: fstConfig ?? const FstWriterConfig(), + ); + } + } + + bool _collectSignals(Module module) { + final moduleSignalUniquifier = Uniquifier(); + var hasContents = false; + + _writer.pushScope(module.uniqueInstanceName); + + for (final sig in module.signals) { + if (sig is Const) { + continue; + } + if (signalFilter != null && !signalFilter!(sig)) { + continue; + } + + hasContents = true; + final baseName = Sanitizer.sanitizeSV(sig.name); + final signalName = moduleSignalUniquifier.getUniqueName( + initialName: baseName, + reserved: sig.isPort, + ); + final handle = _writer.declareSignal( + signalName, + sig.width, + direction: _directionOf(sig), + ); + _signalHandles[sig] = handle; + onSignalCollected(sig); + + sig.changed.listen((_) { + _changedThisTimestamp.add(sig); + }); + } + + for (final subModule in module.subModules) { + if (subModule is InlineSystemVerilog) { + continue; + } + hasContents = _collectSignals(subModule) || hasContents; + } + + _writer.popScope(); + return hasContents; + } + + WaveformSignalDirection _directionOf(Logic signal) { + if (!signal.isPort) { + return WaveformSignalDirection.implicit; + } + return signal.isInput + ? WaveformSignalDirection.input + : WaveformSignalDirection.output; + } + + 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; + } + + final snapshot = Set.of(_changedThisTimestamp); + final changes = [ + for (final sig in snapshot) + WaveformValueChange(_signalHandles[sig]!, _binaryValue(sig)), + ]; + + if (changes.isNotEmpty) { + _writer.emitValueChanges(timestamp, changes); + } + + for (final sig in snapshot) { + onValueChange(sig, timestamp); + } + _changedThisTimestamp.clear(); + + if (snapshot.isNotEmpty) { + onTimestampCapture(timestamp, snapshot); + } + } + + String _binaryValue(Logic signal) => signal.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); + + Future _terminate() => _writer.close(); + + /// The artifacts this service produces. + /// + /// The waveform is written on-the-fly through [WaveformWriter], so this + /// service does not retain artifacts to report. + @override + Iterable get artifacts => const []; + + /// Returns a JSON-serialisable summary of this service. + @override + Map toJson() => { + 'outputPath': outputPath, + 'format': format.name, + 'signalCount': _signalHandles.length, + 'timescale': timescale, + if (startTime != null) 'startTime': startTime, + if (stopTime != null) 'stopTime': stopTime, + 'writer': _writer.toJson(), + }; +} diff --git a/lib/src/diagnostics/waveform_writer.dart b/lib/src/diagnostics/waveform_writer.dart new file mode 100644 index 000000000..c9156da8f --- /dev/null +++ b/lib/src/diagnostics/waveform_writer.dart @@ -0,0 +1,352 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_writer.dart +// Common output backend API for waveform capture services. +// +// 2026 July 17 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; + +/// 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. + fst; + + /// The filename extension associated with this format. + String get fileExtension => switch (this) { + WaveOutputFormat.vcd => 'vcd', + WaveOutputFormat.fst => 'fst', + }; + + /// The media type associated with this format. + String get mediaType => switch (this) { + WaveOutputFormat.vcd => 'text/x-vcd', + WaveOutputFormat.fst => 'application/vnd.gtkwave.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, +} + +/// Direction metadata for a signal emitted into a waveform file. +enum WaveformSignalDirection { + /// Input port. + input, + + /// Output port. + output, + + /// Internal or implicit signal. + implicit, +} + +/// Initial value for a declared waveform signal. +class WaveformInitialValue { + /// The writer-specific handle returned by [WaveformWriter.declareSignal]. + final Object handle; + + /// The MSB-first binary value string. + final String value; + + /// Creates an initial value entry. + const WaveformInitialValue(this.handle, this.value); +} + +/// Timestamped value change for a declared waveform signal. +class WaveformValueChange extends WaveformInitialValue { + /// Creates a value-change entry. + const WaveformValueChange(super.handle, super.value); +} + +/// Common backend contract for waveform file formats. +abstract class WaveformWriter { + /// The file format emitted by this writer. + WaveOutputFormat get format; + + /// Pushes a scope onto the declaration hierarchy. + void pushScope(String name); + + /// Pops the current declaration scope. + void popScope(); + + /// Declares a signal and returns a writer-specific handle. + Object declareSignal( + String name, + int width, { + required WaveformSignalDirection direction, + }); + + /// Finishes declarations and emits initial values. + void finishDeclarations( + Iterable initialValues, { + required int timestamp, + }); + + /// Emits all value changes for [timestamp]. + void emitValueChanges(int timestamp, Iterable changes); + + /// Flushes and closes the waveform output. + Future close(); + + /// Returns a JSON-serialisable summary of writer state. + Map toJson(); +} + +/// VCD implementation of [WaveformWriter]. +class VcdWaveformWriter implements WaveformWriter { + /// Creates a VCD writer at [outputPath]. + VcdWaveformWriter( + this.outputPath, { + this.timescale = '1ps', + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + }) { + if (overwritePolicy == OverwritePolicy.failIfExists) { + final existingFile = File(outputPath); + if (existingFile.existsSync()) { + throw FileSystemException( + 'Waveform output file already exists and overwritePolicy is ' + 'failIfExists.', + outputPath, + ); + } + } + + _outputFile = File(outputPath)..createSync(recursive: true); + _outFileSink = _outputFile.openWrite(); + _writeHeader(); + } + + /// The output file path. + final String outputPath; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Number of characters accumulated before flushing to disk. + final int flushBufferSize; + + /// Existing-file policy. + final OverwritePolicy overwritePolicy; + + late final File _outputFile; + late final IOSink _outFileSink; + final StringBuffer _fileBuffer = StringBuffer(); + final StringBuffer _scopeBuffer = StringBuffer(); + final Map _handleWidths = {}; + var _signalMarkerIdx = 0; + var _indent = 0; + var _closed = false; + + @override + WaveOutputFormat get format => WaveOutputFormat.vcd; + + @override + void pushScope(String name) { + final padding = List.filled(_indent, ' ').join(); + _scopeBuffer.write('$padding\$scope module $name \$end\n'); + _indent++; + } + + @override + void popScope() { + _indent--; + final padding = List.filled(_indent, ' ').join(); + _scopeBuffer.write('$padding\$upscope \$end\n'); + } + + @override + Object declareSignal( + String name, + int width, { + required WaveformSignalDirection direction, + }) { + final marker = 's${_signalMarkerIdx++}'; + final padding = List.filled(_indent, ' ').join(); + _scopeBuffer.write('$padding\$var wire $width $marker $name \$end\n'); + _handleWidths[marker] = width; + return marker; + } + + @override + void finishDeclarations( + Iterable initialValues, { + required int timestamp, + }) { + _writeToBuffer(_scopeBuffer.toString()); + _writeToBuffer('\$enddefinitions \$end\n'); + _writeToBuffer('\$dumpvars\n'); + for (final initialValue in initialValues) { + _writeValueUpdate(initialValue.handle, initialValue.value); + } + _writeToBuffer('\$end\n'); + } + + @override + void emitValueChanges( + int timestamp, + Iterable changes, + ) { + _writeToBuffer('#$timestamp\n'); + for (final change in changes) { + _writeValueUpdate(change.handle, change.value); + } + } + + @override + Future close() async { + if (_closed) { + return; + } + _closed = true; + _flushBuffer(); + await _outFileSink.flush(); + await _outFileSink.close(); + } + + @override + Map toJson() => { + 'format': format.name, + 'signalCount': _handleWidths.length, + 'timescale': timescale, + }; + + 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 _writeValueUpdate(Object handle, String value) { + final width = _handleWidths[handle]; + if (width == null) { + throw StateError('Unknown VCD signal handle: $handle'); + } + final updateValue = width > 1 ? 'b$value ' : value; + _writeToBuffer('$updateValue$handle\n'); + } + + void _writeToBuffer(String contents) { + _fileBuffer.write(contents); + if (_fileBuffer.length > flushBufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + _outFileSink.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } +} + +/// FST implementation of [WaveformWriter]. +class FstWaveformWriter implements WaveformWriter { + /// Creates an FST writer at [outputPath]. + FstWaveformWriter( + String outputPath, { + FstWriterConfig config = const FstWriterConfig(), + }) : writer = FstWriter(outputPath, config: config); + + /// The low-level FST binary writer. + final FstWriter writer; + + @override + WaveOutputFormat get format => WaveOutputFormat.fst; + + @override + void pushScope(String name) { + writer.pushScope(name); + } + + @override + void popScope() { + writer.popScope(); + } + + @override + Object declareSignal( + String name, + int width, { + required WaveformSignalDirection direction, + }) => + writer.declareSignal( + name, + width, + direction: _fstDirection(direction), + ); + + @override + void finishDeclarations( + Iterable initialValues, { + required int timestamp, + }) { + writer.writeHeader(); + for (final initialValue in initialValues) { + writer.emitValueChange( + timestamp, + initialValue.handle as FstSignalHandle, + initialValue.value, + ); + } + } + + @override + void emitValueChanges( + int timestamp, + Iterable changes, + ) { + for (final change in changes) { + writer.emitValueChange( + timestamp, + change.handle as FstSignalHandle, + change.value, + ); + } + } + + @override + Future close() async { + writer.finish(); + } + + @override + Map toJson() => { + 'format': format.name, + }; + + FstVarDirection _fstDirection(WaveformSignalDirection direction) { + switch (direction) { + case WaveformSignalDirection.input: + return FstVarDirection.input; + case WaveformSignalDirection.output: + return FstVarDirection.output; + case WaveformSignalDirection.implicit: + return FstVarDirection.implicit; + } + } +} diff --git a/lib/src/fst/fst_types.dart b/lib/src/fst/fst_types.dart new file mode 100644 index 000000000..13e829c28 --- /dev/null +++ b/lib/src/fst/fst_types.dart @@ -0,0 +1,236 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_types.dart +// Enumerations and constants for the FST (Fast Signal Trace) binary format. +// +// 2026 February +// Author: Desmond Kirkpatrick + +/// FST block types (from fstapi.h). +enum FstBlockType { + /// File header. + header(0), + + /// Value change data (zlib compressed). + vcData(1), + + /// Blackout regions. + blackout(2), + + /// Geometry (per-variable back-pointers for random access). + geometry(3), + + /// Hierarchy (zlib compressed). + hierarchy(4), + + /// Value changes with dynamic aliases (zlib). + vcDataDynamicAlias(5), + + /// Hierarchy (LZ4 compressed). + hierarchyLz4(6), + + /// Hierarchy (LZ4 double compressed). + hierarchyLz4Duo(7), + + /// Value changes with dynamic aliases v2 (modern recommended format). + vcDataDynamicAlias2(8), + + /// GZip wrapper. + gzipWrapper(254), + + /// Skip/padding. + skip(255); + + const FstBlockType(this.value); + + /// The numeric value of this block type as written in FST files. + final int value; +} + +/// FST scope types. +enum FstScopeType { + /// A Verilog/SystemVerilog module instantiation scope. + module(0), + + /// A Verilog/SystemVerilog task scope. + task(1), + + /// A Verilog/SystemVerilog function scope. + function_(2), + + /// A named `begin`..`end` block scope (Verilog). + begin(3), + + /// A named `fork`..`join` block scope (Verilog). + fork(4), + + /// A `generate` block scope (SystemVerilog). + generate(5), + + /// A `struct` type scope (SystemVerilog). + struct_(6), + + /// A `union` type scope (SystemVerilog). + union(7), + + /// A `class` scope (SystemVerilog). + class_(8), + + /// An `interface` scope (SystemVerilog). + interface(9), + + /// A `package` scope (SystemVerilog). + package(10), + + /// A `program` scope (SystemVerilog). + program(11); + + const FstScopeType(this.value); + + /// The numeric value of this scope type as written in FST files. + final int value; +} + +/// FST variable types. +enum FstVarType { + /// An event variable. + event(0), + + /// A Verilog `integer` variable (32-bit, 4-state). + integer(1), + + /// A Verilog `parameter` or `localparam`. + parameter(2), + + /// A `real` variable (double-precision floating point). + real(3), + + /// A `real` parameter. + realParameter(4), + + /// A `reg` variable (Verilog 4-state storage). + reg(5), + + /// A `supply0` net (logic-0 power supply). + supply0(6), + + /// A `supply1` net (logic-1 power supply). + supply1(7), + + /// A `time` variable. + time(8), + + /// A `tri` net (tri-state, same resolution as `wire`). + tri(9), + + /// A `triand` net (tri-state with wired-AND resolution). + triAnd(10), + + /// A `trior` net (tri-state with wired-OR resolution). + triOr(11), + + /// A `trireg` net (retains last driven value when undriven). + triReg(12), + + /// A `tri0` net (pulls to 0 when undriven). + tri0(13), + + /// A `tri1` net (pulls to 1 when undriven). + tri1(14), + + /// A `wand` net (wired-AND). + wand(15), + + /// A `wire` net (standard Verilog interconnect). + wire(16), + + /// A `wor` net (wired-OR). + wor(17), + + /// A port variable. + port(18), + + /// A sparse array variable. + sparseArray(19), + + /// A `realtime` variable. + realTime(20), + + /// A generic string variable. + genericString(21), + + // SystemVerilog types + + /// A SystemVerilog `bit` type (2-state, unsigned). + bit(22), + + /// A SystemVerilog `logic` type (4-state). + logic(23), + + /// A SystemVerilog `int` type (32-bit, 2-state, signed). + int_(24), + + /// A SystemVerilog `shortint` type (16-bit, 2-state, signed). + shortInt(25), + + /// A SystemVerilog `longint` type (64-bit, 2-state, signed). + longInt(26), + + /// A SystemVerilog `byte` type (8-bit, 2-state, signed). + byte_(27), + + /// A SystemVerilog `enum` type. + enum_(28), + + /// A SystemVerilog `shortreal` type (single-precision float). + shortReal(29); + + const FstVarType(this.value); + + /// The numeric value of this variable type as written in FST files. + final int value; +} + +/// FST variable direction. +enum FstVarDirection { + /// No direction specified (implicit net). + implicit(0), + + /// Input port. + input(1), + + /// Output port. + output(2), + + /// Bidirectional (inout) port. + inout(3), + + /// Buffer port (output that can be read back). + buffer(4), + + /// Linkage port (VHDL linkage mode). + linkage(5); + + const FstVarDirection(this.value); + + /// The numeric value of this direction as written in FST files. + final int value; +} + +/// FST file type. +enum FstFileType { + /// Verilog source. + verilog(0), + + /// VHDL source. + vhdl(1), + + /// Mixed Verilog and VHDL source. + verilogVhdl(2); + + const FstFileType(this.value); + + /// The numeric value of this file type as written in FST files. + final int value; +} diff --git a/lib/src/fst/fst_writer.dart b/lib/src/fst/fst_writer.dart new file mode 100644 index 000000000..69c864067 --- /dev/null +++ b/lib/src/fst/fst_writer.dart @@ -0,0 +1,1045 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_writer.dart +// Pure Dart implementation of FST (Fast Signal Trace) binary writer. +// +// Writes FST files compatible with GTKWave, Surfer, and wellen/fst-reader. +// Implements the public FST binary format in pure Dart. +// +// 2026 February +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; +import 'package:rohd/rohd.dart'; + +/// Configuration for the FST writer. +class FstWriterConfig { + /// Timescale exponent. The timescale is 10^exponent seconds. + /// Default: -12 (picoseconds). + final int timescaleExponent; + + /// Zlib compression level (0-9). Higher = smaller but slower. + /// Default: 4. + final int compressionLevel; + + /// Writer version string embedded in the file header. + final String version; + + /// File type: Verilog, VHDL, or combined. + final FstFileType fileType; + + /// Maximum number of value changes to buffer before auto-flushing + /// a VcData block to disk. Set to 0 (default) to disable auto-flush + /// and write a single block at [FstWriter.finish]. + /// + /// When non-zero, [FstWriter.emitValueChange] automatically calls + /// [FstWriter.flushBlock] once the buffer reaches this threshold. + /// This bounds memory usage and makes historical data available on + /// disk for read-back. + final int maxChangesPerBlock; + + /// Creates configuration for the FST writer. + const FstWriterConfig({ + this.timescaleExponent = -12, + this.compressionLevel = 4, + this.version = 'ROHD FST Writer', + this.fileType = FstFileType.verilog, + this.maxChangesPerBlock = 0, + }); +} + +/// A handle to a declared signal in the FST file. +/// +/// Handles are 1-based (matching VST convention). Index 0 is unused. +class FstSignalHandle { + /// The 1-based handle value. + final int handle; + + /// Creates a signal handle from a 1-based handle value. + const FstSignalHandle(this.handle); +} + +/// Metadata about a flushed VcData block in the FST file. +/// +/// Each entry in [FstWriter.blockIndex] represents a block that has been +/// written to disk and can be read back independently for on-demand +/// signal queries without loading the entire file into memory. +class FstBlockIndex { + /// File offset of the block_type byte in the FST file. + final int fileOffset; + + /// Section length (the section_length field from the block header). The full + /// block occupies bytes [fileOffset .. fileOffset + 1 + sectionLength). + final int sectionLength; + + /// First timestamp in this block. + final int startTime; + + /// Last timestamp in this block. + final int endTime; + + /// Creates a block index entry. + const FstBlockIndex({ + required this.fileOffset, + required this.sectionLength, + required this.startTime, + required this.endTime, + }); +} + +/// Public metadata about a declared signal in the FST writer. +class FstSignalInfo { + /// Signal name. + final String name; + + /// Bit width (number of bits for digital signals, 8 for real). + final int width; + + /// Whether this is a real-valued (f64) signal. + final bool isReal; + + /// Creates signal info. + const FstSignalInfo({ + required this.name, + required this.width, + required this.isReal, + }); +} + +/// Internal: information about a declared signal. +class _SignalDecl { + final String name; + final int width; + final FstVarType varType; + final FstVarDirection direction; + final bool isReal; + + _SignalDecl({ + required this.name, + required this.width, + required this.varType, + required this.direction, + this.isReal = false, + }); + + /// The geometry file_format value for this signal. + int get geometryValue { + if (isReal) { + return 0; + } + return width; // 1 for 1-bit, N for N-bit + } + + /// The number of bytes this signal occupies in the frame section. + int get frameLength { + if (isReal) { + return 8; + } + return width; // 1 byte per bit for character-encoded values + } +} + +/// Internal: a buffered value change. +class _ValueChange { + final int time; + final int handleIndex; // 0-based + final String value; + + _ValueChange(this.time, this.handleIndex, this.value); +} + +/// Internal: an entry in the hierarchy being built. +sealed class _HierarchyEntry {} + +class _ScopeEntry extends _HierarchyEntry { + final FstScopeType type; + final String name; + final String component; + _ScopeEntry(this.type, this.name, {this.component = ''}); +} + +class _UpScopeEntry extends _HierarchyEntry {} + +class _VarEntry extends _HierarchyEntry { + final FstVarType varType; + final FstVarDirection direction; + final String name; + final int width; + final int handle; // 1-based + _VarEntry(this.varType, this.direction, this.name, this.width, this.handle); +} + +/// Pure Dart writer for the FST (Fast Signal Trace) binary format. +/// +/// Usage: +/// ```dart +/// final writer = FstWriter('output.fst'); +/// writer.pushScope('top'); +/// final clk = writer.declareSignal('clk', 1); +/// final data = writer.declareSignal('data', 8); +/// writer.popScope(); +/// writer.writeHeader(); +/// +/// writer.emitValueChange(0, clk, '0'); +/// writer.emitValueChange(0, data, '00000000'); +/// writer.emitValueChange(5, clk, '1'); +/// writer.emitValueChange(10, clk, '0'); +/// +/// writer.finish(); +/// ``` +class FstWriter { + /// The output file path. + final String filePath; + + /// Writer configuration. + final FstWriterConfig config; + + /// All declared signals (0-indexed). + final List<_SignalDecl> _signals = []; + + /// Hierarchy entries in declaration order. + final List<_HierarchyEntry> _hierEntries = []; + + /// Scope counts for header. + int _scopeCount = 0; + + /// Variable counts for header (including aliases). + int _varCount = 0; + + /// Buffered value changes. + final List<_ValueChange> _changes = []; + + /// The start time of the simulation. + int _startTime = 0; + + /// The end time of the simulation. + int _endTime = 0; + + /// Whether the header has been written yet. + bool _headerWritten = false; + + /// The output file random access handle. + late final RandomAccessFile _file; + + /// Current value of each signal (tracks latest emitted value). + /// Initialized in [writeHeader]. + late List _currentValues; + + /// Base values for the next block's frame section. + /// Updated after each [flushBlock] call. + late List _nextFrameBase; + + /// Index of flushed VcData blocks for read-back. + final List _blockIndex = []; + + /// Number of VcData blocks written so far. + int _vcSectionCount = 0; + + /// Creates an FST writer that will write to [filePath]. + FstWriter(this.filePath, {this.config = const FstWriterConfig()}) { + final file = File(filePath)..createSync(recursive: true); + _file = file.openSync(mode: FileMode.write); + } + + /// Pushes a new scope onto the hierarchy. + void pushScope( + String name, { + FstScopeType type = FstScopeType.module, + String component = '', + }) { + _hierEntries.add(_ScopeEntry(type, name, component: component)); + _scopeCount++; + } + + /// Pops the current scope. + void popScope() { + _hierEntries.add(_UpScopeEntry()); + } + + /// Declares a signal and returns its handle. + /// + /// [name] is the signal name. [width] is the bit width (1 for single bit). + /// Returns an [FstSignalHandle] used for emitting value changes. + FstSignalHandle declareSignal( + String name, + int width, { + FstVarType varType = FstVarType.wire, + FstVarDirection direction = FstVarDirection.implicit, + }) { + final handle = _signals.length + 1; // 1-based + final decl = _SignalDecl( + name: name, + width: width, + varType: varType, + direction: direction, + isReal: varType == FstVarType.real || varType == FstVarType.realParameter, + ); + _signals.add(decl); + _hierEntries.add(_VarEntry(varType, direction, name, width, handle)); + _varCount++; + return FstSignalHandle(handle); + } + + /// Writes the FST file header. + /// + /// Must be called after all signals are declared and before any value + /// changes. The header is initially written with placeholder values for + /// start_time and end_time, which are fixed up during [finish]. + void writeHeader() { + if (_headerWritten) { + throw StateError('Header already written'); + } + _writeHeaderBlock(); + _headerWritten = true; + + // Initialize value tracking for incremental block flushing + final defaults = List.generate(_signals.length, (i) { + final sig = _signals[i]; + return sig.isReal ? '0.0' : 'x' * sig.width; + }); + _currentValues = List.from(defaults); + _nextFrameBase = List.from(defaults); + } + + /// Records a value change for a signal at a given simulation time. + /// + /// [time] is the simulation timestamp. + /// [handle] is the signal handle returned by [declareSignal]. + /// [value] is the new value as a string (e.g., '0', '1', '01010101', 'x'). + void emitValueChange(int time, FstSignalHandle handle, String value) { + if (!_headerWritten) { + throw StateError('Must call writeHeader() before emitting value changes'); + } + if (_endTime < time) { + _endTime = time; + } + _changes.add(_ValueChange(time, handle.handle - 1, value)); + _currentValues[handle.handle - 1] = value; + + // Auto-flush if threshold is reached + if (config.maxChangesPerBlock > 0 && + _changes.length >= config.maxChangesPerBlock) { + flushBlock(); + } + } + + /// Finalizes the FST file: flushes remaining value changes, writes + /// geometry and hierarchy blocks, fixes up the header, and closes the file. + void finish() { + if (!_headerWritten) { + writeHeader(); + } + + // Flush any remaining buffered changes as a final VcData block + flushBlock(); + + _writeGeometryBlock(); + _writeHierarchyBlock(); + _fixupHeader(); + + _file.closeSync(); + } + + /// Releases resources. Call [finish] first for a valid file. + void dispose() { + try { + _file.closeSync(); + } on FileSystemException { + // already closed + } + } + + /// Flushes buffered value changes to disk as a VcData block. + /// + /// After flushing, the changes are cleared from memory and the block + /// is recorded in [blockIndex] for later read-back. This enables + /// incremental writing where only recent unflushed changes remain + /// in memory while historical data lives on disk. + /// + /// Does nothing if no changes are buffered. + void flushBlock() { + if (_changes.isEmpty) { + return; + } + if (!_headerWritten) { + throw StateError('Must call writeHeader() before flushing blocks'); + } + + // Sort changes by time, then by handle + _changes.sort((a, b) { + final cmp = a.time.compareTo(b.time); + return cmp != 0 ? cmp : a.handleIndex.compareTo(b.handleIndex); + }); + + final blockStart = _changes.first.time; + final blockEnd = _changes.last.time; + + // Build frame: carry-over state from previous block, overridden by + // any changes at this block's start time. + final frameValues = List.from(_nextFrameBase); + for (final c in _changes) { + if (c.time == blockStart) { + frameValues[c.handleIndex] = c.value; + } + } + + final blockOffset = _file.positionSync(); + _writeVcDataBlock( + blockStartTime: blockStart, + blockEndTime: blockEnd, + frameValues: frameValues, + ); + final blockEndPos = _file.positionSync(); + + // Record block in the index for read-back + _blockIndex.add( + FstBlockIndex( + fileOffset: blockOffset, + sectionLength: blockEndPos - blockOffset - 1, + startTime: blockStart, + endTime: blockEnd, + ), + ); + _vcSectionCount++; + + // Update global time range + if (_vcSectionCount == 1) { + _startTime = blockStart; + } + _endTime = blockEnd; + + // Carry-over state for next block's frame + _nextFrameBase = List.from(_currentValues); + _changes.clear(); + } + + // ─── Public query API for hybrid disk+memory access ─── + + /// Index of all flushed VcData blocks. + /// + /// Each entry contains the file offset and time range, enabling + /// the `FstBlockReader` to read specific blocks on demand. + List get blockIndex => List.unmodifiable(_blockIndex); + + /// Number of declared signals. + int get signalCount => _signals.length; + + /// Public metadata about each declared signal (indexed by handle-1). + List get signalInfoList => _signals + .map((s) => FstSignalInfo(name: s.name, width: s.width, isReal: s.isReal)) + .toList(); + + /// The output file handle for read-back by `FstBlockReader`. + /// + /// **Warning**: The caller must not close or modify the file position + /// without restoring it. The writer uses this same handle for writing. + RandomAccessFile get file => _file; + + /// Query unflushed value changes for a specific signal handle. + /// + /// Returns changes from the hot buffer for signal [handleIndex] (0-based) + /// within the time range \[startTime, endTime\]. + List<({int time, String value})> queryHotBuffer( + int handleIndex, + int startTime, + int endTime, + ) => + _changes + .where( + (c) => + c.handleIndex == handleIndex && + c.time >= startTime && + c.time <= endTime, + ) + .map((c) => (time: c.time, value: c.value)) + .toList(); + + /// Returns the current (latest) value of signal [handleIndex] (0-based). + String getCurrentValue(int handleIndex) => _currentValues[handleIndex]; + + /// Returns the latest known values of all signals (read-only). + List get currentValues => List.unmodifiable(_currentValues); + + // ─────────────── Header Block ─────────────── + + static const int _headerLength = 329; + static const int _headerVersionMaxLen = 128; + static const int _headerDateMaxLen = 119; + + /// Writes the FST_BL_HDR block. + void _writeHeaderBlock() { + _file.writeByteSync(FstBlockType.header.value); + _writeU64(_headerLength); // section_length (fixed size) + _writeU64(_startTime); // start_time (placeholder) + _writeU64(_endTime); // end_time (placeholder) + _writeF64LE(math.e); // double endian test + _writeU64(0); // memory_used_by_writer + _writeU64(_scopeCount); // scope_count + _writeU64(_varCount); // var_count + _writeU64(_signals.length); // max_var_id_code + _writeU64(1); // vc_section_count (we write one block) + _file.writeByteSync(config.timescaleExponent & 0xFF); // timescale_exponent + _writeFixedString(config.version, _headerVersionMaxLen); + _writeFixedString(_dateString(), _headerDateMaxLen); + _file.writeByteSync(config.fileType.value); // file_type + _writeU64(0); // time_zero + } + + /// Fixes up the header with actual start/end times and block count. + void _fixupHeader() { + final savedPos = _file.positionSync(); + _file.setPositionSync(1 + 8); // skip block_type + section_length + _writeU64(_startTime); + _writeU64(_endTime); + // Fix vc_section_count with actual number of blocks written + // Layout: block_type(1) + section_length(8) + start_time(8) + + // end_time(8) + endian_test(8) + memory_used(8) + scope_count(8) + + // var_count(8) + max_var_id(8) = offset 65 + _file.setPositionSync( + 1 + 8 + 8 + 8 + 8 + 8 + 8 + 8 + 8, + ); // at vc_section_count + _writeU64(_vcSectionCount); + _file.setPositionSync(savedPos); + } + + // ─────────────── Hierarchy Block ─────────────── + + static const int _hierTypeScopeBegin = 254; + static const int _hierTypeUpScope = 255; + + /// Writes the FST_BL_HIER block (zlib/gzip compressed hierarchy). + void _writeHierarchyBlock() { + // Build uncompressed hierarchy bytes + final buf = BytesBuilder(copy: false); + var handleCount = 0; + + for (final entry in _hierEntries) { + switch (entry) { + case _ScopeEntry(): + buf + ..addByte(_hierTypeScopeBegin) + ..addByte(entry.type.value) + ..add(_cString(entry.name)) + ..add(_cString(entry.component)); + case _UpScopeEntry(): + buf.addByte(_hierTypeUpScope); + case _VarEntry(): + buf + ..addByte(entry.varType.value) + ..addByte(entry.direction.value) + ..add(_cString(entry.name)) + ..add(encodeVarint(entry.width)) // length + // alias = 0 means "new handle, not an alias" + ..add(encodeVarint(0)); + handleCount++; + } + } + + final uncompressed = buf.toBytes(); + assert( + handleCount == _signals.length, + 'Handle count mismatch: $handleCount vs ${_signals.length}', + ); + + // Write as FST_BL_HIER (type 4) with gzip compression + _file.writeByteSync(FstBlockType.hierarchy.value); + final sectionLengthPos = _file.positionSync(); + _writeU64(0); // placeholder section_length + _writeU64(uncompressed.length); // uncompressed_length + + // Write gzip header + deflate-compressed data + _writeGzipCompressed(uncompressed); + + // Fix section_length + final endPos = _file.positionSync(); + final sectionLength = endPos - sectionLengthPos; + _file.setPositionSync(sectionLengthPos); + _writeU64(sectionLength); + _file.setPositionSync(endPos); + } + + // ─────────────── Geometry Block ─────────────── + + /// Writes the FST_BL_GEOM block. + void _writeGeometryBlock() { + // Build uncompressed geometry: one varint per signal + final buf = BytesBuilder(copy: false); + for (final sig in _signals) { + buf.add(encodeVarint(sig.geometryValue)); + } + final uncompressed = buf.toBytes(); + final compressed = _zlibCompress( + uncompressed, + config.compressionLevel, + allowRaw: true, + ); + + _file.writeByteSync(FstBlockType.geometry.value); + final sectionLength = 3 * 8 + compressed.length; + _writeU64(sectionLength); // section_length + _writeU64(uncompressed.length); // uncompressed_length + _writeU64(_signals.length); // max_handle + _file.writeFromSync(compressed); + } + + // ─────────────── VcData Block (DynamicAlias2) ─────────────── + + /// Writes a single FST_BL_VCDATA_DYN_ALIAS2 block from the current + /// `_changes` buffer. + /// + /// [blockStartTime] and [blockEndTime] are the time range for this block. + /// [frameValues] contains the initial value of each signal at the block's + /// start time (carry-over state plus changes at blockStartTime). + /// + /// Assumes `_changes` is already sorted by time, then by handle. + void _writeVcDataBlock({ + required int blockStartTime, + required int blockEndTime, + required List frameValues, + }) { + // Build sorted unique time table. + // Only include timestamps that have signal chain entries (i.e., after + // blockStartTime). Changes at blockStartTime go into the frame section. + // The fst-reader only reads the frame when time_table[0] > start_time; + // if blockStartTime were included, the frame would be skipped and all + // signals would appear as 'x'. + final timeSet = {}; + for (final c in _changes) { + if (c.time != blockStartTime) { + timeSet.add(c.time); + } + } + final timeTable = timeSet.toList()..sort(); + // Map timestamp → index + final timeToIndex = {}; + for (var i = 0; i < timeTable.length; i++) { + timeToIndex[timeTable[i]] = i; + } + + // Build per-signal value change chains + final signalData = _buildSignalData(timeToIndex, blockStartTime); + + // Pack each signal's data (store uncompressed with varint(0) prefix) + final packedSignals = []; + for (final data in signalData) { + if (data.isEmpty) { + packedSignals.add(Uint8List(0)); + } else { + final packed = BytesBuilder(copy: false) + ..add(encodeVarint(0)) // means "uncompressed" + ..add(data); + packedSignals.add(packed.toBytes()); + } + } + + // Build frame bytes + final frameBytes = _buildFrameBytes(frameValues); + final frameCompressed = _zlibCompress( + frameBytes, + config.compressionLevel, + allowRaw: true, + ); + + // Build the signal offset chain (DynamicAlias2 format) + final chainBytes = _buildOffsetChain(packedSignals); + + // Build time table bytes + final timeTableBytes = _buildTimeTableBytes(timeTable); + + // Compute memory required for traversal + var memRequired = 0; + for (final ps in packedSignals) { + memRequired += ps.length; + } + + // Now assemble the VcData block + _file.writeByteSync(FstBlockType.vcDataDynamicAlias2.value); + final sectionLengthPos = _file.positionSync(); + _writeU64(0); // placeholder section_length + _writeU64(blockStartTime); // start_time + _writeU64(blockEndTime); // end_time + _writeU64(memRequired); // mem_required_for_traversal + + // Frame section + _file + ..writeFromSync(encodeVarint(frameBytes.length)) // unc len + ..writeFromSync(encodeVarint(frameCompressed.length)) // comp len + ..writeFromSync(encodeVarint(_signals.length)) // max_handle + ..writeFromSync(frameCompressed) + // Value change section + ..writeFromSync(encodeVarint(_signals.length)) // max_handle + ..writeByteSync(0x5A); // pack_type = 'Z' (zlib) + + // Write per-signal packed data + packedSignals.forEach(_file.writeFromSync); + + // Write offset chain + _file.writeFromSync(chainBytes); + _writeU64(chainBytes.length); // chain_compressed_length + + // Write time table + _file.writeFromSync(timeTableBytes); + + // Fix section_length + final endPos = _file.positionSync(); + final sectionLength = endPos - sectionLengthPos; + _file.setPositionSync(sectionLengthPos); + _writeU64(sectionLength); + _file.setPositionSync(endPos); + } + + /// Builds frame bytes: the initial value of each signal concatenated. + Uint8List _buildFrameBytes(List initialValues) { + final buf = BytesBuilder(copy: false); + for (var i = 0; i < _signals.length; i++) { + final sig = _signals[i]; + if (sig.isReal) { + // Encode as f64 little-endian bytes + final d = double.tryParse(initialValues[i]) ?? 0.0; + final bd = ByteData(8)..setFloat64(0, d, Endian.little); + buf.add(bd.buffer.asUint8List()); + } else { + // Character-encoded value: one byte per bit + final val = initialValues[i]; + for (var j = 0; j < sig.width; j++) { + buf.addByte(j < val.length ? val.codeUnitAt(j) : 0x78); // 'x' + } + } + } + return buf.toBytes(); + } + + /// Builds per-signal value change encoded data. + /// + /// Returns a list of byte arrays, one per signal (0-indexed). + /// Each byte array contains the encoded value change chain for that signal. + /// Changes at [blockStartTime] are skipped (captured in the frame). + List _buildSignalData( + Map timeToIndex, + int blockStartTime, + ) { + // Group changes by signal handle index + final signalChanges = List>.generate( + _signals.length, + (_) => [], + ); + for (final c in _changes) { + // Skip changes at blockStartTime — those are captured in the frame + if (c.time == blockStartTime) { + continue; + } + signalChanges[c.handleIndex].add(c); + } + + final result = []; + for (var sigIdx = 0; sigIdx < _signals.length; sigIdx++) { + final changes = signalChanges[sigIdx]; + if (changes.isEmpty) { + result.add(Uint8List(0)); + continue; + } + + final sig = _signals[sigIdx]; + final buf = BytesBuilder(copy: false); + var prevTimeIndex = 0; + + for (final c in changes) { + final timeIndex = timeToIndex[c.time]!; + final timeDelta = timeIndex - prevTimeIndex; + prevTimeIndex = timeIndex; + + if (sig.frameLength == 1) { + // 1-bit signal: compact encoding + buf.add(_encodeOneBitChange(timeDelta, c.value)); + } else if (sig.isReal) { + // Real signal + buf.add(_encodeRealChange(timeDelta, c.value)); + } else { + // Multi-bit signal + buf.add(_encodeMultiBitChange(timeDelta, c.value, sig.width)); + } + } + result.add(buf.toBytes()); + } + return result; + } + + /// Encodes a 1-bit signal value change. + /// + /// Format: varint where: + /// - Normal (0/1): bit0=0, bit1=value, bits2+= time_index_delta + /// - Special (x/z/etc): bit0=1, bits1-3=rcv_index, bits4+=time_index_delta + Uint8List _encodeOneBitChange(int timeDelta, String value) { + // RCV_STR: [x, z, h, u, w, l, -, ?] + const rcvChars = 'xzhuwl-?'; + final ch = value.isNotEmpty ? value[value.length - 1] : 'x'; + + int vli; + if (ch == '0') { + vli = (timeDelta << 2) | (0 << 1) | 0; // bit0=0, bit1=0 + } else if (ch == '1') { + vli = (timeDelta << 2) | (1 << 1) | 0; // bit0=0, bit1=1 + } else { + final rcvIdx = rcvChars.indexOf(ch); + final idx = rcvIdx >= 0 ? rcvIdx : 0; // default to 'x' + vli = (timeDelta << 4) | (idx << 1) | 1; // bit0=1, bits1-3=idx + } + return encodeVarint(vli); + } + + /// Encodes a multi-bit signal value change. + /// + /// Format: varint(time_delta << 1 | encoding_bit) then value bytes. + /// encoding_bit=0: 2-state packed bits; encoding_bit=1: 4-state characters. + Uint8List _encodeMultiBitChange(int timeDelta, String value, int width) { + final buf = BytesBuilder(copy: false); + + // Check if value contains only 0/1 (2-state) + final is2State = value.runes.every((c) => c == 0x30 || c == 0x31); + + if (is2State) { + // 2-state: pack bits into bytes, MSB first + buf.add(encodeVarint((timeDelta << 1) | 0)); + final byteCount = (width + 7) ~/ 8; + final bytes = Uint8List(byteCount); + for (var i = 0; i < width; i++) { + if (i < value.length && value[i] == '1') { + final byteIdx = i ~/ 8; + final bitIdx = 7 - (i % 8); + bytes[byteIdx] |= 1 << bitIdx; + } + } + buf.add(bytes); + } else { + // 4-state: raw character bytes + buf.add(encodeVarint((timeDelta << 1) | 1)); + for (var i = 0; i < width; i++) { + buf.addByte(i < value.length ? value.codeUnitAt(i) : 0x78); + } + } + return buf.toBytes(); + } + + /// Encodes a real signal value change. + Uint8List _encodeRealChange(int timeDelta, String value) { + final buf = BytesBuilder(copy: false) + ..add(encodeVarint((timeDelta << 1) | 1)); + final d = double.tryParse(value) ?? 0.0; + final bd = ByteData(8)..setFloat64(0, d, Endian.little); + buf.add(bd.buffer.asUint8List()); + return buf.toBytes(); + } + + /// Builds the offset chain for DynamicAlias2 format. + /// + /// The chain encodes the byte offset and presence of each signal's + /// packed data within the value change section. + Uint8List _buildOffsetChain(List packedSignals) { + final buf = BytesBuilder(copy: false); + var currentOffset = 0; // byte offset within vc section (after pack_type) + var prevOffset = 0; + var consecutiveEmpty = 0; + + // Offset 0 is the pack_type byte itself. Signal data starts at offset 1. + currentOffset = 1; // skip the pack_type byte + + for (var i = 0; i < packedSignals.length; i++) { + final ps = packedSignals[i]; + if (ps.isEmpty) { + consecutiveEmpty++; + } else { + // Flush any consecutive empty signals + if (consecutiveEmpty > 0) { + // Write: varint((count << 1) | 0) — bit0=0 means "zero block" + buf.add(encodeVarint(consecutiveEmpty << 1)); + consecutiveEmpty = 0; + } + // Write positive offset delta (signed varint with bit0=1) + // In DynamicAlias2: bit0=1 + signed_varint >> 1 > 0 means + // new incremental offset delta. + // Encoding: signed_varint((delta << 1) | 1) + // Reader does: shval = read_variant_i64() >> 1 = delta + final offsetDelta = currentOffset - prevOffset; + buf.add(encodeSignedVarint((offsetDelta << 1) | 1)); + prevOffset = currentOffset; + currentOffset += ps.length; + } + } + + // Flush trailing empty signals + if (consecutiveEmpty > 0) { + buf.add(encodeVarint(consecutiveEmpty << 1)); + } + + return buf.toBytes(); + } + + /// Builds the time table section (appended at end of VcData block). + /// + /// The time table is: compressed delta-encoded timestamps, followed by + /// 3 u64s: uncompressed_length, compressed_length, num_entries. + Uint8List _buildTimeTableBytes(List timeTable) { + // Delta-encode the time table + final deltaBuf = BytesBuilder(copy: false); + var prevTime = 0; + for (final t in timeTable) { + deltaBuf.add(encodeVarint(t - prevTime)); + prevTime = t; + } + final uncompressed = deltaBuf.toBytes(); + final compressed = _zlibCompress( + uncompressed, + config.compressionLevel, + allowRaw: true, + ); + + // Build the full time section: compressed data + 3 u64s + final result = BytesBuilder(copy: false) + ..add(compressed) + ..add(_encodeU64(uncompressed.length)) + ..add(_encodeU64(compressed.length)) + ..add(_encodeU64(timeTable.length)); + return result.toBytes(); + } + + // ─────────────── Low-level I/O helpers ─────────────── + + /// Writes a big-endian u64. + void _writeU64(int value) { + final bd = ByteData(8)..setUint64(0, value); + _file.writeFromSync(bd.buffer.asUint8List()); + } + + /// Encodes a big-endian u64 to bytes. + Uint8List _encodeU64(int value) { + final bd = ByteData(8)..setUint64(0, value); + return bd.buffer.asUint8List(); + } + + /// Writes a little-endian f64 (for double endian test). + void _writeF64LE(double value) { + final bd = ByteData(8)..setFloat64(0, value, Endian.little); + _file.writeFromSync(bd.buffer.asUint8List()); + } + + /// Writes a fixed-length NUL-padded string. + void _writeFixedString(String value, int maxLen) { + final bytes = utf8.encode(value); + final len = bytes.length < maxLen ? bytes.length : maxLen - 1; + _file + ..writeFromSync(bytes.sublist(0, len)) + // Pad with zeros + ..writeFromSync(Uint8List(maxLen - len)); + } + + /// Encodes a NUL-terminated string. + Uint8List _cString(String value) { + final bytes = utf8.encode(value); + final result = Uint8List(bytes.length + 1) + ..setRange(0, bytes.length, bytes); + // last byte is already 0 + return result; + } + + /// Encodes an unsigned integer as LEB128 varint. + static Uint8List encodeVarint(int value) { + if (value < 0) { + throw ArgumentError('Value must be non-negative: $value'); + } + if (value <= 0x7F) { + return Uint8List.fromList([value]); + } + final bytes = []; + var v = value; + while (v != 0) { + final nextV = v >> 7; + final mask = nextV == 0 ? 0 : 0x80; + bytes.add((v & 0x7F) | mask); + v = nextV; + } + return Uint8List.fromList(bytes); + } + + /// Encodes a signed integer as signed LEB128 varint. + static Uint8List encodeSignedVarint(int value) { + if (value >= -64 && value <= 63) { + return Uint8List.fromList([value & 0x7F]); + } + + final bytes = []; + var v = value; + var more = true; + while (more) { + var byte_ = v & 0x7F; + v >>= 7; + // Check if we're done + if ((v == 0 && (byte_ & 0x40) == 0) || (v == -1 && (byte_ & 0x40) != 0)) { + more = false; + } else { + byte_ |= 0x80; + } + bytes.add(byte_); + } + return Uint8List.fromList(bytes); + } + + /// Writes gzip-compressed bytes (gzip header + deflate data). + void _writeGzipCompressed(Uint8List data) { + // Gzip header (10 bytes) + const gzipHeader = [ + 0x1F, 0x8B, // magic + 0x08, // deflate + 0x00, // no flags + 0x00, 0x00, 0x00, 0x00, // timestamp = 0 + 0x00, // compression level + 0xFF, // OS = unknown + ]; + _file.writeFromSync(Uint8List.fromList(gzipHeader)); + + // Deflate-compressed data (raw deflate, not zlib-wrapped) + final compressed = _deflateCompress(data, config.compressionLevel); + _file.writeFromSync(compressed); + } + + /// Compresses bytes using zlib (with zlib header, for geometry/frame/etc). + static Uint8List _zlibCompress( + Uint8List data, + int level, { + bool allowRaw = false, + }) { + final compressed = ZLibCodec(level: level).encode(data); + final result = Uint8List.fromList(compressed); + if (allowRaw && result.length >= data.length) { + // Compression didn't help, return uncompressed + return data; + } + return result; + } + + /// Compresses bytes using raw deflate (no zlib header, for gzip hierarchy). + static Uint8List _deflateCompress(Uint8List data, int level) { + final compressed = ZLibCodec(level: level, raw: true).encode(data); + return Uint8List.fromList(compressed); + } + + /// Generates a date string for the header. + String _dateString() { + final now = DateTime.now(); + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', // + ]; + final day = days[now.weekday - 1]; + final month = months[now.month - 1]; + final d = now.day.toString().padLeft(2); + final h = now.hour.toString().padLeft(2, '0'); + final m = now.minute.toString().padLeft(2, '0'); + final s = now.second.toString().padLeft(2, '0'); + return '$day $month $d $h:$m:$s ${now.year}\n'; + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index a1cb8ec5c..c43d20048 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,11 +13,8 @@ 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 @@ -118,7 +115,7 @@ abstract class Module { ..._inputs.values, ..._outputs.values, ..._inOuts.values, - ...internalSignals, + ...internalSignals ]); /// Accesses the [Logic] associated with this [Module]s [input] port @@ -341,7 +338,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. @@ -1133,39 +1130,80 @@ abstract class Module { /// [hierarchy], this is only valid after [build] has been called. String get hierarchicalName => _hierarchyListToString(hierarchy()); - /// Returns a synthesized version of this [Module]. + /// Generates synthesized SystemVerilog for this [Module]. /// - /// Currently returns one long file in SystemVerilog, but in the future - /// may have other output formats, languages, files, etc. + /// Access [SystemVerilogService.output] for a single in-memory SystemVerilog + /// file. This legacy convenience method writes to [outputPath] when provided. + /// With [multiFile] `true`, [outputPath] must be a directory and each module + /// definition is written there. Otherwise, it is the path to a single + /// concatenated SystemVerilog file. /// - /// The [configuration] controls options specific to SystemVerilog output. - String generateSynth({ + /// For additional output controls and access to synthesis results, use + /// [SystemVerilogService] directly. + SystemVerilogService dumpSystemVerilog({ + String? outputPath, + bool multiFile = false, SystemVerilogSynthesizerConfiguration configuration = const SystemVerilogSynthesizerConfiguration(), }) { - if (!_hasBuilt) { - throw ModuleNotBuiltException(this); + final service = SystemVerilogService( + this, + outputDirectory: multiFile ? outputPath ?? '.' : '.', + multiFile: multiFile, + configuration: configuration, + ); + if (outputPath != null) { + if (multiFile) { + service.writeOutputs(); + } else { + service.writeLegacyOutputPath(outputPath); + } } + return service; + } - final synthHeader = ''' -/** - * Generated by ROHD - www.github.com/intel/rohd - * Generation time: ${Timestamper.stamp()} - * ROHD Version: ${Config.version} - */ - -'''; - return synthHeader + - SynthBuilder( - this, - SystemVerilogSynthesizer(configuration: configuration), - ).getSynthFileContents().join('\n\n////////////////////\n\n'); + /// Attaches waveform dumping for this [Module] to a VCD at [outputPath]. + /// + /// For filtering, alternative formats, and other waveform controls, use + /// [WaveformService] directly. + WaveformService dumpWaves({String outputPath = 'waves.vcd'}) { + final (outputDirectory, outputFileName) = _legacyOutputLocation(outputPath); + return WaveformService( + this, + outputDirectory: outputDirectory, + outputFileName: outputFileName, + ); } + + /// Returns a synthesized version of this [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 [SystemVerilogService] (and + /// [SystemVerilogService.output] for the equivalent one-shot string). + /// The [configuration] controls options specific to SystemVerilog output. + @Deprecated('Use Module.dumpSystemVerilog(configuration: ...).output for ' + 'in-memory output, Module.dumpSystemVerilog(outputPath: outputPath, ' + 'configuration: ...) ' + 'for direct file output, or SystemVerilogService for advanced options.') + String generateSynth({ + SystemVerilogSynthesizerConfiguration configuration = + const SystemVerilogSynthesizerConfiguration(), + }) => + dumpSystemVerilog(configuration: configuration).output; } -extension on LogicStructure { - /// Indicates that a [LogicStructure] has a [Const] element within it or - /// within one of its [elements]. - bool get hasConsts => - elements.any((e) => e is Const || (e is LogicStructure && e.hasConsts)); +/// Splits a legacy file [outputPath] into its directory and exact filename. +(String, String) _legacyOutputLocation(String outputPath) { + final normalized = outputPath.replaceAll(r'\', '/'); + final separatorIndex = normalized.lastIndexOf('/'); + final directory = switch (separatorIndex) { + -1 => '.', + 0 => '/', + _ => normalized.substring(0, separatorIndex), + }; + final fileName = normalized.substring(separatorIndex + 1); + return (directory, fileName); } diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index cd9aa8750..df6062b48 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // flop.dart @@ -92,6 +92,10 @@ class FlipFlop extends Module with SystemVerilog { /// reset. If no `reset` is provided, this will have no effect. final bool asyncReset; + /// The constant reset value, or `null` when reset is absent or data-driven. + LogicValue? get constantResetValue => + _reset == null || _resetValuePort != null ? null : _resetValueConst; + /// Constructs a flip flop which is positive edge triggered on [clk]. /// /// When optional [en] is provided, an additional input will be created for @@ -146,7 +150,7 @@ class FlipFlop extends Module with SystemVerilog { var contents = [q < _d]; if (_en != null) { - contents = [If(_en!, then: contents)]; + contents = [If(_en, then: contents)]; } Sequential( diff --git a/lib/src/signals/const.dart b/lib/src/signals/const.dart index 3e6989145..72c2544b2 100644 --- a/lib/src/signals/const.dart +++ b/lib/src/signals/const.dart @@ -75,28 +75,20 @@ class Const extends Logic { /// outputs and its normalized name. Supported values are 2, 8, 10, and 16. /// If omitted, generated outputs select a radix automatically and the name /// uses decimal. Values containing `x` or `z` may fall back to binary. - Const( - dynamic val, { - int? width, - bool fill = false, - int? preferredRadix, - }) : this._( - LogicValue.of( - val, - width: width ?? (val is LogicValue ? val.width : 1), - fill: fill, - ), - preferredRadix: _validatePreferredRadix(preferredRadix), - ); + Const(dynamic val, {int? width, bool fill = false, int? preferredRadix}) + : this._( + LogicValue.of(val, + width: width ?? (val is LogicValue ? val.width : 1), + fill: fill), + preferredRadix: _validatePreferredRadix(preferredRadix)); /// Constructs a [Const] from an already normalized [value]. Const._(LogicValue value, {required this.preferredRadix}) : super( - name: _constName(value, preferredRadix), - width: value.width, - // we don't care about maintaining this node unless necessary - naming: Naming.unnamed, - ) { + name: _constName(value, preferredRadix), + width: value.width, + // we don't care about maintaining this node unless necessary + naming: Naming.unnamed) { _wire ..put(value, signalName: name) ..makeImmutable(this, reason: _unassignableMessage); diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index ef463b9bb..0ee7b17dd 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_structure.dart @@ -640,6 +640,10 @@ class LogicStructure implements Logic { elements.any((e) => e.isNet || (e is LogicStructure && e.hasNets)) || isNet; + /// Indicates whether this structure contains a [Const] at any depth. + bool get hasConsts => _hasConsts; + late final bool _hasConsts = leafElements.any((element) => element is Const); + @override Iterable get srcConnections => { for (final element in elements) ...element.srcConnections diff --git a/lib/src/signals/wire_net.dart b/lib/src/signals/wire_net.dart index f93529b0f..881bbdd84 100644 --- a/lib/src/signals/wire_net.dart +++ b/lib/src/signals/wire_net.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // wire_net.dart diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart new file mode 100644 index 000000000..950ca3529 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -0,0 +1,12 @@ +// 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 'netlist_service.dart'; +export 'netlist_synthesizer.dart'; +export 'netlist_synthesizer_configuration.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_cell.dart b/lib/src/synthesizers/netlist/netlist_cell.dart new file mode 100644 index 000000000..2f920288c --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_cell.dart @@ -0,0 +1,53 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_cell.dart +// Typed representation of a serialized netlist cell. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; + +/// A cell in a synthesized netlist. +@internal +class NetlistCell { + /// Whether consumers should hide this cell's name. + final int hideName; + + /// The Yosys cell type or module definition name. + final String type; + + /// Parameters configuring the cell. + final Map parameters; + + /// Attributes attached to the cell. + final Map attributes; + + /// Directions of the cell's ports. + final Map portDirections; + + /// Bits connected to each cell port. + final Map> connections; + + /// Creates a netlist cell. + const NetlistCell({ + required this.type, + required this.portDirections, + required this.connections, + this.parameters = const {}, + this.attributes = const {}, + this.hideName = 0, + }); + + /// Serializes this cell to the Yosys-compatible JSON structure. + Map toJson() => { + 'hide_name': hideName, + 'type': type, + 'parameters': parameters, + 'attributes': attributes, + 'port_directions': serializePortDirections(portDirections), + 'connections': connections, + }; +} diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart new file mode 100644 index 000000000..eb3a16742 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -0,0 +1,634 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_cell_mapper.dart +// Maps selected ROHD modules to Yosys-primitive cell representations. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; + +/// The result of mapping a netlist cell module to a Yosys-style cell. +@internal +typedef NetlistCellMapping = ({ + String cellType, + Map portDirs, + Map> connections, + Map parameters, +}); + +/// Context provided to each netlist-cell mapping handler. +/// +/// Contains the module instance plus the raw ROHD port directions and +/// connections built by the synthesizer, so handlers can remap them to +/// Yosys-primitive port names. +@internal +class NetlistCellContext { + /// The ROHD [Module] being mapped. + final Module module; + + /// Raw ROHD port-direction map. + final Map rawPortDirs; + + /// Raw ROHD connection map (`{'portName': [wireId, ...]}`). + final Map> rawConns; + + /// Creates a [NetlistCellContext]. + NetlistCellContext( + this.module, + Map rawPortDirs, + Map> rawConns, + ) : rawPortDirs = + Map.unmodifiable(rawPortDirs), + rawConns = Map>.unmodifiable({ + for (final entry in rawConns.entries) + entry.key: List.unmodifiable(entry.value), + }); + + // ── Shared helper methods ─────────────────────────────────────────── + + /// 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] ?? NetlistPortDirection.output; + cn[netlistPortName] = rawConns[rohdName] ?? []; + } + return (portDirs: pd, connections: cn); + } +} + +/// Signature for a netlist-cell mapping handler. +/// +/// Returns a [NetlistCellMapping] if the handler recognises the module, +/// or `null` to let the next handler try. +@internal +typedef NetlistCellHandler = NetlistCellMapping? Function( + NetlistCellContext ctx); + +/// Maps modules already selected as netlist leaves to Yosys-primitive cell +/// representations. +/// +/// Handlers are registered via [register] and tried in registration order. +/// Hierarchy stopping is controlled separately by [SynthModuleStopPolicy]. +@internal +class NetlistCellMapper { + /// Ordered list of registered handlers. + final _handlers = []; + + /// Creates an empty [NetlistCellMapper] with no registered handlers. + NetlistCellMapper(); + + /// Creates a mapper with all built-in ROHD netlist cell types registered. + factory NetlistCellMapper.withDefaults() => + NetlistCellMapper().._registerDefaults(); + + /// Register a mapping [handler]. + /// + /// Handlers are tried in registration order; the first non-null result + /// wins. Register more-specific handlers before less-specific ones. + void register(NetlistCellHandler handler) { + _handlers.add(handler); + } + + /// Try to map [module] to a Yosys-primitive cell. + /// + /// Returns `null` if no registered handler matches. + NetlistCellMapping? map( + Module module, + Map rawPortDirs, + Map> rawConns, + ) { + final ctx = NetlistCellContext(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 NetlistCellMapping? unaryAY(NetlistCellContext 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 NetlistCellMapping? binaryABY( + NetlistCellContext 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), + }, + ); + } + + /// Maps a shift gate to a Yosys binary shift cell. + static NetlistCellMapping? shiftABY( + NetlistCellContext ctx, + String cellType, { + required bool aSigned, + }) { + final a = ctx.findInput('_in'); + final b = ctx.findInput('_shiftAmount'); + final y = ctx.firstOutput; + if (a == null || b == null || y == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', y: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': aSigned ? 1 : 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(y), + }, + ); + } + + /// Map a two-input gate with ports A, B, Y (e.g. `$pow`, `$div`, `$mod`), + /// including the standard Yosys `A_SIGNED`/`B_SIGNED` parameters. + /// + /// Unlike [binaryABY], this always emits the full standard parameter set + /// (`A_SIGNED`, `A_WIDTH`, `B_SIGNED`, `B_WIDTH`, `Y_WIDTH`) so the result + /// is directly consumable by standard Yosys tooling that expects these + /// arithmetic cells to be fully specified. + static NetlistCellMapping? binaryABYSigned( + NetlistCellContext ctx, + String cellType, { + required String inAPrefix, + required String inBPrefix, + bool aSigned = false, + bool bSigned = false, + }) { + final a = ctx.findInput(inAPrefix); + final b = ctx.findInput(inBPrefix); + final out = ctx.firstOutput; + if (a == null || b == null || out == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': aSigned ? 1 : 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': bSigned ? 1 : 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + // ══════════════════════════════════════════════════════════════════════ + // Built-in handler registration + // ══════════════════════════════════════════════════════════════════════ + + /// Registers the built-in ROHD-to-Yosys primitive cell mappings. + void _registerDefaults() { + // Helper to reduce boilerplate for type-map-based handlers. + void registerByTypeMap( + Map typeMap, + NetlistCellMapping? Function(NetlistCellContext ctx, String cellType) + handler, + ) { + register((ctx) { + final cellType = typeMap[ctx.module.runtimeType]; + return cellType == null ? null : handler(ctx, cellType); + }); + } + + this + // ── 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] = NetlistPortDirection.input; + cn[label] = ctx.rawConns[ik] ?? []; + params['IN${i}_WIDTH'] = w; + bitOffset += w; + } + if (outName != null) { + pd['Y'] = NetlistPortDirection.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 sumBits = ctx.rawConns[sumName] ?? []; + final carryBits = carryName.isEmpty + ? const [] + : ctx.rawConns[carryName] ?? []; + final pd = { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }; + final cn = >{ + 'A': ctx.rawConns[in0] ?? [], + 'B': ctx.rawConns[in1] ?? [], + 'Y': [...sumBits, ...carryBits], + }; + return ( + cellType: r'$add', + portDirs: pd, + connections: cn, + parameters: { + 'A_WIDTH': ctx.width(in0), + 'B_WIDTH': ctx.width(in1), + 'Y_WIDTH': sumBits.length + carryBits.length, + }, + ); + }) + // ── FlipFlop → Yosys register cells ─────────────────────────────── + ..register((ctx) { + final flipFlop = ctx.module; + if (flipFlop 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 hasEnable = en != null && ctx.rawConns.containsKey(en); + final hasReset = rst != null && ctx.rawConns.containsKey(rst); + final rstVal = + ctx.findInput('_resetValue') ?? ctx.findInput('resetValue'); + final hasDynamicResetValue = + hasReset && rstVal != null && ctx.rawConns.containsKey(rstVal); + + String cellType; + if (!hasReset) { + cellType = hasEnable ? r'$dffe' : r'$dff'; + } else if (flipFlop.asyncReset) { + cellType = hasDynamicResetValue + ? (hasEnable ? r'$aldffe' : r'$aldff') + : (hasEnable ? r'$adffe' : r'$adff'); + } else if (!hasDynamicResetValue) { + cellType = hasEnable ? r'$sdffe' : r'$sdff'; + } else { + // Dynamic synchronous reset values are lowered to standard mux cells + // by NetlistModuleTranslation. + return null; + } + + final pd = { + 'CLK': NetlistPortDirection.input, + 'D': NetlistPortDirection.input, + 'Q': NetlistPortDirection.output, + }; + final cn = >{ + 'CLK': ctx.rawConns[clk] ?? [], + 'D': ctx.rawConns[d] ?? [], + 'Q': ctx.rawConns[q] ?? [], + }; + if (hasEnable) { + pd['EN'] = NetlistPortDirection.input; + cn['EN'] = ctx.rawConns[en] ?? []; + } + if (hasReset) { + final resetPort = flipFlop.asyncReset + ? (hasDynamicResetValue ? 'ALOAD' : 'ARST') + : 'SRST'; + pd[resetPort] = NetlistPortDirection.input; + cn[resetPort] = ctx.rawConns[rst] ?? []; + } + if (hasDynamicResetValue) { + pd['AD'] = NetlistPortDirection.input; + cn['AD'] = ctx.rawConns[rstVal] ?? []; + } + + final parameters = { + 'WIDTH': ctx.width(d), + 'CLK_POLARITY': 1, + if (hasEnable) 'EN_POLARITY': 1, + if (hasReset && flipFlop.asyncReset) + (hasDynamicResetValue ? 'ALOAD_POLARITY' : 'ARST_POLARITY'): 1, + if (hasReset && !flipFlop.asyncReset) 'SRST_POLARITY': 1, + if (hasReset && !hasDynamicResetValue) + if (flipFlop.asyncReset) + 'ARST_VALUE': + flipFlop.constantResetValue!.toString(includeWidth: false) + else + 'SRST_VALUE': + flipFlop.constantResetValue!.toString(includeWidth: false), + }; + return ( + cellType: cellType, + portDirs: pd, + connections: cn, + parameters: parameters, + ); + }); + + // ── Type-map-based gates ─────────────────────────────────────────── + final gateRegistrations = <( + Map, + NetlistCellMapping? Function(NetlistCellContext, 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'}, + (ctx, type) => shiftABY(ctx, type, aSigned: false), + ), + ( + const {ARShift: r'$sshr'}, + (ctx, type) => shiftABY(ctx, type, aSigned: true), + ), + ( + const { + Power: r'$pow', + Divide: r'$div', + Modulo: r'$mod', + }, + (ctx, type) => + binaryABYSigned(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ]; + for (final (typeMap, handler) in gateRegistrations) { + registerByTypeMap(typeMap, handler); + } + + // ── IndexGate → $shiftx ───────────────────────────────────────────── + // + // `$shiftx` extracts `Y_WIDTH` bits of `A` starting at bit offset `B`, + // producing `x` when the offset is out of range. This matches + // [IndexGate]'s bit-select semantics (`original[index]`, `Y_WIDTH == 1`) + // exactly, including its out-of-range-selects-`x` behavior. + register((ctx) { + if (ctx.module is! IndexGate) { + return null; + } + final inputNames = ctx.module.inputs.keys.toList(); + if (inputNames.length != 2) { + return null; + } + final a = inputNames[0]; + final b = inputNames[1]; + final y = ctx.firstOutput; + if (y == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', y: 'Y'}); + return ( + cellType: r'$shiftx', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_SIGNED': 0, + 'A_WIDTH': ctx.width(a), + 'B_SIGNED': 0, + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(y), + }, + ); + }); + + // ── TriStateBuffer → $tribuf ────────────────────────────────────── + register((ctx) { + if (ctx.module is! TriStateBuffer) { + 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'}); + r.portDirs['Y'] = NetlistPortDirection.output; + return ( + cellType: r'$tribuf', + portDirs: r.portDirs, + connections: r.connections, + parameters: {'WIDTH': ctx.width(inName)}, + ); + }); + } +} diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart new file mode 100644 index 000000000..eead0033a --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -0,0 +1,925 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_module_translation.dart +// Per-module state and ordered phases for netlist synthesis. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// Mutable state for translating one module level into a netlist. +@internal +class NetlistModuleTranslation { + /// The module being translated. + final Module _module; + + /// The synthesis definition for this module level, when one can be built. + final NetlistSynthModuleDefinition? synthDef; + + final NetlistCellMapper _netlistCellMapper; + final bool Function(Module module) _generatesDefinition; + final String Function(Module module) _getInstanceTypeOfModule; + + /// The next available integer wire identifier. + /// + /// Starts at 2 so consumers never confuse wire IDs 0 or 1 with the + /// Yosys-JSON constant bit strings `"0"` and `"1"`. + int _nextId = 2; + + final Map> _synthLogicIds = {}; + + /// Emitted module ports. + final Map> ports = {}; + + /// Emitted cells. + final Map> cells = {}; + + /// Emitted netnames. + final Map netnames = {}; + + final Set _blockedConstSynthLogics = {}; + + late final ({ + Set arrayConcatOutputs, + Set directSubmoduleOutputs, + }) _submoduleOutputDrivers = _indexSubmoduleOutputDrivers(); + + late final NetlistAlwaysBlockPortCollapseIndex? + _alwaysBlockPortCollapseIndex = + synthDef == null ? null : NetlistAlwaysBlockPortCollapseIndex(synthDef!); + + /// Creates translation state for one [module]. + NetlistModuleTranslation( + Module module, { + required NetlistCellMapper netlistCellMapper, + required bool Function(Module module) generatesDefinition, + required String Function(Module module) getInstanceTypeOfModule, + }) : _module = module, + _netlistCellMapper = netlistCellMapper, + _generatesDefinition = generatesDefinition, + _getInstanceTypeOfModule = getInstanceTypeOfModule, + synthDef = module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none + ? null + : NetlistSynthModuleDefinition(module); + + /// Allocates the next wire identifier. + int allocateWireId() => _nextId++; + + /// Allocates or returns the wire identifiers for [synthLogic]. + List getIds(SynthLogic synthLogic) { + final resolved = synthLogic.isConstant ? synthLogic : synthLogic.resolved; + return _synthLogicIds.putIfAbsent( + resolved, + () => List.generate(resolved.width, (_) => allocateWireId()), + ); + } + + /// Emits input, output, and inout ports in canonical allocation order. + void processPorts() { + final portGroups = [ + (NetlistPortDirection.input, synthDef?.inputs, _module.inputs), + (NetlistPortDirection.output, synthDef?.outputs, _module.outputs), + (NetlistPortDirection.inout, synthDef?.inOuts, _module.inOuts), + ]; + for (final (direction, synthLogics, modulePorts) in portGroups) { + if (synthLogics != null) { + final portNames = + NetlistUtils.portNamesForSynthLogics(synthLogics, modulePorts); + for (final synthLogic in synthLogics) { + final portName = portNames[synthLogic]; + if (portName != null) { + final portLogic = modulePorts[portName]; + final emitOutputArrayConcat = + direction == NetlistPortDirection.output && + portLogic is LogicArray && + !_hasExistingOutputArrayConcat(synthLogic) && + !_hasDirectSubmoduleOutputDriver(synthLogic); + final originalIds = getIds(synthLogic); + final ids = emitOutputArrayConcat + ? List.generate(synthLogic.width, (_) => allocateWireId()) + : originalIds; + ports[portName] = { + 'direction': direction.name, + 'bits': ids, + if (portLogic != null) + 'logic_type': NetlistUtils.buildLogicType(portLogic, ids), + }; + if (emitOutputArrayConcat) { + _emitOutputArrayConcat(portName, portLogic, ids); + } + } + } + } else { + for (final entry in modulePorts.entries) { + final ids = List.generate( + entry.value.width, + (_) => allocateWireId(), + ); + ports[entry.key] = { + 'direction': direction.name, + 'bits': ids, + 'logic_type': NetlistUtils.buildLogicType(entry.value, ids), + }; + } + } + } + } + + /// Emits a concat cell that assembles a LogicArray output port. + void _emitOutputArrayConcat( + String portName, + LogicArray array, + List outputIds, + ) { + _emitOutputArrayConcatForArray(portName, array, outputIds); + } + + /// Recursively emits concat cells for nested LogicArray output elements. + bool _emitOutputArrayConcatForArray( + String concatName, + LogicArray array, + List outputIds, + ) { + final definition = synthDef; + if (definition == null) { + return false; + } + + final concatConnections = >{}; + final concatDirections = {}; + var lowerIndex = 0; + + for (final (index, element) in array.elements.indexed) { + final synthLogic = definition.logicToSynthMap[element]; + if (synthLogic == null) { + return false; + } + var elementIds = getIds(synthLogic); + if (element is LogicArray && + !_hasExistingOutputArrayConcat(synthLogic) && + !_hasDirectSubmoduleOutputDriver(synthLogic)) { + final aggregateIds = List.generate( + synthLogic.width, + (_) => allocateWireId(), + ); + if (!_emitOutputArrayConcatForArray( + '${concatName}_$index', + element, + aggregateIds, + )) { + return false; + } + elementIds = aggregateIds; + } + final upperIndex = lowerIndex + elementIds.length - 1; + concatConnections['[$upperIndex:$lowerIndex]'] = + elementIds.cast(); + concatDirections['[$upperIndex:$lowerIndex]'] = + NetlistPortDirection.input; + lowerIndex = upperIndex + 1; + } + + if (lowerIndex != outputIds.length) { + return false; + } + + concatConnections['Y'] = outputIds.cast(); + concatDirections['Y'] = NetlistPortDirection.output; + + final cellName = NetlistUtils.synthesizedCellName( + operationName: 'array_concat_output', + destination: array, + ); + cells[cellName] = NetlistCell( + type: r'$concat', + parameters: { + for (var index = 0; index < array.elements.length; index++) + 'IN${index}_WIDTH': array.elements[index].width, + }, + portDirections: concatDirections, + connections: concatConnections, + ).toJson(); + + return true; + } + + /// Checks whether [synthLogic] is already driven by an output concat cell. + bool _hasExistingOutputArrayConcat(SynthLogic synthLogic) => + _submoduleOutputDrivers.arrayConcatOutputs.contains(synthLogic.resolved); + + /// Checks whether [synthLogic] is driven directly by a non-concat submodule. + bool _hasDirectSubmoduleOutputDriver(SynthLogic synthLogic) => + _submoduleOutputDrivers.directSubmoduleOutputs + .contains(synthLogic.resolved); + + ({ + Set arrayConcatOutputs, + Set directSubmoduleOutputs, + }) _indexSubmoduleOutputDrivers() { + final definition = synthDef; + if (definition == null) { + return ( + arrayConcatOutputs: {}, + directSubmoduleOutputs: {}, + ); + } + + final arrayConcatOutputs = {}; + final directSubmoduleOutputs = {}; + for (final instance in definition.subModuleInstantiations) { + (instance.module is SynthArrayConcat + ? arrayConcatOutputs + : directSubmoduleOutputs) + .addAll( + instance.outputMapping.values.map((output) => output.resolved), + ); + } + + return ( + arrayConcatOutputs: arrayConcatOutputs, + directSubmoduleOutputs: directSubmoduleOutputs, + ); + } + + /// Preallocates internal wires in [Module.internalSignals] order. + void processInternalWires() { + final definition = synthDef; + if (definition == null) { + return; + } + _module.internalSignals + .map((signal) => definition.logicToSynthMap[signal]) + .whereType() + .where((synthLogic) => !synthLogic.isConstant) + .forEach(getIds); + } + + /// Emits cells and removes instances cleared by procedural-port collapsing. + void processCells() { + final definition = synthDef; + if (definition == null) { + return; + } + + final emittedCellKeys = {}; + for (final instance in definition.subModuleInstantiations) { + if (!instance.needsInstantiation) { + continue; + } + + final submodule = instance.module; + final cellKey = instance.name; + final isLeaf = !_generatesDefinition(submodule); + final defaultCellType = isLeaf + ? submodule.definitionName + : _getInstanceTypeOfModule(submodule); + final rawPortDirs = {}; + final rawConnections = >{}; + + for (final (direction, mapping) in [ + (NetlistPortDirection.input, instance.inputMapping), + (NetlistPortDirection.output, instance.outputMapping), + (NetlistPortDirection.inout, instance.inOutMapping), + ]) { + for (final entry in mapping.entries) { + rawPortDirs[entry.key] = direction; + rawConnections[entry.key] = getIds(entry.value).cast(); + } + } + + final mapped = isLeaf + ? _netlistCellMapper.map(submodule, rawPortDirs, rawConnections) + : null; + if (mapped == null && + submodule is FlipFlop && + _emitDynamicSynchronousResetFlipFlop( + cellKey, + submodule, + rawPortDirs, + rawConnections, + )) { + emittedCellKeys[instance] = cellKey; + continue; + } + final cellPortDirs = mapped?.portDirs ?? rawPortDirs; + final cellConnections = mapped?.connections ?? rawConnections; + emittedCellKeys[instance] = cellKey; + + if (submodule is Combinational || submodule is Sequential) { + NetlistUtils.collapseAlwaysBlockPorts( + _alwaysBlockPortCollapseIndex!, + instance, + cellPortDirs, + cellConnections, + getIds, + ); + _filterProceduralConstants(instance, cellPortDirs, cellConnections); + _renameProceduralPorts(instance, cellPortDirs, cellConnections); + } + + if (!isLeaf) { + for (final portEntry in submodule.inputs.entries) { + final portName = portEntry.key; + final port = portEntry.value; + if (port is! LogicArray || + cellPortDirs[portName] != NetlistPortDirection.input) { + continue; + } + final bits = cellConnections[portName]; + if (bits == null || bits.length != port.width) { + continue; + } + + final concatConnections = >{}; + final concatDirections = {}; + var lowerIndex = 0; + for (final element in port.elements) { + final upperIndex = lowerIndex + element.width - 1; + final concatPort = '[$upperIndex:$lowerIndex]'; + concatConnections[concatPort] = bits.sublist( + lowerIndex, + upperIndex + 1, + ); + concatDirections[concatPort] = NetlistPortDirection.input; + lowerIndex = upperIndex + 1; + } + + final concatOutput = [ + for (var i = 0; i < bits.length; i++) allocateWireId(), + ]; + concatConnections['Y'] = concatOutput; + concatDirections['Y'] = NetlistPortDirection.output; + cellConnections[portName] = concatOutput; + + cells['array_concat_${cellKey}_$portName'] = NetlistCell( + type: r'$concat', + parameters: { + for (var index = 0; index < port.elements.length; index++) + 'IN${index}_WIDTH': port.elements[index].width, + }, + portDirections: concatDirections, + connections: concatConnections, + ).toJson(); + } + } + + cells[cellKey] = NetlistCell( + type: mapped?.cellType ?? defaultCellType, + parameters: mapped?.parameters ?? const {}, + portDirections: cellPortDirs, + connections: cellConnections, + ).toJson(); + } + + definition.subModuleInstantiations + .where((instance) => !instance.needsInstantiation) + .map((instance) => emittedCellKeys[instance]) + .whereType() + .forEach(cells.remove); + } + + /// Lowers a flip-flop with a dynamic synchronous reset value to Yosys cells. + /// + /// `$sdff` requires a constant reset value. The reset mux precedes the + /// enable, and the enable is ORed with reset so reset retains priority. + bool _emitDynamicSynchronousResetFlipFlop( + String cellKey, + FlipFlop flipFlop, + Map rawPortDirs, + Map> rawConnections, + ) { + if (flipFlop.asyncReset) { + return false; + } + + String? findInput(String unpreferredName, String name) { + for (final entry in rawPortDirs.entries) { + if (entry.value == NetlistPortDirection.input && + (entry.key.startsWith(unpreferredName) || entry.key == name)) { + return entry.key; + } + } + return null; + } + + final clk = findInput('_clk', 'clk'); + final d = findInput('_d', 'd'); + final en = findInput('_en', 'en'); + final reset = findInput('_reset', 'reset'); + final resetValue = findInput('_resetValue', 'resetValue'); + final q = rawPortDirs.entries + .where((entry) => entry.value == NetlistPortDirection.output) + .map((entry) => entry.key) + .firstOrNull; + if (clk == null || + d == null || + reset == null || + resetValue == null || + q == null) { + return false; + } + + final dBits = rawConnections[d] ?? const []; + final resetValueBits = rawConnections[resetValue] ?? const []; + final resetBits = rawConnections[reset] ?? const []; + final clkBits = rawConnections[clk] ?? const []; + final qBits = rawConnections[q] ?? const []; + if (dBits.isEmpty || + dBits.length != resetValueBits.length || + resetBits.length != 1 || + clkBits.length != 1 || + qBits.length != dBits.length) { + return false; + } + + final resetMuxOutput = + List.generate(dBits.length, (_) => allocateWireId()); + cells['${cellKey}_reset_mux'] = NetlistCell( + type: r'$mux', + parameters: {'WIDTH': dBits.length}, + portDirections: { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'S': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: { + 'A': dBits, + 'B': resetValueBits, + 'S': resetBits, + 'Y': resetMuxOutput, + }, + ).toJson(); + + final hasEnable = en != null && rawConnections.containsKey(en); + final dffConnections = >{ + 'CLK': clkBits, + 'D': resetMuxOutput, + 'Q': qBits, + }; + final dffDirections = { + 'CLK': NetlistPortDirection.input, + 'D': NetlistPortDirection.input, + 'Q': NetlistPortDirection.output, + }; + final dffParameters = { + 'WIDTH': dBits.length, + 'CLK_POLARITY': 1, + }; + if (hasEnable) { + final enableBits = rawConnections[en] ?? const []; + if (enableBits.length != 1) { + return false; + } + final effectiveEnable = [allocateWireId()]; + cells['${cellKey}_reset_enable'] = NetlistCell( + type: r'$or', + parameters: { + 'A_WIDTH': 1, + 'B_WIDTH': 1, + 'Y_WIDTH': 1, + }, + portDirections: { + 'A': NetlistPortDirection.input, + 'B': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: { + 'A': enableBits, + 'B': resetBits, + 'Y': effectiveEnable, + }, + ).toJson(); + dffConnections['EN'] = effectiveEnable; + dffDirections['EN'] = NetlistPortDirection.input; + dffParameters['EN_POLARITY'] = 1; + } + + cells[cellKey] = NetlistCell( + type: hasEnable ? r'$dffe' : r'$dff', + parameters: dffParameters, + portDirections: dffDirections, + connections: dffConnections, + ).toJson(); + return true; + } + + /// Emits port and internal netnames, fills unnamed connection coverage, + /// and optionally removes names for undriven wires. + void processNetnames({ + required List Function(List bits) applyAlias, + required Map arraySliceOldToNew, + required Map arrayConcatOldToNew, + required bool pruneUndriven, + required Set drivenBits, + }) { + final emittedNames = {}; + final isInlineSystemVerilog = _module is InlineSystemVerilog; + + void addNetname( + String name, + List bits, { + bool hideName = false, + bool computed = false, + Map? logicType, + }) { + if (!emittedNames.add(name)) { + return; + } + netnames[name] = { + 'bits': bits, + if (hideName) 'hide_name': 1, + if (logicType != null) 'logic_type': logicType, + 'attributes': { + if (computed || isInlineSystemVerilog) 'computed': 1, + }, + }; + } + + for (final port in ports.entries) { + addNetname( + Sanitizer.sanitizeSV(port.key), + (port.value['bits']! as List).cast(), + logicType: port.value['logic_type'] as Map?, + ); + } + + final aggregateConstructors = + inputBits, List outputBits})>>{}; + for (final cellEntry in cells.entries) { + final cell = cellEntry.value; + final cellType = cell['type'] as String?; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final inputBits = []; + final outputBits = []; + + if (cellType == r'$concat') { + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if (dirs[portEntry.key] == 'output') { + outputBits.addAll(bits); + } else { + inputBits.addAll(bits); + } + } + } else if (cellType == r'$struct_pack') { + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if (portEntry.key == 'Y' && dirs[portEntry.key] == 'output') { + outputBits.addAll(bits); + } else if (dirs[portEntry.key] == 'input') { + inputBits.addAll(bits); + } + } + } else { + continue; + } + + if (inputBits.length == outputBits.length) { + aggregateConstructors + .putIfAbsent(Object.hashAll(inputBits), () => []) + .add(( + inputBits: inputBits, + outputBits: outputBits, + )); + } + } + + List resolveAggregateBits(List bits) { + final candidates = aggregateConstructors[Object.hashAll(bits)]; + if (candidates == null) { + return bits; + } + for (final constructorBits in candidates) { + if (bits.length == constructorBits.inputBits.length) { + var matches = true; + for (var index = 0; index < bits.length; index++) { + if (bits[index] != constructorBits.inputBits[index]) { + matches = false; + break; + } + } + if (matches) { + return constructorBits.outputBits; + } + } + } + return bits; + } + + if (synthDef != null) { + for (final entry in _synthLogicIds.entries.where( + (entry) => !entry.key.isConstant && !entry.key.declarationCleared, + )) { + final synthLogic = entry.key; + final name = NetlistUtils.tryGetSynthLogicName(synthLogic); + if (name == null) { + continue; + } + var bits = applyAlias(entry.value.cast()); + if (arraySliceOldToNew.isNotEmpty && + synthLogic is SynthLogicArrayElement) { + bits = [ + for (final bit in bits) + if (bit is int) arraySliceOldToNew[bit] ?? bit else bit, + ]; + } + if (arrayConcatOldToNew.isNotEmpty && + synthLogic is SynthLogicArrayElement) { + bits = [ + for (final bit in bits) + if (bit is int) arrayConcatOldToNew[bit] ?? bit else bit, + ]; + } + bits = resolveAggregateBits(bits); + final typeLogic = NetlistUtils.typeLogicFromSynthLogic(synthLogic); + addNetname( + Sanitizer.sanitizeSV(name), + bits, + logicType: typeLogic == null + ? null + : NetlistUtils.buildLogicType(typeLogic, bits), + ); + } + } + + for (final cell in cells.entries.where( + (entry) => entry.value['type'] == r'$const', + )) { + final connections = + cell.value['connections'] as Map>?; + if (connections != null && connections.isNotEmpty) { + addNetname(cell.key, connections.values.first, computed: true); + } + } + + final coveredIds = netnames.values + .expand( + (netname) => + ((netname! as Map)['bits'] as List?) ?? [], + ) + .whereType() + .toSet(); + for (final cell in cells.entries) { + final connections = + cell.value['connections'] as Map? ?? {}; + for (final connection in connections.entries) { + final missingBits = []; + for (final bit in connection.value as List) { + if (bit is int && coveredIds.add(bit)) { + missingBits.add(bit); + } + } + if (missingBits.isNotEmpty) { + addNetname( + Sanitizer.sanitizeSV('${cell.key}_${connection.key}'), + missingBits, + hideName: true, + ); + } + } + } + + if (pruneUndriven) { + netnames.removeWhere((_, rawNetname) { + final netname = rawNetname as Map?; + final bits = netname?['bits'] as List?; + if (bits == null) { + return false; + } + final integerBits = bits.whereType(); + return integerBits.isNotEmpty && !integerBits.any(drivenBits.contains); + }); + } + } + + /// Separates passthrough outputs and removes dead cells when requested. + void processCellCleanup({required bool enableDce}) { + final inputBitIds = ports.values + .where( + (port) => + port['direction'] == 'input' || port['direction'] == 'inout', + ) + .expand((port) => port['bits']! as List) + .whereType() + .toSet(); + var bufferIndex = 0; + for (final port in ports.entries.where( + (entry) => entry.value['direction'] == 'output', + )) { + final outputBits = (port.value['bits']! as List).cast(); + if (!outputBits.any((bit) => bit is int && inputBitIds.contains(bit))) { + continue; + } + final freshBits = List.generate( + outputBits.length, + (_) => allocateWireId(), + ); + cells['passthrough_buf_$bufferIndex'] = NetlistUtils.makeBufCell( + outputBits.length, + outputBits, + freshBits, + ); + port.value['bits'] = freshBits; + bufferIndex++; + } + + if (!enableDce) { + return; + } + var changed = true; + while (changed) { + changed = false; + final drivenIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'input', 'inout'}, + cellDirection: 'output', + ); + final consumedIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'output', 'inout'}, + cellDirection: 'input', + ); + + cells + ..removeWhere((_, rawCell) { + final cell = rawCell as Map; + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + final inputPorts = connections.entries.where( + (port) => directions[port.key] == 'input', + ); + if (inputPorts.isEmpty) { + return false; + } + final allUndriven = !inputPorts + .expand((port) => port.value as List) + .any( + (bit) => + (bit is int && drivenIds.contains(bit)) || bit is String, + ); + if (allUndriven) { + changed = true; + } + return allUndriven; + }) + ..removeWhere((_, rawCell) { + final cell = rawCell as Map; + final cellType = cell['type'] as String? ?? ''; + if (!cellType.startsWith(r'$')) { + return false; + } + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + final outputPorts = connections.entries.where( + (port) => directions[port.key] == 'output', + ); + if (outputPorts.isEmpty) { + return false; + } + final allUnconsumed = !outputPorts + .expand((port) => port.value as List) + .whereType() + .any(consumedIds.contains); + if (allUnconsumed) { + changed = true; + } + return allUnconsumed; + }); + } + } + + /// Emits constant driver cells and optionally removes floating constants. + void processConstants({ + required List Function(List bits) applyAlias, + required bool pruneFloating, + }) { + var constantIndex = 0; + final emittedConstantWires = {}; + for (final entry in _synthLogicIds.entries + .where((entry) => entry.key.isConstant) + .where((entry) => !_blockedConstSynthLogics.contains(entry.key)) + .where((entry) => entry.value.isNotEmpty)) { + final constant = NetlistUtils.constValueFromSynthLogic(entry.key); + if (constant == null) { + continue; + } + final resolvedIds = applyAlias(entry.value.cast()); + final firstWire = resolvedIds.firstWhere( + (bit) => bit is int, + orElse: () => -1, + ); + if (firstWire is int && firstWire >= 0) { + if (emittedConstantWires.contains(firstWire)) { + continue; + } + emittedConstantWires.addAll(resolvedIds.whereType()); + } + + final valuePart = NetlistUtils.constValuePart(constant); + final cellName = 'const_${constantIndex}_$valuePart'; + final valueLiteral = valuePart.replaceFirst('_', "'"); + cells[cellName] = NetlistCell( + type: r'$const', + portDirections: { + valueLiteral: NetlistPortDirection.output, + }, + connections: >{valueLiteral: resolvedIds}, + ).toJson(); + constantIndex++; + } + + if (!pruneFloating) { + return; + } + final consumedIds = NetlistValidation.connectedBits( + ports, + cells, + portDirections: const {'output', 'inout'}, + cellDirection: 'input', + ); + cells.removeWhere((_, rawCell) { + final cell = rawCell as Map; + if (cell['type'] != r'$const') { + return false; + } + final connections = cell['connections']! as Map; + final directions = cell['port_directions']! as Map; + return !connections.entries + .where((port) => directions[port.key] == 'output') + .expand((port) => port.value as List) + .whereType() + .any(consumedIds.contains); + }); + } + + /// Removes procedural constant ports and records their constants as blocked. + void _filterProceduralConstants( + SynthSubModuleInstantiation instance, + Map portDirections, + Map> connections, + ) { + final portsToRemove = []; + for (final port in connections.entries) { + final synthLogic = + instance.inputMapping[port.key] ?? instance.inOutMapping[port.key]; + if (synthLogic != null && NetlistUtils.isConstantSynthLogic(synthLogic)) { + portsToRemove.add(port.key); + _blockedConstSynthLogics.add(synthLogic.resolved); + } + } + for (final portName in portsToRemove) { + connections.remove(portName); + portDirections.remove(portName); + } + } + + /// Renames procedural ports to match their resolved synth logic names. + void _renameProceduralPorts( + SynthSubModuleInstantiation instance, + Map portDirections, + Map> connections, + ) { + final renames = {}; + for (final portName in connections.keys.toList()) { + final synthLogic = instance.inputMapping[portName] ?? + instance.outputMapping[portName] ?? + instance.inOutMapping[portName]; + if (synthLogic == null) { + continue; + } + final resolvedName = NetlistUtils.tryGetSynthLogicName( + synthLogic.resolved, + ); + if (resolvedName != null && resolvedName != portName) { + renames[portName] = resolvedName; + } + } + + for (final rename in renames.entries) { + final bits = connections.remove(rename.key)!; + final direction = portDirections.remove(rename.key)!; + var newName = rename.value; + if (connections.containsKey(newName)) { + newName = '${rename.value}_${rename.key}'; + } + connections[newName] = bits; + portDirections[newName] = direction; + } + } +} diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart new file mode 100644 index 000000000..4f22c5d0b --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -0,0 +1,734 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_passes.dart +// Post-processing optimization passes for netlist synthesis. +// +// 2025 February 11 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; + +/// Post-processing optimization passes for netlist synthesis. +/// +/// All methods are static — no instances are created. +@internal +class NetlistPasses { + /// Prevents construction of this static utility class. + NetlistPasses._(); + + /// Collects a combined modules map from [SynthesisResult]s suitable for + /// JSON emission. + static Map> collectModuleEntries( + Iterable results, { + Module? topModule, + bool includeCellConnections = true, + }) { + final allModules = >{}; + for (final result in results) { + if (result is NetlistSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = _copyObjectMap(result.attributes); + if (topModule != null && result.module == topModule) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': _copyNestedMaps(result.ports), + 'cells': _copyCells( + result.cells, + includeConnections: includeCellConnections, + ), + 'netnames': _copyObjectMap(result.netnames), + }; + } + } + return allModules; + } + + /// Deep-copies cell maps, optionally omitting connection payloads. + static Map> _copyCells( + Map> source, { + required bool includeConnections, + }) => + { + for (final entry in source.entries) + entry.key: _copyObjectMap( + includeConnections + ? entry.value + : (Map.of(entry.value)..remove('connections')), + ), + }; + + /// Deep-copies a map whose values are JSON-like object maps. + static Map> _copyNestedMaps( + Map> source, + ) => + { + for (final entry in source.entries) + entry.key: _copyObjectMap(entry.value), + }; + + /// Deep-copies a JSON-like object map. + static Map _copyObjectMap(Map source) => { + for (final entry in source.entries) + entry.key: _copyJsonValue(entry.value), + }; + + /// Deep-copies a JSON-like value while preserving scalar objects. + static Object? _copyJsonValue(Object? value) { + if (value is Map) { + return { + for (final entry in value.entries) + entry.key as String: _copyJsonValue(entry.value), + }; + } + if (value is List) { + return [for (final element in value) _copyJsonValue(element)]; + } + return value; + } + + // ════════════════════════════════════════════════════════════════════ + // Unified transparent-cell clustering + // ════════════════════════════════════════════════════════════════════ + + /// Transparent cell types that only reshuffle / rename bits and can be + /// cleaned up when their outputs are unconsumed. + static const _transparentCleanupTypes = { + r'$buf', + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + /// Transparent cell types whose bit mappings can be safely clustered. + static const _clusterableTransparentTypes = { + r'$buf', + r'$slice', + }; + + /// Unified transparent-cell clustering pass. + /// + /// **Phase 1 — Cluster identification:** + /// 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 (_clusterableTransparentTypes.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); + } + } + + /// Removes transparent helper cells whose outputs are not consumed by any + /// other cell or module output. + static void removeUnconsumedTransparentCells( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + var changed = true; + while (changed) { + changed = false; + final consumedBits = {}; + + for (final cell in cells.values) { + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + for (final entry in conns.entries) { + final direction = dirs[entry.key] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + consumedBits.addAll((entry.value as List).whereType()); + } + } + for (final port in ports.values) { + final portMap = port as Map; + final direction = portMap['direction'] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + consumedBits.addAll((portMap['bits'] as List).whereType()); + } + + cells.removeWhere((_, cell) { + if (!_transparentCleanupTypes.contains(cell['type'] as String?)) { + return false; + } + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final outputBits = {}; + for (final entry in conns.entries) { + final direction = dirs[entry.key] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + outputBits.addAll((entry.value as List).whereType()); + } + final remove = + outputBits.isNotEmpty && !outputBits.any(consumedBits.contains); + changed = changed || remove; + return remove; + }); + } + } + } + + /// Removes `$concat` cells that only rename an already-named bit vector. + /// + /// Explicit array concat cells are useful when they show a real regrouping, + /// but a concat whose flattened inputs exactly match an existing netname is + /// just an alias. Redirect its consumers to the named source bits and remove + /// the cell. + static void removeTrivialConcatAliases( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + final netnames = moduleDef['netnames'] as Map?; + if (cells == null || cells.isEmpty || netnames == null) { + continue; + } + + final namedBitVectors = [ + for (final rawNetname in netnames.values) + if (rawNetname is Map && rawNetname['bits'] is List) + (rawNetname['bits'] as List).cast(), + ]; + if (namedBitVectors.isEmpty) { + continue; + } + + var changed = true; + while (changed) { + changed = false; + final replacementByOutputBit = {}; + final cellsToRemove = {}; + + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$concat' || + entry.key.startsWith('array_concat_output_')) { + continue; + } + + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + final outputBits = []; + final inputBits = []; + + for (final portEntry in conns.entries) { + final bits = (portEntry.value as List).cast(); + if ((dirs[portEntry.key] as String?) == 'output') { + outputBits.addAll(bits); + } else { + inputBits.addAll(bits); + } + } + + if (outputBits.length != inputBits.length || + !_matchesNamedVector(inputBits, namedBitVectors)) { + continue; + } + + for (var index = 0; index < outputBits.length; index++) { + final outputBit = outputBits[index]; + if (outputBit is int) { + replacementByOutputBit[outputBit] = inputBits[index]; + } + } + cellsToRemove.add(entry.key); + } + + if (replacementByOutputBit.isEmpty) { + continue; + } + + void rewriteBits(List bits) { + for (var index = 0; index < bits.length; index++) { + final bit = bits[index]; + if (bit is int && replacementByOutputBit.containsKey(bit)) { + bits[index] = replacementByOutputBit[bit]!; + } + } + } + + final ports = moduleDef['ports'] as Map? ?? {}; + for (final rawPort in ports.values) { + final port = rawPort as Map; + rewriteBits((port['bits'] as List).cast()); + } + + for (final entry in cells.entries) { + if (cellsToRemove.contains(entry.key)) { + continue; + } + final cell = entry.value; + final conns = cell['connections'] as Map? ?? {}; + for (final rawBits in conns.values) { + rewriteBits((rawBits as List).cast()); + } + } + + for (final rawNetname in netnames.values) { + if (rawNetname is Map && rawNetname['bits'] is List) { + rewriteBits((rawNetname['bits'] as List).cast()); + } + } + + cellsToRemove.forEach(cells.remove); + changed = true; + } + } + } + + /// Replaces a `$concat` of adjacent `$slice` outputs from the same source + /// with one wider `$slice`. + static void collapseConcatOfAdjacentSlices( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final ports = moduleDef['ports'] as Map? ?? {}; + final cellsToRemove = {}; + + for (final concatEntry in cells.entries.toList()) { + final concat = concatEntry.value; + if (concat['type'] != r'$concat' || + concatEntry.key.startsWith('array_concat_output_')) { + continue; + } + + final concatDirs = + concat['port_directions'] as Map? ?? {}; + final concatConns = + concat['connections'] as Map? ?? {}; + final inputSliceRefs = <({String name, Map cell})>[]; + final outputBits = []; + var valid = true; + + for (final portEntry in concatConns.entries) { + if ((concatDirs[portEntry.key] as String?) == 'output') { + outputBits.addAll((portEntry.value as List).cast()); + continue; + } + + final inputBits = (portEntry.value as List).cast(); + final sliceEntry = _findSliceDrivingBits(cells, inputBits); + if (sliceEntry == null) { + valid = false; + break; + } + inputSliceRefs.add((name: sliceEntry.key, cell: sliceEntry.value)); + } + + if (!valid || inputSliceRefs.isEmpty || outputBits.isEmpty) { + continue; + } + + final firstSlice = inputSliceRefs.first.cell; + final firstParams = + firstSlice['parameters'] as Map? ?? {}; + final firstConnections = + firstSlice['connections'] as Map?; + final sourceRawBits = firstConnections?['A'] as List?; + if (sourceRawBits == null) { + continue; + } + final sourceBits = sourceRawBits.cast(); + final startOffset = firstParams['OFFSET'] as int?; + final sourceWidth = firstParams['A_WIDTH'] as int?; + if (startOffset == null || sourceWidth == null) { + continue; + } + + var expectedOffset = startOffset; + var combinedWidth = 0; + for (final sliceRef in inputSliceRefs) { + final slice = sliceRef.cell; + final params = slice['parameters'] as Map? ?? {}; + final conns = slice['connections'] as Map? ?? {}; + final sliceSourceBits = (conns['A'] as List).cast(); + final offset = params['OFFSET'] as int?; + final width = params['Y_WIDTH'] as int?; + + if (offset != expectedOffset || + width == null || + params['A_WIDTH'] != sourceWidth || + !_sameBits(sliceSourceBits, sourceBits)) { + valid = false; + break; + } + + expectedOffset += width; + combinedWidth += width; + } + + if (!valid || combinedWidth != outputBits.length) { + continue; + } + + cells[concatEntry.key] = NetlistCell( + hideName: concat['hide_name'] as int? ?? 0, + type: r'$slice', + parameters: { + 'OFFSET': startOffset, + 'A_WIDTH': sourceWidth, + 'Y_WIDTH': combinedWidth, + }, + attributes: (concat['attributes'] as Map?)?.cast() ?? + const {}, + portDirections: const { + 'A': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: >{ + 'A': sourceBits, + 'Y': outputBits, + }, + ).toJson(); + + for (final sliceRef in inputSliceRefs) { + if (!_sliceOutputConsumedOutside( + sliceRef.name, + sliceRef.cell, + cells, + ports, + )) { + cellsToRemove.add(sliceRef.name); + } + } + } + + cellsToRemove.forEach(cells.remove); + } + } + + /// Finds a slice cell whose output bits exactly match [bits]. + static MapEntry>? _findSliceDrivingBits( + Map> cells, + List bits, + ) { + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$slice') { + continue; + } + final conns = cell['connections'] as Map? ?? {}; + final yBits = (conns['Y'] as List?)?.cast(); + if (yBits != null && _sameBits(yBits, bits)) { + return entry; + } + } + return null; + } + + /// Checks whether a slice output is still consumed outside that slice cell. + static bool _sliceOutputConsumedOutside( + String sliceName, + Map slice, + Map> cells, + Map ports, + ) { + final sliceConns = slice['connections'] as Map? ?? {}; + final outputBits = + ((sliceConns['Y'] as List?) ?? const []).whereType(); + final outputBitSet = outputBits.toSet(); + if (outputBitSet.isEmpty) { + return false; + } + + for (final rawPort in ports.values) { + final port = rawPort as Map; + final direction = port['direction'] as String?; + if (direction != 'output' && direction != 'inout') { + continue; + } + final bits = (port['bits'] as List).whereType(); + if (bits.any(outputBitSet.contains)) { + return true; + } + } + + for (final entry in cells.entries) { + if (entry.key == sliceName) { + continue; + } + final cell = entry.value; + final dirs = cell['port_directions'] as Map? ?? {}; + final conns = cell['connections'] as Map? ?? {}; + for (final portEntry in conns.entries) { + final direction = dirs[portEntry.key] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + final bits = (portEntry.value as List).whereType(); + if (bits.any(outputBitSet.contains)) { + return true; + } + } + } + return false; + } + + /// Checks whether [bits] exactly matches any known named bit vector. + static bool _matchesNamedVector( + List bits, + List> namedBitVectors, + ) => + namedBitVectors.any( + (namedBits) => + namedBits.length == bits.length && + namedBits.indexed.every((entry) => entry.$2 == bits[entry.$1]), + ); + + /// Checks whether two bit vectors have identical contents and order. + static bool _sameBits(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); + + /// Populates [bitMap] with output-wire-bit → input-wire-bit entries + /// for a single transparent cell. + static void _mapCellBits(Map cell, Map bitMap) { + final type = cell['type']! as String; + 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; + } + } + } + } + + /// 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_port_direction.dart b/lib/src/synthesizers/netlist/netlist_port_direction.dart new file mode 100644 index 000000000..610ab774e --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_port_direction.dart @@ -0,0 +1,27 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_port_direction.dart +// Type-safe netlist port directions and JSON serialization. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +/// A port direction while constructing a netlist. +@internal +enum NetlistPortDirection { + input, + output, + inout, +} + +/// Converts typed [directions] to the strings required by Yosys JSON. +@internal +Map serializePortDirections( + Map directions, +) => + { + for (final entry in directions.entries) entry.key: entry.value.name, + }; diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart new file mode 100644 index 000000000..c63104907 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -0,0 +1,310 @@ +// 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 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/output_file_writer.dart' + if (dart.library.io) 'package:rohd/src/diagnostics/output_file_writer_io.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 ArtifactProducingService { + /// 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 default location written by [write], or `null`. + final String? outputPath; + + /// 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 [configuration] for netlist synthesis and optionally + /// [register]s this instance with [ModuleServices] for DevTools lookup. + NetlistService( + Module module, { + NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), + String? packageRoot, + bool register = true, + this.outputPath, + super.outputDirectory, + super.outputBaseName, + }) : super(module) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating NetlistService. ' + 'Call build() first.', + ); + } + + final effectiveRoot = packageRoot; + synthesizer = NetlistSynthesizer(configuration: configuration); + 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 generated netlist JSON artifact. + @override + Iterable get artifacts => [ + ModuleServiceArtifact( + fileName: '$outputBaseName.rohd.json', + mediaType: 'application/json', + openRead: () => Stream.value(utf8.encode(json)), + ), + ]; + + /// 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. + void write([String? path]) { + final target = + path ?? outputPath ?? '$outputDirectory/$outputBaseName.rohd.json'; + writeOutputTextFile(target, _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(); + + /// Builds the slim hierarchy JSON with per-cell connections omitted. + 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_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart new file mode 100644 index 000000000..4ba4fe581 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -0,0 +1,139 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synth_module_definition.dart +// Synth module definition specialization for netlist synthesis. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthModuleDefinition] that preserves cells for netlist synthesis. +@internal +class NetlistSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a netlist synthesis definition for [module]. + NetlistSynthModuleDefinition(Module module) : super(module) { + // Create explicit $slice cells for LogicArray input ports so the + // netlist shows select gates for element extraction rather than + // flat bit aliasing. + module.inputs.values.whereType().forEach( + _subsetReceiveArrayPort, + ); + + // Same for LogicArray outputs on submodules (received into this scope). + final subModuleOutputArrays = module.subModules + .expand((sub) => sub.outputs.values) + .whereType() + .toSet() + ..forEach(_subsetReceiveArrayPort); + + // Create explicit $concat cells for internal LogicArrays whose elements + // are driven independently (e.g. by constants) and then consumed by + // submodule input ports. This parallels what _subsetReceiveArrayPort does + // on the decomposition side. + // + // Skip arrays that were merged with a port array's SynthLogic; those are + // already structurally decomposed by the $slice cells created above. + // Also skip submodule output arrays that already received $slice cells. + final portArrays = { + ...module.inputs.values.whereType(), + ...module.outputs.values.whereType(), + ...module.inOuts.values.whereType(), + }; + final excludedArrays = { + ...portArrays, + ...subModuleOutputArrays, + }; + + void addNestedArrays(LogicArray array) { + for (final element in array.elements) { + if (element is LogicArray) { + excludedArrays.add(element); + addNestedArrays(element); + } + } + } + + { + ...portArrays, + ...subModuleOutputArrays, + }.forEach(addNestedArrays); + final portArraySynthLogics = {}; + for (final portArray in excludedArrays) { + final synthLogic = logicToSynthMap[portArray]; + if (synthLogic != null) { + portArraySynthLogics.add(synthLogic.resolved); + } + } + module.internalSignals.whereType().where((signal) { + if (excludedArrays.contains(signal)) { + return false; + } + final synthLogic = logicToSynthMap[signal]; + if (synthLogic == null) { + return false; + } + return !portArraySynthLogics.contains(synthLogic.resolved); + }).forEach(_concatAssembleArray); + } + + /// Adds slice cells that decompose a LogicArray port into element signals. + void _subsetReceiveArrayPort(LogicArray port) { + final portSynth = getSynthLogic(port)!; + + var index = 0; + for (final element in port.elements) { + final elementSynth = getSynthLogic(element)!; + internalSignals.add(elementSynth); + + final subsetModule = SynthArraySlice( + Logic(width: port.width, name: 'DUMMY'), + index, + index + element.width - 1, + destination: element, + ); + + getSynthSubModuleInstantiation(subsetModule) + ..setOutputMapping(subsetModule.subset.name, elementSynth) + ..setInputMapping(subsetModule.original.name, portSynth) + ..pickName(module); + + index += element.width; + } + } + + /// Adds a concat cell that assembles independent LogicArray element signals. + void _concatAssembleArray(LogicArray array) { + final arraySynth = getSynthLogic(array)!; + final dummyElements = [ + for (final element in array.elements) + Logic(width: element.width, name: 'DUMMY'), + ]; + + // Swizzle reverses its inputs, so reverse here to keep in0 aligned with + // element[0], the least-significant array element. + final concatModule = SynthArrayConcat( + dummyElements.reversed.toList(), + destination: array, + ); + final instantiation = getSynthSubModuleInstantiation(concatModule) + ..setOutputMapping(concatModule.out.name, arraySynth); + + for (var index = 0; index < array.elements.length; index++) { + final elementSynth = getSynthLogic(array.elements[index])!; + internalSignals.add(elementSynth); + final inputName = concatModule.inputs.keys.elementAt(index); + instantiation.setInputMapping(inputName, elementSynth); + } + + instantiation.pickName(module); + } + + @override + void process() { + // Netlist synthesis preserves every submodule as a cell. + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart new file mode 100644 index 000000000..4e312cbb1 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -0,0 +1,122 @@ +// 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:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [SynthesisResult] that holds the netlist representation of a single +/// module level: its ports, cells, and netnames. +@internal +class NetlistSynthesisResult extends SynthesisResult { + /// The ports map: name → {direction, bits}. + final Map> ports; + + /// 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 Map> ports, + required Map> cells, + required Map netnames, + Map attributes = const {}, + }) : ports = _freezeNestedMap(ports), + cells = _freezeNestedMap(cells), + netnames = _freezeObjectMap(netnames), + attributes = _freezeObjectMap(attributes); + + /// Builds the JSON representation for this single module entry. + String _buildJson() { + 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': NetlistSynthesizer.formatVersion, + 'modules': {typeName: moduleEntry}, + }); + return [ + SynthFileContents( + name: '$typeName.rohd.json', + description: 'netlist for $typeName', + contents: contents, + ), + ]; + } +} + +Map> _freezeNestedMap( + Map> source, +) => + Map.unmodifiable({ + for (final entry in source.entries) + entry.key: _freezeObjectMap(entry.value), + }); + +Map _freezeObjectMap(Map source) => + Map.unmodifiable({ + for (final entry in source.entries) entry.key: _freezeObject(entry.value), + }); + +Object? _freezeObject(Object? value) { + if (value is Map) { + return _freezeObjectMap(value); + } + if (value is Map) { + return Map.unmodifiable({ + for (final entry in value.entries) entry.key: _freezeObject(entry.value), + }); + } + if (value is List) { + return List.unmodifiable(value.map(_freezeObject)); + } + if (value is Set) { + return Set.unmodifiable(value.map(_freezeObject)); + } + return value; +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart new file mode 100644 index 000000000..5bac59407 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -0,0 +1,1118 @@ +// 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:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_utils.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// 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 configuration = NetlistSynthesizerConfiguration( +/// collapseTransparentClusters: true, +/// ); +/// final synth = NetlistSynthesizer(configuration: configuration); +/// final builder = SynthBuilder(topModule, synth); +/// final json = synth.synthesizeToJson(topModule); +/// ``` +class NetlistSynthesizer extends Synthesizer { + /// The version of the ROHD extensions to the Yosys JSON netlist format. + /// + /// Consumers of ROHD-generated netlists must reject an unsupported version. + /// This version changes when ROHD adds or changes fields that affect how a + /// consumer interprets the netlist. + static const String formatVersion = '0.0.1'; + + /// The configuration controlling netlist synthesis. + /// + /// See [NetlistSynthesizerConfiguration] for documentation on individual + /// fields. + final NetlistSynthesizerConfiguration configuration; + + final SynthModuleStopPolicy _moduleStopPolicy; + + final NetlistCellMapper _netlistCellMapper; + + /// The hierarchy stopping policy used by this synthesizer. + SynthModuleStopPolicy get moduleStopPolicy => _moduleStopPolicy; + + /// Convenience accessor for the netlist-cell mapper. + @visibleForTesting + NetlistCellMapper get netlistCellMapper => _netlistCellMapper; + + /// Creates a [NetlistSynthesizer]. + /// + /// All synthesis parameters are bundled in [configuration]; see + /// [NetlistSynthesizerConfiguration] for documentation on each field. + NetlistSynthesizer({ + this.configuration = const NetlistSynthesizerConfiguration(), + }) : _moduleStopPolicy = configuration.moduleStopPolicy ?? + SynthModuleStopPolicy.netlist( + leafModulePredicate: configuration.leafModulePredicate), + _netlistCellMapper = + configuration.netlistCellMapper ?? NetlistCellMapper.withDefaults(); + + @override + bool generatesDefinition(Module module) => + moduleStopPolicy.generatesDefinition(module); + + @override + SynthesisResult synthesize( + Module module, + String Function(Module module) getInstanceTypeOfModule, { + SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults, + }) { + final attr = {'src': 'generated'}; + + final translation = NetlistModuleTranslation(module, + netlistCellMapper: netlistCellMapper, + generatesDefinition: generatesDefinition, + getInstanceTypeOfModule: getInstanceTypeOfModule) + ..processPorts() + ..processInternalWires() + ..processCells(); + final synthDef = translation.synthDef; + final ports = translation.ports; + final cells = translation.cells; + final getIds = translation.getIds; + + // -- Wire-ID aliasing from remaining assignments ------------------- + // SynthModuleDefinition._collapseAssignments may leave assignments + // 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 elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds + })>[]; + + // Pending $struct_pack fields: for output struct ports, instead of + // aliasing port bits to leaf bits (which causes "shorting"), we + // collect structure-pack field operations and emit explicit cells later. + // Each entry records: field (src) → port sub-range [lower:upper]. + final structPackFields = <({ + 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). + void aliasArrayChildren(SynthLogic src, SynthLogic dst) { + final srcLogic = src.logics.firstOrNull; + final dstLogic = dst.logics.firstOrNull; + if (srcLogic is! LogicArray || dstLogic is! LogicArray) { + return; + } + if (srcLogic.elements.length != dstLogic.elements.length) { + return; + } + + for (final (index, srcElement) in srcLogic.elements.indexed) { + final dstElement = dstLogic.elements[index]; + final srcElementSynth = synthDef.logicToSynthMap[srcElement]; + final dstElementSynth = synthDef.logicToSynthMap[dstElement]; + if (srcElementSynth == null || dstElementSynth == null) { + continue; + } + + final srcElementLogic = srcElementSynth.logics.firstOrNull; + final dstElementLogic = dstElementSynth.logics.firstOrNull; + if (srcElementLogic is LogicArray && dstElementLogic is LogicArray) { + aliasArrayChildren(srcElementSynth, dstElementSynth); + } + + final srcElementIds = getIds(srcElementSynth); + final dstElementIds = getIds(dstElementSynth); + final len = srcElementIds.length < dstElementIds.length + ? srcElementIds.length + : dstElementIds.length; + for (var i = 0; i < len; i++) { + if (dstElementIds[i] != srcElementIds[i]) { + idAlias[dstElementIds[i]] = srcElementIds[i]; + } + } + } + } + + for (final assignment + in synthDef.assignments.where((a) => a is! PartialSynthAssignment)) { + final srcIds = getIds(assignment.src); + final dstIds = getIds(assignment.dst); + final len = + 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]; + } + } + aliasArrayChildren(assignment.src, assignment.dst); + } + + // 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. + structPackFields.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 + // SynthArraySlice / SynthArrayConcat), 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 SynthArraySlice + // 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 SynthArraySlice) { + // 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 = inputSL.resolved; + 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 SynthArrayConcat) { + // 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_pack 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(( + 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(( + 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/concat 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( + SynthArraySlice.operationName, + )) { + 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) + if (b is int) + arraySliceOldToNew.putIfAbsent( + b, + translation.allocateWireId, + ) + else + 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( + SynthArraySlice.operationName, + )) { + 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) + if (b is int) arraySliceOldToNew[b] ?? b else b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + } + + // -- Elide trivial $slice cells ---------------------------------- + // Also elide struct_slice cells ([SynthStructureSlice] instances from + // `_subsetReceiveStructPort`) because the new `$struct_unpack` cells + // emitted below supersede them with better-named field-level connections. + cells.removeWhere((cellKey, cell) { + if (cell['type'] != r'$slice') { + return false; + } + // Unconditionally remove struct_slice cells — they are duplicated by + // $struct_unpack cells which carry field names. + if (cellKey.startsWith(SynthStructureSlice.operationName)) { + 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 = 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); + + final structLayout = parentLogic is LogicStructure + ? SynthStructureLayout(parentLogic) + : null; + + // Build port_directions and connections with one output per field. + final portDirs = { + 'A': NetlistPortDirection.input, + }; + final conns = >{'A': resolvedParentBits}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = structLayout?.fieldNameAt(f.offset, + fallbackName: f.elemLogic.name, anonymousUnpreferred: true) ?? + f.elemLogic.name; + // Disambiguate duplicate field names with index suffix. + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = NetlistPortDirection.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'] = structLayout?.fieldNameAt(f.offset, + fallbackName: f.elemLogic.name, anonymousUnpreferred: true) ?? + f.elemLogic.name; + params['FIELD_${i}_OFFSET'] = f.offset; + params['FIELD_${i}_WIDTH'] = f.width; + } + + cells['struct_unpack_${suIdx}_$structName'] = NetlistCell( + type: r'$struct_unpack', + parameters: params, + portDirections: portDirs, + connections: conns, + ).toJson(); + 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 emits explicit structure packing cells. + if (structPackFields.isNotEmpty) { + // Group by destination SynthLogic identity. + final packGroups = srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + })>>{}; + for (final sc in structPackFields) { + (packGroups[sc.dstSynthLogic] ??= []).add(sc); + } + + for (final entry in packGroups.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 metadata from the destination Logic. + final dstLogic = dstSynthLogic.logics.firstOrNull; + final structName = + dstLogic != null ? Sanitizer.sanitizeSV(dstLogic.name) : 'struct'; + final structLayout = + dstLogic is LogicStructure ? SynthStructureLayout(dstLogic) : null; + final cellName = dstLogic != null + ? NetlistUtils.synthesizedCellName( + operationName: SynthStructureConcat.operationName, + destination: dstLogic, + ) + : SynthStructureConcat.operationName; + + // Build port_directions and connections. + final portDirs = {}; + final conns = >{}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = structLayout?.fieldNameAt(f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name) ?? + f.srcSynthLogic.resolved.name; + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = NetlistPortDirection.input; + conns[portName] = f.resolvedSrcBits; + } + + // Output port Y: full destination bus. + portDirs['Y'] = NetlistPortDirection.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'] = structLayout?.fieldNameAt(f.dstLowerIndex, + fallbackName: f.srcSynthLogic.resolved.name) ?? + f.srcSynthLogic.resolved.name; + params['FIELD_${i}_OFFSET'] = f.dstLowerIndex; + params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; + } + + cells['${cellName}_$structName'] = NetlistCell( + type: r'$struct_pack', + parameters: params, + portDirections: portDirs, + connections: conns, + ).toJson(); + } + } + + translation + ..processCellCleanup(enableDce: configuration.enableDeadCellElimination) + ..processConstants( + applyAlias: applyAlias, + pruneFloating: configuration.enableDeadCellElimination); + + // -- Break shared wire IDs for array_concat cells -------------------- + // After aliasing, concat Y can share wire IDs with the independently + // driven element inputs (because LogicArray elements share the parent's + // bit storage). This makes concat Y a second driver of the element wires. + // + // Allocate fresh IDs for concat Y and redirect downstream consumers to + // those fresh IDs. The concat inputs keep the original element IDs, so + // data flow is: + // element drivers → concat input → concat Y (fresh IDs) → consumer + final arrayConcatOldToNew = {}; + final arrayConcatReplacements = + <({String cellKey, List oldBits, List newBits})>[]; + final outputPortBitSets = [ + for (final port in ports.values) + if ((port as Map)['direction'] == 'output') + (port['bits'] as List).whereType().toSet(), + ]; + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith(SynthArrayConcat.operationName)) { + continue; + } + if (cellEntry.key.startsWith('array_concat_output_')) { + 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(); + final oldBitSet = oldBits.whereType().toSet(); + if (outputPortBitSets.any((outputBits) => + outputBits.length == oldBitSet.length && + outputBits.containsAll(oldBitSet))) { + continue; + } + final newBits = [ + for (final b in oldBits) + if (b is int) translation.allocateWireId() else b, + ]; + conns[portEntry.key] = newBits; + arrayConcatReplacements + .add((cellKey: cellEntry.key, oldBits: oldBits, newBits: newBits)); + } + } + + final arrayConcatOutputProducers = >{}; + for (final (index, replacement) in arrayConcatReplacements.indexed) { + for (final bit in replacement.oldBits) { + if (bit is int) { + (arrayConcatOutputProducers[bit] ??= []).add(index); + } + } + } + + List rewriteArrayConcatConsumerBits( + List bits, { + String? consumingCellKey, + }) { + for (final replacement in arrayConcatReplacements) { + if (replacement.cellKey == consumingCellKey || + replacement.oldBits.length != bits.length) { + continue; + } + if (bits.indexed + .every((entry) => entry.$2 == replacement.oldBits[entry.$1])) { + return replacement.newBits; + } + } + + final newBits = []; + var changed = false; + for (final bit in bits) { + if (bit is! int) { + newBits.add(bit); + continue; + } + final producerIndices = arrayConcatOutputProducers[bit] + ?.where((index) => + arrayConcatReplacements[index].cellKey != consumingCellKey) + .toList(); + if (producerIndices == null || producerIndices.length != 1) { + newBits.add(bit); + continue; + } + final producer = arrayConcatReplacements[producerIndices.single]; + final bitIndex = producer.oldBits.indexOf(bit); + if (bitIndex < 0) { + newBits.add(bit); + continue; + } + newBits.add(producer.newBits[bitIndex]); + changed = true; + } + return changed ? newBits : bits; + } + + // Redirect downstream consumers: any input port or module output bit that + // matches an old concat Y ID gets replaced with the corresponding fresh ID. + if (arrayConcatReplacements.isNotEmpty) { + for (final portEntry in ports.values) { + final port = portEntry as Map; + if (port['direction'] != 'output') { + continue; + } + final bits = (port['bits'] as List).cast(); + final newBits = rewriteArrayConcatConsumerBits(bits); + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + port['bits'] = newBits; + } + } + + for (final cellEntry in cells.entries) { + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'input') { + continue; + } + final bits = (portEntry.value as List).cast(); + final newBits = rewriteArrayConcatConsumerBits(bits, + consumingCellKey: cellEntry.key); + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + + translation.processNetnames( + applyAlias: applyAlias, + arraySliceOldToNew: arraySliceOldToNew, + arrayConcatOldToNew: arrayConcatOldToNew, + pruneUndriven: configuration.enableDeadCellElimination, + drivenBits: configuration.enableDeadCellElimination + ? NetlistValidation.connectedBits(ports, cells, + portDirections: const {'input', 'inout'}, + cellDirection: 'output') + : const {}); + final netnames = translation.netnames; + + // -- Structural validation ------------------------------------------- + NetlistValidation.validate(ports, cells, module.name, netnames: netnames); + + return NetlistSynthesisResult(module, getInstanceTypeOfModule, + ports: ports, cells: cells, netnames: netnames, attributes: attr); + } + + /// Apply all post-processing passes to the modules map. + /// + /// This is the canonical pass ordering used by both the slim and full JSON + /// projections produced by [buildModulesMap] and [synthesizeToJson]. + void applyPostProcessingPasses(Map> modules) { + if (configuration.collapseTransparentClusters) { + NetlistPasses.collapseConcatOfAdjacentSlices(modules); + NetlistPasses.removeTrivialConcatAliases(modules); + NetlistPasses.applyTransparentClustering(modules); + NetlistPasses.removeUnconsumedTransparentCells(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. [slimMode] overrides the configured + /// default for this projection without modifying the retained results. + Map> buildModulesMap( + SynthBuilder synth, Module top, + {bool? slimMode}) { + final effectiveSlimMode = slimMode ?? configuration.slimMode; + final swEntries = Stopwatch()..start(); + final modules = NetlistPasses.collectModuleEntries(synth.synthesisResults, + topModule: top, includeCellConnections: !effectiveSlimMode); + 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, + {bool? slimMode}) { + final swCollect = Stopwatch()..start(); + final modules = buildModulesMap(synth, top, slimMode: slimMode); + swCollect.stop(); + + final swCompress = Stopwatch()..start(); + if (configuration.compressBitRanges) { + _compressModulesMap(modules); + } + swCompress.stop(); + + final combined = { + 'creator': 'NetlistSynthesizer (rohd)', + 'version': formatVersion, + 'modules': modules + }; + + final swEncode = Stopwatch()..start(); + final encoder = configuration.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] as List).cast()); + } + } + } + } + + 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. [slimMode] overrides the configured + /// output mode for this call, allowing expansion after a slim request. + String synthesizeToJson(Module top, {String? packageRoot, bool? slimMode}) { + final sb = SynthBuilder(top, this); + return generateCombinedJson(sb, top, slimMode: slimMode); + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart new file mode 100644 index 000000000..d7429cefc --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -0,0 +1,118 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer_configuration.dart +// Configuration for netlist synthesis. +// +// 2026 March 12 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; +export '../utilities/synth_module_stop_policy.dart'; + +/// Configuration for netlist synthesis. +/// +/// The netlist synthesizer serves two main consumer flows, both configured +/// through this configuration: +/// +/// **Flow 1 — Slim JSON** ([NetlistSynthesizer.synthesizeToJson] with +/// `slimMode: true`): +/// 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** ([NetlistSynthesizer.synthesizeToJson] with +/// `slimMode: false`): +/// Synthesizes the entire design with complete cell connections. +/// +/// [NetlistService] exposes these projections through [NetlistService.slimJson] +/// and [NetlistService.moduleJson], preserving a shared netlist representation +/// across the two flows. +/// +/// Both flows retain complete per-module synthesis results. Flow 1 skips cell +/// connection copying while collecting the emitted JSON projection. This keeps +/// slim output lightweight while guaranteeing a later expanded request has the +/// same cell keys, wire IDs, and connectivity as an initially expanded request. +/// +/// Bundles all parameters that control netlist generation into a single +/// object, making it easier to pass through call chains and to store +/// for incremental synthesis. +/// +/// Example usage: +/// ```dart +/// final synth = NetlistSynthesizer(); +/// ``` +class NetlistSynthesizerConfiguration { + /// The policy used to decide which modules stop hierarchy traversal and are + /// emitted as cells in their parent instead of as separate module + /// definitions. When `null`, [SynthModuleStopPolicy.netlist] is used. + /// + /// When this is provided, it owns the complete stopping policy and + /// [leafModulePredicate] is ignored. + final SynthModuleStopPolicy? moduleStopPolicy; + + /// Determines which modules should stop netlist hierarchy traversal and be + /// emitted as cells in their parent. + /// + /// Defaults to matching [FlipFlop] and its subclasses, which contain internal + /// sequential submodules but should be emitted as `$dff` netlist cells. + final SynthModuleLeafPredicate leafModulePredicate; + + /// The netlist-internal mapper used to convert selected leaf modules to + /// Yosys primitive cell types. When `null`, each synthesizer creates its own + /// mapper containing the default handlers. + @internal + final NetlistCellMapper? netlistCellMapper; + + /// When `true`, a single unified pass finds connected components of + /// all transparent cells (`$buf`, `$slice`, `$concat`, + /// `$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. + @internal + final bool collapseTransparentClusters; + + /// When `true`, dead-cell elimination is performed after aliasing to + /// remove cells whose inputs are entirely undriven or whose outputs + /// are entirely unconsumed. + @internal + final bool enableDeadCellElimination; + + /// When `true`, the synthesizer produces "slim" output: cell connection maps + /// are not copied into the emitted JSON projection. Netnames and ports are + /// still emitted with full wire-ID fidelity, while per-module synthesis + /// results retain complete connectivity. + final bool slimMode; + + /// When `true`, contiguous ascending runs of ≥3 integer bit IDs in + /// `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 `:`. + @internal + 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 configuration for netlist synthesis. + const NetlistSynthesizerConfiguration({ + this.moduleStopPolicy, + this.leafModulePredicate = _isFlipFlop, + this.netlistCellMapper, + @visibleForTesting this.collapseTransparentClusters = false, + @visibleForTesting this.enableDeadCellElimination = true, + this.slimMode = false, + @visibleForTesting this.compressBitRanges = false, + this.compactJson = false, + }); +} + +bool _isFlipFlop(Module module) => module is FlipFlop; diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart new file mode 100644 index 000000000..638afbd45 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -0,0 +1,536 @@ +// 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:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +typedef _BusSubsetCollapseInfo = ( + BusSubset, + SynthLogic, + SynthSubModuleInstantiation, +); + +typedef _SwizzleCollapseInfo = ( + String, + int, + int, + SynthLogic, + SynthSubModuleInstantiation, +); + +/// Reusable indexes for collapsing procedural-cell ports. +@internal +class NetlistAlwaysBlockPortCollapseIndex { + final Module _module; + final Map _busSubsets = {}; + final Map _swizzles = {}; + + /// Indexes aggregate-producing submodules in [synthDef]. + NetlistAlwaysBlockPortCollapseIndex(SynthModuleDefinition synthDef) + : _module = synthDef.module { + for (final instance in synthDef.subModuleInstantiations) { + final module = instance.module; + if (module is BusSubset) { + final output = instance.outputMapping.values.firstOrNull; + final input = instance.inputMapping.values.firstOrNull; + if (output != null && input != null) { + _busSubsets[output.resolved] = (module, input.resolved, instance); + } + } else if (module is Swizzle) { + final output = instance.outputMapping.values.firstOrNull; + if (output == null) { + continue; + } + + var offset = 0; + for (final input in instance.inputMapping.entries) { + final resolvedInput = input.value.resolved; + _swizzles[resolvedInput] = ( + input.key, + offset, + resolvedInput.width, + output.resolved, + instance, + ); + offset += resolvedInput.width; + } + } + } + } +} + +/// Shared utility functions for netlist synthesis and post-processing passes. +/// +/// All methods are static. +@internal +abstract class NetlistUtils { + /// Returns a deterministic cell name for an operation producing + /// [destination]. + static String synthesizedCellName({ + required String operationName, + required Logic destination, + }) => + '${Sanitizer.sanitizeSV(operationName)}_' + '${_destinationSuffix(destination)}'; + + static String _destinationSuffix(Logic destination) { + final module = destination.parentModule; + if (module == null) { + throw SynthException( + 'Cannot derive a netlist cell key for ${destination.name}: ' + 'the destination has no parent module.', + ); + } + + final parts = [ + _rootSignalIndexInModule(module, _rootLogic(destination)), + ..._logicElementPathIndices(destination), + ]; + return parts.map((part) => part.toString()).join('_'); + } + + static Logic _rootLogic(Logic destination) { + var root = destination; + while (root.parentStructure != null) { + root = root.parentStructure!; + } + return root; + } + + static List _logicElementPathIndices(Logic destination) { + final elementPath = []; + var current = destination; + while (current.parentStructure != null) { + final parent = current.parentStructure!; + final index = parent.elements.indexWhere( + (element) => identical(element, current), + ); + elementPath.insert(0, index < 0 ? current.arrayIndex ?? 0 : index); + current = parent; + } + return elementPath; + } + + static int _rootSignalIndexInModule(Module module, Logic root) { + final inputIndex = _identityIndex(module.inputs.values, root); + if (inputIndex != null) { + return inputIndex; + } + + final outputIndex = _identityIndex(module.outputs.values, root); + if (outputIndex != null) { + return module.inputs.length + outputIndex; + } + + final inOutIndex = _identityIndex(module.inOuts.values, root); + if (inOutIndex != null) { + return module.inputs.length + module.outputs.length + inOutIndex; + } + + final internalIndex = _identityIndex(module.internalSignals, root); + if (internalIndex != null) { + return module.inputs.length + + module.outputs.length + + module.inOuts.length + + internalIndex; + } + + throw SynthException( + 'Cannot derive a netlist cell key for ${root.name}: ' + 'the logic root is not registered with module ${module.name}.', + ); + } + + static int? _identityIndex(Iterable logics, Logic target) { + var index = 0; + for (final logic in logics) { + if (identical(logic, target)) { + return index; + } + index++; + } + return null; + } + + /// Indexes [synthLogics] by their corresponding name in [portMap]. + static Map portNamesForSynthLogics( + Iterable synthLogics, + Map portMap, + ) { + final namesByLogic = Map.identity() + ..addEntries( + portMap.entries.map((entry) => MapEntry(entry.value, entry.key)), + ); + final portNames = Map.identity(); + for (final synthLogic in synthLogics) { + for (final logic in synthLogic.logics) { + final portName = namesByLogic[logic]; + if (portName != null) { + portNames[synthLogic] = portName; + break; + } + } + } + return portNames; + } + + /// 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) => sl.nameOrNull; + + /// Create a `$buf` cell map. + static Map makeBufCell( + int width, + List aBits, + List yBits, + ) => + NetlistCell( + type: r'$buf', + parameters: {'WIDTH': width}, + portDirections: { + 'A': NetlistPortDirection.input, + 'Y': NetlistPortDirection.output, + }, + connections: >{'A': aBits, 'Y': yBits}, + ).toJson(); + + /// Collapses bit-slice ports of a Combinational/Sequential cell into + /// aggregate ports. + /// + /// **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( + NetlistAlwaysBlockPortCollapseIndex index, + SynthSubModuleInstantiation instance, + Map portDirs, + Map> connections, + List Function(SynthLogic) getIds, + ) { + // ── Input-side collapsing (BusSubset → Combinational) ────────────── + + // 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 = e.value.resolved; + final info = index._busSubsets[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 Dead Cell Elimination 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] = NetlistPortDirection.input; + } + + // ── Output-side collapsing (Combinational → Swizzle) ─────────────── + + // 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 = e.value.resolved; + final info = index._swizzles[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 hasModulePort = entry.value.any((member) { + final sl = instance.outputMapping[member.$1]; + if (sl == null) { + return false; + } + final resolved = sl.resolved; + return resolved.isPort(index._module); + }); + 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] = NetlistPortDirection.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) => sl.resolved.isConstant; + + /// Extract the Const value from a constant SynthLogic. + static Const? constValueFromSynthLogic(SynthLogic sl) { + final resolved = sl.resolved; + 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/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart new file mode 100644 index 000000000..45f0f86b8 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -0,0 +1,224 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_validation.dart +// Structural validation utilities for emitted netlists. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/src/exceptions/synth_exception.dart'; + +typedef _NetlistDriver = ({String description, bool isTriState}); + +/// Graph queries and structural checks for an emitted module netlist. +@internal +class NetlistValidation { + static const _nonDrivingAliasTypes = { + r'$slice', + r'$concat', + r'$struct_unpack', + r'$struct_pack', + }; + + /// Prevents construction of this static utility class. + NetlistValidation._(); + + /// Collects module-port and cell-connection bits with matching directions. + static Set connectedBits( + Map> ports, + Map> cells, { + required Set portDirections, + required String cellDirection, + }) => + { + ...ports.values + .where((port) => portDirections.contains(port['direction'])) + .expand((port) => (port['bits'] as List?) ?? const []) + .whereType(), + ...cells.values.expand((cell) { + final connections = + cell['connections'] as Map? ?? const {}; + final directions = + cell['port_directions'] as Map? ?? const {}; + return connections.entries + .where((port) => directions[port.key] == cellDirection) + .expand((port) => (port.value as List?) ?? const []) + .whereType(); + }), + }; + + /// Throws [NetlistValidationException] if the netlist has structural errors. + static void validate( + Map> ports, + Map> cells, + String moduleName, { + Map? netnames, + }) { + final issues = []; + + final driversByBit = _driversByBit(ports, cells); + + for (final entry in driversByBit.entries) { + if (!_hasConflictingDrivers(entry.value)) { + continue; + } + final drivers = entry.value.map((driver) => driver.description).toList(); + issues.add(NetlistValidationIssue( + 'wire bit ${entry.key} has multiple drivers: ' + '${drivers.join(', ')}', + wireBit: entry.key, + drivers: drivers, + )); + } + + if (netnames != null) { + for (final entry in netnames.entries) { + final netname = entry.value; + if (netname is! Map) { + continue; + } + final logicType = netname['logic_type']; + if (logicType is! Map || + (logicType['arrayDims'] is! List && logicType['fields'] is! List)) { + continue; + } + final bits = (netname['bits'] as List?)?.whereType() ?? const []; + final aggregateDrivers = <_NetlistDriver>{ + for (final bit in bits) + ...driversByBit[bit] ?? const <_NetlistDriver>[], + }; + if (!_hasConflictingDrivers(aggregateDrivers)) { + continue; + } + final drivers = + aggregateDrivers.map((driver) => driver.description).toList(); + issues.add(NetlistValidationIssue( + 'aggregate net "${entry.key}" is reached from multiple drivers: ' + '${drivers.join(', ')}', + netname: entry.key, + drivers: drivers, + )); + } + } + + if (issues.isNotEmpty) { + throw NetlistValidationException(moduleName, issues); + } + } + + /// Collects the port and cell output drivers for each integer bit ID. + static Map> _driversByBit( + Map> ports, + Map> cells, + ) { + final drivers = >{}; + + void addDriver(int bit, String description, {bool isTriState = false}) => + (drivers[bit] ??= <_NetlistDriver>[]) + .add((description: description, isTriState: isTriState)); + + for (final entry in ports.entries) { + final direction = entry.value['direction'] as String?; + if (direction != 'input') { + continue; + } + for (final bit in (entry.value['bits'] as List?) ?? const []) { + if (bit is int) { + addDriver(bit, 'port ${entry.key} ($direction)'); + } + } + } + + for (final entry in cells.entries) { + final connections = entry.value['connections'] as Map?; + final directions = + entry.value['port_directions'] as Map?; + if (connections == null || directions == null) { + continue; + } + final type = entry.value['type'] as String? ?? 'unknown'; + if (_nonDrivingAliasTypes.contains(type)) { + continue; + } + for (final port in connections.entries) { + final direction = directions[port.key] as String?; + final isTriStateOutput = type == r'$tribuf' && + (direction == 'output' || direction == 'inout'); + if (direction != 'output' && !isTriStateOutput) { + continue; + } + for (final bit in (port.value as List?) ?? const []) { + if (bit is int) { + addDriver( + bit, + 'cell ${entry.key}.${port.key} ($type)', + isTriState: isTriStateOutput, + ); + } + } + } + } + + return drivers; + } + + static bool _hasConflictingDrivers(Iterable<_NetlistDriver> drivers) { + var count = 0; + var allTriState = true; + for (final driver in drivers) { + count++; + allTriState &= driver.isTriState; + } + return count > 1 && !allTriState; + } +} + +/// A structural netlist validation failure. +@internal +class NetlistValidationException extends SynthException { + /// The module containing the structural errors. + final String moduleName; + + /// The structural errors found in [moduleName]. + final List issues; + + /// Creates a validation exception for [moduleName]. + NetlistValidationException( + this.moduleName, Iterable issues) + : issues = List.unmodifiable(issues), + super('Netlist validation failed for $moduleName.'); + + @override + String toString() => 'Netlist validation failed for $moduleName: ' + '${issues.length} issue(s) found.\n' + '${issues.join('\n')}'; +} + +/// A structural problem found while validating an emitted netlist. +@internal +class NetlistValidationIssue { + /// A human-readable explanation of the structural problem. + final String description; + + /// The affected wire bit, when the problem concerns one bit. + final int? wireBit; + + /// The affected aggregate net name, when applicable. + final String? netname; + + /// The drivers involved in the problem, when applicable. + final List drivers; + + /// Creates a structural validation issue. + NetlistValidationIssue( + this.description, { + this.wireBit, + this.netname, + Iterable drivers = const [], + }) : drivers = List.unmodifiable(drivers); + + @override + String toString() => description; +} diff --git a/lib/src/synthesizers/synthesis_result.dart b/lib/src/synthesizers/synthesis_result.dart index 27abb8fe9..b1b34e9b9 100644 --- a/lib/src/synthesizers/synthesis_result.dart +++ b/lib/src/synthesizers/synthesis_result.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesis_result.dart diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index 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/system_verilog_service.dart b/lib/src/synthesizers/systemverilog/system_verilog_service.dart new file mode 100644 index 000000000..814f14973 --- /dev/null +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -0,0 +1,218 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// system_verilog_service.dart +// Service wrapper for SystemVerilog synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +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 = SystemVerilogService( +/// dut, +/// outputDirectory: 'build', +/// multiFile: true, +/// ); +/// +/// // Write individual .sv files: +/// sv.writeOutputs(); +/// +/// // Or get the concatenated output (like generateSynth): +/// print(sv.output); +/// ``` +class SystemVerilogService extends CodeGenService { + /// The most recently registered [SystemVerilogService], or `null`. + static SystemVerilogService? current; + + /// The separator inserted between module definitions in the + /// concatenated single-file output from [allContents]. + /// + /// Matches the historical single-file synthesis output format. + static const moduleSeparator = '\n\n////////////////////\n\n'; + + /// Whether the artifact layout emits one `.sv` file per module definition + /// (`true`) or a single concatenated file (`false`). + final bool multiFile; + + /// Whether generated SV files include the ROHD generation header. + /// + /// Defaults to `true` for single-file output and `false` for multi-file + /// output, preserving the historical layout. Set explicitly to select any + /// combination of header and file layout. + final bool includeHeader; + + /// Configuration controlling generated SystemVerilog. + final SystemVerilogSynthesizerConfiguration configuration; + + /// The underlying [SynthBuilder] that drove synthesis. + late final SynthBuilder synthBuilder; + + /// The generated file contents (one per unique module definition). + late final List fileContents; + + /// Creates a [SystemVerilogService] for [module]. + /// + /// [module] must already be built. + /// + /// [outputDirectory] defaults to the current directory and [outputBaseName] + /// defaults to [Module.definitionName]. [includeHeader] defaults to + /// `!multiFile` to preserve the historical layout. Set [register] to `false` + /// to keep this service out of [ModuleServices]. + SystemVerilogService( + Module module, { + super.outputDirectory, + super.outputBaseName, + this.multiFile = false, + bool? includeHeader, + this.configuration = const SystemVerilogSynthesizerConfiguration(), + bool register = true, + }) : includeHeader = includeHeader ?? !multiFile, + super(module) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating SystemVerilogService. ' + 'Call build() first.'); + } + + synthBuilder = SynthBuilder( + module, SystemVerilogSynthesizer(configuration: configuration)); + fileContents = synthBuilder.getSynthFileContents(); + + 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 [output]. + 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} + */ + +'''; + + /// The generation header included in each emitted SV file, when enabled. + /// + /// Cached so output and emitted files always use the same timestamp. + late final String header = includeHeader ? synthHeader : ''; + + /// 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. + @override + late final String output = header + allContents; + + /// Returns SV output for a generated module [instanceTypeName], or `null` + /// when that instance type was not generated. + /// + /// The instance type name is [SynthesisResult.instanceTypeName], the + /// uniquified definition name used in the generated SV. + String? instanceTypeOutput(String instanceTypeName) { + for (final fc in fileContents) { + if (fc.name == instanceTypeName) { + return fc.contents; + } + } + return null; + } + + /// Returns a map from generated module instance type name to its SV contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + @Deprecated('Use instanceTypeOutput(instanceTypeName) for lookup or ' + 'fileContents for iteration instead.') + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + + /// The artifacts generated by this service. + /// + /// Single-file output is named `.sv`. Split output retains + /// the generated definition names so module references remain obvious. + @override + Iterable get artifacts => multiFile + ? [ + for (final fc in fileContents) + ModuleServiceArtifact( + fileName: '${fc.name}.sv', + mediaType: 'text/x-systemverilog', + openRead: () => Stream.value(utf8.encode(header + fc.contents)), + ), + ] + : [ + ModuleServiceArtifact( + fileName: '$outputBaseName.sv', + mediaType: 'text/x-systemverilog', + openRead: () => Stream.value(utf8.encode(output)), + ), + ]; + + /// Writes this service's artifacts to [outputDirectory]. + void writeOutputs() { + final directory = Directory(outputDirectory)..createSync(recursive: true); + if (multiFile) { + for (final fc in fileContents) { + File('${directory.path}/${fc.name}.sv') + .writeAsStringSync(header + fc.contents); + } + } else { + File('${directory.path}/$outputBaseName.sv').writeAsStringSync(output); + } + } + + /// Writes the single-file output to an exact legacy [outputPath]. + /// + /// This preserves the legacy convenience API's arbitrary filename behavior + /// without changing the service artifact naming convention. + void writeLegacyOutputPath(String outputPath) { + if (multiFile) { + throw StateError( + 'writeLegacyOutputPath is only valid for single-file output.', + ); + } + File(outputPath) + ..parent.createSync(recursive: true) + ..writeAsStringSync(output); + } + + /// 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 6990cbe2a..0db21104f 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,6 +1,7 @@ // Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'system_verilog_service.dart'; export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; export 'systemverilog_synthesizer_configuration.dart'; diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart new file mode 100644 index 000000000..168ec0e01 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_array_concat.dart +// Shared array concatenation helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [Swizzle] used by synthesis backends to explicitly assemble a +/// [LogicArray] from its elements. +@internal +class SynthArrayConcat extends Swizzle { + /// The canonical base name for synthesized array concat operations. + static const String operationName = 'array_concat'; + + final LogicArray _destination; + + /// Creates a synthesis array concatenation from [signals]. + SynthArrayConcat(super.signals, {required LogicArray destination}) + : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart new file mode 100644 index 000000000..ec4446826 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_array_slice.dart +// Shared array slice helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [BusSubset] used by synthesis backends to explicitly extract a +/// [LogicArray] element from its packed parent representation. +@internal +class SynthArraySlice extends BusSubset { + /// The canonical base name for synthesized array slice operations. + static const String operationName = 'array_slice'; + + final Logic _destination; + + /// Creates a synthesis array slice over the selected indices of [bus]. + SynthArraySlice( + super.bus, + super.startIndex, + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_assignment.dart b/lib/src/synthesizers/utilities/synth_assignment.dart index aaafda601..0fcb132b6 100644 --- a/lib/src/synthesizers/utilities/synth_assignment.dart +++ b/lib/src/synthesizers/utilities/synth_assignment.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_assignment.dart diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index d29cc84f3..d5fe10223 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -220,6 +220,21 @@ class SynthLogic { return _name!; } + /// The chosen name of this, or `null` if a name has not been picked or this + /// has been replaced. + String? get nameOrNull { + if (_name == null || _replacement != null) { + return null; + } + + assert( + isConstant || Sanitizer.isSanitary(_name!), + 'Signal names should be sanitary, but found $_name.', + ); + + return _name; + } + /// The name of this, if it has been picked. String? _name; diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart new file mode 100644 index 000000000..9447f2764 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -0,0 +1,65 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_module_stop_policy.dart +// Shared module hierarchy stopping policy for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Determines whether a synthesizer should stop hierarchy traversal at a +/// [Module] and treat it as a leaf in its parent. +typedef SynthModuleLeafPredicate = bool Function(Module module); + +/// Determines whether a [Module] would normally receive its own synthesized +/// definition before leaf predicates are applied. +typedef SynthModuleDefinitionPredicate = bool Function(Module module); + +/// Shared hierarchy stopping policy for synthesis backends. +/// +/// A synthesizer configures this with backend-specific leaf predicates and a +/// default definition rule, then queries [isLeaf] or [generatesDefinition] +/// while walking a module hierarchy. +class SynthModuleStopPolicy { + final List _leafPredicates; + final SynthModuleDefinitionPredicate _generatesDefinitionByDefault; + + /// Creates a module stopping policy. + SynthModuleStopPolicy({ + SynthModuleDefinitionPredicate? generatesDefinitionByDefault, + Iterable leafPredicates = const [], + }) : _generatesDefinitionByDefault = + generatesDefinitionByDefault ?? ((_) => true), + _leafPredicates = List.unmodifiable(leafPredicates); + + /// Creates the default SystemVerilog stopping policy. + factory SynthModuleStopPolicy.systemVerilog() => SynthModuleStopPolicy( + leafPredicates: [ + (module) => + module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none, + ], + ); + + /// Creates the default netlist stopping policy. + factory SynthModuleStopPolicy.netlist({ + SynthModuleLeafPredicate leafModulePredicate = _isFlipFlop, + }) => + SynthModuleStopPolicy( + generatesDefinitionByDefault: (module) => module.subModules.isNotEmpty, + leafPredicates: [leafModulePredicate], + ); + + /// Returns `true` when [module] should be treated as a leaf cell in its + /// parent instead of receiving its own generated definition. + bool isLeaf(Module module) => + !_generatesDefinitionByDefault(module) || + _leafPredicates.any((predicate) => predicate(module)); + + /// Returns `true` when [module] should receive its own generated definition. + bool generatesDefinition(Module module) => !isLeaf(module); +} + +bool _isFlipFlop(Module module) => module is FlipFlop; diff --git a/lib/src/synthesizers/utilities/synth_structure_concat.dart b/lib/src/synthesizers/utilities/synth_structure_concat.dart new file mode 100644 index 000000000..dfc23408a --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_concat.dart @@ -0,0 +1,35 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_concat.dart +// Shared structure concatenation helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [Swizzle] used by synthesis backends to explicitly assemble a +/// [LogicStructure] from its leaf elements. +@internal +class SynthStructureConcat extends Swizzle { + /// The canonical base name for synthesized structure concat operations. + static const String operationName = 'struct_concat'; + + final LogicStructure _destination; + + /// Creates a synthesis structure concatenation from [signals]. + SynthStructureConcat(super.signals, {required LogicStructure destination}) + : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart new file mode 100644 index 000000000..db972c929 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -0,0 +1,125 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_layout.dart +// Shared packed LogicStructure layout utility for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// An exclusive-end bit range within a packed [LogicStructure]. +typedef SynthStructureBitRange = ({int start, int end}); + +typedef _SynthStructureRange = ({ + int start, + int end, + String name, + String path, + String fieldPath, + int indexInParent, +}); + +/// Provides bit ranges and field names for a packed [LogicStructure]. +class SynthStructureLayout { + final List<_SynthStructureRange> _ranges = []; + + /// Creates a layout with elements ordered from least to most significant. + SynthStructureLayout(LogicStructure structure) { + _addStructure(structure, 0, '', ''); + } + + void _addStructure( + LogicStructure structure, + int baseOffset, + String parentPath, + String parentFieldPath, + ) { + var offset = baseOffset; + for (var index = 0; index < structure.elements.length; index++) { + final element = structure.elements[index]; + final end = offset + element.width; + final path = + parentPath.isEmpty ? element.name : '${parentPath}_${element.name}'; + final fieldPath = parentFieldPath.isEmpty + ? element.name + : '$parentFieldPath.${element.name}'; + _ranges.add(( + start: offset, + end: end, + name: element.name, + path: path, + fieldPath: fieldPath, + indexInParent: index, + )); + if (element is LogicStructure && element is! LogicArray) { + _addStructure(element, offset, path, fieldPath); + } + offset = end; + } + } + + /// Returns the exclusive-end bit range for a dot-separated [fieldPath]. + /// + /// For example, `a.b` returns the range for the nested `b` field in `a`. + /// Returns `null` when [fieldPath] does not identify a field. + SynthStructureBitRange? bitRangeForPath(String fieldPath) { + for (final range in _ranges) { + if (range.fieldPath == fieldPath) { + return (start: range.start, end: range.end); + } + } + return null; + } + + /// Returns the best field name containing [bitOffset]. + /// + /// When [anonymousUnpreferred] is true, an unpreferred leaf with no named + /// ancestor is represented by its index rather than its raw name. + String fieldNameAt( + int bitOffset, { + required String fallbackName, + bool anonymousUnpreferred = false, + }) { + _SynthStructureRange? bestNamed; + _SynthStructureRange? narrowest; + + for (final range in _ranges) { + if (bitOffset < range.start || bitOffset >= range.end) { + continue; + } + final span = range.end - range.start; + if (narrowest == null || span < narrowest.end - narrowest.start) { + narrowest = range; + } + if (!Naming.isUnpreferred(range.name) && + (bestNamed == null || span < bestNamed.end - bestNamed.start)) { + bestNamed = range; + } + } + + if (bestNamed != null) { + if (narrowest != null && + narrowest.end - narrowest.start < bestNamed.end - bestNamed.start) { + final prefix = bestNamed.path; + if (narrowest.path.length > prefix.length && + narrowest.path.startsWith(prefix)) { + final suffix = narrowest.path.substring(prefix.length + 1); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + + if (anonymousUnpreferred && + narrowest != null && + Naming.isUnpreferred(narrowest.name)) { + return 'anonymous_${narrowest.indexInParent}'; + } + return narrowest?.name ?? fallbackName; + } +} diff --git a/lib/src/synthesizers/utilities/synth_structure_slice.dart b/lib/src/synthesizers/utilities/synth_structure_slice.dart new file mode 100644 index 000000000..d3c7dfc18 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_structure_slice.dart @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_slice.dart +// Shared structure slice helper for synthesis backends. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// A [BusSubset] used by synthesis backends to explicitly extract a +/// [LogicStructure] leaf from its packed parent representation. +@internal +class SynthStructureSlice extends BusSubset { + /// The canonical base name for synthesized structure slice operations. + static const String operationName = 'struct_slice'; + + final Logic _destination; + + /// Creates a synthesis structure slice over the selected indices of [bus]. + SynthStructureSlice( + super.bus, + super.startIndex, + super.endIndex, { + required Logic destination, + }) : _destination = destination, + super(name: operationName); + + @override + bool get hasBuilt => true; + + @override + Object get instanceNameKey => ( + operationName: operationName, + destination: _destination, + ); +} diff --git a/lib/src/synthesizers/utilities/utilities.dart b/lib/src/synthesizers/utilities/utilities.dart index c3cccdf32..1a02d1952 100644 --- a/lib/src/synthesizers/utilities/utilities.dart +++ b/lib/src/synthesizers/utilities/utilities.dart @@ -1,7 +1,13 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'synth_array_concat.dart'; +export 'synth_array_slice.dart'; export 'synth_assignment.dart'; export 'synth_logic.dart'; export 'synth_module_definition.dart'; +export 'synth_module_stop_policy.dart'; +export 'synth_structure_concat.dart'; +export 'synth_structure_layout.dart'; +export 'synth_structure_slice.dart'; export 'synth_sub_module_instantiation.dart'; diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index 40c56abc8..63c522e7b 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -92,8 +92,10 @@ class Vector { } return arrAssigns.toString(); } else { - final signalVal = - LogicValue.of(inputValues[signalName], width: signal.width); + final signalVal = LogicValue.of( + inputValues[signalName], + width: signal.width, + ); return '$signalName = $signalVal;'; } }).join('\n'); @@ -114,8 +116,10 @@ class Vector { var index = 0; for (final leaf in outputPort.leafElements) { final subVal = expectedValue.getRange(index, index + leaf.width); - checksList.add(_errorCheckString( - leaf.structureName, subVal, subVal, inputStimulus)); + checksList.add( + _errorCheckString( + leaf.structureName, subVal, subVal, inputStimulus), + ); index += leaf.width; } } else { @@ -169,51 +173,56 @@ abstract class SimCompare { } if (enableChecking) { - unawaited(Simulator.postTick.first.then((value) { - for (final signalName in vector.expectedOutputValues.keys) { - final value = vector.expectedOutputValues[signalName]; - final o = - module.tryOutput(signalName) ?? module.inOut(signalName); - - final errorReason = - 'For vector #${vectors.indexOf(vector)} $vector,' - ' expected $o to be $value, but it was ${o.value}.'; - if (value is int) { - expect(o.value.isValid, isTrue, reason: errorReason); - expect(o.value.toBigInt(), - equals(BigInt.from(value).toUnsigned(o.width)), - reason: errorReason); - } else if (value is BigInt) { - expect(o.value.isValid, isTrue, reason: errorReason); - expect(o.value.toBigInt(), equals(value), reason: errorReason); - } else if (value is LogicValue) { - if (o.width > 1 && - (value == LogicValue.x || value == LogicValue.z)) { - for (final oBit in o.value.toList()) { - expect(oBit, equals(value), reason: errorReason); + unawaited( + Simulator.postTick.first.then((value) { + for (final signalName in vector.expectedOutputValues.keys) { + final value = vector.expectedOutputValues[signalName]; + final o = + module.tryOutput(signalName) ?? module.inOut(signalName); + + final errorReason = + 'For vector #${vectors.indexOf(vector)} $vector,' + ' expected $o to be $value, but it was ${o.value}.'; + if (value is int) { + expect(o.value.isValid, isTrue, reason: errorReason); + expect(o.value.toBigInt(), + equals(BigInt.from(value).toUnsigned(o.width)), + reason: errorReason); + } else if (value is BigInt) { + expect(o.value.isValid, isTrue, reason: errorReason); + expect(o.value.toBigInt(), equals(value), + reason: errorReason); + } else if (value is LogicValue) { + if (o.width > 1 && + (value == LogicValue.x || value == LogicValue.z)) { + for (final oBit in o.value.toList()) { + expect(oBit, equals(value), reason: errorReason); + } + } else { + expect(o.value, equals(value), reason: errorReason); } + } else if (value is String) { + expect(o.value, LogicValue.of(value, width: o.width), + reason: errorReason); } else { - expect(o.value, equals(value), reason: errorReason); + throw NonSupportedTypeException(value); } - } else if (value is String) { - expect(o.value, LogicValue.of(value, width: o.width), - reason: errorReason); - } else { - 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; } - Simulator.registerAction(timestamp + Vector._period, - () {}); // just so it does one more thing at the end + Simulator.registerAction( + timestamp + Vector._period, + () {}, + ); // just so it does one more thing at the end Simulator.setMaxSimTime(timestamp + 2 * Vector._period); await Simulator.run(); } @@ -348,9 +357,9 @@ abstract class SimCompare { allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); final moduleInstance = '$topModule dut($moduleConnections);'; final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = module.generateSynth( - configuration: synthesizerConfiguration, - ); + final generatedVerilog = + SystemVerilogService(module, configuration: synthesizerConfiguration) + .output; // so that when they run in parallel, they dont step on each other final uniqueId = @@ -404,13 +413,10 @@ abstract class SimCompare { print(maskedOutput); } - return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), - caseSensitive: false)); + return output.toString().contains( + RegExp(['error', 'unable', if (!allowWarnings) 'warning'].join('|'), + caseSensitive: false), + ); } if (printIfContentsAndCheckError(compileResult.stdout)) { @@ -432,14 +438,22 @@ abstract class SimCompare { if (!dontDeleteTmpFiles) { try { - File(tmpOutput).deleteSync(); - File(tmpTestFile).deleteSync(); + final outFile = File(tmpOutput); + if (outFile.existsSync()) { + outFile.deleteSync(); + } + final testFile = File(tmpTestFile); + if (testFile.existsSync()) { + testFile.deleteSync(); + } if (dumpWaves) { - File(tmpVcdFile).deleteSync(); + final vcdFile = File(tmpVcdFile); + if (vcdFile.existsSync()) { + vcdFile.deleteSync(); + } } } on Exception catch (e) { print("Couldn't delete: $e"); - return false; } } return true; diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 3a37e55ea..cde2427c7 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -1,233 +1,72 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // wave_dumper.dart -// Waveform dumper for a given module hierarchy, dumps to ".vcd" file. +// Deprecated waveform dumper; use Module.dumpWaves instead. // // 2021 May 7 // Author: Max Korbel -import 'dart:collection'; -import 'dart:io'; 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'; -/// A waveform dumper for simulations. +/// Deprecated: use [Module.dumpWaves] instead. /// -/// Outputs to vcd format at [outputPath]. [module] must be built prior to -/// attaching the [WaveDumper]. +/// [WaveDumper] is a simple wrapper around [WaveformService] for backward +/// compatibility. It provides the legacy API for recording all signal changes +/// in a simulation to a VCD file. /// -/// The waves will only dump to the file periodically and then once the -/// simulation has completed. +/// **Migration guide:** +/// +/// Replace: +/// ```dart +/// var dumper = WaveDumper(module, outputPath: 'output.vcd'); +/// ``` +/// +/// With: +/// ```dart +/// var service = module.dumpWaves(outputPath: 'output.vcd'); +/// ``` +/// +/// For more control over filtering, timescale, and recording windows, create a +/// [WaveformService] directly: +/// ```dart +/// final service = WaveformService( +/// module, +/// outputDirectory: 'build', +/// outputBaseName: 'debug', +/// writeToFile: true, +/// timescale: '1ns', +/// startTime: 100, +/// signalFilter: (signal) => signal.name.startsWith('debug_'), +/// ); +/// ``` +@Deprecated('Use Module.dumpWaves() for simple VCD output, or ' + 'WaveformService for advanced waveform configuration.') class WaveDumper { + /// The underlying [WaveformService]. + final WaveformService _service; + /// The [Module] being dumped. - final Module module; + Module get module => _service.module; /// The output filepath of the generated waveforms. - final String outputPath; - - /// The file to write dumped output waveform to. - final File _outputFile; - - /// A sink to write contents into [_outputFile]. - late final IOSink _outFileSink; - - /// A buffer for contents before writing to the file sink. - final StringBuffer _fileBuffer = StringBuffer(); - - /// A counter for tracking signal names in the VCD file. - int _signalMarkerIdx = 0; - - /// Stores the mapping from [Logic] to signal marker in the VCD file. - final Map _signalToMarkerMap = {}; - - /// A set of all [Logic]s that have changed in this timestamp so far. - /// - /// This spans across multiple inject or changed events if they are in the - /// same timestamp of the [Simulator]. - final Set _changedLogicsThisTimestamp = HashSet(); - - /// The timestamp which is currently being collected for a dump. - /// - /// When the [Simulator] time progresses beyond this, it will dump all the - /// signals that have changed up until that point at this saved time value. - int _currentDumpingTimestamp = Simulator.time; + String get outputPath => _service.outputPath; /// Attaches a [WaveDumper] to record all signal changes in a simulation of /// [module] in a VCD file at [outputPath]. - WaveDumper(this.module, {this.outputPath = 'waves.vcd'}) - : _outputFile = File(outputPath)..createSync(recursive: true) { - if (!module.hasBuilt) { - throw Exception( - 'Module must be built before passed to dumper. Call build() first.'); - } - - _outFileSink = _outputFile.openWrite(); - - _collectAllSignals(); - - _writeHeader(); - _writeScope(); - - Simulator.preTick.listen((args) { - if (Simulator.time != _currentDumpingTimestamp) { - if (_changedLogicsThisTimestamp.isNotEmpty) { - // no need to write blank timestamps - _captureTimestamp(_currentDumpingTimestamp); - } - _currentDumpingTimestamp = Simulator.time; - } - }); - - Simulator.registerEndOfSimulationAction(() async { - _captureTimestamp(Simulator.time); - - await _terminate(); - }); - } - - /// Number of characters in the buffer after which it will - /// write contents to the output file. - static const _fileBufferLimit = 100000; - - /// Buffers [contents] to be written to the output file. - void _writeToBuffer(String contents) { - _fileBuffer.write(contents); - - if (_fileBuffer.length > _fileBufferLimit) { - _writeToFile(); - } - } - - /// Writes all pending items in the [_fileBuffer] to the file. - void _writeToFile() { - _outFileSink.write(_fileBuffer.toString()); - _fileBuffer.clear(); - } - - /// Terminates the waveform dumping, including closing the file. - Future _terminate() async { - _writeToFile(); - await _outFileSink.flush(); - await _outFileSink.close(); - } - - /// Registers all signal value changes to write updates to the dumped VCD. - void _collectAllSignals() { - 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) { - // constant values are "boring" to inspect - continue; - } - - _signalToMarkerMap[sig] = 's${_signalMarkerIdx++}'; - sig.changed.listen((args) { - _changedLogicsThisTimestamp.add(sig); - }); - } - for (final subm in m.subModules) { - if (subm is InlineSystemVerilog) { - // the InlineSystemVerilog modules are "boring" to inspect - continue; - } - modulesToParse.add(subm); - } - } - } - - /// Writes the top header for the VCD file. - void _writeHeader() { - final dateString = Timestamper.stamp(); - const timescale = '1ps'; - final header = ''' -\$date - $dateString -\$end -\$version - ROHD v${Config.version} -\$end -\$comment - Generated by ROHD - www.github.com/intel/rohd -\$end -\$timescale $timescale \$end -'''; - _writeToBuffer(header); - } - - /// Writes the scope of the VCD, including signal and hierarchy declarations, - /// as well as initial values. - void _writeScope() { - var scopeString = _computeScopeString(module); - scopeString += '\$enddefinitions \$end\n'; - scopeString += '\$dumpvars\n'; - _writeToBuffer(scopeString); - _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); - _writeToBuffer('\$end\n'); - } - - /// Generates the top of the scope string (signal and hierarchy definitions). - 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) { - // no need to dump empty scopes - return ''; - } - scopeString += innerScopeString.toString(); - scopeString += '$padding\$upscope \$end\n'; - return scopeString; - } - - /// Writes the current timestamp to the VCD. - void _captureTimestamp(int timestamp) { - final timestampString = '#$timestamp\n'; - _writeToBuffer(timestampString); - - _changedLogicsThisTimestamp - ..forEach(_writeSignalValueUpdate) - ..clear(); - } - - /// Writes the current value of [signal] to the VCD. - 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]; - final updateString = '$updateValue$marker\n'; - _writeToBuffer(updateString); - } + /// + /// [module] must be built prior to construction. + /// + /// **Deprecated:** Use [Module.dumpWaves] for simple VCD output, or + /// [WaveformService] for signal filtering, custom timescale, recording + /// windows, and extensibility hooks for streaming applications. + @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' + 'WaveformService for advanced waveform configuration.') + WaveDumper(Module module, {String outputPath = 'waves.vcd'}) + : _service = module.dumpWaves(outputPath: outputPath); } -/// Deprecated: use [WaveDumper] instead. -@Deprecated('Use WaveDumper instead') +/// Deprecated: use [Module.dumpWaves] instead. +@Deprecated('Use Module.dumpWaves() for simple VCD output, or ' + 'WaveformService for advanced waveform configuration.') typedef Dumper = WaveDumper; diff --git a/packages/rohd_hierarchy/analysis_options.yaml b/packages/rohd_hierarchy/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_hierarchy/analysis_options.yaml +++ b/packages/rohd_hierarchy/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart index 2f7cb3f76..dc79dbb6c 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -12,5 +12,6 @@ export 'hierarchy_occurrence.dart'; export 'hierarchy_search_result.dart'; export 'occurrence_address.dart'; export 'occurrence_search_result.dart'; +export 'occurrence_trie.dart'; export 'signal_occurrence.dart'; export 'signal_search_result.dart'; diff --git a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart new file mode 100644 index 000000000..31ea2d647 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -0,0 +1,108 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie.dart +// Compact storage for values keyed by hierarchy occurrence addresses. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/occurrence_address.dart'; + +/// A prefix-sharing map from [OccurrenceAddress] values to values of type [T]. +/// +/// Common address prefixes are stored once, making this more compact than a +/// conventional map when many values belong to the same hierarchy subtree. +class OccurrenceTrie { + final _OccurrenceTrieNode _root = _OccurrenceTrieNode(); + + /// Whether this trie contains no values. + bool get isEmpty => _root.isEmpty; + + /// The value stored at [address], if any. + T? operator [](OccurrenceAddress address) { + var node = _root; + for (final index in _validatedPath(address)) { + final child = node.children[index]; + if (child == null) { + return null; + } + node = child; + } + return node.value; + } + + /// Associates [value] with [address]. + /// + /// Returns the value previously stored at [address], if any. + T? set(OccurrenceAddress address, T value) { + var node = _root; + for (final index in _validatedPath(address)) { + node = node.children.putIfAbsent(index, _OccurrenceTrieNode.new); + } + final previous = node.value; + node.value = value; + return previous; + } + + /// Removes and returns the value stored at [address], if any. + T? remove(OccurrenceAddress address) { + final path = _validatedPath(address); + final nodes = <_OccurrenceTrieNode>[_root]; + var node = _root; + for (final index in path) { + final child = node.children[index]; + if (child == null) { + return null; + } + nodes.add(child); + node = child; + } + + final previous = node.value; + if (previous == null) { + return null; + } + node.value = null; + for (var index = path.length - 1; index >= 0; index--) { + final child = nodes[index + 1]; + if (!child.isEmpty) { + break; + } + nodes[index].children.remove(path[index]); + } + return previous; + } + + /// Removes every value from this trie. + void clear() { + _root + ..value = null + ..children.clear(); + } + + static List _validatedPath(OccurrenceAddress address) { + if (address.path.isEmpty) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must not be empty.', + ); + } + if (address.path.any((index) => index < 0)) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must contain non-negative indices.', + ); + } + return address.path; + } +} + +class _OccurrenceTrieNode { + final Map> children = {}; + T? value; + + bool get isEmpty => value == null && children.isEmpty; +} diff --git a/packages/rohd_hierarchy/pubspec.yaml b/packages/rohd_hierarchy/pubspec.yaml index c75cd3d34..8a3351519 100644 --- a/packages/rohd_hierarchy/pubspec.yaml +++ b/packages/rohd_hierarchy/pubspec.yaml @@ -5,8 +5,6 @@ repository: https://github.com/intel/rohd version: 0.1.0 issue_tracker: https://github.com/intel/rohd/issues -publish_to: none - environment: sdk: '>=3.0.0 <4.0.0' diff --git a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart index 6a8fe173a..b7778fd54 100644 --- a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart +++ b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart @@ -8,6 +8,9 @@ // 2026 April // Author: Desmond Kirkpatrick +@TestOn('vm') +library; + import 'dart:io'; import 'package:rohd_hierarchy/rohd_hierarchy.dart'; @@ -172,19 +175,6 @@ void main() { expect(outputs.map((s) => s.name), contains('done')); }); - test(r'isPrimitiveType is true for $-prefixed types', () { - expect(HierarchyOccurrence.isPrimitiveType(r'$mux'), isTrue); - expect(HierarchyOccurrence.isPrimitiveType(r'$and'), isTrue); - }); - - test(r'isPrimitiveType is false for non-$-prefixed types', () { - expect(HierarchyOccurrence.isPrimitiveType('FilterBank'), isFalse); - }); - - test('isPrimitiveType is false for empty string', () { - expect(HierarchyOccurrence.isPrimitiveType(''), isFalse); - }); - test('isPrimitiveCell reflects isPrimitive field and type', () { // A node marked isPrimitive=true final primCell = service.root.children.firstWhere((c) => c.isPrimitive); @@ -293,56 +283,6 @@ void main() { // At least ch0_1 and ch1_1 have children expect(withSlash, isNotEmpty); }); - - test('hasRegexChars is false for plain text', () { - expect(HierarchyService.hasRegexChars('clk'), isFalse); - }); - - test('hasRegexChars detects * glob', () { - expect(HierarchyService.hasRegexChars('c*'), isTrue); - }); - - test('hasRegexChars detects ? glob', () { - expect(HierarchyService.hasRegexChars('cl?'), isTrue); - }); - - test('hasRegexChars detects character class', () { - expect(HierarchyService.hasRegexChars('[a-z]'), isTrue); - }); - - test('hasRegexChars detects group alternation', () { - expect(HierarchyService.hasRegexChars('(a|b)'), isTrue); - }); - - test('hasRegexChars detects + quantifier', () { - expect(HierarchyService.hasRegexChars('a+'), isTrue); - }); - - test('longestCommonPrefix finds shared prefix', () { - expect( - HierarchyService.longestCommonPrefix([ - 'FilterBank/ch0', - 'FilterBank/ch1', - ]), - 'FilterBank/ch', - ); - }); - - test('longestCommonPrefix returns null for empty list', () { - expect(HierarchyService.longestCommonPrefix([]), isNull); - }); - - test('longestCommonPrefix returns null for no common prefix', () { - expect(HierarchyService.longestCommonPrefix(['abc', 'xyz']), isNull); - }); - - test('longestCommonPrefix is case-sensitive', () { - final prefix = HierarchyService.longestCommonPrefix([ - 'Filter/abc', - 'Filter/abd', - ]); - expect(prefix, 'Filter/ab'); - }); }); // ─────────────── HierarchySearchController ─────────────── @@ -456,18 +396,6 @@ void main() { }); }); - // ─────────────── BaseHierarchyAdapter edge case ─────────────── - // The real uninitialized-root StateError test lives in - // coverage_gaps_test.dart. Here we just verify fromTree works. - - group('BaseHierarchyAdapter — fromTree produces usable root', () { - test('fromTree immediately sets root', () { - final tree = HierarchyOccurrence(name: 'r'); - final svc = BaseHierarchyAdapter.fromTree(tree); - expect(svc.root.name, 'r'); - }); - }); - // ─────────────── Multiple instantiation (dedup) ─────────────── group('Multiple instantiation — FilterChannel dedup', () { diff --git a/packages/rohd_hierarchy/test/hierarchy_model_test.dart b/packages/rohd_hierarchy/test/hierarchy_model_test.dart new file mode 100644 index 000000000..9ef3485c6 --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_model_test.dart @@ -0,0 +1,71 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_model_test.dart +// Cross-platform hierarchy model and utility tests. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('HierarchyOccurrence primitive detection', () { + test(r'isPrimitiveType is true for $-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType(r'$mux'), isTrue); + expect(HierarchyOccurrence.isPrimitiveType(r'$and'), isTrue); + }); + + test(r'isPrimitiveType is false for non-$-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType('FilterBank'), isFalse); + }); + + test('isPrimitiveType is false for empty string', () { + expect(HierarchyOccurrence.isPrimitiveType(''), isFalse); + }); + }); + + group('HierarchyService search utilities', () { + test('hasRegexChars is false for plain text', () { + expect(HierarchyService.hasRegexChars('clk'), isFalse); + }); + + test('hasRegexChars detects glob and regex syntax', () { + for (final query in ['c*', 'cl?', '[a-z]', '(a|b)', 'a+']) { + expect(HierarchyService.hasRegexChars(query), isTrue); + } + }); + + test('longestCommonPrefix finds shared prefix', () { + expect( + HierarchyService.longestCommonPrefix([ + 'FilterBank/ch0', + 'FilterBank/ch1', + ]), + 'FilterBank/ch', + ); + }); + + test('longestCommonPrefix returns null without a shared prefix', () { + expect(HierarchyService.longestCommonPrefix([]), isNull); + expect(HierarchyService.longestCommonPrefix(['abc', 'xyz']), isNull); + }); + + test('longestCommonPrefix is case-sensitive', () { + expect( + HierarchyService.longestCommonPrefix(['Filter/abc', 'Filter/abd']), + 'Filter/ab', + ); + }); + }); + + group('BaseHierarchyAdapter', () { + test('fromTree immediately sets root', () { + final service = + BaseHierarchyAdapter.fromTree(HierarchyOccurrence(name: 'r')); + + expect(service.root.name, 'r'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/occurrence_trie_test.dart b/packages/rohd_hierarchy/test/occurrence_trie_test.dart new file mode 100644 index 000000000..ef9e7d4bc --- /dev/null +++ b/packages/rohd_hierarchy/test/occurrence_trie_test.dart @@ -0,0 +1,54 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie_test.dart +// Tests for compact occurrence-address trie storage. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + test('stores values with shared occurrence-address prefixes', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + + expect(trie.set(first, 'first'), isNull); + expect(trie.set(second, 'second'), isNull); + + expect(trie[first], 'first'); + expect(trie[second], 'second'); + expect(trie[const OccurrenceAddress([0, 2, 6])], isNull); + }); + + test('prunes an address branch after removing its final value', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + trie + ..set(first, 'first') + ..set(second, 'second'); + + expect(trie.remove(first), 'first'); + expect(trie[first], isNull); + expect(trie[second], 'second'); + expect(trie.remove(second), 'second'); + expect(trie.isEmpty, isTrue); + }); + + test('rejects an address that cannot identify a signal', () { + final trie = OccurrenceTrie(); + + expect( + () => trie.set(OccurrenceAddress.root, 'root'), + throwsArgumentError, + ); + expect( + () => trie[const OccurrenceAddress([0, -1])], + throwsArgumentError, + ); + }); +} diff --git a/packages/rohd_waveform/analysis_options.yaml b/packages/rohd_waveform/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_waveform/analysis_options.yaml +++ b/packages/rohd_waveform/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/packages/rohd_waveform/lib/src/waveform_api.dart b/packages/rohd_waveform/lib/src/waveform_api.dart index 8daa6ffa7..db8ad65cb 100644 --- a/packages/rohd_waveform/lib/src/waveform_api.dart +++ b/packages/rohd_waveform/lib/src/waveform_api.dart @@ -33,14 +33,13 @@ abstract class SignalWaveformApi { required List signalIds, int? startTime, int? endTime, - }) async { - // Base implementation: must be overridden by concrete implementations - // Port no longer contains data - implementations must fetch from - // their source. - throw UnimplementedError( - 'getWaveformData must be implemented by subclasses', - ); - } + }) => + Future.error( + // Base implementation: must be overridden by concrete implementations + // Port no longer contains data - implementations must fetch from + // their source. + UnimplementedError('getWaveformData must be implemented by subclasses'), + ); /// Streams waveform data incrementally for specific signals. /// @@ -71,12 +70,11 @@ abstract class SignalWaveformApi { /// /// Returns a [Future] that completes with the current time as an integer, /// or null if the time cannot be determined. - Future getCurrentTime() async { - // Default implementation: must be overridden by concrete implementations - throw UnimplementedError( - 'getCurrentTime must be implemented by subclasses', - ); - } + Future getCurrentTime() => + // Default implementation: must be overridden by concrete implementations + Future.error( + UnimplementedError('getCurrentTime must be implemented by subclasses'), + ); /// Retrieves a snapshot of all signal values at the given [time]. /// @@ -87,9 +85,10 @@ abstract class SignalWaveformApi { /// - `direction`: signal direction (if port) /// /// Returns null if the snapshot could not be retrieved. - Future>?> getSnapshot(int time) async { - throw UnimplementedError('getSnapshot must be implemented by subclasses'); - } + Future>?> getSnapshot(int time) => + Future.error( + UnimplementedError('getSnapshot must be implemented by subclasses'), + ); /// Proactively expand all slim module definitions so the client-side /// evaluator can compute internal signals immediately. @@ -97,5 +96,5 @@ abstract class SignalWaveformApi { /// Called when the user enables "internal signals" in the wave viewer. /// Default implementation is a no-op; overridden by implementations /// that support client-side synthesis. - Future expandAllSlimModules() async {} + Future expandAllSlimModules() => Future.value(); } diff --git a/packages/rohd_waveform/lib/src/waveform_repository.dart b/packages/rohd_waveform/lib/src/waveform_repository.dart index ba24cf12d..610cd5f67 100644 --- a/packages/rohd_waveform/lib/src/waveform_repository.dart +++ b/packages/rohd_waveform/lib/src/waveform_repository.dart @@ -152,10 +152,9 @@ class SignalWaveformRepository { } /// Get the current simulation time from the waveform API. - Future getCurrentTime() async { - await _ensureReady(); - return _signalWaveformApi.getCurrentTime(); - } + Future getCurrentTime() => _ensureReady().then( + (_) => _signalWaveformApi.getCurrentTime(), + ); /// Retrieves waveform data for specific signals. /// diff --git a/pubspec.yaml b/pubspec.yaml index 42949a933..f281fd024 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ issue_tracker: https://github.com/intel/rohd/issues documentation: https://intel.github.io/rohd-website/docs/sample-example/ environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ^3.6.0 dependencies: collection: ^1.15.0 diff --git a/rohd-multipackage.code-workspace b/rohd-multipackage.code-workspace deleted file mode 100644 index 3c0ed09fc..000000000 --- a/rohd-multipackage.code-workspace +++ /dev/null @@ -1,30 +0,0 @@ -// Opens each nested Dart package as a workspace root so the Dart analyzer uses -// its own package context and reports fewer cross-package Problems. Open this -// file in VS Code with File > Open Workspace from File... . -{ - "folders": [ - { - "name": "rohd", - "path": "." - }, - { - "name": "rohd_hierarchy", - "path": "packages/rohd_hierarchy" - }, - { - "name": "rohd_waveform", - "path": "packages/rohd_waveform" - }, - { - "name": "rohd_devtools_extension", - "path": "rohd_devtools_extension" - }, - { - "name": "rohd_devtools_widgets", - "path": "rohd_devtools_extension/packages/rohd_devtools_widgets" - } - ], - "settings": { - "dart.projectSearchDepth": 8 - } -} diff --git a/rohd_devtools_extension/analysis_options.yaml b/rohd_devtools_extension/analysis_options.yaml index f82d6cc51..1bfb3d100 100644 --- a/rohd_devtools_extension/analysis_options.yaml +++ b/rohd_devtools_extension/analysis_options.yaml @@ -2,6 +2,14 @@ # https://rydmike.com/blog_flutter_linting.html analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** language: strict-casts: true strict-inference: true diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart index 7701234b6..0165d152c 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart @@ -17,12 +17,8 @@ import 'package:vm_service/vm_service.dart'; /// Service helpers for evaluating and filtering the ROHD module tree. class TreeService { - /// Primary expression for hierarchy JSON — available in all ROHD versions - /// that ship inspector_service.dart (i.e. main and later). - static const _primaryInvokeFunc = 'ModuleTree.instance.hierarchyJSON'; - - /// Fallback kept for any pre-inspector ROHD target. - static const _legacyInvokeFunc = 'ModuleTree.instance.hierarchyJSON'; + /// Expression for retrieving the module hierarchy JSON. + static const _hierarchyExpression = 'ModuleTree.instance.hierarchyJson'; /// Eval wrapper for accessing ROHD code in the target isolate. final EvalOnDartLibrary rohdControllerEval; @@ -68,19 +64,14 @@ class TreeService { } Future _evalTreePayload() async { - final expressions = [_primaryInvokeFunc, _legacyInvokeFunc]; - - for (final expression in expressions) { - try { - final treeInstance = await rohdControllerEval.evalInstance(expression, - isAlive: evalDisposable); - return treeInstance.valueAsString; - } on Exception catch (e) { - debugPrint('[TreeService] Eval failed for "$expression": $e'); - } + try { + final treeInstance = await rohdControllerEval + .evalInstance(_hierarchyExpression, isAlive: evalDisposable); + return treeInstance.valueAsString; + } on Exception catch (e) { + debugPrint('[TreeService] Eval failed for "$_hierarchyExpression": $e'); + return null; } - - return null; } /// Returns whether the current module or any descendant matches the search. diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart b/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart index 7c35ef0a8..948d8ca3f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart @@ -19,7 +19,7 @@ import 'package:vm_service/vm_service.dart' as vm; /// VM-backed [SignalValueSource] that refreshes on debugger pause events. class VmServiceSignalValueSource implements SignalValueSource { static const _moduleTreeHierarchyExpression = - 'ModuleTree.instance.hierarchyJSON'; + 'ModuleTree.instance.hierarchyJson'; static const _currentTimeExpressions = [ 'WaveformService.instance.currentTime', @@ -193,10 +193,10 @@ class VmServiceSignalValueSource implements SignalValueSource { } static String _waveformSnapshotExpression(int time) => - 'WaveformService.instance.getSnapshotCompactJSON($time)'; + 'WaveformService.instance.getSnapshotCompactJson($time)'; static const _moduleTreeSignalValuesExpression = - 'ModuleTree.instance.signalValuesJSON'; + 'ModuleTree.instance.signalValuesJson'; Future _readCurrentTimeFromExtension() async { final response = await _callExtension(_currentTimeExtension); diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart index 140da744b..aca86772a 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // signal_table.dart diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md index e40b328bb..fa99b412a 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md @@ -26,6 +26,48 @@ across DevTools packages. - ROHD extension client/status abstractions: `RohdExtensionClient`, `NullExtensionClient`, `RohdModuleInfo`, and `RohdFormatInfo`. +## Widgets & Utilities + +### UI Controls & Buttons + +- **`MarkdownHelpButton`** — A help button that displays Markdown content from an asset file in a dialog. Supports tooltip text and rich formatting. + +- **`ExportPngButton`** — A camera icon button for triggering PNG export functionality. Includes customizable tooltip text. + +- **`CrossProbeButton`** — A toolbar button for toggling cross-probing between viewers. Shows a bidirectional arrows icon that reflects the active/inactive state. + +### Overlays & Layout + +- **`AppBarOverlay`** — An auto-hiding AppBar that slides in from the top edge when the mouse approaches. When disabled, behaves like a standard AppBar. + +### Export & Capture + +- **`CaptureBoundary`** — Utility for capturing a `RepaintBoundary` as PNG, saving/downloading, and showing user feedback via toast notifications. + +- **`ExportToast`** — Toast notification widget for export feedback and status messages. + +### Cross-Probing + +- **`CrossProbeService`** — Service for managing cross-probe state between multiple viewers/debuggers. Handles bidirectional signal selection synchronization. + +- **`CrossProbeMenu`** — Shared context menu integration for cross-probing actions across different ROHD DevTools surfaces. + +### Signal & Bit Field Utilities + +- **`LogicTypeUtils`** — Utilities for working with ROHD logic types and formatting logic values for display. + +- **`BitFieldUtils`** — Utilities for parsing, validating, and formatting bit field ranges and named bit fields. + +- **`BitExpansionMenu`** — Shared popup menu items for "Expand Bits" and "Define Bit Fields" actions used across signal selection overlays and panels. + +- **`SignalValueFormatRegistry`** — Shared registry for signal display-format preferences, allowing consistent formatting across multiple viewers. + +### Extension Integration + +- **`RohdExtensionClient`** — Abstract interface for querying the ROHD VS Code extension. Supports multiple implementations (DevTools, VS Code webview, offline mode). + +- **`RohdExtensionStatus`** — Status information and connection state for the ROHD extension. + ## Usage Add this package as a path dependency from a ROHD DevTools package and import the shared widgets you need: diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart index 452567fae..ca31b9ace 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart @@ -36,6 +36,9 @@ export 'src/bit_field_utils.dart'; // Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers export 'src/bit_expansion_menu.dart'; +// Shared signal display-format preferences and value formatting +export 'src/signal_value_format_registry.dart'; + // ROHD extension client export 'src/rohd_extension_status.dart'; export 'src/rohd_extension_client.dart'; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart new file mode 100644 index 000000000..bf5fdcad7 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -0,0 +1,239 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry.dart +// Shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:rohd/rohd.dart' + show LogicValue, LogicValueConstructionException; +import 'package:rohd_hierarchy/rohd_hierarchy.dart' + show OccurrenceAddress, OccurrenceTrie; + +/// The available display formats for signal values. +enum SignalValueFormat { + /// The source waveform representation. + waveform, + + /// A binary representation. + binary, + + /// A hexadecimal representation. + hexadecimal, + + /// An unsigned decimal representation. + unsignedDecimal, + + /// A two's-complement signed decimal representation. + signedDecimal, + + /// An octal representation. + octal, + + /// An ASCII representation. + ascii, +} + +/// A format preference for one signal occurrence address. +class SignalValueFormatPreference { + /// Creates a preference for [address] using [format]. + SignalValueFormatPreference( + this.address, + this.format, + ); + + /// The occurrence address, including the signal index. + final OccurrenceAddress address; + + /// The selected display format. + final SignalValueFormat format; +} + +/// Shared display-format preferences keyed by occurrence address. +/// +/// Viewer packages publish [SignalValueFormat] values; embedded surfaces use +/// the same values without depending on viewer-local format enums. +class SignalValueFormatRegistry { + SignalValueFormatRegistry._(); + + static final _formatTrie = OccurrenceTrie(); + + /// Notifies listeners whenever occurrence-format preferences change. + static final changes = ValueNotifier(0); + + /// Replaces all occurrence-format preferences with [preferences]. + static void update(Iterable preferences) { + _formatTrie.clear(); + for (final preference in preferences) { + _formatTrie.set(preference.address, preference.format); + } + _notifyListeners(); + } + + /// Removes all occurrence-format preferences. + static void clear() { + if (_formatTrie.isEmpty) { + return; + } + _formatTrie.clear(); + _notifyListeners(); + } + + /// Sets [format] for each signal occurrence in [addresses]. + static void setFormatFor( + Iterable addresses, + SignalValueFormat format, + ) { + var changed = false; + for (final address in addresses) { + changed = (_formatTrie.set(address, format) != format) || changed; + } + if (changed) { + _notifyListeners(); + } + } + + /// Converts a serialized format name to its corresponding enum value. + /// + /// Returns `null` when [value] is not a known format name. + static SignalValueFormat? formatFromString(String value) { + for (final format in SignalValueFormat.values) { + if (format.name == value) { + return format; + } + } + return null; + } + + /// Converts [format] to its serialized format name. + static String formatToString(SignalValueFormat format) => format.name; + + /// Returns the requested format for [address], or the waveform default. + static SignalValueFormat formatFor(OccurrenceAddress address) { + return formatForAny([address]); + } + + /// Returns the first registered format matching [addresses]. + static SignalValueFormat formatForAny( + Iterable addresses, { + SignalValueFormat fallback = SignalValueFormat.waveform, + }) { + for (final address in addresses) { + if (address == null) { + continue; + } + final format = _formatTrie[address]; + if (format != null) { + return format; + } + } + return fallback; + } + + static void _notifyListeners() => changes.value++; + + static bool _containsUnknownDigits(String value) { + final lower = value.toLowerCase(); + final apostrophe = lower.indexOf("'"); + final digits = apostrophe > 0 && apostrophe + 2 <= lower.length + ? lower.substring(apostrophe + 2) + : lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + return digits.contains('x') || digits.contains('z'); + } + + /// Formats a ROHD radix literal according to [format]. + static String formatValue( + String value, + SignalValueFormat format, + int width, + ) { + final waveformValue = _waveformValue(value, width); + if (waveformValue == null) { + return _canonicalWaveformValue(value, width); + } + final (logicValue, canonical) = waveformValue; + if (format == SignalValueFormat.waveform || + _containsUnknownDigits(canonical)) { + return canonical; + } + return switch (format) { + SignalValueFormat.binary => logicValue.toRadixString( + leadingZeros: true, + includeWidth: false, + sepChar: '', + ), + SignalValueFormat.hexadecimal => + logicValue.toRadixString(radix: 16, sepChar: ''), + SignalValueFormat.unsignedDecimal => + logicValue.toRadixString(radix: 10, includeWidth: false, sepChar: ''), + SignalValueFormat.signedDecimal => + logicValue.toBigInt().toSigned(logicValue.width).toString(), + SignalValueFormat.octal => + '0o${logicValue.toRadixString(radix: 8, includeWidth: false, sepChar: '')}', + SignalValueFormat.ascii => String.fromCharCodes( + List.generate( + ((logicValue.width + 7) ~/ 8).clamp(1, 32), + (index) { + final shift = + (((logicValue.width + 7) ~/ 8).clamp(1, 32) - index - 1) * 8; + final code = + ((logicValue.toBigInt() >> shift) & BigInt.from(0xff)) + .toInt(); + return code >= 0x20 && code <= 0x7e ? code : 0x2e; + }, + ), + ), + SignalValueFormat.waveform => canonical, + }; + } + + static (LogicValue, String)? _waveformValue(String value, int width) { + final trimmed = value.trim().replaceAll('\u0000', ''); + if (trimmed.isEmpty || _containsUnknownDigits(trimmed)) { + return null; + } + final lower = trimmed.toLowerCase(); + final displayWidth = width > 0 ? width : 1; + final isRadixLiteral = RegExp(r"^\d+'[bqodh]").hasMatch(lower); + final digits = lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + final radix = lower.startsWith('0x') + ? 'h' + : lower.startsWith('0b') + ? 'b' + : isRadixLiteral + ? lower[lower.indexOf("'") + 1] + : digits.codeUnits.every( + (codeUnit) => codeUnit == 0x30 || codeUnit == 0x31, + ) + ? 'b' + : digits.codeUnits.any( + (codeUnit) => + (codeUnit >= 0x61 && codeUnit <= 0x66) || + (codeUnit >= 0x41 && codeUnit <= 0x46), + ) + ? 'h' + : 'd'; + final radixLiteral = isRadixLiteral ? lower : "$displayWidth'$radix$digits"; + try { + final logicValue = LogicValue.ofRadixString(radixLiteral); + return ( + logicValue, + isRadixLiteral ? trimmed : logicValue.toString(), + ); + } on LogicValueConstructionException { + return null; + } + } + + static String _canonicalWaveformValue(String value, int width) { + final waveformValue = _waveformValue(value, width); + return waveformValue?.$2 ?? value.trim().replaceAll('\u0000', ''); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml index dcb1b7a95..3f24d6f92 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: flutter: {sdk: flutter} rohd: ^0.6.9 + rohd_hierarchy: + path: ../../../packages/rohd_hierarchy web: ^1.0.0 dev_dependencies: flutter_test: {sdk: flutter} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart new file mode 100644 index 000000000..3b0eec1a7 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -0,0 +1,173 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry_test.dart +// Tests for shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter_test/flutter_test.dart'; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +void main() { + tearDown(SignalValueFormatRegistry.clear); + + test('formats bare binary and hexadecimal waveform values', () { + expect( + SignalValueFormatRegistry.formatValue( + '0000', + SignalValueFormat.waveform, + 4, + ), + "4'h0", + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.signedDecimal, + 8, + ), + '-1', + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + 'ff', + SignalValueFormat.unsignedDecimal, + 8, + ), + '255', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x0', + SignalValueFormat.unsignedDecimal, + 4, + ), + '0', + ); + }); + + test('uses ROHD radix literals for typed format conversions', () { + expect( + SignalValueFormatRegistry.formatValue( + "8'd255", + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + '1010', + SignalValueFormat.octal, + 4, + ), + '0o12', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x4142', + SignalValueFormat.ascii, + 16, + ), + 'AB', + ); + }); + + test('looks up an occurrence address from the format trie', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.signedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.signedDecimal, + ); + }); + + test('looks up a fallback occurrence address', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.unsignedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatForAny([ + const OccurrenceAddress([7, 8, 9]), + const OccurrenceAddress([0, 2, 4]), + ]), + SignalValueFormat.unsignedDecimal, + ); + }); + + test('stores shared address prefixes once in the format trie', () { + SignalValueFormatRegistry.update([ + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 4]), + SignalValueFormat.unsignedDecimal, + ), + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 5]), + SignalValueFormat.signedDecimal, + ), + ]); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.unsignedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 5])), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 6])), + SignalValueFormat.waveform, + ); + }); + + test('rejects an invalid signal occurrence address', () { + expect( + () => SignalValueFormatRegistry.setFormatFor( + const [OccurrenceAddress([])], + SignalValueFormat.unsignedDecimal, + ), + throwsArgumentError, + ); + expect( + () => SignalValueFormatRegistry.formatFor( + const OccurrenceAddress([0, -1]), + ), + throwsArgumentError, + ); + }); + + test('converts between serialized names and format enum values', () { + expect( + SignalValueFormatRegistry.formatFromString('signedDecimal'), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatToString( + SignalValueFormat.signedDecimal, + ), + 'signedDecimal', + ); + expect(SignalValueFormatRegistry.formatFromString('unknown'), isNull); + }); +} diff --git a/rohd_devtools_extension/tool/test_devtools_install.dart b/rohd_devtools_extension/tool/test_devtools_install.dart index 01e6a1c04..f04097f14 100644 --- a/rohd_devtools_extension/tool/test_devtools_install.dart +++ b/rohd_devtools_extension/tool/test_devtools_install.dart @@ -61,7 +61,7 @@ Future main(List args) async { _requireFile(extensionAssetsPath, 'flutter.js'); _requireFile(extensionAssetsPath, 'main.dart.js'); _requireFile(extensionAssetsPath, 'version.json'); - _requireFile(extensionAssetsPath, p.join('assets', 'AssetManifest.json')); + _requireFile(extensionAssetsPath, p.join('assets', 'AssetManifest.bin')); _requireFile(extensionAssetsPath, p.join('assets', 'FontManifest.json')); _requireFile(extensionAssetsPath, p.join('canvaskit', 'canvaskit.js')); _requireFile(extensionAssetsPath, p.join('canvaskit', 'canvaskit.wasm')); diff --git a/rohd_extension/README.md b/rohd_extension/README.md index f93af8f63..5c54461c5 100644 --- a/rohd_extension/README.md +++ b/rohd_extension/README.md @@ -83,7 +83,7 @@ completions below narrow the ROHD-specific options by cursor location. | Prefix | Expands to | Description | |--------|-----------|-------------| | `mod`, `Module` | `class … extends Module { … }` | Module scaffold with `clk`, `reset`, `a`/`b` inputs, `depth`, `latchData`, `addInput`/`addOutput`, `definitionName`, and instance naming parameters | -| `sim`, `Simulator` | Clock, reset, `WaveDumper`, `Simulator.run()` | Simulation / testbench boilerplate | +| `sim`, `Simulator` | Clock, reset, `dumpWaves`, `Simulator.run()` | Simulation / testbench boilerplate | | `fsmModule`, `FSMModule` | enum + `class extends Module` + `FiniteStateMachine` | Full standalone FSM module scaffold | | `vf`, `tb`, `testbench` | `rohd_vf` testbench | Agent / Driver / Monitor / Sequencer template | diff --git a/rohd_extension/dart/lib/dtd_service.dart b/rohd_extension/dart/lib/dtd_service.dart index 917017986..5024d4429 100644 --- a/rohd_extension/dart/lib/dtd_service.dart +++ b/rohd_extension/dart/lib/dtd_service.dart @@ -44,7 +44,9 @@ class DtdService { /// /// Returns `true` if connection and registration succeeded. Future connect(String uri) async { - if (_disposed) return false; + if (_disposed) { + return false; + } try { _channel = WebSocketChannel.connect(Uri.parse(uri)); @@ -66,6 +68,7 @@ class DtdService { } on Exception catch (e) { _peer = null; _channel = null; + // Report the failed optional DTD connection to help diagnose setup. // ignore: avoid_print print('[DtdService] Failed to connect to DTD at $uri: $e'); return false; diff --git a/rohd_extension/snippets/rohd.json b/rohd_extension/snippets/rohd.json index 5c3c9ae73..463353e65 100644 --- a/rohd_extension/snippets/rohd.json +++ b/rohd_extension/snippets/rohd.json @@ -50,7 +50,7 @@ "body": [ "final clk = SimpleClockGenerator(10).clk;", "final reset = Logic(name: 'reset');", - "WaveDumper(module, outputPath: 'wavedumpername.vcd');", + "module.dumpWaves(outputPath: 'waves.vcd');", "Simulator.setMaxSimTime(100);", "unawaited(Simulator.run());", "", @@ -158,8 +158,8 @@ " // Build the DUT", " await tb.dut.build();", "", - " // Attach a waveform dumper to the DUT", - " WaveDumper(tb.dut);", + " // Enable waveform dumping for the DUT", + " tb.dut.dumpWaves();", "", " // Set a maximum simulation time so it doesn't run forever", " Simulator.setMaxSimTime(300);", diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index a287cba6f..eb4917506 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -2248,7 +2248,7 @@ void main() { test('simple 1d collapse', () async { final mod = SimpleLAPassthrough(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign laOut = laIn;')); }); @@ -2256,7 +2256,7 @@ void main() { test('array collapse for cross-module connection', () async { final mod = ArrayTopMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains(RegExp(r'ArraySubModIn.*\.inp\(inp\)'))); expect(sv, contains(RegExp(r'ArraySubModOut.*\.arrOut\(inp\)'))); @@ -2267,7 +2267,7 @@ void main() { LogicArray([3, 3], 1), LogicArray([3, 3], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -2284,7 +2284,7 @@ void main() { test('partial array assignments collapse into range assignment', () async { final mod = PartialArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[4:2] = src[4:2];')); @@ -2306,7 +2306,7 @@ void main() { () async { final mod = ChainedPartialArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[4:2] = src[4:2];')); @@ -2328,7 +2328,7 @@ void main() { () async { final mod = ChainedSubrangeArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[3:2] = src[6:5];')); @@ -2353,7 +2353,7 @@ void main() { test('three-deep chained range assignments collapse iteratively', () async { final mod = ThreeDeepChainedPartialArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[4:2] = src[4:2];')); @@ -2376,7 +2376,7 @@ void main() { () async { final mod = LongChainedPartialArrayRangeAssignment(); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog().output); expect(topBody, contains('assign dst[4:2] = src[4:2];')); expect(topBody, isNot(contains('intermediate'))); @@ -2396,7 +2396,7 @@ void main() { test('multi-use chained range intermediate stays expanded', () async { final mod = ChainedPartialArrayRangeAssignment(exposeIntermediate: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -2419,7 +2419,7 @@ void main() { test('renameable chained range intermediate stays expanded', () async { final mod = ChainedPartialArrayRangeAssignment(intermediateNaming: null); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -2442,7 +2442,7 @@ void main() { () async { final mod = PartialBusToArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[5:2] = src[5:2];')); @@ -2467,7 +2467,7 @@ void main() { test('full array-to-bus assignSubset has no subset intermediate', () async { final mod = ArrayToBusAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('_subset'))); @@ -2486,7 +2486,7 @@ void main() { () async { final mod = ArrayToBusAssignSubsetRangeAssignment(partial: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[5:2] = src[5:2];')); @@ -2523,7 +2523,7 @@ void main() { driveLowBits: config.driveLowBits, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); if (config.driveLowBits) { @@ -2560,7 +2560,7 @@ void main() { test('bus subset helpers with extra consumers are preserved', () async { final mod = BusSubsetBitsWithExtraConsumers(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[3:0] = src[5:2];')); @@ -2582,7 +2582,7 @@ void main() { test('partial slice helper with extra consumer is preserved', () async { final mod = PartialSliceWithExtraConsumer(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[8:5] = src[9:6];')); @@ -2613,7 +2613,7 @@ void main() { test('sparse bus runs feeding assignSubset collapse independently', () async { final mod = SparseBusRunsToAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[31:20] = srcA[15:4];')); @@ -2646,7 +2646,7 @@ void main() { test('constant-backed upper range remains tied off after collapse', () async { final mod = TiedRangeToAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2671,7 +2671,7 @@ void main() { tieNaming: tieNaming, ); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog().output); expect(topBody, contains('logic [7:0] tie;')); expect(topBody, contains("assign tie = 8'h0;")); @@ -2690,7 +2690,7 @@ void main() { busNaming: Naming.renameable, ); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog().output); expect(topBody, contains('logic [31:0] bus;')); expect(topBody, contains('.data(bus)')); @@ -2706,7 +2706,7 @@ void main() { test('constant-backed range concatenates with sibling output', () async { final mod = TiedSiblingRangeToAssignSubsetAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2727,7 +2727,7 @@ void main() { test('constant-backed range concatenates into late child input', () async { final mod = TiedSiblingRangeToLateInputSource(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2747,7 +2747,7 @@ void main() { test('named constant subsets survive scalar output collapse', () async { final mod = ScalarSiblingOutputsWithNamedTieTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2776,7 +2776,7 @@ void main() { () async { final mod = InteriorNamedTieWithMappedOutputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains("assign bus[6:4] = 3'h0;")); @@ -2811,7 +2811,7 @@ void main() { fanout: fanout, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2849,7 +2849,7 @@ void main() { () async { final mod = InvalidConstantsToAssignSubsetTop(); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog().output); expect(topBody, contains("2'bxx")); expect(topBody, isNot(contains("2'bzz"))); @@ -2875,7 +2875,7 @@ void main() { () async { final mod = InternalBusRunsToAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[44:13] = src[31:0];')); @@ -2909,7 +2909,7 @@ void main() { computedSource: true, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[44:13] = srcStage[31:0];')); @@ -2939,7 +2939,7 @@ void main() { () async { final mod = WideTemporarySliceToArrayWords(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign y[0][15:0] = src[47:32];')); @@ -2968,7 +2968,7 @@ void main() { () async { final mod = WideTemporarySliceToArrayWords(extraConsumers: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign y[0][15:0] = src[47:32];')); @@ -2996,7 +2996,7 @@ void main() { () async { final mod = ManualSubsetNamedArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('manual_subset')); @@ -3018,7 +3018,7 @@ void main() { test('reordered bus-to-array assignments stay expanded', () async { final mod = PartialBusToArrayRangeAssignment(reversed: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[5:2] = src[5:2];'))); @@ -3046,7 +3046,7 @@ void main() { test('bus-to-unpacked-array assignments stay expanded', () async { final mod = PartialBusToArrayRangeAssignment(numUnpackedDimensions: 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[5:2] = src[5:2];'))); @@ -3073,7 +3073,7 @@ void main() { test('non-contiguous partial array assignments stay expanded', () async { final mod = PartialArrayRangeAssignment(reversed: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2]'))); @@ -3097,7 +3097,7 @@ void main() { () async { final mod = PartialInnerArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[1][3:1] = src[1][3:1];')); @@ -3122,7 +3122,7 @@ void main() { test('unpacked outer dimension still collapses inner packed range', () async { final mod = PartialInnerArrayRangeAssignment(numUnpackedDimensions: 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[1][3:1] = src[1][3:1];')); @@ -3146,7 +3146,7 @@ void main() { test('unpacked one-dimensional partial assignments stay expanded', () async { final mod = PartialUnpackedArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -3168,7 +3168,7 @@ void main() { test('wide element partial array assignments stay expanded', () async { final mod = PartialWideArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[2:1] = src[2:1];'))); @@ -3194,7 +3194,7 @@ void main() { test('net array partial assignments stay in net connection flow', () async { final mod = PartialNetArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -3217,7 +3217,7 @@ void main() { () async { final mod = PartialLogicNetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -3239,7 +3239,7 @@ void main() { final mod = ArrayWithShuffledAssignment(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign b[0] = a[3];')); expect(sv, contains('assign b[3] = a[0];')); @@ -3256,7 +3256,7 @@ void main() { LogicArray([3, 3], 1, numUnpackedDimensions: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -3273,7 +3273,7 @@ void main() { final mod = ArrayModule(LogicArray([4, 4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign d = c[0];')); expect(sv, contains('assign b = a;')); @@ -3300,7 +3300,7 @@ void main() { name: 'constant_leaf_array_assignment_${cfg.name.replaceAll(' ', '_')}', ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); for (final row in [0, 1]) { @@ -3366,7 +3366,7 @@ void main() { elementWidth: cfg.elementWidth, reversed: cfg.reversed); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // the intermediate array (and every declaration of it) must be gone expect(sv, isNot(contains('arr'))); @@ -3401,7 +3401,7 @@ void main() { LogicNet(width: total), LogicNet(width: total), dimensions: cfg.dimensions, elementWidth: cfg.elementWidth); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // the intermediate array and its net_connects must be gone expect(sv, isNot(contains('arr'))); @@ -3425,7 +3425,7 @@ void main() { final mod = PartiallyDrivenArray(Logic(width: total - 2), dimensions: dimensions); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // the array must remain declared since undriven bits must stay `z` expect(sv, contains('arr')); @@ -3442,7 +3442,7 @@ void main() { test('aggregate-used array is not inlined', () async { final mod = ArrayElementsWithAggregateUse(Logic(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // the array stays (aggregate use), so elements are not inlined into ports expect(sv, contains('arr')); @@ -3459,7 +3459,7 @@ void main() { test('input-array port elements are not inlined away', () async { final mod = ArrayPortElementsToSubmodules(LogicArray([2, 2], 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // the array port must remain declared expect(sv, contains('a')); @@ -3492,7 +3492,7 @@ void main() { () async { final mod = ConstantToSingleElementArrayInputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains(".data((8'h0))")); @@ -3509,7 +3509,7 @@ void main() { () async { final mod = ConstantToSingleElementArrayInputTop(value: 0xa5); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains(".data((8'ha5))")); @@ -3543,7 +3543,7 @@ void main() { elementWidth: cfg.elementWidth, perm: cfg.perm); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the intermediate array (and every per-element assignment) is gone, @@ -3580,7 +3580,7 @@ void main() { const n = 4; final mod = MergedSourcesToArrayPort(List.generate(n, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('intermediate'))); @@ -3606,7 +3606,7 @@ void main() { () async { final mod = RangeSourcesToArrayPort(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains(RegExp(r'\.a\(\(\{\s*src,')))); @@ -3649,7 +3649,7 @@ void main() { List.generate(cfg.n, (_) => LogicNet()), LogicNet(width: cfg.n), perm: cfg.perm); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the intermediate array and its net_connects are gone, replaced by a @@ -3680,7 +3680,7 @@ void main() { // restriction prevents collapsing and the array stays declared final mod = MultiUseAggregate(List.generate(4, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // with two whole-array uses, the array stays declared and its per-element @@ -3725,7 +3725,7 @@ void main() { final mod = ArrayPortToIndividualNets( List.generate(4, (_) => LogicNet()), LogicNet(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the intermediate array and its net_connects collapse into a single @@ -3752,7 +3752,7 @@ void main() { // result must still be correct. final mod = RearrangeOneArray(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // this pass did not fabricate a consolidating concatenation on the port @@ -3780,7 +3780,7 @@ void main() { final mod = IndividualSignalsToExpressionlessPort( List.generate(4, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // no inline concatenation on the expressionless port; per-element @@ -3806,7 +3806,7 @@ void main() { () async { final mod = WholeNetBusCollapseNamingCollision(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('bussubset ('))); @@ -3828,7 +3828,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: busNaming); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); if (busNaming == Naming.mergeable) { @@ -3861,7 +3861,7 @@ void main() { final mod = WholeNetBusToPortWithInlineSubsetConsumer( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.data')); @@ -3887,7 +3887,7 @@ void main() { final mod = WholeNetBusToPortWithReadOnlyInlineSubsetConsumer(LogicNet(width: n)); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog().output); expect(topBody, contains('wire [3:0] bus')); expect(topBody, contains(RegExp('net_connect.*_subset_0_0_bus'))); @@ -3900,7 +3900,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: Naming.reserved); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // a reserved name must be preserved, so the bus and its net_connects stay @@ -3925,7 +3925,7 @@ void main() { final mod = WholeNetBusMultiUse(List.generate(n, (_) => LogicNet()), LogicNet(width: n), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // used as a whole twice, so the single-use restriction keeps the bus @@ -3957,7 +3957,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: busNaming); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); if (busNaming == Naming.mergeable) { @@ -3991,7 +3991,7 @@ void main() { final mod = BitwiseNetBusToArrayPortWithInlineSubsetConsumer( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.data')); @@ -4019,7 +4019,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: Naming.reserved); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // a reserved bus name must be preserved, so it is not traced away expect(sv, contains('bus')); @@ -4047,7 +4047,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), toArray: toArray); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the self-connection leaves a per-bit net_connect structure intact @@ -4078,7 +4078,7 @@ void main() { () async { final mod = PureSelfLoopNetBus(LogicNet(width: 2), toArray: toArray); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the bus collapses into an inline concatenation of the merged net, and @@ -4101,7 +4101,7 @@ void main() { final mod = AssignSubsetReceiver( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // no intermediate subset array, and no per-bit net_connects remain @@ -4126,7 +4126,7 @@ void main() { final mod = AssignSubsetReceiverScrambled( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('_subset'))); @@ -4152,7 +4152,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: Naming.renameable); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the named bus is preserved, but the per-bit `*_subset` pass-through and @@ -4180,7 +4180,7 @@ void main() { final mod = AssignSubsetDriver( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the per-bit `*_subset` pass-throughs and per-bit `net_connect`s are @@ -4205,7 +4205,7 @@ void main() { const n = 4; final mod = AssignSubsetLogicDriver(List.generate(n, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // the intermediate `sig_subset` array is forwarded straight into the @@ -4230,7 +4230,7 @@ void main() { () async { final mod = LateSubsetInputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4248,7 +4248,7 @@ void main() { () async { final mod = LateSlicedSubsetInputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4266,7 +4266,7 @@ void main() { test('sibling output can drive subset of sibling input source', () async { final mod = SiblingOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4283,7 +4283,7 @@ void main() { test('sibling full output can drive sibling full input source', () async { final mod = SiblingFullOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4300,7 +4300,7 @@ void main() { test('sibling output stays connected beside range assignments', () async { final mod = SiblingOutputWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.result(bus[7])')); @@ -4326,7 +4326,7 @@ void main() { () async { final mod = IndexedSiblingOutputWithRangeAssignmentsTop(outputIndex); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, contains('.result(bus[$outputIndex])')); @@ -4353,7 +4353,7 @@ void main() { test('multiple sibling outputs stay connected in packed concat', () async { final mod = MultipleSiblingOutputsWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4385,7 +4385,7 @@ void main() { () async { final mod = FanoutSiblingOutputWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4411,7 +4411,7 @@ void main() { test('wide sibling output stays connected after range collapse', () async { final mod = WideSiblingOutputWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4436,7 +4436,7 @@ void main() { test('wide sibling output keeps fanout between constant ranges', () async { final mod = WideSiblingOutputWithConstantsAndFanoutTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4463,7 +4463,7 @@ void main() { () async { final mod = SiblingArrayOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4481,7 +4481,7 @@ void main() { () async { final mod = SiblingStructOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4498,7 +4498,7 @@ void main() { test('sibling inout can drive subset of sibling inout source', () async { final mod = SiblingInOutToInOutSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4515,7 +4515,7 @@ void main() { test('sibling boundary kitchen sink keeps mixed source mappings', () async { final mod = SiblingBoundaryProductTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); for (final portName in [ @@ -4556,7 +4556,7 @@ void main() { final mod = AssignSubsetPartial( List.generate(n ~/ 2, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; final topBody = _topModuleBody(sv); // not every element is a pass-through, so the subset array is preserved @@ -4660,7 +4660,7 @@ void main() { } await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog().output); // --- structural expectations (only where confidently predictable) --- if (config.noSubset) { diff --git a/test/assignment_test.dart b/test/assignment_test.dart index 712ebd9ee..aedc8a624 100644 --- a/test/assignment_test.dart +++ b/test/assignment_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // assignment_test.dart diff --git a/test/benchmark_test.dart b/test/benchmark_test.dart index a65020166..c660b505e 100644 --- a/test/benchmark_test.dart +++ b/test/benchmark_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2024 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // benchmark_test.dart diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..e7ad3633b 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // bus_test.dart @@ -228,7 +228,7 @@ void main() { final mod = SingleBitBusSubsetMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign result = oneBit')); final vectors = [ @@ -401,7 +401,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv.contains("assign const_subset = 16'habcd;"), true); }); }); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 8128eea2d..2c35aa6d7 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -66,7 +66,7 @@ void main() { test('collapse pretty', () async { final mod = CollapseTestModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // make sure e=a&b&c is in there, to prove there was some inlining expect(sv, contains(RegExp('e.*=.*a.*&.*b.*&.*c'))); @@ -78,7 +78,7 @@ void main() { final mod = CombinationalLoopCollapseModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains(' | a')); expect(sv, contains(' == a')); }); diff --git a/test/comb_math_test.dart b/test/comb_math_test.dart index b2a7165ed..79a095a0d 100644 --- a/test/comb_math_test.dart +++ b/test/comb_math_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // comb_math_test.dart diff --git a/test/comb_mod_test.dart b/test/comb_mod_test.dart index 280d19e2e..3e5fe5775 100644 --- a/test/comb_mod_test.dart +++ b/test/comb_mod_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // comb_mod_test.dart diff --git a/test/config_test.dart b/test/config_test.dart index beca0b576..7968de48e 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -14,7 +14,7 @@ import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; -import 'wave_dumper_test.dart'; +import 'waveform_service_test.dart'; class SimpleModule extends Module { SimpleModule(Logic a, Logic b) { @@ -46,14 +46,30 @@ void main() { final mod = SimpleModule(Logic(), Logic()); await mod.build(); + final sv = mod.dumpSystemVerilog().output; + + expect(sv, contains(version)); + }); + + test( + 'should contains ROHD version number when deprecated synth is generated.', + () async { + const version = Config.version; + + final mod = SimpleModule(Logic(), Logic()); + await mod.build(); + + // This test verifies that the deprecated API still includes the version. + // ignore: deprecated_member_use_from_same_package final sv = mod.generateSynth(); expect(sv, contains(version)); }); if (!kIsWeb) { - test('should contains ROHD version number when wavedumper is generated.', - () async { + test( + 'should contains ROHD version number when ' + 'waveform service is generated.', () async { const version = Config.version; final mod = SimpleModule(Logic(), Logic()); diff --git a/test/const_radix_test.dart b/test/const_radix_test.dart index 4e9beb4eb..654cd37ca 100644 --- a/test/const_radix_test.dart +++ b/test/const_radix_test.dart @@ -140,7 +140,7 @@ void main() { final module = _ConstRadixModule(); await module.build(); - final systemVerilog = module.generateSynth(); + final systemVerilog = module.dumpSystemVerilog().output; expect(systemVerilog, contains("assign autoHex = 8'h2a;")); expect(systemVerilog, contains("assign binaryValue = 8'b101010;")); @@ -154,7 +154,7 @@ void main() { final module = _ExpressionlessRadixTop(); await module.build(); - final systemVerilog = module.generateSynth(); + final systemVerilog = module.dumpSystemVerilog().output; expect(systemVerilog, contains("assign in = 8'd42;")); expect(systemVerilog, contains('.in(in)')); diff --git a/test/counter_test.dart b/test/counter_test.dart index 8f59b58d7..2fa6a9f5d 100644 --- a/test/counter_test.dart +++ b/test/counter_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // counter_test.dart @@ -48,7 +48,7 @@ void main() { final reset = Logic(); final counter = Counter(Logic(), reset); await counter.build(); - // WaveDumper(counter); + // counter.dumpWaves(); unawaited(reset.nextPosedge .then((value) => expect(counter.val.value.toInt(), equals(0)))); diff --git a/test/counter_wintf_test.dart b/test/counter_wintf_test.dart index 7889369cc..873563722 100644 --- a/test/counter_wintf_test.dart +++ b/test/counter_wintf_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // 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 = mod.dumpSystemVerilog().output; expect(!sv.contains('en_0'), true); }); diff --git a/test/external_test.dart b/test/external_test.dart index 09ac84c87..93c2f8edc 100644 --- a/test/external_test.dart +++ b/test/external_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2024 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // 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 = mod.dumpSystemVerilog().output; // make sure we instantiate the external module properly expect( diff --git a/test/fixtures/gate_catalog.rohd.json b/test/fixtures/gate_catalog.rohd.json new file mode 100644 index 000000000..ffa54ef77 --- /dev/null +++ b/test/fixtures/gate_catalog.rohd.json @@ -0,0 +1,4773 @@ +{ + "creator": "NetlistSynthesizer (rohd)", + "version": "0.0.1", + "modules": { + "GateCatalog": { + "attributes": { + "src": "generated", + "top": 1 + }, + "ports": { + "clk": { + "direction": "input", + "bits": [ + 2 + ], + "logic_type": { + "width": 1 + } + }, + "en": { + "direction": "input", + "bits": [ + 3 + ], + "logic_type": { + "width": 1 + } + }, + "reset": { + "direction": "input", + "bits": [ + 4 + ], + "logic_type": { + "width": 1 + } + }, + "muxSel": { + "direction": "input", + "bits": [ + 5 + ], + "logic_type": { + "width": 1 + } + }, + "enableTri": { + "direction": "input", + "bits": [ + 6 + ], + "logic_type": { + "width": 1 + } + }, + "a4": { + "direction": "input", + "bits": [ + 7, + 8, + 9, + 10 + ], + "logic_type": { + "width": 4 + } + }, + "b4": { + "direction": "input", + "bits": [ + 11, + 12, + 13, + 14 + ], + "logic_type": { + "width": 4 + } + }, + "a8": { + "direction": "input", + "bits": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "logic_type": { + "width": 8 + } + }, + "b8": { + "direction": "input", + "bits": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "logic_type": { + "width": 8 + } + }, + "d4": { + "direction": "input", + "bits": [ + 31, + 32, + 33, + 34 + ], + "logic_type": { + "width": 4 + } + }, + "shamt4": { + "direction": "input", + "bits": [ + 35, + 36, + 37, + 38 + ], + "logic_type": { + "width": 4 + } + }, + "idx3": { + "direction": "input", + "bits": [ + 39, + 40, + 41 + ], + "logic_type": { + "width": 3 + } + }, + "idx5": { + "direction": "input", + "bits": [ + 42, + 43, + 44, + 45, + 46 + ], + "logic_type": { + "width": 5 + } + }, + "resetValueDyn4": { + "direction": "input", + "bits": [ + 47, + 48, + 49, + 50 + ], + "logic_type": { + "width": 4 + } + }, + "not_out": { + "direction": "output", + "bits": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ], + "logic_type": { + "width": 8 + } + }, + "and_ll_out": { + "direction": "output", + "bits": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ], + "logic_type": { + "width": 8 + } + }, + "and_lc_out": { + "direction": "output", + "bits": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ], + "logic_type": { + "width": 8 + } + }, + "or_ll_out": { + "direction": "output", + "bits": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ], + "logic_type": { + "width": 8 + } + }, + "or_lc_out": { + "direction": "output", + "bits": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ], + "logic_type": { + "width": 8 + } + }, + "xor_ll_out": { + "direction": "output", + "bits": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ], + "logic_type": { + "width": 8 + } + }, + "xor_lc_out": { + "direction": "output", + "bits": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ], + "logic_type": { + "width": 8 + } + }, + "reduce_and_out": { + "direction": "output", + "bits": [ + 107 + ], + "logic_type": { + "width": 1 + } + }, + "reduce_or_out": { + "direction": "output", + "bits": [ + 108 + ], + "logic_type": { + "width": 1 + } + }, + "reduce_xor_out": { + "direction": "output", + "bits": [ + 109 + ], + "logic_type": { + "width": 1 + } + }, + "add_ll_sum": { + "direction": "output", + "bits": [ + 110, + 111, + 112, + 113 + ], + "logic_type": { + "width": 4 + } + }, + "add_ll_carry": { + "direction": "output", + "bits": [ + 114 + ], + "logic_type": { + "width": 1 + } + }, + "add_lc_sum": { + "direction": "output", + "bits": [ + 115, + 116, + 117, + 118 + ], + "logic_type": { + "width": 4 + } + }, + "add_lc_carry": { + "direction": "output", + "bits": [ + 119 + ], + "logic_type": { + "width": 1 + } + }, + "sub_ll_out": { + "direction": "output", + "bits": [ + 120, + 121, + 122, + 123 + ], + "logic_type": { + "width": 4 + } + }, + "sub_lc_out": { + "direction": "output", + "bits": [ + 124, + 125, + 126, + 127 + ], + "logic_type": { + "width": 4 + } + }, + "mul_ll_out": { + "direction": "output", + "bits": [ + 128, + 129, + 130, + 131 + ], + "logic_type": { + "width": 4 + } + }, + "mul_lc_out": { + "direction": "output", + "bits": [ + 132, + 133, + 134, + 135 + ], + "logic_type": { + "width": 4 + } + }, + "div_ll_out": { + "direction": "output", + "bits": [ + 136, + 137, + 138, + 139 + ], + "logic_type": { + "width": 4 + } + }, + "div_lc_out": { + "direction": "output", + "bits": [ + 140, + 141, + 142, + 143 + ], + "logic_type": { + "width": 4 + } + }, + "mod_ll_out": { + "direction": "output", + "bits": [ + 144, + 145, + 146, + 147 + ], + "logic_type": { + "width": 4 + } + }, + "mod_lc_out": { + "direction": "output", + "bits": [ + 148, + 149, + 150, + 151 + ], + "logic_type": { + "width": 4 + } + }, + "pow_ll_out": { + "direction": "output", + "bits": [ + 152, + 153, + 154, + 155 + ], + "logic_type": { + "width": 4 + } + }, + "pow_lc_out": { + "direction": "output", + "bits": [ + 156, + 157, + 158, + 159 + ], + "logic_type": { + "width": 4 + } + }, + "eq_ll_out": { + "direction": "output", + "bits": [ + 160 + ], + "logic_type": { + "width": 1 + } + }, + "eq_lc_out": { + "direction": "output", + "bits": [ + 161 + ], + "logic_type": { + "width": 1 + } + }, + "neq_ll_out": { + "direction": "output", + "bits": [ + 162 + ], + "logic_type": { + "width": 1 + } + }, + "neq_lc_out": { + "direction": "output", + "bits": [ + 163 + ], + "logic_type": { + "width": 1 + } + }, + "lt_ll_out": { + "direction": "output", + "bits": [ + 164 + ], + "logic_type": { + "width": 1 + } + }, + "lt_lc_out": { + "direction": "output", + "bits": [ + 165 + ], + "logic_type": { + "width": 1 + } + }, + "gt_ll_out": { + "direction": "output", + "bits": [ + 166 + ], + "logic_type": { + "width": 1 + } + }, + "gt_lc_out": { + "direction": "output", + "bits": [ + 167 + ], + "logic_type": { + "width": 1 + } + }, + "le_ll_out": { + "direction": "output", + "bits": [ + 168 + ], + "logic_type": { + "width": 1 + } + }, + "le_lc_out": { + "direction": "output", + "bits": [ + 169 + ], + "logic_type": { + "width": 1 + } + }, + "ge_ll_out": { + "direction": "output", + "bits": [ + 170 + ], + "logic_type": { + "width": 1 + } + }, + "ge_lc_out": { + "direction": "output", + "bits": [ + 171 + ], + "logic_type": { + "width": 1 + } + }, + "lshift_ll_out": { + "direction": "output", + "bits": [ + 172, + 173, + 174, + 175 + ], + "logic_type": { + "width": 4 + } + }, + "lshift_lc_out": { + "direction": "output", + "bits": [ + 176, + 177, + 178, + 179 + ], + "logic_type": { + "width": 4 + } + }, + "rshift_ll_out": { + "direction": "output", + "bits": [ + 180, + 181, + 182, + 183 + ], + "logic_type": { + "width": 4 + } + }, + "rshift_lc_out": { + "direction": "output", + "bits": [ + 184, + 185, + 186, + 187 + ], + "logic_type": { + "width": 4 + } + }, + "arshift_ll_out": { + "direction": "output", + "bits": [ + 188, + 189, + 190, + 191 + ], + "logic_type": { + "width": 4 + } + }, + "arshift_lc_out": { + "direction": "output", + "bits": [ + 192, + 193, + 194, + 195 + ], + "logic_type": { + "width": 4 + } + }, + "mux_class_out": { + "direction": "output", + "bits": [ + 196, + 197, + 198, + 199 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_dynamic_out": { + "direction": "output", + "bits": [ + 200, + 201, + 202, + 203 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_const1_out": { + "direction": "output", + "bits": [ + 407, + 408, + 409, + 410 + ], + "logic_type": { + "width": 4 + } + }, + "mux_fn_const0_out": { + "direction": "output", + "bits": [ + 411, + 412, + 413, + 414 + ], + "logic_type": { + "width": 4 + } + }, + "index_natural_out": { + "direction": "output", + "bits": [ + 212 + ], + "logic_type": { + "width": 1 + } + }, + "index_oversized_out": { + "direction": "output", + "bits": [ + 213 + ], + "logic_type": { + "width": 1 + } + }, + "replicate_x3_out": { + "direction": "output", + "bits": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ], + "logic_type": { + "width": 12 + } + }, + "replicate_x5_out": { + "direction": "output", + "bits": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ], + "logic_type": { + "width": 20 + } + }, + "slice_out": { + "direction": "output", + "bits": [ + 246, + 247, + 248, + 249 + ], + "logic_type": { + "width": 4 + } + }, + "swizzle_out": { + "direction": "output", + "bits": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ], + "logic_type": { + "width": 8 + } + }, + "tribuf_readback_out": { + "direction": "output", + "bits": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ], + "logic_type": { + "width": 8 + } + }, + "q_dff": { + "direction": "output", + "bits": [ + 266, + 267, + 268, + 269 + ], + "logic_type": { + "width": 4 + } + }, + "q_dffe": { + "direction": "output", + "bits": [ + 270, + 271, + 272, + 273 + ], + "logic_type": { + "width": 4 + } + }, + "q_sdff": { + "direction": "output", + "bits": [ + 274, + 275, + 276, + 277 + ], + "logic_type": { + "width": 4 + } + }, + "q_sdffe": { + "direction": "output", + "bits": [ + 278, + 279, + 280, + 281 + ], + "logic_type": { + "width": 4 + } + }, + "q_adff": { + "direction": "output", + "bits": [ + 282, + 283, + 284, + 285 + ], + "logic_type": { + "width": 4 + } + }, + "q_adffe": { + "direction": "output", + "bits": [ + 286, + 287, + 288, + 289 + ], + "logic_type": { + "width": 4 + } + }, + "q_aldff": { + "direction": "output", + "bits": [ + 290, + 291, + 292, + 293 + ], + "logic_type": { + "width": 4 + } + }, + "q_aldffe": { + "direction": "output", + "bits": [ + 294, + 295, + 296, + 297 + ], + "logic_type": { + "width": 4 + } + }, + "q_dynsync_noen": { + "direction": "output", + "bits": [ + 298, + 299, + 300, + 301 + ], + "logic_type": { + "width": 4 + } + }, + "q_dynsync_en": { + "direction": "output", + "bits": [ + 302, + 303, + 304, + 305 + ], + "logic_type": { + "width": 4 + } + }, + "bus": { + "direction": "inout", + "bits": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "logic_type": { + "width": 8 + } + } + }, + "cells": { + "not_": { + "hide_name": 0, + "type": "$not", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ] + } + }, + "and_": { + "hide_name": 0, + "type": "$and", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ] + } + }, + "and__0": { + "hide_name": 0, + "type": "$and", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ], + "Y": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ] + } + }, + "or_": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ] + } + }, + "or__0": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ], + "Y": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ] + } + }, + "xor_": { + "hide_name": 0, + "type": "$xor", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "Y": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ] + } + }, + "xor__0": { + "hide_name": 0, + "type": "$xor", + "parameters": { + "A_WIDTH": 8, + "B_WIDTH": 8, + "Y_WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ], + "Y": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ] + } + }, + "uand": { + "hide_name": 0, + "type": "$reduce_and", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 107 + ] + } + }, + "uor": { + "hide_name": 0, + "type": "$reduce_or", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 108 + ] + } + }, + "uxor": { + "hide_name": 0, + "type": "$reduce_xor", + "parameters": { + "A_WIDTH": 8, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 109 + ] + } + }, + "add": { + "hide_name": 0, + "type": "$add", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 5 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 110, + 111, + 112, + 113, + 114 + ] + } + }, + "add_0": { + "hide_name": 0, + "type": "$add", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 5 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 338, + 339, + 340, + 341 + ], + "Y": [ + 115, + 116, + 117, + 118, + 119 + ] + } + }, + "subtract": { + "hide_name": 0, + "type": "$sub", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 120, + 121, + 122, + 123 + ] + } + }, + "subtract_0": { + "hide_name": 0, + "type": "$sub", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 342, + 343, + 344, + 345 + ], + "Y": [ + 124, + 125, + 126, + 127 + ] + } + }, + "multiply": { + "hide_name": 0, + "type": "$mul", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 128, + 129, + 130, + 131 + ] + } + }, + "multiply_0": { + "hide_name": 0, + "type": "$mul", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 346, + 347, + 348, + 349 + ], + "Y": [ + 132, + 133, + 134, + 135 + ] + } + }, + "divide": { + "hide_name": 0, + "type": "$div", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 136, + 137, + 138, + 139 + ] + } + }, + "divide_0": { + "hide_name": 0, + "type": "$div", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 350, + 351, + 352, + 353 + ], + "Y": [ + 140, + 141, + 142, + 143 + ] + } + }, + "modulo": { + "hide_name": 0, + "type": "$mod", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 144, + 145, + 146, + 147 + ] + } + }, + "modulo_0": { + "hide_name": 0, + "type": "$mod", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 354, + 355, + 356, + 357 + ], + "Y": [ + 148, + 149, + 150, + 151 + ] + } + }, + "power": { + "hide_name": 0, + "type": "$pow", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 152, + 153, + 154, + 155 + ] + } + }, + "power_0": { + "hide_name": 0, + "type": "$pow", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 358, + 359, + 360, + 361 + ], + "Y": [ + 156, + 157, + 158, + 159 + ] + } + }, + "equals": { + "hide_name": 0, + "type": "$eq", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 160 + ] + } + }, + "equals_0": { + "hide_name": 0, + "type": "$eq", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 362, + 363, + 364, + 365 + ], + "Y": [ + 161 + ] + } + }, + "notEquals": { + "hide_name": 0, + "type": "$ne", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 162 + ] + } + }, + "notEquals_0": { + "hide_name": 0, + "type": "$ne", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 366, + 367, + 368, + 369 + ], + "Y": [ + 163 + ] + } + }, + "lessthan": { + "hide_name": 0, + "type": "$lt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 164 + ] + } + }, + "lessthan_0": { + "hide_name": 0, + "type": "$lt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 370, + 371, + 372, + 373 + ], + "Y": [ + 165 + ] + } + }, + "greaterThan": { + "hide_name": 0, + "type": "$gt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 166 + ] + } + }, + "greaterThan_0": { + "hide_name": 0, + "type": "$gt", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 374, + 375, + 376, + 377 + ], + "Y": [ + 167 + ] + } + }, + "lessThanOrEqual": { + "hide_name": 0, + "type": "$le", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 168 + ] + } + }, + "lessThanOrEqual_0": { + "hide_name": 0, + "type": "$le", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 378, + 379, + 380, + 381 + ], + "Y": [ + 169 + ] + } + }, + "greaterThanOrEqual": { + "hide_name": 0, + "type": "$ge", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 170 + ] + } + }, + "greaterThanOrEqual_0": { + "hide_name": 0, + "type": "$ge", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 382, + 383, + 384, + 385 + ], + "Y": [ + 171 + ] + } + }, + "lshift": { + "hide_name": 0, + "type": "$shl", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 172, + 173, + 174, + 175 + ] + } + }, + "lshift_0": { + "hide_name": 0, + "type": "$shl", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 403, + 404, + 405, + 406 + ], + "Y": [ + 176, + 177, + 178, + 179 + ] + } + }, + "rshift": { + "hide_name": 0, + "type": "$shr", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 180, + 181, + 182, + 183 + ] + } + }, + "rshift_0": { + "hide_name": 0, + "type": "$shr", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 2, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 390, + 391 + ], + "Y": [ + 184, + 185, + 186, + 187 + ] + } + }, + "arshift": { + "hide_name": 0, + "type": "$sshr", + "parameters": { + "A_SIGNED": 1, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 4, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 35, + 36, + 37, + 38 + ], + "Y": [ + 188, + 189, + 190, + 191 + ] + } + }, + "arshift_0": { + "hide_name": 0, + "type": "$sshr", + "parameters": { + "A_SIGNED": 1, + "A_WIDTH": 4, + "B_SIGNED": 0, + "B_WIDTH": 2, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "B": [ + 392, + 393 + ], + "Y": [ + 192, + 193, + 194, + 195 + ] + } + }, + "mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "S": [ + 5 + ], + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 196, + 197, + 198, + 199 + ] + } + }, + "mux_0": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "S": [ + 5 + ], + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 200, + 201, + 202, + 203 + ] + } + }, + "unnamed_module": { + "hide_name": 0, + "type": "$shiftx", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 8, + "B_SIGNED": 0, + "B_WIDTH": 3, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 39, + 40, + 41 + ], + "Y": [ + 212 + ] + } + }, + "unnamed_module_0": { + "hide_name": 0, + "type": "$shiftx", + "parameters": { + "A_SIGNED": 0, + "A_WIDTH": 8, + "B_SIGNED": 0, + "B_WIDTH": 5, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "B": [ + 42, + 43, + 44, + 45, + 46 + ], + "Y": [ + 213 + ] + } + }, + "unnamed_module_1": { + "hide_name": 0, + "type": "ReplicationOp", + "parameters": {}, + "attributes": {}, + "port_directions": { + "_a4": "input", + "_replicated_a4": "output" + }, + "connections": { + "_a4": [ + 7, + 8, + 9, + 10 + ], + "_replicated_a4": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ] + } + }, + "unnamed_module_2": { + "hide_name": 0, + "type": "ReplicationOp", + "parameters": {}, + "attributes": {}, + "port_directions": { + "_a4": "input", + "_replicated_a4": "output" + }, + "connections": { + "_a4": [ + 7, + 8, + 9, + 10 + ], + "_replicated_a4": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ] + } + }, + "bussubset": { + "hide_name": 0, + "type": "$slice", + "parameters": { + "OFFSET": 2, + "A_WIDTH": 8, + "Y_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "Y": [ + 246, + 247, + 248, + 249 + ] + } + }, + "swizzle": { + "hide_name": 0, + "type": "$concat", + "parameters": { + "A_WIDTH": 4, + "B_WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 11, + 12, + 13, + 14 + ], + "B": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ] + } + }, + "flipflop": { + "hide_name": 0, + "type": "$dff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 266, + 267, + 268, + 269 + ] + } + }, + "flipflop_0": { + "hide_name": 0, + "type": "$dffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 270, + 271, + 272, + 273 + ], + "EN": [ + 3 + ] + } + }, + "flipflop_1": { + "hide_name": 0, + "type": "$sdff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "SRST_POLARITY": 1, + "SRST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "SRST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 274, + 275, + 276, + 277 + ], + "SRST": [ + 4 + ] + } + }, + "flipflop_2": { + "hide_name": 0, + "type": "$sdffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "SRST_POLARITY": 1, + "SRST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "SRST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 278, + 279, + 280, + 281 + ], + "EN": [ + 3 + ], + "SRST": [ + 4 + ] + } + }, + "flipflop_3": { + "hide_name": 0, + "type": "$adff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "ARST_POLARITY": 1, + "ARST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "ARST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 282, + 283, + 284, + 285 + ], + "ARST": [ + 4 + ] + } + }, + "flipflop_4": { + "hide_name": 0, + "type": "$adffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "ARST_POLARITY": 1, + "ARST_VALUE": "1001" + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "ARST": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 286, + 287, + 288, + 289 + ], + "EN": [ + 3 + ], + "ARST": [ + 4 + ] + } + }, + "flipflop_5": { + "hide_name": 0, + "type": "$aldff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "ALOAD_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "ALOAD": "input", + "AD": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 290, + 291, + 292, + 293 + ], + "ALOAD": [ + 4 + ], + "AD": [ + 47, + 48, + 49, + 50 + ] + } + }, + "flipflop_6": { + "hide_name": 0, + "type": "$aldffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1, + "ALOAD_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input", + "ALOAD": "input", + "AD": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 31, + 32, + 33, + 34 + ], + "Q": [ + 294, + 295, + 296, + 297 + ], + "EN": [ + 3 + ], + "ALOAD": [ + 4 + ], + "AD": [ + 47, + 48, + 49, + 50 + ] + } + }, + "flipflop_7_reset_mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "S": "input", + "Y": "output" + }, + "connections": { + "A": [ + 31, + 32, + 33, + 34 + ], + "B": [ + 47, + 48, + 49, + 50 + ], + "S": [ + 4 + ], + "Y": [ + 394, + 395, + 396, + 397 + ] + } + }, + "flipflop_7": { + "hide_name": 0, + "type": "$dff", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 394, + 395, + 396, + 397 + ], + "Q": [ + 298, + 299, + 300, + 301 + ] + } + }, + "flipflop_8_reset_mux": { + "hide_name": 0, + "type": "$mux", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "S": "input", + "Y": "output" + }, + "connections": { + "A": [ + 31, + 32, + 33, + 34 + ], + "B": [ + 47, + 48, + 49, + 50 + ], + "S": [ + 4 + ], + "Y": [ + 398, + 399, + 400, + 401 + ] + } + }, + "flipflop_8_reset_enable": { + "hide_name": 0, + "type": "$or", + "parameters": { + "A_WIDTH": 1, + "B_WIDTH": 1, + "Y_WIDTH": 1 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + }, + "connections": { + "A": [ + 3 + ], + "B": [ + 4 + ], + "Y": [ + 402 + ] + } + }, + "flipflop_8": { + "hide_name": 0, + "type": "$dffe", + "parameters": { + "WIDTH": 4, + "CLK_POLARITY": 1, + "EN_POLARITY": 1 + }, + "attributes": {}, + "port_directions": { + "CLK": "input", + "D": "input", + "Q": "output", + "EN": "input" + }, + "connections": { + "CLK": [ + 2 + ], + "D": [ + 398, + 399, + 400, + 401 + ], + "Q": [ + 302, + 303, + 304, + 305 + ], + "EN": [ + 402 + ] + } + }, + "tsb": { + "hide_name": 0, + "type": "$tribuf", + "parameters": { + "WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "EN": "input", + "Y": "output" + }, + "connections": { + "A": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "EN": [ + 6 + ], + "Y": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ] + } + }, + "passthrough_buf_0": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 7, + 8, + 9, + 10 + ], + "Y": [ + 407, + 408, + 409, + 410 + ] + } + }, + "passthrough_buf_1": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 4 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 11, + 12, + 13, + 14 + ], + "Y": [ + 411, + 412, + 413, + 414 + ] + } + }, + "passthrough_buf_2": { + "hide_name": 0, + "type": "$buf", + "parameters": { + "WIDTH": 8 + }, + "attributes": {}, + "port_directions": { + "A": "input", + "Y": "output" + }, + "connections": { + "A": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "Y": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ] + } + }, + "const_0_8_haa": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'haa": "output" + }, + "connections": { + "8'haa": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ] + } + }, + "const_1_8_h55": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'h55": "output" + }, + "connections": { + "8'h55": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ] + } + }, + "const_2_8_hf": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "8'hf": "output" + }, + "connections": { + "8'hf": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ] + } + }, + "const_3_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 338, + 339, + 340, + 341 + ] + } + }, + "const_4_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 342, + 343, + 344, + 345 + ] + } + }, + "const_5_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 346, + 347, + 348, + 349 + ] + } + }, + "const_6_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 350, + 351, + 352, + 353 + ] + } + }, + "const_7_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 354, + 355, + 356, + 357 + ] + } + }, + "const_8_4_h3": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h3": "output" + }, + "connections": { + "4'h3": [ + 358, + 359, + 360, + 361 + ] + } + }, + "const_9_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 362, + 363, + 364, + 365 + ] + } + }, + "const_10_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 366, + 367, + 368, + 369 + ] + } + }, + "const_11_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 370, + 371, + 372, + 373 + ] + } + }, + "const_12_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 374, + 375, + 376, + 377 + ] + } + }, + "const_13_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 378, + 379, + 380, + 381 + ] + } + }, + "const_14_4_h5": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h5": "output" + }, + "connections": { + "4'h5": [ + 382, + 383, + 384, + 385 + ] + } + }, + "const_15_4_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "4'h2": "output" + }, + "connections": { + "4'h2": [ + 403, + 404, + 405, + 406 + ] + } + }, + "const_16_2_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "2'h2": "output" + }, + "connections": { + "2'h2": [ + 390, + 391 + ] + } + }, + "const_17_2_h2": { + "hide_name": 0, + "type": "$const", + "parameters": {}, + "attributes": {}, + "port_directions": { + "2'h2": "output" + }, + "connections": { + "2'h2": [ + 392, + 393 + ] + } + } + }, + "netnames": { + "clk": { + "bits": [ + 2 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "en": { + "bits": [ + 3 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reset": { + "bits": [ + 4 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "muxSel": { + "bits": [ + 5 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "enableTri": { + "bits": [ + 6 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "a4": { + "bits": [ + 7, + 8, + 9, + 10 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "b4": { + "bits": [ + 11, + 12, + 13, + 14 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "a8": { + "bits": [ + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "b8": { + "bits": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "d4": { + "bits": [ + 31, + 32, + 33, + 34 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "shamt4": { + "bits": [ + 35, + 36, + 37, + 38 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "idx3": { + "bits": [ + 39, + 40, + 41 + ], + "logic_type": { + "width": 3 + }, + "attributes": {} + }, + "idx5": { + "bits": [ + 42, + 43, + 44, + 45, + 46 + ], + "logic_type": { + "width": 5 + }, + "attributes": {} + }, + "resetValueDyn4": { + "bits": [ + 47, + 48, + 49, + 50 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "not_out": { + "bits": [ + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "and_ll_out": { + "bits": [ + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "and_lc_out": { + "bits": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "or_ll_out": { + "bits": [ + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "or_lc_out": { + "bits": [ + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "xor_ll_out": { + "bits": [ + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "xor_lc_out": { + "bits": [ + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "reduce_and_out": { + "bits": [ + 107 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reduce_or_out": { + "bits": [ + 108 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "reduce_xor_out": { + "bits": [ + 109 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "add_ll_sum": { + "bits": [ + 110, + 111, + 112, + 113 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "add_ll_carry": { + "bits": [ + 114 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "add_lc_sum": { + "bits": [ + 115, + 116, + 117, + 118 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "add_lc_carry": { + "bits": [ + 119 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "sub_ll_out": { + "bits": [ + 120, + 121, + 122, + 123 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "sub_lc_out": { + "bits": [ + 124, + 125, + 126, + 127 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mul_ll_out": { + "bits": [ + 128, + 129, + 130, + 131 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mul_lc_out": { + "bits": [ + 132, + 133, + 134, + 135 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "div_ll_out": { + "bits": [ + 136, + 137, + 138, + 139 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "div_lc_out": { + "bits": [ + 140, + 141, + 142, + 143 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mod_ll_out": { + "bits": [ + 144, + 145, + 146, + 147 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mod_lc_out": { + "bits": [ + 148, + 149, + 150, + 151 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "pow_ll_out": { + "bits": [ + 152, + 153, + 154, + 155 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "pow_lc_out": { + "bits": [ + 156, + 157, + 158, + 159 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "eq_ll_out": { + "bits": [ + 160 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "eq_lc_out": { + "bits": [ + 161 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "neq_ll_out": { + "bits": [ + 162 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "neq_lc_out": { + "bits": [ + 163 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lt_ll_out": { + "bits": [ + 164 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lt_lc_out": { + "bits": [ + 165 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "gt_ll_out": { + "bits": [ + 166 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "gt_lc_out": { + "bits": [ + 167 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "le_ll_out": { + "bits": [ + 168 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "le_lc_out": { + "bits": [ + 169 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "ge_ll_out": { + "bits": [ + 170 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "ge_lc_out": { + "bits": [ + 171 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "lshift_ll_out": { + "bits": [ + 172, + 173, + 174, + 175 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "lshift_lc_out": { + "bits": [ + 176, + 177, + 178, + 179 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "rshift_ll_out": { + "bits": [ + 180, + 181, + 182, + 183 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "rshift_lc_out": { + "bits": [ + 184, + 185, + 186, + 187 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "arshift_ll_out": { + "bits": [ + 188, + 189, + 190, + 191 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "arshift_lc_out": { + "bits": [ + 192, + 193, + 194, + 195 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_class_out": { + "bits": [ + 196, + 197, + 198, + 199 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_dynamic_out": { + "bits": [ + 200, + 201, + 202, + 203 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_const1_out": { + "bits": [ + 407, + 408, + 409, + 410 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "mux_fn_const0_out": { + "bits": [ + 411, + 412, + 413, + 414 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "index_natural_out": { + "bits": [ + 212 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "index_oversized_out": { + "bits": [ + 213 + ], + "logic_type": { + "width": 1 + }, + "attributes": {} + }, + "replicate_x3_out": { + "bits": [ + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224, + 225 + ], + "logic_type": { + "width": 12 + }, + "attributes": {} + }, + "replicate_x5_out": { + "bits": [ + 226, + 227, + 228, + 229, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240, + 241, + 242, + 243, + 244, + 245 + ], + "logic_type": { + "width": 20 + }, + "attributes": {} + }, + "slice_out": { + "bits": [ + 246, + 247, + 248, + 249 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "swizzle_out": { + "bits": [ + 250, + 251, + 252, + 253, + 254, + 255, + 256, + 257 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "tribuf_readback_out": { + "bits": [ + 415, + 416, + 417, + 418, + 419, + 420, + 421, + 422 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "q_dff": { + "bits": [ + 266, + 267, + 268, + 269 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dffe": { + "bits": [ + 270, + 271, + 272, + 273 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_sdff": { + "bits": [ + 274, + 275, + 276, + 277 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_sdffe": { + "bits": [ + 278, + 279, + 280, + 281 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_adff": { + "bits": [ + 282, + 283, + 284, + 285 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_adffe": { + "bits": [ + 286, + 287, + 288, + 289 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_aldff": { + "bits": [ + 290, + 291, + 292, + 293 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_aldffe": { + "bits": [ + 294, + 295, + 296, + 297 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dynsync_noen": { + "bits": [ + 298, + 299, + 300, + 301 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "q_dynsync_en": { + "bits": [ + 302, + 303, + 304, + 305 + ], + "logic_type": { + "width": 4 + }, + "attributes": {} + }, + "bus": { + "bits": [ + 306, + 307, + 308, + 309, + 310, + 311, + 312, + 313 + ], + "logic_type": { + "width": 8 + }, + "attributes": {} + }, + "const_0_8_haa": { + "bits": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321 + ], + "attributes": { + "computed": 1 + } + }, + "const_1_8_h55": { + "bits": [ + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329 + ], + "attributes": { + "computed": 1 + } + }, + "const_2_8_hf": { + "bits": [ + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337 + ], + "attributes": { + "computed": 1 + } + }, + "const_3_4_h5": { + "bits": [ + 338, + 339, + 340, + 341 + ], + "attributes": { + "computed": 1 + } + }, + "const_4_4_h3": { + "bits": [ + 342, + 343, + 344, + 345 + ], + "attributes": { + "computed": 1 + } + }, + "const_5_4_h3": { + "bits": [ + 346, + 347, + 348, + 349 + ], + "attributes": { + "computed": 1 + } + }, + "const_6_4_h3": { + "bits": [ + 350, + 351, + 352, + 353 + ], + "attributes": { + "computed": 1 + } + }, + "const_7_4_h3": { + "bits": [ + 354, + 355, + 356, + 357 + ], + "attributes": { + "computed": 1 + } + }, + "const_8_4_h3": { + "bits": [ + 358, + 359, + 360, + 361 + ], + "attributes": { + "computed": 1 + } + }, + "const_9_4_h5": { + "bits": [ + 362, + 363, + 364, + 365 + ], + "attributes": { + "computed": 1 + } + }, + "const_10_4_h5": { + "bits": [ + 366, + 367, + 368, + 369 + ], + "attributes": { + "computed": 1 + } + }, + "const_11_4_h5": { + "bits": [ + 370, + 371, + 372, + 373 + ], + "attributes": { + "computed": 1 + } + }, + "const_12_4_h5": { + "bits": [ + 374, + 375, + 376, + 377 + ], + "attributes": { + "computed": 1 + } + }, + "const_13_4_h5": { + "bits": [ + 378, + 379, + 380, + 381 + ], + "attributes": { + "computed": 1 + } + }, + "const_14_4_h5": { + "bits": [ + 382, + 383, + 384, + 385 + ], + "attributes": { + "computed": 1 + } + }, + "const_15_4_h2": { + "bits": [ + 403, + 404, + 405, + 406 + ], + "attributes": { + "computed": 1 + } + }, + "const_16_2_h2": { + "bits": [ + 390, + 391 + ], + "attributes": { + "computed": 1 + } + }, + "const_17_2_h2": { + "bits": [ + 392, + 393 + ], + "attributes": { + "computed": 1 + } + }, + "flipflop_7_reset_mux_Y": { + "bits": [ + 394, + 395, + 396, + 397 + ], + "hide_name": 1, + "attributes": {} + }, + "flipflop_8_reset_mux_Y": { + "bits": [ + 398, + 399, + 400, + 401 + ], + "hide_name": 1, + "attributes": {} + }, + "flipflop_8_reset_enable_Y": { + "bits": [ + 402 + ], + "hide_name": 1, + "attributes": {} + } + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/gate_catalog_module.dart b/test/fixtures/gate_catalog_module.dart new file mode 100644 index 000000000..ccc6fe95e --- /dev/null +++ b/test/fixtures/gate_catalog_module.dart @@ -0,0 +1,207 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gate_catalog_module.dart +// A single ROHD module that instantiates every public gate API in +// `lib/src/modules/gates.dart` (plus a few closely related primitives: +// FlipFlop variants, TriStateBuffer, BusSubset, and Swizzle) so that the +// netlist synthesizer's cell-mapper coverage can be captured in one +// deterministic, checked-in JSON asset. +// +// Every instantiated gate's output (or, for multi-output gates, every +// output) is wired directly to a uniquely named top-level output port. This +// guarantees dead-cell elimination cannot prune any of the cells this file +// is meant to exercise. +// +// See `test/gate_catalog_test.dart` for the test that verifies the checked-in +// `test/fixtures/gate_catalog.rohd.json` asset still matches what this module +// produces, and `tool/generate_gate_catalog.dart` for the script that +// (re)generates that asset. +// +// 2026 August 20 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// A gate-catalog top-level module. +/// +/// Instantiates one (or a small number of representative variants) of every +/// gate [Module] and top-level gate-building function exposed by +/// `lib/src/modules/gates.dart`, along with [FlipFlop] (in all mapper- +/// supported configurations), [TriStateBuffer], [BusSubset], and [Swizzle]. +/// +/// All inputs are plain free (unconnected) top-level signals; this module is +/// intended purely for structural (netlist) synthesis, not simulation. +class GateCatalog extends Module { + /// Creates the gate catalog module. + /// + /// All inputs are supplied by the caller so that construction is fully + /// deterministic and repeatable byte-for-byte across runs. + GateCatalog({ + required Logic clk, + required Logic en, + required Logic reset, + required Logic muxSel, + required Logic enableTri, + required Logic a4, + required Logic b4, + required Logic a8, + required Logic b8, + required Logic d4, + required Logic shamt4, + required Logic idx3, + required Logic idx5, + required Logic resetValueDyn4, + required LogicNet busNet, + }) : super(name: 'gate_catalog', definitionName: 'GateCatalog') { + clk = addInput('clk', clk); + en = addInput('en', en); + reset = addInput('reset', reset); + muxSel = addInput('muxSel', muxSel); + enableTri = addInput('enableTri', enableTri); + a4 = addInput('a4', a4, width: 4); + b4 = addInput('b4', b4, width: 4); + a8 = addInput('a8', a8, width: 8); + b8 = addInput('b8', b8, width: 8); + d4 = addInput('d4', d4, width: 4); + shamt4 = addInput('shamt4', shamt4, width: 4); + idx3 = addInput('idx3', idx3, width: 3); + idx5 = addInput('idx5', idx5, width: 5); + resetValueDyn4 = addInput('resetValueDyn4', resetValueDyn4, width: 4); + final bus = addInOut('bus', busNet, width: 8); + + // ── NotGate → $not ─────────────────────────────────────────────── + addOutput('not_out', width: 8) <= ~a8; + + // ── And2Gate → $and (logic/logic and logic/const variants) ─────── + addOutput('and_ll_out', width: 8) <= a8 & b8; + addOutput('and_lc_out', width: 8) <= + And2Gate(a8, Const(0xaa, width: 8)).out; + + // ── Or2Gate → $or (logic/logic and logic/const variants) ───────── + addOutput('or_ll_out', width: 8) <= a8 | b8; + addOutput('or_lc_out', width: 8) <= Or2Gate(a8, Const(0x55, width: 8)).out; + + // ── Xor2Gate → $xor (logic/logic and logic/const variants) ─────── + addOutput('xor_ll_out', width: 8) <= a8 ^ b8; + addOutput('xor_lc_out', width: 8) <= + Xor2Gate(a8, Const(0x0f, width: 8)).out; + + // ── Unary reductions → $reduce_and / $reduce_or / $reduce_xor ───── + addOutput('reduce_and_out') <= a8.and(); + addOutput('reduce_or_out') <= a8.or(); + addOutput('reduce_xor_out') <= a8.xor(); + + // ── Add → $add (logic/logic and logic/const variants) ──────────── + final addLl = Add(a4, b4); + addOutput('add_ll_sum', width: 4) <= addLl.sum; + addOutput('add_ll_carry') <= addLl.carry; + final addLc = Add(a4, 5); + addOutput('add_lc_sum', width: 4) <= addLc.sum; + addOutput('add_lc_carry') <= addLc.carry; + + // ── Subtract → $sub (logic/logic and logic/const variants) ─────── + addOutput('sub_ll_out', width: 4) <= a4 - b4; + addOutput('sub_lc_out', width: 4) <= a4 - 3; + + // ── Multiply → $mul (logic/logic and logic/const variants) ─────── + addOutput('mul_ll_out', width: 4) <= a4 * b4; + addOutput('mul_lc_out', width: 4) <= a4 * 3; + + // ── Divide → $div (logic/logic and logic/const variants) ───────── + addOutput('div_ll_out', width: 4) <= a4 / b4; + addOutput('div_lc_out', width: 4) <= a4 / 3; + + // ── Modulo → $mod (logic/logic and logic/const variants) ───────── + addOutput('mod_ll_out', width: 4) <= a4 % b4; + addOutput('mod_lc_out', width: 4) <= a4 % 3; + + // ── Power → $pow (logic/logic and logic/const variants) ────────── + addOutput('pow_ll_out', width: 4) <= a4.pow(b4); + addOutput('pow_lc_out', width: 4) <= a4.pow(3); + + // ── Comparisons → $eq/$ne/$lt/$gt/$le/$ge ───────────────────────── + addOutput('eq_ll_out') <= a4.eq(b4); + addOutput('eq_lc_out') <= a4.eq(5); + addOutput('neq_ll_out') <= a4.neq(b4); + addOutput('neq_lc_out') <= a4.neq(5); + addOutput('lt_ll_out') <= a4.lt(b4); + addOutput('lt_lc_out') <= a4.lt(5); + addOutput('gt_ll_out') <= (a4 > b4); + addOutput('gt_lc_out') <= (a4 > 5); + addOutput('le_ll_out') <= a4.lte(b4); + addOutput('le_lc_out') <= a4.lte(5); + addOutput('ge_ll_out') <= (a4 >= b4); + addOutput('ge_lc_out') <= (a4 >= 5); + + // ── Shifts → $shl / $shr / $sshr (dynamic and constant amounts) ── + addOutput('lshift_ll_out', width: 4) <= LShift(a4, shamt4).out; + addOutput('lshift_lc_out', width: 4) <= LShift(a4, 2).out; + addOutput('rshift_ll_out', width: 4) <= RShift(a4, shamt4).out; + addOutput('rshift_lc_out', width: 4) <= RShift(a4, 2).out; + addOutput('arshift_ll_out', width: 4) <= ARShift(a4, shamt4).out; + addOutput('arshift_lc_out', width: 4) <= ARShift(a4, 2).out; + + // ── Mux / mux() ──────────────────────────────────────────────── + // + // Dynamic control ⇒ a real `$mux` cell is instantiated. + addOutput('mux_class_out', width: 4) <= Mux(muxSel, a4, b4).out; + addOutput('mux_fn_dynamic_out', width: 4) <= mux(muxSel, a4, b4); + + // Constant, valid control ⇒ `mux()` folds to the selected input + // directly at *build* time: no `$mux` cell is instantiated for these two + // outputs at all. This documents/validates the function-level constant + // fold described by `mux()`'s doc comment. These outputs are wired + // directly to `a4`/`b4` (via a `$buf`-shaped netlist alias, if any) with + // no arithmetic/select cell in between. + addOutput('mux_fn_const1_out', width: 4) <= mux(Const(1, width: 1), a4, b4); + addOutput('mux_fn_const0_out', width: 4) <= mux(Const(0, width: 1), a4, b4); + + // ── IndexGate → $shiftx (natural and oversized index widths) ───── + addOutput('index_natural_out') <= a8[idx3]; + addOutput('index_oversized_out') <= a8[idx5]; + + // ── ReplicationOp (retained as an explicit, unmapped cell; no + // standard Yosys `$concat`/`$pos`-style cell models replication of a + // single dynamic operand cleanly, so it is intentionally left visible + // as its own `ReplicationOp`-typed cell rather than force-mapped) ──── + addOutput('replicate_x3_out', width: 12) <= a4.replicate(3); + addOutput('replicate_x5_out', width: 20) <= a4.replicate(5); + + // ── BusSubset → $slice ──────────────────────────────────────────── + addOutput('slice_out', width: 4) <= a8.getRange(2, 6); + + // ── Swizzle → $concat ───────────────────────────────────────────── + addOutput('swizzle_out', width: 8) <= [a4, b4].swizzle(); + + // ── TriStateBuffer → $tribuf ─────────────────────────────────────── + TriStateBuffer(a8, enable: enableTri, name: 'tsb').out.gets(bus); + addOutput('tribuf_readback_out', width: 8) <= bus; + + // ── FlipFlop variants → $dff/$dffe/$sdff/$sdffe/$adff/$adffe/ + // $aldff/$aldffe, plus dynamic-synchronous-reset lowering ──────── + addOutput('q_dff', width: 4) <= flop(clk, d4); + addOutput('q_dffe', width: 4) <= flop(clk, d4, en: en); + addOutput('q_sdff', width: 4) <= flop(clk, d4, reset: reset, resetValue: 9); + addOutput('q_sdffe', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: 9); + addOutput('q_adff', width: 4) <= + flop(clk, d4, reset: reset, resetValue: 9, asyncReset: true); + addOutput('q_adffe', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: 9, asyncReset: true); + addOutput('q_aldff', width: 4) <= + flop(clk, d4, + reset: reset, resetValue: resetValueDyn4, asyncReset: true); + addOutput('q_aldffe', width: 4) <= + flop(clk, d4, + en: en, reset: reset, resetValue: resetValueDyn4, asyncReset: true); + // Dynamic synchronous reset value: `$sdff`/`$sdffe` require a *constant* + // reset value, so the netlist translator lowers these to a `$mux` + // (selecting the reset value) feeding a plain `$dff`/`$dffe` (the enable + // ORed with reset so reset retains priority when enabled). + addOutput('q_dynsync_noen', width: 4) <= + flop(clk, d4, reset: reset, resetValue: resetValueDyn4); + addOutput('q_dynsync_en', width: 4) <= + flop(clk, d4, en: en, reset: reset, resetValue: resetValueDyn4); + } +} diff --git a/test/flop_test.dart b/test/flop_test.dart index 4e3def505..1372877e7 100644 --- a/test/flop_test.dart +++ b/test/flop_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // flop_test.dart diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..d4261ccdb 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2024 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // fsm_test.dart @@ -183,7 +183,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains("b = 1'h0;")); }); @@ -192,7 +192,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('priority case')); }); @@ -201,7 +201,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('MyStates_state1 : begin')); }); diff --git a/test/fst_writer_test.dart b/test/fst_writer_test.dart new file mode 100644 index 000000000..3f16a53a9 --- /dev/null +++ b/test/fst_writer_test.dart @@ -0,0 +1,428 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_writer_test.dart +// Tests for FST writer and WaveformService FST format support. +// +// 2026 February +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import 'pipeline_test.dart' show SimplePipelineModule; + +/// A simple module for testing. +class _SimpleModule extends Module { + _SimpleModule(Logic a) { + a = addInput('a', a); + addOutput('b') <= a; + } +} + +/// A module with multi-bit signals for testing. +class _MultiBitModule extends Module { + _MultiBitModule(Logic a, Logic clk) { + a = addInput('a', a, width: a.width); + final aClk = addInput('clk', clk); + addOutput('q', width: a.width) <= FlipFlop(aClk, a).q; + } +} + +const _tempDumpDir = 'tmp_test'; + +/// Gets the path of the FST file based on a name. +String _temporaryFstPath(String name) => '$_tempDumpDir/temp_dump_$name.fst'; + +/// Attaches a [WaveformService] to [module] with FST format. +void _createFstDump(Module module, String name) { + Directory(_tempDumpDir).createSync(recursive: true); + final tmpDumpFile = _temporaryFstPath(name); + WaveformService.fromOutputPath( + module, + outputPath: tmpDumpFile, + format: WaveOutputFormat.fst, + ); +} + +/// Deletes the temporary FST file associated with [name]. +void _deleteFstDump(String name) { + final tmpDumpFile = _temporaryFstPath(name); + if (File(tmpDumpFile).existsSync()) { + File(tmpDumpFile).deleteSync(); + } +} + +/// Reads a big-endian u64 from [data] at [offset]. +int _readU64(Uint8List data, int offset) { + var result = 0; + for (var i = 0; i < 8; i++) { + result = (result << 8) | data[offset + i]; + } + return result; +} + +/// Parses FST file blocks and returns a map of block types to counts. +Map _parseFstBlocks(Uint8List data) { + final blocks = {}; + var pos = 0; + while (pos < data.length) { + final blockType = data[pos]; + pos++; + if (pos + 8 > data.length) { + break; + } + final sectionLength = _readU64(data, pos); + blocks[blockType] = (blocks[blockType] ?? 0) + 1; + pos += sectionLength; + if (sectionLength == 0) { + break; + } + } + return blocks; +} + +/// Parses FST header and returns key fields. +Map _parseFstHeader(Uint8List data) { + // Skip block type byte (0) + if (data[0] != 0) { + throw FormatException('Expected header block type 0, got ${data[0]}'); + } + final sectionLength = _readU64(data, 1); + if (sectionLength != 329) { + throw FormatException( + 'Expected header section length 329, got $sectionLength'); + } + return { + 'start_time': _readU64(data, 9), + 'end_time': _readU64(data, 17), + // skip double_endian_test (8 bytes at offset 25) + 'scope_count': _readU64(data, 41), + 'var_count': _readU64(data, 49), + 'max_var_id': _readU64(data, 57), + 'vc_section_count': _readU64(data, 65), + 'timescale_exponent': data[73], // offset 73 = 1 + 8*9 + }; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('FstWriter unit tests', () { + test('writes valid header block', () { + const path = '$_tempDumpDir/fst_header_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + FstWriter(path) + ..pushScope('top') + ..declareSignal('clk', 1) + ..declareSignal('data', 8) + ..popScope() + ..finish(); + + final data = File(path).readAsBytesSync(); + expect(data[0], equals(0), reason: 'First byte should be header type'); + final sectionLength = _readU64(data, 1); + expect(sectionLength, equals(329), reason: 'Header is 329 bytes'); + + // Parse header fields + final header = _parseFstHeader(data); + expect(header['scope_count'], equals(1)); + expect(header['var_count'], equals(2)); + expect(header['max_var_id'], equals(2)); + + File(path).deleteSync(); + }); + + test('writes all required block types', () { + const path = '$_tempDumpDir/fst_blocks_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + final writer = FstWriter(path)..pushScope('top'); + final clk = writer.declareSignal('clk', 1); + writer + ..popScope() + ..writeHeader() + ..emitValueChange(0, clk, '0') + ..emitValueChange(5, clk, '1') + ..finish(); + + final data = File(path).readAsBytesSync(); + final blocks = _parseFstBlocks(data); + + // Must have: Header(0), VcDataDynamicAlias2(8), Geometry(3), + // Hierarchy(4) + expect(blocks.containsKey(0), isTrue, reason: 'Must have header'); + expect(blocks.containsKey(8), isTrue, reason: 'Must have VcData block'); + expect(blocks.containsKey(3), isTrue, reason: 'Must have geometry'); + expect(blocks.containsKey(4), isTrue, reason: 'Must have hierarchy'); + + File(path).deleteSync(); + }); + + test('geometry encodes signal widths correctly', () { + const path = '$_tempDumpDir/fst_geometry_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + FstWriter(path) + ..pushScope('top') + ..declareSignal('bit1', 1) + ..declareSignal('byte8', 8) + ..declareSignal('word32', 32) + ..popScope() + ..finish(); + + final data = File(path).readAsBytesSync(); + + // Find the geometry block (type 3) + var pos = 0; + while (pos < data.length) { + if (data[pos] == 3) { + // Geometry block + final sectionLength = _readU64(data, pos + 1); + final maxHandle = _readU64(data, pos + 1 + 16); + expect(maxHandle, equals(3)); + + // Geometry data is after section_length(8) + unc_len(8) + + // max_handle(8) = 24 bytes from section_length start + // May be compressed, so just check the block exists + expect(sectionLength, greaterThan(24)); + break; + } + pos++; + if (pos + 8 > data.length) { + break; + } + final sl = _readU64(data, pos); + pos += sl; + if (sl == 0) { + break; + } + } + + File(path).deleteSync(); + }); + }); + + group('WaveformService FST format', () { + test('basic 1-bit signal FST dump', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dumpName = 'fstBasic'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(100); + await Simulator.run(); + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + final data = fstFile.readAsBytesSync(); + // File should have valid FST header + expect(data[0], equals(0), reason: 'First byte is header block type'); + expect(_readU64(data, 1), equals(329)); + + // Check blocks are present + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue, reason: 'header'); + expect(blocks.containsKey(3), isTrue, reason: 'geometry'); + expect(blocks.containsKey(4), isTrue, reason: 'hierarchy'); + + _deleteFstDump(dumpName); + }); + + test('multi-bit signal FST dump', () async { + final a = Logic(name: 'a', width: 8); + final clk = SimpleClockGenerator(10).clk; + final mod = _MultiBitModule(a, clk); + await mod.build(); + + const dumpName = 'fstMultiBit'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(100); + unawaited(Simulator.run()); + + await clk.nextPosedge; + a.inject(0xAB); + await clk.nextPosedge; + a.inject(0xFF); + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + final data = fstFile.readAsBytesSync(); + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue); + expect(blocks.containsKey(8), isTrue, + reason: 'VcData block with changes'); + + _deleteFstDump(dumpName); + }); + + test('FST file creates non-existent directories', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dir1Path = '$_tempDumpDir/fst_dir1'; + const fstPath = '$dir1Path/dir2/waves.fst'; + + WaveformService.fromOutputPath( + mod, + outputPath: fstPath, + format: WaveOutputFormat.fst, + ); + + a.put(0); + Simulator.setMaxSimTime(10); + await Simulator.run(); + + expect(File(fstPath).existsSync(), isTrue); + + if (Directory(dir1Path).existsSync()) { + Directory(dir1Path).deleteSync(recursive: true); + } + }); + + test('FST header has correct signal counts', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dumpName = 'fstCounts'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(10); + await Simulator.run(); + + final data = File(_temporaryFstPath(dumpName)).readAsBytesSync(); + final header = _parseFstHeader(data); + + // _SimpleModule has 2 signals: input 'a' and output 'b' + expect(header['var_count'], equals(2)); + + _deleteFstDump(dumpName); + }); + + test('FST and VCD both produce output', () async { + // Create a module + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + // Dump as FST + const fstName = 'fstCompare'; + _createFstDump(mod, fstName); + + a.put(0); + Simulator.setMaxSimTime(50); + unawaited(Simulator.run()); + + a.inject(1); + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(fstName)); + expect(fstFile.existsSync(), isTrue); + final fstSize = fstFile.lengthSync(); + expect(fstSize, greaterThan(330), reason: 'FST should be > header size'); + + _deleteFstDump(fstName); + + // Reset and dump as VCD + await Simulator.reset(); + + final a2 = Logic(name: 'a'); + final mod2 = _SimpleModule(a2); + await mod2.build(); + + const vcdPath = '$_tempDumpDir/temp_dump_vcdCompare.vcd'; + Directory(_tempDumpDir).createSync(recursive: true); + WaveformService.fromOutputPath(mod2, outputPath: vcdPath); + + a2.put(0); + Simulator.setMaxSimTime(50); + unawaited(Simulator.run()); + + a2.inject(1); + + await Simulator.simulationEnded; + + final vcdFile = File(vcdPath); + expect(vcdFile.existsSync(), isTrue); + expect(vcdFile.lengthSync(), greaterThan(0)); + + vcdFile.deleteSync(); + }); + + test('pipeline FST has VcData and is readable by fst2vcd', () async { + // Build a 3-stage 8-bit pipeline that generates many signal changes. + final a = Logic(name: 'a', width: 8); + final mod = SimplePipelineModule(a); + await mod.build(); + + const dumpName = 'fstPipeline'; + _createFstDump(mod, dumpName); + + // Drive 200 clock cycles worth of incrementing inputs. + // The 10ps clock gives 2000ps total, producing many VcData changes. + a.put(0); + Simulator.setMaxSimTime(2000); + unawaited(Simulator.run()); + + // Inject a new value every 10ps to keep signals active + for (var i = 1; i <= 200; i++) { + await Future.delayed(Duration.zero); + a.inject(i & 0xFF); + } + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + // File should be substantially larger than just the header (329 bytes) + final fileSize = fstFile.lengthSync(); + expect(fileSize, greaterThan(600), + reason: 'Pipeline FST should have VcData content'); + + // Parse blocks: must include at least one VcData block (type 8) + final data = fstFile.readAsBytesSync(); + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue, reason: 'header block'); + expect(blocks.containsKey(8), isTrue, reason: 'VcData block'); + expect(blocks.containsKey(3), isTrue, reason: 'geometry block'); + expect(blocks.containsKey(4), isTrue, reason: 'hierarchy block'); + + // Validate with fst2vcd (GTKWave tool) if available. + final fst2vcd = Process.runSync('which', ['fst2vcd']); + if (fst2vcd.exitCode == 0) { + final result = Process.runSync('fst2vcd', [fstFile.path]); + expect(result.exitCode, equals(0), + reason: 'fst2vcd failed: ${result.stdout}\n${result.stderr}'); + final vcdOutput = result.stdout as String; + expect(vcdOutput, contains(r'$timescale'), + reason: 'fst2vcd output should be valid VCD'); + } + + _deleteFstDump(dumpName); + }); + }); +} diff --git a/test/gate_catalog_test.dart b/test/gate_catalog_test.dart new file mode 100644 index 000000000..965decb45 --- /dev/null +++ b/test/gate_catalog_test.dart @@ -0,0 +1,289 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gate_catalog_test.dart +// Verifies that the checked-in gate-catalog netlist asset +// (`test/fixtures/gate_catalog.rohd.json`) is byte-for-byte reproducible from +// `GateCatalog` (see `test/fixtures/gate_catalog_module.dart`), and spot +// checks coverage of every gate API this catalog is meant to exercise. +// +// If this test fails only because of an intentional change to gate lowering +// or the netlist cell mapper, regenerate the asset with: +// dart run tool/generate_gate_catalog.dart +// and review the diff before committing it. +// +// 2026 August 20 +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import 'fixtures/gate_catalog_module.dart'; + +/// Builds a fresh [GateCatalog] with deterministic, freshly-allocated input +/// signals. +GateCatalog _buildCatalog() => GateCatalog( + clk: Logic(name: 'clk'), + en: Logic(name: 'en'), + reset: Logic(name: 'reset'), + muxSel: Logic(name: 'muxSel'), + enableTri: Logic(name: 'enableTri'), + a4: Logic(name: 'a4', width: 4), + b4: Logic(name: 'b4', width: 4), + a8: Logic(name: 'a8', width: 8), + b8: Logic(name: 'b8', width: 8), + d4: Logic(name: 'd4', width: 4), + shamt4: Logic(name: 'shamt4', width: 4), + idx3: Logic(name: 'idx3', width: 3), + idx5: Logic(name: 'idx5', width: 5), + resetValueDyn4: Logic(name: 'resetValueDyn4', width: 4), + busNet: LogicNet(name: 'busNet', width: 8), + ); + +/// Synthesizes [GateCatalog] to combined netlist JSON using the default +/// [NetlistSynthesizerConfiguration] (the same defaults a typical consumer +/// would use). +Future _synthesizeCatalogJson() async { + final catalog = _buildCatalog(); + await catalog.build(); + final synth = NetlistSynthesizer(); + return synth.synthesizeToJson(catalog); +} + +/// Path (relative to the package root, where `dart test` runs) to the +/// checked-in fixture asset. +const _fixturePath = 'test/fixtures/gate_catalog.rohd.json'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('GateCatalog netlist JSON matches checked-in fixture byte-for-byte', + () async { + final generated = await _synthesizeCatalogJson(); + final fixtureFile = File(_fixturePath); + + expect(fixtureFile.existsSync(), isTrue, + reason: 'Missing fixture at $_fixturePath. Generate it with: ' + 'dart run tool/generate_gate_catalog.dart'); + + final checkedIn = fixtureFile.readAsStringSync(); + expect( + generated, + equals(checkedIn), + reason: 'Generated gate-catalog netlist JSON no longer matches the ' + 'checked-in fixture. If this change is intentional, regenerate ' + 'the asset with `dart run tool/generate_gate_catalog.dart` and ' + 'review/commit the diff.', + ); + }); + + test('GateCatalog netlist JSON is deterministic across repeated synthesis', + () async { + final first = await _synthesizeCatalogJson(); + await Simulator.reset(); + final second = await _synthesizeCatalogJson(); + expect(second, equals(first)); + }); + + group('gate catalog coverage', () { + late Map json; + late Map moduleDef; + late Map cells; + + setUpAll(() async { + final text = await _synthesizeCatalogJson(); + json = jsonDecode(text) as Map; + final modules = json['modules'] as Map; + moduleDef = modules['GateCatalog'] as Map; + cells = moduleDef['cells'] as Map; + }); + + List> cellsOfType(String type) => cells.values + .cast>() + .where((c) => c['type'] == type) + .toList(); + + test('every standard Yosys arithmetic/logic/compare/shift cell exists', () { + const expectedTypes = { + r'$not', + r'$and', + r'$or', + r'$xor', + r'$reduce_and', + r'$reduce_or', + r'$reduce_xor', + r'$add', + r'$sub', + r'$mul', + r'$div', + r'$mod', + r'$pow', + r'$eq', + r'$ne', + r'$lt', + r'$gt', + r'$le', + r'$ge', + r'$shl', + r'$shr', + r'$sshr', + r'$mux', + r'$shiftx', + r'$slice', + r'$concat', + r'$tribuf', + r'$dff', + r'$dffe', + r'$sdff', + r'$sdffe', + r'$adff', + r'$adffe', + r'$aldff', + r'$aldffe', + }; + for (final type in expectedTypes) { + expect(cellsOfType(type), isNotEmpty, reason: 'missing $type cell'); + } + }); + + test('Power/Divide/Modulo cells have full standard A/B/Y parameters', () { + for (final type in [r'$pow', r'$div', r'$mod']) { + final matches = cellsOfType(type); + expect(matches, isNotEmpty, reason: type); + for (final cell in matches) { + expect( + cell['parameters'], + equals({ + 'A_SIGNED': 0, + 'A_WIDTH': 4, + 'B_SIGNED': 0, + 'B_WIDTH': 4, + 'Y_WIDTH': 4, + }), + reason: type, + ); + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + reason: type, + ); + } + } + }); + + test(r'IndexGate cells map to $shiftx with full standard parameters', () { + final matches = cellsOfType(r'$shiftx'); + // One "natural" 3-bit index and one "oversized" 5-bit index. + expect(matches, hasLength(2)); + final bWidths = + matches.map((c) => (c['parameters'] as Map)['B_WIDTH']).toSet(); + expect(bWidths, equals({3, 5})); + for (final cell in matches) { + final params = cell['parameters'] as Map; + expect(params['A_SIGNED'], 0); + expect(params['B_SIGNED'], 0); + expect(params['A_WIDTH'], 8); + expect(params['Y_WIDTH'], 1); + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + ); + } + }); + + test('ReplicationOp is retained as an explicit, visible (unmapped) cell', + () { + final replicationCells = cellsOfType('ReplicationOp'); + expect(replicationCells, hasLength(2)); + // Confirm it is not force-mapped to any standard Yosys cell type + // (e.g. `$concat`): its cell `type` field is the raw ROHD + // `definitionName`, not a `$`-prefixed standard primitive. + for (final cell in replicationCells) { + expect(cell['type'], isNot(startsWith(r'$'))); + } + final outputWidths = replicationCells.map((c) { + final connections = + (c['connections'] as Map).values.cast>(); + return connections.map((l) => l.length).reduce((a, b) => a > b ? a : b); + }).toSet(); + expect(outputWidths, equals({12, 20})); + }); + + test(r'constant-control mux() folds away at build time (no extra $mux)', + () { + // Two dynamic-control mux instantiations (Mux class + mux() function) + // produce two explicit `$mux` cells; the two dynamic-synchronous-reset + // flip-flops (see below) each lower to an additional `$mux` (selecting + // the reset value) for four `$mux` cells total. The two + // constant-control mux() calls fold away entirely at build time and + // contribute no additional `$mux` cells. + expect(cellsOfType(r'$mux'), hasLength(4)); + + final ports = moduleDef['ports'] as Map; + final const1Bits = + (ports['mux_fn_const1_out'] as Map)['bits'] as List; + final const0Bits = + (ports['mux_fn_const0_out'] as Map)['bits'] as List; + final aBits = (ports['a4'] as Map)['bits'] as List; + final bBits = (ports['b4'] as Map)['bits'] as List; + + // Find the (non-$mux) driver of a port's bits: since `mux()` folded + // away, the only thing between the port and its source signal is a + // plain `$buf` passthrough (emitted whenever an output port aliases an + // input directly), never a `$mux`. + // + // Note: `List`'s `==` is identity-based, not element-wise, so bit + // lists must be compared with an explicit element-wise check. + bool sameBits(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + Map driverOf(List outputBits) => + cells.values.cast>().singleWhere((c) { + final y = (c['connections'] as Map)['Y']; + return y is List && sameBits(y, outputBits); + }); + + final const1Driver = driverOf(const1Bits); + final const0Driver = driverOf(const0Bits); + expect(const1Driver['type'], r'$buf'); + expect(const0Driver['type'], r'$buf'); + + // mux(Const(1), a4, b4) folds to a4; mux(Const(0), a4, b4) folds to b4. + expect((const1Driver['connections'] as Map)['A'], equals(aBits)); + expect((const0Driver['connections'] as Map)['A'], equals(bBits)); + }); + + test(r'dynamic synchronous reset flip-flops lower to $mux + $dff/$dffe', + () { + // 8 explicit register cells (dff/dffe/sdff/sdffe/adff/adffe/aldff/ + // aldffe) + 2 lowered dynamic-sync-reset flops (1 dff-shaped, 1 + // dffe-shaped) = 9 $dff-family cells total (dffe used twice: q_dffe + // and the lowered en-variant). + expect(cellsOfType(r'$dff'), hasLength(2)); // q_dff, q_dynsync_noen + expect(cellsOfType(r'$dffe'), hasLength(2)); // q_dffe, q_dynsync_en + expect(cellsOfType(r'$sdff'), hasLength(1)); + expect(cellsOfType(r'$sdffe'), hasLength(1)); + expect(cellsOfType(r'$adff'), hasLength(1)); + expect(cellsOfType(r'$adffe'), hasLength(1)); + expect(cellsOfType(r'$aldff'), hasLength(1)); + expect(cellsOfType(r'$aldffe'), hasLength(1)); + }); + }); +} diff --git a/test/gate_test.dart b/test/gate_test.dart index b2c47c21e..a86dcef73 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -396,7 +396,7 @@ void main() { expect(alias.value, LogicValue.zero); await module.build(); - expect(module.generateSynth(), contains("1'h0")); + expect(module.dumpSystemVerilog().output, contains("1'h0")); }); test('bitwise NOT folds Const inputs', () { @@ -753,7 +753,7 @@ void main() { useExplicitConstShiftModules: true, ); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = gtm.dumpSystemVerilog().output; expect(sv, isNot(contains("0'h0"))); diff --git a/test/inout_loopback_test.dart b/test/inout_loopback_test.dart index 0c3b5b343..92be82f11 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 = mod.dumpSystemVerilog().output; // 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 = mod.dumpSystemVerilog().output; 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 = mod.dumpSystemVerilog().output; // 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 = mod.dumpSystemVerilog().output; // 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 = mod.dumpSystemVerilog().output; // 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 c2cb73d0f..a885169b1 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -751,7 +751,7 @@ void main() { ]; if (checkNoSwizzle) { - expect(mod.generateSynth().contains('swizzle'), false, + expect(mod.dumpSystemVerilog().output.contains('swizzle'), false, reason: 'Expected no swizzles but found one.'); } @@ -800,7 +800,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv.contains(RegExp(r'\[7:0\]\s*laIn\s*\[2:0\]')), true); expect(sv.contains(RegExp(r'\[7:0\]\s*laOut\s*\[2:0\]')), true); }); @@ -818,7 +818,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect( sv.contains(RegExp( r'\[2:0\]\s*\[1:0\]\s*\[7:0\]\s*laIn\s*\[4:0\]\s*\[3:0\]')), @@ -846,7 +846,7 @@ void main() { await testArrayPassthrough(mod); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('input logic [2:0][1:0][2:0][7:0] laIn')); expect(sv, contains('output logic [2:0][1:0][2:0][7:0] laOut')); }); @@ -861,7 +861,7 @@ void main() { await testArrayPassthrough(mod, noSvSim: true); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('input logic [1:0][2:0][7:0] laIn [2:0]')); expect(sv, contains('output logic [1:0][2:0][7:0] laOut [2:0]')); }); @@ -928,7 +928,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv.contains('logic [2:0][3:0][7:0] intermediate [1:0]'), true); }); }); @@ -981,7 +981,7 @@ void main() { test('3d', () async { final mod = SimpleArraysAndHierarchy(LogicArray([2], 8)); await testArrayPassthrough(mod); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('SimpleLAPassthrough simple_la_passthrough')); }); @@ -992,7 +992,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - expect(mod.generateSynth(), contains('SimpleLAPassthrough')); + expect(mod.dumpSystemVerilog().output, contains('SimpleLAPassthrough')); }); }); @@ -1001,7 +1001,7 @@ void main() { final mod = FancyArraysAndHierarchy(LogicArray([4, 3, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // make sure the 4th one is there (since we expect 4) expect(sv, contains('SimpleLAPassthrough simple_la_passthrough_2')); @@ -1043,7 +1043,8 @@ void main() { final mod = WithSetArrayOffsetModule(LogicArray([2, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); // make sure we're reassigning both times it overlaps! expect( diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 1a5fb1d98..8e0bb89d1 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_name_config_test.dart @@ -30,7 +30,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; expect(sv, contains('intermediate')); }); @@ -45,7 +45,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; // no intermediate expect(sv.contains('intermediate'), isFalse); @@ -58,7 +58,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; // 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 = dut.dumpSystemVerilog().output; // 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 = dut.dumpSystemVerilog().output; // held one sticks expect(sv, contains('intermediate_1 = in1')); @@ -108,7 +108,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + dut.dumpSystemVerilog().output; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -124,7 +124,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + dut.dumpSystemVerilog().output; 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(); + dut.dumpSystemVerilog().output; 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 = dut.dumpSystemVerilog().output; expect(sv, contains('goodname')); }); @@ -220,7 +220,7 @@ void main() { out1 <= ~prev; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; 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 = dut.dumpSystemVerilog().output; expect(sv, contains('intermediate')); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 3447847c5..0d9d9a506 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -223,13 +223,13 @@ void main() { final mod = LogicWithInternalSignalModule(Logic()); await mod.build(); - expect(mod.generateSynth(), contains('shouldExist')); + expect(mod.dumpSystemVerilog().output, 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 = pMod.dumpSystemVerilog().output; expect(RegExp('logic a[,;\n]').allMatches(sv).length, 2); }); @@ -237,7 +237,7 @@ void main() { test('assigns and gates', () async { final mod = SensitiveNaming(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('e = a & d')); expect(sv, contains('b = a')); expect(sv, contains('d = c')); @@ -246,7 +246,7 @@ void main() { test('bus subset', () async { final mod = BusSubsetNaming(Logic(width: 32)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('c = b[3]')); }); }); @@ -255,7 +255,7 @@ void main() { test('unconnected floating', () async { final mod = DrivenOutputModule(null); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // shouldn't add a Z in there if left floating expect(!sv.contains('z'), true); @@ -264,7 +264,7 @@ void main() { test('driven to z', () async { final mod = DrivenOutputModule(Const('z')); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // should add a Z if it's explicitly added expect(sv, contains('z')); @@ -277,7 +277,7 @@ void main() { portANaming: Naming.renameable, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect( sv, @@ -293,7 +293,7 @@ void main() { () async { final mod = NameCollisionArrayTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect( sv, @@ -310,7 +310,7 @@ void main() { await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; expect(sv, contains('_wow_______')); }); @@ -319,7 +319,7 @@ void main() { final mod = StructElementNamingModule(VariousNamingStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign outp[0] = outp_renameable;')); expect(sv, contains('assign outp[1] = reserved_outp;')); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index a1de7e79a..520287a37 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -175,7 +175,7 @@ void main() { final mod = StructModuleWithInstrumentation(Const(0, width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv.contains('swizzle'), isFalse, reason: 'Should not pack from instrumentation!'); @@ -191,6 +191,19 @@ void main() { expect(s.name, 'structure'); }); + test('hasConsts detects constants at any depth', () { + final withoutConsts = LogicStructure([Logic()]); + final withDirectConst = LogicStructure([Logic(), Const(0)]); + final withNestedConst = LogicStructure([ + Logic(), + LogicStructure([Logic(), Const(1)]), + ]); + + expect(withoutConsts.hasConsts, isFalse); + expect(withDirectConst.hasConsts, isTrue); + expect(withNestedConst.hasConsts, isTrue); + }); + test('sub logic in two structures throws exception', () { final s = LogicStructure([ Logic(), diff --git a/test/logic_test.dart b/test/logic_test.dart index cd3169cab..21a8299a7 100644 --- a/test/logic_test.dart +++ b/test/logic_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_test.dart diff --git a/test/mac_unit_test.dart b/test/mac_unit_test.dart new file mode 100644 index 000000000..1dcda6026 --- /dev/null +++ b/test/mac_unit_test.dart @@ -0,0 +1,82 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// mac_unit_test.dart +// Tests for the filter-bank multiply-accumulate example. +// +// 2026 August 24 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank/mac_unit.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('disabled pipeline holds its result and intermediate stages', () async { + const dataWidth = 8; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final enable = Logic(); + final sample = Logic(width: dataWidth); + final coefficient = Logic(width: dataWidth); + final accumulator = Logic(width: dataWidth); + final dut = MacUnit( + sample, + coefficient, + accumulator, + clk, + reset, + enable, + dataWidth: dataWidth, + ); + await dut.build(); + + reset.inject(1); + enable.inject(0); + sample.inject(0); + coefficient.inject(0); + accumulator.inject(0); + Simulator.setMaxSimTime(200); + unawaited(Simulator.run()); + + await clk.nextPosedge; + reset.inject(0); + enable.inject(1); + sample.inject(3); + coefficient.inject(4); + accumulator.inject(5); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect(dut.result.value.toInt(), 17); + + enable.inject(0); + sample.inject(7); + coefficient.inject(8); + accumulator.inject(9); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect( + dut.result.value.toInt(), + 17, + reason: 'Both pipeline stages must hold while enable is low.', + ); + + enable.inject(1); + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextNegedge; + expect(dut.result.value.toInt(), 65); + + await Simulator.endSimulation(); + }); +} diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..111e1c4c3 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // 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 = mod.dumpSystemVerilog().output; 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 = gtm.dumpSystemVerilog().output; 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 2aa1eb2de..031168972 100644 --- a/test/module_merging_test.dart +++ b/test/module_merging_test.dart @@ -91,7 +91,7 @@ void main() { () async { final dut = TrunkWithLeaves(Logic(), Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; expect('module ComplicatedLeaf'.allMatches(sv).length, 1); }); @@ -99,7 +99,7 @@ void main() { test('different reserved definition name modules stay separate', () async { final dut = ParentOfDifferentModuleDefNames(Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; 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..3560c1fb0 --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,270 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Unit tests for ModuleServices, the service base types, and +// SystemVerilogService. +// +// 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(() { + SystemVerilogService.current = null; + 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('SystemVerilogService', () { + test('registers by default', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + + expect(SystemVerilogService.current, same(sv)); + expect( + ModuleServices.instance.lookup(), + same(sv), + ); + }); + + test('can opt out of registration', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod, register: false); + + expect(SystemVerilogService.current, isNull); + expect( + ModuleServices.instance.lookup(), + isNull, + ); + expect(sv.output, isNotEmpty); + }); + + test('is a CodeGenService', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(SystemVerilogService(mod), isA()); + }); + + test('allContents is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + expect(sv.allContents, isNotEmpty); + }); + + test('output is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + expect(sv.output, isNotEmpty); + }); + + test('artifact defaults to the module definition name', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + + final artifact = sv.artifacts.single; + + expect(artifact.fileName, equals('${mod.definitionName}.sv')); + expect(artifact.mediaType, equals('text/x-systemverilog')); + expect( + (await artifact.openRead().expand((bytes) => bytes).toList()) + .isNotEmpty, + isTrue, + ); + }); + + test('instanceTypeOutput returns the instance type contents', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + + final contents = sv.fileContents.single; + expect(sv.instanceTypeOutput(contents.name), equals(contents.contents)); + expect(sv.instanceTypeOutput('DoesNotExist'), isNull); + }); + + test('toJson lists generated modules', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + expect(sv.toJson()['modules'], isList); + }); + + test('writeOutputs creates SV files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + SystemVerilogService( + mod, + outputDirectory: dir.path, + multiFile: true, + ).writeOutputs(); + final files = dir.listSync().whereType().toList(); + expect(files, isNotEmpty); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('writeOutputs emits a single file', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + final configuredSv = SystemVerilogService( + mod, + outputDirectory: dir.path, + outputBaseName: 'out', + )..writeOutputs(); + final path = '${dir.path}/out.sv'; + expect(File(path).readAsStringSync(), equals(configuredSv.output)); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('multiFile writeOutputs emits a directory of files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + SystemVerilogService( + mod, + outputDirectory: dir.path, + multiFile: true, + ).writeOutputs(); + final files = dir.listSync().whereType().toList(); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('defaults headers by output layout', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final singleFile = SystemVerilogService(mod); + final multiFile = SystemVerilogService(mod, multiFile: true); + + expect(singleFile.includeHeader, isTrue); + expect(singleFile.output, startsWith(singleFile.header)); + expect(multiFile.includeHeader, isFalse); + expect(multiFile.header, isEmpty); + }); + + test('writes headers in either output layout when requested', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + final singlePath = '${dir.path}/single.sv'; + final singleFile = SystemVerilogService( + mod, + outputDirectory: dir.path, + outputBaseName: 'single', + includeHeader: false, + )..writeOutputs(); + expect( + File(singlePath).readAsStringSync(), + equals(singleFile.allContents), + ); + + final multiFile = SystemVerilogService( + mod, + outputDirectory: dir.path, + multiFile: true, + includeHeader: true, + )..writeOutputs(); + final output = + File('${dir.path}/${multiFile.fileContents.single.name}.sv') + .readAsStringSync(); + expect(output, startsWith(multiFile.header)); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + expect(() => SystemVerilogService(mod), throwsException); + }); + }); +} diff --git a/test/module_test.dart b/test/module_test.dart index c42532445..b920b5120 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -7,6 +7,8 @@ // 2023 September 11 // Author: Max Korbel +import 'dart:io'; + import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/sv_cleaner.dart'; @@ -231,6 +233,40 @@ class MissingInputRegistrationTopModule extends Module { } void main() { + group('output convenience methods', () { + test('dumpSystemVerilog generates in-memory output', () async { + final mod = FlexibleModule(); + await mod.build(); + + final service = mod.dumpSystemVerilog(); + + expect(service, isA()); + expect(service.output, isNotEmpty); + }); + + test('dumpSystemVerilog preserves an arbitrary legacy output filename', + () async { + final mod = FlexibleModule(); + await mod.build(); + final directory = Directory.systemTemp.createTempSync('sv_test_'); + try { + final outputPath = '${directory.path}/design.verilog'; + + final service = mod.dumpSystemVerilog(outputPath: outputPath); + + expect(File(outputPath).existsSync(), isTrue); + expect(File(outputPath).readAsStringSync(), equals(service.output)); + expect(service.outputDirectory, equals('.')); + expect( + service.artifacts.single.fileName, + equals('${mod.definitionName}.sv'), + ); + } finally { + directory.deleteSync(recursive: true); + } + }, testOn: 'vm'); + }); + group('try ports', () { test('tryInput, exists', () { final mod = ModuleWithMaybePorts(addIn: true); @@ -303,8 +339,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,(a ? 1'h0 : 1'h1)}")); @@ -320,8 +356,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,a}")); @@ -336,7 +372,8 @@ void main() { TopStructInoutWrap(LogicNet(), LogicNet(), LogicNet(width: 2)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect( sv, @@ -352,7 +389,7 @@ void main() { expect( mod.internalSignals.firstWhereOrNull((e) => e.name == 't0'), isNotNull); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign a_concat[0] = t0;')); }); @@ -363,7 +400,7 @@ void main() { expect(mod.internalSignals.firstWhereOrNull((e) => e.name == 'unconnected'), isNotNull); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign a_arr[1] = unconnected;')); }); diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 52470dc8c..f91f8a10b 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multimodule4_test.dart @@ -54,7 +54,7 @@ void main() { .isNotEmpty, 'Should find a z two levels deep'); - final synth = ftm.generateSynth(); + final synth = ftm.dumpSystemVerilog().output; // "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..e40b5e03c 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multimodule5_test.dart @@ -35,7 +35,7 @@ void main() { final mod = TopModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('Passthrough')); }); diff --git a/test/name_test.dart b/test/name_test.dart index d169b913b..3a8580837 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -208,7 +208,7 @@ void main() { reserveInstanceName: false, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('module specialName (')); }); test('uniquified with conflicts', () async { @@ -218,7 +218,7 @@ void main() { causeInstConflict: false, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('module specialName (')); expect(sv, contains('module specialName_0 (')); }); @@ -229,7 +229,7 @@ void main() { causeInstConflict: false, ); await mod.build(); - expect(mod.generateSynth, throwsException); + expect(() => mod.dumpSystemVerilog().output, throwsException); }); }); @@ -241,7 +241,7 @@ void main() { causeInstConflict: false, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('specialInstanceName(')); expect(sv, contains('specialInstanceName_0(')); diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart index b936dd2e2..7fd54acdb 100644 --- a/test/naming_cases_test.dart +++ b/test/naming_cases_test.dart @@ -533,7 +533,7 @@ void main() { // ── Golden SV snapshot ────────────────────────────────────── test('golden SV output snapshot', () { - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // Port declarations. expect(sv, contains('input logic [7:0] inp')); diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart index 9f1c0c31f..afec4304d 100644 --- a/test/naming_namespace_test.dart +++ b/test/naming_namespace_test.dart @@ -82,7 +82,7 @@ void main() { test('constant value appears as literal in SV output', () async { final dut = _ConstantNamingModule(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; // The constant "1" should appear as a literal 1'h1 in the output, // not as a declared signal. @@ -92,7 +92,7 @@ void main() { test('constNameDisallowed falls through to signal naming', () async { final dut = _ConstNameDisallowedModule(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; // The output assignment should NOT use the raw constant literal // as a wire name; a proper signal name should be used instead. @@ -109,7 +109,7 @@ void main() { 'in the shared namespace', () async { final dut = _InstanceSignalCollision(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; // With a single shared namespace, one of the two "inner" identifiers // must be suffixed to avoid collision. @@ -129,7 +129,7 @@ void main() { reason: 'Instance should win the shared namespace ' 'and keep the bare name'); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; // The wire (signal) must carry the suffix, not the instance. expect(sv, contains('inner_0'), reason: 'Colliding signal should be renamed to inner_0'); @@ -148,8 +148,8 @@ void main() { String stripHeader(String sv) => sv.replaceFirst(RegExp(r'/\*\*.*?\*/\n', dotAll: true), ''); - final sv1 = stripHeader(dut.generateSynth()); - final sv2 = stripHeader(dut.generateSynth()); + final sv1 = stripHeader(dut.dumpSystemVerilog().output); + final sv2 = stripHeader(dut.dumpSystemVerilog().output); expect(sv2, equals(sv1), reason: 'Repeated synthesis passes must produce identical ' @@ -159,7 +159,7 @@ void main() { test('duplicate instance names get uniquified', () async { final dut = _DuplicateInstances(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog().output; // Two instances named 'blk' — one should be 'blk', the other 'blk_0'. expect(sv, contains('blk')); diff --git a/test/nested_array_struct_port_synthesis_test.dart b/test/nested_array_struct_port_synthesis_test.dart index bd3bc8f6d..dedaeb16c 100644 --- a/test/nested_array_struct_port_synthesis_test.dart +++ b/test/nested_array_struct_port_synthesis_test.dart @@ -7,6 +7,9 @@ // 2026 August 18 // Author: Max Korbel +@TestOn('vm') +library; + import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; @@ -181,7 +184,7 @@ void main() { ); await module.build(); - final generated = module.generateSynth(); + final generated = module.dumpSystemVerilog().output; expect(generated, contains('module ArrayRecordHierarchy')); expect(generated, contains('.inputData(')); @@ -197,7 +200,7 @@ void main() { final module = RootArrayPassThrough(LogicArray([2, 3], 4)); await module.build(); - final generated = module.generateSynth(); + final generated = module.dumpSystemVerilog().output; expect(generated, contains('input logic [1:0][2:0][3:0] inputData')); expect(generated, contains('output logic [1:0][2:0][3:0] outputData')); diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index 0901acac3..7626977a6 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( + mod.dumpSystemVerilog().output); 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 = dut.dumpSystemVerilog().output; expect( sv, @@ -455,7 +456,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect( sv, contains( @@ -517,7 +518,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect( sv, @@ -590,7 +591,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog().output); if (netTypeName == LogicNet) { expect( sv, @@ -620,7 +621,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog().output); if (netTypeName == LogicNet) { expect( sv, @@ -750,8 +751,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); 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( + mod.dumpSystemVerilog().output); expect( sv, @@ -781,8 +782,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect( sv, @@ -799,8 +800,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect( sv, @@ -817,8 +818,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect( sv, @@ -835,8 +836,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); 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( + mod.dumpSystemVerilog().output); expect( sv, @@ -942,7 +943,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog().output); checkSV(sv); final vectors = [ @@ -962,7 +963,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog().output); 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( + mod.dumpSystemVerilog().output); 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( + mod.dumpSystemVerilog().output); expect( sv, diff --git a/test/net_test.dart b/test/net_test.dart index 0cab0fe03..0d9ebc3f2 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 = mod.dumpSystemVerilog().output; 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 = mod.dumpSystemVerilog().output; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -515,7 +515,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -526,7 +526,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(' submod'.allMatches(sv).length, 2); }); }); @@ -611,7 +611,7 @@ void main() { isNotNull); } - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // 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 = mod.dumpSystemVerilog().output; // 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 = mod.dumpSystemVerilog().output; 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..21ea92edb --- /dev/null +++ b/test/netlist_example_test.dart @@ -0,0 +1,297 @@ +// 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(); + + 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..ac1697eea --- /dev/null +++ b/test/netlist_synthesizer_test.dart @@ -0,0 +1,2671 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer_test.dart +// Comprehensive tests for the netlist synthesizer. +// +// 2026 April 13 +// Author: Desmond Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_structure_concat.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/filter_bank/filter_bank_modules.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +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); + } +} + +/// A custom [FlipFlop] used to verify inheritance-aware leaf matching. +class CustomFlipFlop extends FlipFlop { + CustomFlipFlop(super.clk, super.d); +} + +/// Exercises flip-flops with optional control signals. +class ControlledFlopModule extends Module { + ControlledFlopModule( + Logic clk, + Logic d, { + Logic? en, + Logic? reset, + Logic? resetValue, + int? constantResetValue, + bool asyncReset = false, + }) : super(name: 'controlledflop') { + clk = addInput('clk', clk); + d = addInput('d', d, width: d.width); + if (en != null) { + en = addInput('en', en); + } + if (reset != null) { + reset = addInput('reset', reset); + } + if (resetValue != null) { + resetValue = addInput('resetValue', resetValue, width: d.width); + } + addOutput('q', width: d.width) <= + flop( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue ?? constantResetValue, + asyncReset: asyncReset, + ); + } +} + +/// Exercises Add. +class AddModule extends Module { + Logic get sum => output('sum'); + 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 both the sum and carry outputs of [Add]. +class AddWithCarryModule extends Module { + AddWithCarryModule(Logic a, Logic b, {int width = 8}) + : super(name: 'addwithcarry') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final add = Add(a, b); + addOutput('sum', width: width) <= add.sum; + addOutput('carry') <= add.carry; + } +} + +/// Wraps an [AddModule] so stop-policy tests can choose whether the child +/// receives its own definition or is emitted as a netlist cell. +class AddWrapperModule extends Module { + Logic get sum => output('sum'); + + AddWrapperModule({int width = 8}) : super(name: 'addwrapper') { + final a = addInput('a', Logic(width: width), width: width); + final b = addInput('b', Logic(width: width), width: width); + final child = AddModule(a, b, width: width); + addOutput('sum', width: width) <= child.sum; + } +} + +/// Exercises Multiply. +class MulModule extends Module { + Logic get prod => output('prod'); + 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(); + } +} + +/// Child with a LogicArray input, used to exercise array port netlisting. +class ArrayInputChildModule extends Module { + LogicArray get values => input('values') as LogicArray; + + Logic get packedOut => output('packedOut'); + + ArrayInputChildModule(LogicArray values) : super(name: 'arrayinputchild') { + values = addInputArray( + 'values', + values, + dimensions: values.dimensions, + elementWidth: values.elementWidth, + ); + addOutput('packedOut', width: values.width) <= + [for (final element in values.elements.reversed) element].swizzle(); + } +} + +/// Child with a LogicArray output, used to exercise regrouping array output +/// elements into another child array input. +class ArrayOutputChildModule extends Module { + LogicArray get values => output('values') as LogicArray; + + ArrayOutputChildModule() : super(name: 'arrayoutputchild') { + addOutputArray('values', dimensions: [4], elementWidth: 8); + } +} + +/// Provides multiple array outputs to verify synthesized concat cell names. +class MultipleArrayOutputModule extends Module { + MultipleArrayOutputModule() + : super( + name: 'multiplearrayoutput', + definitionName: 'MultipleArrayOutputModule', + ) { + final dataA = addInput('dataA', Logic(width: 8), width: 8); + final dataB = addInput('dataB', Logic(width: 8), width: 8); + final first = addOutputArray('first', dimensions: [2], elementWidth: 8); + final second = addOutputArray('second', dimensions: [2], elementWidth: 8); + for (final element in first.elements) { + element <= dataA; + } + for (final element in second.elements) { + element <= dataB; + } + final child = NotModule(dataA[0]); + addOutput('childOut') <= child.y; + } +} + +/// Parent whose internal LogicArray elements independently feed a child array +/// input port. +class InternalArrayToChildModule extends Module { + InternalArrayToChildModule() : super(name: 'internalarraytochild') { + final first = addInput('first', Logic(width: 8), width: 8); + final second = addInput('second', Logic(width: 8), width: 8); + final values = LogicArray([2], 8, name: 'values'); + + values.elements[0] <= first; + values.elements[1] <= second; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent whose internal LogicArray groups elements from another child output +/// array before feeding a child input port. +class RegroupedArrayOutputToChildModule extends Module { + RegroupedArrayOutputToChildModule() + : super(name: 'regroupedarrayoutputtochild') { + final source = ArrayOutputChildModule(); + final values = LogicArray([2], 8, name: 'values'); + + values.elements[0] <= source.values.elements[2]; + values.elements[1] <= source.values.elements[3]; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent with nested internal LogicArrays, used to exercise concat-to-concat +/// consumer rewrites after array concat outputs receive fresh wire IDs. +class NestedInternalArrayToChildModule extends Module { + NestedInternalArrayToChildModule() + : super(name: 'nestedinternalarraytochild') { + final inputs = [ + for (var index = 0; index < 4; index++) + addInput('in$index', Logic(width: 8), width: 8), + ]; + final lower = LogicArray([2], 8, name: 'lower'); + final upper = LogicArray([2], 8, name: 'upper'); + final values = LogicArray([2], 16, name: 'values'); + + lower.elements[0] <= inputs[0]; + lower.elements[1] <= inputs[1]; + upper.elements[0] <= inputs[2]; + upper.elements[1] <= inputs[3]; + values.elements[0] <= lower; + values.elements[1] <= upper; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Parent whose 2D LogicArray.net rows are driven by independent child array +/// outputs before feeding a child array input port. +class NestedNetArrayRowsToChildModule extends Module { + NestedNetArrayRowsToChildModule() : super(name: 'nestednetarrayrowstochild') { + final lower = ArrayOutputChildModule(); + final upper = ArrayOutputChildModule(); + final values = LogicArray.net([2, 4], 8, name: 'values'); + + values.elements[0] <= lower.values; + values.elements[1] <= upper.values; + final child = ArrayInputChildModule(values); + addOutput('packedOut', width: values.width) <= child.packedOut; + } +} + +/// Simple two-field structure used to demonstrate netlist struct unpack/pack +/// cells. +class NetlistPairStruct extends LogicStructure { + Logic get low => elements[0]; + + Logic get high => elements[1]; + + NetlistPairStruct({super.name = 'pair'}) + : super([Logic(name: 'low', width: 4), Logic(name: 'high', width: 4)]); + + @override + NetlistPairStruct clone({String? name}) => NetlistPairStruct(name: name); +} + +/// Consumes fields of a typed structure input independently, requiring the +/// netlist to unpack the aggregate port into named field connections. +class StructInputConsumerModule extends Module { + StructInputConsumerModule(NetlistPairStruct pair) + : super(name: 'structinputconsumer') { + pair = addTypedInput('pair', pair); + addOutput('packedOut', width: pair.width) <= + [pair.high, pair.low].swizzle(); + } +} + +/// Drives fields of a typed structure output independently, requiring the +/// netlist to pack the field wires back into the aggregate output port. +class StructOutputProducerModule extends Module { + StructOutputProducerModule() : super(name: 'structoutputproducer') { + final low = addInput('low', Logic(width: 4), width: 4); + final high = addInput('high', Logic(width: 4), width: 4); + final pair = NetlistPairStruct(name: 'pairValue'); + + pair.low <= low; + pair.high <= high ^ Const(1, width: 4); + addTypedOutput('pair', pair.clone).gets(pair); + } +} + +/// Instantiates identical structure-packing children at distinct parent paths. +class StructOutputProducerDedupTop extends Module { + StructOutputProducerDedupTop() : super(name: 'structoutputproducerdeduptop') { + final first = StructOutputProducerModule(); + final second = StructOutputProducerModule(); + + addOutput('first', width: 8) <= first.output('pair'); + addOutput('second', width: 8) <= second.output('pair'); + } +} + +/// Exercises arithmetic right shift (ARShift). +class ARShiftModule extends Module { + Logic get y => output('y'); + 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, { + NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), +}) async { + await mod.build(); + final synth = + SynthBuilder(mod, NetlistSynthesizer(configuration: configuration)); + 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; + }); + }); +} + +({List undrivenInputs, Map> driversByBit}) + _connectivityReport(Map moduleDef) { + final ports = _ports(moduleDef); + final cells = _cells(moduleDef); + final producedBits = {}; + final driversByBit = >{}; + + void addDriver(int bit, String driver) { + producedBits.add(bit); + (driversByBit[bit] ??= []).add(driver); + } + + for (final entry in ports.entries) { + final port = entry.value as Map; + final direction = port['direction'] as String?; + if (direction != 'input' && direction != 'inout') { + continue; + } + for (final bit in (port['bits'] as List).whereType()) { + addDriver(bit, 'port ${entry.key}'); + } + } + + for (final entry in cells.entries) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map? ?? {}; + final connections = cell['connections'] as Map? ?? {}; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] != 'output' && + directions[portEntry.key] != 'inout') { + continue; + } + for (final bit in (portEntry.value as List).whereType()) { + addDriver(bit, 'cell ${entry.key}.${portEntry.key}'); + } + } + } + + final undrivenInputs = []; + for (final entry in cells.entries) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map? ?? {}; + final connections = cell['connections'] as Map? ?? {}; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] != 'input') { + continue; + } + final undrivenBits = (portEntry.value as List) + .whereType() + .where((bit) => !producedBits.contains(bit)) + .toList(); + if (undrivenBits.isNotEmpty) { + undrivenInputs.add( + '${entry.key}.${portEntry.key}: ${undrivenBits.take(8).join(', ')}', + ); + } + } + } + + return (undrivenInputs: undrivenInputs, driversByBit: driversByBit); +} + +// ──────────────────────────────────────────────────────────────────── +// Tests +// ──────────────────────────────────────────────────────────────────── + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // ── Group 1: Leaf cell mapper — individual gate mappings ─────────── + + group('netlist cell mapping', () { + test(r'And2Gate maps to $and cell', () async { + final json = await _synthToMap(AndModule(Logic(), Logic())); + expect(_hasCellType(json, r'$and'), isTrue); + }); + + 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('FlipFlop controls map to standard Yosys register cells', () async { + final clk = SimpleClockGenerator(10).clk; + final d = Logic(width: 4); + final en = Logic(); + final reset = Logic(); + final resetValue = Logic(width: 4); + final cases = <( + ControlledFlopModule module, + String type, + Set ports, + Map parameters, + )>[ + ( + ControlledFlopModule(clk, d, en: en), + r'$dffe', + {'CLK', 'D', 'EN', 'Q'}, + {'WIDTH': 4, 'CLK_POLARITY': 1, 'EN_POLARITY': 1}, + ), + ( + ControlledFlopModule(clk, d, reset: reset, constantResetValue: 9), + r'$sdff', + {'CLK', 'D', 'SRST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'SRST_POLARITY': 1, + 'SRST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + constantResetValue: 9, + ), + r'$sdffe', + {'CLK', 'D', 'EN', 'SRST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'SRST_POLARITY': 1, + 'SRST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + reset: reset, + constantResetValue: 9, + asyncReset: true, + ), + r'$adff', + {'CLK', 'D', 'ARST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'ARST_POLARITY': 1, + 'ARST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + constantResetValue: 9, + asyncReset: true, + ), + r'$adffe', + {'CLK', 'D', 'EN', 'ARST', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'ARST_POLARITY': 1, + 'ARST_VALUE': '1001', + }, + ), + ( + ControlledFlopModule( + clk, + d, + reset: reset, + resetValue: resetValue, + asyncReset: true, + ), + r'$aldff', + {'CLK', 'D', 'ALOAD', 'AD', 'Q'}, + {'WIDTH': 4, 'CLK_POLARITY': 1, 'ALOAD_POLARITY': 1}, + ), + ( + ControlledFlopModule( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: true, + ), + r'$aldffe', + {'CLK', 'D', 'EN', 'ALOAD', 'AD', 'Q'}, + { + 'WIDTH': 4, + 'CLK_POLARITY': 1, + 'EN_POLARITY': 1, + 'ALOAD_POLARITY': 1, + }, + ), + ]; + + for (final testCase in cases) { + final (module, type, ports, parameters) = testCase; + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (cell) => cell['type'] == type, + ); + expect( + (cell['port_directions'] as Map).keys.toSet(), + equals(ports), + reason: type, + ); + expect( + cell['parameters'], + equals(parameters), + reason: type, + ); + } + }); + + test('FlipFlop dynamic synchronous reset is lowered to standard cells', + () async { + final module = ControlledFlopModule( + SimpleClockGenerator(10).clk, + Logic(width: 4), + en: Logic(), + reset: Logic(), + resetValue: Logic(width: 4), + ); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef).values.cast>(); + final dff = cells.singleWhere((cell) => cell['type'] == r'$dffe'); + + expect(_hasCellType(json, r'$mux'), isTrue); + expect(_hasCellType(json, r'$or'), isTrue); + expect( + (dff['port_directions'] as Map).keys.toSet(), + equals({'CLK', 'D', 'EN', 'Q'}), + ); + }); + + test(r'Add maps to $add cell', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$add'), isTrue); + }); + + test(r'Add maps carry into the high bit of standard $add Y', () async { + final json = await _synthToMap( + AddWithCarryModule(Logic(width: 8), Logic(width: 8)), + ); + final addCell = _modules(json) + .values + .cast>() + .expand((definition) => _cells(definition).values) + .cast>() + .singleWhere((cell) => cell['type'] == r'$add'); + final directions = addCell['port_directions'] as Map; + final connections = addCell['connections'] as Map; + final parameters = addCell['parameters'] as Map; + + expect(directions.keys.toSet(), equals({'A', 'B', 'Y'})); + expect(connections.keys.toSet(), equals({'A', 'B', 'Y'})); + expect(connections['Y'], hasLength(9)); + expect(parameters['Y_WIDTH'], 9); + }); + + test(r'Subtract maps to $sub cell', () async { + final json = await _synthToMap( + SubModule(Logic(width: 8), Logic(width: 8)), + ); + 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 $sshr cell', () async { + final json = await _synthToMap( + ARShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$sshr'), isTrue); + }); + + test('shift cells use standard ports and signedness parameters', () async { + final cases = <(Module Function() moduleGen, String type, int aSigned)>[ + (() => ShiftModule(Logic(width: 8), Logic(width: 8)), r'$shl', 0), + (() => ShiftModule(Logic(width: 8), Logic(width: 8)), r'$shr', 0), + (() => ARShiftModule(Logic(width: 8), Logic(width: 8)), r'$sshr', 1), + ]; + + for (final (moduleGen, type, aSigned) in cases) { + final module = moduleGen(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (cell) => cell['type'] == type, + ); + + expect( + cell['port_directions'], + equals({'A': 'input', 'B': 'input', 'Y': 'output'}), + reason: type, + ); + expect( + cell['parameters'], + equals({ + 'A_SIGNED': aSigned, + 'A_WIDTH': 8, + 'B_SIGNED': 0, + 'B_WIDTH': 8, + 'Y_WIDTH': 8, + }), + reason: type, + ); + } + }); + + test(r'AndUnary maps to $reduce_and cell', () async { + 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); + final tribuf = _modules(json) + .values + .cast>() + .expand((moduleDef) => _cells(moduleDef).values) + .cast>() + .singleWhere((cell) => cell['type'] == r'$tribuf'); + expect( + tribuf['port_directions'], + equals({'A': 'input', 'EN': 'input', 'Y': 'output'}), + ); + }); + }); + + // ── 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', + ); + }); + + test('structure-pack children at different paths are deduplicated', + () async { + final json = await _synthToMap(StructOutputProducerDedupTop()); + final structProducerDefs = _modules(json) + .keys + .where( + (definitionName) => + definitionName.startsWith('StructOutputProducerModule'), + ) + .toList(); + + expect( + structProducerDefs, + hasLength(1), + reason: + 'The structure-pack cell keys must be local to each child module.', + ); + }); + }); + + // ── Group 4: NetlistSynthesizerConfiguration permutations ────────────────── + + group('NetlistSynthesizerConfiguration', () { + late Module filterBank; + + setUp(() async { + await Simulator.reset(); + filterBank = _buildFilterBank(); + await filterBank.build(); + }); + + test('default configuration produce valid netlist', () { + final synth = SynthBuilder(filterBank, NetlistSynthesizer()); + final json = (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + filterBank, + ); + final parsed = jsonDecode(json) as Map; + expect(parsed['creator'], equals('NetlistSynthesizer (rohd)')); + expect(parsed['version'], equals(NetlistSynthesizer.formatVersion)); + expect(_modules(parsed), isNotEmpty); + }); + + test('slimMode omits connections', () { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(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('slim then expanded matches initially expanded output', () async { + final module = _buildFilterBank(); + await module.build(); + + final translator = NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration(slimMode: true), + ); + final slim = translator.synthesizeToJson(module); + final expanded = translator.synthesizeToJson(module, slimMode: false); + final initiallyExpanded = NetlistSynthesizer().synthesizeToJson(module); + + final slimModules = _modules(jsonDecode(slim) as Map); + expect( + slimModules.values + .expand( + (definition) => _cells(definition as Map).values, + ) + .every((cell) => !(cell as Map).containsKey('connections')), + isTrue, + ); + expect(expanded, initiallyExpanded); + }); + + test( + 'filter bank can stop traversal at an opaque custom SV module', + () { + final synthesizer = NetlistSynthesizer( + configuration: NetlistSynthesizerConfiguration( + leafModulePredicate: (module) => + module is FlipFlop || module is MacUnit, + ), + ); + final json = jsonDecode(synthesizer.synthesizeToJson(filterBank)) + as Map; + final modules = _modules(json); + + expect( + modules.keys.any((name) => name.contains('MacUnit')), + isFalse, + reason: 'MacUnit is treated like externally supplied/custom SV, so ' + 'the netlist should not emit a definition for it.', + ); + + final channelDefs = modules.entries.where( + (entry) => entry.key.contains('FilterChannel'), + ); + expect(channelDefs, isNotEmpty); + + final macCells = channelDefs.expand((entry) { + final def = entry.value as Map; + return _cells(def).values.where((cell) { + final cellMap = cell as Map; + return (cellMap['type'] as String).contains('MacUnit'); + }); + }).toList(); + + expect( + macCells, + isNotEmpty, + reason: 'FilterChannel should still instantiate the opaque MacUnit ' + 'cell; only hierarchy traversal stops at that boundary.', + ); + }, + ); + + test('DCE disabled still produces valid netlist', () { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: 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', () { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: 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( + configuration: + const NetlistSynthesizerConfiguration(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 8: Wire ID and structural invariants ───────────────────── + + group('wire ID and structural invariants', () { + test('default synthesizers do not share mutable leaf mappers', () { + final first = NetlistSynthesizer(); + final second = NetlistSynthesizer(); + + expect( + identical(first.netlistCellMapper, second.netlistCellMapper), + isFalse, + ); + }); + + test( + 'leaf module predicate controls which modules stop traversal', + () async { + final module = AddWrapperModule(); + await module.build(); + + final childDefinitionName = module.subModules.single.definitionName; + final synthesizer = NetlistSynthesizer( + configuration: NetlistSynthesizerConfiguration( + leafModulePredicate: (module) => module is AddModule, + ), + ); + + final json = jsonDecode(synthesizer.synthesizeToJson(module)) + as Map; + final modules = json['modules'] as Map; + final top = modules[module.definitionName] as Map; + final cells = _cells(top); + + expect(modules, isNot(contains(childDefinitionName))); + expect( + cells.values, + contains( + predicate>( + (cell) => cell['type'] == childDefinitionName, + ), + ), + ); + }, + ); + + test('default leaf predicate matches FlipFlop subclasses', () { + const configuration = NetlistSynthesizerConfiguration(); + + expect( + configuration.leafModulePredicate(CustomFlipFlop(Logic(), Logic())), + isTrue, + ); + }); + + test('repeated translation of the same module is identical', () async { + final module = LogicArrayExample( + LogicArray([4], 8, name: 'arrayA'), + Logic(name: 'id', width: 3), + Logic(name: 'selectIndexValue', width: 8), + Logic(name: 'selectFromValue', width: 8), + ); + await module.build(); + + final synthesizer = NetlistSynthesizer(); + final first = synthesizer.synthesizeToJson(module); + final second = synthesizer.synthesizeToJson(module); + + expect(second, first); + }); + + test('reusing a synthesizer resets wire IDs for each module', () async { + final firstModule = AddModule( + Logic(name: 'firstA', width: 8), + Logic(name: 'firstB', width: 8), + ); + final secondModule = AddModule( + Logic(name: 'secondA', width: 8), + Logic(name: 'secondB', width: 8), + ); + await firstModule.build(); + await secondModule.build(); + + final synthesizer = NetlistSynthesizer(); + + int firstWireId(Module module) { + final json = jsonDecode(synthesizer.synthesizeToJson(module)) + as Map; + final definition = + _modules(json)[module.definitionName] as Map; + return _ports(definition) + .values + .expand((port) => (port as Map)['bits'] as List) + .whereType() + .reduce((first, second) => first < second ? first : second); + } + + expect(firstWireId(firstModule), 2); + expect(firstWireId(secondModule), 2); + }); + + test('array concat outputs use fresh wire IDs', () async { + final json = await _synthToMap(InternalArrayToChildModule()); + final moduleDef = + _modules(json)['InternalArrayToChildModule'] as Map; + final cells = _cells(moduleDef); + final arrayConcats = cells.entries.where( + (entry) => entry.key.startsWith('array_concat'), + ); + + expect(arrayConcats, isNotEmpty); + for (final arrayConcat in arrayConcats) { + final connections = (arrayConcat.value + as Map)['connections'] as Map; + final inputBits = connections.entries + .where((entry) => entry.key != 'Y') + .expand((entry) => entry.value as List) + .toSet(); + final outputBits = + (connections['Y'] as List).whereType().toSet(); + + expect(inputBits.intersection(outputBits), isEmpty); + } + }); + + test('array concat output names use unique destination addresses', + () async { + final module = MultipleArrayOutputModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final arrayConcatNames = _cells(moduleDef) + .entries + .where( + (entry) => + (entry.value as Map)['type'] == r'$concat', + ) + .map((entry) => entry.key) + .where((name) => name.startsWith('array_concat_output_')) + .toList(); + + expect(arrayConcatNames, hasLength(2)); + expect(arrayConcatNames.toSet(), hasLength(2)); + }); + + test('regrouped array output elements get explicit concat', () async { + final module = RegroupedArrayOutputToChildModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final arrayConcatEntries = cells.entries.where( + (entry) => entry.key.startsWith('array_concat'), + ); + + expect(arrayConcatEntries, isNotEmpty, reason: cells.keys.join(', ')); + expect( + arrayConcatEntries.any((entry) { + final connections = (entry.value + as Map)['connections'] as Map; + final outputBits = connections['Y'] as List?; + return outputBits?.length == 16; + }), + isTrue, + ); + }); + + test('nested array concats feed downstream concat inputs', () async { + final json = await _synthToMap(NestedInternalArrayToChildModule()); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final concatEntries = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$concat'; + }).toList(); + + final concatOutputBits = {}; + for (final entry in concatEntries) { + final cell = entry.value as Map; + final connections = cell['connections'] as Map; + concatOutputBits.addAll((connections['Y'] as List).whereType()); + } + + final concatInputConsumers = {}; + for (final entry in concatEntries) { + final cell = entry.value as Map; + final connections = cell['connections'] as Map; + final directions = cell['port_directions'] as Map; + for (final portEntry in connections.entries) { + if (directions[portEntry.key] == 'input') { + concatInputConsumers.addAll( + (portEntry.value as List).whereType(), + ); + } + } + } + + expect(concatOutputBits.intersection(concatInputConsumers), isNotEmpty); + }); + + test( + 'nested LogicArray.net aggregate ports have connected concat inputs', + () async { + for (final configuration in [ + const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false), + const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true, + enableDeadCellElimination: false, + ), + ]) { + final json = await _synthToMap( + NestedNetArrayRowsToChildModule(), + configuration: configuration, + ); + final moduleDef = _modules(json) + .values + .cast>() + .reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final nestedArrayConcats = cells.entries.where((entry) { + final cell = entry.value as Map; + return entry.key.startsWith('array_concat') && + cell['type'] == r'$concat'; + }); + final report = _connectivityReport(moduleDef); + final multipleDrivers = report.driversByBit.entries + .where((entry) => entry.value.length > 1) + .toList(); + + expect(nestedArrayConcats, isNotEmpty, reason: cells.keys.join(', ')); + expect( + report.undrivenInputs, + isEmpty, + reason: report.undrivenInputs.join('\n'), + ); + expect( + multipleDrivers, + isEmpty, + reason: multipleDrivers.take(8).join('\n'), + ); + } + }, + ); + + test('struct input fields get explicit unpack cell', () async { + final module = StructInputConsumerModule(NetlistPairStruct()); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef); + final structUnpacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$struct_unpack'; + }).toList(); + + expect(structUnpacks, isNotEmpty, reason: cells.keys.join(', ')); + expect( + structUnpacks.any((entry) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map; + return directions['A'] == 'input' && + directions['low'] == 'output' && + directions['high'] == 'output'; + }), + isTrue, + ); + }); + + test('struct output fields get explicit pack cell', () async { + final module = StructOutputProducerModule(); + final json = await _synthToMap(module); + final moduleDef = + _modules(json).values.cast>().reduce( + (left, right) => + _cells(left).length >= _cells(right).length ? left : right, + ); + final cells = _cells(moduleDef); + final structPacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return entry.key.startsWith( + SynthStructureConcat.operationName, + ) && + cell['type'] == r'$struct_pack'; + }).toList(); + + expect(structPacks, isNotEmpty, reason: cells.keys.join(', ')); + expect( + structPacks.any((entry) { + final cell = entry.value as Map; + final directions = cell['port_directions'] as Map; + return directions['low'] == 'input' && + directions['high'] == 'input' && + directions['Y'] == 'output'; + }), + isTrue, + ); + }); + + test('struct aggregate netnames cannot span multiple drivers', () { + final ports = >{}; + final cells = >{ + 'first_driver': { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': 8}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': List.generate(8, (index) => 100 + index), + 'Y': List.generate(8, (index) => 200 + index), + }, + }, + 'second_driver': { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': 8}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': List.generate(8, (index) => 300 + index), + 'Y': List.generate(8, (index) => 400 + index), + }, + }, + }; + final netnames = { + 'values': { + 'bits': [ + ...List.generate(8, (index) => 200 + index), + ...List.generate(8, (index) => 400 + index), + ], + 'logic_type': { + 'typeName': 'PairStructure', + 'fields': [ + {'name': 'first', 'width': 8}, + {'name': 'second', 'width': 8}, + ], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + cells, + 'struct_module', + netnames: netnames, + ), + throwsA(isA()), + ); + }); + + test('optimized netlist removes concat aliases of named vectors', () async { + final json = await _synthToMap( + NestedInternalArrayToChildModule(), + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ); + + for (final moduleDef in _modules( + json, + ).values.cast>()) { + final namedBitVectors = [ + for (final netname + in (moduleDef['netnames'] as Map).values) + if (netname is Map && netname['bits'] is List) + (netname['bits'] as List).cast(), + ]; + + for (final entry in _cells(moduleDef).entries) { + final cell = entry.value as Map; + if (cell['type'] != r'$concat') { + continue; + } + + final connections = cell['connections'] as Map; + final directions = cell['port_directions'] as Map; + final inputBits = [ + for (final portEntry in connections.entries) + if (directions[portEntry.key] != 'output') + ...(portEntry.value as List).cast(), + ]; + + expect( + namedBitVectors.any( + (bits) => + bits.length == inputBits.length && + bits.indexed.every( + (bitEntry) => bitEntry.$2 == inputBits[bitEntry.$1], + ), + ), + isFalse, + reason: '${entry.key} aliases an already named vector', + ); + } + } + }); + + test('concat of adjacent slices collapses to one slice', () { + final sourceBits = List.generate(32, (index) => 100 + index); + final modules = >{ + 'top': { + 'attributes': {}, + 'ports': >{}, + 'netnames': >{}, + 'cells': >{ + 'slice0': { + 'hide_name': 0, + 'type': r'$slice', + 'parameters': {'OFFSET': 8, 'A_WIDTH': 32, 'Y_WIDTH': 4}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': sourceBits, + 'Y': [1, 2, 3, 4], + }, + }, + 'slice1': { + 'hide_name': 0, + 'type': r'$slice', + 'parameters': {'OFFSET': 12, 'A_WIDTH': 32, 'Y_WIDTH': 4}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': sourceBits, + 'Y': [5, 6, 7, 8], + }, + }, + 'concat': { + 'hide_name': 0, + 'type': r'$concat', + 'parameters': {'IN0_WIDTH': 4, 'IN1_WIDTH': 4}, + 'attributes': {}, + 'port_directions': { + '[3:0]': 'input', + '[7:4]': 'input', + 'Y': 'output', + }, + 'connections': { + '[3:0]': [1, 2, 3, 4], + '[7:4]': [5, 6, 7, 8], + 'Y': [9, 10, 11, 12, 13, 14, 15, 16], + }, + }, + }, + }, + }; + + NetlistPasses.collapseConcatOfAdjacentSlices(modules); + + final topModule = modules['top']!; + final cells = topModule['cells']! as Map>; + final concat = cells['concat']!; + final concatConnections = concat['connections']! as Map; + expect(cells, isNot(contains('slice0'))); + expect(cells, isNot(contains('slice1'))); + expect(concat['type'], equals(r'$slice')); + expect( + concat['parameters'], + equals({'OFFSET': 8, 'A_WIDTH': 32, 'Y_WIDTH': 8}), + ); + expect(concatConnections['A'], equals(sourceBits)); + expect(concatConnections['Y'], equals([9, 10, 11, 12, 13, 14, 15, 16])); + }); + + test('all wire IDs are >= 2 (0 and 1 reserved for constants)', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + 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, + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: 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, + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: 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 configuration', () { + test('collapseTransparentClusters produces valid netlist', () async { + final fb = _buildFilterBank(); + final json = await _synthToMap( + fb, + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ); + expect(_modules(json), isNotEmpty); + }); + + test('validation reports multiple drivers with their locations', () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + 'y': { + 'direction': 'output', + 'bits': [2], + }, + }; + final cells = { + 'driver': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + cells, + 'ShortedModule', + ), + throwsA( + isA() + .having( + (error) => error.moduleName, 'module name', 'ShortedModule') + .having((error) => error.issues, 'issues', hasLength(1)) + .having((error) => error.issues.single.wireBit, 'wire bit', 1) + .having( + (error) => error.issues.single.drivers, + 'drivers', + containsAll(['port a (input)', r'cell driver.Y ($buf)']), + ), + ), + ); + }); + + test('validation ignores structural aliases but counts buffers as drivers', + () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + }; + final concatAlias = { + 'concat': { + 'type': r'$concat', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate(ports, concatAlias, 'StructuralAlias'), + returnsNormally, + ); + + final buffers = { + 'firstBuffer': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [2], + }, + }, + 'secondBuffer': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'Y': [2], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + buffers, + 'BufferedShort', + ), + throwsA(isA()), + ); + }); + + test('validation accepts inout module boundaries', () { + final ports = >{ + 'dataBus': { + 'direction': 'inout', + 'bits': [1], + }, + }; + final cells = >{ + 'SharedDataBus': { + 'type': 'SharedDataBus', + 'port_directions': {'dataBus': 'inout'}, + 'connections': { + 'dataBus': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate(ports, cells, 'FilterBank'), + returnsNormally, + ); + }); + + test('validation allows disconnected cell outputs', () { + final ports = { + 'a': { + 'direction': 'input', + 'bits': [1], + }, + 'b': { + 'direction': 'input', + 'bits': [2], + }, + }; + final cells = { + 'unusedAnd': { + 'type': r'$and', + 'port_directions': {'A': 'input', 'B': 'input', 'Y': 'output'}, + 'connections': { + 'A': [1], + 'B': [2], + 'Y': [3], + }, + }, + }; + + expect( + () => NetlistValidation.validate(ports, cells, 'UnusedOutputModule'), + returnsNormally, + ); + }); + + test('validation allows cells to drive inout ports', () { + final ports = { + 'bus': { + 'direction': 'inout', + 'bits': [1], + }, + }; + final cells = { + 'driver': { + 'type': r'$tribuf', + 'port_directions': {'A': 'input', 'EN': 'input', 'Y': 'output'}, + 'connections': { + 'A': [2], + 'EN': [3], + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + ports, + cells, + 'InOutModule', + ), + returnsNormally, + ); + }); + + test(r'validation allows multiple $tribuf drivers on a resolved net', () { + final cells = >{ + 'firstDriver': { + 'type': r'$tribuf', + 'port_directions': {'Y': 'output'}, + 'connections': { + 'Y': [1], + }, + }, + 'secondDriver': { + 'type': r'$tribuf', + 'port_directions': {'Y': 'inout'}, + 'connections': { + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'ResolvedTriStateNet', + ), + returnsNormally, + ); + }); + + test(r'validation rejects mixed $tribuf and exclusive drivers', () { + final cells = >{ + 'triStateDriver': { + 'type': r'$tribuf', + 'port_directions': {'Y': 'output'}, + 'connections': { + 'Y': [1], + }, + }, + 'exclusiveDriver': { + 'type': r'$buf', + 'port_directions': {'Y': 'output'}, + 'connections': { + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'ConflictingTriStateNet', + ), + throwsA( + isA().having( + (error) => error.issues.single.drivers, + 'drivers', + containsAll([ + r'cell triStateDriver.Y ($tribuf)', + r'cell exclusiveDriver.Y ($buf)', + ]), + ), + ), + ); + }); + }); + + // ── 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..5efb405a8 --- /dev/null +++ b/test/netlist_test.dart @@ -0,0 +1,1070 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_test.dart +// Tests for the netlist synthesizer public surface. +// +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synthesis_result.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/filter_bank/filter_bank_modules.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +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 wrapper that lets tests synthesize a built submodule as the requested top. +class _CompositeWrapperModule extends Module { + late final _CompositeModule child; + + _CompositeWrapperModule(Logic a, Logic b) : super(name: 'composite_wrapper') { + a = addInput('a', a); + b = addInput('b', b); + child = _CompositeModule(a, b); + addOutput('out') <= child.out; + } +} + +/// A simple adder module with a configurable width. +class _AdderModule extends Module { + Logic get sum => output('sum'); + + _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; + } +} + +/// Example for the netlist-only adjacent-slice/concat collapse. +class _AdjacentSliceConcatExample extends Module { + Logic get out => output('out'); + + _AdjacentSliceConcatExample(Logic data) + : super(definitionName: 'AdjacentSliceConcatExample') { + data = addInput('data', data, width: 8); + + final low = data.getRange(0, 4).named('low'); + final high = data.getRange(4, 8).named('high'); + addOutput('out', width: 8) <= [high, low].swizzle(); + } +} + +/// Example for the netlist-only transparent slice/buf cluster collapse. +class _SliceAliasClusterExample extends Module { + Logic get out => output('out'); + + _SliceAliasClusterExample(Logic data) + : super(definitionName: 'SliceAliasClusterExample') { + data = addInput('data', data, width: 8); + + final low = data.getRange(0, 4).named('low'); + final alias = Swizzle([low]).out.named('alias'); + addOutput('out', width: 4) <= alias; + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// 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, + ); +} + +Map _topModuleFromJson(Module module, String json) { + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + return modules[module.definitionName] as Map; +} + +int _cellCount(Map module, String cellType) { + final cells = module['cells'] as Map? ?? {}; + return cells.values.where((cell) { + final cellMap = cell as Map; + return cellMap['type'] == cellType; + }).length; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('FilterBank argument validation', () { + FilterBank construct({ + int numChannels = 2, + int numTaps = 3, + int dataWidth = 16, + List? samples, + List> coefficients = const [ + [1, 2, 1], + [1, -2, 1], + ], + }) => + FilterBank( + Logic(), + Logic(), + Logic(), + samples ?? + List.generate( + numChannels, + (ch) => FilterSample( + dataWidth: dataWidth, + name: 'sample$ch', + ), + ), + Logic(), + numChannels: numChannels, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients, + ); + + test('rejects an empty channel count', () { + expect( + () => construct( + numChannels: 0, + samples: const [], + coefficients: const [], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'numChannels', + ), + ), + ); + }); + + test('rejects an empty tap count', () { + expect( + () => construct( + numTaps: 0, + coefficients: const [[], []], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'numTaps', + ), + ), + ); + }); + + test('rejects a non-positive data width', () { + expect( + () => construct( + dataWidth: 0, + samples: [ + FilterSample(name: 'sample0'), + FilterSample(name: 'sample1'), + ], + ), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'dataWidth', + ), + ), + ); + }); + + test('rejects a sample count that differs from the channel count', () { + expect( + () => construct(samples: [FilterSample(name: 'sample0')]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'samples', + ), + ), + ); + }); + + test('rejects a coefficient count that differs from the channel count', () { + expect( + () => construct(coefficients: const [ + [1, 2, 1], + ]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'coefficients', + ), + ), + ); + }); + + test('rejects a coefficient row with the wrong tap count', () { + expect( + () => construct(coefficients: const [ + [1, 2, 1], + [1, -2], + ]), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'coefficients[1]', + ), + ), + ); + }); + }); + + // ── Example smoke tests ─────────────────────────────────────────────── + // + // Each example is synthesized once, verifying that the netlist is + // 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('requested submodule can be synthesized as top', () async { + final wrapper = _CompositeWrapperModule( + Logic(name: 'a'), + Logic(name: 'b'), + ); + await wrapper.build(); + + final submodule = wrapper.child; + final json = NetlistSynthesizer().synthesizeToJson(submodule); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + + expect(modules, contains(submodule.definitionName)); + expect(modules, isNot(contains(wrapper.definitionName))); + + final topModules = modules.values.where((module) { + final attrs = (module as Map)['attributes'] + as Map?; + return attrs?['top'] == 1; + }); + expect(topModules, hasLength(1)); + + final attrs = (modules[submodule.definitionName] + as Map)['attributes'] as Map; + expect(attrs['top'], equals(1)); + }); + + test('port bit widths match module interface', () async { + const width = 16; + final mod = _AdderModule( + 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); + }); + + test('result maps and nested values are unmodifiable', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults + .whereType() + .firstWhere((result) => result.module == mod); + final firstCell = result.cells.values.first; + final connections = firstCell['connections']! as Map; + + expect( + () => result.cells['replacement'] = {}, + throwsUnsupportedError, + ); + expect( + () => firstCell['type'] = r'$replacement', + throwsUnsupportedError, + ); + expect( + () => connections['replacement'] = [], + throwsUnsupportedError, + ); + }); + }); + + // ── collectModuleEntries ────────────────────────────────────────────── + + 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); + } + }); + }); + + // ── NetlistSynthesizerConfiguration ────────────────────────────────── + group('NetlistSynthesizerConfiguration', () { + test('slimMode omits cell connections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final slimSynth = NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration(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'); + } + } + } + }); + }); + + // ── Netlist-only transparent optimizations ─────────────────────────── + + group('Netlist-only transparent optimizations', () { + test('adjacent slices feeding concat collapse into one wider slice', + () async { + final mod = _AdjacentSliceConcatExample(Logic(name: 'data', width: 8)); + await mod.build(); + + final rawTop = _topModuleFromJson( + mod, + NetlistSynthesizer().synthesizeToJson(mod), + ); + expect(_cellCount(rawTop, r'$slice'), equals(2)); + expect(_cellCount(rawTop, r'$concat'), equals(1)); + + final collapsedTop = _topModuleFromJson( + mod, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ).synthesizeToJson(mod), + ); + expect(_cellCount(collapsedTop, r'$slice'), equals(1)); + expect(_cellCount(collapsedTop, r'$concat'), equals(0)); + }); + + test('slice feeding alias buffer collapses into one buffer', () async { + final mod = _SliceAliasClusterExample(Logic(name: 'data', width: 8)); + await mod.build(); + + final rawTop = _topModuleFromJson( + mod, + NetlistSynthesizer().synthesizeToJson(mod), + ); + expect(_cellCount(rawTop, r'$slice'), equals(1)); + expect(_cellCount(rawTop, r'$buf'), equals(1)); + + final collapsedTop = _topModuleFromJson( + mod, + NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + collapseTransparentClusters: true), + ).synthesizeToJson(mod), + ); + expect(_cellCount(collapsedTop, r'$slice'), equals(0)); + expect(_cellCount(collapsedTop, r'$buf'), equals(1)); + }); + }); + + // ── FilterBank (multi-channel, dedup, loopback) ─────────────────────── + + group('FilterBank netlist', () { + 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('post-processing does not mutate synthesis results', () async { + final module = _AdderModule( + Logic(name: 'a', width: 8), + Logic(name: 'b', width: 8), + ); + await module.build(); + + final synthesizer = NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), + ); + final builder = SynthBuilder(module, synthesizer); + final result = builder.synthesisResults + .whereType() + .firstWhere((result) => result.module == module); + final before = jsonEncode({ + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }); + + synthesizer.generateCombinedJson(builder, module); + + final after = jsonEncode({ + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }); + expect(after, before); + }); + + test('compressBitRanges option produces range strings in JSON', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthCompressed = NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(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( + configuration: + const NetlistSynthesizerConfiguration(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( + configuration: const NetlistSynthesizerConfiguration(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 configuration together produce smallest output', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthBoth = NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration( + compressBitRanges: true, + compactJson: true, + ), + ); + final jsonBoth = synthBoth.synthesizeToJson(mod); + + final synthCompressOnly = NetlistSynthesizer( + configuration: + const NetlistSynthesizerConfiguration(compressBitRanges: true), + ); + final jsonCompressOnly = synthCompressOnly.synthesizeToJson(mod); + + final synthCompactOnly = NetlistSynthesizer( + configuration: const NetlistSynthesizerConfiguration(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( + configuration: + const NetlistSynthesizerConfiguration(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 c6bb7ad96..ed7877223 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 = mod.dumpSystemVerilog().output; 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 a605b5c25..abdc5ce39 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_w_modify_test.dart @@ -94,7 +94,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; 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 46afdbef0..96201c156 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 = mod.dumpSystemVerilog().output; expect(sv, contains('input logic simple_clk')); }); diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart index 31bf38bd9..267ee22db 100644 --- a/test/pipeline_test.dart +++ b/test/pipeline_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // pipeline_test.dart diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 8ee28bb70..572ff8403 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -176,7 +176,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect( sv, diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index a6274a36b..6777ad95d 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -147,7 +147,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect( sv, diff --git a/test/replication_test.dart b/test/replication_test.dart index a6a566353..bbdebff10 100644 --- a/test/replication_test.dart +++ b/test/replication_test.dart @@ -67,7 +67,7 @@ void main() { test('multiply by 1 generates no replication in SystemVerilog', () async { final mod = ReplicationOpModule(Logic(width: 4), 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign b = a;')); expect(sv, isNot(contains('{1{'))); }); @@ -76,7 +76,7 @@ void main() { () async { final mod = SignExtendModule(Logic(), 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, isNot(contains('{1{'))); }); diff --git a/test/sequential_test.dart b/test/sequential_test.dart index cabeda5ec..f3774f492 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -309,7 +309,7 @@ void main() { final mod = NegedgeTriggeredSeq(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('always_ff @(negedge')); final vectors = [ diff --git a/test/struct_port_pruning_test.dart b/test/struct_port_pruning_test.dart new file mode 100644 index 000000000..2003b6228 --- /dev/null +++ b/test/struct_port_pruning_test.dart @@ -0,0 +1,143 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// struct_port_pruning_test.dart +// Verifies that struct port elements on submodules are not incorrectly +// pruned during SV synthesis. Exercises the `submoduleOutputSynths` / +// `submoduleInputSynths` fix in `_pruneUnused`. +// +// 2026 April 17 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +// ── Struct definition ────────────────────────────────────────── + +class PairStruct extends LogicStructure { + PairStruct({Logic? a, Logic? b, super.name = 'pair'}) + : super([a ?? Logic(name: 'a'), b ?? Logic(name: 'b')]); + + @override + PairStruct clone({String? name}) => PairStruct(name: name); +} + +// ── Leaf submodule with a struct output port ─────────────────── + +class StructProducer extends Module { + Logic get out => PairStruct()..gets(output('out')); + + StructProducer(Logic x, Logic y) : super(name: 'struct_producer') { + x = addInput('x', x); + y = addInput('y', y); + + final s = PairStruct(a: x, b: y); + addOutput('out', width: s.width) <= s; + } +} + +// ── Leaf submodule with a struct input port ──────────────────── + +class StructConsumer extends Module { + Logic get sum => output('sum'); + + StructConsumer(Logic pair) : super(name: 'struct_consumer') { + pair = addInput('pair', pair, width: pair.width); + + final s = PairStruct()..gets(pair); + addOutput('sum') <= s.elements[0] ^ s.elements[1]; + } +} + +// ── Top module: struct output from submodule → struct input ─── + +class StructPipeTop extends Module { + Logic get result => output('result'); + + StructPipeTop(Logic x, Logic y) : super(name: 'struct_pipe_top') { + x = addInput('x', x); + y = addInput('y', y); + + final producer = StructProducer(x, y); + final consumer = StructConsumer(producer.out); + + addOutput('result') <= consumer.sum; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('struct port pruning', () { + test('SV output retains struct element signals from submodule', () async { + final dut = StructPipeTop(Logic(), Logic()); + await dut.build(); + + final svStr = dut.dumpSystemVerilog().output; + + // The struct_producer submodule should appear in the SV. + expect( + svStr, + contains('struct_producer'), + reason: 'Submodule with struct output should not be pruned', + ); + + // The struct_consumer submodule should appear in the SV. + expect( + svStr, + contains('struct_consumer'), + reason: 'Submodule with struct input should not be pruned', + ); + + // The output port 'out' of struct_producer (width 2) must have a + // connection in the parent — it should not be pruned away. + expect( + svStr, + contains('.out('), + reason: 'Struct output port connection should not be pruned', + ); + + // The input port 'pair' of struct_consumer must be connected. + expect( + svStr, + contains('.pair('), + reason: 'Struct input port connection should not be pruned', + ); + }); + + test('struct element signals survive SV synthesis for producer', () async { + final dut = StructProducer(Logic(), Logic()); + await dut.build(); + + final svStr = dut.dumpSystemVerilog().output; + + // Inside StructProducer, the struct elements (a, b from PairStruct) + // drive the output via struct_slice decomposition. They must not + // be pruned. + expect(svStr, contains('out'), reason: 'Output port should appear in SV'); + expect( + svStr, + contains('input'), + reason: 'Input ports should appear in SV', + ); + }); + + test('struct element signals survive SV synthesis for consumer', () async { + final dut = StructConsumer(Logic(width: 2)); + await dut.build(); + + final svStr = dut.dumpSystemVerilog().output; + + // 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 adb4f51fb..2d6d676fe 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -682,7 +682,7 @@ void main() { test('const unary inline op', () async { final mod = ModWithConstInlineUnaryOp(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains("~8'h0"), reason: sv); @@ -701,7 +701,7 @@ void main() { final mod = TieOffSubsetTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains("assign banana_tieoff = 2'h0;")); expect(sv, contains("assign apple_tieoff = 2'h0;")); @@ -722,7 +722,7 @@ void main() { final mod = TieOffPortTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains("assign banana = 1'h0;")); expect(sv, contains(".apple(1'h0)")); @@ -754,7 +754,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 = mod.dumpSystemVerilog().output; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -771,7 +771,7 @@ void main() { () async { final mod = AlphabeticalWidthsModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -797,7 +797,7 @@ void main() { final mod = AlphabeticalSubmodulePorts(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; checkPortConnectionOrder(sv, ['l', 'a', 'w', 'm', 'x', 'b']); }); @@ -806,7 +806,7 @@ void main() { final mod = TopWithExpressions(Logic(), Logic(width: 5)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('.a((a | (b[2])))')); }); @@ -815,7 +815,7 @@ void main() { final mod = ModuleWithFloatingSignals(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // only expect 1 assignment to xylophone expect('assign'.allMatches(sv).length, 1); @@ -829,8 +829,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( + mod.dumpSystemVerilog().output); if (banExpressions) { expect(sv, contains('assign my_fancy_new_signal <= ^fer_swizzle;')); @@ -849,7 +849,7 @@ void main() { final mod = ModuleWithCustomDefinitionEmptyPorts(Logic(), acceptsEmptyPortConnections: acceptsEmptyPortConnections); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; if (acceptsEmptyPortConnections) { expect(sv, contains('.b()')); @@ -864,7 +864,7 @@ void main() { test('custom definition', () async { final mod = TopWithCustomDef(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('module CustomDefinitionModule (')); expect(sv, contains('// this is a custom definition!')); @@ -882,7 +882,7 @@ void main() { final mod = ModWithUselessWireMods(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, isNot(contains('swizzle'))); expect(sv, isNot(contains('replicate'))); @@ -903,7 +903,7 @@ void main() { test('partial array assignment sv', () async { final mod = ModWithPartialArrayAssignment(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign b = aArr[0];')); expect(sv, contains('assign aArr[0] = a;')); @@ -1047,7 +1047,7 @@ void main() { final mod = OutToInOutTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('assign myNet = myOut;')); @@ -1063,7 +1063,7 @@ void main() { () async { final mod = _StructLeafNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, isNot(contains('_in0')), reason: 'Struct leaf from unnamed Logic() should use its ' @@ -1073,7 +1073,7 @@ void main() { test('const merge not blocked by constNameDisallowed', () async { final mod = _ConstNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; 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..15a76583a 100644 --- a/test/sv_param_passthrough_test.dart +++ b/test/sv_param_passthrough_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // 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 = mod.dumpSystemVerilog().output; expect(sv.contains('#'), isFalse); }); } diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index d6ccf9d4f..2acb42f25 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -188,7 +188,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('/*')); expect(sv, contains('*/')); @@ -211,7 +211,7 @@ void main() { final mod = SingleElementSwizzle(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // Single element should not have braces or bit range annotations // Look for bit range annotations specifically (/* number */) @@ -236,7 +236,7 @@ void main() { final mod = AllSingleBitSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // Should have bit range annotations for single bits expect(sv, contains('/*')); @@ -267,7 +267,7 @@ void main() { final mod = NestedSwizzle(Logic(width: 4), Logic(width: 3)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // Should contain annotations for both inner and outer swizzles expect(sv, contains('/*')); @@ -287,7 +287,7 @@ void main() { final mod = InlinedSwizzle(Logic(width: 4), Logic(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // Should have annotations even when swizzle is part of larger expression expect(sv, contains('/*')); @@ -307,7 +307,7 @@ void main() { final mod = VariedWidthSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // Should have aligned bit range annotations expect(sv, contains('/*')); @@ -345,7 +345,7 @@ void main() { // Create a module with indices requiring different digit widths final largeModule = LargeWidthSwizzle(); await largeModule.build(); - final sv = largeModule.generateSynth(); + final sv = largeModule.dumpSystemVerilog().output; // Should have properly aligned annotations despite different digit counts expect(sv, contains('/*')); @@ -389,7 +389,8 @@ void main() { final mod = SwizzleAdjacentBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect(sv, contains('assign out = {a[7:5],a[2:0]};')); expect(sv, isNot(contains('a[7],a[6]'))); @@ -412,7 +413,8 @@ void main() { final mod = SwizzleAllAdjacentBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect(sv, contains('assign out = a[7:5];')); expect(sv, isNot(contains('assign out = {a[7:5]};'))); @@ -430,7 +432,8 @@ void main() { final mod = SwizzleAscendingBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect(sv, isNot(contains('a[2:0]'))); expect(sv, contains('a[0]')); @@ -457,7 +460,8 @@ void main() { final mod = SwizzleNestedAdjacentBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect(sv, contains('assign out = {(a[7:5]),a[4:3]};')); expect(sv, isNot(contains('a[7:3]'))); @@ -475,7 +479,8 @@ void main() { final mod = SwizzleAdjacentRanges(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect(sv, contains('assign out = {(a[5:2]),(a[1:0])};')); expect(sv, isNot(contains('a[5:0]'))); @@ -494,7 +499,8 @@ void main() { final mod = SwizzlePackedArrayElementBits(LogicArray([2, 2], 1)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect(sv, contains('assign out = {arr[1][1:0],arr[0][1:0]};')); expect(sv, isNot(contains('arr[1][1],arr[1][0]'))); @@ -514,7 +520,8 @@ void main() { ); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog().output); expect(sv, isNot(contains('arr[3:0]'))); expect(sv, contains('arr[3]')); @@ -528,7 +535,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; 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..24ec99b77 --- /dev/null +++ b/test/synth_name_parity_test.dart @@ -0,0 +1,379 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_name_parity_test.dart +// Tests that verify signalNameOfBest works consistently across +// different synthesis paths (SV and netlist). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; + +extension _NetlistTestModule on Module { + String generateNetlist( + {NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), + String? packageRoot}) { + if (!hasBuilt) { + throw ModuleNotBuiltException(this); + } + + return NetlistSynthesizer(configuration: configuration) + .synthesizeToJson(this, packageRoot: packageRoot); + } +} + +class _Counter extends Module { + _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi( + [SimpleClockGenerator(10).clk, reset], + [ + If( + reset, + then: [val < 0], + orElse: [ + If(en, then: [val < nextVal]), + ], + ), + ], + ); + } +} + +class _CollidingNames extends Module { + late final Logic firstDup; + late final Logic secondDup; + + _CollidingNames(Logic a, Logic b) : super(name: 'collidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + + firstDup = Logic(name: 'dup'); + secondDup = Logic(name: 'dup'); + + firstDup <= a & b; + secondDup <= a | b; + y <= firstDup ^ secondDup; + } +} + +class _PartiallyInlineCollidingNames extends Module { + late final Logic inlinedDup; + late final Logic retainedDup; + + _PartiallyInlineCollidingNames(Logic a, Logic b) + : super(name: 'partiallyInlineCollidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + + inlinedDup = Logic(name: 'dup'); + retainedDup = Logic(name: 'dup'); + + inlinedDup <= a & b; + retainedDup <= a | b; + y <= inlinedDup ^ retainedDup; + z <= retainedDup & a; + } +} + +class _CollapsedInstanceCollidingNames extends Module { + late final Logic retainedDup; + + _CollapsedInstanceCollidingNames(Logic a, Logic b) + : super(name: 'collapsedInstanceCollidingNames') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + + final collapsedInstanceOut = And2Gate(a, b, name: 'dup').out; + retainedDup = Logic(name: 'dup'); + + retainedDup <= a | b; + y <= collapsedInstanceOut ^ retainedDup; + z <= retainedDup; + } +} + +class _ReverseInternalSignalOrderSynthModuleDefinition + extends SynthModuleDefinition { + _ReverseInternalSignalOrderSynthModuleDefinition(super.module); + + @override + void process() { + internalSignals + ..clear() + ..addAll(internalSignals.toList().reversed); + } +} + +Future> _collisionNamesAfter( + Iterable synthesize, +) async { + final mod = _CollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'firstDup': mod.namer.signalNameOfBest([mod.firstDup]), + 'secondDup': mod.namer.signalNameOfBest([mod.secondDup]), + }; +} + +Future> _collisionNamesAfterSynthDefinition( + SynthModuleDefinition Function(_CollidingNames) createSynthDefinition, +) async { + final mod = _CollidingNames(Logic(), Logic()); + await mod.build(); + + createSynthDefinition(mod); + + return { + 'firstDup': mod.namer.signalNameOfBest([mod.firstDup]), + 'secondDup': mod.namer.signalNameOfBest([mod.secondDup]), + }; +} + +Future> _partialInlineCollisionNamesAfter( + Iterable synthesize, +) async { + final mod = _PartiallyInlineCollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'retainedDup': mod.namer.signalNameOfBest([mod.retainedDup]), + 'inlinedDup': mod.namer.signalNameOfBest([mod.inlinedDup]), + }; +} + +Future> _collapsedInstanceCollisionNamesAfter( + Iterable synthesize, +) async { + final mod = _CollapsedInstanceCollidingNames(Logic(), Logic()); + await mod.build(); + + for (final synth in synthesize) { + synth(mod); + } + + return { + 'retainedDup': mod.namer.signalNameOfBest([mod.retainedDup]), + }; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('signalNameOfBest after netlist synthesis', () { + test('counter — returns names after netlist synthesis', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + mod.generateNetlist(); + + expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); + expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); + expect(mod.namer.signalNameOfBest([mod.output('val')]), equals('val')); + }); + + test('filter_bank — returns names for sub-module signals', () async { + const dataWidth = 16; + const numTaps = 3; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samples = List.generate(2, (ch) => FilterSample(name: 'sample$ch')); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samples, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + dut.generateNetlist(); + + expect(dut.namer.signalNameOfBest([dut.input('clk')]), equals('clk')); + expect(dut.namer.signalNameOfBest([dut.input('reset')]), equals('reset')); + expect(dut.namer.signalNameOfBest([dut.output('done')]), equals('done')); + }); + }); + + group('signalNameOfBest after SV synthesis', () { + test('counter — returns best signal name after SV synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + mod.dumpSystemVerilog(); + + 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 signalNameOfBest', + () async { + final modNetlist = _Counter(Logic(), Logic()); + await modNetlist.build(); + modNetlist.generateNetlist(); + await Simulator.reset(); + + final modSv = _Counter(Logic(), Logic()); + await modSv.build(); + modSv.dumpSystemVerilog(); + + // Both paths use the same Namer, so names must match. + final enNetlist = modNetlist.namer.signalNameOfBest([ + modNetlist.input('en'), + ]); + final enSv = modSv.namer.signalNameOfBest([modSv.input('en')]); + + expect( + enSv, + equals(enNetlist), + reason: 'SV and netlist should produce identical canonical names', + ); + }, + ); + + test( + 'colliding mergeable names remain stable across synthesis order', + () async { + void runNetlist(_CollidingNames mod) => mod.generateNetlist(); + void runSv(_CollidingNames mod) => mod.dumpSystemVerilog(); + + final netlistOnly = await _collisionNamesAfter([runNetlist]); + await Simulator.reset(); + + final svOnly = await _collisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _collisionNamesAfter([runNetlist, runSv]); + await Simulator.reset(); + + final svThenNetlist = await _collisionNamesAfter([runSv, runNetlist]); + + expect(netlistOnly, equals(svOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + + expect( + netlistOnly['secondDup'], + isNot(equals(netlistOnly['firstDup'])), + ); + }, + ); + + test( + 'colliding mergeable names ignore internal signal walk order', + () async { + final forward = await _collisionNamesAfterSynthDefinition( + SynthModuleDefinition.new, + ); + await Simulator.reset(); + + final reversed = await _collisionNamesAfterSynthDefinition( + _ReverseInternalSignalOrderSynthModuleDefinition.new, + ); + + expect(reversed, equals(forward)); + expect(forward['firstDup'], equals('dup')); + expect(forward['secondDup'], equals('dup_0')); + }, + ); + + test('colliding names stay stable when SV inlines one signal', () async { + void runNetlist(_PartiallyInlineCollidingNames mod) => + mod.generateNetlist(); + void runSv(_PartiallyInlineCollidingNames mod) => mod.dumpSystemVerilog(); + + final netlistOnly = await _partialInlineCollisionNamesAfter([runNetlist]); + await Simulator.reset(); + + final svOnly = await _partialInlineCollisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _partialInlineCollisionNamesAfter([ + runNetlist, + runSv, + ]); + await Simulator.reset(); + + final svThenNetlist = await _partialInlineCollisionNamesAfter([ + runSv, + runNetlist, + ]); + + expect(svOnly, equals(netlistOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + expect(netlistOnly['inlinedDup'], equals('dup')); + expect(netlistOnly['retainedDup'], equals('dup_0')); + }); + + test( + 'signal names stay stable when SV collapses a colliding instance', + () async { + void runNetlist(_CollapsedInstanceCollidingNames mod) => + mod.generateNetlist(); + void runSv(_CollapsedInstanceCollidingNames mod) => + mod.dumpSystemVerilog(); + + final netlistOnly = await _collapsedInstanceCollisionNamesAfter([ + runNetlist, + ]); + await Simulator.reset(); + + final svOnly = await _collapsedInstanceCollisionNamesAfter([runSv]); + await Simulator.reset(); + + final netlistThenSv = await _collapsedInstanceCollisionNamesAfter([ + runNetlist, + runSv, + ]); + await Simulator.reset(); + + final svThenNetlist = await _collapsedInstanceCollisionNamesAfter([ + runSv, + runNetlist, + ]); + + expect(svOnly, equals(netlistOnly)); + expect(netlistThenSv, equals(netlistOnly)); + expect(svThenNetlist, equals(netlistOnly)); + expect(netlistOnly['retainedDup'], equals('dup')); + }, + ); + }); +} diff --git a/test/synth_structure_layout_test.dart b/test/synth_structure_layout_test.dart new file mode 100644 index 000000000..9491598ff --- /dev/null +++ b/test/synth_structure_layout_test.dart @@ -0,0 +1,93 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_structure_layout_test.dart +// Tests for packed LogicStructure layout synthesis utilities. +// +// 2026 July 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +void main() { + group('SynthStructureLayout', () { + test('uses least-significant-first element offsets', () { + final structure = LogicStructure([ + Logic(name: 'low', width: 2), + Logic(name: 'high', width: 3), + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), 'low'); + expect(layout.fieldNameAt(1, fallbackName: 'fallback'), 'low'); + expect(layout.fieldNameAt(2, fallbackName: 'fallback'), 'high'); + expect(layout.fieldNameAt(4, fallbackName: 'fallback'), 'high'); + expect(layout.fieldNameAt(5, fallbackName: 'fallback'), 'fallback'); + }); + + test('qualifies an unpreferred nested leaf by parent and index', () { + final nested = LogicStructure([ + Logic(name: Naming.unpreferredName('first'), width: 2), + Logic(name: Naming.unpreferredName('second'), width: 2), + ], name: 'payload'); + final structure = LogicStructure([ + Logic(name: 'header'), + nested, + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(1, fallbackName: 'fallback'), 'payload_0'); + expect(layout.fieldNameAt(3, fallbackName: 'fallback'), 'payload_1'); + }); + + test('returns bit ranges for nested field paths', () { + final nested = LogicStructure([ + Logic(name: 'b', width: 2), + LogicStructure([ + Logic(name: 'd', width: 3), + ], name: 'c'), + ], name: 'a'); + final structure = LogicStructure([ + Logic(name: 'prefix'), + nested, + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.bitRangeForPath('a'), (start: 1, end: 6)); + expect(layout.bitRangeForPath('a.b'), (start: 1, end: 3)); + expect(layout.bitRangeForPath('a.c'), (start: 3, end: 6)); + expect(layout.bitRangeForPath('a.c.d'), (start: 3, end: 6)); + expect(layout.bitRangeForPath('a.missing'), isNull); + }); + + test('supports unpack-specific anonymous field names', () { + final fieldName = Naming.unpreferredName('field'); + final structure = LogicStructure([Logic(name: fieldName)]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), fieldName); + expect( + layout.fieldNameAt( + 0, + fallbackName: 'fallback', + anonymousUnpreferred: true, + ), + 'anonymous_0', + ); + }); + + test('does not recurse into LogicArray elements', () { + final structure = LogicStructure([ + LogicArray([2], 3, name: 'entries'), + Logic(name: 'tail'), + ]); + final layout = SynthStructureLayout(structure); + + expect(layout.fieldNameAt(0, fallbackName: 'fallback'), 'entries'); + expect(layout.fieldNameAt(5, fallbackName: 'fallback'), 'entries'); + expect(layout.fieldNameAt(6, fallbackName: 'fallback'), 'tail'); + }); + }); +} diff --git a/test/systemverilog_port_types_test.dart b/test/systemverilog_port_types_test.dart index 80ca66505..6ca0fb141 100644 --- a/test/systemverilog_port_types_test.dart +++ b/test/systemverilog_port_types_test.dart @@ -125,7 +125,10 @@ void main() { final module = _PortTypesModule(); await module.build(); - final sv = module.generateSynth(configuration: testCase.configuration); + final sv = SystemVerilogService( + module, + configuration: testCase.configuration, + ).output; final declarations = { testCase.inputPrefix: [ diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index 80f328073..714f1bed3 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -227,7 +227,7 @@ void main() { final mod = SimpleStructModuleContainer(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, isNot(contains('internal_struct'))); @@ -249,7 +249,7 @@ void main() { expect(mod.anyOut, isA()); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, contains('input logic [3:0][1:0] anyIn')); expect(sv, contains('output logic [3:0][1:0] anyOut')); @@ -275,7 +275,7 @@ void main() { final mod = ParentModuleWithStructsContainingPorts(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // if naming is wrong, these names will appear in the SV in ports expect( @@ -355,7 +355,7 @@ void main() { SimpleStructModuleContainer(LogicNet(), LogicNet(), asNet: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv, isNot(contains('internal_struct'))); @@ -500,7 +500,7 @@ void main() { final mod = ModuleWithOneBitStructPort(OneBitStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; // no slicing on single-bit signals expect(sv, contains('assign outStruct = outStruct_oneBit')); diff --git a/test/wave_dumper_test.dart b/test/wave_dumper_test.dart deleted file mode 100644 index 07aafc8c8..000000000 --- a/test/wave_dumper_test.dart +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright (C) 2021-2024 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// wave_dumper_test.dart -// Tests for the WaveDumper -// -// 2021 November 4 -// Author: Max Korbel - -@TestOn('vm') -library; - -import 'dart:async'; -import 'dart:io'; - -import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/vcd_parser.dart'; -import 'package:test/test.dart'; - -class SimpleModule extends Module { - SimpleModule(Logic a) { - a = addInput('a', a, width: a.width); - addOutput('b', width: a.width) <= ~a; - } -} - -class SimpleModWithSeq extends Module { - Logic get val => output('val'); - SimpleModWithSeq(Logic asyncReset, Logic clk) { - clk = addInput('clk', clk); - asyncReset = addInput('asyncReset', asyncReset); - addOutput('val'); - - val <= flop(clk, Const(1), reset: asyncReset, asyncReset: true); - } -} - -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]. -void createTemporaryDump(Module module, String name) { - Directory(tempDumpDir).createSync(recursive: true); - final tmpDumpFile = temporaryDumpPath(name); - WaveDumper(module, outputPath: tmpDumpFile); -} - -/// Deletes the temporary VCD file associated with [name]. -void deleteTemporaryDump(String name) { - final tmpDumpFile = temporaryDumpPath(name); - File(tmpDumpFile).deleteSync(); -} - -void main() { - tearDown(() async { - await Simulator.reset(); - }); - - test('attach dumper after put', () async { - final a = Logic(name: 'a'); - final mod = SimpleModule(a); - await mod.build(); - - const dumpName = 'dumpAfterPut'; - - a.put(1); - createTemporaryDump(mod, dumpName); - - Simulator.registerAction(10, () => a.put(0)); - await Simulator.run(); - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 5, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), - equals(true)); - - deleteTemporaryDump(dumpName); - }); - - test('attach dumper before put', () async { - final a = Logic(name: 'a'); - final mod = SimpleModule(a); - await mod.build(); - - const dumpName = 'dumpBeforePut'; - - createTemporaryDump(mod, dumpName); - a.inject(1); - - Simulator.registerAction(10, () => a.put(0)); - Simulator.registerAction(20, () => a.put(1)); - await Simulator.run(); - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 1, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 20, LogicValue.ofString('1')), - equals(true)); - - deleteTemporaryDump(dumpName); - }); - - test('multiple injects in the same timestamp', () async { - final clk = SimpleClockGenerator(10).clk; - final a = Logic(name: 'a'); - final mod = SimpleModule(a); - a <= clk; - - await mod.build(); - - const dumpName = 'multiInject'; - - createTemporaryDump(mod, dumpName); - - Simulator.setMaxSimTime(100); - unawaited(Simulator.run()); - - await clk.nextPosedge; - await clk.nextPosedge; - await clk.nextPosedge; - - // inject a 0 on a when it should be 1 already from the clock - a.inject(0); - - await Simulator.simulationEnded; - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('0')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 5, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 35, LogicValue.ofString('0')), - equals(true)); - - deleteTemporaryDump(dumpName); - }); - - test('multi-bit value', () async { - final a = Logic(name: 'a', width: 8); - final mod = SimpleModule(a); - await mod.build(); - - const dumpName = 'multiBit'; - - createTemporaryDump(mod, dumpName); - a.inject(0x5a); - - Simulator.registerAction(10, () => a.put(0xa5)); - await Simulator.run(); - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofInt(0x5a, 8)), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofInt(0xa5, 8)), - equals(true)); - - deleteTemporaryDump(dumpName); - }); - - test('multi-bit value mixed invalid', () async { - final a = Logic(name: 'a', width: 8); - final mod = SimpleModule(a); - await mod.build(); - - const dumpName = 'multiBitInvalid'; - - createTemporaryDump(mod, dumpName); - a.inject(LogicValue.ofString('01xzzx10')); - - Simulator.registerAction(10, () => a.put(LogicValue.ofString('0x0x1z1z'))); - await Simulator.run(); - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue( - vcdContents, 'a', 0, LogicValue.ofString('01xzzx10')), - equals(true)); - expect( - VcdParser.confirmValue( - vcdContents, 'a', 10, LogicValue.ofString('0x0x1z1z')), - equals(true)); - - deleteTemporaryDump(dumpName); - }); - - test('dump after max sim time works', () async { - final a = SimpleClockGenerator(10).clk; - final mod = SimpleModule(a); - await mod.build(); - - const dumpName = 'maxSimTime'; - - createTemporaryDump(mod, dumpName); - - Simulator.setMaxSimTime(100); - - await Simulator.run(); - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue(vcdContents, 'a', 99, LogicValue.one), - equals(true), - ); - - deleteTemporaryDump(dumpName); - }); - - test('create non-existent output directories', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - const dir1Path = '$tempDumpDir/dir1'; - - final waveDumper = WaveDumper(mod, outputPath: '$dir1Path/dir2/waves.vcd'); - - expect(File(waveDumper.outputPath).existsSync(), equals(true)); - - if (File(waveDumper.outputPath).existsSync()) { - File(dir1Path).deleteSync(recursive: true); - } - }); - - test('async reset shown in waves correctly', () async { - final reset = Logic(); - final clk = SimpleClockGenerator(10).clk; - final mod = SimpleModWithSeq(reset, clk); - - await mod.build(); - - const dumpName = 'asyncReset'; - - Simulator.setMaxSimTime(100); - Simulator.registerAction(13, () => reset.put(1)); - reset.put(0); - - // add wave dumper *after* the put to reset - createTemporaryDump(mod, dumpName); - - // check functional matches - Simulator.registerAction(0, () => expect(reset.value.toInt(), 0)); - Simulator.registerAction(6, () => expect(mod.val.value.toInt(), 1)); - Simulator.registerAction(14, () => expect(mod.val.value.toInt(), 0)); - - await Simulator.run(); - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - // reset is 0 initially - expect( - VcdParser.confirmValue(vcdContents, 'asyncReset', 1, LogicValue.zero), - equals(true)); - - // 1 after first clock edge - expect(VcdParser.confirmValue(vcdContents, 'val', 6, LogicValue.one), - equals(true)); - - // 0 after async reset - expect(VcdParser.confirmValue(vcdContents, 'val', 14, LogicValue.zero), - equals(true)); - - deleteTemporaryDump(dumpName); - }); -} diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart new file mode 100644 index 000000000..ae7a5a75a --- /dev/null +++ b/test/waveform_service_test.dart @@ -0,0 +1,514 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_service_test.dart +// Tests for WaveformService output and VCD/FST event parity. +// +// 2026 July 17 +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/vcd_parser.dart'; +import 'package:test/test.dart'; + +class _SimpleWaveModule extends Module { + _SimpleWaveModule(Logic a) { + a = addInput('a', a, width: a.width); + addOutput('b', width: a.width) <= ~a; + } +} + +const _tempDumpDir = 'tmp_test'; + +String _temporaryVcdPath(String name) => '$_tempDumpDir/temp_wave_$name.vcd'; + +String _temporaryFstPath(String name) => '$_tempDumpDir/temp_wave_$name.fst'; + +// ─── Public helpers used by sibling tests (e.g. config_test.dart) ──────────── + +/// Directory into which sibling tests place their temporary waveform dumps. +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 [WaveformService] to [module] to VCD with [name]. +void createTemporaryDump(Module module, String name) { + Directory(tempDumpDir).createSync(recursive: true); + WaveformService( + module, + outputDirectory: tempDumpDir, + outputBaseName: 'temp_dump_$name', + ); +} + +/// Deletes the VCD file previously created by [createTemporaryDump]. +void deleteTemporaryDump(String name) { + final tmpDumpFile = temporaryDumpPath(name); + File(tmpDumpFile).deleteSync(); +} + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + test('registers with ModuleServices by default', () async { + final a = Logic(name: 'a'); + final mod = _SimpleWaveModule(a); + await mod.build(); + + Directory(_tempDumpDir).createSync(recursive: true); + final dumpPath = _temporaryVcdPath('serviceRegistration'); + + WaveformService.fromOutputPath(mod, outputPath: dumpPath); + + final service = ModuleServices.instance.lookup(); + expect(service, isNotNull); + final waveformJson = jsonEncode(service!.toJson()); + expect(waveformJson, contains('"format":"vcd"')); + + File(dumpPath).deleteSync(); + }); + + test('captures waveform to VCD output path', () async { + final a = Logic(name: 'a'); + final mod = _SimpleWaveModule(a); + await mod.build(); + + Directory(_tempDumpDir).createSync(recursive: true); + final dumpPath = _temporaryVcdPath('serviceCapture'); + + WaveformService.fromOutputPath(mod, outputPath: dumpPath, register: false); + + a.inject(1); + Simulator.registerAction(10, () => a.put(0)); + await Simulator.run(); + + final vcdContents = File(dumpPath).readAsStringSync(); + expect( + VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), + equals(true), + ); + expect( + VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), + equals(true), + ); + + File(dumpPath).deleteSync(); + }); + + test('captures waveform to FST format', () async { + final a = Logic(name: 'a'); + final mod = _SimpleWaveModule(a); + await mod.build(); + + Directory(_tempDumpDir).createSync(recursive: true); + final dumpPath = _temporaryFstPath('fstCapture'); + + WaveformService.fromOutputPath( + mod, + outputPath: dumpPath, + format: WaveOutputFormat.fst, + register: false, + ); + + a.inject(1); + Simulator.registerAction(10, () => a.put(0)); + await Simulator.run(); + + final fstFile = File(dumpPath); + expect(fstFile.existsSync(), isTrue); + expect(fstFile.lengthSync(), greaterThan(100)); + + fstFile.deleteSync(); + }); + + test('VCD and FST contain matching value-change events', () async { + final vcdPath = _temporaryVcdPath('parity'); + final fstPath = _temporaryFstPath('parity'); + + await _dumpParityWaveform(vcdPath, WaveOutputFormat.vcd); + final vcdEvents = _readVcdEvents(vcdPath, const {'a', 'b'}); + + await Simulator.reset(); + ModuleServices.instance.reset(); + + await _dumpParityWaveform(fstPath, WaveOutputFormat.fst); + final fstEvents = _readFstEvents( + fstPath, + signalNames: const ['a', 'b'], + signalWidths: const [4, 4], + ); + + expect(fstEvents, equals(vcdEvents)); + + File(vcdPath).deleteSync(); + File(fstPath).deleteSync(); + }); +} + +Future _dumpParityWaveform( + String outputPath, WaveOutputFormat format) async { + Directory(_tempDumpDir).createSync(recursive: true); + + final a = Logic(name: 'a', width: 4); + final mod = _SimpleWaveModule(a); + await mod.build(); + + a.put(0x1); + WaveformService.fromOutputPath( + mod, + outputPath: outputPath, + format: format, + register: false, + ); + + Simulator.registerAction(10, () => a.put(0x2)); + Simulator.registerAction(20, () => a.put(0xf)); + await Simulator.run(); +} + +Map> _readVcdEvents( + String path, + Set signalNames, +) { + final lines = File(path).readAsLinesSync(); + final markerToSignal = {}; + final markerToWidth = {}; + final events = >{ + for (final name in signalNames) name: {}, + }; + + final sigNameRegexp = RegExp( + r'\s*\$var\s(wire|reg)\s(\d+)\s(\S*)\s(\S*)\s+(\[\d+\:\d+\])?\s*\$end', + ); + var currentTime = 0; + var inValues = false; + + for (final line in lines) { + final match = sigNameRegexp.firstMatch(line); + if (match != null) { + final width = int.parse(match.group(2)!); + final marker = match.group(3)!; + final name = match.group(4)!; + if (signalNames.contains(name)) { + markerToSignal[marker] = name; + markerToWidth[marker] = width; + } + continue; + } + + if (line == r'$dumpvars') { + inValues = true; + continue; + } + if (!inValues) { + continue; + } + if (line == r'$end') { + continue; + } + if (line.startsWith('#')) { + currentTime = int.parse(line.substring(1)); + continue; + } + + final parsed = _parseVcdValueUpdate(line, markerToWidth); + if (parsed == null) { + continue; + } + + final signalName = markerToSignal[parsed.marker]; + if (signalName != null) { + events[signalName]![currentTime] = parsed.value; + } + } + + return events; +} + +({String marker, String value})? _parseVcdValueUpdate( + String line, + Map markerToWidth, +) { + if (line.startsWith('b')) { + final parts = line.split(' '); + if (parts.length != 2 || !markerToWidth.containsKey(parts[1])) { + return null; + } + return (marker: parts[1], value: parts[0].substring(1)); + } + + for (final marker in markerToWidth.keys) { + if (line.endsWith(marker)) { + return (marker: marker, value: line[0]); + } + } + return null; +} + +Map> _readFstEvents( + String path, { + required List signalNames, + required List signalWidths, +}) { + final data = File(path).readAsBytesSync(); + final events = >{ + for (final name in signalNames) name: {}, + }; + + var blockOffset = 0; + while (blockOffset < data.length) { + final blockType = data[blockOffset]; + final sectionLength = _readU64(data, blockOffset + 1); + final blockEnd = blockOffset + 1 + sectionLength; + + if (blockType == 8) { + _readFstVcDataBlock( + data, + blockOffset, + blockEnd, + signalNames: signalNames, + signalWidths: signalWidths, + events: events, + ); + } + + blockOffset = blockEnd; + } + + return events; +} + +void _readFstVcDataBlock( + Uint8List data, + int blockOffset, + int blockEnd, { + required List signalNames, + required List signalWidths, + required Map> events, +}) { + final startTime = _readU64(data, blockOffset + 9); + var offset = blockOffset + 33; + + final frameUncompressed = _readVarint(data, offset); + offset = frameUncompressed.next; + final frameCompressed = _readVarint(data, offset); + offset = frameCompressed.next; + final maxHandle = _readVarint(data, offset); + offset = maxHandle.next; + + final frameBytes = _inflateIfNeeded( + data.sublist(offset, offset + frameCompressed.value), + frameUncompressed.value, + ); + offset += frameCompressed.value; + + var frameOffset = 0; + for (var i = 0; i < signalNames.length; i++) { + final width = signalWidths[i]; + final value = String.fromCharCodes( + frameBytes.sublist(frameOffset, frameOffset + width)); + frameOffset += width; + events[signalNames[i]]![startTime] = value; + } + + final valueMaxHandle = _readVarint(data, offset); + offset = valueMaxHandle.next; + final valueSectionStart = offset; + offset++; // pack_type + + final timeCount = _readU64(data, blockEnd - 8); + final timeCompressedLength = _readU64(data, blockEnd - 16); + final timeUncompressedLength = _readU64(data, blockEnd - 24); + final timeDataStart = blockEnd - 24 - timeCompressedLength; + final timeBytes = _inflateIfNeeded( + data.sublist(timeDataStart, timeDataStart + timeCompressedLength), + timeUncompressedLength, + ); + final timeTable = _decodeTimeTable(timeBytes, timeCount); + + final chainLength = _readU64(data, timeDataStart - 8); + final chainStart = timeDataStart - 8 - chainLength; + final signalOffsets = _decodeFstOffsetChain( + data.sublist(chainStart, timeDataStart - 8), + valueMaxHandle.value, + ); + + for (var signalIndex = 0; signalIndex < signalNames.length; signalIndex++) { + final signalOffset = signalOffsets[signalIndex]; + if (signalOffset == null) { + continue; + } + + final nextOffset = signalOffsets + .skip(signalIndex + 1) + .whereType() + .cast() + .firstWhere((offset) => offset != null, orElse: () => null); + final signalDataStart = valueSectionStart + signalOffset; + final signalDataEnd = + nextOffset == null ? chainStart : valueSectionStart + nextOffset; + _decodeFstSignalData( + data.sublist(signalDataStart, signalDataEnd), + width: signalWidths[signalIndex], + signalName: signalNames[signalIndex], + timeTable: timeTable, + events: events, + ); + } +} + +List _decodeTimeTable(Uint8List bytes, int count) { + final times = []; + var offset = 0; + var previousTime = 0; + for (var i = 0; i < count; i++) { + final delta = _readVarint(bytes, offset); + offset = delta.next; + previousTime += delta.value; + times.add(previousTime); + } + return times; +} + +List _decodeFstOffsetChain(Uint8List bytes, int maxHandle) { + final offsets = List.filled(maxHandle, null); + var byteOffset = 0; + var signalIndex = 0; + var previousOffset = 0; + + while (signalIndex < maxHandle && byteOffset < bytes.length) { + final encoded = _readSignedVarint(bytes, byteOffset); + byteOffset = encoded.next; + if (encoded.value.isEven) { + signalIndex += encoded.value >> 1; + } else { + previousOffset += encoded.value >> 1; + offsets[signalIndex] = previousOffset; + signalIndex++; + } + } + + return offsets; +} + +void _decodeFstSignalData( + Uint8List bytes, { + required int width, + required String signalName, + required List timeTable, + required Map> events, +}) { + var offset = 0; + final compression = _readVarint(bytes, offset); + offset = compression.next; + expect(compression.value, equals(0), + reason: 'Only uncompressed signal chains are expected'); + + var timeIndex = 0; + while (offset < bytes.length) { + if (width == 1) { + final encoded = _readVarint(bytes, offset); + offset = encoded.next; + String value; + int timeDelta; + if (encoded.value.isEven) { + value = ((encoded.value >> 1) & 1).toString(); + timeDelta = encoded.value >> 2; + } else { + const rcvChars = 'xzhuwl-?'; + value = rcvChars[(encoded.value >> 1) & 0x7]; + timeDelta = encoded.value >> 4; + } + timeIndex += timeDelta; + events[signalName]![timeTable[timeIndex]] = value; + } else { + final encoded = _readVarint(bytes, offset); + offset = encoded.next; + timeIndex += encoded.value >> 1; + + final isFourState = encoded.value.isOdd; + String value; + if (isFourState) { + value = String.fromCharCodes(bytes.sublist(offset, offset + width)); + offset += width; + } else { + final byteCount = (width + 7) ~/ 8; + final packed = bytes.sublist(offset, offset + byteCount); + offset += byteCount; + value = _unpackTwoStateBits(packed, width); + } + + events[signalName]![timeTable[timeIndex]] = value; + } + } +} + +String _unpackTwoStateBits(Uint8List bytes, int width) { + final bits = StringBuffer(); + for (var i = 0; i < width; i++) { + final byteIndex = i ~/ 8; + final bitIndex = 7 - (i % 8); + bits.write(((bytes[byteIndex] >> bitIndex) & 1).toString()); + } + return bits.toString(); +} + +Uint8List _inflateIfNeeded(Uint8List bytes, int uncompressedLength) { + if (bytes.length == uncompressedLength) { + return bytes; + } + return Uint8List.fromList(ZLibCodec().decode(bytes)); +} + +({int value, int next}) _readVarint(Uint8List data, int offset) { + var value = 0; + var shift = 0; + var next = offset; + + while (true) { + final byte = data[next++]; + value |= (byte & 0x7f) << shift; + if ((byte & 0x80) == 0) { + return (value: value, next: next); + } + shift += 7; + } +} + +({int value, int next}) _readSignedVarint(Uint8List data, int offset) { + var value = 0; + var shift = 0; + var next = offset; + late int byte; + + do { + byte = data[next++]; + value |= (byte & 0x7f) << shift; + shift += 7; + } while ((byte & 0x80) != 0); + + if (shift < 64 && (byte & 0x40) != 0) { + value |= -(1 << shift); + } + + return (value: value, next: next); +} + +int _readU64(Uint8List data, int offset) { + var result = 0; + for (var i = 0; i < 8; i++) { + result = (result << 8) | data[offset + i]; + } + return result; +} diff --git a/tool/generate_gate_catalog.dart b/tool/generate_gate_catalog.dart new file mode 100644 index 000000000..120a9cb54 --- /dev/null +++ b/tool/generate_gate_catalog.dart @@ -0,0 +1,67 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// generate_gate_catalog.dart +// Regenerates the checked-in gate-catalog netlist asset +// (`test/fixtures/gate_catalog.rohd.json`) from `GateCatalog` (see +// `test/fixtures/gate_catalog_module.dart`) using the default +// [NetlistSynthesizerConfiguration]. +// +// Usage: +// dart run tool/generate_gate_catalog.dart +// +// After regenerating, review the diff to `test/fixtures/gate_catalog.rohd.json` +// before committing it, and re-run `dart test test/gate_catalog_test.dart` to +// confirm the fixture, determinism, and coverage checks all pass. +// +// 2026 August 20 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:rohd/rohd.dart'; + +import '../test/fixtures/gate_catalog_module.dart'; + +/// Relative (to the package root) output path for the generated fixture. +const _fixturePath = 'test/fixtures/gate_catalog.rohd.json'; + +/// Builds a fresh [GateCatalog] with deterministic, freshly-allocated input +/// signals. +/// +/// This must stay in sync with `_buildCatalog()` in +/// `test/gate_catalog_test.dart` so that the fixture this tool generates is +/// exactly what that test's byte-for-byte comparison expects. +GateCatalog _buildCatalog() => GateCatalog( + clk: Logic(name: 'clk'), + en: Logic(name: 'en'), + reset: Logic(name: 'reset'), + muxSel: Logic(name: 'muxSel'), + enableTri: Logic(name: 'enableTri'), + a4: Logic(name: 'a4', width: 4), + b4: Logic(name: 'b4', width: 4), + a8: Logic(name: 'a8', width: 8), + b8: Logic(name: 'b8', width: 8), + d4: Logic(name: 'd4', width: 4), + shamt4: Logic(name: 'shamt4', width: 4), + idx3: Logic(name: 'idx3', width: 3), + idx5: Logic(name: 'idx5', width: 5), + resetValueDyn4: Logic(name: 'resetValueDyn4', width: 4), + busNet: LogicNet(name: 'busNet', width: 8), + ); + +Future main() async { + final catalog = _buildCatalog(); + await catalog.build(); + + final synth = NetlistSynthesizer(); + final json = synth.synthesizeToJson(catalog); + + final fixtureFile = File(_fixturePath); + fixtureFile.parent.createSync(recursive: true); + fixtureFile.writeAsStringSync(json); + + stdout.writeln('Wrote $_fixturePath (${json.length} bytes).'); + + await Simulator.reset(); +} diff --git a/tool/gh_actions/check_tmp_test.sh b/tool/gh_actions/check_tmp_test.sh index a21154d8d..b506317ea 100755 --- a/tool/gh_actions/check_tmp_test.sh +++ b/tool/gh_actions/check_tmp_test.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2022-2024 Intel Corporation +# Copyright (C) 2022-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # check_tmp_test.sh diff --git a/tool/gh_actions/install_node.sh b/tool/gh_actions/install_node.sh index 8e1b5b74b..01a29748a 100755 --- a/tool/gh_actions/install_node.sh +++ b/tool/gh_actions/install_node.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # install_node.sh