diff --git a/CHANGELOG.md b/CHANGELOG.md index 6294c1c42..c7d0a2e8a 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()` returns simple in-memory output; + `dumpWaves()` provides standard VCD capture as the replacement for + `WaveDumper`. `WaveDumper` and `generateSynth` are deprecated in favor of + these `Module` methods. + ## 0.6.10 - Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bb9116ce..1c10e0bfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Anyone interested in participating in ROHD is more than welcome to help! ## Code of Conduct -ROHD adopts the [Contributor Covenant](https://www.contributor-covenant.org/) v2.1 for the code of conduct. It can be accessed [here](CODE_OF_CONDUCT.md). +ROHD adopts the [Contributor Covenant](https://www.contributor-covenant.org/) v2.1 for the [Code of Conduct](CODE_OF_CONDUCT.md). ## Getting Help @@ -124,7 +124,7 @@ Please include the SPDX tag near the top of any new files you create: Here is an example of a recommended file header template: ```dart -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // example.dart diff --git a/benchmark/many_submodules_benchmark.dart b/benchmark/many_submodules_benchmark.dart index 763261a4c..44fb74fdb 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(); } } 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/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..ad1eee935 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()}'); } 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..9406d1e1d 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()); 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..5ad3f29f3 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()); } diff --git a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart index be347b040..1cf09cbb0 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()); 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..1a2e27411 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()); 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..153213a8c 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()); 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..ae1efd096 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()); 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..9d0004769 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()); 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..c005d864b 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()); 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..d3dc88e29 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()); 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..ffc204440 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()); 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..0afd3dfb8 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()); 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..775cfe91b 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()); 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..96abf4c60 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()); 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..d9fcbf501 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()); - WaveDumper(counter, + counter.dumpWaves( outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); Simulator.registerAction(25, () { intf.en.put(1); diff --git a/doc/tutorials/chapter_8/oven_fsm.dart b/doc/tutorials/chapter_8/oven_fsm.dart index 0fcd8002a..844baa8ec 100644 --- a/doc/tutorials/chapter_8/oven_fsm.dart +++ b/doc/tutorials/chapter_8/oven_fsm.dart @@ -192,7 +192,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); + oven.dumpWaves(outputPath: 'doc/tutorials/chapter_8/oven.vcd'); } if (!noPrint) { diff --git a/doc/tutorials/chapter_9/rohd_vf.md b/doc/tutorials/chapter_9/rohd_vf.md index 7d6035930..ceb5c58d1 100644 --- a/doc/tutorials/chapter_9/rohd_vf.md +++ b/doc/tutorials/chapter_9/rohd_vf.md @@ -435,7 +435,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + tb.counter.dumpWaves(); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart index 4b1ef9c34..782a06e45 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart +++ b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart @@ -315,7 +315,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + tb.counter.dumpWaves(); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index e4e625952..2fc7aea8c 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -1,13 +1,14 @@ --- title: "Generating Outputs" permalink: /docs/generation/ -last_modified_at: 2023-11-13 +last_modified_at: 2026-08-19 toc: true --- Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. -The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: +The simplest way to write SystemVerilog is with `dumpSystemVerilog` on +`Module`: ```dart void main() async { @@ -16,24 +17,40 @@ void main() async { // remember that `build` returns a `Future`, hence the `await` here await myModule.build(); - final generatedSv = myModule.generateSynth(); - - // you can print it out... - print(generatedSv); - - // or write it to a file - File('myHardware.sv').writeAsStringSync(generatedSv); + myModule.dumpSystemVerilog(outputPath: 'myHardware.sv'); } ``` -The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. + `dumpSystemVerilog` writes one file containing the SystemVerilog `module` + definitions for the top-level module and all recursive submodules. To write + one `.sv` file per module definition instead, pass a directory and set + `multiFile` to `true`: + + ```dart + myModule.dumpSystemVerilog( + outputPath: 'build/systemverilog', + multiFile: true, + ); + ``` + + For generated text without writing a file, use `dumpSystemVerilog` without an + `outputPath`: + + ```dart + final generatedSv = myModule.dumpSystemVerilog().output; + ``` + + The dump methods preserve the legacy one-shot output workflow. For the service + API, artifact streams, or explicit output configuration, use + `SystemVerilogService` directly. ## Controlling port types Generated ports default to `input logic`, `output logic`, and `inout wire`, preserving the traditional ROHD declarations. Use a `SystemVerilogSynthesizerConfiguration` to independently control whether object types, such as `wire` and `var`, and data types, such as `logic`, are explicit for each port direction: ```dart -final generatedSv = myModule.generateSynth( +myModule.dumpSystemVerilog( + outputPath: 'myHardware.sv', configuration: const SystemVerilogSynthesizerConfiguration( inputPortType: SystemVerilogPortTypeConfiguration( objectType: SystemVerilogPortType.explicit, @@ -51,7 +68,8 @@ final generatedSv = myModule.generateSynth( ); ``` -The same configuration can be passed directly to `SystemVerilogSynthesizer` when using `SynthBuilder`. +The same configuration can be passed directly to `SystemVerilogSynthesizer` +when using `SynthBuilder`. ## Controlling naming @@ -79,6 +97,113 @@ Internal signals, unlike ports, don't need to always have the same exact name as The `Naming.unpreferredName` function will modify a signal name to indicate to downstream flows that the name is preferably omitted from the output, but preferable to an unnamed signal. This is generally most useful for things like output ports of `InlineSystemVerilog` modules. -## More advanced generation +## Services API + +`ModuleService` is the shared API for module-scoped generation, capture, and +inspection. Services can register with `ModuleServices` for lookup by DevTools +and other consumers. `ArtifactProducingService` implementations expose named +artifacts as media-typed byte streams, so consumers do not need to require a +local output file. + +### Registering and discovering services + +Services such as `SystemVerilogService` and `WaveformService` register +themselves by default when constructed. `ModuleServices` keeps the most recently +registered service of each concrete type. This allows DevTools, application +code, and other services to discover an optional capability without requiring +the creator to pass the service instance to every consumer: + +```dart +final systemVerilog = SystemVerilogService(myModule); + +final registeredSystemVerilog = + ModuleServices.instance.lookup(); + +assert(identical(systemVerilog, registeredSystemVerilog)); +assert(identical(systemVerilog, SystemVerilogService.current)); +``` + +Constructing another `SystemVerilogService` replaces the previous +`SystemVerilogService` in the registry. Other service types remain registered. +Use `unregister()` to remove one service type or `reset()` to clear the +registry. + +Registration also enables services to collaborate without directly depending +on how the application created them. For example, a generation or capture +service can look up an optional tracing service and include source file, line, +and column information when tracing is available. Because a service may consult +the registry during construction or generation, register supporting services +before constructing the services that consume them. + +For one-shot work that should not change globally discoverable service state, +set `register` to `false`: + +```dart +final oneShotSystemVerilog = SystemVerilogService( + myModule, + register: false, +); +``` -Under the hood of `generateSynth`, it's actually using a [`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) which accepts a `Module` and a `Synthesizer` (usually a `SystemVerilogSynthesizer`) as arguments. This `SynthBuilder` can provide a collection of `String` file contents via `getFileContents`, or you can ask for the full set of `synthesisResults`, which contains `SynthesisResult`s which can each be converted `toSynthFileContents` but also has context about the `module` it refers to, the `instanceTypeName`, etc. With these APIs, you can easily generate named files, add file headers, ignore generation of some modules, generate file lists for other tools, etc. The `SynthBuilder.multi` constructor makes it convenient to generate outputs for multiple independent hierarchies. +The `Module.dumpSystemVerilog()` and `Module.dumpWaves()` convenience methods +use the normal registration defaults. Construct the corresponding service +directly with `register: false` when this side effect is not desired. + +`SystemVerilogService` is the direct synthesis service. Its `outputDirectory` +defaults to the current directory and its `outputBaseName` defaults to the top +module's `definitionName`. The service generates output in memory; call +`writeOutputs` only when files are required: + +```dart +final service = SystemVerilogService( + myModule, + outputDirectory: 'build/netlist', + outputBaseName: 'accelerator', + configuration: const SystemVerilogSynthesizerConfiguration(), +); + +// Use the generated SystemVerilog directly. +final generatedSv = service.output; + +// Or inspect a transport-neutral artifact stream. +final artifact = service.artifacts.single; +final bytes = await artifact.openRead().expand((chunk) => chunk).toList(); + +// Write build/netlist/accelerator.sv. +service.writeOutputs(); +``` + +With `multiFile: true`, `SystemVerilogService` writes one `.sv` file per +generated module definition. For custom synthesis flows, +[`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) +accepts a `Module` and a `Synthesizer` (usually a +`SystemVerilogSynthesizer`). + +## Capturing waveforms + +Use `dumpWaves` for the legacy-compatible common case of writing all simulation +signals to a VCD file: + +```dart +myModule.dumpWaves(outputPath: 'waves.vcd'); +``` + +`WaveformService` records VCD data in memory by default and exposes it as a +`ModuleServiceArtifact`. Its output directory and basename use the same +defaults as other artifact-producing services. Set `writeToFile` when capture +should also create a VCD file: + +```dart +final waveform = WaveformService( + myModule, + outputDirectory: 'build/waves', + outputBaseName: 'interesting-signals', + writeToFile: true, + timescale: '1ns', + startTime: 100, + stopTime: 1000, + signalFilter: (signal) => signal.name.startsWith('debug_'), +); + +final vcdArtifact = waveform.artifacts.single; +``` diff --git a/example/example.dart b/example/example.dart index 74abf8e4b..53a48f38b 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(); 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 index 710096c3e..155e629d5 100644 --- a/example/filter_bank.dart +++ b/example/filter_bank.dart @@ -63,7 +63,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(dut, outputPath: 'filter_bank.vcd'); + dut.dumpWaves(outputPath: 'filter_bank.vcd'); } // Kick off the simulation. diff --git a/example/fir_filter.dart b/example/fir_filter.dart index 571bbd1ed..765a7ef3b 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(); 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..2ec2e5dfe 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(); 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..4c79584f5 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(); if (!noPrint) { print(generatedSystemVerilog); } diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..bda2229c1 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,13 @@ -// 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'; @@ -12,6 +19,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..8ff34cf81 --- /dev/null +++ b/lib/src/diagnostics/diagnostics.dart @@ -0,0 +1,12 @@ +// 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'; 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..f77c0fa1d --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,488 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_service.dart +// Base waveform service: file output with filtering, timescale, and +// flush/overwrite control. Designed to be subclassed by the DevTools +// streaming variant. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +// ─── Supporting types ──────────────────────────────────────────────────────── + +/// The output format for waveform capture. +enum WaveOutputFormat { + /// Value Change Dump — the classic text-based waveform format. + vcd, + + /// Fast Signal Trace — a compact binary format. + /// + /// Requires an FST writer to be available; see the DevTools subclass for + /// a fully FST-backed implementation. + fst; + + /// 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, +} + +// ─── Service ───────────────────────────────────────────────────────────────── + +/// A waveform capture service that records signal changes. +/// +/// This is the base class for waveform capture. It handles: +/// - Signal collection (with optional [signalFilter]) +/// - In-memory VCD output with configurable [timescale] +/// - Selective recording via [startTime] / [stopTime] +/// - Optional file output with periodic buffer flushing and [overwritePolicy] +/// - Optional registration with [ModuleServices] +/// +/// **Subclassing for DevTools streaming:** +/// +/// Override the protected hooks below to intercept the simulation event loop +/// without re-implementing the file-writing logic: +/// +/// - [onSignalCollected] — called once per tracked signal at startup; use +/// it to register signals in a VM-service index. +/// - [onValueChange] — called for every value-change event within the +/// [startTime]/[stopTime] window; use it to feed an in-memory store for +/// streaming. +/// - [onTimestampCapture] — called once per simulation timestamp that +/// contains at least one change; the full changed-signal set is passed. +/// - [onSimulationEnd] — called after the final timestamp is written and +/// the file is closed; use it to finalise any streaming buffers. +/// +/// Example subclass skeleton: +/// ```dart +/// class DevToolsWaveformService extends WaveformService { +/// DevToolsWaveformService( +/// super.module, { +/// super.outputDirectory, +/// super.outputBaseName, +/// }); +/// +/// @override +/// void onSignalCollected(Logic signal) { +/// super.onSignalCollected(signal); +/// _registerWithVmService(signal); +/// } +/// +/// @override +/// void onValueChange(Logic signal, int timestamp) { +/// super.onValueChange(signal, timestamp); +/// _recordInMemory(signal, timestamp); +/// } +/// } +/// ``` +class WaveformService extends ArtifactProducingService { + /// The most recently registered [WaveformService], or `null`. + static WaveformService? current; + + /// Path of the output waveform file. + /// + /// Derived from [outputDirectory], [outputBaseName], and [format]. + String get outputFilePath => '$outputDirectory${Platform.pathSeparator}' + '${outputFileName ?? '$outputBaseName.${format.fileExtension}'}'; + + /// The output filepath of the generated waveforms. + /// + /// This matches the legacy waveform dumper's `outputPath` name. + String get outputPath => outputFilePath; + + /// 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; + + /// Output format. + final WaveOutputFormat format; + + /// Optional predicate that determines whether a given [Logic] signal is + /// captured. + /// + /// When `null`, all non-[Const] signals in the hierarchy are captured, + /// matching the legacy waveform dumper behaviour. + final bool Function(Logic signal)? signalFilter; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Simulation time at which recording begins. + /// + /// Signals are still collected before this time so they appear in the scope + /// definition, but value-change events are suppressed until [startTime] is + /// reached. `null` means "from the very start". + final int? startTime; + + /// Simulation time at which recording ends. + /// + /// Value-change events after this time are suppressed. `null` means "until + /// end of simulation". + final int? stopTime; + + /// Number of characters accumulated in the write buffer before it is flushed + /// to disk. + final int flushBufferSize; + + /// What to do when the output file already exists. + final OverwritePolicy overwritePolicy; + + /// Whether to register this service with [ModuleServices] for inspection. + final bool register; + + /// Whether waveform bytes are written to [outputFilePath]. + /// + /// When `false`, the service retains its waveform bytes in memory and exposes + /// them through [artifacts]. + final bool writeToFile; + + // ─── Internal file-writing state ───────────────────────────── + + /// Sink writing to [outputFilePath] when [writeToFile] is true. + IOSink? _outFileSink; + + /// Write buffer; flushed when it exceeds [flushBufferSize]. + final StringBuffer _fileBuffer = StringBuffer(); + + /// The complete waveform output retained for streaming artifacts. + final StringBuffer _inMemoryOutput = StringBuffer(); + + /// Counter for assigning compact signal markers in the VCD. + int _signalMarkerIdx = 0; + + /// Maps each captured [Logic] to its VCD marker string. + final Map _signalToMarkerMap = {}; + + /// Signals that changed during the current simulation timestamp. + final Set _changedThisTimestamp = HashSet(); + + /// The timestamp currently being accumulated. + int _currentDumpingTimestamp = Simulator.time; + + // ─── Constructor ───────────────────────────────────────────── + + /// Creates a [WaveformService] for [module]. + /// + /// [module] must be built before construction. + /// + /// [outputDirectory] defaults to the current directory and [outputBaseName] + /// defaults to [Module.definitionName]. The selected [format] determines the + /// output filename extension. Only [WaveOutputFormat.vcd] is currently + /// supported by this service. + /// + /// Use the optional constructor parameters to configure format, filtering, + /// timescale, start/stop times, flush size, and overwrite policy. + 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.writeToFile = false, + }) : super(module) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating WaveformService. ' + 'Call build() first.', + ); + } + if (format != WaveOutputFormat.vcd) { + throw UnsupportedError( + 'Waveform format ${format.name} is not supported by WaveformService.', + ); + } + + if (writeToFile && overwritePolicy == OverwritePolicy.failIfExists) { + final f = File(outputFilePath); + if (f.existsSync()) { + throw FileSystemException( + 'Waveform output file already exists and overwritePolicy is ' + 'failIfExists.', + outputFilePath, + ); + } + } + + if (writeToFile) { + _outFileSink = + (File(outputFilePath)..createSync(recursive: true)).openWrite(); + } + + _collectSignals(); + _writeHeader(); + _writeScope(); + + Simulator.preTick.listen((_) { + if (Simulator.time != _currentDumpingTimestamp) { + if (_changedThisTimestamp.isNotEmpty) { + _captureTimestamp(_currentDumpingTimestamp); + } + _currentDumpingTimestamp = Simulator.time; + } + }); + + Simulator.registerEndOfSimulationAction(() async { + _captureTimestamp(Simulator.time); + await _terminate(); + onSimulationEnd(); + }); + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + // ─── Extensibility hooks ────────────────────────────────────── + + /// Called once for each [Logic] signal that passes + /// [signalFilter] during initial signal collection. + /// + /// Override in a subclass to register signals with an in-memory store, + /// VM service index, or FST handle map. Always call `super` first. + @protected + void onSignalCollected(Logic signal) {} + + /// Called for every value-change event on [signal] at [timestamp]. + /// + /// Only called within the [startTime] / [stopTime] window. + /// + /// Override in a subclass to feed an in-memory waveform store or + /// streaming buffer. Always call `super` first. + @protected + void onValueChange(Logic signal, int timestamp) {} + + /// Called once per simulation timestamp that contains at least one change, + /// after all value-change events for that timestamp have been processed. + /// + /// [changed] is the set of signals that changed at [timestamp]. + /// + /// Override in a subclass to flush incremental streaming payloads. + /// Always call `super` first. + @protected + void onTimestampCapture(int timestamp, Set changed) {} + + /// Called after the final timestamp has been written and the file is closed. + /// + /// Override in a subclass to finalise any streaming buffers or emit + /// end-of-simulation notifications. + @protected + void onSimulationEnd() {} + + // ─── Internal signal collection ────────────────────────────── + + void _collectSignals() { + final modulesToParse = [module]; + for (var i = 0; i < modulesToParse.length; i++) { + final m = modulesToParse[i]; + for (final sig in m.signals) { + if (sig is Const) { + continue; + } + if (signalFilter != null && !signalFilter!(sig)) { + continue; + } + + _signalToMarkerMap[sig] = 's${_signalMarkerIdx++}'; + onSignalCollected(sig); + + sig.changed.listen((_) { + _changedThisTimestamp.add(sig); + }); + } + + for (final subm in m.subModules) { + if (subm is InlineSystemVerilog) { + continue; + } + modulesToParse.add(subm); + } + } + } + + // ─── VCD output helpers ─────────────────────────────────────── + + void _writeHeader() { + final header = ''' +\$date + ${Timestamper.stamp()} +\$end +\$version + ROHD v${Config.version} +\$end +\$comment + Generated by ROHD - www.github.com/intel/rohd +\$end +\$timescale $timescale \$end +'''; + _writeToBuffer(header); + } + + void _writeScope() { + var scopeString = _computeScopeString(module); + scopeString += '\$enddefinitions \$end\n'; + scopeString += '\$dumpvars\n'; + _writeToBuffer(scopeString); + _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + _writeToBuffer('\$end\n'); + } + + String _computeScopeString(Module m, {int indent = 0}) { + final moduleSignalUniquifier = Uniquifier(); + final padding = List.filled(indent, ' ').join(); + var scopeString = '$padding\$scope module ${m.uniqueInstanceName} \$end\n'; + final innerScopeString = StringBuffer(); + + for (final sig in m.signals) { + if (!_signalToMarkerMap.containsKey(sig)) { + continue; + } + final width = sig.width; + final marker = _signalToMarkerMap[sig]; + var signalName = Sanitizer.sanitizeSV(sig.name); + signalName = moduleSignalUniquifier.getUniqueName( + initialName: signalName, + reserved: sig.isPort, + ); + innerScopeString.write( + ' $padding\$var wire $width $marker $signalName \$end\n', + ); + } + for (final subModule in m.subModules) { + innerScopeString.write( + _computeScopeString(subModule, indent: indent + 1), + ); + } + if (innerScopeString.isEmpty) { + return ''; + } + + scopeString += innerScopeString.toString(); + scopeString += '$padding\$upscope \$end\n'; + return scopeString; + } + + bool _isInRecordingWindow(int timestamp) { + if (startTime != null && timestamp < startTime!) { + return false; + } + if (stopTime != null && timestamp > stopTime!) { + return false; + } + return true; + } + + void _captureTimestamp(int timestamp) { + if (!_isInRecordingWindow(timestamp)) { + _changedThisTimestamp.clear(); + return; + } + + _writeToBuffer('#$timestamp\n'); + + final snapshot = Set.of(_changedThisTimestamp); + for (final sig in snapshot) { + _writeSignalValueUpdate(sig); + onValueChange(sig, timestamp); + } + _changedThisTimestamp.clear(); + + onTimestampCapture(timestamp, snapshot); + } + + void _writeSignalValueUpdate(Logic signal) { + final binaryValue = signal.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); + final updateValue = signal.width > 1 + ? 'b$binaryValue ' + : signal.value.toString(includeWidth: false); + final marker = _signalToMarkerMap[signal]; + _writeToBuffer('$updateValue$marker\n'); + } + + // ─── Buffered I/O ───────────────────────────────────────────── + + void _writeToBuffer(String contents) { + _fileBuffer.write(contents); + _inMemoryOutput.write(contents); + if (_fileBuffer.length > flushBufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + _outFileSink?.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } + + Future _terminate() async { + _flushBuffer(); + await _outFileSink?.flush(); + await _outFileSink?.close(); + } + + // ─── Inspection ─────────────────────────────────────────────── + + /// The waveform artifact produced by this service. + @override + Iterable get artifacts => [ + ModuleServiceArtifact( + fileName: outputFileName ?? '$outputBaseName.${format.fileExtension}', + mediaType: format.mediaType, + openRead: () => Stream.value(utf8.encode(_inMemoryOutput.toString())), + ), + ]; + + /// Returns a JSON-serialisable summary of this service. + @override + Map toJson() => { + 'outputDirectory': outputDirectory, + 'outputBaseName': outputBaseName, + 'outputFilePath': outputFilePath, + 'writeToFile': writeToFile, + 'format': format.name, + 'signalCount': _signalToMarkerMap.length, + 'timescale': timescale, + if (startTime != null) 'startTime': startTime!, + if (stopTime != null) 'stopTime': stopTime!, + }; +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 4a6a9e07f..602e56bfa 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,11 +13,8 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; -import 'package:rohd/src/diagnostics/inspector_service.dart'; -import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a synthesizable hardware entity with clearly defined interface @@ -341,7 +338,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. @@ -1133,32 +1130,59 @@ abstract class Module { /// [hierarchy], this is only valid after [build] has been called. String get hierarchicalName => _hierarchyListToString(hierarchy()); + /// Generates synthesized SystemVerilog for this [Module]. + /// + /// Returns one concatenated in-memory SystemVerilog file. The [configuration] + /// controls options specific to SystemVerilog output. + /// + /// For file output, multiple files, artifacts, or access to synthesis + /// results, use [SystemVerilogService] directly. + String dumpSystemVerilog({ + SystemVerilogSynthesizerConfiguration configuration = + const SystemVerilogSynthesizerConfiguration(), + }) => + SystemVerilogService( + this, + configuration: configuration, + register: false, + ).output; + + /// 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 normalized = outputPath.replaceAll(r'\', '/'); + final separatorIndex = normalized.lastIndexOf('/'); + final outputDirectory = switch (separatorIndex) { + -1 => '.', + 0 => '/', + _ => normalized.substring(0, separatorIndex), + }; + final outputFileName = normalized.substring(separatorIndex + 1); + + return WaveformService( + this, + outputDirectory: outputDirectory, + outputFileName: outputFileName, + writeToFile: true, + ); + } + /// 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: ...) for in-memory ' + 'output or SystemVerilogService for advanced options.') String generateSynth({ SystemVerilogSynthesizerConfiguration configuration = const SystemVerilogSynthesizerConfiguration(), - }) { - 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'); - } + }) => + dumpSystemVerilog(configuration: configuration); } diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index 4930d6c96..df6062b48 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -150,7 +150,7 @@ class FlipFlop extends Module with SystemVerilog { var contents = [q < _d]; if (_en != null) { - contents = [If(_en!, then: contents)]; + contents = [If(_en, then: contents)]; } Sequential( diff --git a/lib/src/signals/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 index 0e86e506f..950ca3529 100644 --- a/lib/src/synthesizers/netlist/netlist.dart +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -7,5 +7,6 @@ // 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_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_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index ceacb6ff8..190bada6b 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -975,10 +975,8 @@ class NetlistSynthesizer extends Synthesizer { /// Apply all post-processing passes to the modules map. /// - /// This is the canonical pass ordering used by both netlist flows: - /// **Flow 1** (slim batch via `_synthesizeSlimModules`) and - /// **Flow 2** (incremental full via `moduleNetlistJson`). - /// Also used internally by [buildModulesMap] / [synthesizeToJson]. + /// 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); @@ -1128,7 +1126,6 @@ class NetlistSynthesizer extends Synthesizer { /// The [packageRoot] parameter is accepted for API compatibility with /// downstream trace-enabled branches. [slimMode] overrides the configured /// output mode for this call, allowing expansion after a slim request. - @visibleForTesting String synthesizeToJson(Module top, {String? packageRoot, bool? slimMode}) { final sb = SynthBuilder(top, this); return generateCombinedJson(sb, top, slimMode: slimMode); diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart index 6d60ad4f8..d7429cefc 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -17,15 +17,19 @@ export '../utilities/synth_module_stop_policy.dart'; /// The netlist synthesizer serves two main consumer flows, both configured /// through this configuration: /// -/// **Flow 1 — Slim JSON** (`NetlistService.slimJson`): +/// **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, incremental** (`NetlistService.moduleJson`): -/// Returns the complete netlist (with cell connections) for a single -/// module definition on demand. Results are cached; the first call -/// may trigger a lazy `SynthBuilder` run on the requested subtree. +/// **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 diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index f2c8a6b87..45f0f86b8 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -10,6 +10,8 @@ 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 { @@ -59,14 +61,15 @@ class NetlistValidation { final driversByBit = _driversByBit(ports, cells); for (final entry in driversByBit.entries) { - if (entry.value.length <= 1) { + if (!_hasConflictingDrivers(entry.value)) { continue; } + final drivers = entry.value.map((driver) => driver.description).toList(); issues.add(NetlistValidationIssue( 'wire bit ${entry.key} has multiple drivers: ' - '${entry.value.join(', ')}', + '${drivers.join(', ')}', wireBit: entry.key, - drivers: entry.value, + drivers: drivers, )); } @@ -82,17 +85,20 @@ class NetlistValidation { continue; } final bits = (netname['bits'] as List?)?.whereType() ?? const []; - final aggregateDrivers = { - for (final bit in bits) ...driversByBit[bit] ?? const [], + final aggregateDrivers = <_NetlistDriver>{ + for (final bit in bits) + ...driversByBit[bit] ?? const <_NetlistDriver>[], }; - if (aggregateDrivers.length <= 1) { + if (!_hasConflictingDrivers(aggregateDrivers)) { continue; } + final drivers = + aggregateDrivers.map((driver) => driver.description).toList(); issues.add(NetlistValidationIssue( 'aggregate net "${entry.key}" is reached from multiple drivers: ' - '${aggregateDrivers.join(', ')}', + '${drivers.join(', ')}', netname: entry.key, - drivers: aggregateDrivers.toList(), + drivers: drivers, )); } } @@ -103,14 +109,15 @@ class NetlistValidation { } /// Collects the port and cell output drivers for each integer bit ID. - static Map> _driversByBit( + static Map> _driversByBit( Map> ports, Map> cells, ) { - final drivers = >{}; + final drivers = >{}; - void addDriver(int bit, String driver) => - (drivers[bit] ??= []).add(driver); + 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?; @@ -137,13 +144,18 @@ class NetlistValidation { } for (final port in connections.entries) { final direction = directions[port.key] as String?; - final isTriStateOutput = type == r'$tribuf' && direction == 'inout'; + 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)'); + addDriver( + bit, + 'cell ${entry.key}.${port.key} ($type)', + isTriState: isTriStateOutput, + ); } } } @@ -151,6 +163,16 @@ class NetlistValidation { 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. 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..76798e522 --- /dev/null +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -0,0 +1,216 @@ +// 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 ModuleNotBuiltException(module); + } + + 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_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/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index 40c56abc8..63c522e7b 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -92,8 +92,10 @@ class Vector { } return arrAssigns.toString(); } else { - final signalVal = - LogicValue.of(inputValues[signalName], width: signal.width); + final signalVal = LogicValue.of( + inputValues[signalName], + width: signal.width, + ); return '$signalName = $signalVal;'; } }).join('\n'); @@ -114,8 +116,10 @@ class Vector { var index = 0; for (final leaf in outputPort.leafElements) { final subVal = expectedValue.getRange(index, index + leaf.width); - checksList.add(_errorCheckString( - leaf.structureName, subVal, subVal, inputStimulus)); + checksList.add( + _errorCheckString( + leaf.structureName, subVal, subVal, inputStimulus), + ); index += leaf.width; } } else { @@ -169,51 +173,56 @@ abstract class SimCompare { } if (enableChecking) { - unawaited(Simulator.postTick.first.then((value) { - for (final signalName in vector.expectedOutputValues.keys) { - final value = vector.expectedOutputValues[signalName]; - final o = - module.tryOutput(signalName) ?? module.inOut(signalName); - - final errorReason = - 'For vector #${vectors.indexOf(vector)} $vector,' - ' expected $o to be $value, but it was ${o.value}.'; - if (value is int) { - expect(o.value.isValid, isTrue, reason: errorReason); - expect(o.value.toBigInt(), - equals(BigInt.from(value).toUnsigned(o.width)), - reason: errorReason); - } else if (value is BigInt) { - expect(o.value.isValid, isTrue, reason: errorReason); - expect(o.value.toBigInt(), equals(value), reason: errorReason); - } else if (value is LogicValue) { - if (o.width > 1 && - (value == LogicValue.x || value == LogicValue.z)) { - for (final oBit in o.value.toList()) { - expect(oBit, equals(value), reason: errorReason); + unawaited( + Simulator.postTick.first.then((value) { + for (final signalName in vector.expectedOutputValues.keys) { + final value = vector.expectedOutputValues[signalName]; + final o = + module.tryOutput(signalName) ?? module.inOut(signalName); + + final errorReason = + 'For vector #${vectors.indexOf(vector)} $vector,' + ' expected $o to be $value, but it was ${o.value}.'; + if (value is int) { + expect(o.value.isValid, isTrue, reason: errorReason); + expect(o.value.toBigInt(), + equals(BigInt.from(value).toUnsigned(o.width)), + reason: errorReason); + } else if (value is BigInt) { + expect(o.value.isValid, isTrue, reason: errorReason); + expect(o.value.toBigInt(), equals(value), + reason: errorReason); + } else if (value is LogicValue) { + if (o.width > 1 && + (value == LogicValue.x || value == LogicValue.z)) { + for (final oBit in o.value.toList()) { + expect(oBit, equals(value), reason: errorReason); + } + } else { + expect(o.value, equals(value), reason: errorReason); } + } else if (value is String) { + expect(o.value, LogicValue.of(value, width: o.width), + reason: errorReason); } else { - expect(o.value, equals(value), reason: errorReason); + throw NonSupportedTypeException(value); } - } else if (value is String) { - expect(o.value, LogicValue.of(value, width: o.width), - reason: errorReason); - } else { - throw NonSupportedTypeException(value); } - } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { - Simulator.throwException(err as Exception, stackTrace); - }, - )); + }).catchError( + test: (error) => error is Exception, + (Object err, StackTrace stackTrace) { + Simulator.throwException(err as Exception, stackTrace); + }, + ), + ); } }); timestamp += Vector._period; } - Simulator.registerAction(timestamp + Vector._period, - () {}); // just so it does one more thing at the end + Simulator.registerAction( + timestamp + Vector._period, + () {}, + ); // just so it does one more thing at the end Simulator.setMaxSimTime(timestamp + 2 * Vector._period); await Simulator.run(); } @@ -348,9 +357,9 @@ abstract class SimCompare { allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); final moduleInstance = '$topModule dut($moduleConnections);'; final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = module.generateSynth( - configuration: synthesizerConfiguration, - ); + final generatedVerilog = + SystemVerilogService(module, configuration: synthesizerConfiguration) + .output; // so that when they run in parallel, they dont step on each other final uniqueId = @@ -404,13 +413,10 @@ abstract class SimCompare { print(maskedOutput); } - return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), - caseSensitive: false)); + return output.toString().contains( + RegExp(['error', 'unable', if (!allowWarnings) 'warning'].join('|'), + caseSensitive: false), + ); } if (printIfContentsAndCheckError(compileResult.stdout)) { @@ -432,14 +438,22 @@ abstract class SimCompare { if (!dontDeleteTmpFiles) { try { - File(tmpOutput).deleteSync(); - File(tmpTestFile).deleteSync(); + final outFile = File(tmpOutput); + if (outFile.existsSync()) { + outFile.deleteSync(); + } + final testFile = File(tmpTestFile); + if (testFile.existsSync()) { + testFile.deleteSync(); + } if (dumpWaves) { - File(tmpVcdFile).deleteSync(); + final vcdFile = File(tmpVcdFile); + if (vcdFile.existsSync()) { + vcdFile.deleteSync(); + } } } on Exception catch (e) { print("Couldn't delete: $e"); - return false; } } return true; diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 3a37e55ea..7fbd15dea 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.outputFilePath; /// 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..938c96f30 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -0,0 +1,115 @@ +// 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 { + /// The root node, which represents [OccurrenceAddress.root]. + 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]. + /// + /// This is equivalent to [set], without returning the previous value. + void operator []=(OccurrenceAddress address, T value) { + set(address, 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(); + } + + /// Returns [address]'s valid, non-negative path. + static List _validatedPath(OccurrenceAddress address) { + if (address.path.any((index) => index < 0)) { + throw ArgumentError.value( + address, + 'address', + 'An occurrence address must contain non-negative indices.', + ); + } + return address.path; + } +} + +/// A node in an [OccurrenceTrie]. +class _OccurrenceTrieNode { + /// Descendants indexed by their address path component. + final Map> children = {}; + + /// The value stored at this node, if one has been assigned. + T? value; + + /// Whether this node has neither a value nor descendants. + bool get isEmpty => value == null && children.isEmpty; +} 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..3faa8a92a --- /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); + trie[OccurrenceAddress.root] = 'root'; + expect(trie.set(second, 'second'), isNull); + + expect(trie[OccurrenceAddress.root], 'root'); + 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('accepts root addresses and rejects negative path indices', () { + final trie = OccurrenceTrie(); + + trie[OccurrenceAddress.root] = 'root'; + expect(trie[OccurrenceAddress.root], 'root'); + 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..7869e7df0 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 <4.0.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..fd2abee35 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 + +- **`captureBoundaryToPng`** — Captures a `RepaintBoundary` as PNG and saves or downloads it. + +- **`showExportToast`** — Shows export feedback and status messages. + +### Cross-Probing + +- **`CrossProbeService`** — Service for managing cross-probe state between multiple viewers/debuggers. Handles bidirectional signal selection synchronization. + +- **`buildGotoSourceMenuItems`** — Builds source-navigation menu items for ROHD DevTools surfaces. + +### Signal & Bit Field Utilities + +- **`expandLogicType`**, **`formatFieldValue`**, and **`formatTypeTooltip`** — Format ROHD logic types and values for display. + +- **`BitFieldDef`**, **`showBitRangeDialog`**, and **`showDefineBitFieldsDialog`** — Define and edit bit-field ranges. + +- **`buildBitExpansionMenuItems`** and **`resolveBitExpansionMenuValue`** — Build and resolve the "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). + +- **`RohdSourceFormat`** and **`RohdFormatInfo`** — Describe source formats and their availability. + ## 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..5d224154a --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -0,0 +1,271 @@ +// 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 { + /// Prevents instantiation. + SignalValueFormatRegistry._(); + + /// Stores the registered preference for each signal occurrence. + static OccurrenceTrie _formatTrie = + OccurrenceTrie(); + + /// Tracks registry changes. + static final ValueNotifier _changes = ValueNotifier(0); + + /// Notifies listeners whenever occurrence-format preferences change. + static ValueListenable get changes => _changes; + + /// Replaces all occurrence-format preferences with [preferences]. + static void update(Iterable preferences) { + final replacement = OccurrenceTrie(); + for (final preference in preferences) { + _validateSignalAddress(preference.address); + replacement[preference.address] = preference.format; + } + _formatTrie = replacement; + _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, + ) { + final requestedAddresses = addresses.toList(growable: false); + for (final address in requestedAddresses) { + _validateSignalAddress(address); + } + + var changed = false; + for (final address in requestedAddresses) { + 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; + } + _validateSignalAddress(address); + final format = _formatTrie[address]; + if (format != null) { + return format; + } + } + return fallback; + } + + /// Increments the registry change generation after a successful mutation. + static void _notifyListeners() => _changes.value++; + + /// Ensures [address] identifies a signal rather than an occurrence. + static void _validateSignalAddress(OccurrenceAddress address) { + if (address.path.isEmpty) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must not be empty.', + ); + } + } + + /// Returns whether [value] contains unknown (`x` or `z`) digits. + 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 => () { + final byteCount = (logicValue.width + 7) ~/ 8; + return String.fromCharCodes( + List.generate( + byteCount, + (index) { + final shift = (byteCount - index - 1) * 8; + final code = + ((logicValue.toBigInt() >> shift) & BigInt.from(0xff)) + .toInt(); + return code >= 0x20 && code <= 0x7e ? code : 0x2e; + }, + ), + ); + }(), + SignalValueFormat.waveform => canonical, + }; + } + + /// Parses [value] as a waveform literal and returns it with its canonical + /// waveform representation. + 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; + } + } + + /// Returns [value] in its canonical waveform representation when possible. + 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..187f7a223 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -0,0 +1,224 @@ +// 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', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x${List.filled(33, '41').join()}', + SignalValueFormat.ascii, + 264, + ), + List.filled(33, 'A').join(), + ); + }); + + 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('preserves registry state when a preference batch is invalid', () { + const address = OccurrenceAddress([0, 2, 4]); + SignalValueFormatRegistry.setFormatFor( + [address], + SignalValueFormat.unsignedDecimal, + ); + final changesBefore = SignalValueFormatRegistry.changes.value; + + expect( + () => SignalValueFormatRegistry.update([ + SignalValueFormatPreference(address, SignalValueFormat.signedDecimal), + SignalValueFormatPreference( + OccurrenceAddress.root, + SignalValueFormat.hexadecimal, + ), + ]), + throwsArgumentError, + ); + expect(SignalValueFormatRegistry.formatFor(address), + SignalValueFormat.unsignedDecimal); + expect(SignalValueFormatRegistry.changes.value, changesBefore); + }); + + test('preserves registry state when an address batch is invalid', () { + const address = OccurrenceAddress([0, 2, 4]); + SignalValueFormatRegistry.setFormatFor( + [address], + SignalValueFormat.unsignedDecimal, + ); + final changesBefore = SignalValueFormatRegistry.changes.value; + + expect( + () => SignalValueFormatRegistry.setFormatFor( + [address, OccurrenceAddress.root], + SignalValueFormat.signedDecimal, + ), + throwsArgumentError, + ); + expect(SignalValueFormatRegistry.formatFor(address), + SignalValueFormat.unsignedDecimal); + expect(SignalValueFormatRegistry.changes.value, changesBefore); + }); + + test('converts between serialized names and format enum values', () { + expect( + SignalValueFormatRegistry.formatFromString('signedDecimal'), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatToString( + SignalValueFormat.signedDecimal, + ), + 'signedDecimal', + ); + expect(SignalValueFormatRegistry.formatFromString('unknown'), isNull); + }); +} diff --git a/rohd_devtools_extension/tool/test_devtools_install.dart b/rohd_devtools_extension/tool/test_devtools_install.dart index 01e6a1c04..f04097f14 100644 --- a/rohd_devtools_extension/tool/test_devtools_install.dart +++ b/rohd_devtools_extension/tool/test_devtools_install.dart @@ -61,7 +61,7 @@ Future main(List args) async { _requireFile(extensionAssetsPath, 'flutter.js'); _requireFile(extensionAssetsPath, 'main.dart.js'); _requireFile(extensionAssetsPath, 'version.json'); - _requireFile(extensionAssetsPath, p.join('assets', 'AssetManifest.json')); + _requireFile(extensionAssetsPath, p.join('assets', 'AssetManifest.bin')); _requireFile(extensionAssetsPath, p.join('assets', 'FontManifest.json')); _requireFile(extensionAssetsPath, p.join('canvaskit', 'canvaskit.js')); _requireFile(extensionAssetsPath, p.join('canvaskit', 'canvaskit.wasm')); diff --git a/rohd_extension/README.md b/rohd_extension/README.md index f93af8f63..5c54461c5 100644 --- a/rohd_extension/README.md +++ b/rohd_extension/README.md @@ -83,7 +83,7 @@ completions below narrow the ROHD-specific options by cursor location. | Prefix | Expands to | Description | |--------|-----------|-------------| | `mod`, `Module` | `class … extends Module { … }` | Module scaffold with `clk`, `reset`, `a`/`b` inputs, `depth`, `latchData`, `addInput`/`addOutput`, `definitionName`, and instance naming parameters | -| `sim`, `Simulator` | Clock, reset, `WaveDumper`, `Simulator.run()` | Simulation / testbench boilerplate | +| `sim`, `Simulator` | Clock, reset, `dumpWaves`, `Simulator.run()` | Simulation / testbench boilerplate | | `fsmModule`, `FSMModule` | enum + `class extends Module` + `FiniteStateMachine` | Full standalone FSM module scaffold | | `vf`, `tb`, `testbench` | `rohd_vf` testbench | Agent / Driver / Monitor / Sequencer template | diff --git a/rohd_extension/dart/lib/dtd_service.dart b/rohd_extension/dart/lib/dtd_service.dart index 917017986..5024d4429 100644 --- a/rohd_extension/dart/lib/dtd_service.dart +++ b/rohd_extension/dart/lib/dtd_service.dart @@ -44,7 +44,9 @@ class DtdService { /// /// Returns `true` if connection and registration succeeded. Future connect(String uri) async { - if (_disposed) return false; + if (_disposed) { + return false; + } try { _channel = WebSocketChannel.connect(Uri.parse(uri)); @@ -66,6 +68,7 @@ class DtdService { } on Exception catch (e) { _peer = null; _channel = null; + // Report the failed optional DTD connection to help diagnose setup. // ignore: avoid_print print('[DtdService] Failed to connect to DTD at $uri: $e'); return false; diff --git a/rohd_extension/snippets/rohd.json b/rohd_extension/snippets/rohd.json index 5c3c9ae73..463353e65 100644 --- a/rohd_extension/snippets/rohd.json +++ b/rohd_extension/snippets/rohd.json @@ -50,7 +50,7 @@ "body": [ "final clk = SimpleClockGenerator(10).clk;", "final reset = Logic(name: 'reset');", - "WaveDumper(module, outputPath: 'wavedumpername.vcd');", + "module.dumpWaves(outputPath: 'waves.vcd');", "Simulator.setMaxSimTime(100);", "unawaited(Simulator.run());", "", @@ -158,8 +158,8 @@ " // Build the DUT", " await tb.dut.build();", "", - " // Attach a waveform dumper to the DUT", - " WaveDumper(tb.dut);", + " // Enable waveform dumping for the DUT", + " tb.dut.dumpWaves();", "", " // Set a maximum simulation time so it doesn't run forever", " Simulator.setMaxSimTime(300);", diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index a287cba6f..0e23e216e 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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()); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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()); 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()); 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(); 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(); 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(); 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(); 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(); 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()); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); // 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(); // 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(); // 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(); // 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(); // 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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()); 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(); 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(); 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(); 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(); 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(); // 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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()); // --- structural expectations (only where confidently predictable) --- if (config.noSubset) { 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..4133ff71c 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(); expect(sv, contains('assign result = oneBit')); final vectors = [ @@ -401,7 +401,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv.contains("assign const_subset = 16'habcd;"), true); }); }); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 8128eea2d..ead337233 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -66,7 +66,7 @@ void main() { test('collapse pretty', () async { final mod = CollapseTestModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // make sure e=a&b&c is in there, to prove there was some inlining expect(sv, contains(RegExp('e.*=.*a.*&.*b.*&.*c'))); @@ -78,7 +78,7 @@ void main() { final mod = CombinationalLoopCollapseModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains(' | a')); expect(sv, contains(' == a')); }); diff --git a/test/config_test.dart b/test/config_test.dart index beca0b576..ad2bc306f 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,6 +46,21 @@ void main() { final mod = SimpleModule(Logic(), Logic()); await mod.build(); + final sv = mod.dumpSystemVerilog(); + + 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)); diff --git a/test/const_radix_test.dart b/test/const_radix_test.dart index 4e9beb4eb..0c7aa1374 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(); 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(); expect(systemVerilog, contains("assign in = 8'd42;")); expect(systemVerilog, contains('.in(in)')); diff --git a/test/counter_test.dart b/test/counter_test.dart index 8f59b58d7..2fa6a9f5d 100644 --- a/test/counter_test.dart +++ b/test/counter_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // counter_test.dart @@ -48,7 +48,7 @@ void main() { final reset = Logic(); final counter = Counter(Logic(), reset); await counter.build(); - // WaveDumper(counter); + // counter.dumpWaves(); unawaited(reset.nextPosedge .then((value) => expect(counter.val.value.toInt(), equals(0)))); diff --git a/test/counter_wintf_test.dart b/test/counter_wintf_test.dart index 7889369cc..680e33120 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(); expect(!sv.contains('en_0'), true); }); diff --git a/test/external_test.dart b/test/external_test.dart index 09ac84c87..29b882a5e 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(); // make sure we instantiate the external module properly expect( diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..ea80c79ba 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(); 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(); 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(); expect(sv, contains('MyStates_state1 : begin')); }); diff --git a/test/gate_test.dart b/test/gate_test.dart index b2c47c21e..da5307b4e 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(), 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(); expect(sv, isNot(contains("0'h0"))); diff --git a/test/inout_loopback_test.dart b/test/inout_loopback_test.dart index 0c3b5b343..3cbf45cb1 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(); // 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(); 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(); // 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(); // 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(); // 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..ad8245017 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().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(); 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(); 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(); 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(); 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(); 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(); 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(), 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(); // 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()); // 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..42028e132 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(); expect(sv, contains('intermediate')); }); @@ -45,7 +45,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); // 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(); // 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(); // 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(); // held one sticks expect(sv, contains('intermediate_1 = in1')); @@ -108,7 +108,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + dut.dumpSystemVerilog(); 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(); 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(); 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(); expect(sv, contains('goodname')); }); @@ -220,7 +220,7 @@ void main() { out1 <= ~prev; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); 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(); expect(sv, contains('intermediate')); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 3447847c5..e0a2c2d12 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(), 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(); 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(); 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(); 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(); // 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(); // 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(); expect( sv, @@ -293,7 +293,7 @@ void main() { () async { final mod = NameCollisionArrayTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, @@ -310,7 +310,7 @@ void main() { await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); 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(); 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 695942bbe..6ae500016 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(); expect(sv.contains('swizzle'), isFalse, reason: 'Should not pack from instrumentation!'); 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/math_test.dart b/test/math_test.dart index d9ada00a0..aabb7f490 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(); 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(); 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..81ed42df0 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(); 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(); 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..c16070c10 --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,293 @@ +// 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('legacy generateSynth does not register a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + // ignore: deprecated_member_use_from_same_package - compatibility coverage + expect(mod.generateSynth(), isNotEmpty); + expect(SystemVerilogService.current, isNull); + expect( + ModuleServices.instance.lookup(), + isNull, + ); + }); + + test('legacy generateSynth preserves ModuleNotBuiltException', () { + final mod = SimpleModule(Logic()); + + // ignore: deprecated_member_use_from_same_package - compatibility coverage + expect(mod.generateSynth, throwsA(isA())); + }); + + 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), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/module_test.dart b/test/module_test.dart index c42532445..07963890c 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -231,6 +231,19 @@ class MissingInputRegistrationTopModule extends Module { } void main() { + group('output convenience methods', () { + test('dumpSystemVerilog generates in-memory output', () async { + final mod = FlexibleModule(); + await mod.build(); + + final output = mod.dumpSystemVerilog(); + + expect(output, isA()); + expect(output, isNotEmpty); + expect(SystemVerilogService.current, isNull); + }); + }); + group('try ports', () { test('tryInput, exists', () { final mod = ModuleWithMaybePorts(addIn: true); @@ -304,7 +317,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,(a ? 1'h0 : 1'h1)}")); @@ -321,7 +334,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,a}")); @@ -336,7 +349,8 @@ void main() { TopStructInoutWrap(LogicNet(), LogicNet(), LogicNet(width: 2)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect( sv, @@ -352,7 +366,7 @@ void main() { expect( mod.internalSignals.firstWhereOrNull((e) => e.name == 't0'), isNotNull); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign a_concat[0] = t0;')); }); @@ -363,7 +377,7 @@ void main() { expect(mod.internalSignals.firstWhereOrNull((e) => e.name == 'unconnected'), isNotNull); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign a_arr[1] = unconnected;')); }); diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 52470dc8c..5db4ec513 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(); // "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..a8813276f 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(); expect(sv, contains('Passthrough')); }); diff --git a/test/name_test.dart b/test/name_test.dart index d169b913b..37321806e 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(); 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(); 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, throwsException); }); }); @@ -241,7 +241,7 @@ void main() { causeInstConflict: false, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); 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..c16358c82 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(); // 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..db51fea16 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(); // 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(); // 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(); // 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(); // 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()); + final sv2 = stripHeader(dut.dumpSystemVerilog()); 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(); // 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..797cf432f 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(); 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(); 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..e09c1c829 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()); 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(); expect( sv, @@ -455,7 +456,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, contains( @@ -517,7 +518,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, @@ -590,7 +591,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); if (netTypeName == LogicNet) { expect( sv, @@ -620,7 +621,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); 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()); 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()); expect( sv, @@ -781,8 +782,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -799,8 +800,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -817,8 +818,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -835,8 +836,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); 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()); expect( sv, @@ -942,7 +943,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); checkSV(sv); final vectors = [ @@ -962,7 +963,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); checkSV(sv); final vectors = [ @@ -1205,7 +1206,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect( sv, @@ -1226,7 +1227,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect( sv, diff --git a/test/net_test.dart b/test/net_test.dart index 0cab0fe03..ebe8e7fcb 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(); 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(); expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -515,7 +515,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -526,7 +526,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(' submod'.allMatches(sv).length, 2); }); }); @@ -611,7 +611,7 @@ void main() { isNotNull); } - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // 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(); // 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(); expect(sv, contains('assign c = _a_and_b;')); expect(sv, contains('assign d = _aIntermediate_or_bIntermediate;')); diff --git a/test/netlist_example_test.dart b/test/netlist_example_test.dart index 08b56b7cc..21ea92edb 100644 --- a/test/netlist_example_test.dart +++ b/test/netlist_example_test.dart @@ -52,7 +52,6 @@ void main() { final counter = Counter(en, reset, clk); await counter.build(); - counter.generateSynth(); final modules = await convertTestWriteNetlist( counter, diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 59cbdda3b..34f43b125 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2643,6 +2643,71 @@ void main() { 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 ───────────────────────────── diff --git a/test/pair_interface_hier_test.dart b/test/pair_interface_hier_test.dart index c6bb7ad96..41f688707 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(); 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..3c02eadd6 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(); 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..dbc0e364f 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(); expect(sv, contains('input logic simple_clk')); }); diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 8ee28bb70..2db4b7bd8 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(); expect( sv, diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index a6274a36b..9c3c7e876 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(); expect( sv, diff --git a/test/replication_test.dart b/test/replication_test.dart index a6a566353..1060a3cc9 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(); 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(); expect(sv, isNot(contains('{1{'))); }); diff --git a/test/sequential_test.dart b/test/sequential_test.dart index cabeda5ec..df90f0efa 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(); expect(sv, contains('always_ff @(negedge')); final vectors = [ diff --git a/test/struct_port_pruning_test.dart b/test/struct_port_pruning_test.dart index b13346ebe..030218440 100644 --- a/test/struct_port_pruning_test.dart +++ b/test/struct_port_pruning_test.dart @@ -75,7 +75,7 @@ void main() { final dut = StructPipeTop(Logic(), Logic()); await dut.build(); - final svStr = dut.generateSynth(); + final svStr = dut.dumpSystemVerilog(); // The struct_producer submodule should appear in the SV. expect( @@ -111,7 +111,7 @@ void main() { final dut = StructProducer(Logic(), Logic()); await dut.build(); - final svStr = dut.generateSynth(); + final svStr = dut.dumpSystemVerilog(); // Inside StructProducer, the struct elements (a, b from PairStruct) // drive the output via struct_slice decomposition. They must not @@ -128,7 +128,7 @@ void main() { final dut = StructConsumer(Logic(width: 2)); await dut.build(); - final svStr = dut.generateSynth(); + final svStr = dut.dumpSystemVerilog(); // Inside StructConsumer, the struct elements are extracted from the // packed input. The XOR of elements drives the output. diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index adb4f51fb..f12eec5bf 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(); 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(); 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(); 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(); // 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(); // 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(); 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(); 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(); // 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()); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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..2ddf81825 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(); expect(sv.contains('#'), isFalse); }); } diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index d6ccf9d4f..8a8fe50d2 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(); 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(); // 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(); // 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(); // 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(); // 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(); // 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(); // 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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(); expect(sv, contains(''' assign b = { diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index dc10da6e1..24ec99b77 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -233,7 +233,7 @@ void main() { final mod = _Counter(Logic(), Logic()); await mod.build(); - mod.generateSynth(); + mod.dumpSystemVerilog(); expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); @@ -251,7 +251,7 @@ void main() { final modSv = _Counter(Logic(), Logic()); await modSv.build(); - modSv.generateSynth(); + modSv.dumpSystemVerilog(); // Both paths use the same Namer, so names must match. final enNetlist = modNetlist.namer.signalNameOfBest([ @@ -271,7 +271,7 @@ void main() { 'colliding mergeable names remain stable across synthesis order', () async { void runNetlist(_CollidingNames mod) => mod.generateNetlist(); - void runSv(_CollidingNames mod) => mod.generateSynth(); + void runSv(_CollidingNames mod) => mod.dumpSystemVerilog(); final netlistOnly = await _collisionNamesAfter([runNetlist]); await Simulator.reset(); @@ -316,7 +316,7 @@ void main() { test('colliding names stay stable when SV inlines one signal', () async { void runNetlist(_PartiallyInlineCollidingNames mod) => mod.generateNetlist(); - void runSv(_PartiallyInlineCollidingNames mod) => mod.generateSynth(); + void runSv(_PartiallyInlineCollidingNames mod) => mod.dumpSystemVerilog(); final netlistOnly = await _partialInlineCollisionNamesAfter([runNetlist]); await Simulator.reset(); @@ -347,7 +347,8 @@ void main() { () async { void runNetlist(_CollapsedInstanceCollidingNames mod) => mod.generateNetlist(); - void runSv(_CollapsedInstanceCollidingNames mod) => mod.generateSynth(); + void runSv(_CollapsedInstanceCollidingNames mod) => + mod.dumpSystemVerilog(); final netlistOnly = await _collapsedInstanceCollisionNamesAfter([ runNetlist, 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..2f05069f2 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(); expect(sv, isNot(contains('internal_struct'))); @@ -249,7 +249,7 @@ void main() { expect(mod.anyOut, isA()); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); 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(); // 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(); 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(); // no slicing on single-bit signals expect(sv, contains('assign outStruct = outStruct_oneBit')); diff --git a/test/wave_dumper_test.dart b/test/waveform_service_test.dart similarity index 66% rename from test/wave_dumper_test.dart rename to test/waveform_service_test.dart index 07aafc8c8..b6f0134da 100644 --- a/test/wave_dumper_test.dart +++ b/test/waveform_service_test.dart @@ -1,8 +1,8 @@ -// Copyright (C) 2021-2024 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// wave_dumper_test.dart -// Tests for the WaveDumper +// waveform_service_test.dart +// Tests for the WaveformService // // 2021 November 4 // Author: Max Korbel @@ -40,10 +40,26 @@ const tempDumpDir = 'tmp_test'; /// Gets the path of the VCD file based on a name. String temporaryDumpPath(String name) => '$tempDumpDir/temp_dump_$name.vcd'; -/// Attaches a [WaveDumper] to [module] to VCD with [name]. +/// Attaches a [WaveformService] to [module] to VCD with [name]. void createTemporaryDump(Module module, String name) { + Directory(tempDumpDir).createSync(recursive: true); + WaveformService( + module, + outputDirectory: tempDumpDir, + outputBaseName: 'temp_dump_$name', + writeToFile: true, + ); +} + +// The helper intentionally exercises the deprecated WaveDumper compatibility +// path. +// ignore: deprecated_member_use_from_same_package +/// Attaches the deprecated [WaveDumper] to [module] to VCD with [name]. +void createTemporaryWaveDumperDump(Module module, String name) { Directory(tempDumpDir).createSync(recursive: true); final tmpDumpFile = temporaryDumpPath(name); + // The deprecated WaveDumper constructor is invoked to test its behavior. + // ignore: deprecated_member_use_from_same_package WaveDumper(module, outputPath: tmpDumpFile); } @@ -86,6 +102,98 @@ void main() { deleteTemporaryDump(dumpName); }); + test('attach deprecated wave dumper after put', () async { + final a = Logic(name: 'a'); + final mod = SimpleModule(a); + await mod.build(); + + const dumpName = 'deprecatedDumpAfterPut'; + + a.put(1); + createTemporaryWaveDumperDump(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('dumpWaves returns a waveform service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + const dumpName = 'moduleDumpWaveforms'; + final outputPath = temporaryDumpPath(dumpName); + Directory(tempDumpDir).createSync(recursive: true); + final service = mod.dumpWaves(outputPath: outputPath); + + expect(service, isA()); + expect(service.module, same(mod)); + expect(service.outputPath, outputPath); + expect(service.outputFilePath, outputPath); + expect(File(service.outputPath).existsSync(), isTrue); + + await Simulator.run(); + deleteTemporaryDump(dumpName); + }); + + test('dumpWaves preserves an arbitrary legacy output filename', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + const outputPath = '$tempDumpDir/capture.trace'; + final service = mod.dumpWaves(outputPath: outputPath); + + expect(service.outputPath, equals(outputPath)); + expect(File(outputPath).existsSync(), isTrue); + + await Simulator.run(); + File(outputPath).deleteSync(); + }); + + test('waveform artifact derives its extension from the format', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final waveformService = WaveformService( + mod, + outputDirectory: tempDumpDir, + outputBaseName: 'capture', + ); + + final artifact = waveformService.artifacts.single; + + expect(artifact.fileName, equals('capture.vcd')); + expect(artifact.mediaType, equals('text/x-vcd')); + expect(File(waveformService.outputFilePath).existsSync(), isFalse); + expect( + (await artifact.openRead().expand((bytes) => bytes).toList()).isNotEmpty, + isTrue, + ); + }); + + test('rejects formats without a matching waveform writer', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + expect( + () => WaveformService(mod, format: WaveOutputFormat.fst), + throwsUnsupportedError, + ); + }); + test('attach dumper before put', () async { final a = Logic(name: 'a'); final mod = SimpleModule(a); @@ -241,11 +349,16 @@ void main() { const dir1Path = '$tempDumpDir/dir1'; - final waveDumper = WaveDumper(mod, outputPath: '$dir1Path/dir2/waves.vcd'); + final waveformService = WaveformService( + mod, + outputDirectory: '$dir1Path/dir2', + outputBaseName: 'waves', + writeToFile: true, + ); - expect(File(waveDumper.outputPath).existsSync(), equals(true)); + expect(File(waveformService.outputFilePath).existsSync(), equals(true)); - if (File(waveDumper.outputPath).existsSync()) { + if (File(waveformService.outputFilePath).existsSync()) { File(dir1Path).deleteSync(recursive: true); } }); @@ -263,7 +376,7 @@ void main() { Simulator.registerAction(13, () => reset.put(1)); reset.put(0); - // add wave dumper *after* the put to reset + // add waveform service *after* the put to reset createTemporaryDump(mod, dumpName); // check functional matches diff --git a/tool/generate_gate_catalog.dart b/tool/generate_gate_catalog.dart index 0923b459b..705a6d37f 100644 --- a/tool/generate_gate_catalog.dart +++ b/tool/generate_gate_catalog.dart @@ -53,8 +53,6 @@ Future main(List arguments) async { await catalog.build(); final synth = NetlistSynthesizer(); - // We will migrate to a new public API in a future PR - // ignore: invalid_use_of_visible_for_testing_member final json = synth.synthesizeToJson(catalog); final outputFile = File(outputPath); 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