diff --git a/README.md b/README.md index a996ccccb..969d8046a 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ You can also open this repository in a GitHub Codespace to run the example in yo - Easy **IP integration** and **interfaces**; using an IP is as easy as an import. Reduces tedious, redundant, and error prone aspects of integration - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer -- Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform capture service** to .vcd file format +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption via **SvService** - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/benchmark/many_submodules_benchmark.dart b/benchmark/many_submodules_benchmark.dart index 763261a4c..277170143 100644 --- a/benchmark/many_submodules_benchmark.dart +++ b/benchmark/many_submodules_benchmark.dart @@ -33,7 +33,7 @@ class ManySubmodulesBenchmark extends AsyncBenchmarkBase { Future run() async { final dut = ManySubmodulesModule(Logic(), numSubModules: 10000); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; } } diff --git a/benchmark/wave_dump_benchmark.dart b/benchmark/wave_dump_benchmark.dart index 777b42eb0..1adfbdbc1 100644 --- a/benchmark/wave_dump_benchmark.dart +++ b/benchmark/wave_dump_benchmark.dart @@ -56,7 +56,7 @@ class WaveDumpBenchmark extends AsyncBenchmarkBase { _mod = _ModuleToDump(Logic(), _clk); await _mod.build(); - WaveDumper(_mod, outputPath: _vcdTemporaryPath); + WaveformService(_mod, outputPath: _vcdTemporaryPath); await Simulator.run(); diff --git a/doc/tutorials/chapter_2/helper.dart b/doc/tutorials/chapter_2/helper.dart index ecd0b5d98..af1dc6eaf 100644 --- a/doc/tutorials/chapter_2/helper.dart +++ b/doc/tutorials/chapter_2/helper.dart @@ -13,7 +13,8 @@ import 'package:rohd/rohd.dart'; Future displaySystemVerilog(Module mod) async { await mod.build(); - print('\nYour System Verilog Equivalent Code: \n ${mod.generateSynth()}'); + print('\nYour System Verilog Equivalent Code: \n' + '${SvService(mod).synthOutput}'); } class LogicInitialization extends Module { diff --git a/doc/tutorials/chapter_3/answers/exercise_sv.dart b/doc/tutorials/chapter_3/answers/exercise_sv.dart index b4ead1f2c..fce71a6fe 100644 --- a/doc/tutorials/chapter_3/answers/exercise_sv.dart +++ b/doc/tutorials/chapter_3/answers/exercise_sv.dart @@ -33,7 +33,7 @@ void main() async { await fSub.build(); // ignore: avoid_print - print(fSub.generateSynth()); + print(SvService(fSub).synthOutput); test('should return 0 when a and b equal 1', () async { a.put(1); diff --git a/doc/tutorials/chapter_3/full_adder.dart b/doc/tutorials/chapter_3/full_adder.dart index 5e798a69d..27f82896c 100644 --- a/doc/tutorials/chapter_3/full_adder.dart +++ b/doc/tutorials/chapter_3/full_adder.dart @@ -77,5 +77,5 @@ void main() async { final mod = FullAdderModule(a, b, cIn, faOps); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); } diff --git a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart index b324d4201..a3c7c8bf2 100644 --- a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart @@ -10,7 +10,7 @@ void main() async { final mod = NBitAdder(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 255 when both inputs are added', () { a.put(127); diff --git a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart index c4d3137db..a2716e6f8 100644 --- a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart @@ -9,7 +9,7 @@ void main() async { final mod = NBitSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 5 when a is 25 and b is 20', () { a.put(25); diff --git a/doc/tutorials/chapter_4/basic_generation_sv.dart b/doc/tutorials/chapter_4/basic_generation_sv.dart index 4c3ff86c0..8e23637b2 100644 --- a/doc/tutorials/chapter_4/basic_generation_sv.dart +++ b/doc/tutorials/chapter_4/basic_generation_sv.dart @@ -78,7 +78,7 @@ void main() async { await nbitAdder.build(); - print(nbitAdder.generateSynth()); + print(SvService(nbitAdder).synthOutput); test('should return 10 when both inputs are 5.', () async { a.put(5); diff --git a/doc/tutorials/chapter_5/answers/full_adder.dart b/doc/tutorials/chapter_5/answers/full_adder.dart index b7b54d5ed..f994a9387 100644 --- a/doc/tutorials/chapter_5/answers/full_adder.dart +++ b/doc/tutorials/chapter_5/answers/full_adder.dart @@ -49,7 +49,7 @@ void main() async { final mod = FullAdder(a: a, b: b, carryIn: cIn); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return true if result sum similar to truth table.', () async { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/full_subtractor.dart b/doc/tutorials/chapter_5/answers/full_subtractor.dart index 162b2d72c..bf4e8acf0 100644 --- a/doc/tutorials/chapter_5/answers/full_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/full_subtractor.dart @@ -44,7 +44,7 @@ Future main() async { await diff.build(); - print(diff.generateSynth()); + print(SvService(diff).synthOutput); test('should return true if results matched truth table', () async { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart index 2f12ab6fe..224999e5e 100644 --- a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart @@ -36,7 +36,7 @@ Future main() async { final mod = NBitFullSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(SvService(mod).synthOutput); test('should return 1 when a is 8 and b is 7.', () { a.put(8); diff --git a/doc/tutorials/chapter_5/n_bit_adder.dart b/doc/tutorials/chapter_5/n_bit_adder.dart index 8a4a61944..9eb0c1a63 100644 --- a/doc/tutorials/chapter_5/n_bit_adder.dart +++ b/doc/tutorials/chapter_5/n_bit_adder.dart @@ -79,7 +79,7 @@ void main() async { await nbitAdder.build(); - // print(nbitAdder.generateSynth()); + // print(SvService(nbitAdder).synthOutput); test('should return 20 when A and B perform add.', () async { a.put(15); diff --git a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart index 15395d7ec..bd63338bf 100644 --- a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart +++ b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart @@ -44,7 +44,7 @@ Future main() async { final dff = DFlipFlop(data, reset, clk); await dff.build(); - print(dff.generateSynth()); + print(SvService(dff).synthOutput); data.inject(1); reset.inject(1); @@ -60,7 +60,7 @@ Future main() async { unawaited(Simulator.run()); - WaveDumper(dff, + WaveformService(dff, outputPath: 'doc/tutorials/chapter_7/answers/d_flip_flop.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_7/shift_register.dart b/doc/tutorials/chapter_7/shift_register.dart index 1dc98c4f1..2324a860b 100644 --- a/doc/tutorials/chapter_7/shift_register.dart +++ b/doc/tutorials/chapter_7/shift_register.dart @@ -69,7 +69,7 @@ void main() async { // kick-off the simulator, but we don't want to wait unawaited(Simulator.run()); - WaveDumper(shiftReg, + WaveformService(shiftReg, outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart index d8315156d..2cbc89916 100644 --- a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart +++ b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart @@ -139,7 +139,7 @@ void main() async { await tb.build(); - print(tb.generateSynth()); + print(SvService(tb).synthOutput); testInterface.cs.inject(0); testInterface.sdi.inject(0); @@ -163,7 +163,7 @@ void main() async { Simulator.setMaxSimTime(100); unawaited(Simulator.run()); - WaveDumper(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); + WaveformService(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); await drive(LogicValue.ofString('01010101')); } diff --git a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart index 6912f1313..26d18cd16 100644 --- a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart +++ b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart @@ -49,13 +49,13 @@ Future main(List args) async { final toyCap = ToyCapsuleFSM(clk, reset, dispenseBtn, coin); await toyCap.build(); - print(toyCap.generateSynth()); + print(SvService(toyCap).synthOutput); toyCap.toyCapsuleStateMachine.generateDiagram(); reset.inject(1); - WaveDumper(toyCap, outputPath: 'toyCapsuleFSM.vcd'); + WaveformService(toyCap, outputPath: 'toyCapsuleFSM.vcd'); Simulator.setMaxSimTime(100); Simulator.registerAction(25, () { diff --git a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart index ef93701b3..2a0350698 100644 --- a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart +++ b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart @@ -34,14 +34,14 @@ void main(List args) async { final pipe = Pipeline4Stages(clk, reset, a); await pipe.build(); - // print(pipe.generateSynth()); + // print(SvService(pipe).synthOutput); a.inject(5); reset.inject(1); Simulator.registerAction(10, () => reset.put(0)); - WaveDumper(pipe, outputPath: 'answer_1.vcd'); + WaveformService(pipe, outputPath: 'answer_1.vcd'); Simulator.registerAction(50, () async { // stage 4 / result: 30 + (30 * 3) = 120 diff --git a/doc/tutorials/chapter_8/carry_save_multiplier.dart b/doc/tutorials/chapter_8/carry_save_multiplier.dart index 82b9521da..783e54f6f 100644 --- a/doc/tutorials/chapter_8/carry_save_multiplier.dart +++ b/doc/tutorials/chapter_8/carry_save_multiplier.dart @@ -108,7 +108,7 @@ void main() async { reset.inject(1); // Attach a waveform dumper so we can see what happens. - WaveDumper(csm, outputPath: 'csm.vcd'); + WaveformService(csm, outputPath: 'csm.vcd'); Simulator.registerAction(10, () { reset.inject(0); diff --git a/doc/tutorials/chapter_8/counter_interface.dart b/doc/tutorials/chapter_8/counter_interface.dart index 41a49eb0e..6d5261620 100644 --- a/doc/tutorials/chapter_8/counter_interface.dart +++ b/doc/tutorials/chapter_8/counter_interface.dart @@ -63,9 +63,9 @@ Future main() async { await counter.build(); - print(counter.generateSynth()); + print(SvService(counter).synthOutput); - WaveDumper(counter, + WaveformService(counter, outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); Simulator.registerAction(25, () { intf.en.put(1); diff --git a/doc/tutorials/chapter_8/oven_fsm.dart b/doc/tutorials/chapter_8/oven_fsm.dart index 172d83006..01f653198 100644 --- a/doc/tutorials/chapter_8/oven_fsm.dart +++ b/doc/tutorials/chapter_8/oven_fsm.dart @@ -192,7 +192,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); + WaveformService(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); } if (!noPrint) { diff --git a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart index 4b1ef9c34..77d2a5a32 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart +++ b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart @@ -315,7 +315,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + WaveformService(tb.counter); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml b/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml index e763ab748..27d99489a 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml +++ b/doc/tutorials/chapter_9/rohd_vf_example/pubspec.yaml @@ -8,10 +8,14 @@ environment: # Add regular dependencies here. dependencies: - rohd: ^0.4.2 - rohd_vf: ^0.4.1 + rohd: ^0.6.0 + rohd_vf: ^0.6.0 logging: ^1.0.1 +dependency_overrides: + rohd: + path: ../../../../ + dev_dependencies: lints: ^2.0.0 test: ^1.21.0 diff --git a/example/example.dart b/example/example.dart index 2ddbfc738..d2c2ee81d 100644 --- a/example/example.dart +++ b/example/example.dart @@ -61,7 +61,7 @@ Future main({bool noPrint = false}) async { // Let's see what this module looks like as SystemVerilog, so we can pass it // to other tools. - final systemVerilogCode = counter.generateSynth(); + final systemVerilogCode = SvService(counter).synthOutput; if (!noPrint) { print(systemVerilogCode); } @@ -70,7 +70,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(counter); + WaveformService(counter); } // Let's also print a message every time the value on the counter changes, diff --git a/example/fir_filter.dart b/example/fir_filter.dart index 1f17f0d3d..d16168552 100644 --- a/example/fir_filter.dart +++ b/example/fir_filter.dart @@ -96,7 +96,7 @@ Future main({bool noPrint = false}) async { await firFilter.build(); // Generate SystemVerilog code. - final systemVerilogCode = firFilter.generateSynth(); + final systemVerilogCode = SvService(firFilter).synthOutput; if (!noPrint) { // Print SystemVerilog code to console. print(systemVerilogCode); @@ -108,7 +108,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper. if (!noPrint) { - WaveDumper(firFilter); + WaveformService(firFilter); } // Let's set the initial setting. diff --git a/example/logic_array.dart b/example/logic_array.dart index f772c4929..0b149717e 100644 --- a/example/logic_array.dart +++ b/example/logic_array.dart @@ -58,14 +58,14 @@ Future main({bool noPrint = false}) async { // Build the module await logicArrayExample.build(); - final systemVerilogCode = logicArrayExample.generateSynth(); + final systemVerilogCode = SvService(logicArrayExample).synthOutput; if (!noPrint) { print(systemVerilogCode); } // Simulate the module if (!noPrint) { - WaveDumper(logicArrayExample); + WaveformService(logicArrayExample); } // Set the input values diff --git a/example/oven_fsm.dart b/example/oven_fsm.dart index 2788baa55..50c30197e 100644 --- a/example/oven_fsm.dart +++ b/example/oven_fsm.dart @@ -225,7 +225,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'oven.vcd'); + WaveformService(oven, outputPath: 'oven.vcd'); } // Kick off the simulation. diff --git a/example/tree.dart b/example/tree.dart index f5c30a979..12eb86a63 100644 --- a/example/tree.dart +++ b/example/tree.dart @@ -85,7 +85,7 @@ Future main({bool noPrint = false}) async { // Below will generate an output of the ROHD-generated SystemVerilog: await tree.build(); - final generatedSystemVerilog = tree.generateSynth(); + final generatedSystemVerilog = SvService(tree).synthOutput; if (!noPrint) { print(generatedSystemVerilog); } diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..b4fc541b7 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,9 +1,14 @@ // Copyright (C) 2021-2023 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/module_service.dart'; +export 'src/diagnostics/module_services.dart'; +export 'src/diagnostics/waveform_service.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; +export 'src/fst/fst_types.dart'; +export 'src/fst/fst_writer.dart'; export 'src/interfaces/interfaces.dart'; export 'src/module.dart'; export 'src/modules/modules.dart'; @@ -12,6 +17,7 @@ export 'src/signals/signals.dart'; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; +export 'src/synthesizers/systemverilog/sv_service.dart'; export 'src/utilities/naming.dart'; export 'src/values/values.dart'; export 'src/wave_dumper.dart'; diff --git a/lib/src/diagnostics/module_service.dart b/lib/src/diagnostics/module_service.dart new file mode 100644 index 000000000..ed20fe42c --- /dev/null +++ b/lib/src/diagnostics/module_service.dart @@ -0,0 +1,82 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_service.dart +// Common base types shared by all module-scoped services. +// +// 2026 June 23 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +import 'package:rohd/src/diagnostics/output_file_writer.dart' + if (dart.library.io) 'package:rohd/src/diagnostics/output_file_writer_io.dart'; + +/// The common contract implemented by every module-scoped service that +/// registers with [ModuleServices]. +/// +/// A service wraps some derived view of a built [Module] (synthesis output, +/// netlist, source trace, waveform, etc.) and exposes a JSON-serialisable +/// summary via [toJson]. Concrete services additionally expose their own +/// format-specific accessors; consumers reach them through +/// [ModuleServices.lookup] or the service's own `current` accessor rather than +/// through getters on the registry. +abstract interface class ModuleService { + /// The top-level [Module] this service operates on. + Module get module; + + /// A JSON-serialisable summary of this service. + Map toJson(); +} + +/// A [ModuleService] that emits output to one or more files. +/// +/// Establishes the common output convention shared by synthesis, netlist, +/// trace, and waveform services: +/// - [outputPath] — the default file or directory written by [write]. +/// - [multiFile] — whether [write] emits one file per module definition +/// (a directory) or a single combined file. +/// - [write] — performs the write, honouring [multiFile]. +abstract class OutputService implements ModuleService { + /// The default location written by [write]. + /// + /// Interpreted as a directory when [multiFile] is `true`, otherwise as a + /// single file path. May be `null` when no default has been configured, in + /// which case a path must be passed to [write]. + String? get outputPath; + + /// Whether [write] emits one file per module definition (`true`) or a single + /// combined file (`false`). + bool get multiFile; + + /// Writes this service's output to [path], or to [outputPath] when [path] is + /// omitted. + void write([String? path]); + + /// Writes [contents] to [path] on platforms that support file IO. + /// + /// Browser integrations can still construct services for in-memory output; + /// calling write APIs there throws [UnsupportedError]. + void writeTextFile(String path, String contents) => + writeOutputTextFile(path, contents); +} + +/// An [OutputService] that generates source-code text, keyed per module +/// definition. +/// +/// Shared by the language code-generation services (e.g. SystemVerilog and +/// SystemC), which all produce a combined single-file [output] as well as +/// per-definition contents. +abstract class CodegenService extends OutputService { + /// The combined single-file generated output (including any header). + String get output; + + /// The generated output keyed by module definition name + /// ([Module.definitionName]). + Map get contentsByDefinitionName; + + /// The generated output for a single module [definitionName], or `null` when + /// that definition was not generated. + String? moduleOutput(String definitionName) => + contentsByDefinitionName[definitionName]; +} diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..98f2341f7 --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,73 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services.dart +// Slim, type-keyed registry of module-scoped services for DevTools and other +// inspection tools. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; + +/// A slim, type-keyed registry of [ModuleService]s. +/// +/// Services register themselves here on construction (keyed by their concrete +/// type) and are retrieved with [lookup]. The registry intentionally exposes +/// no per-format accessors: each service owns its own JSON and output methods, +/// reached through [lookup] or the service's own static `current` accessor. +/// +/// The registry references no specific service type, so it is identical across +/// all feature branches that contribute services. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJSON] — set by [Module.build]. +class ModuleServices { + ModuleServices._(); + + /// The singleton instance. + static final ModuleServices instance = ModuleServices._(); + + // ─── Hierarchy (auto-registered by Module.build) ────────────── + + /// The most recently built top-level [Module]. + /// + /// Set automatically at the end of [Module.build]. + Module? rootModule; + + /// Returns the module hierarchy as a JSON string. + /// + /// DevTools evaluates this via `EvalOnDartLibrary` to display the module + /// hierarchy. Richer design views (e.g. a slim netlist) are composed by the + /// DevTools client from the relevant registered service. + String get hierarchyJSON { + ModuleTree.rootModuleInstance = rootModule; + return ModuleTree.instance.hierarchyJSON; + } + + // ─── Type-keyed service registry ────────────────────────────── + + final Map _services = {}; + + /// Registers [service] under the type argument [T]. + /// + /// Replaces any previously registered service of the same type. + void register(T service) { + _services[T] = service; + } + + /// Returns the registered service of type [T], or `null` if none. + T? lookup() => _services[T] as T?; + + /// Removes the registered service of type [T], if any. + void unregister() { + _services.remove(T); + } + + /// Resets all services. Intended for test teardown. + void reset() { + rootModule = null; + _services.clear(); + } +} diff --git a/lib/src/diagnostics/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..33eb83d6e --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,428 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_service.dart +// Base waveform service: file output with filtering, timescale, and +// flush/overwrite control. Designed to be subclassed by the DevTools +// streaming variant. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:collection'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +// ─── Supporting types ──────────────────────────────────────────────────────── + +/// The output format for waveform capture. +enum WaveOutputFormat { + /// Value Change Dump — the classic text-based waveform format. + vcd, + + /// Fast Signal Trace — a compact binary format. + /// + /// Requires an FST writer to be available; see the DevTools subclass for + /// a fully FST-backed implementation. + fst, +} + +/// Policy applied when the output file already exists at construction time. +enum OverwritePolicy { + /// Silently overwrite any existing file. + overwrite, + + /// Throw a [FileSystemException] if the file already exists. + failIfExists, +} + +// ─── Service ───────────────────────────────────────────────────────────────── + +/// A waveform capture service that writes signal changes to a file. +/// +/// This is the base class for waveform capture. It handles: +/// - Signal collection (with optional [signalFilter]) +/// - VCD file output with configurable [timescale] +/// - Selective recording via [startTime] / [stopTime] +/// - Periodic buffer flushing and [overwritePolicy] +/// - Optional registration with [ModuleServices] +/// +/// **Subclassing for DevTools streaming:** +/// +/// Override the protected hooks below to intercept the simulation event loop +/// without re-implementing the file-writing logic: +/// +/// - [onSignalCollected] — called once per tracked signal at startup; use +/// it to register signals in a VM-service index. +/// - [onValueChange] — called for every value-change event within the +/// [startTime]/[stopTime] window; use it to feed an in-memory store for +/// streaming. +/// - [onTimestampCapture] — called once per simulation timestamp that +/// contains at least one change; the full changed-signal set is passed. +/// - [onSimulationEnd] — called after the final timestamp is written and +/// the file is closed; use it to finalise any streaming buffers. +/// +/// Example subclass skeleton: +/// ```dart +/// class DevToolsWaveformService extends WaveformService { +/// DevToolsWaveformService(super.module, {super.outputPath}); +/// +/// @override +/// void onSignalCollected(Logic signal) { +/// super.onSignalCollected(signal); +/// _registerWithVmService(signal); +/// } +/// +/// @override +/// void onValueChange(Logic signal, int timestamp) { +/// super.onValueChange(signal, timestamp); +/// _recordInMemory(signal, timestamp); +/// } +/// } +/// ``` +class WaveformService implements ModuleService { + /// The most recently registered [WaveformService], or `null`. + static WaveformService? current; + + /// The top-level [Module] being captured. + @override + final Module module; + + /// Path of the output waveform file. + /// + /// The parent directory is created if necessary. + final String outputPath; + + /// Output format. + final WaveOutputFormat format; + + /// Optional predicate that determines whether a given [Logic] signal is + /// captured. + /// + /// When `null`, all non-[Const] signals in the hierarchy are captured, + /// matching the legacy waveform dumper behaviour. + final bool Function(Logic signal)? signalFilter; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Simulation time at which recording begins. + /// + /// Signals are still collected before this time so they appear in the scope + /// definition, but value-change events are suppressed until [startTime] is + /// reached. `null` means "from the very start". + final int? startTime; + + /// Simulation time at which recording ends. + /// + /// Value-change events after this time are suppressed. `null` means "until + /// end of simulation". + final int? stopTime; + + /// Number of characters accumulated in the write buffer before it is flushed + /// to disk. + final int flushBufferSize; + + /// What to do when the output file already exists. + final OverwritePolicy overwritePolicy; + + /// Whether to register this service with [ModuleServices] for inspection. + final bool register; + + /// Whether to enable DevTools streaming. + /// + /// The base [WaveformService] stores this flag but takes no action on it. + /// The DevTools subclass uses it to conditionally register extensions. + final bool enableDevToolsStreaming; + + // ─── Internal file-writing state ───────────────────────────── + + /// The output file. + late final File _outputFile; + + /// Sink writing into [_outputFile]. + late final IOSink _outFileSink; + + /// Write buffer; flushed when it exceeds [flushBufferSize]. + final StringBuffer _fileBuffer = StringBuffer(); + + /// Counter for assigning compact signal markers in the VCD. + int _signalMarkerIdx = 0; + + /// Maps each captured [Logic] to its VCD marker string. + final Map _signalToMarkerMap = {}; + + /// Signals that changed during the current simulation timestamp. + final Set _changedThisTimestamp = HashSet(); + + /// The timestamp currently being accumulated. + int _currentDumpingTimestamp = Simulator.time; + + // ─── Constructor ───────────────────────────────────────────── + + /// Creates a [WaveformService] for [module]. + /// + /// [module] must be built before construction. + /// + /// Use the optional constructor parameters to configure format, path, + /// filtering, timescale, start/stop times, flush size, and overwrite policy. + WaveformService( + this.module, { + this.outputPath = 'waves.vcd', + this.format = WaveOutputFormat.vcd, + this.signalFilter, + this.timescale = '1ps', + this.startTime, + this.stopTime, + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + this.register = true, + this.enableDevToolsStreaming = false, + }) { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating WaveformService. ' + 'Call build() first.'); + } + + if (overwritePolicy == OverwritePolicy.failIfExists) { + final f = File(outputPath); + if (f.existsSync()) { + throw FileSystemException( + 'Waveform output file already exists and overwritePolicy is ' + 'failIfExists.', + outputPath); + } + } + + _outputFile = File(outputPath)..createSync(recursive: true); + _outFileSink = _outputFile.openWrite(); + + _collectSignals(); + _writeHeader(); + _writeScope(); + + Simulator.preTick.listen((_) { + if (Simulator.time != _currentDumpingTimestamp) { + if (_changedThisTimestamp.isNotEmpty) { + _captureTimestamp(_currentDumpingTimestamp); + } + _currentDumpingTimestamp = Simulator.time; + } + }); + + Simulator.registerEndOfSimulationAction(() async { + _captureTimestamp(Simulator.time); + await _terminate(); + onSimulationEnd(); + }); + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + // ─── Extensibility hooks ────────────────────────────────────── + + /// Called once for each [Logic] signal that passes + /// [signalFilter] during initial signal collection. + /// + /// Override in a subclass to register signals with an in-memory store, + /// VM service index, or FST handle map. Always call `super` first. + @protected + void onSignalCollected(Logic signal) {} + + /// Called for every value-change event on [signal] at [timestamp]. + /// + /// Only called within the [startTime] / [stopTime] window. + /// + /// Override in a subclass to feed an in-memory waveform store or + /// streaming buffer. Always call `super` first. + @protected + void onValueChange(Logic signal, int timestamp) {} + + /// Called once per simulation timestamp that contains at least one change, + /// after all value-change events for that timestamp have been processed. + /// + /// [changed] is the set of signals that changed at [timestamp]. + /// + /// Override in a subclass to flush incremental streaming payloads. + /// Always call `super` first. + @protected + void onTimestampCapture(int timestamp, Set changed) {} + + /// Called after the final timestamp has been written and the file is closed. + /// + /// Override in a subclass to finalise any streaming buffers or emit + /// end-of-simulation notifications. + @protected + void onSimulationEnd() {} + + // ─── Internal signal collection ────────────────────────────── + + void _collectSignals() { + final modulesToParse = [module]; + for (var i = 0; i < modulesToParse.length; i++) { + final m = modulesToParse[i]; + for (final sig in m.signals) { + if (sig is Const) { + continue; + } + if (signalFilter != null && !signalFilter!(sig)) { + continue; + } + + _signalToMarkerMap[sig] = 's${_signalMarkerIdx++}'; + onSignalCollected(sig); + + sig.changed.listen((_) { + _changedThisTimestamp.add(sig); + }); + } + + for (final subm in m.subModules) { + if (subm is InlineSystemVerilog) { + continue; + } + modulesToParse.add(subm); + } + } + } + + // ─── VCD output helpers ─────────────────────────────────────── + + void _writeHeader() { + final header = ''' +\$date + ${Timestamper.stamp()} +\$end +\$version + ROHD v${Config.version} +\$end +\$comment + Generated by ROHD - www.github.com/intel/rohd +\$end +\$timescale $timescale \$end +'''; + _writeToBuffer(header); + } + + void _writeScope() { + var scopeString = _computeScopeString(module); + scopeString += '\$enddefinitions \$end\n'; + scopeString += '\$dumpvars\n'; + _writeToBuffer(scopeString); + _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + _writeToBuffer('\$end\n'); + } + + String _computeScopeString(Module m, {int indent = 0}) { + final moduleSignalUniquifier = Uniquifier(); + final padding = List.filled(indent, ' ').join(); + var scopeString = '$padding\$scope module ${m.uniqueInstanceName} \$end\n'; + final innerScopeString = StringBuffer(); + + for (final sig in m.signals) { + if (!_signalToMarkerMap.containsKey(sig)) { + continue; + } + final width = sig.width; + final marker = _signalToMarkerMap[sig]; + var signalName = Sanitizer.sanitizeSV(sig.name); + signalName = moduleSignalUniquifier.getUniqueName( + initialName: signalName, reserved: sig.isPort); + innerScopeString + .write(' $padding\$var wire $width $marker $signalName \$end\n'); + } + for (final subModule in m.subModules) { + innerScopeString + .write(_computeScopeString(subModule, indent: indent + 1)); + } + if (innerScopeString.isEmpty) { + return ''; + } + + scopeString += innerScopeString.toString(); + scopeString += '$padding\$upscope \$end\n'; + return scopeString; + } + + bool _isInRecordingWindow(int timestamp) { + if (startTime != null && timestamp < startTime!) { + return false; + } + if (stopTime != null && timestamp > stopTime!) { + return false; + } + return true; + } + + void _captureTimestamp(int timestamp) { + if (!_isInRecordingWindow(timestamp)) { + _changedThisTimestamp.clear(); + return; + } + + _writeToBuffer('#$timestamp\n'); + + final snapshot = Set.of(_changedThisTimestamp); + for (final sig in snapshot) { + _writeSignalValueUpdate(sig); + onValueChange(sig, timestamp); + } + _changedThisTimestamp.clear(); + + onTimestampCapture(timestamp, snapshot); + } + + void _writeSignalValueUpdate(Logic signal) { + final binaryValue = signal.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); + final updateValue = signal.width > 1 + ? 'b$binaryValue ' + : signal.value.toString(includeWidth: false); + final marker = _signalToMarkerMap[signal]; + _writeToBuffer('$updateValue$marker\n'); + } + + // ─── Buffered I/O ───────────────────────────────────────────── + + void _writeToBuffer(String contents) { + _fileBuffer.write(contents); + if (_fileBuffer.length > flushBufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + _outFileSink.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } + + Future _terminate() async { + _flushBuffer(); + await _outFileSink.flush(); + await _outFileSink.close(); + } + + // ─── Inspection ─────────────────────────────────────────────── + + /// Returns a JSON-serialisable summary of this service. + @override + Map toJson() => { + 'outputPath': outputPath, + 'format': format.name, + 'signalCount': _signalToMarkerMap.length, + 'timescale': timescale, + if (startTime != null) 'startTime': startTime!, + if (stopTime != null) 'stopTime': stopTime!, + }; +} diff --git a/lib/src/fst/fst_types.dart b/lib/src/fst/fst_types.dart new file mode 100644 index 000000000..b39b9ef5c --- /dev/null +++ b/lib/src/fst/fst_types.dart @@ -0,0 +1,236 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_types.dart +// Enumerations and constants for the FST (Fast Signal Trace) binary format. +// +// 2026 February +// Author: Desmond Kirkpatrick + +/// FST block types (from fstapi.h). +enum FstBlockType { + /// File header. + header(0), + + /// Value change data (zlib compressed). + vcData(1), + + /// Blackout regions. + blackout(2), + + /// Geometry (per-variable back-pointers for random access). + geometry(3), + + /// Hierarchy (zlib compressed). + hierarchy(4), + + /// Value changes with dynamic aliases (zlib). + vcDataDynamicAlias(5), + + /// Hierarchy (LZ4 compressed). + hierarchyLz4(6), + + /// Hierarchy (LZ4 double compressed). + hierarchyLz4Duo(7), + + /// Value changes with dynamic aliases v2 (modern recommended format). + vcDataDynamicAlias2(8), + + /// GZip wrapper. + gzipWrapper(254), + + /// Skip/padding. + skip(255); + + const FstBlockType(this.value); + + /// The numeric value of this block type as written in FST files. + final int value; +} + +/// FST scope types. +enum FstScopeType { + /// A Verilog/SystemVerilog module instantiation scope. + module(0), + + /// A Verilog/SystemVerilog task scope. + task(1), + + /// A Verilog/SystemVerilog function scope. + function_(2), + + /// A named `begin`..`end` block scope (Verilog). + begin(3), + + /// A named `fork`..`join` block scope (Verilog). + fork(4), + + /// A `generate` block scope (SystemVerilog). + generate(5), + + /// A `struct` type scope (SystemVerilog). + struct_(6), + + /// A `union` type scope (SystemVerilog). + union(7), + + /// A `class` scope (SystemVerilog). + class_(8), + + /// An `interface` scope (SystemVerilog). + interface(9), + + /// A `package` scope (SystemVerilog). + package(10), + + /// A `program` scope (SystemVerilog). + program(11); + + const FstScopeType(this.value); + + /// The numeric value of this scope type as written in FST files. + final int value; +} + +/// FST variable types. +enum FstVarType { + /// An event variable. + event(0), + + /// A Verilog `integer` variable (32-bit, 4-state). + integer(1), + + /// A Verilog `parameter` or `localparam`. + parameter(2), + + /// A `real` variable (double-precision floating point). + real(3), + + /// A `real` parameter. + realParameter(4), + + /// A `reg` variable (Verilog 4-state storage). + reg(5), + + /// A `supply0` net (logic-0 power supply). + supply0(6), + + /// A `supply1` net (logic-1 power supply). + supply1(7), + + /// A `time` variable. + time(8), + + /// A `tri` net (tri-state, same resolution as `wire`). + tri(9), + + /// A `triand` net (tri-state with wired-AND resolution). + triAnd(10), + + /// A `trior` net (tri-state with wired-OR resolution). + triOr(11), + + /// A `trireg` net (retains last driven value when undriven). + triReg(12), + + /// A `tri0` net (pulls to 0 when undriven). + tri0(13), + + /// A `tri1` net (pulls to 1 when undriven). + tri1(14), + + /// A `wand` net (wired-AND). + wand(15), + + /// A `wire` net (standard Verilog interconnect). + wire(16), + + /// A `wor` net (wired-OR). + wor(17), + + /// A port variable. + port(18), + + /// A sparse array variable. + sparseArray(19), + + /// A `realtime` variable. + realTime(20), + + /// A generic string variable. + genericString(21), + + // SystemVerilog types + + /// A SystemVerilog `bit` type (2-state, unsigned). + bit(22), + + /// A SystemVerilog `logic` type (4-state). + logic(23), + + /// A SystemVerilog `int` type (32-bit, 2-state, signed). + int_(24), + + /// A SystemVerilog `shortint` type (16-bit, 2-state, signed). + shortInt(25), + + /// A SystemVerilog `longint` type (64-bit, 2-state, signed). + longInt(26), + + /// A SystemVerilog `byte` type (8-bit, 2-state, signed). + byte_(27), + + /// A SystemVerilog `enum` type. + enum_(28), + + /// A SystemVerilog `shortreal` type (single-precision float). + shortReal(29); + + const FstVarType(this.value); + + /// The numeric value of this variable type as written in FST files. + final int value; +} + +/// FST variable direction. +enum FstVarDirection { + /// No direction specified (implicit net). + implicit(0), + + /// Input port. + input(1), + + /// Output port. + output(2), + + /// Bidirectional (inout) port. + inout(3), + + /// Buffer port (output that can be read back). + buffer(4), + + /// Linkage port (VHDL linkage mode). + linkage(5); + + const FstVarDirection(this.value); + + /// The numeric value of this direction as written in FST files. + final int value; +} + +/// FST file type. +enum FstFileType { + /// Verilog source. + verilog(0), + + /// VHDL source. + vhdl(1), + + /// Mixed Verilog and VHDL source. + verilogVhdl(2); + + const FstFileType(this.value); + + /// The numeric value of this file type as written in FST files. + final int value; +} diff --git a/lib/src/fst/fst_writer.dart b/lib/src/fst/fst_writer.dart new file mode 100644 index 000000000..11849a5d7 --- /dev/null +++ b/lib/src/fst/fst_writer.dart @@ -0,0 +1,1043 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_writer.dart +// Pure Dart implementation of FST (Fast Signal Trace) binary writer. +// +// Writes valid FST files compatible with GTKWave, Surfer, wellen reader. +// Reference: fst-reader 0.14.2 (io.rs, types.rs) and fstapi.c from GTKWave. +// +// 2026 February +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; +import 'package:rohd/rohd.dart'; + +/// Configuration for the FST writer. +class FstWriterConfig { + /// Timescale exponent. The timescale is 10^exponent seconds. + /// Default: -12 (picoseconds). + final int timescaleExponent; + + /// Zlib compression level (0-9). Higher = smaller but slower. + /// Default: 4. + final int compressionLevel; + + /// Writer version string embedded in the file header. + final String version; + + /// File type: Verilog, VHDL, or combined. + final FstFileType fileType; + + /// Maximum number of value changes to buffer before auto-flushing + /// a VcData block to disk. Set to 0 (default) to disable auto-flush + /// and write a single block at [FstWriter.finish]. + /// + /// When non-zero, [FstWriter.emitValueChange] automatically calls + /// [FstWriter.flushBlock] once the buffer reaches this threshold. + /// This bounds memory usage and makes historical data available on + /// disk for read-back. + final int maxChangesPerBlock; + + /// Creates configuration for the FST writer. + const FstWriterConfig({ + this.timescaleExponent = -12, + this.compressionLevel = 4, + this.version = 'ROHD FST Writer', + this.fileType = FstFileType.verilog, + this.maxChangesPerBlock = 0, + }); +} + +/// A handle to a declared signal in the FST file. +/// +/// Handles are 1-based (matching VST convention). Index 0 is unused. +class FstSignalHandle { + /// The 1-based handle value. + final int handle; + + /// Creates a signal handle from a 1-based handle value. + const FstSignalHandle(this.handle); +} + +/// Metadata about a flushed VcData block in the FST file. +/// +/// Each entry in [FstWriter.blockIndex] represents a block that has been +/// written to disk and can be read back independently for on-demand +/// signal queries without loading the entire file into memory. +class FstBlockIndex { + /// File offset of the block_type byte in the FST file. + final int fileOffset; + + /// Section length (the section_length field from the block header). The full + /// block occupies bytes [fileOffset .. fileOffset + 1 + sectionLength). + final int sectionLength; + + /// First timestamp in this block. + final int startTime; + + /// Last timestamp in this block. + final int endTime; + + /// Creates a block index entry. + const FstBlockIndex({ + required this.fileOffset, + required this.sectionLength, + required this.startTime, + required this.endTime, + }); +} + +/// Public metadata about a declared signal in the FST writer. +class FstSignalInfo { + /// Signal name. + final String name; + + /// Bit width (number of bits for digital signals, 8 for real). + final int width; + + /// Whether this is a real-valued (f64) signal. + final bool isReal; + + /// Creates signal info. + const FstSignalInfo({ + required this.name, + required this.width, + required this.isReal, + }); +} + +/// Internal: information about a declared signal. +class _SignalDecl { + final String name; + final int width; + final FstVarType varType; + final FstVarDirection direction; + final bool isReal; + + _SignalDecl({ + required this.name, + required this.width, + required this.varType, + required this.direction, + this.isReal = false, + }); + + /// The geometry file_format value for this signal. + int get geometryValue { + if (isReal) { + return 0; + } + return width; // 1 for 1-bit, N for N-bit + } + + /// The number of bytes this signal occupies in the frame section. + int get frameLength { + if (isReal) { + return 8; + } + return width; // 1 byte per bit for character-encoded values + } +} + +/// Internal: a buffered value change. +class _ValueChange { + final int time; + final int handleIndex; // 0-based + final String value; + + _ValueChange(this.time, this.handleIndex, this.value); +} + +/// Internal: an entry in the hierarchy being built. +sealed class _HierarchyEntry {} + +class _ScopeEntry extends _HierarchyEntry { + final FstScopeType type; + final String name; + final String component; + _ScopeEntry(this.type, this.name, {this.component = ''}); +} + +class _UpScopeEntry extends _HierarchyEntry {} + +class _VarEntry extends _HierarchyEntry { + final FstVarType varType; + final FstVarDirection direction; + final String name; + final int width; + final int handle; // 1-based + _VarEntry(this.varType, this.direction, this.name, this.width, this.handle); +} + +/// Pure Dart writer for the FST (Fast Signal Trace) binary format. +/// +/// Usage: +/// ```dart +/// final writer = FstWriter('output.fst'); +/// writer.pushScope('top'); +/// final clk = writer.declareSignal('clk', 1); +/// final data = writer.declareSignal('data', 8); +/// writer.popScope(); +/// writer.writeHeader(); +/// +/// writer.emitValueChange(0, clk, '0'); +/// writer.emitValueChange(0, data, '00000000'); +/// writer.emitValueChange(5, clk, '1'); +/// writer.emitValueChange(10, clk, '0'); +/// +/// writer.finish(); +/// ``` +class FstWriter { + /// The output file path. + final String filePath; + + /// Writer configuration. + final FstWriterConfig config; + + /// All declared signals (0-indexed). + final List<_SignalDecl> _signals = []; + + /// Hierarchy entries in declaration order. + final List<_HierarchyEntry> _hierEntries = []; + + /// Scope counts for header. + int _scopeCount = 0; + + /// Variable counts for header (including aliases). + int _varCount = 0; + + /// Buffered value changes. + final List<_ValueChange> _changes = []; + + /// The start time of the simulation. + int _startTime = 0; + + /// The end time of the simulation. + int _endTime = 0; + + /// Whether the header has been written yet. + bool _headerWritten = false; + + /// The output file random access handle. + late final RandomAccessFile _file; + + /// Current value of each signal (tracks latest emitted value). + /// Initialized in [writeHeader]. + late List _currentValues; + + /// Base values for the next block's frame section. + /// Updated after each [flushBlock] call. + late List _nextFrameBase; + + /// Index of flushed VcData blocks for read-back. + final List _blockIndex = []; + + /// Number of VcData blocks written so far. + int _vcSectionCount = 0; + + /// Creates an FST writer that will write to [filePath]. + FstWriter(this.filePath, {this.config = const FstWriterConfig()}) { + final file = File(filePath)..createSync(recursive: true); + _file = file.openSync(mode: FileMode.write); + } + + /// Pushes a new scope onto the hierarchy. + void pushScope( + String name, { + FstScopeType type = FstScopeType.module, + String component = '', + }) { + _hierEntries.add(_ScopeEntry(type, name, component: component)); + _scopeCount++; + } + + /// Pops the current scope. + void popScope() { + _hierEntries.add(_UpScopeEntry()); + } + + /// Declares a signal and returns its handle. + /// + /// [name] is the signal name. [width] is the bit width (1 for single bit). + /// Returns an [FstSignalHandle] used for emitting value changes. + FstSignalHandle declareSignal( + String name, + int width, { + FstVarType varType = FstVarType.wire, + FstVarDirection direction = FstVarDirection.implicit, + }) { + final handle = _signals.length + 1; // 1-based + final decl = _SignalDecl( + name: name, + width: width, + varType: varType, + direction: direction, + isReal: varType == FstVarType.real || varType == FstVarType.realParameter, + ); + _signals.add(decl); + _hierEntries.add(_VarEntry(varType, direction, name, width, handle)); + _varCount++; + return FstSignalHandle(handle); + } + + /// Writes the FST file header. + /// + /// Must be called after all signals are declared and before any value + /// changes. The header is initially written with placeholder values for + /// start_time and end_time, which are fixed up during [finish]. + void writeHeader() { + if (_headerWritten) { + throw StateError('Header already written'); + } + _writeHeaderBlock(); + _headerWritten = true; + + // Initialize value tracking for incremental block flushing + final defaults = List.generate(_signals.length, (i) { + final sig = _signals[i]; + return sig.isReal ? '0.0' : 'x' * sig.width; + }); + _currentValues = List.from(defaults); + _nextFrameBase = List.from(defaults); + } + + /// Records a value change for a signal at a given simulation time. + /// + /// [time] is the simulation timestamp. + /// [handle] is the signal handle returned by [declareSignal]. + /// [value] is the new value as a string (e.g., '0', '1', '01010101', 'x'). + void emitValueChange(int time, FstSignalHandle handle, String value) { + if (!_headerWritten) { + throw StateError('Must call writeHeader() before emitting value changes'); + } + if (_endTime < time) { + _endTime = time; + } + _changes.add(_ValueChange(time, handle.handle - 1, value)); + _currentValues[handle.handle - 1] = value; + + // Auto-flush if threshold is reached + if (config.maxChangesPerBlock > 0 && + _changes.length >= config.maxChangesPerBlock) { + flushBlock(); + } + } + + /// Finalizes the FST file: flushes remaining value changes, writes + /// geometry and hierarchy blocks, fixes up the header, and closes the file. + void finish() { + if (!_headerWritten) { + writeHeader(); + } + + // Flush any remaining buffered changes as a final VcData block + flushBlock(); + + _writeGeometryBlock(); + _writeHierarchyBlock(); + _fixupHeader(); + + _file.closeSync(); + } + + /// Releases resources. Call [finish] first for a valid file. + void dispose() { + try { + _file.closeSync(); + } on FileSystemException { + // already closed + } + } + + /// Flushes buffered value changes to disk as a VcData block. + /// + /// After flushing, the changes are cleared from memory and the block + /// is recorded in [blockIndex] for later read-back. This enables + /// incremental writing where only recent unflushed changes remain + /// in memory while historical data lives on disk. + /// + /// Does nothing if no changes are buffered. + void flushBlock() { + if (_changes.isEmpty) { + return; + } + if (!_headerWritten) { + throw StateError('Must call writeHeader() before flushing blocks'); + } + + // Sort changes by time, then by handle + _changes.sort((a, b) { + final cmp = a.time.compareTo(b.time); + return cmp != 0 ? cmp : a.handleIndex.compareTo(b.handleIndex); + }); + + final blockStart = _changes.first.time; + final blockEnd = _changes.last.time; + + // Build frame: carry-over state from previous block, overridden by + // any changes at this block's start time. + final frameValues = List.from(_nextFrameBase); + for (final c in _changes) { + if (c.time == blockStart) { + frameValues[c.handleIndex] = c.value; + } + } + + final blockOffset = _file.positionSync(); + _writeVcDataBlock( + blockStartTime: blockStart, + blockEndTime: blockEnd, + frameValues: frameValues, + ); + final blockEndPos = _file.positionSync(); + + // Record block in the index for read-back + _blockIndex.add( + FstBlockIndex( + fileOffset: blockOffset, + sectionLength: blockEndPos - blockOffset - 1, + startTime: blockStart, + endTime: blockEnd, + ), + ); + _vcSectionCount++; + + // Update global time range + if (_vcSectionCount == 1) { + _startTime = blockStart; + } + _endTime = blockEnd; + + // Carry-over state for next block's frame + _nextFrameBase = List.from(_currentValues); + _changes.clear(); + } + + // ─── Public query API for hybrid disk+memory access ─── + + /// Index of all flushed VcData blocks. + /// + /// Each entry contains the file offset and time range, enabling + /// the `FstBlockReader` to read specific blocks on demand. + List get blockIndex => List.unmodifiable(_blockIndex); + + /// Number of declared signals. + int get signalCount => _signals.length; + + /// Public metadata about each declared signal (indexed by handle-1). + List get signalInfoList => _signals + .map((s) => FstSignalInfo(name: s.name, width: s.width, isReal: s.isReal)) + .toList(); + + /// The output file handle for read-back by `FstBlockReader`. + /// + /// **Warning**: The caller must not close or modify the file position + /// without restoring it. The writer uses this same handle for writing. + RandomAccessFile get file => _file; + + /// Query unflushed value changes for a specific signal handle. + /// + /// Returns changes from the hot buffer for signal [handleIndex] (0-based) + /// within the time range \[startTime, endTime\]. + List<({int time, String value})> queryHotBuffer( + int handleIndex, + int startTime, + int endTime, + ) => + _changes + .where( + (c) => + c.handleIndex == handleIndex && + c.time >= startTime && + c.time <= endTime, + ) + .map((c) => (time: c.time, value: c.value)) + .toList(); + + /// Returns the current (latest) value of signal [handleIndex] (0-based). + String getCurrentValue(int handleIndex) => _currentValues[handleIndex]; + + /// Returns the latest known values of all signals (read-only). + List get currentValues => List.unmodifiable(_currentValues); + + // ─────────────── Header Block ─────────────── + + static const int _headerLength = 329; + static const int _headerVersionMaxLen = 128; + static const int _headerDateMaxLen = 119; + + /// Writes the FST_BL_HDR block. + void _writeHeaderBlock() { + _file.writeByteSync(FstBlockType.header.value); + _writeU64(_headerLength); // section_length (fixed size) + _writeU64(_startTime); // start_time (placeholder) + _writeU64(_endTime); // end_time (placeholder) + _writeF64LE(math.e); // double endian test + _writeU64(0); // memory_used_by_writer + _writeU64(_scopeCount); // scope_count + _writeU64(_varCount); // var_count + _writeU64(_signals.length); // max_var_id_code + _writeU64(1); // vc_section_count (we write one block) + _file.writeByteSync(config.timescaleExponent & 0xFF); // timescale_exponent + _writeFixedString(config.version, _headerVersionMaxLen); + _writeFixedString(_dateString(), _headerDateMaxLen); + _file.writeByteSync(config.fileType.value); // file_type + _writeU64(0); // time_zero + } + + /// Fixes up the header with actual start/end times and block count. + void _fixupHeader() { + final savedPos = _file.positionSync(); + _file.setPositionSync(1 + 8); // skip block_type + section_length + _writeU64(_startTime); + _writeU64(_endTime); + // Fix vc_section_count with actual number of blocks written + // Layout: block_type(1) + section_length(8) + start_time(8) + + // end_time(8) + endian_test(8) + memory_used(8) + scope_count(8) + + // var_count(8) + max_var_id(8) = offset 65 + _file.setPositionSync( + 1 + 8 + 8 + 8 + 8 + 8 + 8 + 8 + 8, + ); // at vc_section_count + _writeU64(_vcSectionCount); + _file.setPositionSync(savedPos); + } + + // ─────────────── Hierarchy Block ─────────────── + + static const int _hierTypeScopeBegin = 254; + static const int _hierTypeUpScope = 255; + + /// Writes the FST_BL_HIER block (zlib/gzip compressed hierarchy). + void _writeHierarchyBlock() { + // Build uncompressed hierarchy bytes + final buf = BytesBuilder(copy: false); + var handleCount = 0; + + for (final entry in _hierEntries) { + switch (entry) { + case _ScopeEntry(): + buf.addByte(_hierTypeScopeBegin); + buf.addByte(entry.type.value); + buf.add(_cString(entry.name)); + buf.add(_cString(entry.component)); + case _UpScopeEntry(): + buf.addByte(_hierTypeUpScope); + case _VarEntry(): + buf.addByte(entry.varType.value); + buf.addByte(entry.direction.value); + buf.add(_cString(entry.name)); + buf.add(encodeVarint(entry.width)); // length + // alias = 0 means "new handle, not an alias" + buf.add(encodeVarint(0)); + handleCount++; + } + } + + final uncompressed = buf.toBytes(); + assert( + handleCount == _signals.length, + 'Handle count mismatch: $handleCount vs ${_signals.length}', + ); + + // Write as FST_BL_HIER (type 4) with gzip compression + _file.writeByteSync(FstBlockType.hierarchy.value); + final sectionLengthPos = _file.positionSync(); + _writeU64(0); // placeholder section_length + _writeU64(uncompressed.length); // uncompressed_length + + // Write gzip header + deflate-compressed data + _writeGzipCompressed(uncompressed); + + // Fix section_length + final endPos = _file.positionSync(); + final sectionLength = endPos - sectionLengthPos; + _file.setPositionSync(sectionLengthPos); + _writeU64(sectionLength); + _file.setPositionSync(endPos); + } + + // ─────────────── Geometry Block ─────────────── + + /// Writes the FST_BL_GEOM block. + void _writeGeometryBlock() { + // Build uncompressed geometry: one varint per signal + final buf = BytesBuilder(copy: false); + for (final sig in _signals) { + buf.add(encodeVarint(sig.geometryValue)); + } + final uncompressed = buf.toBytes(); + final compressed = _zlibCompress( + uncompressed, + config.compressionLevel, + allowRaw: true, + ); + + _file.writeByteSync(FstBlockType.geometry.value); + final sectionLength = 3 * 8 + compressed.length; + _writeU64(sectionLength); // section_length + _writeU64(uncompressed.length); // uncompressed_length + _writeU64(_signals.length); // max_handle + _file.writeFromSync(compressed); + } + + // ─────────────── VcData Block (DynamicAlias2) ─────────────── + + /// Writes a single FST_BL_VCDATA_DYN_ALIAS2 block from the current + /// `_changes` buffer. + /// + /// [blockStartTime] and [blockEndTime] are the time range for this block. + /// [frameValues] contains the initial value of each signal at the block's + /// start time (carry-over state plus changes at blockStartTime). + /// + /// Assumes `_changes` is already sorted by time, then by handle. + void _writeVcDataBlock({ + required int blockStartTime, + required int blockEndTime, + required List frameValues, + }) { + // Build sorted unique time table. + // Only include timestamps that have signal chain entries (i.e., after + // blockStartTime). Changes at blockStartTime go into the frame section. + // The fst-reader only reads the frame when time_table[0] > start_time; + // if blockStartTime were included, the frame would be skipped and all + // signals would appear as 'x'. + final timeSet = {}; + for (final c in _changes) { + if (c.time != blockStartTime) { + timeSet.add(c.time); + } + } + final timeTable = timeSet.toList()..sort(); + // Map timestamp → index + final timeToIndex = {}; + for (var i = 0; i < timeTable.length; i++) { + timeToIndex[timeTable[i]] = i; + } + + // Build per-signal value change chains + final signalData = _buildSignalData(timeToIndex, blockStartTime); + + // Pack each signal's data (store uncompressed with varint(0) prefix) + final packedSignals = []; + for (final data in signalData) { + if (data.isEmpty) { + packedSignals.add(Uint8List(0)); + } else { + final packed = BytesBuilder(copy: false) + ..add(encodeVarint(0)) // means "uncompressed" + ..add(data); + packedSignals.add(packed.toBytes()); + } + } + + // Build frame bytes + final frameBytes = _buildFrameBytes(frameValues); + final frameCompressed = _zlibCompress( + frameBytes, + config.compressionLevel, + allowRaw: true, + ); + + // Build the signal offset chain (DynamicAlias2 format) + final chainBytes = _buildOffsetChain(packedSignals); + + // Build time table bytes + final timeTableBytes = _buildTimeTableBytes(timeTable); + + // Compute memory required for traversal + var memRequired = 0; + for (final ps in packedSignals) { + memRequired += ps.length; + } + + // Now assemble the VcData block + _file.writeByteSync(FstBlockType.vcDataDynamicAlias2.value); + final sectionLengthPos = _file.positionSync(); + _writeU64(0); // placeholder section_length + _writeU64(blockStartTime); // start_time + _writeU64(blockEndTime); // end_time + _writeU64(memRequired); // mem_required_for_traversal + + // Frame section + _file + ..writeFromSync(encodeVarint(frameBytes.length)) // unc len + ..writeFromSync(encodeVarint(frameCompressed.length)) // comp len + ..writeFromSync(encodeVarint(_signals.length)) // max_handle + ..writeFromSync(frameCompressed) + // Value change section + ..writeFromSync(encodeVarint(_signals.length)) // max_handle + ..writeByteSync(0x5A); // pack_type = 'Z' (zlib) + + // Write per-signal packed data + packedSignals.forEach(_file.writeFromSync); + + // Write offset chain + _file.writeFromSync(chainBytes); + _writeU64(chainBytes.length); // chain_compressed_length + + // Write time table + _file.writeFromSync(timeTableBytes); + + // Fix section_length + final endPos = _file.positionSync(); + final sectionLength = endPos - sectionLengthPos; + _file.setPositionSync(sectionLengthPos); + _writeU64(sectionLength); + _file.setPositionSync(endPos); + } + + /// Builds frame bytes: the initial value of each signal concatenated. + Uint8List _buildFrameBytes(List initialValues) { + final buf = BytesBuilder(copy: false); + for (var i = 0; i < _signals.length; i++) { + final sig = _signals[i]; + if (sig.isReal) { + // Encode as f64 little-endian bytes + final d = double.tryParse(initialValues[i]) ?? 0.0; + final bd = ByteData(8)..setFloat64(0, d, Endian.little); + buf.add(bd.buffer.asUint8List()); + } else { + // Character-encoded value: one byte per bit + final val = initialValues[i]; + for (var j = 0; j < sig.width; j++) { + buf.addByte(j < val.length ? val.codeUnitAt(j) : 0x78); // 'x' + } + } + } + return buf.toBytes(); + } + + /// Builds per-signal value change encoded data. + /// + /// Returns a list of byte arrays, one per signal (0-indexed). + /// Each byte array contains the encoded value change chain for that signal. + /// Changes at [blockStartTime] are skipped (captured in the frame). + List _buildSignalData( + Map timeToIndex, + int blockStartTime, + ) { + // Group changes by signal handle index + final signalChanges = List>.generate( + _signals.length, + (_) => [], + ); + for (final c in _changes) { + // Skip changes at blockStartTime — those are captured in the frame + if (c.time == blockStartTime) { + continue; + } + signalChanges[c.handleIndex].add(c); + } + + final result = []; + for (var sigIdx = 0; sigIdx < _signals.length; sigIdx++) { + final changes = signalChanges[sigIdx]; + if (changes.isEmpty) { + result.add(Uint8List(0)); + continue; + } + + final sig = _signals[sigIdx]; + final buf = BytesBuilder(copy: false); + var prevTimeIndex = 0; + + for (final c in changes) { + final timeIndex = timeToIndex[c.time]!; + final timeDelta = timeIndex - prevTimeIndex; + prevTimeIndex = timeIndex; + + if (sig.frameLength == 1) { + // 1-bit signal: compact encoding + buf.add(_encodeOneBitChange(timeDelta, c.value)); + } else if (sig.isReal) { + // Real signal + buf.add(_encodeRealChange(timeDelta, c.value)); + } else { + // Multi-bit signal + buf.add(_encodeMultiBitChange(timeDelta, c.value, sig.width)); + } + } + result.add(buf.toBytes()); + } + return result; + } + + /// Encodes a 1-bit signal value change. + /// + /// Format: varint where: + /// - Normal (0/1): bit0=0, bit1=value, bits2+= time_index_delta + /// - Special (x/z/etc): bit0=1, bits1-3=rcv_index, bits4+=time_index_delta + Uint8List _encodeOneBitChange(int timeDelta, String value) { + // RCV_STR: [x, z, h, u, w, l, -, ?] + const rcvChars = 'xzhuwl-?'; + final ch = value.isNotEmpty ? value[value.length - 1] : 'x'; + + int vli; + if (ch == '0') { + vli = (timeDelta << 2) | (0 << 1) | 0; // bit0=0, bit1=0 + } else if (ch == '1') { + vli = (timeDelta << 2) | (1 << 1) | 0; // bit0=0, bit1=1 + } else { + final rcvIdx = rcvChars.indexOf(ch); + final idx = rcvIdx >= 0 ? rcvIdx : 0; // default to 'x' + vli = (timeDelta << 4) | (idx << 1) | 1; // bit0=1, bits1-3=idx + } + return encodeVarint(vli); + } + + /// Encodes a multi-bit signal value change. + /// + /// Format: varint(time_delta << 1 | encoding_bit) then value bytes. + /// encoding_bit=0: 2-state packed bits; encoding_bit=1: 4-state characters. + Uint8List _encodeMultiBitChange(int timeDelta, String value, int width) { + final buf = BytesBuilder(copy: false); + + // Check if value contains only 0/1 (2-state) + final is2State = value.runes.every((c) => c == 0x30 || c == 0x31); + + if (is2State) { + // 2-state: pack bits into bytes, MSB first + buf.add(encodeVarint((timeDelta << 1) | 0)); + final byteCount = (width + 7) ~/ 8; + final bytes = Uint8List(byteCount); + for (var i = 0; i < width; i++) { + if (i < value.length && value[i] == '1') { + final byteIdx = i ~/ 8; + final bitIdx = 7 - (i % 8); + bytes[byteIdx] |= 1 << bitIdx; + } + } + buf.add(bytes); + } else { + // 4-state: raw character bytes + buf.add(encodeVarint((timeDelta << 1) | 1)); + for (var i = 0; i < width; i++) { + buf.addByte(i < value.length ? value.codeUnitAt(i) : 0x78); + } + } + return buf.toBytes(); + } + + /// Encodes a real signal value change. + Uint8List _encodeRealChange(int timeDelta, String value) { + final buf = BytesBuilder(copy: false) + ..add(encodeVarint((timeDelta << 1) | 1)); + final d = double.tryParse(value) ?? 0.0; + final bd = ByteData(8)..setFloat64(0, d, Endian.little); + buf.add(bd.buffer.asUint8List()); + return buf.toBytes(); + } + + /// Builds the offset chain for DynamicAlias2 format. + /// + /// The chain encodes the byte offset and presence of each signal's + /// packed data within the value change section. + Uint8List _buildOffsetChain(List packedSignals) { + final buf = BytesBuilder(copy: false); + var currentOffset = 0; // byte offset within vc section (after pack_type) + var prevOffset = 0; + var consecutiveEmpty = 0; + + // Offset 0 is the pack_type byte itself. Signal data starts at offset 1. + currentOffset = 1; // skip the pack_type byte + + for (var i = 0; i < packedSignals.length; i++) { + final ps = packedSignals[i]; + if (ps.isEmpty) { + consecutiveEmpty++; + } else { + // Flush any consecutive empty signals + if (consecutiveEmpty > 0) { + // Write: varint((count << 1) | 0) — bit0=0 means "zero block" + buf.add(encodeVarint(consecutiveEmpty << 1)); + consecutiveEmpty = 0; + } + // Write positive offset delta (signed varint with bit0=1) + // In DynamicAlias2: bit0=1 + signed_varint >> 1 > 0 means + // new incremental offset delta. + // Encoding: signed_varint((delta << 1) | 1) + // Reader does: shval = read_variant_i64() >> 1 = delta + final offsetDelta = currentOffset - prevOffset; + buf.add(encodeSignedVarint((offsetDelta << 1) | 1)); + prevOffset = currentOffset; + currentOffset += ps.length; + } + } + + // Flush trailing empty signals + if (consecutiveEmpty > 0) { + buf.add(encodeVarint(consecutiveEmpty << 1)); + } + + return buf.toBytes(); + } + + /// Builds the time table section (appended at end of VcData block). + /// + /// The time table is: compressed delta-encoded timestamps, followed by + /// 3 u64s: uncompressed_length, compressed_length, num_entries. + Uint8List _buildTimeTableBytes(List timeTable) { + // Delta-encode the time table + final deltaBuf = BytesBuilder(copy: false); + var prevTime = 0; + for (final t in timeTable) { + deltaBuf.add(encodeVarint(t - prevTime)); + prevTime = t; + } + final uncompressed = deltaBuf.toBytes(); + final compressed = _zlibCompress( + uncompressed, + config.compressionLevel, + allowRaw: true, + ); + + // Build the full time section: compressed data + 3 u64s + final result = BytesBuilder(copy: false) + ..add(compressed) + ..add(_encodeU64(uncompressed.length)) + ..add(_encodeU64(compressed.length)) + ..add(_encodeU64(timeTable.length)); + return result.toBytes(); + } + + // ─────────────── Low-level I/O helpers ─────────────── + + /// Writes a big-endian u64. + void _writeU64(int value) { + final bd = ByteData(8)..setUint64(0, value); + _file.writeFromSync(bd.buffer.asUint8List()); + } + + /// Encodes a big-endian u64 to bytes. + Uint8List _encodeU64(int value) { + final bd = ByteData(8)..setUint64(0, value); + return bd.buffer.asUint8List(); + } + + /// Writes a little-endian f64 (for double endian test). + void _writeF64LE(double value) { + final bd = ByteData(8)..setFloat64(0, value, Endian.little); + _file.writeFromSync(bd.buffer.asUint8List()); + } + + /// Writes a fixed-length NUL-padded string. + void _writeFixedString(String value, int maxLen) { + final bytes = utf8.encode(value); + final len = bytes.length < maxLen ? bytes.length : maxLen - 1; + _file + ..writeFromSync(bytes.sublist(0, len)) + // Pad with zeros + ..writeFromSync(Uint8List(maxLen - len)); + } + + /// Encodes a NUL-terminated string. + Uint8List _cString(String value) { + final bytes = utf8.encode(value); + final result = Uint8List(bytes.length + 1) + ..setRange(0, bytes.length, bytes); + // last byte is already 0 + return result; + } + + /// Encodes an unsigned integer as LEB128 varint. + static Uint8List encodeVarint(int value) { + if (value < 0) { + throw ArgumentError('Value must be non-negative: $value'); + } + if (value <= 0x7F) { + return Uint8List.fromList([value]); + } + final bytes = []; + var v = value; + while (v != 0) { + final nextV = v >> 7; + final mask = nextV == 0 ? 0 : 0x80; + bytes.add((v & 0x7F) | mask); + v = nextV; + } + return Uint8List.fromList(bytes); + } + + /// Encodes a signed integer as signed LEB128 varint. + static Uint8List encodeSignedVarint(int value) { + if (value >= -64 && value <= 63) { + return Uint8List.fromList([value & 0x7F]); + } + + final bytes = []; + var v = value; + var more = true; + while (more) { + var byte_ = v & 0x7F; + v >>= 7; + // Check if we're done + if ((v == 0 && (byte_ & 0x40) == 0) || (v == -1 && (byte_ & 0x40) != 0)) { + more = false; + } else { + byte_ |= 0x80; + } + bytes.add(byte_); + } + return Uint8List.fromList(bytes); + } + + /// Writes gzip-compressed bytes (gzip header + deflate data). + void _writeGzipCompressed(Uint8List data) { + // Gzip header (10 bytes) + const gzipHeader = [ + 0x1F, 0x8B, // magic + 0x08, // deflate + 0x00, // no flags + 0x00, 0x00, 0x00, 0x00, // timestamp = 0 + 0x00, // compression level + 0xFF, // OS = unknown + ]; + _file.writeFromSync(Uint8List.fromList(gzipHeader)); + + // Deflate-compressed data (raw deflate, not zlib-wrapped) + final compressed = _deflateCompress(data, config.compressionLevel); + _file.writeFromSync(compressed); + } + + /// Compresses bytes using zlib (with zlib header, for geometry/frame/etc). + static Uint8List _zlibCompress( + Uint8List data, + int level, { + bool allowRaw = false, + }) { + final compressed = ZLibCodec(level: level).encode(data); + final result = Uint8List.fromList(compressed); + if (allowRaw && result.length >= data.length) { + // Compression didn't help, return uncompressed + return data; + } + return result; + } + + /// Compresses bytes using raw deflate (no zlib header, for gzip hierarchy). + static Uint8List _deflateCompress(Uint8List data, int level) { + final compressed = ZLibCodec(level: level, raw: true).encode(data); + return Uint8List.fromList(compressed); + } + + /// Generates a date string for the header. + String _dateString() { + final now = DateTime.now(); + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', // + ]; + final day = days[now.weekday - 1]; + final month = months[now.month - 1]; + final d = now.day.toString().padLeft(2); + final h = now.hour.toString().padLeft(2, '0'); + final m = now.minute.toString().padLeft(2, '0'); + final s = now.second.toString().padLeft(2, '0'); + return '$day $month $d $h:$m:$s ${now.year}\n'; + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index ffeff9fc8..d5cf5edb5 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. @@ -1137,23 +1134,16 @@ abstract class Module { /// /// Currently returns one long file in SystemVerilog, but in the future /// may have other output formats, languages, files, etc. + /// + /// For richer access to per-module file contents, named maps, and individual + /// file writing, see [SvService] (and [SvService.synthOutput] for the + /// equivalent one-shot string). String generateSynth() { if (!_hasBuilt) { throw ModuleNotBuiltException(this); } - final synthHeader = ''' -/** - * Generated by ROHD - www.github.com/intel/rohd - * Generation time: ${Timestamper.stamp()} - * ROHD Version: ${Config.version} - */ - -'''; - return synthHeader + - SynthBuilder(this, SystemVerilogSynthesizer()) - .getSynthFileContents() - .join('\n\n////////////////////\n\n'); + return SvService(this, register: false).synthOutput; } } diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart new file mode 100644 index 000000000..97cf0c45e --- /dev/null +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sv_service.dart +// Service wrapper for SystemVerilog synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; + +/// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. +/// +/// Provides access to the generated SV file contents and per-module +/// synthesis results, and optionally registers with [ModuleServices] +/// for DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final sv = SvService(dut); +/// +/// // Write individual .sv files: +/// sv.writeFiles('build/'); +/// +/// // Or get the concatenated output (like generateSynth): +/// print(sv.allContents); +/// ``` +class SvService extends CodegenService { + /// The separator inserted between module definitions in the + /// concatenated single-file output from [allContents]. + /// + /// Matches the format historically produced by `Module.generateSynth()`. + static const moduleSeparator = '\n\n////////////////////\n\n'; + + /// The most recently registered [SvService], or `null`. + static SvService? current; + + /// The top-level [Module] being synthesized. + @override + final Module module; + + /// The default location written by [write]. + /// + /// A directory when [multiFile] is `true`, otherwise a single file path. + @override + final String? outputPath; + + /// Whether [write] emits one `.sv` file per module definition (`true`) or a + /// single concatenated file (`false`). + @override + final bool multiFile; + + /// The underlying [SynthBuilder] that drove synthesis. + late final SynthBuilder synthBuilder; + + /// The generated file contents (one per unique module definition). + late final List fileContents; + + /// Creates an [SvService] for [module]. + /// + /// [module] must already be built. + /// + /// If [outputPath] is provided, output is written immediately: a directory + /// of per-module files when [multiFile] is `true`, otherwise the + /// concatenated SV output (with header) to that single file. + SvService(this.module, + {bool register = true, this.outputPath, this.multiFile = false}) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating SvService. ' + 'Call build() first.', + ); + } + + synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); + fileContents = synthBuilder.getSynthFileContents(); + + if (outputPath != null) { + write(); + } + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + /// All [SynthesisResult]s produced by synthesis. + Set get synthesisResults => synthBuilder.synthesisResults; + + /// Returns the concatenated SystemVerilog module definitions as a single + /// string, without the generation header. + /// + /// For the full output with header (matching `Module.generateSynth()`), + /// use [synthOutput]. + String get allContents => + fileContents.map((fc) => fc.contents).join(moduleSeparator); + + /// The ROHD generation header prepended to single-file output. + String get synthHeader => ''' +/** + * Generated by ROHD - www.github.com/intel/rohd + * Generation time: ${Timestamper.stamp()} + * ROHD Version: ${Config.version} + */ + +'''; + + /// Returns the full single-file SystemVerilog output with header, + /// identical to `Module.generateSynth()`. + /// + /// Computed once and cached so the timestamped header is stable for the + /// lifetime of this service. + late final String synthOutput = synthHeader + allContents; + + /// The combined single-file generated output (alias for [synthOutput]). + @override + String get output => synthOutput; + + /// Returns a map from module definition name to its SV file contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + + /// Returns a map from module definition name + /// ([Module.definitionName]) to its SV file contents. + /// + /// This uses the original definition name (not uniquified), matching + /// the keys used by FLC trace data. + @override + Map get contentsByDefinitionName { + final result = {}; + for (final sr in synthesisResults) { + final defName = sr.module.definitionName; + final instanceName = sr.instanceTypeName; + // Find the file content matching this instance type name. + final fc = fileContents.firstWhereOrNull((f) => f.name == instanceName); + if (fc != null) { + result[defName] = fc.contents; + } + } + return result; + } + + /// Writes each module's SV to a separate file in [directory]. + /// + /// Files are named `.sv`. + void writeFiles(String directory) { + final dir = Directory(directory)..createSync(recursive: true); + for (final fc in fileContents) { + File('${dir.path}/${fc.name}.sv').writeAsStringSync(fc.contents); + } + } + + /// Writes the SV output to [path], or to [outputPath] when [path] is omitted. + /// + /// When [multiFile] is `true`, writes one `.sv` file per module definition + /// into the target directory (see [writeFiles]); otherwise writes the + /// concatenated [synthOutput] to the target file. + @override + void write([String? path]) { + final target = path ?? outputPath; + if (target == null) { + throw ArgumentError( + 'No output path provided: pass a path to write() or set outputPath.', + ); + } + if (multiFile) { + writeFiles(target); + } else { + File(target) + ..parent.createSync(recursive: true) + ..writeAsStringSync(synthOutput); + } + } + + /// Returns a JSON-serialisable summary of the SV synthesis. + /// + /// Contains the list of generated module definition names. + @override + Map toJson() => { + 'modules': [for (final fc in fileContents) fc.name], + }; +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog.dart b/lib/src/synthesizers/systemverilog/systemverilog.dart index 281b05df9..e5f772e44 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,5 +1,6 @@ // Copyright (C) 2021-2024 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'sv_service.dart'; export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..0ebabe68e 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -104,10 +104,7 @@ class Vector { final outputPort = module.tryInOut(outputName) ?? module.output(outputName); final expected = expectedOutput.value; - final expectedValue = LogicValue.of( - expected, - width: outputPort.width, - ); + final expectedValue = LogicValue.of(expected, width: outputPort.width); final inputStimulus = inputValues.toString(); if (outputPort is LogicArray) { @@ -125,12 +122,8 @@ class Vector { } final checks = checksList.join('\n'); - final tbVerilog = [ - assignments, - '#$_offset', - checks, - '#${_period - _offset}', - ].join('\n'); + final tbVerilog = + [assignments, '#$_offset', checks, '#${_period - _offset}'].join('\n'); return tbVerilog; } } @@ -202,12 +195,10 @@ abstract class SimCompare { throw NonSupportedTypeException(value); } } - }).catchError( - test: (error) => error is Exception, - (Object err, StackTrace stackTrace) { - Simulator.throwException(err as Exception, stackTrace); - }, - )); + }).catchError(test: (error) => error is Exception, + (Object err, StackTrace stackTrace) { + Simulator.throwException(err as Exception, stackTrace); + })); } }); timestamp += Vector._period; @@ -224,23 +215,20 @@ abstract class SimCompare { RegExp(r'sorry: constant selects in always_\* processes' ' are not currently supported'), RegExp('warning: always_comb process has no sensitivities'), - RegExp('finish called at'), + RegExp('finish called at') ]; /// Executes [vectors] against the Icarus Verilog simulator and checks /// that it passes. - static void checkIverilogVector( - Module module, - List vectors, { - String? moduleName, - bool dontDeleteTmpFiles = false, - bool dumpWaves = false, - List iverilogExtraArgs = const [], - bool allowWarnings = false, - bool maskKnownWarnings = true, - bool enableChecking = true, - bool buildOnly = false, - }) { + static void checkIverilogVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool enableChecking = true, + bool buildOnly = false}) { final result = iverilogVector(module, vectors, moduleName: moduleName, dontDeleteTmpFiles: dontDeleteTmpFiles, @@ -255,17 +243,14 @@ abstract class SimCompare { } /// Executes [vectors] against the Icarus Verilog simulator. - static bool iverilogVector( - Module module, - List vectors, { - String? moduleName, - bool dontDeleteTmpFiles = false, - bool dumpWaves = false, - List iverilogExtraArgs = const [], - bool allowWarnings = false, - bool maskKnownWarnings = true, - bool buildOnly = false, - }) { + static bool iverilogVector(Module module, List vectors, + {String? moduleName, + bool dontDeleteTmpFiles = false, + bool dumpWaves = false, + List iverilogExtraArgs = const [], + bool allowWarnings = false, + bool maskKnownWarnings = true, + bool buildOnly = false}) { if (kIsWeb) { // if running in web mode, then we can't run icarus verilog return true; @@ -307,7 +292,7 @@ abstract class SimCompare { final topModule = moduleName ?? module.definitionName; final allSignals = { for (final v in vectors) ...v.inputValues.keys, - for (final v in vectors) ...v.expectedOutputValues.keys, + for (final v in vectors) ...v.expectedOutputValues.keys }; late final tbWireUniquifier = Uniquifier(); @@ -335,14 +320,14 @@ abstract class SimCompare { final sigDecl = signalDeclaration(logicName, adjust: toTbWireName, signalTypeOverride: 'wire'); return '$sigDecl; assign $wireName = $logicName;'; - }), + }) ].join('\n'); final moduleConnections = allSignals.map((e) => '.$e(${logicToWireMapping[e] ?? e})').join(', '); final moduleInstance = '$topModule dut($moduleConnections);'; final stimulus = vectors.map((e) => e.toTbVerilog(module)).join('\n'); - final generatedVerilog = module.generateSynth(); + final generatedVerilog = SvService(module, register: false).synthOutput; // so that when they run in parallel, they dont step on each other final uniqueId = @@ -370,7 +355,7 @@ abstract class SimCompare { stimulus, r'$finish;', // so the test doesn't run forever if there's a clock gen 'end', - 'endmodule', + 'endmodule' ].join('\n'); Directory(dir).createSync(recursive: true); @@ -397,11 +382,7 @@ abstract class SimCompare { } return output.toString().contains(RegExp( - [ - 'error', - 'unable', - if (!allowWarnings) 'warning', - ].join('|'), + ['error', 'unable', if (!allowWarnings) 'warning'].join('|'), caseSensitive: false)); } diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 3a37e55ea..1e426f02a 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -1,11 +1,13 @@ -// 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. +// Waveform dumper for a given module hierarchy, dumps to ".vcd" or ".fst" file. // // 2021 May 7 // Author: Max Korbel +// 2026 February - Added FST format support +// Author: Desmond Kirkpatrick import 'dart:collection'; import 'dart:io'; @@ -15,13 +17,31 @@ import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; +/// Waveform output format. +enum WaveFormat { + /// VCD (Value Change Dump) — IEEE 1364 standard text format. + vcd, + + /// FST (Fast Signal Trace) — GTKWave binary format. + /// + /// FST files are compressed, support random access, and are compatible + /// with GTKWave, Surfer, and the wellen reader. + fst, +} + /// A waveform dumper for simulations. /// -/// Outputs to vcd format at [outputPath]. [module] must be built prior to -/// attaching the [WaveDumper]. +/// Outputs to VCD or FST format at [outputPath]. [module] must be built prior +/// to attaching the [WaveDumper]. /// /// The waves will only dump to the file periodically and then once the /// simulation has completed. +/// +/// +/// To output FST (compressed binary) instead of VCD (text): +/// ```dart +/// WaveDumper(module, outputPath: 'waves.fst', format: WaveFormat.fst); +/// ``` class WaveDumper { /// The [Module] being dumped. final Module module; @@ -29,13 +49,20 @@ class WaveDumper { /// The output filepath of the generated waveforms. final String outputPath; - /// The file to write dumped output waveform to. - final File _outputFile; + /// The waveform output format (VCD or FST). + final WaveFormat format; + + /// The FST writer configuration (only used when [format] is + /// [WaveFormat.fst]). + final FstWriterConfig? fstConfig; + + /// The file to write dumped output waveform to (VCD only). + File? _outputFile; - /// A sink to write contents into [_outputFile]. - late final IOSink _outFileSink; + /// A sink to write contents into [_outputFile] (VCD only). + IOSink? _outFileSink; - /// A buffer for contents before writing to the file sink. + /// A buffer for contents before writing to the file sink (VCD only). final StringBuffer _fileBuffer = StringBuffer(); /// A counter for tracking signal names in the VCD file. @@ -44,6 +71,12 @@ class WaveDumper { /// Stores the mapping from [Logic] to signal marker in the VCD file. final Map _signalToMarkerMap = {}; + /// Stores the mapping from [Logic] to FST signal handle (FST only). + final Map _signalToFstHandle = {}; + + /// The FST writer instance (FST only). + FstWriter? _fstWriter; + /// 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 @@ -57,20 +90,27 @@ class WaveDumper { int _currentDumpingTimestamp = Simulator.time; /// 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) { + /// [module] in a waveform file at [outputPath]. + /// + /// The output [format] defaults to [WaveFormat.vcd] for VCD text files. + /// Set to [WaveFormat.fst] for compressed FST binary files. + /// + WaveDumper( + this.module, { + this.outputPath = 'waves.vcd', + this.format = WaveFormat.vcd, + this.fstConfig, + }) { if (!module.hasBuilt) { throw Exception( 'Module must be built before passed to dumper. Call build() first.'); } - _outFileSink = _outputFile.openWrite(); - - _collectAllSignals(); - - _writeHeader(); - _writeScope(); + if (format == WaveFormat.fst) { + _initFst(); + } else { + _initVcd(); + } Simulator.preTick.listen((args) { if (Simulator.time != _currentDumpingTimestamp) { @@ -93,7 +133,81 @@ class WaveDumper { /// write contents to the output file. static const _fileBufferLimit = 100000; - /// Buffers [contents] to be written to the output file. + // ─────────────── VCD initialization ─────────────── + + /// Initializes VCD output. + void _initVcd() { + _outputFile = File(outputPath)..createSync(recursive: true); + _outFileSink = _outputFile!.openWrite(); + _collectAllSignals(); + _writeVcdHeader(); + _writeVcdScope(); + } + + // ─────────────── FST initialization ─────────────── + + /// Initializes FST output. + void _initFst() { + _fstWriter = + FstWriter(outputPath, config: fstConfig ?? const FstWriterConfig()); + + // Walk module hierarchy and declare signals + _collectAllSignalsFst(module); + + // Write header after all signals declared + _fstWriter!.writeHeader(); + } + + /// Collects signals from the module hierarchy and declares them in the FST + /// writer. + void _collectAllSignalsFst(Module m) { + _fstWriter!.pushScope(m.uniqueInstanceName); + var hasSignals = false; + + final moduleSignalUniquifier = Uniquifier(); + + for (final sig in m.signals) { + if (sig is Const) { + continue; + } + + hasSignals = true; + final baseName = Sanitizer.sanitizeSV(sig.name); + final signalName = moduleSignalUniquifier.getUniqueName( + initialName: baseName, reserved: sig.isPort); + + final handle = _fstWriter!.declareSignal( + signalName, + sig.width, + direction: sig.isPort + ? (sig.isInput ? FstVarDirection.input : FstVarDirection.output) + : FstVarDirection.implicit, + ); + _signalToFstHandle[sig] = handle; + + sig.changed.listen((args) { + _changedLogicsThisTimestamp.add(sig); + }); + } + + for (final subm in m.subModules) { + if (subm is InlineSystemVerilog) { + continue; + } + _collectAllSignalsFst(subm); + } + + // Only pop scope if we had content (matching VCD empty-scope behavior) + if (!hasSignals && + m.subModules.where((s) => s is! InlineSystemVerilog).isEmpty) { + // empty scope — we still need to pop what we pushed + } + _fstWriter!.popScope(); + } + + // ─────────────── Shared methods ─────────────── + + /// Buffers [contents] to be written to the VCD output file. void _writeToBuffer(String contents) { _fileBuffer.write(contents); @@ -102,17 +216,23 @@ class WaveDumper { } } - /// Writes all pending items in the [_fileBuffer] to the file. + /// Writes all pending items in the [_fileBuffer] to the VCD file. void _writeToFile() { - _outFileSink.write(_fileBuffer.toString()); + _outFileSink?.write(_fileBuffer.toString()); _fileBuffer.clear(); } /// Terminates the waveform dumping, including closing the file. Future _terminate() async { - _writeToFile(); - await _outFileSink.flush(); - await _outFileSink.close(); + if (format == WaveFormat.fst) { + // For FST: flush any remaining changes and finalize + _fstWriter?.finish(); + } else { + // For VCD: flush buffer and close file + _writeToFile(); + await _outFileSink?.flush(); + await _outFileSink?.close(); + } } /// Registers all signal value changes to write updates to the dumped VCD. @@ -131,6 +251,7 @@ class WaveDumper { _changedLogicsThisTimestamp.add(sig); }); } + for (final subm in m.subModules) { if (subm is InlineSystemVerilog) { // the InlineSystemVerilog modules are "boring" to inspect @@ -141,8 +262,10 @@ class WaveDumper { } } + // ─────────────── VCD-specific methods ─────────────── + /// Writes the top header for the VCD file. - void _writeHeader() { + void _writeVcdHeader() { final dateString = Timestamper.stamp(); const timescale = '1ps'; final header = ''' @@ -162,12 +285,13 @@ class WaveDumper { /// Writes the scope of the VCD, including signal and hierarchy declarations, /// as well as initial values. - void _writeScope() { + void _writeVcdScope() { var scopeString = _computeScopeString(module); scopeString += '\$enddefinitions \$end\n'; scopeString += '\$dumpvars\n'; _writeToBuffer(scopeString); _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + _writeToBuffer('\$end\n'); } @@ -184,12 +308,13 @@ class WaveDumper { final width = sig.width; final marker = _signalToMarkerMap[sig]; - var signalName = Sanitizer.sanitizeSV(sig.name); - signalName = moduleSignalUniquifier.getUniqueName( - initialName: signalName, reserved: sig.isPort); + final baseName = Sanitizer.sanitizeSV(sig.name); + final signalName = moduleSignalUniquifier.getUniqueName( + initialName: baseName, 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)); @@ -203,8 +328,19 @@ class WaveDumper { return scopeString; } - /// Writes the current timestamp to the VCD. + // ─────────────── Timestamp capture ─────────────── + + /// Captures all signal changes at the current timestamp. void _captureTimestamp(int timestamp) { + if (format == WaveFormat.fst) { + _captureTimestampFst(timestamp); + } else { + _captureTimestampVcd(timestamp); + } + } + + /// Captures a VCD timestamp: writes the timestamp marker and changed values. + void _captureTimestampVcd(int timestamp) { final timestampString = '#$timestamp\n'; _writeToBuffer(timestampString); @@ -213,6 +349,23 @@ class WaveDumper { ..clear(); } + /// Captures an FST timestamp: emits value changes for all changed signals. + void _captureTimestampFst(int timestamp) { + for (final sig in _changedLogicsThisTimestamp) { + final handle = _signalToFstHandle[sig]; + if (handle == null) { + continue; + } + + final binaryValue = sig.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); + _fstWriter!.emitValueChange(timestamp, handle, binaryValue); + } + _changedLogicsThisTimestamp.clear(); + } + /// Writes the current value of [signal] to the VCD. void _writeSignalValueUpdate(Logic signal) { final binaryValue = signal.value.reversed diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart index 0d3fdeb3a..12a29927f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_details_card.dart @@ -16,10 +16,7 @@ import 'package:rohd_devtools_extension/rohd_devtools/ui/signal_table.dart'; class SignalDetailsCard extends StatefulWidget { final TreeModel? module; - const SignalDetailsCard({ - Key? key, - this.module, - }) : super(key: key); + const SignalDetailsCard({Key? key, this.module}) : super(key: key); @override SignalDetailsCardState createState() => SignalDetailsCardState(); 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 8e97328d8..1b66861da 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart @@ -90,29 +90,12 @@ class _SignalTableState extends State { TableRow _generateSignalRow(SignalModel signal) { return TableRow( children: [ + SizedBox(height: 32, child: Center(child: Text(signal.name))), + SizedBox(height: 32, child: Center(child: Text(signal.direction))), + SizedBox(height: 32, child: Center(child: Text(signal.value))), SizedBox( height: 32, - child: Center( - child: Text(signal.name), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.direction), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.value), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.width.toString()), - ), + child: Center(child: Text(signal.width.toString())), ), ], ); @@ -124,10 +107,7 @@ class _SignalTableState extends State { child: Center( child: Text( text, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), ), ); diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 0aa366e78..8b5bb226f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.1.0 + bloc_lint: ^0.3.7 flutter: uses-material-design: true diff --git a/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart b/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart index 8ccd28d4c..15287d70e 100644 --- a/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart +++ b/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart @@ -29,71 +29,83 @@ void main() { }); testWidgets( - 'displays ModuleTreeCard when state is RohdServiceLoaded with treeModel', - (tester) async { - final treeModel = MockTreeModel(); + 'displays ModuleTreeCard when state is RohdServiceLoaded with treeModel', + (tester) async { + final treeModel = MockTreeModel(); - when(() => rohdServiceCubit.state) - .thenReturn(RohdServiceLoaded(treeModel)); - when(() => rohdServiceCubit.stream) - .thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); - when(() => treeSearchTermCubit.state).thenReturn(null); - when(() => treeSearchTermCubit.stream) - .thenAnswer((_) => Stream.value(null)); + when( + () => rohdServiceCubit.state, + ).thenReturn(RohdServiceLoaded(treeModel)); + when( + () => rohdServiceCubit.stream, + ).thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); + when(() => treeSearchTermCubit.state).thenReturn(null); + when( + () => treeSearchTermCubit.stream, + ).thenAnswer((_) => Stream.value(null)); - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: rohdServiceCubit), - BlocProvider.value(value: treeSearchTermCubit), - ], - child: MaterialApp( - home: Scaffold( - body: TreeStructurePage(screenSize: const Size(2000, 1000)), + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: rohdServiceCubit), + BlocProvider.value( + value: treeSearchTermCubit, + ), + ], + child: MaterialApp( + home: Scaffold( + body: TreeStructurePage(screenSize: const Size(2000, 1000)), + ), ), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - expect(find.byType(ModuleTreeCard), findsOneWidget); - }); + expect(find.byType(ModuleTreeCard), findsOneWidget); + }, + ); testWidgets( - 'displays SignalDetailsCard when state is RohdServiceLoaded with selected module', - (tester) async { - final treeModel = MockTreeModel(); - final signalModelList = [ - MockSignalModel(), - MockSignalModel() - ]; - when(() => rohdServiceCubit.state) - .thenReturn(RohdServiceLoaded(treeModel)); - when(() => rohdServiceCubit.stream) - .thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); - when(() => treeModel.inputs).thenReturn(signalModelList); - when(() => treeModel.outputs).thenReturn(signalModelList); - when(() => treeSearchTermCubit.stream) - .thenAnswer((_) => Stream.value(null)); + 'displays SignalDetailsCard when state is RohdServiceLoaded with selected module', + (tester) async { + final treeModel = MockTreeModel(); + final signalModelList = [ + MockSignalModel(), + MockSignalModel(), + ]; + when( + () => rohdServiceCubit.state, + ).thenReturn(RohdServiceLoaded(treeModel)); + when( + () => rohdServiceCubit.stream, + ).thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); + when(() => treeModel.inputs).thenReturn(signalModelList); + when(() => treeModel.outputs).thenReturn(signalModelList); + when( + () => treeSearchTermCubit.stream, + ).thenAnswer((_) => Stream.value(null)); - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: rohdServiceCubit), - BlocProvider.value(value: treeSearchTermCubit), - ], - child: MaterialApp( - home: Scaffold( - body: TreeStructurePage(screenSize: const Size(800, 600)), + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: rohdServiceCubit), + BlocProvider.value( + value: treeSearchTermCubit, + ), + ], + child: MaterialApp( + home: Scaffold( + body: TreeStructurePage(screenSize: const Size(800, 600)), + ), ), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - expect(find.byType(SignalDetailsCard), findsOneWidget); - }); + expect(find.byType(SignalDetailsCard), findsOneWidget); + }, + ); }); } diff --git a/rohd_devtools_extension/web/icons/Icon-192.png b/rohd_devtools_extension/web/icons/Icon-192.png index b749bfef0..acd165142 100644 Binary files a/rohd_devtools_extension/web/icons/Icon-192.png and b/rohd_devtools_extension/web/icons/Icon-192.png differ diff --git a/rohd_devtools_extension/web/icons/Icon-512.png b/rohd_devtools_extension/web/icons/Icon-512.png index 88cfd48df..bb667c609 100644 Binary files a/rohd_devtools_extension/web/icons/Icon-512.png and b/rohd_devtools_extension/web/icons/Icon-512.png differ diff --git a/rohd_devtools_extension/web/icons/Icon-maskable-192.png b/rohd_devtools_extension/web/icons/Icon-maskable-192.png index eb9b4d76e..acd165142 100644 Binary files a/rohd_devtools_extension/web/icons/Icon-maskable-192.png and b/rohd_devtools_extension/web/icons/Icon-maskable-192.png differ diff --git a/rohd_devtools_extension/web/icons/Icon-maskable-512.png b/rohd_devtools_extension/web/icons/Icon-maskable-512.png index d69c56691..bb667c609 100644 Binary files a/rohd_devtools_extension/web/icons/Icon-maskable-512.png and b/rohd_devtools_extension/web/icons/Icon-maskable-512.png differ diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 1e01f0431..1b9357ec0 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -915,7 +915,7 @@ void main() { test('simple 1d collapse', () async { final mod = SimpleLAPassthrough(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign laOut = laIn;')); }); @@ -923,7 +923,7 @@ void main() { test('array collapse for cross-module connection', () async { final mod = ArrayTopMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(RegExp(r'ArraySubModIn.*\.inp\(inp\)'))); expect(sv, contains(RegExp(r'ArraySubModOut.*\.arrOut\(inp\)'))); @@ -934,7 +934,7 @@ void main() { LogicArray([3, 3], 1), LogicArray([3, 3], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -952,7 +952,7 @@ void main() { final mod = ArrayWithShuffledAssignment(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).allContents; expect(sv, contains('assign b[0] = a[3];')); expect(sv, contains('assign b[3] = a[0];')); @@ -969,7 +969,7 @@ void main() { LogicArray([3, 3], 1, numUnpackedDimensions: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -986,7 +986,7 @@ void main() { final mod = ArrayModule(LogicArray([4, 4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign d = c[0];')); expect(sv, contains('assign b = a;')); diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..7fbd7f44b 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -228,7 +228,7 @@ void main() { final mod = SingleBitBusSubsetMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign result = oneBit')); final vectors = [ @@ -401,7 +401,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains("assign const_subset = 16'habcd;"), true); }); }); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..70d6ff73d 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -57,7 +57,7 @@ void main() { test('collapse pretty', () async { final mod = CollapseTestModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure e=a&b&c is in there, to prove there was some inlining expect(sv, contains(RegExp('e.*=.*a.*&.*b.*&.*c'))); diff --git a/test/config_test.dart b/test/config_test.dart index 28cd2e7d8..1730ab77a 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -46,7 +46,7 @@ void main() async { final mod = SimpleModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(version)); }); diff --git a/test/counter_wintf_test.dart b/test/counter_wintf_test.dart index 7889369cc..91986aea2 100644 --- a/test/counter_wintf_test.dart +++ b/test/counter_wintf_test.dart @@ -145,7 +145,7 @@ void main() { test('interface ports dont get doubled up', () async { final mod = Counter(CounterInterface(8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(!sv.contains('en_0'), true); }); diff --git a/test/external_test.dart b/test/external_test.dart index 09ac84c87..3ba33c649 100644 --- a/test/external_test.dart +++ b/test/external_test.dart @@ -31,7 +31,7 @@ void main() { test('instantiate', () async { final mod = TopModule(Logic(width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure we instantiate the external module properly expect( diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..b4e879403 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -183,7 +183,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("b = 1'h0;")); }); @@ -192,7 +192,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('priority case')); }); @@ -201,7 +201,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('MyStates_state1 : begin')); }); diff --git a/test/fst_writer_test.dart b/test/fst_writer_test.dart new file mode 100644 index 000000000..7374a2239 --- /dev/null +++ b/test/fst_writer_test.dart @@ -0,0 +1,420 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_writer_test.dart +// Tests for FST writer and WaveDumper FST format support. +// +// 2026 February +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import 'pipeline_test.dart' show SimplePipelineModule; + +/// A simple module for testing. +class _SimpleModule extends Module { + _SimpleModule(Logic a) { + a = addInput('a', a); + addOutput('b') <= a; + } +} + +/// A module with multi-bit signals for testing. +class _MultiBitModule extends Module { + _MultiBitModule(Logic a, Logic clk) { + a = addInput('a', a, width: a.width); + final aClk = addInput('clk', clk); + addOutput('q', width: a.width) <= FlipFlop(aClk, a).q; + } +} + +const _tempDumpDir = 'tmp_test'; + +/// Gets the path of the FST file based on a name. +String _temporaryFstPath(String name) => '$_tempDumpDir/temp_dump_$name.fst'; + +/// Attaches a [WaveDumper] to [module] with FST format. +void _createFstDump(Module module, String name) { + Directory(_tempDumpDir).createSync(recursive: true); + final tmpDumpFile = _temporaryFstPath(name); + WaveDumper(module, outputPath: tmpDumpFile, format: WaveFormat.fst); +} + +/// Deletes the temporary FST file associated with [name]. +void _deleteFstDump(String name) { + final tmpDumpFile = _temporaryFstPath(name); + if (File(tmpDumpFile).existsSync()) { + File(tmpDumpFile).deleteSync(); + } +} + +/// Reads a big-endian u64 from [data] at [offset]. +int _readU64(Uint8List data, int offset) { + var result = 0; + for (var i = 0; i < 8; i++) { + result = (result << 8) | data[offset + i]; + } + return result; +} + +/// Parses FST file blocks and returns a map of block types to counts. +Map _parseFstBlocks(Uint8List data) { + final blocks = {}; + var pos = 0; + while (pos < data.length) { + final blockType = data[pos]; + pos++; + if (pos + 8 > data.length) { + break; + } + final sectionLength = _readU64(data, pos); + blocks[blockType] = (blocks[blockType] ?? 0) + 1; + pos += sectionLength; + if (sectionLength == 0) { + break; + } + } + return blocks; +} + +/// Parses FST header and returns key fields. +Map _parseFstHeader(Uint8List data) { + // Skip block type byte (0) + if (data[0] != 0) { + throw FormatException('Expected header block type 0, got ${data[0]}'); + } + final sectionLength = _readU64(data, 1); + if (sectionLength != 329) { + throw FormatException( + 'Expected header section length 329, got $sectionLength'); + } + return { + 'start_time': _readU64(data, 9), + 'end_time': _readU64(data, 17), + // skip double_endian_test (8 bytes at offset 25) + 'scope_count': _readU64(data, 41), + 'var_count': _readU64(data, 49), + 'max_var_id': _readU64(data, 57), + 'vc_section_count': _readU64(data, 65), + 'timescale_exponent': data[73], // offset 73 = 1 + 8*9 + }; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('FstWriter unit tests', () { + test('writes valid header block', () { + const path = '$_tempDumpDir/fst_header_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + FstWriter(path) + ..pushScope('top') + ..declareSignal('clk', 1) + ..declareSignal('data', 8) + ..popScope() + ..finish(); + + final data = File(path).readAsBytesSync(); + expect(data[0], equals(0), reason: 'First byte should be header type'); + final sectionLength = _readU64(data, 1); + expect(sectionLength, equals(329), reason: 'Header is 329 bytes'); + + // Parse header fields + final header = _parseFstHeader(data); + expect(header['scope_count'], equals(1)); + expect(header['var_count'], equals(2)); + expect(header['max_var_id'], equals(2)); + + File(path).deleteSync(); + }); + + test('writes all required block types', () { + const path = '$_tempDumpDir/fst_blocks_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + final writer = FstWriter(path)..pushScope('top'); + final clk = writer.declareSignal('clk', 1); + writer + ..popScope() + ..writeHeader() + ..emitValueChange(0, clk, '0') + ..emitValueChange(5, clk, '1') + ..finish(); + + final data = File(path).readAsBytesSync(); + final blocks = _parseFstBlocks(data); + + // Must have: Header(0), VcDataDynamicAlias2(8), Geometry(3), + // Hierarchy(4) + expect(blocks.containsKey(0), isTrue, reason: 'Must have header'); + expect(blocks.containsKey(8), isTrue, reason: 'Must have VcData block'); + expect(blocks.containsKey(3), isTrue, reason: 'Must have geometry'); + expect(blocks.containsKey(4), isTrue, reason: 'Must have hierarchy'); + + File(path).deleteSync(); + }); + + test('geometry encodes signal widths correctly', () { + const path = '$_tempDumpDir/fst_geometry_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + FstWriter(path) + ..pushScope('top') + ..declareSignal('bit1', 1) + ..declareSignal('byte8', 8) + ..declareSignal('word32', 32) + ..popScope() + ..finish(); + + final data = File(path).readAsBytesSync(); + + // Find the geometry block (type 3) + var pos = 0; + while (pos < data.length) { + if (data[pos] == 3) { + // Geometry block + final sectionLength = _readU64(data, pos + 1); + final maxHandle = _readU64(data, pos + 1 + 16); + expect(maxHandle, equals(3)); + + // Geometry data is after section_length(8) + unc_len(8) + + // max_handle(8) = 24 bytes from section_length start + // May be compressed, so just check the block exists + expect(sectionLength, greaterThan(24)); + break; + } + pos++; + if (pos + 8 > data.length) { + break; + } + final sl = _readU64(data, pos); + pos += sl; + if (sl == 0) { + break; + } + } + + File(path).deleteSync(); + }); + }); + + group('WaveDumper FST format', () { + test('basic 1-bit signal FST dump', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dumpName = 'fstBasic'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(100); + await Simulator.run(); + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + final data = fstFile.readAsBytesSync(); + // File should have valid FST header + expect(data[0], equals(0), reason: 'First byte is header block type'); + expect(_readU64(data, 1), equals(329)); + + // Check blocks are present + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue, reason: 'header'); + expect(blocks.containsKey(3), isTrue, reason: 'geometry'); + expect(blocks.containsKey(4), isTrue, reason: 'hierarchy'); + + _deleteFstDump(dumpName); + }); + + test('multi-bit signal FST dump', () async { + final a = Logic(name: 'a', width: 8); + final clk = SimpleClockGenerator(10).clk; + final mod = _MultiBitModule(a, clk); + await mod.build(); + + const dumpName = 'fstMultiBit'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(100); + unawaited(Simulator.run()); + + await clk.nextPosedge; + a.inject(0xAB); + await clk.nextPosedge; + a.inject(0xFF); + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + final data = fstFile.readAsBytesSync(); + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue); + expect(blocks.containsKey(8), isTrue, + reason: 'VcData block with changes'); + + _deleteFstDump(dumpName); + }); + + test('FST file creates non-existent directories', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dir1Path = '$_tempDumpDir/fst_dir1'; + const fstPath = '$dir1Path/dir2/waves.fst'; + + WaveDumper(mod, outputPath: fstPath, format: WaveFormat.fst); + + a.put(0); + Simulator.setMaxSimTime(10); + await Simulator.run(); + + expect(File(fstPath).existsSync(), isTrue); + + if (Directory(dir1Path).existsSync()) { + Directory(dir1Path).deleteSync(recursive: true); + } + }); + + test('FST header has correct signal counts', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dumpName = 'fstCounts'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(10); + await Simulator.run(); + + final data = File(_temporaryFstPath(dumpName)).readAsBytesSync(); + final header = _parseFstHeader(data); + + // _SimpleModule has 2 signals: input 'a' and output 'b' + expect(header['var_count'], equals(2)); + + _deleteFstDump(dumpName); + }); + + test('FST and VCD both produce output', () async { + // Create a module + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + // Dump as FST + const fstName = 'fstCompare'; + _createFstDump(mod, fstName); + + a.put(0); + Simulator.setMaxSimTime(50); + unawaited(Simulator.run()); + + a.inject(1); + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(fstName)); + expect(fstFile.existsSync(), isTrue); + final fstSize = fstFile.lengthSync(); + expect(fstSize, greaterThan(330), reason: 'FST should be > header size'); + + _deleteFstDump(fstName); + + // Reset and dump as VCD + await Simulator.reset(); + + final a2 = Logic(name: 'a'); + final mod2 = _SimpleModule(a2); + await mod2.build(); + + const vcdPath = '$_tempDumpDir/temp_dump_vcdCompare.vcd'; + Directory(_tempDumpDir).createSync(recursive: true); + WaveDumper(mod2, outputPath: vcdPath); + + a2.put(0); + Simulator.setMaxSimTime(50); + unawaited(Simulator.run()); + + a2.inject(1); + + await Simulator.simulationEnded; + + final vcdFile = File(vcdPath); + expect(vcdFile.existsSync(), isTrue); + expect(vcdFile.lengthSync(), greaterThan(0)); + + vcdFile.deleteSync(); + }); + + test('pipeline FST has VcData and is readable by fst2vcd', () async { + // Build a 3-stage 8-bit pipeline that generates many signal changes. + final a = Logic(name: 'a', width: 8); + final mod = SimplePipelineModule(a); + await mod.build(); + + const dumpName = 'fstPipeline'; + _createFstDump(mod, dumpName); + + // Drive 200 clock cycles worth of incrementing inputs. + // The 10ps clock gives 2000ps total, producing many VcData changes. + a.put(0); + Simulator.setMaxSimTime(2000); + unawaited(Simulator.run()); + + // Inject a new value every 10ps to keep signals active + for (var i = 1; i <= 200; i++) { + await Future.delayed(Duration.zero); + a.inject(i & 0xFF); + } + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + // File should be substantially larger than just the header (329 bytes) + final fileSize = fstFile.lengthSync(); + expect(fileSize, greaterThan(600), + reason: 'Pipeline FST should have VcData content'); + + // Parse blocks: must include at least one VcData block (type 8) + final data = fstFile.readAsBytesSync(); + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue, reason: 'header block'); + expect(blocks.containsKey(8), isTrue, reason: 'VcData block'); + expect(blocks.containsKey(3), isTrue, reason: 'geometry block'); + expect(blocks.containsKey(4), isTrue, reason: 'hierarchy block'); + + // Validate with fst2vcd (GTKWave tool) if available. + final fst2vcd = Process.runSync('which', ['fst2vcd']); + if (fst2vcd.exitCode == 0) { + final result = Process.runSync('fst2vcd', [fstFile.path]); + expect(result.exitCode, equals(0), + reason: 'fst2vcd failed: ${result.stdout}\n${result.stderr}'); + final vcdOutput = result.stdout as String; + expect(vcdOutput, contains(r'$timescale'), + reason: 'fst2vcd output should be valid VCD'); + } + + _deleteFstDump(dumpName); + }); + }); +} diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..4fabc3074 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -537,7 +537,7 @@ void main() { final gtm = ShiftTestModule(Logic(width: 3), Logic(width: 8), constant: 0); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = SvService(gtm).synthOutput; expect(sv, isNot(contains("0'h0"))); diff --git a/test/inout_loopback_test.dart b/test/inout_loopback_test.dart index 0c3b5b343..b2fe711b2 100644 --- a/test/inout_loopback_test.dart +++ b/test/inout_loopback_test.dart @@ -237,7 +237,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The outer module should NOT contain an internal net_connect // for the loopback — the submodule ports should just be wired to the @@ -268,7 +268,7 @@ void main() { final mod = SimpleOuterLoopback(LogicNet(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; final outerModuleSv = _extractModuleSv(sv, 'simpleOuter'); expect(outerModuleSv, isNot(contains('net_connect')), @@ -284,7 +284,7 @@ void main() { final mod = LoopbackPairTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Check for net_connect in the top module final topModuleSv = _extractModuleSv(sv, 'LoopbackPairTop'); @@ -309,7 +309,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The inner module SHOULD have a net_connect (connecting ioA <= ioB). final innerModuleSv = _extractModuleSv(sv, 'innerConnected'); @@ -347,7 +347,7 @@ void main() { final mod = OuterClkLoopback(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // The outer module should NOT have a net_connect — the loopback net // is only used as port connections in the inner instantiation. diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..f6d9ca784 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -751,7 +751,7 @@ void main() { ]; if (checkNoSwizzle) { - expect(mod.generateSynth().contains('swizzle'), false, + expect(SvService(mod).synthOutput.contains('swizzle'), false, reason: 'Expected no swizzles but found one.'); } @@ -800,7 +800,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains(RegExp(r'\[7:0\]\s*laIn\s*\[2:0\]')), true); expect(sv.contains(RegExp(r'\[7:0\]\s*laOut\s*\[2:0\]')), true); }); @@ -818,7 +818,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv.contains(RegExp( r'\[2:0\]\s*\[1:0\]\s*\[7:0\]\s*laIn\s*\[4:0\]\s*\[3:0\]')), @@ -846,7 +846,7 @@ void main() { await testArrayPassthrough(mod); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [2:0][1:0][2:0][7:0] laIn')); expect(sv, contains('output logic [2:0][1:0][2:0][7:0] laOut')); }); @@ -861,7 +861,7 @@ void main() { await testArrayPassthrough(mod, noSvSim: true); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [1:0][2:0][7:0] laIn [2:0]')); expect(sv, contains('output logic [1:0][2:0][7:0] laOut [2:0]')); }); @@ -928,7 +928,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('logic [2:0][3:0][7:0] intermediate [1:0]'), true); }); }); @@ -981,7 +981,7 @@ void main() { test('3d', () async { final mod = SimpleArraysAndHierarchy(LogicArray([2], 8)); await testArrayPassthrough(mod); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('SimpleLAPassthrough simple_la_passthrough')); }); @@ -992,7 +992,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - expect(mod.generateSynth(), contains('SimpleLAPassthrough')); + expect(SvService(mod).synthOutput, contains('SimpleLAPassthrough')); }); }); @@ -1001,7 +1001,7 @@ void main() { final mod = FancyArraysAndHierarchy(LogicArray([4, 3, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // make sure the 4th one is there (since we expect 4) expect(sv, contains('SimpleLAPassthrough simple_la_passthrough_2')); @@ -1043,7 +1043,8 @@ void main() { final mod = WithSetArrayOffsetModule(LogicArray([2, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); // make sure we're reassigning both times it overlaps! expect( diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 1a5fb1d98..b2cc6c95d 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -30,7 +30,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('intermediate')); }); @@ -45,7 +45,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // no intermediate expect(sv.contains('intermediate'), isFalse); @@ -58,7 +58,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // just the ports expect('logic'.allMatches(sv).length, 3); @@ -71,7 +71,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // just the ports expect('logic'.allMatches(sv).length, 3); @@ -90,7 +90,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; // held one sticks expect(sv, contains('intermediate_1 = in1')); @@ -108,7 +108,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -124,7 +124,7 @@ void main() { out1 <= intermediate; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -145,7 +145,7 @@ void main() { out1 <= intermediate | intermediate2; }); await dut.build(); - dut.generateSynth(); + SvService(dut).synthOutput; fail('expected an exception!'); } on Exception catch (e) { expect(e, isA()); @@ -167,7 +167,7 @@ void main() { out1 <= ~intermediatePost; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('goodname')); }); @@ -220,7 +220,7 @@ void main() { out1 <= ~prev; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains(expectedName), reason: 'Amongst ${l.map((e) => e.name).toList()},' @@ -235,7 +235,7 @@ void main() { intermediate <= in1; }); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('intermediate')); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 8ce9d5f40..457d3ed52 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -225,13 +225,13 @@ void main() { final mod = LogicWithInternalSignalModule(Logic()); await mod.build(); - expect(mod.generateSynth(), contains('shouldExist')); + expect(SvService(mod).synthOutput, contains('shouldExist')); }); test('unconnected port does not duplicate internal signal', () async { final pMod = ParentMod(Logic(), Logic()); await pMod.build(); - final sv = pMod.generateSynth(); + final sv = SvService(pMod).synthOutput; expect(RegExp('logic a[,;\n]').allMatches(sv).length, 2); }); @@ -239,7 +239,7 @@ void main() { test('assigns and gates', () async { final mod = SensitiveNaming(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('e = a & d')); expect(sv, contains('b = a')); expect(sv, contains('d = c')); @@ -248,7 +248,7 @@ void main() { test('bus subset', () async { final mod = BusSubsetNaming(Logic(width: 32)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('c = b[3]')); }); }); @@ -257,7 +257,7 @@ void main() { test('unconnected floating', () async { final mod = DrivenOutputModule(null); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // shouldn't add a Z in there if left floating expect(!sv.contains('z'), true); @@ -266,7 +266,7 @@ void main() { test('driven to z', () async { final mod = DrivenOutputModule(Const('z')); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // should add a Z if it's explicitly added expect(sv, contains('z')); @@ -279,7 +279,7 @@ void main() { portANaming: Naming.renameable, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -295,7 +295,7 @@ void main() { () async { final mod = NameCollisionArrayTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -312,7 +312,7 @@ void main() { await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('_wow_______')); }); @@ -321,7 +321,7 @@ void main() { final mod = StructElementNamingModule(VariousNamingStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign outp[0] = outp_renameable;')); expect(sv, contains('assign outp[1] = reserved_outp;')); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index fdc522e96..85c75468f 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -175,7 +175,7 @@ void main() { final mod = StructModuleWithInstrumentation(Const(0, width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('swizzle'), isFalse, reason: 'Should not pack from instrumentation!'); diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..041ce7ed6 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -91,7 +91,7 @@ void main() { final mod = AddWithCarryMod(Logic(width: 8), Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign {carry, sum} = a + b')); }); @@ -119,7 +119,7 @@ void main() { final gtm = MathTestModule(Logic(width: 8), Logic(width: 8)); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = SvService(gtm).synthOutput; final lines = sv.split('\n'); // ensure we never lshift by a constant directly diff --git a/test/module_merging_test.dart b/test/module_merging_test.dart index 5a5590ce9..bc14a4228 100644 --- a/test/module_merging_test.dart +++ b/test/module_merging_test.dart @@ -91,7 +91,7 @@ void main() async { () async { final dut = TrunkWithLeaves(Logic(), Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect('module ComplicatedLeaf'.allMatches(sv).length, 1); }); @@ -99,7 +99,7 @@ void main() async { test('different reserved definition name modules stay separate', () async { final dut = ParentOfDifferentModuleDefNames(Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect(sv, contains('module def1')); expect(sv, contains('module def2')); diff --git a/test/module_services_test.dart b/test/module_services_test.dart new file mode 100644 index 000000000..246263191 --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,201 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Unit tests for ModuleServices, the service base types, and SvService. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class SimpleModule extends Module { + SimpleModule(Logic a) : super(name: 'simple') { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// A minimal [ModuleService] used to exercise the type-keyed registry. +class FakeService implements ModuleService { + FakeService(this.module); + + @override + final Module module; + + @override + Map toJson() => {'kind': 'fake'}; +} + +void main() { + tearDown(ModuleServices.instance.reset); + + group('ModuleServices registry', () { + test('rootModule is set after build', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, equals(mod)); + }); + + test('hierarchyJSON returns valid JSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final json = ModuleServices.instance.hierarchyJSON; + expect(() => jsonDecode(json), returnsNormally); + }); + + test('register and lookup round-trips a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final fake = FakeService(mod); + ModuleServices.instance.register(fake); + expect(ModuleServices.instance.lookup(), same(fake)); + }); + + test('lookup returns null when no service registered', () { + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('unregister removes a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.register(FakeService(mod)); + ModuleServices.instance.unregister(); + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('reset clears rootModule and all services', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.register(FakeService(mod)); + expect(ModuleServices.instance.rootModule, isNotNull); + + ModuleServices.instance.reset(); + expect(ModuleServices.instance.rootModule, isNull); + expect(ModuleServices.instance.lookup(), isNull); + }); + }); + + group('SvService', () { + test('registers with ModuleServices and sets current', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(ModuleServices.instance.lookup(), same(sv)); + expect(SvService.current, same(sv)); + }); + + test('is a CodegenService', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(SvService(mod), isA()); + }); + + test('allContents is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.allContents, isNotEmpty); + }); + + test('output equals synthOutput', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.output, equals(sv.synthOutput)); + }); + + test('contentsByName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByName, isNotEmpty); + }); + + test('contentsByDefinitionName has entries', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.contentsByDefinitionName, isNotEmpty); + expect(sv.contentsByDefinitionName.containsKey('SimpleModule'), isTrue); + }); + + test('moduleOutput returns the definition contents', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.moduleOutput('SimpleModule'), isNotNull); + expect(sv.moduleOutput('DoesNotExist'), isNull); + }); + + test('toJson lists generated modules', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.toJson()['modules'], isList); + }); + + test('writeFiles creates SV files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + sv.writeFiles(dir.path); + final files = dir.listSync().whereType().toList(); + expect(files, isNotEmpty); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('write() emits a single file', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod, register: false); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + final path = '${dir.path}/out.sv'; + sv.write(path); + expect(File(path).readAsStringSync(), equals(sv.synthOutput)); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('write() with multiFile emits a directory of files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + // Construction with outputPath writes immediately. + SvService(mod, register: false, outputPath: dir.path, multiFile: true); + final files = dir.listSync().whereType().toList(); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('register false does not register', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.reset(); + SvService(mod, register: false); + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + expect(() => SvService(mod), throwsException); + }); + }); +} diff --git a/test/module_test.dart b/test/module_test.dart index 08502d6f8..ff6714e01 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -303,8 +303,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,(a ? 1'h0 : 1'h1)}")); @@ -320,8 +320,8 @@ void main() { disconnectOutputs: disconnectOutputs); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,a}")); @@ -336,7 +336,8 @@ void main() { TopStructInoutWrap(LogicNet(), LogicNet(), LogicNet(width: 2)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); expect( sv, @@ -352,7 +353,7 @@ void main() { expect( mod.internalSignals.firstWhereOrNull((e) => e.name == 't0'), isNotNull); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign a_concat[0] = t0;')); }); @@ -363,7 +364,7 @@ void main() { expect(mod.internalSignals.firstWhereOrNull((e) => e.name == 'unconnected'), isNotNull); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign a_arr[1] = unconnected;')); }); diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 52470dc8c..779ae13ed 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -54,7 +54,7 @@ void main() { .isNotEmpty, 'Should find a z two levels deep'); - final synth = ftm.generateSynth(); + final synth = SvService(ftm).synthOutput; // "z = 1" means it correctly traversed down from inputs assert(synth.contains('z = 1'), diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index b7642bb24..fb20a6106 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -35,7 +35,7 @@ void main() { final mod = TopModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('Passthrough')); }); diff --git a/test/name_test.dart b/test/name_test.dart index afa757cc8..c41573f92 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -194,20 +194,20 @@ void main() { test('respected with no conflicts', () async { final mod = SpeciallyNamedModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module specialName (')); }); test('uniquified with conflicts', () async { final mod = TopModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module specialName (')); expect(sv, contains('module specialName_0 (')); }); test('reserved throws exception with conflicts', () async { final mod = TopModule(Logic(), true, false); await mod.build(); - expect(mod.generateSynth, throwsException); + expect(() => SvService(mod).synthOutput, throwsException); }); }); @@ -215,7 +215,7 @@ void main() { test('uniquified with conflicts', () async { final mod = TopModule(Logic(), false, false); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('specialInstanceName(')); expect(sv, contains('specialInstanceName_0(')); diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index 2d5fccdc0..9bcd680a2 100644 --- a/test/net_bus_test.dart +++ b/test/net_bus_test.dart @@ -255,7 +255,8 @@ void main() { final mod = NicePortPassingTop(LogicNet(width: 8), LogicNet(width: 8)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(SvService(mod).synthOutput); expect(sv.contains('net_connect'), isFalse); expect(sv, @@ -314,7 +315,7 @@ void main() { final dut = DoubleNetPassthrough(LogicNet(width: 8), LogicNet(width: 8)); await dut.build(); - final sv = dut.generateSynth(); + final sv = SvService(dut).synthOutput; expect( sv, @@ -455,7 +456,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, contains( @@ -517,7 +518,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect( sv, @@ -590,7 +591,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); if (netTypeName == LogicNet) { expect( sv, @@ -620,7 +621,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); if (netTypeName == LogicNet) { expect( sv, @@ -750,8 +751,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect(sv, contains('net_connect (swizzled, ({in0[0],in1[0]}));')); }); @@ -764,8 +765,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -781,8 +782,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -799,8 +800,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -817,8 +818,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -835,8 +836,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect(sv, contains('assign _in1 = in0;')); expect( @@ -852,8 +853,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -942,7 +943,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); checkSV(sv); final vectors = [ @@ -962,7 +963,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + SvService(mod).synthOutput); checkSV(sv); final vectors = [ @@ -1204,8 +1205,8 @@ void main() { final mod = ReplicateMod(LogicNet(width: 4), 2); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, @@ -1225,8 +1226,8 @@ void main() { final mod = ReplicateMod(LogicNet(width: 4), 2); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); expect( sv, diff --git a/test/net_test.dart b/test/net_test.dart index c8d15b7d7..be2c51d1f 100644 --- a/test/net_test.dart +++ b/test/net_test.dart @@ -461,7 +461,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('intermediate1')); expect(sv, contains('intermediate2')); expect(sv, contains('intermediate3')); @@ -504,7 +504,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -515,7 +515,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -526,7 +526,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(' submod'.allMatches(sv).length, 2); }); }); @@ -611,7 +611,7 @@ void main() { isNotNull); } - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // test that " _b;" is not present (indication that a leftover internal // signal was there) @@ -631,7 +631,7 @@ void main() { final mod = NetArrayTopMod(Logic(width: 8), NetArrayIntf()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // print(sv); expect(sv, contains('wire [1:0][1:0][7:0] bd3')); }); @@ -677,7 +677,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign c = _a_and_b;')); expect(sv, contains('assign d = _aIntermediate_or_bIntermediate;')); diff --git a/test/pair_interface_hier_test.dart b/test/pair_interface_hier_test.dart index e665b7102..71c7cbc92 100644 --- a/test/pair_interface_hier_test.dart +++ b/test/pair_interface_hier_test.dart @@ -91,7 +91,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_hier_w_modify_test.dart b/test/pair_interface_hier_w_modify_test.dart index 47c65318c..fe6255c8c 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_w_modify_test.dart @@ -93,7 +93,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 0167335f3..536576d1b 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -192,7 +192,7 @@ void main() { await mod.build(); // Make sure the "modify" went through: - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic simple_clk')); }); diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 97c15f648..8e5f17177 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -176,7 +176,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' module Provider ( diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index d34c8e374..87b8495aa 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -146,7 +146,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' module Provider ( diff --git a/test/sequential_test.dart b/test/sequential_test.dart index ade256cf3..324777f0a 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -241,7 +241,7 @@ void main() { final mod = NegedgeTriggeredSeq(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('always_ff @(negedge')); final vectors = [ diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..dc9e68ddd 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -698,7 +698,7 @@ void main() { final mod = TieOffSubsetTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("assign banana_tieoff = 2'h0;")); expect(sv, contains("assign apple_tieoff = 2'h0;")); @@ -719,7 +719,7 @@ void main() { final mod = TieOffPortTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains("assign banana = 1'h0;")); expect(sv, contains(".apple(1'h0)")); @@ -751,7 +751,7 @@ void main() { test('input, output, and internal signals are sorted', () async { final mod = AlphabeticalModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -768,7 +768,7 @@ void main() { () async { final mod = AlphabeticalWidthsModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -794,7 +794,7 @@ void main() { final mod = AlphabeticalSubmodulePorts(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; checkPortConnectionOrder(sv, ['l', 'a', 'w', 'm', 'x', 'b']); }); @@ -803,7 +803,7 @@ void main() { final mod = TopWithExpressions(Logic(), Logic(width: 5)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('.a((a | (b[2])))')); }); @@ -812,7 +812,7 @@ void main() { final mod = ModuleWithFloatingSignals(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // only expect 1 assignment to xylophone expect('assign'.allMatches(sv).length, 1); @@ -826,8 +826,8 @@ void main() { final mod = TopCustomSvWrap(Logic(), Logic(), useOld: useOld, banExpressions: banExpressions); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + SvService(mod).synthOutput); if (banExpressions) { expect(sv, contains('assign my_fancy_new_signal <= ^fer_swizzle;')); @@ -846,7 +846,7 @@ void main() { final mod = ModuleWithCustomDefinitionEmptyPorts(Logic(), acceptsEmptyPortConnections: acceptsEmptyPortConnections); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; if (acceptsEmptyPortConnections) { expect(sv, contains('.b()')); @@ -861,7 +861,7 @@ void main() { test('custom definition', () async { final mod = TopWithCustomDef(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('module CustomDefinitionModule (')); expect(sv, contains('// this is a custom definition!')); @@ -879,7 +879,7 @@ void main() { final mod = ModWithUselessWireMods(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('swizzle'))); expect(sv, isNot(contains('replicate'))); @@ -897,7 +897,7 @@ endmodule : ModWithUselessWireMods''')); test('partial array assignment sv', () async { final mod = ModWithPartialArrayAssignment(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign b = aArr[0];')); expect(sv, contains('assign aArr[0] = a;')); @@ -1041,7 +1041,7 @@ endmodule : ModWithUselessWireMods''')); final mod = OutToInOutTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('assign myNet = myOut;')); @@ -1057,7 +1057,7 @@ endmodule : ModWithUselessWireMods''')); () async { final mod = _StructLeafNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('_in0')), reason: 'Struct leaf from unnamed Logic() should use its ' @@ -1067,7 +1067,7 @@ endmodule : ModWithUselessWireMods''')); test('const merge not blocked by constNameDisallowed', () async { final mod = _ConstNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; final constAssignments = RegExp(r"assign \w+ = 8'h0;").allMatches(sv).length; diff --git a/test/sv_param_passthrough_test.dart b/test/sv_param_passthrough_test.dart index e7b0876dd..041eb8ce5 100644 --- a/test/sv_param_passthrough_test.dart +++ b/test/sv_param_passthrough_test.dart @@ -162,7 +162,7 @@ void main() { () async { final mod = TopForEmptyParams(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv.contains('#'), isFalse); }); } diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index 6e1fc0949..691497eab 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -123,7 +123,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('/*')); expect(sv, contains('*/')); @@ -146,7 +146,7 @@ void main() { final mod = SingleElementSwizzle(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Single element should not have braces or bit range annotations // Look for bit range annotations specifically (/* number */) @@ -171,7 +171,7 @@ void main() { final mod = AllSingleBitSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have bit range annotations for single bits expect(sv, contains('/*')); @@ -202,7 +202,7 @@ void main() { final mod = NestedSwizzle(Logic(width: 4), Logic(width: 3)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should contain annotations for both inner and outer swizzles expect(sv, contains('/*')); @@ -222,7 +222,7 @@ void main() { final mod = InlinedSwizzle(Logic(width: 4), Logic(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have annotations even when swizzle is part of larger expression expect(sv, contains('/*')); @@ -242,7 +242,7 @@ void main() { final mod = VariedWidthSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // Should have aligned bit range annotations expect(sv, contains('/*')); @@ -280,7 +280,7 @@ void main() { // Create a module with indices requiring different digit widths final largeModule = LargeWidthSwizzle(); await largeModule.build(); - final sv = largeModule.generateSynth(); + final sv = SvService(largeModule).synthOutput; // Should have properly aligned annotations despite different digit counts expect(sv, contains('/*')); @@ -323,7 +323,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains(''' assign b = { diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..8f27bd0fb 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -229,7 +229,7 @@ void main() { final mod = SimpleStructModuleContainer(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('internal_struct'))); @@ -251,7 +251,7 @@ void main() { expect(mod.anyOut, isA()); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, contains('input logic [3:0][1:0] anyIn')); expect(sv, contains('output logic [3:0][1:0] anyOut')); @@ -277,7 +277,7 @@ void main() { final mod = ParentModuleWithStructsContainingPorts(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // if naming is wrong, these names will appear in the SV in ports expect( @@ -357,7 +357,7 @@ void main() { SimpleStructModuleContainer(LogicNet(), LogicNet(), asNet: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; expect(sv, isNot(contains('internal_struct'))); @@ -502,7 +502,7 @@ void main() { final mod = ModuleWithOneBitStructPort(OneBitStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = SvService(mod).synthOutput; // no slicing on single-bit signals expect(sv, contains('assign outStruct = outStruct_oneBit')); diff --git a/test/wave_dumper_test.dart b/test/wave_dumper_test.dart index 07aafc8c8..a94d06620 100644 --- a/test/wave_dumper_test.dart +++ b/test/wave_dumper_test.dart @@ -40,11 +40,11 @@ const tempDumpDir = 'tmp_test'; /// Gets the path of the VCD file based on a name. String temporaryDumpPath(String name) => '$tempDumpDir/temp_dump_$name.vcd'; -/// Attaches a [WaveDumper] to [module] to VCD with [name]. +/// Attaches a [WaveformService] to [module] to VCD with [name]. void createTemporaryDump(Module module, String name) { Directory(tempDumpDir).createSync(recursive: true); final tmpDumpFile = temporaryDumpPath(name); - WaveDumper(module, outputPath: tmpDumpFile); + WaveformService(module, outputPath: tmpDumpFile); } /// Deletes the temporary VCD file associated with [name]. @@ -241,7 +241,8 @@ void main() { const dir1Path = '$tempDumpDir/dir1'; - final waveDumper = WaveDumper(mod, outputPath: '$dir1Path/dir2/waves.vcd'); + final waveDumper = + WaveformService(mod, outputPath: '$dir1Path/dir2/waves.vcd'); expect(File(waveDumper.outputPath).existsSync(), equals(true));