diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..a8a6082fd 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +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'; @@ -12,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 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/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 92fc410e0..1fdecf9ba 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -14,10 +14,7 @@ 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'; import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a synthesizable hardware entity with clearly defined interface @@ -317,7 +314,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. @@ -1113,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 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/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); + }); + }); +}