diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index f2ed2cf65..be0e32b16 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -61,9 +61,18 @@ jobs: - name: Install software - Icarus Verilog run: tool/gh_actions/install_iverilog.sh + - name: Install software - Accellera SystemC + run: tool/gh_actions/install_systemc.sh + + - name: Pre-build SystemC PCH and Makefile + run: tool/gh_actions/setup_systemc_pch.sh + - name: Run project tests run: tool/gh_actions/run_tests.sh + - name: Clean SystemC temporary files + run: tool/gh_actions/cleanup_systemc_tmp.sh + - name: Check temporary test files run: tool/gh_actions/check_tmp_test.sh @@ -71,7 +80,10 @@ jobs: - name: Build dev container and run tests in it uses: devcontainers/ci@v0.3 with: - runCmd: tool/gh_actions/run_tests.sh + runCmd: | + tool/gh_actions/run_tests.sh + tool/gh_actions/cleanup_systemc_tmp.sh + tool/gh_actions/check_tmp_test.sh deploy-documentation: name: Deploy Documentation @@ -121,4 +133,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/README.md b/README.md index a996ccccb..1863edac9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ [![Chat](https://img.shields.io/discord/1001179329411166267?label=Chat)](https://discord.gg/jubxF84yGw) [![License](https://img.shields.io/badge/License-BSD--3-blue)](https://github.com/intel/rohd/blob/main/LICENSE) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/intel/rohd/blob/main/CODE_OF_CONDUCT.md) -[![Coverage](https://raw.githubusercontent.com/intel/rohd/refs/heads/badges/coverage/main.svg)](https://github.com/intel/rohd/blob/main/.github/workflows/coverage.yml) ROHD (pronounced like "road") is a framework for describing and verifying hardware in the Dart programming language. @@ -45,7 +44,7 @@ You can also open this repository in a GitHub Codespace to run the example in yo - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. @@ -69,5 +68,6 @@ One of ROHD's goals is to help grow an open-source community around reusable har ROHD is under active development. If you're interested in contributing, have feedback or a question, or found a bug, please see [CONTRIBUTING.md](https://github.com/intel/rohd/blob/main/CONTRIBUTING.md). ---------------- + Copyright (C) 2021-2026 Intel Corporation SPDX-License-Identifier: BSD-3-Clause 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..901372836 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,9 @@ +tags: + benchmark: + timeout: 2x + ffi: + # Tests requiring dart:ffi and native shared libraries. + +presets: + no-ffi: + exclude_tags: ffi diff --git a/doc/architecture.md b/doc/architecture.md index cc1e775ae..aacf29c35 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -24,7 +24,7 @@ The `Simulator` acts as a statically accessible driver of the overall simulation ### Synthesizer -A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog. +A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog or SystemC. ## Organization @@ -44,7 +44,7 @@ Contains a collection of `Module` implementations that can be used as primitive ### Synthesizers -Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types (including those not yet supported). +Contains logic for synthesizing `Module`s into some output (e.g. SystemVerilog, SystemC). It is structured to maximize reusability across different output types. ### Utilities diff --git a/doc/tutorials/README.md b/doc/tutorials/README.md index 385529ba9..734081d63 100644 --- a/doc/tutorials/README.md +++ b/doc/tutorials/README.md @@ -78,7 +78,7 @@ Below are the ROHD tutorial content page, you can also find ROHD tutorial in our - [Shift Register](./chapter_7/00_sequential_logic.md#shift-register) - [ROHD Simulator](./chapter_7/00_sequential_logic.md#rohd-simulator) - [Unit Test in Sequential Logic](./chapter_7/00_sequential_logic.md#unit-test-in-sequential-logic) -- [Wave Dumper](./chapter_7/00_sequential_logic.md#wave-dumper) +- [Waveform Dumping](./chapter_7/00_sequential_logic.md#waveform-dumping) - [Exercise](./chapter_7/00_sequential_logic.md#exercise) ## Chapter 8: Abstractions 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..a25a7ca10 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -5,7 +5,7 @@ last_modified_at: 2023-11-13 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. +Hardware in ROHD is convertible to an output format via `Synthesizer`s. The most popular output format is SystemVerilog, with SystemC also available. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog or SystemC with structure, hierarchy, ports, and names maintained. The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: @@ -28,6 +28,26 @@ void main() async { The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. +## SystemC generation + +ROHD can also generate SystemC (C++ with the SystemC library) from the same hardware description. Use the `generateSystemC` helper method: + +```dart +void main() async { + final myModule = MyModule(); + await myModule.build(); + + final generatedSc = myModule.generateSystemC(); + + // write it to a file + File('myHardware.h').writeAsStringSync(generatedSc); +} +``` + +The generated SystemC uses `SC_MODULE`, `SC_METHOD`, and `SC_CTHREAD` constructs. Combinational logic becomes `SC_METHOD` processes, sequential logic (flip-flops and `Sequential` blocks) sharing the same clock and reset are consolidated into a single `SC_CTHREAD`, and sub-modules are instantiated with port bindings. All signal types map to SystemC equivalents (`bool`, `sc_uint`, `sc_biguint`). + +For more control over SystemC generation, use `SynthBuilder` with `SystemCSynthesizer()` 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: @@ -82,3 +102,114 @@ The `Naming.unpreferredName` function will modify a signal name to indicate to d ## More advanced generation 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. + +## 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, +); +``` + +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/01-overview.md b/doc/user_guide/_get-started/01-overview.md index c1a98cdc1..c30c9f87f 100644 --- a/doc/user_guide/_get-started/01-overview.md +++ b/doc/user_guide/_get-started/01-overview.md @@ -19,7 +19,7 @@ Features of ROHD include: - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/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..6ddf4c297 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,7 +13,7 @@ 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/synthesizers/systemc/systemc.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; @@ -118,7 +118,7 @@ abstract class Module { ..._inputs.values, ..._outputs.values, ..._inOuts.values, - ...internalSignals, + ...internalSignals ]); /// Accesses the [Logic] associated with this [Module]s [input] port @@ -341,7 +341,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. @@ -1133,39 +1133,101 @@ abstract class Module { /// [hierarchy], this is only valid after [build] has been called. String get hierarchicalName => _hierarchyListToString(hierarchy()); + /// Generates synthesized SystemVerilog for this [Module]. + /// + /// 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. + /// + /// For additional output controls and access to synthesis results, use + /// [SystemVerilogService] directly. + SystemVerilogService dumpSystemVerilog({ + String? outputPath, + bool multiFile = false, + SystemVerilogSynthesizerConfiguration configuration = + const SystemVerilogSynthesizerConfiguration(), + }) { + final service = SystemVerilogService( + this, + outputDirectory: multiFile ? outputPath ?? '.' : '.', + multiFile: multiFile, + configuration: configuration, + ); + if (outputPath != null) { + if (multiFile) { + service.writeOutputs(); + } else { + service.writeLegacyOutputPath(outputPath); + } + } + return service; + } + + /// 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. + /// 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; + + /// Returns a synthesized SystemC version of this [Module]. + /// + /// Generates SystemC code that is equivalent to the hardware described by + /// this module, using the same naming strategy as [dumpSystemVerilog]. + String generateSystemC() { if (!_hasBuilt) { throw ModuleNotBuiltException(this); } - final synthHeader = ''' -/** - * Generated by ROHD - www.github.com/intel/rohd - * Generation time: ${Timestamper.stamp()} - * ROHD Version: ${Config.version} - */ - -'''; - return synthHeader + - SynthBuilder( - this, - SystemVerilogSynthesizer(configuration: configuration), - ).getSynthFileContents().join('\n\n////////////////////\n\n'); + final synthBuilder = SynthBuilder(this, SystemCSynthesizer()); + final moduleContents = + synthBuilder.getSynthFileContents().map((e) => e.contents).join('\n'); + return '// Generated by ROHD - www.github.com/intel/rohd\n' + '// Generation time: ${Timestamper.stamp()}\n' + '// ROHD Version: ${Config.version}\n' + '\n' + '#include \n' + '\n' + '$moduleContents'; } } -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/modules/conditionals/sequential.dart b/lib/src/modules/conditionals/sequential.dart index 5c8a63f40..65bbc27d1 100644 --- a/lib/src/modules/conditionals/sequential.dart +++ b/lib/src/modules/conditionals/sequential.dart @@ -135,6 +135,14 @@ class Sequential extends Always { /// The input edge triggers used in this block. final List<_SequentialTrigger> _triggers = []; + /// Returns the edge polarity for each trigger input port. + /// + /// Each entry pairs the trigger input port name with whether the trigger + /// fires on a positive edge (`true`) or negative edge (`false`). + List<({String portName, bool isPosedge})> get triggerEdges => _triggers + .map((t) => (portName: t.signal.name, isPosedge: t.isPosedge)) + .toList(); + /// When `false`, an [SignalRedrivenException] will be thrown during /// simulation if the same signal is driven multiple times within this /// [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/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart new file mode 100644 index 000000000..854572236 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesizer.dart +// Definition for SystemC Synthesizer +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; + +/// A [Synthesizer] which generates equivalent SystemC as the given [Module]. +/// +/// Attempts to maintain signal naming and structure as much as possible, +/// using the same naming strategy as the SystemVerilog synthesizer. +class SystemCSynthesizer extends Synthesizer { + @override + bool generatesDefinition(Module module) => + // SystemC generation must still detect legacy SV-backed custom modules + // until the upstream API is migrated away from this deprecated type. + // ignore: deprecated_member_use_from_same_package + !((module is CustomSystemVerilog) || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none)); + + @override + SynthesisResult synthesize(Module module, + String Function(Module module) getInstanceTypeOfModule) => + SystemCSynthesisResult(module, getInstanceTypeOfModule); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart new file mode 100644 index 000000000..e670279d4 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_module_definition.dart +// Definition for SystemCSynthModuleDefinition +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A special [SynthModuleDefinition] for SystemC modules. +class SystemCSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a new [SystemCSynthModuleDefinition] for the given [module]. + SystemCSynthModuleDefinition(super.module); + + @override + void process() { + // For now, do not collapse inline modules. Each InlineSystemVerilog gate + // remains as a sub-module instantiation and gets emitted as an assign-style + // expression in the generated SystemC (similar to SV `assign x = a & b`). + // + // Future: implement chain-collapsing for compound expressions. + } + + @override + SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => + SystemCSynthSubModuleInstantiation(m); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart new file mode 100644 index 000000000..7e692ff8a --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_sub_module_instantiation.dart +// Definition for SystemCSynthSubModuleInstantiation +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Represents a submodule instantiation for SystemC. +class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { + /// Creates a new [SystemCSynthSubModuleInstantiation] for the given + /// [module]. + SystemCSynthSubModuleInstantiation(super.module); + + /// If [module] is [InlineSystemVerilog], this will be the [SynthLogic] that + /// is the `result` of that module. Otherwise, `null`. + SynthLogic? get inlineResultLogic => module is! InlineSystemVerilog + ? null + : (outputMapping[(module as InlineSystemVerilog).resultSignalName] ?? + inOutMapping[(module as InlineSystemVerilog).resultSignalName]); + + /// Mapping from [SynthLogic]s which are outputs of inlineable modules to + /// those inlineable modules. + Map? + synthLogicToInlineableSynthSubmoduleMap; + + /// Provides a mapping from ports of this module to a string that can be fed + /// into that port, which may include inline expressions. + Map _modulePortsMapWithInline( + Map plainPorts) => + plainPorts.map((name, synthLogic) => MapEntry( + name, + synthLogicToInlineableSynthSubmoduleMap?[synthLogic] + ?.inlineSystemC() ?? + (synthLogic.declarationCleared ? '' : synthLogic.name))); + + /// Provides the inline SystemC expression for this module. + /// + /// Should only be called if [module] is [InlineSystemVerilog]. + String inlineSystemC() { + final portNameToValueMapping = _modulePortsMapWithInline( + {...inputMapping, ...inOutMapping} + ..remove((module as InlineSystemVerilog).resultSignalName), + ); + + final inlineRepresentation = + _inlineSystemCExpression(portNameToValueMapping); + + return '($inlineRepresentation)'; + } + + /// Generates the inline SystemC expression for the gate module. + String _inlineSystemCExpression(Map inputs) { + final m = module; + + if (m is NotGate) { + final inVal = inputs.values.first; + return '~$inVal'; + } else if (m is And2Gate) { + return '${inputs.values.first} & ${inputs.values.last}'; + } else if (m is Or2Gate) { + return '${inputs.values.first} | ${inputs.values.last}'; + } else if (m is Xor2Gate) { + return '${inputs.values.first} ^ ${inputs.values.last}'; + } else if (m is Mux) { + // Mux has inputs: control, d0, d1 → output: y + // In SystemC: control ? d1 : d0 + final entries = inputs.entries.toList(); + final control = entries[0].value; + final d0 = entries[1].value; + final d1 = entries[2].value; + return '$control ? $d1 : $d0'; + } else if (m is InlineSystemVerilog) { + // Fallback: use the verilog inline expression as a reasonable + // approximation (many operators are identical between SV and C++) + return m.inlineVerilog(inputs); + } + + throw SynthException('Unsupported inline module type: ${m.runtimeType}'); + } + + /// Provides the full SystemC instantiation for this module as a member + /// declaration and port binding in the constructor. + /// + /// Returns null if this module does not need instantiation. + String? memberDeclaration(String instanceType) { + if (!needsInstantiation) { + return null; + } + return '$instanceType $name{"$name"};'; + } + + /// Generates port binding statements for the constructor body. + String? portBindings() { + if (!needsInstantiation) { + return null; + } + final bindings = []; + final allPorts = {...inputMapping, ...outputMapping, ...inOutMapping}; + for (final entry in allPorts.entries) { + final portName = entry.key; + final synthLogic = entry.value; + if (!synthLogic.declarationCleared) { + bindings.add('$name.$portName(${synthLogic.name});'); + } + } + return bindings.join('\n'); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart new file mode 100644 index 000000000..5bcafc12a --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -0,0 +1,1764 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesis_result.dart +// Definition for SystemCSynthesisResult +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/always.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthesisResult] representing a conversion of a [Module] to SystemC. +class SystemCSynthesisResult extends SynthesisResult { + /// A cached copy of the generated ports. + late final String _portsString; + + /// A cached copy of the generated module body (used for matching). + late final String _moduleBodyString; + + /// The main [SynthModuleDefinition] for this. + final SynthModuleDefinition _synthModuleDefinition; + + @override + List get supportingModules => + _synthModuleDefinition.supportingModules; + + // Cached sections for final assembly + late final String _internalSigs; + late final String _subMembers; + late final String _ctorBody; + late final String _methodBodies; + + /// Creates a new [SystemCSynthesisResult] for the given [module]. + SystemCSynthesisResult(super.module, super.getInstanceTypeOfModule) + : _synthModuleDefinition = SystemCSynthModuleDefinition(module) { + _findClockResetSignals(); + _portsString = _systemCPorts(); + _buildModuleBody(getInstanceTypeOfModule); + _moduleBodyString = '$_ctorBody|$_methodBodies'; + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is SystemCSynthesisResult && + other._portsString == _portsString && + other._moduleBodyString == _moduleBodyString; + + @override + int get matchHashCode => _portsString.hashCode ^ _moduleBodyString.hashCode; + + @override + String toFileContents() => _toSystemC(); + + @override + List toSynthFileContents() => List.unmodifiable([ + SynthFileContents( + name: instanceTypeName, + description: 'SystemC module definition for $instanceTypeName', + contents: _toSystemC(), + ) + ]); + + // ──────────────────────────────────────────────────────────────────── + // Line/column position tracking for debug tracing + // ──────────────────────────────────────────────────────────────────── + + /// SystemC line map: signal/instance name → list of `'line:col'` positions + /// in the generated SystemC output (both 1-based). + /// + /// Each name's list contains the first occurrence (the declaration / port / + /// submodule member line) followed by each assignment LHS line where that + /// name appears on the left of `=` in a method body. Positions are in + /// textual (source) order; consumers that need the "assignments first, + /// declaration last" convention should reorder at emit time. + /// + /// Populated by [_buildScLineMap] after the final text is assembled. + /// Keys match the names used in the FLC trace data: canonical signal + /// names (from [SynthLogic.name]) for signals and + /// [Module.uniqueInstanceName] for submodule instances. + Map> get scLineMap => Map.unmodifiable( + _scLineMap.map((k, v) => MapEntry(k, List.unmodifiable(v)))); + final Map> _scLineMap = {}; + + /// Walks the already-generated [scText] counting newlines, and records + /// the 1-based `line:col` of each signal declaration, port, submodule + /// instance member, and assignment LHS. + /// + /// This mirrors the approach used by the SystemVerilog synthesizer's + /// `_buildSvLineMap` in the `source_debug` branch, enabling the + /// `SignalSourceTracer` to emit FLC data with both SV and SC positions. + void _buildScLineMap(String scText) { + _scLineMap.clear(); + + final targets = { + for (final sig in _synthModuleDefinition.inputs) sig.name, + for (final sig in _synthModuleDefinition.outputs) sig.name, + for (final sig in _synthModuleDefinition.inOuts) sig.name, + for (final sig in _synthModuleDefinition.internalSignals + .where((e) => e.needsDeclaration)) + sig.name, + for (final smi in _synthModuleDefinition.subModuleInstantiations + .where((s) => s.needsInstantiation)) + smi.name, + }; + + if (targets.isEmpty) { + return; + } + + // Single-pass: tokenize each line once, check tokens against target set. + // Record the first occurrence (declaration) and any subsequent occurrence + // that is an assignment LHS (identifier followed by `=` but not `==`). + final identRe = RegExp(r'[A-Za-z_]\w*'); + var lineNum = 1; + var lineStart = 0; + final len = scText.length; + + for (var i = 0; i <= len; i++) { + if (i == len || scText[i] == '\n') { + final lineText = scText.substring(lineStart, i); + for (final match in identRe.allMatches(lineText)) { + final word = match.group(0)!; + if (!targets.contains(word)) { + continue; + } + final pos = '$lineNum:${match.start + 1}'; + final list = _scLineMap[word]; + if (list == null) { + // First occurrence — declaration / port / sub-module member. + _scLineMap[word] = [pos]; + } else if (_isAssignmentLhs(lineText, match.end) && + !list.contains(pos)) { + // Subsequent occurrence on an assignment LHS — record it. + list.add(pos); + } + } + lineNum++; + lineStart = i + 1; + } + } + } + + /// Returns true if the identifier ending at [afterIdent] in [lineText] is + /// followed (after optional whitespace) by a single `=` (and not `==`). + static bool _isAssignmentLhs(String lineText, int afterIdent) { + var j = afterIdent; + while (j < lineText.length && + (lineText.codeUnitAt(j) == 0x20 || lineText.codeUnitAt(j) == 0x09)) { + j++; + } + if (j >= lineText.length || lineText[j] != '=') { + return false; + } + if (j + 1 < lineText.length && lineText[j + 1] == '=') { + return false; + } + return true; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock/reset detection + // ──────────────────────────────────────────────────────────────────── + + /// Internal clock signals promoted to ports (from SimpleClockGenerator). + late final Set _promotedClockSignals; + + /// Pre-scans sub-module instantiations to identify clock/reset signals + /// and internal clocks that should be promoted to ports. + void _findClockResetSignals() { + final promotedClocks = {}; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + // Detect SimpleClockGenerator and promote its output to a port + if (m is SimpleClockGenerator) { + for (final entry in ssmi.outputMapping.entries) { + promotedClocks.add(entry.value.name); + } + } + } + _promotedClockSignals = promotedClocks; + } + + // ──────────────────────────────────────────────────────────────────── + // Type mapping + // ──────────────────────────────────────────────────────────────────── + + /// Sanitize a signal/port name to be a valid C++ identifier. + /// Replaces `[N]` with `_N_` (LogicArray element indexing). + static String _scName(String name) => + name.replaceAllMapped(RegExp(r'\[(\d+)\]'), (m) => '_${m[1]}_'); + + /// Maps a signal width to the appropriate SystemC data type. + static String systemCType(int width) { + if (width == 1) { + return 'bool'; + } else if (width <= 64) { + return 'sc_uint<$width>'; + } else { + return 'sc_biguint<$width>'; + } + } + + /// SystemC input port type for a given width. + static String systemCInType(int width) => 'sc_in<${systemCType(width)}>'; + + /// SystemC output port type for a given width. + static String systemCOutType(int width) => 'sc_out<${systemCType(width)}>'; + + /// SystemC inout port type for a given width. + static String systemCInOutType(int width) => + 'sc_inout<${systemCType(width)}>'; + + /// SystemC signal type for a given width. + static String systemCSignalType(int width) => + 'sc_signal<${systemCType(width)}>'; + + // ──────────────────────────────────────────────────────────────────── + // Port declarations + // ──────────────────────────────────────────────────────────────────── + + String _systemCPorts() { + final lines = []; + for (final sig in _synthModuleDefinition.inputs) { + final n = _scName(sig.name); + lines.add(' ${systemCInType(sig.width)} $n{"$n"};'); + } + // Promote internal clock signals (from SimpleClockGenerator) to ports + for (final clkName in _promotedClockSignals) { + final n = _scName(clkName); + lines.add(' ${systemCInType(1)} $n{"$n"};'); + } + for (final sig in _synthModuleDefinition.outputs) { + final n = _scName(sig.name); + lines.add(' ${systemCOutType(sig.width)} $n{"$n"};'); + } + for (final sig in _synthModuleDefinition.inOuts) { + final n = _scName(sig.name); + lines.add(' ${systemCInOutType(sig.width)} $n{"$n"};'); + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Internal signals + // ──────────────────────────────────────────────────────────────────── + + String _buildInternalSignals() { + final declarations = []; + for (final sig in _synthModuleDefinition.internalSignals + .where((e) => e.needsDeclaration) + .where((e) => !_promotedClockSignals.contains(e.name)) + .sorted((a, b) => a.name.compareTo(b.name))) { + final n = _scName(sig.name); + declarations.add(' ${systemCSignalType(sig.width)} $n{"$n"};'); + } + + // Declare individual signals for array elements that are written to + // (FlipFlop/Sequential outputs targeting array elements) + for (final elemName in _arrayElementsWritten.keys) { + final n = _scName(elemName); + final width = _arrayElementsWritten[elemName]!; + declarations.add(' ${systemCSignalType(width)} $n{"$n"};'); + } + return declarations.join('\n'); + } + + /// Maps array element names (e.g. "delayLine[0]") to their widths. + /// These need separate signal declarations because SystemC can't do + /// partial writes to sc_signal. + late final Map _arrayElementsWritten = + _findArrayElementsWritten(); + + /// Groups array elements by parent: parentName → list of (index, elemWidth). + late final Map> + _arrayElementsByParent = _groupArrayElementsByParent(); + + Map _findArrayElementsWritten() { + final result = {}; + + void addIfArrayElement(SynthLogic sl) { + if (sl is SynthLogicArrayElement) { + result[sl.name] = sl.logic.width; + } + } + + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + + // All submodule output mappings + ssmi.outputMapping.values.forEach(addIfArrayElement); + + // Inline gate result logics + if (ssmi is SystemCSynthSubModuleInstantiation) { + final rl = ssmi.inlineResultLogic; + if (rl != null) { + addIfArrayElement(rl); + } + } + + // Scan conditionals for nested array element receivers + if (m is Combinational) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } else if (m is Sequential) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } + } + + // Wire assignments targeting array elements + for (final assignment in _synthModuleDefinition.assignments) { + addIfArrayElement(assignment.dst); + } + + return result; + } + + /// Recursively walks a conditionals tree to find all receivers that + /// are array elements and adds them to [result]. + void _collectArrayReceiversFromConditionals( + List conditionals, Map result) { + for (final c in conditionals) { + for (final receiver in c.receivers) { + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl is SynthLogicArrayElement && !result.containsKey(sl.name)) { + result[sl.name] = sl.logic.width; + } + } + // Recurse into sub-conditionals + _collectArrayReceiversFromConditionals(c.conditionals, result); + } + } + + /// Groups array elements by their root parent signal, + /// computing flat bit offsets for nested elements. + Map> + _groupArrayElementsByParent() { + final result = >{}; + + void addElement(SynthLogicArrayElement sl) { + // Walk up to root and compute flat bit offset + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex; + if (idx == null) { + return; // pruned element — skip + } + flatOffset += idx * current.logic.width; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = current.name; + + final entry = ( + // Use flat bit offset as "index" for assembly ordering + index: flatOffset, + width: sl.logic.width, + elemName: sl.name, + ); + // Avoid duplicates + final list = result.putIfAbsent(rootName, () => []); + if (!list.any((e) => e.elemName == entry.elemName)) { + list.add(entry); + } + } + + // Use logicToSynthMap to find the SynthLogicArrayElement for each written + // element, rather than re-scanning submodule instantiations. + for (final sl in _synthModuleDefinition.logicToSynthMap.values) { + if (sl is SynthLogicArrayElement && sl.replacement == null) { + // Skip elements whose parent has been pruned or not named + final parent = sl.parentArray.replacement ?? sl.parentArray; + if (parent.declarationCleared) { + continue; + } + if (_arrayElementsWritten.containsKey(sl.name)) { + addElement(sl); + } + } + } + + // Sort each list by flat bit offset + for (final list in result.values) { + list.sort((a, b) => a.index.compareTo(b.index)); + } + return result; + } + + // ──────────────────────────────────────────────────────────────────── + // Inline gate expressions + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if a module is a SystemVerilog gate that generates no + /// definition and should be inlined (like Add). + static bool _isInlinableSystemVerilogGate(Module m) => + m is SystemVerilog && + m is! InlineSystemVerilog && + m is! Always && + m is! FlipFlop && + m.generatedDefinitionType == DefinitionGenerationType.none; + + /// Converts a [SynthLogic] to a SystemC read expression. + /// Constants become typed literals; signals get `.read()`. + /// Array elements become range expressions on their parent. + static String _synthLogicReadExpr(SynthLogic sl) { + if (sl.isConstant) { + final c = sl.logics.whereType().first; + return _typedConstExpr(c.value, c.width); + } + if (sl is SynthLogicArrayElement) { + return _arrayElementReadExpr(sl); + } + return '${_scName(sl.name)}.read()'; + } + + /// Generates a typed constant expression for SystemC. + /// Handles x/z values by treating them as 0. + static String _typedConstExpr(LogicValue val, int width) { + if (val.isValid) { + if (width == 0) { + return '0'; + } + final bigVal = val.toBigInt(); + if (width > 64) { + // Use hex string constructor for sc_biguint + var hex = bigVal.toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '${systemCType(width)}("0x$hex")'; + } + // For uint64 values above INT64_MAX, add ULL suffix + if (bigVal > (BigInt.one << 63) - BigInt.one) { + return '${systemCType(width)}' + '(${bigVal.toUnsigned(width)}ULL)'; + } + return '${systemCType(width)}(${bigVal.toUnsigned(width)})'; + } + // For values with x/z, use 0 (SystemC doesn't have x/z) + return '${systemCType(width)}(0)'; + } + + /// Generates a range read expression for an array element. e.g. + /// deserialized[0] (8-bit in 32-bit parent) → deserialized.read().range(7, 0) + /// Generates a range read expression for an array element, handling + /// arbitrary nesting depth. e.g. `laIn[2][1]` in a `[3,2]x8` array + /// → `laIn.read().range(47, 40)`. + static String _arrayElementReadExpr(SynthLogicArrayElement sl) { + final elemWidth = sl.logic.width; + + // Walk up the parent chain to find the root signal and accumulate + // the flat bit offset. + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex!; + final w = current.logic.width; + flatOffset += idx * w; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = _scName(current.name); + final rootWidth = current.width; + + final lo = flatOffset; + final hi = lo + elemWidth - 1; + + // If the root is 1-bit (bool), subscript/range is not valid + if (rootWidth == 1) { + return '$rootName.read()'; + } + if (elemWidth == 1) { + return 'static_cast($rootName.read()[$lo])'; + } + final rangeType = elemWidth <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$elemWidth>($rootName.read().range($hi, $lo))'; + } + + /// Returns the sensitivity signal name for a SynthLogic. + /// For array elements, walks up to the root (non-array-element) parent. + static String _sensitivityName(SynthLogic sl) { + var current = sl; + while (current is SynthLogicArrayElement) { + current = current.parentArray.replacement ?? current.parentArray; + } + return _scName(current.name); + } + + /// Generates an SC_METHOD for inline gates (like SV `assign` stmts). + _MethodResult? _buildInlineGates() { + final inlineGates = _synthModuleDefinition.subModuleInstantiations + .where((s) => + s.needsInstantiation && + (s.module is InlineSystemVerilog || + _isInlinableSystemVerilogGate(s.module))) + .cast() + .toList(); + + if (inlineGates.isEmpty) { + return null; + } + + final assignments = <_ScMethodAssignment>[]; + + for (final ssmi in inlineGates) { + final m = ssmi.module; + final sensitivities = {}; + final bodyLines = []; + final destinations = {}; + + // Collect inputs — constants become literals, signals get .read(). + // Inline modules connected to LogicNets can have source ports mapped as + // inouts, so include non-result inout mappings as inputs. + final inputExprs = {}; + final inputMappings = {...ssmi.inputMapping, ...ssmi.inOutMapping}; + if (m is InlineSystemVerilog) { + inputMappings.remove(m.resultSignalName); + } + for (final entry in inputMappings.entries) { + final sl = entry.value; + if (!sl.isConstant) { + sensitivities.add(_sensitivityName(sl)); + } + inputExprs[entry.key] = _synthLogicReadExpr(sl); + } + + if (m is InlineSystemVerilog) { + final resultSynthLogic = ssmi.inlineResultLogic; + if (resultSynthLogic == null) { + continue; + } + final expr = _gateExpression(m, inputExprs); + final dst = _scName(resultSynthLogic.name); + destinations.add(dst); + bodyLines.add(' $dst = $expr;'); + } else if (m is Add) { + // Add has two outputs: sum and carry. + // Emit inline expressions for each used output. + final vals = inputExprs.values.toList(); + final sumPortName = m.sum.name; + for (final entry in ssmi.outputMapping.entries) { + final portName = entry.key; + final dst = _scName(entry.value.name); + destinations.add(dst); + if (portName == sumPortName) { + bodyLines.add(' $dst = ${vals[0]} + ${vals[1]};'); + } else { + // carry: high bit of (width+1)-bit addition + final w = m.width; + final w1 = w + 1; + final utype = systemCType(w1); + final carryExpr = 'static_cast' + '($utype($utype(${vals[0]})' + ' + $utype(${vals[1]}))[$w])'; + bodyLines.add(' $dst = $carryExpr;'); + } + } + } + + if (bodyLines.isEmpty) { + continue; + } + + assignments.add(_ScMethodAssignment( + bodyLines: bodyLines, + sensitivities: sensitivities, + destinations: destinations, + )); + ssmi.clearInstantiation(); + } + + if (assignments.isEmpty) { + return null; + } + + return _emitGroupedAssignments('assign', assignments); + } + + /// Maps an InlineSystemVerilog gate to a C++ expression. + /// + /// Handles all gate types that have SV-specific syntax which needs + /// translation to valid SystemC/C++. + String _gateExpression(InlineSystemVerilog m, Map inputs) { + // ── Single-output bitwise gates (C++ operators identical to SV) ── + if (m is NotGate) { + // For bool (width-1), use logical not; for wider, bitwise not + if ((m as Module).outputs.values.first.width == 1) { + return '!${inputs.values.first}'; + } + return '~${inputs.values.first}'; + } + + // ── Binary operator gates (C++ operators identical to SV) ── + const binaryOps = { + And2Gate: '&', + Or2Gate: '|', + Xor2Gate: '^', + Subtract: '-', + Multiply: '*', + }; + final binOp = binaryOps[m.runtimeType]; + if (binOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $binOp ${vals[1]}'; + } + if (m is Divide || m is Modulo) { + final vals = inputs.values.toList(); + final op = m is Divide ? '/' : '%'; + // Guard against zero divisor (sc_uint defaults to 0 at time-0) + return '(${vals[1]} != 0 ? ${vals[0]} $op ${vals[1]} : 0)'; + } + if (m is Power) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + return '${systemCType(w)}' + '(static_cast' + '(pow(static_cast(${vals[0]}),' + ' static_cast(${vals[1]}))))'; + } + + // ── Comparison (operators identical) ── + const cmpOps = { + Equals: '==', + NotEquals: '!=', + LessThan: '<', + GreaterThan: '>', + LessThanOrEqual: '<=', + GreaterThanOrEqual: '>=', + }; + final cmpOp = cmpOps[m.runtimeType]; + if (cmpOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $cmpOp ${vals[1]}'; + } + + // ── Shifts ── + // Cast shift amount to int to avoid ambiguous overloads. + // Width 1 maps to bool in SystemC (no .to_int()), so use (int) cast. + // Clamp: if shift amount >= operand width, result is 0 (or sign-fill + // for arshift), avoiding .to_int() overflow on huge shift amounts. + if (m is LShift || m is RShift || m is ARShift) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + final outType = systemCType(w); + final shiftAmtWidth = (m as Module).inputs.values.toList()[1].width; + final shiftExpr = + shiftAmtWidth == 1 ? '(int)(${vals[1]})' : '(${vals[1]}).to_int()'; + if (m is ARShift) { + final signedType = w <= 64 ? 'sc_int<$w>' : 'sc_bigint<$w>'; + final shiftOp = '$outType(($signedType(${vals[0]})) >> $shiftExpr)'; + if (shiftAmtWidth > 31) { + // Sign-fill: shift by width-1 to replicate MSB when shift >= width + final overflow = '$outType(($signedType(${vals[0]})) >> ${w - 1})'; + return '(${vals[1]} >= $w) ? $overflow : $shiftOp'; + } + return shiftOp; + } + final op = m is LShift ? '<<' : '>>'; + final shiftOp = '$outType(${vals[0]} $op $shiftExpr)'; + if (shiftAmtWidth > 31) { + return '(${vals[1]} >= $w) ? $outType(0) : $shiftOp'; + } + return shiftOp; + } + + // ── Unary reductions ── + if (m is AndUnary || m is OrUnary || m is XorUnary) { + final inputWidth = (m as Module).inputs.values.first.width; + // 1-bit: reduce is identity (and bool has no .xor_reduce() in SystemC) + if (inputWidth == 1) { + return 'static_cast(${inputs.values.first})'; + } + if (m is AndUnary) { + return '${inputs.values.first}.and_reduce()'; + } else if (m is OrUnary) { + return '${inputs.values.first}.or_reduce()'; + } else { + return '${inputs.values.first}.xor_reduce()'; + } + } + + // ── Bus subset (slice / index) ── + if (m is BusSubset) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + // If input is already 1-bit (bool), extracting bit 0 is identity + if (inputWidth == 1 && m.startIndex == 0 && m.endIndex == 0) { + return a; + } + if (m.startIndex == m.endIndex) { + return 'static_cast($a[${m.startIndex}])'; + } + if (m.startIndex > m.endIndex) { + // Reverse order — build bit-by-bit concat + // bits[0]=a[endIndex], ..., bits[N]=a[startIndex] + // SystemC concat is MSB-first: output MSB = input[endIndex] + // Use sc_uint<1> (not bool) so SystemC concat operator is invoked + final bits = List.generate(m.startIndex - m.endIndex + 1, + (i) => 'sc_uint<1>($a[${m.endIndex + i}])'); + return '(${bits.join(', ')})'; + } + final w = m.endIndex - m.startIndex + 1; + final rangeType = w <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$w>($a.range(${m.endIndex}, ${m.startIndex}))'; + } + + // ── Dynamic bit index ── + if (m is IndexGate) { + final vals = inputs.values.toList(); + return 'static_cast(${vals[0]}[${vals[1]}])'; + } + + // ── Mux (ternary) ── + if (m is Mux) { + final vals = inputs.values.toList(); + final w = m.out.width; + final utype = systemCType(w); + // Cast both branches to avoid C++ ternary type mismatch + // (e.g., when one branch is bool and the other is sc_uint<1>) + return '${vals[0]}' + ' ? $utype(${vals[2]})' + ' : $utype(${vals[1]})'; + } + + // ── Replication ── + if (m is ReplicationOp) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + final outputWidth = m.replicated.width; + final numReps = outputWidth ~/ inputWidth; + if (inputWidth == 1) { + // Single-bit replicate: all-1s or all-0s + final utype = systemCType(outputWidth); + return '$utype(' + '$a ' + '? $utype(-1) ' + ': $utype(0))'; + } + // Multi-bit replicate: concat N copies + final copies = List.filled(numReps, a); + return '(${copies.join(', ')})'; + } + + // ── Swizzle (concatenation) ── + if (m is Swizzle) { + // SystemC concatenation: (sig1, sig2, sig3) + // bool operands must be cast to sc_uint<1> to use SystemC concat + // (otherwise C++ comma operator is invoked instead) + final modInputs = (m as Module).inputs.values.toList(); + final exprList = []; + var i = 0; + for (final expr in inputs.values) { + final w = modInputs[i].width; + if (w == 0) { + i++; + continue; // skip zero-width padding + } + // Wrap 1-bit (bool) operands in sc_uint<1>() for concat + if (w == 1) { + exprList.add('sc_uint<1>($expr)'); + } else { + exprList.add(expr); + } + i++; + } + if (exprList.length == 1) { + return exprList.first; + } + // Swizzle stores inputs LSB-first (in0=LSB), but SystemC concat + // is MSB-first: (msb, ..., lsb). So reverse. + return '(${exprList.reversed.join(', ')})'; + } + + // Fallback: use SV inline (may not be valid C++ — flag for review) + return '/* TODO: ${m.runtimeType} */ ${m.inlineVerilog(inputs)}'; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock / trigger edge resolution + // ──────────────────────────────────────────────────────────────────── + + /// Resolves a trigger [SynthLogic] to the effective clock port and edge. + /// + /// If the trigger signal is a module input port, it can be used directly + /// with `SC_CTHREAD`. If it is an internal signal derived from a [NotGate], + /// the method traces through the inversion chain to find the original port + /// and flips the edge accordingly (`negedge(~clk) = posedge(clk)`). + ({String clockName, bool isPort, bool isPosedge}) _resolveClockAndEdge( + SynthLogic triggerSL, bool isPosedge) { + final sl = triggerSL.replacement ?? triggerSL; + + if (sl.isPort(_synthModuleDefinition.module)) { + return (clockName: sl.name, isPort: true, isPosedge: isPosedge); + } + + // Try to trace through a NotGate inversion + for (final logic in sl.logics) { + final src = logic.srcConnection; + if (src != null && src.parentModule is NotGate) { + final notInput = src.parentModule!.inputs.values.first; + final notInputSrc = notInput.srcConnection; + if (notInputSrc != null) { + final srcSL = _synthModuleDefinition.logicToSynthMap[notInputSrc]; + if (srcSL != null) { + // Inversion flips the edge + return _resolveClockAndEdge(srcSL, !isPosedge); + } + } + } + } + + // Fallback — use the signal as-is (SC_THREAD will be needed) + return (clockName: sl.name, isPort: false, isPosedge: isPosedge); + } + + // ──────────────────────────────────────────────────────────────────── + // Combinational / Sequential processes + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildProcesses() { + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var idx = 0; + + // Collect clocked processes for consolidation by (clock, reset) pair. + // Sequentials and FlipFlops sharing the same clock/reset are merged + // into a single SC_CTHREAD, eliminating repeated async_reset_signal_is. + final clockedGroups = {}; + + for (final ssmi + in _synthModuleDefinition.subModuleInstantiations.toList()) { + ssmi as SystemCSynthSubModuleInstantiation; + final m = ssmi.module; + + if (m is Combinational) { + final name = 'comb_$idx'; + idx++; + + final sensitivities = ssmi.inputMapping.values + .where((sl) => !sl.declarationCleared && !sl.isConstant) + .map(_sensitivityName) + .toSet(); + + setupBuf.writeln(' SC_METHOD($name);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + // Build maps keyed by port name (what verilogContents expects) + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + bodyBuf.writeln(' void $name() {'); + for (final c in m.conditionals) { + bodyBuf.write(_conditionalToSC(c, 2, inputsMap, outputsMap)); + } + bodyBuf + ..writeln(' }') + ..writeln(); + ssmi.clearInstantiation(); + } else if (m is Sequential) { + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset')) + .firstOrNull; + + // Detect async reset: either explicitly via asyncReset flag, or + // implicitly when the reset signal is also listed as a trigger + // (e.g. Sequential.multi([clk, reset], reset: reset, ...)). + final isAsync = m.asyncReset || + (resetEntry != null && + ssmi.inputMapping.entries.any((e) => + e.key.contains('trigger') && + e.value.name == resetEntry.value.name)); + + // Resolve ALL trigger entries to (signalName, edge, isPort). + final triggerEdges = m.triggerEdges; + final triggerEntries = ssmi.inputMapping.entries + .where((e) => e.key.contains('trigger')) + .toList(); + + final resolvedTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + + for (final te in triggerEntries) { + final triggerSL = te.value; + // Skip if this trigger is the async reset signal + if (resetEntry != null && triggerSL.name == resetEntry.value.name) { + continue; + } + // Skip constant triggers (e.g. clk <= Const(0) — never toggles) + if (triggerSL.isConstant) { + continue; + } + final isPosedge = triggerEdges + .where((t) => t.portName == te.key) + .firstOrNull + ?.isPosedge ?? + true; + final resolved = _resolveClockAndEdge(triggerSL, isPosedge); + // Skip if the resolved signal is constant + final resolvedSL = _synthModuleDefinition.logicToSynthMap.values + .where((sl) => sl.replacement == null && !sl.declarationCleared) + .where((sl) => sl.name == resolved.clockName) + .firstOrNull; + if (resolvedSL != null && resolvedSL.isConstant) { + continue; + } + resolvedTriggers.add(( + signalName: resolved.clockName, + isPosedge: resolved.isPosedge, + isPort: resolved.isPort, + )); + } + + // Deduplicate by (signalName, isPosedge) + final seen = {}; + final uniqueTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + for (final t in resolvedTriggers) { + final key = '${t.signalName}|${t.isPosedge}'; + if (seen.add(key)) { + uniqueTriggers.add(t); + } + } + + // Build group key from all trigger signals + reset + final triggerKey = uniqueTriggers + .map((t) => '${t.signalName}:${t.isPosedge}') + .join(','); + final groupKey = '$triggerKey|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: isAsync, + )); + // Add all triggers to the group (dedup handled by emission) + for (final t in uniqueTriggers) { + if (!group.triggers.any((existing) => + existing.signalName == t.signalName && + existing.isPosedge == t.isPosedge)) { + group.triggers.add(t); + } + } + if (isAsync) { + group.isAsyncReset = true; + } + + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + for (final outName in outputsMap.values) { + group.resetLines.add(' $outName = 0;'); + } + final condBuf = StringBuffer(); + for (final c in m.conditionals) { + condBuf.write(_conditionalToSC(c, 3, inputsMap, outputsMap)); + } + group.whileBodyLines.add(condBuf.toString()); + ssmi.clearInstantiation(); + } else if (m is FlipFlop) { + // Resolve port signals via the input/output mapping + final clkSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('clk')) + .value; + final dSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('d')) + .value; + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset') && !e.key.contains('Value')) + .firstOrNull; + final enEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('en')) + .firstOrNull; + final resetValueEntry = ssmi.inputMapping.entries + .where( + (e) => e.key.contains('resetValue') || e.key.contains('Value')) + .firstOrNull; + final qSl = ssmi.outputMapping.values.first; + + final groupKey = + '${clkSl.name}:true|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: m.asyncReset, + )); + // FlipFlop always posedge + if (!group.triggers + .any((t) => t.signalName == clkSl.name && t.isPosedge)) { + group.triggers.add(( + signalName: clkSl.name, + isPosedge: true, + isPort: clkSl.isPort(_synthModuleDefinition.module), + )); + } + if (m.asyncReset) { + group.isAsyncReset = true; + } + + // Reset value + String resetValExpr; + if (resetValueEntry != null) { + resetValExpr = _synthLogicReadExpr(resetValueEntry.value); + } else if (m.constantResetValue != null) { + resetValExpr = m.constantResetValue!.toBigInt().toString(); + } else { + resetValExpr = '0'; + } + group.resetLines.add(' ${_scName(qSl.name)} = $resetValExpr;'); + + // Build the data assignment (with optional enable gate) + final assignExpr = + ' ${_scName(qSl.name)} = ${_synthLogicReadExpr(dSl)};\n'; + final bodyLine = enEntry != null + ? ' if (${_synthLogicReadExpr(enEntry.value)}) {\n' + ' $assignExpr' + ' }\n' + : assignExpr; + + // Wrap in sync reset check if needed + if (resetEntry != null && !m.asyncReset) { + group.whileBodyLines + .add(' if (${_scName(resetEntry.value.name)}.read()) {\n' + ' ${_scName(qSl.name)} = $resetValExpr;\n' + ' } else {\n' + ' $bodyLine' + ' }\n'); + } else { + group.whileBodyLines.add(bodyLine); + } + ssmi.clearInstantiation(); + } + } + + // Emit one SC_CTHREAD or SC_THREAD per (clock, reset) group + for (final group in clockedGroups.values) { + final name = 'clocked_$idx'; + idx++; + + final triggers = group.triggers; + + if (triggers.isEmpty) { + // All triggers were constant — skip this group + continue; + } + + // Determine if we can use SC_CTHREAD: + // - exactly one trigger signal + // - that signal is a port (sc_in) + // - only one edge direction + final distinctSignals = triggers.map((t) => t.signalName).toSet(); + final useCthread = distinctSignals.length == 1 && + triggers.first.isPort && + triggers.length == 1; + + if (useCthread) { + final t = triggers.first; + final clockRef = _scName(t.signalName); + final edge = t.isPosedge ? '.pos()' : '.neg()'; + setupBuf.writeln(' SC_CTHREAD($name, $clockRef$edge);'); + if (group.resetName != null && group.isAsyncReset) { + setupBuf.writeln(' async_reset_signal_is(' + '${_scName(group.resetName!)}, true);'); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' wait();') + ..writeln(' while (true) {'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' wait();') + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } else { + // SC_THREAD with explicit wait on events + setupBuf.writeln(' SC_THREAD($name);'); + + // Build wait expression from all trigger events + String waitExpr; + if (distinctSignals.length == 1) { + // Same signal, but both edges + final sig = _scName(triggers.first.signalName); + final edges = triggers.map((t) => t.isPosedge).toSet(); + if (edges.length == 2) { + waitExpr = '$sig.value_changed_event()'; + } else if (edges.first) { + waitExpr = '$sig.posedge_event()'; + } else { + waitExpr = '$sig.negedge_event()'; + } + } else { + // Multiple distinct trigger signals — OR them together + final eventExprs = []; + for (final t in triggers) { + final sig = _scName(t.signalName); + eventExprs + .add('$sig.${t.isPosedge ? 'posedge' : 'negedge'}_event()'); + } + waitExpr = eventExprs.join(' | '); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' while (true) {') + ..writeln(' wait($waitExpr);'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } + } + + if (setupBuf.isEmpty && bodyBuf.isEmpty) { + return null; + } + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Regular sub-module instantiations + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if the sub-module is handled inline (not a real child + /// instantiation) — i.e. it is an inline gate, Always, FlipFlop, or clock. + static bool _isHandledInline(SystemCSynthSubModuleInstantiation ssmi) => + !ssmi.needsInstantiation || + ssmi.module is InlineSystemVerilog || + ssmi.module is Always || + ssmi.module is FlipFlop || + ssmi.module is SimpleClockGenerator || + _isInlinableSystemVerilogGate(ssmi.module); + + String _buildSubModuleMembers( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + final instanceType = getInstanceTypeOfModule(ssmi.module); + lines.add(' $instanceType ${ssmi.name}{"${ssmi.name}"};'); + } + return lines.join('\n'); + } + + /// Dummy signal declarations needed for unconnected submodule output ports. + /// Populated by [_buildSubModuleBindings]. + final List _unconnectedOutputSignals = []; + + /// Signal declarations for constants bound to submodule input ports. + /// Populated by [_buildSubModuleBindings]. + final List _constInputSignals = []; + + /// Initialization statements for constant signals (in constructor body). + /// Populated by [_buildSubModuleBindings]. + final List _constInputInits = []; + + String _buildSubModuleBindings( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + var unconnIdx = 0; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + + // Bind connected ports (inputs, outputs, inouts) + final allPorts = { + ...ssmi.inputMapping, + ...ssmi.outputMapping, + ...ssmi.inOutMapping, + }; + for (final entry in allPorts.entries) { + if (!entry.value.declarationCleared) { + if (entry.value.isConstant) { + // Constants can't be bound directly to sc_in ports; + // create a signal, initialize it, and bind that. + final constName = _scName('_const_${ssmi.name}' + '_${entry.key}_${_constInputSignals.length}'); + final w = entry.value.width; + final c = entry.value.logics.whereType().first; + final constVal = _typedConstExpr(c.value, c.width); + _constInputSignals + .add(' ${systemCSignalType(w)} $constName{"$constName"};'); + _constInputInits.add(' $constName.write($constVal);'); + lines.add(' ${ssmi.name}.${entry.key}($constName);'); + } else { + lines.add(' ' + '${ssmi.name}.${entry.key}(${_scName(entry.value.name)});'); + } + } + } + + // Bind unconnected ports to dummy signals + // (SystemC requires all sc_in/sc_out ports to be bound) + for (final entry in [ + ...ssmi.outputMapping.entries, + ...ssmi.inputMapping.entries, + ]) { + if (entry.value.declarationCleared) { + final dummyName = '_unused_${ssmi.name}_${entry.key}_$unconnIdx'; + final w = entry.value.width; + _unconnectedOutputSignals + .add(' ${systemCSignalType(w)} $dummyName{"$dummyName"};'); + lines.add(' ${ssmi.name}.${entry.key}($dummyName);'); + unconnIdx++; + } + } + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Wire assignments + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildWireAssignments() { + if (_synthModuleDefinition.assignments.isEmpty) { + return null; + } + + final assignments = <_ScMethodAssignment>[]; + + // Group partial assignments by destination for concatenated writes + final partialsByDst = >{}; + + for (final assignment in _synthModuleDefinition.assignments) { + if (assignment is PartialSynthAssignment) { + partialsByDst + .putIfAbsent(_scName(assignment.dst.name), () => []) + .add(assignment); + } else { + final sensitivities = {}; + if (!assignment.src.isConstant) { + sensitivities.add(_sensitivityName(assignment.src)); + } + final bodyLine = ' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};'; + assignments.add(_ScMethodAssignment( + bodyLines: [bodyLine], + sensitivities: sensitivities, + destinations: {_scName(assignment.dst.name)}, + )); + } + } + + // Emit grouped partial assignments as shift-or concatenation + for (final entry in partialsByDst.entries) { + final dstName = entry.key; + final partials = entry.value + ..sort((a, b) => a.dstLowerIndex.compareTo(b.dstLowerIndex)); + + // Find total width from the destination SynthLogic + final dstWidth = partials.last.dstUpperIndex + 1; + final utype = systemCType(dstWidth); + final parts = []; + final sensitivities = {}; + for (final p in partials) { + if (!p.src.isConstant) { + sensitivities.add(_sensitivityName(p.src)); + } + final srcExpr = _synthLogicReadExpr(p.src); + if (p.dstLowerIndex == 0) { + parts.add('$utype($srcExpr)'); + } else { + parts.add('($utype($srcExpr) << ${p.dstLowerIndex})'); + } + } + assignments.add(_ScMethodAssignment( + bodyLines: [' $dstName = ${parts.join(' | ')};'], + sensitivities: sensitivities, + destinations: {dstName}, + )); + } + + return _emitGroupedAssignments('wire_assign', assignments); + } + + _MethodResult _emitGroupedAssignments( + String methodPrefix, List<_ScMethodAssignment> assignments) { + final groups = <_ScMethodAssignmentGroup>[]; + + for (final assignment in assignments) { + final group = groups.firstWhereOrNull((g) => g.canAdd(assignment)); + if (group == null) { + groups.add(_ScMethodAssignmentGroup()..add(assignment)); + } else { + group.add(assignment); + } + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + + for (var i = 0; i < groups.length; i++) { + final group = groups[i]; + final methodName = '${methodPrefix}_$i'; + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final sig in group.sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(group.bodyLines.join('\n')) + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Conditional → SystemC + // ──────────────────────────────────────────────────────────────────── + + String _conditionalToSC(Conditional conditional, int indent, + Map inputsMap, Map outputsMap) { + final padding = ' ' * indent; + + if (conditional is ConditionalAssign) { + final driverExpr = _resolveDriver(conditional.driver, inputsMap); + final receiver = _resolveReceiver(conditional.receiver, outputsMap); + return '$padding$receiver = $driverExpr;\n'; + } else if (conditional is If) { + return _ifToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is Case) { + return _caseToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is ConditionalGroup) { + final buf = StringBuffer(); + for (final c in conditional.conditionals) { + buf.write(_conditionalToSC(c, indent, inputsMap, outputsMap)); + } + return buf.toString(); + } + return ''; + } + + String _ifToSC(If ifBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (final iff in ifBlock.iffs) { + final header = iff == ifBlock.iffs.first + ? 'if' + : iff is Else + ? ' else' + : ' else if'; + final condition = + iff is! Else ? ' (${_resolveDriver(iff.condition, inputsMap)})' : ''; + buf.write('$padding$header$condition {\n'); + for (final c in iff.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + String _caseToSC(Case caseBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + final expr = _resolveDriver(caseBlock.expression, inputsMap); + + // Check if all case items have compile-time constant values + final allConst = + caseBlock.items.every((item) => _isConstCaseItem(item.value)); + + // CaseZ requires mask matching — always use if/else + // Non-const case items also require if/else + if (caseBlock is CaseZ || !allConst) { + return _caseToIfElseSC(caseBlock, indent, inputsMap, outputsMap, expr); + } + + buf.writeln('${padding}switch ($expr) {'); + for (final item in caseBlock.items) { + buf.writeln('$padding case ${_constLit(item.value)}:'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + if (caseBlock.defaultItem != null) { + buf.writeln('$padding default:'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + buf.writeln('$padding}'); + return buf.toString(); + } + + /// Checks whether a case item value is a compile-time constant. + bool _isConstCaseItem(dynamic value) { + if (value is Const) { + return true; + } + if (value is LogicValue) { + return true; + } + if (value is Logic) { + if (value.srcConnection is Const) { + return true; + } + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + return true; + } + return false; + } + return true; // int, string, etc. + } + + /// Converts a Case/CaseZ block to if/else chain (for non-const items + /// or CaseZ with z-masks). + String _caseToIfElseSC( + Case caseBlock, + int indent, + Map inputsMap, + Map outputsMap, + String expr) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (var i = 0; i < caseBlock.items.length; i++) { + final item = caseBlock.items[i]; + final condition = _caseItemCondition(item.value, expr, inputsMap, + isCaseZ: caseBlock is CaseZ); + final header = i == 0 ? 'if' : ' else if'; + buf.write('$padding$header ($condition) {\n'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + if (caseBlock.defaultItem != null) { + buf.write(' else {\n'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + /// Generates the condition expression for a case item comparison. + String _caseItemCondition( + dynamic value, String expr, Map inputsMap, + {bool isCaseZ = false}) { + // Extract LogicValue from Const for CaseZ mask matching + LogicValue? lv; + if (value is Const) { + lv = value.value; + } else if (value is LogicValue) { + lv = value; + } + if (isCaseZ && lv != null && !lv.isValid) { + // CaseZ: create mask comparison (expr & mask) == pattern + // z bits become don't-care (mask out those bits) + final width = lv.width; + // z→0 in mask, 0/1→1 in mask + var maskStr = ''; + var patStr = ''; + for (var i = width - 1; i >= 0; i--) { + final bit = lv[i]; + if (bit == LogicValue.z || bit == LogicValue.x) { + maskStr += '0'; + patStr += '0'; + } else { + maskStr += '1'; + patStr += bit == LogicValue.one ? '1' : '0'; + } + } + final maskVal = BigInt.parse(maskStr, radix: 2); + final patVal = BigInt.parse(patStr, radix: 2); + return '($expr & $maskVal) == $patVal'; + } + if (value is Logic && value is! Const) { + final resolved = _resolveDriver(value, inputsMap); + return '$expr == $resolved'; + } + return '$expr == ${_constLit(value)}'; + } + + /// Resolves a driver Logic to a SystemC read expression using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveDriver(Logic driver, Map inputsMap) { + if (driver is Const) { + return _constLit(driver); + } + // Look up via logicToSynthMap — the SynthLogic has the canonical name + final sl = _synthModuleDefinition.logicToSynthMap[driver]; + if (sl != null) { + return _synthLogicReadExpr(sl); + } + // Try to find via source connection chain — handles cases where + // the Logic object isn't directly in the map but its source is + var src = driver.srcConnection; + while (src != null) { + final srcSl = _synthModuleDefinition.logicToSynthMap[src]; + if (srcSl != null) { + return _synthLogicReadExpr(srcSl); + } + src = src.srcConnection; + } + // Fallback: try inputsMap by port name + if (inputsMap.containsKey(driver.name)) { + return inputsMap[driver.name]!; + } + return '${_scName(driver.name)}.read()'; + } + + /// Resolves a receiver Logic to a SystemC signal name using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveReceiver(Logic receiver, Map outputsMap) { + // Look up via logicToSynthMap + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl != null) { + return _scName(sl.name); + } + // Fallback + if (outputsMap.containsKey(receiver.name)) { + return outputsMap[receiver.name]!; + } + return _scName(receiver.name); + } + + String _constLit(dynamic value) { + if (value is Const) { + if (value.value.isValid) { + return value.value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is LogicValue) { + if (value.isValid) { + return value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is Logic) { + // If the Logic is driven by a Const, resolve to integer literal + if (value.srcConnection is Const) { + final cv = (value.srcConnection! as Const).value; + return cv.isValid ? cv.toBigInt().toString() : '0'; + } + // Check logicToSynthMap for a constant SynthLogic + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + final constLogic = sl.logics.whereType().firstOrNull; + if (constLogic != null) { + return constLogic.value.isValid + ? constLogic.value.toBigInt().toString() + : '0'; + } + } + // Fallback: use signal read expression + return '${value.name}.read()'; + } + return value.toString(); + } + + // ──────────────────────────────────────────────────────────────────── + // Build all sections + // ──────────────────────────────────────────────────────────────────── + + void _buildModuleBody( + String Function(Module module) getInstanceTypeOfModule) { + _subMembers = _buildSubModuleMembers(getInstanceTypeOfModule); + + final inlineGates = _buildInlineGates(); + final processes = _buildProcesses(); + final wireAssigns = _buildWireAssignments(); + final arrayAssembly = _buildArrayAssemblyMethod(); + final subBindings = _buildSubModuleBindings(getInstanceTypeOfModule); + + // Build internal signals, appending dummy signals for unconnected + // submodule outputs (populated by _buildSubModuleBindings above). + final baseSigs = _buildInternalSignals(); + _internalSigs = [ + baseSigs, + ..._unconnectedOutputSignals, + ..._constInputSignals, + ].where((s) => s.isNotEmpty).join('\n'); + + final ctorParts = [ + if (_constInputInits.isNotEmpty) _constInputInits.join('\n'), + if (inlineGates != null) inlineGates.setup, + if (processes != null) processes.setup, + if (wireAssigns != null) wireAssigns.setup, + if (arrayAssembly != null) arrayAssembly.setup, + if (subBindings.isNotEmpty) subBindings, + ]; + _ctorBody = ctorParts.join(); + + final bodyParts = [ + if (inlineGates != null) inlineGates.body, + if (processes != null) processes.body, + if (wireAssigns != null) wireAssigns.body, + if (arrayAssembly != null) arrayAssembly.body, + ]; + _methodBodies = bodyParts.where((s) => s.isNotEmpty).join('\n'); + } + + /// Builds an SC_METHOD that assembles individual array element signals + /// back into their parent signal via concatenation. + _MethodResult? _buildArrayAssemblyMethod() { + if (_arrayElementsByParent.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + for (final entry in _arrayElementsByParent.entries) { + final parentName = _scName(entry.key); + final elements = entry.value; + final methodName = 'array_assemble_$methodIdx'; + methodIdx++; + + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final elem in elements) { + setupBuf.writeln(' sensitive << ${_scName(elem.elemName)};'); + } + + // Build concatenation: (elem[N-1], ..., elem[1], elem[0]) + // SystemC concat is MSB-first, so highest index first + // Wrap 1-bit (bool) elements in sc_uint<1>() for proper concat + final concatParts = elements.reversed.map((e) { + final read = '${_scName(e.elemName)}.read()'; + return e.width == 1 ? 'sc_uint<1>($read)' : read; + }).toList(); + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' $parentName = (${concatParts.join(', ')});') + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Final assembly + // ──────────────────────────────────────────────────────────────────── + + String _toSystemC() { + final moduleName = getInstanceTypeOfModule(module); + final buf = StringBuffer()..writeln('SC_MODULE($moduleName) {'); + + if (_portsString.isNotEmpty) { + buf.writeln(_portsString); + } + if (_internalSigs.isNotEmpty) { + buf + ..writeln() + ..writeln(_internalSigs); + } + if (_subMembers.isNotEmpty) { + buf + ..writeln() + ..writeln(_subMembers); + } + + buf + ..writeln() + ..writeln(' SC_CTOR($moduleName) {'); + if (_ctorBody.isNotEmpty) { + buf.write(_ctorBody); + } + buf.writeln(' }'); + + if (_methodBodies.isNotEmpty) { + buf + ..writeln() + ..write(_methodBodies) + ..writeln(); + } + + buf.writeln('};'); + final text = buf.toString(); + + _buildScLineMap(text); + + return text; + } +} + +/// Helper to hold a constructor setup string and method body string. +class _MethodResult { + final String setup; + final String body; + const _MethodResult({required this.setup, required this.body}); +} + +class _ScMethodAssignment { + final List bodyLines; + final Set sensitivities; + final Set destinations; + + const _ScMethodAssignment({ + required this.bodyLines, + required this.sensitivities, + required this.destinations, + }); +} + +class _ScMethodAssignmentGroup { + final List bodyLines = []; + final Set sensitivities = {}; + final Set destinations = {}; + + bool canAdd(_ScMethodAssignment assignment) => + !assignment.sensitivities.any(destinations.contains) && + !assignment.destinations.any(sensitivities.contains); + + void add(_ScMethodAssignment assignment) { + bodyLines.addAll(assignment.bodyLines); + sensitivities.addAll(assignment.sensitivities); + destinations.addAll(assignment.destinations); + } +} + +/// Collects clocked process data for consolidation by (clock, reset) pair. +class _ClockedGroupData { + final String? resetName; + bool isAsyncReset; + + /// All distinct trigger events (signal name, edge, and whether it's a port). + final List<({String signalName, bool isPosedge, bool isPort})> triggers = []; + + final List resetLines = []; + final List whileBodyLines = []; + _ClockedGroupData({this.resetName, this.isAsyncReset = false}); +} 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..051adc50b 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -2,22 +2,25 @@ // SPDX-License-Identifier: BSD-3-Clause // // simcompare.dart -// Helper functionality for unit testing (sv testbench generation, iverilog simulation, vectors, checking/comparison, etc.) +// Helper functionality for unit testing (sv testbench generation, +// iverilog simulation, vectors, checking/comparison, etc.) // // 2021 May 7 // Author: Max Korbel -// ignore_for_file: avoid_print - testing lib, printing important for debug - import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; +part 'systemverilog_simcompare.dart'; +part 'systemc_simcompare.dart'; + /// Represents a single test case to check in a single clock cycle. /// /// Useful for testing equivalent behavior in different simulation environments. @@ -43,96 +46,9 @@ class Vector { @override String toString() => '$inputValues => $expectedOutputValues'; - /// Computes a SystemVerilog code string that checks in a SystemVerilog - /// simulation whether a signal [sigName] has the [expected] value given - /// the [inputValues]. - static String _errorCheckString(String sigName, dynamic expected, - LogicValue expectedVal, String inputValues) { - if (expected is! int && - expected is! LogicValue && - expected is! BigInt && - expected is! String) { - throw NonSupportedTypeException(expected); - } - - String expectedHexStr; - if (expected is int) { - expectedHexStr = - BigInt.from(expected).toUnsigned(expectedVal.width).toRadixString(16); - expectedHexStr = '0x$expectedHexStr'; - } else if (expected is BigInt) { - expectedHexStr = expected.toUnsigned(expectedVal.width).toRadixString(16); - expectedHexStr = '0x$expectedHexStr'; - } else { - expectedHexStr = expected.toString(); - } - - final expectedValStr = expectedVal.toString(); - - return 'if($sigName !== $expectedValStr) ' - '\$error(\$sformatf("Expected $sigName=$expectedHexStr,' - ' but found $sigName=0x%x (0b%b) with inputs $inputValues",' - ' $sigName, $sigName));'; - } - /// Converts this vector into a SystemVerilog check. - String toTbVerilog(Module module) { - final assignments = inputValues.keys.map((signalName) { - final signal = module.tryInOut(signalName) ?? module.input(signalName); - - if (signal is LogicArray) { - final arrAssigns = StringBuffer(); - var index = 0; - final fullVal = - LogicValue.of(inputValues[signalName], width: signal.width); - for (final leaf in signal.leafElements) { - final subVal = fullVal.getRange(index, index + leaf.width); - arrAssigns.writeln('${leaf.structureName} = $subVal;'); - index += leaf.width; - } - return arrAssigns.toString(); - } else { - final signalVal = - LogicValue.of(inputValues[signalName], width: signal.width); - return '$signalName = $signalVal;'; - } - }).join('\n'); - - final checksList = []; - for (final expectedOutput in expectedOutputValues.entries) { - final outputName = expectedOutput.key; - final outputPort = - module.tryInOut(outputName) ?? module.output(outputName); - final expected = expectedOutput.value; - final expectedValue = LogicValue.of( - expected, - width: outputPort.width, - ); - final inputStimulus = inputValues.toString(); - - if (outputPort is LogicArray) { - 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)); - index += leaf.width; - } - } else { - checksList.add(_errorCheckString( - outputName, expected, expectedValue, inputStimulus)); - } - } - final checks = checksList.join('\n'); - - final tbVerilog = [ - assignments, - '#$_offset', - checks, - '#${_period - _offset}', - ].join('\n'); - return tbVerilog; - } + String toTbVerilog(Module module) => + _SystemVerilogVectorTestbench(this, module).toTbVerilog(); } /// A utility class for checking a collection of [Vector]s against @@ -202,12 +118,10 @@ abstract class SimCompare { throw NonSupportedTypeException(value); } } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { - Simulator.throwException(err as Exception, stackTrace); - }, - )); + }).catchError(test: (error) => error is Exception, + (Object err, StackTrace stackTrace) { + Simulator.throwException(err as Exception, stackTrace); + })); } }); timestamp += Vector._period; @@ -218,15 +132,6 @@ abstract class SimCompare { await Simulator.run(); } - /// A collection of warnings that are fine to ignore usually. - static final List _knownWarnings = [ - RegExp('sorry: Case unique/unique0 qualities are ignored.'), - RegExp(r'sorry: constant selects in always_\* processes' - ' are not currently supported'), - RegExp('warning: always_comb process has no sensitivities'), - RegExp('finish called at'), - ]; - /// Executes [vectors] against the Icarus Verilog simulator and checks /// that it passes. static void checkIverilogVector( @@ -242,20 +147,20 @@ abstract class SimCompare { bool buildOnly = false, SystemVerilogSynthesizerConfiguration synthesizerConfiguration = const SystemVerilogSynthesizerConfiguration(), - }) { - final result = iverilogVector(module, vectors, + }) => + _SystemVerilogSimCompare.checkIverilogVector( + module, + vectors, moduleName: moduleName, dontDeleteTmpFiles: dontDeleteTmpFiles, dumpWaves: dumpWaves, iverilogExtraArgs: iverilogExtraArgs, allowWarnings: allowWarnings, maskKnownWarnings: maskKnownWarnings, + enableChecking: enableChecking, buildOnly: buildOnly, - synthesizerConfiguration: synthesizerConfiguration); - if (enableChecking) { - expect(result, true); - } - } + synthesizerConfiguration: synthesizerConfiguration, + ); /// Executes [vectors] against the Icarus Verilog simulator. static bool iverilogVector( @@ -270,178 +175,173 @@ abstract class SimCompare { bool buildOnly = false, SystemVerilogSynthesizerConfiguration synthesizerConfiguration = const SystemVerilogSynthesizerConfiguration(), - }) { - if (kIsWeb) { - // if running in web mode, then we can't run icarus verilog - return true; - } - - String signalDeclaration(String signalName, - {String Function(String original)? adjust, - String? signalTypeOverride}) { - final signal = module.signals.firstWhere((e) => e.name == signalName); - - final signalType = signalTypeOverride ?? - ((signal is LogicNet || (signal is LogicArray && signal.isNet)) - ? 'wire' - : 'logic'); - - if (adjust != null) { - signalName = adjust(signalName); - } - - if (signal is LogicArray) { - final unpackedDims = - signal.dimensions.getRange(0, signal.numUnpackedDimensions); - final packedDims = signal.dimensions - .getRange(signal.numUnpackedDimensions, signal.dimensions.length); - final packedDimensions = - packedDims.map((dimension) => '[${dimension - 1}:0]').join(); - final unpackedDimensions = - unpackedDims.map((dimension) => '[${dimension - 1}:0]').join(); - - return '$signalType $packedDimensions' - ' [${signal.elementWidth - 1}:0] $signalName' - '$unpackedDimensions'; - } else if (signal.width != 1) { - return '$signalType [${signal.width - 1}:0] $signalName'; - } else { - return '$signalType $signalName'; - } - } - - final topModule = moduleName ?? module.definitionName; - final allSignals = { - for (final v in vectors) ...v.inputValues.keys, - for (final v in vectors) ...v.expectedOutputValues.keys, - }; - - late final tbWireUniquifier = Uniquifier(); - late final alreadyMappedLogicToWires = {}; - String toTbWireName(String name) => alreadyMappedLogicToWires.putIfAbsent( - name, () => tbWireUniquifier.getUniqueName(initialName: 'wire__$name')); - - final logicToWireMapping = Map.fromEntries(vectors - .map((v) => v.inputValues.keys) - .flattened - .where((name) => module.tryInOut(name) != null) - .map((name) => MapEntry(name, toTbWireName(name)))); - - final localDeclarations = [ - ...allSignals.map((e) { - final sigDecl = signalDeclaration(e, - signalTypeOverride: - logicToWireMapping.containsKey(e) ? 'logic' : null); - return '$sigDecl;'; - }), - ...logicToWireMapping.entries.map((e) { - final logicName = e.key; - final wireName = e.value; - - final sigDecl = signalDeclaration(logicName, - adjust: toTbWireName, signalTypeOverride: 'wire'); - return '$sigDecl; assign $wireName = $logicName;'; - }), - ].join('\n'); - - final moduleConnections = - 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, - ); - - // so that when they run in parallel, they dont step on each other - final uniqueId = - (generatedVerilog + localDeclarations + stimulus + moduleInstance) - .hashCode; - - const dir = 'tmp_test'; - final tmpTestFile = '$dir/tmp_test$uniqueId.sv'; - final tmpOutput = '$dir/tmp_out$uniqueId'; - final tmpVcdFile = '$dir/tmp_waves_$uniqueId.vcd'; - - final waveDumpCode = ''' -\$dumpfile("$tmpVcdFile"); -\$dumpvars(0,dut); -'''; - - final testbench = [ - generatedVerilog, - 'module tb;', - localDeclarations, - moduleInstance, - 'initial begin', - if (dumpWaves) waveDumpCode, - '#1', - stimulus, - r'$finish;', // so the test doesn't run forever if there's a clock gen - 'end', - 'endmodule', - ].join('\n'); + }) => + _SystemVerilogSimCompare.iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration, + ); - Directory(dir).createSync(recursive: true); - File(tmpTestFile).writeAsStringSync(testbench); - final compileResult = Process.runSync('iverilog', - ['-g2012', '-o', tmpOutput, ...iverilogExtraArgs, tmpTestFile]); - bool printIfContentsAndCheckError(dynamic output) { - final maskedOutput = output - .toString() - .split('\n') - .where((element) => element.isNotEmpty) - .map((line) { - for (final knownWarning in _knownWarnings) { - if (knownWarning.hasMatch(line)) { - return null; - } - } - return line; - }) - .nonNulls - .join('\n'); - if (maskedOutput.isNotEmpty) { - print(maskedOutput); - } + /// Cleans up all cached SystemC executables and the precompiled header. + /// Call from `tearDownAll` in tests. + /// + /// If [keepPch] is true (the default), the precompiled header is preserved + /// for faster subsequent runs. Pass `keepPch: false` to remove everything. + static void cleanupSystemCCache({bool keepPch = true}) => + _SystemCSimCompare.cleanupSystemCCache(keepPch: keepPch); - return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), - caseSensitive: false)); - } + /// Compiles a SystemC module into a reusable stdin-driven vector-testbench + /// executable. + /// + /// Returns a [SystemCVectorExecutable] that can be used to run multiple + /// vector sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCVectorExecutable? buildSystemCVectorExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) => + _SystemCSimCompare.buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); - if (printIfContentsAndCheckError(compileResult.stdout)) { - return false; - } - if (printIfContentsAndCheckError(compileResult.stderr)) { - return false; - } + /// Legacy name for [buildSystemCVectorExecutable]. + @Deprecated('Use buildSystemCVectorExecutable instead.') + static SystemCVectorExecutable? buildSystemCExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) => + buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); - if (!buildOnly) { - final simResult = Process.runSync('vvp', [tmpOutput]); - if (printIfContentsAndCheckError(simResult.stdout)) { - return false; - } - if (printIfContentsAndCheckError(simResult.stderr)) { - return false; - } - } + /// Runs [vectors] against a pre-compiled [SystemCVectorExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCVectorExecutable exe, List vectors) => + _SystemCSimCompare.runSystemCVectors(exe, vectors); + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCVectorExecutable exe, List vectors) => + _SystemCSimCompare.checkSystemCVectors(exe, vectors); + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false}) => + _SystemCSimCompare.checkSystemCVector(module, vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + buildOnly: buildOnly); + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) => + _SystemCSimCompare.systemcVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + buildOnly: buildOnly, + ); - if (!dontDeleteTmpFiles) { - try { - File(tmpOutput).deleteSync(); - File(tmpTestFile).deleteSync(); - if (dumpWaves) { - File(tmpVcdFile).deleteSync(); - } - } on Exception catch (e) { - print("Couldn't delete: $e"); - return false; - } - } - return true; - } + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + /// + /// [stimulus] is an async function that sets up and drives the simulation + /// (inject signals, register actions, etc.) but does NOT call + /// [Simulator.run] — that is done internally. + /// + /// [inputNames] and [outputNames] specify which ports to record. If null, + /// all module inputs (excluding clock) and all module outputs are used. + /// + /// Example usage with an existing test: + /// ```dart + /// await SimCompare.systemcSimCompare( + /// counter, + /// clk, + /// stimulus: () async { + /// reset.inject(1); + /// en.inject(0); + /// Simulator.registerAction(25, () { reset.put(0); en.put(1); }); + /// Simulator.setMaxSimTime(100); + /// }, + /// ); + /// ``` + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) => + _SystemCSimCompare.systemcSimCompare( + module, + clk, + stimulus: stimulus, + inputNames: inputNames, + outputNames: outputNames, + clockName: clockName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); } diff --git a/lib/src/utilities/systemc_cosim_ffi.dart b/lib/src/utilities/systemc_cosim_ffi.dart new file mode 100644 index 000000000..10855e307 --- /dev/null +++ b/lib/src/utilities/systemc_cosim_ffi.dart @@ -0,0 +1,1009 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_cosim_ffi.dart +// FFI-based real-time co-simulation with a SystemC compiled module. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:rohd/src/utilities/synchronous_propagator.dart'; +import 'package:rohd/src/utilities/web.dart'; + +// ============================================================================ +// FFI Type Definitions (using only dart:ffi built-in types) +// ============================================================================ + +typedef _DestroyDart = void Function(Pointer); + +typedef _SetInputDart = void Function(Pointer, Pointer, int); + +typedef _SetInputWideDart = void Function( + Pointer, Pointer, Pointer); + +typedef _GetOutputDart = int Function(Pointer, Pointer); + +typedef _GetOutputWideDart = Pointer Function( + Pointer, Pointer); + +typedef _AdvanceDart = void Function(Pointer, int); + +// ============================================================================ +// SystemCFfiCosim — Real-time FFI co-simulation with SystemC +// ============================================================================ + +/// A live peek/poke-style co-simulation wrapper that compiles an ROHD module's +/// SystemC output to a shared library and drives it in lock-step with the ROHD +/// [Simulator]. +/// +/// This is intended for tests that interact with signals procedurally through +/// `inject`, `put`, `await clk.nextPosedge`, and `expect`. Batch [Vector] +/// replay uses the native SystemC vector-testbench executable from +/// [SimCompare.checkSystemCVector] instead of FFI. +/// +/// ## How it works +/// +/// 1. The ROHD module is synthesized to SystemC C++ via +/// [Module.generateSystemC] +/// 2. A C-linkage FFI wrapper is generated around the SystemC module +/// 3. The wrapper is compiled to a `.so` shared library +/// 4. On each [Simulator] tick (at the `clkStable` phase): +/// - Current ROHD input values are pushed to SystemC via FFI +/// - SystemC is advanced by one half clock period (`sc_start`) +/// - SystemC output values are pulled back and `put()` onto ROHD outputs +/// +/// ## Timing compatibility with existing tests +/// +/// The synchronization point at `clkStable` means: +/// - `inject()` calls have already executed (mainTick phase) +/// - Clock has already toggled (mainTick) +/// - `previousValue` was snapshot at preTick (before this tick started) +/// - After clkStable, outputs are updated, then `postTick` fires +/// - `await clk.nextPosedge` resumes after postTick +/// +/// This preserves the same timing semantics as native ROHD Sequential blocks. +/// +/// ## Example +/// +/// ```dart +/// final counter = SimpleCounter(clk, reset, en); +/// await counter.build(); +/// +/// final cosim = await SystemCFfiCosim.create(counter, clk: clk); +/// if (cosim == null) return; // SystemC is unavailable on this platform +/// +/// // Now run your test exactly as before: +/// unawaited(Simulator.run()); +/// reset.inject(1); +/// await clk.nextPosedge; +/// // ... counter.output('val') is driven by SystemC +/// ``` +class SystemCFfiCosim { + /// The ROHD module whose SystemC synthesis is being co-simulated. + final Module module; + + /// The clock signal (null for combinational/clockless mode). + final Logic? clk; + + /// Clock period in nanoseconds (matches SimpleClockGenerator's period). + /// Ignored in combinational mode. + final int clockPeriodNs; + + /// Whether this cosim operates in combinational (clockless) mode. + /// In this mode, inputs are propagated immediately via delta cycles + /// (sc_start(SC_ZERO_TIME)) whenever any input changes. + bool get isCombinational => clk == null; + + /// Handle to the loaded shared library. + DynamicLibrary? _lib; + + /// Opaque handle to the SystemC simulation context. + Pointer _handle = nullptr; + + /// Path to the compiled .so file. + late String? _soPath; + + /// Input port names and widths. + final Map _inputWidths = {}; + + /// Output port names and widths. + final Map _outputWidths = {}; + + /// Clock port name(s) to skip when driving inputs. + final Set _clockNames = {}; + + /// Whether the cosim is actively stepping. + bool _active = false; + + /// Subscription to Simulator.clkStable for per-tick stepping (clocked mode). + StreamSubscription? _clkStableSubscription; + + /// Synchronous subscriptions on input glitches (combinational mode). + final List> + _inputGlitchSubscriptions = []; + + /// Whether a combinational step is already pending in this propagation wave. + /// Prevents re-entrant stepping when multiple inputs change in the same + /// event (e.g. Swizzle feeding a bus). + bool _combStepPending = false; + + // FFI function handles + late final _SetInputDart _setInput; + late final _SetInputWideDart _setInputWide; + late final _GetOutputDart _getOutput; + late final _GetOutputWideDart _getOutputWide; + late final _AdvanceDart _advance; + late final _DestroyDart _destroy; + + // Cached C-string pointers for signal names (allocated once, reused every + // step) + final Map> _inputNamePtrs = {}; + final Map> _outputNamePtrs = {}; + + // Cached signal references (avoid module.input/output map lookups per step) + final Map _inputSignals = {}; + final Map _outputSignals = {}; + + // Pre-allocated buffer for wide hex strings (avoids malloc/free per step). + // 512 chars covers up to 2048-bit signals. + Pointer _hexBuf = nullptr; + static const _hexBufSize = 512; + + // Native memory management + static final _free = DynamicLibrary.process().lookupFunction< + Void Function(Pointer), void Function(Pointer)>('free'); + static final _malloc = DynamicLibrary.process().lookupFunction< + Pointer Function(IntPtr), Pointer Function(int)>('malloc'); + + /// Cache of loaded libraries and handles keyed by .so path. + /// Prevents re-loading a .so that's already in the process, which + /// would crash SystemC's singleton kernel (E113). + static final _loadedLibs = {}; + + /// Deletes all `cosim_ffi_*` source and shared-library files from + /// `tmp_test/` and clears the in-process cache. + /// + /// Call from `tearDownAll` in tests to satisfy `check_tmp_test.sh`. + static void cleanupCache() { + _loadedLibs.clear(); + const dir = 'tmp_test'; + final d = Directory(dir); + if (!d.existsSync()) { + return; + } + for (final entity in d.listSync()) { + final name = entity.uri.pathSegments.last; + if (name.startsWith('cosim_ffi_') || name.startsWith('libcosim_ffi_')) { + try { + entity.deleteSync(recursive: true); + } on Exception catch (_) { + // ignore deletion errors (file may be locked or already removed) + } + } + } + } + + SystemCFfiCosim._(this.module, this.clk, {required this.clockPeriodNs}); + + /// Compiles the module's SystemC output to a shared library, loads it, + /// and begins co-simulation. + /// + /// Returns `null` when SystemC is unavailable on this platform. + /// + /// Throws a [ProcessException] if compiling the generated wrapper fails, or a + /// [StateError] if its native SystemC context cannot be created. + /// + /// If [clk] is provided, the cosim operates in clocked mode — stepping + /// SystemC at each clock edge via `Simulator.clkStable`. + /// + /// If [clk] is omitted (null), the cosim operates in combinational mode — + /// propagating inputs through SystemC delta cycles immediately whenever + /// any input signal changes. This gives the same semantics as native ROHD + /// [Combinational] blocks. + static Future create( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return null; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + if (!cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + )) { + return null; + } + + cosim + .._cachePortInfo() + .._start(); + return cosim; + } + + /// Pre-elaborates the module's SystemC code without starting co-simulation. + /// + /// Call this in `setUpAll` for every module configuration that will be + /// cosim-tested in the file. This ensures all SystemC module types are + /// instantiated during the elaboration phase (before `sc_start`), which + /// avoids E113 errors when multiple configurations are tested. + /// + /// Returns `false` when SystemC is unavailable on this platform. + /// + /// Throws a [ProcessException] if compiling the generated wrapper fails, or a + /// [StateError] if its native SystemC context cannot be created. + static Future preElaborate( + Module module, { + Logic? clk, + int clockPeriodNs = 10, + String? systemcHome, + String? systemcLib, + }) async { + if (kIsWeb) { + return false; + } + + final cosim = SystemCFfiCosim._(module, clk, clockPeriodNs: clockPeriodNs); + + return cosim._compileAndLoad( + systemcHome: systemcHome ?? '', + systemcLib: systemcLib ?? '', + ); + } + + /// Compiles the SystemC wrapper to .so and loads it. + /// Uses a static cache to avoid re-loading the same .so (which would + /// crash SystemC's singleton kernel with E113). + bool _compileAndLoad({ + required String systemcHome, + required String systemcLib, + }) { + final resolvedHome = _resolveHome(systemcHome); + final resolvedLib = _resolveLib(systemcLib); + if (resolvedHome == null || resolvedLib == null) { + return false; + } + + // Collect port widths — treat clocks as regular 1-bit inputs driven + // manually via sc_signal (avoids sc_clock phase alignment issues + // when reusing the cached SystemC kernel across tests). + for (final entry in module.inputs.entries) { + final name = entry.key; + if (name == 'clk' || name.contains('clock')) { + _clockNames.add(name); + } + _inputWidths[name] = entry.value.width; + } + for (final entry in module.outputs.entries) { + _outputWidths[entry.key] = entry.value.width; + } + + // Generate wrapper C++ source + final generatedSC = module.generateSystemC(); + + // Compute a content hash to distinguish modules with the same + // definitionName but different logic (e.g., DAZ/FTZ variants). + // Strip non-deterministic lines (e.g. timestamps) before hashing so + // that repeated instantiations of the same module share one .so. + final stableCode = generatedSC + .split('\n') + .where((line) => !line.contains('Generation time:')) + .join('\n'); + final contentHash = stableCode.hashCode.toUnsigned(32).toRadixString(16); + final uniqueName = '${module.definitionName}_$contentHash'; + + // Rename the top-level SC_MODULE in the generated code to the unique name + // so that different logic variants don't collide in the SystemC linker. + final renamedSC = generatedSC.replaceAll(module.definitionName, uniqueName); + final wrapperSrc = _generateWrapper(renamedSC, uniqueName); + + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final cacheKey = uniqueName; + final cppFile = '$dir/cosim_ffi_$cacheKey.cpp'; + _soPath = '$dir/libcosim_ffi_$cacheKey.so'; + + // Check cache — if already loaded in this process, reuse it + if (_loadedLibs.containsKey(cacheKey)) { + final cached = _loadedLibs[cacheKey]!; + _lib = cached.lib; + _handle = cached.handle; + _setInput = cached.setInput; + _setInputWide = cached.setInputWide; + _getOutput = cached.getOutput; + _getOutputWide = cached.getOutputWide; + _advance = cached.advance; + _destroy = cached.destroy; + + // Reset all inputs to 0 (including clock) so the DUT starts fresh. + // The writes are committed by the first sc_start in _step(). + // Note: _cachePortInfo() is called after this, so use temp pointers here. + for (final name in _inputWidths.keys) { + if (_inputNamePtrs.containsKey(name)) { + _setInput(_handle, _inputNamePtrs[name]!.cast(), 0); + } else { + final namePtr = _toCString(name); + _setInput(_handle, namePtr.cast(), 0); + _free(namePtr); + } + } + + return true; + } + + // Compile (only if .so doesn't exist on disk) + if (!File(_soPath!).existsSync()) { + File(cppFile).writeAsStringSync(wrapperSrc); + + final cxxStd = _detectCxxStd(resolvedLib); + final result = Process.runSync('g++', [ + '-std=$cxxStd', + '-shared', + '-fPIC', + '-O2', + '-I$resolvedHome', + '-L$resolvedLib', + '-Wl,-rpath,$resolvedLib', + '-o', + _soPath!, + cppFile, + '-lsystemc', + ]); + + if (result.exitCode != 0) { + throw ProcessException( + 'g++', + [ + '-std=$cxxStd', + '-shared', + '-fPIC', + '-O2', + '-I$resolvedHome', + '-L$resolvedLib', + '-Wl,-rpath,$resolvedLib', + '-o', + _soPath!, + cppFile, + '-lsystemc', + ], + result.stderr.toString(), + result.exitCode, + ); + } + } + + // Load the shared library + _lib = DynamicLibrary.open(_soPath!); + + // Bind function pointers + final create = _lib!.lookupFunction Function(Pointer), + Pointer Function(Pointer)>('sc_cosim_create'); + _setInput = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Uint64), + _SetInputDart>('sc_cosim_set_input'); + _setInputWide = _lib!.lookupFunction< + Void Function(Pointer, Pointer, Pointer), + _SetInputWideDart>('sc_cosim_set_input_wide'); + _getOutput = _lib!.lookupFunction< + Uint64 Function(Pointer, Pointer), + _GetOutputDart>('sc_cosim_get_output'); + _getOutputWide = _lib!.lookupFunction< + Pointer Function(Pointer, Pointer), + _GetOutputWideDart>('sc_cosim_get_output_wide'); + _advance = _lib! + .lookupFunction, Uint64), _AdvanceDart>( + 'sc_cosim_advance'); + _destroy = _lib!.lookupFunction), _DestroyDart>( + 'sc_cosim_destroy'); + + // Create the SystemC context (elaborates the design) + final namePtr = _toCString(module.definitionName); + _handle = create(namePtr.cast()); + _free(namePtr); + + if (_handle == nullptr) { + throw StateError('SystemC FFI: sc_cosim_create returned null.'); + } + + // Cache for reuse + _loadedLibs[cacheKey] = _LoadedCosimLib( + lib: _lib!, + handle: _handle, + setInput: _setInput, + setInputWide: _setInputWide, + getOutput: _getOutput, + getOutputWide: _getOutputWide, + advance: _advance, + destroy: _destroy, + ); + + return true; + } + + /// Pre-allocates cached C-string pointers and signal references. + /// Call once after _compileAndLoad succeeds. + void _cachePortInfo() { + for (final entry in _inputWidths.entries) { + _inputNamePtrs[entry.key] = _toCString(entry.key); + _inputSignals[entry.key] = module.input(entry.key); + } + for (final entry in _outputWidths.entries) { + _outputNamePtrs[entry.key] = _toCString(entry.key); + _outputSignals[entry.key] = module.output(entry.key); + } + // Pre-allocate hex buffer for wide signals + _hexBuf = _malloc(_hexBufSize); + } + + /// Whether an edge occurred in this tick (set by glitch listener). + bool _edgePending = false; + + /// Subscription to clock glitch for edge detection. + SynchronousSubscription? _glitchSubscription; + + /// Starts the co-simulation by hooking into the clock's glitch and + /// Simulator.clkStable — mirroring how ROHD's Sequential works. + /// + /// In clocked mode: Steps SystemC on BOTH posedge and negedge, advancing + /// by half-period each time. This keeps the SystemC clock perfectly aligned + /// with ROHD's: + /// + /// ROHD posedge → sc_start(T/2) → SystemC posedge occurs → read outputs + /// ROHD negedge → sc_start(T/2) → SystemC negedge occurs → read outputs + /// + /// In combinational mode: Listens to input signal glitches and immediately + /// propagates through SystemC via delta cycles (sc_start(0)). This gives + /// the same timing semantics as native ROHD [Combinational] blocks. + void _start() { + _active = true; + + if (isCombinational) { + _startCombinational(); + } else { + _startClocked(); + } + } + + /// Starts clocked mode — step at each clock edge via clkStable. + void _startClocked() { + // Detect any clock edge (0→1 or 1→0) by listening to the glitch stream. + _glitchSubscription = clk!.glitch.listen((event) { + if (!_active) { + return; + } + // Any valid transition on the clock (posedge or negedge) + final isPosedge = event.previousValue == LogicValue.zero && + event.newValue == LogicValue.one; + final isNegedge = event.previousValue == LogicValue.one && + event.newValue == LogicValue.zero; + if ((isPosedge || isNegedge) && !_edgePending) { + _edgePending = true; + // Wait for clkStable (all inputs settled) then step SystemC. + unawaited(Simulator.clkStable.first.then((_) { + if (!_active) { + return; + } + _edgePending = false; + _step(); + })); + } + }); + } + + /// Starts combinational mode — step on any input change (synchronous). + /// + /// Uses synchronous glitch subscriptions so that output values are + /// available immediately after `put()` — matching native ROHD behavior. + /// + /// Does NOT call sc_start here — the kernel transition from ELABORATION + /// to RUNNING is deferred to the first actual `_stepCombinational()` call. + /// This allows multiple module variants to be pre-elaborated before the + /// kernel starts (avoiding E113 errors). + void _startCombinational() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + // Skip clock-like signals (shouldn't exist in combinational mode, + // but guard against it) + if (_clockNames.contains(name)) { + continue; + } + + final signal = module.input(name); + final sub = signal.glitch.listen((event) { + if (!_active) { + return; + } + if (_combStepPending) { + return; + } + _combStepPending = true; + + // Push all current inputs, advance by 1 ps (triggers delta cycles), + // and pull outputs. The _combStepPending flag prevents re-entrant + // calls during the same propagation wave. + _stepCombinational(); + _combStepPending = false; + }); + _inputGlitchSubscriptions.add(sub); + } + } + + /// One co-simulation step: push inputs, advance time, pull outputs. + void _step() { + _pushInputs(); + + // Advance SystemC to process the signal writes (delta cycle). + // We advance T/2 per edge for timing consistency. The clock signal + // is driven manually (not sc_clock), so posedge/negedge detection + // in SystemC relies on the sc_signal transitions we just wrote. + _advance(_handle, clockPeriodNs * 1000 ~/ 2); + + _pullOutputs(); + } + + /// Combinational step: push inputs, advance minimally, pull outputs. + /// + /// Advances by 1 ps — the minimum non-zero time to trigger the full + /// SystemC evaluate→update→notify loop. Per IEEE 1666 §4.3.4.2, + /// sc_start(SC_ZERO_TIME) explicitly does NOT process delta notifications, + /// so external signal writes cannot trigger SC_METHOD evaluation without + /// a non-zero time advancement. + void _stepCombinational() { + _pushInputs(); + _advance(_handle, 1); // 1 ps — minimum to trigger full eval loop + _pullOutputs(); + } + + /// Pushes all current ROHD input values to the SystemC model via FFI. + void _pushInputs() { + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _inputSignals[name]!; + final val = signal.value; + + if (width <= 64) { + final intVal = val.isValid ? val.toInt() : 0; + _setInput(_handle, _inputNamePtrs[name]!.cast(), intVal); + } else { + final bigVal = + val.isValid ? val.toBigInt().toUnsigned(width) : BigInt.zero; + var hex = bigVal.toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + // Write hex into pre-allocated buffer (no malloc/free per step) + final fullHex = '0x$hex'; + final bytes = utf8.encode(fullHex); + final buf = _hexBuf.cast(); + for (var i = 0; i < bytes.length && i < _hexBufSize - 1; i++) { + (buf + i).value = bytes[i]; + } + (buf + bytes.length).value = 0; + _setInputWide(_handle, _inputNamePtrs[name]!.cast(), _hexBuf.cast()); + } + } + } + + /// Pulls all SystemC output values back to ROHD signals. + void _pullOutputs() { + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + final signal = _outputSignals[name]!; + + if (width <= 64) { + final intVal = _getOutput(_handle, _outputNamePtrs[name]!.cast()); + signal.put(LogicValue.ofInt(intVal, width)); + } else { + final hexCharPtr = + _getOutputWide(_handle, _outputNamePtrs[name]!.cast()); + final hexStr = _fromCString(hexCharPtr); + final bigVal = BigInt.parse( + hexStr.startsWith('0x') ? hexStr.substring(2) : hexStr, + radix: 16); + signal.put(LogicValue.of(bigVal.toUnsigned(width), width: width)); + } + } + } + + /// Stops co-simulation and releases all resources. + Future dispose() async { + _active = false; + await _clkStableSubscription?.cancel(); + _clkStableSubscription = null; + _glitchSubscription?.cancel(); + _glitchSubscription = null; + for (final sub in _inputGlitchSubscriptions) { + sub.cancel(); + } + _inputGlitchSubscriptions.clear(); + // Free cached name pointers + _inputNamePtrs.values.forEach(_free); + _inputNamePtrs.clear(); + _outputNamePtrs.values.forEach(_free); + _outputNamePtrs.clear(); + if (_hexBuf != nullptr) { + _free(_hexBuf); + _hexBuf = nullptr; + } + _inputSignals.clear(); + _outputSignals.clear(); + if (_handle != nullptr) { + _destroy(_handle); + _handle = nullptr; + } + _lib = null; + } + + // ══════════════════════════════════════════════════════════════════════ + // C++ Code Generation + // ══════════════════════════════════════════════════════════════════════ + + static void _writeCode(StringBuffer sb, String code, {String prefix = ''}) { + final lines = code.substring(1).split('\n'); + final nonEmptyLines = lines.where((line) => line.trim().isNotEmpty); + final indent = nonEmptyLines.isEmpty + ? 0 + : nonEmptyLines + .map((line) => line.length - line.trimLeft().length) + .reduce((current, next) => current < next ? current : next); + + sb.write(lines + .map((line) => + line.length < indent ? '' : '$prefix${line.substring(indent)}') + .join('\n')); + } + + /// Generates the C++ wrapper with extern "C" API around the ROHD-generated + /// SystemC module code. + String _generateWrapper(String generatedSystemC, String topModule) { + final sb = StringBuffer(); + + _writeCode(sb, ''' + // Auto-generated SystemC FFI Cosim Wrapper + // Module: $topModule + + #include + #include + #include + #include + using namespace std; + + // ═══ ROHD-Generated SystemC Module(s) ═══ + + '''); + sb.writeln(generatedSystemC); + _writeCode(sb, ''' + // ═══ FFI Cosim Context ═══ + + struct CosimContext { + '''); + + // All input signal declarations (including clocks as sc_signal) + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + // Output signal declarations + for (final entry in _outputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' sc_signal<$type> ${entry.key};'); + } + + _writeCode(sb, ''' + $topModule* dut; + }; + + extern "C" { + + // Required by SystemC linker — we never call it directly + int sc_main(int, char*[]) { return 0; } + + // Track whether the kernel has been initialized + static CosimContext* _active_ctx = nullptr; + + void* sc_cosim_create(const char* name) { + // If a context already exists (same process, new test), + // just return the existing one after resetting signals. + if (_active_ctx != nullptr) { + // Reset all input signals to 0 + '''); + + for (final entry in _inputWidths.entries) { + final type = SystemCSynthesisResult.systemCType(entry.value); + sb.writeln(' _active_ctx->${entry.key}.write($type(0));'); + } + + _writeCode(sb, ''' + return static_cast(_active_ctx); + } + + // Guard: cannot create sc_signal after kernel starts + if (sc_get_status() != SC_ELABORATION && sc_get_status() != SC_BEFORE_END_OF_ELABORATION) { + return nullptr; // E113 prevention + } + + auto* ctx = new CosimContext(); + ctx->dut = new $topModule("dut"); + '''); + + // Bind all inputs (including clocks — driven via sc_signal) + for (final name in _inputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + // Bind outputs + for (final name in _outputWidths.keys) { + sb.writeln(' ctx->dut->$name(ctx->$name);'); + } + + _writeCode(sb, ''' + // Store context — do NOT call sc_start here. + // Deferring sc_start to the first advance allows + // multiple module types to be elaborated before + // the kernel starts (avoids E113). + _active_ctx = ctx; + return static_cast(ctx); + } + + void sc_cosim_set_input(void* handle, const char* name, uint64_t value) { + auto* ctx = static_cast(handle); + '''); + + _generateInputDispatch(sb, narrow: true); + + _writeCode(sb, ''' + } + + void sc_cosim_set_input_wide(void* handle, const char* name, const char* hex_value) { + auto* ctx = static_cast(handle); + '''); + + _generateInputDispatch(sb, narrow: false); + + _writeCode(sb, ''' + } + + uint64_t sc_cosim_get_output(void* handle, const char* name) { + auto* ctx = static_cast(handle); + '''); + + _generateOutputDispatch(sb, narrow: true); + + _writeCode(sb, ''' + return 0; + } + + const char* sc_cosim_get_output_wide(void* handle, const char* name) { + auto* ctx = static_cast(handle); + static char _buf[512]; + '''); + + _generateOutputDispatch(sb, narrow: false); + + _writeCode(sb, ''' + _buf[0] = '0'; _buf[1] = 0; + return _buf; + } + + void sc_cosim_advance(void* handle, uint64_t time_ps) { + // End elaboration on first advance (allows multiple + // module types to be instantiated before starting). + if (sc_get_status() == SC_ELABORATION) { + sc_start(SC_ZERO_TIME); + } + if (time_ps == 0) { + // Zero-time advance: process delta cycles only. + // Use SC_ZERO_TIME explicitly (some implementations + // treat sc_time(0,SC_PS) differently). + sc_start(SC_ZERO_TIME); + } else { + sc_start(sc_time(static_cast(time_ps), SC_PS)); + } + } + + void sc_cosim_destroy(void* handle) { + // Do NOT delete or sc_stop — the SystemC kernel is a + // process-wide singleton. The context is reused if + // sc_cosim_create is called again (same module). + // This avoids E113 "insert primitive channel failed". + } + + } // extern "C" + '''); + + return sb.toString(); + } + + /// Generates the if-else chain for setting input signals. + void _generateInputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _inputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + final type = SystemCSynthesisResult.systemCType(width); + sb.writeln(' ctx->$name.write(static_cast<$type>(value));'); + } else { + _writeCode( + sb, + ''' + sc_biguint<$width> v(hex_value); + ctx->$name.write(v); + ''', + prefix: ' '); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + /// Generates the if-else chain for reading output signals. + void _generateOutputDispatch(StringBuffer sb, {required bool narrow}) { + var first = true; + for (final entry in _outputWidths.entries) { + final name = entry.key; + final width = entry.value; + + if (narrow && width > 64) { + continue; + } + if (!narrow && width <= 64) { + continue; + } + + final ifStr = first ? ' if' : ' } else if'; + first = false; + + sb.writeln('$ifStr (strcmp(name, "$name") == 0) {'); + if (narrow) { + sb.writeln(' return static_cast(ctx->$name.read());'); + } else { + _writeCode( + sb, + ''' + sc_biguint<$width> v = ctx->$name.read(); + string s = v.to_string(SC_HEX_US); + strncpy(_buf, s.c_str(), sizeof(_buf)-1); + _buf[sizeof(_buf)-1] = 0; + return _buf; + ''', + prefix: ' '); + } + } + if (!first) { + sb.writeln(' }'); + } + } + + // ══════════════════════════════════════════════════════════════════════ + // String/Memory Utilities (no package:ffi dependency) + // ══════════════════════════════════════════════════════════════════════ + + /// Allocates a null-terminated C string from a Dart string. + static Pointer _toCString(String s) { + final bytes = utf8.encode(s); + final ptr = _malloc(bytes.length + 1); + final charPtr = ptr.cast(); + for (var i = 0; i < bytes.length; i++) { + (charPtr + i).value = bytes[i]; + } + (charPtr + bytes.length).value = 0; + return ptr; + } + + /// Reads a null-terminated C string into a Dart string. + static String _fromCString(Pointer ptr) { + final bytes = []; + var i = 0; + while (true) { + final byte = (ptr.cast() + i).value; + if (byte == 0) { + break; + } + bytes.add(byte); + i++; + } + return utf8.decode(bytes); + } + + // ══════════════════════════════════════════════════════════════════════ + // SystemC Path Resolution (mirrors SimCompare) + // ══════════════════════════════════════════════════════════════════════ + + static const _defaultHome = '/opt/systemc/include'; + static const _defaultLib = '/opt/systemc/lib'; + static const _packageHome = '/usr/include'; + static const _packageLib = '/usr/lib/x86_64-linux-gnu'; + + static String? _resolveHome(String scHome) { + if (scHome.isNotEmpty && Directory(scHome).existsSync()) { + return scHome; + } + for (final home in [_defaultHome, _packageHome]) { + if (Directory(home).existsSync()) { + return home; + } + } + return null; + } + + static String? _resolveLib(String scLib) { + if (scLib.isNotEmpty && Directory(scLib).existsSync()) { + return scLib; + } + for (final lib in [_defaultLib, _packageLib]) { + if (File('$lib/libsystemc.so').existsSync()) { + return lib; + } + } + return null; + } + + static String _detectCxxStd(String scLib) { + try { + final r = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (r.exitCode == 0) { + final out = r.stdout as String; + if (out.contains('cxx202002L')) { + return 'c++20'; + } + if (out.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // ignore + } + return 'c++20'; + } +} + +/// Cached state for a loaded SystemC cosim shared library. +class _LoadedCosimLib { + final DynamicLibrary lib; + final Pointer handle; + final _SetInputDart setInput; + final _SetInputWideDart setInputWide; + final _GetOutputDart getOutput; + final _GetOutputWideDart getOutputWide; + final _AdvanceDart advance; + final _DestroyDart destroy; + + _LoadedCosimLib({ + required this.lib, + required this.handle, + required this.setInput, + required this.setInputWide, + required this.getOutput, + required this.getOutputWide, + required this.advance, + required this.destroy, + }); +} diff --git a/lib/src/utilities/systemc_simcompare.dart b/lib/src/utilities/systemc_simcompare.dart new file mode 100644 index 000000000..9e3b4fb25 --- /dev/null +++ b/lib/src/utilities/systemc_simcompare.dart @@ -0,0 +1,851 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare.dart +// SystemC simulation comparison support for SimCompare. +// +// 2026 July 20 +// Author: Desmond A. Kirkpatrick + +// SystemC vector execution logs compilation and simulator output for debugging. +// ignore_for_file: avoid_print + +part of 'simcompare.dart'; + +class _SystemCSimCompare { + /// The default SystemC installation path (Accellera). + static const _systemCDefaultHome = '/opt/systemc/include'; + static const _systemCDefaultLib = '/opt/systemc/lib'; + static const _systemCPackageHome = '/usr/include'; + static const _systemCPackageLib = '/usr/lib/x86_64-linux-gnu'; + + /// Cache of compiled SystemC vector-testbench executables keyed by generated + /// code hash. + static final _compilationCache = {}; + + /// Prefix for SystemC artifacts owned by this test process. + static final String tempPrefix = + 'tmp_sc_${pid}_${DateTime.now().microsecondsSinceEpoch}_' + '${Object().hashCode}'; + + /// Path to the precompiled header, built lazily on first compilation. + static String? _pchPath; + + /// Builds the precompiled header for systemc.h if not already done. + /// Returns the directory containing systemc.h.gch, or null on failure. + /// + /// In CI, the PCH is pre-built by `tool/gh_actions/setup_systemc_pch.sh` + /// before tests run, so this just finds it on disk. Locally it builds + /// on first use (safe because local runs are typically sequential). + static String? _ensurePch(String scHome, String cxxStd) { + if (_pchPath != null) { + return _pchPath; + } + + const dir = 'tmp_test'; + const pchDir = '$dir/pch'; + const gchFile = '$pchDir/systemc.h.gch'; + + // Reuse if already on disk (pre-built by CI or a previous run) + if (File(gchFile).existsSync()) { + return _pchPath = pchDir; + } + + Directory(pchDir).createSync(recursive: true); + + // Copy the original header next to the .gch so g++ matches them + File('$scHome/systemc.h').copySync('$pchDir/systemc.h'); + + final args = [ + '-std=$cxxStd', + '-I$scHome', + '-x', + 'c++-header', + '-o', + gchFile, + '$scHome/systemc.h', + ]; + final result = Process.runSync('g++', args); + if (result.exitCode != 0) { + print('PCH compilation failed (falling back to normal headers):'); + print(result.stderr); + return null; + } + + return _pchPath = pchDir; + } + + /// Resolves SystemC home/lib paths. If explicit paths are given, uses them. + /// Otherwise uses the Accellera or Ubuntu package install paths. + static (String?, String?) _resolveSystemCPaths(String scHome, String scLib) { + if (scHome.isNotEmpty && scLib.isNotEmpty) { + if (Directory(scHome).existsSync() && + File('$scLib/libsystemc.so').existsSync()) { + return (scHome, scLib); + } + return (null, null); + } + for (final (home, lib) in [ + (_systemCDefaultHome, _systemCDefaultLib), + (_systemCPackageHome, _systemCPackageLib), + ]) { + if (Directory(home).existsSync() && + File('$lib/libsystemc.so').existsSync()) { + return (home, lib); + } + } + return (null, null); + } + + /// Detects the C++ standard the SystemC library was compiled with + /// by inspecting the `sc_api_version` symbol in libsystemc.so. + static String _detectCxxStandard(String scLib) { + try { + final result = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (result.exitCode == 0) { + final output = result.stdout as String; + if (output.contains('cxx202002L')) { + return 'c++20'; + } + if (output.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // Fall through to default + } + return 'c++20'; + } + + /// Cleans up all cached SystemC executables and the precompiled header. + /// Call from `tearDownAll` in tests. + /// + /// If [keepPch] is true (the default), the precompiled header is preserved + /// for faster subsequent runs. Pass `keepPch: false` to remove everything. + static void cleanupSystemCCache({bool keepPch = true}) { + _compilationCache.clear(); + _pchPath = null; + if (kIsWeb) { + return; + } + try { + final dir = Directory('tmp_test'); + if (dir.existsSync()) { + for (final entity in dir.listSync()) { + // Use entity.path (not entity.uri) to get the basename: Directory.uri + // always appends a trailing slash, making pathSegments.last == "". + final name = entity.path.split('/').last; + + // Remove only SystemC artifacts owned by this test process. Other + // test isolates may be compiling or running from the same tmp_test + // directory concurrently. + if (name.startsWith(tempPrefix) || name == 'Makefile_sc') { + entity.deleteSync(recursive: true); + continue; + } + + // Remove pch/ directory only when keepPch is false + if (!keepPch && entity is Directory && entity.path.endsWith('/pch')) { + entity.deleteSync(recursive: true); + continue; + } + + // Leave everything else (iverilog files from parallel tests) alone + } + } + } on Exception catch (_) {} + } + + /// Compiles a SystemC module into a reusable stdin-driven vector-testbench + /// executable. + /// + /// Returns a [SystemCVectorExecutable] that can be used to run multiple + /// vector sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCVectorExecutable? buildSystemCVectorExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) { + if (kIsWeb) { + return null; + } + + final scHome = systemcHome ?? ''; + final scLib = systemcLib ?? ''; + final (resolvedHome, resolvedLib) = _resolveSystemCPaths(scHome, scLib); + + if (resolvedHome == null || resolvedLib == null) { + print('SystemC installation not found'); + return null; + } + + final topModule = moduleName ?? module.definitionName; + final generatedSystemC = module.generateSystemC(); + + // Check compilation cache + final cacheKey = generatedSystemC.hashCode; + if (_compilationCache.containsKey(cacheKey)) { + final cached = _compilationCache[cacheKey]!; + if (File(cached.binaryPath).existsSync()) { + return cached; + } + // Binary was removed; recompile. + _compilationCache.remove(cacheKey); + } + + // Identify clock signals + final clockSignals = {}; + if (clockName != null) { + clockSignals.add(clockName); + } + for (final input in module.inputs.entries) { + final name = input.key; + if (clockSignals.isEmpty && (name == 'clk' || name.contains('clock'))) { + clockSignals.add(name); + } + } + final promotedClocks = {}; + for (final sub in module.subModules) { + if (sub is SimpleClockGenerator) { + final clkSigName = sub.clk.name; + promotedClocks.add(clkSigName); + clockSignals.add(clkSigName); + } + } + + // Collect ALL module ports for the stdin-driven harness + final inputPorts = {}; + for (final input in module.inputs.entries) { + if (promotedClocks.contains(input.key)) { + continue; + } + inputPorts[input.key] = input.value.width; + } + final outputPorts = {}; + for (final output in module.outputs.entries) { + outputPorts[output.key] = output.value.width; + } + final inOutPorts = {}; + for (final inOut in module.inOuts.entries) { + inOutPorts[inOut.key] = inOut.value.width; + } + + // Generate stdin-driven testbench + final tb = StringBuffer() + ..write(''' +#include +#include +#include +#include +#include +#include +using namespace std; + +''') + ..writeln(generatedSystemC) + ..write(''' +int sc_main(int argc, char* argv[]) { +'''); + + // Clock + for (final clkName in clockSignals) { + tb.writeln( + ' sc_clock $clkName("$clkName", ${Vector._period}, SC_NS);', + ); + } + + // Signals for all non-clock input ports + for (final entry in inputPorts.entries) { + if (clockSignals.contains(entry.key)) { + continue; + } + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + // Signals for all output ports + for (final entry in outputPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + // Signals for all inout ports + for (final entry in inOutPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};', + ); + } + + tb + ..writeln() + // DUT instantiation and port binding + ..writeln(' $topModule dut("dut");'); + for (final name in inputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final clkName in clockSignals) { + if (!inputPorts.containsKey(clkName)) { + tb.writeln(' dut.$clkName($clkName);'); + } + } + for (final name in outputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final name in inOutPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + + tb.write(''' + int _tb_errors = 0; + + // Initial offset + sc_start(sc_time(1, SC_NS)); + + // Read number of vectors + int _tb_nvec; + cin >> _tb_nvec; + + for (int _tb_v = 0; _tb_v < _tb_nvec; _tb_v++) { +'''); + + // Read and drive each non-clock input + final drivableInputs = + inputPorts.keys.where((k) => !clockSignals.contains(k)).toList(); + for (final name in drivableInputs) { + final w = inputPorts[name]!; + if (w > 64) { + // BigInt — read as hex string + tb + ..writeln(' { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); }'); + } else { + tb + ..writeln(' { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); }'); + } + } + for (final entry in inOutPorts.entries) { + final name = entry.key; + final w = entry.value; + tb.writeln(' { int _drive; cin >> _drive;'); + if (w > 64) { + tb + ..writeln(' if (_drive) { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); } }'); + } else { + tb + ..writeln(' if (_drive) { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); } }'); + } + } + + // Advance to check point + tb.write(''' + sc_start(sc_time(${Vector._offset}, SC_NS)); + + // Read number of outputs to check + int _tb_nchk; + cin >> _tb_nchk; + + for (int _tb_c = 0; _tb_c < _tb_nchk; _tb_c++) { + string _tb_pn; + cin >> _tb_pn; +'''); + + // Generate if-else chain for each output and inout port + var first = true; + final checkablePorts = {...outputPorts, ...inOutPorts}; + for (final entry in checkablePorts.entries) { + final name = entry.key; + final w = entry.value; + final ifKey = first ? 'if' : '} else if'; + first = false; + tb.writeln(' $ifKey (_tb_pn == "$name") {'); + if (w > 64) { + tb + ..writeln(' string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _tb_exp(_h.c_str());') + ..writeln(' if ($name.read() != _tb_exp) {'); + } else { + tb + ..writeln(' uint64_t _tb_exp; cin >> _tb_exp;') + ..writeln(' if ($name.read() != _tb_exp) {'); + } + tb + ..writeln( + ' cout << "ERROR vector " << _tb_v' + ' << ": expected $name=" << _tb_exp' + ' << ", got " << $name.read() << endl;', + ) + ..writeln(' _tb_errors++;') + ..writeln(' }'); + } + if (checkablePorts.isNotEmpty) { + tb + ..writeln(' } else {') + ..writeln(' string _d; cin >> _d; // skip unknown') + ..writeln(' }'); + } + + tb.write(''' + } + + sc_start(sc_time(${Vector._period - Vector._offset}, SC_NS)); + } + + if (_tb_errors == 0) { + cout << "PASS" << endl; + } else { + cout << "FAIL: " << _tb_errors << " errors" << endl; + } + return _tb_errors > 0 ? 1 : 0; +} +'''); + + final testbenchCode = tb.toString(); + + // Write and compile + const dir = 'tmp_test'; + Directory(dir).createSync(recursive: true); + final compileDir = Directory( + dir, + ).createTempSync('${tempPrefix}_${generatedSystemC.hashCode}_'); + final tmpCppFile = '${compileDir.path}/main.cpp'; + final tmpOutput = '${compileDir.path}/sim'; + File(tmpCppFile).writeAsStringSync(testbenchCode); + + // Detect C++ standard for this installation + final cxxStd = _detectCxxStandard(resolvedLib); + + // Build precompiled header on first use + final pchDir = _ensurePch(resolvedHome, cxxStd); + final pchArgs = pchDir != null ? ['-I$pchDir'] : []; + + final compileResult = Process.runSync('g++', [ + '-std=$cxxStd', + '-pipe', + ...pchArgs, + '-I$resolvedHome', + '-o', + tmpOutput, + tmpCppFile, + '-L$resolvedLib', + '-lsystemc', + ]); + if (compileResult.exitCode != 0) { + print('SystemC compilation failed:'); + print(compileResult.stdout); + print(compileResult.stderr); + return null; + } + + final exe = SystemCVectorExecutable._( + binaryPath: tmpOutput, + cppFile: tmpCppFile, + scLib: resolvedLib, + clockSignals: clockSignals, + inputPorts: inputPorts, + outputPorts: outputPorts, + inOutPorts: inOutPorts, + ); + _compilationCache[cacheKey] = exe; + return exe; + } + + /// Runs [vectors] against a pre-compiled [SystemCVectorExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCVectorExecutable exe, List vectors) { + if (!File(exe.binaryPath).existsSync()) { + print('SystemC binary not found: ${exe.binaryPath}'); + return false; + } + + // Build stdin data + final sb = StringBuffer()..writeln(vectors.length); + + final drivableInputs = exe.inputPorts.keys + .where((k) => !exe.clockSignals.contains(k)) + .toList(); + + // Track last-driven values (persist across vectors like iverilog) + final lastValues = { + for (final name in drivableInputs) name: '0', + }; + + for (final vector in vectors) { + // Update last-driven values with this vector's inputs + for (final name in drivableInputs) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inputPorts[name]!; + if (w > 64) { + final lv = LogicValue.of(value, width: w); + var hex = lv.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + lastValues[name] = '0x$hex'; + } else { + lastValues[name] = '${_systemcIntValue(value, w)}'; + } + } + } + // Write all input values (using persisted values for unspecified) + for (final name in drivableInputs) { + sb.write('${lastValues[name]} '); + } + for (final name in exe.inOutPorts.keys) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inOutPorts[name]!; + final formattedValue = w > 64 + ? _systemcHexValue(value, w) + : '${_systemcIntValue(value, w)}'; + lastValues[name] = formattedValue; + } + final lastValue = lastValues[name]; + if (lastValue == null) { + sb.write('0 '); + } else { + sb.write('1 $lastValue '); + } + } + sb.writeln(); + + // Write expected outputs: count then name/value pairs + // Skip x/z outputs + final checks = {}; + for (final entry in vector.expectedOutputValues.entries) { + final name = entry.key; + final checkablePorts = {...exe.outputPorts, ...exe.inOutPorts}; + final w = checkablePorts[name]!; + final expectedLV = LogicValue.of(entry.value, width: w); + if (expectedLV.toString().contains('x') || + expectedLV.toString().contains('z')) { + continue; + } + if (w > 64) { + checks[name] = _systemcHexValue(entry.value, w); + } else { + checks[name] = '${_systemcIntValue(entry.value, w)}'; + } + } + sb.write('${checks.length} '); + for (final entry in checks.entries) { + sb.write('${entry.key} ${entry.value} '); + } + sb.writeln(); + } + + // Write vectors to a unique temp file, redirect as stdin. + final stdinDir = Directory('tmp_test').createTempSync('sc_input_'); + final stdinFile = '${stdinDir.path}/input.txt'; + late final ProcessResult result; + try { + File(stdinFile).writeAsStringSync(sb.toString()); + + result = Process.runSync( + 'sh', + ['-c', '${exe.binaryPath} < $stdinFile'], + environment: { + 'LD_LIBRARY_PATH': exe.scLib, + 'SC_COPYRIGHT_MESSAGE': 'DISABLE', + }, + ); + } finally { + if (stdinDir.existsSync()) { + stdinDir.deleteSync(recursive: true); + } + } + + final stdout = result.stdout.toString(); + final stderr = result.stderr.toString(); + + if (stdout.isNotEmpty && !stdout.contains('PASS')) { + print(stdout); + } + if (stderr.isNotEmpty && !stderr.contains('Info:')) { + print(stderr); + } + + return stdout.contains('PASS') && !stdout.contains('FAIL'); + } + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCVectorExecutable exe, List vectors) { + expect(runSystemCVectors(exe, vectors), true); + } + + /// Converts a value to an integer for stdin. + static int _systemcIntValue(dynamic value, int width) { + if (value is int) { + return value; + } + if (value is LogicValue) { + if (!value.isValid) { + return 0; + } + return value.toBigInt().toUnsigned(width).toInt(); + } + if (value is BigInt) { + return value.toUnsigned(width).toInt(); + } + if (value is String) { + final lv = LogicValue.of(value, width: width); + if (!lv.isValid) { + return 0; + } + return lv.toBigInt().toUnsigned(width).toInt(); + } + return 0; + } + + /// Converts a value to a hex string for stdin. + static String _systemcHexValue(dynamic value, int width) { + final lv = LogicValue.of(value, width: width); + var hex = lv.toBigInt().toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '0x$hex'; + } + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (buildOnly) { + // Just verify SystemC code generation succeeds + module.generateSystemC(); + return; + } + final exe = buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + // SystemC not available — skip gracefully. + return; + } + final passed = runSystemCVectors(exe, vectors); + if (!dontDeleteTmpFiles) { + // Single-shot path: clean up this process's compiled artifacts now so + // tests that call checkSystemCVector do not require a tearDownAll. + // The PCH is kept to avoid rebuilding it for subsequent calls. + cleanupSystemCCache(); + } + expect(passed, true); + } + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (kIsWeb) { + return true; + } + final exe = buildSystemCVectorExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + return false; + } + if (buildOnly) { + return true; + } + return runSystemCVectors(exe, vectors); + } + + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) async { + // Determine which signals to record + final clkName = clockName ?? + module.inputs.keys.firstWhere( + (n) => n == 'clk' || n.contains('clock'), + orElse: () => 'clk', + ); + + final inputs = + inputNames ?? module.inputs.keys.where((n) => n != clkName).toList(); + final outputs = outputNames ?? module.outputs.keys.toList(); + + // Record snapshots at each posedge. + // Use previousValue for outputs — this gives us the output state from + // BEFORE the clock edge, which matches what the SystemC testbench sees + // when it checks at offset (before the posedge). + // Use current value for inputs — these are the values being presented + // to the DUT when the clock edge fires. + final recordings = []; + + clk.posedge.listen((_) { + // Sample inputs (current value — what's being driven now) + final inputValues = {}; + for (final name in inputs) { + final sig = module.input(name); + final val = sig.value; + inputValues[name] = val.isValid ? val.toBigInt().toInt() : 0; + } + + // Sample outputs using previousValue — the settled output + // from before this tick started, which is what a testbench + // checking before the clock edge would observe. + final outputValues = {}; + for (final name in outputs) { + final sig = module.output(name); + final prev = sig.previousValue; + if (prev != null && prev.isValid) { + outputValues[name] = prev.toBigInt().toInt(); + } + // Skip null/x/z — no check for this output + } + + recordings.add(Vector(inputValues, outputValues)); + }); + + // Run the user's stimulus setup + await stimulus(); + + // Run the ROHD simulation + await Simulator.run(); + + if (recordings.length < 2) { + print( + 'Warning: only ${recordings.length} clock edges recorded,' + ' need at least 2 for comparison', + ); + return true; + } + + // No shifting needed — previousValue already gives us the output + // state from before the posedge, which matches systemcVector's + // check-before-edge timing. Just pass recordings directly as vectors. + + // Run through SystemC + return systemcVector( + module, + recordings, + clockName: clkName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + } +} + +/// Holds the compiled state of a native SystemC vector-testbench executable for +/// reuse across tests. +class SystemCVectorExecutable { + /// Path to the compiled binary. + final String binaryPath; + + /// Path to the generated C++ source. + final String cppFile; + + /// Path to the SystemC library (for LD_LIBRARY_PATH). + final String scLib; + + /// Clock signal names. + final Set clockSignals; + + /// Input port names and widths (excluding promoted clocks). + final Map inputPorts; + + /// Output port names and widths. + final Map outputPorts; + + /// Inout port names and widths. + final Map inOutPorts; + + SystemCVectorExecutable._({ + required this.binaryPath, + required this.cppFile, + required this.scLib, + required this.clockSignals, + required this.inputPorts, + required this.outputPorts, + required this.inOutPorts, + }); + + /// Deletes the compiled binary and source. + void cleanup() { + void tryDelete(String path) { + final f = File(path); + if (f.existsSync()) { + f.deleteSync(); + } + } + + try { + final compileDir = File(cppFile).parent; + if (compileDir.existsSync() && + compileDir.uri.pathSegments.last.startsWith( + _SystemCSimCompare.tempPrefix, + )) { + compileDir.deleteSync(recursive: true); + return; + } + tryDelete(cppFile); + tryDelete(binaryPath); + } on Exception catch (_) {} + } +} + +/// Legacy name for [SystemCVectorExecutable]. +@Deprecated('Use SystemCVectorExecutable instead.') +typedef SystemCExecutable = SystemCVectorExecutable; diff --git a/lib/src/utilities/systemverilog_simcompare.dart b/lib/src/utilities/systemverilog_simcompare.dart new file mode 100644 index 000000000..10936159c --- /dev/null +++ b/lib/src/utilities/systemverilog_simcompare.dart @@ -0,0 +1,385 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemverilog_simcompare.dart +// SystemVerilog testbench generation and simulation comparison support for +// SimCompare. +// +// 2026 July 20 +// Author: Desmond A. Kirkpatrick + +// SystemVerilog vector execution logs simulator output for debugging. +// ignore_for_file: avoid_print + +part of 'simcompare.dart'; + +class _SystemVerilogVectorTestbench { + final Vector vector; + final Module module; + + _SystemVerilogVectorTestbench(this.vector, this.module); + + /// Computes a SystemVerilog code string that checks in a SystemVerilog + /// simulation whether a signal [sigName] has the [expected] value given + /// the [inputValues]. + static String _errorCheckString( + String sigName, + dynamic expected, + LogicValue expectedVal, + String inputValues, + ) { + if (expected is! int && + expected is! LogicValue && + expected is! BigInt && + expected is! String) { + throw NonSupportedTypeException(expected); + } + + String expectedHexStr; + if (expected is int) { + expectedHexStr = BigInt.from( + expected, + ).toUnsigned(expectedVal.width).toRadixString(16); + expectedHexStr = '0x$expectedHexStr'; + } else if (expected is BigInt) { + expectedHexStr = expected.toUnsigned(expectedVal.width).toRadixString(16); + expectedHexStr = '0x$expectedHexStr'; + } else { + expectedHexStr = expected.toString(); + } + + final expectedValStr = expectedVal.toString(); + + return 'if($sigName !== $expectedValStr) ' + '\$error(\$sformatf("Expected $sigName=$expectedHexStr,' + ' but found $sigName=0x%x (0b%b) with inputs $inputValues",' + ' $sigName, $sigName));'; + } + + String toTbVerilog() { + final assignments = vector.inputValues.keys.map((signalName) { + final signal = module.tryInOut(signalName) ?? module.input(signalName); + + if (signal is LogicArray) { + final arrAssigns = StringBuffer(); + var index = 0; + final fullVal = LogicValue.of( + vector.inputValues[signalName], + width: signal.width, + ); + for (final leaf in signal.leafElements) { + final subVal = fullVal.getRange(index, index + leaf.width); + arrAssigns.writeln('${leaf.structureName} = $subVal;'); + index += leaf.width; + } + return arrAssigns.toString(); + } else { + final signalVal = LogicValue.of( + vector.inputValues[signalName], + width: signal.width, + ); + return '$signalName = $signalVal;'; + } + }).join('\n'); + + final checksList = []; + for (final expectedOutput in vector.expectedOutputValues.entries) { + final outputName = expectedOutput.key; + final outputPort = + module.tryInOut(outputName) ?? module.output(outputName); + final expected = expectedOutput.value; + final expectedValue = LogicValue.of(expected, width: outputPort.width); + final inputStimulus = vector.inputValues.toString(); + + if (outputPort is LogicArray) { + 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, + ), + ); + index += leaf.width; + } + } else { + checksList.add( + _errorCheckString(outputName, expected, expectedValue, inputStimulus), + ); + } + } + final checks = checksList.join('\n'); + + return [ + assignments, + '#${Vector._offset}', + checks, + '#${Vector._period - Vector._offset}', + ].join('\n'); + } +} + +class _SystemVerilogSimCompare { + /// A collection of warnings that are fine to ignore usually. + static final List _knownWarnings = [ + RegExp('sorry: Case unique/unique0 qualities are ignored.'), + RegExp( + r'sorry: constant selects in always_\* processes' + ' are not currently supported', + ), + RegExp('warning: always_comb process has no sensitivities'), + RegExp('finish called at'), + ]; + + static void checkIverilogVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool enableChecking = true, + bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), + }) { + final result = iverilogVector( + module, + vectors, + moduleName: moduleName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + dumpWaves: dumpWaves, + iverilogExtraArgs: iverilogExtraArgs, + allowWarnings: allowWarnings, + maskKnownWarnings: maskKnownWarnings, + buildOnly: buildOnly, + synthesizerConfiguration: synthesizerConfiguration, + ); + if (enableChecking) { + expect(result, true); + } + } + + static bool iverilogVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool buildOnly = false, + SystemVerilogSynthesizerConfiguration synthesizerConfiguration = + const SystemVerilogSynthesizerConfiguration(), + }) { + if (kIsWeb) { + // if running in web mode, then we can't run icarus verilog + return true; + } + + String signalDeclaration( + String signalName, { + String Function(String original)? adjust, + String? signalTypeOverride, + }) { + final signal = module.signals.firstWhere((e) => e.name == signalName); + + final signalType = signalTypeOverride ?? + ((signal is LogicNet || (signal is LogicArray && signal.isNet)) + ? 'wire' + : 'logic'); + + if (adjust != null) { + signalName = adjust(signalName); + } + + if (signal is LogicArray) { + final unpackedDims = signal.dimensions.getRange( + 0, + signal.numUnpackedDimensions, + ); + final packedDims = signal.dimensions.getRange( + signal.numUnpackedDimensions, + signal.dimensions.length, + ); + // Generate packed and unpacked dimensions incrementally for arrays. + // ignore: prefer_interpolation_to_compose_strings + return signalType + + ' ' + + // Preserve the existing incremental dimension construction. + // ignore: prefer_interpolation_to_compose_strings + packedDims.map((d) => '[${d - 1}:0]').join() + + ' [${signal.elementWidth - 1}:0] $signalName' + + unpackedDims.map((d) => '[${d - 1}:0]').join(); + } else if (signal.width != 1) { + return '$signalType [${signal.width - 1}:0] $signalName'; + } else { + return '$signalType $signalName'; + } + } + + final topModule = moduleName ?? module.definitionName; + final allSignals = { + for (final v in vectors) ...v.inputValues.keys, + for (final v in vectors) ...v.expectedOutputValues.keys, + }; + + late final tbWireUniquifier = Uniquifier(); + late final alreadyMappedLogicToWires = {}; + String toTbWireName(String name) => alreadyMappedLogicToWires.putIfAbsent( + name, + () => tbWireUniquifier.getUniqueName(initialName: 'wire__$name'), + ); + + final logicToWireMapping = Map.fromEntries( + vectors + .map((v) => v.inputValues.keys) + .flattened + .where((name) => module.tryInOut(name) != null) + .map((name) => MapEntry(name, toTbWireName(name))), + ); + + final localDeclarations = [ + ...allSignals.map((e) { + final sigDecl = signalDeclaration( + e, + signalTypeOverride: + logicToWireMapping.containsKey(e) ? 'logic' : null, + ); + return '$sigDecl;'; + }), + ...logicToWireMapping.entries.map((e) { + final logicName = e.key; + final wireName = e.value; + + final sigDecl = signalDeclaration( + logicName, + adjust: toTbWireName, + signalTypeOverride: 'wire', + ); + return '$sigDecl; assign $wireName = $logicName;'; + }), + ].join('\n'); + + final moduleConnections = + 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 + .dumpSystemVerilog( + configuration: synthesizerConfiguration, + ) + .output; + + // so that when they run in parallel, they dont step on each other + final uniqueId = + (generatedVerilog + localDeclarations + stimulus + moduleInstance) + .hashCode; + + const dir = 'tmp_test'; + final tmpTestFile = '$dir/tmp_test$uniqueId.sv'; + final tmpOutput = '$dir/tmp_out$uniqueId'; + final tmpVcdFile = '$dir/tmp_waves_$uniqueId.vcd'; + + final waveDumpCode = ''' +\$dumpfile("$tmpVcdFile"); +\$dumpvars(0,dut); +'''; + + final testbench = [ + generatedVerilog, + 'module tb;', + localDeclarations, + moduleInstance, + 'initial begin', + if (dumpWaves) waveDumpCode, + '#1', + stimulus, + r'$finish;', // so the test doesn't run forever if there's a clock gen + 'end', + 'endmodule', + ].join('\n'); + + Directory(dir).createSync(recursive: true); + File(tmpTestFile).writeAsStringSync(testbench); + final compileResult = Process.runSync('iverilog', [ + '-g2012', + '-o', + tmpOutput, + ...iverilogExtraArgs, + tmpTestFile, + ]); + bool printIfContentsAndCheckError(dynamic output) { + final maskedOutput = output + .toString() + .split('\n') + .where((element) => element.isNotEmpty) + .map((line) { + for (final knownWarning in _knownWarnings) { + if (knownWarning.hasMatch(line)) { + return null; + } + } + return line; + }) + .nonNulls + .join('\n'); + if (maskedOutput.isNotEmpty) { + print(maskedOutput); + } + + return output.toString().contains( + RegExp( + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), + caseSensitive: false, + ), + ); + } + + if (printIfContentsAndCheckError(compileResult.stdout)) { + return false; + } + if (printIfContentsAndCheckError(compileResult.stderr)) { + return false; + } + + if (!buildOnly) { + final simResult = Process.runSync('vvp', [tmpOutput]); + if (printIfContentsAndCheckError(simResult.stdout)) { + return false; + } + if (printIfContentsAndCheckError(simResult.stderr)) { + return false; + } + } + + if (!dontDeleteTmpFiles) { + try { + final outFile = File(tmpOutput); + if (outFile.existsSync()) { + outFile.deleteSync(); + } + final testFile = File(tmpTestFile); + if (testFile.existsSync()) { + testFile.deleteSync(); + } + if (dumpWaves) { + 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/arithmetic_shift_right_test.dart b/test/arithmetic_shift_right_test.dart index 1d31f4a51..1ba137d1f 100644 --- a/test/arithmetic_shift_right_test.dart +++ b/test/arithmetic_shift_right_test.dart @@ -41,5 +41,6 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); } 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..3e8e4d9b3 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 @@ -95,6 +95,7 @@ void main() { allowWarnings: true, // since always_comb has no sensitivities ); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(exampleModule, vectors); }); group('assign subset', () { @@ -110,6 +111,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple bits', () async { 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..07adb3cdd 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 = [ @@ -379,6 +379,23 @@ void main() { }); group('simcompare', () { + SystemCVectorExecutable? busSystemCExe; + + setUpAll(() async { + final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + busSystemCExe = SimCompare.buildSystemCVectorExecutable(gtm); + }); + + tearDownAll(SimCompare.cleanupSystemCCache); + + void checkBusSystemC(List vectors) { + final exe = busSystemCExe; + if (exe != null) { + SimCompare.checkSystemCVectors(exe, vectors); + } + } + group('const sv gen', () { test('Subset of a const', () async { final mod = ConstBusModule(0xabcd, subset: true); @@ -389,6 +406,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, dontDeleteTmpFiles: true); }); test('Assignment of a const', () async { @@ -400,8 +418,9 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, dontDeleteTmpFiles: true); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog().output; expect(sv.contains("assign const_subset = 16'habcd;"), true); }); }); @@ -418,6 +437,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('And2Gate bus', () async { @@ -434,6 +454,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Operator indexing', () async { @@ -450,6 +471,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + checkBusSystemC(vectors); }); test('Bus shrink', () async { @@ -487,6 +509,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus reverse slice', () async { @@ -524,6 +547,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus reversed', () async { @@ -537,6 +561,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus range', () async { @@ -582,6 +607,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus swizzle', () async { @@ -597,6 +623,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('Bus bit', () async { @@ -610,6 +637,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('add busses', () async { @@ -625,6 +653,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + checkBusSystemC(vectors); }); test('expression bit select', () async { @@ -635,6 +664,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + checkBusSystemC(vectors); }); test('selectFrom and selectIndex', () async { @@ -652,6 +682,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors, dontDeleteTmpFiles: true); }); test('selectFrom with default Value', () async { @@ -666,6 +697,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors, dontDeleteTmpFiles: true); }); }); } diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 8128eea2d..53ea8f5be 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -61,12 +61,13 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); 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 +79,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/comparison_test.dart b/test/comparison_test.dart index 1a3e5e98c..a3e24ff25 100644 --- a/test/comparison_test.dart +++ b/test/comparison_test.dart @@ -136,6 +136,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); final simResult = SimCompare.iverilogVector(gtm, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(gtm, vectors); }); }); } diff --git a/test/conditionals_test.dart b/test/conditionals_test.dart index 44136fa85..8f424ac2d 100644 --- a/test/conditionals_test.dart +++ b/test/conditionals_test.dart @@ -490,6 +490,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -564,6 +565,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('iffblock comb', () async { @@ -578,6 +580,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('if invalid ', () async { @@ -600,6 +603,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('elseifblock comb', () async { @@ -614,6 +618,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('Conditional assign module with invalid inputs', () async { @@ -654,6 +659,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('case comb', () async { @@ -668,6 +674,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('Unique case', () async { @@ -696,6 +703,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test('should return exception if a conditional is used multiple times.', @@ -717,6 +725,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -731,6 +740,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -745,6 +755,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -780,6 +791,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test( 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..583af1999 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)))); @@ -69,6 +69,7 @@ void main() { await SimCompare.checkFunctionalVector(counter, vectors); final simResult = SimCompare.iverilogVector(counter, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(counter, vectors); }); }); } 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/extend_test.dart b/test/extend_test.dart index 9490c96ce..a6a7431be 100644 --- a/test/extend_test.dart +++ b/test/extend_test.dart @@ -52,6 +52,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); } test('zero extend with same width returns same thing', () async { @@ -117,6 +118,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); final simResult = SimCompare.iverilogVector(mod, vectors); expect(simResult, equals(true)); + SimCompare.checkSystemCVector(mod, vectors); } test('setting with bigger number throws exception', () { 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..d8f8b3fea 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 @@ -53,6 +53,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bit with enable', () async { @@ -74,6 +75,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus', () async { @@ -88,6 +90,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus with enable', () async { @@ -111,6 +114,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, no reset value', () async { @@ -124,6 +128,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, const reset value', () async { @@ -141,6 +146,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, logic reset value', () async { @@ -158,6 +164,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus no reset, const reset value', () async { @@ -174,6 +181,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus, enable, reset, const reset value', () async { @@ -194,6 +202,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } 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..87966ba56 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;')); @@ -689,6 +689,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('build fails with missing inout port', () { 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/systemc_ffi_cosim_test.dart b/test/systemc_ffi_cosim_test.dart new file mode 100644 index 000000000..a2ad3fa27 --- /dev/null +++ b/test/systemc_ffi_cosim_test.dart @@ -0,0 +1,265 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_ffi_cosim_test.dart +// Demonstrates FFI-based SystemC co-simulation with existing ROHD tests. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +@TestOn('vm') +@Tags(['ffi']) +library; + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/systemc_cosim_ffi.dart'; +import 'package:test/test.dart'; + +// ═══════════════════════════════════════════════════════════════════════════ +// DUT: A simple counter (same as systemc_simcompare_test.dart) +// ═══════════════════════════════════════════════════════════════════════════ + +class SimpleCounter extends Module { + Logic get val => output('val'); + + SimpleCounter(Logic clk, Logic reset, Logic en) + : super(name: 'SimpleCounter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Test that runs identically against both ROHD sim and SystemC FFI cosim +// ═══════════════════════════════════════════════════════════════════════════ + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + tearDownAll(SystemCFfiCosim.cleanupCache); + + /// The core test logic — parametrized so it can run against either the + /// native ROHD module or the SystemC FFI co-simulated module. + /// + /// [getVal] provides the output signal to check (from ROHD or cosim). + Future counterTest({ + required Logic Function() getVal, + required Logic clk, + required Logic reset, + required Logic en, + }) async { + Simulator.setMaxSimTime(200); + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + en.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Enable counting + en.inject(1); + await clk.nextPosedge; + + // After first posedge with en=1, counter should have incremented + // previousValue = 0 (value before this edge) + // value = 1 (updated at this edge) + expect(getVal().previousValue!.toInt(), 0); + expect(getVal().value.toInt(), 1); + + await clk.nextPosedge; + expect(getVal().previousValue!.toInt(), 1); + expect(getVal().value.toInt(), 2); + + await clk.nextPosedge; + expect(getVal().value.toInt(), 3); + + // Disable — counter should freeze + en.inject(0); + await clk.nextPosedge; + expect(getVal().value.toInt(), 3); + + await clk.nextPosedge; + expect(getVal().value.toInt(), 3); + + // Re-enable + en.inject(1); + await clk.nextPosedge; + expect(getVal().value.toInt(), 4); + + await Simulator.endSimulation(); + } + + // ───────────────────────────────────────────────────────────────────── + // Test 1: Pure ROHD simulation (baseline) + // ───────────────────────────────────────────────────────────────────── + + test('counter - ROHD native simulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + await counterTest( + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + }); + + // ───────────────────────────────────────────────────────────────────── + // Test 2: SystemC FFI co-simulation (same test logic!) + // ───────────────────────────────────────────────────────────────────── + + test('counter - SystemC FFI cosimulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + // Create the FFI cosim — this compiles the SystemC .so and hooks + // into the Simulator's clkStable phase. + final cosim = await SystemCFfiCosim.create( + counter, + clk: clk, + ); + + // SystemC is optional in local development environments. + if (cosim == null) { + markTestSkipped('SystemC is unavailable on this platform.'); + return; + } + + try { + await counterTest( + // Use the same output signal — the cosim module puts() values + // onto it at clkStable, overriding the ROHD-computed values. + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + } finally { + await cosim.dispose(); + } + }); + + // ───────────────────────────────────────────────────────────────────── + // Test 3: Negedge checking (inject → await negedge → expect pattern) + // ───────────────────────────────────────────────────────────────────── + + /// Test logic that uses negedge for combinational settling checks. + /// Pattern: inject at posedge → await negedge (immediate next edge) → check + Future counterNegedgeTest({ + required Logic Function() getVal, + required Logic clk, + required Logic reset, + required Logic en, + }) async { + Simulator.setMaxSimTime(200); + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + en.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + + // De-assert reset at posedge, check settled at negedge + reset.inject(0); + await clk.nextNegedge; // immediate next edge — no posedge in between + expect(getVal().value.toInt(), 0); // counter still 0 + + // Enable at posedge: inject en=1 at the posedge tick itself + await clk.nextPosedge; // posedge fires with en=0 (inject hasn't happened) + en.inject(1); // will take effect at NEXT mainTick + await clk.nextNegedge; // settle — en is now 1 but Sequential already + // fired at this posedge with en=0 + expect(getVal().value.toInt(), 0); // still 0 + + // Next posedge: Sequential sees en=1 + await clk.nextPosedge; + expect(getVal().value.toInt(), 1); // incremented! + + // Check at negedge: value stable between edges + await clk.nextNegedge; + expect(getVal().value.toInt(), 1); // unchanged + + // Another posedge + await clk.nextPosedge; + expect(getVal().value.toInt(), 2); + + // Disable at posedge, check at negedge + en.inject(0); + await clk.nextNegedge; + expect(getVal().value.toInt(), 2); // still 2 + + // Confirm stays 2 after next posedge with en=0 + await clk.nextPosedge; + expect(getVal().value.toInt(), 2); + + await Simulator.endSimulation(); + } + + test('counter negedge - ROHD native simulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + await counterNegedgeTest( + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + }); + + test('counter negedge - SystemC FFI cosimulation', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final counter = SimpleCounter(clk, reset, en); + await counter.build(); + + final cosim = await SystemCFfiCosim.create( + counter, + clk: clk, + ); + + if (cosim == null) { + markTestSkipped('SystemC is unavailable on this platform.'); + return; + } + + try { + await counterNegedgeTest( + getVal: () => counter.val, + clk: clk, + reset: reset, + en: en, + ); + } finally { + await cosim.dispose(); + } + }); +} diff --git a/test/systemc_simcompare_test.dart b/test/systemc_simcompare_test.dart new file mode 100644 index 000000000..9374b479a --- /dev/null +++ b/test/systemc_simcompare_test.dart @@ -0,0 +1,259 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare_test.dart +// Tests for SystemC synthesis and simulation comparison. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// A simple module with basic gates for testing SystemC synthesis. +class GateModule extends Module { + GateModule(Logic a, Logic b) : super(name: 'GateModule') { + a = addInput('a', a); + b = addInput('b', b); + final aAndB = addOutput('a_and_b'); + final aOrB = addOutput('a_or_b'); + final notA = addOutput('not_a'); + + aAndB <= a & b; + aOrB <= a | b; + notA <= ~a; + } +} + +/// A simple counter for testing sequential SystemC synthesis. +class SimpleCounter extends Module { + SimpleCounter(Logic clk, Logic reset, Logic en) : super(name: 'Counter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +/// A flip-flop module for testing. +class FlopModule extends Module { + FlopModule(Logic clk, Logic reset, Logic d) : super(name: 'FlopModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset); + } +} + +/// A flip-flop with enable. +class FlopEnModule extends Module { + FlopEnModule(Logic clk, Logic reset, Logic en, Logic d) + : super(name: 'FlopEnModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset, en: en); + } +} + +/// A chained combinational module for checking generated sensitivity lists. +class ChainedGateModule extends Module { + ChainedGateModule(Logic a, Logic b, Logic c) + : super(name: 'ChainedGateModule') { + a = addInput('a', a); + b = addInput('b', b); + c = addInput('c', c); + final y = addOutput('y'); + + final mid = (a & b).named('mid'); + y <= mid | c; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + tearDownAll(SimCompare.cleanupSystemCCache); + + group('SimCompare SystemC', () { + test('gate module passes vectors', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = GateModule(a, b); + await mod.build(); + + final vectors = [ + Vector({'a': 0, 'b': 0}, {'a_and_b': 0, 'a_or_b': 0, 'not_a': 1}), + Vector({'a': 1, 'b': 0}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 0}), + Vector({'a': 0, 'b': 1}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 1}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1, 'a_or_b': 1, 'not_a': 0}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('counter module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Same vectors as counter_test.dart (iverilog-compatible timing) + final vectors = [ + Vector({'en': 0, 'reset': 0}, {}), + Vector({'en': 0, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 1}), + Vector({'en': 1, 'reset': 0}, {'val': 2}), + Vector({'en': 1, 'reset': 0}, {'val': 3}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 1, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 5}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final d = Logic(name: 'd', width: 8); + final mod = FlopModule(clk, reset, d); + await mod.build(); + + // Flop: output follows input with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0xAA, 'reset': 0}, {'q': 0}), + Vector({'d': 0xBB, 'reset': 0}, {'q': 0xAA}), + Vector({'d': 0xCC, 'reset': 0}, {'q': 0xBB}), + Vector({'d': 0xDD, 'reset': 0}, {'q': 0xCC}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop with enable passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final d = Logic(name: 'd', width: 8); + final mod = FlopEnModule(clk, reset, en, d); + await mod.build(); + + // When en=0, q holds; when en=1, q follows d with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0x42, 'en': 1, 'reset': 0}, {'q': 0}), + Vector({'d': 0x55, 'en': 1, 'reset': 0}, {'q': 0x42}), + Vector({'d': 0xFF, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x00, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x99, 'en': 1, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0xAA, 'en': 1, 'reset': 0}, {'q': 0x99}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('independent inline gates share a method', () async { + final mod = GateModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final systemc = mod.generateSystemC(); + final assignMethods = + RegExp(r'SC_METHOD\(assign_\d+\);').allMatches(systemc).toList(); + + expect(assignMethods, hasLength(1)); + expect(systemc, contains('sensitive << a;')); + expect(systemc, contains('sensitive << b;')); + expect(systemc, contains('a_and_b = a.read() & b.read();')); + expect(systemc, contains('a_or_b = a.read() | b.read();')); + expect(systemc, contains('not_a = !a.read();')); + }); + + test('chained inline gates use minimal sensitivity lists', () async { + final mod = ChainedGateModule( + Logic(name: 'a'), + Logic(name: 'b'), + Logic(name: 'c'), + ); + await mod.build(); + + final systemc = mod.generateSystemC(); + final sensitivityBlocks = + RegExp(r'SC_METHOD\(assign_\d+\);\n((?: sensitive << .+;\n)+)') + .allMatches(systemc) + .map((match) => match.group(1)!) + .toList(); + + expect(sensitivityBlocks, hasLength(2)); + expect( + sensitivityBlocks, + contains(allOf( + contains('sensitive << a;'), + contains('sensitive << b;'), + isNot(contains('sensitive << mid;')), + )), + ); + expect( + sensitivityBlocks, + contains(allOf( + contains('sensitive << mid;'), + contains('sensitive << c;'), + isNot(contains('sensitive << a;')), + isNot(contains('sensitive << b;')), + )), + ); + }); + + test('counter trace-based comparison', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Use the trace-based approach: just write normal simulation code, + // no vectors needed. The method records all I/O at every clock edge + // and replays through SystemC. + final result = await SimCompare.systemcSimCompare( + mod, + clk, + stimulus: () async { + reset.inject(1); + en.inject(0); + Simulator.registerAction(25, () { + reset.put(0); + en.put(1); + }); + Simulator.registerAction(65, () { + en.put(0); + }); + Simulator.registerAction(85, () { + en.put(1); + }); + Simulator.setMaxSimTime(120); + }, + ); + expect(result, isTrue); + }); + }); +} 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..e9759c9ec 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 @@ -13,9 +13,13 @@ set -euo pipefail declare -r folder_name='tmp_test' -# The "tmp_test" folder after performing the tests should be empty. +# The "tmp_test" folder after performing the tests should be empty, +# except for the precompiled-header cache (pch/) which is intentionally +# persistent and pre-built by CI before the test run. if [ -d "${folder_name}" ]; then - output=$(find ${folder_name} | wc --lines | tee) + output=$(find ${folder_name} -not -path "${folder_name}/pch" \ + -not -path "${folder_name}/pch/*" \ + | wc --lines | tee) if [ "${output}" -eq 1 ]; then echo "Success: directory \"${folder_name}\" is empty!" else diff --git a/tool/gh_actions/cleanup_systemc_tmp.sh b/tool/gh_actions/cleanup_systemc_tmp.sh new file mode 100755 index 000000000..d91149adb --- /dev/null +++ b/tool/gh_actions/cleanup_systemc_tmp.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# cleanup_systemc_tmp.sh +# GitHub Actions step helper: remove SystemC temporary build caches. +# +# 2026 June +# Author: Desmond Kirkpatrick + +set -euo pipefail + +declare -r folder_name='tmp_test' + +if [ ! -d "${folder_name}" ]; then + exit 0 +fi + +find "${folder_name}" -mindepth 1 \ + \( \ + -name 'pch' -o \ + -name 'pch.lock' -o \ + -name 'tmp_sc_*' -o \ + -name 'sc_input_*' -o \ + -name 'Makefile_sc' \ + \) \ + -exec rm -rf {} + + +mkdir -p "${folder_name}" \ No newline at end of file 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 diff --git a/tool/gh_actions/install_systemc.sh b/tool/gh_actions/install_systemc.sh new file mode 100755 index 000000000..5c3fb82bf --- /dev/null +++ b/tool/gh_actions/install_systemc.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# install_systemc.sh +# GitHub Actions step: Install Accellera SystemC library. +# +# Downloads, builds, and installs SystemC to /opt/systemc. +# Uses a cache-friendly layout so the install directory can be +# cached across CI runs. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SYSTEMC_VERSION="${SYSTEMC_VERSION:-3.0.2}" +INSTALL_PREFIX="${SYSTEMC_INSTALL_PREFIX:-/opt/systemc}" + +if [ "$(id -u)" -eq 0 ]; then + SUDO=() +else + SUDO=(sudo) +fi + +# Skip if already installed (e.g. from cache) +if [ -f "$INSTALL_PREFIX/lib/libsystemc.so" ]; then + echo "SystemC already installed at $INSTALL_PREFIX — skipping build." + exit 0 +fi + +echo "Installing Accellera SystemC $SYSTEMC_VERSION to $INSTALL_PREFIX ..." + +# Install build dependencies +"${SUDO[@]}" apt-get update -qq +"${SUDO[@]}" apt-get install --yes --no-install-recommends cmake g++ make + +# Download source +TARBALL="systemc-$SYSTEMC_VERSION.tar.gz" +DOWNLOAD_URL="https://github.com/accellera-official/systemc/archive/refs/tags/$SYSTEMC_VERSION.tar.gz" + +cd /tmp +curl -fsSL -o "$TARBALL" "$DOWNLOAD_URL" +tar xzf "$TARBALL" +cd "systemc-$SYSTEMC_VERSION" + +# Build with CMake +mkdir -p build && cd build +cmake .. \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DBUILD_SHARED_LIBS=ON \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_REGRESSION=OFF \ + -DDISABLE_COPYRIGHT_MESSAGE=ON + +make -j"$(nproc)" +"${SUDO[@]}" make install + +echo "SystemC $SYSTEMC_VERSION installed to $INSTALL_PREFIX" diff --git a/tool/gh_actions/run_tests.sh b/tool/gh_actions/run_tests.sh index 160352cd2..5e2804edc 100755 --- a/tool/gh_actions/run_tests.sh +++ b/tool/gh_actions/run_tests.sh @@ -11,8 +11,9 @@ set -euo pipefail -dart test +# Exclude FFI-dependent tests (dart:ffi unavailable on some CI platforms). +dart test $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) # run tests in JS (increase heap size also) export NODE_OPTIONS="--max-old-space-size=8192" -dart test --platform node \ No newline at end of file +dart test --platform node $(find test -name '*_test.dart' ! -name 'systemc_ffi_cosim_test.dart' | sort) \ No newline at end of file diff --git a/tool/gh_actions/setup_systemc_pch.sh b/tool/gh_actions/setup_systemc_pch.sh new file mode 100755 index 000000000..25d1ff93f --- /dev/null +++ b/tool/gh_actions/setup_systemc_pch.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# setup_systemc_pch.sh +# GitHub Actions step: Pre-build SystemC precompiled header and Makefile. +# +# Run this after install_systemc.sh and before tests to avoid race +# conditions when multiple test isolates run in parallel. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SC_HOME="${SYSTEMC_INCLUDE:-/opt/systemc/include}" +SC_LIB="${SYSTEMC_LIB:-/opt/systemc/lib}" + +if [ ! -d "$SC_HOME" ]; then + echo "SystemC not found at $SC_HOME — skipping PCH setup." + exit 0 +fi + +# Detect C++ standard from the installed library +CXX_STD="c++17" +if command -v nm &>/dev/null && [ -f "$SC_LIB/libsystemc.so" ]; then + if nm -D "$SC_LIB/libsystemc.so" 2>/dev/null | grep -q 'cxx202002L'; then + CXX_STD="c++20" + fi +fi + +echo "Setting up SystemC PCH ($CXX_STD) ..." + +# Build precompiled header +PCH_DIR="tmp_test/pch" +mkdir -p "$PCH_DIR" +cp "$SC_HOME/systemc.h" "$PCH_DIR/systemc.h" +g++ -std="$CXX_STD" -I"$SC_HOME" -x c++-header \ + -o "$PCH_DIR/systemc.h.gch" "$SC_HOME/systemc.h" + +echo "PCH built: $PCH_DIR/systemc.h.gch" diff --git a/tool/gh_codespaces/install_systemc.sh b/tool/gh_codespaces/install_systemc.sh new file mode 100755 index 000000000..2999ae02c --- /dev/null +++ b/tool/gh_codespaces/install_systemc.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# install_systemc.sh +# GitHub Codespaces setup: Install the SystemC development package. +# +# 2026 August + +set -euo pipefail + +sudo apt-get update -qq +sudo apt-get install --yes --no-install-recommends libsystemc-dev diff --git a/tool/gh_codespaces/run_setup.sh b/tool/gh_codespaces/run_setup.sh index 6523e4147..9a2fd924e 100755 --- a/tool/gh_codespaces/run_setup.sh +++ b/tool/gh_codespaces/run_setup.sh @@ -20,5 +20,8 @@ tool/gh_actions/install_dependencies.sh # Install Icarus Verilog. tool/gh_actions/install_iverilog.sh +# Install SystemC. +tool/gh_codespaces/install_systemc.sh + # Install Node tool/gh_actions/install_node.sh \ No newline at end of file diff --git a/tool/run_checks.sh b/tool/run_checks.sh index 6a0ea0080..939750a47 100755 --- a/tool/run_checks.sh +++ b/tool/run_checks.sh @@ -63,6 +63,10 @@ fi print_step 'Run project tests' tool/gh_actions/run_tests.sh +# Clean SystemC temporary files +print_step 'Clean SystemC temporary files' +tool/gh_actions/cleanup_systemc_tmp.sh + # Check temporary test files print_step 'Check temporary test files' tool/gh_actions/check_tmp_test.sh