From e06b6bb73966d3ff82c54cf49e33254241bfcbf8 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 6 May 2026 12:32:17 -0700 Subject: [PATCH 1/2] Add ModuleServices singleton and SvService Introduces a singleton service registry (ModuleServices) that provides a unified query surface for DevTools and inspection tools. Module.build() now registers the root module with ModuleServices.instance. Also adds SvService which wraps SystemVerilog synthesis and registers with ModuleServices for DevTools access to SV metadata. This is a clean separation: no netlist code is included. The netlist branch will later extend ModuleServices with a netlistService field. --- lib/rohd.dart | 1 + lib/src/diagnostics/module_services.dart | 78 +++++++++++ lib/src/module.dart | 3 +- .../systemverilog/sv_service.dart | 114 +++++++++++++++ .../systemverilog/systemverilog.dart | 1 + test/module_services_test.dart | 132 ++++++++++++++++++ 6 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 lib/src/diagnostics/module_services.dart create mode 100644 lib/src/synthesizers/systemverilog/sv_service.dart create mode 100644 test/module_services_test.dart diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..d0ea2a266 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,7 @@ // Copyright (C) 2021-2023 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/module_services.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..a0e583b59 --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,78 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services.dart +// Singleton service registry for DevTools integration. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; + +/// Singleton service registry that provides a unified query surface for +/// DevTools and other inspection tools. +/// +/// Services register themselves here on construction; DevTools evaluates +/// getters on [instance] via `EvalOnDartLibrary` to pull data. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJSON] — set by [Module.build]. +/// +/// **Opt-in (registered by service constructors):** +/// - [svService] — SystemVerilog synthesis results. +/// +/// Additional services (netlist, trace, waveform) can be added by setting +/// the corresponding field after construction. +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. + String get hierarchyJSON { + ModuleTree.rootModuleInstance = rootModule; + return ModuleTree.instance.hierarchyJSON; + } + + /// Returns the primary inspector JSON for DevTools. + /// + /// Returns the hierarchy JSON. Downstream branches (e.g. netlist) may + /// override this to return richer data when available. + String get inspectorJSON => hierarchyJSON; + + // ─── SystemVerilog service (opt-in) ─────────────────────────── + + /// The active [SvService], if one has been registered. + SvService? svService; + + /// Returns SV synthesis metadata as JSON, or an unavailable status. + String get svJSON => + svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); + + // ─── Helpers ────────────────────────────────────────────────── + + static String _unavailable(String service) => jsonEncode({ + 'status': 'unavailable', + 'reason': '$service service not registered', + }); + + /// Resets all services. Intended for test teardown. + void reset() { + rootModule = null; + svService = null; + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 92fc410e0..a44e6b695 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -14,7 +14,6 @@ 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/sanitizer.dart'; import 'package:rohd/src/utilities/timestamper.dart'; @@ -317,7 +316,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart new file mode 100644 index 000000000..e1adf43cd --- /dev/null +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -0,0 +1,114 @@ +// 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'; + +/// 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 { + /// The top-level [Module] being synthesized. + final Module module; + + /// 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. Set [register] to `true` (the + /// default) to register this service with [ModuleServices] for + /// DevTools access. + SvService(this.module, {bool register = true}) { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating SvService. ' + 'Call build() first.'); + } + + synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); + fileContents = synthBuilder.getSynthFileContents(); + + if (register) { + ModuleServices.instance.svService = this; + } + } + + /// All [SynthesisResult]s produced by synthesis. + Set get synthesisResults => synthBuilder.synthesisResults; + + /// Returns the concatenated SystemVerilog output as a single string, + /// matching the format of [Module.generateSynth]. + String get allContents => fileContents.map((fc) => fc.contents).join('\n\n'); + + /// 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. + 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); + } + } + + /// Returns a JSON-serialisable summary of the SV synthesis. + /// + /// Contains the list of generated module definition names. + 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/test/module_services_test.dart b/test/module_services_test.dart new file mode 100644 index 000000000..f8994577e --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,132 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Unit tests for ModuleServices and SvService. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +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; + } +} + +void main() { + tearDown(ModuleServices.instance.reset); + + group('ModuleServices', () { + 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('inspectorJSON matches hierarchyJSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.inspectorJSON, + equals(ModuleServices.instance.hierarchyJSON)); + }); + + test('svJSON returns unavailable when no service registered', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final result = + jsonDecode(ModuleServices.instance.svJSON) as Map; + expect(result['status'], equals('unavailable')); + }); + + test('reset clears all services', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, isNotNull); + ModuleServices.instance.reset(); + expect(ModuleServices.instance.rootModule, isNull); + expect(ModuleServices.instance.svService, isNull); + }); + }); + + group('SvService', () { + test('registers with ModuleServices on creation', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(ModuleServices.instance.svService, equals(sv)); + }); + + test('allContents is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SvService(mod); + expect(sv.allContents, isNotEmpty); + }); + + 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('svJSON returns valid JSON after registration', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + SvService(mod); + final result = + jsonDecode(ModuleServices.instance.svJSON) as Map; + expect(result['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('register false does not register', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.reset(); + SvService(mod, register: false); + expect(ModuleServices.instance.svService, isNull); + }); + + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + expect(() => SvService(mod), throwsException); + }); + }); +} From 9ca2a8f939eb5776a454b1ee6b2abb08b90181d4 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 25 Jun 2026 11:42:00 -0700 Subject: [PATCH 2/2] Slim ModuleServices API branch --- lib/rohd.dart | 3 + lib/src/diagnostics/module_service.dart | 72 +++ lib/src/diagnostics/module_services.dart | 61 ++- lib/src/diagnostics/waveform_service.dart | 435 ++++++++++++++++++ lib/src/module.dart | 19 +- .../systemverilog/sv_service.dart | 108 ++++- test/module_services_test.dart | 107 ++++- 7 files changed, 725 insertions(+), 80 deletions(-) create mode 100644 lib/src/diagnostics/module_service.dart create mode 100644 lib/src/diagnostics/waveform_service.dart diff --git a/lib/rohd.dart b/lib/rohd.dart index d0ea2a266..a8a6082fd 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,7 +1,9 @@ // 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'; @@ -13,6 +15,7 @@ export 'src/signals/signals.dart'; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; +export 'src/synthesizers/systemverilog/sv_service.dart'; export 'src/utilities/naming.dart'; export 'src/values/values.dart'; export 'src/wave_dumper.dart'; diff --git a/lib/src/diagnostics/module_service.dart b/lib/src/diagnostics/module_service.dart new file mode 100644 index 000000000..6e72e1819 --- /dev/null +++ b/lib/src/diagnostics/module_service.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_service.dart +// Common base types shared by all module-scoped services. +// +// 2026 June 23 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// The common contract implemented by every module-scoped service that +/// registers with [ModuleServices]. +/// +/// A service wraps some derived view of a built [Module] (synthesis output, +/// netlist, source trace, waveform, etc.) and exposes a JSON-serialisable +/// summary via [toJson]. Concrete services additionally expose their own +/// format-specific accessors; consumers reach them through +/// [ModuleServices.lookup] or the service's own `current` accessor rather than +/// through getters on the registry. +abstract interface class ModuleService { + /// The top-level [Module] this service operates on. + Module get module; + + /// A JSON-serialisable summary of this service. + Map toJson(); +} + +/// A [ModuleService] that emits output to one or more files. +/// +/// Establishes the common output convention shared by synthesis, netlist, +/// trace, and waveform services: +/// - [outputPath] — the default file or directory written by [write]. +/// - [multiFile] — whether [write] emits one file per module definition +/// (a directory) or a single combined file. +/// - [write] — performs the write, honouring [multiFile]. +abstract class OutputService implements ModuleService { + /// The default location written by [write]. + /// + /// Interpreted as a directory when [multiFile] is `true`, otherwise as a + /// single file path. May be `null` when no default has been configured, in + /// which case a path must be passed to [write]. + String? get outputPath; + + /// Whether [write] emits one file per module definition (`true`) or a single + /// combined file (`false`). + bool get multiFile; + + /// Writes this service's output to [path], or to [outputPath] when [path] is + /// omitted. + void write([String? path]); +} + +/// An [OutputService] that generates source-code text, keyed per module +/// definition. +/// +/// Shared by the language code-generation services (e.g. SystemVerilog and +/// SystemC), which all produce a combined single-file [output] as well as +/// per-definition contents. +abstract class CodegenService extends OutputService { + /// The combined single-file generated output (including any header). + String get output; + + /// The generated output keyed by module definition name + /// ([Module.definitionName]). + Map get contentsByDefinitionName; + + /// The generated output for a single module [definitionName], or `null` when + /// that definition was not generated. + String? moduleOutput(String definitionName) => + contentsByDefinitionName[definitionName]; +} diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart index a0e583b59..98f2341f7 100644 --- a/lib/src/diagnostics/module_services.dart +++ b/lib/src/diagnostics/module_services.dart @@ -2,30 +2,27 @@ // SPDX-License-Identifier: BSD-3-Clause // // module_services.dart -// Singleton service registry for DevTools integration. +// Slim, type-keyed registry of module-scoped services for DevTools and other +// inspection tools. // // 2026 April 25 // Author: Desmond Kirkpatrick -import 'dart:convert'; - import 'package:rohd/rohd.dart'; import 'package:rohd/src/diagnostics/inspector_service.dart'; -/// Singleton service registry that provides a unified query surface for -/// DevTools and other inspection tools. +/// 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. /// -/// Services register themselves here on construction; DevTools evaluates -/// getters on [instance] via `EvalOnDartLibrary` to pull data. +/// 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]. -/// -/// **Opt-in (registered by service constructors):** -/// - [svService] — SystemVerilog synthesis results. -/// -/// Additional services (netlist, trace, waveform) can be added by setting -/// the corresponding field after construction. class ModuleServices { ModuleServices._(); @@ -41,38 +38,36 @@ class ModuleServices { /// Returns the module hierarchy as a JSON string. /// - /// DevTools evaluates this via `EvalOnDartLibrary` to display - /// the module hierarchy. + /// 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; } - /// Returns the primary inspector JSON for DevTools. - /// - /// Returns the hierarchy JSON. Downstream branches (e.g. netlist) may - /// override this to return richer data when available. - String get inspectorJSON => hierarchyJSON; + // ─── Type-keyed service registry ────────────────────────────── - // ─── SystemVerilog service (opt-in) ─────────────────────────── + final Map _services = {}; - /// The active [SvService], if one has been registered. - SvService? svService; - - /// Returns SV synthesis metadata as JSON, or an unavailable status. - String get svJSON => - svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); + /// Registers [service] under the type argument [T]. + /// + /// Replaces any previously registered service of the same type. + void register(T service) { + _services[T] = service; + } - // ─── Helpers ────────────────────────────────────────────────── + /// Returns the registered service of type [T], or `null` if none. + T? lookup() => _services[T] as T?; - static String _unavailable(String service) => jsonEncode({ - 'status': 'unavailable', - 'reason': '$service service not registered', - }); + /// 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; - svService = null; + _services.clear(); } } diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart new file mode 100644 index 000000000..25e47f19c --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,435 @@ +// 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/module.dart b/lib/src/module.dart index a44e6b695..1fdecf9ba 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -14,9 +14,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.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'; /// Represents a synthesizable hardware entity with clearly defined interface @@ -1112,23 +1110,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 index e1adf43cd..97cf0c45e 100644 --- a/lib/src/synthesizers/systemverilog/sv_service.dart +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -11,6 +11,8 @@ 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. /// @@ -30,10 +32,31 @@ import 'package:rohd/rohd.dart'; /// // Or get the concatenated output (like generateSynth): /// print(sv.allContents); /// ``` -class SvService { +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; @@ -42,29 +65,64 @@ class SvService { /// Creates an [SvService] for [module]. /// - /// [module] must already be built. Set [register] to `true` (the - /// default) to register this service with [ModuleServices] for - /// DevTools access. - SvService(this.module, {bool register = true}) { + /// [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.'); + 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) { - ModuleServices.instance.svService = this; + current = this; + ModuleServices.instance.register(this); } } /// All [SynthesisResult]s produced by synthesis. Set get synthesisResults => synthBuilder.synthesisResults; - /// Returns the concatenated SystemVerilog output as a single string, - /// matching the format of [Module.generateSynth]. - String get allContents => fileContents.map((fc) => fc.contents).join('\n\n'); + /// 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. /// @@ -79,6 +137,7 @@ class SvService { /// /// 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) { @@ -103,12 +162,33 @@ class SvService { } } + /// 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, - ], + 'modules': [for (final fc in fileContents) fc.name], }; } diff --git a/test/module_services_test.dart b/test/module_services_test.dart index f8994577e..246263191 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -2,11 +2,14 @@ // SPDX-License-Identifier: BSD-3-Clause // // module_services_test.dart -// Unit tests for ModuleServices and SvService. +// 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'; @@ -20,10 +23,21 @@ class SimpleModule extends Module { } } +/// 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', () { + group('ModuleServices registry', () { test('rootModule is set after build', () async { final mod = SimpleModule(Logic()); await mod.build(); @@ -37,37 +51,51 @@ void main() { expect(() => jsonDecode(json), returnsNormally); }); - test('inspectorJSON matches hierarchyJSON', () async { + test('register and lookup round-trips a service', () async { final mod = SimpleModule(Logic()); await mod.build(); - expect(ModuleServices.instance.inspectorJSON, - equals(ModuleServices.instance.hierarchyJSON)); + final fake = FakeService(mod); + ModuleServices.instance.register(fake); + expect(ModuleServices.instance.lookup(), same(fake)); }); - test('svJSON returns unavailable when no service registered', () async { + 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(); - final result = - jsonDecode(ModuleServices.instance.svJSON) as Map; - expect(result['status'], equals('unavailable')); + ModuleServices.instance.register(FakeService(mod)); + ModuleServices.instance.unregister(); + expect(ModuleServices.instance.lookup(), isNull); }); - test('reset clears all services', () async { + 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.svService, isNull); + expect(ModuleServices.instance.lookup(), isNull); }); }); group('SvService', () { - test('registers with ModuleServices on creation', () async { + test('registers with ModuleServices and sets current', () async { final mod = SimpleModule(Logic()); await mod.build(); final sv = SvService(mod); - expect(ModuleServices.instance.svService, equals(sv)); + 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 { @@ -77,6 +105,13 @@ void main() { 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(); @@ -92,13 +127,19 @@ void main() { expect(sv.contentsByDefinitionName.containsKey('SimpleModule'), isTrue); }); - test('svJSON returns valid JSON after registration', () async { + test('moduleOutput returns the definition contents', () async { final mod = SimpleModule(Logic()); await mod.build(); - SvService(mod); - final result = - jsonDecode(ModuleServices.instance.svJSON) as Map; - expect(result['modules'], isList); + 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 { @@ -116,12 +157,40 @@ void main() { } }); + 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.svService, isNull); + expect(ModuleServices.instance.lookup(), isNull); }); test('throws if module not built', () {