From b6eefaf3a0f71e04bd7690e9361643c90c2fdf57 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 16:20:06 -0700 Subject: [PATCH 01/36] migrated rohd_source_navigator to packages, prepped for publication --- CHANGELOG.md | 5 + analysis_options.yaml | 1 + doc/releases.md | 36 ++++++-- packages/rohd_devtools_widgets/pubspec.yaml | 2 +- packages/rohd_source_navigator/CHANGELOG.md | 9 ++ .../rohd_source_navigator}/LICENSE | 0 packages/rohd_source_navigator/README.md | 91 +++++++++++++++++++ .../lib/dtd_service.dart | 0 .../rohd_source_navigator}/lib/flc_data.dart | 0 .../lib/rohd_source_navigator.dart | 0 .../lib/source_navigator.dart | 0 .../rohd_source_navigator}/pubspec.yaml | 4 +- .../test/flc_data_test.dart | 0 rohd-multipackage.code-workspace | 4 + rohd_extension/dart/README.md | 15 --- test/prepare_release_metadata_test.dart | 22 +++++ tool/check_release.sh | 13 +-- tool/prepare_release.sh | 21 +++-- tool/prepare_release_metadata.dart | 1 + tool/test/check_release_test.sh | 24 +++-- tool/test/prepare_release_test.sh | 39 +++++--- 21 files changed, 228 insertions(+), 59 deletions(-) create mode 100644 packages/rohd_source_navigator/CHANGELOG.md rename {rohd_extension/dart => packages/rohd_source_navigator}/LICENSE (100%) create mode 100644 packages/rohd_source_navigator/README.md rename {rohd_extension/dart => packages/rohd_source_navigator}/lib/dtd_service.dart (100%) rename {rohd_extension/dart => packages/rohd_source_navigator}/lib/flc_data.dart (100%) rename {rohd_extension/dart => packages/rohd_source_navigator}/lib/rohd_source_navigator.dart (100%) rename {rohd_extension/dart => packages/rohd_source_navigator}/lib/source_navigator.dart (100%) rename {rohd_extension/dart => packages/rohd_source_navigator}/pubspec.yaml (64%) rename {rohd_extension/dart => packages/rohd_source_navigator}/test/flc_data_test.dart (100%) delete mode 100644 rohd_extension/dart/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dabd8bfb..5e77c5cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ +## Next Release + +- Exposed the shared `rohd_source_navigator` code used by `rohd-schematic-viewer` as a standalone pub.dev package. +- Corrected the ROHD dependency in `rohd_devtools_widgets` from `^0.6.9` to `^0.6.11` to reflect the minimum compatible ROHD version. + ## 0.6.11 - Added `NetlistSynthesizer` for generating JSON netlists, with configurable synthesis passes, validation, and hierarchy support (). diff --git a/analysis_options.yaml b/analysis_options.yaml index 63d394a99..54df7e247 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,6 +10,7 @@ analyzer: - doc/tutorials/chapter_9/rohd_vf_example - packages/rohd_devtools_widgets - packages/rohd_hierarchy + - packages/rohd_source_navigator - packages/rohd_waveform - reviews/** - rohd_devtools_extension diff --git a/doc/releases.md b/doc/releases.md index dedb27cd9..abf3d5634 100644 --- a/doc/releases.md +++ b/doc/releases.md @@ -18,13 +18,15 @@ released, not a repository-wide version. | [rohd_hierarchy](../packages/rohd_hierarchy) | Shared hierarchy models, addressing, adapters, and search. | Independent pub.dev package; `dart pub publish` from its directory. | Its own `pubspec.yaml`. | Tag `rohd_hierarchy-v`; release **rohd_hierarchy v**. | | [rohd_waveform](../packages/rohd_waveform) | Shared waveform models and data services. | Independent pub.dev package; `dart pub publish` from its directory. | Its own `pubspec.yaml`. | Tag `rohd_waveform-v`; release **rohd_waveform v**. | | [rohd_devtools_widgets](../packages/rohd_devtools_widgets) | Reusable Flutter controls and utilities for debug viewers. | Independent pub.dev package; `flutter pub publish` from its directory. | Its own `pubspec.yaml`. | Tag `rohd_devtools_widgets-v`; release **rohd_devtools_widgets v**. | +| [rohd_source_navigator](../packages/rohd_source_navigator) | Shared FLC data models and source-navigation utilities for debug viewers. | Independent pub.dev package; `dart pub publish` from its directory. | Its own `pubspec.yaml`. | Tag `rohd_source_navigator-v`; release **rohd_source_navigator v**. | | [ROHD DevTools application](../rohd_devtools_extension) | Interactive hardware debug UI embedded in Dart DevTools. | Flutter web build produced by CI and bundled under `extension/devtools` in the ROHD pub.dev package; the application itself is not published to pub.dev. | Tracked by the containing ROHD release and the build's source/artifact commits, not an independent application release version. | Included in ROHD's tag and release notes; no separate release tag. The `artifacts` branch holds CI output, not an immutable release. | | [ROHD VS Code extension](../rohd_extension) | Editor snippets, completions, and cross-probe source navigation. | GitHub Actions builds and attaches a VSIX when its GitHub release is published; a maintainer tests and publishes that VSIX to the VS Code Marketplace separately. | `rohd_extension/package.json`. | Tag `rohd-vscode-v`; release `ROHD VS Code v`, with `rohd-vscode-v.vsix` attached automatically. | -The internal Dart source-navigation port in `rohd_extension/dart` uses -`publish_to: none` and has no independent release or tag. The VS Code extension -compiles its TypeScript implementation, not the Dart port. Tutorial applications -are also not independently released packages. +The source-navigation library lives in `packages/rohd_source_navigator`, alongside +the other independently published libraries. Git dependencies following the move +must use the new package path; older pinned commits retain `rohd_extension/dart`. +The VS Code extension still compiles its separate TypeScript implementation. +Tutorial applications are not independently released packages. ## GitHub Strategy @@ -91,13 +93,15 @@ with a merge or rebase; the preparation PR itself does not need to be merged yet `Check rohd_devtools_widgets` jobs for dependency resolution, formatting, fatal-info analysis, and package tests. These run alongside the root checks and DevTools app job; documentation deployment waits for all of them. + Source navigator is not included in that matrix; run its local checks with + `tool/prepare_release.sh --run-tests rohd_source_navigator`. Preparation skips local test suites by default and does not query GitHub or verify CI status. Use `--run-tests` to repeat selected package suites locally. ROHD's local tests require Icarus Verilog; Verilator is required in CI and when `ROHD_REQUIRE_VERILATOR=1`. 5. Run `tool/prepare_release.sh [--run-tests] [package ...]` on the preparation - branch from the repository root. No package names selects all four; explicit + branch from the repository root. No package names selects all five; explicit names select only those packages. Versions come from their manifests, not command-line arguments. It validates selected metadata before making changes, fetches `main` from `intel/rohd`, and requires that commit to be an ancestor @@ -115,9 +119,10 @@ with a merge or rebase; the preparation PR itself does not need to be merged yet commit, tag, push, or upload anything. If a guard fails, incorporate the latest `main` or wait for its artifact workflow as appropriate, then rerun preparation. 6. To inspect archives without preparing metadata, run - `tool/check_release.sh [package ...]`. Like preparation, it defaults to all four + `tool/check_release.sh [package ...]`. Like preparation, it defaults to all five packages and accepts explicit names: `rohd`, `rohd_hierarchy`, `rohd_waveform`, - and `rohd_devtools_widgets`. Each uses its existing version. The helper runs + `rohd_devtools_widgets`, and `rohd_source_navigator`. Each uses its existing + version. The helper runs `dart pub publish --dry-run` in each selected Dart package directory or `flutter pub publish --dry-run` for widgets. Review warnings, included files, and compressed archive sizes. For ROHD, first prepare @@ -130,7 +135,7 @@ Preparation runs the following checks before invoking `tool/check_release.sh`: | Selected Package | Default Checks | Added With `--run-tests` | | --- | --- | --- | | `rohd` | `tool/run_checks.sh --skip-tests`: dependencies, formatting, analysis, API docs, and temporary-file checks; then `tool/package_vscode.sh` compiles and packages a temporary VSIX. | Simulator prerequisites and ROHD tests via `tool/run_checks.sh`. | -| `rohd_hierarchy`, `rohd_waveform` | In each package directory: `dart pub get`, `dart format --output=none --set-exit-if-changed .`, then `dart analyze --fatal-infos`. | `dart test` in each selected package. | +| `rohd_hierarchy`, `rohd_waveform`, `rohd_source_navigator` | In each package directory: `dart pub get`, `dart format --output=none --set-exit-if-changed .`, then `dart analyze --fatal-infos`. | `dart test` in each selected package. | | `rohd_devtools_widgets` | In its package directory: `flutter pub get`, `dart format --output=none --set-exit-if-changed .`, then `flutter analyze --fatal-infos`. | `flutter test` in the widgets package. | Artifact provenance verification and the DevTools installation smoke test always @@ -155,10 +160,10 @@ separate DevTools application remain a separate step when its shared widgets cha Preparation examples (choose one): ```sh -# All four packages, using each manifest's version; rely on CI for test suites: +# All five packages, using each manifest's version; rely on CI for test suites: tool/prepare_release.sh -# All four packages, also running their test suites locally: +# All five packages, also running their test suites locally: tool/prepare_release.sh --run-tests # ROHD only: @@ -172,6 +177,9 @@ tool/prepare_release.sh rohd rohd_hierarchy rohd_waveform # Only hierarchy and waveform: tool/prepare_release.sh rohd_hierarchy rohd_waveform + +# Only source navigator: +tool/prepare_release.sh rohd_source_navigator ``` Preparation requires Dart for YAML metadata parsing using the root package's @@ -195,6 +203,7 @@ the selected dry runs: tool/check_release.sh --validate-only tool/check_release.sh --validate-only rohd_hierarchy rohd_waveform rohd_devtools_widgets tool/check_release.sh rohd_hierarchy rohd_waveform rohd_devtools_widgets +tool/check_release.sh rohd_source_navigator ``` `--validate-only` checks package names, manifest presence, and SDK availability. @@ -237,6 +246,13 @@ dependencies determine ordering only when the required versions have not yet been published. Sharing a repository or source commit does not require a coordinated release. +`rohd_source_navigator` has only hosted Dart dependencies and can be published +independently of ROHD and the Flutter packages. Once its version is available on +pub.dev, consumers such as the ROHD Schematic Viewer can replace their Git +dependency with `rohd_source_navigator: ^0.1.0` and remove any Git override for +that package. Existing Dart imports remain unchanged. See the +[package README](../packages/rohd_source_navigator/README.md) for migration details. + For each package, wait until its required dependencies are available on pub.dev, then validate from a clean disposable checkout of the release commit without local dependency overrides or reused path-based lockfiles. Remove that checkout's diff --git a/packages/rohd_devtools_widgets/pubspec.yaml b/packages/rohd_devtools_widgets/pubspec.yaml index 952b02313..1da9d6820 100644 --- a/packages/rohd_devtools_widgets/pubspec.yaml +++ b/packages/rohd_devtools_widgets/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: cupertino_ui: '>=1.0.0 <1.1.0' flutter: {sdk: flutter} material_ui: '>=1.1.0 <1.3.0' - rohd: ^0.6.9 + rohd: ^0.6.11 rohd_hierarchy: ^0.1.0 web: ^1.0.0 dev_dependencies: diff --git a/packages/rohd_source_navigator/CHANGELOG.md b/packages/rohd_source_navigator/CHANGELOG.md new file mode 100644 index 000000000..8a1fd5006 --- /dev/null +++ b/packages/rohd_source_navigator/CHANGELOG.md @@ -0,0 +1,9 @@ +## 0.1.0 + +- Prepare the existing ROHD source-navigation library for independent pub.dev + distribution, including FLC parsing, source frames, path utilities, frame + cycling, and DTD integration. +- Preserve the public imports and implementation previously consumed through + a Git dependency. +- Move the package to `packages/rohd_source_navigator`, alongside the other + independently published ROHD libraries. diff --git a/rohd_extension/dart/LICENSE b/packages/rohd_source_navigator/LICENSE similarity index 100% rename from rohd_extension/dart/LICENSE rename to packages/rohd_source_navigator/LICENSE diff --git a/packages/rohd_source_navigator/README.md b/packages/rohd_source_navigator/README.md new file mode 100644 index 000000000..d0675064a --- /dev/null +++ b/packages/rohd_source_navigator/README.md @@ -0,0 +1,91 @@ +# rohd_source_navigator + +Shared Dart source-navigation models and utilities for ROHD debug tools, +including the [ROHD Schematic Viewer](https://github.com/intel/rohd-schematic-viewer). + +## Installation + +```sh +dart pub add rohd_source_navigator +``` + +For Flutter applications, use `flutter pub add rohd_source_navigator`. + +## API + +- `FlcData`, `FlcEntry`, and `FlcFrame`: parse and look up file/line/column + traces from v5/v6 FLC hierarchy JSON or embedded netlist trace attributes. +- `SourceFrame` and `FrameCycler`: represent source locations and cycle through + a selection's frames. +- `normalizePath` and `resolveCandidatePaths`: prepare source paths for a + host application's file resolution. +- `DtdService` and request encoding helpers: source-navigation communication + for Dart Tooling Daemon integrations. + +Import the complete API: + +```dart +import 'package:rohd_source_navigator/rohd_source_navigator.dart'; +``` + +The existing focused imports remain supported: + +```dart +import 'package:rohd_source_navigator/flc_data.dart'; +import 'package:rohd_source_navigator/source_navigator.dart'; +import 'package:rohd_source_navigator/dtd_service.dart'; +``` + +For example, look up a signal's source location: + +```dart +import 'package:rohd_source_navigator/flc_data.dart'; + +void main() { + final data = FlcData.fromJson({ + 'version': 5, + 'files': ['lib/top.dart'], + 'modules': { + 'Top': { + 'tree': [ + ['0:42:5', 'result'], + ], + }, + }, + }); + + final frames = data.lookupSignal('Top', 'result'); + print(frames?.first); // lib/top.dart:42:5 [rohd] +} +``` + +This is a Dart library, not an editor extension. The +[ROHD VS Code extension](https://github.com/intel/rohd/tree/main/rohd_extension) +continues to use its separate TypeScript implementation; installing this +package does not install or activate that extension. + +## Migrating from a Git dependency + +Once version 0.1.0 is available on pub.dev, replace the Git dependency with: + +```yaml +dependencies: + rohd_source_navigator: ^0.1.0 +``` + +Also remove any Git or path entry for `rohd_source_navigator` from +`dependency_overrides` or `pubspec_overrides.yaml`; otherwise Pub will continue +using that override instead of the hosted package. Run `dart pub get` (or +`flutter pub get`) to update the lockfile. Existing Dart imports do not change. + +If continuing to use a Git dependency at a commit containing the package move, +change its `path` from `rohd_extension/dart` to +`packages/rohd_source_navigator`. Dependencies pinned to older commits or tags +keep their original path. + +## Development + +The package lives in `packages/rohd_source_navigator` in the ROHD repository. Run +`dart pub get`, `dart analyze`, and `dart test` from that directory. +Use `dart pub publish --dry-run` there to inspect the publication archive, or +`tool/check_release.sh rohd_source_navigator` from the repository root. diff --git a/rohd_extension/dart/lib/dtd_service.dart b/packages/rohd_source_navigator/lib/dtd_service.dart similarity index 100% rename from rohd_extension/dart/lib/dtd_service.dart rename to packages/rohd_source_navigator/lib/dtd_service.dart diff --git a/rohd_extension/dart/lib/flc_data.dart b/packages/rohd_source_navigator/lib/flc_data.dart similarity index 100% rename from rohd_extension/dart/lib/flc_data.dart rename to packages/rohd_source_navigator/lib/flc_data.dart diff --git a/rohd_extension/dart/lib/rohd_source_navigator.dart b/packages/rohd_source_navigator/lib/rohd_source_navigator.dart similarity index 100% rename from rohd_extension/dart/lib/rohd_source_navigator.dart rename to packages/rohd_source_navigator/lib/rohd_source_navigator.dart diff --git a/rohd_extension/dart/lib/source_navigator.dart b/packages/rohd_source_navigator/lib/source_navigator.dart similarity index 100% rename from rohd_extension/dart/lib/source_navigator.dart rename to packages/rohd_source_navigator/lib/source_navigator.dart diff --git a/rohd_extension/dart/pubspec.yaml b/packages/rohd_source_navigator/pubspec.yaml similarity index 64% rename from rohd_extension/dart/pubspec.yaml rename to packages/rohd_source_navigator/pubspec.yaml index a91365ac2..de739c070 100644 --- a/rohd_extension/dart/pubspec.yaml +++ b/packages/rohd_source_navigator/pubspec.yaml @@ -3,7 +3,9 @@ description: > Dart implementation of the ROHD source navigator — path normalisation, frame cycling, and DTD service for cross-probe source navigation. version: 0.1.0 -publish_to: none +homepage: https://intel.github.io/rohd-website/ +repository: https://github.com/intel/rohd/tree/main/packages/rohd_source_navigator +issue_tracker: https://github.com/intel/rohd/issues environment: sdk: ^3.0.0 diff --git a/rohd_extension/dart/test/flc_data_test.dart b/packages/rohd_source_navigator/test/flc_data_test.dart similarity index 100% rename from rohd_extension/dart/test/flc_data_test.dart rename to packages/rohd_source_navigator/test/flc_data_test.dart diff --git a/rohd-multipackage.code-workspace b/rohd-multipackage.code-workspace index a1df03666..92a31c952 100644 --- a/rohd-multipackage.code-workspace +++ b/rohd-multipackage.code-workspace @@ -15,6 +15,10 @@ "name": "rohd_waveform", "path": "packages/rohd_waveform" }, + { + "name": "rohd_source_navigator", + "path": "packages/rohd_source_navigator" + }, { "name": "rohd_devtools_extension", "path": "rohd_devtools_extension" diff --git a/rohd_extension/dart/README.md b/rohd_extension/dart/README.md deleted file mode 100644 index 878957d74..000000000 --- a/rohd_extension/dart/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# rohd_source_navigator - -Internal Dart port of ROHD source-navigation logic, including source frames, -path normalization, frame cycling, file/line/column data, and a Dart Tooling -Daemon (DTD) service. This package uses `publish_to: none` and is not released -on pub.dev. - -The [ROHD VS Code extension](../README.md) uses its TypeScript implementation. -Its `npm run compile` command runs `tsc` to generate JavaScript in `out/`, and -`.vscodeignore` excludes `dart/` from the VSIX. This Dart port is not compiled -to JavaScript or consumed by the extension's current build. - -## Development - -Run `dart pub get`, `dart analyze`, and `dart test` from this directory. diff --git a/test/prepare_release_metadata_test.dart b/test/prepare_release_metadata_test.dart index 608784122..78c917ff0 100644 --- a/test/prepare_release_metadata_test.dart +++ b/test/prepare_release_metadata_test.dart @@ -53,6 +53,8 @@ void main() { write('packages/rohd_waveform/CHANGELOG.md', '## 2.3.4\n'); write('packages/rohd_devtools_widgets/pubspec.yaml', 'version: 3.4.5\n'); write('packages/rohd_devtools_widgets/CHANGELOG.md', '## Next Release\n'); + write('packages/rohd_source_navigator/pubspec.yaml', 'version: 4.5.6\n'); + write('packages/rohd_source_navigator/CHANGELOG.md', '## Next Release\n'); }); tearDown(() => fixture.deleteSync(recursive: true)); @@ -64,12 +66,14 @@ void main() { 'rohd_hierarchy', 'rohd_waveform', 'rohd_devtools_widgets', + 'rohd_source_navigator', ]); expect(result.exitCode, 0, reason: '${result.stderr}'); expect(result.stdout, contains('rohd: 0.6.11 (prepared')); expect(result.stdout, contains('rohd_hierarchy: 1.2.3 (prepared')); expect(result.stdout, contains('rohd_waveform: 2.3.4 (prepared')); expect(result.stdout, contains('rohd_devtools_widgets: 3.4.5 (prepared')); + expect(result.stdout, contains('rohd_source_navigator: 4.5.6 (prepared')); expect(read('pubspec.yaml'), manifest); expect(read('CHANGELOG.md'), '## 0.6.11\n\n- Update.\n'); expect(read('lib/src/utilities/config.dart'), @@ -77,6 +81,9 @@ void main() { expect(read('packages/rohd_hierarchy/CHANGELOG.md'), '## 1.2.3\n'); expect(read('packages/rohd_waveform/CHANGELOG.md'), '## 2.3.4\n'); expect(read('packages/rohd_devtools_widgets/CHANGELOG.md'), '## 3.4.5\n'); + expect(read('packages/rohd_source_navigator/CHANGELOG.md'), '## 4.5.6\n'); + expect(read('packages/rohd_source_navigator/pubspec.yaml'), + 'version: 4.5.6\n'); }); test('validation does not change metadata', () { @@ -98,6 +105,21 @@ void main() { '## Next Release\n'); }); + test('source navigator selection uses its package directory', () { + final validation = run(['--check', 'rohd_source_navigator']); + expect(validation.exitCode, 0, reason: '${validation.stderr}'); + expect(read('packages/rohd_source_navigator/CHANGELOG.md'), + '## Next Release\n'); + + final result = run(['rohd_source_navigator']); + expect(result.exitCode, 0, reason: '${result.stderr}'); + expect(read('packages/rohd_source_navigator/CHANGELOG.md'), '## 4.5.6\n'); + expect(read('packages/rohd_source_navigator/pubspec.yaml'), + 'version: 4.5.6\n'); + expect(read('CHANGELOG.md'), startsWith('## Next Release')); + expect(read('lib/src/utilities/config.dart'), contains('0.6.10')); + }); + for (final manifest in [ 'name: rohd_hierarchy\n', 'version: 12\n', diff --git a/tool/check_release.sh b/tool/check_release.sh index 0b61b2e11..cd8233ef1 100755 --- a/tool/check_release.sh +++ b/tool/check_release.sh @@ -9,8 +9,8 @@ # Usage (from repo root): # tool/check_release.sh [--validate-only] [package ...] # -# With no package names, check rohd, rohd_hierarchy, rohd_waveform, and -# rohd_devtools_widgets. Explicit names select only those packages. Each package +# With no package names, check rohd, rohd_hierarchy, rohd_waveform, +# rohd_devtools_widgets, and rohd_source_navigator. Each selected package # uses its existing pubspec.yaml version; manifests and changelogs are not edited. # Dart is required for Dart packages; Flutter is required for widgets. # @@ -19,6 +19,7 @@ # tool/check_release.sh rohd # tool/check_release.sh rohd_hierarchy rohd_waveform # tool/check_release.sh rohd_devtools_widgets +# tool/check_release.sh rohd_source_navigator # tool/check_release.sh --validate-only # # --validate-only checks names, manifests, and SDK availability without invoking @@ -42,8 +43,8 @@ export FLUTTER_SUPPRESS_ANALYTICS=true usage() { echo "Usage: $0 [--validate-only] [package ...]" - echo "Packages: rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets" - echo "Defaults to all four packages, using each package's pubspec.yaml version." + echo "Packages: rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator" + echo "Defaults to all five packages, using each package's pubspec.yaml version." echo "Runs publication dry runs only; never uploads packages." } @@ -58,7 +59,7 @@ if [[ "${1:-}" == '--validate-only' ]]; then shift fi if [[ $# -eq 0 ]]; then - set -- rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets + set -- rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator fi readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -73,7 +74,7 @@ for package in "$@"; do directory="$REPO_ROOT" sdk=dart ;; - rohd_hierarchy|rohd_waveform) + rohd_hierarchy|rohd_waveform|rohd_source_navigator) directory="$REPO_ROOT/packages/$package" sdk=dart ;; diff --git a/tool/prepare_release.sh b/tool/prepare_release.sh index eb4300ef4..0e91b7ae1 100755 --- a/tool/prepare_release.sh +++ b/tool/prepare_release.sh @@ -13,8 +13,9 @@ # main into it. When selecting ROHD, wait for that main commit's DevTools build. # Publish from the preparation branch; merge its PR after publication succeeds. # -# With no arguments, select rohd, rohd_hierarchy, rohd_waveform, and -# rohd_devtools_widgets. Explicit package names select only those packages. +# With no arguments, select rohd, rohd_hierarchy, rohd_waveform, +# rohd_devtools_widgets, and rohd_source_navigator. +# Explicit package names select only those packages. # Set each selected package's version in its own pubspec.yaml before running. # The manifests are never rewritten; each pending changelog heading is promoted # to its package's version (an existing version heading is also accepted). @@ -26,7 +27,8 @@ # The DevTools build comes from main, not from branch-only implementation changes. # Each selected sub-package gets dependency resolution, a non-writing format check, # and analysis (including fatal infos) in its own directory. Dart is used -# for hierarchy/waveform, Flutter for widgets, and dart format for all three. +# for hierarchy/waveform/source navigator, Flutter for widgets, and dart format +# for all sub-packages. # Test suites are skipped by default; verify CI results for the release commit. # Add --run-tests to also run all selected package suites locally, including # ROHD's simulator prerequisites when ROHD is selected. @@ -40,10 +42,10 @@ # # Examples: # -# ROHD plus all three publishable sub-packages (the default): +# ROHD plus all four publishable sub-packages (the default): # tool/prepare_release.sh # -# All four packages, including their test suites: +# All five packages, including their test suites: # tool/prepare_release.sh --run-tests # # ROHD only: @@ -58,6 +60,9 @@ # Only hierarchy and waveform (no ROHD metadata or DevTools changes): # tool/prepare_release.sh rohd_hierarchy rohd_waveform # +# Only source navigator: +# tool/prepare_release.sh rohd_source_navigator +# # Sub-package archive checks only (no metadata preparation): # tool/check_release.sh rohd_hierarchy rohd_waveform rohd_devtools_widgets # @@ -75,8 +80,8 @@ export FLUTTER_SUPPRESS_ANALYTICS=true if [[ $# -eq 1 && "$1" == '--help' ]]; then echo "Usage: $0 [--run-tests] [package ...]" - echo "Packages: rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets" - echo "Defaults to all four packages, using each package's pubspec.yaml version." + echo "Packages: rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator" + echo "Defaults to all five packages, using each package's pubspec.yaml version." echo "Test suites are skipped by default; use --run-tests to include them." echo "Artifact verification and its smoke test still run when ROHD is selected." echo "Selecting ROHD also checks VSIX packaging (requires Node.js and npm)." @@ -94,7 +99,7 @@ for argument in "$@"; do done set -- "${packages[@]}" if [[ $# -eq 0 ]]; then - set -- rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets + set -- rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator fi # Resolve all paths and source provenance before changing the working tree. diff --git a/tool/prepare_release_metadata.dart b/tool/prepare_release_metadata.dart index e35e3a3a1..53469e136 100644 --- a/tool/prepare_release_metadata.dart +++ b/tool/prepare_release_metadata.dart @@ -24,6 +24,7 @@ const _packagePaths = { 'rohd_hierarchy': 'packages/rohd_hierarchy', 'rohd_waveform': 'packages/rohd_waveform', 'rohd_devtools_widgets': 'packages/rohd_devtools_widgets', + 'rohd_source_navigator': 'packages/rohd_source_navigator', }; void main(List arguments) { diff --git a/tool/test/check_release_test.sh b/tool/test/check_release_test.sh index 1396b108d..fa11ac326 100755 --- a/tool/test/check_release_test.sh +++ b/tool/test/check_release_test.sh @@ -23,7 +23,7 @@ trap 'rm -rf "$FIXTURE"' EXIT mkdir -p "$FIXTURE/repo/tool" "$FIXTURE/bin" "$FIXTURE/repo/extension/devtools/build" cp "$REPO_ROOT/tool/check_release.sh" "$REPO_ROOT/tool/prepare_release.sh" "$FIXTURE/repo/tool/" -for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets; do +for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator; do mkdir -p "$FIXTURE/repo/packages/$package" touch "$FIXTURE/repo/packages/$package/pubspec.yaml" done @@ -66,24 +66,33 @@ run_case() { run_case 0 "$HELPER" --help [[ ! -s "$SDK_LOG" ]] run_case 0 "$HELPER" -[[ "$(wc -l < "$SDK_LOG")" -eq 4 ]] +[[ "$(wc -l < "$SDK_LOG")" -eq 5 ]] +grep -Fx "dart|$FIXTURE/repo/packages/rohd_source_navigator|pub publish --dry-run" "$SDK_LOG" DASH__SUPPRESS_ANALYTICS=false FLUTTER_SUPPRESS_ANALYTICS=false run_case 0 "$HELPER" rohd_devtools_widgets run_case 0 "$HELPER" --validate-only [[ ! -s "$SDK_LOG" ]] -for invalid in --force --dry-run ../rohd rohd_source_navigator rohd_devtools_extension; do +for invalid in --force --dry-run ../rohd rohd_devtools_extension; do run_case 2 "$HELPER" rohd_hierarchy "$invalid" [[ ! -s "$SDK_LOG" ]] done -run_case 0 "$HELPER" --validate-only rohd rohd_devtools_widgets +run_case 0 "$HELPER" --validate-only rohd rohd_devtools_widgets rohd_source_navigator [[ ! -s "$SDK_LOG" ]] cd "$FIXTURE" -run_case 0 "$HELPER" rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets -[[ "$(wc -l < "$SDK_LOG")" -eq 4 ]] +run_case 0 "$HELPER" rohd rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator +[[ "$(wc -l < "$SDK_LOG")" -eq 5 ]] grep -Fx "dart|$FIXTURE/repo|pub publish --dry-run" "$SDK_LOG" grep -Fx "dart|$FIXTURE/repo/packages/rohd_hierarchy|pub publish --dry-run" "$SDK_LOG" grep -Fx "dart|$FIXTURE/repo/packages/rohd_waveform|pub publish --dry-run" "$SDK_LOG" grep -Fx "flutter|$FIXTURE/repo/packages/rohd_devtools_widgets|pub publish --dry-run" "$SDK_LOG" +grep -Fx "dart|$FIXTURE/repo/packages/rohd_source_navigator|pub publish --dry-run" "$SDK_LOG" + +run_case 0 "$HELPER" rohd_source_navigator +[[ "$(cat "$SDK_LOG")" == "dart|$FIXTURE/repo/packages/rohd_source_navigator|pub publish --dry-run" ]] +FAIL_PACKAGE=rohd_source_navigator run_case 1 "$HELPER" rohd_source_navigator rohd_hierarchy +[[ "$(wc -l < "$SDK_LOG")" -eq 2 ]] +grep -F 'rohd_source_navigator: FAILED (exit 65;' "$FIXTURE/output" +grep -F 'rohd_hierarchy: PASSED' "$FIXTURE/output" export FAIL_PACKAGE=rohd_hierarchy run_case 1 "$HELPER" rohd_hierarchy rohd_waveform @@ -107,6 +116,9 @@ run_case 1 "$HELPER" rohd rm "$FIXTURE/repo/packages/rohd_waveform/pubspec.yaml" run_case 2 "$HELPER" rohd_hierarchy rohd_waveform [[ ! -s "$SDK_LOG" ]] +rm "$FIXTURE/repo/packages/rohd_source_navigator/pubspec.yaml" +run_case 2 "$HELPER" rohd_hierarchy rohd_source_navigator +[[ ! -s "$SDK_LOG" ]] rm "$FIXTURE/bin/flutter" run_case 2 "$HELPER" rohd_hierarchy rohd_devtools_widgets [[ ! -s "$SDK_LOG" ]] diff --git a/tool/test/prepare_release_test.sh b/tool/test/prepare_release_test.sh index c036ed87b..5588f0d26 100644 --- a/tool/test/prepare_release_test.sh +++ b/tool/test/prepare_release_test.sh @@ -86,13 +86,14 @@ printf "static const String version = '0.0.0';\n" > "$UPSTREAM/lib/src/utilities printf '## Next Release\n' > "$UPSTREAM/CHANGELOG.md" printf 'fixture configuration\n' > "$UPSTREAM/extension/devtools/config.yaml" printf 'extension/devtools/build/\n' > "$UPSTREAM/.gitignore" -for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets; do +for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator; do mkdir -p "$UPSTREAM/packages/$package" printf '## Next Release\n' > "$UPSTREAM/packages/$package/CHANGELOG.md" done printf 'version: 1.2.3\n' > "$UPSTREAM/packages/rohd_hierarchy/pubspec.yaml" printf 'version: 2.3.4\n' > "$UPSTREAM/packages/rohd_waveform/pubspec.yaml" printf 'version: 3.4.5\n' > "$UPSTREAM/packages/rohd_devtools_widgets/pubspec.yaml" +printf 'version: 4.5.6\n' > "$UPSTREAM/packages/rohd_source_navigator/pubspec.yaml" git init --quiet --initial-branch=main "$UPSTREAM" git -C "$UPSTREAM" add . @@ -140,7 +141,7 @@ assert_unchanged() { [[ "$(cat "$RELEASE/CHANGELOG.md")" == '## Next Release' ]] [[ "$(cat "$RELEASE/extension/devtools/build/index.html")" == 'original payload' ]] [[ "$(cat "$RELEASE/extension/devtools/config.yaml")" == 'fixture configuration' ]] - for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets; do + for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator; do [[ "$(cat "$RELEASE/packages/$package/CHANGELOG.md")" == '## Next Release' ]] done } @@ -152,11 +153,12 @@ expected_checks() { if [[ "$package" == rohd_devtools_widgets ]]; then sdk=flutter fi - printf '%s|%s|%s\n' "$sdk" "$RELEASE/packages/$package" 'pub get' - printf '%s|%s|%s\n' dart "$RELEASE/packages/$package" 'format --output=none --set-exit-if-changed .' - printf '%s|%s|%s\n' "$sdk" "$RELEASE/packages/$package" 'analyze --fatal-infos' + local directory="$RELEASE/packages/$package" + printf '%s|%s|%s\n' "$sdk" "$directory" 'pub get' + printf '%s|%s|%s\n' dart "$directory" 'format --output=none --set-exit-if-changed .' + printf '%s|%s|%s\n' "$sdk" "$directory" 'analyze --fatal-infos' if [[ "$include_tests" == true ]]; then - printf '%s|%s|%s\n' "$sdk" "$RELEASE/packages/$package" test + printf '%s|%s|%s\n' "$sdk" "$directory" test fi } @@ -179,17 +181,18 @@ assert_unchanged unset SMOKE_STATUS run_case 0 "DevTools source commit (upstream main): $MAIN_COMMIT" -[[ "$(cat "$STAGE_LOG")" == $'smoke\nchecks-skip-tests\nvsix\ndry-run\ndry-run\ndry-run\ndry-run' ]] +[[ "$(cat "$STAGE_LOG")" == $'smoke\nchecks-skip-tests\nvsix\ndry-run\ndry-run\ndry-run\ndry-run\ndry-run' ]] [[ ! -e "$(cat "$VSIX_PATH_LOG")" ]] grep -Fq 'Test suites were not run.' "$FIXTURE/output" expected_log="$( - for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets; do + for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator; do expected_checks "$package" done printf 'dart|%s|pub publish --dry-run\n' "$RELEASE" printf 'dart|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_hierarchy" printf 'dart|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_waveform" printf 'flutter|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_devtools_widgets" + printf 'dart|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_source_navigator" )" [[ "$(cat "$SDK_LOG")" == "$expected_log" ]] [[ "$(cat "$RELEASE/pubspec.yaml")" == "version: '0.6.11' # release" ]] @@ -200,18 +203,20 @@ expected_log="$( [[ "$(cat "$RELEASE/packages/rohd_hierarchy/CHANGELOG.md")" == '## 1.2.3' ]] [[ "$(cat "$RELEASE/packages/rohd_waveform/CHANGELOG.md")" == '## 2.3.4' ]] [[ "$(cat "$RELEASE/packages/rohd_devtools_widgets/CHANGELOG.md")" == '## 3.4.5' ]] +[[ "$(cat "$RELEASE/packages/rohd_source_navigator/CHANGELOG.md")" == '## 4.5.6' ]] git -C "$RELEASE" diff --exit-code -- pubspec.yaml 'packages/*/pubspec.yaml' run_case 0 "DevTools source commit (upstream main): $MAIN_COMMIT" --run-tests -[[ "$(cat "$STAGE_LOG")" == $'smoke\nchecks\nvsix\ndry-run\ndry-run\ndry-run\ndry-run' ]] +[[ "$(cat "$STAGE_LOG")" == $'smoke\nchecks\nvsix\ndry-run\ndry-run\ndry-run\ndry-run\ndry-run' ]] expected_log="$( - for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets; do + for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator; do expected_checks "$package" true done printf 'dart|%s|pub publish --dry-run\n' "$RELEASE" printf 'dart|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_hierarchy" printf 'dart|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_waveform" printf 'flutter|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_devtools_widgets" + printf 'dart|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_source_navigator" )" [[ "$(cat "$SDK_LOG")" == "$expected_log" ]] ! grep -Fq 'Test suites were not run.' "$FIXTURE/output" @@ -267,12 +272,22 @@ expected_log="$( [[ "$(cat "$SDK_LOG")" == "$expected_log" ]] [[ "$(cat "$STAGE_LOG")" == 'dry-run' ]] -for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets; do +run_case 0 'rohd_source_navigator: PASSED' --run-tests rohd_source_navigator +expected_log="$( + expected_checks rohd_source_navigator true + printf 'dart|%s|pub publish --dry-run\n' "$RELEASE/packages/rohd_source_navigator" +)" +[[ "$(cat "$SDK_LOG")" == "$expected_log" ]] +[[ "$(cat "$STAGE_LOG")" == 'dry-run' ]] +[[ "$(cat "$RELEASE/lib/src/utilities/config.dart")" == "static const String version = '0.0.0';" ]] +[[ "$(cat "$RELEASE/CHANGELOG.md")" == '## Next Release' ]] + +for package in rohd_hierarchy rohd_waveform rohd_devtools_widgets rohd_source_navigator; do export FAIL_PACKAGE="$package" FAIL_COMMAND=test run_case 0 "$package: PASSED" "$package" ! grep -Fq '|test' "$SDK_LOG" for check_command in 'pub get' 'format --output=none --set-exit-if-changed .' 'analyze --fatal-infos' test; do - export FAIL_PACKAGE="$package" FAIL_COMMAND="$check_command" + export FAIL_COMMAND="$check_command" check_arguments=() include_tests=false if [[ "$check_command" == test ]]; then From 2fb4863bb6107381297b8d7a9f609c1477fb51df Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 4 Sep 2026 00:42:54 -0700 Subject: [PATCH 02/36] Add module services API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 18 + CONTRIBUTING.md | 4 +- benchmark/many_submodules_benchmark.dart | 4 +- benchmark/wave_dump_benchmark.dart | 4 +- .../chapter_1/01_setup_installation.md | 4 +- doc/tutorials/chapter_2/helper.dart | 3 +- .../chapter_3/answers/exercise_sv.dart | 2 +- doc/tutorials/chapter_3/full_adder.dart | 2 +- .../chapter_4/answers/exercise_1_sv.dart | 2 +- .../chapter_4/answers/exercise_2_sv.dart | 2 +- .../chapter_4/basic_generation_sv.dart | 2 +- doc/tutorials/chapter_5/00_basic_modules.md | 7 +- .../chapter_5/answers/full_adder.dart | 2 +- .../chapter_5/answers/full_subtractor.dart | 2 +- .../chapter_5/answers/n_bit_subtractor.dart | 2 +- doc/tutorials/chapter_5/n_bit_adder.dart | 2 +- .../chapter_7/00_sequential_logic.md | 16 +- .../answers/exercise_1_d_flip_flop.dart | 4 +- doc/tutorials/chapter_7/shift_register.dart | 2 +- doc/tutorials/chapter_8/01_interface.md | 4 +- .../chapter_8/02_finite_state_machine.md | 4 +- doc/tutorials/chapter_8/03_pipeline.md | 2 +- .../chapter_8/answers/exercise_1_spi.dart | 4 +- .../answers/exercise_2_toycapsule_fsm.dart | 4 +- .../answers/exercise_3_pipeline.dart | 4 +- .../chapter_8/carry_save_multiplier.dart | 2 +- .../chapter_8/counter_interface.dart | 4 +- doc/tutorials/chapter_8/oven_fsm.dart | 2 +- doc/tutorials/chapter_9/rohd_vf.md | 2 +- .../rohd_vf_example/lib/rohd_vf_example.dart | 2 +- doc/user_guide/_docs/A21-generation.md | 153 +++++- example/example.dart | 4 +- example/filter_bank.dart | 2 +- example/fir_filter.dart | 5 +- example/logic_array.dart | 4 +- example/oven_fsm.dart | 2 +- example/tree.dart | 2 +- lib/rohd.dart | 10 +- .../collections/iterable_removable_queue.dart | 13 +- lib/src/diagnostics/diagnostics.dart | 12 + lib/src/diagnostics/inspector_service.dart | 41 +- lib/src/diagnostics/module_service.dart | 96 ++++ lib/src/diagnostics/module_services.dart | 79 +++ lib/src/diagnostics/output_file_writer.dart | 13 + .../diagnostics/output_file_writer_io.dart | 17 + lib/src/diagnostics/waveform_service.dart | 488 ++++++++++++++++++ lib/src/module.dart | 74 ++- lib/src/modules/conditionals/flop.dart | 5 +- lib/src/signals/wire_net.dart | 2 +- lib/src/synthesizers/netlist/netlist.dart | 1 + .../synthesizers/netlist/netlist_service.dart | 310 +++++++++++ .../netlist/netlist_synthesizer.dart | 7 +- .../netlist_synthesizer_configuration.dart | 14 +- .../netlist/netlist_validation.dart | 50 +- .../systemverilog/system_verilog_service.dart | 216 ++++++++ .../systemverilog/systemverilog.dart | 1 + .../utilities/synth_assignment.dart | 2 +- lib/src/wave_dumper.dart | 263 ++-------- packages/rohd_hierarchy/analysis_options.yaml | 9 + .../lib/src/occurrence_trie.dart | 9 + .../test/filter_bank_integration_test.dart | 78 +-- .../test/hierarchy_model_test.dart | 71 +++ .../test/occurrence_trie_test.dart | 9 +- .../lib/dtd_service.dart | 2 +- packages/rohd_waveform/analysis_options.yaml | 9 + .../rohd_devtools/services/tree_service.dart | 27 +- .../vm_service_signal_value_source.dart | 6 +- rohd_extension/README.md | 2 +- rohd_extension/snippets/rohd.json | 6 +- test/array_collapsing_test.dart | 182 +++---- test/benchmark_test.dart | 2 +- test/bus_test.dart | 6 +- test/collapse_test.dart | 4 +- test/config_test.dart | 17 +- test/const_radix_test.dart | 4 +- test/counter_test.dart | 4 +- test/counter_wintf_test.dart | 4 +- test/external_test.dart | 4 +- test/fsm_test.dart | 8 +- test/gate_test.dart | 4 +- test/inout_loopback_test.dart | 10 +- test/logic_array_test.dart | 21 +- test/logic_name_test.dart | 20 +- test/logic_structure_test.dart | 2 +- test/logic_test.dart | 2 +- test/math_test.dart | 6 +- test/module_merging_test.dart | 4 +- test/module_services_test.dart | 306 +++++++++++ test/module_test.dart | 24 +- test/multimodule4_test.dart | 4 +- test/multimodule5_test.dart | 4 +- test/name_test.dart | 8 +- test/naming_cases_test.dart | 2 +- test/naming_namespace_test.dart | 14 +- ...sted_array_struct_port_synthesis_test.dart | 7 +- test/net_bus_test.dart | 49 +- test/net_test.dart | 14 +- test/netlist_example_test.dart | 1 - test/netlist_synthesizer_test.dart | 65 +++ test/pair_interface_hier_test.dart | 2 +- test/pair_interface_hier_w_modify_test.dart | 2 +- test/pair_interface_test.dart | 2 +- test/provider_consumer_test.dart | 2 +- test/provider_consumer_w_modify_test.dart | 2 +- test/replication_test.dart | 4 +- test/sequential_test.dart | 2 +- test/struct_port_pruning_test.dart | 6 +- test/sv_gen_test.dart | 34 +- test/sv_param_passthrough_test.dart | 4 +- test/swizzle_test.dart | 37 +- test/synth_name_parity_test.dart | 11 +- test/systemverilog_port_types_test.dart | 5 +- test/typed_port_test.dart | 10 +- ...r_test.dart => waveform_service_test.dart} | 129 ++++- tool/generate_gate_catalog.dart | 2 - tool/gh_actions/install_node.sh | 2 +- 116 files changed, 2566 insertions(+), 724 deletions(-) create mode 100644 lib/src/diagnostics/diagnostics.dart create mode 100644 lib/src/diagnostics/module_service.dart create mode 100644 lib/src/diagnostics/module_services.dart create mode 100644 lib/src/diagnostics/output_file_writer.dart create mode 100644 lib/src/diagnostics/output_file_writer_io.dart create mode 100644 lib/src/diagnostics/waveform_service.dart create mode 100644 lib/src/synthesizers/netlist/netlist_service.dart create mode 100644 lib/src/synthesizers/systemverilog/system_verilog_service.dart create mode 100644 packages/rohd_hierarchy/test/hierarchy_model_test.dart create mode 100644 test/module_services_test.dart rename test/{wave_dumper_test.dart => waveform_service_test.dart} (66%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e77c5cf1..36ccb5b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,24 @@ - Improved generated SystemVerilog to inline packed `Logic.assignSubset` connections into submodule inputs (). - Fixed a bug where isolated bit drivers could be lost beside collapsed ranges (). +- Added the `ModuleService` API for module-scoped generation, capture, and + inspection services. `ModuleServices` registers and looks up services for a + built module hierarchy, and `hierarchyJson` exposes its hierarchy as JSON. +- Added `ArtifactProducingService` and `ModuleServiceArtifact` for + transport-neutral output. Artifact-producing services default + `outputDirectory` to the current directory and `outputBaseName` to the + module definition name, and expose named, media-typed byte streams without + requiring filesystem output. +- Added `SystemVerilogService` for configured SystemVerilog synthesis, + in-memory source output, artifact inspection, and explicit directory writes. + Added `WaveformService` for in-memory waveform capture with optional file + writing through `writeToFile`. +- Added legacy-compatible `Module.dumpSystemVerilog` and `Module.dumpWaves` + convenience methods. `dumpSystemVerilog()` returns simple in-memory output; + `dumpWaves()` provides standard VCD capture as the replacement for + `WaveDumper`. `WaveDumper` and `generateSynth` are deprecated in favor of + these `Module` methods. + ## 0.6.10 - Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f566c324..66336227c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Anyone interested in participating in ROHD is more than welcome to help! ## Code of Conduct -ROHD adopts the [Contributor Covenant](https://www.contributor-covenant.org/) v2.1 for the code of conduct. It can be accessed [here](CODE_OF_CONDUCT.md). +ROHD adopts the [Contributor Covenant](https://www.contributor-covenant.org/) v2.1 for the [Code of Conduct](CODE_OF_CONDUCT.md). ## Getting Help @@ -126,7 +126,7 @@ Please include the SPDX tag near the top of any new files you create: Here is an example of a recommended file header template: ```dart -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // example.dart diff --git a/benchmark/many_submodules_benchmark.dart b/benchmark/many_submodules_benchmark.dart index 763261a4c..44fb74fdb 100644 --- a/benchmark/many_submodules_benchmark.dart +++ b/benchmark/many_submodules_benchmark.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // many_submodules_benchmark.dart @@ -33,7 +33,7 @@ class ManySubmodulesBenchmark extends AsyncBenchmarkBase { Future run() async { final dut = ManySubmodulesModule(Logic(), numSubModules: 10000); await dut.build(); - dut.generateSynth(); + dut.dumpSystemVerilog(); } } diff --git a/benchmark/wave_dump_benchmark.dart b/benchmark/wave_dump_benchmark.dart index 777b42eb0..002e59f48 100644 --- a/benchmark/wave_dump_benchmark.dart +++ b/benchmark/wave_dump_benchmark.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // wave_dump_benchmark.dart @@ -56,7 +56,7 @@ class WaveDumpBenchmark extends AsyncBenchmarkBase { _mod = _ModuleToDump(Logic(), _clk); await _mod.build(); - WaveDumper(_mod, outputPath: _vcdTemporaryPath); + _mod.dumpWaves(outputPath: _vcdTemporaryPath); await Simulator.run(); diff --git a/doc/tutorials/chapter_1/01_setup_installation.md b/doc/tutorials/chapter_1/01_setup_installation.md index 4c5ce37e0..87ef1ecf1 100644 --- a/doc/tutorials/chapter_1/01_setup_installation.md +++ b/doc/tutorials/chapter_1/01_setup_installation.md @@ -162,7 +162,7 @@ Future main({bool noPrint = false}) async { // Let's see what this module looks like as SystemVerilog, so we can pass it // to other tools. - final systemVerilogCode = counter.generateSynth(); + final systemVerilogCode = counter.dumpSystemVerilog().output; if (!noPrint) { print(systemVerilogCode); } @@ -175,7 +175,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(counter); + counter.dumpWaves(); } // Drop reset at time 25. diff --git a/doc/tutorials/chapter_2/helper.dart b/doc/tutorials/chapter_2/helper.dart index 387a57fe3..ad1eee935 100644 --- a/doc/tutorials/chapter_2/helper.dart +++ b/doc/tutorials/chapter_2/helper.dart @@ -13,7 +13,8 @@ import 'package:rohd/rohd.dart'; Future displaySystemVerilog(Module mod) async { await mod.build(); - print('\nYour System Verilog Equivalent Code: \n ${mod.generateSynth()}'); + print('\nYour System Verilog Equivalent Code: \n ' + '${mod.dumpSystemVerilog()}'); } class LogicInitialization extends Module { diff --git a/doc/tutorials/chapter_3/answers/exercise_sv.dart b/doc/tutorials/chapter_3/answers/exercise_sv.dart index 5f8675a37..9406d1e1d 100644 --- a/doc/tutorials/chapter_3/answers/exercise_sv.dart +++ b/doc/tutorials/chapter_3/answers/exercise_sv.dart @@ -33,7 +33,7 @@ void main() async { await fSub.build(); // ignore: avoid_print - tutorial - print(fSub.generateSynth()); + print(fSub.dumpSystemVerilog()); test('should return 0 when a and b equal 1', () { a.put(1); diff --git a/doc/tutorials/chapter_3/full_adder.dart b/doc/tutorials/chapter_3/full_adder.dart index 08ac2cbc4..5ad3f29f3 100644 --- a/doc/tutorials/chapter_3/full_adder.dart +++ b/doc/tutorials/chapter_3/full_adder.dart @@ -76,5 +76,5 @@ void main() async { final mod = FullAdderModule(a, b, cIn, faOps); await mod.build(); - print(mod.generateSynth()); + print(mod.dumpSystemVerilog()); } diff --git a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart index be347b040..1cf09cbb0 100644 --- a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart @@ -10,7 +10,7 @@ void main() async { final mod = NBitAdder(a, b); await mod.build(); - print(mod.generateSynth()); + print(mod.dumpSystemVerilog()); test('should return 255 when both inputs are added', () { a.put(127); diff --git a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart index 9c062a1d9..1a2e27411 100644 --- a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart @@ -9,7 +9,7 @@ void main() async { final mod = NBitSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(mod.dumpSystemVerilog()); test('should return 5 when a is 25 and b is 20', () { a.put(25); diff --git a/doc/tutorials/chapter_4/basic_generation_sv.dart b/doc/tutorials/chapter_4/basic_generation_sv.dart index 55eb4ff07..153213a8c 100644 --- a/doc/tutorials/chapter_4/basic_generation_sv.dart +++ b/doc/tutorials/chapter_4/basic_generation_sv.dart @@ -78,7 +78,7 @@ void main() async { await nbitAdder.build(); - print(nbitAdder.generateSynth()); + print(nbitAdder.dumpSystemVerilog()); test('should return 10 when both inputs are 5.', () { a.put(5); diff --git a/doc/tutorials/chapter_5/00_basic_modules.md b/doc/tutorials/chapter_5/00_basic_modules.md index 65ab303fc..c2207c748 100644 --- a/doc/tutorials/chapter_5/00_basic_modules.md +++ b/doc/tutorials/chapter_5/00_basic_modules.md @@ -95,7 +95,7 @@ Do note that the `build()` method returns a `Future`, not just `void`. Thi ## Converting ROHD Module to System Verilog RTL -Next, we can see how extending your `Logic` to `Module` enables the generation of system Verilog code. Building on the previous example, we've made some slight modifications by adding `simModule.build()` and `simModule.generateSynth()`. +Next, we can see how extending your `Logic` to `Module` enables the generation of system Verilog code. Building on the previous example, we've made some slight modifications by adding `simModule.build()` and `simModule.dumpSystemVerilog().output`. ```dart void main() async { @@ -106,7 +106,7 @@ void main() async { await simModule.build(); // Print out system verilog code - print(simModule.generateSynth()); + print(simModule.dumpSystemVerilog().output); test('should return input value.', () => expect(simModule.out.value.toInt(), equals(1))); @@ -114,7 +114,8 @@ void main() async { // Add this to test on generate system verilog code test( 'should generate system verilog code.', - () => expect(simModule.generateSynth(), contains('module SimpleModule('))); + () => expect(simModule.dumpSystemVerilog().output, + contains('module SimpleModule('))); } ``` diff --git a/doc/tutorials/chapter_5/answers/full_adder.dart b/doc/tutorials/chapter_5/answers/full_adder.dart index 3d932bef9..ae1efd096 100644 --- a/doc/tutorials/chapter_5/answers/full_adder.dart +++ b/doc/tutorials/chapter_5/answers/full_adder.dart @@ -49,7 +49,7 @@ void main() async { final mod = FullAdder(a: a, b: b, carryIn: cIn); await mod.build(); - print(mod.generateSynth()); + print(mod.dumpSystemVerilog()); test('should return true if result sum similar to truth table.', () { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/full_subtractor.dart b/doc/tutorials/chapter_5/answers/full_subtractor.dart index 1e260272c..9d0004769 100644 --- a/doc/tutorials/chapter_5/answers/full_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/full_subtractor.dart @@ -44,7 +44,7 @@ Future main() async { await diff.build(); - print(diff.generateSynth()); + print(diff.dumpSystemVerilog()); test('should return true if results matched truth table', () { for (var i = 0; i <= 1; i++) { diff --git a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart index 7fed1c9d3..c005d864b 100644 --- a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart @@ -36,7 +36,7 @@ Future main() async { final mod = NBitFullSubtractor(a, b); await mod.build(); - print(mod.generateSynth()); + print(mod.dumpSystemVerilog()); test('should return 1 when a is 8 and b is 7.', () { a.put(8); diff --git a/doc/tutorials/chapter_5/n_bit_adder.dart b/doc/tutorials/chapter_5/n_bit_adder.dart index 6d8d32413..d3dc88e29 100644 --- a/doc/tutorials/chapter_5/n_bit_adder.dart +++ b/doc/tutorials/chapter_5/n_bit_adder.dart @@ -79,7 +79,7 @@ void main() async { await nbitAdder.build(); - // print(nbitAdder.generateSynth()); + // print(nbitAdder.dumpSystemVerilog()); test('should return 20 when A and B perform add.', () { a.put(15); diff --git a/doc/tutorials/chapter_7/00_sequential_logic.md b/doc/tutorials/chapter_7/00_sequential_logic.md index ecb2daa25..3eca09077 100644 --- a/doc/tutorials/chapter_7/00_sequential_logic.md +++ b/doc/tutorials/chapter_7/00_sequential_logic.md @@ -5,7 +5,7 @@ - [Shift Register](#shift-register) - [ROHD Simulator](#rohd-simulator) - [Unit Test in Sequential Logic](#unit-test-in-sequential-logic) -- [Wave Dumper](#wave-dumper) +- [Waveform Dumping](#waveform-dumping) - [Exercise](#exercise) ## Learning Outcome @@ -44,7 +44,7 @@ class ShiftRegister extends Module { void main() async { final shiftReg = ShiftRegister(); await shiftReg.build(); - print(shiftReg.generateSynth()); + print(shiftReg.dumpSystemVerilog().output); } ``` @@ -196,7 +196,7 @@ void main() async { final shiftReg = ShiftRegister(clk, reset, sin); await shiftReg.build(); - print(shiftReg.generateSynth()); + print(shiftReg.dumpSystemVerilog().output); // Inject 1 to reset and 0 to shift in reset.inject(1); @@ -217,9 +217,9 @@ void main() async { } ``` -## Wave Dumper +## Waveform Dumping -Let also add `WaveDumper` to view the waveform of the Simulation results. +Let also call `dumpWaves` to view the waveform of the Simulation results. ```dart void main() async { @@ -242,8 +242,8 @@ void main() async { // Run the simulator but don't wait for it unawaited(Simulator.run()); - // Output the simulation waveform using WaveDumper - WaveDumper(shiftReg, + // Output the simulation waveform + shiftReg.dumpWaves( outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); } ``` @@ -252,7 +252,7 @@ Now, let print the flop before the first clock Positive edge. We can just call t ```dart ... -WaveDumper(shiftReg, +shiftReg.dumpWaves( outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); printFlop('Before'); ``` diff --git a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart index 0e81c46fd..ffc204440 100644 --- a/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart +++ b/doc/tutorials/chapter_7/answers/exercise_1_d_flip_flop.dart @@ -44,7 +44,7 @@ Future main() async { final dff = DFlipFlop(data, reset, clk); await dff.build(); - print(dff.generateSynth()); + print(dff.dumpSystemVerilog()); data.inject(1); reset.inject(1); @@ -60,7 +60,7 @@ Future main() async { unawaited(Simulator.run()); - WaveDumper(dff, + dff.dumpWaves( outputPath: 'doc/tutorials/chapter_7/answers/d_flip_flop.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_7/shift_register.dart b/doc/tutorials/chapter_7/shift_register.dart index 046cc05c0..24ced0ee9 100644 --- a/doc/tutorials/chapter_7/shift_register.dart +++ b/doc/tutorials/chapter_7/shift_register.dart @@ -69,7 +69,7 @@ void main() { // kick-off the simulator, but we don't want to wait unawaited(Simulator.run()); - WaveDumper(shiftReg, + shiftReg.dumpWaves( outputPath: 'doc/tutorials/chapter_7/shift_register.vcd'); printFlop('Before'); diff --git a/doc/tutorials/chapter_8/01_interface.md b/doc/tutorials/chapter_8/01_interface.md index ccd44fd62..c8fa9be14 100644 --- a/doc/tutorials/chapter_8/01_interface.md +++ b/doc/tutorials/chapter_8/01_interface.md @@ -168,9 +168,9 @@ Future main() async { counterInterface.en.inject(0); counterInterface.reset.inject(1); - print(counter.generateSynth()); + print(counter.dumpSystemVerilog().output); - WaveDumper(counter, + counter.dumpWaves( outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); Simulator.registerAction(25, () { counterInterface.en.put(1); diff --git a/doc/tutorials/chapter_8/02_finite_state_machine.md b/doc/tutorials/chapter_8/02_finite_state_machine.md index dead94976..5a01d088d 100644 --- a/doc/tutorials/chapter_8/02_finite_state_machine.md +++ b/doc/tutorials/chapter_8/02_finite_state_machine.md @@ -268,11 +268,11 @@ await oven.build(); reset.inject(1); ``` -Let also attach a `WaveDumper` to preview what is the waveform and what happened during the Simulation. +Let also call `dumpWaves` to preview the waveform and what happened during the Simulation. ```dart if (!noPrint) { - WaveDumper(oven, outputPath: 'oven.vcd'); + oven.dumpWaves(outputPath: 'oven.vcd'); } ``` diff --git a/doc/tutorials/chapter_8/03_pipeline.md b/doc/tutorials/chapter_8/03_pipeline.md index 09ec7bffc..5788a8099 100644 --- a/doc/tutorials/chapter_8/03_pipeline.md +++ b/doc/tutorials/chapter_8/03_pipeline.md @@ -282,7 +282,7 @@ void main() async { reset.inject(1); // Attach a waveform dumper so we can see what happens. - WaveDumper(csm, outputPath: 'csm.vcd'); + csm.dumpWaves(outputPath: 'csm.vcd'); Simulator.registerAction(10, () { reset.inject(0); diff --git a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart index 1c67fe3b7..0afd3dfb8 100644 --- a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart +++ b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart @@ -139,7 +139,7 @@ void main() async { await tb.build(); - print(tb.generateSynth()); + print(tb.dumpSystemVerilog()); testInterface.cs.inject(0); testInterface.sdi.inject(0); @@ -163,7 +163,7 @@ void main() async { Simulator.setMaxSimTime(100); unawaited(Simulator.run()); - WaveDumper(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); + peri.dumpWaves(outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); await drive(LogicValue.ofString('01010101')); } diff --git a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart index 9eacf69aa..775cfe91b 100644 --- a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart +++ b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart @@ -49,13 +49,13 @@ Future main(List args) async { final toyCap = ToyCapsuleFSM(clk, reset, dispenseBtn, coin); await toyCap.build(); - print(toyCap.generateSynth()); + print(toyCap.dumpSystemVerilog()); toyCap.toyCapsuleStateMachine.generateDiagram(); reset.inject(1); - WaveDumper(toyCap, outputPath: 'toyCapsuleFSM.vcd'); + toyCap.dumpWaves(outputPath: 'toyCapsuleFSM.vcd'); Simulator.setMaxSimTime(100); Simulator.registerAction(25, () { diff --git a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart index ae810afeb..96abf4c60 100644 --- a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart +++ b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart @@ -34,14 +34,14 @@ void main(List args) { final pipe = Pipeline4Stages(clk, reset, a); await pipe.build(); - // print(pipe.generateSynth()); + // print(pipe.dumpSystemVerilog()); a.inject(5); reset.inject(1); Simulator.registerAction(10, () => reset.put(0)); - WaveDumper(pipe, outputPath: 'answer_1.vcd'); + pipe.dumpWaves(outputPath: 'answer_1.vcd'); Simulator.registerAction(50, () { // stage 4 / result: 30 + (30 * 3) = 120 diff --git a/doc/tutorials/chapter_8/carry_save_multiplier.dart b/doc/tutorials/chapter_8/carry_save_multiplier.dart index 2063f1aaa..fdcbc42aa 100644 --- a/doc/tutorials/chapter_8/carry_save_multiplier.dart +++ b/doc/tutorials/chapter_8/carry_save_multiplier.dart @@ -108,7 +108,7 @@ void main() async { reset.inject(1); // Attach a waveform dumper so we can see what happens. - WaveDumper(csm, outputPath: 'csm.vcd'); + csm.dumpWaves(outputPath: 'csm.vcd'); Simulator.registerAction(10, () { reset.inject(0); diff --git a/doc/tutorials/chapter_8/counter_interface.dart b/doc/tutorials/chapter_8/counter_interface.dart index b2dc142ca..d9fcbf501 100644 --- a/doc/tutorials/chapter_8/counter_interface.dart +++ b/doc/tutorials/chapter_8/counter_interface.dart @@ -63,9 +63,9 @@ Future main() async { await counter.build(); - print(counter.generateSynth()); + print(counter.dumpSystemVerilog()); - WaveDumper(counter, + counter.dumpWaves( outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); Simulator.registerAction(25, () { intf.en.put(1); diff --git a/doc/tutorials/chapter_8/oven_fsm.dart b/doc/tutorials/chapter_8/oven_fsm.dart index 0fcd8002a..844baa8ec 100644 --- a/doc/tutorials/chapter_8/oven_fsm.dart +++ b/doc/tutorials/chapter_8/oven_fsm.dart @@ -192,7 +192,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); + oven.dumpWaves(outputPath: 'doc/tutorials/chapter_8/oven.vcd'); } if (!noPrint) { diff --git a/doc/tutorials/chapter_9/rohd_vf.md b/doc/tutorials/chapter_9/rohd_vf.md index 7d6035930..ceb5c58d1 100644 --- a/doc/tutorials/chapter_9/rohd_vf.md +++ b/doc/tutorials/chapter_9/rohd_vf.md @@ -435,7 +435,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + tb.counter.dumpWaves(); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart index 4b1ef9c34..782a06e45 100644 --- a/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart +++ b/doc/tutorials/chapter_9/rohd_vf_example/lib/rohd_vf_example.dart @@ -315,7 +315,7 @@ Future main({Level loggerLevel = Level.FINER}) async { await tb.counter.build(); // dump wave here - WaveDumper(tb.counter); + tb.counter.dumpWaves(); // Set a maximum simulation time so it doesn't run forever Simulator.setMaxSimTime(300); diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 9c9bb2273..aac4b20ba 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -1,13 +1,14 @@ --- title: "Generating Outputs" permalink: /docs/generation/ -last_modified_at: 2023-11-13 +last_modified_at: 2026-08-19 toc: true --- Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. -The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: +The simplest way to write SystemVerilog is with `dumpSystemVerilog` on +`Module`: ```dart void main() async { @@ -16,17 +17,32 @@ void main() async { // remember that `build` returns a `Future`, hence the `await` here await myModule.build(); - final generatedSv = myModule.generateSynth(); - - // you can print it out... - print(generatedSv); - - // or write it to a file - File('myHardware.sv').writeAsStringSync(generatedSv); + myModule.dumpSystemVerilog(outputPath: 'myHardware.sv'); } ``` -The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. + `dumpSystemVerilog` writes one file containing the SystemVerilog `module` + definitions for the top-level module and all recursive submodules. To write + one `.sv` file per module definition instead, pass a directory and set + `multiFile` to `true`: + + ```dart + myModule.dumpSystemVerilog( + outputPath: 'build/systemverilog', + multiFile: true, + ); + ``` + + For generated text without writing a file, use `dumpSystemVerilog` without an + `outputPath`: + + ```dart + final generatedSv = myModule.dumpSystemVerilog().output; + ``` + + The dump methods preserve the legacy one-shot output workflow. For the service + API, artifact streams, or explicit output configuration, use + `SystemVerilogService` directly. ## Generating a netlist @@ -69,7 +85,8 @@ the complete synthesized hierarchy. Each module entry contains `attributes`, Generated ports default to `input logic`, `output logic`, and `inout wire`, preserving the traditional ROHD declarations. Use a `SystemVerilogSynthesizerConfiguration` to independently control whether object types, such as `wire` and `var`, and data types, such as `logic`, are explicit for each port direction: ```dart -final generatedSv = myModule.generateSynth( +myModule.dumpSystemVerilog( + outputPath: 'myHardware.sv', configuration: const SystemVerilogSynthesizerConfiguration( inputPortType: SystemVerilogPortTypeConfiguration( objectType: SystemVerilogPortType.explicit, @@ -87,7 +104,8 @@ final generatedSv = myModule.generateSynth( ); ``` -The same configuration can be passed directly to `SystemVerilogSynthesizer` when using `SynthBuilder`. +The same configuration can be passed directly to `SystemVerilogSynthesizer` +when using `SynthBuilder`. ## Controlling naming @@ -119,6 +137,113 @@ These modes preserve a generated name, not a separate declaration for every orig The `Naming.unpreferredName` function will modify a signal name to indicate to downstream flows that the name is preferably omitted from the output, but preferable to an unnamed signal. This is generally most useful for things like output ports of `InlineSystemVerilog` modules. -## More advanced generation +## Services API + +`ModuleService` is the shared API for module-scoped generation, capture, and +inspection. Services can register with `ModuleServices` for lookup by DevTools +and other consumers. `ArtifactProducingService` implementations expose named +artifacts as media-typed byte streams, so consumers do not need to require a +local output file. + +### Registering and discovering services + +Services such as `SystemVerilogService` and `WaveformService` register +themselves by default when constructed. `ModuleServices` keeps the most recently +registered service of each concrete type. This allows DevTools, application +code, and other services to discover an optional capability without requiring +the creator to pass the service instance to every consumer: + +```dart +final systemVerilog = SystemVerilogService(myModule); + +final registeredSystemVerilog = + ModuleServices.instance.lookup(); + +assert(identical(systemVerilog, registeredSystemVerilog)); +assert(identical(systemVerilog, SystemVerilogService.current)); +``` + +Constructing another `SystemVerilogService` replaces the previous +`SystemVerilogService` in the registry. Other service types remain registered. +Use `unregister()` to remove one service type or `reset()` to clear the +registry. + +Registration also enables services to collaborate without directly depending +on how the application created them. For example, a generation or capture +service can look up an optional tracing service and include source file, line, +and column information when tracing is available. Because a service may consult +the registry during construction or generation, register supporting services +before constructing the services that consume them. + +For one-shot work that should not change globally discoverable service state, +set `register` to `false`: + +```dart +final oneShotSystemVerilog = SystemVerilogService( + myModule, + register: false, +); +``` -Under the hood of `generateSynth`, it's actually using a [`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) which accepts a `Module` and a `Synthesizer` (usually a `SystemVerilogSynthesizer`) as arguments. This `SynthBuilder` can provide a collection of `String` file contents via `getFileContents`, or you can ask for the full set of `synthesisResults`, which contains `SynthesisResult`s which can each be converted `toSynthFileContents` but also has context about the `module` it refers to, the `instanceTypeName`, etc. With these APIs, you can easily generate named files, add file headers, ignore generation of some modules, generate file lists for other tools, etc. The `SynthBuilder.multi` constructor makes it convenient to generate outputs for multiple independent hierarchies. +The `Module.dumpSystemVerilog()` and `Module.dumpWaves()` convenience methods +use the normal registration defaults. Construct the corresponding service +directly with `register: false` when this side effect is not desired. + +`SystemVerilogService` is the direct synthesis service. Its `outputDirectory` +defaults to the current directory and its `outputBaseName` defaults to the top +module's `definitionName`. The service generates output in memory; call +`writeOutputs` only when files are required: + +```dart +final service = SystemVerilogService( + myModule, + outputDirectory: 'build/netlist', + outputBaseName: 'accelerator', + configuration: const SystemVerilogSynthesizerConfiguration(), +); + +// Use the generated SystemVerilog directly. +final generatedSv = service.output; + +// Or inspect a transport-neutral artifact stream. +final artifact = service.artifacts.single; +final bytes = await artifact.openRead().expand((chunk) => chunk).toList(); + +// Write build/netlist/accelerator.sv. +service.writeOutputs(); +``` + +With `multiFile: true`, `SystemVerilogService` writes one `.sv` file per +generated module definition. For custom synthesis flows, +[`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) +accepts a `Module` and a `Synthesizer` (usually a +`SystemVerilogSynthesizer`). + +## Capturing waveforms + +Use `dumpWaves` for the legacy-compatible common case of writing all simulation +signals to a VCD file: + +```dart +myModule.dumpWaves(outputPath: 'waves.vcd'); +``` + +`WaveformService` records VCD data in memory by default and exposes it as a +`ModuleServiceArtifact`. Its output directory and basename use the same +defaults as other artifact-producing services. Set `writeToFile` when capture +should also create a VCD file: + +```dart +final waveform = WaveformService( + myModule, + outputDirectory: 'build/waves', + outputBaseName: 'interesting-signals', + writeToFile: true, + timescale: '1ns', + startTime: 100, + stopTime: 1000, + signalFilter: (signal) => signal.name.startsWith('debug_'), +); + +final vcdArtifact = waveform.artifacts.single; +``` diff --git a/example/example.dart b/example/example.dart index 74abf8e4b..53a48f38b 100644 --- a/example/example.dart +++ b/example/example.dart @@ -61,7 +61,7 @@ Future main({bool noPrint = false}) async { // Let's see what this module looks like as SystemVerilog, so we can pass it // to other tools. - final systemVerilogCode = counter.generateSynth(); + final systemVerilogCode = counter.dumpSystemVerilog(); if (!noPrint) { print(systemVerilogCode); } @@ -70,7 +70,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(counter); + counter.dumpWaves(); } // Let's also print a message every time the value on the counter changes, diff --git a/example/filter_bank.dart b/example/filter_bank.dart index 710096c3e..155e629d5 100644 --- a/example/filter_bank.dart +++ b/example/filter_bank.dart @@ -63,7 +63,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(dut, outputPath: 'filter_bank.vcd'); + dut.dumpWaves(outputPath: 'filter_bank.vcd'); } // Kick off the simulation. diff --git a/example/fir_filter.dart b/example/fir_filter.dart index 571bbd1ed..765a7ef3b 100644 --- a/example/fir_filter.dart +++ b/example/fir_filter.dart @@ -1,3 +1,4 @@ +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // fir_filter.dart @@ -96,7 +97,7 @@ Future main({bool noPrint = false}) async { await firFilter.build(); // Generate SystemVerilog code. - final systemVerilogCode = firFilter.generateSynth(); + final systemVerilogCode = firFilter.dumpSystemVerilog(); if (!noPrint) { // Print SystemVerilog code to console. print(systemVerilogCode); @@ -108,7 +109,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper. if (!noPrint) { - WaveDumper(firFilter); + firFilter.dumpWaves(); } // Let's set the initial setting. diff --git a/example/logic_array.dart b/example/logic_array.dart index 2cde075ce..2ec2e5dfe 100644 --- a/example/logic_array.dart +++ b/example/logic_array.dart @@ -58,14 +58,14 @@ Future main({bool noPrint = false}) async { // Build the module await logicArrayExample.build(); - final systemVerilogCode = logicArrayExample.generateSynth(); + final systemVerilogCode = logicArrayExample.dumpSystemVerilog(); if (!noPrint) { print(systemVerilogCode); } // Simulate the module if (!noPrint) { - WaveDumper(logicArrayExample); + logicArrayExample.dumpWaves(); } // Set the input values diff --git a/example/oven_fsm.dart b/example/oven_fsm.dart index 144ef14d9..44023fcbf 100644 --- a/example/oven_fsm.dart +++ b/example/oven_fsm.dart @@ -225,7 +225,7 @@ Future main({bool noPrint = false}) async { // Attach a waveform dumper so we can see what happens. if (!noPrint) { - WaveDumper(oven, outputPath: 'oven.vcd'); + oven.dumpWaves(outputPath: 'oven.vcd'); } // Kick off the simulation. diff --git a/example/tree.dart b/example/tree.dart index 395be6a10..4c79584f5 100644 --- a/example/tree.dart +++ b/example/tree.dart @@ -85,7 +85,7 @@ Future main({bool noPrint = false}) async { // Below will generate an output of the ROHD-generated SystemVerilog: await tree.build(); - final generatedSystemVerilog = tree.generateSynth(); + final generatedSystemVerilog = tree.dumpSystemVerilog(); if (!noPrint) { print(generatedSystemVerilog); } diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..bda2229c1 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,13 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +// +// rohd.dart +// Main public API exports for the ROHD framework. +// +// 2026 July +// Author: ROHD Contributors +export 'src/diagnostics/diagnostics.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; @@ -12,6 +19,7 @@ export 'src/signals/signals.dart'; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; +export 'src/synthesizers/systemverilog/system_verilog_service.dart'; export 'src/utilities/naming.dart'; export 'src/values/values.dart'; export 'src/wave_dumper.dart'; diff --git a/lib/src/collections/iterable_removable_queue.dart b/lib/src/collections/iterable_removable_queue.dart index 06b7981e7..0ba15790f 100644 --- a/lib/src/collections/iterable_removable_queue.dart +++ b/lib/src/collections/iterable_removable_queue.dart @@ -53,12 +53,13 @@ class IterableRemovableQueue { return; } - if (_removeWhere == null) { + final removeWhere = _removeWhere; + if (removeWhere == null) { // Nothing to remove. return; } - if (_first == _last && _removeWhere!(_first!.item)) { + if (_first == _last && removeWhere(_first!.item)) { // if size is 1 and its removable, then we can just clear the queue and be // done with it clear(); @@ -77,7 +78,7 @@ class IterableRemovableQueue { } while (_patrol != null) { - if (_removeWhere!(_patrol!.item)) { + if (removeWhere(_patrol!.item)) { assert(size > 0, 'Should not be removing if size is already 0.'); if (_patrol == _first && _first == _last) { @@ -118,7 +119,8 @@ class IterableRemovableQueue { /// Also may remove items from the queue if they are indicated by /// [_removeWhere]. void add(T item) { - if (_removeWhere != null && _removeWhere!(item)) { + final removeWhere = _removeWhere; + if (removeWhere != null && removeWhere(item)) { // If the item should be removed, we don't add it. return; } @@ -183,8 +185,9 @@ class IterableRemovableQueue { var element = _first; _IterableRemovableElement? previous; + final removeWhere = _removeWhere; while (element != null) { - if (_removeWhere != null && _removeWhere!(element.item)) { + if (removeWhere != null && removeWhere(element.item)) { assert(size > 0, 'Should not be removing if size is already 0.'); previous?.next = element.next; diff --git a/lib/src/diagnostics/diagnostics.dart b/lib/src/diagnostics/diagnostics.dart new file mode 100644 index 000000000..8ff34cf81 --- /dev/null +++ b/lib/src/diagnostics/diagnostics.dart @@ -0,0 +1,12 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// diagnostics.dart +// Barrel export for the diagnostics library. +// +// 2026 July 16 +// Author: Desmond Kirkpatrick + +export 'module_service.dart'; +export 'module_services.dart'; +export 'waveform_service.dart'; diff --git a/lib/src/diagnostics/inspector_service.dart b/lib/src/diagnostics/inspector_service.dart index ef4d6ea10..7e39caf1e 100644 --- a/lib/src/diagnostics/inspector_service.dart +++ b/lib/src/diagnostics/inspector_service.dart @@ -8,6 +8,7 @@ // Author: Yao Jing Quek import 'dart:convert'; +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; extension _LogicDevToolUtils on Logic { @@ -74,8 +75,8 @@ extension _ModuleDevToolUtils on Module { /// `ModuleTree` implements the Singleton design pattern /// to ensure there is only one instance of it during runtime. /// -/// This class is used to maintain a tree-like structure -/// for managing modules in an application. +/// This class preserves the legacy DevTools inspector entry point for the +/// built module hierarchy. class ModuleTree { /// Private constructor used to initialize the Singleton instance. ModuleTree._(); @@ -86,8 +87,22 @@ class ModuleTree { static ModuleTree get instance => _instance; static final _instance = ModuleTree._(); - /// Stores the root Module instance. - static Module? rootModuleInstance; + Module? _rootModule; + + /// The root [Module] registered for hierarchy inspection. + @internal + Module? get rootModule => _rootModule; + + /// Sets the root [Module] used to produce downstream hierarchy JSON. + /// + /// This is kept as an internal setter instead of a writable field so callers + /// make the bridge explicit: [ModuleServices] is the public service registry, + /// while [ModuleTree] owns the legacy DevTools hierarchy JSON surface + /// consumed by downstream hierarchy adapters. + @internal + set rootModule(Module? module) { + _rootModule = module; + } /// Returns the `hierarchyString` as JSON. /// @@ -95,10 +110,16 @@ class ModuleTree { /// /// Returns: string representing hierarchical structure of modules in JSON /// format. - String get hierarchyJSON => - rootModuleInstance?.buildModuleTreeJsonSchema(rootModuleInstance!) ?? - json.encode({ - 'status': 'fail', - 'reason': 'module not yet build', - }); + String get hierarchyJson { + final rootModule = _rootModule; + return rootModule?.buildModuleTreeJsonSchema(rootModule) ?? + json.encode({ + 'status': 'fail', + 'reason': 'module not yet build', + }); + } + + /// Returns the built module hierarchy as JSON. + @Deprecated('Use hierarchyJson instead.') + String get hierarchyJSON => hierarchyJson; } diff --git a/lib/src/diagnostics/module_service.dart b/lib/src/diagnostics/module_service.dart new file mode 100644 index 000000000..1a7d4e52d --- /dev/null +++ b/lib/src/diagnostics/module_service.dart @@ -0,0 +1,96 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_service.dart +// Common base types shared by all module-scoped services. +// +// 2026 June 23 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; + +/// The common contract implemented by every module-scoped service that +/// registers with [ModuleServices]. +/// +/// A service wraps some derived view of a built [Module] (synthesis output, +/// netlist, source trace, waveform, etc.) and exposes a JSON-serialisable +/// summary via [toJson]. Concrete services additionally expose their own +/// format-specific accessors; consumers reach them through +/// [ModuleServices.lookup] or the service's own `current` accessor rather than +/// through getters on the registry. +abstract interface class ModuleService { + /// The top-level [Module] this service operates on. + Module get module; + + /// A JSON-serialisable summary of this service. + Map toJson(); +} + +/// A named artifact produced by an [ArtifactProducingService]. +/// +/// Artifacts expose their content as bytes so services can retain data in +/// memory, generate it lazily, or stream it without requiring a filesystem. +class ModuleServiceArtifact { + /// Creates an artifact with [fileName], [mediaType], and byte [openRead]. + const ModuleServiceArtifact({ + required this.fileName, + required this.mediaType, + required Stream> Function() openRead, + }) : _openRead = openRead; + + /// The artifact filename, including its format-specific extension. + final String fileName; + + /// The IANA-style media type of the artifact. + final String mediaType; + + final Stream> Function() _openRead; + + /// Opens a new stream of the artifact's bytes. + Stream> openRead() => _openRead(); +} + +/// A [ModuleService] that produces named output artifacts. +/// +/// The output location is always a directory. [outputBaseName] defaults to the +/// module definition name, while each concrete service configuration determines +/// artifact extensions and layouts. +abstract class ArtifactProducingService implements ModuleService { + /// Creates an artifact-producing service for [module]. + ArtifactProducingService( + this.module, { + this.outputDirectory = '.', + String? outputBaseName, + }) : outputBaseName = outputBaseName ?? module.definitionName; + + /// The top-level [Module] this service operates on. + @override + final Module module; + + /// Directory receiving filesystem artifacts when this service writes them. + final String outputDirectory; + + /// Filename stem used for primary artifacts. + final String outputBaseName; + + /// The artifacts this service can provide. + Iterable get artifacts; +} + +/// An [ArtifactProducingService] that generates source-code text. +/// +/// Shared by language code-generation services, which all produce a combined +/// single-file [output]. +abstract class CodeGenService extends ArtifactProducingService { + /// Creates a code-generation service for [module]. + CodeGenService( + super.module, { + super.outputDirectory, + super.outputBaseName, + }); + + /// The combined single-file generated output (including any header). + String get output; +} diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..86696770e --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,79 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services.dart +// Slim, type-keyed registry of module-scoped services for DevTools and other +// inspection tools. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; + +/// A slim, type-keyed registry of [ModuleService]s. +/// +/// Services register themselves here on construction (keyed by their concrete +/// type) and are retrieved with [lookup]. The registry intentionally exposes +/// no per-format accessors: each service owns its own JSON and output methods, +/// reached through [lookup] or the service's own static `current` accessor. +/// +/// The registry references no specific service type, so it is identical across +/// all feature branches that contribute services. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJson] — set by [Module.build]. +class ModuleServices { + ModuleServices._(); + + /// The singleton instance. + static final ModuleServices instance = ModuleServices._(); + + // ─── Hierarchy (auto-registered by Module.build) ────────────── + + /// The most recently built top-level [Module]. + /// + /// Set automatically at the end of [Module.build]. + Module? get rootModule => ModuleTree.instance.rootModule; + + /// Sets the most recently built top-level [Module]. + /// + /// Intended for internal use by [Module.build] and test reset paths. + @internal + set rootModule(Module? module) { + ModuleTree.instance.rootModule = module; + } + + /// Returns the module hierarchy as a JSON string. + /// + /// DevTools evaluates this via `EvalOnDartLibrary` to display the module + /// hierarchy. Richer design views (e.g. a slim netlist) are composed by the + /// DevTools client from the relevant registered service. + String get hierarchyJson => ModuleTree.instance.hierarchyJson; + + // ─── Type-keyed service registry ────────────────────────────── + + final Map _services = {}; + + /// Registers [service] under the type argument [T]. + /// + /// Replaces any previously registered service of the same type. + void register(T service) { + _services[T] = service; + } + + /// Returns the registered service of type [T], or `null` if none. + T? lookup() => _services[T] as T?; + + /// Removes the registered service of type [T], if any. + void unregister() { + _services.remove(T); + } + + /// Resets all services. Intended for test teardown. + void reset() { + rootModule = null; + _services.clear(); + } +} diff --git a/lib/src/diagnostics/output_file_writer.dart b/lib/src/diagnostics/output_file_writer.dart new file mode 100644 index 000000000..c4053fe7e --- /dev/null +++ b/lib/src/diagnostics/output_file_writer.dart @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// output_file_writer.dart +// Platform-neutral output file writer stub. +// +// 2026 July 5 +// Author: Desmond Kirkpatrick + +/// Writes [contents] to [path] on platforms that support file IO. +void writeOutputTextFile(String path, String contents) { + throw UnsupportedError('File output is not supported on this platform.'); +} diff --git a/lib/src/diagnostics/output_file_writer_io.dart b/lib/src/diagnostics/output_file_writer_io.dart new file mode 100644 index 000000000..6529fe6f3 --- /dev/null +++ b/lib/src/diagnostics/output_file_writer_io.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// output_file_writer_io.dart +// Native output file writer. +// +// 2026 July 5 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +/// Writes [contents] to [path], creating parent directories as needed. +void writeOutputTextFile(String path, String contents) { + File(path) + ..parent.createSync(recursive: true) + ..writeAsStringSync(contents); +} diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart new file mode 100644 index 000000000..f77c0fa1d --- /dev/null +++ b/lib/src/diagnostics/waveform_service.dart @@ -0,0 +1,488 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_service.dart +// Base waveform service: file output with filtering, timescale, and +// flush/overwrite control. Designed to be subclassed by the DevTools +// streaming variant. +// +// 2026 June +// Author: Desmond Kirkpatrick + +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +// ─── Supporting types ──────────────────────────────────────────────────────── + +/// The output format for waveform capture. +enum WaveOutputFormat { + /// Value Change Dump — the classic text-based waveform format. + vcd, + + /// Fast Signal Trace — a compact binary format. + /// + /// Requires an FST writer to be available; see the DevTools subclass for + /// a fully FST-backed implementation. + fst; + + /// The filename extension associated with this format. + String get fileExtension => switch (this) { + WaveOutputFormat.vcd => 'vcd', + WaveOutputFormat.fst => 'fst', + }; + + /// The media type associated with this format. + String get mediaType => switch (this) { + WaveOutputFormat.vcd => 'text/x-vcd', + WaveOutputFormat.fst => 'application/vnd.gtkwave.fst', + }; +} + +/// Policy applied when the output file already exists at construction time. +enum OverwritePolicy { + /// Silently overwrite any existing file. + overwrite, + + /// Throw a [FileSystemException] if the file already exists. + failIfExists, +} + +// ─── Service ───────────────────────────────────────────────────────────────── + +/// A waveform capture service that records signal changes. +/// +/// This is the base class for waveform capture. It handles: +/// - Signal collection (with optional [signalFilter]) +/// - In-memory VCD output with configurable [timescale] +/// - Selective recording via [startTime] / [stopTime] +/// - Optional file output with periodic buffer flushing and [overwritePolicy] +/// - Optional registration with [ModuleServices] +/// +/// **Subclassing for DevTools streaming:** +/// +/// Override the protected hooks below to intercept the simulation event loop +/// without re-implementing the file-writing logic: +/// +/// - [onSignalCollected] — called once per tracked signal at startup; use +/// it to register signals in a VM-service index. +/// - [onValueChange] — called for every value-change event within the +/// [startTime]/[stopTime] window; use it to feed an in-memory store for +/// streaming. +/// - [onTimestampCapture] — called once per simulation timestamp that +/// contains at least one change; the full changed-signal set is passed. +/// - [onSimulationEnd] — called after the final timestamp is written and +/// the file is closed; use it to finalise any streaming buffers. +/// +/// Example subclass skeleton: +/// ```dart +/// class DevToolsWaveformService extends WaveformService { +/// DevToolsWaveformService( +/// super.module, { +/// super.outputDirectory, +/// super.outputBaseName, +/// }); +/// +/// @override +/// void onSignalCollected(Logic signal) { +/// super.onSignalCollected(signal); +/// _registerWithVmService(signal); +/// } +/// +/// @override +/// void onValueChange(Logic signal, int timestamp) { +/// super.onValueChange(signal, timestamp); +/// _recordInMemory(signal, timestamp); +/// } +/// } +/// ``` +class WaveformService extends ArtifactProducingService { + /// The most recently registered [WaveformService], or `null`. + static WaveformService? current; + + /// Path of the output waveform file. + /// + /// Derived from [outputDirectory], [outputBaseName], and [format]. + String get outputFilePath => '$outputDirectory${Platform.pathSeparator}' + '${outputFileName ?? '$outputBaseName.${format.fileExtension}'}'; + + /// The output filepath of the generated waveforms. + /// + /// This matches the legacy waveform dumper's `outputPath` name. + String get outputPath => outputFilePath; + + /// Exact output filename override. + /// + /// Prefer [outputBaseName] for new service code. This override exists for + /// compatibility with legacy APIs that accepted an arbitrary output path. + final String? outputFileName; + + /// Output format. + final WaveOutputFormat format; + + /// Optional predicate that determines whether a given [Logic] signal is + /// captured. + /// + /// When `null`, all non-[Const] signals in the hierarchy are captured, + /// matching the legacy waveform dumper behaviour. + final bool Function(Logic signal)? signalFilter; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Simulation time at which recording begins. + /// + /// Signals are still collected before this time so they appear in the scope + /// definition, but value-change events are suppressed until [startTime] is + /// reached. `null` means "from the very start". + final int? startTime; + + /// Simulation time at which recording ends. + /// + /// Value-change events after this time are suppressed. `null` means "until + /// end of simulation". + final int? stopTime; + + /// Number of characters accumulated in the write buffer before it is flushed + /// to disk. + final int flushBufferSize; + + /// What to do when the output file already exists. + final OverwritePolicy overwritePolicy; + + /// Whether to register this service with [ModuleServices] for inspection. + final bool register; + + /// Whether waveform bytes are written to [outputFilePath]. + /// + /// When `false`, the service retains its waveform bytes in memory and exposes + /// them through [artifacts]. + final bool writeToFile; + + // ─── Internal file-writing state ───────────────────────────── + + /// Sink writing to [outputFilePath] when [writeToFile] is true. + IOSink? _outFileSink; + + /// Write buffer; flushed when it exceeds [flushBufferSize]. + final StringBuffer _fileBuffer = StringBuffer(); + + /// The complete waveform output retained for streaming artifacts. + final StringBuffer _inMemoryOutput = StringBuffer(); + + /// Counter for assigning compact signal markers in the VCD. + int _signalMarkerIdx = 0; + + /// Maps each captured [Logic] to its VCD marker string. + final Map _signalToMarkerMap = {}; + + /// Signals that changed during the current simulation timestamp. + final Set _changedThisTimestamp = HashSet(); + + /// The timestamp currently being accumulated. + int _currentDumpingTimestamp = Simulator.time; + + // ─── Constructor ───────────────────────────────────────────── + + /// Creates a [WaveformService] for [module]. + /// + /// [module] must be built before construction. + /// + /// [outputDirectory] defaults to the current directory and [outputBaseName] + /// defaults to [Module.definitionName]. The selected [format] determines the + /// output filename extension. Only [WaveOutputFormat.vcd] is currently + /// supported by this service. + /// + /// Use the optional constructor parameters to configure format, filtering, + /// timescale, start/stop times, flush size, and overwrite policy. + WaveformService( + Module module, { + super.outputDirectory, + super.outputBaseName, + this.outputFileName, + this.format = WaveOutputFormat.vcd, + this.signalFilter, + this.timescale = '1ps', + this.startTime, + this.stopTime, + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + this.register = true, + this.writeToFile = false, + }) : super(module) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating WaveformService. ' + 'Call build() first.', + ); + } + if (format != WaveOutputFormat.vcd) { + throw UnsupportedError( + 'Waveform format ${format.name} is not supported by WaveformService.', + ); + } + + if (writeToFile && overwritePolicy == OverwritePolicy.failIfExists) { + final f = File(outputFilePath); + if (f.existsSync()) { + throw FileSystemException( + 'Waveform output file already exists and overwritePolicy is ' + 'failIfExists.', + outputFilePath, + ); + } + } + + if (writeToFile) { + _outFileSink = + (File(outputFilePath)..createSync(recursive: true)).openWrite(); + } + + _collectSignals(); + _writeHeader(); + _writeScope(); + + Simulator.preTick.listen((_) { + if (Simulator.time != _currentDumpingTimestamp) { + if (_changedThisTimestamp.isNotEmpty) { + _captureTimestamp(_currentDumpingTimestamp); + } + _currentDumpingTimestamp = Simulator.time; + } + }); + + Simulator.registerEndOfSimulationAction(() async { + _captureTimestamp(Simulator.time); + await _terminate(); + onSimulationEnd(); + }); + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + // ─── Extensibility hooks ────────────────────────────────────── + + /// Called once for each [Logic] signal that passes + /// [signalFilter] during initial signal collection. + /// + /// Override in a subclass to register signals with an in-memory store, + /// VM service index, or FST handle map. Always call `super` first. + @protected + void onSignalCollected(Logic signal) {} + + /// Called for every value-change event on [signal] at [timestamp]. + /// + /// Only called within the [startTime] / [stopTime] window. + /// + /// Override in a subclass to feed an in-memory waveform store or + /// streaming buffer. Always call `super` first. + @protected + void onValueChange(Logic signal, int timestamp) {} + + /// Called once per simulation timestamp that contains at least one change, + /// after all value-change events for that timestamp have been processed. + /// + /// [changed] is the set of signals that changed at [timestamp]. + /// + /// Override in a subclass to flush incremental streaming payloads. + /// Always call `super` first. + @protected + void onTimestampCapture(int timestamp, Set changed) {} + + /// Called after the final timestamp has been written and the file is closed. + /// + /// Override in a subclass to finalise any streaming buffers or emit + /// end-of-simulation notifications. + @protected + void onSimulationEnd() {} + + // ─── Internal signal collection ────────────────────────────── + + void _collectSignals() { + final modulesToParse = [module]; + for (var i = 0; i < modulesToParse.length; i++) { + final m = modulesToParse[i]; + for (final sig in m.signals) { + if (sig is Const) { + continue; + } + if (signalFilter != null && !signalFilter!(sig)) { + continue; + } + + _signalToMarkerMap[sig] = 's${_signalMarkerIdx++}'; + onSignalCollected(sig); + + sig.changed.listen((_) { + _changedThisTimestamp.add(sig); + }); + } + + for (final subm in m.subModules) { + if (subm is InlineSystemVerilog) { + continue; + } + modulesToParse.add(subm); + } + } + } + + // ─── VCD output helpers ─────────────────────────────────────── + + void _writeHeader() { + final header = ''' +\$date + ${Timestamper.stamp()} +\$end +\$version + ROHD v${Config.version} +\$end +\$comment + Generated by ROHD - www.github.com/intel/rohd +\$end +\$timescale $timescale \$end +'''; + _writeToBuffer(header); + } + + void _writeScope() { + var scopeString = _computeScopeString(module); + scopeString += '\$enddefinitions \$end\n'; + scopeString += '\$dumpvars\n'; + _writeToBuffer(scopeString); + _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + _writeToBuffer('\$end\n'); + } + + String _computeScopeString(Module m, {int indent = 0}) { + final moduleSignalUniquifier = Uniquifier(); + final padding = List.filled(indent, ' ').join(); + var scopeString = '$padding\$scope module ${m.uniqueInstanceName} \$end\n'; + final innerScopeString = StringBuffer(); + + for (final sig in m.signals) { + if (!_signalToMarkerMap.containsKey(sig)) { + continue; + } + final width = sig.width; + final marker = _signalToMarkerMap[sig]; + var signalName = Sanitizer.sanitizeSV(sig.name); + signalName = moduleSignalUniquifier.getUniqueName( + initialName: signalName, + reserved: sig.isPort, + ); + innerScopeString.write( + ' $padding\$var wire $width $marker $signalName \$end\n', + ); + } + for (final subModule in m.subModules) { + innerScopeString.write( + _computeScopeString(subModule, indent: indent + 1), + ); + } + if (innerScopeString.isEmpty) { + return ''; + } + + scopeString += innerScopeString.toString(); + scopeString += '$padding\$upscope \$end\n'; + return scopeString; + } + + bool _isInRecordingWindow(int timestamp) { + if (startTime != null && timestamp < startTime!) { + return false; + } + if (stopTime != null && timestamp > stopTime!) { + return false; + } + return true; + } + + void _captureTimestamp(int timestamp) { + if (!_isInRecordingWindow(timestamp)) { + _changedThisTimestamp.clear(); + return; + } + + _writeToBuffer('#$timestamp\n'); + + final snapshot = Set.of(_changedThisTimestamp); + for (final sig in snapshot) { + _writeSignalValueUpdate(sig); + onValueChange(sig, timestamp); + } + _changedThisTimestamp.clear(); + + onTimestampCapture(timestamp, snapshot); + } + + void _writeSignalValueUpdate(Logic signal) { + final binaryValue = signal.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); + final updateValue = signal.width > 1 + ? 'b$binaryValue ' + : signal.value.toString(includeWidth: false); + final marker = _signalToMarkerMap[signal]; + _writeToBuffer('$updateValue$marker\n'); + } + + // ─── Buffered I/O ───────────────────────────────────────────── + + void _writeToBuffer(String contents) { + _fileBuffer.write(contents); + _inMemoryOutput.write(contents); + if (_fileBuffer.length > flushBufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + _outFileSink?.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } + + Future _terminate() async { + _flushBuffer(); + await _outFileSink?.flush(); + await _outFileSink?.close(); + } + + // ─── Inspection ─────────────────────────────────────────────── + + /// The waveform artifact produced by this service. + @override + Iterable get artifacts => [ + ModuleServiceArtifact( + fileName: outputFileName ?? '$outputBaseName.${format.fileExtension}', + mediaType: format.mediaType, + openRead: () => Stream.value(utf8.encode(_inMemoryOutput.toString())), + ), + ]; + + /// Returns a JSON-serialisable summary of this service. + @override + Map toJson() => { + 'outputDirectory': outputDirectory, + 'outputBaseName': outputBaseName, + 'outputFilePath': outputFilePath, + 'writeToFile': writeToFile, + 'format': format.name, + 'signalCount': _signalToMarkerMap.length, + 'timescale': timescale, + if (startTime != null) 'startTime': startTime!, + if (stopTime != null) 'stopTime': stopTime!, + }; +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 4a6a9e07f..602e56bfa 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,11 +13,8 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; -import 'package:rohd/src/diagnostics/inspector_service.dart'; -import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a synthesizable hardware entity with clearly defined interface @@ -341,7 +338,7 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; } /// Confirms that the post-[build] hierarchy is valid. @@ -1133,32 +1130,59 @@ abstract class Module { /// [hierarchy], this is only valid after [build] has been called. String get hierarchicalName => _hierarchyListToString(hierarchy()); + /// Generates synthesized SystemVerilog for this [Module]. + /// + /// Returns one concatenated in-memory SystemVerilog file. The [configuration] + /// controls options specific to SystemVerilog output. + /// + /// For file output, multiple files, artifacts, or access to synthesis + /// results, use [SystemVerilogService] directly. + String dumpSystemVerilog({ + SystemVerilogSynthesizerConfiguration configuration = + const SystemVerilogSynthesizerConfiguration(), + }) => + SystemVerilogService( + this, + configuration: configuration, + register: false, + ).output; + + /// Attaches waveform dumping for this [Module] to a VCD at [outputPath]. + /// + /// For filtering, alternative formats, and other waveform controls, use + /// [WaveformService] directly. + WaveformService dumpWaves({String outputPath = 'waves.vcd'}) { + final normalized = outputPath.replaceAll(r'\', '/'); + final separatorIndex = normalized.lastIndexOf('/'); + final outputDirectory = switch (separatorIndex) { + -1 => '.', + 0 => '/', + _ => normalized.substring(0, separatorIndex), + }; + final outputFileName = normalized.substring(separatorIndex + 1); + + return WaveformService( + this, + outputDirectory: outputDirectory, + outputFileName: outputFileName, + writeToFile: true, + ); + } + /// Returns a synthesized version of this [Module]. /// - /// Currently returns one long file in SystemVerilog, but in the future - /// may have other output formats, languages, files, etc. + /// Currently returns one long file in SystemVerilog, but in the future may + /// have other output formats, languages, files, etc. /// + /// For richer access to per-module file contents, named maps, and individual + /// file writing, see [SystemVerilogService] (and + /// [SystemVerilogService.output] for the equivalent one-shot string). /// The [configuration] controls options specific to SystemVerilog output. + @Deprecated('Use Module.dumpSystemVerilog(configuration: ...) for in-memory ' + 'output or SystemVerilogService for advanced options.') String generateSynth({ SystemVerilogSynthesizerConfiguration configuration = const SystemVerilogSynthesizerConfiguration(), - }) { - if (!_hasBuilt) { - throw ModuleNotBuiltException(this); - } - - final synthHeader = ''' -/** - * Generated by ROHD - www.github.com/intel/rohd - * Generation time: ${Timestamper.stamp()} - * ROHD Version: ${Config.version} - */ - -'''; - return synthHeader + - SynthBuilder( - this, - SystemVerilogSynthesizer(configuration: configuration), - ).getSynthFileContents().join('\n\n////////////////////\n\n'); - } + }) => + dumpSystemVerilog(configuration: configuration); } diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index 4930d6c96..1c2a31032 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -149,8 +149,9 @@ class FlipFlop extends Module with SystemVerilog { void _setup() { var contents = [q < _d]; - if (_en != null) { - contents = [If(_en!, then: contents)]; + final en = _en; + if (en != null) { + contents = [If(en, then: contents)]; } Sequential( diff --git a/lib/src/signals/wire_net.dart b/lib/src/signals/wire_net.dart index f93529b0f..881bbdd84 100644 --- a/lib/src/signals/wire_net.dart +++ b/lib/src/signals/wire_net.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // wire_net.dart diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart index 0e86e506f..950ca3529 100644 --- a/lib/src/synthesizers/netlist/netlist.dart +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -7,5 +7,6 @@ // 2026 February 11 // Author: Desmond Kirkpatrick +export 'netlist_service.dart'; export 'netlist_synthesizer.dart'; export 'netlist_synthesizer_configuration.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart new file mode 100644 index 000000000..c63104907 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -0,0 +1,310 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_service.dart +// Service wrapper for netlist synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/output_file_writer.dart' + if (dart.library.io) 'package:rohd/src/diagnostics/output_file_writer_io.dart'; + +/// A service that wraps netlist (Yosys JSON) synthesis of a [Module] +/// hierarchy. +/// +/// Provides access to the full hierarchy JSON and per-module JSON with +/// lazy caching, and optionally registers with [ModuleServices] for +/// DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final netlist = NetlistService(dut); +/// +/// // Full hierarchy JSON: +/// print(netlist.json); +/// +/// // Single module (lazy, cached): +/// print(netlist.moduleJson('FilterChannel')); +/// ``` +class NetlistService extends ArtifactProducingService { + /// The current format version for netlist JSON produced by this service. + static const String formatVersion = '0.0.5'; + + /// The most recently registered [NetlistService], or `null`. + static NetlistService? current; + + /// The default location written by [write], or `null`. + final String? outputPath; + + /// The [NetlistSynthesizer] used for synthesis. + late final NetlistSynthesizer synthesizer; + + /// The underlying [SynthBuilder]. + late final SynthBuilder synthBuilder; + + /// The combined JSON string for the full hierarchy. + late final String _fullJson; + + /// Cached per-module JSON, keyed by definition name. + final Map _moduleJsonCache = {}; + + /// The parsed modules map from the combined JSON. + late final Map _modulesMap; + + /// The package root directory used for FLC trace injection. + /// + /// When non-null, downstream trace-enabled branches use this path to embed + /// `rohd.src_trace` attributes in the netlist JSON. + late final String? packageRoot; + + /// Creates a netlist service for a built [module]. + /// + /// Uses [configuration] for netlist synthesis and optionally + /// [register]s this instance with [ModuleServices] for DevTools lookup. + NetlistService( + Module module, { + NetlistSynthesizerConfiguration configuration = + const NetlistSynthesizerConfiguration(), + String? packageRoot, + bool register = true, + this.outputPath, + super.outputDirectory, + super.outputBaseName, + }) : super(module) { + if (!module.hasBuilt) { + throw Exception( + 'Module must be built before creating NetlistService. ' + 'Call build() first.', + ); + } + + final effectiveRoot = packageRoot; + synthesizer = NetlistSynthesizer(configuration: configuration); + this.packageRoot = effectiveRoot; + synthBuilder = SynthBuilder(module, synthesizer); + _fullJson = synthesizer.synthesizeToJson( + module, + packageRoot: effectiveRoot, + ); + + final decoded = jsonDecode(_fullJson) as Map; + _modulesMap = + (decoded['modules'] as Map?) ?? {}; + _loadedVersion = decoded['version'] as String?; + + if (outputPath != null) { + write(); + } + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + /// The generated netlist JSON artifact. + @override + Iterable get artifacts => [ + ModuleServiceArtifact( + fileName: '$outputBaseName.rohd.json', + mediaType: 'application/json', + openRead: () => Stream.value(utf8.encode(json)), + ), + ]; + + /// The format version found in the loaded JSON, or `null` if absent. + String? _loadedVersion; + + /// The format version string from the loaded netlist JSON. + /// + /// Returns the `version` field from the JSON if present, otherwise + /// returns [formatVersion] (assumes current format). + String get version => _loadedVersion ?? formatVersion; + + /// Checks whether [version] is compatible with the current + /// [formatVersion]. + /// + /// Compatible means the major version matches. Returns `true` if + /// the loaded JSON can be consumed by this version of the service. + static bool isCompatibleVersion(String version) { + final current = formatVersion.split('.'); + final other = version.split('.'); + if (other.length < 2 || current.length < 2) { + return false; + } + // Major and minor must match for compatibility. + return current[0] == other[0] && current[1] == other[1]; + } + + /// Whether the loaded netlist JSON is compatible with the current format. + bool get isCompatible => isCompatibleVersion(version); + + /// Returns the full netlist hierarchy as a JSON string. + String get json => _fullJson; + + /// Writes the full netlist [json] to [path], or to [outputPath] when [path] + /// is omitted. + void write([String? path]) { + final target = + path ?? outputPath ?? '$outputDirectory/$outputBaseName.rohd.json'; + writeOutputTextFile(target, _fullJson); + } + + /// Returns a JSON-serialisable summary of the netlist synthesis. + /// + /// Contains the netlist format version and the list of module definition + /// names. For the full netlist document, use [json]. + @override + Map toJson() => { + 'creator': 'ROHD netlist synthesizer', + 'version': version, + 'modules': moduleNames.toList(), + }; + + /// Returns the netlist JSON for a single module [definitionName]. + /// + /// The returned JSON is keyed by definition name: + /// `{"DefinitionName": { ports, cells, netnames }}`. + /// This matches the format expected by the DevTools schematic viewer + /// for incremental module fetches. + /// + /// If the module is not found, returns a JSON error object. + String moduleJson(String definitionName) => + _moduleJsonCache.putIfAbsent(definitionName, () { + final modData = _modulesMap[definitionName]; + if (modData == null) { + return jsonEncode({ + 'status': 'not_found', + 'reason': 'module "$definitionName" not in netlist', + }); + } + return jsonEncode({ + 'creator': 'ROHD netlist synthesizer', + 'version': formatVersion, + 'modules': {definitionName: modData}, + }); + }); + + /// Returns the set of module definition names in the netlist. + Set get moduleNames => _modulesMap.keys.toSet(); + + /// Read-only access to the parsed modules map. + /// + /// Each key is a definition name and each value is the Yosys-style + /// module descriptor containing `ports`, `cells`, and `netnames`. + Map get synthesizedModules => + Map.unmodifiable(_modulesMap); + + /// Cached slim JSON (lazy). + String? _slimJsonCache; + + /// Returns a slim netlist JSON string — same structure as [toJson] but + /// with cell `connections` stripped. + /// + /// The slim representation preserves ports, cells (type + port_directions + /// + port_widths), and netnames so the DevTools extension can render the + /// hierarchy and signal tree without the full connectivity payload. + /// Full per-module connectivity is fetched on demand via [moduleJson]. + String get slimJson => _slimJsonCache ??= _buildSlimJson(); + + /// Builds the slim hierarchy JSON with per-cell connections omitted. + String _buildSlimJson() { + final slimModules = {}; + for (final entry in _modulesMap.entries) { + final mod = entry.value as Map; + final cells = mod['cells'] as Map? ?? {}; + final slimCells = {}; + for (final cellEntry in cells.entries) { + final cell = cellEntry.value as Map; + // Compute per-port widths from connections (bit-array lengths). + final conns = cell['connections'] as Map?; + final portWidths = {}; + if (conns != null) { + for (final c in conns.entries) { + final bits = c.value; + if (bits is List) { + portWidths[c.key] = bits.length; + } + } + } + slimCells[cellEntry.key] = { + 'hide_name': cell['hide_name'] ?? 0, + 'type': cell['type'], + 'parameters': cell['parameters'] ?? {}, + 'attributes': cell['attributes'] ?? {}, + 'port_directions': cell['port_directions'] ?? {}, + if (portWidths.isNotEmpty) 'port_widths': portWidths, + // connections intentionally omitted → slim + }; + } + + // Determine which module-level ports have internal connectivity. + final ports = mod['ports'] as Map? ?? {}; + final slimPorts = {}; + final cellConnectedBits = {}; + for (final cellEntry in cells.values) { + final cell = cellEntry as Map; + final conns = cell['connections'] as Map?; + if (conns == null) { + continue; + } + for (final bits in conns.values) { + if (bits is List) { + for (final b in bits) { + if (b is int) { + cellConnectedBits.add(b); + } + } + } + } + } + for (final portEntry in ports.entries) { + final portData = portEntry.value as Map; + final bits = portData['bits'] as List?; + var connected = false; + if (bits != null) { + for (final b in bits) { + if (b is int && cellConnectedBits.contains(b)) { + connected = true; + break; + } + } + } + slimPorts[portEntry.key] = { + ...portData, + if (connected) 'connected': true, + }; + } + + final netnames = mod['netnames'] as Map? ?? {}; + + slimModules[entry.key] = { + 'attributes': { + ...(mod['attributes'] as Map? ?? {}), + 'original_signal_count': netnames.length, + 'original_cell_count': slimCells.length, + }, + 'ports': slimPorts, + 'cells': slimCells, + 'netnames': netnames, + }; + } + + final rootName = module.hasBuilt ? module.uniqueInstanceName : module.name; + + return jsonEncode({ + 'netlist': { + 'creator': 'ROHD NetlistService (slim)', + 'version': formatVersion, + 'rootInstanceName': rootName, + 'modules': slimModules, + }, + }); + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index ceacb6ff8..190bada6b 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -975,10 +975,8 @@ class NetlistSynthesizer extends Synthesizer { /// Apply all post-processing passes to the modules map. /// - /// This is the canonical pass ordering used by both netlist flows: - /// **Flow 1** (slim batch via `_synthesizeSlimModules`) and - /// **Flow 2** (incremental full via `moduleNetlistJson`). - /// Also used internally by [buildModulesMap] / [synthesizeToJson]. + /// This is the canonical pass ordering used by both the slim and full JSON + /// projections produced by [buildModulesMap] and [synthesizeToJson]. void applyPostProcessingPasses(Map> modules) { if (configuration.collapseTransparentClusters) { NetlistPasses.collapseConcatOfAdjacentSlices(modules); @@ -1128,7 +1126,6 @@ class NetlistSynthesizer extends Synthesizer { /// The [packageRoot] parameter is accepted for API compatibility with /// downstream trace-enabled branches. [slimMode] overrides the configured /// output mode for this call, allowing expansion after a slim request. - @visibleForTesting String synthesizeToJson(Module top, {String? packageRoot, bool? slimMode}) { final sb = SynthBuilder(top, this); return generateCombinedJson(sb, top, slimMode: slimMode); diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart index 6d60ad4f8..d7429cefc 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -17,15 +17,19 @@ export '../utilities/synth_module_stop_policy.dart'; /// The netlist synthesizer serves two main consumer flows, both configured /// through this configuration: /// -/// **Flow 1 — Slim JSON** (`NetlistService.slimJson`): +/// **Flow 1 — Slim JSON** ([NetlistSynthesizer.synthesizeToJson] with +/// `slimMode: true`): /// Batch synthesis of the entire design, producing a lightweight /// representation with ports, signals, and cell stubs but **no cell /// connections**. Used for the initial DevTools hierarchy load. /// -/// **Flow 2 — Full JSON, incremental** (`NetlistService.moduleJson`): -/// Returns the complete netlist (with cell connections) for a single -/// module definition on demand. Results are cached; the first call -/// may trigger a lazy `SynthBuilder` run on the requested subtree. +/// **Flow 2 — Full JSON** ([NetlistSynthesizer.synthesizeToJson] with +/// `slimMode: false`): +/// Synthesizes the entire design with complete cell connections. +/// +/// [NetlistService] exposes these projections through [NetlistService.slimJson] +/// and [NetlistService.moduleJson], preserving a shared netlist representation +/// across the two flows. /// /// Both flows retain complete per-module synthesis results. Flow 1 skips cell /// connection copying while collecting the emitted JSON projection. This keeps diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index f2c8a6b87..45f0f86b8 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -10,6 +10,8 @@ import 'package:meta/meta.dart'; import 'package:rohd/src/exceptions/synth_exception.dart'; +typedef _NetlistDriver = ({String description, bool isTriState}); + /// Graph queries and structural checks for an emitted module netlist. @internal class NetlistValidation { @@ -59,14 +61,15 @@ class NetlistValidation { final driversByBit = _driversByBit(ports, cells); for (final entry in driversByBit.entries) { - if (entry.value.length <= 1) { + if (!_hasConflictingDrivers(entry.value)) { continue; } + final drivers = entry.value.map((driver) => driver.description).toList(); issues.add(NetlistValidationIssue( 'wire bit ${entry.key} has multiple drivers: ' - '${entry.value.join(', ')}', + '${drivers.join(', ')}', wireBit: entry.key, - drivers: entry.value, + drivers: drivers, )); } @@ -82,17 +85,20 @@ class NetlistValidation { continue; } final bits = (netname['bits'] as List?)?.whereType() ?? const []; - final aggregateDrivers = { - for (final bit in bits) ...driversByBit[bit] ?? const [], + final aggregateDrivers = <_NetlistDriver>{ + for (final bit in bits) + ...driversByBit[bit] ?? const <_NetlistDriver>[], }; - if (aggregateDrivers.length <= 1) { + if (!_hasConflictingDrivers(aggregateDrivers)) { continue; } + final drivers = + aggregateDrivers.map((driver) => driver.description).toList(); issues.add(NetlistValidationIssue( 'aggregate net "${entry.key}" is reached from multiple drivers: ' - '${aggregateDrivers.join(', ')}', + '${drivers.join(', ')}', netname: entry.key, - drivers: aggregateDrivers.toList(), + drivers: drivers, )); } } @@ -103,14 +109,15 @@ class NetlistValidation { } /// Collects the port and cell output drivers for each integer bit ID. - static Map> _driversByBit( + static Map> _driversByBit( Map> ports, Map> cells, ) { - final drivers = >{}; + final drivers = >{}; - void addDriver(int bit, String driver) => - (drivers[bit] ??= []).add(driver); + void addDriver(int bit, String description, {bool isTriState = false}) => + (drivers[bit] ??= <_NetlistDriver>[]) + .add((description: description, isTriState: isTriState)); for (final entry in ports.entries) { final direction = entry.value['direction'] as String?; @@ -137,13 +144,18 @@ class NetlistValidation { } for (final port in connections.entries) { final direction = directions[port.key] as String?; - final isTriStateOutput = type == r'$tribuf' && direction == 'inout'; + final isTriStateOutput = type == r'$tribuf' && + (direction == 'output' || direction == 'inout'); if (direction != 'output' && !isTriStateOutput) { continue; } for (final bit in (port.value as List?) ?? const []) { if (bit is int) { - addDriver(bit, 'cell ${entry.key}.${port.key} ($type)'); + addDriver( + bit, + 'cell ${entry.key}.${port.key} ($type)', + isTriState: isTriStateOutput, + ); } } } @@ -151,6 +163,16 @@ class NetlistValidation { return drivers; } + + static bool _hasConflictingDrivers(Iterable<_NetlistDriver> drivers) { + var count = 0; + var allTriState = true; + for (final driver in drivers) { + count++; + allTriState &= driver.isTriState; + } + return count > 1 && !allTriState; + } } /// A structural netlist validation failure. diff --git a/lib/src/synthesizers/systemverilog/system_verilog_service.dart b/lib/src/synthesizers/systemverilog/system_verilog_service.dart new file mode 100644 index 000000000..76798e522 --- /dev/null +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -0,0 +1,216 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// system_verilog_service.dart +// Service wrapper for SystemVerilog synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; + +/// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. +/// +/// Provides access to the generated SV file contents and per-module +/// synthesis results, and optionally registers with [ModuleServices] +/// for DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final sv = SystemVerilogService( +/// dut, +/// outputDirectory: 'build', +/// multiFile: true, +/// ); +/// +/// // Write individual .sv files: +/// sv.writeOutputs(); +/// +/// // Or get the concatenated output (like generateSynth): +/// print(sv.output); +/// ``` +class SystemVerilogService extends CodeGenService { + /// The most recently registered [SystemVerilogService], or `null`. + static SystemVerilogService? current; + + /// The separator inserted between module definitions in the + /// concatenated single-file output from [allContents]. + /// + /// Matches the historical single-file synthesis output format. + static const moduleSeparator = '\n\n////////////////////\n\n'; + + /// Whether the artifact layout emits one `.sv` file per module definition + /// (`true`) or a single concatenated file (`false`). + final bool multiFile; + + /// Whether generated SV files include the ROHD generation header. + /// + /// Defaults to `true` for single-file output and `false` for multi-file + /// output, preserving the historical layout. Set explicitly to select any + /// combination of header and file layout. + final bool includeHeader; + + /// Configuration controlling generated SystemVerilog. + final SystemVerilogSynthesizerConfiguration configuration; + + /// The underlying [SynthBuilder] that drove synthesis. + late final SynthBuilder synthBuilder; + + /// The generated file contents (one per unique module definition). + late final List fileContents; + + /// Creates a [SystemVerilogService] for [module]. + /// + /// [module] must already be built. + /// + /// [outputDirectory] defaults to the current directory and [outputBaseName] + /// defaults to [Module.definitionName]. [includeHeader] defaults to + /// `!multiFile` to preserve the historical layout. Set [register] to `false` + /// to keep this service out of [ModuleServices]. + SystemVerilogService( + Module module, { + super.outputDirectory, + super.outputBaseName, + this.multiFile = false, + bool? includeHeader, + this.configuration = const SystemVerilogSynthesizerConfiguration(), + bool register = true, + }) : includeHeader = includeHeader ?? !multiFile, + super(module) { + if (!module.hasBuilt) { + throw ModuleNotBuiltException(module); + } + + synthBuilder = SynthBuilder( + module, SystemVerilogSynthesizer(configuration: configuration)); + fileContents = synthBuilder.getSynthFileContents(); + + if (register) { + current = this; + ModuleServices.instance.register(this); + } + } + + /// All [SynthesisResult]s produced by synthesis. + Set get synthesisResults => synthBuilder.synthesisResults; + + /// Returns the concatenated SystemVerilog module definitions as a single + /// string, without the generation header. + /// + /// For the full output with header (matching `Module.generateSynth()`), + /// use [output]. + String get allContents => + fileContents.map((fc) => fc.contents).join(moduleSeparator); + + /// The ROHD generation header prepended to single-file output. + String get synthHeader => ''' +/** + * Generated by ROHD - www.github.com/intel/rohd + * Generation time: ${Timestamper.stamp()} + * ROHD Version: ${Config.version} + */ + +'''; + + /// The generation header included in each emitted SV file, when enabled. + /// + /// Cached so output and emitted files always use the same timestamp. + late final String header = includeHeader ? synthHeader : ''; + + /// Returns the full single-file SystemVerilog output with header, + /// identical to `Module.generateSynth()`. + /// + /// Computed once and cached so the timestamped header is stable for the + /// lifetime of this service. + @override + late final String output = header + allContents; + + /// Returns SV output for a generated module [instanceTypeName], or `null` + /// when that instance type was not generated. + /// + /// The instance type name is [SynthesisResult.instanceTypeName], the + /// uniquified definition name used in the generated SV. + String? instanceTypeOutput(String instanceTypeName) { + for (final fc in fileContents) { + if (fc.name == instanceTypeName) { + return fc.contents; + } + } + return null; + } + + /// Returns a map from generated module instance type name to its SV contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + @Deprecated('Use instanceTypeOutput(instanceTypeName) for lookup or ' + 'fileContents for iteration instead.') + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + + /// The artifacts generated by this service. + /// + /// Single-file output is named `.sv`. Split output retains + /// the generated definition names so module references remain obvious. + @override + Iterable get artifacts => multiFile + ? [ + for (final fc in fileContents) + ModuleServiceArtifact( + fileName: '${fc.name}.sv', + mediaType: 'text/x-systemverilog', + openRead: () => Stream.value(utf8.encode(header + fc.contents)), + ), + ] + : [ + ModuleServiceArtifact( + fileName: '$outputBaseName.sv', + mediaType: 'text/x-systemverilog', + openRead: () => Stream.value(utf8.encode(output)), + ), + ]; + + /// Writes this service's artifacts to [outputDirectory]. + void writeOutputs() { + final directory = Directory(outputDirectory)..createSync(recursive: true); + if (multiFile) { + for (final fc in fileContents) { + File('${directory.path}/${fc.name}.sv') + .writeAsStringSync(header + fc.contents); + } + } else { + File('${directory.path}/$outputBaseName.sv').writeAsStringSync(output); + } + } + + /// Writes the single-file output to an exact legacy [outputPath]. + /// + /// This preserves the legacy convenience API's arbitrary filename behavior + /// without changing the service artifact naming convention. + void writeLegacyOutputPath(String outputPath) { + if (multiFile) { + throw StateError( + 'writeLegacyOutputPath is only valid for single-file output.', + ); + } + File(outputPath) + ..parent.createSync(recursive: true) + ..writeAsStringSync(output); + } + + /// Returns a JSON-serialisable summary of the SV synthesis. + /// + /// Contains the list of generated module definition names. + @override + Map toJson() => { + 'modules': [for (final fc in fileContents) fc.name], + }; +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog.dart b/lib/src/synthesizers/systemverilog/systemverilog.dart index 6990cbe2a..0db21104f 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,6 +1,7 @@ // Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'system_verilog_service.dart'; export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; export 'systemverilog_synthesizer_configuration.dart'; diff --git a/lib/src/synthesizers/utilities/synth_assignment.dart b/lib/src/synthesizers/utilities/synth_assignment.dart index aaafda601..0fcb132b6 100644 --- a/lib/src/synthesizers/utilities/synth_assignment.dart +++ b/lib/src/synthesizers/utilities/synth_assignment.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_assignment.dart diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 3a37e55ea..7fbd15dea 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -1,233 +1,72 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // wave_dumper.dart -// Waveform dumper for a given module hierarchy, dumps to ".vcd" file. +// Deprecated waveform dumper; use Module.dumpWaves instead. // // 2021 May 7 // Author: Max Korbel -import 'dart:collection'; -import 'dart:io'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/config.dart'; -import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/timestamper.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; -/// A waveform dumper for simulations. +/// Deprecated: use [Module.dumpWaves] instead. /// -/// Outputs to vcd format at [outputPath]. [module] must be built prior to -/// attaching the [WaveDumper]. +/// [WaveDumper] is a simple wrapper around [WaveformService] for backward +/// compatibility. It provides the legacy API for recording all signal changes +/// in a simulation to a VCD file. /// -/// The waves will only dump to the file periodically and then once the -/// simulation has completed. +/// **Migration guide:** +/// +/// Replace: +/// ```dart +/// var dumper = WaveDumper(module, outputPath: 'output.vcd'); +/// ``` +/// +/// With: +/// ```dart +/// var service = module.dumpWaves(outputPath: 'output.vcd'); +/// ``` +/// +/// For more control over filtering, timescale, and recording windows, create a +/// [WaveformService] directly: +/// ```dart +/// final service = WaveformService( +/// module, +/// outputDirectory: 'build', +/// outputBaseName: 'debug', +/// writeToFile: true, +/// timescale: '1ns', +/// startTime: 100, +/// signalFilter: (signal) => signal.name.startsWith('debug_'), +/// ); +/// ``` +@Deprecated('Use Module.dumpWaves() for simple VCD output, or ' + 'WaveformService for advanced waveform configuration.') class WaveDumper { + /// The underlying [WaveformService]. + final WaveformService _service; + /// The [Module] being dumped. - final Module module; + Module get module => _service.module; /// The output filepath of the generated waveforms. - final String outputPath; - - /// The file to write dumped output waveform to. - final File _outputFile; - - /// A sink to write contents into [_outputFile]. - late final IOSink _outFileSink; - - /// A buffer for contents before writing to the file sink. - final StringBuffer _fileBuffer = StringBuffer(); - - /// A counter for tracking signal names in the VCD file. - int _signalMarkerIdx = 0; - - /// Stores the mapping from [Logic] to signal marker in the VCD file. - final Map _signalToMarkerMap = {}; - - /// A set of all [Logic]s that have changed in this timestamp so far. - /// - /// This spans across multiple inject or changed events if they are in the - /// same timestamp of the [Simulator]. - final Set _changedLogicsThisTimestamp = HashSet(); - - /// The timestamp which is currently being collected for a dump. - /// - /// When the [Simulator] time progresses beyond this, it will dump all the - /// signals that have changed up until that point at this saved time value. - int _currentDumpingTimestamp = Simulator.time; + String get outputPath => _service.outputFilePath; /// Attaches a [WaveDumper] to record all signal changes in a simulation of /// [module] in a VCD file at [outputPath]. - WaveDumper(this.module, {this.outputPath = 'waves.vcd'}) - : _outputFile = File(outputPath)..createSync(recursive: true) { - if (!module.hasBuilt) { - throw Exception( - 'Module must be built before passed to dumper. Call build() first.'); - } - - _outFileSink = _outputFile.openWrite(); - - _collectAllSignals(); - - _writeHeader(); - _writeScope(); - - Simulator.preTick.listen((args) { - if (Simulator.time != _currentDumpingTimestamp) { - if (_changedLogicsThisTimestamp.isNotEmpty) { - // no need to write blank timestamps - _captureTimestamp(_currentDumpingTimestamp); - } - _currentDumpingTimestamp = Simulator.time; - } - }); - - Simulator.registerEndOfSimulationAction(() async { - _captureTimestamp(Simulator.time); - - await _terminate(); - }); - } - - /// Number of characters in the buffer after which it will - /// write contents to the output file. - static const _fileBufferLimit = 100000; - - /// Buffers [contents] to be written to the output file. - void _writeToBuffer(String contents) { - _fileBuffer.write(contents); - - if (_fileBuffer.length > _fileBufferLimit) { - _writeToFile(); - } - } - - /// Writes all pending items in the [_fileBuffer] to the file. - void _writeToFile() { - _outFileSink.write(_fileBuffer.toString()); - _fileBuffer.clear(); - } - - /// Terminates the waveform dumping, including closing the file. - Future _terminate() async { - _writeToFile(); - await _outFileSink.flush(); - await _outFileSink.close(); - } - - /// Registers all signal value changes to write updates to the dumped VCD. - void _collectAllSignals() { - final modulesToParse = [module]; - for (var i = 0; i < modulesToParse.length; i++) { - final m = modulesToParse[i]; - for (final sig in m.signals) { - if (sig is Const) { - // constant values are "boring" to inspect - continue; - } - - _signalToMarkerMap[sig] = 's${_signalMarkerIdx++}'; - sig.changed.listen((args) { - _changedLogicsThisTimestamp.add(sig); - }); - } - for (final subm in m.subModules) { - if (subm is InlineSystemVerilog) { - // the InlineSystemVerilog modules are "boring" to inspect - continue; - } - modulesToParse.add(subm); - } - } - } - - /// Writes the top header for the VCD file. - void _writeHeader() { - final dateString = Timestamper.stamp(); - const timescale = '1ps'; - final header = ''' -\$date - $dateString -\$end -\$version - ROHD v${Config.version} -\$end -\$comment - Generated by ROHD - www.github.com/intel/rohd -\$end -\$timescale $timescale \$end -'''; - _writeToBuffer(header); - } - - /// Writes the scope of the VCD, including signal and hierarchy declarations, - /// as well as initial values. - void _writeScope() { - var scopeString = _computeScopeString(module); - scopeString += '\$enddefinitions \$end\n'; - scopeString += '\$dumpvars\n'; - _writeToBuffer(scopeString); - _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); - _writeToBuffer('\$end\n'); - } - - /// Generates the top of the scope string (signal and hierarchy definitions). - String _computeScopeString(Module m, {int indent = 0}) { - final moduleSignalUniquifier = Uniquifier(); - final padding = List.filled(indent, ' ').join(); - var scopeString = '$padding\$scope module ${m.uniqueInstanceName} \$end\n'; - final innerScopeString = StringBuffer(); - for (final sig in m.signals) { - if (!_signalToMarkerMap.containsKey(sig)) { - continue; - } - - final width = sig.width; - final marker = _signalToMarkerMap[sig]; - var signalName = Sanitizer.sanitizeSV(sig.name); - signalName = moduleSignalUniquifier.getUniqueName( - initialName: signalName, reserved: sig.isPort); - innerScopeString - .write(' $padding\$var wire $width $marker $signalName \$end\n'); - } - for (final subModule in m.subModules) { - innerScopeString - .write(_computeScopeString(subModule, indent: indent + 1)); - } - if (innerScopeString.isEmpty) { - // no need to dump empty scopes - return ''; - } - scopeString += innerScopeString.toString(); - scopeString += '$padding\$upscope \$end\n'; - return scopeString; - } - - /// Writes the current timestamp to the VCD. - void _captureTimestamp(int timestamp) { - final timestampString = '#$timestamp\n'; - _writeToBuffer(timestampString); - - _changedLogicsThisTimestamp - ..forEach(_writeSignalValueUpdate) - ..clear(); - } - - /// Writes the current value of [signal] to the VCD. - void _writeSignalValueUpdate(Logic signal) { - final binaryValue = signal.value.reversed - .toList() - .map((e) => e.toString(includeWidth: false)) - .join(); - final updateValue = signal.width > 1 - ? 'b$binaryValue ' - : signal.value.toString(includeWidth: false); - final marker = _signalToMarkerMap[signal]; - final updateString = '$updateValue$marker\n'; - _writeToBuffer(updateString); - } + /// + /// [module] must be built prior to construction. + /// + /// **Deprecated:** Use [Module.dumpWaves] for simple VCD output, or + /// [WaveformService] for signal filtering, custom timescale, recording + /// windows, and extensibility hooks for streaming applications. + @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' + 'WaveformService for advanced waveform configuration.') + WaveDumper(Module module, {String outputPath = 'waves.vcd'}) + : _service = module.dumpWaves(outputPath: outputPath); } -/// Deprecated: use [WaveDumper] instead. -@Deprecated('Use WaveDumper instead') +/// Deprecated: use [Module.dumpWaves] instead. +@Deprecated('Use Module.dumpWaves() for simple VCD output, or ' + 'WaveformService for advanced waveform configuration.') typedef Dumper = WaveDumper; diff --git a/packages/rohd_hierarchy/analysis_options.yaml b/packages/rohd_hierarchy/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_hierarchy/analysis_options.yaml +++ b/packages/rohd_hierarchy/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart index f6d65b2cb..40261f213 100644 --- a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -35,11 +35,20 @@ class OccurrenceTrie { /// Associates [value] with [address]. void operator []=(OccurrenceAddress address, T value) { + set(address, value); + } + + /// Associates [value] with [address]. + /// + /// Returns the value previously stored at [address], if any. + T? set(OccurrenceAddress address, T value) { var node = _root; for (final index in _validatedPath(address)) { node = node.children.putIfAbsent(index, _OccurrenceTrieNode.new); } + final previous = node.value; node.value = value; + return previous; } /// Removes and returns the value stored at [address], if any. diff --git a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart index 6a8fe173a..b7778fd54 100644 --- a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart +++ b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart @@ -8,6 +8,9 @@ // 2026 April // Author: Desmond Kirkpatrick +@TestOn('vm') +library; + import 'dart:io'; import 'package:rohd_hierarchy/rohd_hierarchy.dart'; @@ -172,19 +175,6 @@ void main() { expect(outputs.map((s) => s.name), contains('done')); }); - test(r'isPrimitiveType is true for $-prefixed types', () { - expect(HierarchyOccurrence.isPrimitiveType(r'$mux'), isTrue); - expect(HierarchyOccurrence.isPrimitiveType(r'$and'), isTrue); - }); - - test(r'isPrimitiveType is false for non-$-prefixed types', () { - expect(HierarchyOccurrence.isPrimitiveType('FilterBank'), isFalse); - }); - - test('isPrimitiveType is false for empty string', () { - expect(HierarchyOccurrence.isPrimitiveType(''), isFalse); - }); - test('isPrimitiveCell reflects isPrimitive field and type', () { // A node marked isPrimitive=true final primCell = service.root.children.firstWhere((c) => c.isPrimitive); @@ -293,56 +283,6 @@ void main() { // At least ch0_1 and ch1_1 have children expect(withSlash, isNotEmpty); }); - - test('hasRegexChars is false for plain text', () { - expect(HierarchyService.hasRegexChars('clk'), isFalse); - }); - - test('hasRegexChars detects * glob', () { - expect(HierarchyService.hasRegexChars('c*'), isTrue); - }); - - test('hasRegexChars detects ? glob', () { - expect(HierarchyService.hasRegexChars('cl?'), isTrue); - }); - - test('hasRegexChars detects character class', () { - expect(HierarchyService.hasRegexChars('[a-z]'), isTrue); - }); - - test('hasRegexChars detects group alternation', () { - expect(HierarchyService.hasRegexChars('(a|b)'), isTrue); - }); - - test('hasRegexChars detects + quantifier', () { - expect(HierarchyService.hasRegexChars('a+'), isTrue); - }); - - test('longestCommonPrefix finds shared prefix', () { - expect( - HierarchyService.longestCommonPrefix([ - 'FilterBank/ch0', - 'FilterBank/ch1', - ]), - 'FilterBank/ch', - ); - }); - - test('longestCommonPrefix returns null for empty list', () { - expect(HierarchyService.longestCommonPrefix([]), isNull); - }); - - test('longestCommonPrefix returns null for no common prefix', () { - expect(HierarchyService.longestCommonPrefix(['abc', 'xyz']), isNull); - }); - - test('longestCommonPrefix is case-sensitive', () { - final prefix = HierarchyService.longestCommonPrefix([ - 'Filter/abc', - 'Filter/abd', - ]); - expect(prefix, 'Filter/ab'); - }); }); // ─────────────── HierarchySearchController ─────────────── @@ -456,18 +396,6 @@ void main() { }); }); - // ─────────────── BaseHierarchyAdapter edge case ─────────────── - // The real uninitialized-root StateError test lives in - // coverage_gaps_test.dart. Here we just verify fromTree works. - - group('BaseHierarchyAdapter — fromTree produces usable root', () { - test('fromTree immediately sets root', () { - final tree = HierarchyOccurrence(name: 'r'); - final svc = BaseHierarchyAdapter.fromTree(tree); - expect(svc.root.name, 'r'); - }); - }); - // ─────────────── Multiple instantiation (dedup) ─────────────── group('Multiple instantiation — FilterChannel dedup', () { diff --git a/packages/rohd_hierarchy/test/hierarchy_model_test.dart b/packages/rohd_hierarchy/test/hierarchy_model_test.dart new file mode 100644 index 000000000..9ef3485c6 --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_model_test.dart @@ -0,0 +1,71 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_model_test.dart +// Cross-platform hierarchy model and utility tests. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('HierarchyOccurrence primitive detection', () { + test(r'isPrimitiveType is true for $-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType(r'$mux'), isTrue); + expect(HierarchyOccurrence.isPrimitiveType(r'$and'), isTrue); + }); + + test(r'isPrimitiveType is false for non-$-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType('FilterBank'), isFalse); + }); + + test('isPrimitiveType is false for empty string', () { + expect(HierarchyOccurrence.isPrimitiveType(''), isFalse); + }); + }); + + group('HierarchyService search utilities', () { + test('hasRegexChars is false for plain text', () { + expect(HierarchyService.hasRegexChars('clk'), isFalse); + }); + + test('hasRegexChars detects glob and regex syntax', () { + for (final query in ['c*', 'cl?', '[a-z]', '(a|b)', 'a+']) { + expect(HierarchyService.hasRegexChars(query), isTrue); + } + }); + + test('longestCommonPrefix finds shared prefix', () { + expect( + HierarchyService.longestCommonPrefix([ + 'FilterBank/ch0', + 'FilterBank/ch1', + ]), + 'FilterBank/ch', + ); + }); + + test('longestCommonPrefix returns null without a shared prefix', () { + expect(HierarchyService.longestCommonPrefix([]), isNull); + expect(HierarchyService.longestCommonPrefix(['abc', 'xyz']), isNull); + }); + + test('longestCommonPrefix is case-sensitive', () { + expect( + HierarchyService.longestCommonPrefix(['Filter/abc', 'Filter/abd']), + 'Filter/ab', + ); + }); + }); + + group('BaseHierarchyAdapter', () { + test('fromTree immediately sets root', () { + final service = + BaseHierarchyAdapter.fromTree(HierarchyOccurrence(name: 'r')); + + expect(service.root.name, 'r'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/occurrence_trie_test.dart b/packages/rohd_hierarchy/test/occurrence_trie_test.dart index 7a3bbf678..3faa8a92a 100644 --- a/packages/rohd_hierarchy/test/occurrence_trie_test.dart +++ b/packages/rohd_hierarchy/test/occurrence_trie_test.dart @@ -16,9 +16,9 @@ void main() { const first = OccurrenceAddress([0, 2, 4]); const second = OccurrenceAddress([0, 2, 5]); - trie[first] = 'first'; + expect(trie.set(first, 'first'), isNull); trie[OccurrenceAddress.root] = 'root'; - trie[second] = 'second'; + expect(trie.set(second, 'second'), isNull); expect(trie[OccurrenceAddress.root], 'root'); expect(trie[first], 'first'); @@ -30,8 +30,9 @@ void main() { final trie = OccurrenceTrie(); const first = OccurrenceAddress([0, 2, 4]); const second = OccurrenceAddress([0, 2, 5]); - trie[first] = 'first'; - trie[second] = 'second'; + trie + ..set(first, 'first') + ..set(second, 'second'); expect(trie.remove(first), 'first'); expect(trie[first], isNull); diff --git a/packages/rohd_source_navigator/lib/dtd_service.dart b/packages/rohd_source_navigator/lib/dtd_service.dart index b749cfdc4..4dfe3cce3 100644 --- a/packages/rohd_source_navigator/lib/dtd_service.dart +++ b/packages/rohd_source_navigator/lib/dtd_service.dart @@ -70,7 +70,7 @@ class DtdService { } on Exception catch (e) { _peer = null; _channel = null; - // DTD connections can run without a host-provided logger. + // Report the failed optional DTD connection without requiring a logger. // ignore: avoid_print print('[DtdService] Failed to connect to DTD at $uri: $e'); return false; diff --git a/packages/rohd_waveform/analysis_options.yaml b/packages/rohd_waveform/analysis_options.yaml index f04c6cf0f..a96029588 100644 --- a/packages/rohd_waveform/analysis_options.yaml +++ b/packages/rohd_waveform/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: ../../analysis_options.yaml diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart index 7701234b6..0165d152c 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/tree_service.dart @@ -17,12 +17,8 @@ import 'package:vm_service/vm_service.dart'; /// Service helpers for evaluating and filtering the ROHD module tree. class TreeService { - /// Primary expression for hierarchy JSON — available in all ROHD versions - /// that ship inspector_service.dart (i.e. main and later). - static const _primaryInvokeFunc = 'ModuleTree.instance.hierarchyJSON'; - - /// Fallback kept for any pre-inspector ROHD target. - static const _legacyInvokeFunc = 'ModuleTree.instance.hierarchyJSON'; + /// Expression for retrieving the module hierarchy JSON. + static const _hierarchyExpression = 'ModuleTree.instance.hierarchyJson'; /// Eval wrapper for accessing ROHD code in the target isolate. final EvalOnDartLibrary rohdControllerEval; @@ -68,19 +64,14 @@ class TreeService { } Future _evalTreePayload() async { - final expressions = [_primaryInvokeFunc, _legacyInvokeFunc]; - - for (final expression in expressions) { - try { - final treeInstance = await rohdControllerEval.evalInstance(expression, - isAlive: evalDisposable); - return treeInstance.valueAsString; - } on Exception catch (e) { - debugPrint('[TreeService] Eval failed for "$expression": $e'); - } + try { + final treeInstance = await rohdControllerEval + .evalInstance(_hierarchyExpression, isAlive: evalDisposable); + return treeInstance.valueAsString; + } on Exception catch (e) { + debugPrint('[TreeService] Eval failed for "$_hierarchyExpression": $e'); + return null; } - - return null; } /// Returns whether the current module or any descendant matches the search. diff --git a/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart b/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart index 7c35ef0a8..948d8ca3f 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/services/vm_service_signal_value_source.dart @@ -19,7 +19,7 @@ import 'package:vm_service/vm_service.dart' as vm; /// VM-backed [SignalValueSource] that refreshes on debugger pause events. class VmServiceSignalValueSource implements SignalValueSource { static const _moduleTreeHierarchyExpression = - 'ModuleTree.instance.hierarchyJSON'; + 'ModuleTree.instance.hierarchyJson'; static const _currentTimeExpressions = [ 'WaveformService.instance.currentTime', @@ -193,10 +193,10 @@ class VmServiceSignalValueSource implements SignalValueSource { } static String _waveformSnapshotExpression(int time) => - 'WaveformService.instance.getSnapshotCompactJSON($time)'; + 'WaveformService.instance.getSnapshotCompactJson($time)'; static const _moduleTreeSignalValuesExpression = - 'ModuleTree.instance.signalValuesJSON'; + 'ModuleTree.instance.signalValuesJson'; Future _readCurrentTimeFromExtension() async { final response = await _callExtension(_currentTimeExtension); diff --git a/rohd_extension/README.md b/rohd_extension/README.md index a57170778..37be37362 100644 --- a/rohd_extension/README.md +++ b/rohd_extension/README.md @@ -83,7 +83,7 @@ completions below narrow the ROHD-specific options by cursor location. | Prefix | Expands to | Description | |--------|-----------|-------------| | `mod`, `Module` | `class … extends Module { … }` | Module scaffold with `clk`, `reset`, `a`/`b` inputs, `depth`, `latchData`, `addInput`/`addOutput`, `definitionName`, and instance naming parameters | -| `sim`, `Simulator` | Clock, reset, `WaveDumper`, `Simulator.run()` | Simulation / testbench boilerplate | +| `sim`, `Simulator` | Clock, reset, `dumpWaves`, `Simulator.run()` | Simulation / testbench boilerplate | | `fsmModule`, `FSMModule` | enum + `class extends Module` + `FiniteStateMachine` | Full standalone FSM module scaffold | | `vf`, `tb`, `testbench` | `rohd_vf` testbench | Agent / Driver / Monitor / Sequencer template | diff --git a/rohd_extension/snippets/rohd.json b/rohd_extension/snippets/rohd.json index 5c3c9ae73..463353e65 100644 --- a/rohd_extension/snippets/rohd.json +++ b/rohd_extension/snippets/rohd.json @@ -50,7 +50,7 @@ "body": [ "final clk = SimpleClockGenerator(10).clk;", "final reset = Logic(name: 'reset');", - "WaveDumper(module, outputPath: 'wavedumpername.vcd');", + "module.dumpWaves(outputPath: 'waves.vcd');", "Simulator.setMaxSimTime(100);", "unawaited(Simulator.run());", "", @@ -158,8 +158,8 @@ " // Build the DUT", " await tb.dut.build();", "", - " // Attach a waveform dumper to the DUT", - " WaveDumper(tb.dut);", + " // Enable waveform dumping for the DUT", + " tb.dut.dumpWaves();", "", " // Set a maximum simulation time so it doesn't run forever", " Simulator.setMaxSimTime(300);", diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 0c79b531a..07e4b43a7 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -2339,7 +2339,7 @@ void main() { test('simple 1d collapse', () async { final mod = SimpleLAPassthrough(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign laOut = laIn;')); }); @@ -2347,7 +2347,7 @@ void main() { test('array collapse for cross-module connection', () async { final mod = ArrayTopMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains(RegExp(r'ArraySubModIn.*\.inp\(inp\)'))); expect(sv, contains(RegExp(r'ArraySubModOut.*\.arrOut\(inp\)'))); @@ -2358,7 +2358,7 @@ void main() { LogicArray([3, 3], 1), LogicArray([3, 3], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -2375,7 +2375,7 @@ void main() { test('partial array assignments collapse into range assignment', () async { final mod = PartialArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[4:2] = src[4:2];')); @@ -2397,7 +2397,7 @@ void main() { () async { final mod = ChainedPartialArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[4:2] = src[4:2];')); @@ -2419,7 +2419,7 @@ void main() { () async { final mod = ChainedSubrangeArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[3:2] = src[6:5];')); @@ -2444,7 +2444,7 @@ void main() { test('three-deep chained range assignments collapse iteratively', () async { final mod = ThreeDeepChainedPartialArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[4:2] = src[4:2];')); @@ -2467,7 +2467,7 @@ void main() { () async { final mod = LongChainedPartialArrayRangeAssignment(); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog()); expect(topBody, contains('assign dst[4:2] = src[4:2];')); expect(topBody, isNot(contains('intermediate'))); @@ -2487,7 +2487,7 @@ void main() { test('multi-use chained range intermediate stays expanded', () async { final mod = ChainedPartialArrayRangeAssignment(exposeIntermediate: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -2510,7 +2510,7 @@ void main() { test('renameable chained range intermediate stays expanded', () async { final mod = ChainedPartialArrayRangeAssignment(intermediateNaming: null); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -2533,7 +2533,7 @@ void main() { () async { final mod = PartialBusToArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[5:2] = src[5:2];')); @@ -2558,7 +2558,7 @@ void main() { test('full array-to-bus assignSubset has no subset intermediate', () async { final mod = ArrayToBusAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('_subset'))); @@ -2577,7 +2577,7 @@ void main() { () async { final mod = ArrayToBusAssignSubsetRangeAssignment(partial: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[5:2] = src[5:2];')); @@ -2614,7 +2614,7 @@ void main() { driveLowBits: config.driveLowBits, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); if (config.driveLowBits) { @@ -2651,7 +2651,7 @@ void main() { test('bus subset helpers with extra consumers are preserved', () async { final mod = BusSubsetBitsWithExtraConsumers(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[3:0] = src[5:2];')); @@ -2673,7 +2673,7 @@ void main() { test('partial slice helper with extra consumer is preserved', () async { final mod = PartialSliceWithExtraConsumer(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[8:5] = src[9:6];')); @@ -2704,7 +2704,7 @@ void main() { test('sparse bus runs feeding assignSubset collapse independently', () async { final mod = SparseBusRunsToAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[31:20] = srcA[15:4];')); @@ -2737,7 +2737,7 @@ void main() { test('constant-backed upper range remains tied off after collapse', () async { final mod = TiedRangeToAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2762,7 +2762,7 @@ void main() { tieNaming: tieNaming, ); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog()); expect(topBody, contains('logic [7:0] tie;')); expect(topBody, contains("assign tie = 8'h0;")); @@ -2781,7 +2781,7 @@ void main() { busNaming: Naming.renameable, ); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog()); expect(topBody, contains('logic [31:0] bus;')); expect(topBody, contains('.data(bus)')); @@ -2797,7 +2797,7 @@ void main() { test('constant-backed range concatenates with sibling output', () async { final mod = TiedSiblingRangeToAssignSubsetAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2818,7 +2818,7 @@ void main() { test('constant-backed range concatenates into late child input', () async { final mod = TiedSiblingRangeToLateInputSource(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2838,7 +2838,7 @@ void main() { test('named constant subsets survive scalar output collapse', () async { final mod = ScalarSiblingOutputsWithNamedTieTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2867,7 +2867,7 @@ void main() { () async { final mod = InteriorNamedTieWithMappedOutputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains("assign bus[6:4] = 3'h0;")); @@ -2902,7 +2902,7 @@ void main() { fanout: fanout, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.data(({')); @@ -2940,7 +2940,7 @@ void main() { () async { final mod = InvalidConstantsToAssignSubsetTop(); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog()); expect(topBody, contains("2'bxx")); expect(topBody, isNot(contains("2'bzz"))); @@ -2966,7 +2966,7 @@ void main() { () async { final mod = InternalBusRunsToAssignSubsetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[44:13] = src[31:0];')); @@ -3000,7 +3000,7 @@ void main() { computedSource: true, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[44:13] = srcStage[31:0];')); @@ -3030,7 +3030,7 @@ void main() { () async { final mod = WideTemporarySliceToArrayWords(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign y[0][15:0] = src[47:32];')); @@ -3059,7 +3059,7 @@ void main() { () async { final mod = WideTemporarySliceToArrayWords(extraConsumers: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign y[0][15:0] = src[47:32];')); @@ -3087,7 +3087,7 @@ void main() { () async { final mod = ManualSubsetNamedArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('manual_subset')); @@ -3109,7 +3109,7 @@ void main() { test('reordered bus-to-array assignments stay expanded', () async { final mod = PartialBusToArrayRangeAssignment(reversed: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[5:2] = src[5:2];'))); @@ -3137,7 +3137,7 @@ void main() { test('bus-to-unpacked-array assignments stay expanded', () async { final mod = PartialBusToArrayRangeAssignment(numUnpackedDimensions: 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[5:2] = src[5:2];'))); @@ -3164,7 +3164,7 @@ void main() { test('non-contiguous partial array assignments stay expanded', () async { final mod = PartialArrayRangeAssignment(reversed: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2]'))); @@ -3188,7 +3188,7 @@ void main() { () async { final mod = PartialInnerArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[1][3:1] = src[1][3:1];')); @@ -3213,7 +3213,7 @@ void main() { test('unpacked outer dimension still collapses inner packed range', () async { final mod = PartialInnerArrayRangeAssignment(numUnpackedDimensions: 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('assign dst[1][3:1] = src[1][3:1];')); @@ -3237,7 +3237,7 @@ void main() { test('unpacked one-dimensional partial assignments stay expanded', () async { final mod = PartialUnpackedArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -3259,7 +3259,7 @@ void main() { test('wide element partial array assignments stay expanded', () async { final mod = PartialWideArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[2:1] = src[2:1];'))); @@ -3285,7 +3285,7 @@ void main() { test('net array partial assignments stay in net connection flow', () async { final mod = PartialNetArrayRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -3308,7 +3308,7 @@ void main() { () async { final mod = PartialLogicNetRangeAssignment(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('assign dst[4:2] = src[4:2];'))); @@ -3330,7 +3330,7 @@ void main() { final mod = ArrayWithShuffledAssignment(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign b[0] = a[3];')); expect(sv, contains('assign b[3] = a[0];')); @@ -3347,7 +3347,7 @@ void main() { LogicArray([3, 3], 1, numUnpackedDimensions: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('net_connect #(.WIDTH(9)) net_connect (intermediate, a);')); expect(sv, @@ -3364,7 +3364,7 @@ void main() { final mod = ArrayModule(LogicArray([4, 4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign d = c[0];')); expect(sv, contains('assign b = a;')); @@ -3391,7 +3391,7 @@ void main() { name: 'constant_leaf_array_assignment_${cfg.name.replaceAll(' ', '_')}', ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); for (final row in [0, 1]) { @@ -3457,7 +3457,7 @@ void main() { elementWidth: cfg.elementWidth, reversed: cfg.reversed); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // the intermediate array (and every declaration of it) must be gone expect(sv, isNot(contains('arr'))); @@ -3492,7 +3492,7 @@ void main() { LogicNet(width: total), LogicNet(width: total), dimensions: cfg.dimensions, elementWidth: cfg.elementWidth); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // the intermediate array and its net_connects must be gone expect(sv, isNot(contains('arr'))); @@ -3516,7 +3516,7 @@ void main() { final mod = PartiallyDrivenArray(Logic(width: total - 2), dimensions: dimensions); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // the array must remain declared since undriven bits must stay `z` expect(sv, contains('arr')); @@ -3533,7 +3533,7 @@ void main() { test('aggregate-used array is not inlined', () async { final mod = ArrayElementsWithAggregateUse(Logic(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // the array stays (aggregate use), so elements are not inlined into ports expect(sv, contains('arr')); @@ -3550,7 +3550,7 @@ void main() { test('input-array port elements are not inlined away', () async { final mod = ArrayPortElementsToSubmodules(LogicArray([2, 2], 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // the array port must remain declared expect(sv, contains('a')); @@ -3583,7 +3583,7 @@ void main() { () async { final mod = ConstantToSingleElementArrayInputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains(".data((8'h0))")); @@ -3600,7 +3600,7 @@ void main() { () async { final mod = ConstantToSingleElementArrayInputTop(value: 0xa5); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains(".data((8'ha5))")); @@ -3634,7 +3634,7 @@ void main() { elementWidth: cfg.elementWidth, perm: cfg.perm); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the intermediate array (and every per-element assignment) is gone, @@ -3671,7 +3671,7 @@ void main() { const n = 4; final mod = MergedSourcesToArrayPort(List.generate(n, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('intermediate'))); @@ -3697,7 +3697,7 @@ void main() { () async { final mod = RangeSourcesToArrayPort(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains(RegExp(r'\.a\(\(\{\s*src,')))); @@ -3740,7 +3740,7 @@ void main() { List.generate(cfg.n, (_) => LogicNet()), LogicNet(width: cfg.n), perm: cfg.perm); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the intermediate array and its net_connects are gone, replaced by a @@ -3771,7 +3771,7 @@ void main() { // restriction prevents collapsing and the array stays declared final mod = MultiUseAggregate(List.generate(4, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // with two whole-array uses, the array stays declared and its per-element @@ -3816,7 +3816,7 @@ void main() { final mod = ArrayPortToIndividualNets( List.generate(4, (_) => LogicNet()), LogicNet(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the intermediate array and its net_connects collapse into a single @@ -3843,7 +3843,7 @@ void main() { // result must still be correct. final mod = RearrangeOneArray(LogicArray([4], 1)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // this pass did not fabricate a consolidating concatenation on the port @@ -3871,7 +3871,7 @@ void main() { final mod = IndividualSignalsToExpressionlessPort( List.generate(4, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // no inline concatenation on the expressionless port; per-element @@ -3897,7 +3897,7 @@ void main() { () async { final mod = WholeNetBusCollapseNamingCollision(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('bussubset ('))); @@ -3919,7 +3919,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: busNaming); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); if (busNaming == Naming.mergeable) { @@ -3952,7 +3952,7 @@ void main() { final mod = WholeNetBusToPortWithInlineSubsetConsumer( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.data')); @@ -3978,7 +3978,7 @@ void main() { final mod = WholeNetBusToPortWithReadOnlyInlineSubsetConsumer(LogicNet(width: n)); await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog()); expect(topBody, contains('wire [3:0] bus')); expect(topBody, contains(RegExp('net_connect.*_subset_0_0_bus'))); @@ -3991,7 +3991,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: Naming.reserved); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // a reserved name must be preserved, so the bus and its net_connects stay @@ -4016,7 +4016,7 @@ void main() { final mod = WholeNetBusMultiUse(List.generate(n, (_) => LogicNet()), LogicNet(width: n), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // used as a whole twice, so the single-use restriction keeps the bus @@ -4048,7 +4048,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: busNaming); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); if (busNaming == Naming.mergeable) { @@ -4082,7 +4082,7 @@ void main() { final mod = BitwiseNetBusToArrayPortWithInlineSubsetConsumer( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.data')); @@ -4110,7 +4110,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: Naming.reserved); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // a reserved bus name must be preserved, so it is not traced away expect(sv, contains('bus')); @@ -4138,7 +4138,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), toArray: toArray); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the self-connection leaves a per-bit net_connect structure intact @@ -4169,7 +4169,7 @@ void main() { () async { final mod = PureSelfLoopNetBus(LogicNet(width: 2), toArray: toArray); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the bus collapses into an inline concatenation of the merged net, and @@ -4192,7 +4192,7 @@ void main() { final mod = AssignSubsetReceiver( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // no intermediate subset array, and no per-bit net_connects remain @@ -4217,7 +4217,7 @@ void main() { final mod = AssignSubsetReceiverScrambled( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('_subset'))); @@ -4243,7 +4243,7 @@ void main() { List.generate(n, (_) => LogicNet()), LogicNet(width: n), busNaming: Naming.renameable); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the named bus is preserved, but the per-bit `*_subset` pass-through and @@ -4271,7 +4271,7 @@ void main() { final mod = AssignSubsetDriver( List.generate(n, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the per-bit `*_subset` pass-throughs and per-bit `net_connect`s are @@ -4296,7 +4296,7 @@ void main() { const n = 4; final mod = AssignSubsetLogicDriver(List.generate(n, (_) => Logic())); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // the intermediate `sig_subset` array is forwarded straight into the @@ -4321,7 +4321,7 @@ void main() { () async { final mod = LateSubsetInputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4339,7 +4339,7 @@ void main() { () async { final mod = LateSlicedSubsetInputTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4447,7 +4447,7 @@ void main() { test('sibling output can drive subset of sibling input source', () async { final mod = SiblingOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4464,7 +4464,7 @@ void main() { test('sibling full output can drive sibling full input source', () async { final mod = SiblingFullOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4481,7 +4481,7 @@ void main() { test('sibling output stays connected beside range assignments', () async { final mod = SiblingOutputWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.result(bus[7])')); @@ -4507,7 +4507,7 @@ void main() { () async { final mod = IndexedSiblingOutputWithRangeAssignmentsTop(outputIndex); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, contains('.result(bus[$outputIndex])')); @@ -4534,7 +4534,7 @@ void main() { test('multiple sibling outputs stay connected in packed concat', () async { final mod = MultipleSiblingOutputsWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4566,7 +4566,7 @@ void main() { () async { final mod = FanoutSiblingOutputWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4592,7 +4592,7 @@ void main() { test('wide sibling output stays connected after range collapse', () async { final mod = WideSiblingOutputWithRangeAssignmentsTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4617,7 +4617,7 @@ void main() { test('wide sibling output keeps fanout between constant ranges', () async { final mod = WideSiblingOutputWithConstantsAndFanoutTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.result()'))); @@ -4644,7 +4644,7 @@ void main() { () async { final mod = SiblingArrayOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4662,7 +4662,7 @@ void main() { () async { final mod = SiblingStructOutputToInputSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4679,7 +4679,7 @@ void main() { test('sibling inout can drive subset of sibling inout source', () async { final mod = SiblingInOutToInOutSubsetTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); expect(topBody, isNot(contains('.data()'))); @@ -4696,7 +4696,7 @@ void main() { test('sibling boundary kitchen sink keeps mixed source mappings', () async { final mod = SiblingBoundaryProductTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); for (final portName in [ @@ -4737,7 +4737,7 @@ void main() { final mod = AssignSubsetPartial( List.generate(n ~/ 2, (_) => LogicNet()), LogicNet(width: n)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final topBody = _topModuleBody(sv); // not every element is a pass-through, so the subset array is preserved @@ -4841,7 +4841,7 @@ void main() { } await mod.build(); - final topBody = _topModuleBody(mod.generateSynth()); + final topBody = _topModuleBody(mod.dumpSystemVerilog()); // --- structural expectations (only where confidently predictable) --- if (config.noSubset) { diff --git a/test/benchmark_test.dart b/test/benchmark_test.dart index a65020166..c660b505e 100644 --- a/test/benchmark_test.dart +++ b/test/benchmark_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2024 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // benchmark_test.dart diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..4133ff71c 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // bus_test.dart @@ -228,7 +228,7 @@ void main() { final mod = SingleBitBusSubsetMod(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign result = oneBit')); final vectors = [ @@ -401,7 +401,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv.contains("assign const_subset = 16'habcd;"), true); }); }); diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 8128eea2d..ead337233 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -66,7 +66,7 @@ void main() { test('collapse pretty', () async { final mod = CollapseTestModule(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // make sure e=a&b&c is in there, to prove there was some inlining expect(sv, contains(RegExp('e.*=.*a.*&.*b.*&.*c'))); @@ -78,7 +78,7 @@ void main() { final mod = CombinationalLoopCollapseModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains(' | a')); expect(sv, contains(' == a')); }); diff --git a/test/config_test.dart b/test/config_test.dart index beca0b576..ad2bc306f 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -14,7 +14,7 @@ import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; -import 'wave_dumper_test.dart'; +import 'waveform_service_test.dart'; class SimpleModule extends Module { SimpleModule(Logic a, Logic b) { @@ -46,6 +46,21 @@ void main() { final mod = SimpleModule(Logic(), Logic()); await mod.build(); + final sv = mod.dumpSystemVerilog(); + + expect(sv, contains(version)); + }); + + test( + 'should contains ROHD version number when deprecated synth is generated.', + () async { + const version = Config.version; + + final mod = SimpleModule(Logic(), Logic()); + await mod.build(); + + // This test verifies that the deprecated API still includes the version. + // ignore: deprecated_member_use_from_same_package final sv = mod.generateSynth(); expect(sv, contains(version)); diff --git a/test/const_radix_test.dart b/test/const_radix_test.dart index 4e9beb4eb..0c7aa1374 100644 --- a/test/const_radix_test.dart +++ b/test/const_radix_test.dart @@ -140,7 +140,7 @@ void main() { final module = _ConstRadixModule(); await module.build(); - final systemVerilog = module.generateSynth(); + final systemVerilog = module.dumpSystemVerilog(); expect(systemVerilog, contains("assign autoHex = 8'h2a;")); expect(systemVerilog, contains("assign binaryValue = 8'b101010;")); @@ -154,7 +154,7 @@ void main() { final module = _ExpressionlessRadixTop(); await module.build(); - final systemVerilog = module.generateSynth(); + final systemVerilog = module.dumpSystemVerilog(); expect(systemVerilog, contains("assign in = 8'd42;")); expect(systemVerilog, contains('.in(in)')); diff --git a/test/counter_test.dart b/test/counter_test.dart index 8f59b58d7..2fa6a9f5d 100644 --- a/test/counter_test.dart +++ b/test/counter_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // counter_test.dart @@ -48,7 +48,7 @@ void main() { final reset = Logic(); final counter = Counter(Logic(), reset); await counter.build(); - // WaveDumper(counter); + // counter.dumpWaves(); unawaited(reset.nextPosedge .then((value) => expect(counter.val.value.toInt(), equals(0)))); diff --git a/test/counter_wintf_test.dart b/test/counter_wintf_test.dart index 7889369cc..680e33120 100644 --- a/test/counter_wintf_test.dart +++ b/test/counter_wintf_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // counter_wintf_test.dart @@ -145,7 +145,7 @@ void main() { test('interface ports dont get doubled up', () async { final mod = Counter(CounterInterface(8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(!sv.contains('en_0'), true); }); diff --git a/test/external_test.dart b/test/external_test.dart index 09ac84c87..29b882a5e 100644 --- a/test/external_test.dart +++ b/test/external_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2024 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // external_test.dart @@ -31,7 +31,7 @@ void main() { test('instantiate', () async { final mod = TopModule(Logic(width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // make sure we instantiate the external module properly expect( diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..ea80c79ba 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2024 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // fsm_test.dart @@ -183,7 +183,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains("b = 1'h0;")); }); @@ -192,7 +192,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('priority case')); }); @@ -201,7 +201,7 @@ void main() { final mod = TestModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('MyStates_state1 : begin')); }); diff --git a/test/gate_test.dart b/test/gate_test.dart index b2c47c21e..da5307b4e 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -396,7 +396,7 @@ void main() { expect(alias.value, LogicValue.zero); await module.build(); - expect(module.generateSynth(), contains("1'h0")); + expect(module.dumpSystemVerilog(), contains("1'h0")); }); test('bitwise NOT folds Const inputs', () { @@ -753,7 +753,7 @@ void main() { useExplicitConstShiftModules: true, ); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = gtm.dumpSystemVerilog(); expect(sv, isNot(contains("0'h0"))); diff --git a/test/inout_loopback_test.dart b/test/inout_loopback_test.dart index 0c3b5b343..3cbf45cb1 100644 --- a/test/inout_loopback_test.dart +++ b/test/inout_loopback_test.dart @@ -237,7 +237,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // The outer module should NOT contain an internal net_connect // for the loopback — the submodule ports should just be wired to the @@ -268,7 +268,7 @@ void main() { final mod = SimpleOuterLoopback(LogicNet(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final outerModuleSv = _extractModuleSv(sv, 'simpleOuter'); expect(outerModuleSv, isNot(contains('net_connect')), @@ -284,7 +284,7 @@ void main() { final mod = LoopbackPairTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // Check for net_connect in the top module final topModuleSv = _extractModuleSv(sv, 'LoopbackPairTop'); @@ -309,7 +309,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // The inner module SHOULD have a net_connect (connecting ioA <= ioB). final innerModuleSv = _extractModuleSv(sv, 'innerConnected'); @@ -347,7 +347,7 @@ void main() { final mod = OuterClkLoopback(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // The outer module should NOT have a net_connect — the loopback net // is only used as port connections in the inner instantiation. diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 5fa2545e0..0d939a1ac 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -774,7 +774,7 @@ void main() { final vectors = passthroughVectors(mod.laOut.width); if (checkNoSwizzle) { - expect(mod.generateSynth().contains('swizzle'), false, + expect(mod.dumpSystemVerilog().contains('swizzle'), false, reason: 'Expected no swizzles but found one.'); } @@ -824,7 +824,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv.contains(RegExp(r'\[7:0\]\s*laIn\s*\[2:0\]')), true); expect(sv.contains(RegExp(r'\[7:0\]\s*laOut\s*\[2:0\]')), true); }); @@ -845,7 +845,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv.contains(RegExp( r'\[2:0\]\s*\[1:0\]\s*\[7:0\]\s*laIn\s*\[4:0\]\s*\[3:0\]')), @@ -873,7 +873,7 @@ void main() { await testArrayPassthrough(mod); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('input logic [2:0][1:0][2:0][7:0] laIn')); expect(sv, contains('output logic [2:0][1:0][2:0][7:0] laOut')); }); @@ -888,7 +888,7 @@ void main() { await testArrayPassthrough(mod, noSvSim: true); // ensure ports with interface are still an array - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('input logic [1:0][2:0][7:0] laIn [2:0]')); expect(sv, contains('output logic [1:0][2:0][7:0] laOut [2:0]')); }); @@ -955,7 +955,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv.contains('logic [2:0][3:0][7:0] intermediate [1:0]'), true); }); }); @@ -1008,7 +1008,7 @@ void main() { test('3d', () async { final mod = SimpleArraysAndHierarchy(LogicArray([2], 8)); await testArrayPassthrough(mod); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('SimpleLAPassthrough simple_la_passthrough')); }); @@ -1019,7 +1019,7 @@ void main() { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); - expect(mod.generateSynth(), contains('SimpleLAPassthrough')); + expect(mod.dumpSystemVerilog(), contains('SimpleLAPassthrough')); }); }); @@ -1028,7 +1028,7 @@ void main() { final mod = FancyArraysAndHierarchy(LogicArray([4, 3, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // make sure the 4th one is there (since we expect 4) expect(sv, contains('SimpleLAPassthrough simple_la_passthrough_2')); @@ -1070,7 +1070,8 @@ void main() { final mod = WithSetArrayOffsetModule(LogicArray([2, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); // make sure we're reassigning both times it overlaps! expect( diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 3447847c5..e0a2c2d12 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -223,13 +223,13 @@ void main() { final mod = LogicWithInternalSignalModule(Logic()); await mod.build(); - expect(mod.generateSynth(), contains('shouldExist')); + expect(mod.dumpSystemVerilog(), contains('shouldExist')); }); test('unconnected port does not duplicate internal signal', () async { final pMod = ParentMod(Logic(), Logic()); await pMod.build(); - final sv = pMod.generateSynth(); + final sv = pMod.dumpSystemVerilog(); expect(RegExp('logic a[,;\n]').allMatches(sv).length, 2); }); @@ -237,7 +237,7 @@ void main() { test('assigns and gates', () async { final mod = SensitiveNaming(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('e = a & d')); expect(sv, contains('b = a')); expect(sv, contains('d = c')); @@ -246,7 +246,7 @@ void main() { test('bus subset', () async { final mod = BusSubsetNaming(Logic(width: 32)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('c = b[3]')); }); }); @@ -255,7 +255,7 @@ void main() { test('unconnected floating', () async { final mod = DrivenOutputModule(null); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // shouldn't add a Z in there if left floating expect(!sv.contains('z'), true); @@ -264,7 +264,7 @@ void main() { test('driven to z', () async { final mod = DrivenOutputModule(Const('z')); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // should add a Z if it's explicitly added expect(sv, contains('z')); @@ -277,7 +277,7 @@ void main() { portANaming: Naming.renameable, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, @@ -293,7 +293,7 @@ void main() { () async { final mod = NameCollisionArrayTop(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, @@ -310,7 +310,7 @@ void main() { await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); expect(sv, contains('_wow_______')); }); @@ -319,7 +319,7 @@ void main() { final mod = StructElementNamingModule(VariousNamingStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign outp[0] = outp_renameable;')); expect(sv, contains('assign outp[1] = reserved_outp;')); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index 695942bbe..6ae500016 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -175,7 +175,7 @@ void main() { final mod = StructModuleWithInstrumentation(Const(0, width: 2)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv.contains('swizzle'), isFalse, reason: 'Should not pack from instrumentation!'); diff --git a/test/logic_test.dart b/test/logic_test.dart index cd3169cab..21a8299a7 100644 --- a/test/logic_test.dart +++ b/test/logic_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_test.dart diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..aabb7f490 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // math_test.dart @@ -91,7 +91,7 @@ void main() { final mod = AddWithCarryMod(Logic(width: 8), Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign {carry, sum} = a + b')); }); @@ -119,7 +119,7 @@ void main() { final gtm = MathTestModule(Logic(width: 8), Logic(width: 8)); await gtm.build(); - final sv = gtm.generateSynth(); + final sv = gtm.dumpSystemVerilog(); final lines = sv.split('\n'); // ensure we never lshift by a constant directly diff --git a/test/module_merging_test.dart b/test/module_merging_test.dart index 2aa1eb2de..81ed42df0 100644 --- a/test/module_merging_test.dart +++ b/test/module_merging_test.dart @@ -91,7 +91,7 @@ void main() { () async { final dut = TrunkWithLeaves(Logic(), Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); expect('module ComplicatedLeaf'.allMatches(sv).length, 1); }); @@ -99,7 +99,7 @@ void main() { test('different reserved definition name modules stay separate', () async { final dut = ParentOfDifferentModuleDefNames(Logic()); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); expect(sv, contains('module def1')); expect(sv, contains('module def2')); diff --git a/test/module_services_test.dart b/test/module_services_test.dart new file mode 100644 index 000000000..1db39572b --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,306 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Unit tests for ModuleServices, the service base types, and +// SystemVerilogService. +// +// 2026 April 25 Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; +import 'package:test/test.dart'; + +class SimpleModule extends Module { + SimpleModule(Logic a) : super(name: 'simple') { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// A minimal [ModuleService] used to exercise the type-keyed registry. +class FakeService implements ModuleService { + FakeService(this.module); + + @override + final Module module; + + @override + Map toJson() => {'kind': 'fake'}; +} + +void main() { + tearDown(() { + SystemVerilogService.current = null; + ModuleServices.instance.reset(); + }); + + group('ModuleServices registry', () { + test('rootModule is set after build', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(ModuleServices.instance.rootModule, equals(mod)); + }); + + test('hierarchyJson returns valid JSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final json = ModuleServices.instance.hierarchyJson; + expect(() => jsonDecode(json), returnsNormally); + }); + + test('legacy hierarchyJSON returns the current hierarchy JSON', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + expect( + // This verifies that the deprecated compatibility alias still works. + // ignore: deprecated_member_use_from_same_package + ModuleTree.instance.hierarchyJSON, + equals(ModuleTree.instance.hierarchyJson), + ); + }); + + test('register and lookup round-trips a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final fake = FakeService(mod); + ModuleServices.instance.register(fake); + expect(ModuleServices.instance.lookup(), same(fake)); + }); + + test('lookup returns null when no service registered', () { + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('unregister removes a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.register(FakeService(mod)); + ModuleServices.instance.unregister(); + expect(ModuleServices.instance.lookup(), isNull); + }); + + test('reset clears rootModule and all services', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + ModuleServices.instance.register(FakeService(mod)); + expect(ModuleServices.instance.rootModule, isNotNull); + + ModuleServices.instance.reset(); + expect(ModuleServices.instance.rootModule, isNull); + expect(ModuleServices.instance.lookup(), isNull); + }); + }); + + group('SystemVerilogService', () { + test('legacy generateSynth does not register a service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + // ignore: deprecated_member_use_from_same_package - compatibility coverage + expect(mod.generateSynth(), isNotEmpty); + expect(SystemVerilogService.current, isNull); + expect( + ModuleServices.instance.lookup(), + isNull, + ); + }); + + test('legacy generateSynth preserves ModuleNotBuiltException', () { + final mod = SimpleModule(Logic()); + + // ignore: deprecated_member_use_from_same_package - compatibility coverage + expect(mod.generateSynth, throwsA(isA())); + }); + + test('registers by default', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + + expect(SystemVerilogService.current, same(sv)); + expect( + ModuleServices.instance.lookup(), + same(sv), + ); + }); + + test('can opt out of registration', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod, register: false); + + expect(SystemVerilogService.current, isNull); + expect( + ModuleServices.instance.lookup(), + isNull, + ); + expect(sv.output, isNotEmpty); + }); + + test('is a CodeGenService', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + expect(SystemVerilogService(mod), isA()); + }); + + test('allContents is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + expect(sv.allContents, isNotEmpty); + }); + + test('output is non-empty', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + expect(sv.output, isNotEmpty); + }); + + test('artifact defaults to the module definition name', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + + final artifact = sv.artifacts.single; + + expect(artifact.fileName, equals('${mod.definitionName}.sv')); + expect(artifact.mediaType, equals('text/x-systemverilog')); + expect( + (await artifact.openRead().expand((bytes) => bytes).toList()) + .isNotEmpty, + isTrue, + ); + }); + + test('instanceTypeOutput returns the instance type contents', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + + final contents = sv.fileContents.single; + expect(sv.instanceTypeOutput(contents.name), equals(contents.contents)); + expect(sv.instanceTypeOutput('DoesNotExist'), isNull); + }); + + test('toJson lists generated modules', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final sv = SystemVerilogService(mod); + expect(sv.toJson()['modules'], isList); + }); + + test('writeOutputs creates SV files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + SystemVerilogService( + mod, + outputDirectory: dir.path, + multiFile: true, + ).writeOutputs(); + final files = dir.listSync().whereType().toList(); + expect(files, isNotEmpty); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('writeOutputs emits a single file', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + final configuredSv = SystemVerilogService( + mod, + outputDirectory: dir.path, + outputBaseName: 'out', + )..writeOutputs(); + final path = '${dir.path}/out.sv'; + expect(File(path).readAsStringSync(), equals(configuredSv.output)); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('multiFile writeOutputs emits a directory of files', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + SystemVerilogService( + mod, + outputDirectory: dir.path, + multiFile: true, + ).writeOutputs(); + final files = dir.listSync().whereType().toList(); + expect(files.any((f) => f.path.endsWith('.sv')), isTrue); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('defaults headers by output layout', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final singleFile = SystemVerilogService(mod); + final multiFile = SystemVerilogService(mod, multiFile: true); + + expect(singleFile.includeHeader, isTrue); + expect(singleFile.output, startsWith(singleFile.header)); + expect(multiFile.includeHeader, isFalse); + expect(multiFile.header, isEmpty); + }); + + test('writes headers in either output layout when requested', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('sv_test_'); + try { + final singlePath = '${dir.path}/single.sv'; + final singleFile = SystemVerilogService( + mod, + outputDirectory: dir.path, + outputBaseName: 'single', + includeHeader: false, + )..writeOutputs(); + expect( + File(singlePath).readAsStringSync(), + equals(singleFile.allContents), + ); + + final multiFile = SystemVerilogService( + mod, + outputDirectory: dir.path, + multiFile: true, + includeHeader: true, + )..writeOutputs(); + final output = + File('${dir.path}/${multiFile.fileContents.single.name}.sv') + .readAsStringSync(); + expect(output, startsWith(multiFile.header)); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + expect( + () => SystemVerilogService(mod), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/module_test.dart b/test/module_test.dart index c42532445..07963890c 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -231,6 +231,19 @@ class MissingInputRegistrationTopModule extends Module { } void main() { + group('output convenience methods', () { + test('dumpSystemVerilog generates in-memory output', () async { + final mod = FlexibleModule(); + await mod.build(); + + final output = mod.dumpSystemVerilog(); + + expect(output, isA()); + expect(output, isNotEmpty); + expect(SystemVerilogService.current, isNull); + }); + }); + group('try ports', () { test('tryInput, exists', () { final mod = ModuleWithMaybePorts(addIn: true); @@ -304,7 +317,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,(a ? 1'h0 : 1'h1)}")); @@ -321,7 +334,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); if (!disconnectOutputs) { expect(sv, contains("assign o = {1'h1,a}")); @@ -336,7 +349,8 @@ void main() { TopStructInoutWrap(LogicNet(), LogicNet(), LogicNet(width: 2)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect( sv, @@ -352,7 +366,7 @@ void main() { expect( mod.internalSignals.firstWhereOrNull((e) => e.name == 't0'), isNotNull); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign a_concat[0] = t0;')); }); @@ -363,7 +377,7 @@ void main() { expect(mod.internalSignals.firstWhereOrNull((e) => e.name == 'unconnected'), isNotNull); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign a_arr[1] = unconnected;')); }); diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 52470dc8c..5db4ec513 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multimodule4_test.dart @@ -54,7 +54,7 @@ void main() { .isNotEmpty, 'Should find a z two levels deep'); - final synth = ftm.generateSynth(); + final synth = ftm.dumpSystemVerilog(); // "z = 1" means it correctly traversed down from inputs assert(synth.contains('z = 1'), diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index b7642bb24..a8813276f 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multimodule5_test.dart @@ -35,7 +35,7 @@ void main() { final mod = TopModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('Passthrough')); }); diff --git a/test/name_test.dart b/test/name_test.dart index d169b913b..37321806e 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -208,7 +208,7 @@ void main() { reserveInstanceName: false, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('module specialName (')); }); test('uniquified with conflicts', () async { @@ -218,7 +218,7 @@ void main() { causeInstConflict: false, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('module specialName (')); expect(sv, contains('module specialName_0 (')); }); @@ -229,7 +229,7 @@ void main() { causeInstConflict: false, ); await mod.build(); - expect(mod.generateSynth, throwsException); + expect(mod.dumpSystemVerilog, throwsException); }); }); @@ -241,7 +241,7 @@ void main() { causeInstConflict: false, ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('specialInstanceName(')); expect(sv, contains('specialInstanceName_0(')); diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart index 6ae2c4470..583d7173c 100644 --- a/test/naming_cases_test.dart +++ b/test/naming_cases_test.dart @@ -534,7 +534,7 @@ void main() { // ── Golden SV snapshot ────────────────────────────────────── test('golden SV output snapshot', () { - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // Port declarations. expect(sv, contains('input logic [7:0] inp')); diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart index 9f1c0c31f..db51fea16 100644 --- a/test/naming_namespace_test.dart +++ b/test/naming_namespace_test.dart @@ -82,7 +82,7 @@ void main() { test('constant value appears as literal in SV output', () async { final dut = _ConstantNamingModule(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); // The constant "1" should appear as a literal 1'h1 in the output, // not as a declared signal. @@ -92,7 +92,7 @@ void main() { test('constNameDisallowed falls through to signal naming', () async { final dut = _ConstNameDisallowedModule(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); // The output assignment should NOT use the raw constant literal // as a wire name; a proper signal name should be used instead. @@ -109,7 +109,7 @@ void main() { 'in the shared namespace', () async { final dut = _InstanceSignalCollision(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); // With a single shared namespace, one of the two "inner" identifiers // must be suffixed to avoid collision. @@ -129,7 +129,7 @@ void main() { reason: 'Instance should win the shared namespace ' 'and keep the bare name'); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); // The wire (signal) must carry the suffix, not the instance. expect(sv, contains('inner_0'), reason: 'Colliding signal should be renamed to inner_0'); @@ -148,8 +148,8 @@ void main() { String stripHeader(String sv) => sv.replaceFirst(RegExp(r'/\*\*.*?\*/\n', dotAll: true), ''); - final sv1 = stripHeader(dut.generateSynth()); - final sv2 = stripHeader(dut.generateSynth()); + final sv1 = stripHeader(dut.dumpSystemVerilog()); + final sv2 = stripHeader(dut.dumpSystemVerilog()); expect(sv2, equals(sv1), reason: 'Repeated synthesis passes must produce identical ' @@ -159,7 +159,7 @@ void main() { test('duplicate instance names get uniquified', () async { final dut = _DuplicateInstances(); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); // Two instances named 'blk' — one should be 'blk', the other 'blk_0'. expect(sv, contains('blk')); diff --git a/test/nested_array_struct_port_synthesis_test.dart b/test/nested_array_struct_port_synthesis_test.dart index bd3bc8f6d..797cf432f 100644 --- a/test/nested_array_struct_port_synthesis_test.dart +++ b/test/nested_array_struct_port_synthesis_test.dart @@ -7,6 +7,9 @@ // 2026 August 18 // Author: Max Korbel +@TestOn('vm') +library; + import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; @@ -181,7 +184,7 @@ void main() { ); await module.build(); - final generated = module.generateSynth(); + final generated = module.dumpSystemVerilog(); expect(generated, contains('module ArrayRecordHierarchy')); expect(generated, contains('.inputData(')); @@ -197,7 +200,7 @@ void main() { final module = RootArrayPassThrough(LogicArray([2, 3], 4)); await module.build(); - final generated = module.generateSynth(); + final generated = module.dumpSystemVerilog(); expect(generated, contains('input logic [1:0][2:0][3:0] inputData')); expect(generated, contains('output logic [1:0][2:0][3:0] outputData')); diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index 0901acac3..e09c1c829 100644 --- a/test/net_bus_test.dart +++ b/test/net_bus_test.dart @@ -255,7 +255,8 @@ void main() { final mod = NicePortPassingTop(LogicNet(width: 8), LogicNet(width: 8)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv.contains('net_connect'), isFalse); expect(sv, @@ -314,7 +315,7 @@ void main() { final dut = DoubleNetPassthrough(LogicNet(width: 8), LogicNet(width: 8)); await dut.build(); - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); expect( sv, @@ -455,7 +456,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, contains( @@ -517,7 +518,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, @@ -590,7 +591,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); if (netTypeName == LogicNet) { expect( sv, @@ -620,7 +621,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); if (netTypeName == LogicNet) { expect( sv, @@ -750,8 +751,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect(sv, contains('net_connect (swizzled, ({in0[0],in1[0]}));')); }); @@ -764,8 +765,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -781,8 +782,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -799,8 +800,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -817,8 +818,8 @@ void main() { await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -835,8 +836,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect(sv, contains('assign _in1 = in0;')); expect( @@ -852,8 +853,8 @@ void main() { ]); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); expect( sv, @@ -942,7 +943,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); checkSV(sv); final vectors = [ @@ -962,7 +963,7 @@ void main() { await mod.build(); final sv = SvCleaner.removeSwizzleAnnotationComments( - mod.generateSynth()); + mod.dumpSystemVerilog()); checkSV(sv); final vectors = [ @@ -1205,7 +1206,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect( sv, @@ -1226,7 +1227,7 @@ void main() { await mod.build(); final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect( sv, diff --git a/test/net_test.dart b/test/net_test.dart index 0cab0fe03..ebe8e7fcb 100644 --- a/test/net_test.dart +++ b/test/net_test.dart @@ -461,7 +461,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('intermediate1')); expect(sv, contains('intermediate2')); expect(sv, contains('intermediate3')); @@ -504,7 +504,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -515,7 +515,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect('SubModInoutOnly submod'.allMatches(sv).length, 1); }); @@ -526,7 +526,7 @@ void main() { await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(' submod'.allMatches(sv).length, 2); }); }); @@ -611,7 +611,7 @@ void main() { isNotNull); } - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // test that " _b;" is not present (indication that a leftover internal // signal was there) @@ -631,7 +631,7 @@ void main() { final mod = NetArrayTopMod(Logic(width: 8), NetArrayIntf()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // print(sv); expect(sv, contains('wire [1:0][1:0][7:0] bd3')); }); @@ -677,7 +677,7 @@ void main() { ); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign c = _a_and_b;')); expect(sv, contains('assign d = _aIntermediate_or_bIntermediate;')); diff --git a/test/netlist_example_test.dart b/test/netlist_example_test.dart index 08b56b7cc..21ea92edb 100644 --- a/test/netlist_example_test.dart +++ b/test/netlist_example_test.dart @@ -52,7 +52,6 @@ void main() { final counter = Counter(en, reset, clk); await counter.build(); - counter.generateSynth(); final modules = await convertTestWriteNetlist( counter, diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 59cbdda3b..34f43b125 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2643,6 +2643,71 @@ void main() { returnsNormally, ); }); + + test(r'validation allows multiple $tribuf drivers on a resolved net', () { + final cells = >{ + 'firstDriver': { + 'type': r'$tribuf', + 'port_directions': {'Y': 'output'}, + 'connections': { + 'Y': [1], + }, + }, + 'secondDriver': { + 'type': r'$tribuf', + 'port_directions': {'Y': 'inout'}, + 'connections': { + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'ResolvedTriStateNet', + ), + returnsNormally, + ); + }); + + test(r'validation rejects mixed $tribuf and exclusive drivers', () { + final cells = >{ + 'triStateDriver': { + 'type': r'$tribuf', + 'port_directions': {'Y': 'output'}, + 'connections': { + 'Y': [1], + }, + }, + 'exclusiveDriver': { + 'type': r'$buf', + 'port_directions': {'Y': 'output'}, + 'connections': { + 'Y': [1], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'ConflictingTriStateNet', + ), + throwsA( + isA().having( + (error) => error.issues.single.drivers, + 'drivers', + containsAll([ + r'cell triStateDriver.Y ($tribuf)', + r'cell exclusiveDriver.Y ($buf)', + ]), + ), + ), + ); + }); }); // ── Group 11: Named constant signals ───────────────────────────── diff --git a/test/pair_interface_hier_test.dart b/test/pair_interface_hier_test.dart index c6bb7ad96..41f688707 100644 --- a/test/pair_interface_hier_test.dart +++ b/test/pair_interface_hier_test.dart @@ -91,7 +91,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_hier_w_modify_test.dart b/test/pair_interface_hier_w_modify_test.dart index a605b5c25..3c02eadd6 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_w_modify_test.dart @@ -94,7 +94,7 @@ void main() { final mod = HierTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('HierConsumer unnamed_module')); expect(sv, contains('HierProducer unnamed_module')); diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 46afdbef0..dbc0e364f 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -192,7 +192,7 @@ void main() { await mod.build(); // Make sure the "modify" went through: - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('input logic simple_clk')); }); diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 8ee28bb70..2db4b7bd8 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -176,7 +176,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index a6274a36b..9c3c7e876 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -147,7 +147,7 @@ void main() { Vector({}, {'rsp_data': 9}), ]; - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect( sv, diff --git a/test/replication_test.dart b/test/replication_test.dart index a6a566353..1060a3cc9 100644 --- a/test/replication_test.dart +++ b/test/replication_test.dart @@ -67,7 +67,7 @@ void main() { test('multiply by 1 generates no replication in SystemVerilog', () async { final mod = ReplicationOpModule(Logic(width: 4), 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign b = a;')); expect(sv, isNot(contains('{1{'))); }); @@ -76,7 +76,7 @@ void main() { () async { final mod = SignExtendModule(Logic(), 1); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, isNot(contains('{1{'))); }); diff --git a/test/sequential_test.dart b/test/sequential_test.dart index cabeda5ec..df90f0efa 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -309,7 +309,7 @@ void main() { final mod = NegedgeTriggeredSeq(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('always_ff @(negedge')); final vectors = [ diff --git a/test/struct_port_pruning_test.dart b/test/struct_port_pruning_test.dart index b13346ebe..030218440 100644 --- a/test/struct_port_pruning_test.dart +++ b/test/struct_port_pruning_test.dart @@ -75,7 +75,7 @@ void main() { final dut = StructPipeTop(Logic(), Logic()); await dut.build(); - final svStr = dut.generateSynth(); + final svStr = dut.dumpSystemVerilog(); // The struct_producer submodule should appear in the SV. expect( @@ -111,7 +111,7 @@ void main() { final dut = StructProducer(Logic(), Logic()); await dut.build(); - final svStr = dut.generateSynth(); + final svStr = dut.dumpSystemVerilog(); // Inside StructProducer, the struct elements (a, b from PairStruct) // drive the output via struct_slice decomposition. They must not @@ -128,7 +128,7 @@ void main() { final dut = StructConsumer(Logic(width: 2)); await dut.build(); - final svStr = dut.generateSynth(); + final svStr = dut.dumpSystemVerilog(); // Inside StructConsumer, the struct elements are extracted from the // packed input. The XOR of elements drives the output. diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index adb4f51fb..f12eec5bf 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -682,7 +682,7 @@ void main() { test('const unary inline op', () async { final mod = ModWithConstInlineUnaryOp(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains("~8'h0"), reason: sv); @@ -701,7 +701,7 @@ void main() { final mod = TieOffSubsetTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains("assign banana_tieoff = 2'h0;")); expect(sv, contains("assign apple_tieoff = 2'h0;")); @@ -722,7 +722,7 @@ void main() { final mod = TieOffPortTop(Logic(), withRedirect: redirect); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains("assign banana = 1'h0;")); expect(sv, contains(".apple(1'h0)")); @@ -754,7 +754,7 @@ void main() { test('input, output, and internal signals are sorted', () async { final mod = AlphabeticalModule(Logic(), Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -771,7 +771,7 @@ void main() { () async { final mod = AlphabeticalWidthsModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // as instantiated checkSignalDeclarationOrder(sv, ['l', 'a', 'w']); @@ -797,7 +797,7 @@ void main() { final mod = AlphabeticalSubmodulePorts(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); checkPortConnectionOrder(sv, ['l', 'a', 'w', 'm', 'x', 'b']); }); @@ -806,7 +806,7 @@ void main() { final mod = TopWithExpressions(Logic(), Logic(width: 5)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('.a((a | (b[2])))')); }); @@ -815,7 +815,7 @@ void main() { final mod = ModuleWithFloatingSignals(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // only expect 1 assignment to xylophone expect('assign'.allMatches(sv).length, 1); @@ -829,8 +829,8 @@ void main() { final mod = TopCustomSvWrap(Logic(), Logic(), useOld: useOld, banExpressions: banExpressions); await mod.build(); - final sv = - SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = SvCleaner.removeSwizzleAnnotationComments( + mod.dumpSystemVerilog()); if (banExpressions) { expect(sv, contains('assign my_fancy_new_signal <= ^fer_swizzle;')); @@ -849,7 +849,7 @@ void main() { final mod = ModuleWithCustomDefinitionEmptyPorts(Logic(), acceptsEmptyPortConnections: acceptsEmptyPortConnections); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); if (acceptsEmptyPortConnections) { expect(sv, contains('.b()')); @@ -864,7 +864,7 @@ void main() { test('custom definition', () async { final mod = TopWithCustomDef(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('module CustomDefinitionModule (')); expect(sv, contains('// this is a custom definition!')); @@ -882,7 +882,7 @@ void main() { final mod = ModWithUselessWireMods(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, isNot(contains('swizzle'))); expect(sv, isNot(contains('replicate'))); @@ -903,7 +903,7 @@ void main() { test('partial array assignment sv', () async { final mod = ModWithPartialArrayAssignment(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign b = aArr[0];')); expect(sv, contains('assign aArr[0] = a;')); @@ -1047,7 +1047,7 @@ void main() { final mod = OutToInOutTop(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign myNet = myOut;')); @@ -1063,7 +1063,7 @@ void main() { () async { final mod = _StructLeafNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, isNot(contains('_in0')), reason: 'Struct leaf from unnamed Logic() should use its ' @@ -1073,7 +1073,7 @@ void main() { test('const merge not blocked by constNameDisallowed', () async { final mod = _ConstNamingModule(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); final constAssignments = RegExp(r"assign \w+ = 8'h0;").allMatches(sv).length; diff --git a/test/sv_param_passthrough_test.dart b/test/sv_param_passthrough_test.dart index e7b0876dd..2ddf81825 100644 --- a/test/sv_param_passthrough_test.dart +++ b/test/sv_param_passthrough_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // sv_param_passthrough_test.dart @@ -162,7 +162,7 @@ void main() { () async { final mod = TopForEmptyParams(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv.contains('#'), isFalse); }); } diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index d6ccf9d4f..8a8fe50d2 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -188,7 +188,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('/*')); expect(sv, contains('*/')); @@ -211,7 +211,7 @@ void main() { final mod = SingleElementSwizzle(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // Single element should not have braces or bit range annotations // Look for bit range annotations specifically (/* number */) @@ -236,7 +236,7 @@ void main() { final mod = AllSingleBitSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // Should have bit range annotations for single bits expect(sv, contains('/*')); @@ -267,7 +267,7 @@ void main() { final mod = NestedSwizzle(Logic(width: 4), Logic(width: 3)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // Should contain annotations for both inner and outer swizzles expect(sv, contains('/*')); @@ -287,7 +287,7 @@ void main() { final mod = InlinedSwizzle(Logic(width: 4), Logic(width: 4)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // Should have annotations even when swizzle is part of larger expression expect(sv, contains('/*')); @@ -307,7 +307,7 @@ void main() { final mod = VariedWidthSwizzle(); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // Should have aligned bit range annotations expect(sv, contains('/*')); @@ -345,7 +345,7 @@ void main() { // Create a module with indices requiring different digit widths final largeModule = LargeWidthSwizzle(); await largeModule.build(); - final sv = largeModule.generateSynth(); + final sv = largeModule.dumpSystemVerilog(); // Should have properly aligned annotations despite different digit counts expect(sv, contains('/*')); @@ -389,7 +389,8 @@ void main() { final mod = SwizzleAdjacentBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv, contains('assign out = {a[7:5],a[2:0]};')); expect(sv, isNot(contains('a[7],a[6]'))); @@ -412,7 +413,8 @@ void main() { final mod = SwizzleAllAdjacentBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv, contains('assign out = a[7:5];')); expect(sv, isNot(contains('assign out = {a[7:5]};'))); @@ -430,7 +432,8 @@ void main() { final mod = SwizzleAscendingBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv, isNot(contains('a[2:0]'))); expect(sv, contains('a[0]')); @@ -457,7 +460,8 @@ void main() { final mod = SwizzleNestedAdjacentBitSlices(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv, contains('assign out = {(a[7:5]),a[4:3]};')); expect(sv, isNot(contains('a[7:3]'))); @@ -475,7 +479,8 @@ void main() { final mod = SwizzleAdjacentRanges(); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv, contains('assign out = {(a[5:2]),(a[1:0])};')); expect(sv, isNot(contains('a[5:0]'))); @@ -494,7 +499,8 @@ void main() { final mod = SwizzlePackedArrayElementBits(LogicArray([2, 2], 1)); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv, contains('assign out = {arr[1][1:0],arr[0][1:0]};')); expect(sv, isNot(contains('arr[1][1],arr[1][0]'))); @@ -514,7 +520,8 @@ void main() { ); await mod.build(); - final sv = SvCleaner.removeSwizzleAnnotationComments(mod.generateSynth()); + final sv = + SvCleaner.removeSwizzleAnnotationComments(mod.dumpSystemVerilog()); expect(sv, isNot(contains('arr[3:0]'))); expect(sv, contains('arr[3]')); @@ -528,7 +535,7 @@ void main() { final mod = SwizzleVariety(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains(''' assign b = { diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index dc10da6e1..24ec99b77 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -233,7 +233,7 @@ void main() { final mod = _Counter(Logic(), Logic()); await mod.build(); - mod.generateSynth(); + mod.dumpSystemVerilog(); expect(mod.namer.signalNameOfBest([mod.input('en')]), equals('en')); expect(mod.namer.signalNameOfBest([mod.input('reset')]), equals('reset')); @@ -251,7 +251,7 @@ void main() { final modSv = _Counter(Logic(), Logic()); await modSv.build(); - modSv.generateSynth(); + modSv.dumpSystemVerilog(); // Both paths use the same Namer, so names must match. final enNetlist = modNetlist.namer.signalNameOfBest([ @@ -271,7 +271,7 @@ void main() { 'colliding mergeable names remain stable across synthesis order', () async { void runNetlist(_CollidingNames mod) => mod.generateNetlist(); - void runSv(_CollidingNames mod) => mod.generateSynth(); + void runSv(_CollidingNames mod) => mod.dumpSystemVerilog(); final netlistOnly = await _collisionNamesAfter([runNetlist]); await Simulator.reset(); @@ -316,7 +316,7 @@ void main() { test('colliding names stay stable when SV inlines one signal', () async { void runNetlist(_PartiallyInlineCollidingNames mod) => mod.generateNetlist(); - void runSv(_PartiallyInlineCollidingNames mod) => mod.generateSynth(); + void runSv(_PartiallyInlineCollidingNames mod) => mod.dumpSystemVerilog(); final netlistOnly = await _partialInlineCollisionNamesAfter([runNetlist]); await Simulator.reset(); @@ -347,7 +347,8 @@ void main() { () async { void runNetlist(_CollapsedInstanceCollidingNames mod) => mod.generateNetlist(); - void runSv(_CollapsedInstanceCollidingNames mod) => mod.generateSynth(); + void runSv(_CollapsedInstanceCollidingNames mod) => + mod.dumpSystemVerilog(); final netlistOnly = await _collapsedInstanceCollisionNamesAfter([ runNetlist, diff --git a/test/systemverilog_port_types_test.dart b/test/systemverilog_port_types_test.dart index 80ca66505..6ca0fb141 100644 --- a/test/systemverilog_port_types_test.dart +++ b/test/systemverilog_port_types_test.dart @@ -125,7 +125,10 @@ void main() { final module = _PortTypesModule(); await module.build(); - final sv = module.generateSynth(configuration: testCase.configuration); + final sv = SystemVerilogService( + module, + configuration: testCase.configuration, + ).output; final declarations = { testCase.inputPrefix: [ diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index 80f328073..2f05069f2 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -227,7 +227,7 @@ void main() { final mod = SimpleStructModuleContainer(Logic(), Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, isNot(contains('internal_struct'))); @@ -249,7 +249,7 @@ void main() { expect(mod.anyOut, isA()); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('input logic [3:0][1:0] anyIn')); expect(sv, contains('output logic [3:0][1:0] anyOut')); @@ -275,7 +275,7 @@ void main() { final mod = ParentModuleWithStructsContainingPorts(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // if naming is wrong, these names will appear in the SV in ports expect( @@ -355,7 +355,7 @@ void main() { SimpleStructModuleContainer(LogicNet(), LogicNet(), asNet: true); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, isNot(contains('internal_struct'))); @@ -500,7 +500,7 @@ void main() { final mod = ModuleWithOneBitStructPort(OneBitStruct()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); // no slicing on single-bit signals expect(sv, contains('assign outStruct = outStruct_oneBit')); diff --git a/test/wave_dumper_test.dart b/test/waveform_service_test.dart similarity index 66% rename from test/wave_dumper_test.dart rename to test/waveform_service_test.dart index 07aafc8c8..b6f0134da 100644 --- a/test/wave_dumper_test.dart +++ b/test/waveform_service_test.dart @@ -1,8 +1,8 @@ -// Copyright (C) 2021-2024 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// wave_dumper_test.dart -// Tests for the WaveDumper +// waveform_service_test.dart +// Tests for the WaveformService // // 2021 November 4 // Author: Max Korbel @@ -40,10 +40,26 @@ const tempDumpDir = 'tmp_test'; /// Gets the path of the VCD file based on a name. String temporaryDumpPath(String name) => '$tempDumpDir/temp_dump_$name.vcd'; -/// Attaches a [WaveDumper] to [module] to VCD with [name]. +/// Attaches a [WaveformService] to [module] to VCD with [name]. void createTemporaryDump(Module module, String name) { + Directory(tempDumpDir).createSync(recursive: true); + WaveformService( + module, + outputDirectory: tempDumpDir, + outputBaseName: 'temp_dump_$name', + writeToFile: true, + ); +} + +// The helper intentionally exercises the deprecated WaveDumper compatibility +// path. +// ignore: deprecated_member_use_from_same_package +/// Attaches the deprecated [WaveDumper] to [module] to VCD with [name]. +void createTemporaryWaveDumperDump(Module module, String name) { Directory(tempDumpDir).createSync(recursive: true); final tmpDumpFile = temporaryDumpPath(name); + // The deprecated WaveDumper constructor is invoked to test its behavior. + // ignore: deprecated_member_use_from_same_package WaveDumper(module, outputPath: tmpDumpFile); } @@ -86,6 +102,98 @@ void main() { deleteTemporaryDump(dumpName); }); + test('attach deprecated wave dumper after put', () async { + final a = Logic(name: 'a'); + final mod = SimpleModule(a); + await mod.build(); + + const dumpName = 'deprecatedDumpAfterPut'; + + a.put(1); + createTemporaryWaveDumperDump(mod, dumpName); + + Simulator.registerAction(10, () => a.put(0)); + await Simulator.run(); + + final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); + + expect( + VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), + equals(true)); + expect( + VcdParser.confirmValue(vcdContents, 'a', 5, LogicValue.ofString('1')), + equals(true)); + expect( + VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), + equals(true)); + + deleteTemporaryDump(dumpName); + }); + + test('dumpWaves returns a waveform service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + const dumpName = 'moduleDumpWaveforms'; + final outputPath = temporaryDumpPath(dumpName); + Directory(tempDumpDir).createSync(recursive: true); + final service = mod.dumpWaves(outputPath: outputPath); + + expect(service, isA()); + expect(service.module, same(mod)); + expect(service.outputPath, outputPath); + expect(service.outputFilePath, outputPath); + expect(File(service.outputPath).existsSync(), isTrue); + + await Simulator.run(); + deleteTemporaryDump(dumpName); + }); + + test('dumpWaves preserves an arbitrary legacy output filename', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + const outputPath = '$tempDumpDir/capture.trace'; + final service = mod.dumpWaves(outputPath: outputPath); + + expect(service.outputPath, equals(outputPath)); + expect(File(outputPath).existsSync(), isTrue); + + await Simulator.run(); + File(outputPath).deleteSync(); + }); + + test('waveform artifact derives its extension from the format', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final waveformService = WaveformService( + mod, + outputDirectory: tempDumpDir, + outputBaseName: 'capture', + ); + + final artifact = waveformService.artifacts.single; + + expect(artifact.fileName, equals('capture.vcd')); + expect(artifact.mediaType, equals('text/x-vcd')); + expect(File(waveformService.outputFilePath).existsSync(), isFalse); + expect( + (await artifact.openRead().expand((bytes) => bytes).toList()).isNotEmpty, + isTrue, + ); + }); + + test('rejects formats without a matching waveform writer', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + expect( + () => WaveformService(mod, format: WaveOutputFormat.fst), + throwsUnsupportedError, + ); + }); + test('attach dumper before put', () async { final a = Logic(name: 'a'); final mod = SimpleModule(a); @@ -241,11 +349,16 @@ void main() { const dir1Path = '$tempDumpDir/dir1'; - final waveDumper = WaveDumper(mod, outputPath: '$dir1Path/dir2/waves.vcd'); + final waveformService = WaveformService( + mod, + outputDirectory: '$dir1Path/dir2', + outputBaseName: 'waves', + writeToFile: true, + ); - expect(File(waveDumper.outputPath).existsSync(), equals(true)); + expect(File(waveformService.outputFilePath).existsSync(), equals(true)); - if (File(waveDumper.outputPath).existsSync()) { + if (File(waveformService.outputFilePath).existsSync()) { File(dir1Path).deleteSync(recursive: true); } }); @@ -263,7 +376,7 @@ void main() { Simulator.registerAction(13, () => reset.put(1)); reset.put(0); - // add wave dumper *after* the put to reset + // add waveform service *after* the put to reset createTemporaryDump(mod, dumpName); // check functional matches diff --git a/tool/generate_gate_catalog.dart b/tool/generate_gate_catalog.dart index 0923b459b..705a6d37f 100644 --- a/tool/generate_gate_catalog.dart +++ b/tool/generate_gate_catalog.dart @@ -53,8 +53,6 @@ Future main(List arguments) async { await catalog.build(); final synth = NetlistSynthesizer(); - // We will migrate to a new public API in a future PR - // ignore: invalid_use_of_visible_for_testing_member final json = synth.synthesizeToJson(catalog); final outputFile = File(outputPath); diff --git a/tool/gh_actions/install_node.sh b/tool/gh_actions/install_node.sh index 8e1b5b74b..01a29748a 100755 --- a/tool/gh_actions/install_node.sh +++ b/tool/gh_actions/install_node.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # install_node.sh From 1a69206c65b0bd35216b03474658051cb11b9b8c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 10 Sep 2026 15:17:12 -0700 Subject: [PATCH 03/36] Close waveform sink before test cleanup --- test/waveform_service_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index b6f0134da..599d05929 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -358,6 +358,9 @@ void main() { expect(File(waveformService.outputFilePath).existsSync(), equals(true)); + // Let the service close its asynchronous file sink before cleanup. + await Simulator.run(); + if (File(waveformService.outputFilePath).existsSync()) { File(dir1Path).deleteSync(recursive: true); } From 7269c89407faaab1b368a4e6095e106836033b3a Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 05:30:33 -0700 Subject: [PATCH 04/36] fix tutorial headers --- doc/tutorials/chapter_3/answers/exercise_sv.dart | 9 +++++++++ doc/tutorials/chapter_3/full_adder.dart | 9 +++++++++ doc/tutorials/chapter_4/answers/exercise_1_sv.dart | 9 +++++++++ doc/tutorials/chapter_4/answers/exercise_2_sv.dart | 9 +++++++++ doc/tutorials/chapter_4/basic_generation_sv.dart | 9 +++++++++ doc/tutorials/chapter_5/answers/full_adder.dart | 9 +++++++++ doc/tutorials/chapter_5/answers/full_subtractor.dart | 9 +++++++++ doc/tutorials/chapter_5/answers/n_bit_subtractor.dart | 9 +++++++++ doc/tutorials/chapter_5/n_bit_adder.dart | 9 +++++++++ doc/tutorials/chapter_8/answers/exercise_1_spi.dart | 9 +++++++++ .../chapter_8/answers/exercise_2_toycapsule_fsm.dart | 9 +++++++++ doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart | 9 +++++++++ doc/tutorials/chapter_8/carry_save_multiplier.dart | 9 +++++++++ doc/tutorials/chapter_8/counter_interface.dart | 9 +++++++++ doc/tutorials/chapter_8/oven_fsm.dart | 9 +++++++++ .../chapter_9/rohd_vf_example/lib/rohd_vf_example.dart | 9 +++++++++ lib/src/utilities/naming.dart | 4 ++-- test/logic_name_config_test.dart | 4 ++-- 18 files changed, 148 insertions(+), 4 deletions(-) diff --git a/doc/tutorials/chapter_3/answers/exercise_sv.dart b/doc/tutorials/chapter_3/answers/exercise_sv.dart index 9406d1e1d..f20c996be 100644 --- a/doc/tutorials/chapter_3/answers/exercise_sv.dart +++ b/doc/tutorials/chapter_3/answers/exercise_sv.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// exercise_sv.dart +// Chapter 3 SystemVerilog exercise answer. +// +// 2023 March 30 +// Author: Yao Jing Quek + import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; import 'helper.dart'; diff --git a/doc/tutorials/chapter_3/full_adder.dart b/doc/tutorials/chapter_3/full_adder.dart index 5ad3f29f3..9cb896fb5 100644 --- a/doc/tutorials/chapter_3/full_adder.dart +++ b/doc/tutorials/chapter_3/full_adder.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// full_adder.dart +// Chapter 3 full-adder tutorial example. +// +// 2023 March 3 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'package:rohd/rohd.dart'; diff --git a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart index 1cf09cbb0..7ea00c0f7 100644 --- a/doc/tutorials/chapter_4/answers/exercise_1_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_1_sv.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// exercise_1_sv.dart +// Chapter 4 SystemVerilog exercise answer. +// +// 2023 April 11 +// Author: Yao Jing Quek + import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; diff --git a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart index 1a2e27411..007e62459 100644 --- a/doc/tutorials/chapter_4/answers/exercise_2_sv.dart +++ b/doc/tutorials/chapter_4/answers/exercise_2_sv.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// exercise_2_sv.dart +// Chapter 4 SystemVerilog exercise answer. +// +// 2023 April 11 +// Author: Yao Jing Quek + import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; diff --git a/doc/tutorials/chapter_4/basic_generation_sv.dart b/doc/tutorials/chapter_4/basic_generation_sv.dart index 153213a8c..7a973e753 100644 --- a/doc/tutorials/chapter_4/basic_generation_sv.dart +++ b/doc/tutorials/chapter_4/basic_generation_sv.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// basic_generation_sv.dart +// Chapter 4 basic SystemVerilog generation tutorial example. +// +// 2023 April 11 +// Author: Yao Jing Quek + import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; diff --git a/doc/tutorials/chapter_5/answers/full_adder.dart b/doc/tutorials/chapter_5/answers/full_adder.dart index ae1efd096..95612bb3b 100644 --- a/doc/tutorials/chapter_5/answers/full_adder.dart +++ b/doc/tutorials/chapter_5/answers/full_adder.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// full_adder.dart +// Chapter 5 full-adder exercise answer. +// +// 2023 May 26 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'package:rohd/rohd.dart'; diff --git a/doc/tutorials/chapter_5/answers/full_subtractor.dart b/doc/tutorials/chapter_5/answers/full_subtractor.dart index 9d0004769..45d8c43b9 100644 --- a/doc/tutorials/chapter_5/answers/full_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/full_subtractor.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// full_subtractor.dart +// Chapter 5 full-subtractor exercise answer. +// +// 2023 May 26 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'package:rohd/rohd.dart'; diff --git a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart index c005d864b..585e3b05f 100644 --- a/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart +++ b/doc/tutorials/chapter_5/answers/n_bit_subtractor.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// n_bit_subtractor.dart +// Chapter 5 N-bit subtractor exercise answer. +// +// 2023 May 26 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'package:rohd/rohd.dart'; diff --git a/doc/tutorials/chapter_5/n_bit_adder.dart b/doc/tutorials/chapter_5/n_bit_adder.dart index d3dc88e29..520964a91 100644 --- a/doc/tutorials/chapter_5/n_bit_adder.dart +++ b/doc/tutorials/chapter_5/n_bit_adder.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// n_bit_adder.dart +// Chapter 5 N-bit adder tutorial example. +// +// 2023 April 11 +// Author: Yao Jing Quek + import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; diff --git a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart index 0afd3dfb8..4056d0266 100644 --- a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart +++ b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// exercise_1_spi.dart +// Chapter 8 SPI exercise answer. +// +// 2023 September 7 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'dart:async'; 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 775cfe91b..520e8d96f 100644 --- a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart +++ b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// exercise_2_toycapsule_fsm.dart +// Chapter 8 Toy Capsule FSM exercise answer. +// +// 2023 September 7 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'package:rohd/rohd.dart'; diff --git a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart index 96abf4c60..ccef24bf9 100644 --- a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart +++ b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// exercise_3_pipeline.dart +// Chapter 8 pipeline exercise answer. +// +// 2023 September 7 +// Author: Yao Jing Quek + import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; diff --git a/doc/tutorials/chapter_8/carry_save_multiplier.dart b/doc/tutorials/chapter_8/carry_save_multiplier.dart index fdcbc42aa..1cab976f3 100644 --- a/doc/tutorials/chapter_8/carry_save_multiplier.dart +++ b/doc/tutorials/chapter_8/carry_save_multiplier.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// carry_save_multiplier.dart +// Chapter 8 carry-save multiplier tutorial example. +// +// 2023 September 7 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'package:rohd/rohd.dart'; diff --git a/doc/tutorials/chapter_8/counter_interface.dart b/doc/tutorials/chapter_8/counter_interface.dart index d9fcbf501..c76517cd0 100644 --- a/doc/tutorials/chapter_8/counter_interface.dart +++ b/doc/tutorials/chapter_8/counter_interface.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// counter_interface.dart +// Chapter 8 counter interface tutorial example. +// +// 2023 September 7 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial import 'package:rohd/rohd.dart'; diff --git a/doc/tutorials/chapter_8/oven_fsm.dart b/doc/tutorials/chapter_8/oven_fsm.dart index 844baa8ec..317cb5d40 100644 --- a/doc/tutorials/chapter_8/oven_fsm.dart +++ b/doc/tutorials/chapter_8/oven_fsm.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// oven_fsm.dart +// Chapter 8 oven FSM tutorial example. +// +// 2023 February 24 +// Author: Yao Jing Quek + // ignore_for_file: avoid_print - tutorial // Import the ROHD package. 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 782a06e45..c31eda792 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 @@ -1,3 +1,12 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_vf_example.dart +// Chapter 9 ROHD Visualizer example. +// +// 2023 September 22 +// Author: Yao Jing Quek + import 'dart:async'; import 'dart:collection'; import 'package:logging/logging.dart'; diff --git a/lib/src/utilities/naming.dart b/lib/src/utilities/naming.dart index b52f068b7..0d925d6a4 100644 --- a/lib/src/utilities/naming.dart +++ b/lib/src/utilities/naming.dart @@ -1,10 +1,10 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // name_validator.dart // Performs validation on naming. // -// 2023 October 24 +// 2026 September 15 import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 9469f53de..107b64c9b 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -1,10 +1,10 @@ -// Copyright (C) 2023 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_name_config_test.dart // Unit tests for logic naming using configuration for naming preferences. // -// 2023 November 3 +// 2026 September 15 // Author: Max Korbel import 'package:collection/collection.dart'; From d6cfa6adc8c979770c1722c6ad4e5a2467d5f6bb Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 06:00:10 -0700 Subject: [PATCH 05/36] lint issues with deprecation --- lib/src/utilities/simcompare.dart | 4 ++-- test/array_collapsing_test.dart | 5 ++++- test/logic_name_config_test.dart | 3 +++ test/subset_range_mapping_test.dart | 5 ++++- test/unpacked_array_synthesis_test.dart | 5 ++++- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index e2479cbc3..0f67f0751 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -249,7 +249,7 @@ abstract class SimCompare { reason: 'Could not run $verilatorExecutable --version:\n' '${version.stdout}\n${version.stderr}'); - final generatedVerilog = module.generateSynth( + final generatedVerilog = module.dumpSystemVerilog( configuration: synthesizerConfiguration, ); final withTestbench = !buildOnly || vectors.isNotEmpty; @@ -591,7 +591,7 @@ abstract class SimCompare { instance: moduleInstance, :stimulus, ) = _vectorTestbenchContents(module, vectors, moduleName: moduleName); - final generatedVerilog = module.generateSynth( + final generatedVerilog = module.dumpSystemVerilog( configuration: synthesizerConfiguration, ); diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 07e4b43a7..10cdd3f8f 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -4,9 +4,12 @@ // array_collapsing_test.dart // Tests for array collapsing // -// 2024 June 5 +// 2026 September 15 // Author: Shankar Sharma +// Legacy API calls are intentional coverage for deprecated generateSynth(). +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 107b64c9b..d5ebe114d 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -7,6 +7,9 @@ // 2026 September 15 // Author: Max Korbel +// Legacy API calls are intentional coverage for deprecated generateSynth(). +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; diff --git a/test/subset_range_mapping_test.dart b/test/subset_range_mapping_test.dart index 8271c3cc9..23baa976f 100644 --- a/test/subset_range_mapping_test.dart +++ b/test/subset_range_mapping_test.dart @@ -4,9 +4,12 @@ // subset_range_mapping_test.dart // Tests for packed subset range mapping and isolated-bit preservation // -// 2026 September 8 +// 2026 September 15 // Author: Max Korbel +// Legacy API call is intentional coverage for deprecated generateSynth(). +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart'; diff --git a/test/unpacked_array_synthesis_test.dart b/test/unpacked_array_synthesis_test.dart index 3d1d76653..a1948985f 100644 --- a/test/unpacked_array_synthesis_test.dart +++ b/test/unpacked_array_synthesis_test.dart @@ -4,9 +4,12 @@ // unpacked_array_synthesis_test.dart // Tests for unpacked-array synthesis and input connection inlining. // -// 2026 September 9 +// 2026 September 15 // Author: Max Korbel +// Legacy API calls are intentional coverage for deprecated generateSynth(). +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; From 21e2d67adc536ab6bbe7742bc236457457359ebf Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 06:35:29 -0700 Subject: [PATCH 06/36] restore creation dates --- test/array_collapsing_test.dart | 2 +- test/logic_name_config_test.dart | 2 +- test/subset_range_mapping_test.dart | 2 +- test/unpacked_array_synthesis_test.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 10cdd3f8f..a7adb55f7 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -4,7 +4,7 @@ // array_collapsing_test.dart // Tests for array collapsing // -// 2026 September 15 +// 2024 June 5 // Author: Shankar Sharma // Legacy API calls are intentional coverage for deprecated generateSynth(). diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index d5ebe114d..e8ef49b42 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -4,7 +4,7 @@ // logic_name_config_test.dart // Unit tests for logic naming using configuration for naming preferences. // -// 2026 September 15 +// 2023 November 3 // Author: Max Korbel // Legacy API calls are intentional coverage for deprecated generateSynth(). diff --git a/test/subset_range_mapping_test.dart b/test/subset_range_mapping_test.dart index 23baa976f..b32723205 100644 --- a/test/subset_range_mapping_test.dart +++ b/test/subset_range_mapping_test.dart @@ -4,7 +4,7 @@ // subset_range_mapping_test.dart // Tests for packed subset range mapping and isolated-bit preservation // -// 2026 September 15 +// 2026 September 8 // Author: Max Korbel // Legacy API call is intentional coverage for deprecated generateSynth(). diff --git a/test/unpacked_array_synthesis_test.dart b/test/unpacked_array_synthesis_test.dart index a1948985f..cd8e3b997 100644 --- a/test/unpacked_array_synthesis_test.dart +++ b/test/unpacked_array_synthesis_test.dart @@ -4,7 +4,7 @@ // unpacked_array_synthesis_test.dart // Tests for unpacked-array synthesis and input connection inlining. // -// 2026 September 15 +// 2026 September 9 // Author: Max Korbel // Legacy API calls are intentional coverage for deprecated generateSynth(). From 7cc7d5e9841f4810672ca21a6a64a2ad1b97e713 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 06:44:06 -0700 Subject: [PATCH 07/36] Ignore Visual Studio Code documentation link Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/configs/mlc_config.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index 0c4421967..2feade526 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -19,7 +19,10 @@ "pattern":"^https://docs.cocotb.org/en/stable/$" }, { - "pattern":"^https://www.nandland.com" + "pattern": "^https://code\\.visualstudio\\.com/" + }, + { + "pattern": "^https://www.nandland.com" }, { "pattern":"^https://.*\\.fandom\\.com" From 3a3264c87a5c8a08e47c26d0b18f6b3a1b180a3e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 12:46:21 -0700 Subject: [PATCH 08/36] Pin material UI before unsupported language features --- rohd_devtools_extension/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index f352a624f..cc39f7a29 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_bloc: ^9.0.0 flutter_simple_treeview: ^3.0.2 logging: ^1.2.0 - material_ui: ^1.1.0 + material_ui: ">=1.1.0 <1.3.0" rohd_devtools_widgets: path: ../packages/rohd_devtools_widgets vm_service: ^14.3.1 From c49ea1f904b9e483573d0b5196e509cfd2c66a2d Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 15:44:03 -0700 Subject: [PATCH 09/36] document pin for material_ui Signed-off-by: Desmond A. Kirkpatrick --- rohd_devtools_extension/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index cc39f7a29..aa93d59e6 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: flutter_bloc: ^9.0.0 flutter_simple_treeview: ^3.0.2 logging: ^1.2.0 + # Keep below 1.3.0 for the Dart 3.6 CI toolchain. material_ui: ">=1.1.0 <1.3.0" rohd_devtools_widgets: path: ../packages/rohd_devtools_widgets From fc803d7961f6f25768b1e8f371bed1255d43d824 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 15:55:49 -0700 Subject: [PATCH 10/36] document pin for material_ui in devtools Signed-off-by: Desmond A. Kirkpatrick --- rohd_devtools_extension/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index aa93d59e6..e67196a58 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -41,6 +41,8 @@ flutter: - assets/icons/ dependency_overrides: + # Keep the transitive Cupertino UI package below the Dart 3.6-incompatible release. + cupertino_ui: ">=1.0.0 <1.1.0" rohd: path: ../ rohd_hierarchy: From 5302045df50ca6b10e781d9605953beac5dc8b93 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 22:28:56 -0700 Subject: [PATCH 11/36] waveforms should not buffer by default, better service registration model, updated docs Signed-off-by: Desmond A. Kirkpatrick --- .../chapter_1/01_setup_installation.md | 2 +- doc/tutorials/chapter_5/00_basic_modules.md | 6 +- .../chapter_7/00_sequential_logic.md | 4 +- doc/tutorials/chapter_8/01_interface.md | 2 +- doc/user_guide/_docs/A21-generation.md | 65 +++++---- lib/src/diagnostics/module_services.dart | 13 +- lib/src/diagnostics/waveform_service.dart | 92 ++++++++++--- lib/src/module.dart | 9 +- .../synthesizers/netlist/netlist_service.dart | 32 ++--- .../systemverilog/system_verilog_service.dart | 7 +- lib/src/wave_dumper.dart | 2 +- test/module_services_test.dart | 128 +++++++++++++++++- test/waveform_service_test.dart | 87 +++++++++++- 13 files changed, 366 insertions(+), 83 deletions(-) diff --git a/doc/tutorials/chapter_1/01_setup_installation.md b/doc/tutorials/chapter_1/01_setup_installation.md index 87ef1ecf1..53a961ea7 100644 --- a/doc/tutorials/chapter_1/01_setup_installation.md +++ b/doc/tutorials/chapter_1/01_setup_installation.md @@ -162,7 +162,7 @@ Future main({bool noPrint = false}) async { // Let's see what this module looks like as SystemVerilog, so we can pass it // to other tools. - final systemVerilogCode = counter.dumpSystemVerilog().output; + final systemVerilogCode = counter.dumpSystemVerilog(); if (!noPrint) { print(systemVerilogCode); } diff --git a/doc/tutorials/chapter_5/00_basic_modules.md b/doc/tutorials/chapter_5/00_basic_modules.md index c2207c748..f3b81f88a 100644 --- a/doc/tutorials/chapter_5/00_basic_modules.md +++ b/doc/tutorials/chapter_5/00_basic_modules.md @@ -95,7 +95,7 @@ Do note that the `build()` method returns a `Future`, not just `void`. Thi ## Converting ROHD Module to System Verilog RTL -Next, we can see how extending your `Logic` to `Module` enables the generation of system Verilog code. Building on the previous example, we've made some slight modifications by adding `simModule.build()` and `simModule.dumpSystemVerilog().output`. +Next, we can see how extending your `Logic` to `Module` enables the generation of system Verilog code. Building on the previous example, we've made some slight modifications by adding `simModule.build()` and `simModule.dumpSystemVerilog()`. ```dart void main() async { @@ -106,7 +106,7 @@ void main() async { await simModule.build(); // Print out system verilog code - print(simModule.dumpSystemVerilog().output); + print(simModule.dumpSystemVerilog()); test('should return input value.', () => expect(simModule.out.value.toInt(), equals(1))); @@ -114,7 +114,7 @@ void main() async { // Add this to test on generate system verilog code test( 'should generate system verilog code.', - () => expect(simModule.dumpSystemVerilog().output, + () => expect(simModule.dumpSystemVerilog(), contains('module SimpleModule('))); } ``` diff --git a/doc/tutorials/chapter_7/00_sequential_logic.md b/doc/tutorials/chapter_7/00_sequential_logic.md index 3eca09077..76822821e 100644 --- a/doc/tutorials/chapter_7/00_sequential_logic.md +++ b/doc/tutorials/chapter_7/00_sequential_logic.md @@ -44,7 +44,7 @@ class ShiftRegister extends Module { void main() async { final shiftReg = ShiftRegister(); await shiftReg.build(); - print(shiftReg.dumpSystemVerilog().output); + print(shiftReg.dumpSystemVerilog()); } ``` @@ -196,7 +196,7 @@ void main() async { final shiftReg = ShiftRegister(clk, reset, sin); await shiftReg.build(); - print(shiftReg.dumpSystemVerilog().output); + print(shiftReg.dumpSystemVerilog()); // Inject 1 to reset and 0 to shift in reset.inject(1); diff --git a/doc/tutorials/chapter_8/01_interface.md b/doc/tutorials/chapter_8/01_interface.md index c8fa9be14..6710f6850 100644 --- a/doc/tutorials/chapter_8/01_interface.md +++ b/doc/tutorials/chapter_8/01_interface.md @@ -168,7 +168,7 @@ Future main() async { counterInterface.en.inject(0); counterInterface.reset.inject(1); - print(counter.dumpSystemVerilog().output); + print(counter.dumpSystemVerilog()); counter.dumpWaves( outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index aac4b20ba..407be196f 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -7,8 +7,8 @@ toc: true Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. -The simplest way to write SystemVerilog is with `dumpSystemVerilog` on -`Module`: +The simplest way to generate SystemVerilog text is with +`dumpSystemVerilog` on `Module`: ```dart void main() async { @@ -17,32 +17,39 @@ void main() async { // remember that `build` returns a `Future`, hence the `await` here await myModule.build(); - myModule.dumpSystemVerilog(outputPath: 'myHardware.sv'); + final systemVerilog = myModule.dumpSystemVerilog(); + print(systemVerilog); } ``` - `dumpSystemVerilog` writes one file containing the SystemVerilog `module` - definitions for the top-level module and all recursive submodules. To write - one `.sv` file per module definition instead, pass a directory and set - `multiFile` to `true`: +`dumpSystemVerilog` returns one string containing the SystemVerilog `module` +definitions for the top-level module and all recursive submodules. For +filesystem output, use `SystemVerilogService` and explicitly write its +artifacts: - ```dart - myModule.dumpSystemVerilog( - outputPath: 'build/systemverilog', - multiFile: true, - ); - ``` +```dart +final systemVerilog = SystemVerilogService( + myModule, + outputDirectory: 'build', + outputBaseName: 'myHardware', +); +systemVerilog.writeOutputs(); // Writes build/myHardware.sv. +``` - For generated text without writing a file, use `dumpSystemVerilog` without an - `outputPath`: +To write one `.sv` file per module definition, configure the service with +`multiFile: true`: - ```dart - final generatedSv = myModule.dumpSystemVerilog().output; - ``` +```dart +final systemVerilog = SystemVerilogService( + myModule, + outputDirectory: 'build/systemverilog', + multiFile: true, +); +systemVerilog.writeOutputs(); +``` - The dump methods preserve the legacy one-shot output workflow. For the service - API, artifact streams, or explicit output configuration, use - `SystemVerilogService` directly. +For artifact streams or other explicit output configuration, use +`SystemVerilogService` directly. ## Generating a netlist @@ -85,8 +92,7 @@ the complete synthesized hierarchy. Each module entry contains `attributes`, Generated ports default to `input logic`, `output logic`, and `inout wire`, preserving the traditional ROHD declarations. Use a `SystemVerilogSynthesizerConfiguration` to independently control whether object types, such as `wire` and `var`, and data types, such as `logic`, are explicit for each port direction: ```dart -myModule.dumpSystemVerilog( - outputPath: 'myHardware.sv', +final systemVerilog = myModule.dumpSystemVerilog( configuration: const SystemVerilogSynthesizerConfiguration( inputPortType: SystemVerilogPortTypeConfiguration( objectType: SystemVerilogPortType.explicit, @@ -185,9 +191,11 @@ final oneShotSystemVerilog = SystemVerilogService( ); ``` -The `Module.dumpSystemVerilog()` and `Module.dumpWaves()` convenience methods -use the normal registration defaults. Construct the corresponding service -directly with `register: false` when this side effect is not desired. +`Module.dumpSystemVerilog()` is a one-shot operation and uses +`register: false`, so it does not change globally discoverable service state. +`Module.dumpWaves()` registers its `WaveformService` by default. Construct the +corresponding service directly with `register: false` when registration is not +desired. `SystemVerilogService` is the direct synthesis service. Its `outputDirectory` defaults to the current directory and its `outputBaseName` defaults to the top @@ -230,8 +238,9 @@ myModule.dumpWaves(outputPath: 'waves.vcd'); `WaveformService` records VCD data in memory by default and exposes it as a `ModuleServiceArtifact`. Its output directory and basename use the same -defaults as other artifact-producing services. Set `writeToFile` when capture -should also create a VCD file: +defaults as other artifact-producing services. Set `writeToFile` for a +bounded-memory file-backed capture; set `retainInMemory: true` as well only +when that file-backed capture also needs whole-history in-memory access: ```dart final waveform = WaveformService( diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart index 86696770e..f974e41b2 100644 --- a/lib/src/diagnostics/module_services.dart +++ b/lib/src/diagnostics/module_services.dart @@ -18,10 +18,18 @@ import 'package:rohd/src/diagnostics/inspector_service.dart'; /// 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. +/// Those accessors are backed by this registry, so both discovery routes have +/// the same registration lifetime. /// /// The registry references no specific service type, so it is identical across /// all feature branches that contribute services. /// +/// Service registrations are independent latest registrations by concrete +/// service type; they are not an atomic set associated with [rootModule]. +/// [reset] explicitly clears both the hierarchy and every service +/// registration. [Simulator.reset] does not reset this registry, allowing +/// generated service results to remain available after a simulation ends. +/// /// **Auto-registered:** /// - [rootModule] / [hierarchyJson] — set by [Module.build]. class ModuleServices { @@ -71,7 +79,10 @@ class ModuleServices { _services.remove(T); } - /// Resets all services. Intended for test teardown. + /// Resets the hierarchy and all service registrations. + /// + /// This is independent of [Simulator.reset] and is intended for test + /// teardown or callers that explicitly want to discard discovered services. void reset() { rootModule = null; _services.clear(); diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index f77c0fa1d..736d1816b 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -61,7 +61,7 @@ enum OverwritePolicy { /// /// This is the base class for waveform capture. It handles: /// - Signal collection (with optional [signalFilter]) -/// - In-memory VCD output with configurable [timescale] +/// - Optional whole-history in-memory VCD output with configurable [timescale] /// - Selective recording via [startTime] / [stopTime] /// - Optional file output with periodic buffer flushing and [overwritePolicy] /// - Optional registration with [ModuleServices] @@ -105,7 +105,11 @@ enum OverwritePolicy { /// ``` class WaveformService extends ArtifactProducingService { /// The most recently registered [WaveformService], or `null`. - static WaveformService? current; + /// + /// This is backed by [ModuleServices], so it is cleared by unregistering + /// this service type or resetting the registry. + static WaveformService? get current => + ModuleServices.instance.lookup(); /// Path of the output waveform file. /// @@ -162,10 +166,18 @@ class WaveformService extends ArtifactProducingService { /// Whether waveform bytes are written to [outputFilePath]. /// - /// When `false`, the service retains its waveform bytes in memory and exposes - /// them through [artifacts]. + /// File-backed captures retain only the current [flushBufferSize]-bounded + /// write buffer unless [retainInMemory] is enabled. final bool writeToFile; + /// Whether to retain the complete waveform in memory. + /// + /// By default, this is `true` for in-memory-only VCD debugging captures and + /// `false` for file-backed captures. Set it explicitly to override those + /// defaults when consumers need whole-history waveform queries during or + /// after simulation. + final bool retainInMemory; + // ─── Internal file-writing state ───────────────────────────── /// Sink writing to [outputFilePath] when [writeToFile] is true. @@ -174,7 +186,7 @@ class WaveformService extends ArtifactProducingService { /// Write buffer; flushed when it exceeds [flushBufferSize]. final StringBuffer _fileBuffer = StringBuffer(); - /// The complete waveform output retained for streaming artifacts. + /// The complete waveform output when [retainInMemory] is enabled. final StringBuffer _inMemoryOutput = StringBuffer(); /// Counter for assigning compact signal markers in the VCD. @@ -189,6 +201,9 @@ class WaveformService extends ArtifactProducingService { /// The timestamp currently being accumulated. int _currentDumpingTimestamp = Simulator.time; + /// Whether the recording window's initial signal snapshot has been written. + bool _hasWrittenWindowSnapshot = false; + // ─── Constructor ───────────────────────────────────────────── /// Creates a [WaveformService] for [module]. @@ -202,6 +217,11 @@ class WaveformService extends ArtifactProducingService { /// /// Use the optional constructor parameters to configure format, filtering, /// timescale, start/stop times, flush size, and overwrite policy. + /// + /// In-memory-only VCD debugging captures retain the complete waveform by + /// default. Set [retainInMemory] explicitly to choose whole-history + /// retention; file-backed captures default to bounded memory while retaining + /// a streamable artifact on disk. WaveformService( Module module, { super.outputDirectory, @@ -216,7 +236,9 @@ class WaveformService extends ArtifactProducingService { this.overwritePolicy = OverwritePolicy.overwrite, this.register = true, this.writeToFile = false, - }) : super(module) { + bool? retainInMemory, + }) : retainInMemory = retainInMemory ?? !writeToFile, + super(module) { if (!module.hasBuilt) { throw Exception( 'Module must be built before creating WaveformService. ' @@ -248,6 +270,7 @@ class WaveformService extends ArtifactProducingService { _collectSignals(); _writeHeader(); _writeScope(); + _hasWrittenWindowSnapshot = startTime == null || startTime == 0; Simulator.preTick.listen((_) { if (Simulator.time != _currentDumpingTimestamp) { @@ -255,6 +278,7 @@ class WaveformService extends ArtifactProducingService { _captureTimestamp(_currentDumpingTimestamp); } _currentDumpingTimestamp = Simulator.time; + _writeWindowSnapshotIfNeeded(Simulator.time); } }); @@ -265,7 +289,6 @@ class WaveformService extends ArtifactProducingService { }); if (register) { - current = this; ModuleServices.instance.register(this); } } @@ -415,6 +438,7 @@ class WaveformService extends ArtifactProducingService { return; } + _writeWindowSnapshotIfNeeded(timestamp); _writeToBuffer('#$timestamp\n'); final snapshot = Set.of(_changedThisTimestamp); @@ -427,6 +451,19 @@ class WaveformService extends ArtifactProducingService { onTimestampCapture(timestamp, snapshot); } + void _writeWindowSnapshotIfNeeded(int timestamp) { + if (_hasWrittenWindowSnapshot || + startTime == null || + timestamp < startTime! || + !_isInRecordingWindow(startTime!)) { + return; + } + + _writeToBuffer('#$startTime\n'); + _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + _hasWrittenWindowSnapshot = true; + } + void _writeSignalValueUpdate(Logic signal) { final binaryValue = signal.value.reversed .toList() @@ -442,16 +479,22 @@ class WaveformService extends ArtifactProducingService { // ─── Buffered I/O ───────────────────────────────────────────── void _writeToBuffer(String contents) { - _fileBuffer.write(contents); - _inMemoryOutput.write(contents); - if (_fileBuffer.length > flushBufferSize) { + if (writeToFile) { + _fileBuffer.write(contents); + } + if (retainInMemory) { + _inMemoryOutput.write(contents); + } + if (writeToFile && _fileBuffer.length > flushBufferSize) { _flushBuffer(); } } void _flushBuffer() { - _outFileSink?.write(_fileBuffer.toString()); - _fileBuffer.clear(); + if (writeToFile) { + _outFileSink!.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } } Future _terminate() async { @@ -463,14 +506,24 @@ class WaveformService extends ArtifactProducingService { // ─── Inspection ─────────────────────────────────────────────── /// The waveform artifact produced by this service. + /// + /// File-backed artifacts stream directly from the output file, avoiding a + /// second whole-trace allocation. In-memory artifacts are available only + /// when [retainInMemory] is explicitly enabled. @override - Iterable get artifacts => [ - ModuleServiceArtifact( - fileName: outputFileName ?? '$outputBaseName.${format.fileExtension}', - mediaType: format.mediaType, - openRead: () => Stream.value(utf8.encode(_inMemoryOutput.toString())), - ), - ]; + Iterable get artifacts sync* { + if (!writeToFile && !retainInMemory) { + return; + } + + yield ModuleServiceArtifact( + fileName: outputFileName ?? '$outputBaseName.${format.fileExtension}', + mediaType: format.mediaType, + openRead: writeToFile + ? () => File(outputFilePath).openRead() + : () => Stream.value(utf8.encode(_inMemoryOutput.toString())), + ); + } /// Returns a JSON-serialisable summary of this service. @override @@ -479,6 +532,7 @@ class WaveformService extends ArtifactProducingService { 'outputBaseName': outputBaseName, 'outputFilePath': outputFilePath, 'writeToFile': writeToFile, + 'retainInMemory': retainInMemory, 'format': format.name, 'signalCount': _signalToMarkerMap.length, 'timescale': timescale, diff --git a/lib/src/module.dart b/lib/src/module.dart index 602e56bfa..f7cb03344 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1147,10 +1147,13 @@ abstract class Module { register: false, ).output; - /// Attaches waveform dumping for this [Module] to a VCD at [outputPath]. + /// Attaches bounded-memory waveform dumping for this [Module] to a VCD at + /// [outputPath]. /// - /// For filtering, alternative formats, and other waveform controls, use - /// [WaveformService] directly. + /// The resulting service streams artifacts from the output file rather than + /// retaining the complete trace. For filtering, alternative formats, + /// in-memory history, and other waveform controls, use [WaveformService] + /// directly. WaveformService dumpWaves({String outputPath = 'waves.vcd'}) { final normalized = outputPath.replaceAll(r'\', '/'); final separatorIndex = normalized.lastIndexOf('/'); diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart index c63104907..fd67ce7c4 100644 --- a/lib/src/synthesizers/netlist/netlist_service.dart +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -37,10 +37,11 @@ class NetlistService extends ArtifactProducingService { static const String formatVersion = '0.0.5'; /// The most recently registered [NetlistService], or `null`. - static NetlistService? current; - - /// The default location written by [write], or `null`. - final String? outputPath; + /// + /// This is backed by [ModuleServices], so it is cleared by unregistering + /// this service type or resetting the registry. + static NetlistService? get current => + ModuleServices.instance.lookup(); /// The [NetlistSynthesizer] used for synthesis. late final NetlistSynthesizer synthesizer; @@ -65,15 +66,16 @@ class NetlistService extends ArtifactProducingService { /// Creates a netlist service for a built [module]. /// - /// Uses [configuration] for netlist synthesis and optionally - /// [register]s this instance with [ModuleServices] for DevTools lookup. + /// Uses [configuration] for netlist synthesis and optionally [register]s + /// this instance with [ModuleServices] for DevTools lookup. Construction + /// does not write to the filesystem; call [writeOutputs] to write the + /// service artifacts to [outputDirectory]. NetlistService( Module module, { NetlistSynthesizerConfiguration configuration = const NetlistSynthesizerConfiguration(), String? packageRoot, bool register = true, - this.outputPath, super.outputDirectory, super.outputBaseName, }) : super(module) { @@ -98,12 +100,7 @@ class NetlistService extends ArtifactProducingService { (decoded['modules'] as Map?) ?? {}; _loadedVersion = decoded['version'] as String?; - if (outputPath != null) { - write(); - } - if (register) { - current = this; ModuleServices.instance.register(this); } } @@ -148,12 +145,11 @@ class NetlistService extends ArtifactProducingService { /// Returns the full netlist hierarchy as a JSON string. String get json => _fullJson; - /// Writes the full netlist [json] to [path], or to [outputPath] when [path] - /// is omitted. - void write([String? path]) { - final target = - path ?? outputPath ?? '$outputDirectory/$outputBaseName.rohd.json'; - writeOutputTextFile(target, _fullJson); + /// Writes this service's artifacts to [outputDirectory]. + /// + /// The written filename always matches the corresponding artifact name. + void writeOutputs() { + writeOutputTextFile('$outputDirectory/$outputBaseName.rohd.json', json); } /// Returns a JSON-serialisable summary of the netlist synthesis. diff --git a/lib/src/synthesizers/systemverilog/system_verilog_service.dart b/lib/src/synthesizers/systemverilog/system_verilog_service.dart index 76798e522..59f28dd83 100644 --- a/lib/src/synthesizers/systemverilog/system_verilog_service.dart +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -38,7 +38,11 @@ import 'package:rohd/src/utilities/timestamper.dart'; /// ``` class SystemVerilogService extends CodeGenService { /// The most recently registered [SystemVerilogService], or `null`. - static SystemVerilogService? current; + /// + /// This is backed by [ModuleServices], so it is cleared by unregistering + /// this service type or resetting the registry. + static SystemVerilogService? get current => + ModuleServices.instance.lookup(); /// The separator inserted between module definitions in the /// concatenated single-file output from [allContents]. @@ -93,7 +97,6 @@ class SystemVerilogService extends CodeGenService { fileContents = synthBuilder.getSynthFileContents(); if (register) { - current = this; ModuleServices.instance.register(this); } } diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 7fbd15dea..563c38e94 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -13,7 +13,7 @@ import 'package:rohd/rohd.dart'; /// /// [WaveDumper] is a simple wrapper around [WaveformService] for backward /// compatibility. It provides the legacy API for recording all signal changes -/// in a simulation to a VCD file. +/// in a simulation to a VCD file without retaining the complete VCD in memory. /// /// **Migration guide:** /// diff --git a/test/module_services_test.dart b/test/module_services_test.dart index 1db39572b..51ce65181 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -36,8 +36,8 @@ class FakeService implements ModuleService { } void main() { - tearDown(() { - SystemVerilogService.current = null; + tearDown(() async { + await Simulator.reset(); ModuleServices.instance.reset(); }); @@ -97,6 +97,74 @@ void main() { expect(ModuleServices.instance.rootModule, isNull); expect(ModuleServices.instance.lookup(), isNull); }); + + test('service current accessors follow latest registrations', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final firstWaveform = WaveformService(mod); + final secondWaveform = WaveformService(mod); + final firstNetlist = NetlistService(mod); + final secondNetlist = NetlistService(mod); + final firstSv = SystemVerilogService(mod); + final secondSv = SystemVerilogService(mod); + + expect(WaveformService.current, same(secondWaveform)); + expect(NetlistService.current, same(secondNetlist)); + expect(SystemVerilogService.current, same(secondSv)); + expect(ModuleServices.instance.lookup(), + isNot(same(firstWaveform))); + expect(ModuleServices.instance.lookup(), + isNot(same(firstNetlist))); + expect(ModuleServices.instance.lookup(), + isNot(same(firstSv))); + }); + + test('service opt-out does not replace a registered service', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final waveform = WaveformService(mod); + final netlist = NetlistService(mod); + final sv = SystemVerilogService(mod); + WaveformService(mod, register: false); + NetlistService(mod, register: false); + SystemVerilogService(mod, register: false); + + expect(WaveformService.current, same(waveform)); + expect(NetlistService.current, same(netlist)); + expect(SystemVerilogService.current, same(sv)); + }); + + test('unregister clears matching service current accessors', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + WaveformService(mod); + NetlistService(mod); + SystemVerilogService(mod); + + ModuleServices.instance.unregister(); + ModuleServices.instance.unregister(); + ModuleServices.instance.unregister(); + + expect(WaveformService.current, isNull); + expect(NetlistService.current, isNull); + expect(SystemVerilogService.current, isNull); + }); + + test('reset clears every service current accessor', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + WaveformService(mod); + NetlistService(mod); + SystemVerilogService(mod); + + ModuleServices.instance.reset(); + + expect(WaveformService.current, isNull); + expect(NetlistService.current, isNull); + expect(SystemVerilogService.current, isNull); + }); }); group('SystemVerilogService', () { @@ -303,4 +371,60 @@ void main() { ); }); }); + + group('NetlistService', () { + test('construction does not write configured output', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('netlist_test_'); + try { + final netlist = NetlistService( + mod, + outputDirectory: dir.path, + outputBaseName: 'configured', + ); + + expect(netlist.json, isNotEmpty); + expect(File('${dir.path}/configured.rohd.json').existsSync(), isFalse); + } finally { + dir.deleteSync(recursive: true); + } + }); + + test('artifact defaults to the module definition name', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final netlist = NetlistService(mod); + + final artifact = netlist.artifacts.single; + + expect(artifact.fileName, equals('${mod.definitionName}.rohd.json')); + expect(artifact.mediaType, equals('application/json')); + expect( + utf8.decode( + await artifact.openRead().expand((bytes) => bytes).toList(), + ), + equals(netlist.json), + ); + }); + + test('writeOutputs writes the configured artifact name', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final dir = Directory.systemTemp.createTempSync('netlist_test_'); + try { + final netlist = NetlistService( + mod, + outputDirectory: dir.path, + outputBaseName: 'out', + )..writeOutputs(); + + final outputFile = File('${dir.path}/out.rohd.json'); + expect(outputFile.existsSync(), isTrue); + expect(outputFile.readAsStringSync(), equals(netlist.json)); + } finally { + dir.deleteSync(recursive: true); + } + }); + }); } diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index 599d05929..82289887f 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -11,6 +11,7 @@ library; import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:rohd/rohd.dart'; @@ -143,9 +144,19 @@ void main() { expect(service.module, same(mod)); expect(service.outputPath, outputPath); expect(service.outputFilePath, outputPath); + expect(service.retainInMemory, isFalse); expect(File(service.outputPath).existsSync(), isTrue); await Simulator.run(); + + expect( + (await service.artifacts.single + .openRead() + .expand((bytes) => bytes) + .toList()) + .isNotEmpty, + isTrue, + ); deleteTemporaryDump(dumpName); }); @@ -163,7 +174,8 @@ void main() { File(outputPath).deleteSync(); }); - test('waveform artifact derives its extension from the format', () async { + test('file-only capture streams its artifact after simulation finalization', + () async { final mod = SimpleModule(Logic()); await mod.build(); @@ -171,17 +183,88 @@ void main() { mod, outputDirectory: tempDumpDir, outputBaseName: 'capture', + writeToFile: true, ); final artifact = waveformService.artifacts.single; expect(artifact.fileName, equals('capture.vcd')); expect(artifact.mediaType, equals('text/x-vcd')); - expect(File(waveformService.outputFilePath).existsSync(), isFalse); + expect(waveformService.retainInMemory, isFalse); + expect(File(waveformService.outputFilePath).existsSync(), isTrue); + + await Simulator.run(); + expect( (await artifact.openRead().expand((bytes) => bytes).toList()).isNotEmpty, isTrue, ); + + File(waveformService.outputFilePath).deleteSync(); + }); + + test('in-memory-only debugging capture retains waveform history', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final waveformService = WaveformService( + mod, + outputDirectory: tempDumpDir, + outputBaseName: 'in_memory_capture', + ); + + expect(waveformService.writeToFile, isFalse); + expect(waveformService.retainInMemory, isTrue); + expect(File(waveformService.outputFilePath).existsSync(), isFalse); + + await Simulator.run(); + + final bytes = await waveformService.artifacts.single + .openRead() + .expand((bytes) => bytes) + .toList(); + expect(utf8.decode(bytes), contains(r'$enddefinitions')); + }); + + test('capture without a file or retained history has no artifact', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final waveformService = WaveformService(mod, retainInMemory: false); + + expect(waveformService.artifacts, isEmpty); + }); + + test('recording window snapshots stable signal values at its start', + () async { + final a = Logic(name: 'a'); + final mod = SimpleModule(a); + await mod.build(); + a.inject(0); + + const dumpName = 'windowInitialSnapshot'; + Directory(tempDumpDir).createSync(recursive: true); + WaveformService( + mod, + outputDirectory: tempDumpDir, + outputBaseName: 'temp_dump_$dumpName', + writeToFile: true, + startTime: 10, + ); + + Simulator.registerAction(5, () => a.put(1)); + Simulator.registerAction(15, () {}); + Simulator.registerAction(20, () {}); + await Simulator.run(); + + final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); + expect( + VcdParser.confirmValue(vcdContents, 'a', 15, LogicValue.one), + isTrue, + reason: 'the stable value at startTime must seed the recording window', + ); + + deleteTemporaryDump(dumpName); }); test('rejects formats without a matching waveform writer', () async { From 5ab7534d599c4f905c38f9d93952bfb825e9d3d0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 16 Sep 2026 07:27:36 -0700 Subject: [PATCH 12/36] waveform service stores on demand only, service registration reworked Signed-off-by: Desmond A. Kirkpatrick --- lib/src/diagnostics/waveform_service.dart | 19 ++++++++++++++++ test/waveform_service_test.dart | 27 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index 736d1816b..0aef63a67 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -44,6 +44,15 @@ enum WaveOutputFormat { WaveOutputFormat.vcd => 'text/x-vcd', WaveOutputFormat.fst => 'application/vnd.gtkwave.fst', }; + + /// Whether this format supports querying waveform data directly from a file. + /// + /// FST is indexed and can support on-disk queries without retaining the + /// entire waveform in memory. VCD is a sequential text format and cannot. + bool get supportsOnDiskQueries => switch (this) { + WaveOutputFormat.vcd => false, + WaveOutputFormat.fst => true, + }; } /// Policy applied when the output file already exists at construction time. @@ -178,6 +187,16 @@ class WaveformService extends ArtifactProducingService { /// after simulation. final bool retainInMemory; + /// Whether this service can provide waveform data to a consumer. + /// + /// Capture can be sent when complete history is retained in memory, or when + /// a file-backed [format] supports indexed on-disk queries. This lets + /// consumers select waveform-capable services without depending on a + /// particular retention strategy. VCD requires [retainInMemory]; FST can + /// provide this capability from a file once FST writing is supported. + bool canSendWaveforms() => + retainInMemory || (writeToFile && format.supportsOnDiskQueries); + // ─── Internal file-writing state ───────────────────────────── /// Sink writing to [outputFilePath] when [writeToFile] is true. diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index 82289887f..17a71b7fe 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -145,6 +145,7 @@ void main() { expect(service.outputPath, outputPath); expect(service.outputFilePath, outputPath); expect(service.retainInMemory, isFalse); + expect(service.canSendWaveforms(), isFalse); expect(File(service.outputPath).existsSync(), isTrue); await Simulator.run(); @@ -191,6 +192,7 @@ void main() { expect(artifact.fileName, equals('capture.vcd')); expect(artifact.mediaType, equals('text/x-vcd')); expect(waveformService.retainInMemory, isFalse); + expect(waveformService.canSendWaveforms(), isFalse); expect(File(waveformService.outputFilePath).existsSync(), isTrue); await Simulator.run(); @@ -215,6 +217,7 @@ void main() { expect(waveformService.writeToFile, isFalse); expect(waveformService.retainInMemory, isTrue); + expect(waveformService.canSendWaveforms(), isTrue); expect(File(waveformService.outputFilePath).existsSync(), isFalse); await Simulator.run(); @@ -226,6 +229,24 @@ void main() { expect(utf8.decode(bytes), contains(r'$enddefinitions')); }); + test('retained file-backed capture can send waveforms', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final waveformService = WaveformService( + mod, + outputDirectory: tempDumpDir, + outputBaseName: 'retained_file_capture', + writeToFile: true, + retainInMemory: true, + ); + + expect(waveformService.canSendWaveforms(), isTrue); + + await Simulator.run(); + File(waveformService.outputFilePath).deleteSync(); + }); + test('capture without a file or retained history has no artifact', () async { final mod = SimpleModule(Logic()); await mod.build(); @@ -233,6 +254,12 @@ void main() { final waveformService = WaveformService(mod, retainInMemory: false); expect(waveformService.artifacts, isEmpty); + expect(waveformService.canSendWaveforms(), isFalse); + }); + + test('FST supports querying waveforms from a file', () { + expect(WaveOutputFormat.vcd.supportsOnDiskQueries, isFalse); + expect(WaveOutputFormat.fst.supportsOnDiskQueries, isTrue); }); test('recording window snapshots stable signal values at its start', From 575b8d12ce353ff0ad92f5b9461f74ea92d5173f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 16 Sep 2026 17:10:13 -0700 Subject: [PATCH 13/36] need to thread option for waveform in memory for debug --- lib/src/module.dart | 15 ++++++++------- test/waveform_service_test.dart | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index f7cb03344..072ebc326 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1147,14 +1147,14 @@ abstract class Module { register: false, ).output; - /// Attaches bounded-memory waveform dumping for this [Module] to a VCD at - /// [outputPath]. + /// Attaches waveform dumping for this [Module] to a VCD at [outputPath]. /// - /// The resulting service streams artifacts from the output file rather than - /// retaining the complete trace. For filtering, alternative formats, - /// in-memory history, and other waveform controls, use [WaveformService] - /// directly. - WaveformService dumpWaves({String outputPath = 'waves.vcd'}) { + /// Set [retainInMemory] to `true` when a debugger needs to query the + /// complete waveform history while the simulation is running. + WaveformService dumpWaves({ + String outputPath = 'waves.vcd', + bool retainInMemory = false, + }) { final normalized = outputPath.replaceAll(r'\', '/'); final separatorIndex = normalized.lastIndexOf('/'); final outputDirectory = switch (separatorIndex) { @@ -1169,6 +1169,7 @@ abstract class Module { outputDirectory: outputDirectory, outputFileName: outputFileName, writeToFile: true, + retainInMemory: retainInMemory, ); } diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index 17a71b7fe..94a7351a3 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -161,6 +161,22 @@ void main() { deleteTemporaryDump(dumpName); }); + test('dumpWaves can retain history for debugging', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + + final service = mod.dumpWaves( + outputPath: temporaryDumpPath('debugDump'), + retainInMemory: true, + ); + + expect(service.retainInMemory, isTrue); + expect(service.canSendWaveforms(), isTrue); + + await Simulator.run(); + deleteTemporaryDump('debugDump'); + }); + test('dumpWaves preserves an arbitrary legacy output filename', () async { final mod = SimpleModule(Logic()); await mod.build(); From 3faae0552eafcc18bea881aa52444abbb317f40a Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 06:08:11 -0700 Subject: [PATCH 14/36] Stop pinning material_ui below main Restore the material_ui dependency range used by main and remove the obsolete cupertino_ui override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rohd_devtools_extension/pubspec.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index e67196a58..f352a624f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -19,8 +19,7 @@ dependencies: flutter_bloc: ^9.0.0 flutter_simple_treeview: ^3.0.2 logging: ^1.2.0 - # Keep below 1.3.0 for the Dart 3.6 CI toolchain. - material_ui: ">=1.1.0 <1.3.0" + material_ui: ^1.1.0 rohd_devtools_widgets: path: ../packages/rohd_devtools_widgets vm_service: ^14.3.1 @@ -41,8 +40,6 @@ flutter: - assets/icons/ dependency_overrides: - # Keep the transitive Cupertino UI package below the Dart 3.6-incompatible release. - cupertino_ui: ">=1.0.0 <1.1.0" rohd: path: ../ rohd_hierarchy: From acb1d3e57a1ae43197787e7afe06073aac72354a Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 19:46:56 -0700 Subject: [PATCH 15/36] Constrain Material UI to compatible releases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rohd_devtools_extension/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index f352a624f..cc39f7a29 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_bloc: ^9.0.0 flutter_simple_treeview: ^3.0.2 logging: ^1.2.0 - material_ui: ^1.1.0 + material_ui: ">=1.1.0 <1.3.0" rohd_devtools_widgets: path: ../packages/rohd_devtools_widgets vm_service: ^14.3.1 From eb99c1bc9fc6801facd4dfef79689b9cd8484724 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 21:25:02 -0700 Subject: [PATCH 16/36] Pin Cupertino UI for DevTools CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rohd_devtools_extension/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index cc39f7a29..99c70653d 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -40,6 +40,8 @@ flutter: - assets/icons/ dependency_overrides: + # Keep the transitive Cupertino UI package below the Dart 3.6-incompatible release. + cupertino_ui: ">=1.0.0 <1.1.0" rohd: path: ../ rohd_hierarchy: From c172309c910aed5ff60e19d1d853705a3c861e36 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 21:48:35 -0700 Subject: [PATCH 17/36] Use compatible Flutter SDK for DevTools CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build_devtool.yml | 2 +- .github/workflows/general.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_devtool.yml b/.github/workflows/build_devtool.yml index f6c7ada43..06e4084ac 100644 --- a/.github/workflows/build_devtool.yml +++ b/.github/workflows/build_devtool.yml @@ -22,7 +22,7 @@ jobs: uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.44.0 + version: 3.47.2 - name: Run Flutter Test run: tool/gh_actions/devtool/run_devtool_test.sh diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 14010b74a..7a6e5e72b 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -170,7 +170,7 @@ jobs: uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.44.0 + version: 3.47.2 - name: Run Flutter Test run: tool/gh_actions/devtool/run_devtool_test.sh From 659758d8d62f3683aeecd34304549a3cec1b3c52 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 23:43:39 -0700 Subject: [PATCH 18/36] test: suppress expected Verilator filename warning Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/verilator_compilation_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/verilator_compilation_test.dart b/test/verilator_compilation_test.dart index 6fc02ae82..e765b3243 100644 --- a/test/verilator_compilation_test.dart +++ b/test/verilator_compilation_test.dart @@ -274,7 +274,7 @@ esac final module = ArrayConnectionFixture("{2'h0}"); await module.build(); if (!SimCompare.checkVerilatorVector(module, const [], - buildOnly: true, verilatorExtraArgs: ['-Wall'])) { + buildOnly: true, verilatorExtraArgs: ['-Wall', '-Wno-DECLFILENAME'])) { return; } expect( From 242392d89dd40a45a31d856173fc8648d49bb134 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 07:41:13 -0700 Subject: [PATCH 19/36] fix: notify waveform hooks of window snapshots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/diagnostics/waveform_service.dart | 21 +++++--- test/waveform_service_test.dart | 65 +++++++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index 0aef63a67..e3d146650 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -322,19 +322,21 @@ class WaveformService extends ArtifactProducingService { @protected void onSignalCollected(Logic signal) {} - /// Called for every value-change event on [signal] at [timestamp]. + /// Called for every captured value on [signal] at [timestamp]. /// - /// Only called within the [startTime] / [stopTime] window. + /// When [startTime] is set, this includes one window-entry value for every + /// tracked signal at [startTime]. Those calls describe the state entering + /// the recording window, rather than physical transitions. /// /// 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. + /// Called once after each batch of captured values at [timestamp]. /// - /// [changed] is the set of signals that changed at [timestamp]. + /// When [startTime] is set, the complete window-entry signal snapshot is + /// delivered as a batch at [startTime] before later value-change batches. /// /// Override in a subclass to flush incremental streaming payloads. /// Always call `super` first. @@ -479,8 +481,15 @@ class WaveformService extends ArtifactProducingService { } _writeToBuffer('#$startTime\n'); - _signalToMarkerMap.keys.forEach(_writeSignalValueUpdate); + final snapshot = Set.of(_signalToMarkerMap.keys); + for (final signal in snapshot) { + _writeSignalValueUpdate(signal); + onValueChange(signal, startTime!); + } _hasWrittenWindowSnapshot = true; + if (snapshot.isNotEmpty) { + onTimestampCapture(startTime!, snapshot); + } } void _writeSignalValueUpdate(Logic signal) { diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index 94a7351a3..d6603217d 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -25,6 +25,41 @@ class SimpleModule extends Module { } } +class WindowWaveModule extends Module { + late final Logic a; + + WindowWaveModule() { + a = addOutput('a'); + } +} + +class HistoryWaveformService extends WaveformService { + final Map> history = {}; + final List capturedTimestamps = []; + + HistoryWaveformService(super.module, {super.startTime}); + + @override + void onSignalCollected(Logic signal) { + history[signal] = [(Simulator.time, binaryValue(signal))]; + } + + @override + void onValueChange(Logic signal, int timestamp) { + history[signal]!.add((timestamp, binaryValue(signal))); + } + + @override + void onTimestampCapture(int timestamp, Set changed) { + capturedTimestamps.add(timestamp); + } +} + +String binaryValue(Logic signal) => signal.value.reversed + .toList() + .map((value) => value.toString(includeWidth: false)) + .join(); + class SimpleModWithSeq extends Module { Logic get val => output('val'); SimpleModWithSeq(Logic asyncReset, Logic clk) { @@ -310,6 +345,36 @@ void main() { deleteTemporaryDump(dumpName); }); + test('window-entry snapshot reaches waveform hooks', () async { + final mod = WindowWaveModule(); + await mod.build(); + mod.a.put(0); + final service = HistoryWaveformService(mod, startTime: 10); + + Simulator.registerAction(5, () => mod.a.put(1)); + Simulator.registerAction(15, () {}); + Simulator.registerAction(20, () {}); + await Simulator.run(); + + final vcdContents = utf8.decode( + await service.artifacts.single + .openRead() + .expand((bytes) => bytes) + .toList(), + ); + expect( + VcdParser.confirmValue( + vcdContents, + 'a', + 10, + LogicValue.one, + ), + isTrue, + ); + expect(service.history[mod.a], equals([(0, '0'), (10, '1')])); + expect(service.capturedTimestamps, contains(10)); + }); + test('rejects formats without a matching waveform writer', () async { final mod = SimpleModule(Logic()); await mod.build(); From 237746230e88d1f2ba5e3cb71e65b4989a86d7b5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 07:57:09 -0700 Subject: [PATCH 20/36] docs: clarify netlist module ownership Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/synthesizers/netlist/netlist_service.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart index fd67ce7c4..fedee30a4 100644 --- a/lib/src/synthesizers/netlist/netlist_service.dart +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -55,7 +55,7 @@ class NetlistService extends ArtifactProducingService { /// Cached per-module JSON, keyed by definition name. final Map _moduleJsonCache = {}; - /// The parsed modules map from the combined JSON. + /// The service-owned parsed modules map from the combined JSON. late final Map _modulesMap; /// The package root directory used for FLC trace injection. @@ -190,10 +190,17 @@ class NetlistService extends ArtifactProducingService { /// Returns the set of module definition names in the netlist. Set get moduleNames => _modulesMap.keys.toSet(); - /// Read-only access to the parsed modules map. + /// Read-only, zero-copy access to the parsed modules map. /// /// Each key is a definition name and each value is the Yosys-style - /// module descriptor containing `ports`, `cells`, and `netnames`. + /// module descriptor containing `ports`, `cells`, and `netnames`. The + /// outer map is unmodifiable, but its nested module/cell maps and bit lists + /// are shared with this service's internal representation. + /// + /// Callers must treat the complete returned object graph as immutable. + /// Mutating nested values is unsupported and can make uncached [moduleJson] + /// or [slimJson] results disagree with previously serialized or cached + /// service views. Create a caller-owned copy before making modifications. Map get synthesizedModules => Map.unmodifiable(_modulesMap); From 70d7bc4ccd8923df584a2be4398277097ba645d1 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 07:58:37 -0700 Subject: [PATCH 21/36] fix: preserve netlist version across views Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../synthesizers/netlist/netlist_service.dart | 4 ++-- test/module_services_test.dart | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart index fedee30a4..4046c2f5e 100644 --- a/lib/src/synthesizers/netlist/netlist_service.dart +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -182,7 +182,7 @@ class NetlistService extends ArtifactProducingService { } return jsonEncode({ 'creator': 'ROHD netlist synthesizer', - 'version': formatVersion, + 'version': version, 'modules': {definitionName: modData}, }); }); @@ -304,7 +304,7 @@ class NetlistService extends ArtifactProducingService { return jsonEncode({ 'netlist': { 'creator': 'ROHD NetlistService (slim)', - 'version': formatVersion, + 'version': version, 'rootInstanceName': rootName, 'modules': slimModules, }, diff --git a/test/module_services_test.dart b/test/module_services_test.dart index 51ce65181..84a7e7a4b 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -373,6 +373,24 @@ void main() { }); group('NetlistService', () { + test('uses the synthesized format version across all JSON views', () async { + final mod = SimpleModule(Logic()); + await mod.build(); + final netlist = NetlistService(mod); + + final full = jsonDecode(netlist.json) as Map; + final module = jsonDecode(netlist.moduleJson(mod.definitionName)) + as Map; + final slim = jsonDecode(netlist.slimJson) as Map; + + expect(netlist.version, equals(full['version'])); + expect(module['version'], equals(full['version'])); + expect( + (slim['netlist'] as Map)['version'], + equals(full['version']), + ); + }); + test('construction does not write configured output', () async { final mod = SimpleModule(Logic()); await mod.build(); From 390d5fd3d478637b91d31423a9cc18172ef1adbe Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 08:01:57 -0700 Subject: [PATCH 22/36] fix: align module service contracts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/diagnostics/module_services.dart | 19 ++++--- lib/src/diagnostics/waveform_service.dart | 51 +++++++++++-------- .../synthesizers/netlist/netlist_service.dart | 41 ++++++++++----- test/module_services_test.dart | 9 ++++ test/waveform_service_test.dart | 9 ++++ 5 files changed, 85 insertions(+), 44 deletions(-) diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart index f974e41b2..7ed82e87e 100644 --- a/lib/src/diagnostics/module_services.dart +++ b/lib/src/diagnostics/module_services.dart @@ -14,18 +14,21 @@ 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. -/// Those accessors are backed by this registry, so both discovery routes have -/// the same registration lifetime. +/// Services register themselves here on construction under the type argument +/// supplied to [register] and are retrieved with [lookup]. This key is not +/// necessarily the runtime concrete type: for example, a [WaveformService] +/// subclass registered as `register(this)` is available under +/// [WaveformService], not automatically under its subclass type. 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. Those accessors are backed by this registry, so +/// both discovery routes have the same registration lifetime. /// /// The registry references no specific service type, so it is identical across /// all feature branches that contribute services. /// -/// Service registrations are independent latest registrations by concrete -/// service type; they are not an atomic set associated with [rootModule]. +/// Service registrations are independent latest registrations by registration +/// type; they are not an atomic set associated with [rootModule]. /// [reset] explicitly clears both the hierarchy and every service /// registration. [Simulator.reset] does not reset this registry, allowing /// generated service results to remain available after a simulation ends. diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index e3d146650..01b6b5476 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -82,11 +82,11 @@ enum OverwritePolicy { /// /// - [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 +/// - [onValueChange] — called once for each signal in a captured timestamp's +/// coalesced final-value set; 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. +/// - [onTimestampCapture] — called after each captured timestamp batch, +/// including the possibly empty finalization batch. /// - [onSimulationEnd] — called after the final timestamp is written and /// the file is closed; use it to finalise any streaming buffers. /// @@ -187,13 +187,15 @@ class WaveformService extends ArtifactProducingService { /// after simulation. final bool retainInMemory; - /// Whether this service can provide waveform data to a consumer. + /// Whether this service can service debugger waveform-data queries. /// - /// Capture can be sent when complete history is retained in memory, or when - /// a file-backed [format] supports indexed on-disk queries. This lets - /// consumers select waveform-capable services without depending on a - /// particular retention strategy. VCD requires [retainInMemory]; FST can - /// provide this capability from a file once FST writing is supported. + /// A `true` result promises that a debugger can request captured waveform + /// values, such as selected signals over a time interval. It does not + /// promise that [artifacts] can transfer waveform-file bytes. Capture can be + /// queried when complete history is retained in memory, or when a + /// file-backed [format] supports indexed on-disk queries. VCD requires + /// [retainInMemory]; FST can provide this capability from a file once FST + /// writing is supported. bool canSendWaveforms() => retainInMemory || (writeToFile && format.supportsOnDiskQueries); @@ -238,9 +240,9 @@ class WaveformService extends ArtifactProducingService { /// timescale, start/stop times, flush size, and overwrite policy. /// /// In-memory-only VCD debugging captures retain the complete waveform by - /// default. Set [retainInMemory] explicitly to choose whole-history - /// retention; file-backed captures default to bounded memory while retaining - /// a streamable artifact on disk. + /// default. Set [retainInMemory] to override these defaults; file-backed + /// captures default to bounded memory while retaining a streamable artifact + /// on disk. WaveformService( Module module, { super.outputDirectory, @@ -259,10 +261,7 @@ class WaveformService extends ArtifactProducingService { }) : retainInMemory = retainInMemory ?? !writeToFile, super(module) { if (!module.hasBuilt) { - throw Exception( - 'Module must be built before creating WaveformService. ' - 'Call build() first.', - ); + throw ModuleNotBuiltException(module); } if (format != WaveOutputFormat.vcd) { throw UnsupportedError( @@ -322,7 +321,10 @@ class WaveformService extends ArtifactProducingService { @protected void onSignalCollected(Logic signal) {} - /// Called for every captured value on [signal] at [timestamp]. + /// Called once for each signal's final captured value at [timestamp]. + /// + /// Multiple changes to the same signal within a simulation timestamp are + /// coalesced, so this hook receives that signal once with its final value. /// /// When [startTime] is set, this includes one window-entry value for every /// tracked signal at [startTime]. Those calls describe the state entering @@ -333,10 +335,11 @@ class WaveformService extends ArtifactProducingService { @protected void onValueChange(Logic signal, int timestamp) {} - /// Called once after each batch of captured values at [timestamp]. + /// Called once after each captured timestamp batch. /// /// When [startTime] is set, the complete window-entry signal snapshot is /// delivered as a batch at [startTime] before later value-change batches. + /// Finalization invokes this hook even when its [changed] set is empty. /// /// Override in a subclass to flush incremental streaming payloads. /// Always call `super` first. @@ -535,9 +538,13 @@ class WaveformService extends ArtifactProducingService { /// The waveform artifact produced by this service. /// - /// File-backed artifacts stream directly from the output file, avoiding a - /// second whole-trace allocation. In-memory artifacts are available only - /// when [retainInMemory] is explicitly enabled. + /// The artifact is complete after simulation finalization. During capture, + /// file-backed artifacts expose only bytes already flushed to + /// [outputFilePath]; the current write buffer is not visible. Each + /// file-backed [ModuleServiceArtifact.openRead] opens the current file, not + /// an immutable snapshot or a live tail, so concurrent capture may change + /// what a read observes. In-memory artifacts are available whenever + /// [retainInMemory] is enabled, including its automatic memory-only default. @override Iterable get artifacts sync* { if (!writeToFile && !retainInMemory) { diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart index 4046c2f5e..9d1eec1ac 100644 --- a/lib/src/synthesizers/netlist/netlist_service.dart +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -80,20 +80,14 @@ class NetlistService extends ArtifactProducingService { super.outputBaseName, }) : super(module) { if (!module.hasBuilt) { - throw Exception( - 'Module must be built before creating NetlistService. ' - 'Call build() first.', - ); + throw ModuleNotBuiltException(module); } final effectiveRoot = packageRoot; synthesizer = NetlistSynthesizer(configuration: configuration); this.packageRoot = effectiveRoot; synthBuilder = SynthBuilder(module, synthesizer); - _fullJson = synthesizer.synthesizeToJson( - module, - packageRoot: effectiveRoot, - ); + _fullJson = synthesizer.generateCombinedJson(synthBuilder, module); final decoded = jsonDecode(_fullJson) as Map; _modulesMap = @@ -127,7 +121,7 @@ class NetlistService extends ArtifactProducingService { /// Checks whether [version] is compatible with the current /// [formatVersion]. /// - /// Compatible means the major version matches. Returns `true` if + /// Compatible means both major and minor versions match. Returns `true` if /// the loaded JSON can be consumed by this version of the service. static bool isCompatibleVersion(String version) { final current = formatVersion.split('.'); @@ -163,10 +157,17 @@ class NetlistService extends ArtifactProducingService { 'modules': moduleNames.toList(), }; - /// Returns the netlist JSON for a single module [definitionName]. + /// Returns the netlist JSON for the module named [definitionName]. /// - /// The returned JSON is keyed by definition name: - /// `{"DefinitionName": { ports, cells, netnames }}`. + /// [definitionName] must be one of the generated definition names returned + /// by [moduleNames]. The returned JSON has this shape: + /// ```json + /// { + /// "creator": "ROHD netlist synthesizer", + /// "version": "...", + /// "modules": {"DefinitionName": {"ports": {}, "cells": {}, "netnames": {}}} + /// } + /// ``` /// This matches the format expected by the DevTools schematic viewer /// for incremental module fetches. /// @@ -207,8 +208,20 @@ class NetlistService extends ArtifactProducingService { /// Cached slim JSON (lazy). String? _slimJsonCache; - /// Returns a slim netlist JSON string — same structure as [toJson] but - /// with cell `connections` stripped. + /// Returns a slim netlist JSON string with cell `connections` stripped. + /// + /// The returned JSON has this shape: + /// ```json + /// { + /// "netlist": { + /// "creator": "ROHD NetlistService (slim)", + /// "version": "...", + /// "rootInstanceName": "...", + /// "modules": {"DefinitionName": {"ports": {}, "cells": {}, "netnames": {}}} + /// } + /// } + /// ``` + /// Module lookup keys are the generated definition names in [moduleNames]. /// /// The slim representation preserves ports, cells (type + port_directions /// + port_widths), and netnames so the DevTools extension can render the diff --git a/test/module_services_test.dart b/test/module_services_test.dart index 84a7e7a4b..34e2cbbd3 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -373,6 +373,15 @@ void main() { }); group('NetlistService', () { + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + + expect( + () => NetlistService(mod), + throwsA(isA()), + ); + }); + test('uses the synthesized format version across all JSON views', () async { final mod = SimpleModule(Logic()); await mod.build(); diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index d6603217d..cdbc559e5 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -196,6 +196,15 @@ void main() { deleteTemporaryDump(dumpName); }); + test('throws if module not built', () { + final mod = SimpleModule(Logic()); + + expect( + () => WaveformService(mod), + throwsA(isA()), + ); + }); + test('dumpWaves can retain history for debugging', () async { final mod = SimpleModule(Logic()); await mod.build(); From 56f49ce5254e21e4f3908dbda2c9f9c4673871e8 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 08:27:58 -0700 Subject: [PATCH 23/36] style: wrap netlist JSON documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/synthesizers/netlist/netlist_service.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart index 9d1eec1ac..1f8454b40 100644 --- a/lib/src/synthesizers/netlist/netlist_service.dart +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -165,7 +165,9 @@ class NetlistService extends ArtifactProducingService { /// { /// "creator": "ROHD netlist synthesizer", /// "version": "...", - /// "modules": {"DefinitionName": {"ports": {}, "cells": {}, "netnames": {}}} + /// "modules": { + /// "DefinitionName": {"ports": {}, "cells": {}, "netnames": {}} + /// } /// } /// ``` /// This matches the format expected by the DevTools schematic viewer @@ -217,7 +219,9 @@ class NetlistService extends ArtifactProducingService { /// "creator": "ROHD NetlistService (slim)", /// "version": "...", /// "rootInstanceName": "...", - /// "modules": {"DefinitionName": {"ports": {}, "cells": {}, "netnames": {}}} + /// "modules": { + /// "DefinitionName": {"ports": {}, "cells": {}, "netnames": {}} + /// } /// } /// } /// ``` From ed0a923c20379e4827e079adeef73322fdbddeee Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 10:05:00 -0700 Subject: [PATCH 24/36] test: reduce Verilator array matrix size Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/logic_array_test.dart | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 0d939a1ac..40721c197 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -819,20 +819,20 @@ void main() { testWithVerilator( '1d, unpacked', () => - SimpleLAPassthrough(LogicArray([3], 8, numUnpackedDimensions: 1)), + SimpleLAPassthrough(LogicArray([3], 3, numUnpackedDimensions: 1)), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); final sv = mod.dumpSystemVerilog(); - expect(sv.contains(RegExp(r'\[7:0\]\s*laIn\s*\[2:0\]')), true); - expect(sv.contains(RegExp(r'\[7:0\]\s*laOut\s*\[2:0\]')), true); + expect(sv.contains(RegExp(r'\[2:0\]\s*laIn\s*\[2:0\]')), true); + expect(sv.contains(RegExp(r'\[2:0\]\s*laOut\s*\[2:0\]')), true); }); testWithVerilator( 'single element, unpacked', () => - SimpleLAPassthrough(LogicArray([1], 8, numUnpackedDimensions: 1)), + SimpleLAPassthrough(LogicArray([1], 3, numUnpackedDimensions: 1)), (mod) async { await testArrayPassthrough(mod, noSvSim: true, noIverilog: true); }); @@ -840,7 +840,7 @@ void main() { testWithVerilator( '4d, half packed', () => SimpleLAPassthrough( - LogicArray([5, 4, 3, 2], 8, numUnpackedDimensions: 2)), + LogicArray([2, 2, 2, 2], 3, numUnpackedDimensions: 2)), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); @@ -848,11 +848,11 @@ void main() { final sv = mod.dumpSystemVerilog(); expect( sv.contains(RegExp( - r'\[2:0\]\s*\[1:0\]\s*\[7:0\]\s*laIn\s*\[4:0\]\s*\[3:0\]')), + r'\[1:0\]\s*\[1:0\]\s*\[2:0\]\s*laIn\s*\[1:0\]\s*\[1:0\]')), true); expect( sv.contains(RegExp( - r'\[2:0\]\s*\[1:0\]\s*\[7:0\]\s*laOut\s*\[4:0\]\s*\[3:0\]')), + r'\[1:0\]\s*\[1:0\]\s*\[2:0\]\s*laOut\s*\[1:0\]\s*\[1:0\]')), true); }); @@ -866,7 +866,7 @@ void main() { test('3 dimensions with interface', () async { final mod = LAPassthroughWithIntf(LAPassthroughIntf( dimensions: [3, 2, 3], - elementWidth: 8, + elementWidth: 3, numUnpackedDimensions: 0, )); @@ -889,8 +889,8 @@ void main() { // ensure ports with interface are still an array final sv = mod.dumpSystemVerilog(); - expect(sv, contains('input logic [1:0][2:0][7:0] laIn [2:0]')); - expect(sv, contains('output logic [1:0][2:0][7:0] laOut [2:0]')); + expect(sv, contains('input logic [1:0][2:0][2:0] laIn [2:0]')); + expect(sv, contains('output logic [1:0][2:0][2:0] laOut [2:0]')); }); }); @@ -908,7 +908,7 @@ void main() { testWithVerilator( '3d unpacked', () => PackAndUnpackPassthrough( - LogicArray([5, 3, 2], 8, numUnpackedDimensions: 2)), (mod) async { + LogicArray([2, 2, 2], 3, numUnpackedDimensions: 2)), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, checkNoSwizzle: false, noSvSim: true); }); @@ -934,7 +934,7 @@ void main() { testWithVerilator( '3d unpacked', () => PackAndUnpackWithArraysPassthrough( - LogicArray([4, 3, 2], 8, numUnpackedDimensions: 2), + LogicArray([2, 2, 2], 3, numUnpackedDimensions: 2), intermediateUnpacked: 1), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, checkNoSwizzle: false, noSvSim: true); @@ -950,7 +950,7 @@ void main() { testWithVerilator( '3d unpacked', () => RearrangeArraysPassthrough( - LogicArray([4, 3, 2], 8, numUnpackedDimensions: 2), + LogicArray([4, 3, 2], 3, numUnpackedDimensions: 2), intermediateUnpacked: 1), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); @@ -997,7 +997,7 @@ void main() { testWithVerilator( '3d unpacked', () => ArrayNameConflicts( - LogicArray([4, 3, 2], 8, numUnpackedDimensions: 2), + LogicArray([2, 2, 2], 3, numUnpackedDimensions: 2), intermediateUnpacked: 1), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, checkNoSwizzle: false, noSvSim: true); @@ -1015,7 +1015,7 @@ void main() { testWithVerilator( '3d unpacked', () => SimpleArraysAndHierarchy( - LogicArray([4, 3, 2], 8, numUnpackedDimensions: 2)), (mod) async { + LogicArray([2, 2, 2], 3, numUnpackedDimensions: 2)), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); @@ -1037,7 +1037,7 @@ void main() { testWithVerilator( '3d unpacked', () => FancyArraysAndHierarchy( - LogicArray([4, 3, 2], 8, numUnpackedDimensions: 2), + LogicArray([2, 2, 2], 3, numUnpackedDimensions: 2), intermediateUnpacked: 1), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, checkNoSwizzle: false, noSvSim: true); From f95a896913ac9d42797493848905b39cb0e49874 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 10:05:00 -0700 Subject: [PATCH 25/36] test: reduce Verilator array matrix size Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/logic_array_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 40721c197..a0196afe3 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -882,7 +882,7 @@ void main() { '3 dimensions with interface and unpacked', () => LAPassthroughWithIntf(LAPassthroughIntf( dimensions: [3, 2, 3], - elementWidth: 8, + elementWidth: 3, numUnpackedDimensions: 1, )), (mod) async { await testArrayPassthrough(mod, noSvSim: true); @@ -956,7 +956,7 @@ void main() { await testArrayPassthrough(mod, noSvSim: true); final sv = mod.dumpSystemVerilog(); - expect(sv.contains('logic [2:0][3:0][7:0] intermediate [1:0]'), true); + expect(sv.contains('logic [2:0][3:0][2:0] intermediate [1:0]'), true); }); }); From 424c87145cad7885cae8fd7fe03d02eca6880b5c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 12:02:14 -0700 Subject: [PATCH 26/36] test: correct LogicArray interface width Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/logic_array_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index a0196afe3..f85595141 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -874,8 +874,8 @@ void main() { // ensure ports with interface are still an array final sv = mod.dumpSystemVerilog(); - expect(sv, contains('input logic [2:0][1:0][2:0][7:0] laIn')); - expect(sv, contains('output logic [2:0][1:0][2:0][7:0] laOut')); + expect(sv, contains('input logic [2:0][1:0][2:0][2:0] laIn')); + expect(sv, contains('output logic [2:0][1:0][2:0][2:0] laOut')); }); testWithVerilator( From 5db746fa476f5869b2fd2a9e3c69157ca2a6f6b8 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 13:22:39 -0700 Subject: [PATCH 27/36] test-time reduction --- test/logic_array_test.dart | 51 ++++++++++++++------------------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index f85595141..088ec820e 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -391,21 +391,25 @@ class ConstantAssignmentArrayModule extends Module { laIn = addInputArray('laIn', laIn, dimensions: [3, 3, 3, 3], numUnpackedDimensions: laIn.numUnpackedDimensions, - elementWidth: 8); + elementWidth: laIn.elementWidth); addOutputArray('laOut', dimensions: laIn.dimensions, numUnpackedDimensions: laIn.numUnpackedDimensions, elementWidth: laIn.elementWidth); + final elementWidth = laIn.elementWidth; laOut.elements[1] <= - Const([for (var i = 0; i < 3 * 3 * 3; i++) LogicValue.ofInt(i, 8)] - .rswizzle()); + Const([ + for (var i = 0; i < 3 * 3 * 3; i++) LogicValue.ofInt(i, elementWidth) + ].rswizzle()); laOut.elements[2].elements[1] <= - (Logic(width: 3 * 3 * 8)..gets(Const(0, width: 3 * 3 * 8))); + (Logic(width: 3 * 3 * elementWidth) + ..gets(Const(0, width: 3 * 3 * elementWidth))); laOut.elements[2].elements[2].elements[1] <= - Const(1, width: 3 * 8, fill: true); - laOut.elements[2].elements[2].elements[2].elements[1] <= Const(0, width: 8); + Const(1, width: 3 * elementWidth, fill: true); + laOut.elements[2].elements[2].elements[2].elements[1] <= + Const(0, width: elementWidth); } } @@ -791,11 +795,6 @@ void main() { await testArrayPassthrough(mod); }); - test('single element', () async { - final mod = SimpleLAPassthrough(LogicArray([1], 8)); - await testArrayPassthrough(mod); - }); - test('array of bits', () async { final mod = SimpleLAPassthrough(LogicArray([8], 1)); await testArrayPassthrough(mod); @@ -811,11 +810,6 @@ void main() { await testArrayPassthrough(mod); }); - test('4 dimensions', () async { - final mod = SimpleLAPassthrough(LogicArray([5, 4, 3, 2], 8)); - await testArrayPassthrough(mod); - }); - testWithVerilator( '1d, unpacked', () => @@ -895,11 +889,6 @@ void main() { }); group('pack and unpack', () { - test('1d', () async { - final mod = PackAndUnpackPassthrough(LogicArray([3], 8)); - await testArrayPassthrough(mod, checkNoSwizzle: false); - }); - test('3d', () async { final mod = PackAndUnpackPassthrough(LogicArray([5, 3, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); @@ -915,11 +904,6 @@ void main() { }); group('pack and unpack with arrays', () { - test('1d', () async { - final mod = PackAndUnpackWithArraysPassthrough(LogicArray([3], 8)); - await testArrayPassthrough(mod, checkNoSwizzle: false); - }); - test('2d', () async { final mod = PackAndUnpackWithArraysPassthrough(LogicArray([3, 2], 8)); await testArrayPassthrough(mod, checkNoSwizzle: false); @@ -1084,6 +1068,7 @@ void main() { {bool doSvSim = true}) async { await mod.build(); + final elementWidth = mod.laOut.width ~/ (3 * 3 * 3 * 3); final a = []; var iIdx = 0; for (var i = 0; i < 3; i++) { @@ -1091,16 +1076,16 @@ void main() { for (var k = 0; k < 3; k++) { for (var l = 0; l < 3; l++) { if (i == 1) { - a.add(LogicValue.ofInt(iIdx, 8)); + a.add(LogicValue.ofInt(iIdx, elementWidth)); iIdx++; } else if (i == 2 && j == 1) { - a.add(LogicValue.filled(8, LogicValue.zero)); + a.add(LogicValue.filled(elementWidth, LogicValue.zero)); } else if (i == 2 && j == 2 && k == 1) { - a.add(LogicValue.filled(8, LogicValue.one)); + a.add(LogicValue.filled(elementWidth, LogicValue.one)); } else if (i == 2 && j == 2 && k == 2 && l == 1) { - a.add(LogicValue.filled(8, LogicValue.zero)); + a.add(LogicValue.filled(elementWidth, LogicValue.zero)); } else { - a.add(LogicValue.filled(8, LogicValue.z)); + a.add(LogicValue.filled(elementWidth, LogicValue.z)); } } } @@ -1116,13 +1101,13 @@ void main() { test('with packed only', () async { await testArrayConstantAssignments( - ConstantAssignmentArrayModule(LogicArray([3, 3, 3, 3], 8))); + ConstantAssignmentArrayModule(LogicArray([3, 3, 3, 3], 5))); }); testWithVerilator( 'with unpacked also', () => ConstantAssignmentArrayModule( - LogicArray([3, 3, 3, 3], 8, numUnpackedDimensions: 2)), + LogicArray([3, 3, 3, 3], 5, numUnpackedDimensions: 2)), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayConstantAssignments(mod, doSvSim: false); From 299082a13d6b5a7592cf5f535d3126678f7caa2b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 4 Sep 2026 12:07:35 -0700 Subject: [PATCH 28/36] Add FST waveform writing and block reading support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../03-development-recommendations.md | 4 +- lib/rohd.dart | 3 + lib/src/diagnostics/diagnostics.dart | 1 + lib/src/diagnostics/waveform_service.dart | 581 +++------ lib/src/diagnostics/waveform_writer.dart | 340 ++++++ lib/src/fst/fst_block_reader.dart | 599 ++++++++++ lib/src/fst/fst_types.dart | 236 ++++ lib/src/fst/fst_writer.dart | 1046 +++++++++++++++++ lib/src/module.dart | 11 +- lib/src/wave_dumper.dart | 2 +- test/config_test.dart | 30 +- test/fst_writer_test.dart | 441 +++++++ test/waveform_service_test.dart | 875 +++++++------- 13 files changed, 3251 insertions(+), 918 deletions(-) create mode 100644 lib/src/diagnostics/waveform_writer.dart create mode 100644 lib/src/fst/fst_block_reader.dart create mode 100644 lib/src/fst/fst_types.dart create mode 100644 lib/src/fst/fst_writer.dart create mode 100644 test/fst_writer_test.dart diff --git a/doc/user_guide/_get-started/03-development-recommendations.md b/doc/user_guide/_get-started/03-development-recommendations.md index d224e54df..6ffb5ab7b 100644 --- a/doc/user_guide/_get-started/03-development-recommendations.md +++ b/doc/user_guide/_get-started/03-development-recommendations.md @@ -10,9 +10,9 @@ toc: true - The [ROHD Cosimulation](https://github.com/intel/rohd-cosim) package allows you to cosimulate the ROHD simulator with a variety of SystemVerilog simulators. - The [ROHD Hardware Component Library](https://github.com/intel/rohd-vf) provides a set of reusable and configurable components for design and verification. - Visual Studio Code (vscode) is a great, free IDE with excellent support for Dart. It works well on all platforms, including native Windows or Windows Subsystem for Linux (WSL) which allows you to run a native Linux kernel (e.g. Ubuntu) within Windows. You can also use vscode to develop on a remote machine with the Remote SSH extension. - - vscode: + - vscode: - WSL: - - Remote SSH: + - Remote SSH: - Dart extension for vscode: Head over to the [user guide]({{ site.baseurl }}{% link _docs/A01-sample-example.md %}) to learn more about how to use ROHD. diff --git a/lib/rohd.dart b/lib/rohd.dart index bda2229c1..4ebac2244 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -11,6 +11,9 @@ export 'src/diagnostics/diagnostics.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; +export 'src/fst/fst_block_reader.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'; diff --git a/lib/src/diagnostics/diagnostics.dart b/lib/src/diagnostics/diagnostics.dart index 8ff34cf81..150721f21 100644 --- a/lib/src/diagnostics/diagnostics.dart +++ b/lib/src/diagnostics/diagnostics.dart @@ -10,3 +10,4 @@ export 'module_service.dart'; export 'module_services.dart'; export 'waveform_service.dart'; +export 'waveform_writer.dart'; diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index 01b6b5476..5373472a6 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -2,134 +2,27 @@ // 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. +// Base waveform service: capture module signal changes to waveform writers. // // 2026 June // Author: Desmond Kirkpatrick import 'dart:collection'; -import 'dart:convert'; import 'dart:io'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; -// ─── Supporting types ──────────────────────────────────────────────────────── - -/// The output format for waveform capture. -enum WaveOutputFormat { - /// Value Change Dump — the classic text-based waveform format. - vcd, - - /// Fast Signal Trace — a compact binary format. - /// - /// Requires an FST writer to be available; see the DevTools subclass for - /// a fully FST-backed implementation. - fst; - - /// The filename extension associated with this format. - String get fileExtension => switch (this) { - WaveOutputFormat.vcd => 'vcd', - WaveOutputFormat.fst => 'fst', - }; - - /// The media type associated with this format. - String get mediaType => switch (this) { - WaveOutputFormat.vcd => 'text/x-vcd', - WaveOutputFormat.fst => 'application/vnd.gtkwave.fst', - }; - - /// Whether this format supports querying waveform data directly from a file. - /// - /// FST is indexed and can support on-disk queries without retaining the - /// entire waveform in memory. VCD is a sequential text format and cannot. - bool get supportsOnDiskQueries => switch (this) { - WaveOutputFormat.vcd => false, - WaveOutputFormat.fst => true, - }; -} - -/// Policy applied when the output file already exists at construction time. -enum OverwritePolicy { - /// Silently overwrite any existing file. - overwrite, - - /// Throw a [FileSystemException] if the file already exists. - failIfExists, -} - -// ─── Service ───────────────────────────────────────────────────────────────── - -/// A waveform capture service that records signal changes. -/// -/// This is the base class for waveform capture. It handles: -/// - Signal collection (with optional [signalFilter]) -/// - Optional whole-history in-memory VCD output with configurable [timescale] -/// - Selective recording via [startTime] / [stopTime] -/// - Optional file output with periodic buffer flushing and [overwritePolicy] -/// - Optional registration with [ModuleServices] +/// A waveform capture service that writes signal changes to a file. /// -/// **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 once for each signal in a captured timestamp's -/// coalesced final-value set; use it to feed an in-memory store for -/// streaming. -/// - [onTimestampCapture] — called after each captured timestamp batch, -/// including the possibly empty finalization batch. -/// - [onSimulationEnd] — called after the final timestamp is written and -/// the file is closed; use it to finalise any streaming buffers. -/// -/// Example subclass skeleton: -/// ```dart -/// class DevToolsWaveformService extends WaveformService { -/// DevToolsWaveformService( -/// super.module, { -/// super.outputDirectory, -/// super.outputBaseName, -/// }); -/// -/// @override -/// void onSignalCollected(Logic signal) { -/// super.onSignalCollected(signal); -/// _registerWithVmService(signal); -/// } -/// -/// @override -/// void onValueChange(Logic signal, int timestamp) { -/// super.onValueChange(signal, timestamp); -/// _recordInMemory(signal, timestamp); -/// } -/// } -/// ``` +/// Selects the output backend via [format]; each format is emitted by a +/// dedicated [WaveformWriter] implementation ([VcdWaveformWriter] for +/// [WaveOutputFormat.vcd], [FstWaveformWriter] for [WaveOutputFormat.fst]). class WaveformService extends ArtifactProducingService { /// The most recently registered [WaveformService], or `null`. - /// - /// This is backed by [ModuleServices], so it is cleared by unregistering - /// this service type or resetting the registry. - static WaveformService? get current => - ModuleServices.instance.lookup(); - - /// Path of the output waveform file. - /// - /// Derived from [outputDirectory], [outputBaseName], and [format]. - String get outputFilePath => '$outputDirectory${Platform.pathSeparator}' - '${outputFileName ?? '$outputBaseName.${format.fileExtension}'}'; - - /// The output filepath of the generated waveforms. - /// - /// This matches the legacy waveform dumper's `outputPath` name. - String get outputPath => outputFilePath; + static WaveformService? current; /// Exact output filename override. /// @@ -137,34 +30,31 @@ class WaveformService extends ArtifactProducingService { /// compatibility with legacy APIs that accepted an arbitrary output path. final String? outputFileName; + /// Path of the output waveform file. + /// + /// Derived from [outputDirectory], [outputBaseName], [outputFileName], + /// and [format]. + String get outputPath => '$outputDirectory${Platform.pathSeparator}' + '${outputFileName ?? '$outputBaseName.${format.fileExtension}'}'; + /// Output format. final WaveOutputFormat format; /// Optional predicate that determines whether a given [Logic] signal is /// captured. - /// - /// 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. + /// Number of characters accumulated in the VCD write buffer before it is + /// flushed to disk. final int flushBufferSize; /// What to do when the output file already exists. @@ -173,48 +63,14 @@ class WaveformService extends ArtifactProducingService { /// Whether to register this service with [ModuleServices] for inspection. final bool register; - /// Whether waveform bytes are written to [outputFilePath]. - /// - /// File-backed captures retain only the current [flushBufferSize]-bounded - /// write buffer unless [retainInMemory] is enabled. - final bool writeToFile; - - /// Whether to retain the complete waveform in memory. - /// - /// By default, this is `true` for in-memory-only VCD debugging captures and - /// `false` for file-backed captures. Set it explicitly to override those - /// defaults when consumers need whole-history waveform queries during or - /// after simulation. - final bool retainInMemory; - - /// Whether this service can service debugger waveform-data queries. - /// - /// A `true` result promises that a debugger can request captured waveform - /// values, such as selected signals over a time interval. It does not - /// promise that [artifacts] can transfer waveform-file bytes. Capture can be - /// queried when complete history is retained in memory, or when a - /// file-backed [format] supports indexed on-disk queries. VCD requires - /// [retainInMemory]; FST can provide this capability from a file once FST - /// writing is supported. - bool canSendWaveforms() => - retainInMemory || (writeToFile && format.supportsOnDiskQueries); - - // ─── Internal file-writing state ───────────────────────────── - - /// Sink writing to [outputFilePath] when [writeToFile] is true. - IOSink? _outFileSink; - - /// Write buffer; flushed when it exceeds [flushBufferSize]. - final StringBuffer _fileBuffer = StringBuffer(); - - /// The complete waveform output when [retainInMemory] is enabled. - final StringBuffer _inMemoryOutput = StringBuffer(); + /// The FST writer configuration (only used when [format] is + /// [WaveOutputFormat.fst]). + final FstWriterConfig? fstConfig; - /// Counter for assigning compact signal markers in the VCD. - int _signalMarkerIdx = 0; + late final WaveformWriter _writer; - /// Maps each captured [Logic] to its VCD marker string. - final Map _signalToMarkerMap = {}; + /// Maps each captured [Logic] to its writer-specific signal handle. + final Map _signalHandles = {}; /// Signals that changed during the current simulation timestamp. final Set _changedThisTimestamp = HashSet(); @@ -222,27 +78,13 @@ class WaveformService extends ArtifactProducingService { /// The timestamp currently being accumulated. int _currentDumpingTimestamp = Simulator.time; - /// Whether the recording window's initial signal snapshot has been written. - bool _hasWrittenWindowSnapshot = false; - - // ─── Constructor ───────────────────────────────────────────── - /// Creates a [WaveformService] for [module]. /// - /// [module] must be built before construction. - /// - /// [outputDirectory] defaults to the current directory and [outputBaseName] - /// defaults to [Module.definitionName]. The selected [format] determines the - /// output filename extension. Only [WaveOutputFormat.vcd] is currently - /// supported by this service. - /// - /// Use the optional constructor parameters to configure format, filtering, - /// timescale, start/stop times, flush size, and overwrite policy. - /// - /// In-memory-only VCD debugging captures retain the complete waveform by - /// default. Set [retainInMemory] to override these defaults; file-backed - /// captures default to bounded memory while retaining a streamable artifact - /// on disk. + /// [module] must be built before construction. [outputDirectory] defaults to + /// the current directory and [outputBaseName] defaults to + /// [Module.definitionName]; the on-disk file is + /// `/.`. Pass + /// [outputFileName] to override the filename explicitly. WaveformService( Module module, { super.outputDirectory, @@ -256,39 +98,23 @@ class WaveformService extends ArtifactProducingService { this.flushBufferSize = 100000, this.overwritePolicy = OverwritePolicy.overwrite, this.register = true, - this.writeToFile = false, - bool? retainInMemory, - }) : retainInMemory = retainInMemory ?? !writeToFile, - super(module) { + this.fstConfig, + }) : super(module) { if (!module.hasBuilt) { - throw ModuleNotBuiltException(module); - } - if (format != WaveOutputFormat.vcd) { - throw UnsupportedError( - 'Waveform format ${format.name} is not supported by WaveformService.', + throw Exception( + 'Module must be built before creating WaveformService. ' + 'Call build() first.', ); } - if (writeToFile && overwritePolicy == OverwritePolicy.failIfExists) { - final f = File(outputFilePath); - if (f.existsSync()) { - throw FileSystemException( - 'Waveform output file already exists and overwritePolicy is ' - 'failIfExists.', - outputFilePath, - ); - } - } - - if (writeToFile) { - _outFileSink = - (File(outputFilePath)..createSync(recursive: true)).openWrite(); - } - - _collectSignals(); - _writeHeader(); - _writeScope(); - _hasWrittenWindowSnapshot = startTime == null || startTime == 0; + _writer = _createWriter(); + _collectSignals(module); + _writer.finishDeclarations( + _signalHandles.entries.map( + (entry) => WaveformInitialValue(entry.value, _binaryValue(entry.key)), + ), + timestamp: Simulator.time, + ); Simulator.preTick.listen((_) { if (Simulator.time != _currentDumpingTimestamp) { @@ -296,7 +122,6 @@ class WaveformService extends ArtifactProducingService { _captureTimestamp(_currentDumpingTimestamp); } _currentDumpingTimestamp = Simulator.time; - _writeWindowSnapshotIfNeeded(Simulator.time); } }); @@ -307,143 +132,142 @@ class WaveformService extends ArtifactProducingService { }); if (register) { + current = this; ModuleServices.instance.register(this); } } - // ─── Extensibility hooks ────────────────────────────────────── - - /// Called once for each [Logic] signal that passes - /// [signalFilter] during initial signal collection. + /// Legacy factory that accepts a single `outputPath` argument. /// - /// Override in a subclass to register signals with an in-memory store, - /// VM service index, or FST handle map. Always call `super` first. + /// Splits [outputPath] into an [outputDirectory] and [outputFileName] and + /// delegates to the main constructor. Provided so that pre-services-API + /// callers of the form `WaveformService(module, outputPath: '/tmp/foo.vcd')` + /// still compile. + factory WaveformService.fromOutputPath( + Module module, { + required String outputPath, + WaveOutputFormat format = WaveOutputFormat.vcd, + bool Function(Logic signal)? signalFilter, + String timescale = '1ps', + int? startTime, + int? stopTime, + int flushBufferSize = 100000, + OverwritePolicy overwritePolicy = OverwritePolicy.overwrite, + bool register = true, + FstWriterConfig? fstConfig, + }) { + final normalized = outputPath.replaceAll(r'\', '/'); + final sep = normalized.lastIndexOf('/'); + final directory = switch (sep) { + -1 => '.', + 0 => '/', + _ => normalized.substring(0, sep), + }; + final filename = normalized.substring(sep + 1); + return WaveformService( + module, + outputDirectory: directory, + outputFileName: filename, + format: format, + signalFilter: signalFilter, + timescale: timescale, + startTime: startTime, + stopTime: stopTime, + flushBufferSize: flushBufferSize, + overwritePolicy: overwritePolicy, + register: register, + fstConfig: fstConfig, + ); + } + + /// The concrete output writer used by this service. + @protected + WaveformWriter get writer => _writer; + + /// Called once for each [Logic] signal that passes [signalFilter]. @protected void onSignalCollected(Logic signal) {} - /// Called once for each signal's final captured value at [timestamp]. - /// - /// Multiple changes to the same signal within a simulation timestamp are - /// coalesced, so this hook receives that signal once with its final value. - /// - /// When [startTime] is set, this includes one window-entry value for every - /// tracked signal at [startTime]. Those calls describe the state entering - /// the recording window, rather than physical transitions. - /// - /// Override in a subclass to feed an in-memory waveform store or - /// streaming buffer. Always call `super` first. + /// Called for every value-change event on [signal] at [timestamp]. @protected void onValueChange(Logic signal, int timestamp) {} - /// Called once after each captured timestamp batch. - /// - /// When [startTime] is set, the complete window-entry signal snapshot is - /// delivered as a batch at [startTime] before later value-change batches. - /// Finalization invokes this hook even when its [changed] set is empty. - /// - /// Override in a subclass to flush incremental streaming payloads. - /// Always call `super` first. + /// Called once per simulation timestamp that contains at least one change. @protected void onTimestampCapture(int timestamp, Set changed) {} /// Called after the final timestamp has been written and the file is closed. - /// - /// 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); - } + WaveformWriter _createWriter() { + switch (format) { + case WaveOutputFormat.vcd: + return VcdWaveformWriter( + outputPath, + timescale: timescale, + flushBufferSize: flushBufferSize, + overwritePolicy: overwritePolicy, + ); + case WaveOutputFormat.fst: + return FstWaveformWriter( + outputPath, + config: fstConfig ?? const FstWriterConfig(), + ); } } - // ─── 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}) { + bool _collectSignals(Module module) { final moduleSignalUniquifier = Uniquifier(); - final padding = List.filled(indent, ' ').join(); - var scopeString = '$padding\$scope module ${m.uniqueInstanceName} \$end\n'; - final innerScopeString = StringBuffer(); + var hasContents = false; - for (final sig in m.signals) { - if (!_signalToMarkerMap.containsKey(sig)) { + _writer.pushScope(module.uniqueInstanceName); + + for (final sig in module.signals) { + if (sig is Const) { continue; } - final width = sig.width; - final marker = _signalToMarkerMap[sig]; - var signalName = Sanitizer.sanitizeSV(sig.name); - signalName = moduleSignalUniquifier.getUniqueName( - initialName: signalName, + if (signalFilter != null && !signalFilter!(sig)) { + continue; + } + + hasContents = true; + 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), + final handle = _writer.declareSignal( + signalName, + sig.width, + direction: _directionOf(sig), ); + _signalHandles[sig] = handle; + onSignalCollected(sig); + + sig.changed.listen((_) { + _changedThisTimestamp.add(sig); + }); } - if (innerScopeString.isEmpty) { - return ''; + + for (final subModule in module.subModules) { + if (subModule is InlineSystemVerilog) { + continue; + } + hasContents = _collectSignals(subModule) || hasContents; } - scopeString += innerScopeString.toString(); - scopeString += '$padding\$upscope \$end\n'; - return scopeString; + _writer.popScope(); + return hasContents; + } + + WaveformSignalDirection _directionOf(Logic signal) { + if (!signal.isPort) { + return WaveformSignalDirection.implicit; + } + return signal.isInput + ? WaveformSignalDirection.input + : WaveformSignalDirection.output; } bool _isInRecordingWindow(int timestamp) { @@ -462,116 +286,49 @@ class WaveformService extends ArtifactProducingService { return; } - _writeWindowSnapshotIfNeeded(timestamp); - _writeToBuffer('#$timestamp\n'); - final snapshot = Set.of(_changedThisTimestamp); + final changes = [ + for (final sig in snapshot) + WaveformValueChange(_signalHandles[sig]!, _binaryValue(sig)), + ]; + + if (changes.isNotEmpty) { + _writer.emitValueChanges(timestamp, changes); + } + for (final sig in snapshot) { - _writeSignalValueUpdate(sig); onValueChange(sig, timestamp); } _changedThisTimestamp.clear(); - onTimestampCapture(timestamp, snapshot); - } - - void _writeWindowSnapshotIfNeeded(int timestamp) { - if (_hasWrittenWindowSnapshot || - startTime == null || - timestamp < startTime! || - !_isInRecordingWindow(startTime!)) { - return; - } - - _writeToBuffer('#$startTime\n'); - final snapshot = Set.of(_signalToMarkerMap.keys); - for (final signal in snapshot) { - _writeSignalValueUpdate(signal); - onValueChange(signal, startTime!); - } - _hasWrittenWindowSnapshot = true; if (snapshot.isNotEmpty) { - onTimestampCapture(startTime!, snapshot); + 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'); - } + String _binaryValue(Logic signal) => signal.value.reversed + .toList() + .map((e) => e.toString(includeWidth: false)) + .join(); - // ─── Buffered I/O ───────────────────────────────────────────── + Future _terminate() => _writer.close(); - void _writeToBuffer(String contents) { - if (writeToFile) { - _fileBuffer.write(contents); - } - if (retainInMemory) { - _inMemoryOutput.write(contents); - } - if (writeToFile && _fileBuffer.length > flushBufferSize) { - _flushBuffer(); - } - } - - void _flushBuffer() { - if (writeToFile) { - _outFileSink!.write(_fileBuffer.toString()); - _fileBuffer.clear(); - } - } - - Future _terminate() async { - _flushBuffer(); - await _outFileSink?.flush(); - await _outFileSink?.close(); - } - - // ─── Inspection ─────────────────────────────────────────────── - - /// The waveform artifact produced by this service. + /// The artifacts this service produces. /// - /// The artifact is complete after simulation finalization. During capture, - /// file-backed artifacts expose only bytes already flushed to - /// [outputFilePath]; the current write buffer is not visible. Each - /// file-backed [ModuleServiceArtifact.openRead] opens the current file, not - /// an immutable snapshot or a live tail, so concurrent capture may change - /// what a read observes. In-memory artifacts are available whenever - /// [retainInMemory] is enabled, including its automatic memory-only default. + /// The waveform is written on-the-fly through [WaveformWriter], so this + /// service does not retain artifacts to report. @override - Iterable get artifacts sync* { - if (!writeToFile && !retainInMemory) { - return; - } - - yield ModuleServiceArtifact( - fileName: outputFileName ?? '$outputBaseName.${format.fileExtension}', - mediaType: format.mediaType, - openRead: writeToFile - ? () => File(outputFilePath).openRead() - : () => Stream.value(utf8.encode(_inMemoryOutput.toString())), - ); - } + Iterable get artifacts => const []; /// Returns a JSON-serialisable summary of this service. @override - Map toJson() => { - 'outputDirectory': outputDirectory, - 'outputBaseName': outputBaseName, - 'outputFilePath': outputFilePath, - 'writeToFile': writeToFile, - 'retainInMemory': retainInMemory, + Map toJson() => { + 'outputPath': outputPath, 'format': format.name, - 'signalCount': _signalToMarkerMap.length, + 'signalCount': _signalHandles.length, 'timescale': timescale, - if (startTime != null) 'startTime': startTime!, - if (stopTime != null) 'stopTime': stopTime!, + if (startTime != null) 'startTime': startTime, + if (stopTime != null) 'stopTime': stopTime, + 'writer': _writer.toJson(), }; } diff --git a/lib/src/diagnostics/waveform_writer.dart b/lib/src/diagnostics/waveform_writer.dart new file mode 100644 index 000000000..67a7272b9 --- /dev/null +++ b/lib/src/diagnostics/waveform_writer.dart @@ -0,0 +1,340 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// waveform_writer.dart +// Common output backend API for waveform capture services. +// +// 2026 July 17 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; + +/// The output format for waveform capture. +enum WaveOutputFormat { + /// Value Change Dump, the classic text-based waveform format. + vcd, + + /// Fast Signal Trace, a compact binary format. + fst; + + /// The filename extension associated with this format. + String get fileExtension => switch (this) { + WaveOutputFormat.vcd => 'vcd', + WaveOutputFormat.fst => 'fst', + }; + + /// The media type associated with this format. + String get mediaType => switch (this) { + WaveOutputFormat.vcd => 'text/x-vcd', + WaveOutputFormat.fst => 'application/vnd.gtkwave.fst', + }; +} + +/// Policy applied when the output file already exists at construction time. +enum OverwritePolicy { + /// Silently overwrite any existing file. + overwrite, + + /// Throw a [FileSystemException] if the file already exists. + failIfExists, +} + +/// Direction metadata for a signal emitted into a waveform file. +enum WaveformSignalDirection { + /// Input port. + input, + + /// Output port. + output, + + /// Internal or implicit signal. + implicit, +} + +/// Initial value for a declared waveform signal. +class WaveformInitialValue { + /// The writer-specific handle returned by [WaveformWriter.declareSignal]. + final Object handle; + + /// The MSB-first binary value string. + final String value; + + /// Creates an initial value entry. + const WaveformInitialValue(this.handle, this.value); +} + +/// Timestamped value change for a declared waveform signal. +class WaveformValueChange extends WaveformInitialValue { + /// Creates a value-change entry. + const WaveformValueChange(super.handle, super.value); +} + +/// Common backend contract for waveform file formats. +abstract class WaveformWriter { + /// The file format emitted by this writer. + WaveOutputFormat get format; + + /// Pushes a scope onto the declaration hierarchy. + void pushScope(String name); + + /// Pops the current declaration scope. + void popScope(); + + /// Declares a signal and returns a writer-specific handle. + Object declareSignal( + String name, + int width, { + required WaveformSignalDirection direction, + }); + + /// Finishes declarations and emits initial values. + void finishDeclarations( + Iterable initialValues, { + required int timestamp, + }); + + /// Emits all value changes for [timestamp]. + void emitValueChanges(int timestamp, Iterable changes); + + /// Flushes and closes the waveform output. + Future close(); + + /// Returns a JSON-serialisable summary of writer state. + Map toJson(); +} + +/// VCD implementation of [WaveformWriter]. +class VcdWaveformWriter implements WaveformWriter { + /// Creates a VCD writer at [outputPath]. + VcdWaveformWriter( + this.outputPath, { + this.timescale = '1ps', + this.flushBufferSize = 100000, + this.overwritePolicy = OverwritePolicy.overwrite, + }) { + if (overwritePolicy == OverwritePolicy.failIfExists) { + final existingFile = File(outputPath); + if (existingFile.existsSync()) { + throw FileSystemException( + 'Waveform output file already exists and overwritePolicy is ' + 'failIfExists.', + outputPath, + ); + } + } + + _outputFile = File(outputPath)..createSync(recursive: true); + _outFileSink = _outputFile.openWrite(); + _writeHeader(); + } + + /// The output file path. + final String outputPath; + + /// VCD timescale string, e.g. `'1ps'`, `'1ns'`. + final String timescale; + + /// Number of characters accumulated before flushing to disk. + final int flushBufferSize; + + /// Existing-file policy. + final OverwritePolicy overwritePolicy; + + late final File _outputFile; + late final IOSink _outFileSink; + final StringBuffer _fileBuffer = StringBuffer(); + final StringBuffer _scopeBuffer = StringBuffer(); + final Map _handleWidths = {}; + var _signalMarkerIdx = 0; + var _indent = 0; + var _closed = false; + + @override + WaveOutputFormat get format => WaveOutputFormat.vcd; + + @override + void pushScope(String name) { + final padding = List.filled(_indent, ' ').join(); + _scopeBuffer.write('$padding\$scope module $name \$end\n'); + _indent++; + } + + @override + void popScope() { + _indent--; + final padding = List.filled(_indent, ' ').join(); + _scopeBuffer.write('$padding\$upscope \$end\n'); + } + + @override + Object declareSignal( + String name, + int width, { + required WaveformSignalDirection direction, + }) { + final marker = 's${_signalMarkerIdx++}'; + final padding = List.filled(_indent, ' ').join(); + _scopeBuffer.write('$padding\$var wire $width $marker $name \$end\n'); + _handleWidths[marker] = width; + return marker; + } + + @override + void finishDeclarations( + Iterable initialValues, { + required int timestamp, + }) { + _writeToBuffer(_scopeBuffer.toString()); + _writeToBuffer('\$enddefinitions \$end\n'); + _writeToBuffer('\$dumpvars\n'); + for (final initialValue in initialValues) { + _writeValueUpdate(initialValue.handle, initialValue.value); + } + _writeToBuffer('\$end\n'); + } + + @override + void emitValueChanges(int timestamp, Iterable changes) { + _writeToBuffer('#$timestamp\n'); + for (final change in changes) { + _writeValueUpdate(change.handle, change.value); + } + } + + @override + Future close() async { + if (_closed) { + return; + } + _closed = true; + _flushBuffer(); + await _outFileSink.flush(); + await _outFileSink.close(); + } + + @override + Map toJson() => { + 'format': format.name, + 'signalCount': _handleWidths.length, + 'timescale': timescale, + }; + + void _writeHeader() { + final header = ''' +\$date + ${Timestamper.stamp()} +\$end +\$version + ROHD v${Config.version} +\$end +\$comment + Generated by ROHD - www.github.com/intel/rohd +\$end +\$timescale $timescale \$end +'''; + _writeToBuffer(header); + } + + void _writeValueUpdate(Object handle, String value) { + final width = _handleWidths[handle]; + if (width == null) { + throw StateError('Unknown VCD signal handle: $handle'); + } + final updateValue = width > 1 ? 'b$value ' : value; + _writeToBuffer('$updateValue$handle\n'); + } + + void _writeToBuffer(String contents) { + _fileBuffer.write(contents); + if (_fileBuffer.length > flushBufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + _outFileSink.write(_fileBuffer.toString()); + _fileBuffer.clear(); + } +} + +/// FST implementation of [WaveformWriter]. +class FstWaveformWriter implements WaveformWriter { + /// Creates an FST writer at [outputPath]. + FstWaveformWriter( + String outputPath, { + FstWriterConfig config = const FstWriterConfig(), + }) : writer = FstWriter(outputPath, config: config); + + /// The low-level FST binary writer. + final FstWriter writer; + + @override + WaveOutputFormat get format => WaveOutputFormat.fst; + + @override + void pushScope(String name) { + writer.pushScope(name); + } + + @override + void popScope() { + writer.popScope(); + } + + @override + Object declareSignal( + String name, + int width, { + required WaveformSignalDirection direction, + }) => + writer.declareSignal(name, width, direction: _fstDirection(direction)); + + @override + void finishDeclarations( + Iterable initialValues, { + required int timestamp, + }) { + writer.writeHeader(); + for (final initialValue in initialValues) { + writer.emitValueChange( + timestamp, + initialValue.handle as FstSignalHandle, + initialValue.value, + ); + } + } + + @override + void emitValueChanges(int timestamp, Iterable changes) { + for (final change in changes) { + writer.emitValueChange( + timestamp, + change.handle as FstSignalHandle, + change.value, + ); + } + } + + @override + Future close() async { + writer.finish(); + } + + @override + Map toJson() => {'format': format.name}; + + FstVarDirection _fstDirection(WaveformSignalDirection direction) { + switch (direction) { + case WaveformSignalDirection.input: + return FstVarDirection.input; + case WaveformSignalDirection.output: + return FstVarDirection.output; + case WaveformSignalDirection.implicit: + return FstVarDirection.implicit; + } + } +} diff --git a/lib/src/fst/fst_block_reader.dart b/lib/src/fst/fst_block_reader.dart new file mode 100644 index 000000000..7b35843d3 --- /dev/null +++ b/lib/src/fst/fst_block_reader.dart @@ -0,0 +1,599 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_block_reader.dart +// Lightweight companion reader for FstWriter that reads back VcData blocks +// from disk using the writer's block index. +// +// This is NOT a general-purpose FST reader. For full FST file reading +// (post-simulation), use the wellen library via the rohd-wave-viewer's +// dart_wellen package which wraps the Rust fst-reader crate. +// +// 2026 February +// Author: Desmond Kirkpatrick + +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:rohd/src/fst/fst_writer.dart'; + +/// A value change record decoded from an FST VcData block. +class FstValueChange { + /// The simulation timestamp. + final int time; + + /// The signal value as a string. + final String value; + + /// Creates a decoded value change. + const FstValueChange(this.time, this.value); + + @override + String toString() => 'FstValueChange(t=$time, v=$value)'; +} + +/// Lightweight reader that decodes VcData DynamicAlias2 blocks written by +/// [FstWriter]. +/// +/// Uses the [FstWriter.blockIndex] to locate blocks on disk and reads only +/// the requested signals, avoiding loading the entire FST file into memory. +/// +/// ## Architecture +/// +/// During a live simulation the data flow is: +/// +/// ```dart +/// FstWriter ──(flushBlock)──> .fst file (VcData blocks on disk) +/// │ +/// FstBlockReader ────(readBlock)────┘ +/// │ +/// WaveformDataService <──(merge)──── disk data + hot buffer +/// │ +/// └──(VM Service extensions)──> DevTools extension +/// ``` +/// +/// Post-simulation, the finalized `.fst` file can be read by the **wellen** +/// library (Rust, via `dart_wellen`) in the rohd-wave-viewer for full +/// waveform browsing. +class FstBlockReader { + /// Signal metadata from the writer (indexed by handle - 1). + final List _signals; + + /// Path to the FST file on disk. + final String filePath; + + /// Creates a block reader for the FST file at [filePath]. + /// + /// [signals] must match the signals declared in the associated [FstWriter]. + /// The reader opens its own file handle for each read, so it is safe to use + /// after the writer has been finished or closed. + FstBlockReader(this.filePath, List signals) + : _signals = List.unmodifiable(signals); + + /// Reads a single VcData block and returns value changes for the + /// specified signal handles. + /// + /// [block] identifies which block to read (from [FstWriter.blockIndex]). + /// [handleIndices] are the 0-based signal indices to extract. + /// If [startTime] / [endTime] are provided, only changes within that + /// range are returned. + /// + /// Returns a map from handle index to a list of value changes. + Map> readBlock( + FstBlockIndex block, { + Set? handleIndices, + int? startTime, + int? endTime, + }) { + final file = File(filePath).openSync(); + try { + return _readBlockImpl( + file, + block, + handleIndices: handleIndices, + startTime: startTime, + endTime: endTime, + ); + } finally { + file.closeSync(); + } + } + + /// Read the frame (initial values at block start) for all signals. + /// + /// Returns a list indexed by handle (0-based) of initial value strings. + List readBlockFrame(FstBlockIndex block) { + final file = File(filePath).openSync(); + try { + return _readFrameImpl(file, block); + } finally { + file.closeSync(); + } + } + + // ─────────────── Implementation ─────────────── + + Map> _readBlockImpl( + RandomAccessFile file, + FstBlockIndex block, { + Set? handleIndices, + int? startTime, + int? endTime, + }) { + // The block layout (DynamicAlias2): + // block_type(1) + section_length(8) + start_time(8) + end_time(8) + // + mem_required(8) = 33 bytes of header + // Then: frame, vc data, offset chain, time table + // + // We read from the end backwards to get time table first, + // then offset chain, then signal data. + + final sectionStart = block.fileOffset + 1; // after block_type byte + final sectionEnd = sectionStart + block.sectionLength; + + // 1. Read the time table (last 24 bytes of section) + final timeTable = _readTimeTable(file, sectionStart, sectionEnd); + + // 2. Locate the offset chain + // Last 24 bytes: time_uncomp(8), time_comp(8), time_items(8) + final timeTableMetaStart = sectionEnd - 24; + file.setPositionSync(timeTableMetaStart + 8); + final compressedTimeLen = _readU64(file); + final timeDataStart = timeTableMetaStart - compressedTimeLen; + + // chain_compressed_length is 8 bytes before time data + final chainLenOffset = timeDataStart - 8; + file.setPositionSync(chainLenOffset); + final chainLen = _readU64(file); + + // 3. Read the offset chain bytes + final chainStart = chainLenOffset - chainLen; + file.setPositionSync(chainStart); + final chainBytes = file.readSync(chainLen); + + // 4. Read frame and VC header to find signal data positions + // Skip: section_length(8) + start_time(8) + end_time(8) + mem(8) + file.setPositionSync(sectionStart + 32); + final frameUncLen = _readVarint(file); + final frameCompLen = _readVarint(file); + _readVarint(file); // maxHandle + final frameDataStart = file.positionSync(); + file.setPositionSync(frameDataStart + frameCompLen); + + // VC header: max_handle varint + pack_type byte + _readVarint(file); // vcMaxHandle + final vcBase = file.positionSync(); // position of pack_type byte + final packType = file.readSync(1)[0]; + // Signal data starts at vcBase + 1 (offset 1 in writer's scheme) + + // Read and decompress the frame + file.setPositionSync(frameDataStart); + final frameCompressed = file.readSync(frameCompLen); + final frameBytes = (frameCompLen == frameUncLen) + ? Uint8List.fromList(frameCompressed) + : _zlibDecompress(Uint8List.fromList(frameCompressed)); + final frameValues = _decodeFrame(frameBytes); + + // 5. Parse offset chain to get signal data locations + // Offsets are relative to vcBase (pack_type position). + // Signal data section ends at chainStart. + final signalDataEndRel = chainStart - vcBase; + final signalLocations = _parseOffsetChain( + chainBytes, + _signals.length, + signalDataEndRel, + ); + + // 6. Decode each requested signal + final result = >{}; + final requestedHandles = handleIndices ?? + Set.from(List.generate(_signals.length, (i) => i)); + + for (final handleIdx in requestedHandles) { + if (handleIdx < 0 || handleIdx >= _signals.length) { + continue; + } + + final loc = signalLocations[handleIdx]; + if (loc == null) { + // No data for this signal in this block — use frame value + if ((startTime == null || block.startTime >= startTime) && + (endTime == null || block.startTime <= endTime)) { + result[handleIdx] = [ + FstValueChange(block.startTime, frameValues[handleIdx]), + ]; + } else { + result[handleIdx] = []; + } + continue; + } + + // Read the packed signal data (offset relative to vcBase) + file.setPositionSync(vcBase + loc.offset); + final packedData = file.readSync(loc.length); + + // Unpack: varint prefix indicates compression + final unpacked = _unpackSignalData( + Uint8List.fromList(packedData), + packType, + ); + + // Decode value changes using the time table + final sig = _signals[handleIdx]; + final changes = _decodeSignalChanges( + unpacked, + sig, + timeTable, + startTime: startTime, + endTime: endTime, + ); + + // Prepend frame value at block start time if in range + final allChanges = []; + if ((startTime == null || block.startTime >= startTime) && + (endTime == null || block.startTime <= endTime)) { + allChanges.add(FstValueChange(block.startTime, frameValues[handleIdx])); + } + allChanges.addAll(changes); + result[handleIdx] = allChanges; + } + + return result; + } + + List _readFrameImpl(RandomAccessFile file, FstBlockIndex block) { + final sectionStart = block.fileOffset + 1; + file.setPositionSync(sectionStart + 32); + final frameUncLen = _readVarint(file); + final frameCompLen = _readVarint(file); + _readVarint(file); // maxHandle + final frameCompressed = file.readSync(frameCompLen); + final frameBytes = (frameCompLen == frameUncLen) + ? Uint8List.fromList(frameCompressed) + : _zlibDecompress(Uint8List.fromList(frameCompressed)); + return _decodeFrame(frameBytes); + } + + // ─────────────── Time table decoding ─────────────── + + /// Reads the time table from the end of a VcData section. + List _readTimeTable( + RandomAccessFile file, + int sectionStart, + int sectionEnd, + ) { + file.setPositionSync(sectionEnd - 24); + final uncLen = _readU64(file); + final compLen = _readU64(file); + final numItems = _readU64(file); + + final timeDataOffset = sectionEnd - 24 - compLen; + file.setPositionSync(timeDataOffset); + final compressedData = file.readSync(compLen); + + final Uint8List uncompressed; + if (compLen == uncLen) { + uncompressed = Uint8List.fromList(compressedData); + } else { + uncompressed = _zlibDecompress(Uint8List.fromList(compressedData)); + } + + final timeTable = []; + var offset = 0; + var prevTime = 0; + for (var i = 0; i < numItems; i++) { + final (delta, newOffset) = _decodeVarintFromBytes(uncompressed, offset); + offset = newOffset; + prevTime += delta; + timeTable.add(prevTime); + } + return timeTable; + } + + // ─────────────── Offset chain parsing ─────────────── + + /// Parses the DynamicAlias2 offset chain to locate each signal's data. + /// + /// [signalDataEndRel] is the byte offset (relative to vcBase) where + /// signal data ends (i.e., the chain start position). + Map _parseOffsetChain( + Uint8List chainBytes, + int signalCount, + int signalDataEndRel, + ) { + final locs = {}; + var offset = 0; + var handleIdx = 0; + var currentOffset = 0; + final offsets = {}; + + while (offset < chainBytes.length && handleIdx < signalCount) { + final firstByte = chainBytes[offset]; + + if ((firstByte & 1) == 1) { + final (raw, newOffset) = _decodeSignedVarintFromBytes( + chainBytes, + offset, + ); + offset = newOffset; + final shval = raw >> 1; + + if (shval > 0) { + currentOffset += shval; + offsets[handleIdx] = currentOffset; + handleIdx++; + } else if (shval < 0) { + locs[handleIdx] = null; // alias + handleIdx++; + } else { + locs[handleIdx] = null; // same alias + handleIdx++; + } + } else { + final (raw, newOffset) = _decodeVarintFromBytes(chainBytes, offset); + offset = newOffset; + final zeros = raw >> 1; + for (var i = 0; i < zeros && handleIdx < signalCount; i++) { + locs[handleIdx] = null; + handleIdx++; + } + } + } + + while (handleIdx < signalCount) { + locs[handleIdx] = null; + handleIdx++; + } + + // Compute lengths from consecutive offsets + final sortedHandles = offsets.keys.toList()..sort(); + for (var i = 0; i < sortedHandles.length; i++) { + final h = sortedHandles[i]; + final start = offsets[h]!; + final end = (i + 1 < sortedHandles.length) + ? offsets[sortedHandles[i + 1]]! + : signalDataEndRel; + locs[h] = _SignalLoc(start, end - start); + } + + return locs; + } + + // ─────────────── Frame decoding ─────────────── + + /// Decodes frame bytes back to string values, one per signal. + List _decodeFrame(Uint8List frameBytes) { + final result = []; + var offset = 0; + + for (final sig in _signals) { + if (sig.isReal) { + if (offset + 8 <= frameBytes.length) { + final bd = ByteData.sublistView(frameBytes, offset, offset + 8); + final d = bd.getFloat64(0, Endian.little); + result.add(d.toString()); + } else { + result.add('0.0'); + } + offset += 8; + } else { + final width = sig.width; + final buf = StringBuffer(); + for (var j = 0; j < width; j++) { + if (offset + j < frameBytes.length) { + buf.writeCharCode(frameBytes[offset + j]); + } else { + buf.write('x'); + } + } + result.add(buf.toString()); + offset += width; + } + } + return result; + } + + // ─────────────── Signal data decoding ─────────────── + + /// Unpacks a signal's data: reads the varint(uncomp_len) prefix and + /// decompresses if needed. + Uint8List _unpackSignalData(Uint8List packedData, int packType) { + if (packedData.isEmpty) { + return Uint8List(0); + } + + var offset = 0; + final (uncLen, newOffset) = _decodeVarintFromBytes(packedData, offset); + offset = newOffset; + + final payload = packedData.sublist(offset); + if (uncLen == 0) { + // Raw/uncompressed (writer stores varint(0) prefix for raw data) + return Uint8List.fromList(payload); + } + + // Compressed — decompress + return _zlibDecompress(Uint8List.fromList(payload)); + } + + /// Decodes value changes from a signal's uncompressed data stream. + List _decodeSignalChanges( + Uint8List data, + FstSignalInfo sig, + List timeTable, { + int? startTime, + int? endTime, + }) { + if (data.isEmpty) { + return []; + } + + final changes = []; + var offset = 0; + var timeIdx = 0; + + while (offset < data.length) { + if (sig.width == 1 && !sig.isReal) { + // 1-bit signal + final (vli, newOffset) = _decodeVarintFromBytes(data, offset); + offset = newOffset; + + String value; + int timeDelta; + if ((vli & 1) == 0) { + value = ((vli >> 1) & 1) == 0 ? '0' : '1'; + timeDelta = vli >> 2; + } else { + const rcvChars = 'xzhuwl-?'; + final rcvIdx = (vli >> 1) & 7; + value = rcvIdx < rcvChars.length ? rcvChars[rcvIdx] : 'x'; + timeDelta = vli >> 4; + } + timeIdx += timeDelta; + if (timeIdx < timeTable.length) { + final t = timeTable[timeIdx]; + if ((startTime == null || t >= startTime) && + (endTime == null || t <= endTime)) { + changes.add(FstValueChange(t, value)); + } + } + } else if (sig.isReal) { + // Real signal + final (vli, newOffset) = _decodeVarintFromBytes(data, offset); + offset = newOffset; + final timeDelta = vli >> 1; + timeIdx += timeDelta; + + if (offset + 8 <= data.length) { + final bd = ByteData.sublistView(data, offset, offset + 8); + final d = bd.getFloat64(0, Endian.little); + offset += 8; + if (timeIdx < timeTable.length) { + final t = timeTable[timeIdx]; + if ((startTime == null || t >= startTime) && + (endTime == null || t <= endTime)) { + changes.add(FstValueChange(t, d.toString())); + } + } + } else { + break; + } + } else { + // Multi-bit signal + final (vli, newOffset) = _decodeVarintFromBytes(data, offset); + offset = newOffset; + final timeDelta = vli >> 1; + final is4State = (vli & 1) == 1; + timeIdx += timeDelta; + + String value; + if (!is4State) { + final byteCount = (sig.width + 7) ~/ 8; + if (offset + byteCount > data.length) { + break; + } + final buf = StringBuffer(); + for (var i = 0; i < sig.width; i++) { + final byteIdx = i ~/ 8; + final bitIdx = 7 - (i % 8); + final bit = (data[offset + byteIdx] >> bitIdx) & 1; + buf.write(bit == 0 ? '0' : '1'); + } + value = buf.toString(); + offset += byteCount; + } else { + if (offset + sig.width > data.length) { + break; + } + value = String.fromCharCodes(data, offset, offset + sig.width); + offset += sig.width; + } + + if (timeIdx < timeTable.length) { + final t = timeTable[timeIdx]; + if ((startTime == null || t >= startTime) && + (endTime == null || t <= endTime)) { + changes.add(FstValueChange(t, value)); + } + } + } + } + + return changes; + } + + // ─────────────── Low-level I/O helpers ─────────────── + + /// Reads a big-endian u64 from [file] at the current position. + static int _readU64(RandomAccessFile file) { + final bytes = file.readSync(8); + final bd = ByteData.sublistView(Uint8List.fromList(bytes)); + return bd.getUint64(0); + } + + /// Reads an unsigned LEB128 varint from [file] at the current position. + static int _readVarint(RandomAccessFile file) { + var result = 0; + var shift = 0; + while (true) { + final byte = file.readSync(1)[0]; + result |= (byte & 0x7F) << shift; + if ((byte & 0x80) == 0) { + break; + } + shift += 7; + } + return result; + } + + /// Decodes an unsigned LEB128 varint from [bytes] at [offset]. + static (int, int) _decodeVarintFromBytes(Uint8List bytes, int offset) { + var result = 0; + var shift = 0; + var pos = offset; + while (pos < bytes.length) { + final byte = bytes[pos++]; + result |= (byte & 0x7F) << shift; + if ((byte & 0x80) == 0) { + break; + } + shift += 7; + } + return (result, pos); + } + + /// Decodes a signed LEB128 varint from [bytes] at [offset]. + static (int, int) _decodeSignedVarintFromBytes(Uint8List bytes, int offset) { + var result = 0; + var shift = 0; + var pos = offset; + int byte; + do { + byte = bytes[pos++]; + result |= (byte & 0x7F) << shift; + shift += 7; + } while ((byte & 0x80) != 0 && pos < bytes.length); + + if (shift < 64 && (byte & 0x40) != 0) { + result |= ~0 << shift; + } + return (result, pos); + } + + /// Decompresses zlib-compressed data. + static Uint8List _zlibDecompress(Uint8List data) { + final decompressed = ZLibCodec().decode(data); + return Uint8List.fromList(decompressed); + } +} + +/// Internal: location of a signal's data within the VC section. +class _SignalLoc { + /// Byte offset relative to vcBase (pack_type byte position). + final int offset; + + /// Length in bytes. + final int length; + + const _SignalLoc(this.offset, this.length); +} diff --git a/lib/src/fst/fst_types.dart b/lib/src/fst/fst_types.dart new file mode 100644 index 000000000..13e829c28 --- /dev/null +++ b/lib/src/fst/fst_types.dart @@ -0,0 +1,236 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_types.dart +// Enumerations and constants for the FST (Fast Signal Trace) binary format. +// +// 2026 February +// Author: Desmond Kirkpatrick + +/// FST block types (from fstapi.h). +enum FstBlockType { + /// File header. + header(0), + + /// Value change data (zlib compressed). + vcData(1), + + /// Blackout regions. + blackout(2), + + /// Geometry (per-variable back-pointers for random access). + geometry(3), + + /// Hierarchy (zlib compressed). + hierarchy(4), + + /// Value changes with dynamic aliases (zlib). + vcDataDynamicAlias(5), + + /// Hierarchy (LZ4 compressed). + hierarchyLz4(6), + + /// Hierarchy (LZ4 double compressed). + hierarchyLz4Duo(7), + + /// Value changes with dynamic aliases v2 (modern recommended format). + vcDataDynamicAlias2(8), + + /// GZip wrapper. + gzipWrapper(254), + + /// Skip/padding. + skip(255); + + const FstBlockType(this.value); + + /// The numeric value of this block type as written in FST files. + final int value; +} + +/// FST scope types. +enum FstScopeType { + /// A Verilog/SystemVerilog module instantiation scope. + module(0), + + /// A Verilog/SystemVerilog task scope. + task(1), + + /// A Verilog/SystemVerilog function scope. + function_(2), + + /// A named `begin`..`end` block scope (Verilog). + begin(3), + + /// A named `fork`..`join` block scope (Verilog). + fork(4), + + /// A `generate` block scope (SystemVerilog). + generate(5), + + /// A `struct` type scope (SystemVerilog). + struct_(6), + + /// A `union` type scope (SystemVerilog). + union(7), + + /// A `class` scope (SystemVerilog). + class_(8), + + /// An `interface` scope (SystemVerilog). + interface(9), + + /// A `package` scope (SystemVerilog). + package(10), + + /// A `program` scope (SystemVerilog). + program(11); + + const FstScopeType(this.value); + + /// The numeric value of this scope type as written in FST files. + final int value; +} + +/// FST variable types. +enum FstVarType { + /// An event variable. + event(0), + + /// A Verilog `integer` variable (32-bit, 4-state). + integer(1), + + /// A Verilog `parameter` or `localparam`. + parameter(2), + + /// A `real` variable (double-precision floating point). + real(3), + + /// A `real` parameter. + realParameter(4), + + /// A `reg` variable (Verilog 4-state storage). + reg(5), + + /// A `supply0` net (logic-0 power supply). + supply0(6), + + /// A `supply1` net (logic-1 power supply). + supply1(7), + + /// A `time` variable. + time(8), + + /// A `tri` net (tri-state, same resolution as `wire`). + tri(9), + + /// A `triand` net (tri-state with wired-AND resolution). + triAnd(10), + + /// A `trior` net (tri-state with wired-OR resolution). + triOr(11), + + /// A `trireg` net (retains last driven value when undriven). + triReg(12), + + /// A `tri0` net (pulls to 0 when undriven). + tri0(13), + + /// A `tri1` net (pulls to 1 when undriven). + tri1(14), + + /// A `wand` net (wired-AND). + wand(15), + + /// A `wire` net (standard Verilog interconnect). + wire(16), + + /// A `wor` net (wired-OR). + wor(17), + + /// A port variable. + port(18), + + /// A sparse array variable. + sparseArray(19), + + /// A `realtime` variable. + realTime(20), + + /// A generic string variable. + genericString(21), + + // SystemVerilog types + + /// A SystemVerilog `bit` type (2-state, unsigned). + bit(22), + + /// A SystemVerilog `logic` type (4-state). + logic(23), + + /// A SystemVerilog `int` type (32-bit, 2-state, signed). + int_(24), + + /// A SystemVerilog `shortint` type (16-bit, 2-state, signed). + shortInt(25), + + /// A SystemVerilog `longint` type (64-bit, 2-state, signed). + longInt(26), + + /// A SystemVerilog `byte` type (8-bit, 2-state, signed). + byte_(27), + + /// A SystemVerilog `enum` type. + enum_(28), + + /// A SystemVerilog `shortreal` type (single-precision float). + shortReal(29); + + const FstVarType(this.value); + + /// The numeric value of this variable type as written in FST files. + final int value; +} + +/// FST variable direction. +enum FstVarDirection { + /// No direction specified (implicit net). + implicit(0), + + /// Input port. + input(1), + + /// Output port. + output(2), + + /// Bidirectional (inout) port. + inout(3), + + /// Buffer port (output that can be read back). + buffer(4), + + /// Linkage port (VHDL linkage mode). + linkage(5); + + const FstVarDirection(this.value); + + /// The numeric value of this direction as written in FST files. + final int value; +} + +/// FST file type. +enum FstFileType { + /// Verilog source. + verilog(0), + + /// VHDL source. + vhdl(1), + + /// Mixed Verilog and VHDL source. + verilogVhdl(2); + + const FstFileType(this.value); + + /// The numeric value of this file type as written in FST files. + final int value; +} diff --git a/lib/src/fst/fst_writer.dart b/lib/src/fst/fst_writer.dart new file mode 100644 index 000000000..c9fe9058c --- /dev/null +++ b/lib/src/fst/fst_writer.dart @@ -0,0 +1,1046 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_writer.dart +// Pure Dart implementation of FST (Fast Signal Trace) binary writer. +// +// Writes FST files compatible with GTKWave, Surfer, and wellen/fst-reader. +// Implements the public FST binary format in pure Dart. +// +// 2026 February +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:rohd/rohd.dart'; + +/// Configuration for the FST writer. +class FstWriterConfig { + /// Timescale exponent. The timescale is 10^exponent seconds. + /// Default: -12 (picoseconds). + final int timescaleExponent; + + /// Zlib compression level (0-9). Higher = smaller but slower. + /// Default: 4. + final int compressionLevel; + + /// Writer version string embedded in the file header. + final String version; + + /// File type: Verilog, VHDL, or combined. + final FstFileType fileType; + + /// Maximum number of value changes to buffer before auto-flushing + /// a VcData block to disk. Set to 0 (default) to disable auto-flush + /// and write a single block at [FstWriter.finish]. + /// + /// When non-zero, [FstWriter.emitValueChange] automatically calls + /// [FstWriter.flushBlock] once the buffer reaches this threshold. + /// This bounds memory usage and makes historical data available on + /// disk for read-back. + final int maxChangesPerBlock; + + /// Creates configuration for the FST writer. + const FstWriterConfig({ + this.timescaleExponent = -12, + this.compressionLevel = 4, + this.version = 'ROHD FST Writer', + this.fileType = FstFileType.verilog, + this.maxChangesPerBlock = 0, + }); +} + +/// A handle to a declared signal in the FST file. +/// +/// Handles are 1-based (matching VST convention). Index 0 is unused. +class FstSignalHandle { + /// The 1-based handle value. + final int handle; + + /// Creates a signal handle from a 1-based handle value. + const FstSignalHandle(this.handle); +} + +/// Metadata about a flushed VcData block in the FST file. +/// +/// Each entry in [FstWriter.blockIndex] represents a block that has been +/// written to disk and can be read back independently for on-demand +/// signal queries without loading the entire file into memory. +class FstBlockIndex { + /// File offset of the block_type byte in the FST file. + final int fileOffset; + + /// Section length (the section_length field from the block header). The full + /// block occupies bytes [fileOffset .. fileOffset + 1 + sectionLength). + final int sectionLength; + + /// First timestamp in this block. + final int startTime; + + /// Last timestamp in this block. + final int endTime; + + /// Creates a block index entry. + const FstBlockIndex({ + required this.fileOffset, + required this.sectionLength, + required this.startTime, + required this.endTime, + }); +} + +/// Public metadata about a declared signal in the FST writer. +class FstSignalInfo { + /// Signal name. + final String name; + + /// Bit width (number of bits for digital signals, 8 for real). + final int width; + + /// Whether this is a real-valued (f64) signal. + final bool isReal; + + /// Creates signal info. + const FstSignalInfo({ + required this.name, + required this.width, + required this.isReal, + }); +} + +/// Internal: information about a declared signal. +class _SignalDecl { + final String name; + final int width; + final FstVarType varType; + final FstVarDirection direction; + final bool isReal; + + _SignalDecl({ + required this.name, + required this.width, + required this.varType, + required this.direction, + this.isReal = false, + }); + + /// The geometry file_format value for this signal. + int get geometryValue { + if (isReal) { + return 0; + } + return width; // 1 for 1-bit, N for N-bit + } + + /// The number of bytes this signal occupies in the frame section. + int get frameLength { + if (isReal) { + return 8; + } + return width; // 1 byte per bit for character-encoded values + } +} + +/// Internal: a buffered value change. +class _ValueChange { + final int time; + final int handleIndex; // 0-based + final String value; + + _ValueChange(this.time, this.handleIndex, this.value); +} + +/// Internal: an entry in the hierarchy being built. +sealed class _HierarchyEntry {} + +class _ScopeEntry extends _HierarchyEntry { + final FstScopeType type; + final String name; + final String component; + _ScopeEntry(this.type, this.name, {this.component = ''}); +} + +class _UpScopeEntry extends _HierarchyEntry {} + +class _VarEntry extends _HierarchyEntry { + final FstVarType varType; + final FstVarDirection direction; + final String name; + final int width; + final int handle; // 1-based + _VarEntry(this.varType, this.direction, this.name, this.width, this.handle); +} + +/// Pure Dart writer for the FST (Fast Signal Trace) binary format. +/// +/// Usage: +/// ```dart +/// final writer = FstWriter('output.fst'); +/// writer.pushScope('top'); +/// final clk = writer.declareSignal('clk', 1); +/// final data = writer.declareSignal('data', 8); +/// writer.popScope(); +/// writer.writeHeader(); +/// +/// writer.emitValueChange(0, clk, '0'); +/// writer.emitValueChange(0, data, '00000000'); +/// writer.emitValueChange(5, clk, '1'); +/// writer.emitValueChange(10, clk, '0'); +/// +/// writer.finish(); +/// ``` +class FstWriter { + /// The output file path. + final String filePath; + + /// Writer configuration. + final FstWriterConfig config; + + /// All declared signals (0-indexed). + final List<_SignalDecl> _signals = []; + + /// Hierarchy entries in declaration order. + final List<_HierarchyEntry> _hierEntries = []; + + /// Scope counts for header. + int _scopeCount = 0; + + /// Variable counts for header (including aliases). + int _varCount = 0; + + /// Buffered value changes. + final List<_ValueChange> _changes = []; + + /// The start time of the simulation. + int _startTime = 0; + + /// The end time of the simulation. + int _endTime = 0; + + /// Whether the header has been written yet. + bool _headerWritten = false; + + /// The output file random access handle. + late final RandomAccessFile _file; + + /// Current value of each signal (tracks latest emitted value). + /// Initialized in [writeHeader]. + late List _currentValues; + + /// Base values for the next block's frame section. + /// Updated after each [flushBlock] call. + late List _nextFrameBase; + + /// Index of flushed VcData blocks for read-back. + final List _blockIndex = []; + + /// Number of VcData blocks written so far. + int _vcSectionCount = 0; + + /// Creates an FST writer that will write to [filePath]. + FstWriter(this.filePath, {this.config = const FstWriterConfig()}) { + final file = File(filePath)..createSync(recursive: true); + _file = file.openSync(mode: FileMode.write); + } + + /// Pushes a new scope onto the hierarchy. + void pushScope( + String name, { + FstScopeType type = FstScopeType.module, + String component = '', + }) { + _hierEntries.add(_ScopeEntry(type, name, component: component)); + _scopeCount++; + } + + /// Pops the current scope. + void popScope() { + _hierEntries.add(_UpScopeEntry()); + } + + /// Declares a signal and returns its handle. + /// + /// [name] is the signal name. [width] is the bit width (1 for single bit). + /// Returns an [FstSignalHandle] used for emitting value changes. + FstSignalHandle declareSignal( + String name, + int width, { + FstVarType varType = FstVarType.wire, + FstVarDirection direction = FstVarDirection.implicit, + }) { + final handle = _signals.length + 1; // 1-based + final decl = _SignalDecl( + name: name, + width: width, + varType: varType, + direction: direction, + isReal: varType == FstVarType.real || varType == FstVarType.realParameter, + ); + _signals.add(decl); + _hierEntries.add(_VarEntry(varType, direction, name, width, handle)); + _varCount++; + return FstSignalHandle(handle); + } + + /// Writes the FST file header. + /// + /// Must be called after all signals are declared and before any value + /// changes. The header is initially written with placeholder values for + /// start_time and end_time, which are fixed up during [finish]. + void writeHeader() { + if (_headerWritten) { + throw StateError('Header already written'); + } + _writeHeaderBlock(); + _headerWritten = true; + + // Initialize value tracking for incremental block flushing + final defaults = List.generate(_signals.length, (i) { + final sig = _signals[i]; + return sig.isReal ? '0.0' : 'x' * sig.width; + }); + _currentValues = List.from(defaults); + _nextFrameBase = List.from(defaults); + } + + /// Records a value change for a signal at a given simulation time. + /// + /// [time] is the simulation timestamp. + /// [handle] is the signal handle returned by [declareSignal]. + /// [value] is the new value as a string (e.g., '0', '1', '01010101', 'x'). + void emitValueChange(int time, FstSignalHandle handle, String value) { + if (!_headerWritten) { + throw StateError('Must call writeHeader() before emitting value changes'); + } + if (_endTime < time) { + _endTime = time; + } + _changes.add(_ValueChange(time, handle.handle - 1, value)); + _currentValues[handle.handle - 1] = value; + + // Auto-flush if threshold is reached + if (config.maxChangesPerBlock > 0 && + _changes.length >= config.maxChangesPerBlock) { + flushBlock(); + } + } + + /// Finalizes the FST file: flushes remaining value changes, writes + /// geometry and hierarchy blocks, fixes up the header, and closes the file. + void finish() { + if (!_headerWritten) { + writeHeader(); + } + + // Flush any remaining buffered changes as a final VcData block + flushBlock(); + + _writeGeometryBlock(); + _writeHierarchyBlock(); + _fixupHeader(); + + _file.closeSync(); + } + + /// Releases resources. Call [finish] first for a valid file. + void dispose() { + try { + _file.closeSync(); + } on FileSystemException { + // already closed + } + } + + /// Flushes buffered value changes to disk as a VcData block. + /// + /// After flushing, the changes are cleared from memory and the block + /// is recorded in [blockIndex] for later read-back. This enables + /// incremental writing where only recent unflushed changes remain + /// in memory while historical data lives on disk. + /// + /// Does nothing if no changes are buffered. + void flushBlock() { + if (_changes.isEmpty) { + return; + } + if (!_headerWritten) { + throw StateError('Must call writeHeader() before flushing blocks'); + } + + // Sort changes by time, then by handle + _changes.sort((a, b) { + final cmp = a.time.compareTo(b.time); + return cmp != 0 ? cmp : a.handleIndex.compareTo(b.handleIndex); + }); + + final blockStart = _changes.first.time; + final blockEnd = _changes.last.time; + + // Build frame: carry-over state from previous block, overridden by + // any changes at this block's start time. + final frameValues = List.from(_nextFrameBase); + for (final c in _changes) { + if (c.time == blockStart) { + frameValues[c.handleIndex] = c.value; + } + } + + final blockOffset = _file.positionSync(); + _writeVcDataBlock( + blockStartTime: blockStart, + blockEndTime: blockEnd, + frameValues: frameValues, + ); + final blockEndPos = _file.positionSync(); + + // Record block in the index for read-back + _blockIndex.add( + FstBlockIndex( + fileOffset: blockOffset, + sectionLength: blockEndPos - blockOffset - 1, + startTime: blockStart, + endTime: blockEnd, + ), + ); + _vcSectionCount++; + + // Update global time range + if (_vcSectionCount == 1) { + _startTime = blockStart; + } + _endTime = blockEnd; + + // Carry-over state for next block's frame + _nextFrameBase = List.from(_currentValues); + _changes.clear(); + } + + // ─── Public query API for hybrid disk+memory access ─── + + /// Index of all flushed VcData blocks. + /// + /// Each entry contains the file offset and time range, enabling + /// the `FstBlockReader` to read specific blocks on demand. + List get blockIndex => List.unmodifiable(_blockIndex); + + /// Number of declared signals. + int get signalCount => _signals.length; + + /// Public metadata about each declared signal (indexed by handle-1). + List get signalInfoList => _signals + .map((s) => FstSignalInfo(name: s.name, width: s.width, isReal: s.isReal)) + .toList(); + + /// The output file handle for read-back by `FstBlockReader`. + /// + /// **Warning**: The caller must not close or modify the file position + /// without restoring it. The writer uses this same handle for writing. + RandomAccessFile get file => _file; + + /// Query unflushed value changes for a specific signal handle. + /// + /// Returns changes from the hot buffer for signal [handleIndex] (0-based) + /// within the time range \[startTime, endTime\]. + List<({int time, String value})> queryHotBuffer( + int handleIndex, + int startTime, + int endTime, + ) => + _changes + .where( + (c) => + c.handleIndex == handleIndex && + c.time >= startTime && + c.time <= endTime, + ) + .map((c) => (time: c.time, value: c.value)) + .toList(); + + /// Returns the current (latest) value of signal [handleIndex] (0-based). + String getCurrentValue(int handleIndex) => _currentValues[handleIndex]; + + /// Returns the latest known values of all signals (read-only). + List get currentValues => List.unmodifiable(_currentValues); + + // ─────────────── Header Block ─────────────── + + static const int _headerLength = 329; + static const int _headerVersionMaxLen = 128; + static const int _headerDateMaxLen = 119; + + /// Writes the FST_BL_HDR block. + void _writeHeaderBlock() { + _file.writeByteSync(FstBlockType.header.value); + _writeU64(_headerLength); // section_length (fixed size) + _writeU64(_startTime); // start_time (placeholder) + _writeU64(_endTime); // end_time (placeholder) + _writeF64LE(math.e); // double endian test + _writeU64(0); // memory_used_by_writer + _writeU64(_scopeCount); // scope_count + _writeU64(_varCount); // var_count + _writeU64(_signals.length); // max_var_id_code + _writeU64(1); // vc_section_count (we write one block) + _file.writeByteSync(config.timescaleExponent & 0xFF); // timescale_exponent + _writeFixedString(config.version, _headerVersionMaxLen); + _writeFixedString(_dateString(), _headerDateMaxLen); + _file.writeByteSync(config.fileType.value); // file_type + _writeU64(0); // time_zero + } + + /// Fixes up the header with actual start/end times and block count. + void _fixupHeader() { + final savedPos = _file.positionSync(); + _file.setPositionSync(1 + 8); // skip block_type + section_length + _writeU64(_startTime); + _writeU64(_endTime); + // Fix vc_section_count with actual number of blocks written + // Layout: block_type(1) + section_length(8) + start_time(8) + + // end_time(8) + endian_test(8) + memory_used(8) + scope_count(8) + + // var_count(8) + max_var_id(8) = offset 65 + _file.setPositionSync( + 1 + 8 + 8 + 8 + 8 + 8 + 8 + 8 + 8, + ); // at vc_section_count + _writeU64(_vcSectionCount); + _file.setPositionSync(savedPos); + } + + // ─────────────── Hierarchy Block ─────────────── + + static const int _hierTypeScopeBegin = 254; + static const int _hierTypeUpScope = 255; + + /// Writes the FST_BL_HIER block (zlib/gzip compressed hierarchy). + void _writeHierarchyBlock() { + // Build uncompressed hierarchy bytes + final buf = BytesBuilder(copy: false); + var handleCount = 0; + + for (final entry in _hierEntries) { + switch (entry) { + case _ScopeEntry(): + buf + ..addByte(_hierTypeScopeBegin) + ..addByte(entry.type.value) + ..add(_cString(entry.name)) + ..add(_cString(entry.component)); + case _UpScopeEntry(): + buf.addByte(_hierTypeUpScope); + case _VarEntry(): + buf + ..addByte(entry.varType.value) + ..addByte(entry.direction.value) + ..add(_cString(entry.name)) + ..add(encodeVarint(entry.width)) // length + // alias = 0 means "new handle, not an alias" + ..add(encodeVarint(0)); + handleCount++; + } + } + + final uncompressed = buf.toBytes(); + assert( + handleCount == _signals.length, + 'Handle count mismatch: $handleCount vs ${_signals.length}', + ); + + // Write as FST_BL_HIER (type 4) with gzip compression + _file.writeByteSync(FstBlockType.hierarchy.value); + final sectionLengthPos = _file.positionSync(); + _writeU64(0); // placeholder section_length + _writeU64(uncompressed.length); // uncompressed_length + + // Write gzip header + deflate-compressed data + _writeGzipCompressed(uncompressed); + + // Fix section_length + final endPos = _file.positionSync(); + final sectionLength = endPos - sectionLengthPos; + _file.setPositionSync(sectionLengthPos); + _writeU64(sectionLength); + _file.setPositionSync(endPos); + } + + // ─────────────── Geometry Block ─────────────── + + /// Writes the FST_BL_GEOM block. + void _writeGeometryBlock() { + // Build uncompressed geometry: one varint per signal + final buf = BytesBuilder(copy: false); + for (final sig in _signals) { + buf.add(encodeVarint(sig.geometryValue)); + } + final uncompressed = buf.toBytes(); + final compressed = _zlibCompress( + uncompressed, + config.compressionLevel, + allowRaw: true, + ); + + _file.writeByteSync(FstBlockType.geometry.value); + final sectionLength = 3 * 8 + compressed.length; + _writeU64(sectionLength); // section_length + _writeU64(uncompressed.length); // uncompressed_length + _writeU64(_signals.length); // max_handle + _file.writeFromSync(compressed); + } + + // ─────────────── VcData Block (DynamicAlias2) ─────────────── + + /// Writes a single FST_BL_VCDATA_DYN_ALIAS2 block from the current + /// `_changes` buffer. + /// + /// [blockStartTime] and [blockEndTime] are the time range for this block. + /// [frameValues] contains the initial value of each signal at the block's + /// start time (carry-over state plus changes at blockStartTime). + /// + /// Assumes `_changes` is already sorted by time, then by handle. + void _writeVcDataBlock({ + required int blockStartTime, + required int blockEndTime, + required List frameValues, + }) { + // Build sorted unique time table. + // Only include timestamps that have signal chain entries (i.e., after + // blockStartTime). Changes at blockStartTime go into the frame section. + // The fst-reader only reads the frame when time_table[0] > start_time; + // if blockStartTime were included, the frame would be skipped and all + // signals would appear as 'x'. + final timeSet = {}; + for (final c in _changes) { + if (c.time != blockStartTime) { + timeSet.add(c.time); + } + } + final timeTable = timeSet.toList()..sort(); + // Map timestamp → index + final timeToIndex = {}; + for (var i = 0; i < timeTable.length; i++) { + timeToIndex[timeTable[i]] = i; + } + + // Build per-signal value change chains + final signalData = _buildSignalData(timeToIndex, blockStartTime); + + // Pack each signal's data (store uncompressed with varint(0) prefix) + final packedSignals = []; + for (final data in signalData) { + if (data.isEmpty) { + packedSignals.add(Uint8List(0)); + } else { + final packed = BytesBuilder(copy: false) + ..add(encodeVarint(0)) // means "uncompressed" + ..add(data); + packedSignals.add(packed.toBytes()); + } + } + + // Build frame bytes + final frameBytes = _buildFrameBytes(frameValues); + final frameCompressed = _zlibCompress( + frameBytes, + config.compressionLevel, + allowRaw: true, + ); + + // Build the signal offset chain (DynamicAlias2 format) + final chainBytes = _buildOffsetChain(packedSignals); + + // Build time table bytes + final timeTableBytes = _buildTimeTableBytes(timeTable); + + // Compute memory required for traversal + var memRequired = 0; + for (final ps in packedSignals) { + memRequired += ps.length; + } + + // Now assemble the VcData block + _file.writeByteSync(FstBlockType.vcDataDynamicAlias2.value); + final sectionLengthPos = _file.positionSync(); + _writeU64(0); // placeholder section_length + _writeU64(blockStartTime); // start_time + _writeU64(blockEndTime); // end_time + _writeU64(memRequired); // mem_required_for_traversal + + // Frame section + _file + ..writeFromSync(encodeVarint(frameBytes.length)) // unc len + ..writeFromSync(encodeVarint(frameCompressed.length)) // comp len + ..writeFromSync(encodeVarint(_signals.length)) // max_handle + ..writeFromSync(frameCompressed) + // Value change section + ..writeFromSync(encodeVarint(_signals.length)) // max_handle + ..writeByteSync(0x5A); // pack_type = 'Z' (zlib) + + // Write per-signal packed data + packedSignals.forEach(_file.writeFromSync); + + // Write offset chain + _file.writeFromSync(chainBytes); + _writeU64(chainBytes.length); // chain_compressed_length + + // Write time table + _file.writeFromSync(timeTableBytes); + + // Fix section_length + final endPos = _file.positionSync(); + final sectionLength = endPos - sectionLengthPos; + _file.setPositionSync(sectionLengthPos); + _writeU64(sectionLength); + _file.setPositionSync(endPos); + } + + /// Builds frame bytes: the initial value of each signal concatenated. + Uint8List _buildFrameBytes(List initialValues) { + final buf = BytesBuilder(copy: false); + for (var i = 0; i < _signals.length; i++) { + final sig = _signals[i]; + if (sig.isReal) { + // Encode as f64 little-endian bytes + final d = double.tryParse(initialValues[i]) ?? 0.0; + final bd = ByteData(8)..setFloat64(0, d, Endian.little); + buf.add(bd.buffer.asUint8List()); + } else { + // Character-encoded value: one byte per bit + final val = initialValues[i]; + for (var j = 0; j < sig.width; j++) { + buf.addByte(j < val.length ? val.codeUnitAt(j) : 0x78); // 'x' + } + } + } + return buf.toBytes(); + } + + /// Builds per-signal value change encoded data. + /// + /// Returns a list of byte arrays, one per signal (0-indexed). + /// Each byte array contains the encoded value change chain for that signal. + /// Changes at [blockStartTime] are skipped (captured in the frame). + List _buildSignalData( + Map timeToIndex, + int blockStartTime, + ) { + // Group changes by signal handle index + final signalChanges = List>.generate( + _signals.length, + (_) => [], + ); + for (final c in _changes) { + // Skip changes at blockStartTime — those are captured in the frame + if (c.time == blockStartTime) { + continue; + } + signalChanges[c.handleIndex].add(c); + } + + final result = []; + for (var sigIdx = 0; sigIdx < _signals.length; sigIdx++) { + final changes = signalChanges[sigIdx]; + if (changes.isEmpty) { + result.add(Uint8List(0)); + continue; + } + + final sig = _signals[sigIdx]; + final buf = BytesBuilder(copy: false); + var prevTimeIndex = 0; + + for (final c in changes) { + final timeIndex = timeToIndex[c.time]!; + final timeDelta = timeIndex - prevTimeIndex; + prevTimeIndex = timeIndex; + + if (sig.frameLength == 1) { + // 1-bit signal: compact encoding + buf.add(_encodeOneBitChange(timeDelta, c.value)); + } else if (sig.isReal) { + // Real signal + buf.add(_encodeRealChange(timeDelta, c.value)); + } else { + // Multi-bit signal + buf.add(_encodeMultiBitChange(timeDelta, c.value, sig.width)); + } + } + result.add(buf.toBytes()); + } + return result; + } + + /// Encodes a 1-bit signal value change. + /// + /// Format: varint where: + /// - Normal (0/1): bit0=0, bit1=value, bits2+= time_index_delta + /// - Special (x/z/etc): bit0=1, bits1-3=rcv_index, bits4+=time_index_delta + Uint8List _encodeOneBitChange(int timeDelta, String value) { + // RCV_STR: [x, z, h, u, w, l, -, ?] + const rcvChars = 'xzhuwl-?'; + final ch = value.isNotEmpty ? value[value.length - 1] : 'x'; + + int vli; + if (ch == '0') { + vli = (timeDelta << 2) | (0 << 1) | 0; // bit0=0, bit1=0 + } else if (ch == '1') { + vli = (timeDelta << 2) | (1 << 1) | 0; // bit0=0, bit1=1 + } else { + final rcvIdx = rcvChars.indexOf(ch); + final idx = rcvIdx >= 0 ? rcvIdx : 0; // default to 'x' + vli = (timeDelta << 4) | (idx << 1) | 1; // bit0=1, bits1-3=idx + } + return encodeVarint(vli); + } + + /// Encodes a multi-bit signal value change. + /// + /// Format: varint(time_delta << 1 | encoding_bit) then value bytes. + /// encoding_bit=0: 2-state packed bits; encoding_bit=1: 4-state characters. + Uint8List _encodeMultiBitChange(int timeDelta, String value, int width) { + final buf = BytesBuilder(copy: false); + + // Check if value contains only 0/1 (2-state) + final is2State = value.runes.every((c) => c == 0x30 || c == 0x31); + + if (is2State) { + // 2-state: pack bits into bytes, MSB first + buf.add(encodeVarint((timeDelta << 1) | 0)); + final byteCount = (width + 7) ~/ 8; + final bytes = Uint8List(byteCount); + for (var i = 0; i < width; i++) { + if (i < value.length && value[i] == '1') { + final byteIdx = i ~/ 8; + final bitIdx = 7 - (i % 8); + bytes[byteIdx] |= 1 << bitIdx; + } + } + buf.add(bytes); + } else { + // 4-state: raw character bytes + buf.add(encodeVarint((timeDelta << 1) | 1)); + for (var i = 0; i < width; i++) { + buf.addByte(i < value.length ? value.codeUnitAt(i) : 0x78); + } + } + return buf.toBytes(); + } + + /// Encodes a real signal value change. + Uint8List _encodeRealChange(int timeDelta, String value) { + final buf = BytesBuilder(copy: false) + ..add(encodeVarint((timeDelta << 1) | 1)); + final d = double.tryParse(value) ?? 0.0; + final bd = ByteData(8)..setFloat64(0, d, Endian.little); + buf.add(bd.buffer.asUint8List()); + return buf.toBytes(); + } + + /// Builds the offset chain for DynamicAlias2 format. + /// + /// The chain encodes the byte offset and presence of each signal's + /// packed data within the value change section. + Uint8List _buildOffsetChain(List packedSignals) { + final buf = BytesBuilder(copy: false); + var currentOffset = 0; // byte offset within vc section (after pack_type) + var prevOffset = 0; + var consecutiveEmpty = 0; + + // Offset 0 is the pack_type byte itself. Signal data starts at offset 1. + currentOffset = 1; // skip the pack_type byte + + for (var i = 0; i < packedSignals.length; i++) { + final ps = packedSignals[i]; + if (ps.isEmpty) { + consecutiveEmpty++; + } else { + // Flush any consecutive empty signals + if (consecutiveEmpty > 0) { + // Write: varint((count << 1) | 0) — bit0=0 means "zero block" + buf.add(encodeVarint(consecutiveEmpty << 1)); + consecutiveEmpty = 0; + } + // Write positive offset delta (signed varint with bit0=1) + // In DynamicAlias2: bit0=1 + signed_varint >> 1 > 0 means + // new incremental offset delta. + // Encoding: signed_varint((delta << 1) | 1) + // Reader does: shval = read_variant_i64() >> 1 = delta + final offsetDelta = currentOffset - prevOffset; + buf.add(encodeSignedVarint((offsetDelta << 1) | 1)); + prevOffset = currentOffset; + currentOffset += ps.length; + } + } + + // Flush trailing empty signals + if (consecutiveEmpty > 0) { + buf.add(encodeVarint(consecutiveEmpty << 1)); + } + + return buf.toBytes(); + } + + /// Builds the time table section (appended at end of VcData block). + /// + /// The time table is: compressed delta-encoded timestamps, followed by + /// 3 u64s: uncompressed_length, compressed_length, num_entries. + Uint8List _buildTimeTableBytes(List timeTable) { + // Delta-encode the time table + final deltaBuf = BytesBuilder(copy: false); + var prevTime = 0; + for (final t in timeTable) { + deltaBuf.add(encodeVarint(t - prevTime)); + prevTime = t; + } + final uncompressed = deltaBuf.toBytes(); + final compressed = _zlibCompress( + uncompressed, + config.compressionLevel, + allowRaw: true, + ); + + // Build the full time section: compressed data + 3 u64s + final result = BytesBuilder(copy: false) + ..add(compressed) + ..add(_encodeU64(uncompressed.length)) + ..add(_encodeU64(compressed.length)) + ..add(_encodeU64(timeTable.length)); + return result.toBytes(); + } + + // ─────────────── Low-level I/O helpers ─────────────── + + /// Writes a big-endian u64. + void _writeU64(int value) { + final bd = ByteData(8)..setUint64(0, value); + _file.writeFromSync(bd.buffer.asUint8List()); + } + + /// Encodes a big-endian u64 to bytes. + Uint8List _encodeU64(int value) { + final bd = ByteData(8)..setUint64(0, value); + return bd.buffer.asUint8List(); + } + + /// Writes a little-endian f64 (for double endian test). + void _writeF64LE(double value) { + final bd = ByteData(8)..setFloat64(0, value, Endian.little); + _file.writeFromSync(bd.buffer.asUint8List()); + } + + /// Writes a fixed-length NUL-padded string. + void _writeFixedString(String value, int maxLen) { + final bytes = utf8.encode(value); + final len = bytes.length < maxLen ? bytes.length : maxLen - 1; + _file + ..writeFromSync(bytes.sublist(0, len)) + // Pad with zeros + ..writeFromSync(Uint8List(maxLen - len)); + } + + /// Encodes a NUL-terminated string. + Uint8List _cString(String value) { + final bytes = utf8.encode(value); + final result = Uint8List(bytes.length + 1) + ..setRange(0, bytes.length, bytes); + // last byte is already 0 + return result; + } + + /// Encodes an unsigned integer as LEB128 varint. + static Uint8List encodeVarint(int value) { + if (value < 0) { + throw ArgumentError('Value must be non-negative: $value'); + } + if (value <= 0x7F) { + return Uint8List.fromList([value]); + } + final bytes = []; + var v = value; + while (v != 0) { + final nextV = v >> 7; + final mask = nextV == 0 ? 0 : 0x80; + bytes.add((v & 0x7F) | mask); + v = nextV; + } + return Uint8List.fromList(bytes); + } + + /// Encodes a signed integer as signed LEB128 varint. + static Uint8List encodeSignedVarint(int value) { + if (value >= -64 && value <= 63) { + return Uint8List.fromList([value & 0x7F]); + } + + final bytes = []; + var v = value; + var more = true; + while (more) { + var byte_ = v & 0x7F; + v >>= 7; + // Check if we're done + if ((v == 0 && (byte_ & 0x40) == 0) || (v == -1 && (byte_ & 0x40) != 0)) { + more = false; + } else { + byte_ |= 0x80; + } + bytes.add(byte_); + } + return Uint8List.fromList(bytes); + } + + /// Writes gzip-compressed bytes (gzip header + deflate data). + void _writeGzipCompressed(Uint8List data) { + // Gzip header (10 bytes) + const gzipHeader = [ + 0x1F, 0x8B, // magic + 0x08, // deflate + 0x00, // no flags + 0x00, 0x00, 0x00, 0x00, // timestamp = 0 + 0x00, // compression level + 0xFF, // OS = unknown + ]; + _file.writeFromSync(Uint8List.fromList(gzipHeader)); + + // Deflate-compressed data (raw deflate, not zlib-wrapped) + final compressed = _deflateCompress(data, config.compressionLevel); + _file.writeFromSync(compressed); + } + + /// Compresses bytes using zlib (with zlib header, for geometry/frame/etc). + static Uint8List _zlibCompress( + Uint8List data, + int level, { + bool allowRaw = false, + }) { + final compressed = ZLibCodec(level: level).encode(data); + final result = Uint8List.fromList(compressed); + if (allowRaw && result.length >= data.length) { + // Compression didn't help, return uncompressed + return data; + } + return result; + } + + /// Compresses bytes using raw deflate (no zlib header, for gzip hierarchy). + static Uint8List _deflateCompress(Uint8List data, int level) { + final compressed = ZLibCodec(level: level, raw: true).encode(data); + return Uint8List.fromList(compressed); + } + + /// Generates a date string for the header. + String _dateString() { + final now = DateTime.now(); + const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', // + ]; + final day = days[now.weekday - 1]; + final month = months[now.month - 1]; + final d = now.day.toString().padLeft(2); + final h = now.hour.toString().padLeft(2, '0'); + final m = now.minute.toString().padLeft(2, '0'); + final s = now.second.toString().padLeft(2, '0'); + return '$day $month $d $h:$m:$s ${now.year}\n'; + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 072ebc326..52519cab6 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1149,12 +1149,9 @@ abstract class Module { /// Attaches waveform dumping for this [Module] to a VCD at [outputPath]. /// - /// Set [retainInMemory] to `true` when a debugger needs to query the - /// complete waveform history while the simulation is running. - WaveformService dumpWaves({ - String outputPath = 'waves.vcd', - bool retainInMemory = false, - }) { + /// For filtering, alternative formats, and other waveform controls, use + /// [WaveformService] directly. + WaveformService dumpWaves({String outputPath = 'waves.vcd'}) { final normalized = outputPath.replaceAll(r'\', '/'); final separatorIndex = normalized.lastIndexOf('/'); final outputDirectory = switch (separatorIndex) { @@ -1168,8 +1165,6 @@ abstract class Module { this, outputDirectory: outputDirectory, outputFileName: outputFileName, - writeToFile: true, - retainInMemory: retainInMemory, ); } diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 563c38e94..c53741cea 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -50,7 +50,7 @@ class WaveDumper { Module get module => _service.module; /// The output filepath of the generated waveforms. - String get outputPath => _service.outputFilePath; + String get outputPath => _service.outputPath; /// Attaches a [WaveDumper] to record all signal changes in a simulation of /// [module] in a VCD file at [outputPath]. diff --git a/test/config_test.dart b/test/config_test.dart index ad2bc306f..133e746d0 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -9,11 +9,13 @@ // Author: Yao Jing Quek import 'dart:io'; + import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; import 'package:yaml/yaml.dart'; + import 'waveform_service_test.dart'; class SimpleModule extends Module { @@ -23,7 +25,7 @@ class SimpleModule extends Module { final c = addOutput('c'); Combinational([ - If(a, then: [c < a], orElse: [c < b]) + If(a, then: [c < a], orElse: [c < b]), ]); } } @@ -52,23 +54,25 @@ void main() { }); test( - 'should contains ROHD version number when deprecated synth is generated.', - () async { - const version = Config.version; + 'should contains ROHD version number when deprecated synth is generated.', + () async { + const version = Config.version; - final mod = SimpleModule(Logic(), Logic()); - await mod.build(); + final mod = SimpleModule(Logic(), Logic()); + await mod.build(); - // This test verifies that the deprecated API still includes the version. - // ignore: deprecated_member_use_from_same_package - final sv = mod.generateSynth(); + // This test verifies that the deprecated API still includes the version. + // ignore: deprecated_member_use_from_same_package + final sv = mod.generateSynth(); - expect(sv, contains(version)); - }); + expect(sv, contains(version)); + }, + ); if (!kIsWeb) { - test('should contains ROHD version number when wavedumper is generated.', - () async { + test( + 'should contains ROHD version number when ' + 'waveform service is generated.', () async { const version = Config.version; final mod = SimpleModule(Logic(), Logic()); diff --git a/test/fst_writer_test.dart b/test/fst_writer_test.dart new file mode 100644 index 000000000..8da79a2ff --- /dev/null +++ b/test/fst_writer_test.dart @@ -0,0 +1,441 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_writer_test.dart +// Tests for FST writer and WaveformService FST format support. +// +// 2026 February +// Author: Desmond Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import 'pipeline_test.dart' show SimplePipelineModule; + +/// A simple module for testing. +class _SimpleModule extends Module { + _SimpleModule(Logic a) { + a = addInput('a', a); + addOutput('b') <= a; + } +} + +/// A module with multi-bit signals for testing. +class _MultiBitModule extends Module { + _MultiBitModule(Logic a, Logic clk) { + a = addInput('a', a, width: a.width); + final aClk = addInput('clk', clk); + addOutput('q', width: a.width) <= FlipFlop(aClk, a).q; + } +} + +const _tempDumpDir = 'tmp_test'; + +/// Gets the path of the FST file based on a name. +String _temporaryFstPath(String name) => '$_tempDumpDir/temp_dump_$name.fst'; + +/// Attaches a [WaveformService] to [module] with FST format. +void _createFstDump(Module module, String name) { + Directory(_tempDumpDir).createSync(recursive: true); + final tmpDumpFile = _temporaryFstPath(name); + WaveformService.fromOutputPath( + module, + outputPath: tmpDumpFile, + format: WaveOutputFormat.fst, + ); +} + +/// Deletes the temporary FST file associated with [name]. +void _deleteFstDump(String name) { + final tmpDumpFile = _temporaryFstPath(name); + if (File(tmpDumpFile).existsSync()) { + File(tmpDumpFile).deleteSync(); + } +} + +/// Reads a big-endian u64 from [data] at [offset]. +int _readU64(Uint8List data, int offset) { + var result = 0; + for (var i = 0; i < 8; i++) { + result = (result << 8) | data[offset + i]; + } + return result; +} + +/// Parses FST file blocks and returns a map of block types to counts. +Map _parseFstBlocks(Uint8List data) { + final blocks = {}; + var pos = 0; + while (pos < data.length) { + final blockType = data[pos]; + pos++; + if (pos + 8 > data.length) { + break; + } + final sectionLength = _readU64(data, pos); + blocks[blockType] = (blocks[blockType] ?? 0) + 1; + pos += sectionLength; + if (sectionLength == 0) { + break; + } + } + return blocks; +} + +/// Parses FST header and returns key fields. +Map _parseFstHeader(Uint8List data) { + // Skip block type byte (0) + if (data[0] != 0) { + throw FormatException('Expected header block type 0, got ${data[0]}'); + } + final sectionLength = _readU64(data, 1); + if (sectionLength != 329) { + throw FormatException( + 'Expected header section length 329, got $sectionLength', + ); + } + return { + 'start_time': _readU64(data, 9), + 'end_time': _readU64(data, 17), + // skip double_endian_test (8 bytes at offset 25) + 'scope_count': _readU64(data, 41), + 'var_count': _readU64(data, 49), + 'max_var_id': _readU64(data, 57), + 'vc_section_count': _readU64(data, 65), + 'timescale_exponent': data[73], // offset 73 = 1 + 8*9 + }; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('FstWriter unit tests', () { + test('writes valid header block', () { + const path = '$_tempDumpDir/fst_header_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + FstWriter(path) + ..pushScope('top') + ..declareSignal('clk', 1) + ..declareSignal('data', 8) + ..popScope() + ..finish(); + + final data = File(path).readAsBytesSync(); + expect(data[0], equals(0), reason: 'First byte should be header type'); + final sectionLength = _readU64(data, 1); + expect(sectionLength, equals(329), reason: 'Header is 329 bytes'); + + // Parse header fields + final header = _parseFstHeader(data); + expect(header['scope_count'], equals(1)); + expect(header['var_count'], equals(2)); + expect(header['max_var_id'], equals(2)); + + File(path).deleteSync(); + }); + + test('writes all required block types', () { + const path = '$_tempDumpDir/fst_blocks_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + final writer = FstWriter(path)..pushScope('top'); + final clk = writer.declareSignal('clk', 1); + writer + ..popScope() + ..writeHeader() + ..emitValueChange(0, clk, '0') + ..emitValueChange(5, clk, '1') + ..finish(); + + final data = File(path).readAsBytesSync(); + final blocks = _parseFstBlocks(data); + + // Must have: Header(0), VcDataDynamicAlias2(8), Geometry(3), + // Hierarchy(4) + expect(blocks.containsKey(0), isTrue, reason: 'Must have header'); + expect(blocks.containsKey(8), isTrue, reason: 'Must have VcData block'); + expect(blocks.containsKey(3), isTrue, reason: 'Must have geometry'); + expect(blocks.containsKey(4), isTrue, reason: 'Must have hierarchy'); + + File(path).deleteSync(); + }); + + test('geometry encodes signal widths correctly', () { + const path = '$_tempDumpDir/fst_geometry_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + FstWriter(path) + ..pushScope('top') + ..declareSignal('bit1', 1) + ..declareSignal('byte8', 8) + ..declareSignal('word32', 32) + ..popScope() + ..finish(); + + final data = File(path).readAsBytesSync(); + + // Find the geometry block (type 3) + var pos = 0; + while (pos < data.length) { + if (data[pos] == 3) { + // Geometry block + final sectionLength = _readU64(data, pos + 1); + final maxHandle = _readU64(data, pos + 1 + 16); + expect(maxHandle, equals(3)); + + // Geometry data is after section_length(8) + unc_len(8) + + // max_handle(8) = 24 bytes from section_length start + // May be compressed, so just check the block exists + expect(sectionLength, greaterThan(24)); + break; + } + pos++; + if (pos + 8 > data.length) { + break; + } + final sl = _readU64(data, pos); + pos += sl; + if (sl == 0) { + break; + } + } + + File(path).deleteSync(); + }); + }); + + group('WaveformService FST format', () { + test('basic 1-bit signal FST dump', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dumpName = 'fstBasic'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(100); + await Simulator.run(); + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + final data = fstFile.readAsBytesSync(); + // File should have valid FST header + expect(data[0], equals(0), reason: 'First byte is header block type'); + expect(_readU64(data, 1), equals(329)); + + // Check blocks are present + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue, reason: 'header'); + expect(blocks.containsKey(3), isTrue, reason: 'geometry'); + expect(blocks.containsKey(4), isTrue, reason: 'hierarchy'); + + _deleteFstDump(dumpName); + }); + + test('multi-bit signal FST dump', () async { + final a = Logic(name: 'a', width: 8); + final clk = SimpleClockGenerator(10).clk; + final mod = _MultiBitModule(a, clk); + await mod.build(); + + const dumpName = 'fstMultiBit'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(100); + unawaited(Simulator.run()); + + await clk.nextPosedge; + a.inject(0xAB); + await clk.nextPosedge; + a.inject(0xFF); + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + final data = fstFile.readAsBytesSync(); + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue); + expect( + blocks.containsKey(8), + isTrue, + reason: 'VcData block with changes', + ); + + _deleteFstDump(dumpName); + }); + + test('FST file creates non-existent directories', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dir1Path = '$_tempDumpDir/fst_dir1'; + const fstPath = '$dir1Path/dir2/waves.fst'; + + WaveformService.fromOutputPath( + mod, + outputPath: fstPath, + format: WaveOutputFormat.fst, + ); + + a.put(0); + Simulator.setMaxSimTime(10); + await Simulator.run(); + + expect(File(fstPath).existsSync(), isTrue); + + if (Directory(dir1Path).existsSync()) { + Directory(dir1Path).deleteSync(recursive: true); + } + }); + + test('FST header has correct signal counts', () async { + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + const dumpName = 'fstCounts'; + _createFstDump(mod, dumpName); + + a.put(0); + Simulator.setMaxSimTime(10); + await Simulator.run(); + + final data = File(_temporaryFstPath(dumpName)).readAsBytesSync(); + final header = _parseFstHeader(data); + + // _SimpleModule has 2 signals: input 'a' and output 'b' + expect(header['var_count'], equals(2)); + + _deleteFstDump(dumpName); + }); + + test('FST and VCD both produce output', () async { + // Create a module + final a = Logic(name: 'a'); + final mod = _SimpleModule(a); + await mod.build(); + + // Dump as FST + const fstName = 'fstCompare'; + _createFstDump(mod, fstName); + + a.put(0); + Simulator.setMaxSimTime(50); + unawaited(Simulator.run()); + + a.inject(1); + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(fstName)); + expect(fstFile.existsSync(), isTrue); + final fstSize = fstFile.lengthSync(); + expect(fstSize, greaterThan(330), reason: 'FST should be > header size'); + + _deleteFstDump(fstName); + + // Reset and dump as VCD + await Simulator.reset(); + + final a2 = Logic(name: 'a'); + final mod2 = _SimpleModule(a2); + await mod2.build(); + + const vcdPath = '$_tempDumpDir/temp_dump_vcdCompare.vcd'; + Directory(_tempDumpDir).createSync(recursive: true); + WaveformService.fromOutputPath(mod2, outputPath: vcdPath); + + a2.put(0); + Simulator.setMaxSimTime(50); + unawaited(Simulator.run()); + + a2.inject(1); + + await Simulator.simulationEnded; + + final vcdFile = File(vcdPath); + expect(vcdFile.existsSync(), isTrue); + expect(vcdFile.lengthSync(), greaterThan(0)); + + vcdFile.deleteSync(); + }); + + test('pipeline FST has VcData and is readable by fst2vcd', () async { + // Build a 3-stage 8-bit pipeline that generates many signal changes. + final a = Logic(name: 'a', width: 8); + final mod = SimplePipelineModule(a); + await mod.build(); + + const dumpName = 'fstPipeline'; + _createFstDump(mod, dumpName); + + // Drive 200 clock cycles worth of incrementing inputs. + // The 10ps clock gives 2000ps total, producing many VcData changes. + a.put(0); + Simulator.setMaxSimTime(2000); + unawaited(Simulator.run()); + + // Inject a new value every 10ps to keep signals active + for (var i = 1; i <= 200; i++) { + await Future.delayed(Duration.zero); + a.inject(i & 0xFF); + } + + await Simulator.simulationEnded; + + final fstFile = File(_temporaryFstPath(dumpName)); + expect(fstFile.existsSync(), isTrue); + + // File should be substantially larger than just the header (329 bytes) + final fileSize = fstFile.lengthSync(); + expect( + fileSize, + greaterThan(600), + reason: 'Pipeline FST should have VcData content', + ); + + // Parse blocks: must include at least one VcData block (type 8) + final data = fstFile.readAsBytesSync(); + final blocks = _parseFstBlocks(data); + expect(blocks.containsKey(0), isTrue, reason: 'header block'); + expect(blocks.containsKey(8), isTrue, reason: 'VcData block'); + expect(blocks.containsKey(3), isTrue, reason: 'geometry block'); + expect(blocks.containsKey(4), isTrue, reason: 'hierarchy block'); + + // Validate with fst2vcd (GTKWave tool) if available. + final fst2vcd = Process.runSync('which', ['fst2vcd']); + if (fst2vcd.exitCode == 0) { + final result = Process.runSync('fst2vcd', [fstFile.path]); + expect( + result.exitCode, + equals(0), + reason: 'fst2vcd failed: ${result.stdout}\n${result.stderr}', + ); + final vcdOutput = result.stdout as String; + expect( + vcdOutput, + contains(r'$timescale'), + reason: 'fst2vcd output should be valid VCD', + ); + } + + _deleteFstDump(dumpName); + }); + }); +} diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index cdbc559e5..d8a99747e 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -1,76 +1,39 @@ -// Copyright (C) 2021-2026 Intel Corporation +// Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // waveform_service_test.dart -// Tests for the WaveformService +// Tests for WaveformService output and VCD/FST event parity. // -// 2021 November 4 -// Author: Max Korbel +// 2026 July 17 +// Author: Desmond Kirkpatrick @TestOn('vm') library; -import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/vcd_parser.dart'; import 'package:test/test.dart'; -class SimpleModule extends Module { - SimpleModule(Logic a) { +class _SimpleWaveModule extends Module { + _SimpleWaveModule(Logic a) { a = addInput('a', a, width: a.width); addOutput('b', width: a.width) <= ~a; } } -class WindowWaveModule extends Module { - late final Logic a; +const _tempDumpDir = 'tmp_test'; - WindowWaveModule() { - a = addOutput('a'); - } -} - -class HistoryWaveformService extends WaveformService { - final Map> history = {}; - final List capturedTimestamps = []; - - HistoryWaveformService(super.module, {super.startTime}); - - @override - void onSignalCollected(Logic signal) { - history[signal] = [(Simulator.time, binaryValue(signal))]; - } - - @override - void onValueChange(Logic signal, int timestamp) { - history[signal]!.add((timestamp, binaryValue(signal))); - } - - @override - void onTimestampCapture(int timestamp, Set changed) { - capturedTimestamps.add(timestamp); - } -} +String _temporaryVcdPath(String name) => '$_tempDumpDir/temp_wave_$name.vcd'; -String binaryValue(Logic signal) => signal.value.reversed - .toList() - .map((value) => value.toString(includeWidth: false)) - .join(); +String _temporaryFstPath(String name) => '$_tempDumpDir/temp_wave_$name.fst'; -class SimpleModWithSeq extends Module { - Logic get val => output('val'); - SimpleModWithSeq(Logic asyncReset, Logic clk) { - clk = addInput('clk', clk); - asyncReset = addInput('asyncReset', asyncReset); - addOutput('val'); - - val <= flop(clk, Const(1), reset: asyncReset, asyncReset: true); - } -} +// ─── Public helpers used by sibling tests (e.g. config_test.dart) ──────────── +/// Directory into which sibling tests place their temporary waveform dumps. const tempDumpDir = 'tmp_test'; /// Gets the path of the VCD file based on a name. @@ -83,23 +46,10 @@ void createTemporaryDump(Module module, String name) { module, outputDirectory: tempDumpDir, outputBaseName: 'temp_dump_$name', - writeToFile: true, ); } -// The helper intentionally exercises the deprecated WaveDumper compatibility -// path. -// ignore: deprecated_member_use_from_same_package -/// Attaches the deprecated [WaveDumper] to [module] to VCD with [name]. -void createTemporaryWaveDumperDump(Module module, String name) { - Directory(tempDumpDir).createSync(recursive: true); - final tmpDumpFile = temporaryDumpPath(name); - // The deprecated WaveDumper constructor is invoked to test its behavior. - // ignore: deprecated_member_use_from_same_package - WaveDumper(module, outputPath: tmpDumpFile); -} - -/// Deletes the temporary VCD file associated with [name]. +/// Deletes the VCD file previously created by [createTemporaryDump]. void deleteTemporaryDump(String name) { final tmpDumpFile = temporaryDumpPath(name); File(tmpDumpFile).deleteSync(); @@ -108,502 +58,463 @@ void deleteTemporaryDump(String name) { void main() { tearDown(() async { await Simulator.reset(); + ModuleServices.instance.reset(); }); - test('attach dumper after put', () async { + test('registers with ModuleServices by default', () async { final a = Logic(name: 'a'); - final mod = SimpleModule(a); + final mod = _SimpleWaveModule(a); await mod.build(); - const dumpName = 'dumpAfterPut'; - - a.put(1); - createTemporaryDump(mod, dumpName); - - Simulator.registerAction(10, () => a.put(0)); - await Simulator.run(); + Directory(_tempDumpDir).createSync(recursive: true); + final dumpPath = _temporaryVcdPath('serviceRegistration'); - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); + WaveformService.fromOutputPath(mod, outputPath: dumpPath); - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 5, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), - equals(true)); + final service = ModuleServices.instance.lookup(); + expect(service, isNotNull); + final waveformJson = jsonEncode(service!.toJson()); + expect(waveformJson, contains('"format":"vcd"')); - deleteTemporaryDump(dumpName); + File(dumpPath).deleteSync(); }); - test('attach deprecated wave dumper after put', () async { + test('captures waveform to VCD output path', () async { final a = Logic(name: 'a'); - final mod = SimpleModule(a); + final mod = _SimpleWaveModule(a); await mod.build(); - const dumpName = 'deprecatedDumpAfterPut'; + Directory(_tempDumpDir).createSync(recursive: true); + final dumpPath = _temporaryVcdPath('serviceCapture'); - a.put(1); - createTemporaryWaveDumperDump(mod, dumpName); + WaveformService.fromOutputPath(mod, outputPath: dumpPath, register: false); + a.inject(1); Simulator.registerAction(10, () => a.put(0)); await Simulator.run(); - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 5, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), - equals(true)); - - deleteTemporaryDump(dumpName); - }); - - test('dumpWaves returns a waveform service', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - const dumpName = 'moduleDumpWaveforms'; - final outputPath = temporaryDumpPath(dumpName); - Directory(tempDumpDir).createSync(recursive: true); - final service = mod.dumpWaves(outputPath: outputPath); - - expect(service, isA()); - expect(service.module, same(mod)); - expect(service.outputPath, outputPath); - expect(service.outputFilePath, outputPath); - expect(service.retainInMemory, isFalse); - expect(service.canSendWaveforms(), isFalse); - expect(File(service.outputPath).existsSync(), isTrue); - - await Simulator.run(); - - expect( - (await service.artifacts.single - .openRead() - .expand((bytes) => bytes) - .toList()) - .isNotEmpty, - isTrue, - ); - deleteTemporaryDump(dumpName); - }); - - test('throws if module not built', () { - final mod = SimpleModule(Logic()); - + final vcdContents = File(dumpPath).readAsStringSync(); expect( - () => WaveformService(mod), - throwsA(isA()), - ); - }); - - test('dumpWaves can retain history for debugging', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - final service = mod.dumpWaves( - outputPath: temporaryDumpPath('debugDump'), - retainInMemory: true, - ); - - expect(service.retainInMemory, isTrue); - expect(service.canSendWaveforms(), isTrue); - - await Simulator.run(); - deleteTemporaryDump('debugDump'); - }); - - test('dumpWaves preserves an arbitrary legacy output filename', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - const outputPath = '$tempDumpDir/capture.trace'; - final service = mod.dumpWaves(outputPath: outputPath); - - expect(service.outputPath, equals(outputPath)); - expect(File(outputPath).existsSync(), isTrue); - - await Simulator.run(); - File(outputPath).deleteSync(); - }); - - test('file-only capture streams its artifact after simulation finalization', - () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - final waveformService = WaveformService( - mod, - outputDirectory: tempDumpDir, - outputBaseName: 'capture', - writeToFile: true, + VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), + equals(true), ); - - final artifact = waveformService.artifacts.single; - - expect(artifact.fileName, equals('capture.vcd')); - expect(artifact.mediaType, equals('text/x-vcd')); - expect(waveformService.retainInMemory, isFalse); - expect(waveformService.canSendWaveforms(), isFalse); - expect(File(waveformService.outputFilePath).existsSync(), isTrue); - - await Simulator.run(); - expect( - (await artifact.openRead().expand((bytes) => bytes).toList()).isNotEmpty, - isTrue, - ); - - File(waveformService.outputFilePath).deleteSync(); - }); - - test('in-memory-only debugging capture retains waveform history', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - final waveformService = WaveformService( - mod, - outputDirectory: tempDumpDir, - outputBaseName: 'in_memory_capture', - ); - - expect(waveformService.writeToFile, isFalse); - expect(waveformService.retainInMemory, isTrue); - expect(waveformService.canSendWaveforms(), isTrue); - expect(File(waveformService.outputFilePath).existsSync(), isFalse); - - await Simulator.run(); - - final bytes = await waveformService.artifacts.single - .openRead() - .expand((bytes) => bytes) - .toList(); - expect(utf8.decode(bytes), contains(r'$enddefinitions')); - }); - - test('retained file-backed capture can send waveforms', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - final waveformService = WaveformService( - mod, - outputDirectory: tempDumpDir, - outputBaseName: 'retained_file_capture', - writeToFile: true, - retainInMemory: true, + VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), + equals(true), ); - expect(waveformService.canSendWaveforms(), isTrue); - - await Simulator.run(); - File(waveformService.outputFilePath).deleteSync(); - }); - - test('capture without a file or retained history has no artifact', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - final waveformService = WaveformService(mod, retainInMemory: false); - - expect(waveformService.artifacts, isEmpty); - expect(waveformService.canSendWaveforms(), isFalse); - }); - - test('FST supports querying waveforms from a file', () { - expect(WaveOutputFormat.vcd.supportsOnDiskQueries, isFalse); - expect(WaveOutputFormat.fst.supportsOnDiskQueries, isTrue); + File(dumpPath).deleteSync(); }); - test('recording window snapshots stable signal values at its start', - () async { + test('captures waveform to FST format', () async { final a = Logic(name: 'a'); - final mod = SimpleModule(a); + final mod = _SimpleWaveModule(a); await mod.build(); - a.inject(0); - const dumpName = 'windowInitialSnapshot'; - Directory(tempDumpDir).createSync(recursive: true); - WaveformService( - mod, - outputDirectory: tempDumpDir, - outputBaseName: 'temp_dump_$dumpName', - writeToFile: true, - startTime: 10, - ); - - Simulator.registerAction(5, () => a.put(1)); - Simulator.registerAction(15, () {}); - Simulator.registerAction(20, () {}); - await Simulator.run(); - - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - expect( - VcdParser.confirmValue(vcdContents, 'a', 15, LogicValue.one), - isTrue, - reason: 'the stable value at startTime must seed the recording window', - ); - - deleteTemporaryDump(dumpName); - }); - - test('window-entry snapshot reaches waveform hooks', () async { - final mod = WindowWaveModule(); - await mod.build(); - mod.a.put(0); - final service = HistoryWaveformService(mod, startTime: 10); - - Simulator.registerAction(5, () => mod.a.put(1)); - Simulator.registerAction(15, () {}); - Simulator.registerAction(20, () {}); - await Simulator.run(); + Directory(_tempDumpDir).createSync(recursive: true); + final dumpPath = _temporaryFstPath('fstCapture'); - final vcdContents = utf8.decode( - await service.artifacts.single - .openRead() - .expand((bytes) => bytes) - .toList(), - ); - expect( - VcdParser.confirmValue( - vcdContents, - 'a', - 10, - LogicValue.one, - ), - isTrue, - ); - expect(service.history[mod.a], equals([(0, '0'), (10, '1')])); - expect(service.capturedTimestamps, contains(10)); - }); - - test('rejects formats without a matching waveform writer', () async { - final mod = SimpleModule(Logic()); - await mod.build(); - - expect( - () => WaveformService(mod, format: WaveOutputFormat.fst), - throwsUnsupportedError, + WaveformService.fromOutputPath( + mod, + outputPath: dumpPath, + format: WaveOutputFormat.fst, + register: false, ); - }); - - test('attach dumper before put', () async { - final a = Logic(name: 'a'); - final mod = SimpleModule(a); - await mod.build(); - - const dumpName = 'dumpBeforePut'; - createTemporaryDump(mod, dumpName); a.inject(1); - Simulator.registerAction(10, () => a.put(0)); - Simulator.registerAction(20, () => a.put(1)); await Simulator.run(); - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); + final fstFile = File(dumpPath); + expect(fstFile.existsSync(), isTrue); + expect(fstFile.lengthSync(), greaterThan(100)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 1, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 20, LogicValue.ofString('1')), - equals(true)); - - deleteTemporaryDump(dumpName); + fstFile.deleteSync(); }); - test('multiple injects in the same timestamp', () async { - final clk = SimpleClockGenerator(10).clk; - final a = Logic(name: 'a'); - final mod = SimpleModule(a); - a <= clk; - - await mod.build(); - - const dumpName = 'multiInject'; - - createTemporaryDump(mod, dumpName); - - Simulator.setMaxSimTime(100); - unawaited(Simulator.run()); - - await clk.nextPosedge; - await clk.nextPosedge; - await clk.nextPosedge; - - // inject a 0 on a when it should be 1 already from the clock - a.inject(0); + test('VCD and FST contain matching value-change events', () async { + final vcdPath = _temporaryVcdPath('parity'); + final fstPath = _temporaryFstPath('parity'); - await Simulator.simulationEnded; + await _dumpParityWaveform(vcdPath, WaveOutputFormat.vcd); + final vcdEvents = _readVcdEvents(vcdPath, const {'a', 'b'}); - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); - - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofString('0')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 5, LogicValue.ofString('1')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofString('0')), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 35, LogicValue.ofString('0')), - equals(true)); - - deleteTemporaryDump(dumpName); - }); - - test('multi-bit value', () async { - final a = Logic(name: 'a', width: 8); - final mod = SimpleModule(a); - await mod.build(); - - const dumpName = 'multiBit'; - - createTemporaryDump(mod, dumpName); - a.inject(0x5a); - - Simulator.registerAction(10, () => a.put(0xa5)); - await Simulator.run(); + await Simulator.reset(); + ModuleServices.instance.reset(); - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); + await _dumpParityWaveform(fstPath, WaveOutputFormat.fst); + final fstEvents = _readFstEvents( + fstPath, + signalNames: const ['a', 'b'], + signalWidths: const [4, 4], + ); - expect( - VcdParser.confirmValue(vcdContents, 'a', 0, LogicValue.ofInt(0x5a, 8)), - equals(true)); - expect( - VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.ofInt(0xa5, 8)), - equals(true)); + expect(fstEvents, equals(vcdEvents)); - deleteTemporaryDump(dumpName); + File(vcdPath).deleteSync(); + File(fstPath).deleteSync(); }); +} - test('multi-bit value mixed invalid', () async { - final a = Logic(name: 'a', width: 8); - final mod = SimpleModule(a); - await mod.build(); +Future _dumpParityWaveform( + String outputPath, + WaveOutputFormat format, +) async { + Directory(_tempDumpDir).createSync(recursive: true); + + final a = Logic(name: 'a', width: 4); + final mod = _SimpleWaveModule(a); + await mod.build(); + + a.put(0x1); + WaveformService.fromOutputPath( + mod, + outputPath: outputPath, + format: format, + register: false, + ); - const dumpName = 'multiBitInvalid'; + Simulator.registerAction(10, () => a.put(0x2)); + Simulator.registerAction(20, () => a.put(0xf)); + await Simulator.run(); +} - createTemporaryDump(mod, dumpName); - a.inject(LogicValue.ofString('01xzzx10')); +Map> _readVcdEvents( + String path, + Set signalNames, +) { + final lines = File(path).readAsLinesSync(); + final markerToSignal = {}; + final markerToWidth = {}; + final events = >{ + for (final name in signalNames) name: {}, + }; + + final sigNameRegexp = RegExp( + r'\s*\$var\s(wire|reg)\s(\d+)\s(\S*)\s(\S*)\s+(\[\d+\:\d+\])?\s*\$end', + ); + var currentTime = 0; + var inValues = false; + + for (final line in lines) { + final match = sigNameRegexp.firstMatch(line); + if (match != null) { + final width = int.parse(match.group(2)!); + final marker = match.group(3)!; + final name = match.group(4)!; + if (signalNames.contains(name)) { + markerToSignal[marker] = name; + markerToWidth[marker] = width; + } + continue; + } - Simulator.registerAction(10, () => a.put(LogicValue.ofString('0x0x1z1z'))); - await Simulator.run(); + if (line == r'$dumpvars') { + inValues = true; + continue; + } + if (!inValues) { + continue; + } + if (line == r'$end') { + continue; + } + if (line.startsWith('#')) { + currentTime = int.parse(line.substring(1)); + continue; + } - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); + final parsed = _parseVcdValueUpdate(line, markerToWidth); + if (parsed == null) { + continue; + } - expect( - VcdParser.confirmValue( - vcdContents, 'a', 0, LogicValue.ofString('01xzzx10')), - equals(true)); - expect( - VcdParser.confirmValue( - vcdContents, 'a', 10, LogicValue.ofString('0x0x1z1z')), - equals(true)); + final signalName = markerToSignal[parsed.marker]; + if (signalName != null) { + events[signalName]![currentTime] = parsed.value; + } + } - deleteTemporaryDump(dumpName); - }); + return events; +} - test('dump after max sim time works', () async { - final a = SimpleClockGenerator(10).clk; - final mod = SimpleModule(a); - await mod.build(); +({String marker, String value})? _parseVcdValueUpdate( + String line, + Map markerToWidth, +) { + if (line.startsWith('b')) { + final parts = line.split(' '); + if (parts.length != 2 || !markerToWidth.containsKey(parts[1])) { + return null; + } + return (marker: parts[1], value: parts[0].substring(1)); + } - const dumpName = 'maxSimTime'; + for (final marker in markerToWidth.keys) { + if (line.endsWith(marker)) { + return (marker: marker, value: line[0]); + } + } + return null; +} - createTemporaryDump(mod, dumpName); +Map> _readFstEvents( + String path, { + required List signalNames, + required List signalWidths, +}) { + final data = File(path).readAsBytesSync(); + final events = >{ + for (final name in signalNames) name: {}, + }; + + var blockOffset = 0; + while (blockOffset < data.length) { + final blockType = data[blockOffset]; + final sectionLength = _readU64(data, blockOffset + 1); + final blockEnd = blockOffset + 1 + sectionLength; + + if (blockType == 8) { + _readFstVcDataBlock( + data, + blockOffset, + blockEnd, + signalNames: signalNames, + signalWidths: signalWidths, + events: events, + ); + } - Simulator.setMaxSimTime(100); + blockOffset = blockEnd; + } - await Simulator.run(); + return events; +} - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); +void _readFstVcDataBlock( + Uint8List data, + int blockOffset, + int blockEnd, { + required List signalNames, + required List signalWidths, + required Map> events, +}) { + final startTime = _readU64(data, blockOffset + 9); + var offset = blockOffset + 33; + + final frameUncompressed = _readVarint(data, offset); + offset = frameUncompressed.next; + final frameCompressed = _readVarint(data, offset); + offset = frameCompressed.next; + final maxHandle = _readVarint(data, offset); + offset = maxHandle.next; + + final frameBytes = _inflateIfNeeded( + data.sublist(offset, offset + frameCompressed.value), + frameUncompressed.value, + ); + offset += frameCompressed.value; - expect( - VcdParser.confirmValue(vcdContents, 'a', 99, LogicValue.one), - equals(true), + var frameOffset = 0; + for (var i = 0; i < signalNames.length; i++) { + final width = signalWidths[i]; + final value = String.fromCharCodes( + frameBytes.sublist(frameOffset, frameOffset + width), ); + frameOffset += width; + events[signalNames[i]]![startTime] = value; + } - deleteTemporaryDump(dumpName); - }); + final valueMaxHandle = _readVarint(data, offset); + offset = valueMaxHandle.next; + final valueSectionStart = offset; + offset++; // pack_type + + final timeCount = _readU64(data, blockEnd - 8); + final timeCompressedLength = _readU64(data, blockEnd - 16); + final timeUncompressedLength = _readU64(data, blockEnd - 24); + final timeDataStart = blockEnd - 24 - timeCompressedLength; + final timeBytes = _inflateIfNeeded( + data.sublist(timeDataStart, timeDataStart + timeCompressedLength), + timeUncompressedLength, + ); + final timeTable = _decodeTimeTable(timeBytes, timeCount); - test('create non-existent output directories', () async { - final mod = SimpleModule(Logic()); - await mod.build(); + final chainLength = _readU64(data, timeDataStart - 8); + final chainStart = timeDataStart - 8 - chainLength; + final signalOffsets = _decodeFstOffsetChain( + data.sublist(chainStart, timeDataStart - 8), + valueMaxHandle.value, + ); - const dir1Path = '$tempDumpDir/dir1'; + for (var signalIndex = 0; signalIndex < signalNames.length; signalIndex++) { + final signalOffset = signalOffsets[signalIndex]; + if (signalOffset == null) { + continue; + } - final waveformService = WaveformService( - mod, - outputDirectory: '$dir1Path/dir2', - outputBaseName: 'waves', - writeToFile: true, + final nextOffset = signalOffsets + .skip(signalIndex + 1) + .whereType() + .cast() + .firstWhere((offset) => offset != null, orElse: () => null); + final signalDataStart = valueSectionStart + signalOffset; + final signalDataEnd = + nextOffset == null ? chainStart : valueSectionStart + nextOffset; + _decodeFstSignalData( + data.sublist(signalDataStart, signalDataEnd), + width: signalWidths[signalIndex], + signalName: signalNames[signalIndex], + timeTable: timeTable, + events: events, ); + } +} - expect(File(waveformService.outputFilePath).existsSync(), equals(true)); - - // Let the service close its asynchronous file sink before cleanup. - await Simulator.run(); +List _decodeTimeTable(Uint8List bytes, int count) { + final times = []; + var offset = 0; + var previousTime = 0; + for (var i = 0; i < count; i++) { + final delta = _readVarint(bytes, offset); + offset = delta.next; + previousTime += delta.value; + times.add(previousTime); + } + return times; +} - if (File(waveformService.outputFilePath).existsSync()) { - File(dir1Path).deleteSync(recursive: true); +List _decodeFstOffsetChain(Uint8List bytes, int maxHandle) { + final offsets = List.filled(maxHandle, null); + var byteOffset = 0; + var signalIndex = 0; + var previousOffset = 0; + + while (signalIndex < maxHandle && byteOffset < bytes.length) { + final encoded = _readSignedVarint(bytes, byteOffset); + byteOffset = encoded.next; + if (encoded.value.isEven) { + signalIndex += encoded.value >> 1; + } else { + previousOffset += encoded.value >> 1; + offsets[signalIndex] = previousOffset; + signalIndex++; } - }); + } - test('async reset shown in waves correctly', () async { - final reset = Logic(); - final clk = SimpleClockGenerator(10).clk; - final mod = SimpleModWithSeq(reset, clk); + return offsets; +} - await mod.build(); +void _decodeFstSignalData( + Uint8List bytes, { + required int width, + required String signalName, + required List timeTable, + required Map> events, +}) { + var offset = 0; + final compression = _readVarint(bytes, offset); + offset = compression.next; + expect( + compression.value, + equals(0), + reason: 'Only uncompressed signal chains are expected', + ); - const dumpName = 'asyncReset'; + var timeIndex = 0; + while (offset < bytes.length) { + if (width == 1) { + final encoded = _readVarint(bytes, offset); + offset = encoded.next; + String value; + int timeDelta; + if (encoded.value.isEven) { + value = ((encoded.value >> 1) & 1).toString(); + timeDelta = encoded.value >> 2; + } else { + const rcvChars = 'xzhuwl-?'; + value = rcvChars[(encoded.value >> 1) & 0x7]; + timeDelta = encoded.value >> 4; + } + timeIndex += timeDelta; + events[signalName]![timeTable[timeIndex]] = value; + } else { + final encoded = _readVarint(bytes, offset); + offset = encoded.next; + timeIndex += encoded.value >> 1; + + final isFourState = encoded.value.isOdd; + String value; + if (isFourState) { + value = String.fromCharCodes(bytes.sublist(offset, offset + width)); + offset += width; + } else { + final byteCount = (width + 7) ~/ 8; + final packed = bytes.sublist(offset, offset + byteCount); + offset += byteCount; + value = _unpackTwoStateBits(packed, width); + } + + events[signalName]![timeTable[timeIndex]] = value; + } + } +} - Simulator.setMaxSimTime(100); - Simulator.registerAction(13, () => reset.put(1)); - reset.put(0); +String _unpackTwoStateBits(Uint8List bytes, int width) { + final bits = StringBuffer(); + for (var i = 0; i < width; i++) { + final byteIndex = i ~/ 8; + final bitIndex = 7 - (i % 8); + bits.write(((bytes[byteIndex] >> bitIndex) & 1).toString()); + } + return bits.toString(); +} - // add waveform service *after* the put to reset - createTemporaryDump(mod, dumpName); +Uint8List _inflateIfNeeded(Uint8List bytes, int uncompressedLength) { + if (bytes.length == uncompressedLength) { + return bytes; + } + return Uint8List.fromList(ZLibCodec().decode(bytes)); +} - // check functional matches - Simulator.registerAction(0, () => expect(reset.value.toInt(), 0)); - Simulator.registerAction(6, () => expect(mod.val.value.toInt(), 1)); - Simulator.registerAction(14, () => expect(mod.val.value.toInt(), 0)); +({int value, int next}) _readVarint(Uint8List data, int offset) { + var value = 0; + var shift = 0; + var next = offset; - await Simulator.run(); + while (true) { + final byte = data[next++]; + value |= (byte & 0x7f) << shift; + if ((byte & 0x80) == 0) { + return (value: value, next: next); + } + shift += 7; + } +} - final vcdContents = File(temporaryDumpPath(dumpName)).readAsStringSync(); +({int value, int next}) _readSignedVarint(Uint8List data, int offset) { + var value = 0; + var shift = 0; + var next = offset; + late int byte; - // reset is 0 initially - expect( - VcdParser.confirmValue(vcdContents, 'asyncReset', 1, LogicValue.zero), - equals(true)); + do { + byte = data[next++]; + value |= (byte & 0x7f) << shift; + shift += 7; + } while ((byte & 0x80) != 0); - // 1 after first clock edge - expect(VcdParser.confirmValue(vcdContents, 'val', 6, LogicValue.one), - equals(true)); + if (shift < 64 && (byte & 0x40) != 0) { + value |= -(1 << shift); + } - // 0 after async reset - expect(VcdParser.confirmValue(vcdContents, 'val', 14, LogicValue.zero), - equals(true)); + return (value: value, next: next); +} - deleteTemporaryDump(dumpName); - }); +int _readU64(Uint8List data, int offset) { + var result = 0; + for (var i = 0; i < 8; i++) { + result = (result << 8) | data[offset + i]; + } + return result; } From 1e2af356e6d83f4ef1a662b072933c01b165f4cc Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 06:01:36 -0700 Subject: [PATCH 29/36] streaming waveform fix --- lib/src/diagnostics/waveform_service.dart | 13 +++++++++++++ lib/src/diagnostics/waveform_writer.dart | 20 ++++++++++++++++++++ lib/src/module.dart | 10 +++++++--- test/waveform_service_test.dart | 17 +++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index 5373472a6..9d48dbbe0 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -63,6 +63,15 @@ class WaveformService extends ArtifactProducingService { /// Whether to register this service with [ModuleServices] for inspection. final bool register; + /// Whether to retain a complete in-memory copy for debugging consumers. + final bool retainInMemory; + + /// Whether this service can provide waveform data to a consumer. + bool canSendWaveforms() => retainInMemory || format.supportsOnDiskQueries; + + /// The retained VCD waveform, or `null` when retention is disabled. + String? get inMemoryOutput => _writer.inMemoryOutput; + /// The FST writer configuration (only used when [format] is /// [WaveOutputFormat.fst]). final FstWriterConfig? fstConfig; @@ -98,6 +107,7 @@ class WaveformService extends ArtifactProducingService { this.flushBufferSize = 100000, this.overwritePolicy = OverwritePolicy.overwrite, this.register = true, + this.retainInMemory = false, this.fstConfig, }) : super(module) { if (!module.hasBuilt) { @@ -154,6 +164,7 @@ class WaveformService extends ArtifactProducingService { int flushBufferSize = 100000, OverwritePolicy overwritePolicy = OverwritePolicy.overwrite, bool register = true, + bool retainInMemory = false, FstWriterConfig? fstConfig, }) { final normalized = outputPath.replaceAll(r'\', '/'); @@ -176,6 +187,7 @@ class WaveformService extends ArtifactProducingService { flushBufferSize: flushBufferSize, overwritePolicy: overwritePolicy, register: register, + retainInMemory: retainInMemory, fstConfig: fstConfig, ); } @@ -208,6 +220,7 @@ class WaveformService extends ArtifactProducingService { timescale: timescale, flushBufferSize: flushBufferSize, overwritePolicy: overwritePolicy, + memoryBuffer: retainInMemory ? StringBuffer() : null, ); case WaveOutputFormat.fst: return FstWaveformWriter( diff --git a/lib/src/diagnostics/waveform_writer.dart b/lib/src/diagnostics/waveform_writer.dart index 67a7272b9..62c329be9 100644 --- a/lib/src/diagnostics/waveform_writer.dart +++ b/lib/src/diagnostics/waveform_writer.dart @@ -32,6 +32,12 @@ enum WaveOutputFormat { WaveOutputFormat.vcd => 'text/x-vcd', WaveOutputFormat.fst => 'application/vnd.gtkwave.fst', }; + + /// Whether waveform data can be queried from the output file. + bool get supportsOnDiskQueries => switch (this) { + WaveOutputFormat.vcd => false, + WaveOutputFormat.fst => true, + }; } /// Policy applied when the output file already exists at construction time. @@ -105,6 +111,9 @@ abstract class WaveformWriter { /// Returns a JSON-serialisable summary of writer state. Map toJson(); + + /// Returns the retained waveform text, when supported and enabled. + String? get inMemoryOutput; } /// VCD implementation of [WaveformWriter]. @@ -115,6 +124,7 @@ class VcdWaveformWriter implements WaveformWriter { this.timescale = '1ps', this.flushBufferSize = 100000, this.overwritePolicy = OverwritePolicy.overwrite, + this.memoryBuffer, }) { if (overwritePolicy == OverwritePolicy.failIfExists) { final existingFile = File(outputPath); @@ -144,6 +154,9 @@ class VcdWaveformWriter implements WaveformWriter { /// Existing-file policy. final OverwritePolicy overwritePolicy; + /// Optional buffer receiving a complete copy of the VCD output. + final StringBuffer? memoryBuffer; + late final File _outputFile; late final IOSink _outFileSink; final StringBuffer _fileBuffer = StringBuffer(); @@ -156,6 +169,9 @@ class VcdWaveformWriter implements WaveformWriter { @override WaveOutputFormat get format => WaveOutputFormat.vcd; + @override + String? get inMemoryOutput => memoryBuffer?.toString(); + @override void pushScope(String name) { final padding = List.filled(_indent, ' ').join(); @@ -249,6 +265,7 @@ class VcdWaveformWriter implements WaveformWriter { } void _writeToBuffer(String contents) { + memoryBuffer?.write(contents); _fileBuffer.write(contents); if (_fileBuffer.length > flushBufferSize) { _flushBuffer(); @@ -275,6 +292,9 @@ class FstWaveformWriter implements WaveformWriter { @override WaveOutputFormat get format => WaveOutputFormat.fst; + @override + String? get inMemoryOutput => null; + @override void pushScope(String name) { writer.pushScope(name); diff --git a/lib/src/module.dart b/lib/src/module.dart index 52519cab6..0f3511283 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1149,9 +1149,12 @@ abstract class Module { /// Attaches waveform dumping for this [Module] to a VCD at [outputPath]. /// - /// For filtering, alternative formats, and other waveform controls, use - /// [WaveformService] directly. - WaveformService dumpWaves({String outputPath = 'waves.vcd'}) { + /// Set [retainInMemory] to `true` when a debugger needs to query the + /// complete waveform history while the simulation is running. + WaveformService dumpWaves({ + String outputPath = 'waves.vcd', + bool retainInMemory = false, + }) { final normalized = outputPath.replaceAll(r'\', '/'); final separatorIndex = normalized.lastIndexOf('/'); final outputDirectory = switch (separatorIndex) { @@ -1165,6 +1168,7 @@ abstract class Module { this, outputDirectory: outputDirectory, outputFileName: outputFileName, + retainInMemory: retainInMemory, ); } diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index d8a99747e..61bb3a435 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -61,6 +61,23 @@ void main() { ModuleServices.instance.reset(); }); + test('dumpWaves can retain history for debugging', () async { + final mod = _SimpleWaveModule(Logic()); + await mod.build(); + + final service = mod.dumpWaves( + outputPath: temporaryDumpPath('debugDump'), + retainInMemory: true, + ); + + expect(service.retainInMemory, isTrue); + expect(service.canSendWaveforms(), isTrue); + expect(service.inMemoryOutput, isNotNull); + + await Simulator.run(); + deleteTemporaryDump('debugDump'); + }); + test('registers with ModuleServices by default', () async { final a = Logic(name: 'a'); final mod = _SimpleWaveModule(a); From bff7b6005505d30a77a22d01b1f7524b28e787a8 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 22:17:31 -0700 Subject: [PATCH 30/36] Keep waveform service current registry-backed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/diagnostics/waveform_service.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index 9d48dbbe0..a78ddbaa3 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -22,7 +22,11 @@ import 'package:rohd/src/utilities/uniquifier.dart'; /// [WaveOutputFormat.vcd], [FstWaveformWriter] for [WaveOutputFormat.fst]). class WaveformService extends ArtifactProducingService { /// The most recently registered [WaveformService], or `null`. - static WaveformService? current; + /// + /// This is backed by [ModuleServices], so it is cleared by unregistering + /// this service type or resetting the registry. + static WaveformService? get current => + ModuleServices.instance.lookup(); /// Exact output filename override. /// @@ -142,7 +146,6 @@ class WaveformService extends ArtifactProducingService { }); if (register) { - current = this; ModuleServices.instance.register(this); } } From 03a38affd5633eb970ea982332d95e04ed1e0394 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 07:41:13 -0700 Subject: [PATCH 31/36] fix: preserve window snapshots with waveform writers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/diagnostics/waveform_service.dart | 43 +++++++++++++- test/waveform_service_test.dart | 72 +++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index a78ddbaa3..c150552cd 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -91,6 +91,9 @@ class WaveformService extends ArtifactProducingService { /// The timestamp currently being accumulated. int _currentDumpingTimestamp = Simulator.time; + /// Whether the recording window's initial signal snapshot was emitted. + bool _hasWrittenWindowSnapshot = false; + /// Creates a [WaveformService] for [module]. /// /// [module] must be built before construction. [outputDirectory] defaults to @@ -129,6 +132,7 @@ class WaveformService extends ArtifactProducingService { ), timestamp: Simulator.time, ); + _hasWrittenWindowSnapshot = startTime == null || startTime == 0; Simulator.preTick.listen((_) { if (Simulator.time != _currentDumpingTimestamp) { @@ -136,6 +140,7 @@ class WaveformService extends ArtifactProducingService { _captureTimestamp(_currentDumpingTimestamp); } _currentDumpingTimestamp = Simulator.time; + _writeWindowSnapshotIfNeeded(Simulator.time); } }); @@ -203,11 +208,18 @@ class WaveformService extends ArtifactProducingService { @protected void onSignalCollected(Logic signal) {} - /// Called for every value-change event on [signal] at [timestamp]. + /// Called for every captured value on [signal] at [timestamp]. + /// + /// When [startTime] is set, this includes one window-entry value for every + /// tracked signal at [startTime]. Those calls describe the state entering + /// the recording window, rather than physical transitions. @protected void onValueChange(Logic signal, int timestamp) {} - /// Called once per simulation timestamp that contains at least one change. + /// Called once after each batch of captured values at [timestamp]. + /// + /// When [startTime] is set, the complete window-entry signal snapshot is + /// delivered as a batch at [startTime] before later value-change batches. @protected void onTimestampCapture(int timestamp, Set changed) {} @@ -302,6 +314,7 @@ class WaveformService extends ArtifactProducingService { return; } + _writeWindowSnapshotIfNeeded(timestamp); final snapshot = Set.of(_changedThisTimestamp); final changes = [ for (final sig in snapshot) @@ -322,6 +335,32 @@ class WaveformService extends ArtifactProducingService { } } + void _writeWindowSnapshotIfNeeded(int timestamp) { + if (_hasWrittenWindowSnapshot || + startTime == null || + timestamp < startTime! || + !_isInRecordingWindow(startTime!)) { + return; + } + + final snapshot = Set.of(_signalHandles.keys); + _writer.emitValueChanges( + startTime!, + [ + for (final signal in snapshot) + WaveformValueChange(_signalHandles[signal]!, _binaryValue(signal)), + ], + ); + _hasWrittenWindowSnapshot = true; + + for (final signal in snapshot) { + onValueChange(signal, startTime!); + } + if (snapshot.isNotEmpty) { + onTimestampCapture(startTime!, snapshot); + } + } + String _binaryValue(Logic signal) => signal.value.reversed .toList() .map((e) => e.toString(includeWidth: false)) diff --git a/test/waveform_service_test.dart b/test/waveform_service_test.dart index 61bb3a435..8fdf46264 100644 --- a/test/waveform_service_test.dart +++ b/test/waveform_service_test.dart @@ -25,6 +25,47 @@ class _SimpleWaveModule extends Module { } } +class _WindowWaveModule extends Module { + late final Logic a; + + _WindowWaveModule() { + a = addOutput('a'); + } +} + +class _HistoryWaveformService extends WaveformService { + final Map> history = {}; + final List capturedTimestamps = []; + + _HistoryWaveformService( + super.module, { + super.outputDirectory, + super.outputBaseName, + super.startTime, + super.register, + }); + + @override + void onSignalCollected(Logic signal) { + history[signal] = [(Simulator.time, _binaryValue(signal))]; + } + + @override + void onValueChange(Logic signal, int timestamp) { + history[signal]!.add((timestamp, _binaryValue(signal))); + } + + @override + void onTimestampCapture(int timestamp, Set changed) { + capturedTimestamps.add(timestamp); + } +} + +String _binaryValue(Logic signal) => signal.value.reversed + .toList() + .map((value) => value.toString(includeWidth: false)) + .join(); + const _tempDumpDir = 'tmp_test'; String _temporaryVcdPath(String name) => '$_tempDumpDir/temp_wave_$name.vcd'; @@ -96,6 +137,37 @@ void main() { File(dumpPath).deleteSync(); }); + test('window-entry snapshot reaches waveform hooks', () async { + final mod = _WindowWaveModule(); + await mod.build(); + mod.a.put(0); + + Directory(_tempDumpDir).createSync(recursive: true); + final dumpPath = _temporaryVcdPath('windowHookSnapshot'); + final service = _HistoryWaveformService( + mod, + outputDirectory: _tempDumpDir, + outputBaseName: 'temp_wave_windowHookSnapshot', + startTime: 10, + register: false, + ); + + Simulator.registerAction(5, () => mod.a.put(1)); + Simulator.registerAction(15, () {}); + Simulator.registerAction(20, () {}); + await Simulator.run(); + + final vcdContents = File(dumpPath).readAsStringSync(); + expect( + VcdParser.confirmValue(vcdContents, 'a', 10, LogicValue.one), + isTrue, + ); + expect(service.history[mod.a], equals([(0, '0'), (10, '1')])); + expect(service.capturedTimestamps, contains(10)); + + File(dumpPath).deleteSync(); + }); + test('captures waveform to VCD output path', () async { final a = Logic(name: 'a'); final mod = _SimpleWaveModule(a); From 8c0a7f90f76a9e884b9a9e1c8d6b4c8e34b35f6e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 07:55:24 -0700 Subject: [PATCH 32/36] feat: add FST waveform query provider Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/rohd.dart | 1 + lib/src/diagnostics/waveform_service.dart | 10 +++ lib/src/diagnostics/waveform_writer.dart | 4 + lib/src/fst/fst_waveform_query.dart | 104 ++++++++++++++++++++++ test/fst_writer_test.dart | 36 ++++++++ 5 files changed, 155 insertions(+) create mode 100644 lib/src/fst/fst_waveform_query.dart diff --git a/lib/rohd.dart b/lib/rohd.dart index 4ebac2244..7a242c1e8 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -13,6 +13,7 @@ export 'src/external.dart'; export 'src/finite_state_machine.dart'; export 'src/fst/fst_block_reader.dart'; export 'src/fst/fst_types.dart'; +export 'src/fst/fst_waveform_query.dart'; export 'src/fst/fst_writer.dart'; export 'src/interfaces/interfaces.dart'; export 'src/module.dart'; diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index c150552cd..b4e21772d 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/fst/fst_waveform_query.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -82,6 +83,15 @@ class WaveformService extends ArtifactProducingService { late final WaveformWriter _writer; + /// Creates a bounded-memory query provider for an FST capture. + /// + /// Returns `null` for VCD captures, whose text output is not indexed for + /// time-range queries. + FstWaveformQuery? createFstQuery() => switch (_writer) { + FstWaveformWriter() => (_writer as FstWaveformWriter).createQuery(), + _ => null, + }; + /// Maps each captured [Logic] to its writer-specific signal handle. final Map _signalHandles = {}; diff --git a/lib/src/diagnostics/waveform_writer.dart b/lib/src/diagnostics/waveform_writer.dart index 62c329be9..38285af7f 100644 --- a/lib/src/diagnostics/waveform_writer.dart +++ b/lib/src/diagnostics/waveform_writer.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/fst/fst_waveform_query.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/timestamper.dart'; @@ -289,6 +290,9 @@ class FstWaveformWriter implements WaveformWriter { /// The low-level FST binary writer. final FstWriter writer; + /// Creates a bounded-memory query provider for this writer's waveform. + FstWaveformQuery createQuery() => FstWaveformQuery(writer); + @override WaveOutputFormat get format => WaveOutputFormat.fst; diff --git a/lib/src/fst/fst_waveform_query.dart b/lib/src/fst/fst_waveform_query.dart new file mode 100644 index 000000000..7fe576336 --- /dev/null +++ b/lib/src/fst/fst_waveform_query.dart @@ -0,0 +1,104 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// fst_waveform_query.dart +// Live, bounded-memory queries over FST writer output. + +import 'package:rohd/src/fst/fst_block_reader.dart'; +import 'package:rohd/src/fst/fst_writer.dart'; + +/// Queries an [FstWriter]'s flushed blocks and unflushed hot buffer. +/// +/// This provider does not own the writer. It may be used while simulation is +/// running, after [FstWriter.flushBlock] has moved historical changes to disk, +/// or after the writer has finished. +class FstWaveformQuery { + /// Creates a query provider for a writer whose signals have been declared. + FstWaveformQuery(this.writer) + : _reader = FstBlockReader(writer.filePath, writer.signalInfoList); + + /// The writer supplying the indexed disk blocks and hot buffer. + final FstWriter writer; + + final FstBlockReader _reader; + + /// Returns all value changes for [signal] within the inclusive time range. + /// + /// Results merge matching flushed blocks with the writer's unflushed changes + /// and are ordered by timestamp. + List changes( + FstSignalHandle signal, { + required int startTime, + required int endTime, + }) { + if (startTime > endTime) { + throw ArgumentError.value( + endTime, + 'endTime', + 'must be greater than or equal to startTime', + ); + } + + final handleIndex = signal.handle - 1; + _validateHandle(handleIndex); + final result = []; + for (final block in writer.blockIndex) { + if (block.endTime < startTime || block.startTime > endTime) { + continue; + } + result.addAll( + _reader.readBlock( + block, + handleIndices: {handleIndex}, + startTime: startTime, + endTime: endTime, + )[handleIndex] ?? + const [], + ); + } + result.addAll( + writer + .queryHotBuffer(handleIndex, startTime, endTime) + .map((change) => FstValueChange(change.time, change.value)), + ); + result.sort((a, b) => a.time.compareTo(b.time)); + return result; + } + + /// Returns [signal]'s value at or immediately before [time]. + /// + /// Returns `null` when the writer has not emitted a value for [signal]. + String? valueAt(FstSignalHandle signal, int time) { + final handleIndex = signal.handle - 1; + _validateHandle(handleIndex); + + final hotChanges = writer.queryHotBuffer(handleIndex, 0, time); + if (hotChanges.isNotEmpty) { + return hotChanges.last.value; + } + + final blocks = writer.blockIndex; + for (var index = blocks.length - 1; index >= 0; index--) { + final block = blocks[index]; + if (block.startTime > time) { + continue; + } + final changes = _reader.readBlock( + block, + handleIndices: {handleIndex}, + endTime: time, + )[handleIndex]; + if (changes != null && changes.isNotEmpty) { + return changes.last.value; + } + return _reader.readBlockFrame(block)[handleIndex]; + } + return null; + } + + void _validateHandle(int handleIndex) { + if (handleIndex < 0 || handleIndex >= writer.signalCount) { + throw RangeError.index(handleIndex, writer.signalInfoList, 'signal'); + } + } +} diff --git a/test/fst_writer_test.dart b/test/fst_writer_test.dart index 8da79a2ff..b067f18c7 100644 --- a/test/fst_writer_test.dart +++ b/test/fst_writer_test.dart @@ -119,6 +119,42 @@ void main() { }); group('FstWriter unit tests', () { + test('queries flushed blocks and the hot buffer', () { + const path = '$_tempDumpDir/fst_query_test.fst'; + Directory(_tempDumpDir).createSync(recursive: true); + + final writer = FstWriter( + path, + config: const FstWriterConfig(maxChangesPerBlock: 2), + )..pushScope('top'); + final signal = writer.declareSignal('signal', 1); + writer + ..popScope() + ..writeHeader() + ..emitValueChange(0, signal, '0') + ..emitValueChange(5, signal, '1') + ..emitValueChange(10, signal, '0'); + + final query = FstWaveformQuery(writer); + expect( + query + .changes(signal, startTime: 0, endTime: 10) + .map((change) => (change.time, change.value)), + equals([(0, '0'), (5, '1'), (10, '0')]), + ); + expect(query.valueAt(signal, 7), equals('1')); + expect(query.valueAt(signal, 10), equals('0')); + + writer.finish(); + expect( + query + .changes(signal, startTime: 0, endTime: 10) + .map((change) => (change.time, change.value)), + equals([(0, '0'), (5, '1'), (10, '0')]), + ); + File(path).deleteSync(); + }); + test('writes valid header block', () { const path = '$_tempDumpDir/fst_header_test.fst'; Directory(_tempDumpDir).createSync(recursive: true); From 5e5e87e7aa96b4cd589deeed4f7a37a9642373da Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 08:05:49 -0700 Subject: [PATCH 33/36] test: isolate waveform registry artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/module_services_test.dart | 35 ++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/test/module_services_test.dart b/test/module_services_test.dart index 34e2cbbd3..3ca48422e 100644 --- a/test/module_services_test.dart +++ b/test/module_services_test.dart @@ -35,10 +35,33 @@ class FakeService implements ModuleService { Map toJson() => {'kind': 'fake'}; } +final _waveformOutputDirectories = []; + +WaveformService _createWaveformService( + Module module, { + bool register = true, +}) { + final directory = Directory.systemTemp.createTempSync( + 'rohd_module_services_waveform_', + ); + _waveformOutputDirectories.add(directory); + return WaveformService.fromOutputPath( + module, + outputPath: '${directory.path}/capture.vcd', + register: register, + ); +} + void main() { tearDown(() async { await Simulator.reset(); ModuleServices.instance.reset(); + for (final directory in _waveformOutputDirectories) { + if (directory.existsSync()) { + directory.deleteSync(recursive: true); + } + } + _waveformOutputDirectories.clear(); }); group('ModuleServices registry', () { @@ -102,8 +125,8 @@ void main() { final mod = SimpleModule(Logic()); await mod.build(); - final firstWaveform = WaveformService(mod); - final secondWaveform = WaveformService(mod); + final firstWaveform = _createWaveformService(mod); + final secondWaveform = _createWaveformService(mod); final firstNetlist = NetlistService(mod); final secondNetlist = NetlistService(mod); final firstSv = SystemVerilogService(mod); @@ -124,10 +147,10 @@ void main() { final mod = SimpleModule(Logic()); await mod.build(); - final waveform = WaveformService(mod); + final waveform = _createWaveformService(mod); final netlist = NetlistService(mod); final sv = SystemVerilogService(mod); - WaveformService(mod, register: false); + _createWaveformService(mod, register: false); NetlistService(mod, register: false); SystemVerilogService(mod, register: false); @@ -139,7 +162,7 @@ void main() { test('unregister clears matching service current accessors', () async { final mod = SimpleModule(Logic()); await mod.build(); - WaveformService(mod); + _createWaveformService(mod); NetlistService(mod); SystemVerilogService(mod); @@ -155,7 +178,7 @@ void main() { test('reset clears every service current accessor', () async { final mod = SimpleModule(Logic()); await mod.build(); - WaveformService(mod); + _createWaveformService(mod); NetlistService(mod); SystemVerilogService(mod); From cbf4b3a63a0b7f9932a9ce2dd6f48f2dcf396cd5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 08:28:00 -0700 Subject: [PATCH 34/36] style: resolve FST analyzer lints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/diagnostics/waveform_service.dart | 1 - lib/src/diagnostics/waveform_writer.dart | 1 - lib/src/fst/fst_waveform_query.dart | 13 +++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/src/diagnostics/waveform_service.dart b/lib/src/diagnostics/waveform_service.dart index b4e21772d..8f53ccfe6 100644 --- a/lib/src/diagnostics/waveform_service.dart +++ b/lib/src/diagnostics/waveform_service.dart @@ -12,7 +12,6 @@ import 'dart:io'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/fst/fst_waveform_query.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; diff --git a/lib/src/diagnostics/waveform_writer.dart b/lib/src/diagnostics/waveform_writer.dart index 38285af7f..c598acd84 100644 --- a/lib/src/diagnostics/waveform_writer.dart +++ b/lib/src/diagnostics/waveform_writer.dart @@ -10,7 +10,6 @@ import 'dart:io'; import 'package:rohd/rohd.dart'; -import 'package:rohd/src/fst/fst_waveform_query.dart'; import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/timestamper.dart'; diff --git a/lib/src/fst/fst_waveform_query.dart b/lib/src/fst/fst_waveform_query.dart index 7fe576336..4116518c3 100644 --- a/lib/src/fst/fst_waveform_query.dart +++ b/lib/src/fst/fst_waveform_query.dart @@ -56,12 +56,13 @@ class FstWaveformQuery { const [], ); } - result.addAll( - writer - .queryHotBuffer(handleIndex, startTime, endTime) - .map((change) => FstValueChange(change.time, change.value)), - ); - result.sort((a, b) => a.time.compareTo(b.time)); + result + ..addAll( + writer + .queryHotBuffer(handleIndex, startTime, endTime) + .map((change) => FstValueChange(change.time, change.value)), + ) + ..sort((a, b) => a.time.compareTo(b.time)); return result; } From 7dddc07ab9725597d9478790a6a11213f87144b9 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 10:29:03 -0700 Subject: [PATCH 35/36] Align FST material_ui with main Remove the obsolete material_ui upper bound and cupertino_ui override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rohd_devtools_extension/pubspec.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 99c70653d..f352a624f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_bloc: ^9.0.0 flutter_simple_treeview: ^3.0.2 logging: ^1.2.0 - material_ui: ">=1.1.0 <1.3.0" + material_ui: ^1.1.0 rohd_devtools_widgets: path: ../packages/rohd_devtools_widgets vm_service: ^14.3.1 @@ -40,8 +40,6 @@ flutter: - assets/icons/ dependency_overrides: - # Keep the transitive Cupertino UI package below the Dart 3.6-incompatible release. - cupertino_ui: ">=1.0.0 <1.1.0" rohd: path: ../ rohd_hierarchy: From 67ee54080ad819a28b6f299dd6cc30612d270681 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 18 Sep 2026 10:34:14 -0700 Subject: [PATCH 36/36] Revert "Align FST material_ui with main" This reverts commit 4842ff5101850584f6302a458fd29a9c566f60f8. --- rohd_devtools_extension/pubspec.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index f352a624f..99c70653d 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_bloc: ^9.0.0 flutter_simple_treeview: ^3.0.2 logging: ^1.2.0 - material_ui: ^1.1.0 + material_ui: ">=1.1.0 <1.3.0" rohd_devtools_widgets: path: ../packages/rohd_devtools_widgets vm_service: ^14.3.1 @@ -40,6 +40,8 @@ flutter: - assets/icons/ dependency_overrides: + # Keep the transitive Cupertino UI package below the Dart 3.6-incompatible release. + cupertino_ui: ">=1.0.0 <1.1.0" rohd: path: ../ rohd_hierarchy: