diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 12e172c74b..88f9ff31d5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,6 +4,12 @@ { "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "24" + } + }, + "updateContentCommand": "tool/gh_codespaces/run_setup.sh", "customizations": { diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index 98d888fe97..0b6ff56de4 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -27,11 +27,20 @@ { "pattern": "^https://code\\.visualstudio\\.com/" }, + { + "pattern": "^https://marketplace\\.visualstudio\\.com/items\\?itemName=Dart-Code\\.dart-code$" + }, + { + "pattern": "^https://raw\\.githubusercontent\\.com/intel/rohd/refs/heads/badges/coverage/rohd_devtools_extension/main\\.svg$" + }, { "pattern": "^https://www.nandland.com" }, { "pattern":"^https://.*\\.fandom\\.com" + }, + { + "pattern": "^https://intel\\.github\\.io/rohd/rohd/" } ] } diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ae81ea8411..db04775f56 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -37,6 +37,22 @@ jobs: - name: Generate coverage and badge run: tool/generate_coverage.sh --generate-svg + - name: Save ROHD coverage badge + run: cp /tmp/coverage-badge.svg /tmp/rohd-coverage-badge.svg + + - name: Setup Flutter + uses: flutter-actions/setup-flutter@v2 + with: + channel: stable + version: 3.47.2 + + - name: Install DevTools dependencies + run: flutter pub get --directory=rohd_devtools_extension + + - name: Generate DevTools coverage and badge + working-directory: rohd_devtools_extension + run: ../tool/generate_coverage.sh --generate-svg + - name: Commit coverage badge run: | BRANCH_NAME="${{ github.ref_name }}" @@ -49,10 +65,11 @@ jobs: # Clean and copy badge git rm -rf . 2>/dev/null || true - mkdir -p coverage - cp /tmp/coverage-badge.svg "coverage/${BRANCH_NAME}.svg" + mkdir -p coverage/rohd_devtools_extension + cp /tmp/rohd-coverage-badge.svg "coverage/${BRANCH_NAME}.svg" + cp /tmp/coverage-badge.svg "coverage/rohd_devtools_extension/${BRANCH_NAME}.svg" - git add "coverage/${BRANCH_NAME}.svg" + git add "coverage/${BRANCH_NAME}.svg" "coverage/rohd_devtools_extension/${BRANCH_NAME}.svg" git diff --staged --quiet || git commit -m "Update coverage badge for ${BRANCH_NAME} [skip ci]" git push origin badges --force diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 10e20747c5..bf93c9c861 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -114,10 +114,6 @@ jobs: if: matrix.platform == 'vm' run: bash tool/gh_actions/install_gtkwave.sh - - name: Install software - GTKWave - if: matrix.platform == 'vm' - run: bash tool/gh_actions/install_gtkwave.sh - - name: Run project tests run: tool/gh_actions/run_tests.sh ${{ matrix.platform }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d02b969ae..8c1a5e3fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Major Features +- Added `TypedLogicArray` for multidimensional arrays with fixed typed hardware elements and associated semantic values, including typed snapshots and ports, traversal, cloning, net support, and synthesis. Added shaped `LogicValueArray` and codec-backed `TypedLogicValueArray` values with nested construction, packed `LogicValue` interoperability, and hardware-array assignment. Existing `LogicArray`, `LogicValueArray`, and concrete array port APIs remain available as specializations (). - Added `LogicEnum` hardware signals with explicit enum encodings, mapped member assignments, driveable cloning, and SystemVerilog enum generation (). - Added compact FST waveform capture alongside VCD, with block-based reading and waveform query APIs for incremental access (). - Added SystemC generation, runtime support, and `SystemCService` for generating and writing module artifacts. Generated SystemC uses a two-state representation and does not preserve X/Z semantics (). @@ -27,7 +28,12 @@ ### Improvements +- Improved generated synthesis wiring across module boundaries and driver-name selection (). +- Same-width `Logic.zeroExtend` now returns the original signal instead of creating a redundant extension (). +- Migrated `rohd_source_navigator` into its package in preparation for independent publication (). - Netlist JSON format is now `0.1.0`, preserving enum types and encodings on retained netnames using Yosys-style attributes, including constant-driven enum uses (). +- Refined the typed-array hierarchy so `LogicArray` directly specializes `TypedLogicArray`, and strengthened custom `TypedLogicArray` cloning to preserve runtime types, codecs, unpacked dimensions, and constructor-only naming prefixes without exposing them as public axis metadata (). +- Expanded `TypedLogicArray` support for recursively nested typed and ordinary arrays, structures containing array fields, mixed packed/unpacked dimensions, structured net and inout ports, and recursive netlist type metadata. Fixed generated SystemVerilog for array-valued elements to use packed offsets consistent with declarations and module-port connections, avoiding invalid chained selections in Icarus and Verilator (). - Added configurable waveform timescales, recording windows, and output overwrite policies (). - Added opt-in `DebugSession` setup for source tracing, netlist and FLC services, and live waveform recording (). - Expanded reusable `rohd_devtools_widgets` components for hierarchy browsing, signal formatting, bit expansion, overlays, help, capture, and cross-probing (). @@ -35,6 +41,7 @@ ### Bug Fixes +- Fixed a netlist pass that could drop connections to module output ports (). - Fixed `LogicValue.ofRadixString` round trips with empty separators and separators containing regular-expression metacharacters (). ## 0.6.11 diff --git a/README.md b/README.md index aeee83e1ba..97543d0419 100644 --- a/README.md +++ b/README.md @@ -38,28 +38,32 @@ You can also open this repository in a GitHub Codespace to run the example in yo ## About ROHD - Full power of the modern **Dart language** for hardware design and verification -- Makes **validation collateral** simpler to develop and debug. The [ROHD Verification Framework](https://github.com/intel/rohd-vf) helps build well-structured testbenches. +- Can use the excellent pub.dev **package manager** and all the packages it has to offer - Develop **layers of abstraction** within a hardware design, making it more flexible and powerful -- Easy **IP integration** and **interfaces**; using an IP is as easy as an import. Reduces tedious, redundant, and error prone aspects of integration +- **Less verbose** than alternatives (fewer lines of code) +- Fewer bugs and lines of code means **shorter development schedule** +- Enables **higher quality** development - **Simple and fast build**, free of complex build systems and EDA vendor tools -- Can use the excellent pub.dev **package manager** and all the packages it has to offer -- Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform capture service** to .vcd or .fst file format +- Excellent, simple, fast **unit-testing** framework +- Simulate with **various abstraction levels of models** from architectural, to functional, to cycle-accurate, to RTL levels in the same language and environment +- Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. +- Use **modern IDEs** like Visual Studio Code, with excellent static analysis, fast autocomplete, built-in debugger, linting, git integration, extensions, and much more +- Easy **IP integration** and **interfaces**; using an IP is as easy as an import. Reduces tedious, redundant, and error prone aspects of integration - Conversion of modules to human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents -- Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. -- Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. -- Excellent, simple, fast **unit-testing** framework -- **Less verbose** than alternatives (fewer lines of code) -- Enables **higher quality** development - Replaces hacky perl/python scripting for automation with powerful **native control of design generation** -- Fewer bugs and lines of code means **shorter development schedule** -- Support for **cosimulation with verilog modules** (via [ROHD Cosim](https://github.com/intel/rohd-cosim)) and **instantiation of verilog modules** in generated SystemVerilog code -- Use **modern IDEs** like Visual Studio Code, with excellent static analysis, fast autocomplete, built-in debugger, linting, git integration, extensions, and much more -- Simulate with **various abstraction levels of models** from architectural, to functional, to cycle-accurate, to RTL levels in the same language and environment. -ROHD is *not* a new language, it is *not* a hardware description language (HDL), and it is *not* a version of High-Level Synthesis (HLS). ROHD can be classified as a generator framework. +- The **ROHD Framework** is a complete design and verification framework: + - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform capture service** to .vcd or .fst file format + - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components + - Makes **validation collateral** simpler to develop and debug. The [ROHD Verification Framework](https://github.com/intel/rohd-vf) helps build well-structured testbenches + - Powerful capabilities like the [ROHD Schematic Viewer](https://github.com/rohd-schematic-viewer) for high-fidelity rendering of ROHD designs as well as [ROHD Wave Viewer](https://github.com/rohd-wave-viewer) for viewing simulation waveforms even for complex ROHD structures + - Debug your design with [ROHD Live Debugger](https://github.com/intel/rohd/rohd_devtools_extension), an integrated debugger with cross-probing into ROHD source and synthesized RTL output from schematics rendering and waveform display all connected to live RTL simulation + - Support for **cosimulation with verilog modules** (via [ROHD Cosim](https://github.com/intel/rohd-cosim)) and **instantiation of verilog modules** in generated SystemVerilog code + +ROHD is *not* a new language, it is *not* a hardware description language (HDL), and it is *not* a version of High-Level Synthesis (HLS). ROHD can be classified as a generator framework, extending a powerful software language into the hardware domain and providing a full suite of tools and capabilities to accelerate hardware design and validation. -You can think of this project as an attempt to *replace* SystemVerilog and related build systems as the front-end methodology of choice in the industry. +You can think of this project as an attempt to *replace* SystemVerilog and related design systems as the front-end methodology of choice in the industry. One of ROHD's goals is to help grow an open-source community around reusable hardware designs and verification components. diff --git a/doc/releases.md b/doc/releases.md index a44cbfda63..ccd5e18bbe 100644 --- a/doc/releases.md +++ b/doc/releases.md @@ -167,10 +167,13 @@ It runs `pub get` and `pub downgrade`, each followed by fatal-info analysis of downgraded resolution. Required dependencies must already be available on pub.dev; there is no fallback to local packages. Command failures stop CI and preparation. Pana's report is printed for review, and package scoring findings such as missing -examples or newer dependency major versions block CI and preparation, matching -the root package's policy. The runner passes `--exit-code-threshold 0` for every -sub-package, requiring a full score. The sole temporary exception is -`rohd_source_navigator`, which uses threshold `10` while its canonical +examples or newer dependency major versions count against the CI and preparation +score gate. Pana's `--exit-code-threshold` is the number of +points that may be lost from its 160-point maximum: `0` requires a full score. +`rohd_devtools_widgets` permits up to 10 points lost (a minimum score of 150) +while newer `cupertino_ui` and `material_ui` releases require a Dart SDK +upgrade and accompanying formatter changes. `rohd_source_navigator` also uses +threshold `10` temporarily while its canonical `https://github.com/intel/rohd/tree/main/packages/rohd_source_navigator` repository URL cannot be validated before the path exists on upstream `main`; remove the exception when [#718](https://github.com/intel/rohd/pull/718) merges. diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 5650f48c43..889db3b827 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -1,11 +1,16 @@ --- title: "Logic Arrays" permalink: /docs/logic-arrays/ -last_modified_at: 2022-6-5 +last_modified_at: 2026-09-16 toc: true --- -A [`LogicArray`](https://intel.github.io/rohd/rohd/LogicArray-class.html) is a type of `LogicStructure` that mirrors multi-dimensional arrays in hardware languages like SystemVerilog. In ROHD, the `LogicArray` type inherits a lot of functionality from `LogicStructure`, so it can behave like a `Logic` where it makes sense or be individually referenced in other places. +Use [`LogicArray`](https://intel.github.io/rohd/rohd/LogicArray-class.html) +for multidimensional arrays of ordinary `Logic`. Use +[`TypedLogicArray`](https://intel.github.io/rohd/rohd/TypedLogicArray-class.html) +when each array position has a specialized hardware type and semantic value +type. Both are `LogicStructure`s, so they can be indexed as arrays while still +participating in ordinary packed `Logic` assignments and operations. `LogicArray`s can be constructed easily using the constructor: @@ -22,6 +27,145 @@ LogicArray([5, 5, 5], 128); As long as the total width of a `LogicArray` and another type of `Logic` (including `Logic`, `LogicStructure`, and another `LogicArray`) are the same, assignments and bitwise operations will work in per-element order. This means you can assign two `LogicArray`s of different dimensions to each other as long as the total width matches. +## Typed arrays + +Use `TypedLogicArray` when every array position has the same +specialized hardware type and associated semantic value type. `LogicArray` is +the ordinary `TypedLogicArray` specialization. + +For example, these sample hardware and value types preserve named fields in hardware while exposing typed snapshots: + +```dart +class Sample extends LogicStructure { + final Logic data; + final Logic valid; + + factory Sample({String? name}) => Sample._( + Logic(name: 'data', width: 8), + Logic(name: 'valid'), + name: name ?? 'sample', + ); + + Sample._(this.data, this.valid, {required String name}) + : super([data, valid], name: name); + + @override + Sample clone({String? name}) => Sample(name: name ?? this.name); +} + +class SampleValue { + final LogicValue value; + + SampleValue(this.value); +} + +SampleValue decodeSample(LogicValue value) => SampleValue(value); +LogicValue encodeSample(SampleValue value) => value.value; + +const sampleCodec = LogicValueCodec( + decode: decodeSample, + encode: encodeSample, +); + +final samples = TypedLogicArray( + [2, 3], + Sample.new, + valueCodec: sampleCodec, +); + +final bottomRightData = samples.at([1, 2]).data; +final TypedLogicValueArray currentSamples = samples.value; +``` + +The element builder must consistently produce the configured type, width, +ordered structure, and net kind. Every element must be entirely variable or +entirely net; a structure cannot mix `Logic` and `LogicNet` leaves. Use +`elementCompatibility` when equal width is not enough to establish compatible +representations. + +`valueCodec` is optional only when `TValue` is `LogicValue`. A custom codec +should decode every four-state value that can appear in its hardware. +`dimensionNames` controls child naming during construction and cloning; it is +not public axis metadata. See the +[`TypedLogicArray` API documentation](https://intel.github.io/rohd/rohd/TypedLogicArray-class.html) +for the complete constructor and cloning contracts. + +Hardware shape changes should use ordinary construction and connection APIs rather than specialized typed-array adapters. Construct a new `TypedLogicArray` with the desired dimensions and builder, then connect it with `gets`/`<=` when row-major assignment is sufficient. For a transpose, connect corresponding coordinates explicitly with `indexedElements` and `at`; whole-array assignment does not infer a permutation. This keeps construction disconnected and leaves driver ownership with the caller. + +### Traversal boundaries + +These APIs intentionally stop at different boundaries: + +- `elements` contains the immediate children of the outermost structure or array level. +- `TypedLogicArray.arrayElements` traverses exactly the dimensions declared by that array and then stops at `TLogic`. It is an unmodifiable `List`. +- `indexedElements` pairs those same typed array elements with their row-major multidimensional indices. +- `at(indices)` returns one typed array element. +- `leafElements` recursively traverses every nested array and structure until it reaches non-structure signals. + +For example, a `[2, 3]` array of two-field `Sample`s has six `arrayElements` and twelve recursive `leafElements`. A `[2]` array whose elements are `[3]` arrays has two `arrayElements` and six recursive leaves. An eight-bit `Logic` is one leaf, not eight. + +A `TypedLogicArray` element can be a `LogicStructure`, but it must be driveable. +Direct `Const` elements and structures containing a `Const` are rejected. +Nested arrays are supported for ROHD construction, packed assignment, and +`arrayElements`, `at`, and `indexedElements` traversal. + +At a generated SystemVerilog boundary, a nested array-valued element may occupy +packed bits within its containing element rather than introduce another visible +array dimension. Its row-major value and structure-field order are preserved; +use `at`, `indexedElements`, and named fields in ROHD instead of depending on +the textual shape of generated selections. + +Some simulators do not accept unpacked `inout` array ports. Prefer packed +outer dimensions for portable bidirectional interfaces. + +Zero-sized arrays are supported for simulation and value operations. +SystemVerilog generation rejects them because the language has no portable +zero-width array declaration. + +See the [`TypedLogicArray` API documentation](https://intel.github.io/rohd/rohd/TypedLogicArray-class.html) +for subclassing and cloning details. + +## Value-domain arrays + +Use `LogicValueArray` for fixed-width array data outside the hardware graph. +Nested lists are the ordinary construction form and describe the shape +directly. Flat row-major values use an explicitly named `fromFlat` constructor +with shape metadata: + +```dart +final values = LogicValueArray.fromInts( + [ + [1, 2, 3], + [4, 5, 6], + ], + elementWidth: 8, +); +final sameValues = LogicValueArray.fromFlatInts( + [2, 3], + [1, 2, 3, 4, 5, 6], + elementWidth: 8, +); +final emptyRows = + LogicValueArray.fromFlat([2, 0], const [], elementWidth: 8); + +final transposed = values.transpose2D(); // Dimensions: [3, 2] +``` + +Nested lists infer shape and reject ragged or inconsistent input. Use `fromFlat` +for row-major data, empty arrays, or list-valued semantic elements whose +nesting would be ambiguous. Empty input needs explicit dimensions and element +width. `stack` requires arrays to share the same codec instance. + +Decoded semantic values are exposed by reference; mutating a mutable value does +not update its packed bits, so immutable semantic values are usually the +simplest choice. Both value-array types retain packed `LogicValue` behavior: +same-width assignment is based on packed bits, not shape. See the +[`LogicValueArray`](https://intel.github.io/rohd/rohd/LogicValueArray-class.html) +and +[`TypedLogicValueArray`](https://intel.github.io/rohd/rohd/TypedLogicValueArray-class.html) +API documentation for codec normalization, shape operations, packed-value +semantics, and snapshots. + ## Unpacked arrays In SystemVerilog, there is a concept of "packed" vs. "unpacked" arrays which have different use cases and capabilities. In ROHD, all arrays act the same and you get the best of both worlds. You can indicate when constructing a `LogicArray` that some number of the dimensions should be "unpacked" as a hint to `Synthesizer`s. Marking an array with a non-zero `numUnpackedDimensions`, for example, will make that many of the dimensions "unpacked" in generated SystemVerilog signal declarations. @@ -43,9 +187,11 @@ You can declare ports of `Module`s as being arrays (including with some dimensio Array ports in generated SystemVerilog will match dimensions (including unpacked) as specified when the port is created. +Use the existing `addTypedInput`, `addTypedOutput`, and `addTypedInOut` methods for `TypedLogicArray` ports. Their generic `LogicType` preserves the complete array subtype, including its hardware element type, semantic value type, codec, net kind, dimensions, and unpacked-dimension configuration. This allows the module to access fields such as `samples.at([1, 2]).data` directly. The established `addInputArray`, `addOutputArray`, and `addInOutArray` APIs remain the concrete `LogicArray` helpers. + ## Elements of arrays -To iterate through or access elements of a `LogicArray` (or bits of a simple `Logic`), use [`elements`](https://intel.github.io/rohd/rohd/Logic/elements.html). Using the normal `[n]` accessors will return the `n`th bit regardless for `LogicArray` and `Logic` to maintain API consistency. +Use `elements` to inspect immediate children, `arrayElements` or `indexedElements` to traverse declared array positions, and `leafElements` only when fully recursive traversal is intended. The normal `[n]` operator selects the `n`th packed bit for both `LogicArray` and `Logic`; use `at` for multidimensional typed element indexing. ## Index-based Selection in an Array diff --git a/lib/rohd.dart b/lib/rohd.dart index e0bbd3c093..70b66603c4 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -19,7 +19,7 @@ export 'src/interfaces/interfaces.dart'; export 'src/module.dart'; export 'src/modules/modules.dart'; export 'src/selection.dart'; -export 'src/signals/signals.dart' hide LogicDef; +export 'src/signals/signals.dart' hide BaseLogicArray, LogicDef; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; diff --git a/lib/src/interfaces/interface.dart b/lib/src/interfaces/interface.dart index 4791625a15..6e75f818dc 100644 --- a/lib/src/interfaces/interface.dart +++ b/lib/src/interfaces/interface.dart @@ -10,6 +10,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; /// Represents a logical interface to a [Module]. /// @@ -85,35 +86,45 @@ class Interface { if (inputTags != null) { for (final port in getPorts(inputTags).values) { port <= - (port is LogicArray - ? module.addInputArray( - uniquify(port.name), - srcInterface.port(port.name), - dimensions: port.dimensions, - elementWidth: port.elementWidth, - numUnpackedDimensions: port.numUnpackedDimensions, - ) - : module.addInput( - uniquify(port.name), - srcInterface.port(port.name), - width: port.width, - )); + switch (port) { + LogicArray() => module.addInputArray( + uniquify(port.name), + srcInterface.port(port.name), + dimensions: port.dimensions, + elementWidth: port.elementWidth, + numUnpackedDimensions: port.numUnpackedDimensions, + ), + BaseLogicArray() => module.addTypedInput( + uniquify(port.name), + srcInterface.port(port.name) as BaseLogicArray, + ), + _ => module.addInput( + uniquify(port.name), + srcInterface.port(port.name), + width: port.width, + ), + }; } } if (outputTags != null) { for (final port in getPorts(outputTags).values) { - final output = (port is LogicArray - ? module.addOutputArray( - uniquify(port.name), - dimensions: port.dimensions, - elementWidth: port.elementWidth, - numUnpackedDimensions: port.numUnpackedDimensions, - ) - : module.addOutput( - uniquify(port.name), - width: port.width, - )); + final output = switch (port) { + LogicArray() => module.addOutputArray( + uniquify(port.name), + dimensions: port.dimensions, + elementWidth: port.elementWidth, + numUnpackedDimensions: port.numUnpackedDimensions, + ), + BaseLogicArray() => module.addTypedOutput( + uniquify(port.name), + port.clone, + ), + _ => module.addOutput( + uniquify(port.name), + width: port.width, + ), + }; output <= port; srcInterface.port(port.name) <= output; } @@ -121,7 +132,7 @@ class Interface { if (inOutTags != null) { for (final port in getPorts(inOutTags).values) { - if (port is LogicArray) { + if (port is BaseLogicArray) { if (!port.isNet) { throw PortTypeException( port, 'LogicArray nets must be used for inOut array ports.'); @@ -132,19 +143,24 @@ class Interface { } port <= - (port is LogicArray - ? module.addInOutArray( - uniquify(port.name), - srcInterface.port(port.name), - dimensions: port.dimensions, - elementWidth: port.elementWidth, - numUnpackedDimensions: port.numUnpackedDimensions, - ) - : module.addInOut( - uniquify(port.name), - srcInterface.port(port.name), - width: port.width, - )); + switch (port) { + LogicArray() => module.addInOutArray( + uniquify(port.name), + srcInterface.port(port.name), + dimensions: port.dimensions, + elementWidth: port.elementWidth, + numUnpackedDimensions: port.numUnpackedDimensions, + ), + BaseLogicArray() => module.addTypedInOut( + uniquify(port.name), + srcInterface.port(port.name) as BaseLogicArray, + ), + _ => module.addInOut( + uniquify(port.name), + srcInterface.port(port.name), + width: port.width, + ), + }; } } } diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index 3edfacc235..2bbf8cada6 100644 --- a/lib/src/interfaces/pair_interface.dart +++ b/lib/src/interfaces/pair_interface.dart @@ -11,6 +11,7 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; /// A direction for signals between a pair of components. @@ -108,6 +109,8 @@ class PairInterface extends Interface { p.numUnpackedDimensions) : LogicArray.port(name, p.dimensions, p.elementWidth, p.numUnpackedDimensions); + case BaseLogicArray(): + return p.clone(); case LogicNet(): return LogicNet.port(name, p.width); default: diff --git a/lib/src/module.dart b/lib/src/module.dart index 7da13458e1..bc2825e9c5 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,6 +13,7 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -694,12 +695,15 @@ abstract class Module { /// /// This is a good way to construct [input]s that have matching widths or /// dimensions to their [source] signal, or to make a [LogicStructure] an - /// [input]. You can use this on a [Logic], [LogicArray], or [LogicStructure]. + /// [input]. You can use this on a [Logic], [LogicArray], + /// [TypedLogicArray], or [LogicStructure]. A [TypedLogicArray] retains both + /// its hardware element type and semantic value type. /// /// The [source] cannot be or contain any [LogicNet]s. If [source] is a - /// [Const] (or is a [LogicStructure] that includes a [Const]), the - /// [LogicType] must be set to [Logic], since [Const]s cannot be driven and - /// are not suitable as ports. + /// [Const], the [LogicType] must be set to [Logic], since [Const]s cannot be + /// driven and are not suitable as ports. A [LogicStructure] containing + /// [Const]s is accepted when its [Logic.clone] implementation creates a + /// matching structure with only driveable elements. /// /// The return value is the same as what is returned by [input] and should /// only be used within this [Module]. The provided [source] is accessible via @@ -708,13 +712,26 @@ abstract class Module { String name, LogicType source) { _checkForSafePortName(name); - source = _validateType(source, isOutput: false, name: name); + final hasTypedStructuredConstants = + LogicType != Logic && source is LogicStructure && source.hasConsts; + if (!hasTypedStructuredConstants) { + source = _validateType(source, isOutput: false, name: name); + } if (source.isNet || (source is LogicStructure && source.hasNets)) { throw PortTypeException(source, 'Typed inputs cannot have nets in them.'); } - final inPort = (source.clone(name: name) as LogicType)..gets(source); + final cloned = source.clone(name: name); + if (cloned is! LogicType) { + throw PortTypeException(source, + 'The `clone` method did not preserve the requested port type.'); + } + final inPort = _validateType( + cloned, + isOutput: true, + name: name, + )..gets(source); if (inPort.name != name) { throw PortTypeException.forIntendedName(name, @@ -755,7 +772,7 @@ abstract class Module { _inOutDrivers.add(source); // we need to properly detect all inout sources, even for arrays - if (source.isArrayMember || source is LogicArray) { + if (source.isArrayMember || source is BaseLogicArray) { final sourceElems = TraverseableCollection()..add(source); for (var i = 0; i < sourceElems.length; i++) { final sei = sourceElems[i]; @@ -765,7 +782,7 @@ abstract class Module { sourceElems.add(sei.parentStructure!); } - if (sei is LogicArray) { + if (sei is BaseLogicArray) { sourceElems.addAll(sei.elements); } } @@ -798,7 +815,9 @@ abstract class Module { /// /// This is a good way to construct [inOut]s that have matching widths or /// dimensions to their [source] signal, or to make a [LogicStructure] an - /// [inOut]. You can use this on a [Logic], [LogicArray], or [LogicStructure]. + /// [inOut]. You can use this on a [Logic], [LogicArray], + /// [TypedLogicArray], or [LogicStructure]. A [TypedLogicArray] retains both + /// its hardware element type and semantic value type. /// /// The [source] must be or exclusively contain [LogicNet]s. If [source] is a /// [Const] (or is a [LogicStructure] that includes a [Const]), the @@ -947,7 +966,9 @@ abstract class Module { /// /// This is a good way to construct [output]s that have matching widths or /// dimensions to another signal, or to make a [LogicStructure] an [output]. - /// You can use this on a [Logic], [LogicArray], or [LogicStructure]. + /// You can use this on a [Logic], [LogicArray], [TypedLogicArray], or + /// [LogicStructure]. A [TypedLogicArray] retains both its hardware element + /// type and semantic value type. /// /// The [logicGenerator] cannot create ports that are or contain any /// [LogicNet]s in them. If a [Const] is generated (or included in a @@ -1040,7 +1061,7 @@ abstract class Module { sourceElems.add(sei.parentStructure!); } - if (sei is LogicArray) { + if (sei is BaseLogicArray) { sourceElems.addAll(sei.elements); } } diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index bf7c6f4a1e..7fbaee7bd9 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -11,6 +11,7 @@ import 'dart:math' show max; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; /// A [Module] which gives access to a subset range of signals of the input. /// @@ -433,7 +434,7 @@ ${inputLines.join('\n')} var current = logic; while (current?.parentStructure != null) { final parentStructure = current!.parentStructure!; - if (parentStructure is LogicArray && + if (parentStructure is BaseLogicArray && parentStructure.numUnpackedDimensions > 0) { return true; } diff --git a/lib/src/signals/base_logic_array.dart b/lib/src/signals/base_logic_array.dart new file mode 100644 index 0000000000..155a04d48e --- /dev/null +++ b/lib/src/signals/base_logic_array.dart @@ -0,0 +1,237 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// base_logic_array.dart +// Definition of the shared base for logic arrays. +// +// 2023 May 1 +// Author: Max Korbel + +part of 'signals.dart'; + +/// Shared structural base for multidimensional logic arrays. +/// +/// Arrays are [LogicStructure]s rather than direct subclasses of [Logic]: +/// their signal identity is the hierarchy of child signals, while the +/// inherited [Logic] behavior is provided by [LogicStructure] through its +/// packed representation. This lets arrays participate in ordinary structure +/// traversal and assignment while adding dimensions and array-boundary +/// traversal. Construct [LogicArray] for ordinary logic values or +/// [TypedLogicArray] for hardware elements with an associated semantic value +/// type. +@internal +abstract class BaseLogicArray extends LogicStructure { + /// The number of elements at each level of the array, starting from the most + /// significant outermost level. + /// + /// For example `[3, 2]` would indicate a 2-dimensional array, where it is + /// an array with 3 arrays, each containing 2 arrays. + final List dimensions; + + /// The width of leaf elements in this array. + /// + /// An empty [LogicArray] retains its historical zero element width. + /// An empty [TypedLogicArray] retains the width of its constructed prototype. + final int elementWidth; + + /// Cached view of the configured array-element boundary. + late final List _arrayElements = + UnmodifiableListView(_calculateArrayElements()); + + /// Elements reached after traversing exactly [dimensions] array levels. + /// + /// Unlike [LogicStructure.leafElements], traversal stops at the configured + /// array leaf. This distinction matters when an array leaf is itself a + /// [LogicStructure] or another array. + List get arrayElements => _arrayElements; + + @override + final Naming naming; + + @override + String toString() => [ + '$runtimeType($dimensions, $elementWidth): $name', + if (isArrayMember) 'index $arrayIndex of ($parentStructure)', + if (isNet) '[Net]' + ].join(', '); + + /// The number of [dimensions] which should be treated as "unpacked", starting + /// from the outermost (first) elements of [dimensions]. + /// + /// This has no functional impact on simulation or behavior. It is only used + /// as a hint for [Synthesizer]s. + final int numUnpackedDimensions; + + @override + final bool isNet; + + /// Creates an array from pre-built [elements]. + /// + /// This constructor supports subclasses whose leaf dimension contains a + /// specialized [Logic] or [LogicStructure]. For arrays with more than one + /// dimension, [elements] must be arrays matching the remaining + /// dimensions. For a one-dimensional array, each element must have + /// [elementWidth] bits. + @protected + BaseLogicArray.structured(List elements, + {required List dimensions, + required this.elementWidth, + String? name, + this.numUnpackedDimensions = 0, + Naming? naming, + bool? isNet}) + : dimensions = List.unmodifiable(dimensions), + isNet = isNet ?? elements.every((element) => element.isNet), + naming = Naming.chooseNaming(name, naming), + super(elements, + name: Naming.chooseName(name, naming, nullStarter: 'a')) { + if (dimensions.isEmpty) { + throw LogicConstructionException( + 'Arrays must have at least 1 dimension.'); + } + if (dimensions.any((dimension) => dimension < 0)) { + throw LogicConstructionException( + 'Array dimensions must be non-negative.'); + } + if (elementWidth < 0) { + throw LogicConstructionException( + 'Array element width must be non-negative.'); + } + if (numUnpackedDimensions < 0 || + numUnpackedDimensions > dimensions.length) { + throw LogicConstructionException( + 'The number of unpacked dimensions must be between 0 and the array ' + 'rank.'); + } + if (elements.length != dimensions.first) { + throw LogicConstructionException( + 'Array elements must match the first dimension.'); + } + + if (dimensions.length == 1) { + if (elements.any((element) => element.width != elementWidth)) { + throw LogicConstructionException( + 'Array leaves must match elementWidth.'); + } + } else { + final childDimensions = dimensions.sublist(1); + if (elements.any((element) => + element is! BaseLogicArray || + !_sameDimensions(element.dimensions, childDimensions) || + element.elementWidth != elementWidth)) { + throw LogicConstructionException( + 'Child arrays must match the remaining dimensions and width.'); + } + } + + for (final (index, element) in elements.indexed) { + element._arrayIndex = index; + } + } + + /// Creates an array with the same shape and element representation. + @override + BaseLogicArray clone({String? name}); + + /// Creates and connects an array with the requested [name]. + @override + BaseLogicArray named(String name, {Naming? naming}); + + /// Internal factory constructor. + /// + /// Creates an array with specified [dimensions] and [elementWidth] named + /// [name]. + /// + /// Setting the [numUnpackedDimensions] gives a hint to [Synthesizer]s about + /// the intent for declaration of signals. By default, all dimensions are + /// packed, but if the value is set to more than `0`, then the outer-most + /// dimensions (first in [dimensions]) will become unpacked. It must be less + /// than or equal to the length of [dimensions]. Modifying it will have no + /// impact on simulation functionality or behavior. In SystemVerilog, there + /// are some differences in access patterns for packed vs. unpacked arrays. + /// + /// The [logicBuilder] and [logicArrayBuilder] functions should generate + /// proper types of [Logic]s as elements for the array. + factory BaseLogicArray._factory(List dimensions, int elementWidth, + {required String? name, + required int numUnpackedDimensions, + required Naming? naming, + required bool isNet, + required Logic Function({int width, Naming naming, String name}) + logicBuilder, + required BaseLogicArray Function(List nextDimensions, int width, + {int numUnpackedDimensions, String name}) + logicArrayBuilder, + required BaseLogicArray Function(List elements, + {required List dimensions, + required int elementWidth, + required int numUnpackedDimensions, + required String name, + required Naming naming, + required bool isNet}) + arrayBuilder}) { + if (dimensions.isEmpty) { + throw LogicConstructionException( + 'Arrays must have at least 1 dimension.'); + } + if (dimensions.any((dimension) => dimension < 0)) { + throw LogicConstructionException( + 'Array dimensions must be non-negative.'); + } + if (elementWidth < 0) { + throw LogicConstructionException( + 'Array element width must be non-negative.'); + } + if (numUnpackedDimensions < 0 || + numUnpackedDimensions > dimensions.length) { + throw LogicConstructionException( + 'The number of unpacked dimensions must be between 0 and the array ' + 'rank.'); + } + + final nextDimensions = dimensions.length == 1 + ? null + : List.unmodifiable(dimensions.getRange(1, dimensions.length)); + + if (elementWidth != 0 && dimensions.reduce((a, b) => a * b) == 0) { + elementWidth = 0; + } + + final newNaming = Naming.chooseNaming(name, naming); + final newName = Naming.chooseName(name, naming, nullStarter: 'a'); + naming = newNaming; + name = newName; + + final elements = List.generate( + dimensions.first, + (index) => (dimensions.length == 1 + ? logicBuilder( + width: elementWidth, + naming: Naming.renameable, + name: '${name}_$index') + : logicArrayBuilder(nextDimensions!, elementWidth, + numUnpackedDimensions: max(0, numUnpackedDimensions - 1), + name: '${name}_$index')) + .._arrayIndex = index, + growable: false); + + return arrayBuilder(elements, + dimensions: List.unmodifiable(dimensions), + elementWidth: elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + name: name, + naming: naming, + isNet: isNet); + } + + List _calculateArrayElements() => dimensions.length == 1 + ? elements + : elements + .cast() + .expand((element) => element.arrayElements) + .toList(growable: false); +} + +bool _sameDimensions(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index b685f5af79..9daf72d947 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -133,8 +133,13 @@ class Logic { LogicStructure? get parentStructure => _parentStructure; LogicStructure? _parentStructure; - /// True if this is a member of a [LogicArray]. - bool get isArrayMember => parentStructure is LogicArray; + /// Whether the immediate [parentStructure] is a [BaseLogicArray]. + /// + /// This tests direct membership, not recursive array containment. For + /// example, a structure configured as an element of a [TypedLogicArray] has + /// `isArrayMember == true`, while a field inside that structure has + /// `isArrayMember == false` even though the field has an array ancestor. + bool get isArrayMember => parentStructure is BaseLogicArray; /// Returns the name relative to the [parentStructure]-defined hierarchy, if /// one exists. Otherwise, this is the same as [name]. @@ -143,7 +148,7 @@ class Logic { /// [LogicArray] or [LogicStructure]. String get structureName { if (parentStructure != null) { - if (parentStructure is LogicArray) { + if (parentStructure is BaseLogicArray) { return '${parentStructure!.structureName}[${arrayIndex!}]'; } else { return '${parentStructure!.structureName}.$name'; @@ -153,10 +158,12 @@ class Logic { } } - /// If this is a part of a [LogicArray], the index within that array. - /// Othwerise, returns `null`. + /// The index within the immediate parent array, if there is one. /// - /// If [isArrayMember] is true, this will be non-`null`. + /// This is non-`null` exactly when [isArrayMember] is true, including for + /// elements of a [TypedLogicArray]. A field whose immediate parent is a + /// non-array [LogicStructure] has no [arrayIndex], even when that structure + /// has an array ancestor. int? get arrayIndex => _arrayIndex; int? _arrayIndex; diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 6e7c9bd34f..367f0dfa55 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // logic_array.dart @@ -9,226 +9,144 @@ part of 'signals.dart'; -/// Represents a multi-dimensional array structure of independent [Logic]s. -class LogicArray extends LogicStructure { - /// The number of elements at each level of the array, starting from the most - /// significant outermost level. - /// - /// For example `[3, 2]` would indicate a 2-dimensional array, where it is - /// an array with 3 arrays, each containing 2 arrays. - final List dimensions; - - /// The width of leaf elements in this array. - /// - /// If the array has no leaf elements and/or the [width] is 0, then the - /// [elementWidth] is always 0. - final int elementWidth; - - @override - final Naming naming; - - @override - String toString() => [ - 'LogicArray($dimensions, $elementWidth): $name', - if (isArrayMember) 'index $arrayIndex of ($parentStructure)', - if (isNet) '[Net]' - ].join(', '); - - /// The number of [dimensions] which should be treated as "unpacked", starting - /// from the outermost (first) elements of [dimensions]. - /// - /// This has no functional impact on simulation or behavior. It is only used - /// as a hint for [Synthesizer]s. - final int numUnpackedDimensions; - +/// A multi-dimensional array structure of independent [Logic]s. +/// +/// This is the ordinary [Logic]-leaf specialization of [TypedLogicArray]. It +/// has the same construction, port, clone, and naming API as the historical +/// `LogicArray` type. +class LogicArray extends TypedLogicArray { /// Creates an array with specified [dimensions] and [elementWidth] named /// [name]. /// - /// Setting the [numUnpackedDimensions] gives a hint to [Synthesizer]s about - /// the intent for declaration of signals. By default, all dimensions are - /// packed, but if the value is set to more than `0`, then the outer-most - /// dimensions (first in [dimensions]) will become unpacked. It must be less - /// than or equal to the length of [dimensions]. Modifying it will have no - /// impact on simulation functionality or behavior. In SystemVerilog, there - /// are some differences in access patterns for packed vs. unpacked arrays. + /// [numUnpackedDimensions] is a [Synthesizer] hint. When it is greater than + /// zero, that many outermost dimensions are emitted as unpacked dimensions + /// in SystemVerilog. It has no effect on simulation behavior. + /// + /// Zero-sized arrays are supported for simulation and value operations, but + /// cannot be represented in generated SystemVerilog. factory LogicArray(List dimensions, int elementWidth, {String? name, int numUnpackedDimensions = 0, Naming? naming}) => - LogicArray._factory( - dimensions, - elementWidth, - name: name, - numUnpackedDimensions: numUnpackedDimensions, - naming: naming, - logicBuilder: Logic.new, - logicArrayBuilder: LogicArray.new, - isNet: false, - ); - - @override - final bool isNet; - - /// Creates an array of [LogicNet]s with specified [dimensions] and - /// [elementWidth] named [name]. + LogicArray._factory(dimensions, elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + naming: naming, + logicBuilder: Logic.new, + logicArrayBuilder: LogicArray.new, + isNet: false); + + /// Creates an array of [LogicNet]s with [dimensions] and [elementWidth] + /// named [name]. /// - /// Setting the [numUnpackedDimensions] gives a hint to [Synthesizer]s about - /// the intent for declaration of signals. By default, all dimensions are - /// packed, but if the value is set to more than `0`, then the outer-most - /// dimensions (first in [dimensions]) will become unpacked. It must be less - /// than or equal to the length of [dimensions]. Modifying it will have no - /// impact on simulation functionality or behavior. In SystemVerilog, there - /// are some differences in access patterns for packed vs. unpacked arrays. + /// [numUnpackedDimensions] has the same synthesis-only meaning as in + /// [LogicArray]. factory LogicArray.net(List dimensions, int elementWidth, {String? name, int numUnpackedDimensions = 0, Naming? naming}) => - LogicArray._factory( + LogicArray._factory(dimensions, elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + naming: naming, + logicBuilder: LogicNet.new, + logicArrayBuilder: LogicArray.net, + isNet: true); + + LogicArray._(List elements, + {required super.dimensions, + required super.elementWidth, + required super.numUnpackedDimensions, + required super.name, + required super.naming, + required bool isNet}) + : super._structured( + elements, + ({name}) => isNet + ? LogicNet(name: name, width: elementWidth) + : Logic(name: name, width: elementWidth), + valueCodec: LogicValueCodec.logicValue, + isNet: isNet, + ); + + factory LogicArray._factory(List dimensions, int elementWidth, + {required String? name, + required int numUnpackedDimensions, + required Naming? naming, + required bool isNet, + required Logic Function({int width, Naming naming, String name}) + logicBuilder, + required LogicArray Function(List nextDimensions, int width, + {int numUnpackedDimensions, String name}) + logicArrayBuilder}) => + BaseLogicArray._factory(dimensions, elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + naming: naming, + logicBuilder: logicBuilder, + logicArrayBuilder: logicArrayBuilder, + arrayBuilder: LogicArray._, + isNet: isNet) as LogicArray; + + @override + LogicValueArray get value => LogicValueArray.fromFlat( dimensions, - elementWidth, - name: name, - numUnpackedDimensions: numUnpackedDimensions, - naming: naming, - logicBuilder: LogicNet.new, - logicArrayBuilder: LogicArray.net, - isNet: true, + arrayElements.map((element) => element.value), + elementWidth: elementWidth, ); - /// Internal factory constructor. - /// - /// Creates an array with specified [dimensions] and [elementWidth] named - /// [name]. - /// - /// Setting the [numUnpackedDimensions] gives a hint to [Synthesizer]s about - /// the intent for declaration of signals. By default, all dimensions are - /// packed, but if the value is set to more than `0`, then the outer-most - /// dimensions (first in [dimensions]) will become unpacked. It must be less - /// than or equal to the length of [dimensions]. Modifying it will have no - /// impact on simulation functionality or behavior. In SystemVerilog, there - /// are some differences in access patterns for packed vs. unpacked arrays. - /// - /// The [logicBuilder] and [logicArrayBuilder] functions should generate - /// proper types of [Logic]s as elements for the array. - factory LogicArray._factory( - List dimensions, - int elementWidth, { - required String? name, - required int numUnpackedDimensions, - required Naming? naming, - required bool isNet, - required Logic Function({ - int width, - Naming naming, - String name, - }) logicBuilder, - required LogicArray Function( - List nextDimensions, - int width, { - int numUnpackedDimensions, - String name, - }) logicArrayBuilder, - }) { - if (dimensions.isEmpty) { - throw LogicConstructionException( - 'Arrays must have at least 1 dimension.'); - } - - if (numUnpackedDimensions > dimensions.length) { - throw LogicConstructionException( - 'Cannot unpack more than all of the dimensions.'); - } - - // calculate the next layer's dimensions - final nextDimensions = dimensions.length == 1 - ? null - : List.unmodifiable(dimensions.getRange(1, dimensions.length)); - - // if the total width will eventually be 0, then force element width to 0 - if (elementWidth != 0 && dimensions.reduce((a, b) => a * b) == 0) { - elementWidth = 0; - } + @override + LogicValueArray? get previousValue => + arrayElements.any((element) => element.previousValue == null) + ? null + : LogicValueArray.fromFlat( + dimensions, + arrayElements.map((element) => element.previousValue!), + elementWidth: elementWidth, + ); - // choose name and naming before creating (and naming) elements - final newNaming = Naming.chooseNaming(name, naming); - final newName = Naming.chooseName(name, naming, nullStarter: 'a'); - naming = newNaming; - name = newName; + @override - return LogicArray._( - List.generate( - dimensions.first, - (index) => (dimensions.length == 1 - ? logicBuilder( - width: elementWidth, - naming: Naming.renameable, - name: '${name}_$index', - ) - : logicArrayBuilder( - nextDimensions!, - elementWidth, - numUnpackedDimensions: max(0, numUnpackedDimensions - 1), - name: '${name}_$index', - )) - .._arrayIndex = index, - growable: false), - dimensions: List.unmodifiable(dimensions), - elementWidth: elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - name: name, - naming: naming, - isNet: isNet, - ); - } + /// Creates a [LogicArray] with the same dimensions, element width, unpacked + /// dimensions, net type, and name as this array unless [name] is provided. + LogicArray clone({String? name}) => + LogicArray._factory(dimensions, elementWidth, + name: name ?? this.name, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: naming, + newNaming: null), + logicBuilder: isNet ? LogicNet.new : Logic.new, + logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new, + isNet: isNet); @override - LogicArray _clone({String? name, Naming? naming}) => LogicArray._factory( - dimensions, - elementWidth, - name: name ?? this.name, - numUnpackedDimensions: numUnpackedDimensions, - naming: Naming.chooseCloneNaming( - originalName: this.name, - newName: name, - originalNaming: this.naming, - newNaming: naming), - logicBuilder: isNet ? LogicNet.new : Logic.new, - logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new, - isNet: isNet, - ); - /// Creates a new [LogicArray] which has the same [dimensions], - /// [elementWidth], [numUnpackedDimensions], and [isNet] as `this`. - /// - /// If no new [name] is specified, then it will also have the same name. - /// - /// It is expected that any implementation will override this in a way that - /// returns the same type as itself. - @override - @mustBeOverridden - LogicArray clone({String? name}) => _clone(name: name); + /// Clones this array with [name] and connects the clone to this array. + LogicArray named(String name, {Naming? naming}) => + LogicArray._factory(dimensions, elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: this.naming, + newNaming: naming), + logicBuilder: isNet ? LogicNet.new : Logic.new, + logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new, + isNet: isNet) + ..gets(this); - /// Makes a [clone] with the provided [name] and optionally [naming], then - /// assigns it to be driven by `this`. - /// - /// This is a useful utility for naming the result of some hardware - /// construction without separately declaring a new named signal and then - /// assigning. @override - LogicArray named(String name, {Naming? naming}) => - _clone(name: name, naming: naming)..gets(this); + Iterable get majorSlices { + if (dimensions.length < 2) { + throw StateError('majorSlices requires at least two dimensions.'); + } + return elements.cast(); + } - /// Private constructor for the factory [LogicArray] constructor. + /// Creates an array port with a convenient constructor signature. /// - /// The [name] and [naming] should have been identified before calling this. - LogicArray._( - super.elements, { - required this.dimensions, - required this.elementWidth, - required this.numUnpackedDimensions, - required String super.name, - required this.naming, - required this.isNet, - }); - - /// Constructs a new [LogicArray] with a more convenient constructor signature - /// for when many ports in an interface are declared together. Also performs - /// some basic checks on the legality of the array as a port of a [Module]. + /// The port uses mergeable naming so repeated interface connections retain a + /// single SystemVerilog port declaration. factory LogicArray.port(String name, [List dimensions = const [1], int elementWidth = 1, @@ -237,20 +155,16 @@ class LogicArray extends LogicStructure { throw InvalidPortNameException(name); } - return LogicArray( - dimensions, elementWidth, - numUnpackedDimensions: numUnpackedDimensions, name: name, - - // make port names mergeable so we don't duplicate the ports - // when calling connectIO - naming: Naming.mergeable, - ); + return LogicArray(dimensions, elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + name: name, + naming: Naming.mergeable); } - /// Constructs a new [LogicArray.net] with a more convenient constructor - /// signature for when many ports in an interface are declared together. Also - /// performs some basic checks on the legality of the array as a port of a - /// [Module]. + /// Creates a net array port with a convenient constructor signature. + /// + /// The port uses mergeable naming so repeated interface connections retain a + /// single SystemVerilog port declaration. factory LogicArray.netPort(String name, [List dimensions = const [1], int elementWidth = 1, @@ -259,14 +173,9 @@ class LogicArray extends LogicStructure { throw InvalidPortNameException(name); } - return LogicArray.net( - dimensions, elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - name: name, - - // make port names mergeable so we don't duplicate the ports - // when calling connectIO - naming: Naming.mergeable, - ); + return LogicArray.net(dimensions, elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + name: name, + naming: Naming.mergeable); } } diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index f80d676208..1196799c7c 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -115,7 +115,7 @@ class LogicStructure implements Logic { int? _arrayIndex; @override - bool get isArrayMember => parentStructure is LogicArray; + bool get isArrayMember => parentStructure is BaseLogicArray; @override void put(dynamic val, {bool fill = false}) { @@ -405,9 +405,7 @@ class LogicStructure implements Logic { final elementStart = index; final elementEnd = index + elementWidth; - final elementInRange = - ((elementStart >= startIndex) && (elementStart < endIndex)) || - ((elementEnd > startIndex) && (elementEnd <= endIndex)); + final elementInRange = elementStart < endIndex && elementEnd > startIndex; if (elementInRange) { newElement <= diff --git a/lib/src/signals/signals.dart b/lib/src/signals/signals.dart index ac0f97b1bd..03b760ed06 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause library; @@ -20,7 +20,9 @@ part 'logic.dart'; part 'wire.dart'; part 'wire_net.dart'; part 'logic_structure.dart'; +part 'base_logic_array.dart'; part 'logic_array.dart'; +part 'typed_logic_array.dart'; part 'logic_net.dart'; part 'logic_enum.dart'; part 'logic_def.dart'; diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart new file mode 100644 index 0000000000..3f4a821a3f --- /dev/null +++ b/lib/src/signals/typed_logic_array.dart @@ -0,0 +1,513 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_logic_array.dart +// Definition of typed logic arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +part of 'signals.dart'; + +/// Builds one leaf element of a [TypedLogicArray]. +typedef TypedLogicArrayElementBuilder = T Function( + {String? name}); + +/// Checks whether two typed hardware elements use compatible value formats. +typedef TypedLogicArrayElementCompatibility = bool Function( + T prototype, T element); + +/// Selects the canonical packed-value codec when [V] is [LogicValue]. +/// Resolves the supplied codec or the canonical identity codec for +/// [LogicValue] semantic values. +LogicValueCodec _resolveTypedLogicValueCodec( + LogicValueCodec? valueCodec) { + if (valueCodec != null) { + return valueCodec; + } + if (V == LogicValue) { + return LogicValueCodec.logicValue as LogicValueCodec; + } + throw ArgumentError.notNull('valueCodec'); +} + +/// A multidimensional logic array with hardware leaves of type [T] and +/// semantic values of type [V]. +/// +/// This is a [LogicStructure] because an array owns a hierarchy of child +/// signals, just like [LogicArray]. It is therefore indirectly a [Logic] +/// through the structure's packed representation, rather than a scalar +/// [Logic] whose wire is subdivided into array elements. +/// +/// [LogicArray] directly extends [TypedLogicArray]<[Logic], [LogicValue]> as +/// the ordinary-[Logic] specialization, inheriting its typed-array API. +/// [TypedLogicArray] extends the internal [BaseLogicArray], where the shared +/// array metadata and traversal live, while [T] preserves the type at the +/// declared array boundary. +/// +/// [value] and [previousValue] preserve [V] through [TypedLogicValueArray] +/// snapshots. Standard [Logic.changed] events remain packed +/// [LogicValueChanged] events. +/// +/// The optional `dimensionNames` constructor argument is construction metadata +/// used when naming typed-array hierarchy members and when cloning. It is not +/// exposed as a public axis-metadata property; ordinary [LogicArray] naming +/// continues to follow its existing generated-name convention. +class TypedLogicArray extends BaseLogicArray { + /// Construction prefixes retained for typed-array cloning. + final List _dimensionNames; + + /// Recreates [T] elements when cloning this array. + final TypedLogicArrayElementBuilder _elementBuilder; + + /// Converts each element's packed bits to and from its semantic value type. + final LogicValueCodec valueCodec; + + /// Additional representation compatibility required between all elements. + // Safe because it is invoked only on elements produced by [_elementBuilder]. + // ignore: unsafe_variance + final TypedLogicArrayElementCompatibility? _elementCompatibility; + + /// Cached typed view of the configured array-element boundary. + late final List _typedArrayElements = + List.unmodifiable(super.arrayElements.cast()); + + /// Typed elements reached after traversing exactly [dimensions] array levels. + /// + /// Traversal stops at the configured [T] boundary rather than recursively + /// flattening a [LogicStructure] or nested array stored as a [T]. This + /// differs from [LogicStructure.leafElements], which recursively visits + /// every structure leaf. + /// + /// For example, a `[2, 3]` array of two-field `Sample` structures has six + /// [arrayElements] and twelve recursive [LogicStructure.leafElements]. A + /// `[2]` array whose [T] is a three-element [LogicArray] has two + /// [arrayElements] and six recursive leaves. An eight-bit [Logic] is one + /// leaf, not eight leaves. + @override + List get arrayElements => _typedArrayElements; + + /// Creates an array with [dimensions] and typed leaves from [elementBuilder]. + /// + /// [valueCodec] may be omitted only when [V] is [LogicValue], in which case + /// the canonical identity codec is used. + /// + /// Every element produced by [elementBuilder] must have the same recursive + /// hardware layout and must contain only driveable signals. + /// + /// [numUnpackedDimensions] controls how many outer dimensions are emitted as + /// unpacked dimensions by synthesis. + TypedLogicArray( + List dimensions, + TypedLogicArrayElementBuilder elementBuilder, { + LogicValueCodec? valueCodec, + TypedLogicArrayElementCompatibility? elementCompatibility, + List? dimensionNames, + String? name, + Naming? naming, + int numUnpackedDimensions = 0, + }) : this._( + _TypedLogicArrayBuild.build(dimensions, elementBuilder, + _resolveTypedLogicValueCodec(valueCodec), elementCompatibility, + dimensionNames: dimensionNames, + numUnpackedDimensions: numUnpackedDimensions), + elementBuilder, + _resolveTypedLogicValueCodec(valueCodec), + elementCompatibility, + name: name, + naming: naming, + numUnpackedDimensions: numUnpackedDimensions); + + /// Initializes an array from validated construction data. + /// + /// This constructor is used for recursively building nested arrays and + /// assumes [build] has already validated the hierarchy. + TypedLogicArray._( + _TypedLogicArrayBuild build, + this._elementBuilder, + this.valueCodec, + this._elementCompatibility, { + required super.numUnpackedDimensions, + super.name, + super.naming, + }) : _dimensionNames = build.dimensionNames, + super.structured(build.elements, + dimensions: build.dimensions, + elementWidth: build.elementWidth, + isNet: build.isNet); + + /// Creates an array from trusted, prebuilt [elements]. + /// + /// This library-private constructor is used by [LogicArray] after its + /// factory has established the required representation metadata. It + /// intentionally bypasses the normal builder validation path. + TypedLogicArray._structured( + super.elements, + this._elementBuilder, { + required this.valueCodec, + required super.dimensions, + required super.elementWidth, + required super.numUnpackedDimensions, + required String name, + required Naming naming, + required super.isNet, + TypedLogicArrayElementCompatibility? elementCompatibility, + List? dimensionNames, + }) : _elementCompatibility = elementCompatibility, + _dimensionNames = + _normalizeLogicArrayDimensionNames(dimensions, dimensionNames), + super.structured( + name: name, + naming: naming, + ); + + /// Array elements paired with their row-major multidimensional indices. + Iterable<(List, T)> get indexedElements => Iterable.generate( + arrayElements.length, + (index) => (_arrayIndices(dimensions, index), arrayElements[index])); + + /// Returns the array element at multidimensional [indices]. + T at(List indices) { + if (indices.length != dimensions.length) { + throw RangeError.range(indices.length, dimensions.length, + dimensions.length, 'indices.length'); + } + + Logic current = this; + for (var dimension = 0; dimension < indices.length; dimension++) { + final index = indices[dimension]; + final size = dimensions[dimension]; + if (index < 0 || index >= size) { + throw RangeError.range(index, 0, size - 1, 'indices[$dimension]'); + } + current = (current as LogicStructure).elements[index]; + } + return current as T; + } + + /// Immediate typed child arrays along the first dimension. + Iterable> get majorSlices { + if (dimensions.length < 2) { + throw StateError('majorSlices requires at least two dimensions.'); + } + return elements.cast>(); + } + + @override + TypedLogicValueArray get value => TypedLogicValueArray.fromPacked( + dimensions, + elementWidth, + arrayElements.map((element) => element.value), + codec: valueCodec, + ); + + @override + TypedLogicValueArray? get previousValue => + arrayElements.any((element) => element.previousValue == null) + ? null + : TypedLogicValueArray.fromPacked( + dimensions, + elementWidth, + arrayElements.map((element) => element.previousValue!), + codec: valueCodec, + ); + + /// Creates a clone while allowing subclasses to preserve their runtime type. + /// + /// The base implementation retains all construction state, including its + /// private dimension-name prefixes. A subclass that overrides this method + /// should retain any constructor-only arguments that affect reconstruction + /// in its own private fields and pass them to its constructor here. In + /// particular, a subclass exposing `dimensionNames` as a constructor + /// argument must retain a private copy because the base copy is intentionally + /// library-private. Public metadata such as [dimensions] and + /// [numUnpackedDimensions] can be read directly, while [elementWidth] and + /// [isNet] are re-derived by construction. + @protected + TypedLogicArray createClone({ + String? name, + Naming? naming, + int? numUnpackedDimensions, + }) => + TypedLogicArray(dimensions, _elementBuilder, + valueCodec: valueCodec, + elementCompatibility: _elementCompatibility, + dimensionNames: _dimensionNames, + name: name ?? this.name, + naming: naming, + numUnpackedDimensions: + numUnpackedDimensions ?? this.numUnpackedDimensions); + + @override + TypedLogicArray _clone({String? name, Naming? naming}) => createClone( + name: name, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: this.naming, + newNaming: naming, + ), + numUnpackedDimensions: numUnpackedDimensions, + ); + + @override + TypedLogicArray clone({String? name}) => _clone(name: name); + + @override + TypedLogicArray named(String name, {Naming? naming}) => _clone( + name: name, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: this.naming, + newNaming: naming, + ), + )..gets(this); +} + +/// Validated construction data prepared before initializing [TypedLogicArray]. +class _TypedLogicArrayBuild { + /// Validated array dimensions. + final List dimensions; + + /// Validated labels corresponding to [dimensions]. + final List dimensionNames; + + /// Immediate children of the outermost dimension. + final List elements; + + /// Width shared by every typed array element. + final int elementWidth; + + /// Whether every typed array element is a net. + final bool isNet; + + /// Stores construction data after all validation is complete. + _TypedLogicArrayBuild._(this.dimensions, this.dimensionNames, this.elements, + this.elementWidth, this.isNet); + + /// Builds and validates the complete typed-array hierarchy. + factory _TypedLogicArrayBuild.build( + List dimensions, + TypedLogicArrayElementBuilder elementBuilder, + LogicValueCodec valueCodec, + TypedLogicArrayElementCompatibility? elementCompatibility, + {List? dimensionNames, + int numUnpackedDimensions = 0, + T? emptyPrototype, + bool validateValueCodec = true}) { + final normalizedDimensions = List.unmodifiable(dimensions); + if (normalizedDimensions.isEmpty) { + throw LogicConstructionException( + 'TypedLogicArray must have at least 1 dimension.'); + } + if (normalizedDimensions.any((dimension) => dimension < 0)) { + throw LogicConstructionException( + 'TypedLogicArray dimensions must be non-negative.'); + } + if (numUnpackedDimensions < 0 || + numUnpackedDimensions > normalizedDimensions.length) { + throw LogicConstructionException( + 'numUnpackedDimensions must be between 0 and the number of ' + 'dimensions.'); + } + + final normalizedNames = _normalizeLogicArrayDimensionNames( + normalizedDimensions, dimensionNames); + emptyPrototype ??= _arrayLength(normalizedDimensions) == 0 + ? elementBuilder(name: '${normalizedNames.last}prototype') + : null; + + final elements = List.generate(normalizedDimensions.first, (index) { + final elementName = '${normalizedNames.first}$index'; + return normalizedDimensions.length == 1 + ? elementBuilder(name: elementName) + : TypedLogicArray._( + _TypedLogicArrayBuild.build( + normalizedDimensions.sublist(1), + elementBuilder, + valueCodec, + elementCompatibility, + dimensionNames: normalizedNames.sublist(1), + numUnpackedDimensions: max(0, numUnpackedDimensions - 1), + emptyPrototype: emptyPrototype, + validateValueCodec: false, + ), + elementBuilder, + valueCodec, + elementCompatibility, + name: elementName, + numUnpackedDimensions: max(0, numUnpackedDimensions - 1), + ); + }, growable: false); + final typedLeaves = normalizedDimensions.length == 1 + ? elements.cast().toList(growable: false) + : elements + .cast>() + .expand((element) => element.arrayElements) + .toList(growable: false); + if (typedLeaves.any(_containsUnassignableLeaf)) { + throw LogicConstructionException( + 'TypedLogicArray leaves must be driveable.'); + } + final prototype = typedLeaves.isEmpty ? emptyPrototype! : typedLeaves.first; + if (_containsUnassignableLeaf(prototype)) { + throw LogicConstructionException( + 'TypedLogicArray leaves must be driveable.'); + } + final prototypeNetComposition = _netComposition(prototype); + final elementsForNetValidation = + typedLeaves.isEmpty ? [prototype] : typedLeaves; + if (elementsForNetValidation.any((element) { + final composition = _netComposition(element); + return composition.contains(true) && composition.contains(false); + })) { + throw LogicConstructionException( + 'TypedLogicArray elements cannot mix net and non-net leaves.'); + } + if (elementsForNetValidation.any((element) => !_sameNetComposition( + _netComposition(element), prototypeNetComposition))) { + throw LogicConstructionException( + 'All TypedLogicArray elements must have matching net composition.'); + } + if (elementsForNetValidation + .any((element) => !_sameElementLayout(prototype, element))) { + throw LogicConstructionException( + 'All TypedLogicArray elements must have matching hardware layouts.'); + } + if (elementCompatibility != null && + elementsForNetValidation + .cast() + .any((element) => !elementCompatibility(prototype, element))) { + throw LogicConstructionException( + 'All TypedLogicArray elements must have compatible value formats.'); + } + final elementWidth = _validateElementWidths(typedLeaves, prototype.width); + if (validateValueCodec) { + _validateValueCodec(prototype.value, elementWidth, valueCodec); + } + return _TypedLogicArrayBuild._( + normalizedDimensions, + normalizedNames, + elements, + elementWidth, + prototypeNetComposition.every((isNet) => isNet)); + } + + /// Whether [element] is or recursively contains an unassignable signal. + static bool _containsUnassignableLeaf(Logic element) => + element is LogicStructure + ? element.leafElements.any(_containsUnassignableLeaf) + : element._unassignable; + + /// Whether two configured elements have the same recursive hardware layout. + static bool _sameElementLayout(Logic left, Logic right) { + if (left.width != right.width || left.isNet != right.isNet) { + return false; + } + if (left is BaseLogicArray || right is BaseLogicArray) { + if (left is! BaseLogicArray || right is! BaseLogicArray) { + return false; + } + return left.elementWidth == right.elementWidth && + left.numUnpackedDimensions == right.numUnpackedDimensions && + _sameDimensions(left.dimensions, right.dimensions) && + left.elements.length == right.elements.length && + left.elements.indexed.every( + (entry) => _sameElementLayout( + entry.$2, + right.elements[entry.$1], + ), + ); + } + if (left is LogicStructure || right is LogicStructure) { + if (left is! LogicStructure || + right is! LogicStructure || + left.runtimeType != right.runtimeType || + left.elements.length != right.elements.length) { + return false; + } + return left.elements.indexed.every((entry) { + final other = right.elements[entry.$1]; + return entry.$2.name == other.name && + _sameElementLayout(entry.$2, other); + }); + } + return true; + } + + /// Net kinds of [element]'s recursive leaves in packed order. + static List _netComposition(Logic element) { + if (element is! LogicStructure) { + return [element.isNet]; + } + if (element.leafElements.isNotEmpty) { + return element.leafElements + .map((leaf) => leaf.isNet) + .toList(growable: false); + } + return [element is BaseLogicArray && element.isNet]; + } + + /// Whether two recursive net-kind signatures are identical. + static bool _sameNetComposition(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); + + /// Validates that [elements] all have [expectedWidth]. + static int _validateElementWidths( + List elements, int expectedWidth) { + if (elements.any((element) => element.width != expectedWidth)) { + throw LogicConstructionException( + 'All TypedLogicArray leaves must have the same width.'); + } + return expectedWidth; + } + + /// Validates four-state decoding and the encoded semantic value width. + static void _validateValueCodec( + LogicValue prototype, int elementWidth, LogicValueCodec valueCodec) { + final encodedPrototype = valueCodec.encode(valueCodec.decode(prototype)); + if (encodedPrototype.width != elementWidth) { + throw LogicConstructionException( + 'The value codec must encode values with width $elementWidth.'); + } + } +} + +/// Returns the number of array positions described by [dimensions]. +int _arrayLength(List dimensions) => + dimensions.fold(1, (length, dimension) => length * dimension); + +/// Converts [flatIndex] to row-major indices for [dimensions]. +List _arrayIndices(List dimensions, int flatIndex) { + final indices = List.filled(dimensions.length, 0); + for (var dimension = dimensions.length - 1; dimension >= 0; dimension--) { + final size = dimensions[dimension]; + indices[dimension] = size == 0 ? 0 : flatIndex % size; + flatIndex = size == 0 ? 0 : flatIndex ~/ size; + } + return indices; +} + +/// Creates the default element-name prefix for each array dimension. +List _defaultDimensionNames(int rank) => + List.generate(rank, (dimension) => 'd${dimension}_', growable: false); + +/// Validates and freezes dimension-name prefixes. +List _normalizeLogicArrayDimensionNames( + List dimensions, List? dimensionNames) { + final normalized = List.unmodifiable( + dimensionNames ?? _defaultDimensionNames(dimensions.length), + ); + if (normalized.length != dimensions.length) { + throw LogicConstructionException( + 'dimensionNames must match the number of dimensions.'); + } + if (normalized.any((name) => !Sanitizer.isSanitary(name)) || + normalized.toSet().length != normalized.length) { + throw LogicConstructionException( + 'dimensionNames must be sanitary and unique.'); + } + return normalized; +} diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 9e08d4efe2..a66fe37262 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -9,6 +9,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; @@ -131,7 +132,7 @@ class NetlistModuleTranslation { final portLogic = modulePorts[portName]; final emitOutputArrayConcat = direction == NetlistPortDirection.output && - portLogic is LogicArray && + portLogic is BaseLogicArray && !_hasExistingOutputArrayConcat(synthLogic) && !_hasDirectSubmoduleOutputDriver(synthLogic); final originalIds = getIds(synthLogic); @@ -168,7 +169,7 @@ class NetlistModuleTranslation { /// Emits a concat cell that assembles a LogicArray output port. void _emitOutputArrayConcat( String portName, - LogicArray array, + BaseLogicArray array, List outputIds, ) { _emitOutputArrayConcatForArray(portName, array, outputIds); @@ -177,7 +178,7 @@ class NetlistModuleTranslation { /// Recursively emits concat cells for nested LogicArray output elements. bool _emitOutputArrayConcatForArray( String concatName, - LogicArray array, + BaseLogicArray array, List outputIds, ) { final definition = synthDef; @@ -195,7 +196,7 @@ class NetlistModuleTranslation { return false; } var elementIds = getIds(synthLogic); - if (element is LogicArray && + if (element is BaseLogicArray && !_hasExistingOutputArrayConcat(synthLogic) && !_hasDirectSubmoduleOutputDriver(synthLogic)) { final aggregateIds = List.generate( @@ -210,6 +211,12 @@ class NetlistModuleTranslation { return false; } elementIds = aggregateIds; + } else if (element is LogicStructure && element is! BaseLogicArray) { + _emitOutputArrayStructurePack( + '${concatName}_$index', + element, + elementIds, + ); } final upperIndex = lowerIndex + elementIds.length - 1; concatConnections['[$upperIndex:$lowerIndex]'] = @@ -244,6 +251,74 @@ class NetlistModuleTranslation { return true; } + void _emitOutputArrayStructurePack( + String packName, + LogicStructure structure, + List outputIds, + ) { + final definition = synthDef; + if (definition == null) { + return; + } + + final layout = SynthStructureLayout(structure); + final connections = >{}; + final directions = {}; + final fields = <({String name, int offset, int width})>[]; + var offset = 0; + + for (final leaf in structure.leafElements) { + final leafSynth = definition.logicToSynthMap[leaf]; + if (leafSynth == null) { + throw StateError( + 'Missing synthesized logic for array structure field ${leaf.name}.', + ); + } + + final fieldName = layout.fieldNameAt( + offset, + fallbackName: leaf.name, + anonymousUnpreferred: true, + ); + var portName = fieldName; + var suffix = 1; + while (portName == 'Y' || connections.containsKey(portName)) { + portName = '${fieldName}_${suffix++}'; + } + + connections[portName] = getIds(leafSynth).cast(); + directions[portName] = NetlistPortDirection.input; + fields.add((name: fieldName, offset: offset, width: leaf.width)); + offset += leaf.width; + } + + if (offset != outputIds.length) { + throw StateError( + 'Array structure fields occupy $offset bits, but ${structure.name} ' + 'occupies ${outputIds.length} bits.', + ); + } + + connections['Y'] = outputIds.cast(); + directions['Y'] = NetlistPortDirection.output; + + cells[Sanitizer.sanitizeSV('struct_pack_$packName')] = NetlistCell( + type: r'$struct_pack', + origin: NetlistCellOrigin.structurePack, + parameters: { + 'STRUCT_NAME': structure.name, + 'FIELD_COUNT': fields.length, + for (final (index, field) in fields.indexed) ...{ + 'FIELD_${index}_NAME': field.name, + 'FIELD_${index}_OFFSET': field.offset, + 'FIELD_${index}_WIDTH': field.width, + }, + }, + portDirections: directions, + connections: connections, + ).toJson(); + } + /// Checks whether [synthLogic] is already driven by an output concat cell. bool _hasExistingOutputArrayConcat(SynthLogic synthLogic) => _submoduleOutputDrivers.arrayConcatOutputs.contains(synthLogic.resolved); @@ -362,7 +437,7 @@ class NetlistModuleTranslation { for (final portEntry in submodule.inputs.entries) { final portName = portEntry.key; final port = portEntry.value; - if (port is! LogicArray || + if (port is! BaseLogicArray || cellPortDirs[portName] != NetlistPortDirection.input) { continue; } diff --git a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart index 4ba4fe5815..4227226e40 100644 --- a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -9,6 +9,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// A [SynthModuleDefinition] that preserves cells for netlist synthesis. @@ -19,14 +20,14 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { // Create explicit $slice cells for LogicArray input ports so the // netlist shows select gates for element extraction rather than // flat bit aliasing. - module.inputs.values.whereType().forEach( + module.inputs.values.whereType().forEach( _subsetReceiveArrayPort, ); // Same for LogicArray outputs on submodules (received into this scope). final subModuleOutputArrays = module.subModules .expand((sub) => sub.outputs.values) - .whereType() + .whereType() .toSet() ..forEach(_subsetReceiveArrayPort); @@ -39,25 +40,25 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { // already structurally decomposed by the $slice cells created above. // Also skip submodule output arrays that already received $slice cells. final portArrays = { - ...module.inputs.values.whereType(), - ...module.outputs.values.whereType(), - ...module.inOuts.values.whereType(), + ...module.inputs.values.whereType(), + ...module.outputs.values.whereType(), + ...module.inOuts.values.whereType(), }; - final excludedArrays = { + final excludedArrays = { ...portArrays, ...subModuleOutputArrays, }; - void addNestedArrays(LogicArray array) { + void addNestedArrays(BaseLogicArray array) { for (final element in array.elements) { - if (element is LogicArray) { + if (element is BaseLogicArray) { excludedArrays.add(element); addNestedArrays(element); } } } - { + { ...portArrays, ...subModuleOutputArrays, }.forEach(addNestedArrays); @@ -68,7 +69,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { portArraySynthLogics.add(synthLogic.resolved); } } - module.internalSignals.whereType().where((signal) { + module.internalSignals.whereType().where((signal) { if (excludedArrays.contains(signal)) { return false; } @@ -81,7 +82,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { } /// Adds slice cells that decompose a LogicArray port into element signals. - void _subsetReceiveArrayPort(LogicArray port) { + void _subsetReceiveArrayPort(BaseLogicArray port) { final portSynth = getSynthLogic(port)!; var index = 0; @@ -101,12 +102,16 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { ..setInputMapping(subsetModule.original.name, portSynth) ..pickName(module); + if (element is BaseLogicArray) { + _subsetReceiveArrayPort(element); + } + index += element.width; } } /// Adds a concat cell that assembles independent LogicArray element signals. - void _concatAssembleArray(LogicArray array) { + void _concatAssembleArray(BaseLogicArray array) { final arraySynth = getSynthLogic(array)!; final dummyElements = [ for (final element in array.elements) diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 086b314397..ea6b15d2be 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -11,6 +11,7 @@ import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; @@ -164,7 +165,7 @@ class NetlistSynthesizer extends Synthesizer { void aliasArrayChildren(SynthLogic src, SynthLogic dst) { final srcLogic = src.logics.firstOrNull; final dstLogic = dst.logics.firstOrNull; - if (srcLogic is! LogicArray || dstLogic is! LogicArray) { + if (srcLogic is! BaseLogicArray || dstLogic is! BaseLogicArray) { return; } if (srcLogic.elements.length != dstLogic.elements.length) { @@ -181,7 +182,8 @@ class NetlistSynthesizer extends Synthesizer { final srcElementLogic = srcElementSynth.logics.firstOrNull; final dstElementLogic = dstElementSynth.logics.firstOrNull; - if (srcElementLogic is LogicArray && dstElementLogic is LogicArray) { + if (srcElementLogic is BaseLogicArray && + dstElementLogic is BaseLogicArray) { aliasArrayChildren(srcElementSynth, dstElementSynth); } @@ -231,16 +233,33 @@ class NetlistSynthesizer extends Synthesizer { void addStructAndDescendants(LogicStructure struct, Set set) { set.add(struct); for (final elem in struct.elements) { - if (elem is LogicStructure && elem is! LogicArray) { + if (elem is LogicStructure && elem is! BaseLogicArray) { addStructAndDescendants(elem, set); } } } + void addArrayStructureElements(BaseLogicArray array) { + for (final element in array.elements) { + if (element is BaseLogicArray) { + addArrayStructureElements(element); + } else if (element is LogicStructure) { + addStructAndDescendants(element, outputStructPortLogics); + } + } + } + + module.outputs.values + .whereType() + .forEach(addArrayStructureElements); + for (final pa in synthDef.assignments.whereType()) { final srcIds = getIds(pa.src); final dstIds = getIds(pa.dst); + final selectedSrcIds = pa is RangeSynthAssignment + ? srcIds.sublist(pa.srcLowerIndex, pa.srcUpperIndex + 1) + : srcIds; // Detect: is pa.dst an output struct port of the current module? final isCurrentModuleOutputPort = @@ -250,12 +269,13 @@ class NetlistSynthesizer extends Synthesizer { // (LogicStructure but not LogicArray, and not an output of the // current module.) final isSubModuleInputStructPort = !isCurrentModuleOutputPort && - pa.dst.logics.any((l) => l is LogicStructure && l is! LogicArray); + pa.dst.logics + .any((l) => l is LogicStructure && l is! BaseLogicArray); if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { // Record as pending compose cell instead of aliasing. structPackFields.add(( - srcIds: srcIds, + srcIds: selectedSrcIds, dstIds: dstIds, dstLowerIndex: pa.dstLowerIndex, dstUpperIndex: pa.dstUpperIndex, @@ -265,16 +285,16 @@ class NetlistSynthesizer extends Synthesizer { // Track the Logic (and nested structs) so Step 3 skips // $struct_unpack for them. for (final l in pa.dst.logics) { - if (l is LogicStructure && l is! LogicArray) { + if (l is LogicStructure && l is! BaseLogicArray) { addStructAndDescendants(l, outputStructPortLogics); } } } else { // Non-struct sub-module input port: alias as before. - for (var i = 0; i < srcIds.length; i++) { + for (var i = 0; i < selectedSrcIds.length; i++) { final dstIdx = pa.dstLowerIndex + i; - if (dstIdx < dstIds.length && dstIds[dstIdx] != srcIds[i]) { - idAlias[dstIds[dstIdx]] = srcIds[i]; + if (dstIdx < dstIds.length && dstIds[dstIdx] != selectedSrcIds[i]) { + idAlias[dstIds[dstIdx]] = selectedSrcIds[i]; } } } @@ -339,7 +359,7 @@ class NetlistSynthesizer extends Synthesizer { ) .map((e) => e.key) .firstOrNull; - if (logic != null && logic is LogicArray) { + if (logic != null && logic is BaseLogicArray) { arraysWithExplicitCells.add(logic); } // Also check the resolved replacement chain. @@ -348,7 +368,7 @@ class NetlistSynthesizer extends Synthesizer { .where((e) => e.value == resolved) .map((e) => e.key) .firstOrNull; - if (logic2 != null && logic2 is LogicArray) { + if (logic2 != null && logic2 is BaseLogicArray) { arraysWithExplicitCells.add(logic2); } } @@ -362,7 +382,7 @@ class NetlistSynthesizer extends Synthesizer { ) .map((e) => e.key) .firstOrNull; - if (logic != null && logic is LogicArray) { + if (logic != null && logic is BaseLogicArray) { arraysWithExplicitCells.add(logic); } } @@ -377,7 +397,7 @@ class NetlistSynthesizer extends Synthesizer { final parentSL = entry.value; final parentIds = getIds(parentSL); - if (logic is LogicArray) { + if (logic is BaseLogicArray) { // Skip aliasing for arrays that have explicit $slice/$concat cells. if (arraysWithExplicitCells.contains(logic)) { continue; @@ -426,7 +446,7 @@ class NetlistSynthesizer extends Synthesizer { fullParentIds: parentIds, )); } - } else if (elem is LogicStructure && elem is! LogicArray) { + } else if (elem is LogicStructure && elem is! BaseLogicArray) { // Nested InterfaceStructure: the intermediate struct // itself has no SynthLogic, but its leaf elements do // (created by _subsetReceiveStructPort). Walk leaf @@ -486,6 +506,7 @@ class NetlistSynthesizer extends Synthesizer { // (Populated inside the alias block below; declared here so netnames // can reference it later.) final arraySliceOldToNew = {}; + final arraySliceOutputBitsByCell = >{}; // Alias port bits. if (idAlias.isNotEmpty) { @@ -524,12 +545,17 @@ class NetlistSynthesizer extends Synthesizer { final cell = cellEntry.value as Map; final conns = cell['connections'] as Map; final dirs = cell['port_directions'] as Map; + final oldOutputBits = arraySliceOutputBitsByCell.putIfAbsent( + cellEntry.key, + () => {}, + ); for (final portEntry in conns.entries.toList()) { if (dirs[portEntry.key] != 'output') { continue; } final oldBits = (portEntry.value as List).cast(); + oldOutputBits.addAll(oldBits.whereType()); conns[portEntry.key] = [ for (final b in oldBits) if (b is int) @@ -558,18 +584,11 @@ class NetlistSynthesizer extends Synthesizer { } for (final cellEntry in cells.entries) { - if (NetlistCell.hasOrigin( - cellEntry.value, - NetlistCellOrigin.arraySlice, - )) { - // Keep each slice input connected to the original parent-array - // bits. Rewriting it would bypass the slice by connecting both - // its input and fresh output to the replacement IDs. - continue; - } final cell = cellEntry.value as Map; final conns = cell['connections'] as Map; final dirs = cell['port_directions'] as Map; + final ownOldOutputBits = + arraySliceOutputBitsByCell[cellEntry.key] ?? const {}; for (final portEntry in conns.entries.toList()) { if (dirs[portEntry.key] != 'input') { @@ -578,7 +597,10 @@ class NetlistSynthesizer extends Synthesizer { final bits = (portEntry.value as List).cast(); final newBits = [ for (final b in bits) - if (b is int) arraySliceOldToNew[b] ?? b else b, + if (b is int && !ownOldOutputBits.contains(b)) + arraySliceOldToNew[b] ?? b + else + b, ]; if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { conns[portEntry.key] = newBits; @@ -588,6 +610,14 @@ class NetlistSynthesizer extends Synthesizer { } } + List applyArraySliceOutputs(List bits) => + arraySliceOldToNew.isEmpty + ? bits + : [ + for (final bit in bits) + if (bit is int) arraySliceOldToNew[bit] ?? bit else bit, + ]; + // -- Elide trivial $slice cells ---------------------------------- // Also elide struct_slice cells ([SynthStructureSlice] instances from // `_subsetReceiveStructPort`) because the new `$struct_unpack` cells @@ -644,7 +674,9 @@ class NetlistSynthesizer extends Synthesizer { final parentLogic = entry.key; final fields = entry.value; final fullParentIds = fields.first.fullParentIds; - final resolvedParentBits = applyAlias(fullParentIds.cast()); + final resolvedParentBits = applyArraySliceOutputs( + applyAlias(fullParentIds.cast()), + ); // Filter out trivial fields (input slice == output after aliasing). final nonTrivialFields = fields @@ -1022,7 +1054,7 @@ class NetlistSynthesizer extends Synthesizer { final netnames = translation.netnames; // -- Structural validation ------------------------------------------- - NetlistValidation.validate(ports, cells, module.name); + NetlistValidation.validate(ports, cells, module.name, netnames: netnames); return NetlistSynthesisResult( module, diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 638afbd45b..e9df3f9495 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -9,6 +9,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_port_direction.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; @@ -424,7 +425,7 @@ abstract class NetlistUtils { Logic logic, [ List? bits, ]) { - if (logic is LogicArray) { + if (logic is BaseLogicArray) { final result = { 'width': logic.width, 'arrayDims': logic.dimensions, @@ -434,9 +435,9 @@ abstract class NetlistUtils { // include the element type metadata for recursive expansion. if (logic.elements.isNotEmpty) { final first = logic.elements.first; - if (first is LogicStructure && first is! LogicArray) { + if (first is LogicStructure && first is! BaseLogicArray) { result['elementType'] = buildLogicType(first); - } else if (first is LogicArray) { + } else if (first is BaseLogicArray) { // Nested array — encode inner dimensions via recursive call. result['elementType'] = buildLogicType(first); } @@ -447,13 +448,13 @@ abstract class NetlistUtils { final fields = logic.elements.map((e) { final fieldBits = bits?.sublist(offset, offset + e.width); offset += e.width; - if (e is LogicStructure && e is! LogicArray) { + if (e is LogicStructure && e is! BaseLogicArray) { return { 'name': e.name, if (fieldBits != null) 'bits': fieldBits, 'type': buildLogicType(e, fieldBits), }; - } else if (e is LogicArray) { + } else if (e is BaseLogicArray) { return { 'name': e.name, 'width': e.width, @@ -483,7 +484,7 @@ abstract class NetlistUtils { final logics = sl.logics; return logics .whereType() - .where((l) => l is! LogicArray) + .where((l) => l is! BaseLogicArray) .firstOrNull ?? logics.firstOrNull; } diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index c8399f2ae2..0a6a570dcf 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -53,21 +53,31 @@ class NetlistValidation { static void validate( Map> ports, Map> cells, - String moduleName, - ) { + String moduleName, { + Map? netnames, + }) { final issues = []; final driversByBit = _driversByBit(ports, cells); + final aggregateNetnamesByBit = _aggregateNetnamesByBit(netnames); for (final entry in driversByBit.entries) { if (!_hasConflictingDrivers(entry.value)) { continue; } final drivers = entry.value.map((driver) => driver.description).toList(); + final aggregateNetnames = + aggregateNetnamesByBit[entry.key] ?? const {}; + final aggregateContext = aggregateNetnames.isEmpty + ? '' + : ' in aggregate net${aggregateNetnames.length == 1 ? '' : 's'} ' + '${aggregateNetnames.map((name) => '"$name"').join(', ')}'; issues.add(NetlistValidationIssue( 'wire bit ${entry.key} has multiple drivers: ' - '${drivers.join(', ')}', + '${drivers.join(', ')}$aggregateContext', wireBit: entry.key, + netname: + aggregateNetnames.length == 1 ? aggregateNetnames.single : null, drivers: drivers, )); } @@ -77,6 +87,33 @@ class NetlistValidation { } } + static Map> _aggregateNetnamesByBit( + Map? netnames, + ) { + final netnamesByBit = >{}; + if (netnames == null) { + return netnamesByBit; + } + + for (final entry in netnames.entries) { + final netname = entry.value; + if (netname is! Map) { + continue; + } + final logicType = netname['logic_type']; + if (logicType is! Map || + (logicType['arrayDims'] is! List && logicType['fields'] is! List)) { + continue; + } + for (final bit + in (netname['bits'] as List?)?.whereType() ?? const []) { + (netnamesByBit[bit] ??= {}).add(entry.key); + } + } + + return netnamesByBit; + } + /// Collects the port and cell output drivers for each integer bit ID. static Map> _driversByBit( Map> ports, diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart index 3b4cf586e7..5e759fd1f8 100644 --- a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -1058,6 +1058,7 @@ class SystemCSynthesisResult extends SynthesisResult { condBuf.write(_conditionalToSC(c, 3, inputsMap, outputsMap)); } group.whileBodyLines.add(condBuf.toString()); + group.resetBodyLines.add(condBuf.toString()); ssmi.clearInstantiation(); } else if (m is FlipFlop) { // Resolve port signals via the input/output mapping @@ -1114,6 +1115,7 @@ class SystemCSynthesisResult extends SynthesisResult { resetValExpr = '0'; } group.resetLines.add(' ${_scName(qSl.name)} = $resetValExpr;'); + group.asyncResetLines.add(' ${_scName(qSl.name)} = $resetValExpr;'); // Build the data assignment (with optional enable gate) final assignExpr = @@ -1217,7 +1219,8 @@ class SystemCSynthesisResult extends SynthesisResult { ..writeln(' wait($waitExpr);'); if (group.resetName != null && group.isAsyncReset) { bodyBuf.writeln(' if (${_scName(group.resetName!)}.read()) {'); - group.resetLines.forEach(bodyBuf.writeln); + group.asyncResetLines.forEach(bodyBuf.writeln); + group.resetBodyLines.forEach(bodyBuf.write); bodyBuf.writeln(' } else {'); group.whileBodyLines.forEach(bodyBuf.write); bodyBuf.writeln(' }'); @@ -1913,6 +1916,14 @@ class _ClockedGroupData { final List resetLines = []; final List whileBodyLines = []; + + /// Reset assignments taken by the asynchronous reset branch: flop reset + /// values only. + final List asyncResetLines = []; + + /// Sequential bodies, which already contain their own reset branch. + final List resetBodyLines = []; + _ClockedGroupData({this.resetName, this.isAsyncReset = false}); } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index e2d646257d..1124580576 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -8,6 +8,7 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; @@ -33,6 +34,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { @override void process() { + _expandNestedUnpackedArrayAssignments(); _inlinePackedRangesIntoSubmoduleInputs(); _collapseAggregateConnections(); _collapseWholeNetBuses(); @@ -135,6 +137,64 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { return SynthAssignment(source, destination); } + /// Expands whole assignments involving nested payloads across unpacked + /// dimensions into assignments of their packed children. + /// + /// Some simulators do not support whole unpacked-array assignments. Existing + /// ordinary array output is left unchanged; this lowering only applies when + /// the configured array element itself contains another array. + void _expandNestedUnpackedArrayAssignments() { + final expanded = []; + + void addAssignment( + SynthLogic source, + SynthLogic destination, { + bool expandArray = false, + }) { + final sourceArray = source.logics.firstOrNull; + final destinationArray = destination.logics.firstOrNull; + final startsNestedUnpackedExpansion = sourceArray is BaseLogicArray && + destinationArray is BaseLogicArray && + (sourceArray.numUnpackedDimensions > 0 || + destinationArray.numUnpackedDimensions > 0) && + (_hasNestedArrayPayload(sourceArray) || + _hasNestedArrayPayload(destinationArray)); + if (sourceArray is BaseLogicArray && + destinationArray is BaseLogicArray && + (expandArray || startsNestedUnpackedExpansion) && + sourceArray.elements.length == destinationArray.elements.length) { + for (var index = 0; index < sourceArray.elements.length; index++) { + addAssignment( + getSynthLogic(sourceArray.elements[index])!.resolved, + getSynthLogic(destinationArray.elements[index])!.resolved, + expandArray: true, + ); + } + } else { + expanded.add(SynthAssignment(source, destination)); + } + } + + for (final assignment in assignments) { + if (assignment is PartialSynthAssignment) { + expanded.add(assignment); + } else { + addAssignment(assignment.src, assignment.dst); + } + } + + assignments + ..clear() + ..addAll(expanded); + } + + static bool _hasNestedArrayPayload(BaseLogicArray array) => + array.arrayElements.any(_containsArray); + + static bool _containsArray(Logic logic) => + logic is BaseLogicArray || + (logic is LogicStructure && logic.elements.any(_containsArray)); + /// Inlines a fully covered packed bus into its sole submodule input. /// /// Each driver must cover the next contiguous destination range and supply @@ -519,7 +579,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // Gather each element's single source (the other end of its single // connecting assignment), in element order (index 0 = LSB). final elementLogics = agg.logics - .whereType() + .whereType() .first .elements .map(getSynthLogic) @@ -527,7 +587,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { .toList(); final aggregateLogic = agg.logics.singleOrNull; - final isPackedBitArray = aggregateLogic is LogicArray && + final isPackedBitArray = aggregateLogic is BaseLogicArray && aggregateLogic.dimensions.length == 1 && aggregateLogic.elementWidth == 1 && aggregateLogic.numUnpackedDimensions == 0; @@ -1322,7 +1382,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } final allElements = parentArray.logics - .whereType() + .whereType() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1386,7 +1446,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { final swizzle = _SwizzleConnect(dummySignals, unpacked: agg.logics - .whereType() + .whereType() .any((array) => array.numUnpackedDimensions > 0)); final swizzleInst = getSynthSubModuleInstantiation(swizzle) @@ -1488,7 +1548,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { bool elementsAllAbsent(SynthLogic parentArray) => parentArray.logics.every((logic) => - !(logic as LogicArray).elements.any(logicHasPresentSynthLogic)); + !(logic as BaseLogicArray).elements.any(logicHasPresentSynthLogic)); var droppedAny = true; while (droppedAny) { diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 2ca7afa988..4c8cb310aa 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -170,7 +170,12 @@ class SystemVerilogSynthesisResult extends SynthesisResult { module.tryOutput(sig.name) != null, 'Named output ${sig.name} not found in module ${module.name}.', ); - return _verilogPort('output', 'var', configuration.outputPortType, sig); + return _verilogPort( + 'output', + 'var', + configuration.outputPortType, + sig, + ); }); /// Representation of all inout port declarations in generated SV. @@ -215,8 +220,8 @@ class SystemVerilogSynthesisResult extends SynthesisResult { final comment = _assignedSignalNames.contains(sig.name) ? '' : traceHelper.signalComment(sig); - final decl = - '${sig.definitionType(useEnumType: configuration.generateEnums)} ' + final type = sig.definitionType(useEnumType: configuration.generateEnums); + final decl = '$type ' '${sig.definitionName(useEnumType: configuration.generateEnums)};'; declarations.add(_SvTraceHelper._pad(decl, comment)); } @@ -230,11 +235,6 @@ class SystemVerilogSynthesisResult extends SynthesisResult { // that only the *first* assignment for each signal is annotated. final tracedDsts = {}; - String rangeString(int upperIndex, int lowerIndex) => - upperIndex == lowerIndex - ? '[$upperIndex]' - : '[$upperIndex:$lowerIndex]'; - for (final assignment in _synthModuleDefinition.assignments) { assert( !(assignment.src.isNet && assignment.dst.isNet), @@ -242,39 +242,37 @@ class SystemVerilogSynthesisResult extends SynthesisResult { ' bidirectional net connections.', ); - var dstSliceString = ''; - var srcSliceString = ''; final assignsWholeDestination = assignment is! PartialSynthAssignment || (assignment.dstLowerIndex == 0 && assignment.dstUpperIndex == assignment.dst.width - 1); final normalizesWholeEnumDestination = assignment.dst.isEnum && assignsWholeDestination; + var destination = assignment.dst.name; + var source = assignment.src.name; if (assignment is RangeSynthAssignment) { if (!normalizesWholeEnumDestination) { - dstSliceString = rangeString( - assignment.dstUpperIndex, + destination = assignment.dst.rangeName( assignment.dstLowerIndex, + assignment.dstUpperIndex, ); } - if (assignment.src.isArray || - assignment.src.width <= 1 || - assignment.srcLowerIndex != 0 || - assignment.srcUpperIndex != assignment.src.width - 1) { - srcSliceString = rangeString( - assignment.srcUpperIndex, - assignment.srcLowerIndex, - ); - } + source = assignment.srcLowerIndex == 0 && + assignment.srcUpperIndex == assignment.src.width - 1 + ? assignment.src.name + : assignment.src.rangeName( + assignment.srcLowerIndex, + assignment.srcUpperIndex, + ); } else if (assignment is PartialSynthAssignment && assignment.width > 1 && !normalizesWholeEnumDestination) { - dstSliceString = rangeString( - assignment.dstUpperIndex, + destination = assignment.dst.rangeName( assignment.dstLowerIndex, + assignment.dstUpperIndex, ); } - var sourceExpression = '${assignment.src.name}$srcSliceString'; + var sourceExpression = source; final sourceIsPackedEnumInput = assignment.src.isEnum && _synthModuleDefinition.inputs.contains(assignment.src.resolved); @@ -283,6 +281,8 @@ class SystemVerilogSynthesisResult extends SynthesisResult { if (configuration.generateEnums && normalizesWholeEnumDestination && (sourceIsPackedEnumInput || + assignment.src is SynthLogicArrayElement || + assignment.src is SynthLogicArrayStructureElement || !assignment.src.isEnum || assignment is RangeSynthAssignment || !identical( @@ -293,8 +293,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { sourceExpression = "$enumType'($sourceExpression)"; } - final line = 'assign ${assignment.dst.name}$dstSliceString' - ' = $sourceExpression;'; + final line = 'assign $destination = $sourceExpression;'; // Emit the trace comment on the first assignment to this signal. final comment = tracedDsts.add(assignment.dst.name) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart index 717f9f30e0..2cf8c281a8 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart @@ -19,6 +19,10 @@ enum SystemVerilogPortType { /// Configuration for types in a SystemVerilog port declaration. class SystemVerilogPortTypeConfiguration { /// Whether the object type, such as `wire` or `var`, is explicit. + /// + /// When an output port omits its object type but includes an explicit data + /// type, SystemVerilog infers a variable. When both are omitted, it infers a + /// net. See IEEE 1800-2023 section 23.2.2.3. final SystemVerilogPortType objectType; /// Whether the data type, such as `logic`, is explicit. diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index 168ec0e01a..3046d371a0 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -9,18 +9,19 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; /// A [Swizzle] used by synthesis backends to explicitly assemble a -/// [LogicArray] from its elements. +/// [BaseLogicArray] from its elements. @internal class SynthArrayConcat extends Swizzle { /// The canonical base name for synthesized array concat operations. static const String operationName = 'array_concat'; - final LogicArray _destination; + final BaseLogicArray _destination; /// Creates a synthesis array concatenation from [signals]. - SynthArrayConcat(super.signals, {required LogicArray destination}) + SynthArrayConcat(super.signals, {required BaseLogicArray destination}) : _destination = destination, super(name: operationName); diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart index ec44468268..6bd5092658 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -9,9 +9,10 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; /// A [BusSubset] used by synthesis backends to explicitly extract a -/// [LogicArray] element from its packed parent representation. +/// [BaseLogicArray] element from its packed parent representation. @internal class SynthArraySlice extends BusSubset { /// The canonical base name for synthesized array slice operations. diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 448daa3dc2..b5eafb405a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -10,6 +10,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; @@ -173,7 +174,7 @@ class SynthLogic { /// Whether this represents a net. bool get isNet => // can just look at the first since nets and non-nets cannot be merged - logics.first.isNet || (isArray && (logics.first as LogicArray).isNet); + logics.first.isNet || (isArray && (logics.first as BaseLogicArray).isNet); /// Whether this represents an enum. bool get isEnum => characteristicEnum != null; @@ -269,7 +270,7 @@ class SynthLogic { /// without making the resulting signal clearable. bool get mergeable => !hasPreservedName && _constLogic == null; - /// True only if this represents a [LogicArray]. + /// True only if this represents a [BaseLogicArray]. final bool isArray; /// The chosen name of this. @@ -289,6 +290,18 @@ class SynthLogic { return _name!; } + /// Returns a reference to the inclusive packed range within this signal. + /// + /// Subclasses that already represent a selected portion of another signal + /// override this to compose the ranges rather than emit chained part + /// selects. + String rangeName(int lowerIndex, int upperIndex) { + assert(lowerIndex >= 0, 'Range lower index must not be negative.'); + assert(upperIndex >= lowerIndex, 'Range must not be reversed.'); + assert(upperIndex < width, 'Range must fit within the signal.'); + return '$name${_packedRangeSuffix(lowerIndex, upperIndex)}'; + } + /// The chosen name of this, or `null` if a name has not been picked or this /// has been replaced. String? get nameOrNull { @@ -369,7 +382,7 @@ class SynthLogic { Naming? namingOverride, bool constNameDisallowed = false, Object? generatedNameKey, - }) : isArray = initialLogic is LogicArray, + }) : isArray = initialLogic is BaseLogicArray, _generatedNameKey = generatedNameKey, _constNameDisallowed = constNameDisallowed { _addLogic(initialLogic, namingOverride: namingOverride); @@ -388,6 +401,12 @@ class SynthLogic { ) { assert(a != b, 'Cannot merge a SynthLogic with itself.'); + if (a.isArray != b.isArray) { + return null; + } + if (a.isArray && !_arrayRepresentationsCompatible(a, b)) { + return null; + } if (a.isEnum && b.isEnum && !_enumTypesCompatible(a, b)) { return null; } @@ -486,15 +505,8 @@ class SynthLogic { return false; } - if (a.isArray) { - final arrayA = a.logics.first as LogicArray; - final arrayB = b.logics.first as LogicArray; - if (arrayA.elementWidth != arrayB.elementWidth || - arrayA.numUnpackedDimensions != arrayB.numUnpackedDimensions || - !const ListEquality() - .equals(arrayA.dimensions, arrayB.dimensions)) { - return false; - } + if (a.isArray && !_arrayRepresentationsCompatible(a, b)) { + return false; } final preservedLogics = [ @@ -507,6 +519,54 @@ class SynthLogic { return preservedLogics.every((logic) => Namer.baseName(logic) == name); } + /// Whether two array signals have compatible declarations and element trees. + static bool _arrayRepresentationsCompatible(SynthLogic a, SynthLogic b) => + _arrayLayoutsCompatible( + a.logics.first as BaseLogicArray, + b.logics.first as BaseLogicArray, + ); + + static bool _arrayLayoutsCompatible( + BaseLogicArray a, + BaseLogicArray b, + ) { + if (a.elementWidth != b.elementWidth || + a.numUnpackedDimensions != b.numUnpackedDimensions || + !const ListEquality().equals(a.dimensions, b.dimensions) || + a.elements.length != b.elements.length) { + return false; + } + if (a.elements.isEmpty) { + return true; + } + return _elementLayoutsCompatible(a.elements.first, b.elements.first); + } + + static bool _elementLayoutsCompatible(Logic a, Logic b) { + if (a.width != b.width || a.isNet != b.isNet) { + return false; + } + if (a is BaseLogicArray || b is BaseLogicArray) { + return a is BaseLogicArray && + b is BaseLogicArray && + _arrayLayoutsCompatible(a, b); + } + if (a is LogicStructure || b is LogicStructure) { + if (a is! LogicStructure || + b is! LogicStructure || + a.runtimeType != b.runtimeType || + a.elements.length != b.elements.length) { + return false; + } + return a.elements.indexed.every((entry) { + final other = b.elements[entry.$1]; + return entry.$2.name == other.name && + _elementLayoutsCompatible(entry.$2, other); + }); + } + return true; + } + /// Merges [other] to be represented by `this` instead, and updates the /// [other] that it has been replaced. /// @@ -623,7 +683,15 @@ class SynthLogic { final logic = logics.first; if (isArray) { - final logicArr = logic as LogicArray; + final logicArr = logic as BaseLogicArray; + if (logicArr.elementWidth == 0 || + logicArr.dimensions.any((dimension) => dimension == 0)) { + throw SynthException( + 'SystemVerilog cannot represent zero-width array ' + '${logicArr.name} with dimensions ${logicArr.dimensions} and ' + 'element width ${logicArr.elementWidth}.', + ); + } final packedDimsBuf = StringBuffer(); final unpackedDimsBuf = StringBuffer(); @@ -701,7 +769,6 @@ class SynthLogicPackedBitReference extends SynthLogic { !packedBase.isArray, 'Packed reference base must not be an array.', ), - assert(!packedBase.isNet, 'Packed reference base must not be a net.'), assert( !packedBase.isConstant, 'Packed reference base must not be a constant.', @@ -713,6 +780,9 @@ class SynthLogicPackedBitReference extends SynthLogic { ), super(Logic()); + @override + bool get isNet => packedBase.resolved.isNet; + @override bool get needsDeclaration => false; @@ -737,9 +807,9 @@ class SynthLogicPackedBitReference extends SynthLogic { bitIndex < resolvedBase.width, 'Packed reference index must fit within its resolved base.', ); - final reference = '${resolvedBase.name}[$bitIndex]'; + final reference = resolvedBase.rangeName(bitIndex, bitIndex); assert( - Sanitizer.isSanitary(resolvedBase.name), + Sanitizer.isSanitary(resolvedBase.name.split('[').first), 'Packed reference base should be sanitary, but found $reference.', ); return reference; @@ -751,6 +821,15 @@ class SynthLogicPackedBitReference extends SynthLogic { final baseName = resolvedBase.nameOrNull; return baseName == null ? null : '$baseName[$bitIndex]'; } + + @override + String rangeName(int lowerIndex, int upperIndex) { + assert( + lowerIndex == 0 && upperIndex == 0, + 'A packed bit reference only contains bit zero.', + ); + return name; + } } /// A non-owning reference to a range of a packed [SynthLogic]. @@ -775,7 +854,6 @@ class SynthLogicPackedRangeReference extends SynthLogic { required super.parentSynthModuleDefinition, }) : assert( !packedBase.isArray, 'Packed reference base must not be an array.'), - assert(!packedBase.isNet, 'Packed reference base must not be a net.'), assert( !packedBase.isConstant, 'Packed reference base must not be a constant.', @@ -791,6 +869,9 @@ class SynthLogicPackedRangeReference extends SynthLogic { ), super(Logic(width: upperIndex - lowerIndex + 1)); + @override + bool get isNet => packedBase.resolved.isNet; + @override bool get needsDeclaration => false; @@ -815,9 +896,9 @@ class SynthLogicPackedRangeReference extends SynthLogic { upperIndex < resolvedBase.width, 'Packed reference index must fit within its resolved base.', ); - final reference = '${resolvedBase.name}[$upperIndex:$lowerIndex]'; + final reference = resolvedBase.rangeName(lowerIndex, upperIndex); assert( - Sanitizer.isSanitary(resolvedBase.name), + Sanitizer.isSanitary(resolvedBase.name.split('[').first), 'Packed reference base should be sanitary, but found $reference.', ); return reference; @@ -829,9 +910,20 @@ class SynthLogicPackedRangeReference extends SynthLogic { final baseName = resolvedBase.nameOrNull; return baseName == null ? null : '$baseName[$upperIndex:$lowerIndex]'; } + + @override + String rangeName(int lowerIndex, int upperIndex) { + assert(lowerIndex >= 0, 'Range lower index must not be negative.'); + assert(upperIndex >= lowerIndex, 'Range must not be reversed.'); + assert(upperIndex < width, 'Range must fit within the reference.'); + return packedBase.resolved.rangeName( + this.lowerIndex + lowerIndex, + this.lowerIndex + upperIndex, + ); + } } -/// Represents an element of a [LogicArray]. +/// Represents an element of a [BaseLogicArray]. /// /// Does not fully override or properly implement all characteristics of /// [SynthLogic], so this should be used cautiously. @@ -891,17 +983,19 @@ class SynthLogicArrayElement extends SynthLogic { } @override - String get name { - final parentArrayName = parentArray.replacement?.name ?? parentArray.name; - final n = '$parentArrayName[${logic.arrayIndex!}]'; - assert( - Sanitizer.isSanitary( - n.substring(0, n.contains('[') ? n.indexOf('[') : null), - ), - 'Array name should be sanitary, but found $n', - ); - return n; - } + String get name => logic is BaseLogicArray && + logic.parentStructure is! BaseLogicArray && + !logic.isNet + ? super.name + : _synthArrayReferenceName(logic, parentSynthModuleDefinition); + + @override + String rangeName(int lowerIndex, int upperIndex) => _synthArrayReferenceName( + logic, + parentSynthModuleDefinition, + lowerIndex: lowerIndex, + upperIndex: upperIndex, + ); @override String? get nameOrNull { @@ -915,13 +1009,13 @@ class SynthLogicArrayElement extends SynthLogic { /// The element of the [parentArray]. final Logic logic; - /// Creates an instance of an element of a [LogicArray]. + /// Creates an instance of an element of a [BaseLogicArray]. SynthLogicArrayElement( this.logic, { required super.parentSynthModuleDefinition, }) : assert( logic.isArrayMember, - 'Should only be used for elements in a LogicArray', + 'Should only be used for elements in a BaseLogicArray', ), super(logic) { // make sure we have created the synthLogic for the parent array @@ -933,3 +1027,266 @@ class SynthLogicArrayElement extends SynthLogic { ' parentArray=($parentArray), element ${logic.arrayIndex}, logic: $logic' ' logics contained: ${logics.map((e) => e.name).toList()}'; } + +/// Represents a scalar field within a structured [BaseLogicArray] element. +/// +/// The field has no standalone declaration: it is selected from the packed +/// array element that contains it. +@internal +class SynthLogicArrayStructureElement extends SynthLogic { + /// The scalar field represented by this synthesized signal. + final Logic logic; + + /// The synthesized nearest array containing [logic]. + SynthLogic get parentArray { + var parent = logic.parentStructure; + while (parent != null && parent is! BaseLogicArray) { + parent = parent.parentStructure; + } + if (parent == null) { + throw StateError('Structured array field has no array ancestor.'); + } + return parentSynthModuleDefinition.getSynthLogic(parent)!; + } + + /// The synthesized outermost array ancestor containing [logic]. + SynthLogic get rootArray { + BaseLogicArray? rootArray; + var parent = logic.parentStructure; + while (parent != null) { + if (parent is BaseLogicArray) { + rootArray = parent; + } + parent = parent.parentStructure; + } + if (rootArray == null) { + throw StateError('Structured array field has no array ancestor.'); + } + return parentSynthModuleDefinition.getSynthLogic(rootArray)!; + } + + @override + bool get needsDeclaration => false; + + @override + bool get mergeable => false; + + @override + bool isPort([Module? module]) => rootArray.isPort(module); + + @override + bool hasSrcConnectionsPresent() => + super.hasSrcConnectionsPresent() || rootArray.hasSrcConnectionsPresent(); + + @override + bool hasDstConnectionsPresent() => + super.hasDstConnectionsPresent() || rootArray.hasDstConnectionsPresent(); + + @override + String get name => + _synthArrayReferenceName(logic, parentSynthModuleDefinition); + + @override + String rangeName(int lowerIndex, int upperIndex) => _synthArrayReferenceName( + logic, + parentSynthModuleDefinition, + lowerIndex: lowerIndex, + upperIndex: upperIndex, + ); + + /// Creates a synthesized reference for a scalar structured-array field. + SynthLogicArrayStructureElement( + this.logic, { + required super.parentSynthModuleDefinition, + }) : assert( + _arrayLeafAncestor(logic) != null, + 'Structured array field must have an array ancestor', + ), + super(logic); + + /// Returns the configured array leaf that contains [logic], if any. + static Logic? _arrayLeafAncestor(Logic logic) { + var current = logic; + var parent = current.parentStructure; + while (parent != null) { + if (parent is BaseLogicArray) { + return current; + } + current = parent; + parent = current.parentStructure; + } + return null; + } +} + +/// Renders an array descendant using the declaration shape of its root array. +/// +/// Nested arrays are represented as packed payloads of the root element, so +/// their coordinates become a packed range instead of additional SV indices. +String _synthArrayReferenceName( + Logic target, + SynthModuleDefinition parentSynthModuleDefinition, { + int? lowerIndex, + int? upperIndex, +}) { + assert( + (lowerIndex == null) == (upperIndex == null), + 'Both range bounds must be provided together.', + ); + assert( + lowerIndex == null || lowerIndex >= 0, + 'Range lower index must not be negative.', + ); + assert( + upperIndex == null || + (upperIndex >= lowerIndex! && upperIndex < target.width), + 'Range must be ordered and fit within its target.', + ); + + var current = target; + BaseLogicArray? rootArray; + while (current.parentStructure != null) { + final parent = current.parentStructure!; + if (parent is BaseLogicArray) { + rootArray = parent; + } + current = parent; + } + if (rootArray == null) { + throw StateError('Array descendant has no array ancestor.'); + } + + final nestedArray = + rootArray.isNet ? null : _nearestNestedArray(target, rootArray); + if (nestedArray != null) { + return _synthRepresentedArrayReference( + target, + nestedArray, + parentSynthModuleDefinition, + lowerIndex: lowerIndex, + upperIndex: upperIndex, + ); + } + + return _synthRepresentedArrayReference( + target, + rootArray, + parentSynthModuleDefinition, + lowerIndex: lowerIndex, + upperIndex: upperIndex, + ); +} + +/// Renders [target] relative to the declaration represented by [array]. +String _synthRepresentedArrayReference( + Logic target, + BaseLogicArray array, + SynthModuleDefinition parentSynthModuleDefinition, { + int? lowerIndex, + int? upperIndex, +}) { + final indices = []; + var current = target; + while (!identical(current, array)) { + if (current.isArrayMember) { + indices.add(current.arrayIndex!); + } + current = current.parentStructure!; + } + final orderedIndices = indices.reversed.toList(growable: false); + final arrayRank = array.dimensions.length; + final arraySynth = parentSynthModuleDefinition.getSynthLogic(array)!; + final arrayName = arraySynth.replacement?.name ?? arraySynth.name; + if (orderedIndices.length < arrayRank) { + if (target is! BaseLogicArray) { + throw StateError('Array descendant is missing declared array indices.'); + } + final partialReference = + '$arrayName${orderedIndices.map((index) => '[$index]').join()}'; + return lowerIndex == null + ? partialReference + : '$partialReference${_packedRangeSuffix(lowerIndex, upperIndex!)}'; + } + + final declarationIndices = + orderedIndices.take(arrayRank).map((index) => '[$index]').join(); + final arrayElement = _arrayElementAt(array, orderedIndices, arrayRank); + if (identical(target, arrayElement) && lowerIndex == null) { + return '$arrayName$declarationIndices'; + } + + final offset = _packedOffset(arrayElement, target); + final lower = offset + (lowerIndex ?? 0); + final upper = offset + (upperIndex ?? target.width - 1); + final range = _packedRangeSuffix(lower, upper); + final rendered = '$arrayName$declarationIndices$range'; + assert( + Sanitizer.isSanitary(arrayName), + 'Array name should be sanitary, but found $rendered', + ); + return rendered; +} + +String _packedRangeSuffix(int lowerIndex, int upperIndex) => + upperIndex == lowerIndex ? '[$lowerIndex]' : '[$upperIndex:$lowerIndex]'; + +/// Finds the nearest nested array that is an array-valued structure field. +BaseLogicArray? _nearestNestedArray( + Logic target, + BaseLogicArray rootArray, +) { + var current = target; + while (true) { + if (current is BaseLogicArray && + !identical(current, rootArray) && + current.parentStructure is LogicStructure && + current.parentStructure is! BaseLogicArray) { + return current; + } + final parent = current.parentStructure; + if (parent == null) { + return null; + } + current = parent; + } +} + +/// Finds the root element containing a target from row-major array indices. +Logic _arrayElementAt( + BaseLogicArray rootArray, + List indices, + int rootRank, +) { + Logic current = rootArray; + for (final index in indices.take(rootRank)) { + current = (current as LogicStructure).elements[index]; + } + return current; +} + +/// Returns the packed offset of [target] within [container]. +int _packedOffset(Logic container, Logic target) { + if (identical(container, target)) { + return 0; + } + if (container is! LogicStructure) { + throw StateError('Target is not contained in ${container.name}.'); + } + + var offset = 0; + for (final element in container.elements) { + if (_containsLogic(element, target)) { + return offset + _packedOffset(element, target); + } + offset += element.width; + } + throw StateError('Target is not contained in ${container.name}.'); +} + +bool _containsLogic(Logic container, Logic target) { + if (identical(container, target)) { + return true; + } + return container is LogicStructure && + container.elements.any((element) => _containsLogic(element, target)); +} diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index e0dbbe3e74..d53b696486 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -13,6 +13,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; @@ -337,6 +338,11 @@ class SynthModuleDefinition { logic, parentSynthModuleDefinition: this, ); + } else if (logic is! LogicStructure && _hasTopLevelArrayAncestor(logic)) { + newSynth = SynthLogicArrayStructureElement( + logic, + parentSynthModuleDefinition: this, + ); } else { final parentModule = logic.parentModule; final disallowConstName = (logic.isInput || logic.isInOut) && @@ -378,7 +384,7 @@ class SynthModuleDefinition { logicToSynthMap[logic] = newSynth; - if (logic is LogicArray) { + if (logic is BaseLogicArray) { // if we are an array, make sure we go down the stack of elements too logic.elements.forEach(getSynthLogic); } @@ -387,6 +393,21 @@ class SynthModuleDefinition { } } + /// Whether [logic] belongs to an array that is represented directly. + static bool _hasTopLevelArrayAncestor(Logic logic) { + var current = logic; + var parent = current.parentStructure; + BaseLogicArray? rootArray; + while (parent != null) { + if (parent is BaseLogicArray) { + rootArray = parent; + } + current = parent; + parent = current.parentStructure; + } + return rootArray != null && rootArray.parentStructure == null; + } + /// A [List] of supporting modules that need to be instantiated within this /// definition. final List supportingModules = []; @@ -394,8 +415,13 @@ class SynthModuleDefinition { /// Retains [signal] and every array ancestor needed to name and declare it. void _retainInternalSignal(SynthLogic signal) { final signalAndAncestors = [signal]; - while (signal is SynthLogicArrayElement) { - signal = signal.parentArray.resolved; + while (signal is SynthLogicArrayElement || + signal is SynthLogicArrayStructureElement) { + signal = switch (signal) { + SynthLogicArrayElement() => signal.parentArray.resolved, + SynthLogicArrayStructureElement() => signal.parentArray.resolved, + _ => throw StateError('Unexpected synthesized array signal'), + }; signalAndAncestors.add(signal); } @@ -415,7 +441,7 @@ class SynthModuleDefinition { /// the module, or for driving the input of a sub-module. @protected void _partialAssignStructPort(LogicStructure port) { - assert(port is! LogicArray, 'Should only be used on non-array structs'); + assert(port is! BaseLogicArray, 'Should only be used on non-array structs'); final portSynth = getSynthLogic(port)!; @@ -492,8 +518,10 @@ class SynthModuleDefinition { final outputSynth = getSynthLogic(output)!; outputs.add(outputSynth); - if (output is LogicStructure && output is! LogicArray) { + if (output is LogicStructure && output is! BaseLogicArray) { _partialAssignStructPort(output); + } else if (output is BaseLogicArray) { + _connectNestedArrayFields(output); } } @@ -502,8 +530,10 @@ class SynthModuleDefinition { final inputSynth = getSynthLogic(input)!; inputs.add(inputSynth); - if (input is LogicStructure && input is! LogicArray) { + if (input is LogicStructure && input is! BaseLogicArray) { _subsetReceiveStructPort(input); + } else if (input is BaseLogicArray) { + _connectNestedArrayFields(input, receive: true); } } @@ -511,10 +541,12 @@ class SynthModuleDefinition { for (final inOut in module.inOuts.values) { inOuts.add(getSynthLogic(inOut)!); - if (inOut is LogicStructure && inOut is! LogicArray) { + if (inOut is LogicStructure && inOut is! BaseLogicArray) { // for nets, we can just use the normal bus subset here in either // direction! _subsetReceiveStructPort(inOut); + } else if (inOut is BaseLogicArray) { + _connectNestedArrayFields(inOut, receive: true); } } @@ -536,18 +568,26 @@ class SynthModuleDefinition { subModule.inputs.values .whereType() - .where((e) => e is! LogicArray) + .where((e) => e is! BaseLogicArray) .forEach(_partialAssignStructPort); - + subModule.inputs.values + .whereType() + .forEach(_connectNestedArrayFields); subModule.outputs.values .whereType() - .where((e) => e is! LogicArray) + .where((e) => e is! BaseLogicArray) .forEach(_subsetReceiveStructPort); + subModule.outputs.values + .whereType() + .forEach((array) => _connectNestedArrayFields(array, receive: true)); subModule.inOuts.values .whereType() - .where((e) => e is! LogicArray) + .where((e) => e is! BaseLogicArray) .forEach(_subsetReceiveStructPort); + subModule.inOuts.values + .whereType() + .forEach((array) => _connectNestedArrayFields(array, receive: true)); } // search for other modules contained within this module @@ -1130,7 +1170,7 @@ class SynthModuleDefinition { } candidatesByArray.forEach((parentArray, arrayCandidates) { final allElementSynthLogics = parentArray.logics - .whereType() + .whereType() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1499,6 +1539,91 @@ class SynthModuleDefinition { } } + /// Connects array-valued fields nested below an array port to the packed + /// array-element representation used at the module boundary. + /// + /// Nested arrays are retained as separate internal signals, while their + /// scalar fields occupy the containing structure's packed bits. These + /// connections preserve that existing representation without changing names + /// or declarations. + void _connectNestedArrayFields( + BaseLogicArray port, { + bool receive = false, + }) { + void visit(Logic current, SynthLogic packedParent) { + if (current is BaseLogicArray) { + for (final element in current.elements) { + visit(element, getSynthLogic(element)!); + } + return; + } + + if (current is! LogicStructure) { + return; + } + + var index = 0; + for (final leafElement in current.leafElements) { + var ancestor = leafElement.parentStructure; + var isNestedArrayField = false; + while (ancestor != null && ancestor != current) { + if (ancestor is BaseLogicArray) { + isNestedArrayField = true; + break; + } + ancestor = ancestor.parentStructure; + } + + if (isNestedArrayField) { + final leafSynth = getSynthLogic(leafElement)!; + if (!port.isNet) { + _retainInternalSignal(leafSynth); + } + if (receive) { + if (port.isNet) { + final packedReference = leafElement.width == 1 + ? SynthLogicPackedBitReference( + packedParent, + index, + parentSynthModuleDefinition: this, + ) + : SynthLogicPackedRangeReference( + packedParent, + index, + index + leafElement.width - 1, + parentSynthModuleDefinition: this, + ); + assignments.add(SynthAssignment(leafSynth, packedReference)); + } else { + assignments.add( + RangeSynthAssignment( + packedParent, + leafSynth, + srcUpperIndex: index + leafElement.width - 1, + srcLowerIndex: index, + dstUpperIndex: leafElement.width - 1, + dstLowerIndex: 0, + ), + ); + } + } else { + assignments.add( + PartialSynthAssignment( + leafSynth, + packedParent, + dstUpperIndex: index + leafElement.width - 1, + dstLowerIndex: index, + ), + ); + } + } + index += leafElement.width; + } + } + + visit(port, getSynthLogic(port)!); + } + /// Updates all sub-module instantiations with information about which /// [SynthLogic] should be used for their ports. void _assignSubmodulePortMapping() { @@ -1804,7 +1929,7 @@ class SynthModuleDefinition { return false; } final parentLogic = signal.parentArray.resolved.logics.singleOrNull; - return parentLogic is LogicArray && + return parentLogic is BaseLogicArray && parentLogic.dimensions.length == 1 && parentLogic.elementWidth == 1 && parentLogic.numUnpackedDimensions == 0; @@ -2062,7 +2187,7 @@ class SynthModuleDefinition { } final srcLogic = srcArray.logics.first; final dstLogic = dstArray.logics.first; - if (srcLogic is! LogicArray || dstLogic is! LogicArray) { + if (srcLogic is! BaseLogicArray || dstLogic is! BaseLogicArray) { return false; } @@ -2453,7 +2578,8 @@ class SynthModuleDefinition { } final dst = assignment.dst.resolved; - if (dst.width <= 1 || dst.logics.any((logic) => logic is LogicArray)) { + if (dst.width <= 1 || + dst.logics.any((logic) => logic is BaseLogicArray)) { updatedAssignments.add(assignment); continue; } @@ -2718,7 +2844,7 @@ class SynthModuleDefinition { return false; } final logic = base.logics.first; - return logic is LogicArray && + return logic is BaseLogicArray && logic.dimensions.length == 1 && logic.elementWidth == 1 && logic.numUnpackedDimensions == 0; @@ -3827,7 +3953,7 @@ class SynthModuleDefinition { } final arrayLogic = parentArray.logics.first; - if (arrayLogic is! LogicArray || + if (arrayLogic is! BaseLogicArray || indexedInputs.length != arrayLogic.elements.length) { continue; } @@ -3888,7 +4014,7 @@ class SynthModuleDefinition { } final logic = intermediate.logics.firstOrNull; - return logic is! LogicArray || logic.numUnpackedDimensions == 0; + return logic is! BaseLogicArray || logic.numUnpackedDimensions == 0; } /// Whether [intermediate] has the unnamed packed-array shape generated by @@ -3903,7 +4029,7 @@ class SynthModuleDefinition { } final arrayLogic = intermediate.logics.singleOrNull; - return arrayLogic is LogicArray && arrayLogic.naming == Naming.unnamed; + return arrayLogic is BaseLogicArray && arrayLogic.naming == Naming.unnamed; } /// Whether [swizzleOutput] feeds a full-width disposable internal signal. @@ -3929,7 +4055,7 @@ class SynthModuleDefinition { return false; } - return dst.logics.singleOrNull is! LogicArray; + return dst.logics.singleOrNull is! BaseLogicArray; }); } @@ -4123,12 +4249,13 @@ class SynthModuleDefinition { if (mergedAway.isArray) { for (final (keptElementIndex, keptElementLogic) - in (kept.logics.first as LogicArray).elements.indexed) { + in (kept.logics.first as BaseLogicArray).elements.indexed) { // should be safe to just check the first logic's elements since they // should all be the same synth, and arrays only merge with arrays final keptElement = getSynthLogic(keptElementLogic)!; final mergedAwayElement = getSynthLogic( - (mergedAway.logics.first as LogicArray).elements[keptElementIndex], + (mergedAway.logics.first as BaseLogicArray) + .elements[keptElementIndex], )!; if (keptElement == mergedAwayElement) { diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart index db972c9290..fbcbc0b522 100644 --- a/lib/src/synthesizers/utilities/synth_structure_layout.dart +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -8,6 +8,7 @@ // Author: Desmond Kirkpatrick import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; /// An exclusive-end bit range within a packed [LogicStructure]. typedef SynthStructureBitRange = ({int start, int end}); @@ -53,7 +54,7 @@ class SynthStructureLayout { fieldPath: fieldPath, indexInParent: index, )); - if (element is LogicStructure && element is! LogicArray) { + if (element is LogicStructure && element is! BaseLogicArray) { _addStructure(element, offset, path, fieldPath); } offset = end; diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index 3c693acac8..9fdec1130d 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -14,6 +14,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart'; import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; import 'package:rohd/src/utilities/web.dart'; @@ -91,15 +92,15 @@ class Vector { final value = LogicValue.of(inputValues[signalName], width: signal.width); return '${packedInputDrivers[signalName]} = $value;'; - } else if (signal is LogicArray) { + } else if (signal is BaseLogicArray) { final arrAssigns = StringBuffer(); var index = 0; final fullVal = LogicValue.of(inputValues[signalName], width: signal.width); - for (final leaf in signal.leafElements) { - final subVal = fullVal.getRange(index, index + leaf.width); - arrAssigns.writeln('${leaf.structureName} = $subVal;'); - index += leaf.width; + for (final element in signal.arrayElements) { + final subVal = fullVal.getRange(index, index + element.width); + arrAssigns.writeln('${element.structureName} = $subVal;'); + index += element.width; } return arrAssigns.toString(); } else { @@ -121,13 +122,13 @@ class Vector { ); final inputStimulus = inputValues.toString(); - if (outputPort is LogicArray) { + if (outputPort is BaseLogicArray) { var index = 0; - for (final leaf in outputPort.leafElements) { - final subVal = expectedValue.getRange(index, index + leaf.width); + for (final element in outputPort.arrayElements) { + final subVal = expectedValue.getRange(index, index + element.width); checksList.add(_errorCheckString( - leaf.structureName, subVal, subVal, inputStimulus)); - index += leaf.width; + element.structureName, subVal, subVal, inputStimulus)); + index += element.width; } } else { checksList.add(_errorCheckString( @@ -707,9 +708,12 @@ abstract class SimCompare { {String Function(String original)? adjust, String? signalTypeOverride}) { final signal = module.signals.firstWhere((e) => e.name == signalName); + final isOutput = module.tryOutput(signalName) != null; final signalType = signalTypeOverride ?? - ((signal is LogicNet || (signal is LogicArray && signal.isNet)) + ((isOutput || + signal is LogicNet || + (signal is BaseLogicArray && signal.isNet)) ? 'wire' : 'logic'); @@ -717,7 +721,7 @@ abstract class SimCompare { signalName = adjust(signalName); } - if (signal is LogicArray) { + if (signal is BaseLogicArray) { final unpackedDims = signal.dimensions.getRange(0, signal.numUnpackedDimensions); final packedDims = signal.dimensions @@ -759,7 +763,7 @@ abstract class SimCompare { if (usePackedInputDrivers) { for (final signalName in allSignals) { final signal = module.tryInput(signalName); - if (signal is! LogicArray || signal.numUnpackedDimensions == 0) { + if (signal is! BaseLogicArray) { continue; } final driver = @@ -767,10 +771,10 @@ abstract class SimCompare { packedInputDrivers[signalName] = driver; packedInputDeclarations.add('logic [${signal.width - 1}:0] $driver;'); var offset = 0; - for (final leaf in signal.leafElements) { - packedInputDeclarations.add('assign ${leaf.structureName} = ' - '$driver[${offset + leaf.width - 1}:$offset];'); - offset += leaf.width; + for (final element in signal.arrayElements) { + packedInputDeclarations.add('assign ${element.structureName} = ' + '$driver[${offset + element.width - 1}:$offset];'); + offset += element.width; } } } diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart index 42f8f6f24c..c3826dc162 100644 --- a/lib/src/values/logic_value.dart +++ b/lib/src/values/logic_value.dart @@ -38,6 +38,12 @@ abstract class LogicValue implements Comparable { const LogicValue._(this.width) : assert(width >= 0, 'Width must be greater than or equal to 0.'); + /// Returns the ordinary packed representation of this value. + /// + /// Shaped value subtypes override this to provide their bit-level fallback. + // ignore: avoid_returning_this - ordinary values are already packed. + LogicValue get packed => this; + /// Converts `bool` [value] to a valid [LogicValue] with 1 bits either /// one or zero. // ignore: avoid_positional_boolean_parameters @@ -305,7 +311,8 @@ abstract class LogicValue implements Comparable { // shift small chunks in together before shifting BigInt's, since // shifting BigInt's is expensive - for (final lv in it) { + for (final value in it) { + final lv = value.packed; final lvPlusSmall = lv.width + smallBuffer.width; if (lvPlusSmall <= INT_BITS) { smallBuffer = lv._concatenate(smallBuffer); @@ -527,11 +534,13 @@ abstract class LogicValue implements Comparable { return false; } - if (other.width != width) { + final left = packed; + final right = other.packed; + if (right.width != left.width) { return false; } - return _equals(other); + return left._equals(right); } /// Returns true iff all bits of `this` are equal to [other]. @@ -1214,13 +1223,15 @@ abstract class LogicValue implements Comparable { LogicValue other, LogicValue Function(LogicValue, LogicValue) op, ) { - if (width != other.width) { + final left = packed; + final right = other.packed; + if (left.width != right.width) { throw Exception('Widths must match, but found $this and $other'); } - if (other is _FilledLogicValue && this is! _FilledLogicValue) { - return op(other, this); + if (right is _FilledLogicValue && left is! _FilledLogicValue) { + return op(right, left); } - return op(this, other); + return op(left, right); } /// Calculates the absolute value, assuming that the diff --git a/lib/src/values/logic_value_array.dart b/lib/src/values/logic_value_array.dart new file mode 100644 index 0000000000..866dfe39b6 --- /dev/null +++ b/lib/src/values/logic_value_array.dart @@ -0,0 +1,171 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_value_array.dart +// Definition of multi-dimensional logic value arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +part of 'values.dart'; + +/// A shaped [LogicValue] containing fixed-width [LogicValue] array elements. +/// +/// The nested constructor infers shape and element width. Use +/// [LogicValueArray.fromFlat] when values are already row-major or when an +/// empty array requires explicit shape and width metadata. +class LogicValueArray extends TypedLogicValueArray { + /// Creates a value array from nested [values]. + factory LogicValueArray(List values) { + final nested = + _parseNestedArray(values, argumentName: 'values'); + if (nested.values.isEmpty) { + throw ArgumentError.value(values, 'values', + 'Cannot infer elementWidth from empty input; use fromFlat.'); + } + final elementWidth = nested.values.first.width; + _validateElementWidths(nested.values, elementWidth, 'values'); + return LogicValueArray._stored( + nested.dimensions, elementWidth, nested.values); + } + + /// Creates a value array from row-major [values]. + /// + /// For nonempty input, [elementWidth] defaults to the width of the first + /// value and all values must have that width. Empty input requires an + /// explicit [elementWidth]. + factory LogicValueArray.fromFlat( + List dimensions, Iterable values, + {int? elementWidth}) { + final normalizedDimensions = _validateValueArrayDimensions(dimensions); + final packedValues = values.toList(growable: false); + _validateValueCount(normalizedDimensions, packedValues.length, 'values'); + if (packedValues.isEmpty && elementWidth == null) { + throw ArgumentError.value( + values, 'values', 'An elementWidth is required for empty input.'); + } + final resolvedElementWidth = elementWidth ?? packedValues.first.width; + _validateValueArrayElementWidth(resolvedElementWidth); + _validateElementWidths(packedValues, resolvedElementWidth, 'values'); + return LogicValueArray._stored( + normalizedDimensions, resolvedElementWidth, packedValues); + } + + /// Creates an empty, zero-width value array. + factory LogicValueArray.empty() => + LogicValueArray.fromFlat(const [0], const [], elementWidth: 0); + + /// Generates values from row-major multidimensional indices. + factory LogicValueArray.generate( + List dimensions, LogicValue Function(List indices) generator, + {int? elementWidth}) { + final normalizedDimensions = _validateValueArrayDimensions(dimensions); + final elementCount = _valueArrayLength(normalizedDimensions); + final values = [ + for (var index = 0; index < elementCount; index++) + generator(_valueArrayIndices(normalizedDimensions, index)), + ]; + return LogicValueArray.fromFlat( + normalizedDimensions, + values, + elementWidth: elementWidth, + ); + } + + /// Creates a value array from nested integer [values]. + factory LogicValueArray.fromInts( + List values, { + required int elementWidth, + }) { + final nested = _parseNestedArray(values, argumentName: 'values'); + if (nested.values.isEmpty) { + throw ArgumentError.value(values, 'values', + 'Cannot infer dimensions from empty input; use fromFlatInts.'); + } + return LogicValueArray.fromFlat( + nested.dimensions, + nested.values.map((value) => LogicValue.ofInt(value, elementWidth)), + elementWidth: elementWidth, + ); + } + + /// Creates a value array from flat row-major integer [values]. + factory LogicValueArray.fromFlatInts( + List dimensions, Iterable values, + {required int elementWidth}) => + LogicValueArray.fromFlat( + dimensions, + values.map((value) => LogicValue.ofInt(value, elementWidth)), + elementWidth: elementWidth, + ); + + /// Stacks equally shaped arrays along a new outer dimension. + factory LogicValueArray.stack(Iterable arrays) { + final slices = arrays.toList(growable: false); + if (slices.isEmpty) { + throw ArgumentError.value(arrays, 'arrays', 'Must not be empty.'); + } + + final first = slices.first; + slices.skip(1).forEach(first._checkStackCompatible); + return LogicValueArray._stored( + [slices.length, ...first.dimensions], + first.elementWidth, + slices.expand((slice) => slice.arrayValues).toList(growable: false), + ); + } + + /// Stores [values] that are already-normalized packed inputs using the + /// identity codec. + /// + /// The semantic and packed representations are the same for + /// [LogicValueArray], so this constructor avoids re-encoding them. + LogicValueArray._stored( + List dimensions, + int elementWidth, + List values, + ) : super._normalized( + dimensions, + elementWidth, + values, + values, + LogicValueCodec.logicValue, + ); + + @override + Iterable get majorSlices => + super.majorSlices.cast(); + + @override + LogicValueArray map(LogicValue Function(LogicValue value) transform) => + LogicValueArray.fromFlat( + dimensions, + arrayValues.map(transform), + elementWidth: elementWidth, + ); + + @override + LogicValueArray indexedMap( + LogicValue Function(List indices, LogicValue value) transform, + ) => + LogicValueArray.fromFlat( + dimensions, + indexedValues.map((entry) => transform(entry.$1, entry.$2)), + elementWidth: elementWidth, + ); + + @override + LogicValueArray reshape(List newDimensions) => + super.reshape(newDimensions) as LogicValueArray; + + @override + LogicValueArray transpose2D() => super.transpose2D() as LogicValueArray; + + @override + LogicValueArray _createStored( + List dimensions, + List arrayValues, + List packedElements, + ) => + LogicValueArray._stored(dimensions, elementWidth, packedElements); +} diff --git a/lib/src/values/typed_logic_value_array.dart b/lib/src/values/typed_logic_value_array.dart new file mode 100644 index 0000000000..8ce8bbcda6 --- /dev/null +++ b/lib/src/values/typed_logic_value_array.dart @@ -0,0 +1,530 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_logic_value_array.dart +// Definition of typed multi-dimensional logic value arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +part of 'values.dart'; + +/// Returns an ordinary packed value unchanged when decoding. +LogicValue _decodeIdentityLogicValue(LogicValue value) => value; + +/// Returns an ordinary packed value unchanged when encoding. +LogicValue _encodeIdentityLogicValue(LogicValue value) => value; + +/// Converts semantic values of type [T] to and from packed [LogicValue]s. +class LogicValueCodec { + /// Converts a packed value into a semantic value. + final T Function(LogicValue value) decode; + + /// Type-erased encoder retained to support the generic [encode] method. + final Function _encode; + + /// Creates a bidirectional value [decode]/[encode] codec. + const LogicValueCodec({ + required this.decode, + required LogicValue Function(T value) encode, + }) : _encode = encode; + + /// Codec used when semantic values are already [LogicValue]s. + static const logicValue = LogicValueCodec( + decode: _decodeIdentityLogicValue, + encode: _encodeIdentityLogicValue, + ); + + /// Converts a semantic [value] into its packed representation. + LogicValue encode(T value) => + (_encode as LogicValue Function(T value))(value); +} + +/// A multidimensional array of semantic values backed by packed logic values. +/// +/// [arrayValues] contains one value for each position in [dimensions], in +/// row-major order. The ordinary [LogicValue] contract applies to the packed +/// representation: [width] and deprecated `length` count bits, `[]` indexes +/// packed bits, and equality and hashing do not consider shape. +/// +/// The nested constructor infers dimensions from its input. The root list +/// always represents an array dimension. Below the root, an object matching [T] +/// is treated as a semantic value before considering whether it is also a list. +/// Use [TypedLogicValueArray.fromFlat] when list-valued semantic data is +/// ambiguous or when empty data requires explicit shape and width metadata. +/// +/// Values are normalized through [codec] at construction: each value is +/// encoded and then immediately decoded. The packed representation is +/// authoritative, so lossy codecs expose their normalized semantic values. +/// Decoded semantic elements are exposed by reference; mutating a mutable +/// semantic object does not update the stored packed bits. Callers using a +/// mutable [T] are responsible for treating those objects consistently with +/// this snapshot semantics. +class TypedLogicValueArray extends LogicValue { + /// The number of elements at each array level. + final List dimensions; + + /// Width of each packed array element. + final int elementWidth; + + /// Codec used at each packed element boundary. + final LogicValueCodec codec; + + /// Semantic values in row-major order, one for each array position. + final List _arrayValues; + + /// Packed values corresponding position-for-position with [_arrayValues]. + final List _packedElements; + + /// The ordinary packed representation of this shaped value. + @override + final LogicValue packed; + + /// Creates a typed value array from nested [values]. + /// + /// Empty or ragged nested input is rejected. Use + /// [TypedLogicValueArray.fromFlat] to construct an empty array because its + /// element width cannot be inferred. + factory TypedLogicValueArray( + List values, { + required LogicValueCodec codec, + }) { + final nested = _parseNestedArray(values, argumentName: 'values'); + if (nested.values.isEmpty) { + throw ArgumentError.value(values, 'values', + 'Cannot infer elementWidth from empty input; use fromFlat.'); + } + + final encoded = nested.values.map(codec.encode).toList(growable: false); + final elementWidth = encoded.first.width; + _validateElementWidths(encoded, elementWidth, 'values'); + return TypedLogicValueArray._normalized( + nested.dimensions, + elementWidth, + encoded.map(codec.decode).toList(growable: false), + encoded, + codec, + ); + } + + /// Creates a typed value array from row-major [values] and explicit metadata. + factory TypedLogicValueArray.fromFlat( + List dimensions, + int elementWidth, + Iterable values, { + required LogicValueCodec codec, + }) { + final normalizedDimensions = _validateValueArrayDimensions(dimensions); + _validateValueArrayElementWidth(elementWidth); + final semanticValues = values.toList(growable: false); + _validateValueCount(normalizedDimensions, semanticValues.length, 'values'); + final encoded = semanticValues.map(codec.encode).toList(growable: false); + _validateElementWidths(encoded, elementWidth, 'values'); + return TypedLogicValueArray._normalized( + normalizedDimensions, + elementWidth, + encoded.map(codec.decode).toList(growable: false), + encoded, + codec, + ); + } + + /// Decodes row-major packed [values] with [codec]. + factory TypedLogicValueArray.fromPacked( + List dimensions, + int elementWidth, + Iterable values, { + required LogicValueCodec codec, + }) { + final normalizedDimensions = _validateValueArrayDimensions(dimensions); + _validateValueArrayElementWidth(elementWidth); + final packedValues = values.toList(growable: false); + _validateValueCount(normalizedDimensions, packedValues.length, 'values'); + _validateElementWidths(packedValues, elementWidth, 'values'); + return TypedLogicValueArray._normalized( + normalizedDimensions, + elementWidth, + packedValues.map(codec.decode).toList(growable: false), + packedValues, + codec, + ); + } + + /// Decodes the packed elements of [values] with [codec]. + factory TypedLogicValueArray.fromLogicValueArray( + TypedLogicValueArray values, { + required LogicValueCodec codec, + }) => + TypedLogicValueArray.fromPacked( + values.dimensions, + values.elementWidth, + values._packedElements, + codec: codec, + ); + + /// Stacks equally shaped arrays along a new outer dimension. + /// + /// All arrays must use the identical [LogicValueCodec] instance. Codec + /// functions cannot be compared for semantic equivalence. + factory TypedLogicValueArray.stack(Iterable> arrays) { + final slices = arrays.toList(growable: false); + if (slices.isEmpty) { + throw ArgumentError.value(arrays, 'arrays', 'Must not be empty.'); + } + + final first = slices.first; + slices.skip(1).forEach(first._checkStackCompatible); + return TypedLogicValueArray._normalized( + [slices.length, ...first.dimensions], + first.elementWidth, + slices.expand((slice) => slice._arrayValues).toList(growable: false), + slices.expand((slice) => slice._packedElements).toList(growable: false), + first.codec, + ); + } + + /// Stores already-normalized semantic and packed values without invoking + /// [codec] again. + /// + /// [arrayValues] and [packedElements] must contain corresponding entries + /// in row-major order. + TypedLogicValueArray._normalized( + List dimensions, + this.elementWidth, + List arrayValues, + List packedElements, + this.codec, + ) : dimensions = List.unmodifiable(dimensions), + _arrayValues = List.unmodifiable(arrayValues), + _packedElements = List.unmodifiable(packedElements), + packed = LogicValue.ofIterable(packedElements), + super._(_valueArrayLength(dimensions) * elementWidth) { + assert(_arrayValues.length == _valueArrayLength(dimensions), + 'Semantic value count must match dimensions.'); + assert(_packedElements.length == _arrayValues.length, + 'Packed and semantic value counts must match.'); + assert(_packedElements.every((value) => value.width == elementWidth), + 'Packed elements must match elementWidth.'); + assert( + packed.width == width, 'Packed width must match the LogicValue width.'); + } + + /// Semantic array elements in row-major order. + List get arrayValues => _arrayValues; + + /// Values paired with their row-major multidimensional indices. + Iterable<(List, T)> get indexedValues => Iterable.generate( + arrayValues.length, + (index) => ( + _valueArrayIndices(dimensions, index), + _arrayValues[index], + ), + ); + + /// Slices along the first dimension. + Iterable> get majorSlices sync* { + if (dimensions.length < 2) { + throw StateError('majorSlices requires at least two dimensions.'); + } + + final sliceDimensions = dimensions.sublist(1); + final sliceElementCount = _valueArrayLength(sliceDimensions); + for (var slice = 0; slice < dimensions.first; slice++) { + final start = slice * sliceElementCount; + final end = start + sliceElementCount; + yield _createStored( + sliceDimensions, + _arrayValues.sublist(start, end), + _packedElements.sublist(start, end), + ); + } + } + + /// Returns the semantic value at multidimensional [indices]. + T at(List indices) => + _arrayValues[_valueArrayFlatIndex(dimensions, indices)]; + + /// Maps semantic values while preserving shape and codec. + TypedLogicValueArray map(T Function(T value) transform) => + TypedLogicValueArray.fromFlat( + dimensions, + elementWidth, + _arrayValues.map(transform), + codec: codec, + ); + + /// Maps semantic values with their multidimensional indices. + TypedLogicValueArray indexedMap( + T Function(List indices, T value) transform, + ) => + TypedLogicValueArray.fromFlat( + dimensions, + elementWidth, + indexedValues.map((entry) => transform(entry.$1, entry.$2)), + codec: codec, + ); + + /// Returns the same row-major values with [newDimensions]. + TypedLogicValueArray reshape(List newDimensions) { + final normalizedDimensions = _validateValueArrayDimensions(newDimensions); + if (_valueArrayLength(normalizedDimensions) != arrayValues.length) { + throw ArgumentError.value(newDimensions, 'newDimensions', + 'Must contain ${arrayValues.length} array elements.'); + } + return _createStored(normalizedDimensions, _arrayValues, _packedElements); + } + + /// Transposes this two-dimensional value array. + TypedLogicValueArray transpose2D() { + if (dimensions.length != 2) { + throw StateError('Expected exactly two dimensions, got $dimensions.'); + } + final newDimensions = [dimensions[1], dimensions[0]]; + final semanticValues = []; + final packedElements = []; + for (var row = 0; row < newDimensions[0]; row++) { + for (var column = 0; column < newDimensions[1]; column++) { + final source = _valueArrayFlatIndex(dimensions, [column, row]); + semanticValues.add(_arrayValues[source]); + packedElements.add(_packedElements[source]); + } + } + return _createStored(newDimensions, semanticValues, packedElements); + } + + /// Rebuilds a shaped value from already-normalized semantic and packed data. + TypedLogicValueArray _createStored( + List dimensions, + List arrayValues, + List packedElements, + ) => + TypedLogicValueArray._normalized( + dimensions, elementWidth, arrayValues, packedElements, codec); + + /// Validates shape, width, and codec identity for + /// [TypedLogicValueArray.stack]. + void _checkStackCompatible(TypedLogicValueArray other) { + if (!identical(codec, other.codec)) { + throw ArgumentError.value( + other.codec, 'arrays', 'All arrays must use the identical codec.'); + } + if (elementWidth != other.elementWidth || + !_sameValueArrayDimensions(dimensions, other.dimensions)) { + throw ArgumentError.value( + other.dimensions, + 'arrays', + 'All arrays must have dimensions $dimensions and ' + 'elementWidth $elementWidth.'); + } + } + + @override + bool _equals(Object other) => other is LogicValue && packed == other.packed; + + @override + int get _hashCode => packed._hashCode; + + @override + LogicValue _getIndex(int index) => packed._getIndex(index); + + @override + LogicValue _getRange(int start, int end) => packed._getRange(start, end); + + @override + LogicValue get reversed => packed.reversed; + + @override + bool get isValid => packed.isValid; + + @override + bool get isFloating => packed.isFloating; + + @override + int toInt() => packed.toInt(); + + @override + BigInt toBigInt() => packed.toBigInt(); + + @override + LogicValue operator ~() => ~packed; + + @override + LogicValue _and2(LogicValue other) => packed & other.packed; + + @override + LogicValue _or2(LogicValue other) => packed | other.packed; + + @override + LogicValue _xor2(LogicValue other) => packed ^ other.packed; + + @override + LogicValue _triState2(LogicValue other) => packed.triState(other.packed); + + @override + LogicValue and() => packed.and(); + + @override + LogicValue or() => packed.or(); + + @override + LogicValue xor() => packed.xor(); + + @override + bool get isZero => packed.isZero; + + @override + LogicValue _shiftRight(int shamt) => packed._shiftRight(shamt); + + @override + LogicValue _shiftLeft(int shamt) => packed._shiftLeft(shamt); + + @override + LogicValue _shiftArithmeticRight(int shamt) => + packed._shiftArithmeticRight(shamt); + + @override + BigInt get _bigIntValue => packed._bigIntValue; + + @override + BigInt get _bigIntInvalid => packed._bigIntInvalid; + + @override + int get _intValue => packed._intValue; + + @override + int get _intInvalid => packed._intInvalid; +} + +/// Shape and row-major leaves extracted from nested constructor input. +class _NestedValueArray { + /// Inferred dimension sizes from outermost to innermost. + final List dimensions; + + /// Flattened semantic leaves in row-major order. + final List values; + + /// Stores one validated nested-input parse result. + _NestedValueArray(this.dimensions, this.values); +} + +/// Parses arbitrary-depth nested lists into a rectangular row-major array. +/// +/// Values matching [T] are treated as leaves before checking whether they are +/// lists, which keeps list-valued semantic types unambiguous. +_NestedValueArray _parseNestedArray( + List root, { + required String argumentName, +}) { + _NestedValueArray parse(List values, List path) { + if (values.isEmpty) { + return _NestedValueArray(const [0], const []); + } + + final children = <_NestedValueArray>[]; + for (var index = 0; index < values.length; index++) { + final value = values[index]; + if (value is T) { + children.add(_NestedValueArray(const [], [value])); + } else if (value is List) { + children.add(parse(value.cast(), [...path, index])); + } else { + throw ArgumentError.value(value, argumentName, + 'Expected $T or a nested List at ${[...path, index]}.'); + } + } + + final childDimensions = children.first.dimensions; + if (children.skip(1).any((child) => + !_sameValueArrayDimensions(child.dimensions, childDimensions))) { + throw ArgumentError.value( + root, argumentName, 'Nested lists must be rectangular.'); + } + return _NestedValueArray( + [values.length, ...childDimensions], + children.expand((child) => child.values).toList(growable: false), + ); + } + + return parse(root, const []); +} + +/// Validates dimension sizes and returns an immutable shape. +List _validateValueArrayDimensions(List dimensions) { + final normalized = List.unmodifiable(dimensions); + if (normalized.isEmpty) { + throw ArgumentError.value(dimensions, 'dimensions', 'Must not be empty.'); + } + if (normalized.any((dimension) => dimension < 0)) { + throw ArgumentError.value( + dimensions, 'dimensions', 'Dimensions must be non-negative.'); + } + return normalized; +} + +/// Rejects a negative packed width for each array element. +void _validateValueArrayElementWidth(int elementWidth) { + if (elementWidth < 0) { + throw ArgumentError.value( + elementWidth, 'elementWidth', 'Must be non-negative.'); + } +} + +/// Requires the flat value count implied by [dimensions]. +void _validateValueCount( + List dimensions, int valueCount, String argumentName) { + final expected = _valueArrayLength(dimensions); + if (valueCount != expected) { + throw ArgumentError.value(valueCount, argumentName, + 'Must contain exactly $expected values for shape $dimensions.'); + } +} + +/// Requires every packed value to have [elementWidth] bits. +void _validateElementWidths( + Iterable values, int elementWidth, String argumentName) { + for (final value in values) { + if (value.width != elementWidth) { + throw ArgumentError.value( + value, argumentName, 'All values must have width $elementWidth.'); + } + } +} + +/// Computes the number of row-major elements represented by [dimensions]. +int _valueArrayLength(List dimensions) => + dimensions.fold(1, (length, dimension) => length * dimension); + +/// Converts a row-major [flatIndex] into one index per dimension. +List _valueArrayIndices(List dimensions, int flatIndex) { + final indices = List.filled(dimensions.length, 0); + for (var dimension = dimensions.length - 1; dimension >= 0; dimension--) { + final size = dimensions[dimension]; + indices[dimension] = size == 0 ? 0 : flatIndex % size; + flatIndex = size == 0 ? 0 : flatIndex ~/ size; + } + return indices; +} + +/// Converts multidimensional [indices] into a validated row-major flat index. +int _valueArrayFlatIndex(List dimensions, List indices) { + if (indices.length != dimensions.length) { + throw RangeError.range( + indices.length, dimensions.length, dimensions.length, 'indices.length'); + } + + var flatIndex = 0; + for (var dimension = 0; dimension < dimensions.length; dimension++) { + final index = indices[dimension]; + final size = dimensions[dimension]; + if (index < 0 || index >= size) { + throw RangeError.range(index, 0, size - 1, 'indices[$dimension]'); + } + flatIndex = flatIndex * size + index; + } + return flatIndex; +} + +/// Whether two ordered dimension lists describe the same shape. +bool _sameValueArrayDimensions(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); diff --git a/lib/src/values/values.dart b/lib/src/values/values.dart index b58ce04032..066dd4f19a 100644 --- a/lib/src/values/values.dart +++ b/lib/src/values/values.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause library; @@ -13,6 +13,8 @@ import 'package:rohd/src/utilities/index_utilities.dart'; import 'package:rohd/src/utilities/web.dart'; part 'logic_value.dart'; +part 'logic_value_array.dart'; +part 'typed_logic_value_array.dart'; part 'small_logic_value.dart'; part 'big_logic_value.dart'; part 'filled_logic_value.dart'; diff --git a/rohd_devtools_extension/README.md b/rohd_devtools_extension/README.md index 25bb347848..6fc36321d4 100644 --- a/rohd_devtools_extension/README.md +++ b/rohd_devtools_extension/README.md @@ -1,5 +1,7 @@ # ROHD DevTools Extension +[![Coverage](https://raw.githubusercontent.com/intel/rohd/refs/heads/badges/coverage/rohd_devtools_extension/main.svg)](https://github.com/intel/rohd/actions/workflows/coverage.yml) + The ROHD DevTools extension provides debugging support for ROHD hardware designers. It connects to a running Dart VM, reads the ROHD module hierarchy, and displays live signal information while the debugged program is paused. diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index e2164a8f20..34311d38ec 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -26,11 +26,13 @@ dependencies: flutter_web_plugins: sdk: flutter logging: ^1.2.0 - material_ui: ^1.1.0 + # Keep below 1.3.0 for the Dart 3.6 CI toolchain. + material_ui: ">=1.1.0 <1.3.0" path: ^1.9.0 path_provider: ^2.1.0 rohd: ^0.6.11 - rohd_devtools_widgets: ^0.1.2 + rohd_devtools_widgets: + path: ../packages/rohd_devtools_widgets rohd_hierarchy: ^0.1.1 rohd_schematic_viewer: ^0.2.4 rohd_source_navigator: ^0.1.0 diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 088ec820e0..ddd6de3332 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -16,6 +16,14 @@ import 'package:rohd/src/utilities/sv_cleaner.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; +class _TypedInputModule extends Module { + late final T values; + + _TypedInputModule(T source) { + values = addTypedInput('values', source); + } +} + class SimpleLAPassthrough extends Module { LogicArray get laOut => output('laOut') as LogicArray; SimpleLAPassthrough( @@ -625,6 +633,88 @@ void main() { expect(arr.elementWidth, 0); }); + test('empty arrays preserve the selected net kind', () { + for (final dimensions in [ + [0], + [2, 0], + [0, 2], + ]) { + final logicArray = LogicArray(dimensions, 8); + final netArray = LogicArray.net(dimensions, 8); + + expect(logicArray.isNet, isFalse); + expect(netArray.isNet, isTrue); + expect( + logicArray.elements.every((element) => !element.isNet), + isTrue, + ); + expect(netArray.elements.every((element) => element.isNet), isTrue); + + final module = _TypedInputModule(logicArray); + expect( + module.values, + isA(), + ); + expect( + () => _TypedInputModule(netArray), + throwsA(isA()), + ); + } + }); + + test('zero-width arrays fail SystemVerilog synthesis explicitly', () async { + for (final array in [ + LogicArray([2, 0], 3), + LogicArray([2], 0), + ]) { + final module = _TypedInputModule(array); + await module.build(); + expect( + module.dumpSystemVerilog, + throwsA( + isA().having( + (exception) => exception.message, + 'message', + contains('zero-width array'), + ), + ), + ); + } + }); + + test('packed value assignment remains shape-independent', () async { + final source = LogicArray([2], 8)..put(0x1234); + final target = LogicArray([1], 16); + final bitSource = LogicArray([1], 1)..put(1); + final fillTarget = LogicArray([3], 1); + + target.put(source.value); + expect(source.value, isA()); + expect(target.value, isA()); + expect(target.value.toInt(), 0x1234); + + fillTarget.put(bitSource.value, fill: true); + expect(fillTarget.value.toInt(), 0x7); + expect( + () => target.put(source.value, fill: true), + throwsA(isA()), + ); + + target + ..put(0) + ..inject(source.value); + fillTarget + ..put(0) + ..inject(bitSource.value, fill: true); + expect(target.value.toInt(), 0); + expect(fillTarget.value.toInt(), 0); + + await Simulator.run(); + expect(target.previousValue, isA()); + expect(target.value.toInt(), 0x1234); + expect(fillTarget.value.toInt(), 0x7); + }); + test('single-dim array', () { final dim = [5]; const w = 16; diff --git a/test/logic_enum_test.dart b/test/logic_enum_test.dart index f4399611e4..202b6b8815 100644 --- a/test/logic_enum_test.dart +++ b/test/logic_enum_test.dart @@ -569,6 +569,79 @@ class EnumArrayBoundaryModule extends Module { } } +LogicEnum _arrayState({String? name}) => + LogicEnum.withMapping( + {TestEnum.a: 1, TestEnum.c: 5}, + width: 3, + name: name, + definitionName: 'ArrayState', + ); + +TypedLogicArray, LogicValue> _enumArray( + List dimensions, { + int numUnpackedDimensions = 0, +}) => + TypedLogicArray, LogicValue>( + dimensions, + _arrayState, + name: 'states', + numUnpackedDimensions: numUnpackedDimensions, + ); + +class _EnumArrayChild extends Module { + _EnumArrayChild(TypedLogicArray, LogicValue> source) { + final input = addTypedInput('statesIn', source); + final output = addTypedOutput('statesOut', input.clone); + addTypedOutput('stateTap', _arrayState) <= input.arrayElements.first; + for (var index = 0; index < input.arrayElements.length; index++) { + output.arrayElements[index] <= + input.arrayElements[input.arrayElements.length - index - 1]; + } + } +} + +class _EnumArrayParent extends Module { + late final TypedLogicArray, LogicValue> statesIn; + late final TypedLogicArray, LogicValue> statesOut; + + _EnumArrayParent(TypedLogicArray, LogicValue> source) { + statesIn = addTypedInput('statesIn', source); + final child = _EnumArrayChild(statesIn); + statesOut = addTypedOutput('statesOut', statesIn.clone) + ..gets(child.output('statesOut')); + addTypedOutput('stateTap', _arrayState).gets(child.output('stateTap')); + } +} + +class _EnumArrayPacket extends LogicStructure { + final LogicEnum state; + final Logic payload; + + factory _EnumArrayPacket({String? name}) => _EnumArrayPacket._( + _arrayState(name: 'state'), + Logic(name: 'payload', width: 2), + name: name, + ); + + _EnumArrayPacket._(this.state, this.payload, {super.name}) + : super([state, payload]); + + @override + _EnumArrayPacket clone({String? name}) => _EnumArrayPacket(name: name); +} + +class _EnumPacketArrayModule extends Module { + _EnumPacketArrayModule(TypedLogicArray<_EnumArrayPacket, LogicValue> source) { + final input = addTypedInput('packetsIn', source); + final output = addTypedOutput('packetsOut', input.clone); + addTypedOutput('stateTap', _arrayState) <= input.arrayElements.first.state; + for (var index = 0; index < input.arrayElements.length; index++) { + output.arrayElements[index].state <= input.arrayElements[index].state; + output.arrayElements[index].payload <= input.arrayElements[index].payload; + } + } +} + class EnumHierarchyChild extends Module { Logic get result => output('result'); @@ -2992,6 +3065,168 @@ void main() { expect(module.dumpSystemVerilog(), contains("TestEnum'(")); }); + test('typed enum array preserves mapping, shape, and normalized leaves', + () async { + final states = _enumArray([2, 2]); + final copy = states.clone(name: 'copy'); + final named = states.named('named'); + + for (final array in [states, copy, named]) { + expect(array.at([1, 0]), isA>()); + expect(array.at([1, 0]).mapping, { + TestEnum.a: LogicValue.ofInt(1, 3), + TestEnum.c: LogicValue.ofInt(5, 3), + }); + expect(array.dimensions, [2, 2]); + expect(array.value.dimensions, [2, 2]); + expect(array.value.elementWidth, 3); + } + expect(copy.name, 'copy'); + expect(named.name, 'named'); + expect(copy.value.packed.isFloating, isTrue); + + states.at([0, 0]).put(TestEnum.a); + states.at([0, 1]).put(TestEnum.c); + states.at([1, 0]).put(2); + states.at([1, 1]).put(LogicValue.ofString('x0z')); + await Simulator.run(); + + expect(states.value.arrayValues, [ + LogicValue.ofInt(1, 3), + LogicValue.ofInt(5, 3), + LogicValue.ofString('xxx'), + LogicValue.ofString('xxx'), + ]); + expect(states.value.at([1, 0]), LogicValue.ofString('xxx')); + expect(named.value.packed, states.value.packed); + expect(copy.value.packed.isFloating, isTrue); + + states.at([1, 1]).put(LogicValue.ofString('zzz')); + expect(states.value.at([1, 1]), LogicValue.ofString('zzz')); + }); + + for (final (layout, unpacked) in [ + ('packed', 0), + ('unpacked', 2), + ('mixed', 1), + ]) { + test('typed enum array crosses child boundary with $layout layout', + () async { + final module = _EnumArrayParent( + _enumArray([2, 2], numUnpackedDimensions: unpacked)); + final mapping = module.statesIn.at([1, 1]).mapping; + expect(module.statesOut.at([0, 0]).mapping, mapping); + expect(module.statesOut.numUnpackedDimensions, unpacked); + expect((module.output('stateTap') as LogicEnum).mapping, + mapping); + await module.build(); + + final input = LogicValue.ofIterable([ + for (final value in [1, 5, 5, 5]) LogicValue.ofInt(value, 3), + ]); + final output = LogicValue.ofIterable([ + for (final value in [5, 5, 5, 1]) LogicValue.ofInt(value, 3), + ]); + final vectors = [ + Vector({'statesIn': input}, {'statesOut': output, 'stateTap': 1}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + + for (final generateEnums in [true, false]) { + final configuration = SystemVerilogSynthesizerConfiguration( + generateEnums: generateEnums); + final sv = module.dumpSystemVerilog(configuration: configuration); + expect(sv, contains('statesIn[')); + expect(sv, contains('statesOut[')); + if (generateEnums) { + expect(sv, contains('typedef enum')); + expect(sv, contains("ArrayState'(")); + } else { + expect(sv, isNot(contains('typedef enum'))); + expect(sv, isNot(contains("ArrayState'("))); + } + SimCompare.checkVerilatorVector( + module, + vectors, + synthesizerConfiguration: configuration, + ); + if (unpacked == 0) { + SimCompare.checkIverilogVector( + module, + vectors, + synthesizerConfiguration: configuration, + ); + } + } + }, tags: ['verilator']); + } + + test('array of enum-bearing structures keeps fields and metadata', + () async { + final source = TypedLogicArray<_EnumArrayPacket, LogicValue>( + [2], _EnumArrayPacket.new); + final module = _EnumPacketArrayModule(source); + await module.build(); + + final input = LogicValue.ofIterable([ + LogicValue.ofInt((2 << 3) | 1, 5), + LogicValue.ofInt((3 << 3) | 5, 5), + ]); + final vectors = [ + Vector({'packetsIn': input}, {'packetsOut': input, 'stateTap': 1}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('packetsIn[')); + expect(sv, contains('packetsOut[')); + expect(sv, contains("ArrayState'(")); + SimCompare.checkVerilatorVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + + final netlist = jsonDecode(NetlistSynthesizer( + configuration: NetlistSynthesizerConfiguration( + moduleStopPolicy: SynthModuleStopPolicy())) + .synthesizeToJson(module)) as Map; + final definition = + (netlist['modules'] as Map)['_EnumPacketArrayModule'] + as Map; + final inputPort = (definition['ports'] + as Map)['packetsIn'] as Map; + final arrayType = inputPort['logic_type'] as Map; + final elementType = arrayType['elementType'] as Map; + final fields = + (elementType['fields'] as List).cast>(); + expect(arrayType['arrayDims'], [2]); + expect(fields.map((field) => (field['name'], field['width'])), + containsAll([('state', 3), ('payload', 2)])); + expect(sv, contains('packetsIn[0][2:0]')); + expect(sv, contains('packetsIn[0][4:3]')); + final cells = (definition['cells'] as Map) + .values + .cast>(); + final unpackParameters = cells + .where((cell) => cell['type'] == r'$struct_unpack') + .map((cell) => cell['parameters'] as Map); + expect( + unpackParameters, + contains(predicate>((params) => + params['FIELD_0_NAME'] == 'state' && + params['FIELD_0_OFFSET'] == 0 && + params['FIELD_1_NAME'] == 'payload' && + params['FIELD_1_OFFSET'] == 3)), + ); + final netnames = definition['netnames'] as Map; + final enumAttributes = netnames.values + .cast>() + .map((value) => value['attributes'] as Map) + .where((attributes) => attributes['wiretype'] == r'\ArrayState'); + expect(enumAttributes, isNotEmpty); + for (final attributes in enumAttributes) { + expect(attributes['enum_value_001'], r'\a'); + expect(attributes['enum_value_101'], r'\c'); + } + }, tags: ['verilator']); + test('enum metadata crosses a submodule boundary in both modes', () async { final module = EnumHierarchyModule(Logic(width: 2)); final vectors = [ diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 236766e29c..5723051f72 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -96,6 +96,37 @@ class _PreservedArrayChain extends Module { } } +class _PreservedTypedArrayChain extends Module { + final aliases = >[]; + + _PreservedTypedArrayChain() { + var previous = addTypedInput( + 'source', + TypedLogicArray( + [2, 2], + ({name}) => Logic(name: name, width: 2), + ), + ); + for (final naming in [ + Naming.reserved, + Naming.renameable, + Naming.reserved + ]) { + final alias = TypedLogicArray( + [2, 2], + ({name}) => Logic(name: name, width: 2), + name: 'shared', + naming: naming, + )..gets(previous); + aliases.add(alias); + previous = alias; + } + final shared = addTypedOutput('shared', previous.clone)..gets(previous); + addOutput('result', width: 8) <= shared.packed; + addOutput('early', width: 8) <= aliases.first.packed; + } +} + class _PreservedNetChain extends Module { final aliases = []; @@ -395,6 +426,39 @@ void main() { }); } + test('base typed array chain preserves same-name aliases', () async { + final dut = _PreservedTypedArrayChain(); + await dut.build(); + final definition = SystemVerilogSynthModuleDefinition(dut); + final port = dut.output('shared') as TypedLogicArray; + final shared = definition.getSynthLogic(port)!; + + expect(shared.name, 'shared'); + expect(shared.isPort(dut), isTrue); + expect(shared.logics, containsAll(dut.aliases)); + for (final alias in dut.aliases) { + expect(definition.getSynthLogic(alias), same(shared)); + for (var index = 0; index < alias.leafElements.length; index++) { + expect( + definition.getSynthLogic(alias.leafElements[index]), + same(definition.getSynthLogic(port.leafElements[index])), + ); + } + } + + final sv = dut.dumpSystemVerilog(); + expect(sv, isNot(contains('shared_'))); + final vectors = [ + for (final value in [0, 0xa5, 0x5a, '10xz01zx']) + Vector( + {'source': value}, + {'shared': value, 'result': value, 'early': value}, + ), + ]; + await SimCompare.checkFunctionalVector(dut, vectors); + SimCompare.checkIverilogVector(dut, vectors); + }); + for (final reversed in [false, true]) { test('net chain merges with reversed=$reversed', () async { final dut = _PreservedNetChain(reversed: reversed); @@ -453,6 +517,168 @@ void main() { final secondElement = definition.getSynthLogic(secondArray.elements.first)!; expect(SynthLogic.tryMerge(firstElement, secondElement), isNull); + + final mergeableScalar = synth(Logic(width: 4, naming: Naming.unnamed)); + final mergeableTypedArray = synth( + TypedLogicArray( + [2], + ({name}) => Logic(name: name, width: 2), + naming: Naming.unnamed, + ), + ); + expect(mergeableScalar.mergeable, isTrue); + expect(mergeableTypedArray.mergeable, isTrue); + expect( + SynthLogic.tryMerge(mergeableScalar, mergeableTypedArray), + isNull, + ); + expect( + SynthLogic.tryMerge(mergeableTypedArray, mergeableScalar), + isNull, + ); + }); + + test('base typed arrays merge only with matching declaration shapes', + () async { + final dut = FunctionGeneratedModule((in1, in2, out1) { + out1 <= in1 | in2; + }); + await dut.build(); + final definition = SynthModuleDefinition(dut); + + SynthLogic synth({ + required List dimensions, + required int elementWidth, + int numUnpackedDimensions = 0, + Naming naming = Naming.reserved, + }) => + SynthLogic( + TypedLogicArray( + dimensions, + ({name}) => Logic(name: name, width: elementWidth), + name: 'shared', + naming: naming, + numUnpackedDimensions: numUnpackedDimensions, + ), + parentSynthModuleDefinition: definition, + ); + + final compatibleFirst = synth(dimensions: [2, 2], elementWidth: 2); + final compatibleSecond = synth(dimensions: [2, 2], elementWidth: 2); + final merged = SynthLogic.tryMerge(compatibleFirst, compatibleSecond); + expect(merged, isNotNull); + expect(merged!.kept.logics, hasLength(2)); + + for (final incompatible in [ + synth(dimensions: [4, 1], elementWidth: 2), + synth(dimensions: [0], elementWidth: 3), + synth( + dimensions: [2, 2], + elementWidth: 2, + numUnpackedDimensions: 1, + ), + ]) { + final reference = incompatible.width == 0 + ? synth(dimensions: [0], elementWidth: 2) + : synth(dimensions: [2, 2], elementWidth: 2); + expect(SynthLogic.tryMerge(reference, incompatible), isNull); + + final reverseReference = incompatible.width == 0 + ? synth(dimensions: [0], elementWidth: 2) + : synth(dimensions: [2, 2], elementWidth: 2); + expect(SynthLogic.tryMerge(incompatible, reverseReference), isNull); + } + + final mergeableFirst = synth( + dimensions: [2, 2], + elementWidth: 2, + naming: Naming.unnamed, + ); + final mergeableDifferentShape = synth( + dimensions: [4, 1], + elementWidth: 2, + naming: Naming.unnamed, + ); + expect(mergeableFirst.mergeable, isTrue); + expect(mergeableDifferentShape.mergeable, isTrue); + expect( + SynthLogic.tryMerge(mergeableFirst, mergeableDifferentShape), + isNull, + ); + }); + + test('typed arrays require compatible configured element layouts', + () async { + final dut = FunctionGeneratedModule((in1, in2, out1) { + out1 <= in1 | in2; + }); + await dut.build(); + final definition = SynthModuleDefinition(dut); + SynthLogic synth(Logic logic) => + SynthLogic(logic, parentSynthModuleDefinition: definition); + + TypedLogicArray structured( + int firstWidth, + int secondWidth, + ) => + TypedLogicArray( + [2], + ({name}) => LogicStructure( + [ + Logic(name: 'first', width: firstWidth), + Logic(name: 'second', width: secondWidth), + ], + name: name, + ), + name: 'shared', + naming: Naming.reserved, + ); + + final compatibleFirst = synth(structured(1, 2)); + final compatibleSecond = synth(structured(1, 2)); + expect( + SynthLogic.tryMerge(compatibleFirst, compatibleSecond), + isNotNull, + ); + + final scalarElements = synth( + TypedLogicArray( + [2], + ({name}) => Logic(name: name, width: 3), + name: 'shared', + naming: Naming.reserved, + ), + ); + final structuredElements = synth(structured(1, 2)); + expect( + SynthLogic.tryMerge(scalarElements, structuredElements), + isNull, + ); + expect( + SynthLogic.tryMerge(structuredElements, scalarElements), + isNull, + ); + + final differentFieldLayout = synth(structured(2, 1)); + expect( + SynthLogic.tryMerge(synth(structured(1, 2)), differentFieldLayout), + isNull, + ); + + SynthLogic nested(int innerUnpackedDimensions) => synth( + TypedLogicArray, LogicValue>( + [2], + ({name}) => TypedLogicArray( + [3], + ({name}) => Logic(name: name, width: 2), + name: name, + numUnpackedDimensions: innerUnpackedDimensions, + ), + name: 'shared', + naming: Naming.reserved, + ), + ); + expect(SynthLogic.tryMerge(nested(0), nested(1)), isNull); }); }); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index 7fea35e0bf..362706f8e7 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -250,6 +250,21 @@ void main() { // try a withSet of a subset of the second field MyFancyStruct().withSet(72, Logic(width: 4)); }); + + test('withSet range can be contained within a wide field', () { + final structure = LogicStructure([ + Logic(name: 'wide', width: 8), + ]) + ..put(LogicValue.ofString('10100101')); + final update = Const(1); + + final updated = structure.withSet(3, update); + + expect( + updated.value, + structure.value.withSet(3, update.value), + ); + }); }); group('LogicStructures with modules', () { diff --git a/test/logic_value_array_test.dart b/test/logic_value_array_test.dart new file mode 100644 index 0000000000..10ac6ab427 --- /dev/null +++ b/test/logic_value_array_test.dart @@ -0,0 +1,411 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_value_array_test.dart +// Tests for logic value arrays. +// +// 2026 September 16 +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +void main() { + group('LogicValueArray', () { + test('constructs equivalent nested and flat row-major values', () { + final leaves = [ + for (var value = 1; value <= 4; value++) LogicValue.ofInt(value, 8), + ]; + final nested = LogicValueArray([ + [leaves[0], leaves[1]], + [leaves[2], leaves[3]], + ]); + final flat = LogicValueArray.fromFlat(const [2, 2], leaves); + final nestedInts = LogicValueArray.fromInts(const [ + [1, 2], + [3, 4], + ], elementWidth: 8); + final flatInts = LogicValueArray.fromFlatInts( + const [2, 2], + const [1, 2, 3, 4], + elementWidth: 8, + ); + + expect(nested.dimensions, [2, 2]); + expect(nested.elementWidth, 8); + expect(nested.arrayValues.length, 4); + expect(nested.width, 32); + expect(nested.arrayValues, leaves); + expect(nested.packed, LogicValue.ofInt(0x04030201, 32)); + expect(flat, nested); + expect(nestedInts, nested); + expect(flatInts, nested); + }); + + test('indexes, slices, reshapes, transposes, and maps values', () { + final values = LogicValueArray.fromInts(const [ + [1, 2, 3], + [4, 5, 6], + ], elementWidth: 8); + + expect(values.at([1, 1]).toInt(), 5); + expect(values.indexedValues.last.$1, equals([1, 2])); + expect( + values.majorSlices.map((slice) => + slice.arrayValues.map((value) => value.toInt()).toList()), + equals([ + [1, 2, 3], + [4, 5, 6], + ]), + ); + expect(values.reshape([3, 2]).at([2, 1]).toInt(), 6); + expect( + values.transpose2D().arrayValues.map((value) => value.toInt()), + [1, 4, 2, 5, 3, 6], + ); + expect( + values + .indexedMap((indices, value) => + LogicValue.ofInt(value.toInt() + indices[0], 8)) + .arrayValues + .map((value) => value.toInt()), + equals([1, 2, 3, 5, 6, 7]), + ); + expect( + LogicValueArray.stack(values.majorSlices).dimensions, + equals([2, 3]), + ); + }); + + test('supports empty, generated, mapped, and empty-stack contracts', () { + var generatorCalls = 0; + final generated = LogicValueArray.generate( + const [2, 3], + (indices) { + generatorCalls++; + return LogicValue.ofInt(indices[0] * 3 + indices[1] + 1, 8); + }, + ); + final mapped = generated.map( + (value) => LogicValue.ofInt(value.toInt() + 1, 8), + ); + final empty = LogicValueArray.empty(); + var emptyMapCalls = 0; + final mappedEmpty = empty.map((value) { + emptyMapCalls++; + return value; + }); + final generatedEmpty = LogicValueArray.generate( + const [2, 0], + (indices) => throw StateError('Generator must not run for $indices.'), + elementWidth: 8, + ); + + expect(generatorCalls, 6); + expect(generated.elementWidth, 8); + expect(generated.dimensions, [2, 3]); + expect( + generated.arrayValues.map((value) => value.toInt()), + [1, 2, 3, 4, 5, 6], + ); + expect(mapped, isA()); + expect( + mapped.arrayValues.map((value) => value.toInt()), + [2, 3, 4, 5, 6, 7], + ); + expect(empty.dimensions, [0]); + expect(empty.elementWidth, 0); + expect(empty.arrayValues.length, 0); + expect(empty.width, 0); + expect(empty.arrayValues, isEmpty); + expect(empty.packed, LogicValue.empty); + expect(emptyMapCalls, 0); + expect(mappedEmpty.dimensions, [0]); + expect(generatedEmpty.dimensions, [2, 0]); + expect(generatedEmpty.elementWidth, 8); + expect(generatedEmpty.arrayValues, isEmpty); + expect( + () => LogicValueArray.stack(const []), + throwsArgumentError, + ); + expect( + () => TypedLogicValueArray.stack( + const >[]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.stack( + LogicValueArray.fromFlat(const [0, 2], const [], elementWidth: 8) + .majorSlices, + ), + throwsArgumentError, + ); + }); + + test('round-trips slices with an empty inner dimension', () { + final values = + LogicValueArray.fromFlat(const [2, 0], const [], elementWidth: 8); + final slices = values.majorSlices.toList(growable: false); + + expect(slices, hasLength(2)); + expect(slices.map((slice) => slice.dimensions), [ + [0], + [0], + ]); + expect(slices.map((slice) => slice.elementWidth), [8, 8]); + expect(LogicValueArray.stack(slices).dimensions, [2, 0]); + }); + + test('matches packed semantics across storage and validity domains', () { + final smallInvalid = LogicValueArray([ + LogicValue.ofString('1xz0'), + LogicValue.ofString('z011'), + ]); + final wideInvalid = LogicValueArray([ + LogicValue.ofString('${'10' * 33}1xz0'), + LogicValue.ofString('z0x1${'01' * 33}'), + ]); + final wideValid = LogicValueArray([ + LogicValue.ofBigInt( + (BigInt.one << 69) | BigInt.from(0x1234), + 70, + ), + LogicValue.ofBigInt( + (BigInt.one << 65) | BigInt.from(0x5678), + 70, + ), + ]); + final testCases = [ + ( + name: 'small X/Z', + shaped: smallInvalid, + other: LogicValue.ofString('10z101x0'), + ), + ( + name: 'wide X/Z', + shaped: wideInvalid, + other: LogicValue.ofString('01xz' * 35), + ), + ( + name: 'wide valid', + shaped: wideValid, + other: LogicValue.ofBigInt( + (BigInt.one << 138) | BigInt.from(0x9abc), + 140, + ), + ), + ]; + final binaryOperations = <({ + String name, + LogicValue Function(LogicValue left, LogicValue right) apply, + })>[ + (name: 'and', apply: (left, right) => left & right), + (name: 'or', apply: (left, right) => left | right), + (name: 'xor', apply: (left, right) => left ^ right), + (name: 'triState', apply: (left, right) => left.triState(right)), + (name: 'add', apply: (left, right) => left + right), + (name: 'subtract', apply: (left, right) => left - right), + (name: 'multiply', apply: (left, right) => left * right), + ]; + + for (final testCase in testCases) { + final shaped = testCase.shaped; + final packed = shaped.packed; + final other = testCase.other; + final reshaped = shaped.reshape([1, shaped.arrayValues.length]); + + expect(shaped, packed, reason: testCase.name); + expect(packed, shaped, reason: testCase.name); + expect(shaped, reshaped, reason: testCase.name); + expect(shaped.hashCode, packed.hashCode, reason: testCase.name); + expect(shaped.hashCode, reshaped.hashCode, reason: testCase.name); + expect({shaped, packed, reshaped}, hasLength(1)); + expect(LogicValue.ofIterable([shaped]), packed); + expect(shaped.isValid, packed.isValid); + expect(shaped.isFloating, packed.isFloating); + expect(shaped.isZero, packed.isZero); + expect(shaped[0], packed[0]); + expect(shaped[-1], packed[-1]); + expect(shaped.getRange(2, shaped.width - 2), + packed.getRange(2, packed.width - 2)); + expect(shaped.slice(shaped.width - 3, 1), + packed.slice(packed.width - 3, 1)); + expect(shaped.reversed, packed.reversed); + expect(~shaped, ~packed); + expect(shaped.and(), packed.and()); + expect(shaped.or(), packed.or()); + expect(shaped.xor(), packed.xor()); + expect(shaped << 3, packed << 3); + expect(shaped >> 3, packed >> 3); + expect(shaped >>> 3, packed >>> 3); + if (shaped.isValid) { + expect(shaped.toBigInt(), packed.toBigInt()); + } + if (testCase.name.startsWith('wide')) { + expect(shaped.width, greaterThan(64)); + } + + for (final operation in binaryOperations) { + expect( + operation.apply(shaped, other), + operation.apply(packed, other), + reason: '${testCase.name}: shaped ${operation.name} packed', + ); + expect( + operation.apply(other, shaped), + operation.apply(other, packed), + reason: '${testCase.name}: packed ${operation.name} shaped', + ); + } + } + }); + + test('rejects ragged, inconsistent, empty, and mismatched input', () { + final one = LogicValue.ofInt(1, 8); + expect(() => LogicValueArray(const []), throwsArgumentError); + expect( + () => LogicValueArray([ + [one], + [one, one], + ]), + throwsArgumentError, + ); + expect( + () => LogicValueArray([ + one, + [one], + ]), + throwsArgumentError, + ); + expect( + () => LogicValueArray([one, LogicValue.ofInt(2, 4)]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat(const [2, 2], [one]), + throwsArgumentError, + ); + expect( + LogicValueArray.fromFlat( + const [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], + ).elementWidth, + 4, + ); + expect( + LogicValueArray.fromFlat( + const [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], + elementWidth: 4, + ).elementWidth, + 4, + ); + expect( + () => LogicValueArray.fromFlat( + const [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 3)], + ), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat( + const [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], + elementWidth: 3, + ), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromInts(const [], elementWidth: 8), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat(const [], const [], elementWidth: 8), + throwsArgumentError); + expect( + LogicValueArray.fromFlat(const [2, 0], const [], elementWidth: 8) + .elementWidth, + 8, + ); + expect( + () => LogicValueArray.fromFlat(const [2, 0], const []), + throwsArgumentError, + ); + }); + + test('rejects invalid value indices, shapes, widths, and stacks', () { + final values = LogicValueArray.fromFlatInts( + const [2, 2], + const [1, 2, 3, 4], + elementWidth: 8, + ); + final oneDimensional = LogicValueArray.fromFlatInts( + const [4], + const [1, 2, 3, 4], + elementWidth: 8, + ); + final threeDimensional = LogicValueArray.fromFlatInts( + const [1, 2, 2], + const [1, 2, 3, 4], + elementWidth: 8, + ); + + for (final indices in >[ + [0], + [-1, 0], + [2, 0], + [0, 2], + ]) { + expect(() => values.at(indices), throwsA(isA())); + } + expect(() => values.reshape([3]), throwsArgumentError); + expect(() => values.reshape([-2, -2]), throwsArgumentError); + expect( + () => oneDimensional.majorSlices.toList(), + throwsA(isA()), + ); + expect( + oneDimensional.transpose2D, + throwsA(isA()), + ); + expect( + threeDimensional.transpose2D, + throwsA(isA()), + ); + expect( + () => LogicValueArray.fromFlat(const [-1], const [], elementWidth: 8), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat(const [1], const [], elementWidth: -1), + throwsArgumentError, + ); + expect( + () => LogicValueArray.generate( + const [1], + (indices) => LogicValue.ofInt(indices.single, 4), + elementWidth: 8, + ), + throwsArgumentError, + ); + expect( + () => LogicValueArray.stack([ + LogicValueArray.fromFlatInts(const [2], const [1, 2], + elementWidth: 8), + LogicValueArray.fromFlatInts(const [1, 2], const [3, 4], + elementWidth: 8), + ]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.stack([ + LogicValueArray.fromFlatInts(const [2], const [1, 2], + elementWidth: 8), + LogicValueArray.fromFlatInts(const [2], const [3, 4], + elementWidth: 4), + ]), + throwsArgumentError, + ); + }); + }); +} diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart index 583d7173c6..b863ad41e9 100644 --- a/test/naming_cases_test.dart +++ b/test/naming_cases_test.dart @@ -110,6 +110,10 @@ // = "parent.field" → sanitized ("_") // AR Array element: isArrayMember // → uses logic.name (index-based) +// TY Typed array: root port and configured element names +// → exact port name and retained dimension prefixes +// TN Nested typed-array fields in sibling elements +// → indexed, collision-free synthesized names // // ════════════════════════════════════════════════════════ @@ -150,6 +154,35 @@ class _UnprefSub extends Module { } } +class _TypedNamingStructure extends LogicStructure { + final Logic before; + final TypedLogicArray<_SimpleStruct, LogicValue> samples; + final Logic after; + + factory _TypedNamingStructure({String? name}) => _TypedNamingStructure._( + Logic(name: 'before'), + TypedLogicArray<_SimpleStruct, LogicValue>( + [2], + ({name}) => _SimpleStruct(name: name ?? 'sample'), + name: 'samples', + dimensionNames: const ['sample_'], + ), + Logic(name: 'after'), + name: name ?? 'typedElement', + ); + + _TypedNamingStructure._( + this.before, + this.samples, + this.after, { + required String name, + }) : super([before, samples, after], name: name); + + @override + _TypedNamingStructure clone({String? name}) => + _TypedNamingStructure(name: name ?? this.name); +} + // ── Main test module ────────────────────────────── // One module that exercises every valid naming case in a minimal design. // Each signal is tagged with the row number from the table above. @@ -183,6 +216,7 @@ class _AllNamingCases extends Module { // Structure/array elements (ST, AR): late final LogicStructure structPort; late final LogicArray arrayPort; + late final TypedLogicArray<_TypedNamingStructure, LogicValue> typedArrayPort; _AllNamingCases() : super(name: 'allcases') { // ── Row 1: reserved + this-port + preferred ────────────────── @@ -265,6 +299,17 @@ class _AllNamingCases extends Module { arrayPort = LogicArray([3], 8, name: 'arIn'); addInputArray('arIn', arrayPort, dimensions: [3], elementWidth: 8); + // ── TY/TN: typed root, configured leaves, and nested fields ── + typedArrayPort = addTypedInput( + 'typedIn', + TypedLogicArray<_TypedNamingStructure, LogicValue>( + [2], + _TypedNamingStructure.new, + name: 'typedSource', + dimensionNames: const ['entry_'], + ), + ); + // Drive output to use all signals (prevents pruning). out <= mergeTarget | @@ -518,6 +563,37 @@ void main() { expect(n, 'arIn'); }); + // ── TY: typed-array root and element hierarchy ────────────── + + test('TY: typed array keeps port and configured element names', () { + expect(names[mod.typedArrayPort], 'typedIn'); + + final first = mod.typedArrayPort.at([0]); + expect(first.name, 'entry_0'); + expect(first.structureName, 'typedIn[0]'); + // `before` is a SystemVerilog keyword and is sanitized at construction. + expect(first.before.structureName, 'typedIn[0].before_'); + expect(first.samples.structureName, 'typedIn[0].samples'); + expect( + first.samples.at([1]).structureName, + 'typedIn[0].samples[1]', + ); + expect( + first.samples.at([1]).field1.structureName, + 'typedIn[0].samples[1].a', + ); + }); + + // ── TN: nested typed-array field synthesis names ──────────── + + test('TN: sibling nested typed-array fields get distinct names', () { + final firstSamples = mod.typedArrayPort.at([0]).samples; + final secondSamples = mod.typedArrayPort.at([1]).samples; + expect(names[firstSamples], 'typedIn_0__samples'); + expect(names[secondSamples], 'typedIn_1__samples'); + expect(names[firstSamples], isNot(names[secondSamples])); + }); + // ── Impossible cases ──────────────────────────────────────── test('unnamed + reserved throws at construction time', () { @@ -542,6 +618,9 @@ void main() { expect(sv, contains('_uinp')); expect(sv, contains('mport')); expect(sv, contains('_muprt')); + expect(sv, contains('typedIn')); + expect(sv, contains('typedIn_0__samples')); + expect(sv, contains('typedIn_1__samples')); // Reserved internals. expect(sv, contains('resv')); diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 8aed6ccf6a..dd4da394c5 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -397,6 +397,22 @@ class NestedNetArrayRowsToChildModule extends Module { } } +/// Exposes nested output-array rows already driven by child array outputs. +class NestedArrayRowsOutputModule extends Module { + NestedArrayRowsOutputModule() : super(name: 'nestedarrayrowsoutput') { + final lower = ArrayOutputChildModule(); + final upper = ArrayOutputChildModule(); + final values = addOutputArray( + 'values', + dimensions: [2, 4], + elementWidth: 8, + ); + + values.elements[0] <= lower.values; + values.elements[1] <= upper.values; + } +} + /// Simple two-field structure used to demonstrate netlist struct unpack/pack /// cells. class NetlistPairStruct extends LogicStructure { @@ -2250,12 +2266,21 @@ void main() { return entry.key.startsWith('array_concat') && cell['type'] == r'$concat'; }); + final structurePacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$struct_pack'; + }).toList(); final report = _connectivityReport(moduleDef); final multipleDrivers = report.driversByBit.entries .where((entry) => entry.value.length > 1) .toList(); expect(nestedArrayConcats, isNotEmpty, reason: cells.keys.join(', ')); + expect( + structurePacks, + isEmpty, + reason: 'Nested arrays must reuse their aggregate driver IDs.', + ); expect( report.undrivenInputs, isEmpty, @@ -2270,6 +2295,43 @@ void main() { }, ); + test('nested output arrays reuse existing aggregate drivers', () async { + final module = NestedArrayRowsOutputModule(); + final json = await _synthToMap( + module, + configuration: const NetlistSynthesizerConfiguration( + enableDeadCellElimination: false, + ), + ); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef); + final structurePacks = cells.entries.where((entry) { + final cell = entry.value as Map; + return cell['type'] == r'$struct_pack'; + }).toList(); + final report = _connectivityReport(moduleDef); + final multipleDrivers = report.driversByBit.entries + .where((entry) => entry.value.length > 1) + .toList(); + + expect( + structurePacks, + isEmpty, + reason: 'Nested arrays must reuse their aggregate driver IDs.', + ); + expect( + report.undrivenInputs, + isEmpty, + reason: report.undrivenInputs.join('\n'), + ); + expect( + multipleDrivers, + isEmpty, + reason: multipleDrivers.take(8).join('\n'), + ); + }); + test('struct input fields get explicit unpack cell', () async { final module = StructInputConsumerModule(NetlistPairStruct()); final json = await _synthToMap(module); @@ -2350,8 +2412,36 @@ void main() { }, }, }; + final aggregateBits = [ + ...List.generate(8, (index) => 200 + index), + ...List.generate(8, (index) => 400 + index), + ]; + final netnames = { + 'struct_values': { + 'bits': aggregateBits, + 'logic_type': { + 'typeName': 'NetPair', + 'fields': [ + {'name': 'first', 'width': 8}, + {'name': 'second', 'width': 8}, + ], + }, + }, + 'array_values': { + 'bits': aggregateBits, + 'logic_type': { + 'typeName': 'NetArray', + 'arrayDims': [2] + }, + }, + }; expect( - () => NetlistValidation.validate(ports, cells, 'struct_module'), + () => NetlistValidation.validate( + ports, + cells, + 'struct_module', + netnames: netnames, + ), returnsNormally, ); }); @@ -2376,16 +2466,171 @@ void main() { }, }, }; + final netnames = { + 'values': { + 'bits': [200, 201, 202], + 'logic_type': { + 'typeName': 'NetPair', + 'fields': [ + {'name': 'first', 'width': 1}, + {'name': 'second', 'width': 2}, + ], + }, + }, + }; expect( - () => NetlistValidation.validate(ports, cells, 'struct_module'), + () => NetlistValidation.validate( + ports, + cells, + 'struct_module', + netnames: netnames, + ), throwsA( - isA().having( - (error) => error.issues.map((issue) => issue.wireBit), - 'overlapping wire bit', - contains(201), + isA() + .having( + (error) => error.issues.map((issue) => issue.wireBit), + 'overlapping wire bit', + contains(201), + ) + .having( + (error) => error.issues.single.netname, + 'aggregate netname', + 'values', + ), + ), + ); + }); + + test('struct net aggregates allow distinct tri-state field drivers', () { + final cells = >{ + for (var index = 0; index < 2; index++) + 'driver_$index': { + 'type': r'$tribuf', + 'port_directions': {'A': 'input', 'EN': 'input', 'Y': 'output'}, + 'connections': { + 'A': [100 + index], + 'EN': [200 + index], + 'Y': [300 + index], + }, + }, + }; + final netnames = { + 'values': { + 'bits': [300, 301], + 'logic_type': { + 'typeName': 'NetPair', + 'fields': [ + {'name': 'first', 'width': 1}, + {'name': 'second', 'width': 1}, + ], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'structured_net_module', + netnames: netnames, + ), + returnsNormally, + ); + }); + + test( + 'struct net aggregates allow disjoint mixed tri-state and normal drivers', + () { + final cells = >{ + 'tri_state': { + 'type': r'$tribuf', + 'port_directions': {'A': 'input', 'EN': 'input', 'Y': 'output'}, + 'connections': { + 'A': [100], + 'EN': [200], + 'Y': [300] + }, + }, + 'normal': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [101], + 'Y': [301] + }, + }, + }; + final netnames = { + 'values': { + 'bits': [300, 301], + 'logic_type': { + 'typeName': 'NetPair', + 'fields': [ + {'name': 'first', 'width': 1}, + {'name': 'second', 'width': 1}, + ], + }, + }, + }; + + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'structured_net_module', + netnames: netnames, ), + returnsNormally, + ); + }, + ); + + test('struct net aggregates reject mixed partial overlaps', () { + final cells = >{ + 'tri_state': { + 'type': r'$tribuf', + 'port_directions': {'A': 'input', 'EN': 'input', 'Y': 'output'}, + 'connections': { + 'A': [100, 101], + 'EN': [200], + 'Y': [300, 301] + }, + }, + 'normal': { + 'type': r'$buf', + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': { + 'A': [102], + 'Y': [301] + }, + }, + }; + final netnames = { + 'values': { + 'bits': [300, 301], + 'logic_type': {'typeName': 'NetPair', 'fields': []}, + }, + }; + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'structured_net_module', + netnames: netnames, ), + throwsA(isA() + .having((error) => error.issues.single.wireBit, + 'overlapping wire bit', 301) + .having((error) => error.issues.single.netname, 'aggregate netname', + 'values') + .having( + (error) => error.issues.single.drivers, + 'conflicting drivers', + containsAll([ + r'cell tri_state.Y ($tribuf)', + r'cell normal.Y ($buf)', + ]))), ); }); diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 7b72005870..57bf3874ba 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -190,6 +190,49 @@ class SubInterfaceTestModule extends Module { } } +class _PairSample extends LogicStructure { + final Logic low; + final Logic high; + + factory _PairSample({String? name}) => _PairSample._( + Logic(name: 'low'), + Logic(name: 'high', width: 2), + name: name ?? 'sample', + ); + + _PairSample._(this.low, this.high, {required String name}) + : super([low, high], name: name); + + @override + _PairSample clone({String? name}) => _PairSample(name: name ?? this.name); +} + +class _PairValue { + final LogicValue value; + + _PairValue(this.value); +} + +_PairValue _decodePairValue(LogicValue value) => _PairValue(value); + +LogicValue _encodePairValue(_PairValue value) => value.value; + +const _pairValueCodec = LogicValueCodec<_PairValue>( + decode: _decodePairValue, + encode: _encodePairValue, +); + +class _TypedPairConsumer extends Module { + late final TypedLogicArray<_PairSample, _PairValue> samples; + + _TypedPairConsumer(PairInterface source) { + final internal = addPairInterfacePorts(source, PairRole.consumer); + samples = + internal.port('samples') as TypedLogicArray<_PairSample, _PairValue>; + addOutput('selected', width: samples.elementWidth) <= samples.at([1, 1]); + } +} + void main() { tearDown(() async { await Simulator.reset(); @@ -204,6 +247,43 @@ void main() { expect(sv, contains('input logic simple_clk')); }); + test('clone preserves typed arrays through pair interface ports', () async { + final samples = TypedLogicArray<_PairSample, _PairValue>( + [2, 2], + _PairSample.new, + valueCodec: _pairValueCodec, + dimensionNames: const ['row_', 'column_'], + name: 'samples', + ); + final source = PairInterface(portsFromProvider: [samples]); + final clone = source.clone(); + final clonedSamples = + clone.port('samples') as TypedLogicArray<_PairSample, _PairValue>; + + expect(clonedSamples, isNot(same(samples))); + expect(clonedSamples.dimensions, [2, 2]); + expect(clonedSamples.arrayElements, everyElement(isA<_PairSample>())); + expect(clonedSamples.at([1, 1]).high.width, 2); + expect(identical(clonedSamples.valueCodec, _pairValueCodec), isTrue); + expect(clonedSamples.value, isA>()); + + final module = _TypedPairConsumer(source); + await module.build(); + + expect(module.samples.dimensions, [2, 2]); + expect(module.samples.at([1, 1]), isA<_PairSample>()); + expect(module.samples.at([1, 1]).high.width, 2); + expect(identical(module.samples.valueCodec, _pairValueCodec), isTrue); + expect(module.samples.value, isA>()); + + final vectors = [ + Vector({'samples': 0xabc}, {'selected': 5}), + Vector({'samples': 0xe00}, {'selected': 7}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); + group('drive and receive other', () { Future testDriveAndReceive( {required bool useConditional, required bool useConnectApi}) async { diff --git a/test/subset_range_mapping_test.dart b/test/subset_range_mapping_test.dart index e79d3eed30..ccb5d8fd15 100644 --- a/test/subset_range_mapping_test.dart +++ b/test/subset_range_mapping_test.dart @@ -679,7 +679,7 @@ void main() { expect(reference.name, '${replacement.name}[11:3]'); }); - test('packed range reference rejects invalid bounds and unsupported bases', + test('packed range reference rejects invalid bounds, arrays, and constants', () async { final module = Producer(Logic(width: 16)); await module.build(); @@ -702,7 +702,6 @@ void main() { } for (final unsupported in [ LogicArray([16], 1), - LogicNet(width: 16), Const(0, width: 16), ]) { expect( @@ -717,6 +716,32 @@ void main() { } }); + test('packed references support net bases', () async { + final module = Producer(Logic(width: 16)); + await module.build(); + final definition = SynthModuleDefinition(module); + final base = SynthLogic( + LogicNet(name: 'netBase', width: 16), + parentSynthModuleDefinition: definition, + )..pickName(); + final bit = SynthLogicPackedBitReference( + base, + 5, + parentSynthModuleDefinition: definition, + ); + final range = SynthLogicPackedRangeReference( + base, + 3, + 11, + parentSynthModuleDefinition: definition, + ); + + expect(bit.isNet, isTrue); + expect(bit.name, '${base.name}[5]'); + expect(range.isNet, isTrue); + expect(range.name, '${base.name}[11:3]'); + }); + test('isolated source bit survives beside collapsed ranges', () async { final module = MixedTop( useRange: false, diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index 09ebfcebc3..250a87a29c 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -8,6 +8,8 @@ // 2026 April 14 // Author: Desmond Kirkpatrick +import 'dart:convert'; + import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/simcompare.dart'; @@ -134,6 +136,52 @@ class _CollapsedInstanceCollidingNames extends Module { } } +class _NestedNameStructure extends LogicStructure { + final Logic before; + final LogicArray lanes; + final Logic after; + + factory _NestedNameStructure({String? name}) => _NestedNameStructure._( + Logic(name: 'before', width: 2), + LogicArray([2], 3, name: 'lanes'), + Logic(name: 'after'), + name: name ?? 'nested', + ); + + _NestedNameStructure._( + this.before, + this.lanes, + this.after, { + required String name, + }) : super([before, lanes, after], name: name); + + @override + _NestedNameStructure clone({String? name}) => + _NestedNameStructure(name: name ?? this.name); +} + +class _NestedNameModule extends Module { + late final TypedLogicArray<_NestedNameStructure, LogicValue> valuesIn; + late final TypedLogicArray<_NestedNameStructure, LogicValue> valuesOut; + + _NestedNameModule() : super(name: 'nestedNameModule') { + final source = TypedLogicArray<_NestedNameStructure, LogicValue>( + [2], + _NestedNameStructure.new, + ); + valuesIn = addTypedInput('valuesIn', source); + valuesOut = addTypedOutput('valuesOut', valuesIn.clone); + for (var index = 0; index < 2; index++) { + valuesOut.at([index]).before <= valuesIn.at([index]).before; + valuesOut.at([index]).after <= ~valuesIn.at([index]).after; + for (var lane = 0; lane < 2; lane++) { + valuesOut.at([index]).lanes.at([lane]) <= + valuesIn.at([index]).lanes.at([lane]); + } + } + } +} + class _ReverseInternalSignalOrderSynthModuleDefinition extends SynthModuleDefinition { _ReverseInternalSignalOrderSynthModuleDefinition(super.module); @@ -512,5 +560,50 @@ void main() { expect(netlistOnly['retainedDup'], equals('dup')); }, ); + + test('nested typed-array names match between SV and netlist synthesis', + () async { + Map names(_NestedNameModule module) => { + 'input': module.namer.signalNameOfBest([module.valuesIn]), + 'output': module.namer.signalNameOfBest([module.valuesOut]), + 'inputLanes': module.namer.signalNameOfBest([ + module.valuesIn.at([0]).lanes, + ]), + 'outputLanes': module.namer.signalNameOfBest([ + module.valuesOut.at([1]).lanes, + ]), + }; + + final netlistModule = _NestedNameModule(); + await netlistModule.build(); + final netlist = + jsonDecode(netlistModule.generateNetlist()) as Map; + final netlistModules = netlist['modules'] as Map; + final emittedNetlistNames = { + for (final moduleDefinition + in netlistModules.values.cast>()) ...[ + ...(moduleDefinition['ports'] as Map).keys, + ...(moduleDefinition['netnames'] as Map).keys, + ], + }; + final netlistNames = names(netlistModule); + await Simulator.reset(); + + final svModule = _NestedNameModule(); + await svModule.build(); + final sv = svModule.dumpSystemVerilog(); + final svNames = names(svModule); + + expect(svNames, equals(netlistNames)); + expect(svNames['input'], 'valuesIn'); + expect(svNames['output'], 'valuesOut'); + expect(svNames['inputLanes'], 'valuesIn_0__lanes'); + expect(svNames['outputLanes'], 'valuesOut_1__lanes'); + for (final name in svNames.values) { + expect(sv, contains(name)); + expect(emittedNetlistNames, contains(name), + reason: 'SV and netlist should preserve signal name $name.'); + } + }); }); } diff --git a/test/systemc_async_reset_test.dart b/test/systemc_async_reset_test.dart new file mode 100644 index 0000000000..abcd64654d --- /dev/null +++ b/test/systemc_async_reset_test.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_async_reset_test.dart +// Verifies that an asynchronously reset Sequential emits its declared reset +// value in the SystemC reset branch, rather than zero. +// +// 2026 October 18 +// Author: Desmond A. Kirkpatrick + +@TestOn('vm') +library; + +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class _AsyncResetTop extends Module { + _AsyncResetTop(Logic clk, Logic reset, Logic d) : super(name: 'top') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + + final r = Logic(name: 'r', width: 8); + Sequential(clk, [r < d], + reset: reset, asyncReset: true, resetValues: {r: 0xAB}); + q <= r; + } +} + +void main() { + tearDown(ModuleServices.instance.reset); + + test('async Sequential reset branch emits the declared reset value', + () async { + final top = _AsyncResetTop( + Logic(name: 'clk'), + Logic(name: 'reset'), + Logic(name: 'd', width: 8), + ); + await top.build(); + + final outDir = Directory.systemTemp.createTempSync('rohd_sc_async_reset_'); + try { + final sc = SystemCService(top, outputDirectory: outDir.path); + final src = sc.contentsByDefinitionName.values.join('\n'); + + // The async reset arm is the branch guarded by reset.read(). + final resetArm = RegExp(r'if \(reset\.read\(\)\) \{([\s\S]*?)\} else \{') + .firstMatch(src) + ?.group(1); + expect(resetArm, isNotNull, reason: 'no async reset branch:\n$src'); + expect(resetArm, contains('171'), + reason: 'reset branch must carry 0xAB (171):\n$resetArm'); + expect(resetArm, isNot(contains('r = 0;')), + reason: 'reset branch must not force zero:\n$resetArm'); + } finally { + outDir.deleteSync(recursive: true); + } + }); +} diff --git a/test/systemc_service_test.dart b/test/systemc_service_test.dart index facbc8b378..eb6a13a3ae 100644 --- a/test/systemc_service_test.dart +++ b/test/systemc_service_test.dart @@ -3,6 +3,9 @@ // // systemc_service_test.dart // Tests SystemC service multi-file output support. +// +// 2026 October 16 +// Author: Desmond A. Kirkpatrick @TestOn('vm') library; diff --git a/test/systemc_split_packed_output_test.dart b/test/systemc_split_packed_output_test.dart index a71285a1b2..b9ef4c9c8b 100644 --- a/test/systemc_split_packed_output_test.dart +++ b/test/systemc_split_packed_output_test.dart @@ -6,6 +6,7 @@ // ranges that drive separate signals. // // 2026 October 7 +// Author: Desmond A. Kirkpatrick import 'package:rohd/rohd.dart'; import 'package:test/test.dart'; diff --git a/test/systemverilog_port_types_test.dart b/test/systemverilog_port_types_test.dart index 58765cd266..3a8473a629 100644 --- a/test/systemverilog_port_types_test.dart +++ b/test/systemverilog_port_types_test.dart @@ -56,6 +56,41 @@ class _PortTypesModule extends Module { } } +class _NestedPortTypesModule extends Module { + _NestedPortTypesModule() { + final source = + TypedLogicArray, LogicValue>( + [2], + ({name}) => TypedLogicArray( + [3], + ({name}) => Logic(name: name, width: 3), + name: name, + ), + numUnpackedDimensions: 1, + ); + final valuesIn = addTypedInput('valuesIn', source); + final child = _NestedPortTypesChild(valuesIn); + addTypedOutput('valuesOut', source.clone).gets(child.valuesOut); + } +} + +class _NestedPortTypesChild extends Module { + late final TypedLogicArray, LogicValue> + valuesOut; + + _NestedPortTypesChild( + TypedLogicArray, LogicValue> + valuesIn) { + valuesIn = addTypedInput('valuesIn', valuesIn); + valuesOut = addTypedOutput('valuesOut', valuesIn.clone); + for (var outer = 0; outer < 2; outer++) { + for (var inner = 0; inner < 3; inner++) { + valuesOut.at([outer]).at([inner]) <= valuesIn.at([outer]).at([inner]); + } + } + } +} + void main() { tearDown(() async { await Simulator.reset(); @@ -167,4 +202,77 @@ void main() { ); }); } + + // Icarus 12.0 compiles the variable modes below, but a child-driven + // unpacked output remains X. Verilator 5.020 simulates every mode. + test('nested outputs honor configured object and data types', () async { + final module = _NestedPortTypesModule(); + await module.build(); + final input = LogicValue.ofIterable([ + for (var value = 1; value <= 6; value++) LogicValue.ofInt(value, 3), + ]); + final vectors = [ + Vector({'valuesIn': input}, {'valuesOut': input}), + ]; + final configurations = [ + ( + name: 'implicit variable', + configuration: const SystemVerilogSynthesizerConfiguration(), + declaration: 'output logic [8:0] valuesOut [1:0]', + expectedCount: 2, + checkIverilog: false, + ), + ( + name: 'explicit variable', + configuration: const SystemVerilogSynthesizerConfiguration( + outputPortType: SystemVerilogPortTypeConfiguration(), + ), + declaration: 'output var logic [8:0] valuesOut [1:0]', + expectedCount: 2, + checkIverilog: false, + ), + ( + name: 'implicit net', + configuration: const SystemVerilogSynthesizerConfiguration( + outputPortType: SystemVerilogPortTypeConfiguration( + objectType: SystemVerilogPortType.implicit, + dataType: SystemVerilogPortType.implicit, + ), + ), + declaration: 'output [8:0] valuesOut [1:0]', + expectedCount: 2, + checkIverilog: true, + ), + ]; + + for (final configuration in configurations) { + final sv = module.dumpSystemVerilog( + configuration: configuration.configuration, + ); + expect( + RegExp(RegExp.escape(configuration.declaration)).allMatches(sv), + hasLength(configuration.expectedCount), + reason: configuration.name, + ); + if (configuration.checkIverilog) { + SimCompare.checkIverilogVector( + module, + vectors, + synthesizerConfiguration: configuration.configuration, + ); + } else { + SimCompare.checkIverilogVector( + module, + const [], + buildOnly: true, + synthesizerConfiguration: configuration.configuration, + ); + } + SimCompare.checkVerilatorVector( + module, + vectors, + synthesizerConfiguration: configuration.configuration, + ); + } + }, tags: ['verilator']); } diff --git a/test/typed_logic_array_inout_test.dart b/test/typed_logic_array_inout_test.dart new file mode 100644 index 0000000000..241f614790 --- /dev/null +++ b/test/typed_logic_array_inout_test.dart @@ -0,0 +1,375 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_logic_array_inout_test.dart +// Tests for typed logic array inout behavior. +// +// 2026 September 16 +// Author: Desmond A. Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +class _NetMatrixCell extends LogicStructure { + final LogicNet low; + final LogicNet high; + + factory _NetMatrixCell({String? name}) => _NetMatrixCell._( + LogicNet(name: 'low'), + LogicNet(name: 'high', width: 2), + name: name ?? 'netMatrixCell', + ); + + _NetMatrixCell._(this.low, this.high, {required String name}) + : super([low, high], name: name); + + @override + _NetMatrixCell clone({String? name}) => + _NetMatrixCell(name: name ?? this.name); +} + +class _ArrayValuedNetElementStructure extends LogicStructure { + final LogicNet prefix; + final TypedLogicArray, LogicValue> + matrix; + final LogicNet suffix; + + factory _ArrayValuedNetElementStructure({String? name}) => + _ArrayValuedNetElementStructure._( + LogicNet(name: 'prefix', width: 2), + TypedLogicArray, + LogicValue>( + [2], + ({name}) => TypedLogicArray<_NetMatrixCell, LogicValue>( + [3], + _NetMatrixCell.new, + name: name, + ), + name: 'matrix', + ), + LogicNet(name: 'suffix'), + name: name ?? 'arrayValuedNetElement', + ); + + _ArrayValuedNetElementStructure._( + this.prefix, + this.matrix, + this.suffix, { + required String name, + }) : super([prefix, matrix, suffix], name: name); + + @override + _ArrayValuedNetElementStructure clone({String? name}) => + _ArrayValuedNetElementStructure(name: name ?? this.name); +} + +class _NestedNetArrayStructure extends LogicStructure { + final LogicNet before; + final LogicArray lanes; + final LogicNet after; + + factory _NestedNetArrayStructure({ + String? name, + int numUnpackedDimensions = 0, + }) => + _NestedNetArrayStructure._( + LogicNet(name: 'before', width: 2), + LogicArray.net( + [2], + 3, + name: 'lanes', + numUnpackedDimensions: numUnpackedDimensions, + ), + LogicNet(name: 'after'), + name: name ?? 'nestedNet', + ); + + _NestedNetArrayStructure._( + this.before, + this.lanes, + this.after, { + required String name, + }) : super([before, lanes, after], name: name); + + @override + _NestedNetArrayStructure clone({String? name}) => _NestedNetArrayStructure( + name: name ?? this.name, + numUnpackedDimensions: lanes.numUnpackedDimensions, + ); +} + +class _NestedStructuredInOutDriveModule extends Module { + late final TypedLogicArray<_NestedNetArrayStructure, LogicValue> bus; + + _NestedStructuredInOutDriveModule( + TypedLogicArray<_NestedNetArrayStructure, LogicValue> source, + Logic enable, + Logic driveValue) { + bus = addTypedInOut('bus', source); + enable = addInput('enable', enable); + driveValue = addInput('driveValue', driveValue, width: bus.width); + final child = _NestedStructuredInOutDriveChild(bus, enable, driveValue); + addOutput('observed', width: bus.width).gets(child.observed); + } +} + +class _NestedStructuredInOutDriveChild extends Module { + late final Logic observed; + + _NestedStructuredInOutDriveChild( + TypedLogicArray<_NestedNetArrayStructure, LogicValue> source, + Logic enable, + Logic driveValue) { + final bus = addTypedInOut('bus', source); + enable = addInput('enable', enable); + driveValue = addInput('driveValue', driveValue, width: bus.width); + var driveOffset = 0; + for (final leaf in bus.leafElements) { + leaf <= + TriStateBuffer( + driveValue.getRange(driveOffset, driveOffset + leaf.width), + enable: enable, + ).out; + driveOffset += leaf.width; + } + observed = addOutput('observed', width: bus.width); + Combinational([observed < bus.packed]); + } +} + +class _ArrayValuedStructuredInOutDriveModule extends Module { + late final TypedLogicArray<_ArrayValuedNetElementStructure, LogicValue> bus; + + _ArrayValuedStructuredInOutDriveModule( + TypedLogicArray<_ArrayValuedNetElementStructure, LogicValue> source, + Logic enable, + Logic driveValue) { + bus = addTypedInOut('bus', source); + enable = addInput('enable', enable); + driveValue = addInput('driveValue', driveValue, width: bus.width); + final child = + _ArrayValuedStructuredInOutDriveChild(bus, enable, driveValue); + addOutput('observed', width: bus.width).gets(child.observed); + } +} + +class _ArrayValuedStructuredInOutDriveChild extends Module { + late final Logic observed; + + _ArrayValuedStructuredInOutDriveChild( + TypedLogicArray<_ArrayValuedNetElementStructure, LogicValue> source, + Logic enable, + Logic driveValue) { + final bus = addTypedInOut('bus', source); + enable = addInput('enable', enable); + driveValue = addInput('driveValue', driveValue, width: bus.width); + var driveOffset = 0; + for (final leaf in bus.leafElements) { + leaf <= + TriStateBuffer( + driveValue.getRange(driveOffset, driveOffset + leaf.width), + enable: enable, + ).out; + driveOffset += leaf.width; + } + observed = addOutput('observed', width: bus.width); + Combinational([observed < bus.packed]); + } +} + +/// Checks the backends used for four-state inout resolution. +/// +/// This specialized suite intentionally has narrower backend coverage than +/// the ordinary typed-array tests and does not include Verilator. +Future _checkRohdAndIverilogVectors( + Module module, + List vectors, +) async { + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); +} + +void _expectCompatibleNetConnectDefinition(String systemVerilog) { + expect( + systemVerilog, + contains('module net_connect #(parameter int WIDTH=1) (w, w);'), + ); + expect(systemVerilog, contains('inout wire[WIDTH-1:0] w;')); + expect(systemVerilog, isNot(contains('tran ('))); +} + +List> _triStateCells( + Map moduleDefinitions) => + [ + for (final moduleDefinition + in moduleDefinitions.values.cast>()) + for (final cell in (moduleDefinition['cells'] as Map) + .values + .cast>()) + if (cell['type'] == r'$tribuf') cell, + ]; + +void _expectTriStateCells( + Map moduleDefinitions, { + required int count, +}) { + final cells = _triStateCells(moduleDefinitions); + expect(cells, hasLength(count)); + for (final cell in cells) { + final directions = cell['port_directions'] as Map; + final connections = cell['connections'] as Map; + expect(directions['Y'], 'output'); + expect(connections['Y'] as List, isNotEmpty); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('TypedLogicArray inout', () { + test('drives and releases a nested structured array', () async { + final source = TypedLogicArray<_NestedNetArrayStructure, LogicValue>( + [2], + ({name}) => _NestedNetArrayStructure( + name: name, + numUnpackedDimensions: 1, + ), + ); + final enable = Logic(); + final driveValue = Logic(width: source.width); + final module = + _NestedStructuredInOutDriveModule(source, enable, driveValue); + await module.build(); + final vectors = [ + Vector( + {'bus': 0x2a155, 'enable': 0, 'driveValue': 0}, + {'observed': 0x2a155}, + ), + Vector( + {'bus': LogicValue.z, 'enable': 1, 'driveValue': 0x15caa}, + {'observed': 0x15caa}, + ), + Vector( + {'bus': LogicValue.z, 'enable': 0, 'driveValue': 0}, + {'bus': LogicValue.z}, + ), + Vector( + { + 'bus': LogicValue.ofInt(0, source.width), + 'enable': 1, + 'driveValue': LogicValue.ofBigInt( + (BigInt.one << source.width) - BigInt.one, + source.width, + ), + }, + {'observed': LogicValue.x}, + ), + ]; + + await _checkRohdAndIverilogVectors(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('inout wire [1:0][8:0] bus')); + _expectCompatibleNetConnectDefinition(sv); + expect(source.numUnpackedDimensions, 0); + expect(source.at([0]).lanes.numUnpackedDimensions, 1); + + final netlist = jsonDecode( + NetlistSynthesizer().synthesizeToJson(module), + ) as Map; + final modules = netlist['modules'] as Map; + final top = + modules['_NestedStructuredInOutDriveModule'] as Map; + final ports = top['ports'] as Map; + final busPort = ports['bus'] as Map; + expect(ports, contains('observed')); + expect(busPort['direction'], 'inout'); + expect(busPort['bits'], hasLength(source.width)); + _expectTriStateCells( + modules, + count: source.leafElements.length, + ); + }); + + test('resolves array-valued structured fields', () async { + final source = + TypedLogicArray<_ArrayValuedNetElementStructure, LogicValue>( + [2], + _ArrayValuedNetElementStructure.new, + ); + final enable = Logic(); + final driveValue = Logic(width: source.width); + final module = + _ArrayValuedStructuredInOutDriveModule(source, enable, driveValue); + await module.build(); + + final externalValue = LogicValue.ofBigInt( + BigInt.parse('2aa9552d5', radix: 16), + source.width, + ); + final internalValue = LogicValue.ofBigInt( + BigInt.parse('1556aa5ab', radix: 16), + source.width, + ); + final allOnes = LogicValue.ofBigInt( + (BigInt.one << source.width) - BigInt.one, + source.width, + ); + final allZeroes = LogicValue.ofBigInt(BigInt.zero, source.width); + final vectors = [ + Vector( + {'bus': externalValue, 'enable': 0, 'driveValue': allZeroes}, + {'observed': externalValue}, + ), + Vector( + {'bus': LogicValue.z, 'enable': 1, 'driveValue': internalValue}, + {'observed': internalValue}, + ), + Vector( + {'bus': LogicValue.z, 'enable': 0, 'driveValue': allZeroes}, + {'bus': LogicValue.z}, + ), + Vector( + {'bus': allZeroes, 'enable': 1, 'driveValue': allOnes}, + {'observed': LogicValue.x}, + ), + ]; + + await _checkRohdAndIverilogVectors(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('inout wire [1:0][20:0] bus')); + _expectCompatibleNetConnectDefinition(sv); + + final netlist = jsonDecode( + NetlistSynthesizer().synthesizeToJson(module), + ) as Map; + final modules = netlist['modules'] as Map; + final top = modules['_ArrayValuedStructuredInOutDriveModule'] + as Map; + final ports = top['ports'] as Map; + final busPort = ports['bus'] as Map; + final busType = busPort['logic_type'] as Map; + final elementType = busType['elementType'] as Map; + final fields = + (elementType['fields'] as List).cast>(); + final matrixType = fields.singleWhere( + (field) => field['name'] == 'matrix', + )['type'] as Map; + final matrixElementType = + matrixType['elementType'] as Map; + expect(busPort['direction'], 'inout'); + expect(busPort['bits'], hasLength(source.width)); + expect(busType['arrayDims'], [2]); + expect(matrixType['arrayDims'], [2]); + expect(matrixElementType['arrayDims'], [3]); + _expectTriStateCells( + modules, + count: source.leafElements.length, + ); + }); + }); +} diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart new file mode 100644 index 0000000000..9ab1728a41 --- /dev/null +++ b/test/typed_logic_array_test.dart @@ -0,0 +1,2169 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_logic_array_test.dart +// Tests for typed logic and logic value arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +// Test fixtures intentionally use mutable literals to exercise array APIs. +// ignore_for_file: prefer_const_literals_to_create_immutables + +import 'dart:convert'; + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +class _SampleStructure extends LogicStructure { + final Logic low; + final Logic high; + final String format; + + factory _SampleStructure({String? name, String format = 'sample'}) => + _SampleStructure._( + Logic(name: 'low'), + Logic(name: 'high', width: 2), + format, + name: name ?? 'sample', + ); + + _SampleStructure._(this.low, this.high, this.format, {required String name}) + : super([low, high], name: name); + + @override + _SampleStructure clone({String? name}) => + _SampleStructure(name: name ?? this.name, format: format); +} + +@immutable +class _SampleValue { + final LogicValue value; + + _SampleValue(this.value) { + if (value.width != 3) { + throw ArgumentError.value(value, 'value', 'Must have width 3.'); + } + } + + LogicValue get low => value[0]; + + LogicValue get high => value.getRange(1, 3); + + @override + bool operator ==(Object other) => + other is _SampleValue && value == other.value; + + @override + int get hashCode => value.hashCode; +} + +_SampleValue _decodeSampleValue(LogicValue value) => _SampleValue(value); + +LogicValue _encodeSampleValue(_SampleValue value) => value.value; + +const _sampleValueCodec = LogicValueCodec<_SampleValue>( + decode: _decodeSampleValue, + encode: _encodeSampleValue, +); + +class _NetSampleStructure extends LogicStructure { + final LogicNet low; + final LogicNet high; + + factory _NetSampleStructure({String? name}) => _NetSampleStructure._( + LogicNet(name: 'low'), + LogicNet(name: 'high', width: 2), + name: name ?? 'netSample', + ); + + _NetSampleStructure._(this.low, this.high, {required String name}) + : super([low, high], name: name); + + @override + _NetSampleStructure clone({String? name}) => + _NetSampleStructure(name: name ?? this.name); +} + +class _PartiallyNetSampleStructure extends LogicStructure { + factory _PartiallyNetSampleStructure({String? name}) => + _PartiallyNetSampleStructure._( + Logic(name: 'low'), + LogicNet(name: 'high', width: 2), + name: name ?? 'partiallyNetSample', + ); + + _PartiallyNetSampleStructure._( + Logic low, + LogicNet high, { + required String name, + }) : super([low, high], name: name); + + @override + _PartiallyNetSampleStructure clone({String? name}) => + _PartiallyNetSampleStructure(name: name ?? this.name); +} + +class _EmptyStructure extends LogicStructure { + _EmptyStructure({String? name}) + : super(const [], name: name ?? 'empty'); + + @override + _EmptyStructure clone({String? name}) => + _EmptyStructure(name: name ?? this.name); +} + +class _SampleArray extends TypedLogicArray<_SampleStructure, _SampleValue> { + final String schema; + final List? _constructionDimensionNames; + + // ignore: use_super_parameters - fixes the element builder for this subtype. + _SampleArray( + List dimensions, { + required this.schema, + List? dimensionNames, + String? name, + Naming? naming, + int numUnpackedDimensions = 0, + }) : _constructionDimensionNames = dimensionNames == null + ? null + : List.unmodifiable(dimensionNames), + super( + dimensions, + _SampleStructure.new, + valueCodec: _sampleValueCodec, + dimensionNames: dimensionNames, + name: name, + naming: naming, + numUnpackedDimensions: numUnpackedDimensions, + ); + + @override + _SampleArray createClone({ + String? name, + Naming? naming, + int? numUnpackedDimensions, + }) => + _SampleArray( + dimensions, + schema: schema, + dimensionNames: _constructionDimensionNames, + name: name ?? this.name, + naming: naming, + numUnpackedDimensions: + numUnpackedDimensions ?? this.numUnpackedDimensions, + ); + + @override + _SampleArray clone({String? name}) => super.clone(name: name) as _SampleArray; +} + +class _TypedArrayPortModule extends Module { + TypedLogicArray<_SampleStructure, _SampleValue> get valuesOut => + output('valuesOut') as TypedLogicArray<_SampleStructure, _SampleValue>; + + _TypedArrayPortModule( + TypedLogicArray<_SampleStructure, _SampleValue> valuesIn) { + valuesIn = addTypedInput('valuesIn', valuesIn); + final valuesOut = addTypedOutput('valuesOut', valuesIn.clone); + for (final (index, source) in valuesIn.arrayElements.indexed) { + final destination = valuesOut.arrayElements[index]; + destination.low <= source.low; + destination.high <= ~source.high; + } + } +} + +class _TypedNetArrayPortModule extends Module { + late final TypedLogicArray<_NetSampleStructure, _SampleValue> values; + + _TypedNetArrayPortModule( + TypedLogicArray<_NetSampleStructure, _SampleValue> source) { + values = addTypedInOut('values', source); + } +} + +class _TypedArrayInterface extends Interface<_TypedArrayDirection> { + _TypedArrayInterface() { + setPorts([ + _SampleArray( + [2], + schema: 'interface', + name: 'values', + ), + ], [ + _TypedArrayDirection.data + ]); + } + + @override + _TypedArrayInterface clone() => _TypedArrayInterface(); +} + +enum _TypedArrayDirection { data } + +class _TypedArrayInterfaceModule extends Module { + _TypedArrayInterfaceModule(_TypedArrayInterface values) { + _TypedArrayInterface().connectIO( + this, + values, + inputTags: [_TypedArrayDirection.data], + ); + } +} + +class _TypedArrayHierarchy extends Module { + TypedLogicArray<_SampleStructure, _SampleValue> get valuesOut => + output('valuesOut') as TypedLogicArray<_SampleStructure, _SampleValue>; + + _TypedArrayHierarchy(TypedLogicArray<_SampleStructure, _SampleValue> source) { + final valuesIn = addTypedInput('valuesIn', source); + final child = _TypedArrayPortModule(valuesIn); + addTypedOutput('valuesOut', valuesIn.clone).gets(child.valuesOut); + } +} + +class _NestedArrayStructure extends LogicStructure { + final Logic before; + final LogicArray lanes; + final TypedLogicArray<_SampleStructure, LogicValue> samples; + final Logic after; + + factory _NestedArrayStructure({ + String? name, + int numUnpackedDimensions = 0, + }) => + _NestedArrayStructure._( + Logic(name: 'before', width: 2), + LogicArray( + [2], + 3, + name: 'lanes', + numUnpackedDimensions: numUnpackedDimensions, + ), + TypedLogicArray<_SampleStructure, LogicValue>( + [2], + _SampleStructure.new, + name: 'samples', + numUnpackedDimensions: numUnpackedDimensions, + ), + Logic(name: 'after'), + name: name ?? 'nested', + ); + + _NestedArrayStructure._( + this.before, + this.lanes, + this.samples, + this.after, { + required String name, + }) : super([before, lanes, samples, after], name: name); + + @override + _NestedArrayStructure clone({String? name}) => _NestedArrayStructure( + name: name ?? this.name, + numUnpackedDimensions: lanes.numUnpackedDimensions, + ); +} + +class _NineBitNestedStructure extends LogicStructure { + final Logic prefix; + final LogicArray payload; + final Logic suffix; + + factory _NineBitNestedStructure({String? name}) => _NineBitNestedStructure._( + Logic(name: 'prefix', width: 2), + LogicArray([2], 3, name: 'payload'), + Logic(name: 'suffix'), + name: name ?? 'nineBitNested', + ); + + _NineBitNestedStructure._( + this.prefix, + this.payload, + this.suffix, { + required String name, + }) : super([prefix, payload, suffix], name: name); + + @override + _NineBitNestedStructure clone({String? name}) => + _NineBitNestedStructure(name: name ?? this.name); +} + +class _MatrixCell extends LogicStructure { + final Logic low; + final Logic high; + + factory _MatrixCell({String? name}) => _MatrixCell._( + Logic(name: 'low'), + Logic(name: 'high', width: 2), + name: name ?? 'matrixCell', + ); + + _MatrixCell._(this.low, this.high, {required String name}) + : super([low, high], name: name); + + @override + _MatrixCell clone({String? name}) => _MatrixCell(name: name ?? this.name); +} + +class _ArrayValuedElementStructure extends LogicStructure { + final Logic prefix; + final TypedLogicArray, LogicValue> + matrix; + final Logic suffix; + final List matrixDimensions; + final int matrixUnpackedDimensions; + + factory _ArrayValuedElementStructure({ + List matrixDimensions = const [2], + int matrixUnpackedDimensions = 0, + String? name, + }) => + _ArrayValuedElementStructure._( + Logic(name: 'prefix', width: 2), + TypedLogicArray, LogicValue>( + matrixDimensions, + ({name}) => TypedLogicArray<_MatrixCell, LogicValue>( + [3], + _MatrixCell.new, + name: name, + ), + name: 'matrix', + numUnpackedDimensions: matrixUnpackedDimensions, + ), + Logic(name: 'suffix'), + matrixDimensions: List.unmodifiable(matrixDimensions), + matrixUnpackedDimensions: matrixUnpackedDimensions, + name: name ?? 'arrayValuedElement', + ); + + _ArrayValuedElementStructure._( + this.prefix, + this.matrix, + this.suffix, { + required this.matrixDimensions, + required this.matrixUnpackedDimensions, + required String name, + }) : super([prefix, matrix, suffix], name: name); + + @override + _ArrayValuedElementStructure clone({String? name}) => + _ArrayValuedElementStructure( + matrixDimensions: matrixDimensions, + matrixUnpackedDimensions: matrixUnpackedDimensions, + name: name ?? this.name, + ); +} + +class _NestedPackedOffsetModule extends Module { + _NestedPackedOffsetModule( + TypedLogicArray, + LogicValue> + source) { + final values = addTypedInput('values', source); + addOutput('selected', width: 2) <= values.at([1]).at([2]).prefix; + } +} + +class _NestedArrayBoundaryModule extends Module { + _NestedArrayBoundaryModule( + TypedLogicArray, + LogicValue> + source) { + final input = addTypedInput('valuesIn', source); + final child = _NestedArrayBoundaryChild(input); + addTypedOutput('valuesOut', input.clone).gets(child.valuesOut); + } +} + +class _NestedArrayBoundaryChild extends Module { + late final TypedLogicArray, + LogicValue> valuesOut; + + _NestedArrayBoundaryChild( + TypedLogicArray, + LogicValue> + input) { + input = addTypedInput('valuesIn', input); + valuesOut = addTypedOutput('valuesOut', input.clone); + for (var outer = 0; outer < 2; outer++) { + for (var inner = 0; inner < 3; inner++) { + final sourceElement = input.at([1 - outer]).at([2 - inner]); + final destination = valuesOut.at([outer]).at([inner]); + destination.before <= sourceElement.before; + destination.after <= ~sourceElement.after; + for (var lane = 0; lane < 2; lane++) { + destination.lanes.at([lane]) <= sourceElement.lanes.at([1 - lane]); + final sourceSample = sourceElement.samples.at([1 - lane]); + final destinationSample = destination.samples.at([lane]); + destinationSample.low <= sourceSample.low; + destinationSample.high <= ~sourceSample.high; + } + } + } + } +} + +class _ArrayValuedElementFieldModule extends Module { + _ArrayValuedElementFieldModule( + TypedLogicArray<_ArrayValuedElementStructure, LogicValue> source, { + List matrixIndices = const [1], + }) { + final input = addTypedInput('values', source); + final child = _ArrayValuedElementFieldChild(input, matrixIndices); + addOutput('selected', width: 2).gets(child.selected); + } +} + +class _ArrayValuedElementFieldChild extends Module { + late final Logic selected; + + _ArrayValuedElementFieldChild( + TypedLogicArray<_ArrayValuedElementStructure, LogicValue> source, + List matrixIndices, + ) { + final values = addTypedInput('values', source); + selected = addOutput('selected', width: 2); + selected <= values.at([1]).matrix.at(matrixIndices).at([2]).high; + } +} + +class _ArrayValuedElementPayloadModule extends Module { + _ArrayValuedElementPayloadModule( + TypedLogicArray<_ArrayValuedElementStructure, LogicValue> source) { + final values = addTypedInput('values', source); + addOutput('selected', width: 9) <= values.at([1]).matrix.at([1]); + } +} + +class _MixedNestedArrayModule extends Module { + _MixedNestedArrayModule( + TypedLogicArray, LogicValue> source) { + final input = addTypedInput('valuesIn', source); + final child = _MixedNestedArrayChild(input); + addTypedOutput('valuesOut', input.clone).gets(child.valuesOut); + } +} + +class _MixedNestedArrayChild extends Module { + late final TypedLogicArray, LogicValue> + valuesOut; + + _MixedNestedArrayChild( + TypedLogicArray, LogicValue> input) { + input = addTypedInput('valuesIn', input); + valuesOut = addTypedOutput('valuesOut', input.clone); + for (final (outerIndex, sourceElement) in input.indexedElements) { + final destination = valuesOut.at(outerIndex); + for (var row = 0; row < 4; row++) { + for (var column = 0; column < 5; column++) { + destination.at([row, column]) <= + sourceElement.at([3 - row, 4 - column]); + } + } + } + } +} + +class _TypedConditionalModule extends Module { + late final TypedLogicArray<_SampleStructure, LogicValue> valuesOut; + + _TypedConditionalModule( + TypedLogicArray<_SampleStructure, LogicValue> first, + TypedLogicArray<_SampleStructure, LogicValue> second, + Logic select, + ) { + first = addTypedInput('first', first); + second = addTypedInput('second', second); + select = addInput('select', select); + valuesOut = addTypedOutput('valuesOut', first.clone); + Combinational([ + If( + select, + then: [valuesOut < first], + orElse: [valuesOut < second], + ), + ]); + } +} + +LogicValue _nestedPacked( + int before, + int lane0, + int lane1, + int sample0, + int sample1, + int after, +) => + LogicValue.ofInt( + before | + (lane0 << 2) | + (lane1 << 5) | + (sample0 << 8) | + (sample1 << 11) | + (after << 14), + 15, + ); + +LogicValue _nineBitNestedPacked( + int prefix, int payload0, int payload1, int suffix) => + LogicValue.ofInt( + prefix | (payload0 << 2) | (payload1 << 5) | (suffix << 8), + 9, + ); + +LogicValue _arrayValuedElementPacked( + List matrixDimensions, + List matrixIndices, +) { + var matrixElementIndex = 0; + for (var index = 0; index < matrixDimensions.length; index++) { + matrixElementIndex = + matrixElementIndex * matrixDimensions[index] + matrixIndices[index]; + } + final matrixWidth = matrixDimensions.fold(1, (width, size) => width * size); + return LogicValue.ofBigInt( + BigInt.from(2) << (2 + matrixElementIndex * 9 + 7), + 2 + matrixWidth * 9 + 1, + ); +} + +class _TypedInputModule extends Module { + late final T values; + + _TypedInputModule(T source) { + values = addTypedInput('values', source); + } +} + +void main() { + group('TypedLogicArray', () { + test('requires at least one dimension', () { + expect( + () => TypedLogicArray( + [], + ({name}) => Logic(name: name), + ), + throwsA( + predicate( + (exception) => exception.reason.contains('at least 1 dimension'), + ), + ), + ); + }); + + test('keeps structured leaves at the array boundary', () { + final values = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 3], + _SampleStructure.new, + dimensionNames: const ['row_', 'column_'], + ); + + expect(values, isA>()); + expect(values.dimensions, equals([2, 3])); + expect(values.elementWidth, 3); + expect(values.arrayElements, hasLength(6)); + expect(values.arrayElements, everyElement(isA<_SampleStructure>())); + expect(values.leafElements, hasLength(12)); + expect(values.at([1, 2]), same(values.arrayElements[5])); + expect(values.indexedElements.last.$1, equals([1, 2])); + }); + + test('preserves typed metadata through hardware cloning', () { + final values = _SampleArray( + [2, 2], + schema: 'sample-v2', + dimensionNames: const ['row_', 'column_'], + numUnpackedDimensions: 1, + ); + final assigned = TypedLogicValueArray<_SampleValue>.fromFlat( + [2, 2], + 3, + [ + _SampleValue(LogicValue.ofString('1xz')), + _SampleValue(LogicValue.ofString('z01')), + _SampleValue(LogicValue.ofString('010')), + _SampleValue(LogicValue.ofString('111')), + ], + codec: _sampleValueCodec, + ); + + values.put(assigned); + + expect(values.value, isA>()); + expect(values.value.arrayValues, assigned.arrayValues); + expect(values.value.packed, assigned.packed); + expect(values.value.at([0, 0]).value, LogicValue.ofString('1xz')); + expect(values.value.at([0, 1]).high, LogicValue.ofString('z0')); + expect(identical(values.value.codec, _sampleValueCodec), isTrue); + + final transformed = [ + values.clone(), + ]; + for (final result in transformed) { + expect(result, isA>()); + expect(identical(result.valueCodec, _sampleValueCodec), isTrue); + expect(result.numUnpackedDimensions, 1); + expect(result.value, isA>()); + } + }); + + test('preserves typed values through inherited inject and previousValue', + () async { + addTearDown(Simulator.reset); + final values = _SampleArray([2], schema: 'sample-v2')..put(0); + final injected = TypedLogicValueArray<_SampleValue>.fromFlat( + [2], + 3, + [ + _SampleValue(LogicValue.ofString('1xz')), + _SampleValue(LogicValue.ofString('z01')), + ], + codec: _sampleValueCodec, + ); + final changes = []; + final subscription = values.changed.listen(changes.add); + addTearDown(subscription.cancel); + + values.inject(injected); + expect(values.value.packed, LogicValue.ofInt(0, 6)); + + await Simulator.run(); + + expect(values.value, isA>()); + expect(values.value.arrayValues, injected.arrayValues); + expect(values.previousValue, isA>()); + expect(values.previousValue!.packed, LogicValue.ofInt(0, 6)); + expect(identical(values.previousValue!.codec, _sampleValueCodec), isTrue); + expect(changes, hasLength(1)); + expect(changes.single.previousValue, LogicValue.ofInt(0, 6)); + expect(changes.single.newValue, injected.packed); + }); + + test('requires semantic codecs and compatible element formats', () { + expect( + () => TypedLogicArray<_SampleStructure, _SampleValue>( + [1], + _SampleStructure.new, + ), + throwsArgumentError, + ); + + var buildIndex = 0; + expect( + () => TypedLogicArray<_SampleStructure, _SampleValue>( + [2], + ({name}) => _SampleStructure( + name: name, + format: buildIndex++ == 0 ? 'binary16' : 'binary32', + ), + valueCodec: _sampleValueCodec, + elementCompatibility: (prototype, element) => + prototype.format == element.format, + ), + throwsA(isA()), + ); + + final wrongWidthCodec = LogicValueCodec<_SampleValue>( + decode: _decodeSampleValue, + encode: (value) => value.value.zeroExtend(4), + ); + expect( + () => TypedLogicArray<_SampleStructure, _SampleValue>( + [1], + _SampleStructure.new, + valueCodec: wrongWidthCodec, + ), + throwsA(isA()), + ); + + var layoutBuildIndex = 0; + expect( + () => TypedLogicArray( + [2], + ({name}) { + final reverseWidths = layoutBuildIndex++ == 1; + return LogicStructure( + [ + Logic(name: 'first', width: reverseWidths ? 2 : 1), + Logic(name: 'second', width: reverseWidths ? 1 : 2), + ], + name: name, + ); + }, + ), + throwsA(isA()), + ); + + var nestedArrayBuildIndex = 0; + expect( + () => TypedLogicArray, LogicValue>( + [2], + ({name}) => TypedLogicArray( + [2], + ({name}) => Logic(name: name, width: 3), + name: name, + numUnpackedDimensions: nestedArrayBuildIndex++, + ), + ), + throwsA(isA()), + ); + }); + + test('matches ordinary LogicArray contracts for Logic leaves', () { + final arrays = >[ + LogicArray( + [2, 3], + 4, + name: 'ordinary', + numUnpackedDimensions: 1, + ), + TypedLogicArray( + [2, 3], + ({name}) => Logic(name: name, width: 4), + name: 'typed', + numUnpackedDimensions: 1, + ), + ]; + final packed = LogicValueArray.fromFlatInts( + [6], + [1, 2, 3, 4, 5, 6], + elementWidth: 4, + ); + + for (final array in arrays) { + array.put(packed); + + expect(array.dimensions, [2, 3]); + expect(array.elementWidth, 4); + expect(array.width, 24); + expect(array.numUnpackedDimensions, 1); + expect( + array.indexedElements.map((entry) => entry.$1), + [ + [0, 0], + [0, 1], + [0, 2], + [1, 0], + [1, 1], + [1, 2], + ], + ); + expect(array.at([1, 2]), same(array.arrayElements[5])); + expect( + array.value.arrayValues.map((value) => value.toInt()), + [1, 2, 3, 4, 5, 6], + ); + + final slices = array.majorSlices.toList(growable: false); + expect(slices, hasLength(2)); + expect(slices.first, same(array.elements.first)); + expect(slices.last.dimensions, [3]); + expect(slices.last.elementWidth, 4); + expect(slices.last.numUnpackedDimensions, 0); + expect(slices.last.at([2]), same(array.at([1, 2]))); + + final clone = array.clone(); + expect(clone is LogicArray, array is LogicArray); + expect(clone.dimensions, array.dimensions); + expect(clone.elementWidth, array.elementWidth); + expect(clone.numUnpackedDimensions, array.numUnpackedDimensions); + expect(clone.isNet, array.isNet); + } + }); + + test('inherits packed LogicArray operations for structured leaves', () { + final source = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 2], + _SampleStructure.new, + dimensionNames: const ['row_', 'column_'], + numUnpackedDimensions: 1, + )..put(LogicValue.ofInt(0xabc, 12)); + final differentlyShaped = TypedLogicArray<_SampleStructure, LogicValue>( + [4], + _SampleStructure.new, + )..gets(source); + final slice = source.slice(8, 2); + final update = Const(0x15, width: 5); + final updated = source.withSet(2, update); + + expect(differentlyShaped.value.packed, source.value.packed); + expect(slice.value, source.value.packed.slice(8, 2)); + expect(updated, isA>()); + final typedUpdated = + updated as TypedLogicArray<_SampleStructure, LogicValue>; + expect(typedUpdated.dimensions, [2, 2]); + expect(typedUpdated.elementWidth, 3); + expect(typedUpdated.numUnpackedDimensions, 1); + expect( + typedUpdated.value.packed, + source.value.packed.withSet(2, update.value), + ); + }); + + test('withSet updates a range contained within one structured leaf', () { + final values = TypedLogicArray( + [2], + ({name}) => LogicStructure( + [Logic(name: 'wide', width: 4)], + name: name, + ), + )..put(LogicValue.ofString('10100101')); + final update = Const(1); + + final updated = values.withSet(2, update); + + expect(updated, isA>()); + expect( + updated.value, + values.value.packed.withSet(2, update.value), + ); + }); + + test('majorSlices returns existing typed child arrays', () { + final values = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 3, 2], + _SampleStructure.new, + dimensionNames: const ['row_', 'column_', 'lane_'], + numUnpackedDimensions: 2, + ); + final slices = values.majorSlices.toList(growable: false); + + expect(slices, hasLength(2)); + expect(slices.first, same(values.elements.first)); + expect(slices.last, same(values.elements.last)); + for (final slice in slices) { + expect(slice, isA>()); + expect(slice.dimensions, [3, 2]); + expect(slice.elementWidth, 3); + expect(slice.numUnpackedDimensions, 1); + expect( + slice.arrayElements, + everyElement(isA<_SampleStructure>()), + ); + } + expect(slices.last.at([2, 1]), same(values.at([1, 2, 1]))); + expect( + () => TypedLogicArray<_SampleStructure, LogicValue>( + [2], + _SampleStructure.new, + ).majorSlices.toList(), + throwsA(isA()), + ); + }); + + test('reports immediate array ancestry for structured and nested elements', + () { + final values = TypedLogicArray<_ArrayValuedElementStructure, LogicValue>( + [2], + _ArrayValuedElementStructure.new, + ); + final element = values.at([1]); + final matrix = element.matrix; + final row = matrix.at([1]); + final cell = row.at([2]); + + expect(values.isArrayMember, isFalse); + expect(values.arrayIndex, isNull); + expect(element.isArrayMember, isTrue); + expect(element.arrayIndex, 1); + expect(element.prefix.isArrayMember, isFalse); + expect(element.prefix.arrayIndex, isNull); + expect(matrix.isArrayMember, isFalse); + expect(matrix.arrayIndex, isNull); + expect(row.isArrayMember, isTrue); + expect(row.arrayIndex, 1); + expect(cell.isArrayMember, isTrue); + expect(cell.arrayIndex, 2); + expect(cell.high.isArrayMember, isFalse); + expect(cell.high.arrayIndex, isNull); + }); + + test('provides typed indexing and cloning', () { + final values = TypedLogicArray( + [2, 2], + ({name}) => Logic(name: name, width: 8), + ); + final clone = values.clone(name: 'clone'); + + expect(values.at([1, 0]), same(values.arrayElements[2])); + expect(clone, isA>()); + expect(clone.name, 'clone'); + expect(clone.arrayElements, hasLength(4)); + values <= LogicArray([4], 8); + expect( + () => values <= LogicArray([2, 2], 4), + throwsA(isA()), + ); + }); + + test('preserves metadata and snapshots across zero dimensions', () { + for (final dimensions in [ + [0, 2], + [2, 0], + [2, 0, 3], + ]) { + var builds = 0; + final values = TypedLogicArray<_SampleStructure, LogicValue>( + dimensions, + ({name}) { + builds++; + return _SampleStructure(name: name); + }, + numUnpackedDimensions: 1, + ); + + expect(builds, 1, reason: '$dimensions'); + expect(values.width, 0, reason: '$dimensions'); + expect(values.elementWidth, 3, reason: '$dimensions'); + expect(values.arrayElements, isEmpty, reason: '$dimensions'); + expect(values.indexedElements, isEmpty, reason: '$dimensions'); + expect(values.value.dimensions, dimensions, reason: '$dimensions'); + expect(values.value.elementWidth, 3, reason: '$dimensions'); + expect(values.value.packed, LogicValue.empty, reason: '$dimensions'); + expect(values.previousValue, isNotNull, reason: '$dimensions'); + expect( + values.previousValue!.packed, + LogicValue.empty, + reason: '$dimensions', + ); + + final clone = values.clone(); + expect(clone.dimensions, dimensions, reason: '$dimensions'); + expect(clone.elementWidth, 3, reason: '$dimensions'); + expect(clone.numUnpackedDimensions, 1, reason: '$dimensions'); + expect(clone.arrayElements, isEmpty, reason: '$dimensions'); + + final slices = values.majorSlices.toList(growable: false); + expect(slices, hasLength(dimensions.first), reason: '$dimensions'); + if (slices.isNotEmpty) { + expect(slices.first.dimensions, dimensions.sublist(1)); + expect(slices.first.arrayElements, isEmpty); + } + expect( + () => values.at(List.filled(dimensions.length, 0)), + throwsA(isA()), + reason: '$dimensions', + ); + } + }); + + test('rejects zero-width arrays during SystemVerilog synthesis', () async { + for (final values in >[ + TypedLogicArray( + [2, 0], + ({name}) => Logic(name: name, width: 3), + ), + TypedLogicArray( + [2], + ({name}) => Logic(name: name, width: 0), + ), + ]) { + final module = _TypedInputModule(values); + await module.build(); + expect( + module.dumpSystemVerilog, + throwsA( + isA().having( + (exception) => exception.message, + 'message', + contains('zero-width array'), + ), + ), + ); + } + }); + + test('ordinary assignment drives structured leaves in row-major order', () { + final values = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 2], + _SampleStructure.new, + ); + final packed = LogicArray([2, 2], 3) + ..put(LogicValueArray.fromFlatInts( + [4], + [1, 2, 3, 4], + elementWidth: 3, + )); + + values <= packed; + + expect(values.value.packed, packed.value.packed); + expect( + values.value.arrayValues.map((value) => value.toInt()), + [1, 2, 3, 4], + ); + expect( + values.leafElements.every((leaf) => leaf.srcConnections.isNotEmpty), + isTrue, + ); + }); + + test('conditional assignment drives every typed structured leaf', () async { + await Simulator.reset(); + addTearDown(Simulator.reset); + final first = TypedLogicArray<_SampleStructure, LogicValue>( + [2], + _SampleStructure.new, + ); + final second = TypedLogicArray<_SampleStructure, LogicValue>( + [2], + _SampleStructure.new, + ); + final module = _TypedConditionalModule(first, second, Logic()); + await module.build(); + final vectors = [ + Vector( + {'first': 0x09, 'second': 0x32, 'select': 0}, + {'valuesOut': 0x32}, + ), + Vector( + {'first': 0x09, 'second': 0x32, 'select': 1}, + {'valuesOut': 0x09}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkVerilatorVector(module, vectors); + }, tags: ['verilator']); + + test('derives the net kind for empty multidimensional typed arrays', () { + var logicBuilds = 0; + var netBuilds = 0; + final logicValues = TypedLogicArray( + [2, 0], + ({name}) { + logicBuilds++; + return Logic(name: name, width: 8); + }, + ); + final netValues = TypedLogicArray( + [2, 3, 0], + ({name}) { + netBuilds++; + return LogicNet(name: name, width: 8); + }, + ); + + expect(logicBuilds, 1); + expect(netBuilds, 1); + expect(logicValues.isNet, isFalse); + expect(logicValues.elements.every((element) => !element.isNet), isTrue); + expect(netValues.isNet, isTrue); + expect(netValues.elements.every((element) => element.isNet), isTrue); + expect( + _TypedInputModule(logicValues).values, + isA>(), + ); + }); + + test('requires a uniform recursive net composition', () { + var buildIndex = 0; + expect( + () => TypedLogicArray( + [2], + ({name}) => buildIndex++ == 0 + ? Logic(name: name, width: 3) + : LogicNet(name: name, width: 3), + ), + throwsA(isA()), + ); + expect( + () => TypedLogicArray<_PartiallyNetSampleStructure, LogicValue>( + [2], + _PartiallyNetSampleStructure.new, + ), + throwsA(isA()), + ); + expect( + () => TypedLogicArray<_PartiallyNetSampleStructure, LogicValue>( + [0], + _PartiallyNetSampleStructure.new, + ), + throwsA(isA()), + ); + var structureBuildIndex = 0; + expect( + () => TypedLogicArray( + [2], + ({name}) => structureBuildIndex++ == 0 + ? _SampleStructure(name: name) + : _NetSampleStructure(name: name), + ), + throwsA(isA()), + ); + + final logicValues = TypedLogicArray<_SampleStructure, LogicValue>( + [2], + _SampleStructure.new, + ); + final emptyStructureValues = TypedLogicArray<_EmptyStructure, LogicValue>( + [0], + _EmptyStructure.new, + ); + final netValues = TypedLogicArray<_NetSampleStructure, LogicValue>( + [2, 2], + _NetSampleStructure.new, + ); + expect(logicValues.isNet, isFalse); + expect(emptyStructureValues.isNet, isFalse); + expect(netValues.isNet, isTrue); + expect(netValues.arrayElements, everyElement(isA<_NetSampleStructure>())); + expect( + netValues.arrayElements + .expand((element) => element.leafElements) + .every((element) => element.isNet), + isTrue, + ); + }); + + test('validates dimensions, names, and leaf widths', () { + expect( + () => TypedLogicArray(const [], Logic.new), + throwsA(isA()), + ); + expect( + () => TypedLogicArray( + [2], + Logic.new, + dimensionNames: const [], + ), + throwsA(isA()), + ); + expect( + () => TypedLogicArray( + [2, 2], + Logic.new, + dimensionNames: const ['row.', 'column_'], + ), + throwsA(isA()), + ); + expect( + () => TypedLogicArray( + [2, 2], + Logic.new, + dimensionNames: const ['row_', 'row_'], + ), + throwsA(isA()), + ); + expect( + () => TypedLogicArray( + [2], + ({name}) => Const(0, width: 1), + ), + throwsA(isA()), + ); + + expect( + () => TypedLogicArray( + [1], + ({name}) { + final element = Logic(name: name) + ..makeUnassignable(reason: 'test fixture'); + return element; + }, + ), + throwsA(isA()), + ); + expect( + () => TypedLogicArray( + [1], + ({name}) { + final field = Logic(name: 'field') + ..makeUnassignable(reason: 'test fixture'); + return LogicStructure([field], name: name); + }, + ), + throwsA(isA()), + ); + + var width = 1; + expect( + () => TypedLogicArray( + [2], + ({name}) => Logic(name: name, width: width++), + ), + throwsA(isA()), + ); + + for (final create in [ + () => LogicArray([-1], 1), + () => LogicArray([0], -1), + () => LogicArray([1], 1, numUnpackedDimensions: -1), + () => TypedLogicArray( + [-1], + ({name}) => Logic(name: name), + ), + () => TypedLogicArray( + [1], + ({name}) => Logic(name: name), + numUnpackedDimensions: -1, + ), + () => TypedLogicArray( + [1], + ({name}) => Logic(name: name), + numUnpackedDimensions: 2, + ), + ]) { + expect(create, throwsA(isA())); + } + + final reused = Logic(width: 3); + expect( + () => TypedLogicArray( + [2], + ({name}) => reused, + ), + throwsA(isA()), + ); + }); + + test('rejects invalid indexing', () { + final values = TypedLogicArray( + [2, 2], + ({name}) => Logic(name: name, width: 2), + ); + + for (final indices in >[ + [0], + [-1, 0], + [2, 0], + [0, 2], + ]) { + expect(() => values.at(indices), throwsA(isA())); + } + }); + + test('named applies every requested naming mode', () { + final values = TypedLogicArray( + [2], + Logic.new, + name: 'values', + naming: Naming.mergeable, + ); + + for (final naming in Naming.values) { + final name = 'renamed_${naming.name}'; + final renamed = values.named(name, naming: naming); + expect(renamed.name, name); + expect(renamed.naming, naming); + } + }); + + test('matches LogicArray clone naming policy for every naming mode', () { + for (final originalNaming in Naming.values) { + final typed = TypedLogicArray( + [2], + Logic.new, + name: 'typed_${originalNaming.name}', + naming: originalNaming, + ); + final ordinary = LogicArray( + [2], + 1, + name: 'ordinary_${originalNaming.name}', + naming: originalNaming, + ); + final typedClone = typed.clone(); + final ordinaryClone = ordinary.clone(); + final typedRenamed = typed.clone(name: 'typedClone'); + final ordinaryRenamed = ordinary.clone(name: 'ordinaryClone'); + + expect(typedClone.naming, Naming.mergeable); + expect(typedClone.naming, ordinaryClone.naming); + expect(typedClone.name, typed.name); + expect(typedRenamed.naming, Naming.renameable); + expect(typedRenamed.naming, ordinaryRenamed.naming); + expect(typedRenamed.name, 'typedClone'); + } + }); + + test('custom subclasses preserve runtime type and metadata when cloned', + () { + final values = _SampleArray( + [2, 3], + schema: 'sample-v2', + dimensionNames: const ['row_', 'column_'], + name: 'values', + naming: Naming.reserved, + numUnpackedDimensions: 1, + ); + final clone = values.clone(); + final named = values.named('renamed', naming: Naming.reserved); + + for (final result in [clone, named]) { + expect(result, isA<_SampleArray>()); + final specialized = result as _SampleArray; + expect(specialized.schema, 'sample-v2'); + expect(specialized.dimensions, [2, 3]); + expect(specialized.elementWidth, 3); + expect(specialized.numUnpackedDimensions, 1); + expect(specialized.isNet, isFalse); + expect(identical(specialized.valueCodec, _sampleValueCodec), isTrue); + expect(specialized.value, isA>()); + expect( + specialized.arrayElements, + everyElement(isA<_SampleStructure>()), + ); + expect( + specialized.elements.map((element) => element.name), + ['row_0', 'row_1'], + ); + expect( + specialized.arrayElements.map((element) => element.name), + [ + 'column_0', + 'column_1', + 'column_2', + 'column_0', + 'column_1', + 'column_2', + ], + ); + } + expect(clone.name, 'values'); + expect(clone.naming, Naming.mergeable); + expect(named.name, 'renamed'); + expect(named.naming, Naming.reserved); + }); + + test('assigns packed source forms independent of shape', () async { + addTearDown(Simulator.reset); + final codec = LogicValueCodec( + decode: (value) => value.toInt(), + encode: (value) => LogicValue.ofInt(value, 3), + ); + final values = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 2], + _SampleStructure.new, + ); + final sameShape = LogicValueArray.fromFlatInts( + [2, 2], + [0, 1, 2, 3], + elementWidth: 3, + ); + final differentShape = sameShape.reshape([4]); + final typedValues = TypedLogicValueArray.fromFlat( + [1, 4], + 3, + [7, 6, 5, 4], + codec: codec, + ); + final packedWithInvalid = LogicValue.ofString('10xz01z110x0'); + final assignmentCases = <({ + String name, + Object value, + LogicValue expected, + })>[ + ( + name: 'integer', + value: 0xa53, + expected: LogicValue.ofInt(0xa53, 12), + ), + ( + name: 'ordinary LogicValue with X/Z', + value: packedWithInvalid, + expected: packedWithInvalid, + ), + ( + name: 'same-shape LogicValueArray', + value: sameShape, + expected: sameShape.packed, + ), + ( + name: 'different-shape LogicValueArray', + value: differentShape, + expected: differentShape.packed, + ), + ( + name: 'generic TypedLogicValueArray', + value: typedValues, + expected: typedValues.packed, + ), + ]; + + for (final testCase in assignmentCases) { + values.put(testCase.value); + expect( + values.value.packed, + testCase.expected, + reason: testCase.name, + ); + expect(values.value.dimensions, [2, 2]); + expect(values.value.elementWidth, 3); + } + + final emptyValues = + LogicValueArray.fromFlat([0, 3], const [], elementWidth: 3); + final emptyTarget = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 0], + _SampleStructure.new, + )..put(emptyValues); + expect(emptyTarget.value.dimensions, [2, 0]); + expect(emptyTarget.value.elementWidth, 3); + expect(emptyTarget.value.packed, LogicValue.empty); + + values.put(LogicValue.one, fill: true); + expect( + values.value.packed, + LogicValue.filled(values.width, LogicValue.one), + ); + expect( + () => values.put(typedValues, fill: true), + throwsA(isA()), + ); + + values + ..put(0) + ..inject(typedValues); + expect(values.value.packed, LogicValue.ofInt(0, values.width)); + await Simulator.run(); + expect( + TypedLogicValueArray.fromLogicValueArray( + values.value, + codec: codec, + ).arrayValues, + [7, 6, 5, 4], + ); + }); + + test('value snapshots do not add hardware to structured arrays', () async { + addTearDown(Simulator.reset); + final initial = LogicValue.ofString('10xz01'); + final updated = LogicValue.ofString('z001x1'); + final values = TypedLogicArray<_SampleStructure, LogicValue>( + [2], + _SampleStructure.new, + )..put(initial); + final leaves = values.leafElements; + + Simulator.registerAction(10, () => values.put(updated)); + await Simulator.run(); + expect( + leaves.where((leaf) => + leaf.srcConnections.isNotEmpty || leaf.dstConnections.isNotEmpty), + isEmpty, + ); + final first = values.value; + final second = values.value; + final previous = values.previousValue; + + expect(first, second); + expect(first.dimensions, [2]); + expect(first.elementWidth, 3); + expect(first.packed, updated); + expect(previous, isNotNull); + expect(previous!.packed, initial); + expect( + leaves.where((leaf) => + leaf.srcConnections.isNotEmpty || leaf.dstConnections.isNotEmpty), + isEmpty, + ); + }); + + test('tracks previous values and coalesces array changes per tick', + () async { + addTearDown(Simulator.reset); + final values = TypedLogicArray<_SampleStructure, LogicValue>( + [2], + _SampleStructure.new, + ); + final initial = LogicValue.ofString('01xz10'); + final updated = LogicValue.ofString('z10x01'); + final changes = []; + + values.put(initial); + expect(values.previousValue, isNull); + final subscription = values.changed.listen(changes.add); + addTearDown(subscription.cancel); + Simulator.registerAction(10, () => values.put(updated)); + + await Simulator.run(); + + expect(changes, hasLength(1)); + expect(changes.single.previousValue, initial); + expect(changes.single.newValue, updated); + expect(values.value.packed, updated); + expect(values.previousValue, isA>()); + expect(values.previousValue!.dimensions, [2]); + expect(values.previousValue!.elementWidth, 3); + expect(values.previousValue!.packed, initial); + }); + + test('preserves specialized leaves in typed input and output ports', () { + final source = _SampleArray([2, 3], schema: 'ports'); + final module = _TypedArrayPortModule(source); + final valuesIn = module.input('valuesIn') + as TypedLogicArray<_SampleStructure, _SampleValue>; + + expect(valuesIn, isA>()); + expect(valuesIn.dimensions, [2, 3]); + expect(valuesIn.at([1, 2]), isA<_SampleStructure>()); + expect(identical(valuesIn.valueCodec, _sampleValueCodec), isTrue); + expect(valuesIn.value, isA>()); + expect(module.valuesOut, + isA>()); + expect(identical(module.valuesOut.valueCodec, _sampleValueCodec), isTrue); + expect(module.valuesOut.value, isA>()); + expect(module.valuesOut.at([1, 2]).low.width, 1); + expect(module.valuesOut.at([1, 2]).high.width, 2); + }); + + test('preserves and builds semantic net arrays through typed inout ports', + () async { + final source = TypedLogicArray<_NetSampleStructure, _SampleValue>( + [2], + _NetSampleStructure.new, + valueCodec: _sampleValueCodec, + ); + final module = _TypedNetArrayPortModule(source); + + expect(module.values, + isA>()); + expect(module.values.isNet, isTrue); + expect(identical(module.values.valueCodec, _sampleValueCodec), isTrue); + expect(module.values.value, isA>()); + await module.build(); + SimCompare.checkIverilogVector(module, const [], buildOnly: true); + }); + + test('preserves typed arrays through interfaces and synthesis', () async { + final source = _SampleArray( + [2, 2], + schema: 'hierarchy', + name: 'valuesIn', + ); + final module = _TypedArrayHierarchy(source); + await module.build(); + + final vectors = [ + Vector({'valuesIn': 0x129}, {'valuesOut': 0xc9f}), + Vector({'valuesIn': 0xace}, {'valuesOut': 0x778}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + + final netlist = jsonDecode(NetlistSynthesizer().synthesizeToJson(module)) + as Map; + final modules = (netlist['modules'] as Map) + .values + .cast>(); + final typedModule = modules.firstWhere((moduleDefinition) { + final cells = moduleDefinition['cells'] as Map; + return cells.values.cast>().any( + (cell) => cell['type'] == r'$struct_unpack', + ); + }); + final ports = typedModule['ports'] as Map; + final inputPort = ports['valuesIn'] as Map; + final outputPort = ports['valuesOut'] as Map; + final inputType = inputPort['logic_type'] as Map; + var elementType = inputType; + while (elementType['elementType'] is Map) { + elementType = elementType['elementType'] as Map; + } + final fields = + (elementType['fields'] as List).cast>(); + + expect(inputType['arrayDims'], [2, 2]); + expect(inputType['elementWidth'], 3); + expect(fields, [ + {'name': 'low', 'width': 1}, + {'name': 'high', 'width': 2}, + ]); + expect(inputPort['bits'], hasLength(12)); + expect(outputPort['bits'], hasLength(12)); + + final cells = (typedModule['cells'] as Map) + .values + .cast>(); + final structUnpacks = + cells.where((cell) => cell['type'] == r'$struct_unpack').toList(); + final structPacks = + cells.where((cell) => cell['type'] == r'$struct_pack').toList(); + final arraySlices = + cells.where((cell) => cell['type'] == r'$slice').toList(); + final arrayConcats = + cells.where((cell) => cell['type'] == r'$concat').toList(); + final inversions = + cells.where((cell) => cell['type'] == r'$not').toList(); + final inputBits = (inputPort['bits'] as List).cast(); + final outputBits = (outputPort['bits'] as List).cast(); + List connection(Map cell, String port) => + ((cell['connections'] as Map)[port] as List) + .cast(); + Map parameters(Map cell) => + cell['parameters'] as Map; + String connectionKey(List bits) => bits.join(','); + Iterable> inputConnections(Map cell) { + final directions = cell['port_directions'] as Map; + final connections = cell['connections'] as Map; + return connections.entries + .where((entry) => directions[entry.key] == 'input') + .map((entry) => (entry.value as List).cast()); + } + + expect(structUnpacks, hasLength(4)); + expect(structPacks, hasLength(4)); + expect(arraySlices, hasLength(6)); + expect(arrayConcats, hasLength(3)); + expect(inversions, hasLength(4)); + + final outerSlices = arraySlices + .where((cell) => parameters(cell)['A_WIDTH'] == 12) + .toList(); + final elementSlices = arraySlices + .where((cell) => parameters(cell)['A_WIDTH'] == 6) + .toList(); + expect(outerSlices, hasLength(2)); + expect(elementSlices, hasLength(4)); + expect( + outerSlices.map((cell) => parameters(cell)['OFFSET']).toSet(), + {0, 6}, + ); + for (final slice in outerSlices) { + expect(connection(slice, 'A'), inputBits); + expect(parameters(slice)['Y_WIDTH'], 6); + expect( + elementSlices + .where( + (elementSlice) => + connectionKey(connection(elementSlice, 'A')) == + connectionKey(connection(slice, 'Y')), + ) + .map((elementSlice) => parameters(elementSlice)['OFFSET']) + .toSet(), + {0, 3}, + ); + } + expect( + structUnpacks + .map((cell) => connectionKey(connection(cell, 'A'))) + .toSet(), + elementSlices + .map((cell) => connectionKey(connection(cell, 'Y'))) + .toSet(), + ); + + final elementConcats = arrayConcats + .where((cell) => parameters(cell)['IN0_WIDTH'] == 3) + .toList(); + final rootConcat = arrayConcats + .singleWhere((cell) => parameters(cell)['IN0_WIDTH'] == 6); + expect(elementConcats, hasLength(2)); + expect( + structPacks.map((cell) => connectionKey(connection(cell, 'Y'))).toSet(), + elementConcats.expand(inputConnections).map(connectionKey).toSet(), + ); + expect( + elementConcats.map((cell) => parameters(cell)['IN1_WIDTH']).toSet(), + {3}, + ); + expect(connection(rootConcat, 'Y'), outputBits); + expect( + inputConnections(rootConcat).map(connectionKey).toSet(), + elementConcats + .map((cell) => connectionKey(connection(cell, 'Y'))) + .toSet(), + ); + expect( + structUnpacks + .map((cell) => connectionKey(connection(cell, 'high'))) + .toSet(), + inversions.map((cell) => connectionKey(connection(cell, 'A'))).toSet(), + ); + expect( + inversions.map((cell) => connectionKey(connection(cell, 'Y'))).toSet(), + structPacks + .map((cell) => connectionKey(connection(cell, 'high'))) + .toSet(), + ); + expect( + structUnpacks + .map((cell) => connectionKey(connection(cell, 'low'))) + .toSet(), + structPacks + .map((cell) => connectionKey(connection(cell, 'low'))) + .toSet(), + ); + for (final cell in [...structUnpacks, ...structPacks]) { + final connections = cell['connections'] as Map; + expect(connections['low'], hasLength(1)); + expect(connections['high'], hasLength(2)); + } + + final interfaceModule = + _TypedArrayInterfaceModule(_TypedArrayInterface()); + await interfaceModule.build(); + expect( + interfaceModule.input('values'), + isA>(), + ); + }); + + test('lowers array-valued elements to their parent packed offsets', + () async { + await Simulator.reset(); + final source = TypedLogicArray< + TypedLogicArray<_NineBitNestedStructure, LogicValue>, LogicValue>( + [2], + ({name}) => TypedLogicArray<_NineBitNestedStructure, LogicValue>( + [3], + _NineBitNestedStructure.new, + name: name, + ), + ); + final module = _NestedPackedOffsetModule(source); + await module.build(); + + final elements = [ + _nineBitNestedPacked(0, 1, 2, 0), + _nineBitNestedPacked(1, 2, 3, 1), + _nineBitNestedPacked(2, 3, 4, 0), + _nineBitNestedPacked(3, 4, 5, 1), + _nineBitNestedPacked(0, 5, 6, 0), + _nineBitNestedPacked(2, 6, 7, 1), + ]; + final vectors = [ + Vector( + {'values': LogicValue.ofIterable(elements)}, + {'selected': elements.last.getRange(0, 2)}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('input logic [1:0][26:0] values')); + expect(sv, contains('assign selected = values[1][19:18];')); + expect(sv, isNot(contains('values[1][2][1:0]'))); + SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkVerilatorVector(module, vectors); + }, tags: ['verilator']); + + test( + 'lowers separately declared array-valued structure fields by ' + 'declaration rank', () async { + await Simulator.reset(); + final source = TypedLogicArray<_ArrayValuedElementStructure, LogicValue>( + [2], + _ArrayValuedElementStructure.new, + ); + final module = _ArrayValuedElementFieldModule(source); + await module.build(); + final selectedElement = _arrayValuedElementPacked(const [2], const [1]); + final vectors = [ + Vector( + { + 'values': LogicValue.ofIterable([ + LogicValue.ofBigInt(BigInt.zero, selectedElement.width), + selectedElement, + ]), + }, + {'selected': 2}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('logic [1:0][8:0] values_1__matrix;')); + expect(sv, contains('values_1__matrix[1][8:7]')); + expect(sv, isNot(contains('values_1__matrix[1][2][8:7]'))); + expect( + sv, + contains( + 'assign values_1__matrix_0[1][8:7] = ' + 'values_1__matrix[1][8:7];', + ), + ); + SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkVerilatorVector(module, vectors); + }, tags: ['verilator']); + + test('lowers unpacked separately declared array-valued structure fields', + () async { + await Simulator.reset(); + final source = TypedLogicArray<_ArrayValuedElementStructure, LogicValue>( + [2], + ({name}) => _ArrayValuedElementStructure( + matrixUnpackedDimensions: 1, + name: name, + ), + ); + final module = _ArrayValuedElementFieldModule(source); + await module.build(); + final selectedElement = _arrayValuedElementPacked(const [2], const [1]); + final vectors = [ + Vector( + { + 'values': LogicValue.ofIterable([ + LogicValue.ofInt(0, selectedElement.width), + selectedElement, + ]), + }, + {'selected': 2}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('logic [8:0] values_1__matrix [1:0];')); + expect(sv, contains('values_1__matrix[1][8:7]')); + expect(sv, isNot(contains('values_1__matrix[1][2][8:7]'))); + SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkVerilatorVector(module, vectors); + }, tags: ['verilator']); + + test( + 'limits separately declared multidimensional array fields to ' + 'their declaration rank', () async { + await Simulator.reset(); + const matrixDimensions = [2, 2]; + const matrixIndices = [1, 0]; + final source = TypedLogicArray<_ArrayValuedElementStructure, LogicValue>( + [2], + ({name}) => _ArrayValuedElementStructure( + matrixDimensions: matrixDimensions, + name: name, + ), + ); + final module = _ArrayValuedElementFieldModule( + source, + matrixIndices: matrixIndices, + ); + await module.build(); + final selectedElement = + _arrayValuedElementPacked(matrixDimensions, matrixIndices); + final vectors = [ + Vector( + { + 'values': LogicValue.ofIterable([ + LogicValue.ofInt(0, selectedElement.width), + selectedElement, + ]), + }, + {'selected': 2}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('logic [1:0][1:0][8:0] values_1__matrix;')); + expect(sv, contains('values_1__matrix[1][0][8:7]')); + expect(sv, isNot(contains('values_1__matrix[1][0][2][8:7]'))); + SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkVerilatorVector(module, vectors); + }, tags: ['verilator']); + + test('reads a whole separately declared array-valued element', () async { + await Simulator.reset(); + final source = TypedLogicArray<_ArrayValuedElementStructure, LogicValue>( + [2], + _ArrayValuedElementStructure.new, + ); + final module = _ArrayValuedElementPayloadModule(source); + await module.build(); + const payload = 0x15c; + final arrayElement = LogicValue.ofBigInt( + BigInt.from(payload) << 11, + source.elementWidth, + ); + final vectors = [ + Vector( + { + 'values': LogicValue.ofIterable([ + LogicValue.ofBigInt(BigInt.zero, source.elementWidth), + arrayElement, + ]), + }, + {'selected': payload}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('values_1__matrix[1][8:7]')); + expect(sv, contains('values_1__matrix[1][0]')); + expect(sv, isNot(contains('values_1__matrix[1][2][8:7]'))); + SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkVerilatorVector(module, vectors); + }, tags: ['verilator']); + + test( + 'simulates packed and mixed nested arrays with array-valued ' + 'structure fields', () async { + await Simulator.reset(); + final source = TypedLogicArray< + TypedLogicArray<_NestedArrayStructure, LogicValue>, LogicValue>( + [2], + ({name}) => TypedLogicArray<_NestedArrayStructure, LogicValue>( + [3], + _NestedArrayStructure.new, + name: name, + ), + ); + final module = _NestedArrayBoundaryModule(source); + await module.build(); + + final inputFields = [ + (before: 1, lane0: 2, lane1: 3, sample0: 4, sample1: 5, after: 0), + (before: 2, lane0: 4, lane1: 5, sample0: 1, sample1: 6, after: 1), + (before: 3, lane0: 6, lane1: 7, sample0: 2, sample1: 0, after: 0), + (before: 0, lane0: 1, lane1: 2, sample0: 3, sample1: 7, after: 1), + (before: 3, lane0: 5, lane1: 1, sample0: 4, sample1: 2, after: 0), + (before: 2, lane0: 7, lane1: 4, sample0: 5, sample1: 3, after: 1), + ]; + final input = [ + for (final fields in inputFields) + _nestedPacked( + fields.before, + fields.lane0, + fields.lane1, + fields.sample0, + fields.sample1, + fields.after, + ), + ]; + final output = []; + for (var outer = 0; outer < 2; outer++) { + for (var inner = 0; inner < 3; inner++) { + final fields = inputFields[(1 - outer) * 3 + (2 - inner)]; + output.add( + _nestedPacked( + fields.before, + fields.lane1, + fields.lane0, + fields.sample1 ^ 6, + fields.sample0 ^ 6, + fields.after ^ 1, + ), + ); + } + } + final inputValue = LogicValue.ofIterable(input); + final outputValue = LogicValue.ofIterable(output); + final vectors = [ + Vector({'valuesIn': inputValue}, {'valuesOut': outputValue}), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect(sv, contains('valuesIn[1][31:30]')); + expect(sv, contains('valuesOut[0][31:30]')); + expect( + sv, + contains( + 'assign valuesIn_0__0__lanes[0][2:0] = valuesIn[0][4:2];', + ), + ); + expect(sv, isNot(contains('// struct_slice'))); + final netlistJson = NetlistSynthesizer().synthesizeToJson(module); + final netlist = jsonDecode(netlistJson) as Map; + final modules = netlist['modules'] as Map; + final child = + modules['_NestedArrayBoundaryChild'] as Map; + final parent = + modules['_NestedArrayBoundaryModule'] as Map; + final childPorts = child['ports'] as Map; + final childInput = childPorts['valuesIn'] as Map; + final childInputType = childInput['logic_type'] as Map; + final childNetnames = child['netnames'] as Map; + final firstLanes = + childNetnames['valuesIn_0__0__lanes'] as Map; + final firstSamples = + childNetnames['valuesIn_0__0__samples'] as Map; + final innerType = childInputType['elementType'] as Map; + final elementType = innerType['elementType'] as Map; + final fields = + (elementType['fields'] as List).cast>(); + final parentCells = parent['cells'] as Map; + final childCellEntry = parentCells.entries.singleWhere( + (entry) => + (entry.value as Map)['type'] == + '_NestedArrayBoundaryChild', + ); + final childCell = childCellEntry.value as Map; + final childConnections = childCell['connections'] as Map; + + expect(childInputType['arrayDims'], [2]); + expect(innerType['arrayDims'], [3]); + expect( + fields.singleWhere((field) => field['name'] == 'lanes')['type'], + containsPair('arrayDims', [2]), + ); + final samplesType = + fields.singleWhere((field) => field['name'] == 'samples')['type'] + as Map; + expect(samplesType['arrayDims'], [2]); + expect( + samplesType['elementType'], + containsPair('typeName', '_SampleStructure'), + ); + expect(childConnections['valuesIn'], hasLength(90)); + expect(childConnections['valuesOut'], hasLength(90)); + expect(childCellEntry.key, 'unnamed_module'); + final firstLaneBits = (firstLanes['bits'] as List).cast(); + final childCells = (child['cells'] as Map) + .values + .cast>(); + final firstInputUnpack = childCells.singleWhere((cell) { + if (cell['type'] != r'$struct_unpack') { + return false; + } + final connections = cell['connections'] as Map; + final lanes = (connections['lanes'] as List?)?.cast(); + return lanes != null && + lanes.length == firstLaneBits.length && + lanes.indexed.every((entry) => entry.$2 == firstLaneBits[entry.$1]); + }); + final unpackParameters = + firstInputUnpack['parameters'] as Map; + final unpackConnections = + firstInputUnpack['connections'] as Map; + expect(unpackParameters['FIELD_1_NAME'], 'lanes'); + expect(unpackParameters['FIELD_1_OFFSET'], 2); + expect(unpackParameters['FIELD_1_WIDTH'], 6); + expect(unpackConnections['lanes'], firstLanes['bits']); + expect(unpackParameters['FIELD_2_NAME'], 'samples'); + expect(unpackParameters['FIELD_2_OFFSET'], 8); + expect(unpackParameters['FIELD_2_WIDTH'], 6); + expect(unpackConnections['samples'], firstSamples['bits']); + final netlistNames = { + for (final moduleDefinition + in modules.values.cast>()) ...[ + ...(moduleDefinition['ports'] as Map).keys, + ...(moduleDefinition['netnames'] as Map).keys, + ], + }; + for (final name in [ + 'valuesIn', + 'valuesOut', + 'valuesIn_0__0__lanes', + 'valuesIn_0__0__samples', + 'valuesOut_1__2__lanes', + ]) { + expect(sv, contains(name)); + expect(netlistNames, contains(name), + reason: 'SV and netlist should preserve signal name $name.'); + } + SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkVerilatorVector(module, vectors); + + await Simulator.reset(); + final mixedSource = TypedLogicArray< + TypedLogicArray<_NestedArrayStructure, LogicValue>, LogicValue>( + [2], + ({name}) => TypedLogicArray<_NestedArrayStructure, LogicValue>( + [3], + ({name}) => _NestedArrayStructure( + name: name, + numUnpackedDimensions: 1, + ), + name: name, + numUnpackedDimensions: 1, + ), + numUnpackedDimensions: 1, + ); + final mixedModule = _NestedArrayBoundaryModule(mixedSource); + await mixedModule.build(); + + await SimCompare.checkFunctionalVector(mixedModule, vectors); + final mixedSv = mixedModule.dumpSystemVerilog(); + expect( + mixedSv, + contains('input logic [44:0] valuesIn [1:0]'), + ); + expect( + mixedSv, + contains('output logic [44:0] valuesOut [1:0]'), + ); + expect( + mixedSv, + contains('logic [2:0] valuesIn_0__0__lanes [1:0];'), + ); + expect( + mixedSv, + contains('logic [2:0] valuesIn_0__0__samples [1:0];'), + ); + final mixedNetlist = jsonDecode( + NetlistSynthesizer().synthesizeToJson(mixedModule), + ) as Map; + expect( + mixedNetlist['modules'], + containsPair('_NestedArrayBoundaryModule', isA>()), + ); + // Icarus 12.0 leaves child-driven unpacked array variables unknown. + SimCompare.checkVerilatorVector(mixedModule, vectors); + }, tags: ['verilator']); + + test('simulates mixed nested arrays across a child boundary', () async { + await Simulator.reset(); + final source = + TypedLogicArray, LogicValue>( + [2, 3], + ({name}) => TypedLogicArray( + [4, 5], + ({name}) => Logic(name: name, width: 8), + name: name, + numUnpackedDimensions: 1, + ), + numUnpackedDimensions: 1, + ); + final module = _MixedNestedArrayModule(source); + await module.build(); + final input = LogicValue.ofIterable([ + for (var value = 1; value <= 120; value++) LogicValue.ofInt(value, 8), + ]); + final expected = LogicValue.ofIterable([ + for (var group = 0; group < 6; group++) + for (var value = 20; value >= 1; value--) + LogicValue.ofInt(group * 20 + value, 8), + ]); + + final vectors = [ + Vector({'valuesIn': input}, {'valuesOut': expected}) + ]; + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.dumpSystemVerilog(); + expect( + sv, + contains('input logic [2:0][159:0] valuesIn [1:0]'), + ); + expect( + sv, + contains('output logic [2:0][159:0] valuesOut [1:0]'), + ); + expect( + sv, + contains('valuesOut[1][2][159:152] = ' + 'valuesIn[1][2][7:0]'), + ); + final netlist = jsonDecode(NetlistSynthesizer().synthesizeToJson(module)) + as Map; + final modules = netlist['modules'] as Map; + final top = modules['_MixedNestedArrayModule'] as Map; + final ports = top['ports'] as Map; + for (final portName in ['valuesIn', 'valuesOut']) { + final port = ports[portName] as Map; + final type = port['logic_type'] as Map; + final outerElementType = type['elementType'] as Map; + final innerType = + outerElementType['elementType'] as Map; + expect(type['arrayDims'], [2, 3]); + expect(type['elementWidth'], 160); + expect(outerElementType['arrayDims'], [3]); + expect(innerType['arrayDims'], [4, 5]); + expect(innerType['elementWidth'], 8); + expect(port['bits'], hasLength(960)); + } + // Icarus 12.0 leaves child-driven unpacked array variables unknown. + SimCompare.checkVerilatorVector(module, vectors); + expect(source.numUnpackedDimensions, 1); + expect(source.at([1, 2]).numUnpackedDimensions, 1); + expect(source.at([1, 2]).at([3, 4]).width, 8); + }, tags: ['verilator']); + }); +} diff --git a/test/typed_logic_value_array_test.dart b/test/typed_logic_value_array_test.dart new file mode 100644 index 0000000000..6f4bfce6bb --- /dev/null +++ b/test/typed_logic_value_array_test.dart @@ -0,0 +1,199 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_logic_value_array_test.dart +// Tests for typed logic value arrays. +// +// 2026 September 16 +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +int _decodeLogicValue(LogicValue value) => value.toInt(); + +int _decodeEquivalentLogicValue(LogicValue value) => value.toInt(); + +LogicValue _encodeLogicValue(int value) => LogicValue.ofInt(value, 8); + +double _decodeRoundedLogicValue(LogicValue value) => value.toInt().toDouble(); + +LogicValue _encodeRoundedLogicValue(double value) => + LogicValue.ofInt(value.round(), 8); + +List _decodeListLogicValue(LogicValue value) => [value.toInt()]; + +LogicValue _encodeListLogicValue(List value) => + LogicValue.ofInt(value.single, 8); + +void main() { + group('TypedLogicValueArray', () { + test('rejects empty and non-semantic nested input', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + + expect( + () => TypedLogicValueArray(const [], codec: codec), + throwsArgumentError, + ); + expect( + () => TypedLogicValueArray( + const [ + 1, + 'not an integer', + ], + codec: codec, + ), + throwsArgumentError, + ); + }); + + test('constructs equivalent nested and flat list-valued data', () { + const codec = LogicValueCodec>( + decode: _decodeListLogicValue, + encode: _encodeListLogicValue, + ); + final leaves = [ + [1], + [2], + [3], + [4], + ]; + final nested = TypedLogicValueArray>([ + [leaves[0], leaves[1]], + [leaves[2], leaves[3]], + ], codec: codec); + final flat = TypedLogicValueArray>.fromFlat( + const [2, 2], + 8, + leaves, + codec: codec, + ); + + expect(nested.dimensions, [2, 2]); + expect(flat.dimensions, nested.dimensions); + expect(nested.arrayValues.map((value) => value.single), [1, 2, 3, 4]); + expect(flat.arrayValues.map((value) => value.single), [1, 2, 3, 4]); + expect(nested.at([1, 0]), [3]); + expect(flat.packed, nested.packed); + }); + + test('maps, reshapes, and transposes typed values', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final packed = LogicValueArray.fromInts(const [ + [1, 2, 3], + [4, 5, 6], + ], elementWidth: 8); + final values = TypedLogicValueArray.fromLogicValueArray( + packed, + codec: codec, + ); + + expect(values.at([1, 1]), 5); + expect(values.map((value) => value + 1).at([1, 1]), 6); + expect(values.reshape([3, 2]).at([2, 1]), 6); + expect(values.transpose2D().arrayValues, [1, 4, 2, 5, 3, 6]); + }); + + test('supports slices, indexed mapping, and compatible stacking', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final values = TypedLogicValueArray(const [ + [1, 2], + [3, 4], + ], codec: codec); + + expect(values.majorSlices.map((slice) => slice.arrayValues), [ + [1, 2], + [3, 4], + ]); + expect( + values.indexedMap((indices, value) => value + indices[1]).arrayValues, + [1, 3, 3, 5], + ); + expect( + TypedLogicValueArray.stack(values.majorSlices).arrayValues, + values.arrayValues, + ); + expect(values.packed, LogicValue.ofInt(0x04030201, 32)); + }); + + test('normalizes lossy codecs at construction and preserves values', () { + const codec = LogicValueCodec( + decode: _decodeRoundedLogicValue, + encode: _encodeRoundedLogicValue, + ); + final values = TypedLogicValueArray(const [ + [1.2, 2.4], + [3.6, 4.8], + ], codec: codec); + + expect(values.arrayValues, [1.0, 2.0, 4.0, 5.0]); + expect(values.reshape([4]).arrayValues, values.arrayValues); + expect( + TypedLogicValueArray.stack(values.majorSlices).arrayValues, + values.arrayValues, + ); + expect(values.transpose2D().arrayValues, [1.0, 4.0, 2.0, 5.0]); + }); + + test('requires identical codecs when stacking', () { + const firstCodec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + const secondCodec = LogicValueCodec( + decode: _decodeEquivalentLogicValue, + encode: _encodeLogicValue, + ); + final first = TypedLogicValueArray.fromFlat( + const [1], + 8, + const [1], + codec: firstCodec, + ); + final second = TypedLogicValueArray.fromFlat( + const [1], + 8, + const [2], + codec: secondCodec, + ); + + expect( + () => TypedLogicValueArray.stack([first, second]), + throwsArgumentError, + ); + }); + + test('round-trips typed slices with an empty inner dimension', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final values = TypedLogicValueArray.fromFlat( + const [2, 0], + 8, + const [], + codec: codec, + ); + final slices = values.majorSlices.toList(growable: false); + + expect(slices, hasLength(2)); + expect(slices.map((slice) => slice.dimensions), [ + [0], + [0], + ]); + expect( + TypedLogicValueArray.stack(slices).dimensions, + [2, 0], + ); + }); + }); +} diff --git a/test/unpacked_array_synthesis_test.dart b/test/unpacked_array_synthesis_test.dart index 3960ab407f..27c6599648 100644 --- a/test/unpacked_array_synthesis_test.dart +++ b/test/unpacked_array_synthesis_test.dart @@ -152,14 +152,9 @@ void main() { }); test('$name compiles with Verilator', () async { - for (final value in values.take(2)) { - final module = makeModule(value); - await module.build(); - if (!SimCompare.checkVerilatorVector(module, const [], - buildOnly: true)) { - return; - } - } + final module = makeModule(values[1]); + await module.build(); + SimCompare.checkVerilatorVector(module, const [], buildOnly: true); }, tags: ['verilator']); } } diff --git a/tool/generate_coverage.sh b/tool/generate_coverage.sh index be4f4c2e6c..f402782638 100755 --- a/tool/generate_coverage.sh +++ b/tool/generate_coverage.sh @@ -45,7 +45,7 @@ rm -rf coverage if [ "${IS_FLUTTER_PACKAGE}" = true ]; then # Flutter writes LCOV directly, unlike `dart test --coverage` which writes # VM coverage JSON that must be converted with coverage:format_coverage. - flutter test --coverage --coverage-path=coverage/lcov.info || true + flutter test --coverage --coverage-path=coverage/lcov.info else # Benchmarks are smoke-tested separately and can take too long under # coverage instrumentation. diff --git a/tool/gh_actions/pana_source.sh b/tool/gh_actions/pana_source.sh index 29a6c8342f..a2476fdf0d 100755 --- a/tool/gh_actions/pana_source.sh +++ b/tool/gh_actions/pana_source.sh @@ -40,8 +40,8 @@ case "$package" in pana_score_threshold=10 ;; rohd_devtools_widgets|rohd_waveform) - # These packages intentionally retain dependency bounds for their - # supported SDK ranges. Allow the corresponding 10-point Pana deduction. + # Pana gates points lost: 10 permits a score of 150/160. These packages + # retain dependency bounds for their supported SDK ranges. pana_score_threshold=10 ;; esac diff --git a/tool/test/pana_source_test.sh b/tool/test/pana_source_test.sh index 5e1b75700b..1ca3f50a7c 100644 --- a/tool/test/pana_source_test.sh +++ b/tool/test/pana_source_test.sh @@ -89,6 +89,9 @@ else [[ $# -eq 3 && "$1" == --exit-code-threshold && "$2" == "$EXPECTED_THRESHOLD" && "$3" == . ]] fi + if (( 160 - ${PANA_SCORE:-160} > $2 )); then + exit 1 + fi printf 'changed by Pana\n' > pubspec.yaml fi printf 'pana\n' >> "$SDK_LOG" @@ -148,12 +151,15 @@ run_case 0 "$NAVIGATOR" dart [[ "$(cat "$SDK_LOG")" == $'dart|pub get\ndart|analyze --fatal-infos lib\ndart|pub downgrade\ndart|analyze --fatal-infos lib\npana' ]] export EXPECTED_SDK=flutter EXPECTED_THRESHOLD=10 -run_case 0 "$WIDGETS" flutter +PANA_SCORE=150 run_case 0 "$WIDGETS" flutter [[ "$(cat "$SDK_LOG")" == $'flutter|pub get\nflutter|analyze --fatal-infos --no-pub lib\nflutter|pub downgrade\nflutter|analyze --fatal-infos --no-pub lib\npana' ]] +PANA_SCORE=149 run_case 1 "$WIDGETS" flutter +PANA_STATUS=73 run_case 73 "$WIDGETS" flutter export EXPECTED_SDK=dart EXPECTED_THRESHOLD=10 run_case 0 "$WAVEFORM" dart [[ "$(cat "$SDK_LOG")" == $'dart|pub get\ndart|analyze --fatal-infos lib\ndart|pub downgrade\ndart|analyze --fatal-infos lib\npana' ]] +PANA_STATUS=73 run_case 73 "$WAVEFORM" dart mkdir -p "$FIXTURE/rohd_hierarchy/lib" printf 'name: rohd_hierarchy\n' > "$FIXTURE/rohd_hierarchy/pubspec.yaml"