diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4ca48e8a42..12e172c74b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,11 +2,7 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu { - "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", - - "features": { - "ghcr.io/devcontainers/features/node:1": { "version": "24" } - }, + "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", "updateContentCommand": "tool/gh_codespaces/run_setup.sh", diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 9e3d059419..976836818c 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint Markdown files uses: DavidAnson/markdownlint-cli2-action@v11 @@ -32,17 +32,22 @@ jobs: # use-quiet-mode: yes # output is too noisy, see https://github.com/gaurav-nelson/github-action-markdown-link-check/issues/121 # config-file: .github/configs/mlc_config.json - # get submodules *after* checking markdown, we dont care about markdown errors in submodules - - name: Checkout submodules - run: git submodule update --init --recursive + - name: Setup Dart + uses: dart-lang/setup-dart@v1 - - name: Setup Node - uses: actions/setup-node@v4 + - name: Setup Flutter SDK + uses: flutter-actions/setup-flutter@v2 with: - node-version: '24' + channel: stable + version: 3.47.2 - - name: Setup Dart - uses: dart-lang/setup-dart@v1 + - name: Prefer standalone Dart SDK + run: echo "$DART_HOME/bin" >> "$GITHUB_PATH" + + - name: Report standalone Dart SDK + run: | + command -v dart + dart --version - name: Setup Pana source analysis run: tool/gh_actions/install_pana.sh @@ -62,23 +67,17 @@ jobs: - name: Check project documentation run: tool/gh_actions/generate_documentation.sh - - name: Run project tests + - name: Run root package tests run: tool/gh_actions/run_tests.sh - - - name: Install CAD Suite (includes yosys) - run: tool/gh_actions/install_opencadsuite.sh - - name: Install D3 Schematic viewer - run: tool/gh_actions/install_d3_hwschematic.sh + - name: Generate component netlists + run: dart run gen/generate.dart - - name: Generate HTML for examples - run: tool/gh_actions/create_htmls.sh - - name: Check temporary test files run: tool/gh_actions/check_tmp_test.sh # https://github.com/devcontainers/ci/blob/main/docs/github-action.md - - name: Build dev container and run tests in it + - name: Build dev container and run root tests in it uses: devcontainers/ci@v0.3 with: runCmd: tool/gh_actions/run_tests.sh @@ -90,28 +89,40 @@ jobs: runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} steps: - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive + uses: actions/checkout@v4 - name: Setup Flutter SDK uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.27.0 - + version: 3.47.2 + + - name: Install project dependencies + run: tool/gh_actions/install_dependencies.sh + - name: Analyze flutter source run: tool/gh_actions/analyze_flutter_source.sh - - - name: Run flutter tests + + - name: Run confapp tests run: tool/gh_actions/run_flutter_tests.sh - + - name: Build static site run: tool/gh_actions/hcl_site_generation_build.sh + - name: Test schematic viewer + working-directory: doc/schematic_viewer + run: flutter test + + - name: Analyze schematic viewer + working-directory: doc/schematic_viewer + run: flutter analyze --fatal-infos + + - name: Build schematic viewer + run: tool/gh_actions/build_schematic_viewer.sh + deploy-documentation: name: Deploy Documentation - needs: run-checks + needs: [run-checks, run-flutter-checks] if: github.event_name == 'push' permissions: contents: write # required for "JamesIves/github-pages-deploy-action" @@ -119,32 +130,25 @@ jobs: runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} steps: - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '24' + uses: actions/checkout@v4 - name: Setup Dart uses: dart-lang/setup-dart@v1 + - name: Setup Flutter SDK + uses: flutter-actions/setup-flutter@v2 + with: + channel: stable + version: 3.47.2 + - name: Install project dependencies run: tool/gh_actions/install_dependencies.sh - - name: Install CAD Suite (includes yosys) - run: tool/gh_actions/install_opencadsuite.sh - - - name: Install D3 Schematic viewer - run: tool/gh_actions/install_d3_hwschematic.sh - - name: Generate project documentation run: tool/gh_actions/generate_documentation.sh - - name: Generate HTML for examples - run: tool/gh_actions/create_htmls.sh + - name: Generate schematic pages + run: tool/gh_actions/create_schematic_pages.sh - name: Deploy generated documentation uses: JamesIves/github-pages-deploy-action@v4 @@ -162,23 +166,35 @@ jobs: runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }} steps: - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive + uses: actions/checkout@v4 - name: Setup Flutter SDK uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.27.0 - + version: 3.47.2 + + - name: Install project dependencies + run: tool/gh_actions/install_dependencies.sh + - name: Build static site run: tool/gh_actions/hcl_site_generation_build.sh + - name: Build schematic viewer + run: tool/gh_actions/build_schematic_viewer.sh + - name: Deploy the generated site to website branch uses: JamesIves/github-pages-deploy-action@v4 with: folder: confapp/build/web target-folder: confapp branch: website - clean: false # goes in after the documentation, which does a clean \ No newline at end of file + clean: false # goes in after the documentation, which does a clean + + - name: Deploy schematic viewer to website branch + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: doc/schematic_viewer/build/web + target-folder: schematic_viewer + branch: website + clean: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 40a909c9f9..ffd86b179d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,12 @@ tmp* *.vcd .vscode/* confapp/.vscode/* +confapp/assets/rohd_src/ +/pubspec_overrides.yaml +/pubspec_overrides.yaml.disabled* +/confapp/pubspec_overrides.yaml +/confapp/pubspec_overrides.yaml.disabled* +/.confapp_dependency_sources *tracker.json *tracker.log devtools_options.yaml @@ -23,5 +29,6 @@ devtools_options.yaml # Exceptions !.vscode/extensions.json +!.vscode/tasks.json !confapp/.vscode/extensions.json !confapp/.vscode/launch.json \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 6ab774b8a1..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "doc/d3-hwschematic"] - path = doc/d3-hwschematic - url = https://github.com/Nic30/d3-hwschematic.git diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index c66455f7a3..26f1dd1195 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -6,11 +6,6 @@ }, "ignores": [ - "doc/api/", - "doc/d3-hwschematic-assets/", - "doc/d3-hwschematic-assets/elkjs", - "doc/api/d3-hwschematic-assets/", - "doc/api/d3-hwschematic-assets/elkjs", - "doc/d3-hwschematic/" - ] + "doc/api/" + ] } diff --git a/.pubignore b/.pubignore index 6c37f5f4e4..54c3af8a79 100644 --- a/.pubignore +++ b/.pubignore @@ -16,6 +16,9 @@ tmp* *.vcd .vscode/* confapp/.vscode/* +/pubspec_overrides.yaml +/pubspec_overrides.yaml.disabled* +/.confapp_dependency_sources *tracker.json *tracker.log devtools_options.yaml @@ -30,4 +33,4 @@ devtools_options.yaml # Special things for .pubignore confapp -doc/d3-hwschematic \ No newline at end of file +doc/schematic_viewer \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..99f10f9fc4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,240 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Configure ROHD Dependency", + "type": "shell", + "command": "bash tool/confapp_dev_mode.sh configure-group rohd ${input:rohdSource} && flutter pub get", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CONFAPP_PROMPT_SOURCE_VALUE": "1" + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [], + "detail": "Choose the source for the core rohd package without changing the other dependency selections." + }, + { + "label": "Configure Package Dependencies", + "type": "shell", + "command": "bash tool/confapp_dev_mode.sh configure-group package ${input:packageSource} && flutter pub get", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CONFAPP_PROMPT_SOURCE_VALUE": "1" + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [], + "detail": "Choose the shared source for rohd_hierarchy and rohd_source_navigator." + }, + { + "label": "Configure Widget Dependencies", + "type": "shell", + "command": "bash tool/confapp_dev_mode.sh configure-group widget ${input:widgetSource} && flutter pub get", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CONFAPP_PROMPT_SOURCE_VALUE": "1" + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [], + "detail": "Choose the shared source for rohd_hierarchy, rohd_devtools_widgets, and rohd_source_navigator." + }, + { + "label": "Configure Schematic Dependency", + "type": "shell", + "command": "bash tool/confapp_dev_mode.sh configure-group schematic ${input:schematicSource} && flutter pub get", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CONFAPP_PROMPT_SOURCE_VALUE": "1" + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [], + "detail": "Choose the GitHub or local rohd_schematic_viewer source without changing the ROHD selections." + }, + { + "label": "Configure All ROHD Dependency Sources", + "type": "shell", + "command": "bash tool/confapp_dev_mode.sh configure-group all ${input:allDependencySource} && flutter pub get", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "CONFAPP_PROMPT_SOURCE_VALUE": "1" + } + }, + "presentation": { + "reveal": "always", + "focus": true, + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [], + "detail": "Set every published ROHD-family package to hosted or GitHub without changing the schematic dependency." + }, + { + "label": "Generate Confapp Assets", + "type": "shell", + "command": "bash tool/generate_confapp_assets.sh", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "silent", + "panel": "shared" + } + }, + { + "label": "ROHD-HCL Confapp: Web Release", + "type": "shell", + "command": "bash tool/confapp_web_release.sh", + "options": { + "cwd": "${workspaceFolder}" + }, + "dependsOn": "Generate Confapp Assets", + "isBackground": true, + "problemMatcher": { + "owner": "confapp-web-release", + "pattern": { + "regexp": "^(CONFAPP_TASK_DIAGNOSTIC: .+)$", + "message": 1 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching lib/main\\.dart", + "endsPattern": "is being served at" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated" + }, + "detail": "Builds and serves the JavaScript release with the dependency configuration currently selected at the workspace root." + }, + { + "label": "ROHD-HCL Confapp: Web Release WASM", + "type": "shell", + "command": "bash tool/confapp_web_wasm.sh", + "options": { + "cwd": "${workspaceFolder}" + }, + "dependsOn": "Generate Confapp Assets", + "isBackground": true, + "problemMatcher": { + "owner": "confapp-web-wasm", + "pattern": { + "regexp": "^(CONFAPP_TASK_DIAGNOSTIC: .+)$", + "message": 1 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching lib/main\\.dart", + "endsPattern": "is being served at" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated" + }, + "detail": "Builds and serves the experimental WASM release on CONFAPP_WASM_WEB_PORT (default 8081)." + }, + { + "label": "ROHD-HCL Confapp: Web Debug", + "type": "shell", + "command": "bash tool/confapp_web_debug.sh", + "options": { + "cwd": "${workspaceFolder}" + }, + "dependsOn": "Generate Confapp Assets", + "isBackground": true, + "problemMatcher": { + "owner": "confapp-web", + "pattern": { + "regexp": "^(CONFAPP_TASK_DIAGNOSTIC: .+)$", + "message": 1 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^Launching lib/main\\.dart", + "endsPattern": "is being served at" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "focus": true + }, + "detail": "Runs web debug with the dependency configuration currently selected at the workspace root." + }, + { + "label": "Show Confapp Dependency Mode", + "type": "shell", + "command": "bash tool/confapp_dev_mode.sh show", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "rohdSource", + "type": "pickString", + "description": "Source for rohd", + "options": ["hosted", "git", "local"], + "default": "hosted" + }, + { + "id": "packageSource", + "type": "pickString", + "description": "Source for rohd_hierarchy and rohd_source_navigator", + "options": ["hosted", "git", "local"], + "default": "hosted" + }, + { + "id": "widgetSource", + "type": "pickString", + "description": "Source for rohd_hierarchy, rohd_devtools_widgets, and rohd_source_navigator", + "options": ["hosted", "git", "local"], + "default": "hosted" + }, + { + "id": "schematicSource", + "type": "pickString", + "description": "Source for rohd_schematic_viewer", + "options": ["hosted", "git", "local"], + "default": "hosted" + }, + { + "id": "allDependencySource", + "type": "pickString", + "description": "Source for every published ROHD-family package", + "options": ["hosted", "git", "local"], + "default": "hosted" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e0606c73..48651907eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +## Unreleased + +### Breaking changes + +- The minimum supported Dart SDK is now 3.6.0. Applications using Dart 3.0 through 3.5 must upgrade their SDK before resolving ROHD-HCL. +- Corrected AXI5 signal widths to match the configured protocol widths. Generated interfaces for affected `MMUSID`, `MMUSSID`, and `SUBSYSID` signals may have different port widths; designs that connect to those signals must use the corrected widths (). +- `ChoiceConfigKnob` now requires a stable label for every choice, including on the Dart VM, because `Type.toString()` is not stable in minified builds. The labels are used as both display text and serialized JSON values. Existing custom configurators must migrate to the `choiceLabels` construction pattern: + + ```dart + ChoiceConfigKnob( + [BinaryToOneHot, OneHotToBinary], + value: OneHotToBinary, + choiceLabels: const { + BinaryToOneHot: 'BinaryToOneHot', + OneHotToBinary: 'OneHotToBinary', + }, + ); + ``` + + Labels matching the previous unminified type names, as used by the built-in configurators, preserve compatibility with existing saved values. + +### New and enhanced components + +- Refactored AMBA support to add AXI4-Lite, AXI4-Stream, ACE, and ACE-Lite protocol variants while preserving the existing AXI APIs (). +- Added an abstract `ApbCompleter` and an `ApbCsrCompleter` implementation for connecting APB transactions to CSR blocks (). +- Added `AsyncFifo` for safely transferring data between clock domains (, ). +- Added `ReadyValidInterface`, `ReadyValidFifo`, and `Cam` capabilities in preparation for cache componentry (). +- Added the `Counter.upDown` convenience constructor for incrementing and decrementing by one (). +- Added the `RegisterFile.setData` testbench hook for directly initializing register contents (). +- Added optional early termination to `Serializer` and `Deserializer` for dynamically sized inputs (, ). +- Exposed the module `name` argument on round-robin and related arbiters (). +- Expanded CSR support with heterogeneous block sizes (), configurable asynchronous reset through `CsrTopConfig` (), and write-only fields for command, trigger, and pulse semantics (, ). + +### Configuration application and schematics + +- Migrated the configuration application and generated API-documentation schematics from the legacy D3 flow to `rohd_schematic_viewer`. The new flow renders native ROHD netlists, retains Yosys-based synthesized schematics, and redirects existing component schematic URLs to the new viewer. +- Added selectable ROHD schematic, ROHD source, generated SystemVerilog, and synthesized schematic views, including syntax highlighting, downloadable generated artifacts, configurable schematic expansion, and split-pane output. +- Migrated the configuration application to `material_ui`, including light and dark themes, and removed its legacy Flutter Material dependencies. +- Converted the root package and configuration application to a Pub workspace and added package-by-package hosted, Git, and local dependency-source selection for development. + +### Bug fixes + +- Corrected AXI5 mixin signal widths to match the protocol specification (). +- Ensured the `Serializer` counter has a valid width for single-element inputs (). +- Fixed `loadMemString` data loss across discontinuous sections whose starting addresses are not word-aligned (). +- Prevented `FifoChecker` from reading an invalid previous error value and crashing the simulator (). +- Fixed AXI4 memory-subordinate multi-beat writes so each beat is stored at its corresponding address and stale queued data cannot leak across bursts (). +- Fixed CSR frontdoor decoding for non-power-of-two-aligned block base addresses and corrected the minimum required address width calculation (, ). +- Implemented the documented write-ones-clear CSR behavior, with software writes clearing bits and backdoor hardware writes setting them (, ). +- Enabled independent AXI4 subordinate read and write response-delay callbacks and corrected their timing behavior (, ). + +### Tooling and compatibility + +- Updated analysis settings and added `pana` validation for pub.dev scoring (, ). +- Updated source and analysis configuration for newer Dart lints (, ). +- Updated GitHub workflows to Node.js 24 (). +- Updated compatibility to ROHD 0.6.11 and set the Dart SDK range to `^3.6.0`. + ## 0.2.1 - New Components: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a74c028480..82a184113c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,10 @@ The [ROHD Forum](https://intel.github.io/rohd-website/forum/rohd-forum/) is a pe ### Requirements -You must have [Dart](https://dart.dev/) installed on your system to use ROHD and ROHD-HCL. You can find detailed instructions for how to install Dart here: +You must have [Flutter](https://docs.flutter.dev/get-started/install) installed +to develop ROHD-HCL. Flutter provides the Dart SDK and is required because the +pub workspace contains both the Dart `rohd_hcl` package and the Flutter +`confapp` package. ### Setup Recommendations @@ -67,6 +70,7 @@ The below button will allow you to create a GitHub Codespace with ROHD-HCL alrea To run the component Configurator, especially useful for visualizing the RTL output of your component using different parameters: ```shell +bash tool/generate_confapp_assets.sh cd confapp flutter run --profile -d web-server --web-hostname=0.0.0.0 --web-port=3000 ``` @@ -78,10 +82,30 @@ Once requirements are installed, you can clone and run the test suite: ```shell git clone https://github.com/intel/rohd-hcl.git cd rohd-hcl -dart pub get +flutter pub get dart test ``` +The root `pubspec.yaml` defines a pub workspace containing `confapp`, so +dependency resolution is shared and should be run from the repository root. +The app includes the root `analysis_options.yaml`, keeping both packages on the +same analyzer policy while retaining app-specific generated-file exclusions. +Open `rohd-hcl-multipackage.code-workspace` in VS Code to load both workspace +packages as top-level folders. + +See [confapp/DEVELOPER.md](confapp/DEVELOPER.md) for the local checkout +assumptions, dependency-source selection, and generated override workflow. + +To validate both workspace packages: + +```shell +bash tool/generate_confapp_assets.sh +dart analyze --fatal-infos +dart test +(cd confapp && flutter analyze --fatal-infos) +(cd confapp && flutter test --platform chrome) +``` + ## How to Contribute ### Reporting Vulnerabilities @@ -131,7 +155,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-2024 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // example.dart @@ -152,7 +176,13 @@ To add a new component to the library, you can follow the steps below, referenci 1. Update the component registry in `lib/src/component_config/components/component_registry.dart`. 1. Add documentation to `doc/components/name.md`. -If you would like to generate and view schematics locally, you'll need to have access to some tools. You can either use the dev container (via Codespaces or VS Code) or install the software from `tool/gh_actions/install_opencadsuite.sh` and `tool/gh_actions/install_d3_hwschematic.sh`. Then you can just run `tool/gh_actions/create_htmls.sh`, which will generate component schematic HTML documents in the `build/` directory. +Run `dart run gen/generate.dart` to generate native ROHD netlists in +`build/*.rohd.json`. The ROHD schematic viewer loads these netlists directly; +there is no HTML conversion step. The configuration app also offers a +synthesized schematic by running Yosys in a web worker. For equivalent +command-line conversion, install OSS CAD Suite with +`tool/gh_actions/install_opencadsuite.sh` and run +`tool/converters/verilog_json.sh `. The `confapp/` directory contains the source code for the Flutter configuration app, if you would like to run that locally. diff --git a/analysis_options.yaml b/analysis_options.yaml index 866249dfef..c4f669ee58 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,7 +7,14 @@ analyzer: strict-inference: true strict-raw-types: true exclude: - - confapp/** + - doc/schematic_viewer/** + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** # keep up to date, matching https://dart.dev/tools/linter-rules/all # some lints are not yet available, so disabled and marked with [not currently recognized] @@ -20,6 +27,7 @@ linter: - always_use_package_imports - annotate_overrides - annotate_redeclares + - async_return_with_no_await # - avoid_annotating_with_dynamic - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses @@ -81,18 +89,21 @@ linter: - directives_ordering - discarded_futures - do_not_use_environment - # - document_ignores + - document_ignores - empty_catches - empty_constructor_bodies + - empty_container_bodies - empty_statements - eol_at_end_of_file - exhaustive_cases - file_names - flutter_style_todos + # - future_sync_value - not valid yet, but should be enabled when it is - hash_and_equals - implementation_imports - implicit_call_tearoffs - implicit_reopen + - initialize_in_field_declaration - invalid_case_patterns - invalid_runtime_check_with_js_interop_types - join_return_with_assignment @@ -104,16 +115,19 @@ linter: - lines_longer_than_80_chars - literal_only_boolean_expressions - matching_super_parameters + - migrate_design_widgets - missing_code_block_language_in_doc_comment - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - no_default_cases - no_duplicate_case_values + - no_dynamic_casts - no_leading_underscores_for_library_prefixes - no_leading_underscores_for_local_identifiers - no_literal_bool_comparisons - no_logic_in_create_state - - no_runtimeType_toString + - no_raw_types + - no_runtimetype_tostring - no_self_assignments - no_wildcard_variable_uses - non_constant_identifier_names @@ -128,7 +142,9 @@ linter: - overridden_fields - package_names - package_prefixed_library_names - # - parameter_assignments # common pattern in ROHD modules + # parameter_assignments - disabled; ROHD idiomatically reassigns + # constructor parameters via addInput/addOutput. + # - parameter_assignments - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - prefer_asserts_with_message @@ -171,8 +187,11 @@ linter: - provide_deprecation_message - public_member_api_docs - recursive_getters + - remove_deprecations_in_breaking_versions # - require_trailing_commas - secure_pubspec_urls + - simple_directive_paths + - simplify_variable_pattern - sized_box_for_whitespace - sized_box_shrink_expand - slash_for_doc_comments @@ -184,7 +203,7 @@ linter: # - specify_nonobvious_local_variable_types # - specify_nonobvious_property_types - strict_top_level_inference - # - switch_on_type + - switch_on_type - test_types_in_equals - throw_in_finally - tighten_type_of_initializing_formals @@ -193,11 +212,12 @@ linter: - type_literal_in_constant_pattern - unawaited_futures - unintended_html_in_doc_comment - # - unnecessary_async + - unnecessary_async - unnecessary_await_in_return - unnecessary_brace_in_string_interps - unnecessary_breaks - unnecessary_const + - unnecessary_const_in_enum_constructor - unnecessary_constructor_name # - unnecessary_final - unnecessary_getters_setters @@ -214,19 +234,23 @@ linter: - unnecessary_nullable_for_final_variable_declarations - unnecessary_overrides - unnecessary_parenthesis + - unnecessary_primary_constructor_body - unnecessary_raw_strings # - unnecessary_statements - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this + # - unnecessary_this_alias - not recognized yet, but should be enabled when it is - unnecessary_to_list_in_spreads - # - unnecessary_unawaited + - unnecessary_type_name_in_constructor + - unnecessary_unawaited - unnecessary_underscores # - unreachable_from_main - unrelated_type_equality_checks - # - unsafe_variance + - unsafe_variance - use_build_context_synchronously - use_colored_box + - use_declaring_parameters - use_decorated_box - use_enums - use_full_hex_values_for_flutter_colors @@ -247,4 +271,5 @@ linter: - use_to_and_as_if_applicable - use_truncating_division - valid_regexps + - var_with_no_type_annotation - void_checks \ No newline at end of file diff --git a/confapp/DEVELOPER.md b/confapp/DEVELOPER.md new file mode 100644 index 0000000000..815256dbf0 --- /dev/null +++ b/confapp/DEVELOPER.md @@ -0,0 +1,124 @@ +# ROHD-HCL Configuration App Development + +## Workspace + +The root `rohd_hcl` package and `confapp` form one Pub workspace. Resolve +dependencies from the repository root: + +```bash +flutter pub get +``` + +The workspace has one root `pubspec.lock` and +`.dart_tool/package_config.json`. Overrides are stored beside the manifest +that owns the direct dependency. + +Each workspace member still owns a `pubspec.yaml` and declares only its direct +dependencies. The root `rohd_hcl` package depends directly on `rohd`, while +the Flutter, schematic-viewer, hierarchy, widget, and source-navigator +dependencies belong to `confapp/pubspec.yaml`. Pub resolves both manifests +together. The helper writes the shared `rohd` override at the workspace root +and confapp-only overrides in `confapp/pubspec_overrides.yaml`. + +## Dependency sources + +The checked-in manifests use hosted release constraints. The helper generates +ignored override files beside the owning manifests when a dependency source +differs from its manifest entry: + +```bash +# Set the ROHD-family packages and viewer independently. +bash tool/confapp_dev_mode.sh configure-group all hosted +bash tool/confapp_dev_mode.sh configure-group schematic hosted +``` + +The **Configure ROHD Dependency**, **Configure Package Dependencies**, +**Configure Widget Dependencies**, and **Configure Schematic Dependency** tasks +accept `hosted`, `git`, or `local` and preserve the other source selections. + +The group commands write an ignored `.confapp_dependency_sources` state file. +The generated override contains only Git and Local entries; selecting Hosted +removes that package from the override so Pub uses its `pubspec.yaml` +constraint. Prefer one source for the ROHD companion packages unless +deliberately validating a mixed graph. + +Before a Configure task replaces or disables an existing override, it moves +the file to a sibling `pubspec_overrides.yaml.disabled`. If that backup already +exists, the task uses `.disabled.1`, `.disabled.2`, and so on. This applies to +both generated and manually maintained override files. + +The common local shortcuts are: + +```bash +bash tool/confapp_dev_mode.sh local-rohd +bash tool/confapp_dev_mode.sh local-viewer +bash tool/confapp_dev_mode.sh local-all +``` + +The defaults are `~/max/rohd` and +`~/true-release/rohd-schematic-viewer`; override them with `ROHD_LOCAL_PATH` +and `SCHEMATIC_VIEWER_LOCAL_PATH`. + +Use **Configure All ROHD Dependency Sources** to set the four published +ROHD-family packages at once. The web build and run tasks use the selected +source configuration without changing it. + +Each **Configure** task first collects the source in VS Code Quick Input. Git +and Local selections then open an editable terminal prompt. The prompt is +prefilled from that repository area's current saved configuration. Without +saved state, Git defaults to `github.com/intel/rohd:main` or +`github.com/intel/rohd-schematic-viewer:main`, and Local defaults to +`~/max/rohd` or `~/true-release/rohd-schematic-viewer`. For Git, enter the +repository and ref together, separated by the final colon. GitHub shorthand is +normalized to an HTTPS URL. Hosted does not open a second prompt. The helper +persists the separate ROHD and schematic-viewer Git repositories, refs, and +local paths so configuring one dependency group does not reset another. + +## Running Confapp + +```bash +bash tool/generate_confapp_assets.sh +flutter pub get +cd confapp +flutter run --profile -d web-server --web-hostname=0.0.0.0 --web-port=3000 +``` + +Use **ROHD-HCL Confapp: Web Debug** for the current source configuration. If +port `8080` is in use, set `CONFAPP_WEB_PORT`; do not stop another developer's +server merely to reuse its port. + +Use **ROHD-HCL Confapp: Web Release** to build and serve the supported +JavaScript release target. It uses the same port setting. + +Use **ROHD-HCL Confapp: Web Release WASM** to build and serve the experimental +WASM target on port `8081`. Set `CONFAPP_WASM_WEB_PORT` to override that port. + +## Browser validation + +Open the forwarded web-server URL in VS Code's integrated browser and enable +Flutter accessibility semantics before inspecting controls. Prefer +accessible-name and role locators over canvas coordinates. Use accessibility +snapshots for state and screenshots for visual layout. + +Keep browser-independent behavior in normal VM tests. The JavaScript web build +is the integration gate: + +```bash +cd confapp +flutter test test/hcl/view/hcl_page_test.dart +flutter build web --debug --no-pub +``` + +Confapp's browser implementation uses `dart:js_interop` and `package:web`. +JavaScript remains the supported target; the separately documented Wasm +launcher is experimental. + +## Validation + +```bash +bash -n tool/confapp_dev_mode.sh tool/confapp_web_debug.sh \ + tool/confapp_web_release.sh tool/confapp_web_wasm.sh +python3 -m json.tool .vscode/tasks.json >/dev/null +flutter pub get +dart analyze --fatal-infos +``` diff --git a/confapp/README.md b/confapp/README.md index b38ef2dfd6..d83c32186b 100644 --- a/confapp/README.md +++ b/confapp/README.md @@ -1,14 +1,82 @@ -# ROHD-HCL Flutter Configuration App +# ROHD-HCL Configuration App -This is a web app that allows RTL generation (SystemVerilog) based on the specific configuration. +The Flutter configuration app exposes selected ROHD-HCL components through +editable configuration knobs and presents generated ROHD schematics, ROHD +source, SystemVerilog, and synthesized schematics. -For local testing: +For workspace setup, dependency sources, running the app, and browser +validation, see [DEVELOPER.md](DEVELOPER.md). -```shell -cd confapp -flutter run --profile -d web-server --web-hostname=0.0.0.0 --web-port=3000 +## Adding a component to Confapp + +Adding a reusable library component and making it configurable are related but +separate steps. The component must first be implemented, exported, documented, +and tested in the root package. Then complete the following Confapp work. + +### 1. Create a configurator + +Add a `Configurator` implementation under +`lib/src/component_config/components/`. It supplies: + +- the user-facing `name`; +- a `Map>` describing editable settings; and +- `createModule()`, which constructs the configured ROHD module. + +Follow an existing configurator for the relevant component family. Keep the +configurator's default settings buildable and useful for schematic generation. + +### 2. Register it + +Add an instance to +[`componentRegistry`](../lib/src/component_config/components/component_registry.dart). +The registry supplies the Confapp sidebar through +[`ComponentCubit`](lib/hcl/cubit/component_cubit.dart) and is also consumed by +[`gen/generate.dart`](../gen/generate.dart) for documentation netlists. + +### 3. Add ROHD source mapping + +Map every concrete module runtime type that the configurator can create in +[`moduleSourceAssets`](lib/hcl/module_source_assets.dart). This enables the +ROHD Source view. Source cross-probing is also enabled when a trace-capable +future ROHD release embeds `rohd.src_trace` data in the generated netlist. +The current ROHD 0.6.11 dependency does not emit that data. Point to the source +path beneath `lib/src/`, using the `rohd_src/` asset prefix. + +Run the source asset generator from the repository root after source changes: + +```bash +bash tool/generate_confapp_assets.sh ``` +It mirrors `lib/src/` into the ignored `confapp/assets/rohd_src/` directory and +updates the generated asset-directory list in `confapp/pubspec.yaml`. + +### 4. Verify all generated views + +Confapp builds the selected module and generates: + +- native ROHD netlist JSON for `rohd_schematic_viewer`; +- generated SystemVerilog; +- a Yosys synthesized schematic when enabled; and +- embedded source-navigation data when supplied by a trace-enabled ROHD + netlist. + +Exercise the default configurator values and representative knob values in the +running app. Confirm the ROHD Source view reaches the mapped asset and that +schematic generation does not rely on external signals crossing a module +boundary. When testing with a trace-enabled future ROHD dependency, also +confirm source navigation resolves its embedded trace locations. + +### 5. Add regression coverage + +At minimum: + +- add functional and nested-composition tests for the component in `test/`; +- update tests for changed configurator defaults or knobs; and +- run + [`module_source_assets_test.dart`](test/hcl/module_source_assets_test.dart) + to verify registry and bundled-source coverage. + ## Widget Tree ```mermaid @@ -18,5 +86,5 @@ flowchart TD; ---------------- -Copyright (C) 2023-2024 Intel Corporation +Copyright (C) 2023-2026 Intel Corporation SPDX-License-Identifier: BSD-3-Clause diff --git a/confapp/analysis_options.yaml b/confapp/analysis_options.yaml index 064a573274..f408b9b967 100644 --- a/confapp/analysis_options.yaml +++ b/confapp/analysis_options.yaml @@ -1,6 +1,12 @@ +analyzer: + exclude: + - assets/rohd_src/** + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** -include: package:flutter_lints/flutter.yaml - -linter: - rules: - +include: ../analysis_options.yaml diff --git a/confapp/assets/2.0x/systemc_icon.png b/confapp/assets/2.0x/systemc_icon.png new file mode 100644 index 0000000000..bbe6ec781c Binary files /dev/null and b/confapp/assets/2.0x/systemc_icon.png differ diff --git a/confapp/assets/2.0x/yosys_icon.png b/confapp/assets/2.0x/yosys_icon.png new file mode 100644 index 0000000000..462ef2817e Binary files /dev/null and b/confapp/assets/2.0x/yosys_icon.png differ diff --git a/confapp/assets/3.0x/systemc_icon.png b/confapp/assets/3.0x/systemc_icon.png new file mode 100644 index 0000000000..120699e0f2 Binary files /dev/null and b/confapp/assets/3.0x/systemc_icon.png differ diff --git a/confapp/assets/3.0x/yosys_icon.png b/confapp/assets/3.0x/yosys_icon.png new file mode 100644 index 0000000000..94b5145119 Binary files /dev/null and b/confapp/assets/3.0x/yosys_icon.png differ diff --git a/confapp/assets/rohd_icon.png b/confapp/assets/rohd_icon.png new file mode 100644 index 0000000000..a8f1faecdd Binary files /dev/null and b/confapp/assets/rohd_icon.png differ diff --git a/confapp/assets/systemc_icon.png b/confapp/assets/systemc_icon.png new file mode 100644 index 0000000000..f5173fea02 Binary files /dev/null and b/confapp/assets/systemc_icon.png differ diff --git a/confapp/assets/systemverilog_icon.png b/confapp/assets/systemverilog_icon.png new file mode 100644 index 0000000000..ba64689399 Binary files /dev/null and b/confapp/assets/systemverilog_icon.png differ diff --git a/confapp/assets/third_party_notices.md b/confapp/assets/third_party_notices.md new file mode 100644 index 0000000000..5affffcb0f --- /dev/null +++ b/confapp/assets/third_party_notices.md @@ -0,0 +1,19 @@ +# Third-Party Notices + +## Eclipse Layout Kernel JavaScript + +Confapp bundles an unmodified copy of `elkjs` 0.8.2 +`lib/elk.bundled.js` through the `rohd_schematic_viewer` Flutter package. The +asset is available in distributed web builds at +`assets/packages/rohd_schematic_viewer/assets/js/elk.bundled.js`. + +The ELK JavaScript engine is licensed under the Eclipse Public License 2.0 +(EPL-2.0). It also includes a Google LLC web-worker shim licensed under the +Apache License 2.0. The complete license texts, copyright notices, +source-availability statement, and checksum are distributed with Confapp at +`assets/packages/rohd_schematic_viewer/assets/licenses/elkjs_LICENSES.txt`. + +Corresponding source for the unmodified `elkjs` 0.8.2 asset is available from: + +- +- diff --git a/confapp/assets/yosys_icon.png b/confapp/assets/yosys_icon.png new file mode 100644 index 0000000000..c4f848f005 Binary files /dev/null and b/confapp/assets/yosys_icon.png differ diff --git a/confapp/lib/app.dart b/confapp/lib/app.dart index a24fefc92e..58a086b450 100644 --- a/confapp/lib/app.dart +++ b/confapp/lib/app.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // app.dart @@ -6,12 +6,14 @@ // // 2023 December -import 'package:flutter/material.dart'; import 'package:confapp/hcl/hcl.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; +/// The ROHD-HCL Configuration App root widget. class HCLApp extends MaterialApp { - HCLApp({super.key, required List components}) + /// Creates the app with the selectable component [components]. + HCLApp({required List components, super.key}) : super( home: HCLPage( components: components, diff --git a/confapp/lib/hcl/cross_probe.dart b/confapp/lib/hcl/cross_probe.dart new file mode 100644 index 0000000000..b5d6fa6fc1 --- /dev/null +++ b/confapp/lib/hcl/cross_probe.dart @@ -0,0 +1,304 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// cross_probe.dart +// Cross-probing utilities that consume FLC JSON from SourceTracer +// and map symbols to locations in both the generated SV and ROHD Dart source. +// +// 2026 April 22 +// Author: Desmond Kirkpatrick + +import 'dart:math'; + +/// A location within a text document (1-based line and column). +class SourceLocation { + /// The file path (relative to package root) this location refers to. + final String? file; + + /// The one-based line number. + final int line; + + /// The one-based column number. + final int column; + + /// Length of the symbol name (useful for highlighting). + final int length; + + /// Creates a source location. + const SourceLocation({ + required this.line, + required this.column, + this.file, + this.length = 0, + }); + + @override + String toString() => + '${file != null ? "$file:" : ""}$line:$column (len=$length)'; +} + +/// Cross-probing data built from the FLC JSON produced by +/// `SourceTracer.traceJsonForHierarchy()`. +/// +/// Provides lookups from signal/instance names to their source locations +/// in both the generated SystemVerilog and the original ROHD Dart source. +class CrossProbe { + /// Signal/instance name → SV source location. + final Map svLocations; + + /// Signal/instance name → ROHD Dart source location (from FLC `"src"` + /// fields, filtered to the file matching the loaded asset). + final Map rohdLocations; + + /// Symbols that appear in **both** SV and ROHD source. + late final Set commonSymbols; + + CrossProbe._({required this.svLocations, required this.rohdLocations}) { + commonSymbols = + svLocations.keys.toSet().intersection(rohdLocations.keys.toSet()); + } + + /// Build a [CrossProbe] from FLC JSON (as produced by + /// `SourceTracer.traceJsonForHierarchy()`), the generated SV text, + /// and the loaded ROHD Dart source text. + /// + /// [flcJson] is the decoded JSON map with `"version"`, `"files"`, + /// and `"modules"` keys. + /// + /// [availableAssets] is the set of asset paths that are loadable in the + /// confapp (e.g. `{"rohd_src/summation/counter.dart", ...}`). + /// FLC file references are normalized and matched against this set. + /// Each ROHD [SourceLocation] will have its [SourceLocation.file] set + /// to the matching asset path so the UI can open the correct tab. + /// + factory CrossProbe.fromFlcJson({ + required Map flcJson, + required Set availableAssets, + }) { + final files = (flcJson['files'] as List).cast(); + final modules = + flcJson['modules'] as Map? ?? {}; + + // Build a mapping from FLC file index → asset path for every + // rohd-hcl source file that we have as a loadable asset. + // FLC paths look like `.dart_tool/../lib/src/summation/counter.dart` + // which normalizes to `rohd_src/summation/counter.dart`. + final fileIndexToAsset = {}; + for (var i = 0; i < files.length; i++) { + final norm = files[i].replaceAll('.dart_tool/../', ''); + String? assetPath; + if (norm.startsWith('lib/src/')) { + assetPath = 'rohd_src/${norm.substring('lib/src/'.length)}'; + } else if (norm.startsWith('package:rohd_hcl/src/')) { + // DDC web traces produce raw package: URIs. + assetPath = + 'rohd_src/${norm.substring('package:rohd_hcl/src/'.length)}'; + } + if (assetPath != null && availableAssets.contains(assetPath)) { + fileIndexToAsset[i] = assetPath; + } + } + + final svLocs = {}; + final rohdLocs = {}; + + // Walk all modules in the FLC JSON and extract locations. + // SV locations come exclusively from FLC "sv" fields — no regex + // fallback — so missing entries surface as debugging signals. + for (final modEntry in modules.values) { + final modMap = modEntry as Map; + _extractLocations( + modMap['signals'] as Map?, + files, + fileIndexToAsset, + rohdLocs, + svLocs, + ); + _extractLocations( + modMap['instances'] as Map?, + files, + fileIndexToAsset, + rohdLocs, + svLocs, + ); + } + + return CrossProbe._(svLocations: svLocs, rohdLocations: rohdLocs); + } + + /// Fallback builder that scans SV and ROHD text directly (no FLC JSON). + factory CrossProbe.build({ + required String svText, + required String rohdText, + }) { + final svLocs = _parseSvSymbols(svText); + final rohdLocs = _scanIdentifiers(rohdText, svLocs.keys.toSet()); + return CrossProbe._(svLocations: svLocs, rohdLocations: rohdLocs); + } + + /// Pick a random symbol that exists in both sources. + String? randomCommonSymbol([Random? rng]) { + if (commonSymbols.isEmpty) { + return null; + } + final list = commonSymbols.toList(); + return list[(rng ?? Random()).nextInt(list.length)]; + } + + // --------------------------------------------------------------------------- + // FLC JSON extraction + // --------------------------------------------------------------------------- + + /// Extracts ROHD source locations from a `"signals"` or `"instances"` map + /// in the FLC JSON. + /// + /// Each entry can be either: + /// - v2 enriched: `{ "sv": "42", "src": ["0:55:10", ...] }` + /// - v1 plain: `["0:55:10", ...]` + /// + /// [primaryAsset] (if given) is the primary source file for the current + /// module — frames from this file are preferred over deeper frames. + static void _extractLocations( + Map? section, + List files, + Map fileIndexToAsset, + Map rohdLocs, + Map svLocs, { + String? primaryAsset, + }) { + if (section == null) { + return; + } + + for (final entry in section.entries) { + final name = entry.key; + final value = entry.value; + + List srcFrames; + String? svLine; + + if (value is Map) { + srcFrames = (value['src'] as List?) ?? []; + svLine = value['sv']?.toString(); + } else if (value is List) { + srcFrames = value; + } else { + continue; + } + + // If the FLC provides an SV line, use it (overrides regex-based). + if (svLine != null) { + final svParts = svLine.split(':'); + final svLineNum = int.tryParse(svParts[0]); + final svCol = svParts.length > 1 ? (int.tryParse(svParts[1]) ?? 1) : 1; + if (svLineNum != null) { + svLocs[name] = SourceLocation( + line: svLineNum, + column: svCol, + length: name.length, + ); + } + } + + // Find the best FLC frame: prefer the first frame in the primary + // asset (the module's own source), falling back to the first frame + // in any available asset. + if (!rohdLocs.containsKey(name)) { + SourceLocation? primaryLoc; + SourceLocation? fallbackLoc; + for (final frame in srcFrames) { + final parts = frame.toString().split(':'); + if (parts.length < 2) { + continue; + } + final fileIdx = int.tryParse(parts[0]); + if (fileIdx == null) { + continue; + } + final assetPath = fileIndexToAsset[fileIdx]; + if (assetPath == null) { + continue; + } + final line = int.tryParse(parts[1]) ?? 1; + final col = parts.length > 2 ? (int.tryParse(parts[2]) ?? 1) : 1; + final loc = SourceLocation( + file: assetPath, + line: line, + column: col, + length: name.length, + ); + if (primaryAsset != null && assetPath == primaryAsset) { + // Take the first (deepest) frame in the primary file — that's + // the actual instantiation/creation site. + primaryLoc ??= loc; + } + fallbackLoc ??= loc; + } + if (primaryLoc != null) { + rohdLocs[name] = primaryLoc; + } else if (fallbackLoc != null) { + rohdLocs[name] = fallbackLoc; + } + } + } + } + + // --------------------------------------------------------------------------- + // SV text parsing (fallback / supplement) + // --------------------------------------------------------------------------- + + static final _svDeclRe = RegExp( + r'(?:input|output|inout)?\s*logic\s*(?:\[[^\]]*\])?\s*(\w+)|' + r'assign\s+(\w+)', + ); + + static Map _parseSvSymbols(String text) { + final map = {}; + final lines = text.split('\n'); + for (var i = 0; i < lines.length; i++) { + for (final m in _svDeclRe.allMatches(lines[i])) { + final name = m.group(1) ?? m.group(2); + if (name == null || map.containsKey(name)) { + continue; + } + final col = lines[i].indexOf(name); + map[name] = SourceLocation( + line: i + 1, + column: col + 1, + length: name.length, + ); + } + } + return map; + } + + // --------------------------------------------------------------------------- + // ROHD source scanning (fallback when no FLC JSON) + // --------------------------------------------------------------------------- + + static Map _scanIdentifiers( + String text, Set targets) { + if (targets.isEmpty) { + return {}; + } + final map = {}; + final lines = text.split('\n'); + final pattern = RegExp( + r'\b(' + targets.map(RegExp.escape).join('|') + r')\b', + ); + for (var i = 0; i < lines.length; i++) { + for (final m in pattern.allMatches(lines[i])) { + final name = m.group(1)!; + if (map.containsKey(name)) { + continue; + } + map[name] = SourceLocation( + line: i + 1, + column: m.start + 1, + length: name.length, + ); + } + } + return map; + } +} diff --git a/confapp/lib/hcl/cubit/component_cubit.dart b/confapp/lib/hcl/cubit/component_cubit.dart index f92d53ea56..458f2206f9 100644 --- a/confapp/lib/hcl/cubit/component_cubit.dart +++ b/confapp/lib/hcl/cubit/component_cubit.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // component_cubit.dart @@ -11,14 +11,18 @@ import 'package:rohd_hcl/rohd_hcl.dart'; /// Controls the selected component. class ComponentCubit extends Cubit { + /// The currently selected component configuration. Configurator selectedComponent; + /// The available component configurations. final List components; + /// Creates a controller for selecting from [components]. ComponentCubit(this.components) : selectedComponent = components.first, super(components.first); + /// Selects and emits [selectedComponent]. void setSelectedComponent(Configurator selectedComponent) { this.selectedComponent = selectedComponent; emit(selectedComponent); diff --git a/confapp/lib/hcl/cubit/system_verilog_cubit.dart b/confapp/lib/hcl/cubit/system_verilog_cubit.dart index cf6c0eae72..4877fb9fa5 100644 --- a/confapp/lib/hcl/cubit/system_verilog_cubit.dart +++ b/confapp/lib/hcl/cubit/system_verilog_cubit.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // system_verilog_cubit.dart @@ -8,55 +8,97 @@ import 'package:bloc/bloc.dart'; -enum GenerationState { initial, loading, done } +/// The state of SystemVerilog and SystemC generation. +enum GenerationState { + /// No generation has been requested. + initial, + /// Generation is in progress. + loading, + + /// Generation completed successfully. + done, +} + +/// The generated SystemVerilog, SystemC, and their display metadata. class SystemVerilogCubitState { + /// The generated SystemVerilog source. final String systemVerilog; + + /// The generated SystemC source. + final String systemC; + + /// The current generation state. final GenerationState generationState; + + /// The display name of the generated component. final String name; + + /// The generated module definition name. final String moduleName; - const SystemVerilogCubitState( - {required this.systemVerilog, - required this.generationState, - required this.name, - required this.moduleName}); + /// Creates a generated-source display state. + const SystemVerilogCubitState({ + required this.systemVerilog, + required this.systemC, + required this.generationState, + required this.name, + required this.moduleName, + }); + + /// Creates a state indicating that generation is in progress. const SystemVerilogCubitState.loading() : this( systemVerilog: 'Loading...', + systemC: 'Loading...', generationState: GenerationState.loading, name: 'loading', moduleName: ''); + + /// Creates a state containing completed generated output. const SystemVerilogCubitState.done( - String systemVerilog, String name, String moduleName) + String systemVerilog, String systemC, String name, String moduleName) : this( systemVerilog: systemVerilog, + systemC: systemC, generationState: GenerationState.done, name: name, moduleName: moduleName); + + /// Creates the initial state shown before generation. const SystemVerilogCubitState.initial() : this( systemVerilog: 'Click "Generate RTL"!', + systemC: 'Click "Generate" to see the generated SystemC', generationState: GenerationState.initial, name: 'init', moduleName: ''); } -/// Controls the generated SystemVerilog to display +/// Controls the generated SystemVerilog and SystemC displayed by the app. class SystemVerilogCubit extends Cubit { + /// Creates a controller initialized with placeholder content. SystemVerilogCubit() : super(const SystemVerilogCubitState.loading()) { initializeData(); } - void initializeData() async { + /// Displays the initial generation prompt. + void initializeData() { emit(const SystemVerilogCubitState.initial()); } + /// Displays the loading state. void setLoading() { emit(const SystemVerilogCubitState.loading()); } - void setRTL(String rtl, String name, String moduleName) { - emit(SystemVerilogCubitState.done(rtl, name, moduleName)); + /// Displays generated [rtl] and [systemC] with component metadata. + void setRTL( + String rtl, + String systemC, + String name, + String moduleName, + ) { + emit(SystemVerilogCubitState.done(rtl, systemC, name, moduleName)); } } diff --git a/confapp/lib/hcl/cubit/theme_cubit.dart b/confapp/lib/hcl/cubit/theme_cubit.dart new file mode 100644 index 0000000000..9f9fe9d7db --- /dev/null +++ b/confapp/lib/hcl/cubit/theme_cubit.dart @@ -0,0 +1,25 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// theme_cubit.dart +// Theme mode state management for the configuration app. +// +// 2026 April 22 +// Author: Desmond A. Kirkpatrick + +import 'package:bloc/bloc.dart'; +import 'package:material_ui/material_ui.dart'; + +/// Controls the application's light and dark theme selection. +class ThemeCubit extends Cubit { + /// Creates a theme controller initially using the light theme. + ThemeCubit() : super(ThemeMode.light); + + /// Switches between the light and dark themes. + void toggleTheme() { + emit(state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark); + } + + /// Whether the dark theme is currently selected. + bool get isDark => state == ThemeMode.dark; +} diff --git a/confapp/lib/hcl/module_source_assets.dart b/confapp/lib/hcl/module_source_assets.dart new file mode 100644 index 0000000000..ee886fa58a --- /dev/null +++ b/confapp/lib/hcl/module_source_assets.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_source_assets.dart +// Maps configurable modules to bundled ROHD source assets. +// +// 2026 August 29 +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// Maps configurable module runtime types to bundled ROHD source assets. +const moduleSourceAssets = { + 'BinaryToOneHot': 'rohd_src/encodings/binary_to_one_hot.dart', + 'BinaryToGrayConverter': 'rohd_src/binary_gray.dart', + 'BitonicSort': 'rohd_src/sort.dart', + 'CarrySaveMultiplier': 'rohd_src/arithmetic/carry_save_mutiplier.dart', + 'CarrySelectCompoundAdder': 'rohd_src/arithmetic/compound_adder.dart', + 'CaseOneHotToBinary': 'rohd_src/encodings/case_one_hot_to_binary.dart', + 'CompressionTreeMultiplier': 'rohd_src/arithmetic/multiplier.dart', + 'Counter': 'rohd_src/summation/counter.dart', + 'Deserializer': 'rohd_src/serialization/serialization.dart', + 'EdgeDetector': 'rohd_src/edge_detector.dart', + 'Extrema': 'rohd_src/extrema.dart', + 'Fifo': 'rohd_src/memory/fifo.dart', + 'Find': 'rohd_src/find.dart', + 'FixedPointSqrt': 'rohd_src/arithmetic/fixed_sqrt.dart', + 'FixedToFloat': 'rohd_src/arithmetic/fixed_to_float.dart', + 'FloatToFixed': 'rohd_src/arithmetic/float_to_fixed.dart', + 'FloatingPointAdderDualPath': + 'rohd_src/arithmetic/floating_point/floating_point_adder_dualpath.dart', + 'FloatingPointAdderSinglePath': + 'rohd_src/arithmetic/floating_point/floating_point_adder_singlepath.dart', + 'FloatingPointMultiplierSimple': + 'rohd_src/arithmetic/floating_point/floating_point_multiplier_simple.dart', + 'FloatingPointSqrtSimple': + 'rohd_src/arithmetic/floating_point/floating_point_sqrt_simple.dart', + 'GatedCounter': 'rohd_src/summation/gated_counter.dart', + 'GrayToBinaryConverter': 'rohd_src/binary_gray.dart', + 'HammingEccReceiver': 'rohd_src/error_checking/ecc.dart', + 'LeadingDigitAnticipate': 'rohd_src/arithmetic/leading_digit_anticipate.dart', + 'LeadingZeroAnticipate': 'rohd_src/arithmetic/leading_digit_anticipate.dart', + 'LeadingZeroAnticipateCarry': + 'rohd_src/arithmetic/leading_digit_anticipate.dart', + 'MaskRoundRobinArbiter': 'rohd_src/arbiters/mask_round_robin_arbiter.dart', + 'NativeMultiplier': 'rohd_src/arithmetic/multiplier.dart', + 'ParallelPrefixAdder': 'rohd_src/arithmetic/parallel_prefix_operations.dart', + 'PriorityArbiter': 'rohd_src/arbiters/priority_arbiter.dart', + 'RegisterFile': 'rohd_src/memory/register_file.dart', + 'RippleCarryAdder': 'rohd_src/arithmetic/ripple_carry_adder.dart', + 'RotateLeft': 'rohd_src/rotate.dart', + 'RotateRight': 'rohd_src/rotate.dart', + 'RotateRoundRobinArbiter': + 'rohd_src/arbiters/rotate_round_robin_arbiter.dart', + 'Serializer': 'rohd_src/serialization/serialization.dart', + 'Sum': 'rohd_src/summation/sum.dart', + 'TreeOneHotToBinary': 'rohd_src/encodings/tree_one_hot_to_binary.dart', +}; + +/// Returns the bundled ROHD source asset for [module], if one is indexed. +String? moduleSourceAsset(Module module) => switch (module) { + BinaryToOneHot() => moduleSourceAssets['BinaryToOneHot'], + BinaryToGrayConverter() => moduleSourceAssets['BinaryToGrayConverter'], + BitonicSort() => moduleSourceAssets['BitonicSort'], + CarrySaveMultiplier() => moduleSourceAssets['CarrySaveMultiplier'], + CarrySelectCompoundAdder() => + moduleSourceAssets['CarrySelectCompoundAdder'], + CaseOneHotToBinary() => moduleSourceAssets['CaseOneHotToBinary'], + CompressionTreeMultiplier() => + moduleSourceAssets['CompressionTreeMultiplier'], + GatedCounter() => moduleSourceAssets['GatedCounter'], + Counter() => moduleSourceAssets['Counter'], + Deserializer() => moduleSourceAssets['Deserializer'], + EdgeDetector() => moduleSourceAssets['EdgeDetector'], + Extrema() => moduleSourceAssets['Extrema'], + Fifo() => moduleSourceAssets['Fifo'], + Find() => moduleSourceAssets['Find'], + FixedPointSqrt() => moduleSourceAssets['FixedPointSqrt'], + FixedToFloat() => moduleSourceAssets['FixedToFloat'], + FloatToFixed() => moduleSourceAssets['FloatToFixed'], + FloatingPointAdderDualPath() => + moduleSourceAssets['FloatingPointAdderDualPath'], + FloatingPointAdderSinglePath() => + moduleSourceAssets['FloatingPointAdderSinglePath'], + FloatingPointMultiplierSimple() => + moduleSourceAssets['FloatingPointMultiplierSimple'], + FloatingPointSqrtSimple() => + moduleSourceAssets['FloatingPointSqrtSimple'], + GrayToBinaryConverter() => moduleSourceAssets['GrayToBinaryConverter'], + HammingEccReceiver() => moduleSourceAssets['HammingEccReceiver'], + LeadingDigitAnticipate() => moduleSourceAssets['LeadingDigitAnticipate'], + LeadingZeroAnticipate() => moduleSourceAssets['LeadingZeroAnticipate'], + LeadingZeroAnticipateCarry() => + moduleSourceAssets['LeadingZeroAnticipateCarry'], + MaskRoundRobinArbiter() => moduleSourceAssets['MaskRoundRobinArbiter'], + NativeMultiplier() => moduleSourceAssets['NativeMultiplier'], + ParallelPrefixAdder() => moduleSourceAssets['ParallelPrefixAdder'], + PriorityArbiter() => moduleSourceAssets['PriorityArbiter'], + RegisterFile() => moduleSourceAssets['RegisterFile'], + RippleCarryAdder() => moduleSourceAssets['RippleCarryAdder'], + RotateLeft() => moduleSourceAssets['RotateLeft'], + RotateRight() => moduleSourceAssets['RotateRight'], + RotateRoundRobinArbiter() => + moduleSourceAssets['RotateRoundRobinArbiter'], + Serializer() => moduleSourceAssets['Serializer'], + Sum() => moduleSourceAssets['Sum'], + TreeOneHotToBinary() => moduleSourceAssets['TreeOneHotToBinary'], + _ => null, + }; + +/// Converts a logical ROHD source path to its Flutter asset-bundle key. +String bundledSourceAssetPath(String sourceAsset) => 'assets/$sourceAsset'; diff --git a/confapp/lib/hcl/view/hcl_page.dart b/confapp/lib/hcl/view/hcl_page.dart index fb0d236bf0..f079727a02 100644 --- a/confapp/lib/hcl/view/hcl_page.dart +++ b/confapp/lib/hcl/view/hcl_page.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // hcl_page.dart @@ -6,32 +6,43 @@ // // 2023 December +import 'package:confapp/hcl/cubit/component_cubit.dart'; import 'package:confapp/hcl/cubit/system_verilog_cubit.dart'; -import 'package:flutter/material.dart'; +import 'package:confapp/hcl/cubit/theme_cubit.dart'; +import 'package:confapp/hcl/view/hcl_view.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:confapp/hcl/cubit/component_cubit.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; -import 'hcl_view.dart'; +/// Provides the state controllers needed by the configuration application. class HCLPage extends StatelessWidget { + /// The components available to configure. final List components; - /// {@macro counter_page} - const HCLPage({super.key, required this.components}); + /// Creates the configuration page for [components]. + const HCLPage({required this.components, super.key}); + + @override + Widget build(BuildContext context) => MultiBlocProvider( + providers: [ + BlocProvider( + // Look pretty ugly using static, not sure how to improve this + create: (context) => ComponentCubit(components), + ), + BlocProvider( + create: (context) => SystemVerilogCubit(), + ), + BlocProvider( + create: (context) => ThemeCubit(), + ), + ], + child: const HCLView(), + ); @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - // Look pretty ungly using static, not sure how to improve this - create: (BuildContext context) => ComponentCubit(components), - ), - BlocProvider( - create: (BuildContext context) => SystemVerilogCubit(), - ), - ], - child: const HCLView(), - ); + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('components', components)); } } diff --git a/confapp/lib/hcl/view/hcl_view.dart b/confapp/lib/hcl/view/hcl_view.dart index 9cbc76cd62..adb013d67b 100644 --- a/confapp/lib/hcl/view/hcl_view.dart +++ b/confapp/lib/hcl/view/hcl_view.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // hcl_view.dart @@ -6,72 +6,66 @@ // // 2023 December +import 'package:confapp/hcl/cubit/theme_cubit.dart'; +import 'package:confapp/hcl/view/screen/content_widget.dart'; import 'package:confapp/hcl/view/screen/sidebar_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:rohd_hcl/rohd_hcl.dart'; -import 'package:sidebarx/sidebarx.dart'; - -import 'screen/content_widget.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_ui/material_ui.dart'; +/// The app view that hosts the component sidebar and generated-source panel. class HCLView extends StatelessWidget { + /// Creates the configuration app view. const HCLView({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'ROHD-HCL', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: const Color(0x00082E8A)), - useMaterial3: true, - scaffoldBackgroundColor: const Color(0x00BED9FF)), - home: const MainPage(title: 'ROHD-HCL'), - ); - } + Widget build(BuildContext context) => BlocBuilder( + builder: (context, themeMode) => MaterialApp( + debugShowCheckedModeBanner: false, + title: 'ROHD-HCL', + themeMode: themeMode, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF082E8A), + ), + useMaterial3: true, + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF082E8A), + brightness: Brightness.dark, + ), + useMaterial3: true, + ), + home: const MainPage(title: 'ROHD-HCL'), + ), + ); } -class MainPage extends StatefulWidget { - const MainPage({super.key, required this.title}); +/// The main application page containing the sidebar and content panel. +class MainPage extends StatelessWidget { + /// Creates the main page with the displayed [title]. + const MainPage({required this.title, super.key}); + /// The page title. final String title; @override - State createState() => _MainPageState(); -} - -class _MainPageState extends State { - final _controller = SidebarXController(selectedIndex: 0, extended: true); - List drawerList = []; - - late Configurator component; - List textFormField = []; // shared variable - - final ButtonStyle btnStyle = - ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); - - // Change the input form - void selectComponent(componentGenerator) { - textFormField = []; - component = componentGenerator; + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('title', title)); } @override Widget build(BuildContext context) { - final GlobalKey scaffoldKey = GlobalKey(); + final scaffoldKey = GlobalKey(); return Scaffold( key: scaffoldKey, - body: Row( + body: const Row( children: [ - ComponentsSidebar( - controller: _controller, - updateForm: selectComponent, - ), - Flexible( - child: SVGenerator( - controller: _controller, - ), - ), + ComponentsSidebar(width: 240), + Expanded(child: SVGenerator()), ], ), ); diff --git a/confapp/lib/hcl/view/screen/browser_interop.dart b/confapp/lib/hcl/view/screen/browser_interop.dart new file mode 100644 index 0000000000..d98d2ac6f4 --- /dev/null +++ b/confapp/lib/hcl/view/screen/browser_interop.dart @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_interop.dart +// Selects browser-only integrations when compiling for the web. +// +// 2026 September 25 +// Author: Desmond A. Kirkpatrick + +export 'browser_interop_stub.dart' + if (dart.library.js_interop) 'browser_interop_web.dart'; diff --git a/confapp/lib/hcl/view/screen/browser_interop_stub.dart b/confapp/lib/hcl/view/screen/browser_interop_stub.dart new file mode 100644 index 0000000000..340f1c7aa0 --- /dev/null +++ b/confapp/lib/hcl/view/screen/browser_interop_stub.dart @@ -0,0 +1,36 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_interop_stub.dart +// VM-safe implementations of browser-only integrations. +// +// 2026 September 25 +// Author: Desmond A. Kirkpatrick + +import 'package:flutter/widgets.dart'; + +/// VM placeholder for the web worker used to synthesize generated RTL. +class YosysWorker { + /// Creates a placeholder for the worker at `scriptPath`. + YosysWorker(this._scriptPath); + + final String _scriptPath; + + /// Reports that Yosys synthesis is unavailable on the VM. + Future synthesize(Map message) => Future.error( + UnsupportedError( + 'Yosys synthesis via $_scriptPath is only available on web.', + ), + ); + + /// Releases worker resources. + void dispose() {} +} + +/// Reports that downloading [content] to [fileName] requires a browser. +void downloadFile({required String content, required String fileName}) { + throw UnsupportedError('File downloads are only available on web.'); +} + +/// Returns [child] without browser-specific context-menu handling. +Widget browserContextMenuSuppressor({required Widget child}) => child; diff --git a/confapp/lib/hcl/view/screen/browser_interop_web.dart b/confapp/lib/hcl/view/screen/browser_interop_web.dart new file mode 100644 index 0000000000..7f3f7003e4 --- /dev/null +++ b/confapp/lib/hcl/view/screen/browser_interop_web.dart @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_interop_web.dart +// Browser implementations for Yosys, downloads, and context-menu suppression. +// +// 2026 September 25 +// Author: Desmond A. Kirkpatrick + +import 'package:confapp/hcl/view/screen/browser_interop_web_core.dart'; +import 'package:flutter/widgets.dart'; + +export 'browser_interop_web_core.dart' show YosysWorker, downloadFile; + +/// Prevents the browser context menu from appearing over [child]. +Widget browserContextMenuSuppressor({required Widget child}) => + _BrowserContextMenuSuppressor(child: child); + +class _BrowserContextMenuSuppressor extends StatefulWidget { + const _BrowserContextMenuSuppressor({required this.child}); + + final Widget child; + + @override + State<_BrowserContextMenuSuppressor> createState() => + _BrowserContextMenuSuppressorState(); +} + +class _BrowserContextMenuSuppressorState + extends State<_BrowserContextMenuSuppressor> { + @override + void initState() { + super.initState(); + BrowserContextMenuSuppression.attach(); + } + + @override + void dispose() { + BrowserContextMenuSuppression.detach(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/confapp/lib/hcl/view/screen/browser_interop_web_core.dart b/confapp/lib/hcl/view/screen/browser_interop_web_core.dart new file mode 100644 index 0000000000..872b3d0737 --- /dev/null +++ b/confapp/lib/hcl/view/screen/browser_interop_web_core.dart @@ -0,0 +1,131 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_interop_web_core.dart +// Browser integrations that do not depend on the Flutter framework. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop'; + +import 'package:confapp/hcl/view/screen/serialized_worker_request_queue.dart'; +import 'package:web/web.dart' as web; + +/// Wraps the browser worker used to synthesize generated RTL with Yosys. +class YosysWorker { + /// Creates a worker from the JavaScript file at `scriptPath`. + YosysWorker(String scriptPath) : _worker = web.Worker(scriptPath.toJS) { + _requests = SerializedWorkerRequestQueue(); + _messageSubscription = web.EventStreamProviders.messageEvent + .forTarget(_worker) + .listen(_handleMessage); + _errorSubscription = web.EventStreamProviders.errorWorkerEvent + .forTarget(_worker) + .listen(_handleError); + } + + final web.Worker _worker; + late final SerializedWorkerRequestQueue _requests; + late final StreamSubscription _messageSubscription; + late final StreamSubscription _errorSubscription; + + /// Synthesizes the RTL described by [message]. + /// + /// Requests are serialized because the worker response does not contain a + /// correlation identifier. + Future synthesize(Map message) { + final immutableMessage = Map.unmodifiable(message); + return _requests.add(() => _postMessage(immutableMessage)); + } + + void _postMessage(Map message) { + _worker.postMessage(message.jsify()); + } + + void _handleMessage(web.MessageEvent event) { + try { + final response = event.data.dartify(); + if (response is! String) { + throw const FormatException( + 'The Yosys worker returned a non-string response.', + ); + } + _requests.complete(response); + } on Object catch (error, stackTrace) { + _requests.completeError(error, stackTrace); + } + } + + void _handleError(web.Event event) { + event.preventDefault(); + final errorEvent = event as web.ErrorEvent; + final message = errorEvent.message.trim(); + final filename = errorEvent.filename.trim(); + final location = filename.isEmpty + ? '' + : ' ($filename' + '${errorEvent.lineno > 0 ? ':${errorEvent.lineno}' : ''}' + '${errorEvent.colno > 0 ? ':${errorEvent.colno}' : ''})'; + _requests.failAll( + StateError( + 'Yosys worker failed: ' + '${message.isEmpty ? 'unknown worker error' : message}$location', + ), + ); + _worker.terminate(); + } + + /// Releases the worker and fails any requests that are still pending. + void dispose() { + _requests.dispose(); + unawaited(_messageSubscription.cancel()); + unawaited(_errorSubscription.cancel()); + _worker.terminate(); + } +} + +/// Downloads [content] through a browser anchor using [fileName]. +void downloadFile({required String content, required String fileName}) { + final bytes = base64Encode(utf8.encode(content)); + final uri = 'data:application/octet-stream;base64,$bytes'; + final anchor = web.HTMLAnchorElement() + ..href = uri + ..download = fileName; + web.document.body?.appendChild(anchor); + try { + anchor.click(); + } finally { + anchor.remove(); + } +} + +/// Reference-counts browser context-menu suppression. +class BrowserContextMenuSuppression { + static int _refCount = 0; + static web.EventListener? _listener; + + /// Prevents browser context menus until balanced by [detach]. + static void attach() { + if (_refCount == 0) { + _listener = ((web.Event event) => event.preventDefault()).toJS; + web.document.body?.addEventListener('contextmenu', _listener, true.toJS); + } + _refCount++; + } + + /// Releases one context-menu suppression reference. + static void detach() { + if (_refCount == 0) { + throw StateError('Context-menu suppression is not attached.'); + } + _refCount--; + if (_refCount == 0 && _listener != null) { + web.document.body + ?.removeEventListener('contextmenu', _listener, true.toJS); + _listener = null; + } + } +} diff --git a/confapp/lib/hcl/view/screen/content_widget.dart b/confapp/lib/hcl/view/screen/content_widget.dart index 859844f93f..bf92e85243 100644 --- a/confapp/lib/hcl/view/screen/content_widget.dart +++ b/confapp/lib/hcl/view/screen/content_widget.dart @@ -1,66 +1,2052 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // content_widget.dart // Implementation of the widget for viewing the main content // // 2023 December +// Author: Max Korbel -import 'dart:convert'; - -// need this for creating a download link -// ignore: avoid_web_libraries_in_flutter, deprecated_member_use -import 'dart:html'; -import 'dart:ui_web' as ui_web; +import 'dart:async' show StreamSubscription, unawaited; import 'package:confapp/hcl/cubit/component_cubit.dart'; import 'package:confapp/hcl/cubit/system_verilog_cubit.dart'; -import 'package:confapp/hcl/view/screen/schematic.dart'; -import 'package:flutter/material.dart'; +import 'package:confapp/hcl/cubit/theme_cubit.dart'; +import 'package:confapp/hcl/module_source_assets.dart'; +import 'package:confapp/hcl/view/screen/browser_interop.dart'; +import 'package:confapp/hcl/view/screen/dart_syntax_code_controller.dart'; +import 'package:confapp/hcl/view/screen/flc_trace_data.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:google_fonts/google_fonts.dart'; +import 'package:highlight/languages/cpp.dart' as highlight_cpp; +import 'package:highlight/languages/verilog.dart' as highlight_verilog; +import 'package:material_ui/material_ui.dart' as material_ui; +import 'package:material_ui/material_ui.dart'; +import 'package:rohd/rohd.dart' show Module, NetlistSynthesizer, SynthBuilder; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart' + show + LocalCrossProbeChannel, + LocalCrossProbeService, + RohdExtensionClient, + RohdFormatInfo, + RohdModuleInfo, + RohdSourceFormat; import 'package:rohd_hcl/rohd_hcl.dart'; -import 'package:sidebarx/sidebarx.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart' + show HierarchyOccurrence, HierarchyService, NetlistHierarchyAdapter; +import 'package:rohd_schematic_viewer/schematic_viewer.dart'; +import 'package:rohd_source_navigator/flc_data.dart' + show FlcData, FlcEntry, FlcFrame; -class SVGenerator extends StatefulWidget { - final SidebarXController controller; +const _rohdIconAsset = 'assets/rohd_icon.png'; +const _systemVerilogIconAsset = 'assets/systemverilog_icon.png'; +const _yosysIconAsset = 'assets/yosys_icon.png'; - const SVGenerator({ - super.key, - required this.controller, +typedef _RohdSourceDocument = ({ + String source, + DartSyntaxHighlighter syntaxHighlighter, +}); + +class _ConfappRohdExtensionClient implements RohdExtensionClient { + _ConfappRohdExtensionClient({ + required this.getModuleFormats, + required this.lookupFrames, }); + final Future> Function( + String module, { + List? instancePath, + }) getModuleFormats; + final Future>> Function({ + required List> signals, + String? format, + }) lookupFrames; + + @override + final isAvailable = ValueNotifier(true); + + @override + final currentModuleInfo = ValueNotifier(null); + + @override + Future ping() async { + isAvailable.value = true; + return true; + } + + @override + Future queryModule( + String module, { + List? instancePath, + }) async { + final formatKeys = await getModuleFormats( + module, + instancePath: instancePath, + ); + final formats = {}; + for (final key in formatKeys) { + final format = _parseFormat(key); + if (format != null) { + formats[format] = const RohdFormatInfo( + available: true, + fileFound: true, + ); + } + } + final info = RohdModuleInfo( + extensionAvailable: true, + module: module, + formats: formats, + ); + currentModuleInfo.value = info; + return info; + } + + void refreshModule(String module) { + unawaited(queryModule(module)); + } + + @override + Future>> lookupSignalFrames({ + required List> signals, + String? format, + }) => + lookupFrames(signals: signals, format: format); + + @override + void openSourceLocation({ + required String file, + required int line, + int col = 0, + }) {} + + @override + void dispose() { + isAvailable.dispose(); + currentModuleInfo.dispose(); + } + + static RohdSourceFormat? _parseFormat(String key) => switch (key) { + 'rohd' => RohdSourceFormat.rohd, + 'sv' => RohdSourceFormat.sv, + 'sc' => RohdSourceFormat.sc, + 'fst' => RohdSourceFormat.fst, + _ => null, + }; +} + +/// Displays component configuration controls and generated outputs. +class SVGenerator extends StatefulWidget { + /// Creates the generated-output view. + const SVGenerator({super.key}); + @override State createState() => _SVGeneratorState(); } class _SVGeneratorState extends State - with SingleTickerProviderStateMixin { + with TickerProviderStateMixin { final GlobalKey _formKey = GlobalKey(); - final ButtonStyle btnStyle = - ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)); + final ButtonStyle btnStyle = ElevatedButton.styleFrom( + textStyle: const TextStyle(fontSize: 20), + ); + + late TabController _tabController; + late final _ConfappRohdExtensionClient _sourceFormatClient; + late final StreamSubscription _componentSubscription; + + /// Labels for each logical tab index. + static const _tabLabels = [ + 'ROHD Schematic', + 'ROHD Source', + 'Generated SV', + 'Synth Schematic', + ]; + + /// Returns the icon widget for tab checkbox at [index]. + Widget _tabIconWidget(int index) { + switch (index) { + case 0: + return const SchematicIcon(size: 26); + case 1: + return _tabAssetIcon( + _rohdIconAsset, + semanticLabel: _tabTooltips[index], + ); + case 2: + return _tabAssetIcon( + _systemVerilogIconAsset, + semanticLabel: _tabTooltips[index], + ); + case 3: + return _tabAssetIcon( + _yosysIconAsset, + width: 28, + height: 28, + semanticLabel: _tabTooltips[index], + ); + default: + return const SizedBox.shrink(); + } + } + + Widget _tabAssetIcon( + String asset, { + required String semanticLabel, + double width = 26, + double height = 26, + }) => + Image.asset( + asset, + width: width, + height: height, + fit: BoxFit.contain, + filterQuality: FilterQuality.high, + semanticLabel: semanticLabel, + ); + + Widget _tabLabelWidget(int index) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + _tabIconWidget(index), + const SizedBox(width: 8), + Text(_tabLabels[index]), + ], + ); + + /// Tooltips for the tab-enable checkboxes. + static const _tabTooltips = [ + 'ROHD Schematic', + 'ROHD Source', + 'Generated SV', + 'Synth Schematic (Yosys)', + ]; + + /// Logical indices of currently visible tabs. + List get _visibleTabs => [ + for (var i = 0; i < _tabLabels.length; i++) + if (_tabEnabled[i]) i + ]; + + /// Returns the current visible-tab index for a logical tab index. + int? _visibleTabIndexForLogical(int logicalIndex) { + final idx = _visibleTabs.indexOf(logicalIndex); + return idx >= 0 ? idx : null; + } + + /// Show a logical tab, optionally enabling it first when currently hidden. + void _showLogicalTab(int logicalIndex, {bool enableIfHidden = false}) { + if (!_tabEnabled[logicalIndex]) { + if (!enableIfHidden) { + return; + } + setState(() { + _tabEnabled[logicalIndex] = true; + _rebuildTabController(selectLogical: logicalIndex); + }); + return; + } + + final visibleIdx = _visibleTabIndexForLogical(logicalIndex); + if (visibleIdx != null && _tabController.index != visibleIdx) { + _tabController.animateTo(visibleIdx); + } + } + + /// Rebuild the TabController to match the current set of visible tabs. + void _rebuildTabController({int? selectLogical}) { + final visible = _visibleTabs; + if (visible.isEmpty) { + return; + } + final oldIndex = _tabController.index; + final oldLogical = + oldIndex < visible.length ? visible[oldIndex] : visible.first; + _tabController.removeListener(_onTabChanged); + _tabController.dispose(); + + final newVisible = _visibleTabs; + final initialIndex = () { + if (selectLogical != null) { + final idx = newVisible.indexOf(selectLogical); + if (idx >= 0) { + return idx; + } + } + // Try to stay on the same logical tab. + final idx = newVisible.indexOf(oldLogical); + return idx >= 0 ? idx : 0; + }(); + + _tabController = TabController( + length: newVisible.length, + vsync: this, + initialIndex: initialIndex, + ); + _tabController.addListener(_onTabChanged); + } + + void _onTabChanged() { + if (!_tabController.indexIsChanging) { + setState(() {}); + WidgetsBinding.instance.addPostFrameCallback((_) { + final visible = _visibleTabs; + if (_tabController.index < visible.length) { + _applyPendingScroll(visible[_tabController.index]); + } + }); + } + } + + void _scheduleSourceFormatRefresh() { + final moduleName = _lastModuleName; + if (moduleName == null) { + return; + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _sourceFormatClient.refreshModule(moduleName); + }); + } @override void initState() { super.initState(); + _sourceFormatClient = _ConfappRohdExtensionClient( + getModuleFormats: _getModuleFormats, + lookupFrames: _lookupSignalFrames, + ); + final visible = _visibleTabs; + _tabController = TabController(length: visible.length, vsync: this); + _tabController.addListener(_onTabChanged); + + // Listen for cross-probe events from viewers and highlight editors. + _editorCrossProbeService.incomingSignals.addListener(_onCrossProbeSignals); + + // Clear generated outputs when the selected component changes. + final componentCubit = context.read(); + final rtlCubit = context.read(); + _componentSubscription = componentCubit.stream.listen((component) { + _generationRequestId++; + rtlCubit.initializeData(); + _disposeRohdSubtabs(); + setState(() { + _rohdNetlistJson = null; + _yosysJson = null; + _synthSchematicLoading = false; + _synthSchematicRequestId++; + _flcData = null; + _flcModuleName = null; + _hierarchyService = null; + _lastRohdSource = null; + _highlightedSvLine = null; + _highlightedRohdLine = null; + _rohdSourceFiles = []; + // Clear cached generation state. + _lastBuiltModule = null; + _lastRtlRes = null; + _lastScRes = null; + _lastModuleName = null; + _lastEmbeddedFlcData = null; + _highlightedScLine = null; + }); + // Set up the primary ROHD source subtab (no FLC until Generate). + _initPrimaryRohdSubtab(component); + }); + + // Also set up for the initial component. + _initPrimaryRohdSubtab(componentCubit.state); + } + + /// Set up a single ROHD source subtab for the primary source file of + /// [component]. Additional subtabs are added at generation time when + /// the FLC JSON is loaded by [_buildCrossProbeAndHighlight]. + void _initPrimaryRohdSubtab(Configurator component) { + final mod = component.createModule(); + final primaryAsset = moduleSourceAsset(mod); + if (primaryAsset == null) { + return; + } + + _disposeRohdSubtabs(); + + final subtabCtrl = TabController(length: 1, vsync: this); + subtabCtrl.addListener(() { + if (!subtabCtrl.indexIsChanging) { + setState(() {}); + _applyPendingRohdSubtabScroll(subtabCtrl.index); + } + }); + + setState(() { + _rohdSourceFiles = [primaryAsset]; + _rohdSourceTabController = subtabCtrl; + }); + } + + final yosysWorker = YosysWorker('yosysWorker.js'); + String? _rohdNetlistJson; + String? _yosysJson; + bool _synthSchematicLoading = false; + int _synthSchematicRequestId = 0; + int _generationRequestId = 0; + String _moduleName = ''; + + bool _isCurrentGeneration(int requestId) => + mounted && requestId == _generationRequestId; + + /// Hierarchy service built from the ROHD netlist JSON, used to resolve + /// signal names into full occurrence paths for cross-probe. + HierarchyService? _hierarchyService; + + /// Cross-probe data built after each generation (shared FlcData model). + FlcData? _flcData; + + /// The module name used for FlcData lookups (definition name from build). + String? _flcModuleName; + + /// Cached ROHD source text reserved for the planned source-navigation UI. + // The planned source-navigation UI will read this cached source. + // ignore: unused_field + String? _lastRohdSource; + + /// Current expansion mode for the ROHD schematic viewer. + SchematicExpansionMode _expansionMode = SchematicExpansionMode.defaultView; + + /// Fraction of the total width allocated to the config (left) pane. + /// Defaults to 0.25 (i.e. the old flex 1:3 ratio). + double _splitFraction = 0.25; + + /// Persistent controllers for the code editors so they can be targeted + /// programmatically (e.g. scroll-to-line from a schematic node click). + HighlightCodeController? _svController; + HighlightCodeController? _scController; + SyntaxCodeController? _rohdSourceController; + late final Future _dartSyntaxHighlighter = + DartSyntaxHighlighter.load(); + + /// Focus nodes for the code editors — needed so that selection highlights + /// are visible (Flutter only renders selection when the field has focus). + final FocusNode _svFocusNode = FocusNode(); + final FocusNode _scFocusNode = FocusNode(); + final FocusNode _rohdSourceFocusNode = FocusNode(); + + /// Scroll controllers for the outer SingleChildScrollView wrappers so we + /// can programmatically center a target line in the viewport. + final ScrollController _svScrollController = ScrollController(); + final ScrollController _scScrollController = ScrollController(); + final ScrollController _rohdSourceScrollController = ScrollController(); + final ScrollController _configScrollController = ScrollController(); + final ScrollController _svHorizontalScrollController = ScrollController(); + final ScrollController _scHorizontalScrollController = ScrollController(); + + // ── ROHD Source subtab state ────────────────────────────────────────── + + /// Ordered list of asset paths for all ROHD source files referenced by + /// the current FLC. Each entry corresponds to one subtab. The "primary" + /// source (from [moduleSourceAssets]) is always first. + List _rohdSourceFiles = []; + + /// Tab controller for the ROHD source subtabs (one per file). + TabController? _rohdSourceTabController; + + /// Per-file controllers, scroll controllers, focus nodes, and highlights. + final Map _rohdFileControllers = {}; + final Map _rohdFileScrollControllers = {}; + final Map _rohdFileHorizontalScrollControllers = {}; + final Map _rohdFileFocusNodes = {}; + final Map _rohdFileHighlightLines = {}; + + /// Cached asset-loading futures so FutureBuilder doesn't restart on rebuild. + final Map> _rohdFileFutures = {}; + final Map> _rohdSourceViewFutures = {}; + + /// GlobalKey for the TabBarView to preserve element identity across rebuilds. + GlobalKey? _subtabViewKey; + + /// Pending scroll offsets when the target subtab isn't visible yet. + final Map _rohdFilePendingScrolls = {}; + + // ── End ROHD Source subtab state ────────────────────────────────────── + + // ── Tab visibility checkboxes ───────────────────────────────────────── + + /// Which output tabs are enabled. Controls both visibility in the TabBar + /// and whether the corresponding generation step runs. + /// + /// Index mapping: + /// 0 = ROHD Schematic (netlist) + /// 1 = ROHD Source (FLC/cross-probe) + /// 2 = Generated SV (always on — base for others) + /// 3 = Synth Schematic (Yosys) + final List _tabEnabled = [true, true, true, false]; + + /// Cached module and generated outputs from the last generate, so that lazy + /// generation of newly-enabled tabs can reuse the built module. + Module? _lastBuiltModule; + String? _lastRtlRes; + String? _lastScRes; + String? _lastModuleName; + + /// Trace data cached from a trace-enabled ROHD netlist. + /// + /// The stable ROHD 0.6.11 dependency does not emit this data. Keeping the + /// cache separate from [_flcData] lets a future trace-enabled netlist retain + /// its data until the ROHD Source tab is opened. + FlcData? _lastEmbeddedFlcData; + + // ── End tab visibility checkboxes ───────────────────────────────────── + + /// Pending scroll offsets for editors that aren't currently visible. + /// Applied when the tab becomes visible. + double? _pendingSvScroll; + int? _pendingSvLine; + int? _pendingSvColumn; + double? _pendingScScroll; + int? _pendingScLine; + int? _pendingScColumn; + double? _pendingRohdScroll; + + /// Persistently highlighted line (1-based) for each editor, or null. + int? _highlightedSvLine; + int? _highlightedScLine; + // The planned consolidated ROHD editor will read this highlight. + // ignore: unused_field + int? _highlightedRohdLine; + + /// Last known pointer position (global) — used to anchor popup menus. + Offset _lastPointerPosition = Offset.zero; + + /// When true, `_onCrossProbeSignals` skips source navigation. + /// Set temporarily when "Show Signal in Schematic" triggers the bus. + bool _suppressCrossProbeNav = false; + + /// Cross-probe services shared between schematic viewers and editors. + final LocalCrossProbeChannel _crossProbeChannel = LocalCrossProbeChannel(); + late final LocalCrossProbeService _rohdSchematicCrossProbeService = + LocalCrossProbeService(_crossProbeChannel, source: 'rohd-schematic'); + late final LocalCrossProbeService _synthSchematicCrossProbeService = + LocalCrossProbeService(_crossProbeChannel, source: 'synth-schematic'); + late final LocalCrossProbeService _editorCrossProbeService = + LocalCrossProbeService(_crossProbeChannel, source: 'editor'); + + /// The shared text style used in all CodeField widgets. + /// + /// `height` is set explicitly so it overrides the theme's titleMedium + /// height multiplier that CodeField merges in. This keeps our overlay + /// line-height calculation in sync with the actual rendered text. + static const _codeTextStyle = TextStyle( + fontFamily: 'monospace', + fontSize: 12, + height: 1.5, + ); + + /// Line height (px) computed from the actual font metrics. + late final double _lineHeight = () { + final tp = TextPainter( + text: const TextSpan(text: 'X', style: _codeTextStyle), + textDirection: TextDirection.ltr, + )..layout(); + return tp.preferredLineHeight; + }(); + + /// Vertical offset (px) from the top of the CodeField to the first line + /// of rendered text. Empirically determined — the package's gutter uses + /// `Padding(vertical: 16)` but the TextField with `isCollapsed: true` and + /// the same `contentPadding` renders text at a slightly different offset. + /// We use a custom gutter (below) so that line-numbers, code, and + /// highlight all share this same value. + static const _codeTopPad = 13.0; + static const _minimumCodeColumns = 80; + static const _codeHorizontalEndPad = 96.0; + static const _codeScrollbarThickness = 10.0; + + /// The Material scrollbar paints over its child, including a 2px margin. + /// Reserve a fixed lane plus a small gap so its top thumb never covers code. + static const _codeScrollbarClearance = 16.0; + + /// Cache of measured scroll widths keyed by editor text so that rebuilds + /// triggered by highlight changes don't re-measure every line. + final Map _scrollExtentWidthCache = {}; + + double _measureCodeViewportWidth() => + _measureCodeLineWidth(''.padRight(_minimumCodeColumns, 'M')); + + /// Measure the horizontal scroll extent for [text] using the shared code + /// text style. This is intentionally separate from the viewport width: the + /// viewport stays near 80 columns, while this width lets very long generated + /// lines be reached by horizontal scrolling without visual wrapping. + double _measureCodeScrollExtentWidth(String text) { + final cached = _scrollExtentWidthCache[text]; + if (cached != null) { + return cached; + } + var maxWidth = 0.0; + for (final line in text.split('\n')) { + final lineWidth = _measureCodeLineWidth(line); + if (lineWidth > maxWidth) { + maxWidth = lineWidth; + } + } + final viewportWidth = _measureCodeViewportWidth(); + final contentWidth = maxWidth < viewportWidth ? viewportWidth : maxWidth; + final width = contentWidth + _codeHorizontalEndPad; + if (_scrollExtentWidthCache.length > 16) { + _scrollExtentWidthCache.clear(); + } + _scrollExtentWidthCache[text] = width; + return width; + } + + double _codeViewportWidthFor(BoxConstraints constraints) { + final targetWidth = _measureCodeViewportWidth(); + final availableWidth = constraints.maxWidth; + if (!availableWidth.isFinite || availableWidth >= targetWidth) { + return targetWidth; + } + return availableWidth; + } + + double _measureCodeLineWidth(String line) { + final tp = TextPainter( + text: TextSpan(text: line, style: _codeTextStyle), + textDirection: TextDirection.ltr, + maxLines: 1, + )..layout(); + return tp.width; + } + + /// Navigate a [SyntaxCodeController] to [line] (1-based) and optionally + /// highlight from [column] (1-based) to end-of-line. + /// + /// Sets the selection immediately (works even when off-screen). + /// Returns the ideal scroll offset for centering the line, or null + /// if the scroll controller isn't attached. + double? _setSelectionAndComputeScroll( + SyntaxCodeController controller, { + required int line, + int? column, + ScrollController? scrollController, + }) { + final idx = line - 1; // 0-based + final lineRange = controller.lineRangeAt(idx); + if (lineRange == null) { + return null; + } + + final startOffset = lineRange.start + ((column ?? 1) - 1); + final endOffset = lineRange.end; + + // Clamp to actual text length to avoid invalid selection errors + // (can happen when the controller text is shorter than expected, + // e.g. during tab switches or when folded ranges shift offsets). + final textLen = controller.text.length; + final clampedStart = startOffset.clamp(lineRange.start, endOffset); + final clampedEnd = endOffset.clamp(0, textLen); + + controller.selection = TextSelection( + baseOffset: clampedStart, + extentOffset: clampedEnd, + ); + + // Compute ideal scroll offset. + if (scrollController != null && scrollController.hasClients) { + final targetPixel = idx * _lineHeight; + final viewportHeight = scrollController.position.viewportDimension; + return (targetPixel - viewportHeight / 2).clamp( + 0.0, + scrollController.position.maxScrollExtent, + ); + } + + // Return a raw estimate when the scroll controller isn't attached yet. + final targetPixel = idx * _lineHeight; + return targetPixel > 200 ? targetPixel - 200 : 0; + } + + /// Apply a pending scroll offset for the tab at [tabIndex]. + void _applyPendingScroll(int tabIndex) { + if (tabIndex == 1 && _pendingRohdScroll != null) { + final offset = _pendingRohdScroll!; + _pendingRohdScroll = null; + if (_rohdSourceScrollController.hasClients) { + final clamped = offset.clamp( + 0.0, + _rohdSourceScrollController.position.maxScrollExtent, + ); + _rohdSourceScrollController.animateTo( + clamped, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + // When the ROHD Source main tab becomes visible, also apply any + // pending scroll for the currently-selected subtab. + if (tabIndex == 1 && _rohdSourceTabController != null) { + _applyPendingRohdSubtabScroll(_rohdSourceTabController!.index); + } + if (tabIndex == 2 && _pendingSvScroll != null) { + final offset = _pendingSvScroll!; + final line = _pendingSvLine; + final col = _pendingSvColumn; + _pendingSvScroll = null; + _pendingSvLine = null; + _pendingSvColumn = null; + if (_svScrollController.hasClients) { + final clamped = offset.clamp( + 0.0, + _svScrollController.position.maxScrollExtent, + ); + _svScrollController.animateTo( + clamped, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + // Re-apply selection since the controller may have rebuilt. + if (line != null && _svController != null) { + _setSelectionAndComputeScroll( + _svController!, + line: line, + column: col, + ); + } + } + if (tabIndex == 3 && _pendingScScroll != null) { + final offset = _pendingScScroll!; + final line = _pendingScLine; + final col = _pendingScColumn; + _pendingScScroll = null; + _pendingScLine = null; + _pendingScColumn = null; + if (_scScrollController.hasClients) { + final clamped = offset.clamp( + 0.0, + _scScrollController.position.maxScrollExtent, + ); + _scScrollController.animateTo( + clamped, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + if (line != null && _scController != null) { + _setSelectionAndComputeScroll( + _scController!, + line: line, + column: col, + ); + } + } + } + + /// Set highlight on the Generated SV editor at [line]:[column]. + /// If the SV tab is visible, scrolls immediately; otherwise defers. + void navigateToSV({required int line, int? column}) { + if (_svController == null) { + return; + } + final effectiveLine = line; + setState(() { + _highlightedSvLine = effectiveLine; + }); + final offset = _setSelectionAndComputeScroll( + _svController!, + line: effectiveLine, + column: column, + scrollController: _svScrollController, + ); + final svTabIndex = _visibleTabIndexForLogical(2); + if (svTabIndex != null && _tabController.index != svTabIndex) { + // Switch to the SV tab so the user can see it. + _pendingSvScroll = offset; + _pendingSvLine = effectiveLine; + _pendingSvColumn = column; + _tabController.animateTo(svTabIndex); + } else if (_svScrollController.hasClients) { + // Tab is already visible — scroll now. + if (offset != null) { + _svScrollController.animateTo( + offset.clamp(0.0, _svScrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + _svFocusNode.requestFocus(); + } else { + _pendingSvScroll = offset; + _pendingSvLine = effectiveLine; + _pendingSvColumn = column; + } + } + + /// Set highlight on the generated SystemC editor at [line]:[column]. + /// If the SystemC tab is visible, scrolls immediately; otherwise defers. + void navigateToSC({required int line, int? column}) { + if (_scController == null) { + return; + } + final effectiveLine = line; + setState(() { + _highlightedScLine = effectiveLine; + }); + final offset = _setSelectionAndComputeScroll( + _scController!, + line: effectiveLine, + column: column, + scrollController: _scScrollController, + ); + final scTabIndex = _visibleTabIndexForLogical(3); + if (scTabIndex != null && _tabController.index != scTabIndex) { + _pendingScScroll = offset; + _pendingScLine = effectiveLine; + _pendingScColumn = column; + _tabController.animateTo(scTabIndex); + } else if (_scScrollController.hasClients) { + if (offset != null) { + _scScrollController.animateTo( + offset.clamp(0.0, _scScrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + _scFocusNode.requestFocus(); + } else { + _pendingScScroll = offset; + _pendingScLine = effectiveLine; + _pendingScColumn = column; + } + } + + /// Set highlight on the ROHD Source editor at [line]:[column]. + /// If [file] is given, switches to the correct subtab first. + /// If the Source tab is visible, scrolls immediately; otherwise defers. + void navigateToSource({required int line, int? column, String? file}) { + // Determine which subtab file to target. + final targetFile = + file ?? (_rohdSourceFiles.isNotEmpty ? _rohdSourceFiles.first : null); + + // Switch to the ROHD Source main tab, enabling it if hidden. + _showLogicalTab(1, enableIfHidden: true); + + // Switch to the correct subtab if we have subtabs. + if (targetFile != null && _rohdSourceFiles.contains(targetFile)) { + final tabIdx = _rohdSourceFiles.indexOf(targetFile); + if (_rohdSourceTabController != null && + _rohdSourceTabController!.index != tabIdx) { + _rohdSourceTabController!.animateTo(tabIdx); + } + } + + // Get or create the per-file controller. + final ctrl = targetFile != null + ? _rohdFileControllers[targetFile] + : _rohdSourceController; + if (ctrl == null) { + // Controller not yet created (tab not built yet). Store pending state. + if (targetFile != null) { + _rohdFileHighlightLines[targetFile] = line; + _rohdFilePendingScrolls[targetFile] = 0; + } + return; + } + + // Set highlight line for the target file. + setState(() { + if (targetFile != null) { + // Clear highlights on all other files. + for (final f in _rohdSourceFiles) { + if (f != targetFile) { + _rohdFileHighlightLines[f] = null; + } + } + _rohdFileHighlightLines[targetFile] = line; + } + _highlightedRohdLine = line; + }); + + final scrollCtrl = targetFile != null + ? _rohdFileScrollControllers[targetFile] + : _rohdSourceScrollController; + + final offset = _setSelectionAndComputeScroll( + ctrl, + line: line, + column: column, + scrollController: scrollCtrl, + ); + + // Store pending scroll. The scroll is applied either: + // - Immediately below (if scroll controller already has clients), or + // - From the _rohdSourceFileView builder when the widget is mounted. + if (targetFile != null) { + _rohdFilePendingScrolls[targetFile] = offset; + } else { + _pendingRohdScroll = offset; + } + + // If scroll controller is already attached, scroll now. + if (scrollCtrl != null && scrollCtrl.hasClients && offset != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollCtrl.hasClients) { + scrollCtrl.animateTo( + offset.clamp(0.0, scrollCtrl.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + if (targetFile != null) { + _rohdFilePendingScrolls[targetFile] = null; + } else { + _pendingRohdScroll = null; + } + } + }); + } + } + + /// Shallow list equality check. + static bool _listsEqual(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + /// Look up [pathOrName] in the FlcData as either a signal or instance. + /// + /// [pathOrName] may be a bare signal name (`"highBitsLSB"`) or a full + /// occurrence path rooted at an **instance** name + /// (`"FloatingPointAdderSinglePath/highBitsLSB"`). + /// + /// FLC data is keyed by module **definition** names (e.g. + /// `FloatingPointAdderSinglePath_E4M4`), whereas the schematic canvas roots + /// signal paths at module **instance** names (e.g. + /// `FloatingPointAdderSinglePath`). Following the rohd_devtools_extension + /// convention, the owning module path is translated to its definition name + /// before lookup: + /// 1. Sub-modules: the leading path segment (the top block's instance + /// name) is normalized to the hierarchy root name (its definition + /// name) so `occurrenceByPathname` can resolve the owning module; the + /// resolved [HierarchyOccurrence.definition] is the FLC key. + /// 2. Top module: the netlist roots at the instance name, so we fall back + /// to [_flcModuleName] (the definition name captured at build time). + /// 3. As a final safety net, search all modules by signal name. + /// + /// Returns the FlcEntry if found, null otherwise. + FlcEntry? _lookupFlcEntry(String pathOrName) { + final flcData = _flcData; + if (flcData == null) { + return null; + } + + final segments = pathOrName.split('/'); + final signalName = segments.last; + + // 1. Module-qualified lookup: translate the instance path of the owning + // module to its definition name (the FLC key) via the hierarchy. + if (segments.length >= 2) { + var moduleSegments = segments.sublist(0, segments.length - 1); + // The schematic roots its paths at the top block's *instance* name + // (e.g. `FloatingPointAdderSinglePath`), but the hierarchy/FLC key by + // its *definition* name (e.g. `FloatingPointAdderSinglePath_E4M4`). + // Normalize the leading segment so `occurrenceByPathname` can resolve + // sub-module paths. (For modules whose instance name already matches + // the definition name, this is a no-op.) + final rootName = _hierarchyService?.root.name; + if (rootName != null && + moduleSegments.isNotEmpty && + moduleSegments.first != rootName) { + moduleSegments = [rootName, ...moduleSegments.skip(1)]; + } + final modulePath = moduleSegments.join('/'); + final defName = + _hierarchyService?.occurrenceByPathname(modulePath)?.definition; + if (defName != null) { + final entry = flcData.lookupSignalEntry(defName, signalName) ?? + flcData.lookupInstanceEntry(defName, signalName); + if (entry != null) { + return entry; + } + } + } + + // 2. Top module: the netlist roots at the instance name, but FLC keys by + // the definition name captured at build time. + final modName = _flcModuleName; + if (modName != null) { + final entry = flcData.lookupSignalEntry(modName, signalName) ?? + flcData.lookupInstanceEntry(modName, signalName); + if (entry != null) { + return entry; + } + } + + // 3. Fallback: search all other modules by signal name. + for (final mod in flcData.moduleNames) { + if (mod == modName) { + continue; + } + final entry = flcData.lookupSignalEntry(mod, signalName) ?? + flcData.lookupInstanceEntry(mod, signalName); + if (entry != null) { + return entry; + } + } + return null; + } + + FlcFrame? _outputFrame(FlcEntry entry, String type) => entry.outputFrames + .cast() + .firstWhere((frame) => frame!.type == type, orElse: () => null); + + /// Resolve a bare signal name to its full occurrence path using the + /// hierarchy service (e.g. `"equalsMax"` → `"Serializer_W80_8/counter/equalsMax"`). + /// + /// Returns null if the hierarchy service is not available or the signal + /// cannot be found. When multiple matches exist, returns the first. + String? _resolveOccurrencePath(String signalName) { + final service = _hierarchyService; + if (service == null) { + return null; + } + final results = service.searchSignalPaths(signalName, limit: 5); + // Prefer exact leaf-name match (path ends with /signalName). + for (final path in results) { + final leaf = path.contains('/') ? path.split('/').last : path; + if (leaf == signalName) { + return path; + } + } + // Fall back to first result if any. + return results.isNotEmpty ? results.first : null; + } + + /// Convert an FLC file path to the confapp asset path. + /// `.dart_tool/../lib/src/memory/fifo.dart` → `rohd_src/memory/fifo.dart` + String? _flcFileToAsset(String flcPath) { + final norm = flcPath.replaceAll('.dart_tool/../', ''); + if (norm.startsWith('lib/src/')) { + return 'rohd_src/${norm.substring('lib/src/'.length)}'; + } + return null; + } + + /// Convert a confapp asset path back to the FLC file path suffix. + /// `rohd_src/serialization/serializer.dart` → `lib/src/serialization/serializer.dart` + static String? _assetToFlcFile(String assetPath) { + if (assetPath.startsWith('rohd_src/')) { + return 'lib/src/${assetPath.substring('rohd_src/'.length)}'; + } + return null; + } + + /// Extract the selected text from a [SyntaxCodeController], or if no + /// selection is active, extract the word surrounding the cursor position. + /// + /// Returns null if no meaningful word can be found. + String? _getWordFromController(SyntaxCodeController controller) { + final sel = controller.selection; + final text = controller.text; + if (text.isEmpty) { + return null; + } + + // If there's an active selection (non-collapsed), return the selected text. + if (sel.isValid && !sel.isCollapsed) { + final selected = sel.textInside(text).trim(); + if (selected.isNotEmpty) { + return selected; + } + } + + // No selection — try to extract the word at the cursor. + if (!sel.isValid || sel.baseOffset < 0 || sel.baseOffset > text.length) { + return null; + } + final offset = sel.baseOffset; + + // Expand left/right to find word boundaries (alphanumeric + underscore). + var start = offset; + while (start > 0 && _isWordChar(text.codeUnitAt(start - 1))) { + start--; + } + var end = offset; + while (end < text.length && _isWordChar(text.codeUnitAt(end))) { + end++; + } + if (start == end) { + return null; + } + return text.substring(start, end); + } + + /// Whether [codeUnit] is a word character (letter, digit, or underscore). + static bool _isWordChar(int codeUnit) => + (codeUnit >= 0x30 && codeUnit <= 0x39) || // 0-9 + (codeUnit >= 0x41 && codeUnit <= 0x5A) || // A-Z + (codeUnit >= 0x61 && codeUnit <= 0x7A) || // a-z + codeUnit == 0x5F; // _ + + /// Return the 1-based line number at the cursor position, or null. + static int? _cursorLineFromController(SyntaxCodeController controller) { + final sel = controller.selection; + if (!sel.isValid || sel.baseOffset < 0) { + return null; + } + final text = controller.text; + if (text.isEmpty) { + return null; + } + final offset = sel.baseOffset.clamp(0, text.length); + // Count newlines before the offset → 1-based line. + var line = 1; + for (var i = 0; i < offset; i++) { + if (text.codeUnitAt(i) == 0x0A) { + line++; + } + } + return line; + } + + /// Show a cross-probe context menu at [position] for the word found in + /// [controller]. Offers "Go to ROHD Source" and "Go to Generated SV" + /// when FLC data is available and the symbol is found. + /// + /// When [sourceFile] is provided (ROHD source editors), a reverse lookup + /// by file + line is attempted if the word-based lookup fails. + Future _showCodeContextMenu( + Offset position, + SyntaxCodeController controller, { + String? sourceFile, + }) async { + final word = _getWordFromController(controller); + + final items = >[]; + + // Always offer Copy if there is a selection. + final sel = controller.selection; + final hasSelection = sel.isValid && !sel.isCollapsed; + if (hasSelection) { + items.add( + const PopupMenuItem( + value: 'copy', + height: 32, + child: Text('Copy', style: TextStyle(fontSize: 13)), + ), + ); + } + + // Cross-probe items when we have FLC data and a valid word. + var entry = word != null ? _lookupFlcEntry(word) : null; + var resolvedWord = word; + + // Reverse lookup: if word-based lookup failed and we know the source + // file, find a signal whose ROHD stack trace passes through this line. + if (entry == null && sourceFile != null && _flcData != null) { + final cursorLine = _cursorLineFromController(controller); + if (cursorLine != null) { + final hits = _flcData!.lookupByRohdLine( + sourceFile, + line: cursorLine, + ); + if (hits.isNotEmpty) { + entry = hits.first.entry; + resolvedWord = hits.first.signal; + } + } + } + + if (entry != null) { + if (items.isNotEmpty) { + items.add(const PopupMenuDivider(height: 8)); + } + items.add( + PopupMenuItem( + value: 'showSignal', + height: 32, + child: Text( + 'Show Signal in Schematic ($resolvedWord)', + style: const TextStyle(fontSize: 13), + overflow: TextOverflow.ellipsis, + ), + ), + ); + if (entry.frames.isNotEmpty) { + items.add( + PopupMenuItem( + value: 'goToRohd', + height: 32, + child: Text( + 'Go to ROHD Source ($resolvedWord)', + style: const TextStyle(fontSize: 13), + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + if (entry.svFrame != null) { + items.add( + PopupMenuItem( + value: 'goToSv', + height: 32, + child: Text( + 'Go to Generated SV ($resolvedWord)', + style: const TextStyle(fontSize: 13), + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + } + + if (items.isEmpty) { + return; + } + + final rect = RelativeRect.fromLTRB( + position.dx, + position.dy, + position.dx, + position.dy, + ); + + if (!mounted) { + return; + } + final selected = await showMenu( + context: context, + position: rect, + items: items, + ); + if (selected == null || !mounted) { + return; + } + + switch (selected) { + case 'copy': + final text = sel.textInside(controller.text); + await Clipboard.setData(ClipboardData(text: text)); + case 'showSignal': + // Switch to the ROHD Schematic tab. + _showLogicalTab(0, enableIfHidden: true); + // Suppress source navigation while sending to schematic. + _suppressCrossProbeNav = true; + // Resolve the signal to its full occurrence path using the hierarchy. + final occPath = _resolveOccurrencePath(resolvedWord!); + if (occPath != null) { + _editorCrossProbeService.send([occPath], source: 'editor'); + } else { + // Fallback: prefix with top module name for single-level case. + final prefix = _moduleName.isNotEmpty ? '$_moduleName/' : ''; + _editorCrossProbeService.send( + ['$prefix$resolvedWord'], + source: 'editor', + ); + } + _suppressCrossProbeNav = false; + case 'goToRohd': + await _onGoToSource([resolvedWord!]); + case 'goToSv': + await _onGoToSv([resolvedWord!]); + } + } + + FlcData? _currentFlcData() { + final flcData = _flcData; + if (flcData != null) { + return flcData; + } + return _lastEmbeddedFlcData; + } + + EmbeddedFlcSourceNavigation get _flcSourceNavigation => + EmbeddedFlcSourceNavigation( + _currentFlcData(), + primaryModule: _flcModuleName ?? _lastModuleName, + ); + + bool get _canReceiveSourceCrossProbe => + EmbeddedFlcSourceNavigation(_flcData).isAvailable; + + Future> _getModuleFormats( + String module, { + List? instancePath, + }) async => + _flcSourceNavigation.formatsForModule( + module, + instancePath: instancePath, + ); + + Future>> _lookupSignalFrames({ + required List> signals, + String? format, + }) async => + _flcSourceNavigation.lookupSignalFrames( + signals: signals, + format: format, + ); + + /// Ensure embedded FLC data is available for source navigation. + /// + /// Trace-enabled future ROHD versions embed this data in their netlist. + /// Stable ROHD 0.6.11 leaves source navigation unavailable. + Future _ensureFlcDataLoaded() async { + if (_flcData != null && !_flcData!.isEmpty) { + return true; + } + + if (_lastBuiltModule == null || + _lastRtlRes == null || + _lastModuleName == null) { + return false; + } + + final component = context.read().state; + await _generateFlcTrace(component); + return _flcData != null && !_flcData!.isEmpty; + } + + /// Handle "Go to SV" from the schematic's context menu. + /// + /// Looks up the signal in FlcData and navigates to the corresponding + /// SV position, switching to the Generated SV tab. + Future _onGoToSv(List paths) async { + if (paths.isEmpty) { + return; + } + final hasFlcData = await _ensureFlcDataLoaded(); + if (!mounted) { + return; + } + if (!hasFlcData) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No source-location data available for this component'), + duration: Duration(seconds: 2), + ), + ); + return; + } + + final path = paths.first; + final leaf = path.contains('/') ? path.split('/').last : path; + + final entry = _lookupFlcEntry(path); + if (entry == null || entry.svFrame == null) { + debugPrint('[GoToSV] "$leaf" not found in FlcData or has no SV frame'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No generated SV position for this symbol'), + duration: Duration(seconds: 2), + ), + ); + return; + } + + final svFile = entry.svFrame!.file.split('/').last; + _setSvHighlightAndPendingScroll( + line: entry.svFrame!.line, + column: entry.svFrame!.column, + ); + + // Switch to the Generated SV tab. + _showLogicalTab(2, enableIfHidden: true); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Generated SV position found ' + '$svFile:${entry.svFrame!.line}:${entry.svFrame!.column}', + ), + duration: const Duration(seconds: 3), + ), + ); + } + + /// Handles "Go to SystemC" from schematic/editor context menus. + // The planned SystemC tab will register this navigation handler. + // ignore: unused_element + Future _onGoToSc(List paths) async { + if (paths.isEmpty) { + return; + } + final hasFlcData = await _ensureFlcDataLoaded(); + if (!mounted) { + return; + } + if (!hasFlcData) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No source-location data available for this component'), + duration: Duration(seconds: 2), + ), + ); + return; + } + + final path = paths.first; + final leaf = path.contains('/') ? path.split('/').last : path; + + final entry = _lookupFlcEntry(path); + final scFrame = entry == null ? null : _outputFrame(entry, 'sc'); + if (entry == null || scFrame == null) { + debugPrint('[GoToSC] "$leaf" not found in FlcData or has no SC frame'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No generated SystemC position for this symbol'), + duration: Duration(seconds: 2), + ), + ); + return; + } + + final scFile = scFrame.file.split('/').last; + _setScHighlightAndPendingScroll( + line: scFrame.line, + column: scFrame.column, + ); + + _showLogicalTab(3, enableIfHidden: true); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Generated SystemC position found ' + '$scFile:${scFrame.line}:${scFrame.column}', + ), + duration: const Duration(seconds: 3), + ), + ); + } + + Future _onGoToSourceFormat( + RohdSourceFormat format, + List paths, + ) async { + final formatLabel = switch (format) { + RohdSourceFormat.rohd => 'ROHD (Dart)', + RohdSourceFormat.sv => 'SystemVerilog', + RohdSourceFormat.sc => 'SystemC', + RohdSourceFormat.fst => 'Waveform (FST)', + }; + + switch (format) { + case RohdSourceFormat.rohd: + await _onGoToSource(paths); + case RohdSourceFormat.sv: + await _onGoToSv(paths); + case RohdSourceFormat.sc: + if (!mounted) { + return; + } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('SystemC generation is not available'), + duration: Duration(seconds: 2), + ), + ); + case RohdSourceFormat.fst: + if (!mounted) { + return; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '$formatLabel navigation is not available in Confapp', + ), + duration: const Duration(seconds: 2), + ), + ); + } + } + + /// Handle "Go to Source" from the schematic's context menu. + /// + /// If there are multiple ROHD stack-trace entries, shows a popup menu + /// to let the user pick. Then navigates both the ROHD source editor + /// and the SV editor to the corresponding locations, and shows a + /// snackbar summarizing what was found. + Future _onGoToSource(List paths) async { + if (paths.isEmpty) { + return; + } + final hasFlcData = await _ensureFlcDataLoaded(); + if (!mounted) { + return; + } + if (!hasFlcData) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No source-location data available for this component'), + duration: Duration(seconds: 2), + ), + ); + return; + } + + final candidates = >[]; + final seenPaths = {}; + for (final path in paths) { + if (!seenPaths.add(path)) { + continue; + } + final leaf = path.contains('/') ? path.split('/').last : path; + final entry = _lookupFlcEntry(path); + if (entry != null) { + candidates.add(MapEntry(path, entry)); + } else { + debugPrint('[GoToSource] "$leaf" not found in FlcData'); + } + } + + if (candidates.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No source position located'), + duration: Duration(seconds: 2), + ), + ); + return; + } + + MapEntry? selectedCandidate; + if (candidates.length == 1) { + selectedCandidate = candidates.first; + } else { + selectedCandidate = await _showTraceSelectionMenu(candidates); + if (selectedCandidate == null || !mounted) { + return; + } + } + + final selectedPath = selectedCandidate.key; + final leaf = selectedPath.contains('/') + ? selectedPath.split('/').last + : selectedPath; + final entry = selectedCandidate.value; + + final rohdFrames = entry.frames; + + if (rohdFrames.length > 1) { + // Multiple ROHD frames — show a selection popup. + await _showFrameSelectionMenu(leaf, entry); + } else { + // Single frame (or none) — navigate directly. + final rohdFrame = rohdFrames.isNotEmpty ? rohdFrames.first : null; + _navigateBothEditors(leaf, entry, rohdFrame); + } + } + + Future?> _showTraceSelectionMenu( + List> candidates, + ) async { + final items = >[]; + for (var i = 0; i < candidates.length; i++) { + final path = candidates[i].key; + final entry = candidates[i].value; + final traceCount = entry.frames.length; + final traceLabel = traceCount == 1 ? '1 trace' : '$traceCount traces'; + final svLabel = entry.svFrame == null + ? 'no SV' + : '${entry.svFrame!.file.split('/').last}:${entry.svFrame!.line}'; + items.add( + PopupMenuItem( + value: i, + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + '$path $traceLabel $svLabel', + style: const TextStyle(fontSize: 13), + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + + final pos = _lastPointerPosition; + final rect = RelativeRect.fromLTRB(pos.dx, pos.dy, pos.dx, pos.dy); + + if (!mounted) { + return null; + } + final selected = await showMenu( + context: context, + position: rect, + items: items, + ); + if (selected == null || !mounted) { + return null; + } + return candidates[selected]; + } + + /// Show a popup menu listing the ROHD frames so the user can pick one. + /// + /// Each menu item is enriched with the source line text loaded from + /// the bundled asset (similar to EnrichedFrame in the devtools extension). + Future _showFrameSelectionMenu( + String symbolName, + FlcEntry entry, + ) async { + // Reverse so outermost (module-level) frames appear first in the menu. + final rohdFrames = entry.frames.reversed.toList(); + + // Load source line text for each frame (prefix string). + final labels = []; + for (final frame in rohdFrames) { + final fileName = frame.file.split('/').last; + final locationStr = '$fileName:${frame.line}'; + final asset = _flcFileToAsset(frame.file); + if (asset != null) { + try { + // Use the cached future if already loaded, else load now. + _rohdFileFutures.putIfAbsent( + asset, + () => rootBundle.loadString(bundledSourceAssetPath(asset)), + ); + final source = await _rohdFileFutures[asset]!; + final lines = source.split('\n'); + if (frame.line > 0 && frame.line <= lines.length) { + final lineText = lines[frame.line - 1].trim(); + labels.add('$locationStr $lineText'); + } else { + labels.add(locationStr); + } + } on Object { + labels.add(locationStr); + } + } else { + labels.add(locationStr); + } + } + + final items = >[]; + for (var i = 0; i < rohdFrames.length; i++) { + items.add( + PopupMenuItem( + value: i, + height: 32, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + labels[i], + style: const TextStyle(fontSize: 13), + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + + // Position the menu near the last pointer-down (close to the + // right-click context menu that triggered "Go to Source"). + final pos = _lastPointerPosition; + final rect = RelativeRect.fromLTRB(pos.dx, pos.dy, pos.dx, pos.dy); + + if (!mounted) { + return; + } + final selected = await showMenu( + context: context, + position: rect, + items: items, + ); + if (selected == null || !mounted) { + return; // user dismissed + } + final rohdFrame = rohdFrames[selected]; + _navigateBothEditors(symbolName, entry, rohdFrame); } - Future _generateRTL(Configurator component, - {bool form = true}) async { - if (form && _formKey.currentState!.validate()) { - _formKey.currentState!.save(); + /// Navigate both the ROHD source editor and SV editor, then show a + /// snackbar summarizing what was found. + void _navigateBothEditors( + String symbolName, + FlcEntry entry, + FlcFrame? rohdFrame, + ) { + final messages = []; + + // Navigate ROHD source. + if (rohdFrame != null) { + final asset = _flcFileToAsset(rohdFrame.file); + final rohdFile = rohdFrame.file.split('/').last; + navigateToSource( + line: rohdFrame.line, + column: rohdFrame.column, + file: asset, + ); + messages.add( + 'ROHD source position found $rohdFile:' + '${rohdFrame.line}:${rohdFrame.column}', + ); + } + + // Navigate SV (set highlight + pending scroll; don't switch tabs). + if (entry.svFrame != null) { + final svFile = entry.svFrame!.file.split('/').last; + _setSvHighlightAndPendingScroll( + line: entry.svFrame!.line, + column: entry.svFrame!.column, + ); + messages.add( + 'Generated SV position found $svFile:' + '${entry.svFrame!.line}:${entry.svFrame!.column}', + ); + } else { + messages.add('No generated SV position for this symbol'); + } + + final scFrame = _outputFrame(entry, 'sc'); + if (scFrame != null) { + final scFile = scFrame.file.split('/').last; + _setScHighlightAndPendingScroll( + line: scFrame.line, + column: scFrame.column, + ); + messages.add( + 'Generated SystemC position found $scFile:' + '${scFrame.line}:${scFrame.column}', + ); + } + + if (messages.isEmpty) { + messages.add('No source position located'); + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(messages.join('\n')), + duration: const Duration(seconds: 3), + ), + ); + } + + /// Set the SV editor highlight and store pending scroll without switching + /// to the SV tab. The scroll will apply when the user switches to tab 2. + void _setSvHighlightAndPendingScroll({ + required int line, + int? column, + }) { + if (_svController == null) { + return; + } + final effectiveLine = line; + setState(() { + _highlightedSvLine = effectiveLine; + }); + final offset = _setSelectionAndComputeScroll( + _svController!, + line: effectiveLine, + column: column, + scrollController: _svScrollController, + ); + if (_tabController.index == 2 && _svScrollController.hasClients) { + // SV tab already visible — scroll now. + if (offset != null) { + _svScrollController.animateTo( + offset.clamp(0.0, _svScrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } else { + // Store for when the user switches to the SV tab. + _pendingSvScroll = offset; + _pendingSvLine = effectiveLine; + _pendingSvColumn = column; + } + } + + /// Set the SystemC editor highlight and store pending scroll without + /// switching to the SystemC tab. The scroll applies when tab 3 is visible. + void _setScHighlightAndPendingScroll({ + required int line, + int? column, + }) { + if (_scController == null) { + return; + } + final effectiveLine = line; + setState(() { + _highlightedScLine = effectiveLine; + }); + final offset = _setSelectionAndComputeScroll( + _scController!, + line: effectiveLine, + column: column, + scrollController: _scScrollController, + ); + if (_tabController.index == 3 && _scScrollController.hasClients) { + if (offset != null) { + _scScrollController.animateTo( + offset.clamp(0.0, _scScrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } else { + _pendingScScroll = offset; + _pendingScLine = effectiveLine; + _pendingScColumn = column; + } + } + + /// Handle cross-probe events arriving on the shared bus. + /// + /// Signal paths are hierarchical strings like `"top/counter/clk"`. + /// We strip each to its leaf name and look it up in the [FlcData] + /// to find SV and ROHD source line numbers. For "Send Signals" we + /// navigate automatically to the first/best frame without a popup. + void _onCrossProbeSignals() { + if (_suppressCrossProbeNav) { + return; + } + final paths = _editorCrossProbeService.incomingSignals.value; + if (paths == null || paths.isEmpty || _flcData == null) { + return; + } + + // Use the first signal path for highlighting. + final path = paths.first; + // Extract leaf signal name: "top/counter/clk" → "clk" + final leaf = path.contains('/') ? path.split('/').last : path; + + final entry = _lookupFlcEntry(leaf); + if (entry == null) { + debugPrint('[CrossProbe] signal "$leaf" not found in FlcData'); + return; + } + + // Navigate SV if available. + if (entry.svFrame != null) { + navigateToSV( + line: entry.svFrame!.line, + column: entry.svFrame!.column, + ); + debugPrint('[CrossProbe] scrolled SV to line ${entry.svFrame!.line}'); + } + + final scFrame = _outputFrame(entry, 'sc'); + if (scFrame != null) { + navigateToSC( + line: scFrame.line, + column: scFrame.column, + ); + debugPrint('[CrossProbe] scrolled SystemC to line ${scFrame.line}'); + } + + // Navigate ROHD source to the first frame in an available asset. + if (entry.frames.isNotEmpty) { + // If multiple ROHD frames are available, show the selection menu + // so the user can pick the desired call-site. Otherwise navigate + // directly to the single available frame. + final rohdFrames = entry.frames; + if (rohdFrames.length > 1) { + unawaited(_showFrameSelectionMenu(leaf, entry)); + } else { + final frame = rohdFrames.first; + final asset = _flcFileToAsset(frame.file); + navigateToSource(line: frame.line, column: frame.column, file: asset); + debugPrint( + '[CrossProbe] scrolled ROHD to line ${frame.line}' + ' in ${frame.file.split('/').last}', + ); + } } + } - final res = await component.generateSV(); + /// Builds a read-only code field wrapped in a [Stack] with a persistent + /// line-highlight overlay that is visible regardless of focus. + /// + /// [highlightLine] is 1-based (or null for no highlight). + /// + /// Uses a custom line-number gutter instead of the package's built-in one + /// so that line-numbers, code text, and highlight all share the same + /// vertical top offset ([_codeTopPad]). + Widget _highlightedCodeField({ + required SyntaxCodeController controller, + required FocusNode focusNode, + required ScrollController verticalScrollController, + required ScrollController horizontalScrollController, + Key? fieldKey, + int? highlightLine, + String? sourceFile, + }) { + final lineCount = controller.lineCount; + + return Listener( + onPointerDown: (event) { + // Track pointer for downstream popup positioning. + _lastPointerPosition = event.position; + if (event.buttons == 2) { + // Secondary (right) mouse button. Show our cross-probe menu + // immediately — the controller's selection is already up-to-date + // from the previous pointer events. + unawaited( + _showCodeContextMenu( + event.position, + controller, + sourceFile: sourceFile, + ), + ); + } + }, + child: browserContextMenuSuppressor( + child: LayoutBuilder( + builder: (context, constraints) { + final availableCodeWidth = constraints.maxWidth.isFinite + ? (constraints.maxWidth - 58).clamp(0.0, double.infinity) + : constraints.maxWidth; + final viewportWidth = _codeViewportWidthFor( + BoxConstraints(maxWidth: availableCodeWidth), + ); + final scrollExtentWidth = _measureCodeScrollExtentWidth( + controller.text, + ); - return res; + return Scrollbar( + controller: horizontalScrollController, + thumbVisibility: true, + trackVisibility: true, + interactive: true, + thickness: _codeScrollbarThickness, + radius: const Radius.circular(4), + scrollbarOrientation: ScrollbarOrientation.top, + notificationPredicate: (notification) => + notification.metrics.axis == Axis.horizontal, + child: Padding( + padding: const EdgeInsets.only(top: _codeScrollbarClearance), + child: SingleChildScrollView( + controller: verticalScrollController, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Custom gutter ────────────────────────────────────── + Padding( + padding: + const EdgeInsets.only(top: _codeTopPad, left: 8), + child: SizedBox( + width: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate( + lineCount, + (i) => SizedBox( + height: _lineHeight, + child: Text( + '${i + 1}', + style: _codeTextStyle.copyWith( + color: Colors.grey), + textAlign: TextAlign.right, + ), + )), + ), + ), + ), + const SizedBox(width: 10), + // ── Code + highlight overlay ─────────────────────── + // Keep the viewport near 80 columns. The scroll child is + // as wide as the longest line. Long generated + // assignments remain reachable without adding visual + // rows. The + // horizontal Scrollbar wraps the vertical + // viewport so its top track stays pinned, while the outer + // clearance keeps vertically scrolling code below it. + SizedBox( + width: viewportWidth, + child: SingleChildScrollView( + controller: horizontalScrollController, + scrollDirection: Axis.horizontal, + child: SizedBox( + width: scrollExtentWidth, + child: Stack( + children: [ + TextField( + key: fieldKey, + controller: controller, + focusNode: focusNode, + readOnly: true, + maxLines: null, + scrollPhysics: + const NeverScrollableScrollPhysics(), + smartDashesType: SmartDashesType.disabled, + smartQuotesType: SmartQuotesType.disabled, + style: _codeTextStyle, + decoration: const InputDecoration( + isCollapsed: true, + contentPadding: + EdgeInsets.symmetric(vertical: 16), + disabledBorder: InputBorder.none, + border: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + if (highlightLine != null) + Positioned( + top: _codeTopPad + + (highlightLine - 1) * _lineHeight, + left: 0, + right: 0, + height: _lineHeight, + child: IgnorePointer( + child: Container( + // semi-transparent amber + color: const Color(0x44FFC107), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); } - final yosysWorker = Worker('yosysWorker.js'); - var schematicHTML = ""; + void _invalidateGeneratedOutputs() { + final component = context.read().state; + context.read().initializeData(); + _disposeRohdSubtabs(); + setState(() { + _rohdNetlistJson = null; + _yosysJson = null; + _synthSchematicLoading = false; + _synthSchematicRequestId++; + _flcData = null; + _flcModuleName = null; + _hierarchyService = null; + _lastRohdSource = null; + _highlightedSvLine = null; + _highlightedRohdLine = null; + _highlightedScLine = null; + _rohdSourceFiles = []; + _lastBuiltModule = null; + _lastRtlRes = null; + _lastScRes = null; + _lastModuleName = null; + _lastEmbeddedFlcData = null; + _moduleName = ''; + }); + _initPrimaryRohdSubtab(component); + } - Widget _generateKnobControl(String label, ConfigKnob knob) { + Widget _generateKnobControl(String label, ConfigKnob knob) { final Widget selector; final decoration = InputDecoration( @@ -81,202 +2067,222 @@ class _SVGeneratorState extends State } return null; }, - inputFormatters: [ - FilteringTextInputFormatter.singleLineFormatter, - ], + inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], onChanged: (value) { - setState(() { - if (value.isEmpty) { - return; - } - - knob.setValueFromString(value); - }); + if (value.isEmpty) { + return; + } + knob.setValueFromString(value); + _invalidateGeneratedOutputs(); }, ); } else if (knob is ToggleConfigKnob) { - selector = CheckboxListTile( + selector = Row( key: key, - value: knob.value, - onChanged: (value) { - setState(() { - knob.value = value ?? knob.value; - }); - }, - secondary: Text( - label, - style: const TextStyle(fontSize: 14), - ), + children: [ + Checkbox( + value: knob.value, + onChanged: (value) { + if (value == null || value == knob.value) { + return; + } + knob.value = value; + _invalidateGeneratedOutputs(); + }, + ), + Flexible( + child: Text( + label, + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + ), + ], ); } else if (knob is ChoiceConfigKnob) { selector = DropdownButtonFormField( key: key, + isExpanded: true, decoration: decoration, items: knob.choices - .map((choice) => DropdownMenuItem( - value: choice, - child: Text(choice.toString().split('.').last), - )) + .map( + (choice) => DropdownMenuItem( + value: choice, + child: Text( + knob.labelFor(choice), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ) .toList(), onChanged: (value) { - setState(() { - knob.value = value ?? knob.value; - }); + if (value == null || value == knob.value) { + return; + } + knob.value = value; + _invalidateGeneratedOutputs(); }, - value: knob.value, + initialValue: knob.value, ); } else if (knob is ListOfKnobsKnob) { - selector = _containerOfKnobs(title: knob.name, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text('Add or remove: '), - IconButton( + selector = _containerOfKnobs( + title: knob.name, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text('Add or remove: '), + IconButton( onPressed: () { - setState(() { - knob.value += 1; - }); + knob.value += 1; + _invalidateGeneratedOutputs(); }, - icon: const Icon(Icons.add)), - IconButton( + icon: const Icon(Icons.add), + ), + IconButton( onPressed: () { - setState(() { - if (knob.value == 0) return; - knob.value -= 1; - }); + if (knob.value == 0) { + return; + } + knob.value -= 1; + _invalidateGeneratedOutputs(); }, - icon: const Icon(Icons.remove)), - ], - ), - for (final (index, subKnob) in knob.knobs.indexed) - // Animate size and add/remove transitions for each generated item. - // AnimatedSize handles height changes; AnimatedSwitcher provides - // a fade/size transition when items are added/removed. - AnimatedSize( - key: ValueKey('${knob.name}-$index-anim'), - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axisAlignment: 0.0, - child: child, - ), + icon: const Icon(Icons.remove), ), - child: Container( - key: ValueKey('${knob.name}-$index'), - child: subKnob is GroupOfKnobs - ? Column(children: [ - _containerOfKnobs( - title: '${subKnob.name} $index', - children: [ - for (final subKnobEntry - in subKnob.subKnobs.entries) - _generateKnobControl( - subKnobEntry.key, subKnobEntry.value), - ]), - const SizedBox(height: 12), - ]) - : Column(children: [ - _generateKnobControl('${knob.name} $index', subKnob), - const SizedBox(height: 12), - ]), + ], + ), + for (final (index, subKnob) in knob.knobs.indexed) + // Animate size and add/remove transitions for each generated item. + // AnimatedSize handles height changes; AnimatedSwitcher provides + // a fade/size transition when items are added/removed. + AnimatedSize( + key: ValueKey('${knob.name}-$index-anim'), + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + alignment: Alignment.center, + child: child, + ), + ), + child: Container( + key: ValueKey('${knob.name}-$index'), + child: subKnob is GroupOfKnobs + ? Column( + children: [ + _containerOfKnobs( + title: '${subKnob.name} $index', + children: [ + for (final subKnobEntry + in subKnob.subKnobs.entries) + _generateKnobControl( + subKnobEntry.key, + subKnobEntry.value, + ), + ], + ), + const SizedBox(height: 12), + ], + ) + : Column( + children: [ + _generateKnobControl( + '${knob.name} $index', + subKnob, + ), + const SizedBox(height: 12), + ], + ), + ), ), ), - ), - ]); + ], + ); } else if (knob is GroupOfKnobs) { - selector = _containerOfKnobs(title: knob.name, children: [ - for (final subKnobEntry in knob.subKnobs.entries) - _generateKnobControl(subKnobEntry.key, subKnobEntry.value), - ]); + selector = _containerOfKnobs( + title: knob.name, + children: [ + for (final subKnobEntry in knob.subKnobs.entries) + _generateKnobControl(subKnobEntry.key, subKnobEntry.value), + ], + ); } else { selector = Text('Unknown knob type for $label: ${knob.runtimeType}'); } - return Padding( - padding: const EdgeInsets.only(top: 10.0), - child: selector, - ); - } - - Widget _containerOfKnobs( - {required List children, required String title}) { - return InputDecorator( - decoration: InputDecoration( - labelText: title, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.0)), - ), - child: Padding( - padding: const EdgeInsets.all(1.0), - child: Column(children: children), - ), - ); + return Padding(padding: const EdgeInsets.only(top: 10), child: selector); } - Widget _generateJsonCard( - double screenHeight, - double screenWidth, - ) { - return Card( - child: Container( - constraints: BoxConstraints( - maxHeight: screenHeight * 0.8, maxWidth: screenWidth / 3), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: BlocBuilder( - builder: (context, component) { - final jsonCode = component.toJson(pretty: true); - return Column( - children: [ - _copyAndDownloadButtons( - isLoading: false, - code: jsonCode, - fileName: '${component.name}.config.json'), - Align( - alignment: Alignment.centerLeft, - child: SelectableText( - jsonCode, - style: GoogleFonts.robotoMono( - fontSize: 12, - ), - ), - ) - ], - ); - }), + Widget _containerOfKnobs({ + required List children, + required String title, + }) => + DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Theme.of(context).colorScheme.outline, ), ), - ), - ); - } + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 0), + child: Text( + title, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 4, 12, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + ], + ), + ); - Widget _copyAndDownloadButtons( - {required bool isLoading, - required String code, - required String fileName, - int maxChars = 10000}) { + Widget _copyAndDownloadButtons({ + required bool isLoading, + required String code, + required String fileName, + int maxChars = 1000000, + }) { final tooBig = code.length > maxChars; return Align( - alignment: Alignment.topRight, - child: isLoading - ? const CircularProgressIndicator() - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + alignment: Alignment.topRight, + child: isLoading + ? const CircularProgressIndicator() + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ ElevatedButton( onPressed: tooBig ? null - : () { - Clipboard.setData(ClipboardData(text: code)); + : () async { + await Clipboard.setData( + ClipboardData(text: code), + ); + if (!mounted) { + return; + } ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Text copied to clipboard')), + content: Text('Text copied to clipboard'), + ), ); }, child: tooBig @@ -285,121 +2291,1003 @@ class _SVGeneratorState extends State ), ElevatedButton( onPressed: () { - final bytes = base64Encode(code.codeUnits); - final uri = 'data:application/octet-stream;base64,$bytes'; - AnchorElement(href: uri) - ..setAttribute('download', fileName) - ..click(); + downloadFile(content: code, fileName: fileName); }, child: const Text('Download'), ), - ]))); - } - - Widget _generatedRtlCard(double screenHeight, double screenWidth) { - return Card( - child: Container( - constraints: BoxConstraints( - maxHeight: screenHeight * 0.8, maxWidth: screenWidth / 3), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: BlocBuilder( - builder: (context, state) { - final svCode = state.systemVerilog; - const maxChars = 10000; - final tooBig = svCode.length > maxChars; - return Column( - children: [ - if (state.generationState != GenerationState.initial) - _copyAndDownloadButtons( - isLoading: - state.generationState == GenerationState.loading, - code: svCode, - fileName: '${state.name}.sv'), - Align( - alignment: Alignment.centerLeft, - child: SelectableText( - tooBig - ? '${svCode.substring(0, maxChars)}' - '\n...too long to show' - : svCode, - style: GoogleFonts.robotoMono( - fontSize: 12, + ], + ), + ), + ); + } + + Widget _generatedRtlCard(double screenHeight, double screenWidth) => Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: BlocBuilder( + builder: (context, state) { + if (state.generationState == GenerationState.initial) { + return const Center( + child: Text( + 'Click "Generate" to see the generated SV', + style: TextStyle(fontSize: 16), + ), + ); + } + final svCode = state.systemVerilog; + _svController ??= HighlightCodeController( + text: svCode, + languageName: 'verilog', + language: highlight_verilog.verilog, + ); + _svController!.replaceSource(svCode); + return Column( + children: [ + _copyAndDownloadButtons( + isLoading: state.generationState == GenerationState.loading, + code: svCode, + fileName: '${state.name}.sv', + ), + Expanded( + child: _highlightedCodeField( + controller: _svController!, + focusNode: _svFocusNode, + verticalScrollController: _svScrollController, + horizontalScrollController: _svHorizontalScrollController, + fieldKey: const Key('generatedSV'), + highlightLine: _highlightedSvLine, + ), + ), + ], + ); + }, + ), + ), + ); + + // The planned SystemC tab will display this card. + // ignore: unused_element + Widget _generatedSystemCCard(double screenHeight, double screenWidth) => Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: BlocBuilder( + builder: (context, state) { + if (state.generationState == GenerationState.initial) { + return const Center( + child: Text( + 'Click "Generate" to see the generated SystemC', + style: TextStyle(fontSize: 16), + ), + ); + } + final scCode = state.systemC; + _scController ??= HighlightCodeController( + text: scCode, + languageName: 'cpp', + language: highlight_cpp.cpp, + ); + _scController!.replaceSource(scCode); + return Column( + children: [ + _copyAndDownloadButtons( + isLoading: state.generationState == GenerationState.loading, + code: scCode, + fileName: '${state.name}.sc', + ), + Expanded( + child: _highlightedCodeField( + controller: _scController!, + focusNode: _scFocusNode, + verticalScrollController: _scScrollController, + horizontalScrollController: _scHorizontalScrollController, + highlightLine: _highlightedScLine, + ), + ), + ], + ); + }, + ), + ), + ); + + Widget _generatedSchCard(double screenHeight, double screenWidth) => Card( + child: Column( + children: [ + if (_yosysJson != null) + _copyAndDownloadButtons( + isLoading: false, + code: _yosysJson!, + fileName: '$_moduleName.synth.json', + ), + Expanded( + child: Container( + alignment: Alignment.center, + child: BlocBuilder( + builder: (context, state) { + if (state.generationState == GenerationState.done && + _yosysJson != null) { + return BlocBuilder( + builder: (context, themeMode) { + final isDark = + themeMode == material_ui.ThemeMode.dark; + return Listener( + onPointerDown: (event) { + _lastPointerPosition = event.position; + }, + child: EmbeddedSchematicViewer.fromJson( + key: ValueKey( + ( + 'synth-schematic', + _canReceiveSourceCrossProbe, + ), + ), + schematicJson: _yosysJson!, + themeMode: isDark + ? SchematicThemeMode.dark + : SchematicThemeMode.light, + expansionMode: _expansionMode, + crossProbeService: _canReceiveSourceCrossProbe + ? _synthSchematicCrossProbeService + : null, + onGoToSourceCallback: _onGoToSourceFormat, + extensionClient: _sourceFormatClient, + ), + ); + }, + ); + } else if (state.generationState == GenerationState.done && + _synthSchematicLoading) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 12), + Text( + 'Synthesizing schematic...', + style: TextStyle(fontSize: 16), + ), + ], + ), + ); + } else { + return const Center( + child: Text( + 'Click "Generate" to see the synthesized schematic', + style: TextStyle(fontSize: 16), ), + ); + } + }, + ), + ), + ), + ], + ), + ); + + Widget _rohdSchematicCard(double screenHeight, double screenWidth) => Card( + child: Column( + children: [ + Row( + children: [ + if (_rohdNetlistJson != null) + Expanded( + child: _copyAndDownloadButtons( + isLoading: false, + code: _rohdNetlistJson!, + fileName: '$_moduleName.rohd.json', + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: DropdownButton( + value: _expansionMode, + underline: const SizedBox.shrink(), + isDense: true, + items: const [ + DropdownMenuItem( + value: SchematicExpansionMode.defaultView, + child: Text('Default'), ), - ) - ], - ); - }, + DropdownMenuItem( + value: SchematicExpansionMode.collapsed, + child: Text('Collapsed'), + ), + DropdownMenuItem( + value: SchematicExpansionMode.blocksOnly, + child: Text('Blocks Only'), + ), + DropdownMenuItem( + value: SchematicExpansionMode.fullyExpanded, + child: Text('Fully Expanded'), + ), + ], + onChanged: (mode) { + if (mode != null && mode != _expansionMode) { + setState(() { + _expansionMode = mode; + }); + } + }, + ), + ), + ], + ), + Expanded( + child: Container( + alignment: Alignment.center, + child: BlocBuilder( + builder: (context, state) { + if (state.generationState == GenerationState.done && + _rohdNetlistJson != null) { + return BlocBuilder( + builder: (context, themeMode) { + final isDark = + themeMode == material_ui.ThemeMode.dark; + return Listener( + onPointerDown: (event) { + _lastPointerPosition = event.position; + }, + child: EmbeddedSchematicViewer.fromJson( + key: ValueKey( + ( + 'rohd-schematic', + _canReceiveSourceCrossProbe, + ), + ), + schematicJson: _rohdNetlistJson!, + themeMode: isDark + ? SchematicThemeMode.dark + : SchematicThemeMode.light, + expansionMode: _expansionMode, + crossProbeService: _canReceiveSourceCrossProbe + ? _rohdSchematicCrossProbeService + : null, + onGoToSourceCallback: _onGoToSourceFormat, + extensionClient: _sourceFormatClient, + ), + ); + }, + ); + } else { + return const Center( + child: Text( + 'Click "Generate" to see the ROHD schematic', + style: TextStyle(fontSize: 16), + ), + ); + } + }, + ), + ), ), + ], + ), + ); + + Widget _rohdSourceCard(double screenHeight, double screenWidth) => Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: _rohdSourceFiles.isNotEmpty + ? _rohdSourceSubtabs() + : BlocBuilder( + builder: (context, component) { + final mod = component.createModule(); + final assetPath = moduleSourceAsset(mod); + if (assetPath == null) { + return Center( + child: + Text('No source available for ${mod.runtimeType}'), + ); + } + return _rohdSourceFileView(assetPath); + }, + ), + ), + ); + + /// The subtab view for ROHD source files. Separated so it doesn't get + /// rebuilt inside a BlocBuilder — TabBarView needs stable widget identity + /// during page animations. + Widget _rohdSourceSubtabs() { + final tabCtrl = _rohdSourceTabController; + if (tabCtrl == null) { + return const Center(child: CircularProgressIndicator()); + } + + // Ensure a stable GlobalKey so that Flutter preserves the TabBarView + // element across parent setState rebuilds (highlight changes, etc.). + _subtabViewKey ??= GlobalKey(); + + return Column( + children: [ + TabBar( + controller: tabCtrl, + isScrollable: true, + tabs: _rohdSourceFiles + .map((p) => Tab(text: p.split('/').last)) + .toList(), + ), + Expanded( + child: TabBarView( + key: _subtabViewKey, + controller: tabCtrl, + children: _rohdSourceFiles + .map( + (p) => KeyedSubtree( + key: ValueKey(p), + child: _rohdSourceFileView(p), + ), + ) + .toList(), ), ), + ], + ); + } + + /// Build a single ROHD source file viewer for asset at [assetPath]. + Widget _rohdSourceFileView(String assetPath) { + // Cache the future so FutureBuilder doesn't restart on every rebuild. + _rohdFileFutures.putIfAbsent( + assetPath, + () => rootBundle.loadString(bundledSourceAssetPath(assetPath)), + ); + _rohdSourceViewFutures.putIfAbsent( + assetPath, + () async => ( + source: await _rohdFileFutures[assetPath]!, + syntaxHighlighter: await _dartSyntaxHighlighter, + ), + ); + return FutureBuilder<_RohdSourceDocument>( + future: _rohdSourceViewFutures[assetPath], + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center( + child: Text('Error preparing source view: ${snapshot.error}'), + ); + } + final document = snapshot.data!; + final source = document.source; + + // Create or update per-file controller. + if (!_rohdFileControllers.containsKey(assetPath)) { + _rohdFileControllers[assetPath] = DartSyntaxCodeController( + text: source, + syntaxHighlighter: document.syntaxHighlighter, + ); + } + _rohdFileScrollControllers.putIfAbsent( + assetPath, + ScrollController.new, + ); + _rohdFileHorizontalScrollControllers.putIfAbsent( + assetPath, + ScrollController.new, + ); + _rohdFileFocusNodes.putIfAbsent(assetPath, FocusNode.new); + + final ctrl = _rohdFileControllers[assetPath]!; + final scrollCtrl = _rohdFileScrollControllers[assetPath]!; + final horizontalScrollCtrl = + _rohdFileHorizontalScrollControllers[assetPath]!; + final focusNode = _rohdFileFocusNodes[assetPath]!; + final highlightLine = _rohdFileHighlightLines[assetPath]; + + // Also keep legacy single-controller in sync for backwards compat. + if (_rohdSourceFiles.isNotEmpty && + assetPath == _rohdSourceFiles.first) { + _rohdSourceController = ctrl; + } + + // Apply any pending scroll — schedule via postFrameCallback so + // the SingleChildScrollView has attached to scrollCtrl. + final pendingScroll = _rohdFilePendingScrolls[assetPath]; + if (pendingScroll != null) { + // Recompute the offset now that the controller exists (the stored + // value may be a placeholder 0 from navigateToSource). + final hl = _rohdFileHighlightLines[assetPath]; + final effectiveScroll = (hl != null) + ? (_setSelectionAndComputeScroll( + ctrl, + line: hl, + scrollController: scrollCtrl, + ) ?? + pendingScroll) + : pendingScroll; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollCtrl.hasClients) { + _rohdFilePendingScrolls[assetPath] = null; + scrollCtrl.animateTo( + effectiveScroll.clamp(0.0, scrollCtrl.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _copyAndDownloadButtons( + isLoading: false, + code: source, + fileName: assetPath.split('/').last, + ), + Expanded( + child: _highlightedCodeField( + controller: ctrl, + focusNode: focusNode, + verticalScrollController: scrollCtrl, + horizontalScrollController: horizontalScrollCtrl, + highlightLine: highlightLine, + sourceFile: _assetToFlcFile(assetPath), + ), + ), + ], + ); + }, + ); + } + + // ── Generation subroutines ───────────────────────────────────────────── + // + // Each subroutine handles one output tab's generation step. + // The Generate button calls all enabled steps; lazy enable of a tab + // can call its step individually using cached state. + + /// Step: Build the module and generate SystemVerilog (base output). + /// Populates [_lastBuiltModule], generated text, and module name. + Future _generateBase( + Configurator component, { + required int generationRequestId, + }) async { + final mod = component.createModule(); + await mod.build(); + if (!_isCurrentGeneration(generationRequestId)) { + return false; + } + final moduleName = mod.definitionName; + final rtlRes = mod.generateSynth(); + + _rohdNetlistJson = null; + _hierarchyService = null; + _flcData = null; + _flcModuleName = null; + _lastEmbeddedFlcData = null; + _lastBuiltModule = mod; + _lastRtlRes = rtlRes; + _lastScRes = 'SystemC generation is unavailable with this ROHD version.'; + _lastModuleName = moduleName; + return true; + } + + /// Step: Generate ROHD netlist JSON → Tab 0 (ROHD Schematic). + Future _generateRohdSchematic({ + int? generationRequestId, + }) async { + final requestId = generationRequestId ?? _generationRequestId; + final mod = _lastBuiltModule; + if (mod == null || !_isCurrentGeneration(requestId)) { + return; + } + final synthesizer = NetlistSynthesizer(); + final builder = SynthBuilder(mod, synthesizer); + final netlistJson = synthesizer.generateCombinedJson(builder, mod); + if (!_isCurrentGeneration(requestId)) { + return; + } + setState(() { + _rohdNetlistJson = netlistJson; + _moduleName = _lastModuleName ?? mod.definitionName; + try { + _hierarchyService = NetlistHierarchyAdapter.fromJson(netlistJson); + } on FormatException { + _hierarchyService = null; + } + }); + } + + /// Step: Run Yosys synthesis → Tab 4 (Synth Schematic). + void _generateSynthSchematic() { + final moduleName = _lastModuleName; + final rtlRes = _lastRtlRes; + if (moduleName == null || rtlRes == null) { + return; + } + final requestId = ++_synthSchematicRequestId; + setState(() { + _synthSchematicLoading = true; + }); + unawaited( + _runYosysSynthesis( + requestId: requestId, + moduleName: moduleName, + rtl: rtlRes, ), ); } - Widget _generatedSchCard(double screenHeight, double screenWidth) { - return Card( - child: Container( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: BlocBuilder( - builder: (context, state) { - if (state.generationState == GenerationState.done) { - return const Card( - child: HtmlElementView( - viewType: 'schematic-html', - )); - } else { - return const Padding(padding: EdgeInsets.all(16.0)); - } - })))); + Future _runYosysSynthesis({ + required int requestId, + required String moduleName, + required String rtl, + }) async { + try { + final message = await yosysWorker.synthesize({ + 'module': moduleName, + 'verilog': rtl, + }); + if (!mounted || requestId != _synthSchematicRequestId) { + return; + } + setState(() { + _synthSchematicLoading = false; + _yosysJson = message; + }); + } on Object catch (error, stackTrace) { + if (!mounted || requestId != _synthSchematicRequestId) { + return; + } + setState(() { + _synthSchematicLoading = false; + }); + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stackTrace, + library: 'confapp', + context: ErrorDescription( + 'while synthesizing a schematic with Yosys', + ), + ), + ); + } } - Widget _genRtlButton(SystemVerilogCubit rtlCubit, Configurator component) { - return ElevatedButton( - key: const Key('generateRTL'), - onPressed: () async { - try { - rtlCubit.setLoading(); + /// Cache source trace data embedded by a trace-enabled ROHD netlist. + /// + /// ROHD 0.6.11 does not provide a runtime trace registry. Future ROHD + /// versions can embed `rohd.src_trace` attributes in the generated netlist, + /// which keeps this integration dormant but functional when data is present. + FlcData? _captureEmbeddedFlcData() { + final cached = _lastEmbeddedFlcData; + if (cached != null) { + return cached; + } + final netlistJson = _rohdNetlistJson; + if (netlistJson == null) { + return null; + } - // allow some time for loading spinner to appear - await Future.delayed(const Duration(milliseconds: 10)); + final flcData = embeddedFlcDataFromNetlistJson(netlistJson); + _lastEmbeddedFlcData = flcData; + if (flcData != null) { + debugPrint( + '[FlcData] Loaded embedded netlist trace ' + '(${flcData.files.length} files, ' + 'modules: ${flcData.moduleNames.join(', ')})', + ); + } + return flcData; + } - final rtlRes = await _generateRTL(component); - final moduleName = component.createModule().definitionName; + /// Step: Load embedded FLC trace data → Tab 1 (ROHD Source / cross-probe). + Future _generateFlcTrace( + Configurator component, { + int? generationRequestId, + }) async { + final requestId = generationRequestId ?? _generationRequestId; + final rtlRes = _lastRtlRes; + final moduleName = _lastModuleName; + if (rtlRes == null || + moduleName == null || + !_isCurrentGeneration(requestId)) { + return; + } - yosysWorker.postMessage({'module': moduleName, 'verilog': rtlRes}); + if (_rohdNetlistJson == null) { + await _generateRohdSchematic(generationRequestId: requestId); + if (!_isCurrentGeneration(requestId)) { + return; + } + } + final flcData = _captureEmbeddedFlcData(); - yosysWorker.onMessage.first.then((msg) { - schematicHTML = d3Schematic(msg.data); - ui_web.platformViewRegistry.registerViewFactory( - 'schematic-html', - (int viewID) => IFrameElement() - ..height = '100%' - ..width = '100%' - ..srcdoc = schematicHTML - ..style.border = 'none'); - }); + await _buildCrossProbeAndHighlight( + component, + rtlRes, + moduleName, + capturedFlcData: flcData, + generationRequestId: requestId, + ); + } + + // ── End generation subroutines ──────────────────────────────────────── + + /// Dispatch the content widget for a given logical tab [index]. + Widget _tabContentForIndex( + int index, double screenHeight, double screenWidth) { + switch (index) { + case 0: + return _rohdSchematicCard(screenHeight, screenWidth); + case 1: + return _rohdSourceCard(screenHeight, screenWidth); + case 2: + return _generatedRtlCard(screenHeight, screenWidth); + case 3: + return _generatedSchCard(screenHeight, screenWidth); + default: + return const SizedBox.shrink(); + } + } + + /// Lazily generate data for a newly-enabled tab using cached module. + Future _lazyGenerate(int tabIndex) async { + switch (tabIndex) { + case 0: + if (_rohdNetlistJson == null) { + await _generateRohdSchematic(); + } + case 1: + if (_flcData == null) { + final component = context.read().state; + await _generateFlcTrace(component); + } + _scheduleSourceFormatRefresh(); + case 3: + if (_yosysJson == null && !_synthSchematicLoading) { + _generateSynthSchematic(); + } + // cases 2 and 3 are generated by _generateBase. + } + } + + Widget _genRtlButton(SystemVerilogCubit rtlCubit, Configurator component) => + BlocBuilder( + builder: (context, state) { + final isLoading = state.generationState == GenerationState.loading; + return ElevatedButton( + key: const Key('generateRTL'), + onPressed: isLoading + ? null + : () async { + final generationRequestId = ++_generationRequestId; + try { + rtlCubit.setLoading(); + + // allow some time for loading spinner to appear + await Future.delayed( + const Duration(milliseconds: 10), + ); + if (!_isCurrentGeneration(generationRequestId)) { + return; + } + + // Validate and save form state. + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + } + + // Base: always build the module and generate + // SystemVerilog. + if (!await _generateBase( + component, + generationRequestId: generationRequestId, + )) { + return; + } + + // Tab 0: ROHD Schematic + if (_tabEnabled[0]) { + await _generateRohdSchematic( + generationRequestId: generationRequestId, + ); + if (!_isCurrentGeneration(generationRequestId)) { + return; + } + } + + // Tab 4: Synth Schematic (Yosys) + if (_tabEnabled[3]) { + _generateSynthSchematic(); + } + + // Retain embedded trace data, when supplied by a future + // trace-enabled ROHD netlist, even if Source is hidden. + _captureEmbeddedFlcData(); + + // Tab 1: ROHD Source (FLC/cross-probe) + if (_tabEnabled[1]) { + await _generateFlcTrace( + component, + generationRequestId: generationRequestId, + ); + if (!_isCurrentGeneration(generationRequestId)) { + return; + } + } + + final rtlRes = _lastRtlRes; + final scRes = _lastScRes; + final moduleName = _lastModuleName; + if (rtlRes == null || + scRes == null || + moduleName == null) { + return; + } + + // allow some time for registration to happen + rtlCubit.setLoading(); + await Future.delayed( + const Duration(milliseconds: 200), + ); + if (!_isCurrentGeneration(generationRequestId)) { + return; + } + + rtlCubit.setRTL( + rtlRes, + scRes, + component.sanitaryName, + moduleName, + ); + _scheduleSourceFormatRefresh(); + } on Exception catch (e) { + if (!_isCurrentGeneration(generationRequestId)) { + return; + } + var message = e.toString(); + if (e is RohdHclException) { + message = e.message; + } + rtlCubit.setRTL( + 'Error generating:\n\n$message', + 'Error generating:\n\n$message', + 'error', + '', + ); + } + }, + style: btnStyle, + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Generate'), + ); + }, + ); + + /// Load the ROHD source and populate its subtabs from embedded [FlcData]. + Future _buildCrossProbeAndHighlight( + Configurator component, + String svText, + String moduleName, { + FlcData? capturedFlcData, + int? generationRequestId, + }) async { + final requestId = generationRequestId ?? _generationRequestId; + // Resolve the ROHD source asset path for this component. + final mod = component.createModule(); + final primaryAsset = moduleSourceAsset(mod); + if (primaryAsset == null) { + return; + } + + try { + final rohdSource = + await rootBundle.loadString(bundledSourceAssetPath(primaryAsset)); + if (!_isCurrentGeneration(requestId)) { + return; + } + _lastRohdSource = rohdSource; + + final flcData = capturedFlcData ?? FlcData.empty(); + if (capturedFlcData != null) { + debugPrint( + '[FlcData] Built from embedded netlist trace ' + '(${flcData.files.length} files, ' + 'modules: ${flcData.moduleNames.join(', ')})', + ); + } else { + debugPrint( + '[FlcData] No embedded trace data available; ' + 'ROHD 0.6.11 does not emit source traces', + ); + } + + // Build the set of available rohd_src asset paths from the FLC files. + final availableAssets = {primaryAsset}; + for (final fp in flcData.files) { + final asset = _flcFileToAsset(fp); + if (asset != null && !asset.startsWith('rohd_src/component_config/')) { + availableAssets.add(asset); + } + } - // allow some time for registration to happen - rtlCubit.setLoading(); - await Future.delayed(const Duration(milliseconds: 200)); + // Collect distinct ROHD source files for subtabs. + final sortedFiles = [primaryAsset]; + final others = availableAssets.where((a) => a != primaryAsset).toList() + ..sort(); + sortedFiles.addAll(others); - rtlCubit.setRTL(rtlRes, component.sanitaryName, moduleName); - } on Exception catch (e) { - var message = e.toString(); - if (e is RohdHclException) { - message = e.message; + // Reuse existing subtab state if the file list hasn't changed. + final filesChanged = !_listsEqual(_rohdSourceFiles, sortedFiles) || + _rohdSourceTabController == null; + if (filesChanged) { + _disposeRohdSubtabs(); + + final subtabCtrl = TabController( + length: sortedFiles.length, + vsync: this, + ); + subtabCtrl.addListener(() { + if (!subtabCtrl.indexIsChanging) { + _applyPendingRohdSubtabScroll(subtabCtrl.index); } - rtlCubit.setRTL('Error generating:\n\n$message', 'error', ''); + }); + + setState(() { + _flcData = flcData; + _flcModuleName = moduleName; + _rohdSourceFiles = sortedFiles; + _rohdSourceTabController = subtabCtrl; + _highlightedSvLine = null; + _highlightedScLine = null; + _highlightedRohdLine = null; + }); + } else { + setState(() { + _flcData = flcData; + _flcModuleName = moduleName; + _highlightedSvLine = null; + _highlightedScLine = null; + _highlightedRohdLine = null; + }); + } + } on Exception catch (e) { + // Non-fatal: cross-probe is optional. + // ignore: avoid_print + print('[FlcData] failed to build: $e'); + } + } + + /// Apply a pending scroll for the ROHD source subtab at [subtabIndex]. + void _applyPendingRohdSubtabScroll(int subtabIndex) { + if (subtabIndex < 0 || subtabIndex >= _rohdSourceFiles.length) { + return; + } + final file = _rohdSourceFiles[subtabIndex]; + final pending = _rohdFilePendingScrolls[file]; + if (pending == null) { + return; + } + + final scrollCtrl = _rohdFileScrollControllers[file]; + final ctrl = _rohdFileControllers[file]; + final highlightLine = _rohdFileHighlightLines[file]; + + // Re-apply selection and recompute the scroll offset now that the + // controller is available (the stored `pending` may be a placeholder 0 + // from when the controller didn't exist yet). + var effectiveOffset = pending; + if (ctrl != null && highlightLine != null) { + effectiveOffset = _setSelectionAndComputeScroll( + ctrl, + line: highlightLine, + scrollController: scrollCtrl, + ) ?? + pending; + } + + void attemptScroll(int retries) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollCtrl != null && scrollCtrl.hasClients) { + _rohdFilePendingScrolls[file] = null; + scrollCtrl.animateTo( + effectiveOffset.clamp(0.0, scrollCtrl.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else if (retries > 0) { + attemptScroll(retries - 1); } - }, - style: btnStyle, - child: const Text('Generate RTL'), + }); + } + + attemptScroll(10); + } + + /// Dispose all per-file subtab controllers and reset state. + void _disposeRohdSubtabs() { + // Capture references to old resources and clear the maps immediately + // so new widgets won't see stale entries. + final oldControllers = _rohdFileControllers.values.toList(); + final oldScrollControllers = _rohdFileScrollControllers.values.toList(); + final oldHorizontalScrollControllers = + _rohdFileHorizontalScrollControllers.values.toList(); + final oldFocusNodes = _rohdFileFocusNodes.values.toList(); + final oldTabController = _rohdSourceTabController; + + _rohdFileControllers.clear(); + _rohdFileScrollControllers.clear(); + _rohdFileHorizontalScrollControllers.clear(); + _rohdFileFocusNodes.clear(); + _rohdFileHighlightLines.clear(); + _rohdFilePendingScrolls.clear(); + _rohdFileFutures.clear(); + _rohdSourceViewFutures.clear(); + _rohdSourceController = null; + _subtabViewKey = null; + _rohdSourceTabController = null; + + // Defer actual disposal to after the current frame so the widget tree + // can cleanly detach from these resources during rebuild. + WidgetsBinding.instance.addPostFrameCallback((_) { + for (final c in oldControllers) { + c.dispose(); + } + for (final sc in oldScrollControllers) { + sc.dispose(); + } + for (final sc in oldHorizontalScrollControllers) { + sc.dispose(); + } + for (final fn in oldFocusNodes) { + fn.dispose(); + } + oldTabController?.dispose(); + }); + } + + @override + void dispose() { + unawaited(_componentSubscription.cancel()); + _tabController.dispose(); + _svController?.dispose(); + _scController?.dispose(); + _svFocusNode.dispose(); + _scFocusNode.dispose(); + _rohdSourceFocusNode.dispose(); + _svScrollController.dispose(); + _scScrollController.dispose(); + _rohdSourceScrollController.dispose(); + _configScrollController.dispose(); + _svHorizontalScrollController.dispose(); + _scHorizontalScrollController.dispose(); + // Dispose subtab resources synchronously on final teardown. + for (final c in _rohdFileControllers.values) { + c.dispose(); + } + for (final sc in _rohdFileScrollControllers.values) { + sc.dispose(); + } + for (final sc in _rohdFileHorizontalScrollControllers.values) { + sc.dispose(); + } + for (final fn in _rohdFileFocusNodes.values) { + fn.dispose(); + } + _rohdSourceTabController?.dispose(); + _editorCrossProbeService.incomingSignals.removeListener( + _onCrossProbeSignals, ); + _rohdSchematicCrossProbeService.dispose(); + _synthSchematicCrossProbeService.dispose(); + _editorCrossProbeService.dispose(); + _crossProbeChannel.dispose(); + _sourceFormatClient.dispose(); + yosysWorker.dispose(); + super.dispose(); } @override @@ -408,83 +3296,213 @@ class _SVGeneratorState extends State final screenWidth = MediaQuery.of(context).size.width; final screenHeight = MediaQuery.of(context).size.height; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - BlocBuilder( - builder: (context, component) { - return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - margin: const EdgeInsets.all(10), - constraints: BoxConstraints( - maxHeight: screenHeight * 0.8, - maxWidth: screenWidth / 3, - ), - child: Card( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, + return LayoutBuilder( + builder: (context, constraints) { + final totalWidth = constraints.maxWidth; + const dividerWidth = 8.0; + const minimumLeftWidth = 150.0; + const minimumRightWidth = 300.0; + const minimumContentWidth = + minimumLeftWidth + dividerWidth + minimumRightWidth; + final contentWidth = + totalWidth < minimumContentWidth ? minimumContentWidth : totalWidth; + final leftWidth = (contentWidth * _splitFraction).clamp( + minimumLeftWidth, + contentWidth - minimumRightWidth - dividerWidth, + ); + final rightWidth = contentWidth - leftWidth - dividerWidth; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: contentWidth, + child: Row( + children: [ + SizedBox( + width: leftWidth, + child: BlocBuilder( + builder: (context, component) => Column( children: [ - // Add a title - Text( - component.name, - style: const TextStyle( - fontSize: 25, - fontWeight: FontWeight.bold, + Expanded( + child: Container( + margin: const EdgeInsets.all(10), + child: Card( + child: Scrollbar( + controller: _configScrollController, + thumbVisibility: true, + child: SingleChildScrollView( + controller: _configScrollController, + child: Padding( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Add a title + Text( + component.name, + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + for (final knobEntry + in component.knobs.entries) + _generateKnobControl( + knobEntry.key, + knobEntry.value, + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ), + ), + ), ), ), - for (var knobEntry in component.knobs.entries) - _generateKnobControl(knobEntry.key, knobEntry.value), - const SizedBox( - height: 16, - ), + const SizedBox(height: 8), + _genRtlButton(rtlCubit, component), + const SizedBox(height: 8), ], ), ), - )), - ), - ), - _genRtlButton(rtlCubit, component), - ]); - }), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Card( - child: Container( - constraints: BoxConstraints( - maxHeight: screenHeight * 0.85, - maxWidth: screenWidth / 3), - child: DefaultTabController( - length: 3, + ), + // ---- Draggable vertical divider ---- + GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragUpdate: (details) { + setState(() { + _splitFraction = + ((_splitFraction * contentWidth + details.delta.dx) / + contentWidth) + .clamp( + minimumLeftWidth / contentWidth, + (contentWidth - minimumRightWidth - dividerWidth) / + contentWidth, + ); + }); + }, + child: MouseRegion( + cursor: SystemMouseCursors.resizeColumn, + child: SizedBox( + width: dividerWidth, + child: Center( + child: Container( + width: 4, + height: 40, + decoration: BoxDecoration( + color: Theme.of(context).dividerColor, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + ), + ), + // ---- Right (viewer) pane ---- + SizedBox( + width: rightWidth, + child: Card( + child: Container( + margin: const EdgeInsets.all(10), child: Scaffold( appBar: AppBar( title: const Text('Generated Outputs'), - bottom: const TabBar( - isScrollable: false, + actions: [ + // Tab-visibility checkboxes + for (var i = 0; i < _tabLabels.length; i++) + Tooltip( + message: _tabTooltips[i], + child: InkWell( + borderRadius: BorderRadius.circular(4), + onTap: () { + if (_tabEnabled[i] && + _visibleTabs.length == 1) { + return; + } + setState(() { + _tabEnabled[i] = !_tabEnabled[i]; + _rebuildTabController( + selectLogical: + _tabEnabled[i] ? i : null, + ); + }); + // Lazy generation when a tab is enabled. + if (_tabEnabled[i] && + _lastBuiltModule != null) { + unawaited(_lazyGenerate(i)); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 2, vertical: 4), + child: Opacity( + opacity: _tabEnabled[i] ? 1.0 : 0.35, + child: _tabIconWidget(i), + ), + ), + ), + ), + const SizedBox(width: 8), + BlocBuilder( + builder: (context, themeMode) { + final isDark = + themeMode == material_ui.ThemeMode.dark; + return Tooltip( + message: isDark + ? 'Switch to light theme' + : 'Switch to dark theme', + child: IconButton( + icon: Text( + isDark ? '☀️' : '🌙', + style: const TextStyle(fontSize: 20), + ), + onPressed: () { + context.read().toggleTheme(); + }, + ), + ); + }, + ), + ], + bottom: TabBar( + controller: _tabController, + isScrollable: true, tabs: [ - Tab(text: 'Generated RTL'), - Tab(text: 'Generated Schematic'), - Tab(text: 'JSON Configuration'), + for (final i in _visibleTabs) + Tab(child: _tabLabelWidget(i)), ], ), ), - body: TabBarView( + body: IndexedStack( + index: _tabController.index, children: [ - _generatedRtlCard(screenHeight, screenWidth), - _generatedSchCard(screenHeight, screenWidth), - _generateJsonCard(screenHeight, screenWidth), + for (final i in _visibleTabs) + _tabContentForIndex(i, screenHeight, screenWidth), ], ), ), - ))), - ], - ), - ], + ), + ), + ), + ], + ), + ), + ); + }, ); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('btnStyle', btnStyle)) + ..add( + DiagnosticsProperty('yosysWorker', yosysWorker), + ); + } } diff --git a/confapp/lib/hcl/view/screen/dart_syntax_code_controller.dart b/confapp/lib/hcl/view/screen/dart_syntax_code_controller.dart new file mode 100644 index 0000000000..3666166d50 --- /dev/null +++ b/confapp/lib/hcl/view/screen/dart_syntax_code_controller.dart @@ -0,0 +1,242 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// dart_syntax_code_controller.dart +// Syntax-highlighting controllers for read-only generated source. +// +// 2026 September 9 +// Author: Desmond A. Kirkpatrick + +import 'package:highlight/highlight_core.dart' show Highlight, Mode, Node; +import 'package:material_ui/material_ui.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; + +/// A text controller with source-line navigation support. +abstract class SyntaxCodeController extends TextEditingController { + /// Creates a controller containing [text]. + SyntaxCodeController({required String text}) : super(text: text); + + late List _lineRanges = _computeLineRanges(text); + + /// The number of source lines. + int get lineCount => _lineRanges.length; + + /// Returns the text range for zero-based [lineIndex]. + TextRange? lineRangeAt(int lineIndex) => + lineIndex >= 0 && lineIndex < _lineRanges.length + ? _lineRanges[lineIndex] + : null; + + /// Replaces the displayed source while keeping this controller attached. + void replaceSource(String source) { + if (source == text) { + return; + } + + _lineRanges = _computeLineRanges(source); + value = TextEditingValue(text: source); + } + + static List _computeLineRanges(String source) { + final ranges = []; + var start = 0; + for (final match in '\n'.allMatches(source)) { + ranges.add(TextRange(start: start, end: match.start)); + start = match.end; + } + ranges.add(TextRange(start: start, end: source.length)); + return ranges; + } +} + +/// Highlights source using a language from `package:highlight`. +class HighlightCodeController extends SyntaxCodeController { + /// Creates a controller for [text] written in [language]. + HighlightCodeController({ + required String text, + required String languageName, + required Mode language, + }) : _languageName = languageName, + _language = language, + super(text: text) { + _highlighter.registerLanguage(_languageName, _language); + _nodes = _parse(text); + } + + final String _languageName; + final Mode _language; + final Highlight _highlighter = Highlight(); + late List _nodes; + + static const _lightStyles = { + 'root': TextStyle(color: Color(0xFF333333)), + 'comment': TextStyle(color: Color(0xFF6A737D)), + 'quote': TextStyle(color: Color(0xFF6A737D)), + 'keyword': TextStyle(color: Color(0xFF005CC5)), + 'literal': TextStyle(color: Color(0xFF005CC5)), + 'type': TextStyle(color: Color(0xFF6F42C1)), + 'number': TextStyle(color: Color(0xFF005CC5)), + 'string': TextStyle(color: Color(0xFF032F62)), + 'title': TextStyle(color: Color(0xFF6F42C1)), + 'built_in': TextStyle(color: Color(0xFF005CC5)), + 'meta': TextStyle(color: Color(0xFF6A737D)), + 'variable': TextStyle(color: Color(0xFFE36209)), + 'attr': TextStyle(color: Color(0xFF22863A)), + }; + + static const _darkStyles = { + 'root': TextStyle(color: Color(0xFFDCDCDC)), + 'comment': TextStyle(color: Color(0xFF6A9955)), + 'quote': TextStyle(color: Color(0xFF6A9955)), + 'keyword': TextStyle(color: Color(0xFF569CD6)), + 'literal': TextStyle(color: Color(0xFF569CD6)), + 'type': TextStyle(color: Color(0xFF4EC9B0)), + 'number': TextStyle(color: Color(0xFFB5CEA8)), + 'string': TextStyle(color: Color(0xFFCE9178)), + 'title': TextStyle(color: Color(0xFFDCDCAA)), + 'built_in': TextStyle(color: Color(0xFF4EC9B0)), + 'meta': TextStyle(color: Color(0xFF9B9B9B)), + 'variable': TextStyle(color: Color(0xFFC586C0)), + 'attr': TextStyle(color: Color(0xFF9CDCFE)), + }; + + List _parse(String source) => + _highlighter.parse(source, language: _languageName).nodes ?? + [Node(value: source)]; + + @override + void replaceSource(String source) { + if (source == text) { + return; + } + _nodes = _parse(source); + super.replaceSource(source); + } + + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + bool? withComposing, + }) { + final styles = Theme.of(context).brightness == Brightness.dark + ? _darkStyles + : _lightStyles; + return TextSpan( + style: (styles['root'] ?? const TextStyle()).merge(style), + children: _convertNodes(_nodes, styles), + ); + } + + static List _convertNodes( + List nodes, + Map styles, + ) => + [ + for (final node in nodes) + TextSpan( + text: node.value, + style: node.className == null ? null : styles[node.className], + children: node.children == null + ? null + : _convertNodes(node.children!, styles), + ), + ]; +} + +/// Loads and applies the TextMate grammar used for Dart source highlighting. +class DartSyntaxHighlighter { + DartSyntaxHighlighter._({ + required Highlighter lightHighlighter, + required Highlighter darkHighlighter, + }) : _lightHighlighter = lightHighlighter, + _darkHighlighter = darkHighlighter; + + static Future? _loadFuture; + + final Highlighter _lightHighlighter; + final Highlighter _darkHighlighter; + + /// Loads the Dart grammar and the bundled light and dark themes once. + static Future load() => + _loadFuture ??= _loadHighlighters(); + + static Future _loadHighlighters() async { + await Highlighter.initialize(['dart']); + final themes = await Future.wait([ + HighlighterTheme.loadLightTheme(), + HighlighterTheme.loadDarkTheme(), + ]); + + return DartSyntaxHighlighter._( + lightHighlighter: Highlighter(language: 'dart', theme: themes[0]), + darkHighlighter: Highlighter(language: 'dart', theme: themes[1]), + ); + } + + /// Highlights [source] for the requested display [brightness]. + TextSpan highlight(String source, Brightness brightness) { + final highlighted = brightness == Brightness.dark + ? _darkHighlighter.highlight(source) + : _lightHighlighter.highlight(source); + final flatSpans = []; + + void flatten(TextSpan span, TextStyle inheritedStyle) { + final effectiveStyle = inheritedStyle.merge(span.style); + final text = span.text; + if (text != null && text.isNotEmpty) { + flatSpans.add( + TextSpan( + text: text, + style: TextStyle( + color: effectiveStyle.color, + decoration: effectiveStyle.decoration, + decorationColor: effectiveStyle.decorationColor, + decorationStyle: effectiveStyle.decorationStyle, + decorationThickness: effectiveStyle.decorationThickness, + ), + ), + ); + } + + for (final child in span.children ?? const []) { + if (child is! TextSpan) { + throw StateError( + 'Dart syntax highlighting produced a non-text inline span.', + ); + } + flatten(child, effectiveStyle); + } + } + + flatten(highlighted, const TextStyle()); + return TextSpan(children: flatSpans); + } +} + +/// A read-only controller backed by the Dart TextMate grammar. +class DartSyntaxCodeController extends SyntaxCodeController { + /// Creates a controller for [text] using [syntaxHighlighter]. + DartSyntaxCodeController({ + required super.text, + required DartSyntaxHighlighter syntaxHighlighter, + }) : _syntaxHighlighter = syntaxHighlighter; + + final DartSyntaxHighlighter _syntaxHighlighter; + + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + bool? withComposing, + }) => + TextSpan( + style: style, + children: [ + _syntaxHighlighter.highlight( + text, + Theme.of(context).brightness, + ), + ], + ); +} diff --git a/confapp/lib/hcl/view/screen/flc_trace_data.dart b/confapp/lib/hcl/view/screen/flc_trace_data.dart new file mode 100644 index 0000000000..ca4b14403f --- /dev/null +++ b/confapp/lib/hcl/view/screen/flc_trace_data.dart @@ -0,0 +1,173 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// flc_trace_data.dart +// Reads source trace data embedded in a ROHD netlist. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd_source_navigator/flc_data.dart'; + +/// Returns source trace data embedded in [netlistJson], when present. +/// +/// ROHD 0.6.11 does not emit `rohd.src_trace` attributes. Trace-enabled future +/// ROHD releases can provide them without requiring a Confapp API change. +FlcData? embeddedFlcDataFromNetlistJson(String netlistJson) { + final decoded = jsonDecode(netlistJson); + if (decoded is! Map) { + throw const FormatException('ROHD netlist JSON must contain an object.'); + } + + final flcData = FlcData.fromNetlistJson( + Map.from(decoded), + ); + return flcData.isEmpty ? null : flcData; +} + +/// Adapts [FlcData] to the source-query contract used by schematic viewer +/// 0.2.0. +class EmbeddedFlcSourceNavigation { + /// Creates source navigation backed by [flcData]. + const EmbeddedFlcSourceNavigation( + this.flcData, { + this.primaryModule, + }); + + /// Parsed embedded source trace data, if the netlist supplied any. + final FlcData? flcData; + + /// Definition name to use when the viewer reports an instance-based name. + final String? primaryModule; + + /// Whether this trace can receive schematic cross-probe selections. + bool get isAvailable => flcData != null && !flcData!.isEmpty; + + /// Returns source format identifiers available for [module]. + Set formatsForModule( + String module, { + List? instancePath, + }) { + var formats = _formatsForModule(module); + if (formats.isNotEmpty) { + return formats; + } + + if (instancePath != null) { + final instanceName = instancePath.lastWhere( + (segment) => segment.isNotEmpty, + orElse: () => '', + ); + if (instanceName.isNotEmpty && instanceName != module) { + formats = _formatsForModule(instanceName); + if (formats.isNotEmpty) { + return formats; + } + } + } + + final fallbackModule = primaryModule; + if (fallbackModule != null && fallbackModule != module) { + return _formatsForModule(fallbackModule); + } + + return const {}; + } + + /// Returns source frames for schematic [signals], optionally filtered by + /// [format]. + List> lookupSignalFrames({ + required List> signals, + String? format, + }) { + final data = flcData; + if (data == null) { + return const []; + } + + final frames = >[]; + for (final signal in signals) { + final requestedModule = signal['module']; + final name = signal['name']; + if (name == null) { + continue; + } + + FlcEntry? entry; + if (requestedModule != null && requestedModule.isNotEmpty) { + entry = data.lookupSignalEntry(requestedModule, name) ?? + data.lookupInstanceEntry(requestedModule, name); + } + final fallbackModule = primaryModule; + if (entry == null && fallbackModule != null) { + entry = data.lookupSignalEntry(fallbackModule, name) ?? + data.lookupInstanceEntry(fallbackModule, name); + } + if (entry == null) { + continue; + } + frames.addAll(_sourceFrameMaps(entry, signalName: name)); + } + + if (format == null) { + return frames; + } + return frames.where((frame) => frame['type'] == format).toList(); + } + + Set _formatsForModule(String moduleName) { + final data = flcData; + if (data == null) { + return const {}; + } + + final formats = {}; + + void addEntryFormats(FlcEntry? entry) { + if (entry == null) { + return; + } + if (entry.frames.isNotEmpty) { + formats.add('rohd'); + } + for (final outputFrame in entry.outputFrames) { + if (outputFrame.type.isNotEmpty) { + formats.add(outputFrame.type); + } + } + } + + for (final signal in data.signalNamesFor(moduleName)) { + addEntryFormats(data.lookupSignalEntry(moduleName, signal)); + } + for (final instance in data.instanceNamesFor(moduleName)) { + addEntryFormats(data.lookupInstanceEntry(moduleName, instance)); + } + return formats; + } + + static List> _sourceFrameMaps( + FlcEntry entry, { + required String signalName, + }) => + [ + for (final frame in entry.frames.reversed) + _sourceFrameMap(frame, signalName), + for (final frame in entry.outputFrames) + _sourceFrameMap(frame, signalName), + ]; + + static Map _sourceFrameMap( + FlcFrame frame, + String signalName, + ) => + { + 'file': frame.file, + 'line': frame.line, + 'col': frame.column, + 'desc': signalName, + 'type': frame.type, + }; +} diff --git a/confapp/lib/hcl/view/screen/schematic.dart b/confapp/lib/hcl/view/screen/schematic.dart deleted file mode 100644 index 32c1603795..0000000000 --- a/confapp/lib/hcl/view/screen/schematic.dart +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (C) 2023 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// schematic.dart -// Construction of a d3 schematic HTML from d3 JSON. -// -// 2024 July 3 -// Author: Desmond Kirkpatrick - -const _prefix = r""" - - - - - d3-hwschematic - - - - - - - - - - - - - - -"""; - -String d3Schematic(String json) { - return _prefix + json + _suffix; -} diff --git a/confapp/lib/hcl/view/screen/serialized_worker_request_queue.dart b/confapp/lib/hcl/view/screen/serialized_worker_request_queue.dart new file mode 100644 index 0000000000..e53b0275e4 --- /dev/null +++ b/confapp/lib/hcl/view/screen/serialized_worker_request_queue.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// serialized_worker_request_queue.dart +// Serializes requests sent to a worker that returns uncorrelated responses. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; +import 'dart:collection'; + +/// Serializes worker requests and pairs each with one [Response]. +class SerializedWorkerRequestQueue { + /// Creates an empty worker request queue. + SerializedWorkerRequestQueue(); + + final Queue<_PendingWorkerRequest> _waiting = Queue(); + + _PendingWorkerRequest? _active; + Object? _terminalError; + StackTrace? _terminalStackTrace; + + /// Enqueues [send] and returns the eventual response to that request. + Future add(void Function() send) { + final terminalError = _terminalError; + if (terminalError != null) { + return Future.error(terminalError, _terminalStackTrace); + } + + final pending = _PendingWorkerRequest(send); + _waiting.addLast(pending); + _sendNext(); + return pending.completer.future; + } + + /// Completes the active request with [response] and dispatches the next one. + void complete(Response response) { + if (_terminalError != null) { + return; + } + final active = _takeActive(); + active.completer.complete(response); + _sendNext(); + } + + /// Fails the active request with [error] and dispatches the next one. + void completeError(Object error, [StackTrace? stackTrace]) { + if (_terminalError != null) { + return; + } + final active = _takeActive(); + active.completer.completeError(error, stackTrace ?? StackTrace.current); + _sendNext(); + } + + /// Fails all requests and prevents additional requests from being queued. + void failAll(Object error, [StackTrace? stackTrace]) { + if (_terminalError != null) { + return; + } + final effectiveStackTrace = stackTrace ?? StackTrace.current; + _terminalError = error; + _terminalStackTrace = effectiveStackTrace; + + final pending = [ + if (_active case final active?) active, + ..._waiting, + ]; + _active = null; + _waiting.clear(); + for (final request in pending) { + request.completer.completeError(error, effectiveStackTrace); + } + } + + /// Fails all requests because the worker request queue is no longer used. + void dispose() { + failAll(StateError('The worker request queue was disposed.')); + } + + _PendingWorkerRequest _takeActive() { + final active = _active; + if (active == null) { + throw StateError('The worker responded without an active request.'); + } + _active = null; + return active; + } + + void _sendNext() { + if (_terminalError != null || _active != null || _waiting.isEmpty) { + return; + } + + final next = _waiting.removeFirst(); + _active = next; + try { + next.send(); + } on Object catch (error, stackTrace) { + _active = null; + next.completer.completeError(error, stackTrace); + _sendNext(); + } + } +} + +class _PendingWorkerRequest { + _PendingWorkerRequest(this.send); + + final void Function() send; + final Completer completer = Completer(); +} diff --git a/confapp/lib/hcl/view/screen/sidebar_widget.dart b/confapp/lib/hcl/view/screen/sidebar_widget.dart index e5b0536af9..1f435725f2 100644 --- a/confapp/lib/hcl/view/screen/sidebar_widget.dart +++ b/confapp/lib/hcl/view/screen/sidebar_widget.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // sidebar_widget.dart @@ -6,126 +6,131 @@ // // 2023 December -import 'package:flutter/material.dart'; -import 'package:rohd_hcl/rohd_hcl.dart'; -import 'package:sidebarx/sidebarx.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:confapp/hcl/cubit/component_cubit.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +/// The primary accent color. const primaryColor = Color.fromARGB(255, 160, 153, 240); + +/// The sidebar canvas color. const canvasColor = Color(0xFF2E2E48); + +/// The scaffold background color. const scaffoldBackgroundColor = Color(0xFF464667); + +/// The selected sidebar item color. const accentCanvasColor = Color(0xFF3E3E61); + +/// The common white foreground color. const white = Colors.white; + +/// The sidebar action color. final actionColor = const Color(0xFF5F5FA7).withValues(alpha: 0.6); + +/// A divider matching the sidebar foreground palette. final divider = Divider(color: white.withValues(alpha: 0.3), height: 1); +/// Displays the available configurable components. class ComponentsSidebar extends StatefulWidget { - final Function(void) updateForm; + /// Creates a component sidebar with the given [width]. + const ComponentsSidebar({required this.width, super.key}); - const ComponentsSidebar({ - super.key, - required SidebarXController controller, - required this.updateForm, - }) : _controller = controller; - - final SidebarXController _controller; + /// The width of the sidebar. + final double width; @override State createState() => _ComponentsSidebarState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('width', width)); + } } class _ComponentsSidebarState extends State { - List componentsList = []; - - late Configurator component; - @override Widget build(BuildContext context) { final comCubit = context.read(); - for (int i = 0; i < comCubit.components.length; i++) { - componentsList.add( - SidebarXItem( - // iconWidget: const FlutterLogo(size: 20), - icon: Icons.memory, // The package force to have icon... - label: comCubit.components[i].name, - onTap: () { - comCubit.setSelectedComponent(comCubit.components[i]); - }, - ), - ); - } - return BlocBuilder( - builder: (context, state) { - return SidebarX( - controller: widget._controller, - theme: SidebarXTheme( - margin: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: canvasColor, - borderRadius: BorderRadius.circular(20), - ), - hoverColor: scaffoldBackgroundColor, - textStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)), - selectedTextStyle: const TextStyle(color: Colors.white), - itemTextPadding: const EdgeInsets.only(left: 5), - selectedItemTextPadding: const EdgeInsets.only(left: 5), - itemDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: canvasColor), + builder: (context, selectedComponent) => Container( + width: widget.width, + color: canvasColor, + child: ListView( + padding: const EdgeInsets.fromLTRB(8, 6, 8, 12), + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 12), + child: Text( + 'Components', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), ), - selectedItemDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: actionColor.withValues(alpha: 0.37), + for (final component in comCubit.components) + Padding( + padding: const EdgeInsets.only(left: 16), + child: _ComponentTile( + component: component, + selected: component == selectedComponent, + onTap: () => comCubit.setSelectedComponent(component), + ), ), - gradient: const LinearGradient( - colors: [accentCanvasColor, canvasColor], + ], + ), + ), + ); + } +} + +class _ComponentTile extends StatelessWidget { + const _ComponentTile({ + required this.component, + required this.selected, + required this.onTap, + }); + + final Configurator component; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 1), + child: Material( + color: selected ? accentCanvasColor : Colors.transparent, + borderRadius: BorderRadius.circular(6), + child: InkWell( + borderRadius: BorderRadius.circular(6), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9), + child: Text( + component.name, + style: TextStyle( + color: selected + ? Colors.white + : Colors.white.withValues(alpha: 0.72), + fontSize: 14, + ), ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.28), - blurRadius: 30, - ) - ], - ), - iconTheme: IconThemeData( - color: Colors.white.withValues(alpha: 0.7), - size: 20, - ), - selectedIconTheme: const IconThemeData( - color: Colors.white, - size: 20, - ), - ), - extendedTheme: const SidebarXTheme( - width: 230, - decoration: BoxDecoration( - color: canvasColor, ), ), - footerDivider: divider, - headerBuilder: (context, extended) { - return const SizedBox( - height: 100, - child: Padding( - padding: EdgeInsets.all(16.0), - child: Center( - child: Text( - 'ROHD-HCL', - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - )), - ), - ); - }, - items: componentsList, - ); - }, - ); + ), + ); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('component', component)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(ObjectFlagProperty.has('onTap', onTap)); } } diff --git a/confapp/lib/hcl_bloc_observer.dart b/confapp/lib/hcl_bloc_observer.dart index a8c0eec035..b52aece8ee 100644 --- a/confapp/lib/hcl_bloc_observer.dart +++ b/confapp/lib/hcl_bloc_observer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // hcl_bloc_observer.dart @@ -10,6 +10,7 @@ import 'package:bloc/bloc.dart'; /// [BlocObserver] observe all state changes in the application. class HCLBlocObserver extends BlocObserver { + /// Creates an application-wide BLoC observer. const HCLBlocObserver(); @override diff --git a/confapp/lib/main.dart b/confapp/lib/main.dart index 5eee944448..3b814b164d 100644 --- a/confapp/lib/main.dart +++ b/confapp/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // main.dart @@ -6,10 +6,11 @@ // // 2023 December -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:confapp/app.dart'; import 'package:confapp/hcl_bloc_observer.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +// The component registry is intentionally not part of the package's public API. // ignore: implementation_imports import 'package:rohd_hcl/src/component_config/components/component_registry.dart'; diff --git a/confapp/pubspec.yaml b/confapp/pubspec.yaml index 9c65a5c5fb..20ec85e7d2 100644 --- a/confapp/pubspec.yaml +++ b/confapp/pubspec.yaml @@ -2,29 +2,77 @@ name: confapp description: Configuration app for components. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 0.0.1 +version: 0.1.0 + +resolution: workspace environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.6.0 <4.0.0' + flutter: '>=3.47.0' dependencies: + bloc: ^9.0.0 + cupertino_icons: ^1.0.8 flutter: sdk: flutter - rohd: ^0.6.1 - rohd_hcl: - path: ../ - - sidebarx: ^0.16.0 - flutter_bloc: ^8.1.3 - bloc: ^8.1.2 - google_fonts: 6.1.0 + flutter_bloc: ^9.0.0 + highlight: ^0.7.0 + material_ui: ^1.1.1 + rohd: ^0.6.11 + rohd_devtools_widgets: ^0.1.1 + rohd_hcl: ^0.2.1 + rohd_hierarchy: ^0.1.0 + rohd_schematic_viewer: 0.2.0 + rohd_source_navigator: ^0.1.0 + # 0.5.0 also pulls in its CodeEditor clipboard stack; only highlighting is used. + syntax_highlight: ^0.4.0 + web: ^1.0.0 dev_dependencies: flutter_test: sdk: flutter test: ^1.20.0 - flutter_lints: ^3.0.1 - flutter: uses-material-design: true + assets: + - assets/rohd_src/ + - assets/rohd_src/arbiters/ + - assets/rohd_src/arithmetic/ + - assets/rohd_src/arithmetic/floating_point/ + - assets/rohd_src/arithmetic/multiplier_components/ + - assets/rohd_src/arithmetic/signals/ + - assets/rohd_src/arithmetic/signals/floating_point_logics/ + - assets/rohd_src/arithmetic/values/ + - assets/rohd_src/arithmetic/values/floating_point_values/ + - assets/rohd_src/component_config/ + - assets/rohd_src/component_config/components/ + - assets/rohd_src/component_config/config_knobs/ + - assets/rohd_src/encodings/ + - assets/rohd_src/error_checking/ + - assets/rohd_src/gaskets/ + - assets/rohd_src/gaskets/amba5/ + - assets/rohd_src/gaskets/spi/ + - assets/rohd_src/interfaces/ + - assets/rohd_src/interfaces/amba4/ + - assets/rohd_src/interfaces/amba5/ + - assets/rohd_src/memory/ + - assets/rohd_src/memory/csr/ + - assets/rohd_src/memory/csr/config/ + - assets/rohd_src/models/ + - assets/rohd_src/models/amba/ + - assets/rohd_src/models/amba/amba4_bfm/ + - assets/rohd_src/models/amba/amba5_bfm/ + - assets/rohd_src/models/amba/amba5_bfm/axi/ + - assets/rohd_src/models/amba/amba5_bfm/axi_s/ + - assets/rohd_src/models/amba/amba5_bfm/lti/ + - assets/rohd_src/models/amba/apb_bfm/ + - assets/rohd_src/models/ready_valid_bfm/ + - assets/rohd_src/models/spi_bfm/ + - assets/rohd_src/serialization/ + - assets/rohd_src/summation/ + - assets/third_party_notices.md + - assets/rohd_icon.png + - assets/systemverilog_icon.png + - assets/systemc_icon.png + - assets/yosys_icon.png diff --git a/confapp/test/hcl/module_source_assets_test.dart b/confapp/test/hcl/module_source_assets_test.dart new file mode 100644 index 0000000000..03a8d90384 --- /dev/null +++ b/confapp/test/hcl/module_source_assets_test.dart @@ -0,0 +1,123 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_source_assets_test.dart +// Tests bundled source coverage for configurable modules. +// +// 2026 August 29 +// Author: Desmond A. Kirkpatrick + +import 'package:confapp/hcl/module_source_assets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +// The test verifies every registry entry, which requires the internal registry. +import 'package:rohd_hcl/src/component_config/components/component_registry.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('every configured component has an indexed source asset', () { + for (final configurator in componentRegistry) { + final module = configurator.createModule(); + expect( + moduleSourceAsset(module), + isNotNull, + reason: + '${configurator.name} creates ${module.runtimeType}, which has no ' + 'ROHD source asset', + ); + } + }); + + test('carry-save multiplier uses its implementation source asset', () { + final module = CarrySaveMultiplierConfigurator().createModule(); + + expect(module, isA()); + expect( + moduleSourceAsset(module), + 'rohd_src/arithmetic/carry_save_mutiplier.dart', + ); + }); + + test('nondefault module implementations have indexed source assets', () { + final variants = <({String name, Module module, String asset})>[ + ( + name: 'binary-to-one-hot direction', + module: (OneHotConfigurator()..directionKnob.value = BinaryToOneHot) + .createModule(), + asset: 'rohd_src/encodings/binary_to_one_hot.dart', + ), + ( + name: 'tree one-hot-to-binary width', + module: + (OneHotConfigurator()..inputWidthKnob.value = 16).createModule(), + asset: 'rohd_src/encodings/tree_one_hot_to_binary.dart', + ), + ( + name: 'rotate round-robin implementation', + module: (RoundRobinArbiterConfigurator() + ..implementationKnob.value = RotateRoundRobinArbiter) + .createModule(), + asset: 'rohd_src/arbiters/rotate_round_robin_arbiter.dart', + ), + ( + name: 'deserialize direction', + module: (SerializationConfigurator() + ..directionKnob.value = Deserializer) + .createModule(), + asset: 'rohd_src/serialization/serialization.dart', + ), + ( + name: 'left rotation', + module: (RotateConfigurator() + ..directionKnob.value = RotateDirection.left) + .createModule(), + asset: 'rohd_src/rotate.dart', + ), + ( + name: 'dual-path floating-point adder', + module: (FloatingPointAdderConfigurator() + ..dualPathAdderKnob.value = true) + .createModule(), + asset: + 'rohd_src/arithmetic/floating_point/floating_point_adder_dualpath.dart', + ), + ( + name: 'leading-zero anticipator', + module: (LeadingDigitAnticipateConfigurator() + ..anticipator.value = LeadingZeroAnticipate) + .createModule(), + asset: 'rohd_src/arithmetic/leading_digit_anticipate.dart', + ), + ( + name: 'leading-zero carry anticipator', + module: (LeadingDigitAnticipateConfigurator() + ..anticipator.value = LeadingZeroAnticipateCarry) + .createModule(), + asset: 'rohd_src/arithmetic/leading_digit_anticipate.dart', + ), + ( + name: 'compression-tree multiplier', + module: (MultiplierConfigurator() + ..multiplierSelectKnob.compressionTreeMultiplierKnob.value = true) + .createModule(), + asset: 'rohd_src/arithmetic/multiplier.dart', + ), + ( + name: 'gated counter', + module: (CounterConfigurator()..clockGatingKnob.value = true) + .createModule(), + asset: 'rohd_src/summation/gated_counter.dart', + ), + ]; + + for (final variant in variants) { + expect( + moduleSourceAsset(variant.module), + variant.asset, + reason: '${variant.name} creates ${variant.module.runtimeType}', + ); + } + }); +} diff --git a/confapp/test/hcl/view/dart_syntax_code_controller_test.dart b/confapp/test/hcl/view/dart_syntax_code_controller_test.dart new file mode 100644 index 0000000000..aa55e3bb7b --- /dev/null +++ b/confapp/test/hcl/view/dart_syntax_code_controller_test.dart @@ -0,0 +1,237 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// dart_syntax_code_controller_test.dart +// Tests for Dart syntax highlighting in the confapp source viewer. +// +// 2026 September 9 +// Author: Desmond A. Kirkpatrick + +import 'package:confapp/hcl/view/screen/dart_syntax_code_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:highlight/languages/verilog.dart' as highlight_verilog; +import 'package:material_ui/material_ui.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late DartSyntaxHighlighter syntaxHighlighter; + + setUpAll(() async { + syntaxHighlighter = await DartSyntaxHighlighter.load(); + }); + + testWidgets('ROHD source uses rich Dart token styles', (tester) async { + const source = r''' +/// Counts input pulses. +@immutable +class PulseCounter extends Module { + final int width = 8; + + String label(bool enabled) => enabled ? 'count: $width' : 'off'; +} +'''; + final controller = DartSyntaxCodeController( + text: source, + syntaxHighlighter: syntaxHighlighter, + ); + addTearDown(controller.dispose); + + late TextSpan highlighted; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + highlighted = controller.buildTextSpan( + context: context, + style: const TextStyle(fontFamily: 'monospace'), + withComposing: false, + ); + return const SizedBox(); + }, + ), + ), + ); + + expect(highlighted.toPlainText(), source); + + final segments = _flatten(highlighted); + final tokenColors = { + _colorFor(segments, '/// Counts input pulses.'), + _colorFor(segments, 'class'), + _colorFor(segments, 'PulseCounter'), + _colorFor(segments, 'label'), + _colorFor(segments, '8'), + _colorFor(segments, r"'count: $"), + }; + + expect(tokenColors, isNot(contains(null))); + expect( + tokenColors.length, + greaterThanOrEqualTo(5), + reason: 'Comments, keywords, types, functions, numbers, and strings ' + 'should not collapse to the old basic styling.', + ); + }); + + testWidgets('ROHD source highlighting follows app brightness', + (tester) async { + const source = 'class Counter { String name = "counter"; }'; + final controller = DartSyntaxCodeController( + text: source, + syntaxHighlighter: syntaxHighlighter, + ); + addTearDown(controller.dispose); + + Future classColor(Brightness brightness) async { + late TextSpan highlighted; + await tester.pumpWidget( + Theme( + data: ThemeData(brightness: brightness), + child: Builder( + builder: (context) { + highlighted = controller.buildTextSpan( + context: context, + withComposing: false, + ); + return const SizedBox(); + }, + ), + ), + ); + return _colorFor(_flatten(highlighted), 'Counter'); + } + + final lightColor = await classColor(Brightness.light); + final darkColor = await classColor(Brightness.dark); + + expect(lightColor, isNotNull); + expect(darkColor, isNotNull); + expect(darkColor, isNot(lightColor)); + }); + + testWidgets('syntax colors do not change monospace layout', (tester) async { + const source = + '.rotate(RotateDirection.left, amount: 8, width: originalWidth)'; + final controller = DartSyntaxCodeController( + text: source, + syntaxHighlighter: syntaxHighlighter, + ); + addTearDown(controller.dispose); + + const baseStyle = TextStyle( + fontFamily: 'monospace', + fontSize: 12, + height: 1.5, + ); + late TextSpan highlighted; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + highlighted = controller.buildTextSpan( + context: context, + style: baseStyle, + withComposing: false, + ); + return const SizedBox(); + }, + ), + ), + ); + + final plainPainter = TextPainter( + text: const TextSpan(text: source, style: baseStyle), + textDirection: TextDirection.ltr, + )..layout(); + final highlightedPainter = TextPainter( + text: highlighted, + textDirection: TextDirection.ltr, + )..layout(); + + expect(highlighted.toPlainText(), source); + expect(highlightedPainter.width, plainPainter.width); + expect(highlightedPainter.height, plainPainter.height); + + for (final child in highlighted.children ?? const []) { + final tokenStyle = child.style; + expect(tokenStyle?.fontFamily, isNull); + expect(tokenStyle?.fontSize, isNull); + expect(tokenStyle?.fontWeight, isNull); + expect(tokenStyle?.fontStyle, isNull); + expect(tokenStyle?.letterSpacing, isNull); + expect(tokenStyle?.wordSpacing, isNull); + expect(tokenStyle?.height, isNull); + expect(tokenStyle?.backgroundColor, isNull); + } + }); + + testWidgets('modern TextField renders highlighted code without a bridge', + (tester) async { + final controller = HighlightCodeController( + text: 'module counter;', + languageName: 'verilog', + language: highlight_verilog.verilog, + ); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: TextField( + controller: controller, + readOnly: true, + maxLines: null, + ), + ), + ), + ); + + expect(find.byType(TextField), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + test('source line ranges update with generated text', () { + final controller = HighlightCodeController( + text: 'module counter;\nendmodule', + languageName: 'verilog', + language: highlight_verilog.verilog, + ); + addTearDown(controller.dispose); + + expect(controller.lineCount, 2); + expect(controller.lineRangeAt(1), const TextRange(start: 16, end: 25)); + + controller.replaceSource('logic a;'); + + expect(controller.lineCount, 1); + expect(controller.lineRangeAt(0), const TextRange(start: 0, end: 8)); + }); +} + +List<({String text, TextStyle style})> _flatten(TextSpan root) { + final segments = <({String text, TextStyle style})>[]; + + void visit(TextSpan span, TextStyle inheritedStyle) { + final style = + span.style == null ? inheritedStyle : inheritedStyle.merge(span.style); + final text = span.text; + if (text != null && text.isNotEmpty) { + segments.add((text: text, style: style)); + } + for (final child in span.children ?? const []) { + if (child is TextSpan) { + visit(child, style); + } + } + } + + visit(root, const TextStyle()); + return segments; +} + +Color? _colorFor( + List<({String text, TextStyle style})> segments, + String token, +) => + segments.firstWhere((segment) => segment.text.contains(token)).style.color; diff --git a/confapp/test/hcl/view/hcl_page_test.dart b/confapp/test/hcl/view/hcl_page_test.dart index 716a5b8c81..f1c575ec9c 100644 --- a/confapp/test/hcl/view/hcl_page_test.dart +++ b/confapp/test/hcl/view/hcl_page_test.dart @@ -1,91 +1,160 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // hcl_page_test.dart // Tests for the app // // 2023 December +// Author: Max Korbel import 'package:confapp/hcl/hcl.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:rohd_hcl/src/component_config/components/component_registry.dart'; String? observeOutput(WidgetTester tester) { - final selectableTextFinder = find.byType(SelectableText); - final widget = tester.widget(selectableTextFinder); - return widget.data; + final widget = tester.widget( + find.byKey(const Key('generatedSV')), + ); + return widget.controller?.text; } List components = componentRegistry; +Future pumpHclPage(WidgetTester tester) async { + await tester.binding.setSurfaceSize(const Size(1440, 900)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + MaterialApp( + home: HCLPage( + components: components, + ), + ), + ); + await tester.pump(); +} + +Future showOnlyGeneratedSv(WidgetTester tester) async { + await tester.tap(find.byTooltip('ROHD Schematic')); + await tester.pump(); + await tester.tap(find.byTooltip('ROHD Source')); + await tester.pump(); +} + +Future pumpUntilOutputContains( + WidgetTester tester, + String expected, +) async { + for (var i = 0; i < 100; i++) { + final output = find.byKey(const Key('generatedSV')); + if (tester.any(output) && + tester.widget(output).controller!.text.contains(expected)) { + return; + } + await tester.pump(const Duration(milliseconds: 100)); + } + expect(observeOutput(tester), contains(expected)); +} + void main() { testWidgets('should return message to click generate when page load', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: HCLPage( - components: components, - ), + await pumpHclPage(tester); + + expect( + find.text('Click "Generate" to see the ROHD schematic'), + findsOneWidget, + ); + }); + + testWidgets('keeps horizontally scrolled source below its scrollbar', + (tester) async { + await pumpHclPage(tester); + await showOnlyGeneratedSv(tester); + + await tester.tap(find.byKey(const Key('generateRTL'))); + await pumpUntilOutputContains(tester, 'module'); + + final generatedSv = find.byKey(const Key('generatedSV')); + final horizontalScrollbar = find.ancestor( + of: generatedSv, + matching: find.byWidgetPredicate( + (widget) => + widget is Scrollbar && + widget.scrollbarOrientation == ScrollbarOrientation.top, ), ); + expect(horizontalScrollbar, findsOneWidget); - await tester.pumpAndSettle(); + final scrollbar = tester.widget(horizontalScrollbar); + final scrollController = scrollbar.controller!; + expect(scrollController.position.maxScrollExtent, greaterThan(0)); - final selectableTextFinder = find.byType(SelectableText); - expect(selectableTextFinder, findsOneWidget); + scrollController.jumpTo(scrollController.position.maxScrollExtent / 2); + await tester.pump(); - final widget = tester.widget(selectableTextFinder); - final textOutput = widget.data; + const materialScrollbarMargin = 2.0; + const minimumContentGap = 2.0; + final minimumClearance = + scrollbar.thickness! + materialScrollbarMargin + minimumContentGap; + final actualClearance = tester.getTopLeft(generatedSv).dy - + tester.getTopLeft(horizontalScrollbar).dy; - expect(textOutput?.contains('Click'), true); + expect(actualClearance, greaterThanOrEqualTo(minimumClearance)); }); testWidgets('should return changes when fields is manipulated', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: HCLPage( - components: components, - ), - ), - ); - - await tester.pumpAndSettle(); + await pumpHclPage(tester); + await showOnlyGeneratedSv(tester); final directionKnob = find.byKey(const Key('Direction')); final btnGenerateRTL = find.byKey(const Key('generateRTL')); // tap on the rotate direction field await tester.tap(directionKnob); - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 300)); // change the text to left final rightButton = find.text('left').last; await tester.tap(rightButton); - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 300)); // tap on the generate RTL button await tester.tap(btnGenerateRTL); - // wait for widget to rebuild by pump - await tester.pumpAndSettle(); + await pumpUntilOutputContains(tester, 'RotateLeft'); - expect(observeOutput(tester)?.contains('RotateLeft'), true); + expect(observeOutput(tester), contains('RotateLeft')); }); - testWidgets('should transit to another component when clicked on sidebar', - (tester) async { - await tester.pumpWidget( - MaterialApp( - home: HCLPage( - components: components, - ), + testWidgets('shows stable labels for type-valued choices', (tester) async { + await pumpHclPage(tester); + + await tester.tap(find.text('One-hot Converter')); + await tester.pump(); + + final directionKnob = find.byKey(const Key('Direction')); + expect( + find.descendant( + of: directionKnob, + matching: find.text('OneHotToBinary'), ), + findsOneWidget, ); - await tester.pumpAndSettle(); + await tester.tap(directionKnob); + await tester.pump(); + + expect(find.text('BinaryToOneHot').last, findsOneWidget); + }); + + testWidgets('should transit to another component when clicked on sidebar', + (tester) async { + await pumpHclPage(tester); + await showOnlyGeneratedSv(tester); final sidebarPriorityArbiter = find.text('Priority Arbiter'); final btnGenerateRTL = find.byKey(const Key('generateRTL')); @@ -98,9 +167,26 @@ void main() { // tap on the generate RTL button await tester.tap(btnGenerateRTL); - // wait for changes - await tester.pumpAndSettle(); + await pumpUntilOutputContains(tester, 'PriorityArbiter'); + + expect(observeOutput(tester), contains('PriorityArbiter')); + }); + + testWidgets('discards generation when the selected component changes', + (tester) async { + await pumpHclPage(tester); + await showOnlyGeneratedSv(tester); + + await tester.tap(find.byKey(const Key('generateRTL'))); + await tester.pump(const Duration(milliseconds: 20)); + await tester.tap(find.text('Priority Arbiter')); + await tester.pump(const Duration(milliseconds: 300)); + + expect(tester.takeException(), isNull); + + await tester.tap(find.byKey(const Key('generateRTL'))); + await pumpUntilOutputContains(tester, 'PriorityArbiter'); - expect(observeOutput(tester)?.contains('PriorityArbiter'), true); + expect(observeOutput(tester), contains('PriorityArbiter')); }); } diff --git a/confapp/test/hcl/view/screen/browser_interop_web_test.dart b/confapp/test/hcl/view/screen/browser_interop_web_test.dart new file mode 100644 index 0000000000..7bfebcd521 --- /dev/null +++ b/confapp/test/hcl/view/screen/browser_interop_web_test.dart @@ -0,0 +1,175 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_interop_web_test.dart +// Browser tests for worker, download, and context-menu integrations. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +@TestOn('browser') +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop'; + +import 'package:confapp/hcl/view/screen/browser_interop_web_core.dart'; +import 'package:test/test.dart'; +import 'package:web/web.dart' as web; + +void main() { + test('Yosys worker pairs serialized requests with their responses', () async { + const workerScript = ''' + let activeRequests = 0; + onmessage = function(event) { + activeRequests++; + const request = event.data.verilog; + const delay = request === 'first' ? 20 : 0; + setTimeout(function() { + postMessage(request + ':' + activeRequests); + activeRequests--; + }, delay); + }; + '''; + final workerBlob = web.Blob( + [workerScript.toJS].toJS, + web.BlobPropertyBag(type: 'text/javascript'), + ); + final workerUrl = web.URL.createObjectURL(workerBlob); + final worker = YosysWorker(workerUrl); + addTearDown(() { + worker.dispose(); + web.URL.revokeObjectURL(workerUrl); + }); + + final first = worker.synthesize({ + 'module': 'FirstModule', + 'verilog': 'first', + }); + final second = worker.synthesize({ + 'module': 'SecondModule', + 'verilog': 'second', + }); + + expect(await first, 'first:1'); + expect(await second, 'second:1'); + }); + + test('Yosys worker errors fail active and queued requests', () async { + const workerScript = ''' + onmessage = function() { + throw new Error('intentional worker failure'); + }; + '''; + final workerBlob = web.Blob( + [workerScript.toJS].toJS, + web.BlobPropertyBag(type: 'text/javascript'), + ); + final workerUrl = web.URL.createObjectURL(workerBlob); + final worker = YosysWorker(workerUrl); + addTearDown(() { + worker.dispose(); + web.URL.revokeObjectURL(workerUrl); + }); + + final first = worker.synthesize({ + 'module': 'FirstModule', + 'verilog': 'first', + }); + final second = worker.synthesize({ + 'module': 'SecondModule', + 'verilog': 'second', + }); + final matcher = throwsA( + isA().having( + (error) => error.message, + 'message', + contains('intentional worker failure'), + ), + ); + + await Future.wait([ + expectLater(first, matcher), + expectLater(second, matcher), + ]); + await expectLater( + worker.synthesize({ + 'module': 'ThirdModule', + 'verilog': 'third', + }), + matcher, + ); + }); + + test('download uses the requested filename and UTF-8 bytes', () async { + const content = 'ROHD π'; + const fileName = 'generated-π.sv'; + final clicked = Completer<({String fileName, String href})>(); + late final web.EventListener listener; + listener = ((web.Event event) { + event.preventDefault(); + final anchor = event.target! as web.HTMLAnchorElement; + clicked.complete((fileName: anchor.download, href: anchor.href)); + }).toJS; + web.document.addEventListener('click', listener, true.toJS); + addTearDown( + () => web.document.removeEventListener('click', listener, true.toJS), + ); + + downloadFile(content: content, fileName: fileName); + + final download = await clicked.future; + expect(download.fileName, fileName); + expect( + Uri.parse(download.href).data!.contentAsBytes(), + utf8.encode(content), + ); + }); + + test('context menu suppression is reference counted', () { + final target = web.HTMLDivElement(); + web.document.body!.appendChild(target); + // External JS interop methods cannot be passed as tear-offs. + // ignore: unnecessary_lambdas + addTearDown(() => target.remove()); + var attachedReferences = 0; + addTearDown(() { + while (attachedReferences > 0) { + BrowserContextMenuSuppression.detach(); + attachedReferences--; + } + }); + + BrowserContextMenuSuppression.attach(); + attachedReferences++; + BrowserContextMenuSuppression.attach(); + attachedReferences++; + + final suppressed = web.Event( + 'contextmenu', + web.EventInit(bubbles: true, cancelable: true), + ); + expect(target.dispatchEvent(suppressed), isFalse); + expect(suppressed.defaultPrevented, isTrue); + + BrowserContextMenuSuppression.detach(); + attachedReferences--; + final stillSuppressed = web.Event( + 'contextmenu', + web.EventInit(bubbles: true, cancelable: true), + ); + expect(target.dispatchEvent(stillSuppressed), isFalse); + expect(stillSuppressed.defaultPrevented, isTrue); + + BrowserContextMenuSuppression.detach(); + attachedReferences--; + + final allowed = web.Event( + 'contextmenu', + web.EventInit(bubbles: true, cancelable: true), + ); + expect(target.dispatchEvent(allowed), isTrue); + expect(allowed.defaultPrevented, isFalse); + }); +} diff --git a/confapp/test/hcl/view/screen/flc_trace_data_test.dart b/confapp/test/hcl/view/screen/flc_trace_data_test.dart new file mode 100644 index 0000000000..8bc723ef55 --- /dev/null +++ b/confapp/test/hcl/view/screen/flc_trace_data_test.dart @@ -0,0 +1,135 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// flc_trace_data_test.dart +// Tests source trace data embedded in ROHD netlists. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +import 'dart:convert'; + +import 'package:confapp/hcl/view/screen/flc_trace_data.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:rohd_source_navigator/flc_data.dart'; + +void main() { + test('returns source trace data embedded in a netlist', () { + final flcData = embeddedFlcDataFromNetlistJson( + jsonEncode({ + 'files': ['lib/src/example.dart'], + 'modules': { + 'Example': { + 'attributes': { + 'rohd.src_trace': { + 'signals': { + 'result': ['0:42:7'], + }, + 'instances': {}, + }, + }, + }, + }, + }), + ); + + final entry = flcData!.lookupSignalEntry('Example', 'result'); + expect(entry, isNotNull); + expect(entry!.frames.single.file, 'lib/src/example.dart'); + expect(entry.frames.single.line, 42); + expect(entry.frames.single.column, 7); + + final navigation = EmbeddedFlcSourceNavigation( + flcData, + primaryModule: 'Example', + ); + expect(navigation.isAvailable, isTrue); + expect( + navigation.formatsForModule('GeneratedExample'), + {'rohd'}, + ); + expect( + navigation.lookupSignalFrames( + signals: const [ + {'module': 'GeneratedExample', 'name': 'result'}, + ], + format: 'rohd', + ), + [ + { + 'file': 'lib/src/example.dart', + 'line': 42, + 'col': 7, + 'desc': 'result', + 'type': 'rohd', + }, + ], + ); + }); + + test('returns null when the netlist has no embedded trace data', () { + final flcData = embeddedFlcDataFromNetlistJson( + jsonEncode({ + 'modules': { + 'Example': { + 'attributes': {'top': 1}, + }, + }, + }), + ); + + expect(flcData, isNull); + + const navigation = EmbeddedFlcSourceNavigation(null); + expect(navigation.isAvailable, isFalse); + expect(navigation.formatsForModule('Example'), isEmpty); + expect( + navigation.lookupSignalFrames( + signals: const [ + {'module': 'Example', 'name': 'result'}, + ], + ), + isEmpty, + ); + }); + + test('exposes output-language frames from FLC data', () { + final navigation = EmbeddedFlcSourceNavigation( + FlcData.fromJson({ + 'version': 5, + 'files': ['lib/src/example.dart'], + 'modules': { + 'Example': { + 'svFile': 'Example.sv', + 'tree': [ + ['0:42:7', 'result@sv:8:3'], + ], + }, + }, + }), + primaryModule: 'Example', + ); + + expect( + navigation.formatsForModule('Example'), + {'rohd', 'sv'}, + ); + expect( + navigation.lookupSignalFrames( + signals: const [ + {'module': 'Example', 'name': 'result'}, + ], + format: 'sv', + ), + [ + { + 'file': 'Example.sv', + 'line': 8, + 'col': 3, + 'desc': 'result', + 'type': 'sv', + }, + ], + ); + }); +} diff --git a/confapp/test/hcl/view/screen/serialized_worker_request_queue_test.dart b/confapp/test/hcl/view/screen/serialized_worker_request_queue_test.dart new file mode 100644 index 0000000000..3c40b45e05 --- /dev/null +++ b/confapp/test/hcl/view/screen/serialized_worker_request_queue_test.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// serialized_worker_request_queue_test.dart +// Tests serialized worker request and response pairing. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +import 'package:confapp/hcl/view/screen/serialized_worker_request_queue.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('dispatches only one request until its response arrives', () async { + final dispatched = []; + final queue = SerializedWorkerRequestQueue(); + + final first = queue.add(() => dispatched.add('first')); + final second = queue.add(() => dispatched.add('second')); + + expect(dispatched, ['first']); + + queue.complete('first response'); + expect(await first, 'first response'); + expect(dispatched, ['first', 'second']); + + queue.complete('second response'); + expect(await second, 'second response'); + }); + + test('continues with the next request after an error', () async { + final dispatched = []; + final queue = SerializedWorkerRequestQueue(); + + final first = queue.add(() => dispatched.add('first')); + final second = queue.add(() => dispatched.add('second')); + + queue.completeError(StateError('failed')); + await expectLater(first, throwsStateError); + expect(dispatched, ['first', 'second']); + + queue.complete('second response'); + expect(await second, 'second response'); + }); + + test('terminal errors fail active, queued, and future requests', () async { + final dispatched = []; + final queue = SerializedWorkerRequestQueue(); + final failure = StateError('worker failed'); + + final first = queue.add(() => dispatched.add('first')); + final second = queue.add(() => dispatched.add('second')); + queue.failAll(failure); + + await expectLater(first, throwsA(same(failure))); + await expectLater(second, throwsA(same(failure))); + await expectLater( + queue.add(() => dispatched.add('third')), + throwsA(same(failure)), + ); + expect(dispatched, ['first']); + }); +} diff --git a/confapp/web/index.html b/confapp/web/index.html index 79ef299f6e..d826714ace 100644 --- a/confapp/web/index.html +++ b/confapp/web/index.html @@ -27,13 +27,17 @@ - + - confapp + ⚙️ ROHD-HCL Configurator + + + + diff --git a/doc/Component.md b/doc/Component.md index 721ef50880..786a911489 100755 --- a/doc/Component.md +++ b/doc/Component.md @@ -1,164 +1,193 @@ -# How to Build a Great Component in ROHD - -Since ROHD is an extension of the Dart programming language, please follow all -Dart programming and documentation conventions. - -The `Module` class is the base class used in ROHD to build components, and -calling the constructor the `Module` instantiates the component and connects it -to signals passed into the constructor. - -Since ROHD is written in Dart, you should use the Dart best practices, such as -camel-case variable naming, commenting patterns for all public APIs, and take -advantage of dart format and dart analyze tools. - -## Port Construction and Connection - -A `Module` constructor takes `Logic` arguments and parameters to generate a -hardware component. The `Logic` arguments are actually the external signals -being connected to by the `Module`, and so internal copies must be constructed -and connected to these arguments by the constructor and only then can other -logic signals be connected to these copies. If you do not do this, a trace error -will occur, but that only happens when this module is instantiated in another -- -it will not show up while testing which just instantiates the module in a test -environment. See [Modules](https://intel.github.io/rohd-website/docs/modules/) -for more detail. - -A key pattern used in ROHD-HCL is to have the constructor take only input -signals as arguments and generate the output signal widths based on these -signals and other parameters. - -## Port Types - -Signals can take various forms in ROHD, and it is important to consider what form -you want for the API of the `Module` you are building. ROHD supports the basic -`Logic` signal which has its width encode (therefore you should not be using -width as a parameter to a `Module`). ROHD provides basic cloning and accessor -helper functions like `addInput` and `input`. - -`LogicArray` is a uniform multidimensional array of leaf `Logic` signals. Using -this for input/output will require special routines like `addInputArray` to -connect external and internal signals. Examples of using `LogicArray` are in the -`Serializer` and `Deserializer` components. - -`LogicStructure` is a hierarchical concatenation of named `Logic` fields, where -the `FloatingPoint` arithmetic type is an example used in the -`FloatingPointMultiplierSimple` module. We can also pass in `LogicStructure` as -a type for certain components so that the field structure is not lost on input -and output. A good example of this is `Fifo`, which is templatized on -`LogicType` to allow for us to generate a `Fifo` for a particular -`LogicStructure` to use when pushing and popping the data in and out. Here, -`addTypedInput` is a method used to help with creating the internal signals. - -`Interface` is similar to `LogicStructure`, yet it provides an ability to define -directionality to the internal fields, useful in connecting modules that share a -common protocol such as the `ApbInterface`. See -[Interfaces](https://intel.github.io/rohd-website/docs/interfaces). A few -examples of key general interface types that you can inherit from are the -`PairInterface` and the `DataPortInterface`. the `Memory` module has a good -example of how `DataPortInterface`s are cloned internally using its `connectIO` -method. -The `Fifo` has a good example of using an `Interface` to wrap a `LogicStructure`. - -When wrapping `LogicStructure` with `Interface`, don't name the `LogicStructure` -as `Interface` will need to uniquify (a known bug in `Interface`). - -An important kind of `Interface` is the `PairInterface` which is designed for -bidirectional communication and provides a `pairConnectIO` method for connecting -external and internal ports based on producer/consumer filtering. - -## Logic Internals - -Signal logic is constructed in a ROHD component by assignment and simple logic -operations like and (`&`) and or (`|`) as well as multiplexing (`mux`) and -flopping (`flop`). See -[operations](https://intel.github.io/rohd-website/docs/logic-math-compare/) for -more detail. - - More complex logic can be constructed using - [`Sequental`](https://intel.github.io/rohd-website/docs/sequentials/) and - [`Combinational`](https://intel.github.io/rohd-website/docs/conditionals/) - blocks similar to SystemVerilog `always` blocks. There is also a - [`FiniteStateMachine`](https://intel.github.io/rohd-website/docs/fsm/) - construct for state machines and a - [`Pipeline`](https://intel.github.io/rohd-website/docs/pipelines/) construct - for assisting with pipelined logic. - - Try to minimize the addition of new internal signals, by just reusing the - signals created by the ports or by subcomponents. Use `.named` to create clean - SystemVerilog names. - -### Debug - - If you want to expose internal signals onto the interface of a `Module` for - debug, a simple method is to declare them as a field in the class (Use -`@protected` in case this is exposed, so it doesn't become part of the API). This - signal will be available in tests as module.field. - -## Unit Testing - - A good component has unit tests to validate the component and provide examples - of use. We use the Dart testing framework which requires that tests are stored - in the `test/` directory and are named ending in `_test.dart`. An example of - unit tests for a component is shown below. Note that grouping of tests can - reuse a common component built for multiple tests. Also note that each test - with sequential logic will need a `SimpleClockGenerator`, a `Simulator.run()` - and an `endSimulation`. Some helper methods (like `.waitCycles`) are available - in the rohd-fv package. - - ```dart -void main() { - tearDown(() async { - await Simulator.reset(); - }); - - group('test narrow component', () { - final input = Logic(width: 5); - final component = MyComponent(input); - final output = component.out; - - test('MyComponent smoke test', () async { - final clk = SimpleClockGenerator(10).clk; - - unawaited(Simulator.run()); - reset.inject(1); - await clk.waitCycles(3); - reset.inject(0); - await clk.waitCycles(1); - input.inject(1); - await clk.waitCycles(3); - expect(output.value, equals(Const(1, width: output.width))); - - await Simulator.endSimulation(); - }); - - test('MyComponent second test', () async { - final clk = SimpleClockGenerator(10).clk; - - unawaited(Simulator.run()); - reset.inject(1); - await clk.waitCycles(3); - reset.inject(0); - await clk.waitCycles(1); - input.inject(6); - await clk.waitCycles(3); - expect(output.value, equals(Const(6, width: output.width))); - - await Simulator.endSimulation(); - }); - }); -} -``` - -Prefer using `waitCycles` instead of `nextPosedge` and use `inject` instead of -`put` when working with sequential tests. - -When testing a combinational path, and you `inject` inputs after a positive -clock edge, if you sample at the next clock edge, you will miss the -combinational value. Instead, use the output `previousValue` at the next clock -edge, or sample the output at `nextNegEdge` to look at the value midway through -the clock cycle. - -While creating unit tests, you can just run the tests for your component instead -of running the entire suite of ROHD-HCL tests. The entire regression suite -takes quite a long time and is only necessary if you make changes to some core -functionality. +# How to Build a Great Component in ROHD + +Since ROHD is an extension of the Dart programming language, please follow all +Dart programming and documentation conventions. + +The `Module` class is the base class used in ROHD to build components, and +calling the constructor the `Module` instantiates the component and connects it +to signals passed into the constructor. + +Since ROHD is written in Dart, you should use the Dart best practices, such as +camel-case variable naming, commenting patterns for all public APIs, and take +advantage of dart format and dart analyze tools. + +## Port Construction and Connection + +A `Module` constructor takes `Logic` arguments and parameters to generate a +hardware component. The `Logic` arguments are actually the external signals +being connected to by the `Module`, and so internal copies must be constructed +and connected to these arguments by the constructor and only then can other +logic signals be connected to these copies. If you do not do this, a trace error +will occur, but that only happens when this module is instantiated in another -- +it will not show up while testing which just instantiates the module in a test +environment. See [Modules](https://intel.github.io/rohd-website/docs/modules/) +for more detail. + +A key pattern used in ROHD-HCL is to have the constructor take only input +signals as arguments and generate the output signal widths based on these +signals and other parameters. + +## Port Types + +Signals can take various forms in ROHD, and it is important to consider what form +you want for the API of the `Module` you are building. ROHD supports the basic +`Logic` signal which has its width encode (therefore you should not be using +width as a parameter to a `Module`). ROHD provides basic cloning and accessor +helper functions like `addInput` and `input`. + +`LogicArray` is a uniform multidimensional array of leaf `Logic` signals. Using +this for input/output will require special routines like `addInputArray` to +connect external and internal signals. Examples of using `LogicArray` are in the +`Serializer` and `Deserializer` components. + +`LogicStructure` is a hierarchical concatenation of named `Logic` fields, where +the `FloatingPoint` arithmetic type is an example used in the +`FloatingPointMultiplierSimple` module. We can also pass in `LogicStructure` as +a type for certain components so that the field structure is not lost on input +and output. A good example of this is `Fifo`, which is templatized on +`LogicType` to allow for us to generate a `Fifo` for a particular +`LogicStructure` to use when pushing and popping the data in and out. Here, +`addTypedInput` is a method used to help with creating the internal signals. + +`Interface` is similar to `LogicStructure`, yet it provides an ability to define +directionality to the internal fields, useful in connecting modules that share a +common protocol such as the `ApbInterface`. See +[Interfaces](https://intel.github.io/rohd-website/docs/interfaces). A few +examples of key general interface types that you can inherit from are the +`PairInterface` and the `DataPortInterface`. the `Memory` module has a good +example of how `DataPortInterface`s are cloned internally using its `connectIO` +method. +The `Fifo` has a good example of using an `Interface` to wrap a `LogicStructure`. + +When wrapping `LogicStructure` with `Interface`, don't name the `LogicStructure` +as `Interface` will need to uniquify (a known bug in `Interface`). + +An important kind of `Interface` is the `PairInterface` which is designed for +bidirectional communication and provides a `pairConnectIO` method for connecting +external and internal ports based on producer/consumer filtering. + +## Logic Internals + +Signal logic is constructed in a ROHD component by assignment and simple logic +operations like and (`&`) and or (`|`) as well as multiplexing (`mux`) and +flopping (`flop`). See +[operations](https://intel.github.io/rohd-website/docs/logic-math-compare/) for +more detail. + + More complex logic can be constructed using + [`Sequental`](https://intel.github.io/rohd-website/docs/sequentials/) and + [`Combinational`](https://intel.github.io/rohd-website/docs/conditionals/) + blocks similar to SystemVerilog `always` blocks. There is also a + [`FiniteStateMachine`](https://intel.github.io/rohd-website/docs/fsm/) + construct for state machines and a + [`Pipeline`](https://intel.github.io/rohd-website/docs/pipelines/) construct + for assisting with pipelined logic. + + Try to minimize the addition of new internal signals, by just reusing the + signals created by the ports or by subcomponents. Use `.named` to create clean + SystemVerilog names. + +### Debug + + If you want to expose internal signals onto the interface of a `Module` for + debug, a simple method is to declare them as a field in the class (Use +`@protected` in case this is exposed, so it doesn't become part of the API). This + signal will be available in tests as module.field. + +## Unit Testing + + A good component has unit tests to validate the component and provide examples + of use. We use the Dart testing framework which requires that tests are stored + in the `test/` directory and are named ending in `_test.dart`. An example of + unit tests for a component is shown below. Note that grouping of tests can + reuse a common component built for multiple tests. Also note that each test + with sequential logic will need a `SimpleClockGenerator`, a `Simulator.run()` + and an `endSimulation`. Some helper methods (like `.waitCycles`) are available + in the rohd-fv package. + + ```dart +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('test narrow component', () { + final input = Logic(width: 5); + final component = MyComponent(input); + final output = component.out; + + test('MyComponent smoke test', () async { + final clk = SimpleClockGenerator(10).clk; + + unawaited(Simulator.run()); + reset.inject(1); + await clk.waitCycles(3); + reset.inject(0); + await clk.waitCycles(1); + input.inject(1); + await clk.waitCycles(3); + expect(output.value, equals(Const(1, width: output.width))); + + await Simulator.endSimulation(); + }); + + test('MyComponent second test', () async { + final clk = SimpleClockGenerator(10).clk; + + unawaited(Simulator.run()); + reset.inject(1); + await clk.waitCycles(3); + reset.inject(0); + await clk.waitCycles(1); + input.inject(6); + await clk.waitCycles(3); + expect(output.value, equals(Const(6, width: output.width))); + + await Simulator.endSimulation(); + }); + }); +} +``` + +Prefer using `waitCycles` instead of `nextPosedge` and use `inject` instead of +`put` when working with sequential tests. + +When testing a combinational path, and you `inject` inputs after a positive +clock edge, if you sample at the next clock edge, you will miss the +combinational value. Instead, use the output `previousValue` at the next clock +edge, or sample the output at `nextNegEdge` to look at the value midway through +the clock cycle. + +While creating unit tests, you can just run the tests for your component instead +of running the entire suite of ROHD-HCL tests. The entire regression suite +takes quite a long time and is only necessary if you make changes to some core +functionality. + +## ROHD-HCL Component Integration + +After implementing a component, export it from the appropriate library barrel +and `lib/rohd_hcl.dart`. Add focused tests under `test/`, including a +nested-parent build test for every composable module. + +If the component should appear in the configuration application, create a +`Configurator` under `lib/src/component_config/components/` and register it in +`lib/src/component_config/components/component_registry.dart`. The registry +drives both the configuration app and documentation netlist generation. + +Complete the Confapp source-asset, generated-view, and regression-test work +described in [the Confapp README](../confapp/README.md). Application setup and +dependency-source selection are documented in +[confapp/DEVELOPER.md](../confapp/DEVELOPER.md). + +## Component Validation + +Run focused validation while iterating: + +```bash +dart format lib test +dart analyze --fatal-infos +dart test test/path/to/component_test.dart +``` + +Before submitting a component change, run the applicable broader test suite and +verify that the component builds when instantiated beneath a parent module. diff --git a/doc/README.md b/doc/README.md index f4a233d61a..dc8a59bea5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -137,9 +137,9 @@ Some in-development items will have opened issues, as well. Feel free to create ## Adding a New Component -Please refer to [Component](./Component.md.md) for the best practices for creating new components. +Please refer to [Component](./Component.md) for the best practices for creating new components. ---------------- -Copyright (C) 2023-2025 Intel Corporation +Copyright (C) 2023-2026 Intel Corporation SPDX-License-Identifier: BSD-3-Clause diff --git a/doc/d3-hwschematic b/doc/d3-hwschematic deleted file mode 160000 index 65f6487e1d..0000000000 --- a/doc/d3-hwschematic +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 65f6487e1d664c217d6d963acfffea62a7042991 diff --git a/doc/schematic_viewer/.gitignore b/doc/schematic_viewer/.gitignore new file mode 100644 index 0000000000..5c291245d2 --- /dev/null +++ b/doc/schematic_viewer/.gitignore @@ -0,0 +1,19 @@ +# Flutter/Dart/Pub +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +pubspec_overrides.yaml +pubspec_overrides.yaml.disabled* +/build/ +/coverage/ + +# Editors and platforms not used by this web-only wrapper +.idea/ +.vscode/ +*.iml +android/ +ios/ +linux/ +macos/ +windows/ diff --git a/doc/schematic_viewer/.metadata b/doc/schematic_viewer/.metadata new file mode 100644 index 0000000000..7eb5625037 --- /dev/null +++ b/doc/schematic_viewer/.metadata @@ -0,0 +1,20 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tooling to assess capabilities and perform upgrades. + +version: + revision: "b8962555571d8c170cff8e76023ea7bf60e5ec4b" + channel: "stable" + +project_type: app + +migration: + platforms: + - platform: root + create_revision: b8962555571d8c170cff8e76023ea7bf60e5ec4b + base_revision: b8962555571d8c170cff8e76023ea7bf60e5ec4b + - platform: web + create_revision: b8962555571d8c170cff8e76023ea7bf60e5ec4b + base_revision: b8962555571d8c170cff8e76023ea7bf60e5ec4b + + unmanaged_files: + - 'lib/main.dart' diff --git a/doc/schematic_viewer/README.md b/doc/schematic_viewer/README.md new file mode 100644 index 0000000000..13e124ee2f --- /dev/null +++ b/doc/schematic_viewer/README.md @@ -0,0 +1,21 @@ +# ROHD-HCL schematic viewer + +This web-only Flutter application loads a ROHD netlist selected with the +`json` query parameter and renders it with `package:rohd_schematic_viewer`. + +For example: + +```text +schematic_viewer/index.html?json=../schematics/Component.rohd.json +``` + +CI builds the application with +`tool/gh_actions/build_schematic_viewer.sh`. Component netlists and +backward-compatible API-documentation redirects are generated by +`tool/gh_actions/create_schematic_pages.sh`. + +The checked-in manifests use hosted ROHD packages, including the schematic +viewer. For local testing, +`tool/confapp_dev_mode.sh configure` generates the ignored root +`pubspec_overrides.yaml` with only the packages that should use Git or local +sources. diff --git a/doc/schematic_viewer/analysis_options.yaml b/doc/schematic_viewer/analysis_options.yaml new file mode 100644 index 0000000000..3630223a0c --- /dev/null +++ b/doc/schematic_viewer/analysis_options.yaml @@ -0,0 +1,6 @@ +analyzer: + exclude: + - build/** + - web/** + +include: ../../analysis_options.yaml diff --git a/doc/schematic_viewer/lib/browser_location.dart b/doc/schematic_viewer/lib/browser_location.dart new file mode 100644 index 0000000000..1e50741ce7 --- /dev/null +++ b/doc/schematic_viewer/lib/browser_location.dart @@ -0,0 +1,11 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_location.dart +// Selects browser location access when compiling for the web. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +export 'browser_location_stub.dart' + if (dart.library.js_interop) 'browser_location_web.dart'; diff --git a/doc/schematic_viewer/lib/browser_location_stub.dart b/doc/schematic_viewer/lib/browser_location_stub.dart new file mode 100644 index 0000000000..86d47a2ebf --- /dev/null +++ b/doc/schematic_viewer/lib/browser_location_stub.dart @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_location_stub.dart +// VM-safe placeholder for browser location access. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +/// Reports that the current browser location is unavailable on the VM. +Uri currentBrowserLocation() => throw UnsupportedError( + 'The current browser location is only available on web.', + ); diff --git a/doc/schematic_viewer/lib/browser_location_web.dart b/doc/schematic_viewer/lib/browser_location_web.dart new file mode 100644 index 0000000000..f073562d54 --- /dev/null +++ b/doc/schematic_viewer/lib/browser_location_web.dart @@ -0,0 +1,13 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// browser_location_web.dart +// Browser implementation of location access. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +import 'package:web/web.dart' as web; + +/// Returns the current browser page location. +Uri currentBrowserLocation() => Uri.parse(web.window.location.href); diff --git a/doc/schematic_viewer/lib/main.dart b/doc/schematic_viewer/lib/main.dart new file mode 100644 index 0000000000..a19faff238 --- /dev/null +++ b/doc/schematic_viewer/lib/main.dart @@ -0,0 +1,171 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// main.dart +// Standalone web entry point for viewing generated ROHD schematic netlists. +// +// 2026 September 13 +// Author: Desmond A. Kirkpatrick + +import 'dart:async' show unawaited; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:material_ui/material_ui.dart'; +import 'package:rohd_schematic_viewer/schematic_viewer.dart'; +import 'package:schematic_viewer/browser_location.dart'; + +/// Loads schematic JSON from [uri]. +typedef SchematicJsonLoader = Future Function(Uri uri); + +/// Builds the rendered schematic after its JSON has been loaded and validated. +typedef SchematicContentBuilder = Widget Function( + BuildContext context, + String schematicJson, + SchematicThemeMode themeMode, +); + +/// Runs the standalone schematic viewer application. +void main() { + runApp(const SchematicViewerApp()); +} + +/// The root application for the standalone schematic viewer. +class SchematicViewerApp extends StatelessWidget { + /// Creates the standalone schematic viewer application. + const SchematicViewerApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp( + title: 'ROHD Schematic', + debugShowCheckedModeBanner: false, + theme: ThemeData.light(useMaterial3: true), + darkTheme: ThemeData.dark(useMaterial3: true), + home: const SchematicPage(), + ); +} + +/// Loads and displays the schematic selected by the `json` query parameter. +class SchematicPage extends StatefulWidget { + /// Creates a page for the selected schematic. + const SchematicPage({ + this.location, + this.loadJson, + this.schematicBuilder, + super.key, + }); + + /// Page location containing the `json` query parameter. + /// + /// Defaults to the current browser location. + final Uri? location; + + /// Loads the selected schematic JSON. + /// + /// Defaults to an HTTP read. + final SchematicJsonLoader? loadJson; + + /// Builds the schematic content after a successful load. + /// + /// Defaults to [EmbeddedSchematicViewer]. + final SchematicContentBuilder? schematicBuilder; + + @override + State createState() => _SchematicPageState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('location', location)) + ..add(ObjectFlagProperty.has('loadJson', loadJson)) + ..add( + ObjectFlagProperty.has( + 'schematicBuilder', + schematicBuilder, + ), + ); + } +} + +class _SchematicPageState extends State { + String? _jsonData; + String? _error; + String _title = 'ROHD Schematic'; + + @override + void initState() { + super.initState(); + unawaited(_loadSchematic()); + } + + Future _loadSchematic() async { + final location = widget.location ?? currentBrowserLocation(); + final jsonPath = location.queryParameters['json']; + if (jsonPath == null || jsonPath.isEmpty) { + setState(() { + _error = 'No schematic specified. ' + 'Use ?json=../schematics/ComponentName.rohd.json'; + }); + return; + } + + final filename = jsonPath.split('/').last.replaceAll('.rohd.json', ''); + setState(() { + _title = filename; + }); + + try { + final loader = widget.loadJson ?? http.read; + final response = await loader(location.resolve(jsonPath)); + final decoded = jsonDecode(response); + if (decoded is! Map) { + throw const FormatException( + 'Schematic JSON must contain an object.', + ); + } + if (!mounted) { + return; + } + setState(() { + _jsonData = response; + }); + } on Exception catch (error) { + if (!mounted) { + return; + } + setState(() { + _error = 'Failed to load $jsonPath: $error'; + }); + } + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Scaffold( + appBar: AppBar(title: Text(_title), toolbarHeight: 36), + body: switch ((_error, _jsonData)) { + (final error?, _) => Center( + child: Text(error, style: const TextStyle(fontSize: 16)), + ), + (_, final jsonData?) => (widget.schematicBuilder ?? + ( + context, + schematicJson, + themeMode, + ) => + EmbeddedSchematicViewer.fromJson( + schematicJson: schematicJson, + themeMode: themeMode, + ))( + context, + jsonData, + isDark ? SchematicThemeMode.dark : SchematicThemeMode.light, + ), + _ => const Center(child: CircularProgressIndicator()), + }, + ); + } +} diff --git a/doc/schematic_viewer/pubspec.yaml b/doc/schematic_viewer/pubspec.yaml new file mode 100644 index 0000000000..9015b5de5d --- /dev/null +++ b/doc/schematic_viewer/pubspec.yaml @@ -0,0 +1,24 @@ +name: schematic_viewer +description: Standalone schematic viewer for ROHD-HCL API documentation. +publish_to: none +version: 1.0.0 + +environment: + sdk: '>=3.6.0 <4.0.0' + flutter: '>=3.44.0' + +dependencies: + cupertino_icons: ^1.0.8 + flutter: + sdk: flutter + http: ^1.0.0 + material_ui: ^1.1.1 + rohd_schematic_viewer: 0.2.0 + web: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/doc/schematic_viewer/test/schematic_page_test.dart b/doc/schematic_viewer/test/schematic_page_test.dart new file mode 100644 index 0000000000..72cb75652d --- /dev/null +++ b/doc/schematic_viewer/test/schematic_page_test.dart @@ -0,0 +1,124 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// schematic_page_test.dart +// Tests the standalone documentation schematic page. +// +// 2026 September 30 +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:material_ui/material_ui.dart'; +import 'package:schematic_viewer/main.dart'; + +Widget _testApp({ + required Uri location, + required SchematicJsonLoader loadJson, + SchematicContentBuilder? schematicBuilder, +}) => + MaterialApp( + home: SchematicPage( + location: location, + loadJson: loadJson, + schematicBuilder: schematicBuilder ?? + (context, schematicJson, themeMode) => + Text('Rendered $schematicJson'), + ), + ); + +void main() { + testWidgets('shows guidance when the json parameter is missing', + (tester) async { + var loadCount = 0; + await tester.pumpWidget( + _testApp( + location: Uri.parse('https://example.com/viewer/'), + loadJson: (uri) async { + loadCount++; + return '{}'; + }, + ), + ); + await tester.pump(); + + expect( + find.textContaining('No schematic specified.'), + findsOneWidget, + ); + expect(loadCount, 0); + }); + + testWidgets('loads and renders the selected schematic', (tester) async { + final response = Completer(); + Uri? requestedUri; + const schematicJson = '{"modules":{"Example":{}}}'; + + await tester.pumpWidget( + _testApp( + location: Uri.parse( + 'https://example.com/viewer/?json=../schematics/Example.rohd.json', + ), + loadJson: (uri) { + requestedUri = uri; + return response.future; + }, + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + response.complete(schematicJson); + await tester.pumpAndSettle(); + + expect( + requestedUri, + Uri.parse('https://example.com/schematics/Example.rohd.json'), + ); + expect(find.text('Example'), findsOneWidget); + expect(find.text('Rendered $schematicJson'), findsOneWidget); + expect(find.byType(CircularProgressIndicator), findsNothing); + }); + + testWidgets('shows an error for malformed JSON', (tester) async { + await tester.pumpWidget( + _testApp( + location: Uri.parse( + 'https://example.com/viewer/?json=broken.rohd.json', + ), + loadJson: (uri) async => '{not json', + ), + ); + await tester.pumpAndSettle(); + + expect( + find.textContaining('Failed to load broken.rohd.json:'), + findsOneWidget, + ); + expect(find.textContaining('FormatException'), findsOneWidget); + expect(find.textContaining('Rendered'), findsNothing); + }); + + testWidgets('shows an error when the HTTP load fails', (tester) async { + await tester.pumpWidget( + _testApp( + location: Uri.parse( + 'https://example.com/viewer/?json=missing.rohd.json', + ), + loadJson: (uri) async => throw http.ClientException( + 'HTTP 404', + uri, + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + find.textContaining('Failed to load missing.rohd.json:'), + findsOneWidget, + ); + expect(find.textContaining('HTTP 404'), findsOneWidget); + expect(find.textContaining('Rendered'), findsNothing); + }); +} diff --git a/doc/schematic_viewer/web/index.html b/doc/schematic_viewer/web/index.html new file mode 100644 index 0000000000..176008752e --- /dev/null +++ b/doc/schematic_viewer/web/index.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + ROHD Schematic + + + + + + + + + diff --git a/doc/schematic_viewer/web/manifest.json b/doc/schematic_viewer/web/manifest.json new file mode 100644 index 0000000000..7c562058ba --- /dev/null +++ b/doc/schematic_viewer/web/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "ROHD-HCL Schematic Viewer", + "short_name": "ROHD Schematic", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "ROHD-HCL netlist schematic viewer", + "orientation": "landscape-primary", + "prefer_related_applications": false +} diff --git a/example/async_fifo_example.dart b/example/async_fifo_example.dart index 8752bc73b2..20656f51ba 100644 --- a/example/async_fifo_example.dart +++ b/example/async_fifo_example.dart @@ -7,6 +7,7 @@ // 2026 January 13 // Author: Maifee Ul Asad +// This executable example prints simulation results for interactive inspection. // ignore_for_file: avoid_print import 'dart:async'; diff --git a/example/clock_gating_example.dart b/example/clock_gating_example.dart index 1e7b2282e7..cae1697c52 100644 --- a/example/clock_gating_example.dart +++ b/example/clock_gating_example.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // clock_gating_example.dart @@ -7,6 +7,7 @@ // 2024 September 24 // Author: Max Korbel +// This executable example prints simulation results for interactive inspection. // ignore_for_file: avoid_print import 'dart:async'; diff --git a/example/example.dart b/example/example.dart index 51405e1a38..07aa54d52a 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // example.dart @@ -7,6 +7,7 @@ // 2023 February 17 // Author: Max Korbel +// This executable example prints generated output for interactive inspection. // ignore_for_file: avoid_print import 'package:rohd/rohd.dart'; diff --git a/gen/generate.dart b/gen/generate.dart index d7ad10ea7b..01cc9c99f1 100644 --- a/gen/generate.dart +++ b/gen/generate.dart @@ -1,24 +1,29 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // generate.dart -// Generate a series of examples for documentation. -// -// Call a generator to create an instance of your component for -// schematic viewing. +// Generate ROHD netlist JSON for each component in the registry. +// Used by CI to produce schematic pages in doc/api/schematics/. // // 2023 May 09 // Author: Desmond Kirkpatrick import 'dart:io'; +import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/src/component_config/components/component_registry.dart'; void main() async { Directory('build').createSync(recursive: true); for (final configurator in componentRegistry) { - final sv = await configurator.generateSV(); - final name = configurator.createModule().definitionName; - File('build/$name.sv').writeAsStringSync(sv); + final module = configurator.createModule(); + await module.build(); + + final synthesizer = NetlistSynthesizer(); + final builder = SynthBuilder(module, synthesizer); + final netlistJson = synthesizer.generateCombinedJson(builder, module); + await File( + 'build/${module.definitionName}.rohd.json', + ).writeAsString(netlistJson); } } diff --git a/lib/src/arithmetic/fixed_sqrt.dart b/lib/src/arithmetic/fixed_sqrt.dart index 4e05c51b43..01128261a4 100644 --- a/lib/src/arithmetic/fixed_sqrt.dart +++ b/lib/src/arithmetic/fixed_sqrt.dart @@ -1,8 +1,8 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Indentifier: BSD-3-Clause +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause // -// fixed_point_sqrt.dart -// An abstract base class defining the API for floating-point square root. +// fixed_sqrt.dart +// An abstract base class defining the API for fixed-point square root. // // 2025 March 3 // Authors: James Farwell , diff --git a/lib/src/arithmetic/floating_point/floating_point_multiplier.dart b/lib/src/arithmetic/floating_point/floating_point_multiplier.dart index 224ca2e7f1..d0d426dbb6 100644 --- a/lib/src/arithmetic/floating_point/floating_point_multiplier.dart +++ b/lib/src/arithmetic/floating_point/floating_point_multiplier.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // floating_point_multiplier_simple.dart @@ -70,6 +70,7 @@ abstract class FloatingPointMultiplier, Logic Function(Logic, Logic)) ppGen = KoggeStone.new, diff --git a/lib/src/arithmetic/floating_point/floating_point_sqrt.dart b/lib/src/arithmetic/floating_point/floating_point_sqrt.dart index 83e5e77b2f..062fe34e52 100644 --- a/lib/src/arithmetic/floating_point/floating_point_sqrt.dart +++ b/lib/src/arithmetic/floating_point/floating_point_sqrt.dart @@ -1,5 +1,5 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Indentifier: BSD-3-Clause +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause // // floating_point_sqrt.dart // An abstract base class defining the API for floating-point square root. diff --git a/lib/src/arithmetic/floating_point/floating_point_sqrt_simple.dart b/lib/src/arithmetic/floating_point/floating_point_sqrt_simple.dart index 8f413aba83..ad81f78064 100644 --- a/lib/src/arithmetic/floating_point/floating_point_sqrt_simple.dart +++ b/lib/src/arithmetic/floating_point/floating_point_sqrt_simple.dart @@ -1,8 +1,8 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Indentifier: BSD-3-Clause +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause // -// floating_point_sqrt.dart -// An abstract base class defining the API for floating-point square root. +// floating_point_sqrt_simple.dart +// A simple implementation of floating-point square root. // // 2025 March 4 // Authors: James Farwell , diff --git a/lib/src/arithmetic/multiplier_components/evaluate_compressor.dart b/lib/src/arithmetic/multiplier_components/evaluate_compressor.dart index 69e4bcdb3c..337a93010b 100644 --- a/lib/src/arithmetic/multiplier_components/evaluate_compressor.dart +++ b/lib/src/arithmetic/multiplier_components/evaluate_compressor.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // evaluate_compressor.dart @@ -38,9 +38,8 @@ extension EvaluateLiveColumnCompressor on ColumnCompressor { rowLogic.insert(0, colList[row].logic); } } - final rowBits = [for (final c in rowLogic) c.value].reversed.toList(); - // ignore: cascade_invocations - rowBits.addAll(List.filled(rowShift[row], LogicValue.zero)); + final rowBits = [for (final c in rowLogic) c.value].reversed.toList() + ..addAll(List.filled(rowShift[row], LogicValue.zero)); final rowBitsExtend = rowBits.length < width ? rowBits.swizzle().zeroExtend(width) : rowBits.swizzle(); @@ -57,9 +56,7 @@ extension EvaluateLiveColumnCompressor on ColumnCompressor { ..write('\n'); } - final sum = Logic(width: width); - // ignore: cascade_invocations - sum.put(accum.toSigned(width)); + final sum = Logic(width: width)..put(accum.toSigned(width)); ts.write(sum.elements .listString('p', prefix: 1, extraSpace: extraSpace, intValue: true)); diff --git a/lib/src/arithmetic/multiplier_components/evaluate_partial_product.dart b/lib/src/arithmetic/multiplier_components/evaluate_partial_product.dart index 9c7d7c7cc1..1cf2a30aff 100644 --- a/lib/src/arithmetic/multiplier_components/evaluate_partial_product.dart +++ b/lib/src/arithmetic/multiplier_components/evaluate_partial_product.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // evaluate_partial_product.dart @@ -101,9 +101,8 @@ extension EvaluateLivePartialProduct on PartialProductGeneratorBase { ..write('=' * (colWidth * maxWidth() + prefixCnt)) ..write('\n'); } - final sum = Logic(width: maxWidth()); - // ignore: cascade_invocations - sum.put(LogicValue.ofBigInt(evaluate(), maxWidth())); + final sum = Logic(width: maxWidth()) + ..put(LogicValue.ofBigInt(evaluate(), maxWidth())); str.write(sum.elements.listString('product', prefix: prefixCnt, alignHigh: maxWidth(), diff --git a/lib/src/arithmetic/multiplier_components/mulitiplier_components.dart b/lib/src/arithmetic/multiplier_components/mulitiplier_components.dart index 32ec8a0e52..9e2a727790 100644 --- a/lib/src/arithmetic/multiplier_components/mulitiplier_components.dart +++ b/lib/src/arithmetic/multiplier_components/mulitiplier_components.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multiplier_components.dart @@ -8,8 +8,8 @@ // 2024 August 9 // Author: Desmond Kirkpatrick -export './column_compressor.dart'; -export './multiplicand_selector.dart'; -export './multiplier_encoder.dart'; -export './partial_product_generator.dart'; -export './partial_product_sign_extend.dart'; +export 'column_compressor.dart'; +export 'multiplicand_selector.dart'; +export 'multiplier_encoder.dart'; +export 'partial_product_generator.dart'; +export 'partial_product_sign_extend.dart'; diff --git a/lib/src/arithmetic/multiply_accumulate.dart b/lib/src/arithmetic/multiply_accumulate.dart index afed23c2bb..5fe21860b7 100644 --- a/lib/src/arithmetic/multiply_accumulate.dart +++ b/lib/src/arithmetic/multiply_accumulate.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multiplier_accumulate.dart @@ -289,9 +289,14 @@ class MultiplyOnly extends MultiplyAccumulate { super.signedAddend, }) // Will be overrwridden by multiplyGenerator : super( - // ignore: prefer_interpolation_to_compose_strings - name: 'multiply_only_' + - _genName(mulGen, a, b, signedMultiplicand, signedMultiplier)) { + name: 'multiply_only_${_genName( + mulGen, + a, + b, + signedMultiplicand, + signedMultiplier, + )}', + ) { // Here we need to copy the Config and make sure we access our module's // input by calling .logic(this) on the runtimeConfig. @@ -316,6 +321,7 @@ class MultiplyOnly extends MultiplyAccumulate { accumulate <= mux( + // Signedness is part of the generated multiplier's protected API. // ignore: invalid_use_of_protected_member multiply.isProductSigned, multiply.product.signExtend(accumulate.width), diff --git a/lib/src/arithmetic/values/floating_point_values/floating_point_value_populator.dart b/lib/src/arithmetic/values/floating_point_values/floating_point_value_populator.dart index 024e730cc2..744cc24140 100644 --- a/lib/src/arithmetic/values/floating_point_values/floating_point_value_populator.dart +++ b/lib/src/arithmetic/values/floating_point_values/floating_point_value_populator.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // floating_point_value_populator.dart @@ -71,6 +71,7 @@ class FloatingPointValuePopulator { ..sign = sign ..exponent = exponent ..mantissa = mantissa + // This package helper finalizes the otherwise protected value lifecycle. // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_overriding_member ..validate(); } @@ -329,6 +330,7 @@ class FloatingPointValuePopulator { .ofConstant(constantFloatingPoint)) as FpvType; } final components = + // This package helper supplies fallback data for the protected hook. // ignore: invalid_use_of_visible_for_overriding_member, invalid_use_of_protected_member _unpopulated.getSpecialConstantComponents(constantFloatingPoint) ?? getConstantComponents(constantFloatingPoint); diff --git a/lib/src/clock_gating.dart b/lib/src/clock_gating.dart index 6b014dd39b..7b62b0fe4b 100644 --- a/lib/src/clock_gating.dart +++ b/lib/src/clock_gating.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // clock_gating.dart @@ -8,6 +8,7 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; +// ROHD does not expose the uniquifier required by this interface publicly. // ignore: implementation_imports import 'package:rohd/src/utilities/uniquifier.dart'; @@ -271,7 +272,7 @@ class ClockGate extends Module { if (_reset != null) { // we want to enable the clock during reset so that synchronous resets // work properly - internalEnable |= _reset!; + internalEnable |= _reset; } if (delayControlledSignals) { @@ -286,6 +287,6 @@ class ClockGate extends Module { gatedClk <= _controlIntf! - .gatedClockGenerator(_controlIntf!, _freeClk, internalEnable); + .gatedClockGenerator(_controlIntf, _freeClk, internalEnable); } } diff --git a/lib/src/component_config/components/config_floating_point_adder.dart b/lib/src/component_config/components/config_floating_point_adder.dart index ce24deb071..4f878e2e78 100644 --- a/lib/src/component_config/components/config_floating_point_adder.dart +++ b/lib/src/component_config/components/config_floating_point_adder.dart @@ -59,8 +59,16 @@ class FloatingPointAdderConfigurator extends Configurator { }; /// Controls the type of [ParallelPrefix] tree used in the other functions. - final prefixTreeKnob = - ChoiceConfigKnob(treeGeneratorMap.keys.toList(), value: KoggeStone); + final prefixTreeKnob = ChoiceConfigKnob( + treeGeneratorMap.keys.toList(), + value: KoggeStone, + choiceLabels: const { + Ripple: 'Ripple', + Sklansky: 'Sklansky', + KoggeStone: 'KoggeStone', + BrentKung: 'BrentKung', + }, + ); /// Controls the width of the exponent. final IntConfigKnob exponentWidthKnob = IntConfigKnob(value: 4); diff --git a/lib/src/component_config/components/config_leading_digit_anticipate.dart b/lib/src/component_config/components/config_leading_digit_anticipate.dart index 821218c0d6..0e22c9c85a 100644 --- a/lib/src/component_config/components/config_leading_digit_anticipate.dart +++ b/lib/src/component_config/components/config_leading_digit_anticipate.dart @@ -14,11 +14,18 @@ import 'package:rohd_hcl/rohd_hcl.dart'; class LeadingDigitAnticipateConfigurator extends Configurator { /// Controls whether we anticipate the leading digit change or count /// leading zeros. - final ChoiceConfigKnob anticipator = ChoiceConfigKnob([ - LeadingDigitAnticipate, - LeadingZeroAnticipate, - LeadingZeroAnticipateCarry - ], value: LeadingDigitAnticipate); + final ChoiceConfigKnob anticipator = ChoiceConfigKnob( + [ + LeadingDigitAnticipate, + LeadingZeroAnticipate, + LeadingZeroAnticipateCarry + ], + value: LeadingDigitAnticipate, + choiceLabels: const { + LeadingDigitAnticipate: 'LeadingDigitAnticipate', + LeadingZeroAnticipate: 'LeadingZeroAnticipate', + LeadingZeroAnticipateCarry: 'LeadingZeroAnticipateCarry', + }); /// Controls the width of the input. final IntConfigKnob inputWidthKnob = IntConfigKnob(value: 8); diff --git a/lib/src/component_config/components/config_one_hot.dart b/lib/src/component_config/components/config_one_hot.dart index 9010994d67..9c34ea0267 100644 --- a/lib/src/component_config/components/config_one_hot.dart +++ b/lib/src/component_config/components/config_one_hot.dart @@ -14,8 +14,14 @@ import 'package:rohd_hcl/rohd_hcl.dart'; class OneHotConfigurator extends Configurator { /// Controls whether toggling from binary to one-hot or /// from one-hot to binary. - final ChoiceConfigKnob directionKnob = - ChoiceConfigKnob([BinaryToOneHot, OneHotToBinary], value: OneHotToBinary); + final ChoiceConfigKnob directionKnob = ChoiceConfigKnob( + [BinaryToOneHot, OneHotToBinary], + value: OneHotToBinary, + choiceLabels: const { + BinaryToOneHot: 'BinaryToOneHot', + OneHotToBinary: 'OneHotToBinary', + }, + ); /// Controls the width of the input. final IntConfigKnob inputWidthKnob = IntConfigKnob(value: 8); diff --git a/lib/src/component_config/components/config_parallel_prefix_adder.dart b/lib/src/component_config/components/config_parallel_prefix_adder.dart index a155d2be6b..72eab97146 100644 --- a/lib/src/component_config/components/config_parallel_prefix_adder.dart +++ b/lib/src/component_config/components/config_parallel_prefix_adder.dart @@ -27,8 +27,16 @@ class ParallelPrefixAdderConfigurator extends Configurator { }; /// Controls the type of [ParallelPrefix] tree used in the adder. - final prefixTreeKnob = - ChoiceConfigKnob(generatorMap.keys.toList(), value: KoggeStone); + final prefixTreeKnob = ChoiceConfigKnob( + generatorMap.keys.toList(), + value: KoggeStone, + choiceLabels: const { + Ripple: 'Ripple', + Sklansky: 'Sklansky', + KoggeStone: 'KoggeStone', + BrentKung: 'BrentKung', + }, + ); /// Controls the width of the data.! final IntConfigKnob dataWidthKnob = IntConfigKnob(value: 4); diff --git a/lib/src/component_config/components/config_round_robin_arbiter.dart b/lib/src/component_config/components/config_round_robin_arbiter.dart index e7e6398a4e..91ef8ae085 100644 --- a/lib/src/component_config/components/config_round_robin_arbiter.dart +++ b/lib/src/component_config/components/config_round_robin_arbiter.dart @@ -19,8 +19,13 @@ class RoundRobinArbiterConfigurator extends Configurator { /// A knob controlling the implementation. final ChoiceConfigKnob implementationKnob = ChoiceConfigKnob( - [MaskRoundRobinArbiter, RotateRoundRobinArbiter], - value: MaskRoundRobinArbiter); + [MaskRoundRobinArbiter, RotateRoundRobinArbiter], + value: MaskRoundRobinArbiter, + choiceLabels: const { + MaskRoundRobinArbiter: 'MaskRoundRobinArbiter', + RotateRoundRobinArbiter: 'RotateRoundRobinArbiter', + }, + ); @override final String name = 'Round Robin Arbiter'; diff --git a/lib/src/component_config/components/config_serialization.dart b/lib/src/component_config/components/config_serialization.dart index 7030192707..06724b33ad 100644 --- a/lib/src/component_config/components/config_serialization.dart +++ b/lib/src/component_config/components/config_serialization.dart @@ -13,8 +13,14 @@ import 'package:rohd_hcl/rohd_hcl.dart'; /// A [Configurator] for [Serializer] and [Deserializer]. class SerializationConfigurator extends Configurator { /// Controls whether we are serializing or deserializing. - final ChoiceConfigKnob directionKnob = - ChoiceConfigKnob([Serializer, Deserializer], value: Serializer); + final ChoiceConfigKnob directionKnob = ChoiceConfigKnob( + [Serializer, Deserializer], + value: Serializer, + choiceLabels: const { + Serializer: 'Serializer', + Deserializer: 'Deserializer', + }, + ); /// Controls the width of the input. final IntConfigKnob inputWidthKnob = IntConfigKnob(value: 8); diff --git a/lib/src/component_config/config_knobs/adder_select_knob.dart b/lib/src/component_config/config_knobs/adder_select_knob.dart index 23c43868ca..01761bd42d 100644 --- a/lib/src/component_config/config_knobs/adder_select_knob.dart +++ b/lib/src/component_config/config_knobs/adder_select_knob.dart @@ -28,8 +28,16 @@ class AdderSelectKnob extends GroupOfKnobs { }; /// Controls the type of [ParallelPrefixAdder] used for internal adders. - final parallelPrefixTypeKnob = - ChoiceConfigKnob(adderGeneratorMap.keys.toList(), value: BrentKung); + final parallelPrefixTypeKnob = ChoiceConfigKnob( + adderGeneratorMap.keys.toList(), + value: BrentKung, + choiceLabels: const { + Ripple: 'Ripple', + Sklansky: 'Sklansky', + KoggeStone: 'KoggeStone', + BrentKung: 'BrentKung', + }, + ); /// Whether to instantiate a [ParallelPrefixAdder] (or use a [NativeAdder]). final ToggleConfigKnob parallelPrefixAdderKnob = diff --git a/lib/src/component_config/config_knobs/choice_config_knob.dart b/lib/src/component_config/config_knobs/choice_config_knob.dart index 8ae85949db..0577b98c2b 100644 --- a/lib/src/component_config/config_knobs/choice_config_knob.dart +++ b/lib/src/component_config/config_knobs/choice_config_knob.dart @@ -18,12 +18,89 @@ class ChoiceConfigKnob extends ConfigKnob { /// restrictive. List choices; + /// Stable labels used for both display and JSON serialization. + /// + /// Every choice that is a Dart [Type] must have an entry because + /// `Type.toString()` is not stable in minified builds. Other choice kinds + /// may also provide labels to override their default string representation. + /// + /// When migrating existing [Type] choices, use labels matching their + /// previous unminified names to preserve compatibility with saved JSON. + /// + /// The provided map is copied and exposed as unmodifiable so these + /// serialized values cannot change after construction. + final Map choiceLabels; + /// Creates a new knob to with the specified default [value] of the available /// [choices]. - ChoiceConfigKnob(this.choices, {required super.value}) { + /// + /// If any member of [choices] is a Dart [Type], [choiceLabels] must contain + /// a stable label for every such member. These labels are used as both + /// user-facing text and serialized JSON values: + /// + /// ```dart + /// ChoiceConfigKnob( + /// [BinaryToOneHot, OneHotToBinary], + /// value: OneHotToBinary, + /// choiceLabels: const { + /// BinaryToOneHot: 'BinaryToOneHot', + /// OneHotToBinary: 'OneHotToBinary', + /// }, + /// ); + /// ``` + /// + /// Matching labels to the previous unminified `Type.toString()` values + /// preserves compatibility with existing saved configurations. Every choice + /// must resolve to a unique serialized value; duplicate labels and labels + /// that collide with an unlabeled choice's string representation are + /// rejected. + ChoiceConfigKnob( + this.choices, { + required super.value, + Map choiceLabels = const {}, + }) : choiceLabels = Map.unmodifiable(choiceLabels) { if (!choices.contains(value)) { throw RohdHclException('Default value should be one of the choices.'); } + if (!this.choiceLabels.keys.every(choices.contains)) { + throw RohdHclException('Choice labels should correspond to choices.'); + } + if (choices + .whereType() + .any((choice) => !this.choiceLabels.containsKey(choice))) { + throw RohdHclException( + 'Type choices require stable labels for minified builds.'); + } + final serializedValues = choices.map(_serializedValue).toList(); + if (serializedValues.toSet().length != serializedValues.length) { + throw RohdHclException( + 'Choices must have unique serialized values and labels.'); + } + } + + /// Returns the user-facing label for [choice]. + String labelFor(T choice) { + final label = choiceLabels[choice]; + if (label != null) { + return label; + } + if (choice is Type) { + throw RohdHclException( + 'Type choices require stable labels for minified builds.'); + } + return choice is Enum ? choice.name : choice.toString(); + } + + String _serializedValue(T choice) { + final label = choiceLabels[choice]; + if (label != null) { + return label; + } + if (choice is Type) { + throw RohdHclException( + 'Type choices require stable labels for minified builds.'); + } + return choice.toString(); } @override @@ -38,9 +115,12 @@ class ChoiceConfigKnob extends ConfigKnob { @override void loadJson(Map decodedJson) { value = choices.firstWhere( - (element) => element.toString() == decodedJson['value'] as String); + (element) => _serializedValue(element) == decodedJson['value'] as String, + ); } @override - Map toJson() => {'value': value.toString()}; + Map toJson() => { + 'value': _serializedValue(value), + }; } diff --git a/lib/src/component_config/configurator.dart b/lib/src/component_config/configurator.dart index 259eaea260..5febcca389 100644 --- a/lib/src/component_config/configurator.dart +++ b/lib/src/component_config/configurator.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // configurator.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'package:rohd/rohd.dart'; +// ROHD does not expose the sanitizer required for generated names publicly. // ignore: implementation_imports import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; diff --git a/lib/src/models/amba/amba4_bfm/axi4_data_driver.dart b/lib/src/models/amba/amba4_bfm/axi4_data_driver.dart index 9b39e37d5f..7689a5909a 100644 --- a/lib/src/models/amba/amba4_bfm/axi4_data_driver.dart +++ b/lib/src/models/amba/amba4_bfm/axi4_data_driver.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // axi4_data_driver.dart @@ -87,7 +87,7 @@ class Axi4DataChannelDriver extends PendingClockedDriver { Future _driveRequestPacket(Axi4DataPacket packet) async { await sIntf.clk.nextPosedge; if (packet.data.width <= rIntf.data.width) { - Simulator.injectAction(() async { + Simulator.injectAction(() { rIntf.valid.put(1); rIntf.id?.put(packet.id); rIntf.user?.put(packet.user); @@ -110,7 +110,7 @@ class Axi4DataChannelDriver extends PendingClockedDriver { final it = (packet.data.width / rIntf.data.width).ceil(); for (var i = 0; i < it; i++) { final end = min(packet.data.width, (i + 1) * rIntf.data.width); - Simulator.injectAction(() async { + Simulator.injectAction(() { rIntf.valid.put(1); rIntf.id?.put(packet.id); rIntf.user?.put(packet.user); diff --git a/lib/src/models/amba/amba5_bfm/axi/axi5_driver.dart b/lib/src/models/amba/amba5_bfm/axi/axi5_driver.dart index 75ebe8178b..e5f2c92589 100644 --- a/lib/src/models/amba/amba5_bfm/axi/axi5_driver.dart +++ b/lib/src/models/amba/amba5_bfm/axi/axi5_driver.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // axi5_driver.dart @@ -7,6 +7,7 @@ // 2025 August // Author: Josh Kimmel +// Driver methods schedule protocol actions rather than assign plain properties. // ignore_for_file: use_setters_to_change_properties import 'dart:async'; @@ -525,7 +526,7 @@ class Axi5RChannelDriver extends PendingClockedDriver { if (_linkUtilizationEnabled) { _linkValidCount++; } - Simulator.injectAction(() async { + Simulator.injectAction(() { r.valid.put(1); r.data.put(packet.data[i].data); r.last?.put(packet.data[i].last ?? 1); diff --git a/lib/src/models/amba/amba5_bfm/axi_s/axi5_s_driver.dart b/lib/src/models/amba/amba5_bfm/axi_s/axi5_s_driver.dart index 25bb283bd3..93948fc084 100644 --- a/lib/src/models/amba/amba5_bfm/axi_s/axi5_s_driver.dart +++ b/lib/src/models/amba/amba5_bfm/axi_s/axi5_s_driver.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // axi5_s_driver.dart @@ -7,6 +7,7 @@ // 2025 August // Author: Josh Kimmel +// Driver methods schedule protocol actions rather than assign plain properties. // ignore_for_file: use_setters_to_change_properties import 'dart:async'; @@ -91,7 +92,7 @@ class Axi5StreamDriver extends PendingClockedDriver { if (_linkUtilizationEnabled) { _linkValidCount++; } - Simulator.injectAction(() async { + Simulator.injectAction(() { stream.valid.put(1); stream.id?.put(packet.id ?? 0); stream.user?.put(packet.user ?? 0); diff --git a/lib/src/models/amba/amba5_bfm/lti/lti_driver.dart b/lib/src/models/amba/amba5_bfm/lti/lti_driver.dart index 9fa5b533fe..a0dc821b48 100644 --- a/lib/src/models/amba/amba5_bfm/lti/lti_driver.dart +++ b/lib/src/models/amba/amba5_bfm/lti/lti_driver.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // lti_driver.dart @@ -7,6 +7,7 @@ // 2025 August // Author: Josh Kimmel +// Driver methods schedule protocol actions rather than assign plain properties. // ignore_for_file: use_setters_to_change_properties import 'dart:async'; diff --git a/lib/src/models/ready_valid_bfm/ready_valid_tracker.dart b/lib/src/models/ready_valid_bfm/ready_valid_tracker.dart index 8991918d80..d7c814fc0e 100644 --- a/lib/src/models/ready_valid_bfm/ready_valid_tracker.dart +++ b/lib/src/models/ready_valid_bfm/ready_valid_tracker.dart @@ -1,3 +1,12 @@ +// Copyright (C) 2024-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// ready_valid_tracker.dart +// A tracker for packets transferred over a ready/valid protocol. +// +// 2024 January 8 +// Author: Max Korbel + import 'package:rohd_hcl/src/models/models.dart'; import 'package:rohd_vf/rohd_vf.dart'; diff --git a/lib/src/models/sparse_memory_storage.dart b/lib/src/models/sparse_memory_storage.dart index 8a2b6ae537..568c4c61df 100644 --- a/lib/src/models/sparse_memory_storage.dart +++ b/lib/src/models/sparse_memory_storage.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // sparse_memory_storage.dart @@ -28,6 +28,7 @@ abstract class MemoryStorage { /// Default behavior for [onInvalidWrite]. static void _defaultOnInvalidWrite() { + // The default callback must warn users without requiring a logging setup. // ignore: avoid_print print('WARNING: Memory was cleared by invalid write!'); } @@ -42,6 +43,7 @@ abstract class MemoryStorage { /// Default behavior for [onInvalidRead]. static LogicValue _defaultOnInvalidRead(LogicValue addr, int dataWidth) { + // The default callback must warn users without requiring a logging setup. // ignore: avoid_print print('WARNING: reading from address $addr that has no data!'); return LogicValue.filled(dataWidth, LogicValue.x); diff --git a/pubspec.yaml b/pubspec.yaml index 574ba0d2d8..1c5ae6d0e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,18 +7,17 @@ issue_tracker: https://github.com/intel/rohd-hcl/issues documentation: https://intel.github.io/rohd-hcl/rohd_hcl/rohd_hcl-library.html environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.6.0 <4.0.0' + +workspace: + - confapp dependencies: collection: ^1.18.0 meta: ^1.9.1 - rohd: ^0.6.6 + rohd: ^0.6.11 rohd_vf: ^0.6.0 dev_dependencies: logging: ^1.0.1 test: ^1.25.0 - - - - diff --git a/rohd-hcl-multipackage.code-workspace b/rohd-hcl-multipackage.code-workspace new file mode 100644 index 0000000000..1689f83d8f --- /dev/null +++ b/rohd-hcl-multipackage.code-workspace @@ -0,0 +1,16 @@ +{ + "folders": [ + { + "name": "rohd_hcl", + "path": "." + }, + { + "name": "confapp", + "path": "confapp" + } + ], + "settings": { + "dart.projectSearchDepth": 8 + } +} + diff --git a/test/amba/amba4/axi4_bfm_test.dart b/test/amba/amba4/axi4_bfm_test.dart index 16c6b69046..5a91c7c2f5 100644 --- a/test/amba/amba4/axi4_bfm_test.dart +++ b/test/amba/amba4/axi4_bfm_test.dart @@ -1382,7 +1382,7 @@ void main() { await Test.reset(); }); - setUp(() async { + setUp(() { // Set the logger level Logger.root.level = Level.WARNING; }); diff --git a/test/amba/amba5/axi5_bfm_test.dart b/test/amba/amba5/axi5_bfm_test.dart index a49d202893..c830a2b412 100644 --- a/test/amba/amba5/axi5_bfm_test.dart +++ b/test/amba/amba5/axi5_bfm_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // axi5_bfm_test.dart @@ -350,9 +350,25 @@ class SimpleAxi5StreamSubordinateBfm extends Agent { if (r.last ?? true) { logger.info('Stream with ID ${r.id ?? 0} has completed - dropping.'); for (var j = 0; j < _streams.length; j++) { - logger.info('Stream beat $j data: ' - // ignore: lines_longer_than_80_chars - '${_strobeData(LogicValue.filled(sub.stream.dataWidth, LogicValue.zero), LogicValue.ofBigInt(_streams[j].data, sub.stream.dataWidth), LogicValue.ofInt(_streams[j].strb ?? LogicValue.filled(sub.stream.strbWidth, LogicValue.one).toInt(), sub.stream.strbWidth))}.'); + final zeros = LogicValue.filled( + sub.stream.dataWidth, + LogicValue.zero, + ); + final data = LogicValue.ofBigInt( + _streams[j].data, + sub.stream.dataWidth, + ); + final strobe = LogicValue.ofInt( + _streams[j].strb ?? + LogicValue.filled( + sub.stream.strbWidth, + LogicValue.one, + ).toInt(), + sub.stream.strbWidth, + ); + logger.info( + 'Stream beat $j data: ${_strobeData(zeros, data, strobe)}.', + ); } _streams.clear(); } @@ -640,7 +656,7 @@ void main() { await Test.reset(); }); - setUp(() async { + setUp(() { // Set the logger level Logger.root.level = Level.WARNING; }); diff --git a/test/amba/amba5/lti_bfm_test.dart b/test/amba/amba5/lti_bfm_test.dart index 64b4a19c37..4df2c2973c 100644 --- a/test/amba/amba5/lti_bfm_test.dart +++ b/test/amba/amba5/lti_bfm_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // lti_bfm_test.dart @@ -340,7 +340,7 @@ void main() { await Test.reset(); }); - setUp(() async { + setUp(() { // Set the logger level Logger.root.level = Level.WARNING; }); diff --git a/test/apb_bfm_test.dart b/test/apb_bfm_test.dart index 3db18e9d3f..0dad9a1142 100644 --- a/test/apb_bfm_test.dart +++ b/test/apb_bfm_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // apb_bfm_test.dart @@ -81,9 +81,9 @@ class ApbBfmTest extends Test { final jsonStr = File('$outFolder/apbTracker.tracker.json').readAsStringSync(); - final jsonContents = json.decode(jsonStr); - // ignore: avoid_dynamic_calls - expect(jsonContents['records'].length, 2 * numTransfers); + final jsonContents = json.decode(jsonStr) as Map; + final records = jsonContents['records'] as List; + expect(records.length, 2 * numTransfers); Directory(outFolder).deleteSync(recursive: true); }); diff --git a/test/apb_test.dart b/test/apb_test.dart index 075d22608c..84e142b4ec 100644 --- a/test/apb_test.dart +++ b/test/apb_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // apb_test.dart @@ -141,9 +141,9 @@ class ApbCsrCompleterHwTest extends Test { final jsonStr = File('$outFolder/apbTracker.tracker.json').readAsStringSync(); - final jsonContents = json.decode(jsonStr); - // ignore: avoid_dynamic_calls - expect(jsonContents['records'].length, 2 * numTransfers); + final jsonContents = json.decode(jsonStr) as Map; + final records = jsonContents['records'] as List; + expect(records.length, 2 * numTransfers); Directory(outFolder).deleteSync(recursive: true); }); @@ -238,7 +238,7 @@ void main() { await abpPair.build(); }); - test('abp optional ports null', () async { + test('abp optional ports null', () { final apb = ApbInterface(); expect(apb.aUser, isNull); expect(apb.bUser, isNull); diff --git a/test/arbiter_test.dart b/test/arbiter_test.dart index 23d3b80b07..ee30cdb7e4 100644 --- a/test/arbiter_test.dart +++ b/test/arbiter_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // arbiter_test.dart @@ -16,7 +16,7 @@ void main() { tearDown(() async { await Simulator.reset(); }); - test('priority arbiter', () async { + test('priority arbiter', () { const width = 8; final vector = Logic(width: width); diff --git a/test/arithmetic/adder_test.dart b/test/arithmetic/adder_test.dart index f85f8960a0..9a515ff1db 100644 --- a/test/arithmetic/adder_test.dart +++ b/test/arithmetic/adder_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // adder_test.dart @@ -7,6 +7,7 @@ // 2024 April 4 // Author: Desmond Kirkpatrick +// These white-box tests drive protected adder signals to verify each topology. // ignore_for_file: invalid_use_of_protected_member import 'dart:math'; @@ -31,7 +32,7 @@ void checkAdder(Adder adder, LogicValue av, LogicValue bv, LogicValue cv) { } void testAdderRandomIter(int n, int nSamples, Adder adder) { - test('random ci: ${adder.name}_W${n}_I$nSamples', () async { + test('random ci: ${adder.name}_W${n}_I$nSamples', () { for (var i = 0; i < nSamples; i++) { final aa = Random().nextLogicValue(width: n); final bb = Random().nextLogicValue(width: n); @@ -44,7 +45,7 @@ void testAdderRandomIter(int n, int nSamples, Adder adder) { void testAdderExhaustiveIter(int n, Adder mod) { test( 'exhaustive cin: ${mod.name}_W$n' - '_G${mod.name}', () async { + '_G${mod.name}', () { for (var aa = 0; aa < (1 << n); aa++) { for (var bb = 0; bb < (1 << n); bb++) { for (var cc = 0; cc < 2; cc++) { @@ -143,7 +144,7 @@ void main() { testAdderExhaustive(4, (a, b, {carryIn}) => CarrySelectCompoundAdder(a, b)); }); - test('trivial parallel prefix adder test', () async { + test('trivial parallel prefix adder test', () { const width = 6; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); @@ -157,7 +158,7 @@ void main() { expect(sum.value.toBigInt(), equals(BigInt.from(18 + 24))); }); - test('trivial sign magnitude adder test', () async { + test('trivial sign magnitude adder test', () { const width = 6; final aSign = Logic(name: 'aSign'); final a = Logic(name: 'a', width: width); @@ -289,7 +290,7 @@ void main() { } }); - test('trivial sign magnitude with onescomplement adder test', () async { + test('trivial sign magnitude with onescomplement adder test', () { const width = 8; final aSign = Logic(name: 'aSign'); final a = Logic(name: 'a', width: width); diff --git a/test/arithmetic/arithmetic_utils_test.dart b/test/arithmetic/arithmetic_utils_test.dart index 80155cc12c..1efa5996a1 100644 --- a/test/arithmetic/arithmetic_utils_test.dart +++ b/test/arithmetic/arithmetic_utils_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // arithmetic_utils_test.dart @@ -13,13 +13,7 @@ import 'package:test/test.dart'; void main() { final val = LogicValue.ofInt(200, 8); - final valL = Logic(width: val.width); - // ignore: cascade_invocations - valL.put(val); - final bigVal = LogicValue.ofString('1010101001101010010111101' * 8); - final bigValL = Logic(width: bigVal.width); - // ignore: cascade_invocations - bigValL.put(bigVal); + final valL = Logic(width: val.width)..put(val); test('listString alignment', () { { diff --git a/test/arithmetic/column_compressor_test.dart b/test/arithmetic/column_compressor_test.dart index 2aa429ae9b..8723648a1c 100644 --- a/test/arithmetic/column_compressor_test.dart +++ b/test/arithmetic/column_compressor_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // column_compressor_test.dart @@ -90,7 +90,7 @@ void main() { await Simulator.endSimulation(); }); - test('Column Compressor: single compressor evaluate', () async { + test('Column Compressor: single compressor evaluate', () { const widthX = 3; const widthY = 3; final a = Logic(name: 'a', width: widthX); diff --git a/test/arithmetic/compound_adder_test.dart b/test/arithmetic/compound_adder_test.dart index 3219bfbc12..093585a57e 100644 --- a/test/arithmetic/compound_adder_test.dart +++ b/test/arithmetic/compound_adder_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // compound_adder_test.dart @@ -16,8 +16,10 @@ import 'package:test/test.dart'; void checkCompoundAdder(CompoundAdder adder, LogicValue av, LogicValue bv) { final aB = av.toBigInt(); final bB = bv.toBigInt(); + // This white-box helper drives the adder's protected operand directly. // ignore: invalid_use_of_protected_member adder.a.put(av); + // This white-box helper drives the adder's protected operand directly. // ignore: invalid_use_of_protected_member adder.b.put(bv); @@ -78,7 +80,7 @@ void main() { CarrySelectCompoundAdder.splitSelectAdderAlgorithmNBit(4))); }); - test('trivial compound adder test', () async { + test('trivial compound adder test', () { const width = 6; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); @@ -245,7 +247,7 @@ void main() { expect(adder.sum.value.toInt(), equals(4)); }); - test('CarrySelectOnesComplementCompoundAdder case test', () async { + test('CarrySelectOnesComplementCompoundAdder case test', () { const width = 8; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); diff --git a/test/arithmetic/divider_test.dart b/test/arithmetic/divider_test.dart index 354ca27b37..e8d6210bd2 100644 --- a/test/arithmetic/divider_test.dart +++ b/test/arithmetic/divider_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // divider_test.dart @@ -111,7 +111,7 @@ class MultiCycleDividerDriver extends Driver { unawaited(super.run(phase)); // Listen to new items coming from the sequencer, and add them to a queue - sequencer.stream.listen((newItem) async { + sequencer.stream.listen((newItem) { _driverObjection ??= phase.raiseObjection('div_driver'); unawaited(_driverObjection!.dropped .then((value) => logger.fine('Driver objection dropped'))); diff --git a/test/arithmetic/dotproduct_test.dart b/test/arithmetic/dotproduct_test.dart index 52220c0b76..c130d3e330 100644 --- a/test/arithmetic/dotproduct_test.dart +++ b/test/arithmetic/dotproduct_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // dotproduct_test.dart @@ -23,7 +23,7 @@ void main() { GeneralDotProduct.new ]; - test('dotproduct width mismatch test', () async { + test('dotproduct width mismatch test', () { final multiplicands = [ Logic(width: 3, name: 'a_0'), Logic(width: 4, name: 'a_1'), @@ -60,7 +60,7 @@ void main() { } }); - test('dotproduct signed-variants exhaustive', () async { + test('dotproduct signed-variants exhaustive', () { const widths = [3, 3]; final depth = widths.length; final candValues = List.generate(depth, (_) => BigInt.zero); @@ -136,7 +136,7 @@ void main() { } }); - test('dotproduct signed-variants random', () async { + test('dotproduct signed-variants random', () { const iterations = 100; const widths = [4, 4, 4, 4, 4]; @@ -204,7 +204,7 @@ void main() { } }); - test('dotproduct singleton', () async { + test('dotproduct singleton', () { const widths = [3, 3]; final depth = widths.length; const signedMultiplicand = true; @@ -255,7 +255,7 @@ void main() { '''); }); - test('dotproduct trival case', () async { + test('dotproduct trivial case', () { const width = 4; final multiplicands = [Logic(width: width), Logic(width: width)]; final multipliers = [Logic(width: width), Logic(width: width)]; diff --git a/test/arithmetic/fixed_point_test.dart b/test/arithmetic/fixed_point_test.dart index de4497058d..186640eba7 100644 --- a/test/arithmetic/fixed_point_test.dart +++ b/test/arithmetic/fixed_point_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // fixed_point_test.dart @@ -10,7 +10,7 @@ import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:test/test.dart'; -void main() async { +void main() { test('FX Comparison', () { final fx1 = FixedPoint(integerWidth: 10, fractionWidth: 10); final fx2 = FixedPoint(integerWidth: 10, fractionWidth: 10); diff --git a/test/arithmetic/fixed_sqrt_test.dart b/test/arithmetic/fixed_sqrt_test.dart index e0bc2c466c..8f9370a6eb 100644 --- a/test/arithmetic/fixed_sqrt_test.dart +++ b/test/arithmetic/fixed_sqrt_test.dart @@ -1,5 +1,5 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Indentifier: BSD-3-Clause +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause // // fixed_sqrt_test.dart // Tests for fixed-point square root. @@ -18,7 +18,7 @@ void main() { tearDown(() async { await Simulator.reset(); }); - test('sqrt(negative number)', () async { + test('sqrt(negative number)', () { final fixed = FixedPoint(integerWidth: 3, fractionWidth: 23); expect(() => FixedPointSqrt(fixed), throwsException); }); diff --git a/test/arithmetic/fixed_to_float_test.dart b/test/arithmetic/fixed_to_float_test.dart index 658dcfef37..fd0666da16 100644 --- a/test/arithmetic/fixed_to_float_test.dart +++ b/test/arithmetic/fixed_to_float_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // fixed_to_float_test.dart @@ -12,8 +12,8 @@ import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:test/test.dart'; -void main() async { - test('FixedToFloat: singleton', () async { +void main() { + test('FixedToFloat: singleton', () { final fixed = FixedPoint(integerWidth: 34, fractionWidth: 33); const inDouble = -2.0; fixed.put(fixed.valuePopulator().ofDouble(inDouble)); @@ -59,7 +59,7 @@ void main() async { } }); - test('FixedToFloat addition with Anticipate: signed singleton', () async { + test('FixedToFloat addition with Anticipate: signed singleton', () { const width = 8; final val1 = LogicValue.ofInt(0, width); final val2 = LogicValue.ofInt(192, width); @@ -93,7 +93,7 @@ void main() async { '''); }); - test('FixedToFloat: Add with Anticipate: exhaustive', () async { + test('FixedToFloat: Add with Anticipate: exhaustive', () { const width = 8; final a = Logic(width: width)..put(0); final b = Logic(width: width)..put(0); @@ -140,7 +140,7 @@ void main() async { } }); - test('FixedToFloat: leadingDigit smoke test', () async { + test('FixedToFloat: leadingDigit smoke test', () { const width = 68; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); @@ -171,12 +171,10 @@ void main() async { expect(fpv2, equals(fpv)); }); - test('FixedToFloat: leadingDigit exhaustive', () async { + test('FixedToFloat: leadingDigit exhaustive', () { const width = 16; - final leadPredictIn = Logic(width: width); - // ignore: cascade_invocations - leadPredictIn.put(0); + final leadPredictIn = Logic(width: width)..put(0); final leadZeroCounter = RecursiveModulePriorityEncoder( leadPredictIn.reversed, generateValid: true); diff --git a/test/arithmetic/float_to_fixed_test.dart b/test/arithmetic/float_to_fixed_test.dart index ec7a176f88..0c9fea0137 100644 --- a/test/arithmetic/float_to_fixed_test.dart +++ b/test/arithmetic/float_to_fixed_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // float_to_fixed_test.dart @@ -12,7 +12,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:test/test.dart'; -void main() async { +void main() { test('E5M2 to Q16.16 exhaustive', () async { final float = FloatingPoint(exponentWidth: 5, mantissaWidth: 2); final dut = FloatToFixed(float); diff --git a/test/arithmetic/floating_point/floating_point_adder_dualpath_test.dart b/test/arithmetic/floating_point/floating_point_adder_dualpath_test.dart index 02df768067..3b32aa90dd 100644 --- a/test/arithmetic/floating_point/floating_point_adder_dualpath_test.dart +++ b/test/arithmetic/floating_point/floating_point_adder_dualpath_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // floating_point_dualpath_test.dart @@ -27,7 +27,7 @@ void main() { final fp2 = fpConstructor(); FloatingPointValuePopulator fpvPopulator() => fp1.valuePopulator(); - test('FP: dual-path adder N path singleton', () async { + test('FP: dual-path adder N path singleton', () { final fv1 = fpvPopulator().ofInts(0, 0, sign: true); final fv2 = fpvPopulator().ofInts(0, 1, sign: true); @@ -47,7 +47,7 @@ void main() { // isR is Addition or exponent delta >= 2 // N path is Subtraction & exponent delta < 2 - test('FP: dual-path adder N path, subtraction, delta < 2', () async { + test('FP: dual-path adder N path, subtraction, delta < 2', () { final one = fpvPopulator().ofConstant(FloatingPointConstants.one); fp1.put(one); fp2.put(one); @@ -122,7 +122,7 @@ void main() { await Simulator.endSimulation(); }); - test('FP: dual-path adder R path exhaustive', () async { + test('FP: dual-path adder R path exhaustive', () { fp1.put(0); fp2.put(0); final adder = FloatingPointAdderDualPath(fp1, fp2); @@ -210,7 +210,7 @@ void main() { FloatingPointValuePopulator fpvPopulator() => fp1.valuePopulator(); - test('FP: dual-path adder singleton merged path', () async { + test('FP: dual-path adder singleton merged path', () { fp1.put(0); fp2.put(0); final fv1 = fpvPopulator().ofInts(4, 7); @@ -457,7 +457,7 @@ void main() { }); }); - test('FP: dual-path adder full random wide', () async { + test('FP: dual-path adder full random wide', () { const exponentWidth = 11; const mantissaWidth = 52; diff --git a/test/arithmetic/floating_point/floating_point_adder_singlepath_test.dart b/test/arithmetic/floating_point/floating_point_adder_singlepath_test.dart index c137f0824a..b6af346bd0 100644 --- a/test/arithmetic/floating_point/floating_point_adder_singlepath_test.dart +++ b/test/arithmetic/floating_point/floating_point_adder_singlepath_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // floating_point_singlepath_test.dart @@ -20,7 +20,7 @@ void main() { await Simulator.reset(); }); - test('FP: simple wide singleton test', () async { + test('FP: simple wide singleton test', () { const exponentWidth = 4; const mantissaWidth = 18; FloatingPoint fpConstructor() => FloatingPoint( @@ -55,8 +55,8 @@ void main() { }); test('FP: simple adder truncating random', () { - const exponentWidth = 9; - const mantissaWidth = 15; + const exponentWidth = 5; + const mantissaWidth = 6; FloatingPoint fpConstructor() => FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth); final fp1 = fpConstructor(); @@ -68,7 +68,7 @@ void main() { final adder = FloatingPointAdderSinglePath(fp1, fp2, roundingMode: FloatingPointRoundingMode.truncate); final rand = Random(513); - for (var i = 0; i < 5000; i++) { + for (var i = 0; i < 500; i++) { final fv1 = fpvPopulator().random(rand); final fv2 = fpvPopulator().random(rand); if ((fv1.exponent.toInt() - fv2.exponent.toInt()).abs() > @@ -100,8 +100,8 @@ void main() { }); test('FP: simple adder rounding random', () { - const exponentWidth = 9; - const mantissaWidth = 15; + const exponentWidth = 5; + const mantissaWidth = 6; FloatingPoint fpConstructor() => FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth); @@ -113,7 +113,7 @@ void main() { fp2.put(0); final adder = FloatingPointAdderSinglePath(fp1, fp2); final rand = Random(513); - for (var i = 0; i < 5000; i++) { + for (var i = 0; i < 500; i++) { final fv1 = fpvPopulator().random(rand); final fv2 = fpvPopulator().random(rand); if ((fv1.exponent.toInt() - fv2.exponent.toInt()).abs() > @@ -297,8 +297,8 @@ void main() { }); test('FP: simple adder truncating exhaustive', () { - const exponentWidth = 4; - const mantissaWidth = 4; + const exponentWidth = 3; + const mantissaWidth = 3; FloatingPoint fpConstructor() => FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth); final fp1 = fpConstructor(); @@ -344,8 +344,8 @@ void main() { }); test('FP: simple adder rounding exhaustive', () { - const exponentWidth = 4; - const mantissaWidth = 4; + const exponentWidth = 3; + const mantissaWidth = 3; FloatingPoint fpConstructor() => FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth); final fp1 = fpConstructor(); @@ -511,7 +511,7 @@ void main() { final rand = Random(513); - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 100; i++) { final fv1 = fpvPopulator().random(rand, genSubNormal: false); final fv2 = fpvPopulator().random(rand, genSubNormal: false); @@ -541,7 +541,7 @@ void main() { }); }); - test('FP: adder simple wide mantissa singleton', () async { + test('FP: adder simple wide mantissa singleton', () { const exponentWidth = 2; const mantissaWidth = 20; FloatingPoint fpConstructor() => FloatingPoint( @@ -582,6 +582,7 @@ void main() { test('FP: adder simple wide mantissa random', () async { const exponentWidth = 2; const mantissaWidth = 20; + const randomSamples = 20; FloatingPoint fpConstructor() => FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth); final fp1 = fpConstructor(); @@ -598,7 +599,7 @@ void main() { final rand = Random(513); - for (var i = 0; i < 500; i++) { + for (var i = 0; i < randomSamples; i++) { final fv1 = fpvPopulator().random(rand); final fv2 = fpvPopulator().random(rand); @@ -626,7 +627,7 @@ void main() { }); test('FP: adder simple wide exponent random', () async { - const exponentWidth = 10; + const exponentWidth = 5; const mantissaWidth = 3; FloatingPoint fpConstructor() => FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth); @@ -642,7 +643,7 @@ void main() { final rand = Random(513); - for (var i = 0; i < 5000; i++) { + for (var i = 0; i < 500; i++) { final fv1 = fpvPopulator().random(rand); final fv2 = fpvPopulator().random(rand); if ((fv1.exponent.toInt() - fv2.exponent.toInt()).abs() > @@ -722,7 +723,7 @@ void main() { final fp2 = fpConstructor(); FloatingPointValuePopulator fpvPopulator() => fp1.valuePopulator(); - for (final outMantissaWidth in [6, 7, 8, 9, 15]) { + for (final outMantissaWidth in [6, 9]) { final fpOut = FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: outMantissaWidth); FloatingPointValuePopulator fpvOutPopulator() => fpOut.valuePopulator(); @@ -768,6 +769,8 @@ void main() { group('FP: explicit-jbit addition', () { const exponentWidth = 3; const mantissaWidth = 3; + const representativeExponents = [0, 1, 6, 7]; + const representativeMantissas = [0, 1, 7]; FloatingPointValuePopulator fpvPopulator({required bool explicitJBit}) => FloatingPointValue.populator( @@ -817,8 +820,9 @@ void main() { '''); }); - test('FP: simple adder with mixed explicit/implicit j-bit IO exhaustive', - () { + test( + 'FP: simple adder with mixed explicit/implicit j-bit IO ' + 'representative corners', () { for (final input1ExplicitJBit in [false, true]) { for (final input2ExplicitJBit in [false, true]) { final fp1 = fpConstructor(explicitJBit: input1ExplicitJBit); @@ -831,14 +835,12 @@ void main() { final adder = FloatingPointAdderSinglePath(fp1, fp2, outSum: fpOut); for (final subtract in [0, 1]) { - final expLimit = pow(2, exponentWidth); - final mantLimit = pow(2, mantissaWidth); - for (var e1 = 0; e1 < expLimit; e1++) { - for (var m1 = 0; m1 < mantLimit; m1++) { + for (final e1 in representativeExponents) { + for (final m1 in representativeMantissas) { final fv1 = fp1.valuePopulator().ofInts(e1, m1); if (fv1.isLegalValue()) { - for (var e2 = 0; e2 < expLimit; e2++) { - for (var m2 = 0; m2 < mantLimit; m2++) { + for (final e2 in representativeExponents) { + for (final m2 in representativeMantissas) { final fv2 = fp2 .valuePopulator() .ofInts(e2, m2, sign: subtract == 1); @@ -938,8 +940,8 @@ void main() { test( 'FP: simple adder with mixed explicit/implicit j-bit IO ' - 'widening exhaustive', () { - for (final outMantissaWidth in [3, 4, 5, 6, 7, 8, 9]) { + 'widening representative corners', () { + for (final outMantissaWidth in [3, 6]) { FloatingPoint fpOutConstructor({bool explicitJBit = false}) => FloatingPoint( exponentWidth: exponentWidth, @@ -959,14 +961,12 @@ void main() { FloatingPointAdderSinglePath(fp1, fp2, outSum: fpOut); for (final subtract in [0, 1]) { - final expLimit = pow(2, exponentWidth); - final mantLimit = pow(2, mantissaWidth); - for (var e1 = 0; e1 < expLimit; e1++) { - for (var m1 = 0; m1 < mantLimit; m1++) { + for (final e1 in representativeExponents) { + for (final m1 in representativeMantissas) { final fv1 = fp1.valuePopulator().ofInts(e1, m1); if (fv1.isLegalValue()) { - for (var e2 = 0; e2 < expLimit; e2++) { - for (var m2 = 0; m2 < mantLimit; m2++) { + for (final e2 in representativeExponents) { + for (final m2 in representativeMantissas) { final fv2 = fp2 .valuePopulator() .ofInts(e2, m2, sign: subtract == 1); @@ -1030,6 +1030,7 @@ void main() { test('FP: simple j-bit adder wide mantissa random', () { const exponentWidth = 8; const mantissaWidth = 25; + const randomSamples = 20; FloatingPoint fpConstructor({bool explicitJBit = false}) => FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth, @@ -1047,7 +1048,7 @@ void main() { fp2.put(0); final adder = FloatingPointAdderSinglePath(fp1, fp2, outSum: fpOut); final rand = Random(513); - for (var i = 0; i < 500; i++) { + for (var i = 0; i < randomSamples; i++) { final fv1 = fp1.valuePopulator().random(rand); final fv2 = fp2.valuePopulator().random(rand); if (fv1.isLegalValue() & fv2.isLegalValue()) { @@ -1095,7 +1096,7 @@ void main() { fp2.put(0); final adder = FloatingPointAdderSinglePath(fp1, fp2, outSum: fpout); final rand = Random(513); - for (var i = 0; i < 5000; i++) { + for (var i = 0; i < 100; i++) { final fv1 = fp1.valuePopulator().random(rand); final fv2 = fp2.valuePopulator().random(rand); if (fv1.isLegalValue() & fv2.isLegalValue()) { @@ -1134,8 +1135,8 @@ void main() { group('FP: single-path adder DAZ/FTZ tests', () { const exponentWidth = 3; const mantissaWidth = 3; - final expLimit = pow(2, exponentWidth).toInt(); - final mantLimit = pow(2, mantissaWidth).toInt(); + const representativeExponents = [0, 1, 7]; + const representativeMantissas = [0, 1, 7]; FloatingPoint fpConstructor({bool subNormalAsZero = false}) => FloatingPoint( exponentWidth: exponentWidth, @@ -1146,7 +1147,7 @@ void main() { exponentWidth: exponentWidth, mantissaWidth: mantissaWidth, subNormalAsZero: subNormalAsZero); - test('FP: single-path adder DAZ/FTZ test exhaustive', () { + test('FP: single-path adder DAZ/FTZ representative corners', () { for (final daz1 in [false, true]) { for (final daz2 in [false, true]) { for (final ftz in [false, true]) { @@ -1157,12 +1158,12 @@ void main() { fp1.put(0); fp2.put(0); final adder = FloatingPointAdderSinglePath(fp1, fp2, outSum: fpOut); - for (var e1 = 0; e1 < expLimit; e1++) { - for (var m1 = 0; m1 < mantLimit; m1++) { + for (final e1 in representativeExponents) { + for (final m1 in representativeMantissas) { for (final sign1 in [false, true]) { final fv1 = fp1.valuePopulator().ofInts(e1, m1, sign: sign1); - for (var e2 = 0; e2 < expLimit; e2++) { - for (var m2 = 0; m2 < mantLimit; m2++) { + for (final e2 in representativeExponents) { + for (final m2 in representativeMantissas) { for (final sign2 in [false, true]) { final fv2 = fp2.valuePopulator().ofInts(e2, m2, sign: sign2); diff --git a/test/arithmetic/floating_point/floating_point_conversion_test.dart b/test/arithmetic/floating_point/floating_point_conversion_test.dart index 431d72056a..d48cf54da8 100644 --- a/test/arithmetic/floating_point/floating_point_conversion_test.dart +++ b/test/arithmetic/floating_point/floating_point_conversion_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // floating_point_conversion_test.dart @@ -125,9 +125,8 @@ void main() { const normal = 0; // set to zero for subnormal testing final fp1 = FloatingPoint( - exponentWidth: exponentWidth, mantissaWidth: mantissaWidth); - // ignore: cascade_invocations - fp1.put(0); + exponentWidth: exponentWidth, mantissaWidth: mantissaWidth) + ..put(0); for (var destExponentWidth = exponentWidth; destExponentWidth < exponentWidth + 2; destExponentWidth++) { @@ -490,15 +489,15 @@ expected: $expected }); test('FP: conversion explicit to explicit j-bit exhaustive round-trip', () { - const exponentWidth = 6; - const mantissaWidth = 6; + // Small widths retain every encoding class while keeping this exhaustive. + const exponentWidth = 4; + const mantissaWidth = 4; final fp = FloatingPoint( exponentWidth: exponentWidth, mantissaWidth: mantissaWidth, - explicitJBit: true); - // ignore: cascade_invocations - fp.put(0); + explicitJBit: true) + ..put(0); for (final expDelta in [-2, 2]) { // TODO(desmonddak): fix narrowing bug and improve this test diff --git a/test/arithmetic/floating_point/floating_point_multiplier_test.dart b/test/arithmetic/floating_point/floating_point_multiplier_test.dart index c5930933e0..6f3df1f71c 100644 --- a/test/arithmetic/floating_point/floating_point_multiplier_test.dart +++ b/test/arithmetic/floating_point/floating_point_multiplier_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // floating_point_multiplier_test.dart @@ -174,7 +174,7 @@ void main() { } }); - test('FP: simple multiplier full random', () async { + test('FP: simple multiplier full random', () { const exponentWidth = 4; const mantissaWidth = 4; @@ -211,8 +211,7 @@ void main() { } }); - test('FP: simple multiplier full random with compression tree mult', - () async { + test('FP: simple multiplier full random with compression tree mult', () { const exponentWidth = 4; const mantissaWidth = 4; @@ -368,7 +367,7 @@ void main() { b.floatingPointValue.toDouble())); }); - test('FP: simple multiplier wide random', () async { + test('FP: simple multiplier wide random', () { const exponentWidth = 8; const mantissaWidth = 7; @@ -381,10 +380,9 @@ void main() { const expOutWidth = 8; const mantOutWidth = 23; - final fpofpOutt = FloatingPoint( - exponentWidth: expOutWidth, mantissaWidth: mantOutWidth); - // ignore: cascade_invocations - fpofpOutt.put(0); + final fpofpOutt = + FloatingPoint(exponentWidth: expOutWidth, mantissaWidth: mantOutWidth) + ..put(0); final multiplier = FloatingPointMultiplierSimple(fp1, fp2, outProduct: fpofpOutt); @@ -413,7 +411,7 @@ void main() { } }); - test('FP: simple multiplier sweep wide random', () async { + test('FP: simple multiplier sweep wide random', () { const exponentWidth = 3; const mantissaWidth = 3; @@ -427,9 +425,8 @@ void main() { for (var expOutWidth = 3; expOutWidth < 5; expOutWidth++) { for (var mantOutWidth = 3; mantOutWidth < 16; mantOutWidth += 4) { final fpOut = FloatingPoint( - exponentWidth: expOutWidth, mantissaWidth: mantOutWidth); - // ignore: cascade_invocations - fpOut.put(0); + exponentWidth: expOutWidth, mantissaWidth: mantOutWidth) + ..put(0); final multiplier = FloatingPointMultiplierSimple(fp1, fp2, outProduct: fpOut); diff --git a/test/arithmetic/floating_point/floating_point_sqrt_test.dart b/test/arithmetic/floating_point/floating_point_sqrt_test.dart index f8fa3aca86..3889ebc137 100644 --- a/test/arithmetic/floating_point/floating_point_sqrt_test.dart +++ b/test/arithmetic/floating_point/floating_point_sqrt_test.dart @@ -1,8 +1,8 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Indentifier: BSD-3-Clause +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause // -// floating_point_sqrt.dart -// An abstract base class defining the API for floating-point square root. +// floating_point_sqrt_test.dart +// Tests for floating-point square root implementations. // // 2025 March 5 // Authors: James Farwell , diff --git a/test/arithmetic/multiplier_encoder_test.dart b/test/arithmetic/multiplier_encoder_test.dart index 6bc9e7f797..b8ad4dda1e 100644 --- a/test/arithmetic/multiplier_encoder_test.dart +++ b/test/arithmetic/multiplier_encoder_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multiplier_encoder_test.dart @@ -48,7 +48,7 @@ void testPartialProductExhaustive(PartialProductGeneratorBase pp) { 'SD=${pp.signedMultiplicand ? 1 : 0} ' 'SM=${pp.signedMultiplier ? 1 : 0} ' 'SelD=${pp.selectSignedMultiplicand != null ? 1 : 0} ' - 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () async { + 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () { for (var i = 0; i < limitX; i++) { for (var j = 0; j < limitY; j++) { for (final multiplicandSign in multiplicandSigns) { @@ -92,7 +92,7 @@ void testPartialProductRandom(PartialProductGeneratorBase pp, int iterations) { 'SD=${pp.signedMultiplicand ? 1 : 0} ' 'SM=${pp.signedMultiplier ? 1 : 0} ' 'SelD=${pp.selectSignedMultiplicand != null ? 1 : 0} ' - 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () async { + 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () { final rand = Random(47); for (var i = 0; i < iterations; i++) { for (final multiplicandSign in multiplicandSigns) { @@ -126,7 +126,7 @@ void testPartialProductSingle( 'SD=${pp.signedMultiplicand ? 1 : 0} ' 'SM=${pp.signedMultiplier ? 1 : 0} ' 'SelD=${pp.selectSignedMultiplicand != null ? 1 : 0} ' - 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () async { + 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () { if (pp.selectSignedMultiplicand != null) { pp.selectSignedMultiplicand!.put(X.isNegative ? 1 : 0); } @@ -405,7 +405,7 @@ void main() { test('PartialProduct: flat test', () { const radix = 4; final radixEncoder = RadixEncoder(radix); - const widthX = 6; + const widthX = 4; const widthY = 3; final limitX = pow(2, widthX); final limitY = pow(2, widthY); @@ -433,8 +433,8 @@ void main() { test('PP Matrix: module test', () async { const radix = 4; final radixEncoder = RadixEncoder(radix); - const widthX = 20; - const widthY = 20; + const widthX = 12; + const widthY = 12; final multiplicand = Logic(width: widthX); final multiplier = Logic(width: widthY); @@ -451,10 +451,10 @@ void main() { File('compress.sv').writeAsStringSync(compress.generateSynth()); }); - test('single MAC partial product test', () async { + test('single MAC partial product test', () { final encoder = RadixEncoder(16); const widthX = 8; - const widthY = 18; + const widthY = 10; const i = 1478; const j = 9; @@ -498,10 +498,10 @@ void main() { expect(pp.evaluate(), equals(product)); }); - test('single MAC partial product sign extension test', () async { + test('single MAC partial product sign extension test', () { final encoder = RadixEncoder(16); const widthX = 8; - const widthY = 18; + const widthY = 10; const i = 1478; const j = 9; @@ -545,7 +545,7 @@ void main() { expect(pp.evaluate(), equals(product)); }); - test('majority function', () async { + test('majority function', () { expect(LogicValue.ofBigInt(BigInt.from(7), 5).majority(), true); expect(LogicValue.ofBigInt(BigInt.from(7) << 1, 5).majority(), true); expect(LogicValue.ofBigInt(BigInt.from(11) << 1, 5).majority(), true); diff --git a/test/arithmetic/multiplier_test.dart b/test/arithmetic/multiplier_test.dart index 91b8880390..c4ab0c9449 100644 --- a/test/arithmetic/multiplier_test.dart +++ b/test/arithmetic/multiplier_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multiplier_test.dart @@ -7,6 +7,7 @@ // 2024 August 7 // Author: Desmond Kirkpatrick +// These white-box tests drive protected multiplier signals and inspect state. // ignore_for_file: invalid_use_of_protected_member import 'dart:async'; @@ -254,7 +255,7 @@ void main() { await Simulator.reset(); }); - test('Native multiplier sweep with signage test', () async { + test('Native multiplier sweep with signage test', () { const width = 5; final a = Logic(width: width); final b = Logic(width: width); @@ -519,7 +520,7 @@ void main() { await Simulator.endSimulation(); }); - test('single multiplier', () async { + test('single multiplier', () { const width = 8; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); @@ -562,7 +563,7 @@ void main() { } }); - test('trivial instantiated multiplier', () async { + test('trivial instantiated multiplier', () { // Using this to find trace errors when instantiating multipliers const dataWidth = 5; final av = BigInt.from(-16).toSigned(dataWidth); @@ -571,9 +572,7 @@ void main() { final multA = Logic(name: 'multA', width: dataWidth); final multB = Logic(name: 'multB', width: dataWidth); - final signedOperands = Logic(name: 'signedOperands'); - // ignore: cascade_invocations - signedOperands.put(1); + final signedOperands = Logic(name: 'signedOperands')..put(1); multA.put(av); multB.put(bv); @@ -591,7 +590,7 @@ void main() { checkMultiplyAccumulate(mod, av, bv, BigInt.zero); }); - test('single mac', () async { + test('single mac', () { const width = 8; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); @@ -604,9 +603,7 @@ void main() { final bB = SignedBigInt.fromSignedInt(bv, width, signed: signed); final bC = SignedBigInt.fromSignedInt(cv, width * 2, signed: signed); - final signedOperands = Logic(name: 'signedOperands'); - // ignore: cascade_invocations - signedOperands.put(1); + final signedOperands = Logic(name: 'signedOperands')..put(1); // Set these so that printing inside module build will have Logic values a.put(bA); @@ -625,7 +622,7 @@ void main() { } }); - test('single rectangular mac', () async { + test('single rectangular mac', () { const widthA = 8; const widthB = 8; const widthC = widthA + widthB; @@ -654,7 +651,7 @@ void main() { } }); - test('trivial compression tree multiply-accumulate test', () async { + test('trivial compression tree multiply-accumulate test', () { const widthA = 6; const widthB = 6; const radix = 8; @@ -672,7 +669,7 @@ void main() { expect(accumulate.value.toBigInt(), equals(BigInt.from(15 * 3 + 5))); }); - test('trivial compression MAC signed', () async { + test('trivial compression MAC signed', () { const widthA = 6; const widthB = 6; const widthC = widthA + widthB; @@ -703,7 +700,7 @@ void main() { } }); - test('Multiplier Components exhaustive', () async { + test('Multiplier Components exhaustive', () { const width = 4; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); @@ -736,13 +733,11 @@ void main() { } }); - test('setting PPG', () async { + test('setting PPG', () { const width = 8; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); - final select = Logic(name: 'select'); - // ignore: cascade_invocations - select.put(0); + final select = Logic(name: 'select')..put(0); a.put(6); b.put(3); diff --git a/test/arithmetic/ones_complement_adder_test.dart b/test/arithmetic/ones_complement_adder_test.dart index b3b7ea3db4..41b69751e0 100644 --- a/test/arithmetic/ones_complement_adder_test.dart +++ b/test/arithmetic/ones_complement_adder_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // ones_complement_adder_test.dart @@ -28,7 +28,7 @@ void main() { await Simulator.reset(); }); - test('OnesComplementAdder case test', () async { + test('OnesComplementAdder case test', () { const width = 8; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); @@ -183,7 +183,7 @@ void main() { } }); - test('OnesComplementAdder: trivial test', () async { + test('OnesComplementAdder: trivial test', () { const width = 8; final aSign = Logic(name: 'aSign'); final a = Logic(name: 'a', width: width); diff --git a/test/arithmetic/parallel_prefix_operations_test.dart b/test/arithmetic/parallel_prefix_operations_test.dart index 3b29295c01..9f1933b929 100644 --- a/test/arithmetic/parallel_prefix_operations_test.dart +++ b/test/arithmetic/parallel_prefix_operations_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // parallel_prefix_operations.dart @@ -109,7 +109,7 @@ void main() { }); group('largest_pow2_less_than', () { - test('largest_pow2_less_than', () async { + test('largest_pow2_less_than', () { expect(largestPow2LessThan(5), equals(4)); expect(largestPow2LessThan(4), equals(2)); expect(largestPow2LessThan(3), equals(2)); diff --git a/test/arithmetic/sign_magnitude_adder_test.dart b/test/arithmetic/sign_magnitude_adder_test.dart index e32d16d9c3..77392b4328 100644 --- a/test/arithmetic/sign_magnitude_adder_test.dart +++ b/test/arithmetic/sign_magnitude_adder_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // sign_magnitude_adder_test.dart @@ -28,8 +28,10 @@ void checkSignMagnitudeAdder(SignMagnitudeAdder adder, LogicValue aSign, LogicValue aMagnitude, LogicValue bSign, LogicValue bMagnitude) { adder.aSign.put(aSign); adder.bSign.put(bSign); + // This white-box helper drives the adder's protected magnitude directly. // ignore: invalid_use_of_protected_member adder.a.put(aMagnitude); + // This white-box helper drives the adder's protected magnitude directly. // ignore: invalid_use_of_protected_member adder.b.put(bMagnitude); @@ -144,7 +146,7 @@ void main() { await Simulator.reset(); }); - test('SignMagnitudeAdder case test', () async { + test('SignMagnitudeAdder case test', () { const width = 8; final aSign = Logic(name: 'aSign'); final a = Logic(name: 'a', width: width); @@ -245,7 +247,7 @@ void main() { } }); - test('SignMagnitudeAdder: trivial test', () async { + test('SignMagnitudeAdder: trivial test', () { const width = 4; final aSign = Logic(name: 'aSign'); final a = Logic(name: 'a', width: width); @@ -264,7 +266,7 @@ void main() { expect(sum.value.toBigInt(), equals(BigInt.from(16))); }); - test('SignMagnitudeDualAdder: trivial test', () async { + test('SignMagnitudeDualAdder: trivial test', () { const width = 64; final aSign = Logic(name: 'aSign'); final a = Logic(name: 'a', width: width); @@ -281,7 +283,7 @@ void main() { expect(sum.value.toBigInt(), equals(BigInt.from(1))); }); - test('SignMagnitudeAdder: four case test', () async { + test('SignMagnitudeAdder: four case test', () { const width = 6; final aSign = Logic(name: 'aSign'); final a = Logic(name: 'a', width: width); diff --git a/test/arithmetic/values/floating_point_value_test.dart b/test/arithmetic/values/floating_point_value_test.dart index 89579236eb..942b4a6bcf 100644 --- a/test/arithmetic/values/floating_point_value_test.dart +++ b/test/arithmetic/values/floating_point_value_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // floating_point_value_test.dart @@ -435,7 +435,7 @@ void main() { equals(0)); }); - test('FPV: infinity/NaN conversion tests', () async { + test('FPV: infinity/NaN conversion tests', () { const exponentWidth = 4; const mantissaWidth = 4; final infinity = FloatingPointValue.populator( @@ -465,7 +465,7 @@ void main() { equals(true)); }); - test('FPV: infinity/NaN unrounded conversion tests', () async { + test('FPV: infinity/NaN unrounded conversion tests', () { const exponentWidth = 4; const mantissaWidth = 4; final infinity = FloatingPointValue.populator( @@ -550,7 +550,7 @@ void main() { } } }); - test('FPV: rounding check', () async { + test('FPV: rounding check', () { final fpv1 = FloatingPoint32Value.populator().ofDouble(1); final fpv2 = FloatingPoint32Value.populator().ofDouble(0.5); final fpv3 = FloatingPoint32Value.populator().ofDoubleUnrounded( @@ -780,7 +780,7 @@ void main() { }); }); - test('FloatingPointValue negation', () async { + test('FloatingPointValue negation', () { const exponentWidth = 4; const mantissaWidth = 4; final fp1 = FloatingPoint( @@ -797,7 +797,7 @@ void main() { }); /// Compare two FloatingPointValues differing in jbit - test('FloatingPointValue jbit comparison', () async { + test('FloatingPointValue jbit comparison', () { const exponentWidth = 4; const mantissaWidth = 4; @@ -819,7 +819,7 @@ void main() { expect(val2 > val1, isFalse); }); - test('FloatingPointValue comparison operators', () async { + test('FloatingPointValue comparison operators', () { const exponentWidth = 4; const mantissaWidth = 4; @@ -859,7 +859,7 @@ void main() { } }); - test('FloatingPointValue corner case comparisons', () async { + test('FloatingPointValue corner case comparisons', () { const exponentWidth = 4; const mantissaWidth = 4; final nan = FloatingPointValue.populator( diff --git a/test/bitonic_sort_test.dart b/test/bitonic_sort_test.dart index d5ebfc2498..66d7b6b18f 100644 --- a/test/bitonic_sort_test.dart +++ b/test/bitonic_sort_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // bitonic_sort_test.dart @@ -35,8 +35,7 @@ Future main() async { } group('Bitonic Sort', () { - test('should return RohdHclException if inputs is not power of two.', - () async { + test('should return RohdHclException if inputs is not power of two.', () { const dataWidth = 8; final clk = SimpleClockGenerator(10).clk; final reset = Logic(name: 'reset'); @@ -44,19 +43,18 @@ Future main() async { final toSort = List.generate(3, (index) => Const(index + 1, width: dataWidth)); - expect(() async { + expect(() { BitonicSort(clk, reset, toSort: toSort, name: 'top_level'); }, throwsA((dynamic e) => e is RohdHclException)); }); - test('should return RohdHclException if number of elements is 0.', - () async { + test('should return RohdHclException if number of elements is 0.', () { final clk = SimpleClockGenerator(10).clk; final reset = Logic(name: 'reset'); final toSort = []; - expect(() async { + expect(() { BitonicSort(clk, reset, toSort: toSort, name: 'top_level'); }, throwsA((dynamic e) => e is RohdHclException)); }); @@ -83,7 +81,7 @@ Future main() async { test( 'should return RohdHclException if width ' - 'is difference between each other in the list.', () async { + 'is difference between each other in the list.', () { final clk = SimpleClockGenerator(10).clk; final reset = Logic(name: 'reset'); @@ -94,7 +92,7 @@ Future main() async { Const(2, width: 5), ]; - expect(() async { + expect(() { BitonicSort(clk, reset, toSort: toSort, name: 'top_level'); }, throwsA((dynamic e) => e is RohdHclException)); }); diff --git a/test/configurator_test.dart b/test/configurator_test.dart index bf5126951a..bb7518bb26 100644 --- a/test/configurator_test.dart +++ b/test/configurator_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // configurator_test.dart @@ -31,11 +31,15 @@ void main() { cfg.knobs['e']!.value = 5; for (final k in (cfg.knobs['e']! as ListOfKnobsKnob).knobs) { + // Knob values are dynamic because configurators support mixed + // value types. // ignore: avoid_dynamic_calls k.value += 10; } (cfg.knobs['f']! as GroupOfKnobs).subKnobs.forEach((key, value) { + // Knob values are dynamic because configurators support mixed + // value types. // ignore: avoid_dynamic_calls value.value += 'x'; }); @@ -68,6 +72,75 @@ void main() { expect(rotate.knobs.values.toList()[2], isA()); }); + test('type-valued choice knobs provide stable display labels', () { + final oneHot = OneHotConfigurator(); + + expect(oneHot.directionKnob.labelFor(BinaryToOneHot), 'BinaryToOneHot'); + expect(oneHot.directionKnob.labelFor(OneHotToBinary), 'OneHotToBinary'); + }); + + test('type-valued choice knobs require stable labels', () { + expect( + () => ChoiceConfigKnob( + [BinaryToOneHot, OneHotToBinary], + value: OneHotToBinary, + ), + throwsA(isA()), + ); + }); + + test('choice knobs reject duplicate serialized labels', () { + expect( + () => ChoiceConfigKnob( + [BinaryToOneHot, OneHotToBinary], + value: OneHotToBinary, + choiceLabels: const { + BinaryToOneHot: 'converter', + OneHotToBinary: 'converter', + }, + ), + throwsA(isA()), + ); + }); + + test('choice labels cannot collide with default serialized values', () { + expect( + () => ChoiceConfigKnob( + const [1, 'one'], + value: 1, + choiceLabels: const {1: 'one'}, + ), + throwsA(isA()), + ); + }); + + test('choice labels are stored as an unmodifiable copy', () { + final labels = {1: 'one', 2: 'two'}; + final knob = ChoiceConfigKnob( + const [1, 2], + value: 1, + choiceLabels: labels, + ); + + labels[1] = 'two'; + + expect(knob.toJson(), {'value': 'one'}); + expect( + () => knob.choiceLabels[1] = 'two', + throwsUnsupportedError, + ); + }); + + test('type-valued choice knobs use stable JSON values', () { + final oneHot = OneHotConfigurator()..directionKnob.value = BinaryToOneHot; + + expect(oneHot.directionKnob.toJson(), {'value': 'BinaryToOneHot'}); + + final loaded = OneHotConfigurator() + ..directionKnob.loadJson({'value': 'BinaryToOneHot'}); + expect(loaded.directionKnob.value, BinaryToOneHot); + }); + test('should return RotateRight module when generate() with default value', () async { final rotate = RotateConfigurator(); @@ -386,7 +459,7 @@ void main() { expect(sv, contains('swizzle')); }); - test('sum configurator', () async { + test('sum configurator', () { final cfg = SumConfigurator(); cfg.initialValueKnob.value = 6; cfg.widthKnob.value = 10; @@ -396,11 +469,14 @@ void main() { final mod = cfg.createModule() as Sum; + // Verify the protected signal generated from the public configuration knob. // ignore: invalid_use_of_protected_member expect(mod.initialValueLogic.value.toInt(), 6); expect(mod.width, 10); + // Verify the protected signal generated from the public configuration knob. // ignore: invalid_use_of_protected_member expect(mod.minValueLogic.value.toInt(), 5); + // Verify the protected signal generated from the public configuration knob. // ignore: invalid_use_of_protected_member expect(mod.maxValueLogic.value.toInt(), 25); expect(mod.saturates, true); diff --git a/test/ecc_test.dart b/test/ecc_test.dart index 88ba684fa7..2565fc3ee3 100644 --- a/test/ecc_test.dart +++ b/test/ecc_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // ecc_test.dart @@ -96,7 +96,7 @@ void main() { }); } - test('Hamming(7,4)', () async { + test('Hamming(7,4)', () { final tx = HammingEccTransmitter(Logic(width: 4)); final rx = HammingEccReceiver(tx.transmission); expect(tx.transmission.width, 7); diff --git a/test/extrema_test.dart b/test/extrema_test.dart index 32bf19c2a9..fbd9ad1c2f 100644 --- a/test/extrema_test.dart +++ b/test/extrema_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // extrema_test.dart @@ -146,7 +146,7 @@ void main() { expect(findMax.index.value.toInt(), equals(0)); }); - test('Empty List.', () async { + test('Empty List.', () { // Create a list of Logic objects with final logics = List.empty(); diff --git a/test/find_pattern_test.dart b/test/find_pattern_test.dart index 1756d59ee6..ae24ce6d90 100644 --- a/test/find_pattern_test.dart +++ b/test/find_pattern_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // find_pattern_test.dart @@ -15,25 +15,25 @@ import 'package:test/test.dart'; void main() { group('From start, find first pattern with n as null', () { - test('at 0th position', () async { + test('at 0th position', () { final bus = Const(bin('10000001'), width: 8); final pattern = Const(bin('01'), width: 2); final findPattern = FindPattern(bus, pattern); expect(findPattern.index.value.toInt(), equals(0)); }); - test('at random position', () async { + test('at random position', () { final bus = Const(bin('11011111'), width: 8); final pattern = Const(bin('101'), width: 3); final findPattern = FindPattern(bus, pattern); expect(findPattern.index.value.toInt(), equals(4)); }); - test('at last position', () async { + test('at last position', () { final bus = Const(bin('11101010'), width: 8); final pattern = Const(bin('11101'), width: 5); final findPattern = FindPattern(bus, pattern); expect(findPattern.index.value.toInt(), equals(3)); }); - test('when pattern not present', () async { + test('when pattern not present', () { final bus = Const(bin('00000000'), width: 8); final pattern = Const(bin('111'), width: 3); final findPattern = FindPattern(bus, pattern, generateError: true); @@ -42,25 +42,25 @@ void main() { }); group('From end, find first pattern with n as null', () { - test('at 0th position', () async { + test('at 0th position', () { final bus = Const(bin('11101010'), width: 8); final pattern = Const(bin('11101'), width: 5); final findPattern = FindPattern(bus, pattern, fromStart: false); expect(findPattern.index.value.toInt(), equals(0)); }); - test('at random position', () async { + test('at random position', () { final bus = Const(bin('11011111'), width: 8); final pattern = Const(bin('101'), width: 3); final findPattern = FindPattern(bus, pattern, fromStart: false); expect(findPattern.index.value.toInt(), equals(1)); }); - test('at last position', () async { + test('at last position', () { final bus = Const(bin('10000001'), width: 8); final pattern = Const(bin('01'), width: 2); final findPattern = FindPattern(bus, pattern, fromStart: false); expect(findPattern.index.value.toInt(), equals(6)); }); - test('Pattern not present', () async { + test('Pattern not present', () { final bus = Const(bin('00000000'), width: 8); final pattern = Const(bin('111'), width: 3); final findPattern = @@ -70,21 +70,21 @@ void main() { }); group('From start, find nth pattern', () { - test('where n is 0', () async { + test('where n is 0', () { final bus = Const(bin('10101001'), width: 8); final pattern = Const(bin('01'), width: 2); final n = Const(0, width: log2Ceil(8) + 1); final findPattern = FindPattern(bus, pattern, n: n); expect(findPattern.index.value.toInt(), equals(0)); }); - test('where n is 2 (find the 3rd occurrence, n is zero-index)', () async { + test('where n is 2 (find the 3rd occurrence, n is zero-index)', () { final bus = Const(bin('10101001'), width: 8); final pattern = Const(bin('01'), width: 2); final n = Const(2, width: log2Ceil(8) + 1); final findPattern = FindPattern(bus, pattern, n: n); expect(findPattern.index.value.toInt(), equals(5)); }); - test('where n is outside bound', () async { + test('where n is outside bound', () { final bus = Const(bin('10101001'), width: 8); final pattern = Const(bin('01'), width: 2); final n = Const(5, width: log2Ceil(8) + 1); @@ -94,21 +94,21 @@ void main() { }); group('From end, find nth pattern', () { - test('where n is 0', () async { + test('where n is 0', () { final bus = Const(bin('01010110'), width: 8); final pattern = Const(bin('01'), width: 2); final n = Const(0, width: log2Ceil(8) + 1); final findPattern = FindPattern(bus, pattern, fromStart: false, n: n); expect(findPattern.index.value.toInt(), equals(0)); }); - test('where n is 2 (find the 3rd occurrence, n is zero-index)', () async { + test('where n is 2 (find the 3rd occurrence, n is zero-index)', () { final bus = Const(bin('10101001'), width: 8); final pattern = Const(bin('01'), width: 2); final n = Const(2, width: log2Ceil(8) + 1); final findPattern = FindPattern(bus, pattern, fromStart: false, n: n); expect(findPattern.index.value.toInt(), equals(6)); }); - test('where n is outside bound', () async { + test('where n is outside bound', () { final bus = Const(bin('10101001'), width: 8); final pattern = Const(bin('01'), width: 2); final n = Const(5, width: log2Ceil(8) + 1); @@ -119,7 +119,7 @@ void main() { }); group('Dynamic input', () { - test('where n is null, from start', () async { + test('where n is null, from start', () { final bus = Logic(width: 8); final pattern = Logic(width: 2); final findPattern = FindPattern(bus, pattern, generateError: true); @@ -139,7 +139,7 @@ void main() { expect(findPattern.error!.value.toInt(), equals(1)); }); - test('where n is defined, from start', () async { + test('where n is defined, from start', () { final bus = Logic(width: 8); final pattern = Logic(width: 2); final n = Logic(width: log2Ceil(8) + 1); @@ -163,7 +163,7 @@ void main() { expect(findPattern.error!.value.toInt(), equals(1)); }); - test('where n is null, from end', () async { + test('where n is null, from end', () { final bus = Logic(width: 8); final pattern = Logic(width: 2); final findPattern = @@ -184,7 +184,7 @@ void main() { expect(findPattern.error!.value.toInt(), equals(1)); }); - test('where n is defined, from end', () async { + test('where n is defined, from end', () { final bus = Logic(width: 8); final pattern = Logic(width: 2); final n = Logic(width: log2Ceil(8) + 1); diff --git a/test/memory/cam_test.dart b/test/memory/cam_test.dart index ef600720e8..ef3e568a77 100644 --- a/test/memory/cam_test.dart +++ b/test/memory/cam_test.dart @@ -1,713 +1,713 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// cam_test.dart -// CAM (Contents-Addressable Memory) tests. -// -// 2025 September 18 -// Author: Desmond Kirkpatrick - -import 'dart:async'; - -import 'package:rohd/rohd.dart'; -import 'package:rohd_hcl/rohd_hcl.dart'; -import 'package:test/test.dart'; - -void main() { - tearDown(() async { - await Simulator.reset(); - }); - - test('Cam smoke test', () async { - final clk = SimpleClockGenerator(10).clk; - - final reset = Logic(); - - final wrPort = TagInterface(5, 8); - final wrPort2 = TagInterface(5, 8); - final rdPort = TagInterface(5, 8); - final rdPort2 = TagInterface(5, 8); - - final cam = - Cam(clk, reset, [wrPort, wrPort2], [rdPort, rdPort2], numEntries: 32); - - await cam.build(); - unawaited(Simulator.run()); - - await clk.nextPosedge; - await clk.nextPosedge; - wrPort.en.inject(0); - reset.inject(1); - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - wrPort.en.inject(1); - wrPort2.en.inject(1); - wrPort.hit.inject(1); - wrPort2.hit.inject(1); - wrPort.idx.inject(14); - wrPort.tag.inject(42); - wrPort2.idx.inject(29); - wrPort2.tag.inject(7); - await clk.nextPosedge; - await clk.nextPosedge; - wrPort.en.inject(0); - wrPort2.en.inject(0); - await clk.nextPosedge; - rdPort.tag.inject(42); - rdPort2.tag.inject(7); - await clk.nextPosedge; - expect(rdPort.hit.value, LogicValue.one); - expect(rdPort.idx.value.toInt(), 14); - expect(rdPort2.hit.value, LogicValue.one); - expect(rdPort2.idx.value.toInt(), 29); - await clk.nextPosedge; - await clk.nextPosedge; - - await Simulator.endSimulation(); - }); - - test('Cam with valid tracking', () async { - const numEntries = 4; - const tagWidth = 8; - - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(); - - final wrPort = TagInterface(log2Ceil(numEntries), tagWidth); - final rdPort = TagInterface(log2Ceil(numEntries), tagWidth); - - final cam = Cam( - clk, - reset, - [wrPort], - [rdPort], - numEntries: numEntries, - enableValidTracking: true, - ); - - await cam.build(); - unawaited(Simulator.run()); - - // Reset - reset.inject(1); - wrPort.en.inject(0); - await clk.nextPosedge; - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - // Initially empty - expect(cam.empty!.value.toBool(), isTrue, - reason: 'Should be empty initially'); - expect(cam.full!.value.toBool(), isFalse, - reason: 'Should not be full initially'); - expect(cam.validCount!.value.toInt(), equals(0), - reason: 'Count should be 0'); - - // Write one entry - wrPort.en.inject(1); - wrPort.hit.inject(1); - wrPort.idx.inject(0); - wrPort.tag.inject(0x42); - await clk.nextPosedge; - - expect(cam.empty!.value.toBool(), isFalse, - reason: 'Should not be empty after write'); - expect(cam.full!.value.toBool(), isFalse, - reason: 'Should not be full with 1 entry'); - expect(cam.validCount!.value.toInt(), equals(1), - reason: 'Count should be 1'); - - // Write three more entries to fill the CAM - for (var i = 1; i < numEntries; i++) { - wrPort.idx.inject(i); - wrPort.tag.inject(0x50 + i); - await clk.nextPosedge; - } - - expect(cam.empty!.value.toBool(), isFalse, - reason: 'Should not be empty when full'); - expect(cam.full!.value.toBool(), isTrue, - reason: 'Should be full with all entries'); - expect(cam.validCount!.value.toInt(), equals(numEntries), - reason: 'Count should equal numEntries'); - - wrPort.en.inject(0); - await clk.nextPosedge; - - // Verify lookups work - rdPort.tag.inject(0x42); - await clk.nextPosedge; - expect(rdPort.hit.value.toBool(), isTrue, reason: 'Should find tag 0x42'); - expect(rdPort.idx.value.toInt(), equals(0), reason: 'Should be at index 0'); - - await Simulator.endSimulation(); - }); - - group('Cam read-with-invalidate', () { - test('basic write and lookup with invalidate', () async { - const tagWidth = 8; - const numEntries = 4; - const idWidth = 2; - - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(); - - final writePort = TagInterface(idWidth, tagWidth); - final invalidatePort = TagInterface(idWidth, tagWidth); - final lookupPort = TagInterface(idWidth, tagWidth); - - final cam = Cam( - clk, - reset, - [writePort, invalidatePort], - [lookupPort], - numEntries: numEntries, - ); - - // Wire invalidatePort to use lookupPort's combinational output - // This creates a "find and invalidate" pattern - invalidatePort.idx <= lookupPort.idx; - invalidatePort.tag <= lookupPort.tag; - - await cam.build(); - - unawaited(Simulator.run()); - - // Reset - reset.inject(1); - writePort.en.inject(0); - invalidatePort.en.inject(0); - invalidatePort.hit.inject(0); // Always 0 for invalidation - await clk.nextPosedge; - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - // Write tag 0x42 to entry 0 - writePort.en.inject(1); - writePort.hit.inject(1); - writePort.idx.inject(0); - writePort.tag.inject(0x42); - await clk.nextPosedge; - - // Write tag 0x99 to entry 1 - writePort.idx.inject(1); - writePort.tag.inject(0x99); - await clk.nextPosedge; - - writePort.en.inject(0); - await clk.nextPosedge; - - // Lookup tag 0x42 without invalidate - lookupPort.tag.inject(0x42); - invalidatePort.en.inject(0); - await clk.nextPosedge; - - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should hit for tag 0x42'); - expect(lookupPort.idx.value.toInt(), equals(0), - reason: 'Should return index 0'); - - // Lookup again - should still hit - await clk.nextPosedge; - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should still hit after lookup without invalidate'); - - // Lookup with invalidate: enable invalidatePort on hit - invalidatePort.en.inject(lookupPort.hit.value.toInt()); - - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should hit when invalidate is asserted (original behavior)'); - await clk.nextPosedge; - - // Try to lookup again - should miss because entry was invalidated - invalidatePort.en.inject(0); - await clk.nextPosedge; - - expect(lookupPort.hit.value.toBool(), isFalse, - reason: 'Should miss after entry was invalidated'); - - // Lookup tag 0x99 - should still be valid - lookupPort.tag.inject(0x99); - await clk.nextPosedge; - - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Entry 1 should still be valid'); - expect(lookupPort.idx.value.toInt(), equals(1), - reason: 'Should return index 1'); - - await Simulator.endSimulation(); - }); - - test('multiple lookups with different invalidate behavior', () async { - const tagWidth = 16; - const idWidth = 3; - - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(); - - final writePort = TagInterface(idWidth, tagWidth); - final invalidatePort1 = TagInterface(idWidth, tagWidth); - final invalidatePort2 = TagInterface(idWidth, tagWidth); - final lookupPort1 = TagInterface(idWidth, tagWidth); - final lookupPort2 = TagInterface(idWidth, tagWidth); - - final cam = Cam( - clk, - reset, - [writePort, invalidatePort1, invalidatePort2], - [lookupPort1, lookupPort2], - ); - - // Wire invalidate ports to lookup port outputs - invalidatePort1.idx <= lookupPort1.idx; - invalidatePort1.tag <= lookupPort1.tag; - invalidatePort2.idx <= lookupPort2.idx; - invalidatePort2.tag <= lookupPort2.tag; - - await cam.build(); - - unawaited(Simulator.run()); - - // Reset - reset.inject(1); - writePort.en.inject(0); - invalidatePort1.en.inject(0); - invalidatePort1.hit.inject(0); - invalidatePort2.en.inject(0); - invalidatePort2.hit.inject(0); - await clk.nextPosedge; - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - // Write some entries - writePort.en.inject(1); - writePort.hit.inject(1); - for (var i = 0; i < 4; i++) { - writePort.idx.inject(i); - writePort.tag.inject(0x1000 + i); - await clk.nextPosedge; - } - writePort.en.inject(0); - await clk.nextPosedge; - - // Both ports lookup different tags - lookupPort1.tag.inject(0x1000); - invalidatePort1.en.inject(0); - - lookupPort2.tag.inject(0x1002); - invalidatePort2.en.inject(0); - - await clk.nextPosedge; - - expect(lookupPort1.hit.value.toBool(), isTrue); - expect(lookupPort1.idx.value.toInt(), equals(0)); - expect(lookupPort2.hit.value.toBool(), isTrue); - expect(lookupPort2.idx.value.toInt(), equals(2)); - - // Port 1 invalidates entry 0, port 2 doesn't invalidate - invalidatePort1.en.inject(lookupPort1.hit.value.toInt()); - await clk.nextPosedge; - - // Port 1 should now miss, port 2 should still hit - invalidatePort1.en.inject(0); - await clk.nextPosedge; - - expect(lookupPort1.hit.value.toBool(), isFalse, - reason: 'Entry 0 was invalidated'); - expect(lookupPort2.hit.value.toBool(), isTrue, - reason: 'Entry 2 was not invalidated'); - - await Simulator.endSimulation(); - }); - - test('rewrite after invalidate restores entry', () async { - const tagWidth = 8; - const numEntries = 4; - const idWidth = 2; - - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(); - - final writePort = TagInterface(idWidth, tagWidth); - final invalidatePort = TagInterface(idWidth, tagWidth); - final lookupPort = TagInterface(idWidth, tagWidth); - - final cam = Cam( - clk, - reset, - [writePort, invalidatePort], - [lookupPort], - numEntries: numEntries, - ); - - // Wire invalidatePort to lookupPort output - invalidatePort.idx <= lookupPort.idx; - invalidatePort.tag <= lookupPort.tag; - - await cam.build(); - - unawaited(Simulator.run()); - - // Reset - reset.inject(1); - writePort.en.inject(0); - invalidatePort.en.inject(0); - invalidatePort.hit.inject(0); - await clk.nextPosedge; - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - // Write and invalidate - writePort.en.inject(1); - writePort.hit.inject(1); - writePort.idx.inject(0); - writePort.tag.inject(0xAA); - await clk.nextPosedge; - writePort.en.inject(0); - await clk.nextPosedge; - - // Lookup and invalidate - lookupPort.tag.inject(0xAA); - invalidatePort.en.inject(1); - - // Combinational output check - await clk.nextNegedge; - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should hit when invalidate is asserted (original behavior)'); - await clk.nextPosedge; - - // Should now miss - invalidatePort.en.inject(0); - await clk.nextPosedge; - expect(lookupPort.hit.value.toBool(), isFalse); - - // Rewrite the same entry - writePort.en.inject(1); - writePort.hit.inject(1); - writePort.idx.inject(0); - writePort.tag.inject(0xAA); - await clk.nextPosedge; - writePort.en.inject(0); - await clk.nextPosedge; - - // Should hit again - lookupPort.tag.inject(0xAA); - - // Combinational output check - await clk.nextNegedge; - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Entry should be valid after rewrite'); - - await Simulator.endSimulation(); - }); - - test('invalidate only clears on hit', () async { - const tagWidth = 8; - const numEntries = 4; - const idWidth = 2; - - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(); - - final writePort = TagInterface(idWidth, tagWidth); - final invalidatePort = TagInterface(idWidth, tagWidth); - final lookupPort = TagInterface(idWidth, tagWidth); - - final cam = Cam( - clk, - reset, - [writePort, invalidatePort], - [lookupPort], - numEntries: numEntries, - ); - - // Wire invalidatePort to lookupPort output - invalidatePort.idx <= lookupPort.idx; - invalidatePort.tag <= lookupPort.tag; - - await cam.build(); - - unawaited(Simulator.run()); - - // Reset - reset.inject(1); - writePort.en.inject(0); - invalidatePort.en.inject(0); - invalidatePort.hit.inject(0); - await clk.nextPosedge; - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - // Write entry - writePort.en.inject(1); - writePort.hit.inject(1); - writePort.idx.inject(0); - writePort.tag.inject(0x55); - await clk.nextPosedge; - writePort.en.inject(0); - await clk.nextPosedge; - - // Lookup wrong tag with invalidate - should not affect entry - // Because invalidatePort.idx comes from lookupPort.idx which will be - // 0 (default), but the entry at 0 has tag 0x55, not 0x66 - lookupPort.tag.inject(0x66); // Wrong tag - // When hit is 0, invalidatePort.en being 1 would write to idx 0 - // but since we're wiring en to lookupPort.hit, we only invalidate on hit - await clk.nextNegedge; - - expect(lookupPort.hit.value.toBool(), isFalse, - reason: 'Should miss for wrong tag'); - - // Lookup correct tag - should still hit - lookupPort.tag.inject(0x55); - invalidatePort.en.inject(0); - await clk.nextPosedge; - - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Entry should still be valid (invalidate only on hit)'); - - await Simulator.endSimulation(); - }); - - test('Cam with valid tracking and invalidate', () async { - const numEntries = 4; - const tagWidth = 8; - const idWidth = 2; - - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(); - - final writePort = TagInterface(idWidth, tagWidth); - final invalidatePort = TagInterface(idWidth, tagWidth); - final lookupPort = TagInterface(idWidth, tagWidth); - - final cam = Cam( - clk, - reset, - [writePort, invalidatePort], - [lookupPort], - numEntries: numEntries, - enableValidTracking: true, - ); - - // Wire invalidatePort to lookupPort output - invalidatePort.idx <= lookupPort.idx; - invalidatePort.tag <= lookupPort.tag; - - await cam.build(); - unawaited(Simulator.run()); - - // Reset - reset.inject(1); - writePort.en.inject(0); - invalidatePort.en.inject(0); - invalidatePort.hit.inject(0); - await clk.nextPosedge; - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - // Initially empty - expect(cam.empty!.value.toBool(), isTrue, - reason: 'Should be empty initially'); - expect(cam.full!.value.toBool(), isFalse, - reason: 'Should not be full initially'); - expect(cam.validCount!.value.toInt(), equals(0), - reason: 'Count should be 0'); - - // Write entries - writePort.en.inject(1); - writePort.hit.inject(1); - for (var i = 0; i < numEntries; i++) { - writePort.idx.inject(i); - writePort.tag.inject(0x10 + i); - await clk.nextPosedge; - } - writePort.en.inject(0); - await clk.nextPosedge; - - // Should be full - expect(cam.empty!.value.toBool(), isFalse, reason: 'Should not be empty'); - expect(cam.full!.value.toBool(), isTrue, reason: 'Should be full'); - expect(cam.validCount!.value.toInt(), equals(numEntries), - reason: 'Count should equal numEntries'); - - // Invalidate one entry - lookupPort.tag.inject(0x10); - invalidatePort.en.inject(1); - - // Check combinational output. - await clk.nextNegedge; - // Wait for invalidate to take effect - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should hit when invalidate is asserted (original behavior)'); - await clk.nextPosedge; - - expect(cam.full!.value.toBool(), isFalse, - reason: 'Should not be full after invalidate'); - expect(cam.validCount!.value.toInt(), equals(numEntries - 1), - reason: 'Count should decrease by 1'); - - // Invalidate remaining entries - for (var i = 1; i < numEntries; i++) { - lookupPort.tag.inject(0x10 + i); - await clk.nextPosedge; - await clk.nextPosedge; // Wait for invalidate - } - - // Should be empty now - expect(cam.empty!.value.toBool(), isTrue, - reason: 'Should be empty after invalidating all'); - expect(cam.validCount!.value.toInt(), equals(0), - reason: 'Count should be 0'); - - await Simulator.endSimulation(); - }); - - test('Cam simultaneous write and invalidate', () async { - const numEntries = 8; - const tagWidth = 8; - const idWidth = 3; - - final clk = SimpleClockGenerator(10).clk; - final reset = Logic(); - - final writePort = TagInterface(idWidth, tagWidth); - final invalidatePort = TagInterface(idWidth, tagWidth); - final lookupPort = TagInterface(idWidth, tagWidth); - - final cam = Cam( - clk, - reset, - [writePort, invalidatePort], - [lookupPort], - enableValidTracking: true, - ); - - // Wire invalidatePort to lookupPort output - invalidatePort.idx <= lookupPort.idx; - invalidatePort.tag <= lookupPort.tag; - - await cam.build(); - - unawaited(Simulator.run()); - - // Reset - reset.inject(1); - writePort.en.inject(0); - invalidatePort.en.inject(0); - invalidatePort.hit.inject(0); - await clk.nextPosedge; - await clk.nextPosedge; - reset.inject(0); - await clk.nextPosedge; - - // Fill CAM completely - writePort.en.inject(1); - writePort.hit.inject(1); - for (var i = 0; i < numEntries; i++) { - writePort.idx.inject(i); - writePort.tag.inject(0xA0 + i); - await clk.nextPosedge; - } - - // Verify full - expect(cam.full!.value.toBool(), isTrue, reason: 'CAM should be full'); - expect(cam.validCount!.value.toInt(), equals(numEntries), - reason: 'Count should equal numEntries'); - - // Disable write temporarily - writePort.en.inject(0); - await clk.nextPosedge; - - // Verify we can lookup the entries before starting simultaneous ops, - invalidatePort.en.inject(0); // Don't invalidate yet - lookupPort.tag.inject(0xA0); - await clk.nextPosedge; - - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should find 0xA0 before test'); - lookupPort.tag.inject(0xA1); - await clk.nextPosedge; - - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should find 0xA1 before test'); - - // Now perform simultaneous write and invalidate operations This - // demonstrates that the CAM can handle looking up/invalidating entries - // while simultaneously writing new entries. - // - // Test: Do 4 simultaneous operations (half the CAM size) to demonstrate - // that writes and invalidate-on-read can happen concurrently. - - writePort.en.inject(1); - writePort.hit.inject(1); - invalidatePort.en.inject(1); - - const numSimultaneousOps = numEntries ~/ 2; - for (var i = 0; i < numSimultaneousOps; i++) { - // Write to the upper half while invalidating the lower half. - final writeIdx = i + numSimultaneousOps; // Indices 4-7 - final lookupIdx = i; // Indices 0-3 - final lookupTag = 0xA0 + lookupIdx; - - lookupPort.tag.inject(lookupTag); - writePort.idx.inject(writeIdx); - writePort.tag.inject(0xC0 + writeIdx); // New tag - - await clk.nextNegedge; - - // Should hit when invalidate is asserted (original behavior restored). - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Cycle $i: Should hit when invalidate=1 for ' - 'tag 0x${lookupTag.toRadixString(16)}'); - await clk.nextPosedge; - } - - // Wait for final write and invalidates to settle. - // Need extra cycles because: - // - Writes complete on the clock edge. - // - Invalidates happen one cycle after lookup (registered). - await clk.nextPosedge; - await clk.nextPosedge; - await clk.nextPosedge; - - // After invalidating lower half (indices 0-3) and writing new entries to - // upper half (indices 4-7), we should have exactly the upper half valid. - expect(cam.full!.value.toBool(), isFalse, - reason: 'CAM should not be full - only upper half is valid'); - expect(cam.validCount!.value.toInt(), equals(numSimultaneousOps), - reason: - 'Count should equal number of new writes ($numSimultaneousOps)'); - - // Verify the new entries in upper half exist and can be looked up - invalidatePort.en.inject(0); // Don't invalidate for verification - for (var i = numSimultaneousOps; i < numEntries; i++) { - lookupPort.tag.inject(0xC0 + i); - await clk.nextPosedge; - expect(lookupPort.hit.value.toBool(), isTrue, - reason: 'Should find new tag 0x${(0xC0 + i).toRadixString(16)}'); - expect(lookupPort.idx.value.toInt(), equals(i), - reason: 'Should be at index $i'); - } - - // Verify lower half entries are invalidated (miss on lookup) - for (var i = 0; i < numSimultaneousOps; i++) { - lookupPort.tag.inject(0xA0 + i); - await clk.nextPosedge; - expect(lookupPort.hit.value.toBool(), isFalse, - reason: 'Should NOT find invalidated ' - 'tag 0x${(0xA0 + i).toRadixString(16)}'); - } - - await Simulator.endSimulation(); - }); - }); -} +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// cam_test.dart +// CAM (Contents-Addressable Memory) tests. +// +// 2025 September 18 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('Cam smoke test', () async { + final clk = SimpleClockGenerator(10).clk; + + final reset = Logic(); + + final wrPort = TagInterface(5, 8); + final wrPort2 = TagInterface(5, 8); + final rdPort = TagInterface(5, 8); + final rdPort2 = TagInterface(5, 8); + + final cam = + Cam(clk, reset, [wrPort, wrPort2], [rdPort, rdPort2], numEntries: 32); + + await cam.build(); + unawaited(Simulator.run()); + + await clk.nextPosedge; + await clk.nextPosedge; + wrPort.en.inject(0); + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + wrPort.en.inject(1); + wrPort2.en.inject(1); + wrPort.hit.inject(1); + wrPort2.hit.inject(1); + wrPort.idx.inject(14); + wrPort.tag.inject(42); + wrPort2.idx.inject(29); + wrPort2.tag.inject(7); + await clk.nextPosedge; + await clk.nextPosedge; + wrPort.en.inject(0); + wrPort2.en.inject(0); + await clk.nextPosedge; + rdPort.tag.inject(42); + rdPort2.tag.inject(7); + await clk.nextPosedge; + expect(rdPort.hit.value, LogicValue.one); + expect(rdPort.idx.value.toInt(), 14); + expect(rdPort2.hit.value, LogicValue.one); + expect(rdPort2.idx.value.toInt(), 29); + await clk.nextPosedge; + await clk.nextPosedge; + + await Simulator.endSimulation(); + }); + + test('Cam with valid tracking', () async { + const numEntries = 4; + const tagWidth = 8; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + + final wrPort = TagInterface(log2Ceil(numEntries), tagWidth); + final rdPort = TagInterface(log2Ceil(numEntries), tagWidth); + + final cam = Cam( + clk, + reset, + [wrPort], + [rdPort], + numEntries: numEntries, + enableValidTracking: true, + ); + + await cam.build(); + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + wrPort.en.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Initially empty + expect(cam.empty!.value.toBool(), isTrue, + reason: 'Should be empty initially'); + expect(cam.full!.value.toBool(), isFalse, + reason: 'Should not be full initially'); + expect(cam.validCount!.value.toInt(), equals(0), + reason: 'Count should be 0'); + + // Write one entry + wrPort.en.inject(1); + wrPort.hit.inject(1); + wrPort.idx.inject(0); + wrPort.tag.inject(0x42); + await clk.nextPosedge; + + expect(cam.empty!.value.toBool(), isFalse, + reason: 'Should not be empty after write'); + expect(cam.full!.value.toBool(), isFalse, + reason: 'Should not be full with 1 entry'); + expect(cam.validCount!.value.toInt(), equals(1), + reason: 'Count should be 1'); + + // Write three more entries to fill the CAM + for (var i = 1; i < numEntries; i++) { + wrPort.idx.inject(i); + wrPort.tag.inject(0x50 + i); + await clk.nextPosedge; + } + + expect(cam.empty!.value.toBool(), isFalse, + reason: 'Should not be empty when full'); + expect(cam.full!.value.toBool(), isTrue, + reason: 'Should be full with all entries'); + expect(cam.validCount!.value.toInt(), equals(numEntries), + reason: 'Count should equal numEntries'); + + wrPort.en.inject(0); + await clk.nextPosedge; + + // Verify lookups work + rdPort.tag.inject(0x42); + await clk.nextPosedge; + expect(rdPort.hit.value.toBool(), isTrue, reason: 'Should find tag 0x42'); + expect(rdPort.idx.value.toInt(), equals(0), reason: 'Should be at index 0'); + + await Simulator.endSimulation(); + }); + + group('Cam read-with-invalidate', () { + test('basic write and lookup with invalidate', () async { + const tagWidth = 8; + const numEntries = 4; + const idWidth = 2; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + + final writePort = TagInterface(idWidth, tagWidth); + final invalidatePort = TagInterface(idWidth, tagWidth); + final lookupPort = TagInterface(idWidth, tagWidth); + + final cam = Cam( + clk, + reset, + [writePort, invalidatePort], + [lookupPort], + numEntries: numEntries, + ); + + // Wire invalidatePort to use lookupPort's combinational output + // This creates a "find and invalidate" pattern + invalidatePort.idx <= lookupPort.idx; + invalidatePort.tag <= lookupPort.tag; + + await cam.build(); + + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + writePort.en.inject(0); + invalidatePort.en.inject(0); + invalidatePort.hit.inject(0); // Always 0 for invalidation + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Write tag 0x42 to entry 0 + writePort.en.inject(1); + writePort.hit.inject(1); + writePort.idx.inject(0); + writePort.tag.inject(0x42); + await clk.nextPosedge; + + // Write tag 0x99 to entry 1 + writePort.idx.inject(1); + writePort.tag.inject(0x99); + await clk.nextPosedge; + + writePort.en.inject(0); + await clk.nextPosedge; + + // Lookup tag 0x42 without invalidate + lookupPort.tag.inject(0x42); + invalidatePort.en.inject(0); + await clk.nextPosedge; + + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should hit for tag 0x42'); + expect(lookupPort.idx.value.toInt(), equals(0), + reason: 'Should return index 0'); + + // Lookup again - should still hit + await clk.nextPosedge; + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should still hit after lookup without invalidate'); + + // Lookup with invalidate: enable invalidatePort on hit + invalidatePort.en.inject(lookupPort.hit.value.toInt()); + + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should hit when invalidate is asserted (original behavior)'); + await clk.nextPosedge; + + // Try to lookup again - should miss because entry was invalidated + invalidatePort.en.inject(0); + await clk.nextPosedge; + + expect(lookupPort.hit.value.toBool(), isFalse, + reason: 'Should miss after entry was invalidated'); + + // Lookup tag 0x99 - should still be valid + lookupPort.tag.inject(0x99); + await clk.nextPosedge; + + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Entry 1 should still be valid'); + expect(lookupPort.idx.value.toInt(), equals(1), + reason: 'Should return index 1'); + + await Simulator.endSimulation(); + }); + + test('multiple lookups with different invalidate behavior', () async { + const tagWidth = 16; + const idWidth = 3; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + + final writePort = TagInterface(idWidth, tagWidth); + final invalidatePort1 = TagInterface(idWidth, tagWidth); + final invalidatePort2 = TagInterface(idWidth, tagWidth); + final lookupPort1 = TagInterface(idWidth, tagWidth); + final lookupPort2 = TagInterface(idWidth, tagWidth); + + final cam = Cam( + clk, + reset, + [writePort, invalidatePort1, invalidatePort2], + [lookupPort1, lookupPort2], + ); + + // Wire invalidate ports to lookup port outputs + invalidatePort1.idx <= lookupPort1.idx; + invalidatePort1.tag <= lookupPort1.tag; + invalidatePort2.idx <= lookupPort2.idx; + invalidatePort2.tag <= lookupPort2.tag; + + await cam.build(); + + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + writePort.en.inject(0); + invalidatePort1.en.inject(0); + invalidatePort1.hit.inject(0); + invalidatePort2.en.inject(0); + invalidatePort2.hit.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Write some entries + writePort.en.inject(1); + writePort.hit.inject(1); + for (var i = 0; i < 4; i++) { + writePort.idx.inject(i); + writePort.tag.inject(0x1000 + i); + await clk.nextPosedge; + } + writePort.en.inject(0); + await clk.nextPosedge; + + // Both ports lookup different tags + lookupPort1.tag.inject(0x1000); + invalidatePort1.en.inject(0); + + lookupPort2.tag.inject(0x1002); + invalidatePort2.en.inject(0); + + await clk.nextPosedge; + + expect(lookupPort1.hit.value.toBool(), isTrue); + expect(lookupPort1.idx.value.toInt(), equals(0)); + expect(lookupPort2.hit.value.toBool(), isTrue); + expect(lookupPort2.idx.value.toInt(), equals(2)); + + // Port 1 invalidates entry 0, port 2 doesn't invalidate + invalidatePort1.en.inject(lookupPort1.hit.value.toInt()); + await clk.nextPosedge; + + // Port 1 should now miss, port 2 should still hit + invalidatePort1.en.inject(0); + await clk.nextPosedge; + + expect(lookupPort1.hit.value.toBool(), isFalse, + reason: 'Entry 0 was invalidated'); + expect(lookupPort2.hit.value.toBool(), isTrue, + reason: 'Entry 2 was not invalidated'); + + await Simulator.endSimulation(); + }); + + test('rewrite after invalidate restores entry', () async { + const tagWidth = 8; + const numEntries = 4; + const idWidth = 2; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + + final writePort = TagInterface(idWidth, tagWidth); + final invalidatePort = TagInterface(idWidth, tagWidth); + final lookupPort = TagInterface(idWidth, tagWidth); + + final cam = Cam( + clk, + reset, + [writePort, invalidatePort], + [lookupPort], + numEntries: numEntries, + ); + + // Wire invalidatePort to lookupPort output + invalidatePort.idx <= lookupPort.idx; + invalidatePort.tag <= lookupPort.tag; + + await cam.build(); + + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + writePort.en.inject(0); + invalidatePort.en.inject(0); + invalidatePort.hit.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Write and invalidate + writePort.en.inject(1); + writePort.hit.inject(1); + writePort.idx.inject(0); + writePort.tag.inject(0xAA); + await clk.nextPosedge; + writePort.en.inject(0); + await clk.nextPosedge; + + // Lookup and invalidate + lookupPort.tag.inject(0xAA); + invalidatePort.en.inject(1); + + // Combinational output check + await clk.nextNegedge; + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should hit when invalidate is asserted (original behavior)'); + await clk.nextPosedge; + + // Should now miss + invalidatePort.en.inject(0); + await clk.nextPosedge; + expect(lookupPort.hit.value.toBool(), isFalse); + + // Rewrite the same entry + writePort.en.inject(1); + writePort.hit.inject(1); + writePort.idx.inject(0); + writePort.tag.inject(0xAA); + await clk.nextPosedge; + writePort.en.inject(0); + await clk.nextPosedge; + + // Should hit again + lookupPort.tag.inject(0xAA); + + // Combinational output check + await clk.nextNegedge; + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Entry should be valid after rewrite'); + + await Simulator.endSimulation(); + }); + + test('invalidate only clears on hit', () async { + const tagWidth = 8; + const numEntries = 4; + const idWidth = 2; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + + final writePort = TagInterface(idWidth, tagWidth); + final invalidatePort = TagInterface(idWidth, tagWidth); + final lookupPort = TagInterface(idWidth, tagWidth); + + final cam = Cam( + clk, + reset, + [writePort, invalidatePort], + [lookupPort], + numEntries: numEntries, + ); + + // Wire invalidatePort to lookupPort output + invalidatePort.idx <= lookupPort.idx; + invalidatePort.tag <= lookupPort.tag; + + await cam.build(); + + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + writePort.en.inject(0); + invalidatePort.en.inject(0); + invalidatePort.hit.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Write entry + writePort.en.inject(1); + writePort.hit.inject(1); + writePort.idx.inject(0); + writePort.tag.inject(0x55); + await clk.nextPosedge; + writePort.en.inject(0); + await clk.nextPosedge; + + // Lookup wrong tag with invalidate - should not affect entry + // Because invalidatePort.idx comes from lookupPort.idx which will be + // 0 (default), but the entry at 0 has tag 0x55, not 0x66 + lookupPort.tag.inject(0x66); // Wrong tag + // When hit is 0, invalidatePort.en being 1 would write to idx 0 + // but since we're wiring en to lookupPort.hit, we only invalidate on hit + await clk.nextNegedge; + + expect(lookupPort.hit.value.toBool(), isFalse, + reason: 'Should miss for wrong tag'); + + // Lookup correct tag - should still hit + lookupPort.tag.inject(0x55); + invalidatePort.en.inject(0); + await clk.nextPosedge; + + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Entry should still be valid (invalidate only on hit)'); + + await Simulator.endSimulation(); + }); + + test('Cam with valid tracking and invalidate', () async { + const numEntries = 4; + const tagWidth = 8; + const idWidth = 2; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + + final writePort = TagInterface(idWidth, tagWidth); + final invalidatePort = TagInterface(idWidth, tagWidth); + final lookupPort = TagInterface(idWidth, tagWidth); + + final cam = Cam( + clk, + reset, + [writePort, invalidatePort], + [lookupPort], + numEntries: numEntries, + enableValidTracking: true, + ); + + // Wire invalidatePort to lookupPort output + invalidatePort.idx <= lookupPort.idx; + invalidatePort.tag <= lookupPort.tag; + + await cam.build(); + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + writePort.en.inject(0); + invalidatePort.en.inject(0); + invalidatePort.hit.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Initially empty + expect(cam.empty!.value.toBool(), isTrue, + reason: 'Should be empty initially'); + expect(cam.full!.value.toBool(), isFalse, + reason: 'Should not be full initially'); + expect(cam.validCount!.value.toInt(), equals(0), + reason: 'Count should be 0'); + + // Write entries + writePort.en.inject(1); + writePort.hit.inject(1); + for (var i = 0; i < numEntries; i++) { + writePort.idx.inject(i); + writePort.tag.inject(0x10 + i); + await clk.nextPosedge; + } + writePort.en.inject(0); + await clk.nextPosedge; + + // Should be full + expect(cam.empty!.value.toBool(), isFalse, reason: 'Should not be empty'); + expect(cam.full!.value.toBool(), isTrue, reason: 'Should be full'); + expect(cam.validCount!.value.toInt(), equals(numEntries), + reason: 'Count should equal numEntries'); + + // Invalidate one entry + lookupPort.tag.inject(0x10); + invalidatePort.en.inject(1); + + // Check combinational output. + await clk.nextNegedge; + // Wait for invalidate to take effect + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should hit when invalidate is asserted (original behavior)'); + await clk.nextPosedge; + + expect(cam.full!.value.toBool(), isFalse, + reason: 'Should not be full after invalidate'); + expect(cam.validCount!.value.toInt(), equals(numEntries - 1), + reason: 'Count should decrease by 1'); + + // Invalidate remaining entries + for (var i = 1; i < numEntries; i++) { + lookupPort.tag.inject(0x10 + i); + await clk.nextPosedge; + await clk.nextPosedge; // Wait for invalidate + } + + // Should be empty now + expect(cam.empty!.value.toBool(), isTrue, + reason: 'Should be empty after invalidating all'); + expect(cam.validCount!.value.toInt(), equals(0), + reason: 'Count should be 0'); + + await Simulator.endSimulation(); + }); + + test('Cam simultaneous write and invalidate', () async { + const numEntries = 8; + const tagWidth = 8; + const idWidth = 3; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + + final writePort = TagInterface(idWidth, tagWidth); + final invalidatePort = TagInterface(idWidth, tagWidth); + final lookupPort = TagInterface(idWidth, tagWidth); + + final cam = Cam( + clk, + reset, + [writePort, invalidatePort], + [lookupPort], + enableValidTracking: true, + ); + + // Wire invalidatePort to lookupPort output + invalidatePort.idx <= lookupPort.idx; + invalidatePort.tag <= lookupPort.tag; + + await cam.build(); + + unawaited(Simulator.run()); + + // Reset + reset.inject(1); + writePort.en.inject(0); + invalidatePort.en.inject(0); + invalidatePort.hit.inject(0); + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + await clk.nextPosedge; + + // Fill CAM completely + writePort.en.inject(1); + writePort.hit.inject(1); + for (var i = 0; i < numEntries; i++) { + writePort.idx.inject(i); + writePort.tag.inject(0xA0 + i); + await clk.nextPosedge; + } + + // Verify full + expect(cam.full!.value.toBool(), isTrue, reason: 'CAM should be full'); + expect(cam.validCount!.value.toInt(), equals(numEntries), + reason: 'Count should equal numEntries'); + + // Disable write temporarily + writePort.en.inject(0); + await clk.nextPosedge; + + // Verify we can lookup the entries before starting simultaneous ops, + invalidatePort.en.inject(0); // Don't invalidate yet + lookupPort.tag.inject(0xA0); + await clk.nextPosedge; + + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should find 0xA0 before test'); + lookupPort.tag.inject(0xA1); + await clk.nextPosedge; + + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should find 0xA1 before test'); + + // Now perform simultaneous write and invalidate operations This + // demonstrates that the CAM can handle looking up/invalidating entries + // while simultaneously writing new entries. + // + // Test: Do 4 simultaneous operations (half the CAM size) to demonstrate + // that writes and invalidate-on-read can happen concurrently. + + writePort.en.inject(1); + writePort.hit.inject(1); + invalidatePort.en.inject(1); + + const numSimultaneousOps = numEntries ~/ 2; + for (var i = 0; i < numSimultaneousOps; i++) { + // Write to the upper half while invalidating the lower half. + final writeIdx = i + numSimultaneousOps; // Indices 4-7 + final lookupIdx = i; // Indices 0-3 + final lookupTag = 0xA0 + lookupIdx; + + lookupPort.tag.inject(lookupTag); + writePort.idx.inject(writeIdx); + writePort.tag.inject(0xC0 + writeIdx); // New tag + + await clk.nextNegedge; + + // Should hit when invalidate is asserted (original behavior restored). + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Cycle $i: Should hit when invalidate=1 for ' + 'tag 0x${lookupTag.toRadixString(16)}'); + await clk.nextPosedge; + } + + // Wait for final write and invalidates to settle. + // Need extra cycles because: + // - Writes complete on the clock edge. + // - Invalidates happen one cycle after lookup (registered). + await clk.nextPosedge; + await clk.nextPosedge; + await clk.nextPosedge; + + // After invalidating lower half (indices 0-3) and writing new entries to + // upper half (indices 4-7), we should have exactly the upper half valid. + expect(cam.full!.value.toBool(), isFalse, + reason: 'CAM should not be full - only upper half is valid'); + expect(cam.validCount!.value.toInt(), equals(numSimultaneousOps), + reason: + 'Count should equal number of new writes ($numSimultaneousOps)'); + + // Verify the new entries in upper half exist and can be looked up + invalidatePort.en.inject(0); // Don't invalidate for verification + for (var i = numSimultaneousOps; i < numEntries; i++) { + lookupPort.tag.inject(0xC0 + i); + await clk.nextPosedge; + expect(lookupPort.hit.value.toBool(), isTrue, + reason: 'Should find new tag 0x${(0xC0 + i).toRadixString(16)}'); + expect(lookupPort.idx.value.toInt(), equals(i), + reason: 'Should be at index $i'); + } + + // Verify lower half entries are invalidated (miss on lookup) + for (var i = 0; i < numSimultaneousOps; i++) { + lookupPort.tag.inject(0xA0 + i); + await clk.nextPosedge; + expect(lookupPort.hit.value.toBool(), isFalse, + reason: 'Should NOT find invalidated ' + 'tag 0x${(0xA0 + i).toRadixString(16)}'); + } + + await Simulator.endSimulation(); + }); + }); +} diff --git a/test/memory/csr_test.dart b/test/memory/csr_test.dart index 886a059275..c374cceb9f 100644 --- a/test/memory/csr_test.dart +++ b/test/memory/csr_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // csr_test.dart @@ -277,6 +277,7 @@ class DummyCsrTopModule extends Module { late final Logic _clk; late final Logic _reset; + // Retaining the child module establishes the hierarchy under test. // ignore: unused_field late final CsrTop _top; late final DataPortInterface _fdr; @@ -307,7 +308,7 @@ void main() { await Simulator.reset(); }); - test('simple individual CSRs', () async { + test('simple individual CSRs', () { const dataWidth1 = 32; const dataWidth2 = 8; @@ -1231,7 +1232,7 @@ void main() { }); }); - test('CSR validation failures', () async { + test('CSR validation failures', () { // illegal individual field - no legal values expect( () => CsrFieldConfig( @@ -1324,7 +1325,7 @@ void main() { throwsA(isA())); }); - test('CSR block and top validation failures', () async { + test('CSR block and top validation failures', () { // illegal block - empty expect( () => CsrBlockConfig(name: 'block', baseAddr: 0x0, registers: const []), diff --git a/test/memory/fifo_test.dart b/test/memory/fifo_test.dart index dfcf4ec74b..807182f6a1 100644 --- a/test/memory/fifo_test.dart +++ b/test/memory/fifo_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // fifo_test.dart @@ -677,7 +677,7 @@ void main() { final tracker = FifoTracker(fifoTest.fifo, outputFolder: 'tmp_test', dumpTable: false); - Simulator.registerEndOfSimulationAction(() async => tracker.terminate()); + Simulator.registerEndOfSimulationAction(tracker.terminate); await fifoTest.start(); diff --git a/test/memory/resettable_entries_test.dart b/test/memory/resettable_entries_test.dart index 5d3eecb66f..b05d15ec9e 100644 --- a/test/memory/resettable_entries_test.dart +++ b/test/memory/resettable_entries_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // resettable_entries_test.dart @@ -82,7 +82,7 @@ void main() { expect(vals.map((e) => e.toInt()).toList(), [0, 1, 5, 0xa5a5]); }); - test('missized list throws', () async { + test('missized list throws', () { expect(setupAndDumpRf([1, 2]), throwsA(isA())); }); }); @@ -96,11 +96,11 @@ void main() { expect(vals.map((e) => e.toInt()), [0, 1, 0, 2]); }); - test('too big key throws', () async { + test('too big key throws', () { expect(setupAndDumpRf({5: 2}), throwsA(isA())); }); - test('too small key throws', () async { + test('too small key throws', () { expect(setupAndDumpRf({-1: 2}), throwsA(isA())); }); }); diff --git a/test/memory/sparse_memory_storage_test.dart b/test/memory/sparse_memory_storage_test.dart index fe6991b916..1364661379 100644 --- a/test/memory/sparse_memory_storage_test.dart +++ b/test/memory/sparse_memory_storage_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // sparse_memory_storage_test.dart @@ -18,6 +18,7 @@ void main() { test('can load a simple file (legacy)', () { final hex = File('test/example1.hex').readAsStringSync(); final storage = SparseMemoryStorage(addrWidth: 32, dataWidth: 32) + // This test intentionally verifies the deprecated compatibility API. // ignore: deprecated_member_use_from_same_package ..loadMemHex(hex); diff --git a/test/one_hot_test.dart b/test/one_hot_test.dart index 66ccbffa2d..aa66d6f46b 100644 --- a/test/one_hot_test.dart +++ b/test/one_hot_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // one_hot_test.dart @@ -64,7 +64,7 @@ void main() { ]; for (final ohToBType in ohToBTypes) { - test('simple_decode ${ohToBType.name}', () async { + test('simple_decode ${ohToBType.name}', () { // Compute the first 1 in a binary value for (var pos = 0; pos < ohToBType.max; pos++) { final val = BigInt.two.pow(pos); diff --git a/test/parity_test.dart b/test/parity_test.dart index f9848617a0..95bfa1793e 100644 --- a/test/parity_test.dart +++ b/test/parity_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // arbiter_test.dart @@ -13,7 +13,7 @@ import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:test/test.dart'; void main() { - test('parity transmitter', () async { + test('parity transmitter', () { const width = 8; final vector = Logic(width: width); @@ -45,7 +45,7 @@ void main() { parityTransmitter.transmission.value, LogicValue.ofString('011111111')); }); - test('parity receiver checking', () async { + test('parity receiver checking', () { const width = 9; final vector = Logic(width: width); diff --git a/test/priority_encoder_test.dart b/test/priority_encoder_test.dart index 8e89a8b856..abfffef7d3 100644 --- a/test/priority_encoder_test.dart +++ b/test/priority_encoder_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // priority_encoder_test.dart @@ -76,9 +76,7 @@ void main() { }); test('PrefixPriorityEncoder simple test', () { - final val = Logic(width: 5); - // ignore: cascade_invocations - val.put(3); + final val = Logic(width: 5)..put(3); expect(ParallelPrefixPriorityEncoder(val).out.value.toInt(), equals(0)); expect(ParallelPrefixPriorityEncoder(val.reversed).out.value.toInt(), equals(3)); @@ -88,9 +86,7 @@ void main() { }); test('PrefixPriorityEncoder simple test', () { - final bitVector = Logic(width: 5); - // ignore: cascade_invocations - bitVector.put(8); + final bitVector = Logic(width: 5)..put(8); final encoder = ParallelPrefixPriorityEncoder(bitVector, ppGen: BrentKung.new, generateValid: true); @@ -98,9 +94,7 @@ void main() { }); test('PrefixPriorityEncoder return beyond width if zero', () { - final val = Logic(width: 5); - // ignore: cascade_invocations - val.put(0); + final val = Logic(width: 5)..put(0); expect(ParallelPrefixPriorityEncoder(val).out.value.toInt(), equals(val.width + 1)); expect(ParallelPrefixPriorityEncoder(val.reversed).out.value.toInt(), diff --git a/test/ready_valid_bfm_test.dart b/test/ready_valid_bfm_test.dart index 8f69322847..599ef047d2 100644 --- a/test/ready_valid_bfm_test.dart +++ b/test/ready_valid_bfm_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // ready_valid_bfm_test.dart @@ -99,9 +99,9 @@ class ReadyValidBfmTest extends Test { final jsonStr = File('$outFolder/readyValidTracker.tracker.json').readAsStringSync(); - final jsonContents = json.decode(jsonStr); - // ignore: avoid_dynamic_calls - expect(jsonContents['records'].length, numTransfers); + final jsonContents = json.decode(jsonStr) as Map; + final records = jsonContents['records'] as List; + expect(records.length, numTransfers); Directory(outFolder).deleteSync(recursive: true); }); diff --git a/test/reduction_tree_generator_test.dart b/test/reduction_tree_generator_test.dart index e0acbb3936..cbc16a0fd5 100644 --- a/test/reduction_tree_generator_test.dart +++ b/test/reduction_tree_generator_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // reduction_tree_generator_test.dart @@ -37,7 +37,7 @@ void main() { return a; } - test('reduction tree of add operations -- quick test', () async { + test('reduction tree of add operations -- quick test', () { const width = 13; const length = 79; final vec = []; @@ -54,7 +54,7 @@ void main() { } }); - test('reduction tree of adders -- large', () async { + test('reduction tree of adders -- large', () { final clk = SimpleClockGenerator(10).clk; const width = 17; @@ -158,7 +158,8 @@ void main() { final clk = SimpleClockGenerator(10).clk; const width = 17; - const length = 129; + // Smallest length that exercises four radix-4 reduction levels. + const length = 65; final vec = []; // First sum will be length *(length-1) /2 for (var i = 0; i < length; i++) { diff --git a/test/reduction_tree_test.dart b/test/reduction_tree_test.dart index b5f2bc846f..6db3b2371c 100644 --- a/test/reduction_tree_test.dart +++ b/test/reduction_tree_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2025 Intel Corporation +// Copyright (C) 2025-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // reduction_tree_test.dart @@ -42,7 +42,7 @@ void main() { {int? depth, Logic? control, String name = ''}) => inputs.reduce((v, e) => v + e); - test('reduction tree of add operations -- quick test', () async { + test('reduction tree of add operations -- quick test', () { const width = 13; const length = 79; final vec = []; @@ -111,7 +111,7 @@ void main() { await Simulator.endSimulation(); }); - test('reduction tree of adders -- large', () async { + test('reduction tree of adders -- large', () { final clk = SimpleClockGenerator(10).clk; const width = 17; @@ -184,7 +184,8 @@ void main() { final clk = SimpleClockGenerator(10).clk; const width = 17; - const length = 129; + // Smallest length that exercises four radix-4 reduction levels. + const length = 65; final vec = []; // First sum will be length *(length-1) /2 for (var i = 0; i < length; i++) { diff --git a/test/spi/spi_gaskets_test.dart b/test/spi/spi_gaskets_test.dart index d78cfe2aa7..7e93028924 100644 --- a/test/spi/spi_gaskets_test.dart +++ b/test/spi/spi_gaskets_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // spi_gaskets_test.dart @@ -60,9 +60,9 @@ class SpiMainTest extends Test { const numTransfers = 4; final jsonStr = File('$outFolder/spiTracker.tracker.json').readAsStringSync(); - final jsonContents = json.decode(jsonStr); - // ignore: avoid_dynamic_calls - expect(jsonContents['records'].length, 2 * numTransfers); + final jsonContents = json.decode(jsonStr) as Map; + final records = jsonContents['records'] as List; + expect(records.length, 2 * numTransfers); Directory(outFolder).deleteSync(recursive: true); }); diff --git a/test/summation/counter_test.dart b/test/summation/counter_test.dart index 90ad4a62ec..b1c802504e 100644 --- a/test/summation/counter_test.dart +++ b/test/summation/counter_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // counter_test.dart diff --git a/test/summation/summation_test_utils.dart b/test/summation/summation_test_utils.dart index 89dba06f49..681f3b5e97 100644 --- a/test/summation/summation_test_utils.dart +++ b/test/summation/summation_test_utils.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2024 Intel Corporation +// Copyright (C) 2024-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // summation_test_utils.dart @@ -7,6 +7,7 @@ // 2024 October // Author: Max Korbel +// These white-box helpers drive protected summation inputs across variants. // ignore_for_file: invalid_use_of_protected_member import 'dart:math'; @@ -121,7 +122,7 @@ List genRandomInterfaces(Random rand) { /// Sets up a listener on clock edges to check that counters are functioning /// properly. void checkCounter(Counter counter) { - final sub = counter.clk.posedge.listen((_) async { + final sub = counter.clk.posedge.listen((_) { final errPrefix = '@${Simulator.time}: '; if (counter is GatedCounter && !counter.saturates) { diff --git a/tool/confapp_dev_mode.sh b/tool/confapp_dev_mode.sh new file mode 100644 index 0000000000..6ed3ee7170 --- /dev/null +++ b/tool/confapp_dev_mode.sh @@ -0,0 +1,632 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause + +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +cd "$repo_root" + +mode="${1:-}" +root_overrides="pubspec_overrides.yaml" +confapp_overrides="confapp/pubspec_overrides.yaml" +source_state=".confapp_dependency_sources" +marker="# Generated by tool/confapp_dev_mode.sh. Do not edit by hand." +default_rohd_path="$HOME/max/rohd" +default_viewer_path="$HOME/true-release/rohd-schematic-viewer" + +rohd_git_url="${ROHD_GIT_URL:-https://github.com/intel/rohd.git}" +rohd_git_ref="${ROHD_GIT_REF:-main}" +viewer_git_url="${SCHEMATIC_VIEWER_GIT_URL:-https://github.com/intel/rohd-schematic-viewer.git}" +viewer_git_ref="${SCHEMATIC_VIEWER_GIT_REF:-main}" +rohd_local_path="${ROHD_LOCAL_PATH:-$default_rohd_path}" +viewer_local_path="${SCHEMATIC_VIEWER_LOCAL_PATH:-${ROHD_SCHEMATIC_VIEWER_PATH:-$default_viewer_path}}" + +usage() { + cat <<'USAGE' +Usage: tool/confapp_dev_mode.sh + +Configure individual package sources: + tool/confapp_dev_mode.sh configure ... + where each source is hosted, git, or local. + +For example: + tool/confapp_dev_mode.sh configure \ + rohd_schematic_viewer local \ + rohd_source_navigator local + +Modes: + manifest Disable overrides and use pubspec.yaml sources. + local-rohd Use the local ROHD checkout and its split packages. + local-viewer Use the local schematic-viewer checkout. + local-all Use local ROHD packages and the local schematic viewer. + show Print the current generated override file, if present. + configure-group Configure a dependency group while preserving other sources. + +Supported packages: + rohd + rohd_hierarchy + rohd_devtools_widgets + rohd_source_navigator + rohd_schematic_viewer + +The default local checkouts are ~/max/rohd and +~/true-release/rohd-schematic-viewer. Override them with ROHD_LOCAL_PATH and +SCHEMATIC_VIEWER_LOCAL_PATH. + +Git sources use ROHD_GIT_URL, ROHD_GIT_REF, SCHEMATIC_VIEWER_GIT_URL, and +SCHEMATIC_VIEWER_GIT_REF. The VS Code tasks accept repository:ref in +ROHD_SOURCE_VALUE and SCHEMATIC_VIEWER_SOURCE_VALUE. Hosted sources use the +constraints in pubspec.yaml. + +With no overrides, pubspec.yaml supplies the hosted release dependencies. +USAGE +} + +backup_overrides() { + local overrides="$1" + local backup="$overrides.disabled" + local suffix=1 + + if [[ ! -f "$overrides" ]]; then + return + fi + + while [[ -e "$backup" ]]; do + backup="$overrides.disabled.$suffix" + suffix=$((suffix + 1)) + done + + mv -- "$overrides" "$backup" + echo "Moved existing $overrides to $backup." +} + +write_override_header() { + local output="$1" + + cat >"$output" <&2 + exit 2 + fi + + url="${spec%:*}" + ref="${spec##*:}" + if [[ -z "$url" || -z "$ref" || "$ref" == //* ]]; then + echo "Git source '$spec' must use the format repository:ref." >&2 + exit 2 + fi + + case "$url" in + github.com/*) url="https://$url" ;; + esac + + case "$target" in + rohd) + rohd_git_url="$url" + rohd_git_ref="$ref" + ;; + viewer) + viewer_git_url="$url" + viewer_git_ref="$ref" + ;; + esac +} + +display_git_source() { + local target="$1" + local url + local ref + + case "$target" in + rohd) + url="$rohd_git_url" + ref="$rohd_git_ref" + ;; + viewer) + url="$viewer_git_url" + ref="$viewer_git_ref" + ;; + esac + + case "$url" in + https://github.com/*) + url="${url#https://}" + url="${url%.git}" + ;; + esac + printf '%s:%s\n' "$url" "$ref" +} + +display_local_path() { + local path="$1" + + case "$path" in + "$HOME") printf '~\n' ;; + "$HOME"/*) printf '~/%s\n' "${path#"$HOME"/}" ;; + *) printf '%s\n' "$path" ;; + esac +} + +prompt_source_value() { + local target="$1" + local source="$2" + local default_value + local prompt + local value + + if [[ "${CONFAPP_PROMPT_SOURCE_VALUE:-0}" != 1 || + "$source" == "hosted" ]]; then + return + fi + if [[ ! -t 0 ]]; then + echo "Interactive source prompting requires a terminal." >&2 + exit 2 + fi + + case "$target:$source" in + rohd:git) + default_value="$(display_git_source rohd)" + prompt="ROHD Git repository:ref: " + ;; + rohd:local) + default_value="$(display_local_path "$rohd_local_path")" + prompt="Local ROHD checkout path: " + ;; + viewer:git) + default_value="$(display_git_source viewer)" + prompt="Schematic viewer Git repository:ref: " + ;; + viewer:local) + default_value="$(display_local_path "$viewer_local_path")" + prompt="Local schematic-viewer checkout path: " + ;; + esac + + IFS= read -e -r -i "$default_value" -p "$prompt" value + value="${value:-$default_value}" + if [[ "$target" == "rohd" ]]; then + ROHD_SOURCE_VALUE="$value" + else + SCHEMATIC_VIEWER_SOURCE_VALUE="$value" + fi +} + +resolve_rohd_path() { + local rohd_path="$rohd_local_path" + + rohd_path="$(expand_home_path "$rohd_path")" + [[ "$rohd_path" = /* ]] || rohd_path="$repo_root/$rohd_path" + if [[ ! -f "$rohd_path/pubspec.yaml" ]] || + ! grep -Eq '^name:[[:space:]]*rohd[[:space:]]*$' "$rohd_path/pubspec.yaml"; then + echo "Could not find a ROHD checkout at $rohd_path." >&2 + echo "Set ROHD_LOCAL_PATH to a checkout rooted at the rohd package." >&2 + exit 1 + fi + + realpath "$rohd_path" +} + +resolve_rohd_package_path() { + local package="$1" + local rohd_path + local package_path + + rohd_path="$(resolve_rohd_path)" + if [[ "$package" == "rohd" ]]; then + package_path="$rohd_path" + else + package_path="$rohd_path/packages/$package" + fi + + if [[ ! -f "$package_path/pubspec.yaml" ]] || + ! grep -Eq "^name:[[:space:]]*$package[[:space:]]*$" "$package_path/pubspec.yaml"; then + echo "Missing local ROHD package: $package_path" >&2 + exit 1 + fi + + realpath "$package_path" +} + +resolve_viewer_path() { + local viewer_path="$viewer_local_path" + + viewer_path="$(expand_home_path "$viewer_path")" + [[ "$viewer_path" = /* ]] || viewer_path="$repo_root/$viewer_path" + if [[ ! -f "$viewer_path/pubspec.yaml" ]] || + ! grep -Eq '^name:[[:space:]]*rohd_schematic_viewer[[:space:]]*$' "$viewer_path/pubspec.yaml"; then + echo "Could not find a schematic-viewer checkout at $viewer_path." >&2 + echo "Set SCHEMATIC_VIEWER_LOCAL_PATH to the package checkout." >&2 + exit 1 + fi + + realpath "$viewer_path" +} + +validate_package() { + case "$1" in + rohd|rohd_hierarchy|rohd_devtools_widgets|rohd_source_navigator|rohd_schematic_viewer) ;; + *) + echo "Unsupported package '$1'." >&2 + echo "Run '$0 help' to see the supported packages." >&2 + exit 2 + ;; + esac +} + +validate_source() { + local package="$1" + local source="$2" + + case "$source" in + hosted|git|local) ;; + *) + echo "Invalid dependency source '$source'; expected hosted, git, or local." >&2 + exit 2 + ;; + esac + +} + +append_git_dependency() { + local output="$1" + local package="$2" + local url="$rohd_git_url" + local ref="$rohd_git_ref" + local path="" + + case "$package" in + rohd) ;; + rohd_hierarchy|rohd_devtools_widgets|rohd_source_navigator) + path="packages/$package" + ;; + rohd_schematic_viewer) + url="$viewer_git_url" + ref="$viewer_git_ref" + ;; + esac + + cat >>"$output" <>"$output" + fi +} + +append_local_dependency() { + local output="$1" + local package="$2" + local package_path + + if [[ "$package" == "rohd_schematic_viewer" ]]; then + package_path="$(resolve_viewer_path)" + else + package_path="$(resolve_rohd_package_path "$package")" + fi + + printf ' %s:\n path: %s\n' "$package" "$package_path" >>"$output" +} + +write_configured_overrides() { + local args=("$@") + local package + local source + local output + local root_output + local confapp_output + local index + local root_has_overrides=false + local confapp_has_overrides=false + local -A seen_packages=() + + if [[ "${#args[@]}" -eq 0 || "$(( ${#args[@]} % 2 ))" -ne 0 ]]; then + echo "Usage: $0 configure ..." >&2 + exit 2 + fi + + for ((index = 0; index < ${#args[@]}; index += 2)); do + package="${args[index]}" + source="${args[index + 1]}" + validate_package "$package" + validate_source "$package" "$source" + if [[ -n "${seen_packages[$package]:-}" ]]; then + echo "Package '$package' was configured more than once." >&2 + exit 2 + fi + seen_packages["$package"]=1 + + if [[ "$source" == "local" ]]; then + if [[ "$package" == "rohd_schematic_viewer" ]]; then + resolve_viewer_path >/dev/null + else + resolve_rohd_package_path "$package" >/dev/null + fi + fi + done + + root_output="$(mktemp "${TMPDIR:-/tmp}/rohd-hcl-root-overrides.XXXXXX")" + confapp_output="$(mktemp "${TMPDIR:-/tmp}/rohd-hcl-confapp-overrides.XXXXXX")" + trap 'rm -f "$root_output" "$confapp_output"' EXIT + write_override_header "$root_output" + write_override_header "$confapp_output" + + for ((index = 0; index < ${#args[@]}; index += 2)); do + package="${args[index]}" + source="${args[index + 1]}" + if [[ "$package" == "rohd" ]]; then + output="$root_output" + else + output="$confapp_output" + fi + case "$source" in + hosted) ;; + git) + append_git_dependency "$output" "$package" + if [[ "$package" == "rohd" ]]; then + root_has_overrides=true + else + confapp_has_overrides=true + fi + ;; + local) + append_local_dependency "$output" "$package" + if [[ "$package" == "rohd" ]]; then + root_has_overrides=true + else + confapp_has_overrides=true + fi + ;; + esac + done + + install_overrides "$root_output" "$root_overrides" "$root_has_overrides" + install_overrides \ + "$confapp_output" "$confapp_overrides" "$confapp_has_overrides" + trap - EXIT +} + +write_source_state() { + cat >"$source_state" < " >&2 + exit 2 + fi + + if [[ -f "$source_state" ]]; then + while IFS='=' read -r package saved_source; do + case "$package" in + rohd) rohd_source="$saved_source" ;; + rohd_hierarchy) hierarchy_source="$saved_source" ;; + rohd_devtools_widgets) widgets_source="$saved_source" ;; + rohd_source_navigator) navigator_source="$saved_source" ;; + rohd_schematic_viewer) viewer_source="$saved_source" ;; + rohd_git_url) + [[ -v ROHD_GIT_URL ]] || rohd_git_url="$saved_source" + ;; + rohd_git_ref) + [[ -v ROHD_GIT_REF ]] || rohd_git_ref="$saved_source" + ;; + rohd_local_path) + [[ -v ROHD_LOCAL_PATH ]] || rohd_local_path="$saved_source" + ;; + viewer_git_url) + [[ -v SCHEMATIC_VIEWER_GIT_URL ]] || + viewer_git_url="$saved_source" + ;; + viewer_git_ref) + [[ -v SCHEMATIC_VIEWER_GIT_REF ]] || + viewer_git_ref="$saved_source" + ;; + viewer_local_path) + if [[ ! -v SCHEMATIC_VIEWER_LOCAL_PATH && + ! -v ROHD_SCHEMATIC_VIEWER_PATH ]]; then + viewer_local_path="$saved_source" + fi + ;; + esac + done <"$source_state" + fi + + if [[ "$group" == "schematic" ]]; then + prompt_source_value viewer "$source" + else + prompt_source_value rohd "$source" + fi + + case "$group" in + rohd) + validate_source rohd "$source" + if [[ "$source" == "git" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + configure_git_source "$ROHD_SOURCE_VALUE" rohd + elif [[ "$source" == "local" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + rohd_local_path="$(expand_home_path "$ROHD_SOURCE_VALUE")" + fi + rohd_source="$source" + ;; + package) + validate_source rohd_hierarchy "$source" + if [[ "$source" == "git" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + configure_git_source "$ROHD_SOURCE_VALUE" rohd + elif [[ "$source" == "local" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + rohd_local_path="$(expand_home_path "$ROHD_SOURCE_VALUE")" + fi + hierarchy_source="$source" + navigator_source="$source" + ;; + widget) + validate_source rohd_devtools_widgets "$source" + if [[ "$source" == "git" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + configure_git_source "$ROHD_SOURCE_VALUE" rohd + elif [[ "$source" == "local" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + rohd_local_path="$(expand_home_path "$ROHD_SOURCE_VALUE")" + fi + hierarchy_source="$source" + widgets_source="$source" + navigator_source="$source" + ;; + schematic) + validate_source rohd_schematic_viewer "$source" + if [[ "$source" == "git" && -n "${SCHEMATIC_VIEWER_SOURCE_VALUE:-}" ]]; then + configure_git_source "$SCHEMATIC_VIEWER_SOURCE_VALUE" viewer + elif [[ "$source" == "local" && -n "${SCHEMATIC_VIEWER_SOURCE_VALUE:-}" ]]; then + viewer_local_path="$(expand_home_path "$SCHEMATIC_VIEWER_SOURCE_VALUE")" + fi + viewer_source="$source" + ;; + all) + validate_source rohd "$source" + if [[ "$source" == "git" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + configure_git_source "$ROHD_SOURCE_VALUE" rohd + elif [[ "$source" == "local" && -n "${ROHD_SOURCE_VALUE:-}" ]]; then + rohd_local_path="$(expand_home_path "$ROHD_SOURCE_VALUE")" + fi + rohd_source="$source" + hierarchy_source="$source" + widgets_source="$source" + navigator_source="$source" + ;; + *) + echo "Invalid dependency group '$group'; expected rohd, package, widget, schematic, or all." >&2 + exit 2 + ;; + esac + + write_configured_overrides \ + rohd "$rohd_source" \ + rohd_hierarchy "$hierarchy_source" \ + rohd_devtools_widgets "$widgets_source" \ + rohd_source_navigator "$navigator_source" \ + rohd_schematic_viewer "$viewer_source" + write_source_state \ + "$rohd_source" \ + "$hierarchy_source" \ + "$widgets_source" \ + "$navigator_source" \ + "$viewer_source" +} + +use_manifest_dependencies() { + backup_overrides "$root_overrides" + backup_overrides "$confapp_overrides" + rm -f -- "$source_state" + echo "Using dependencies from pubspec.yaml." +} + +use_local_rohd_dependencies() { + write_group_configuration all local +} + +use_local_viewer_dependency() { + write_group_configuration schematic local +} + +use_all_local_dependencies() { + write_group_configuration all local + write_group_configuration schematic local +} + +case "$mode" in + configure) + shift + write_configured_overrides "$@" + ;; + configure-group) write_group_configuration "${2:-}" "${3:-}" ;; + manifest) use_manifest_dependencies ;; + local-rohd) use_local_rohd_dependencies ;; + local-viewer) use_local_viewer_dependency ;; + local-all) use_all_local_dependencies ;; + show) + for overrides in "$root_overrides" "$confapp_overrides"; do + if [[ -f "$overrides" ]]; then + echo "==> $overrides <==" + cat "$overrides" + else + echo "No $overrides present." + fi + done + ;; + hp-hv) + echo "Mode hp-hv is deprecated; use manifest." >&2 + use_manifest_dependencies + ;; + lp-hv) + echo "Mode lp-hv is deprecated; use local-rohd." >&2 + use_local_rohd_dependencies + ;; + hp-lv) + echo "Mode hp-lv is deprecated; use local-viewer." >&2 + use_local_viewer_dependency + ;; + lp-lv) + echo "Mode lp-lv is deprecated; use local-all." >&2 + use_all_local_dependencies + ;; + -h|--help|help|"") usage ;; + *) usage >&2; exit 2 ;; +esac diff --git a/tool/confapp_web_debug.sh b/tool/confapp_web_debug.sh new file mode 100644 index 0000000000..507337c40b --- /dev/null +++ b/tool/confapp_web_debug.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause + +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +port="${CONFAPP_WEB_PORT:-8080}" +flutter_bin="${CONFAPP_FLUTTER_BIN:-flutter}" + +if [[ "$#" -ne 0 ]]; then + echo "Usage: $0" >&2 + echo "Select dependency sources separately with tool/confapp_dev_mode.sh." >&2 + exit 2 +fi + +flutter_bin="$(command -v "$flutter_bin")" || { + echo "Flutter executable is unavailable." >&2 + exit 1 +} + +cd "$repo_root" +bash "$repo_root/tool/generate_confapp_assets.sh" +"$flutter_bin" pub get +cd confapp +exec "$flutter_bin" run \ + --no-pub \ + -d web-server \ + --web-hostname=127.0.0.1 \ + --web-port="$port" diff --git a/tool/confapp_web_release.sh b/tool/confapp_web_release.sh new file mode 100755 index 0000000000..86cfcd4674 --- /dev/null +++ b/tool/confapp_web_release.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# confapp_web_release.sh +# Builds and serves the Confapp JavaScript release. +# +# 2026 September 28 +# Author: Desmond A. Kirkpatrick + +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +port="${CONFAPP_WEB_PORT:-8080}" +flutter_bin="${CONFAPP_FLUTTER_BIN:-flutter}" + +if [[ "$#" -ne 0 ]]; then + echo "Usage: $0" >&2 + echo "Select dependency sources separately with tool/confapp_dev_mode.sh." >&2 + exit 2 +fi + +flutter_bin="$(command -v "$flutter_bin")" || { + echo "Flutter executable is unavailable." >&2 + exit 1 +} + +cd "$repo_root" +bash "$repo_root/tool/generate_confapp_assets.sh" +"$flutter_bin" pub get +cd confapp +exec "$flutter_bin" run \ + --release \ + --no-pub \ + -d web-server \ + --web-hostname=127.0.0.1 \ + --web-port="$port" diff --git a/tool/confapp_web_wasm.sh b/tool/confapp_web_wasm.sh new file mode 100755 index 0000000000..44c89e3ba9 --- /dev/null +++ b/tool/confapp_web_wasm.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# confapp_web_wasm.sh +# Builds and serves the experimental Confapp WASM release. +# +# 2026 September 28 +# Author: Desmond A. Kirkpatrick + +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +port="${CONFAPP_WASM_WEB_PORT:-8081}" +flutter_bin="${CONFAPP_FLUTTER_BIN:-flutter}" + +if [[ "$#" -ne 0 ]]; then + echo "Usage: $0" >&2 + echo "Select dependency sources separately with tool/confapp_dev_mode.sh." >&2 + exit 2 +fi + +flutter_bin="$(command -v "$flutter_bin")" || { + echo "Flutter executable is unavailable." >&2 + exit 1 +} + +cd "$repo_root" +bash "$repo_root/tool/generate_confapp_assets.sh" +"$flutter_bin" pub get +cd confapp +exec "$flutter_bin" run \ + --wasm \ + --release \ + --no-pub \ + -d web-server \ + --web-hostname=127.0.0.1 \ + --web-port="$port" diff --git a/tool/converters/json_html.sh b/tool/converters/json_html.sh deleted file mode 100755 index a966e761bb..0000000000 --- a/tool/converters/json_html.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2023-2024 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# -# jsonToHtml.sh -# Splice a json file into an HTML form for D3 schematic display -# -# 2023 May 09 -# Author: Desmond Kirkpatrick -# -# -# This script inserts a JSON file in the middle of an HTML file that drives -# a D3 schematic viewer. - -if !(test 1 -eq $#); then - echo One argument required: JSON file - exit 1 -fi; - -json=`cat $1 | sed -e 's/\\\\/_/g'` -pre=' - - - - d3-hwschematic - - - - - - - - - - - - - - - -' -echo "${pre}${json}${post}" diff --git a/tool/converters/verilog_html.sh b/tool/converters/verilog_html.sh deleted file mode 100755 index 977e1d6d9b..0000000000 --- a/tool/converters/verilog_html.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2023-2025 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# -# verilogToJSON.sh -# Run yosys to convert Verilog to JSON -# -# 2023 May 09 -# Author: Desmond Kirkpatrick - -# Executes synthesis given a module name -# Assumes .v extension -# Outputs .json - -if !(test 1 -eq $#); then - echo One argument required: module - exit 1 -fi; - -cd build/ -module=`basename $1 .sv` -../tool/converters/verilog_json.sh $1 -../tool/converters/json_html.sh ${module}.json > ${module}.html diff --git a/tool/converters/verilog_json.sh b/tool/converters/verilog_json.sh index 83021ff4d5..f184eeec6f 100755 --- a/tool/converters/verilog_json.sh +++ b/tool/converters/verilog_json.sh @@ -1,28 +1,29 @@ #!/bin/bash -# Copyright (C) 2023-2025 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # -# verilogToJSON.sh -# Run yosys to convert Verilog to JSON +# verilog_json.sh +# Convert SystemVerilog to a Yosys JSON netlist. # # 2023 May 09 # Author: Desmond Kirkpatrick -# Executes synthesis given a module name -# Assumes .v extension -# Outputs .json +set -euo pipefail -if !(test 1 -eq $#); then - echo One argument required: module +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 exit 1 -fi; +fi -yosys_bin=/oss-cad-suite/bin/yosys -module=`basename $1 .sv` -$yosys_bin -Q -T -q < confapp/assets/rohd_src/" + +MODULE_SOURCE_ASSETS="$REPO_ROOT/confapp/lib/hcl/module_source_assets.dart" +missing_assets=0 +while IFS= read -r source_asset; do + if [[ ! -f "$REPO_ROOT/confapp/assets/$source_asset" ]]; then + echo "Missing generated ROHD source asset: $source_asset" >&2 + missing_assets=1 + fi +done < <( + grep -oE "'rohd_src/[^']+'" "$MODULE_SOURCE_ASSETS" | + tr -d "'" | + sort -u +) + +if [[ "$missing_assets" -ne 0 ]]; then + exit 1 +fi + +PUBSPEC="$REPO_ROOT/confapp/pubspec.yaml" + +rohd_entries=$(find "$ASSET_DIR" -type d \ + | sed "s|$REPO_ROOT/confapp/| - |" \ + | sed 's|$|/|' \ + | sort) + +tmpfile=$(mktemp) +awk -v entries="$rohd_entries" ' + /^ assets:/ { + print + print entries + in_assets = 1 + next + } + in_assets && /^ - assets\/rohd_src/ { next } + in_assets && /^[^ ]/ { in_assets = 0 } + { print } +' "$PUBSPEC" > "$tmpfile" +mv "$tmpfile" "$PUBSPEC" + +dirs=$(echo "$rohd_entries" | wc -l) +echo "Updated pubspec.yaml with $dirs asset directories." diff --git a/tool/gh_actions/analyze_flutter_source.sh b/tool/gh_actions/analyze_flutter_source.sh index 15381d464c..8d376066ab 100755 --- a/tool/gh_actions/analyze_flutter_source.sh +++ b/tool/gh_actions/analyze_flutter_source.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023-2024 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # analyze_flutter_source.sh @@ -11,6 +11,10 @@ set -euo pipefail -cd confapp +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" + +bash "$repo_root/tool/generate_confapp_assets.sh" + +cd "$repo_root/confapp" flutter analyze --fatal-infos diff --git a/tool/gh_actions/analyze_source.sh b/tool/gh_actions/analyze_source.sh index 9a86dfc434..17b5a5bfed 100755 --- a/tool/gh_actions/analyze_source.sh +++ b/tool/gh_actions/analyze_source.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2022-2024 Intel Corporation +# Copyright (C) 2022-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # analyze_source.sh @@ -11,4 +11,10 @@ set -euo pipefail +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" + +bash "$repo_root/tool/generate_confapp_assets.sh" + +cd "$repo_root" + dart analyze --fatal-infos diff --git a/tool/gh_actions/build_schematic_viewer.sh b/tool/gh_actions/build_schematic_viewer.sh new file mode 100755 index 0000000000..73021f3cdd --- /dev/null +++ b/tool/gh_actions/build_schematic_viewer.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# Build the standalone schematic viewer used by API documentation. + +set -euo pipefail + +cd doc/schematic_viewer +flutter pub get +flutter build web \ + --release \ + --base-href /rohd-hcl/schematic_viewer/ diff --git a/tool/gh_actions/create_htmls.sh b/tool/gh_actions/create_htmls.sh deleted file mode 100755 index 83c047d953..0000000000 --- a/tool/gh_actions/create_htmls.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2022-2025 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause - -# Scavenged from Nic30:d3-hwschematic -#cp -r doc/d3-hwschematic-assets doc/api/ - -# Generate components and produce Verilog - -dart gen/generate.dart - -mkdir -p build - -# Convert Verilog into an HTML-based schematic for each -for i in build/*.sv -do - example=`basename $i .sv` - ./tool/converters/verilog_html.sh $example - cp build/$example.html doc/api/$example.html -done diff --git a/tool/gh_actions/create_schematic_pages.sh b/tool/gh_actions/create_schematic_pages.sh new file mode 100755 index 0000000000..02dd4f481d --- /dev/null +++ b/tool/gh_actions/create_schematic_pages.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Copyright (C) 2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# Generate native ROHD netlists and compatibility redirects for API docs. + +set -euo pipefail +shopt -s nullglob + +dart run gen/generate.dart + +netlists=(build/*.rohd.json) +if [[ ${#netlists[@]} -eq 0 ]]; then + echo "No ROHD netlists were generated." >&2 + exit 1 +fi + +mkdir -p doc/api/schematics +cp -- "${netlists[@]}" doc/api/schematics/ + +# Existing API documentation links point to .html. Keep those URLs +# stable with redirects; schematic rendering happens in the Flutter viewer. +for netlist in "${netlists[@]}"; do + name=$(basename "$netlist" .rohd.json) + cat > "doc/api/${name}.html" < + + + + ${name} - ROHD Schematic + + + +

Redirecting to + + ${name} schematic viewer... +

+ + +EOF +done + +echo "Created schematic assets for ${#netlists[@]} components." diff --git a/tool/gh_actions/hcl_site_generation_build.sh b/tool/gh_actions/hcl_site_generation_build.sh index 52b7de54e0..99f433779c 100755 --- a/tool/gh_actions/hcl_site_generation_build.sh +++ b/tool/gh_actions/hcl_site_generation_build.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023-2024 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # hcl_site_generation_build.sh @@ -11,7 +11,14 @@ set -euo pipefail -cd confapp +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" -# Use --profile instead of --release to avoid certain name of the module get replaced -flutter build web --profile --web-renderer html --base-href /rohd-hcl/confapp/ +# The source assets are intentionally generated and ignored by Git. Build +# them here so deployment jobs work from a fresh checkout. +bash "$repo_root/tool/generate_confapp_assets.sh" + +cd "$repo_root/confapp" + +# Use profile instead of release to avoid certain module names being replaced. +# JavaScript is required by the confapp's browser integrations. +flutter build web --profile --base-href /rohd-hcl/confapp/ diff --git a/tool/gh_actions/install_d3_hwschematic.sh b/tool/gh_actions/install_d3_hwschematic.sh deleted file mode 100755 index dba3a7cd77..0000000000 --- a/tool/gh_actions/install_d3_hwschematic.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2023-2024 Intel Corporation -# SPDX-License-Identifier: BSD-3-Clause -# -# install_d3_hwschematic.sh -# GitHub Actions step: Install project dependencies. -# -# 2023 May 09 -# Author: Desmond Kirkpatrick - -set -euo pipefail - -cd doc/d3-hwschematic -npm install -npm install --only=dev -npm run build - -cd ../.. -mkdir -p doc/api/d3-hwschematic-assets -cp -r doc/d3-hwschematic/node_modules/d3 doc/api/d3-hwschematic-assets -cp -r doc/d3-hwschematic/node_modules/elkjs doc/api/d3-hwschematic-assets -cp doc/d3-hwschematic/dist/d3-hwschematic.{css,js} doc/api/d3-hwschematic-assets - diff --git a/tool/gh_actions/install_dependencies.sh b/tool/gh_actions/install_dependencies.sh index ce2f101189..51d6b3aa09 100755 --- a/tool/gh_actions/install_dependencies.sh +++ b/tool/gh_actions/install_dependencies.sh @@ -1,14 +1,20 @@ #!/bin/bash -# Copyright (C) 2022-2024 Intel Corporation +# Copyright (C) 2022-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # install_dependencies.sh -# GitHub Actions step: Install project dependencies. +# Installs dependencies for the complete pub workspace. # # 2022 October 7 # Author: Chykon set -euo pipefail -dart pub get +if ! command -v flutter >/dev/null 2>&1; then + echo "Flutter is required to resolve this pub workspace because confapp" >&2 + echo "declares SDK dependencies from Flutter." >&2 + exit 1 +fi + +flutter pub get diff --git a/tool/gh_actions/run_flutter_tests.sh b/tool/gh_actions/run_flutter_tests.sh index c71d4d3452..a67d6adb98 100755 --- a/tool/gh_actions/run_flutter_tests.sh +++ b/tool/gh_actions/run_flutter_tests.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023-2024 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # run_flutter_tests.sh @@ -11,6 +11,14 @@ set -euo pipefail -cd confapp +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" -flutter test --platform chrome \ No newline at end of file +bash "$repo_root/tool/generate_confapp_assets.sh" + +cd "$repo_root/confapp" + +flutter test +flutter_bin_dir="$(dirname "$(readlink -f "$(command -v flutter)")")" +"$flutter_bin_dir/dart" run test:test \ + --platform chrome \ + test/hcl/view/screen/browser_interop_web_test.dart diff --git a/tool/gh_actions/run_tests.sh b/tool/gh_actions/run_tests.sh index 8ec0d557ad..606ad7ab88 100755 --- a/tool/gh_actions/run_tests.sh +++ b/tool/gh_actions/run_tests.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2022-2024 Intel Corporation +# Copyright (C) 2022-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # run_tests.sh @@ -11,4 +11,7 @@ set -euo pipefail -dart test +# Match the Dart SDK used by Flutter to resolve this workspace, while invoking +# package:test directly for this package's pure-Dart tests. +flutter_bin_dir="$(dirname "$(readlink -f "$(command -v flutter)")")" +"$flutter_bin_dir/dart" run test:test diff --git a/tool/gh_codespaces/install_flutter.sh b/tool/gh_codespaces/install_flutter.sh index 1d32ef5784..44aca74c12 100755 --- a/tool/gh_codespaces/install_flutter.sh +++ b/tool/gh_codespaces/install_flutter.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023-2024 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # install_flutter.sh @@ -14,7 +14,10 @@ set -euo pipefail -wget -O /tmp/flutter_linux.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.16.9-stable.tar.xz +declare -r flutter_version='3.47.2' + +wget -O /tmp/flutter_linux.tar.xz \ + "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${flutter_version}-stable.tar.xz" cd /usr/local sudo tar -xf /tmp/flutter_linux.tar.xz echo 'export PATH="$PATH:/usr/local/flutter/bin"' >> ~/.bashrc diff --git a/tool/gh_codespaces/run_setup.sh b/tool/gh_codespaces/run_setup.sh index 70a564d62c..b3a6780e4e 100755 --- a/tool/gh_codespaces/run_setup.sh +++ b/tool/gh_codespaces/run_setup.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (C) 2023-2024 Intel Corporation +# Copyright (C) 2023-2026 Intel Corporation # SPDX-License-Identifier: BSD-3-Clause # # run_setup.sh @@ -11,20 +11,17 @@ set -euo pipefail -# Initialize submodules -git submodule update --init --recursive - # Install Dart SDK. tool/gh_codespaces/install_dart.sh -# Install Pub dependencies. -tool/gh_actions/install_dependencies.sh +# Install Flutter +tool/gh_codespaces/install_flutter.sh -# Install CAD Suite (includes yosys) -tool/gh_actions/install_opencadsuite.sh +# Make Flutter available to this setup process before resolving dependencies. +export PATH="/usr/local/flutter/bin:$PATH" -# Install D3 Schematic viewer -tool/gh_actions/install_d3_hwschematic.sh +# Install Pub workspace dependencies. +tool/gh_actions/install_dependencies.sh -# Install Flutter -tool/gh_codespaces/install_flutter.sh \ No newline at end of file +# Install CAD Suite (includes yosys) +tool/gh_actions/install_opencadsuite.sh \ No newline at end of file