From e38c3188c0d9b4fdf265b53cb0a679f7bef62910 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 4 Sep 2026 00:05:53 -0700 Subject: [PATCH 01/43] Add typed LogicArrayOf support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .devcontainer/devcontainer.json | 6 + doc/user_guide/_docs/A19-logic-structures.md | 20 + doc/user_guide/_docs/A20-logic-arrays.md | 48 ++- lib/src/signals/logic_array.dart | 430 ++++++++++++++----- lib/src/signals/logic_array_of.dart | 226 ++++++++++ lib/src/signals/logic_structure.dart | 37 ++ lib/src/signals/logic_value_array.dart | 327 ++++++++++++++ lib/src/signals/logic_value_array_of.dart | 152 +++++++ lib/src/signals/signals.dart | 5 +- test/logic_array_of_test.dart | 301 +++++++++++++ test/logic_structure_test.dart | 36 ++ 11 files changed, 1470 insertions(+), 118 deletions(-) create mode 100644 lib/src/signals/logic_array_of.dart create mode 100644 lib/src/signals/logic_value_array.dart create mode 100644 lib/src/signals/logic_value_array_of.dart create mode 100644 test/logic_array_of_test.dart 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/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index aa2d53971a..f6bfab5479 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -41,6 +41,26 @@ rvStruct.elements[0] <= ready; rvStruct.elements[1] <= valid; ``` +## Promoting nested fields + +Use `flattenOuter` to promote fields from direct child structures into a new generic `LogicStructure`. The new fields are clones connected to their original sources, so the original structure remains unchanged. By default, promoted names are prefixed with the direct child structure name to avoid collisions. + +```dart +final config = LogicStructure([ + Logic(name: 'mode', width: 2), + Logic(name: 'valid'), +], name: 'config'); +final control = LogicStructure([ + Logic(name: 'enable'), + config, +], name: 'control'); + +final flattened = control.flattenOuter(); +// Field names: enable, config_mode, config_valid. +``` + +Only direct non-array child structures are promoted. Nested grandchildren remain structures, and a duplicate resulting field name throws `LogicConstructionException`. + ## Making your own structure Referencing elements by index is often not ideal for named signals. We can do better by building our own structure that inherits from `LogicStructure`. diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 5650f48c43..c68555124e 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -1,7 +1,7 @@ --- title: "Logic Arrays" permalink: /docs/logic-arrays/ -last_modified_at: 2022-6-5 +last_modified_at: 2026-7-21 toc: true --- @@ -22,6 +22,50 @@ 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 and value-domain arrays + +Use `LogicArrayOf` when every leaf has the same specialized `Logic` type. It preserves the normal array dimensions while exposing typed leaves with `typedLeafElements` and `elementAt`. `LogicArray` is `LogicArrayOf`, so it retains its existing construction, port, clone, naming, and array APIs while also identifying its leaves as `Logic`. For example, this creates a two-dimensional array of samples with separate data and valid fields: + +```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); +} + +final samples = LogicArrayOf( + [2, 3], + Sample.new, + dimensionNames: ['row_', 'column_'], +); + +final bottomRightData = samples.elementAt([1, 2]).data; +``` + +When typed array leaves are themselves arrays, use `flattenNestedDimensions()` to create one rectangular `LogicArrayOf` with all nested dimensions concatenated. The full address is preserved: `nested.elementAt(outerIndex).elementAt(innerIndex)` maps to `flattened.elementAt([...outerIndex, ...innerIndex])`. Every sibling nested array must have matching dimensions and leaf width. + +Use `LogicValueArray` for fixed-width array data outside the hardware graph. It keeps values in row-major order and supports indexing, reshaping, transposition, and slice operations. `LogicValueArrayOf` adds a codec so application-level values can use the same operations while converting to and from packed `LogicValue`s. + +```dart +final values = LogicValueArray.fromInts([2, 3], 8, [1, 2, 3, 4, 5, 6]); +final transposed = values.transpose2D(); // Dimensions: [3, 2] + +final signals = values.toLogicArray(name: 'values'); +``` + +`LogicValueArray.putInto` drives a compatible `LogicArray` or `LogicArrayOf`, while `LogicArrayOf.logicValues` captures its current packed values. Use `LogicArrayOf.valueArrayOf` and `putValueArrayOf` when a `LogicValueCodec` converts typed value-domain data at the hardware boundary. + ## 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,6 +87,8 @@ 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 `addTypedInput` and `addTypedOutput` for `LogicArrayOf` ports. These methods preserve the array's specialized leaf type, allowing the module to access fields such as `samples.elementAt([1, 2]).data` directly. + ## 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. diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 6e7c9bd34f..3dd97da294 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,8 +9,11 @@ part of 'signals.dart'; -/// Represents a multi-dimensional array structure of independent [Logic]s. -class LogicArray extends LogicStructure { +/// Shared implementation for multidimensional arrays of [Logic] values. +/// +/// Most callers should construct [LogicArray] for ordinary [Logic] leaves or +/// [LogicArrayOf] for a specialized leaf type. +class BaseLogicArray extends LogicStructure { /// The number of elements at each level of the array, starting from the most /// significant outermost level. /// @@ -24,6 +27,14 @@ class LogicArray extends LogicStructure { /// [elementWidth] is always 0. final int elementWidth; + /// Elements at the leaf dimension of this array. + /// + /// Unlike [LogicStructure.leafElements], traversal stops at the configured + /// array leaf. This distinction matters when an array leaf is itself a + /// [LogicStructure], such as a typed floating-point value. + late final List arrayElements = + UnmodifiableListView(_calculateArrayElements()); + @override final Naming naming; @@ -51,18 +62,16 @@ class LogicArray extends LogicStructure { /// 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. - factory LogicArray(List dimensions, int elementWidth, + factory BaseLogicArray(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, - ); + BaseLogicArray._factory(dimensions, elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + naming: naming, + logicBuilder: Logic.new, + logicArrayBuilder: BaseLogicArray.new, + arrayBuilder: BaseLogicArray._, + isNet: false); @override final bool isNet; @@ -77,18 +86,72 @@ class LogicArray extends LogicStructure { /// 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. - factory LogicArray.net(List dimensions, int elementWidth, + factory BaseLogicArray.net(List dimensions, int elementWidth, {String? name, int numUnpackedDimensions = 0, Naming? naming}) => - LogicArray._factory( - dimensions, - elementWidth, - name: name, - numUnpackedDimensions: numUnpackedDimensions, - naming: naming, - logicBuilder: LogicNet.new, - logicArrayBuilder: LogicArray.net, - isNet: true, - ); + BaseLogicArray._factory(dimensions, elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + naming: naming, + logicBuilder: LogicNet.new, + logicArrayBuilder: BaseLogicArray.net, + arrayBuilder: BaseLogicArray._, + isNet: true); + + /// 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 [BaseLogicArray]s matching the remaining + /// dimensions. For a one-dimensional array, each element must have + /// [elementWidth] bits. + @protected + BaseLogicArray.structured(super.elements, + {required List dimensions, + required this.elementWidth, + String? name, + this.numUnpackedDimensions = 0, + Naming? naming, + this.isNet = false}) + : dimensions = List.unmodifiable(dimensions), + naming = Naming.chooseNaming(name, naming), + super(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 (numUnpackedDimensions > dimensions.length) { + throw LogicConstructionException( + 'Cannot unpack more than all of the dimensions.'); + } + 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; + } + } /// Internal factory constructor. /// @@ -105,25 +168,24 @@ class LogicArray extends LogicStructure { /// /// 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, - }) { + 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.'); @@ -150,49 +212,43 @@ class LogicArray extends LogicStructure { naming = newNaming; name = newName; - 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, - ); + return arrayBuilder( + 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); } @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], + BaseLogicArray _clone({String? name, Naming? naming}) => + BaseLogicArray._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 ? BaseLogicArray.net : BaseLogicArray.new, + arrayBuilder: BaseLogicArray._, + isNet: isNet); + + /// Creates a new [BaseLogicArray] 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. @@ -201,7 +257,7 @@ class LogicArray extends LogicStructure { /// returns the same type as itself. @override @mustBeOverridden - LogicArray clone({String? name}) => _clone(name: name); + BaseLogicArray clone({String? name}) => _clone(name: name); /// Makes a [clone] with the provided [name] and optionally [naming], then /// assigns it to be driven by `this`. @@ -210,26 +266,31 @@ class LogicArray extends LogicStructure { /// construction without separately declaring a new named signal and then /// assigning. @override - LogicArray named(String name, {Naming? naming}) => + BaseLogicArray named(String name, {Naming? naming}) => _clone(name: name, naming: naming)..gets(this); - /// Private constructor for the factory [LogicArray] constructor. + /// Private constructor for the factory [BaseLogicArray] constructor. /// /// 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 + BaseLogicArray._(super.elements, + {required this.dimensions, + required this.elementWidth, + required this.numUnpackedDimensions, + required String super.name, + required this.naming, + required this.isNet}); + + List _calculateArrayElements() => dimensions.length == 1 + ? elements + : elements + .cast() + .expand((element) => element.arrayElements) + .toList(growable: false); + + /// Constructs a new [BaseLogicArray] with a 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]. - factory LogicArray.port(String name, + factory BaseLogicArray.port(String name, [List dimensions = const [1], int elementWidth = 1, int numUnpackedDimensions = 0]) { @@ -237,21 +298,141 @@ class LogicArray extends LogicStructure { throw InvalidPortNameException(name); } - return LogicArray( - dimensions, elementWidth, - numUnpackedDimensions: numUnpackedDimensions, name: name, + return BaseLogicArray(dimensions, elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + name: name, - // make port names mergeable so we don't duplicate the ports - // when calling connectIO - naming: Naming.mergeable, - ); + // make port names mergeable so we don't duplicate the ports + // when calling connectIO + naming: Naming.mergeable); } - /// Constructs a new [LogicArray.net] with a more convenient constructor + /// Constructs a new [BaseLogicArray.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]. - factory LogicArray.netPort(String name, + factory BaseLogicArray.netPort(String name, + [List dimensions = const [1], + int elementWidth = 1, + int numUnpackedDimensions = 0]) { + if (!Sanitizer.isSanitary(name)) { + throw InvalidPortNameException(name); + } + + return BaseLogicArray.net(dimensions, elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + name: name, + + // make port names mergeable so we don't duplicate the ports + // when calling connectIO + naming: Naming.mergeable); + } +} + +/// A multi-dimensional array structure of independent [Logic]s. +/// +/// This is the ordinary [Logic]-leaf specialization of [LogicArrayOf]. It has +/// the same construction, port, clone, and naming API as the historical +/// `LogicArray` type. [typedLeafElements] exposes its leaves as `List`. +class LogicArray extends LogicArrayOf { + /// Creates an array with specified [dimensions] and [elementWidth] named + /// [name]. + /// + /// [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. + 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); + + /// Creates an array of [LogicNet]s with [dimensions] and [elementWidth] + /// named [name]. + /// + /// [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(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 super.isNet}) + : super.structured(elements, Logic.new); + + 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 + + /// 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 + + /// 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); + + /// Creates an array port with a convenient constructor signature. + /// + /// 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, int numUnpackedDimensions = 0]) { @@ -259,14 +440,31 @@ class LogicArray extends LogicStructure { throw InvalidPortNameException(name); } - return LogicArray.net( - dimensions, elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - name: name, + return LogicArray(dimensions, elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + name: name, + naming: Naming.mergeable); + } + + /// 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, + int numUnpackedDimensions = 0]) { + if (!Sanitizer.isSanitary(name)) { + throw InvalidPortNameException(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); } } + +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_array_of.dart b/lib/src/signals/logic_array_of.dart new file mode 100644 index 0000000000..ef67689e33 --- /dev/null +++ b/lib/src/signals/logic_array_of.dart @@ -0,0 +1,226 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_array_of.dart +// Definition of typed logic arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +part of 'signals.dart'; + +/// Builds one leaf element of a [LogicArrayOf]. +typedef LogicArrayElementBuilder = T Function({String? name}); + +/// A multidimensional logic array with leaves of type [T]. +/// +/// Intermediate dimensions are [BaseLogicArray]s, while the configured array +/// leaf can be any [Logic], including a [LogicStructure] such as a +/// floating-point signal. Packed conversion happens only at that leaf boundary. +class LogicArrayOf extends BaseLogicArray { + /// Labels used to name elements at each dimension. + final List dimensionNames; + + final LogicArrayElementBuilder _elementBuilder; + + late final List _typedLeafElements = List.unmodifiable( + arrayElements.cast(), + ); + + /// Creates an array with [dimensions] and typed leaves from [elementBuilder]. + LogicArrayOf( + List dimensions, + LogicArrayElementBuilder elementBuilder, { + List? dimensionNames, + String? name, + }) : this._( + _LogicArrayOfBuild.build(dimensions, elementBuilder, + dimensionNames: dimensionNames), + elementBuilder, + name: name); + + LogicArrayOf._( + _LogicArrayOfBuild build, + this._elementBuilder, { + super.name, + }) : dimensionNames = build.dimensionNames, + super.structured(build.elements, + dimensions: build.dimensions, elementWidth: build.elementWidth); + + /// Creates a typed array from structurally compatible [elements]. + @protected + LogicArrayOf.structured( + super.elements, + this._elementBuilder, { + required super.dimensions, + required super.elementWidth, + required super.numUnpackedDimensions, + required String name, + required Naming naming, + required super.isNet, + }) : dimensionNames = List.unmodifiable( + List.generate(dimensions.length, (index) => 'd${index}_')), + super.structured( + name: name, + naming: naming, + ); + + /// Typed leaves in row-major order. + List get typedLeafElements => UnmodifiableListView(_typedLeafElements); + + /// Typed leaves paired with their multidimensional indices. + Iterable<(List, T)> get indexedElements => + indexedLeaves.map((entry) => (entry.$1, entry.$2 as T)); + + /// Returns the typed leaf at multidimensional [indices]. + T elementAt(List indices) => at(indices) as T; + + /// Flattens all nested array dimensions into one typed array of [U] leaves. + /// + /// Each nested array layer must be rectangular: sibling arrays must have the + /// same dimensions and element width. The returned dimensions concatenate + /// every nested layer, preserving row-major ordering and index addresses. + LogicArrayOf flattenNestedDimensions({String? name}) { + var leaves = typedLeafElements.cast().toList(growable: false); + final flattenedDimensions = [...dimensions]; + + while (leaves.any((leaf) => leaf is BaseLogicArray)) { + if (leaves.any((leaf) => leaf is! BaseLogicArray)) { + throw LogicConstructionException( + 'Nested array leaves must have a uniform depth.'); + } + + final arrays = leaves.cast(); + final reference = arrays.first; + if (arrays.any((array) => + !_sameDimensions(array.dimensions, reference.dimensions) || + array.elementWidth != reference.elementWidth)) { + throw LogicConstructionException( + 'Nested array leaves must have matching dimensions and widths.'); + } + + flattenedDimensions.addAll(reference.dimensions); + leaves = + arrays.expand((array) => array.arrayElements).toList(growable: false); + } + + if (leaves.any((leaf) => leaf is! U)) { + throw LogicConstructionException( + 'Nested array leaves must have type $U.'); + } + + final prototype = leaves.first as U; + if (leaves.any((leaf) => leaf.width != prototype.width)) { + throw LogicConstructionException( + 'Nested array leaves must have matching widths.'); + } + + return LogicArrayOf( + flattenedDimensions, + ({name}) => prototype.clone(name: name) as U, + name: name, + )..getsEach(leaves); + } + + /// Current packed leaves in the value domain. + LogicValueArray get logicValues => LogicValueArray.fromLogicArray(this); + + /// Decodes current packed leaves into semantic values using [codec]. + LogicValueArrayOf valueArrayOf(LogicValueCodec codec) => + LogicValueArrayOf.fromLogicValues(logicValues, codec: codec); + + /// Drives typed leaves from [values]. + void putLogicValues(LogicValueArray values) => values.putInto(this); + + /// Drives typed logic leaves from semantic [values]. + void putValueArrayOf(LogicValueArrayOf values) => + putLogicValues(values.logicValues); + + /// Packs typed leaves into a conventional [LogicArray]. + LogicArray toLogicArray({String? name}) => + LogicArray(dimensions, elementWidth, name: name) + ..getsEach(_typedLeafElements.map((element) => element.packed)); + + /// Drives typed leaves from a packed [LogicArray]. + void getsPackedValues(LogicArray packedValues) { + _validateShape(packedValues.dimensions, packedValues.elementWidth); + for (final (target, source) + in _typedLeafElements.zipExact(packedValues.arrayElements)) { + target <= source; + } + } + + void _validateShape(List dimensions, int elementWidth) { + if (!_sameDimensions(this.dimensions, dimensions) || + this.elementWidth != elementWidth) { + throw LogicConstructionException( + 'Values must have dimensions ${this.dimensions} and ' + 'elementWidth ${this.elementWidth}.'); + } + } + + @override + LogicArrayOf clone({String? name}) => + LogicArrayOf(dimensions, _elementBuilder, + dimensionNames: dimensionNames, name: name ?? this.name); + + @override + LogicArrayOf named(String name, {Naming? naming}) => + clone(name: name)..gets(this); +} + +class _LogicArrayOfBuild { + final List dimensions; + final List dimensionNames; + final List elements; + final int elementWidth; + + _LogicArrayOfBuild._( + this.dimensions, this.dimensionNames, this.elements, this.elementWidth); + + factory _LogicArrayOfBuild.build( + List dimensions, LogicArrayElementBuilder elementBuilder, + {List? dimensionNames}) { + final normalizedDimensions = List.unmodifiable(dimensions); + if (normalizedDimensions.isEmpty || + normalizedDimensions.any((dimension) => dimension <= 0)) { + throw LogicConstructionException( + 'LogicArrayOf dimensions must all be positive.'); + } + + final normalizedNames = List.unmodifiable( + dimensionNames ?? + Iterable.generate( + normalizedDimensions.length, (dimension) => 'd${dimension}_'), + ); + if (normalizedNames.length != normalizedDimensions.length) { + throw LogicConstructionException( + 'dimensionNames must match the number of dimensions.'); + } + + final elements = List.generate(normalizedDimensions.first, (index) { + final elementName = '${normalizedNames.first}$index'; + return normalizedDimensions.length == 1 + ? elementBuilder(name: elementName) + : LogicArrayOf(normalizedDimensions.sublist(1), elementBuilder, + dimensionNames: normalizedNames.sublist(1), name: elementName); + }, growable: false); + final typedLeaves = normalizedDimensions.length == 1 + ? elements.cast().toList(growable: false) + : elements + .cast>() + .expand((element) => element.typedLeafElements) + .toList(growable: false); + return _LogicArrayOfBuild._(normalizedDimensions, normalizedNames, elements, + _validateElementWidths(typedLeaves)); + } + + static int _validateElementWidths(List elements) { + final width = elements.first.width; + if (elements.any((element) => element.width != width)) { + throw LogicConstructionException( + 'All LogicArrayOf leaves must have the same width.'); + } + return width; + } +} diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index f80d676208..e6cc9c4f99 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -174,6 +174,43 @@ class LogicStructure implements Logic { _calculateLeafElements(), ); + /// Promotes direct non-array child structures into a new generic structure. + /// + /// Each promoted element is cloned and connected to its source. When + /// [prefixFieldNames] is `true`, promoted fields are named + /// `${child.name}_${field.name}`. This preserves the immediate structure + /// path and avoids common field-name collisions. The method throws a + /// [LogicConstructionException] when the resulting field names collide. + LogicStructure flattenOuter({String? name, bool prefixFieldNames = true}) { + final sources = <(Logic, String)>[]; + for (final element in elements) { + if (element is LogicStructure && element is! BaseLogicArray) { + for (final field in element.elements) { + final prefix = + element.name.endsWith('_') ? element.name : '${element.name}_'; + final fieldName = + prefixFieldNames ? '$prefix${field.name}' : field.name; + sources.add((field, fieldName)); + } + } else { + sources.add((element, element.name)); + } + } + + final names = {}; + for (final source in sources) { + if (!names.add(source.$2)) { + throw LogicConstructionException( + 'Flattened structure contains duplicate field name ${source.$2}.'); + } + } + + final flattenedElements = sources + .map((source) => source.$1.clone(name: source.$2)..gets(source.$1)) + .toList(growable: false); + return LogicStructure(flattenedElements, name: name ?? this.name); + } + /// Compute the list of all leaf elements, to be cached in [leafElements]. List _calculateLeafElements() { final leaves = []; diff --git a/lib/src/signals/logic_value_array.dart b/lib/src/signals/logic_value_array.dart new file mode 100644 index 0000000000..9d8ed6de0d --- /dev/null +++ b/lib/src/signals/logic_value_array.dart @@ -0,0 +1,327 @@ +// 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 'signals.dart'; + +/// Value-domain counterpart to [LogicArray]. +/// +/// Stores fixed-width [LogicValue] leaves with [dimensions] matching the shape +/// used by a [LogicArray]. Values are kept in the same row-major leaf order +/// used by [LogicArray.arrayElements]. +class LogicValueArray { + /// The number of elements at each array level. + final List dimensions; + + /// Width of each leaf value. + final int elementWidth; + + final List _values; + + /// Creates a value array from row-major [values]. + LogicValueArray( + List dimensions, + this.elementWidth, + Iterable values, + ) : dimensions = List.unmodifiable(dimensions), + _values = List.unmodifiable(values) { + if (dimensions.isEmpty) { + throw ArgumentError.value(dimensions, 'dimensions', 'Must not be empty.'); + } + if (dimensions.any((dimension) => dimension < 0)) { + throw ArgumentError.value( + dimensions, 'dimensions', 'Dimensions must be non-negative.'); + } + if (elementWidth < 0) { + throw ArgumentError.value( + elementWidth, 'elementWidth', 'Must be non-negative.'); + } + if (_values.length != length) { + throw ArgumentError.value( + _values.length, 'values', 'Must contain exactly $length values.'); + } + for (final value in _values) { + if (value.width != elementWidth) { + throw ArgumentError.value( + value, 'values', 'All values must have width $elementWidth.'); + } + } + } + + /// Creates an empty zero-width value array. + LogicValueArray.empty() + : dimensions = const [0], + elementWidth = 0, + _values = const []; + + /// Generates values from row-major multidimensional indices. + factory LogicValueArray.generate(List dimensions, int elementWidth, + LogicValue Function(List indices) generator) { + final length = _lengthFor(dimensions); + return LogicValueArray(dimensions, elementWidth, [ + for (var index = 0; index < length; index++) + generator(_indicesFor(dimensions, index)) + ]); + } + + /// Creates a value array from integer values. + factory LogicValueArray.fromInts( + List dimensions, int elementWidth, Iterable values) => + LogicValueArray(dimensions, elementWidth, + values.map((value) => LogicValue.ofInt(value, elementWidth))); + + /// Captures the current values of a [BaseLogicArray]. + factory LogicValueArray.fromLogicArray(BaseLogicArray values) => + LogicValueArray(values.dimensions, values.elementWidth, + values.arrayElements.map((element) => element.packed.value)); + + /// 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; + for (final slice in slices.skip(1)) { + first._checkCompatible(slice.dimensions, slice.elementWidth); + } + return LogicValueArray([slices.length, ...first.dimensions], + first.elementWidth, slices.expand((slice) => slice.flatValues)); + } + + /// Number of leaf values. + int get length => _lengthFor(dimensions); + + /// Row-major leaf values. + List get flatValues => UnmodifiableListView(_values); + + /// Row-major values paired with their multidimensional indices. + Iterable<(List, LogicValue)> get indexedValues => Iterable.generate( + length, (index) => (_indicesFor(dimensions, index), _values[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 sliceLength = _lengthFor(sliceDimensions); + for (var start = 0; start < length; start += sliceLength) { + yield LogicValueArray(sliceDimensions, elementWidth, + _values.getRange(start, start + sliceLength)); + } + } + + /// Returns the value at multidimensional [indices]. + LogicValue at(List indices) => _values[_flatIndex(indices)]; + + /// Returns the row-major flat index for multidimensional [indices]. + int flatIndexOf(List indices) => _flatIndex(indices); + + /// Maps this array while preserving dimensions and element width. + LogicValueArray map(LogicValue Function(LogicValue value) transform) => + LogicValueArray(dimensions, elementWidth, _values.map(transform)); + + /// Maps this array with row-major multidimensional indices. + LogicValueArray indexedMap( + LogicValue Function(List indices, LogicValue value) transform, + ) => + LogicValueArray(dimensions, elementWidth, + indexedValues.map((entry) => transform(entry.$1, entry.$2))); + + /// Maps slices along the first dimension and stacks the results. + LogicValueArray mapMajorSlices( + LogicValueArray Function(LogicValueArray slice) transform, + ) => + LogicValueArray.stack(majorSlices.map(transform)); + + /// Returns a row-major view with new [dimensions]. + LogicValueArray reshape(List dimensions) { + if (_lengthFor(dimensions) != length) { + throw ArgumentError.value( + dimensions, 'dimensions', 'Must contain $length values.'); + } + return LogicValueArray(dimensions, elementWidth, _values); + } + + /// Transposes a two-dimensional value array. + LogicValueArray transpose2D() { + _checkTwoDimensional(dimensions); + return LogicValueArray.generate([dimensions[1], dimensions[0]], + elementWidth, (indices) => at([indices[1], indices[0]])); + } + + /// Creates a [LogicArray] with the same shape and drives it with this value. + LogicArray toLogicArray({String? name}) => + putInto(LogicArray(dimensions, elementWidth, name: name)); + + /// Drives [target] with this value array. + T putInto(T target) { + _checkCompatible(target.dimensions, target.elementWidth); + for (var index = 0; index < _values.length; index++) { + target.arrayElements[index].put(_values[index]); + } + return target; + } + + void _checkCompatible(List otherDimensions, int otherElementWidth) { + if (!_sameDimensions(dimensions, otherDimensions) || + elementWidth != otherElementWidth) { + throw ArgumentError.value(otherDimensions, 'target', + 'Must have dimensions $dimensions and elementWidth $elementWidth.'); + } + } + + int _flatIndex(List indices) { + if (indices.length != dimensions.length) { + throw RangeError.range(indices.length, dimensions.length, + dimensions.length, 'indices.length'); + } + + var index = 0; + for (var dimension = 0; dimension < dimensions.length; dimension++) { + final indexAtDimension = indices[dimension]; + if (indexAtDimension < 0 || indexAtDimension >= dimensions[dimension]) { + throw RangeError.range(indexAtDimension, 0, dimensions[dimension] - 1, + 'indices[$dimension]'); + } + index = index * dimensions[dimension] + indexAtDimension; + } + return index; + } + + static int _lengthFor(List dimensions) => + dimensions.fold(1, (length, dimension) => length * dimension); + + static List _indicesFor(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; + } + + static bool _sameDimensions(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); +} + +/// Functional traversal helpers for [BaseLogicArray]. +extension LogicArrayTraversal on BaseLogicArray { + /// Row-major leaves paired with their multidimensional indices. + Iterable<(List, Logic)> get indexedLeaves => Iterable.generate( + arrayElements.length, + (index) => ( + LogicValueArray._indicesFor(dimensions, index), + arrayElements[index] + ), + ); + + /// Returns the leaf at multidimensional [indices]. + Logic 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; + } + + /// Connects row-major leaves to [sources], requiring equal lengths. + BaseLogicArray getsEach(Iterable sources) { + for (final (target, source) in arrayElements.zipExact(sources)) { + target <= source; + } + return this; + } + + /// Connects each leaf to a value generated from its array indices. + BaseLogicArray getsGenerated(Logic Function(List indices) generator) { + for (final (indices, target) in indexedLeaves) { + target <= generator(indices); + } + return this; + } + + /// Returns a row-major view with new [dimensions]. + BaseLogicArray reshape(List dimensions, {String? name}) { + if (LogicValueArray._lengthFor(dimensions) != arrayElements.length) { + throw ArgumentError.value(dimensions, 'dimensions', + 'Must contain ${arrayElements.length} leaves.'); + } + return LogicArray(dimensions, elementWidth, name: name) + ..getsEach(arrayElements); + } + + /// Transposes a two-dimensional logic array. + BaseLogicArray transpose2D({String? name}) { + _checkTwoDimensional(dimensions); + return LogicArray([dimensions[1], dimensions[0]], elementWidth, name: name) + ..getsGenerated((indices) => at([indices[1], indices[0]])); + } + + /// Immediate child arrays along the first dimension. + Iterable get majorSlices { + if (dimensions.length < 2) { + throw StateError('majorSlices requires at least two dimensions.'); + } + return elements.cast(); + } +} + +/// Exact pairwise iteration for functional array wiring. +extension ExactZip on Iterable { + /// Zips this iterable with [other], throwing when lengths differ. + Iterable<(T, U)> zipExact(Iterable other) sync* { + final left = iterator; + final right = other.iterator; + while (true) { + final hasLeft = left.moveNext(); + final hasRight = right.moveNext(); + if (hasLeft != hasRight) { + throw StateError('Cannot zip iterables of different lengths.'); + } + if (!hasLeft) { + return; + } + yield (left.current, right.current); + } + } +} + +/// Splits an iterable of pairs into two lists. +extension Unzip on Iterable<(T, U)> { + /// Unzips pairs while preserving iteration order. + (List, List) unzip() { + final left = []; + final right = []; + for (final (first, second) in this) { + left.add(first); + right.add(second); + } + return (left, right); + } +} + +void _checkTwoDimensional(List dimensions) { + if (dimensions.length != 2) { + throw StateError('Expected exactly two dimensions, got $dimensions.'); + } +} diff --git a/lib/src/signals/logic_value_array_of.dart b/lib/src/signals/logic_value_array_of.dart new file mode 100644 index 0000000000..1fe85060f2 --- /dev/null +++ b/lib/src/signals/logic_value_array_of.dart @@ -0,0 +1,152 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_value_array_of.dart +// Definition of typed multi-dimensional logic value arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +part of 'signals.dart'; + +/// 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; + + final Function _encode; + + /// Converts a semantic value into its packed representation. + LogicValue encode(T value) => + (_encode as LogicValue Function(T value))(value); + + /// Creates a bidirectional value [decode]/[encode] codec. + const LogicValueCodec( + {required this.decode, required LogicValue Function(T value) encode}) + : _encode = encode; +} + +/// A multidimensional array of semantic values backed by [LogicValue]s. +class LogicValueArrayOf { + final LogicValueArray _logicValues; + final List _values; + + /// Codec used at the packed logic boundary. + final LogicValueCodec codec; + + /// Creates a typed value array from row-major [values]. + factory LogicValueArrayOf( + List dimensions, + int elementWidth, + Iterable values, { + required LogicValueCodec codec, + }) { + final typedValues = List.unmodifiable(values); + return LogicValueArrayOf._( + LogicValueArray( + dimensions, elementWidth, typedValues.map(codec.encode)), + typedValues, + codec); + } + + /// Decodes a packed [LogicValueArray]. + factory LogicValueArrayOf.fromLogicValues(LogicValueArray values, + {required LogicValueCodec codec}) => + LogicValueArrayOf._(values, + List.unmodifiable(values.flatValues.map(codec.decode)), codec); + + /// Stacks equally shaped typed arrays along a new outer dimension. + factory LogicValueArrayOf.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; + return LogicValueArrayOf( + [slices.length, ...first.dimensions], first.elementWidth, + slices.expand((slice) { + first._checkCompatible(slice); + return slice.flatValues; + }), codec: first.codec); + } + + LogicValueArrayOf._(this._logicValues, this._values, this.codec); + + /// Number of elements at each array level. + List get dimensions => _logicValues.dimensions; + + /// Width of each packed leaf. + int get elementWidth => _logicValues.elementWidth; + + /// Number of typed leaves. + int get length => _logicValues.length; + + /// Typed leaves in row-major order. + List get flatValues => UnmodifiableListView(_values); + + /// Packed value-domain representation. + LogicValueArray get logicValues => _logicValues; + + /// Typed values paired with their multidimensional indices. + Iterable<(List, T)> get indexedValues => _logicValues.indexedValues + .zipExact(_values) + .map((entry) => (entry.$1.$1, entry.$2)); + + /// Slices along the first dimension. + Iterable> get majorSlices => + _logicValues.majorSlices.map(_fromLogicValues); + + /// Returns the typed value at multidimensional [indices]. + T at(List indices) => _values[_logicValues.flatIndexOf(indices)]; + + /// Maps typed values while preserving shape and codec. + LogicValueArrayOf map(T Function(T value) transform) => + LogicValueArrayOf(dimensions, elementWidth, _values.map(transform), + codec: codec); + + /// Maps typed values with their multidimensional indices. + LogicValueArrayOf indexedMap( + T Function(List indices, T value) transform, + ) => + LogicValueArrayOf(dimensions, elementWidth, + indexedValues.map((entry) => transform(entry.$1, entry.$2)), + codec: codec); + + /// Maps slices along the first dimension and stacks the results. + LogicValueArrayOf mapMajorSlices( + LogicValueArrayOf Function(LogicValueArrayOf slice) transform, + ) => + LogicValueArrayOf.stack(majorSlices.map(transform)); + + /// Returns a row-major view with new [dimensions]. + LogicValueArrayOf reshape(List dimensions) => + LogicValueArrayOf.fromLogicValues(_logicValues.reshape(dimensions), + codec: codec); + + /// Transposes a two-dimensional typed value array. + LogicValueArrayOf transpose2D() => + LogicValueArrayOf.fromLogicValues(_logicValues.transpose2D(), + codec: codec); + + /// Creates a [LogicArray] driven by the packed values. + LogicArray toLogicArray({String? name}) => + _logicValues.toLogicArray(name: name); + + /// Drives [target] with the packed values. + U putInto(U target) => _logicValues.putInto(target); + + LogicValueArrayOf _fromLogicValues(LogicValueArray values) => + LogicValueArrayOf.fromLogicValues(values, codec: codec); + + void _checkCompatible(LogicValueArrayOf other) { + if (elementWidth != other.elementWidth || + !_sameDimensions(dimensions, other.dimensions)) { + throw ArgumentError.value( + other.dimensions, + 'arrays', + 'All arrays must have dimensions $dimensions and ' + 'elementWidth $elementWidth.'); + } + } +} diff --git a/lib/src/signals/signals.dart b/lib/src/signals/signals.dart index ac0f97b1bd..93e1d5e75b 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; @@ -21,6 +21,9 @@ part 'wire.dart'; part 'wire_net.dart'; part 'logic_structure.dart'; part 'logic_array.dart'; +part 'logic_array_of.dart'; +part 'logic_value_array.dart'; +part 'logic_value_array_of.dart'; part 'logic_net.dart'; part 'logic_enum.dart'; part 'logic_def.dart'; diff --git a/test/logic_array_of_test.dart b/test/logic_array_of_test.dart new file mode 100644 index 0000000000..4d0cd9162e --- /dev/null +++ b/test/logic_array_of_test.dart @@ -0,0 +1,301 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_array_of_test.dart +// Tests for typed logic and logic value arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class _TwoBitStructure extends LogicStructure { + final Logic low; + final Logic high; + + factory _TwoBitStructure({String? name}) => _TwoBitStructure._( + Logic(name: 'low'), + Logic(name: 'high'), + name: name ?? 'twoBit', + ); + + _TwoBitStructure._(this.low, this.high, {required String name}) + : super([low, high], name: name); + + @override + _TwoBitStructure clone({String? name}) => + _TwoBitStructure(name: name ?? this.name); +} + +class _TypedArrayPortModule extends Module { + LogicArrayOf<_TwoBitStructure> get valuesOut => + output('valuesOut') as LogicArrayOf<_TwoBitStructure>; + + _TypedArrayPortModule(LogicArrayOf<_TwoBitStructure> valuesIn) { + valuesIn = addTypedInput('valuesIn', valuesIn); + addTypedOutput('valuesOut', valuesIn.clone) <= valuesIn; + } +} + +int _decodeLogicValue(LogicValue value) => value.toInt(); + +LogicValue _encodeLogicValue(int value) => LogicValue.ofInt(value, 8); + +void main() { + group('LogicArrayOf', () { + test('keeps structured leaves at the array boundary', () { + final values = LogicArrayOf<_TwoBitStructure>( + [2, 3], + _TwoBitStructure.new, + dimensionNames: const ['row_', 'column_'], + ); + + expect(values, isA()); + expect(values.dimensions, equals([2, 3])); + expect(values.elementWidth, 2); + expect(values.arrayElements, hasLength(6)); + expect(values.typedLeafElements, hasLength(6)); + expect(values.arrayElements, everyElement(isA<_TwoBitStructure>())); + expect(values.leafElements, hasLength(12)); + expect(values.elementAt([1, 2]), same(values.arrayElements[5])); + expect(values.indexedElements.last.$1, equals([1, 2])); + }); + + test('provides typed indexing, cloning, and packed conversions', () { + final values = LogicArrayOf( + [2, 2], + ({name}) => Logic(name: name, width: 8), + ); + final packed = values.toLogicArray(name: 'packed'); + final clone = values.clone(name: 'clone'); + + expect(values.elementAt([1, 0]), same(values.typedLeafElements[2])); + expect(packed.dimensions, equals([2, 2])); + expect(packed.elementWidth, 8); + expect(clone, isA>()); + expect(clone.name, 'clone'); + expect(clone.typedLeafElements, hasLength(4)); + expect( + () => values.getsPackedValues(LogicArray([4], 8)), + throwsA(isA()), + ); + expect( + () => values.getsPackedValues(LogicArray([2, 2], 4)), + throwsA(isA()), + ); + }); + + test('recursively flattens nested array dimensions with typed leaves', () { + final nested = LogicArrayOf( + [2, 2], + ({name}) => LogicArray([3, 2], 4, name: name), + ); + final flattened = nested.flattenNestedDimensions(name: 'flat'); + final source = nested.elementAt([1, 0]).elementAt([2, 1]); + final target = flattened.elementAt([1, 0, 2, 1]); + + expect(flattened.dimensions, equals([2, 2, 3, 2])); + expect(flattened.elementWidth, 4); + expect(flattened.typedLeafElements, hasLength(24)); + expect(target, isA()); + expect(target.srcConnections, contains(source)); + }); + + test('flattens every nested array layer', () { + final nested = LogicArrayOf>( + [2], + ({name}) => LogicArrayOf( + [3], + ({name}) => LogicArray([4], 2, name: name), + name: name, + ), + ); + final flattened = nested.flattenNestedDimensions(); + final source = nested.elementAt([1]).elementAt([2]).elementAt([3]); + final target = flattened.elementAt([1, 2, 3]); + + expect(flattened.dimensions, equals([2, 3, 4])); + expect(flattened.typedLeafElements, hasLength(24)); + expect(target.srcConnections, contains(source)); + }); + + test('validates dimensions, names, and leaf widths', () { + expect( + () => LogicArrayOf(const [], Logic.new), + throwsA(isA()), + ); + expect( + () => LogicArrayOf( + [2], + Logic.new, + dimensionNames: const [], + ), + throwsA(isA()), + ); + + var width = 1; + expect( + () => LogicArrayOf( + [2], + ({name}) => Logic(name: name, width: width++), + ), + throwsA(isA()), + ); + }); + + test('drives and captures compatible packed and typed values', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final values = LogicArrayOf( + [2], + ({name}) => Logic(name: name, width: 8), + ); + final packedValues = LogicValueArray.fromInts([2], 8, [12, 34]); + final typedValues = LogicValueArrayOf( + [2], + 8, + [56, 78], + codec: codec, + ); + + expect(packedValues.putInto(values), same(values)); + expect( + values.logicValues.flatValues.map((value) => value.toInt()), + [12, 34], + ); + + values.putValueArrayOf(typedValues); + expect(values.valueArrayOf(codec).flatValues, [56, 78]); + }); + + test('preserves specialized leaves in typed input and output ports', () { + final source = LogicArrayOf<_TwoBitStructure>( + [2, 3], + _TwoBitStructure.new, + ); + final module = _TypedArrayPortModule(source); + final valuesIn = + module.input('valuesIn') as LogicArrayOf<_TwoBitStructure>; + + expect(valuesIn, isA>()); + expect(valuesIn.dimensions, [2, 3]); + expect(valuesIn.elementAt([1, 2]), isA<_TwoBitStructure>()); + expect(module.valuesOut, isA>()); + expect(module.valuesOut.elementAt([1, 2]).low.width, 1); + }); + }); + + group('LogicValueArray', () { + test('indexes, slices, reshapes, and stacks row-major values', () { + final values = LogicValueArray.fromInts([2, 3], 8, [1, 2, 3, 4, 5, 6]); + + expect(values.length, 6); + expect(values.at([1, 1]).toInt(), 5); + expect(values.flatIndexOf([1, 2]), 5); + expect(values.indexedValues.last.$1, equals([1, 2])); + expect( + values.majorSlices.map( + (slice) => slice.flatValues.map((value) => value.toInt()).toList()), + equals([ + [1, 2, 3], + [4, 5, 6], + ]), + ); + expect(values.reshape([3, 2]).at([2, 1]).toInt(), 6); + expect( + LogicValueArray.stack(values.majorSlices).dimensions, + equals([2, 3]), + ); + }); + + test('maps values and validates incompatible operations', () { + final values = LogicValueArray.fromInts([2, 2], 8, [1, 2, 3, 4]); + + expect( + values + .indexedMap((indices, value) => + LogicValue.ofInt(value.toInt() + indices[0], 8)) + .flatValues + .map((value) => value.toInt()), + equals([1, 2, 4, 5]), + ); + expect( + () => values.at([2, 0]), + throwsA(isA()), + ); + expect( + () => values.reshape([3, 2]), + throwsA(isA()), + ); + expect( + () => LogicValueArray.stack([ + values, + LogicValueArray.fromInts([4], 8, [1, 2, 3, 4]), + ]), + throwsA(isA()), + ); + }); + }); + + group('LogicValueArrayOf', () { + test('maps, reshapes, and transposes packed value arrays', () { + final values = LogicValueArray.fromInts([2, 3], 8, [1, 2, 3, 4, 5, 6]); + final transposed = values.transpose2D(); + + expect(transposed.dimensions, equals([3, 2])); + expect( + transposed.flatValues.map((value) => value.toInt()), + equals([1, 4, 2, 5, 3, 6]), + ); + expect(values.reshape([3, 2]).at([2, 1]).toInt(), 6); + + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final typed = LogicValueArrayOf.fromLogicValues( + values, + codec: codec, + ); + expect(typed.at([1, 1]), 5); + expect(typed.map((value) => value + 1).at([1, 1]), 6); + expect(typed.transpose2D().at([2, 1]), 6); + }); + + test('supports slices, indexed mapping, stacking, and conversion', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final values = LogicValueArrayOf( + [2, 2], + 8, + [1, 2, 3, 4], + codec: codec, + ); + + expect(values.majorSlices.map((slice) => slice.flatValues), [ + [1, 2], + [3, 4], + ]); + expect( + values.indexedMap((indices, value) => value + indices[1]).flatValues, + [1, 3, 3, 5], + ); + expect( + LogicValueArrayOf.stack(values.majorSlices).flatValues, + values.flatValues, + ); + expect(values.logicValues.flatValues.map((value) => value.toInt()), [ + 1, + 2, + 3, + 4, + ]); + expect(values.toLogicArray().dimensions, [2, 2]); + }); + }); +} diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index 7fea35e0bf..04d213d34a 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -244,6 +244,42 @@ void main() { expect(orig.clone(name: 'newName').name, 'newName'); }); + test('flatten outer clones and connects promoted fields', () { + final config = LogicStructure([ + Logic(name: 'mode', width: 2), + Logic(name: 'valid'), + ], name: 'config'); + final control = LogicStructure([ + Logic(name: 'enable'), + config, + ], name: 'control'); + + final flattened = control.flattenOuter(name: 'flatControl'); + + expect(flattened.name, 'flatControl'); + expect(flattened.elements.map((element) => element.name), + ['enable', 'config_mode', 'config_valid']); + expect(flattened.elements[1].width, 2); + expect( + flattened.elements[0].srcConnections, contains(control.elements[0])); + expect( + flattened.elements[1].srcConnections, contains(config.elements[0])); + expect( + flattened.elements[2].srcConnections, contains(config.elements[1])); + }); + + test('flatten outer rejects duplicate promoted field names', () { + final control = LogicStructure([ + Logic(name: 'mode'), + LogicStructure([ + Logic(name: 'mode'), + ], name: 'config'), + ], name: 'control'); + + expect(() => control.flattenOuter(prefixFieldNames: false), + throwsA(isA())); + }); + test('tricky withSet', () { // first field has width of 72 so this is the starting point // second field has a width of 12 From bc19b6f34cd5a684472840ab0f89eb909dcd7324 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 5 Sep 2026 05:11:26 -0700 Subject: [PATCH 02/43] missing port management --- lib/src/interfaces/interface.dart | 91 +++++++------ lib/src/module.dart | 30 +++-- lib/src/signals/logic.dart | 6 +- lib/src/signals/logic_structure.dart | 2 +- .../netlist/netlist_module_translation.dart | 10 +- .../netlist_synth_module_definition.dart | 24 ++-- .../netlist/netlist_synthesizer.dart | 22 ++-- .../synthesizers/netlist/netlist_utils.dart | 12 +- ...systemverilog_synth_module_definition.dart | 8 +- .../utilities/synth_array_concat.dart | 6 +- .../utilities/synth_array_slice.dart | 2 +- .../synthesizers/utilities/synth_logic.dart | 120 +++++++++++++++++- .../utilities/synth_module_definition.dart | 69 +++++++--- .../utilities/synth_structure_layout.dart | 2 +- lib/src/utilities/simcompare.dart | 24 ++-- test/logic_array_of_test.dart | 68 +++++++++- 16 files changed, 363 insertions(+), 133 deletions(-) diff --git a/lib/src/interfaces/interface.dart b/lib/src/interfaces/interface.dart index 4791625a15..4e23d5512d 100644 --- a/lib/src/interfaces/interface.dart +++ b/lib/src/interfaces/interface.dart @@ -85,35 +85,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 +131,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 +142,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/module.dart b/lib/src/module.dart index 7da13458e1..d830b0c9e6 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -697,9 +697,10 @@ abstract class Module { /// [input]. You can use this on a [Logic], [LogicArray], or [LogicStructure]. /// /// 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 +709,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 +769,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 +779,7 @@ abstract class Module { sourceElems.add(sei.parentStructure!); } - if (sei is LogicArray) { + if (sei is BaseLogicArray) { sourceElems.addAll(sei.elements); } } @@ -1040,7 +1054,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/signals/logic.dart b/lib/src/signals/logic.dart index b685f5af79..6a1e82f7f3 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -133,8 +133,8 @@ class Logic { LogicStructure? get parentStructure => _parentStructure; LogicStructure? _parentStructure; - /// True if this is a member of a [LogicArray]. - bool get isArrayMember => parentStructure is LogicArray; + /// True if this is a member of a [BaseLogicArray]. + 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 +143,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'; diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index e6cc9c4f99..a3588acd39 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}) { diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 9e08d4efe2..ee5469f137 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -131,7 +131,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 +168,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 +177,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 +195,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( @@ -362,7 +362,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..0b7e0afa0e 100644 --- a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -19,14 +19,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 +39,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 +68,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 +81,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; @@ -106,7 +106,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { } /// 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..49490abdcd 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -164,7 +164,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 +181,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,7 +232,7 @@ 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); } } @@ -250,7 +251,8 @@ 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. @@ -265,7 +267,7 @@ 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); } } @@ -339,7 +341,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 +350,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 +364,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 +379,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 +428,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 diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 638afbd45b..51512dd692 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -424,7 +424,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 +434,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 +447,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 +483,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/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index e2d646257d..c9e2ff6468 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -519,7 +519,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 +527,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 +1322,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } final allElements = parentArray.logics - .whereType() + .whereType() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1488,7 +1488,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/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index 168ec0e01a..dfba7ecf49 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -11,16 +11,16 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.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..f03761c013 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -11,7 +11,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.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..227a1b4169 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -173,7 +173,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 +269,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. @@ -369,7 +369,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); @@ -623,7 +623,7 @@ class SynthLogic { final logic = logics.first; if (isArray) { - final logicArr = logic as LogicArray; + final logicArr = logic as BaseLogicArray; final packedDimsBuf = StringBuffer(); final unpackedDimsBuf = StringBuffer(); @@ -831,7 +831,7 @@ class SynthLogicPackedRangeReference extends SynthLogic { } } -/// 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. @@ -915,13 +915,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 +933,109 @@ 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. +class SynthLogicArrayStructureElement extends SynthLogic { + /// The scalar field represented by this synthesized signal. + final Logic logic; + + /// The synthesized root array containing [logic]. + SynthLogic get rootArray { + var current = logic; + var parent = current.parentStructure; + while (parent != null) { + if (parent is BaseLogicArray) { + current = parent; + parent = current.parentStructure; + } else { + current = parent; + parent = current.parentStructure; + } + } + return parentSynthModuleDefinition.getSynthLogic(current)!; + } + + /// The array elements from the configured array leaf to the root array. + Iterable get _arrayMembers sync* { + var current = logic; + var parent = current.parentStructure; + while (parent != null) { + if (parent is BaseLogicArray) { + yield current; + } + current = parent; + parent = current.parentStructure; + } + } + + /// The packed offset of [logic] within its configured array leaf. + int get _leafOffset { + final leaf = _arrayLeafAncestor(logic)! as LogicStructure; + var offset = 0; + for (final leafElement in leaf.leafElements) { + if (identical(leafElement, logic)) { + return offset; + } + offset += leafElement.width; + } + throw StateError('Array leaf does not contain ${logic.name}.'); + } + + @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 { + final arrayName = rootArray.replacement?.name ?? rootArray.name; + final indices = _arrayMembers + .toList() + .reversed + .map((member) => '[${member.arrayIndex}]') + .join(); + final lower = _leafOffset; + final upper = lower + width - 1; + return '$arrayName$indices' + '${upper == lower ? '[$lower]' : '[$upper:$lower]'}'; + } + + /// 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; + } +} diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index e0dbbe3e74..18c9e25ae2 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -337,6 +337,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 +383,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 +392,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 +414,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.rootArray.resolved, + _ => throw StateError('Unexpected synthesized array signal'), + }; signalAndAncestors.add(signal); } @@ -415,7 +440,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,7 +517,7 @@ 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); } } @@ -502,7 +527,7 @@ 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); } } @@ -511,7 +536,7 @@ 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); @@ -536,17 +561,17 @@ class SynthModuleDefinition { subModule.inputs.values .whereType() - .where((e) => e is! LogicArray) + .where((e) => e is! BaseLogicArray) .forEach(_partialAssignStructPort); subModule.outputs.values .whereType() - .where((e) => e is! LogicArray) + .where((e) => e is! BaseLogicArray) .forEach(_subsetReceiveStructPort); subModule.inOuts.values .whereType() - .where((e) => e is! LogicArray) + .where((e) => e is! BaseLogicArray) .forEach(_subsetReceiveStructPort); } @@ -1130,7 +1155,7 @@ class SynthModuleDefinition { } candidatesByArray.forEach((parentArray, arrayCandidates) { final allElementSynthLogics = parentArray.logics - .whereType() + .whereType() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1804,7 +1829,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 +2087,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 +2478,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 +2744,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 +3853,7 @@ class SynthModuleDefinition { } final arrayLogic = parentArray.logics.first; - if (arrayLogic is! LogicArray || + if (arrayLogic is! BaseLogicArray || indexedInputs.length != arrayLogic.elements.length) { continue; } @@ -3888,7 +3914,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 +3929,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 +3955,7 @@ class SynthModuleDefinition { return false; } - return dst.logics.singleOrNull is! LogicArray; + return dst.logics.singleOrNull is! BaseLogicArray; }); } @@ -4123,12 +4149,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..ce89308c9b 100644 --- a/lib/src/synthesizers/utilities/synth_structure_layout.dart +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -53,7 +53,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..ed5c70f1a2 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -91,15 +91,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 +121,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( @@ -709,7 +709,7 @@ abstract class SimCompare { final signal = module.signals.firstWhere((e) => e.name == signalName); final signalType = signalTypeOverride ?? - ((signal is LogicNet || (signal is LogicArray && signal.isNet)) + ((signal is LogicNet || (signal is BaseLogicArray && signal.isNet)) ? 'wire' : 'logic'); @@ -717,7 +717,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 diff --git a/test/logic_array_of_test.dart b/test/logic_array_of_test.dart index 4d0cd9162e..61d7603654 100644 --- a/test/logic_array_of_test.dart +++ b/test/logic_array_of_test.dart @@ -7,7 +7,10 @@ // 2026 July 21 // 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 _TwoBitStructure extends LogicStructure { @@ -34,7 +37,41 @@ class _TypedArrayPortModule extends Module { _TypedArrayPortModule(LogicArrayOf<_TwoBitStructure> valuesIn) { valuesIn = addTypedInput('valuesIn', valuesIn); - addTypedOutput('valuesOut', valuesIn.clone) <= valuesIn; + final valuesOut = addTypedOutput('valuesOut', valuesIn.clone); + for (final (index, source) in valuesIn.typedLeafElements.indexed) { + final destination = valuesOut.typedLeafElements[index]; + destination.low <= source.low; + destination.high <= ~source.high; + } + } +} + +class _TypedArrayInterface extends Interface<_TypedArrayDirection> { + _TypedArrayInterface() { + setPorts([ + LogicArrayOf<_TwoBitStructure>( + [2], + _TwoBitStructure.new, + 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], + ); } } @@ -186,6 +223,35 @@ void main() { expect(module.valuesOut, isA>()); expect(module.valuesOut.elementAt([1, 2]).low.width, 1); }); + + test('preserves typed arrays through interfaces and synthesis', () async { + final source = LogicArrayOf<_TwoBitStructure>( + [2], + _TwoBitStructure.new, + name: 'valuesIn', + ); + final module = _TypedArrayPortModule(source); + await module.build(); + + final vectors = [ + Vector({'valuesIn': 0x9}, {'valuesOut': 0x3}), + Vector({'valuesIn': 0x6}, {'valuesOut': 0xc}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + + final netlist = jsonDecode(NetlistSynthesizer().synthesizeToJson(module)) + as Map; + expect(netlist['modules'], isNotEmpty); + + final interfaceModule = + _TypedArrayInterfaceModule(_TypedArrayInterface()); + await interfaceModule.build(); + expect( + interfaceModule.input('values'), + isA>(), + ); + }); }); group('LogicValueArray', () { From aefddd918e4ea9ed25520315aac7c75ecd36daf5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 8 Sep 2026 17:23:48 -0700 Subject: [PATCH 03/43] fixed more corner cases than reported on LogicArrayOf mostly cloning / assignment --- lib/src/modules/bus.dart | 2 +- lib/src/signals/logic_array.dart | 63 ++++++++--------- lib/src/signals/logic_array_of.dart | 98 +++++++++++++++++++++++--- lib/src/signals/logic_structure.dart | 9 ++- lib/src/signals/logic_value_array.dart | 2 +- test/logic_array_of_test.dart | 51 ++++++++++++++ test/logic_structure_test.dart | 13 ++++ 7 files changed, 188 insertions(+), 50 deletions(-) diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index bf7c6f4a1e..1c8e6c2e6c 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -433,7 +433,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/logic_array.dart b/lib/src/signals/logic_array.dart index 3dd97da294..adfa3cf3e0 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -70,8 +70,7 @@ class BaseLogicArray extends LogicStructure { naming: naming, logicBuilder: Logic.new, logicArrayBuilder: BaseLogicArray.new, - arrayBuilder: BaseLogicArray._, - isNet: false); + arrayBuilder: BaseLogicArray._); @override final bool isNet; @@ -94,8 +93,7 @@ class BaseLogicArray extends LogicStructure { naming: naming, logicBuilder: LogicNet.new, logicArrayBuilder: BaseLogicArray.net, - arrayBuilder: BaseLogicArray._, - isNet: true); + arrayBuilder: BaseLogicArray._); /// Creates an array from pre-built [elements]. /// @@ -105,16 +103,18 @@ class BaseLogicArray extends LogicStructure { /// dimensions. For a one-dimensional array, each element must have /// [elementWidth] bits. @protected - BaseLogicArray.structured(super.elements, + BaseLogicArray.structured(List elements, {required List dimensions, required this.elementWidth, String? name, this.numUnpackedDimensions = 0, Naming? naming, - this.isNet = false}) + bool? isNet}) : dimensions = List.unmodifiable(dimensions), + isNet = isNet ?? elements.every((element) => element.isNet), naming = Naming.chooseNaming(name, naming), - super(name: Naming.chooseName(name, naming, nullStarter: 'a')) { + super(elements, + name: Naming.chooseName(name, naming, nullStarter: 'a')) { if (dimensions.isEmpty) { throw LogicConstructionException( 'Arrays must have at least 1 dimension.'); @@ -172,7 +172,6 @@ class BaseLogicArray extends LogicStructure { {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, @@ -212,25 +211,26 @@ class BaseLogicArray extends LogicStructure { naming = newNaming; name = newName; - return arrayBuilder( - 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), + 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); + isNet: elements.every((element) => element.isNet)); } @override @@ -245,8 +245,7 @@ class BaseLogicArray extends LogicStructure { newNaming: naming), logicBuilder: isNet ? LogicNet.new : Logic.new, logicArrayBuilder: isNet ? BaseLogicArray.net : BaseLogicArray.new, - arrayBuilder: BaseLogicArray._, - isNet: isNet); + arrayBuilder: BaseLogicArray._); /// Creates a new [BaseLogicArray] which has the same [dimensions], /// [elementWidth], [numUnpackedDimensions], and [isNet] as `this`. @@ -348,8 +347,7 @@ class LogicArray extends LogicArrayOf { numUnpackedDimensions: numUnpackedDimensions, naming: naming, logicBuilder: Logic.new, - logicArrayBuilder: LogicArray.new, - isNet: false); + logicArrayBuilder: LogicArray.new); /// Creates an array of [LogicNet]s with [dimensions] and [elementWidth] /// named [name]. @@ -363,8 +361,7 @@ class LogicArray extends LogicArrayOf { numUnpackedDimensions: numUnpackedDimensions, naming: naming, logicBuilder: LogicNet.new, - logicArrayBuilder: LogicArray.net, - isNet: true); + logicArrayBuilder: LogicArray.net); LogicArray._(List elements, {required super.dimensions, @@ -379,7 +376,6 @@ class LogicArray extends LogicArrayOf { {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, @@ -391,8 +387,7 @@ class LogicArray extends LogicArrayOf { naming: naming, logicBuilder: logicBuilder, logicArrayBuilder: logicArrayBuilder, - arrayBuilder: LogicArray._, - isNet: isNet) as LogicArray; + arrayBuilder: LogicArray._) as LogicArray; @override @@ -408,8 +403,7 @@ class LogicArray extends LogicArrayOf { originalNaming: naming, newNaming: null), logicBuilder: isNet ? LogicNet.new : Logic.new, - logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new, - isNet: isNet); + logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new); @override @@ -424,8 +418,7 @@ class LogicArray extends LogicArrayOf { originalNaming: this.naming, newNaming: naming), logicBuilder: isNet ? LogicNet.new : Logic.new, - logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new, - isNet: isNet) + logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new) ..gets(this); /// Creates an array port with a convenient constructor signature. diff --git a/lib/src/signals/logic_array_of.dart b/lib/src/signals/logic_array_of.dart index ef67689e33..f240003d47 100644 --- a/lib/src/signals/logic_array_of.dart +++ b/lib/src/signals/logic_array_of.dart @@ -28,24 +28,38 @@ class LogicArrayOf extends BaseLogicArray { ); /// Creates an array with [dimensions] and typed leaves from [elementBuilder]. + /// + /// [numUnpackedDimensions] controls how many outer dimensions are emitted as + /// unpacked dimensions by synthesis. LogicArrayOf( List dimensions, LogicArrayElementBuilder elementBuilder, { List? dimensionNames, String? name, + Naming? naming, + int numUnpackedDimensions = 0, }) : this._( _LogicArrayOfBuild.build(dimensions, elementBuilder, - dimensionNames: dimensionNames), + dimensionNames: dimensionNames, + numUnpackedDimensions: numUnpackedDimensions), elementBuilder, - name: name); + name: name, + naming: naming, + numUnpackedDimensions: numUnpackedDimensions); + // ignore: use_super_parameters - invokes the protected structured constructor. LogicArrayOf._( _LogicArrayOfBuild build, this._elementBuilder, { + required int numUnpackedDimensions, super.name, + Naming? naming, }) : dimensionNames = build.dimensionNames, super.structured(build.elements, - dimensions: build.dimensions, elementWidth: build.elementWidth); + dimensions: build.dimensions, + elementWidth: build.elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + naming: naming); /// Creates a typed array from structurally compatible [elements]. @protected @@ -83,6 +97,7 @@ class LogicArrayOf extends BaseLogicArray { LogicArrayOf flattenNestedDimensions({String? name}) { var leaves = typedLeafElements.cast().toList(growable: false); final flattenedDimensions = [...dimensions]; + var flattenedUnpackedDimensions = numUnpackedDimensions; while (leaves.any((leaf) => leaf is BaseLogicArray)) { if (leaves.any((leaf) => leaf is! BaseLogicArray)) { @@ -100,6 +115,7 @@ class LogicArrayOf extends BaseLogicArray { } flattenedDimensions.addAll(reference.dimensions); + flattenedUnpackedDimensions += reference.numUnpackedDimensions; leaves = arrays.expand((array) => array.arrayElements).toList(growable: false); } @@ -119,6 +135,7 @@ class LogicArrayOf extends BaseLogicArray { flattenedDimensions, ({name}) => prototype.clone(name: name) as U, name: name, + numUnpackedDimensions: flattenedUnpackedDimensions, )..getsEach(leaves); } @@ -159,14 +176,40 @@ class LogicArrayOf extends BaseLogicArray { } } - @override - LogicArrayOf clone({String? name}) => + /// Creates a clone while allowing subclasses to preserve their runtime type. + @protected + LogicArrayOf createClone({ + String? name, + Naming? naming, + int? numUnpackedDimensions, + }) => LogicArrayOf(dimensions, _elementBuilder, - dimensionNames: dimensionNames, name: name ?? this.name); + dimensionNames: dimensionNames, + name: name ?? this.name, + naming: naming, + numUnpackedDimensions: + numUnpackedDimensions ?? this.numUnpackedDimensions); @override - LogicArrayOf named(String name, {Naming? naming}) => - clone(name: name)..gets(this); + LogicArrayOf _clone({String? name, Naming? naming}) => createClone( + name: name, + naming: naming, + numUnpackedDimensions: numUnpackedDimensions, + ); + + @override + LogicArrayOf clone({String? name}) => _clone(name: name); + + @override + LogicArrayOf named(String name, {Naming? naming}) => _clone( + name: name, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: this.naming, + newNaming: naming, + ), + )..gets(this); } class _LogicArrayOfBuild { @@ -180,13 +223,19 @@ class _LogicArrayOfBuild { factory _LogicArrayOfBuild.build( List dimensions, LogicArrayElementBuilder elementBuilder, - {List? dimensionNames}) { + {List? dimensionNames, int numUnpackedDimensions = 0}) { final normalizedDimensions = List.unmodifiable(dimensions); if (normalizedDimensions.isEmpty || normalizedDimensions.any((dimension) => dimension <= 0)) { throw LogicConstructionException( 'LogicArrayOf dimensions must all be positive.'); } + if (numUnpackedDimensions < 0 || + numUnpackedDimensions > normalizedDimensions.length) { + throw LogicConstructionException( + 'numUnpackedDimensions must be between 0 and the number of ' + 'dimensions.'); + } final normalizedNames = List.unmodifiable( dimensionNames ?? @@ -197,13 +246,21 @@ class _LogicArrayOfBuild { throw LogicConstructionException( 'dimensionNames must match the number of dimensions.'); } + if (normalizedNames.any((name) => !Sanitizer.isSanitary(name)) || + normalizedNames.toSet().length != normalizedNames.length) { + throw LogicConstructionException( + 'dimensionNames must be sanitary and unique.'); + } final elements = List.generate(normalizedDimensions.first, (index) { final elementName = '${normalizedNames.first}$index'; return normalizedDimensions.length == 1 ? elementBuilder(name: elementName) : LogicArrayOf(normalizedDimensions.sublist(1), elementBuilder, - dimensionNames: normalizedNames.sublist(1), name: elementName); + dimensionNames: normalizedNames.sublist(1), + name: elementName, + numUnpackedDimensions: (numUnpackedDimensions - 1) + .clamp(0, normalizedDimensions.length)); }, growable: false); final typedLeaves = normalizedDimensions.length == 1 ? elements.cast().toList(growable: false) @@ -211,10 +268,31 @@ class _LogicArrayOfBuild { .cast>() .expand((element) => element.typedLeafElements) .toList(growable: false); + if (typedLeaves.any((leaf) => + leaf is LogicStructure && + leaf is! BaseLogicArray && + _containsNestedArray(leaf))) { + throw LogicConstructionException( + 'LogicArrayOf leaves cannot contain nested BaseLogicArray fields.'); + } + if (typedLeaves.any(_containsUnassignableLeaf)) { + throw LogicConstructionException( + 'LogicArrayOf leaves must be driveable and cannot contain Consts.'); + } return _LogicArrayOfBuild._(normalizedDimensions, normalizedNames, elements, _validateElementWidths(typedLeaves)); } + static bool _containsNestedArray(LogicStructure structure) => + structure.elements.any((element) => + element is BaseLogicArray || + (element is LogicStructure && _containsNestedArray(element))); + + static bool _containsUnassignableLeaf(Logic leaf) => + leaf is Const || + (leaf is LogicStructure && + leaf.leafElements.any((element) => element is Const)); + static int _validateElementWidths(List elements) { final width = elements.first.width; if (elements.any((element) => element.width != width)) { diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index a3588acd39..86d18e41e7 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -205,9 +205,12 @@ class LogicStructure implements Logic { } } - final flattenedElements = sources - .map((source) => source.$1.clone(name: source.$2)..gets(source.$1)) - .toList(growable: false); + final flattenedElements = sources.map((source) { + final flattened = source.$1 is Const + ? Logic(width: source.$1.width, name: source.$2) + : source.$1.clone(name: source.$2); + return flattened..gets(source.$1); + }).toList(growable: false); return LogicStructure(flattenedElements, name: name ?? this.name); } diff --git a/lib/src/signals/logic_value_array.dart b/lib/src/signals/logic_value_array.dart index 9d8ed6de0d..9aaf36b1f4 100644 --- a/lib/src/signals/logic_value_array.dart +++ b/lib/src/signals/logic_value_array.dart @@ -78,7 +78,7 @@ class LogicValueArray { /// Captures the current values of a [BaseLogicArray]. factory LogicValueArray.fromLogicArray(BaseLogicArray values) => LogicValueArray(values.dimensions, values.elementWidth, - values.arrayElements.map((element) => element.packed.value)); + values.arrayElements.map((element) => element.value)); /// Stacks equally shaped arrays along a new outer dimension. factory LogicValueArray.stack(Iterable arrays) { diff --git a/test/logic_array_of_test.dart b/test/logic_array_of_test.dart index 61d7603654..bcf00395b5 100644 --- a/test/logic_array_of_test.dart +++ b/test/logic_array_of_test.dart @@ -123,6 +123,20 @@ void main() { ); }); + test('preserves unpacked dimensions through cloning', () { + final values = LogicArrayOf( + [2, 3], + ({name}) => Logic(name: name, width: 4), + numUnpackedDimensions: 1, + ); + + expect(values.clone().numUnpackedDimensions, 1); + expect( + values.named('renamed').numUnpackedDimensions, + 1, + ); + }); + test('recursively flattens nested array dimensions with typed leaves', () { final nested = LogicArrayOf( [2, 2], @@ -170,6 +184,29 @@ void main() { ), throwsA(isA()), ); + expect( + () => LogicArrayOf( + [2, 2], + Logic.new, + dimensionNames: const ['row.', 'column_'], + ), + throwsA(isA()), + ); + expect( + () => LogicArrayOf( + [2, 2], + Logic.new, + dimensionNames: const ['row_', 'row_'], + ), + throwsA(isA()), + ); + expect( + () => LogicArrayOf( + [2], + ({name}) => Const(0, width: 1), + ), + throwsA(isA()), + ); var width = 1; expect( @@ -181,6 +218,20 @@ void main() { ); }); + test('preserves naming when named', () { + final values = LogicArrayOf( + [2], + Logic.new, + name: 'values', + naming: Naming.mergeable, + ); + + final renamed = values.named('renamed', naming: Naming.reserved); + + expect(renamed.name, 'renamed'); + expect(renamed.naming, Naming.reserved); + }); + test('drives and captures compatible packed and typed values', () { const codec = LogicValueCodec( decode: _decodeLogicValue, diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index 04d213d34a..2bce2a829b 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -268,6 +268,19 @@ void main() { flattened.elements[2].srcConnections, contains(config.elements[1])); }); + test('flatten outer connects constant promoted fields', () { + final config = LogicStructure([ + Const(1, width: 1), + ], name: 'config'); + final control = LogicStructure([config], name: 'control'); + + final flattened = control.flattenOuter(name: 'flatControl'); + + expect(flattened.elements.single, isA()); + expect(flattened.elements.single.srcConnections, + contains(config.elements.single)); + }); + test('flatten outer rejects duplicate promoted field names', () { final control = LogicStructure([ Logic(name: 'mode'), From beedc07134bcf702a5b1cf68561504e2f4ba78bd Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 9 Sep 2026 01:08:28 -0700 Subject: [PATCH 04/43] restructure `LogicArrayOf` and address review items --- CHANGELOG.md | 1 + doc/user_guide/_docs/A20-logic-arrays.md | 62 +- lib/src/interfaces/interface.dart | 12 +- lib/src/module.dart | 6 +- lib/src/modules/bus.dart | 2 +- lib/src/signals/logic.dart | 6 +- lib/src/signals/logic_array.dart | 182 ++--- lib/src/signals/logic_array_of.dart | 375 ++++++++-- lib/src/signals/logic_structure.dart | 4 +- lib/src/signals/logic_value_array.dart | 327 --------- lib/src/signals/logic_value_array_of.dart | 152 ---- lib/src/signals/signals.dart | 2 - .../netlist/netlist_module_translation.dart | 84 ++- .../netlist_synth_module_definition.dart | 28 +- .../netlist/netlist_synthesizer.dart | 71 +- .../synthesizers/netlist/netlist_utils.dart | 12 +- ...systemverilog_synth_module_definition.dart | 11 +- .../utilities/synth_array_concat.dart | 6 +- .../utilities/synth_array_slice.dart | 2 +- .../synthesizers/utilities/synth_logic.dart | 21 +- .../utilities/synth_module_definition.dart | 44 +- .../utilities/synth_structure_layout.dart | 2 +- lib/src/utilities/simcompare.dart | 9 +- lib/src/values/logic_value.dart | 25 +- lib/src/values/logic_value_array.dart | 196 +++++ lib/src/values/logic_value_array_of.dart | 505 +++++++++++++ lib/src/values/values.dart | 4 +- test/logic_array_of_test.dart | 683 +++++++++++++++--- test/logic_array_test.dart | 37 + 29 files changed, 2004 insertions(+), 867 deletions(-) delete mode 100644 lib/src/signals/logic_value_array.dart delete mode 100644 lib/src/signals/logic_value_array_of.dart create mode 100644 lib/src/values/logic_value_array.dart create mode 100644 lib/src/values/logic_value_array_of.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d02b969ae..cca2511de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ ## 0.6.10 +- Added `LogicArrayOf` for multidimensional arrays with fixed typed elements, including typed ports, traversal, cloning, shape operations, net support, and synthesis. Added shaped `LogicValueArray` and codec-backed `LogicValueArrayOf` values with nested construction, packed `LogicValue` interoperability, and hardware-array assignment (). - Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (). - Improved simulation performance and generated outputs by avoiding module creation for four-state-safe operations involving `Const`s, zero shifts, and muxes with constant controls (). - Enforced that `Const` values cannot be changed through `put` or `inject`, including through another `Logic` driven by a `Const` (). diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index c68555124e..c01d8c3590 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -1,7 +1,7 @@ --- title: "Logic Arrays" permalink: /docs/logic-arrays/ -last_modified_at: 2026-7-21 +last_modified_at: 2026-9-9 toc: true --- @@ -22,9 +22,9 @@ 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 and value-domain arrays +## Typed arrays -Use `LogicArrayOf` when every leaf has the same specialized `Logic` type. It preserves the normal array dimensions while exposing typed leaves with `typedLeafElements` and `elementAt`. `LogicArray` is `LogicArrayOf`, so it retains its existing construction, port, clone, naming, and array APIs while also identifying its leaves as `Logic`. For example, this creates a two-dimensional array of samples with separate data and valid fields: +Use `LogicArrayOf` when every position at the declared array boundary has the same specialized `Logic` type. `LogicArray` is the ordinary `LogicArrayOf` specialization, so existing `LogicArray` construction, port, clone, naming, and array behavior is retained. For example, this creates a two-dimensional array of samples with separate data and valid fields: ```dart class Sample extends LogicStructure { @@ -50,21 +50,61 @@ final samples = LogicArrayOf( dimensionNames: ['row_', 'column_'], ); -final bottomRightData = samples.elementAt([1, 2]).data; +final bottomRightData = samples.at([1, 2]).data; ``` -When typed array leaves are themselves arrays, use `flattenNestedDimensions()` to create one rectangular `LogicArrayOf` with all nested dimensions concatenated. The full address is preserved: `nested.elementAt(outerIndex).elementAt(innerIndex)` maps to `flattened.elementAt([...outerIndex, ...innerIndex])`. Every sibling nested array must have matching dimensions and leaf width. +The element builder must always produce the configured type, width, and net kind. A zero-sized array calls it once as a prototype so that metadata remains available even though the array has no positions. -Use `LogicValueArray` for fixed-width array data outside the hardware graph. It keeps values in row-major order and supports indexing, reshaping, transposition, and slice operations. `LogicValueArrayOf` adds a codec so application-level values can use the same operations while converting to and from packed `LogicValue`s. +### Traversal boundaries + +These APIs intentionally stop at different boundaries: + +- `elements` contains the immediate children of the outermost structure or array level. +- `LogicArrayOf.arrayElements` traverses exactly the dimensions declared by that array and then stops at `T`. 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. + +When typed array elements are themselves arrays, use `flattenNestedDimensions()` to create one rectangular `LogicArrayOf` with all nested dimensions concatenated. The full address is preserved: `nested.at(outerIndices).at(innerIndices)` maps to `flattened.at([...outerIndices, ...innerIndices])`. Every sibling nested array must have matching dimensions, element width, depth, and unpacked-dimension configuration. Empty shapes are supported because the configured element builders supply the missing metadata. + +A `LogicArrayOf` element can be a `LogicStructure`, but it must be driveable. Direct `Const` elements, structures containing a `Const`, and element structures containing nested arrays are rejected. An array can instead directly contain another array and then be flattened as described above. + +`LogicArrayOf` is also the supported base for custom typed arrays. A subclass using the protected `LogicArrayOf.structured` constructor must supply a correctly configured element builder and should override `createClone` to preserve its runtime type and metadata. + +## 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([2, 3], 8, [1, 2, 3, 4, 5, 6]); -final transposed = values.transpose2D(); // Dimensions: [3, 2] +final values = LogicValueArray.fromInts( + [ + [1, 2, 3], + [4, 5, 6], + ], + elementWidth: 8, +); +final sameValues = LogicValueArray.fromFlatInts( + [2, 3], + 8, + [1, 2, 3, 4, 5, 6], +); +final emptyRows = LogicValueArray.fromFlat([2, 0], 8, const []); +final transposed = values.transpose2D(); // Dimensions: [3, 2] final signals = values.toLogicArray(name: 'values'); ``` -`LogicValueArray.putInto` drives a compatible `LogicArray` or `LogicArrayOf`, while `LogicArrayOf.logicValues` captures its current packed values. Use `LogicArrayOf.valueArrayOf` and `putValueArrayOf` when a `LogicValueCodec` converts typed value-domain data at the hardware boundary. +Nested constructors reject ragged rows, inconsistent nesting depth, and mismatched element widths. Empty nested input cannot reveal the element width or trailing dimensions, so it must use `fromFlat`. `majorSlices` iterates the outer dimension rather than the total element count, so a `[2, 0]` value contains two empty `[0]` slices and can round-trip through `stack`. + +`LogicValueArrayOf` adds a `LogicValueCodec` for application-level values. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. + +The root list of a nested constructor always represents an array dimension. Below the root, an object matching `T` is treated as one semantic value before it is considered as another list dimension. This permits list-valued semantic elements; use `fromFlat` when the intended interpretation would otherwise be ambiguous. + +Both value-array classes are `LogicValue`s. Their `width` and deprecated `length` count packed bits, while `elementCount` counts array positions. Bit indexing, equality, hashing, arithmetic, and bitwise operations use the packed value and do not consider shape. The `packed` getter exposes the ordinary `LogicValue` representation. + +`LogicArrayOf.value` captures a `LogicValueArray` without adding hardware to the graph. A compatible shaped value can drive a hardware array through `put`, `inject`, or `putInto`; ordinary same-width packed `LogicValue` assignment remains supported. ## Unpacked arrays @@ -87,11 +127,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 `addTypedInput` and `addTypedOutput` for `LogicArrayOf` ports. These methods preserve the array's specialized leaf type, allowing the module to access fields such as `samples.elementAt([1, 2]).data` directly. +Use `addTypedInput` and `addTypedOutput` for `LogicArrayOf` ports. These methods preserve the array's specialized leaf type, net kind, dimensions, and unpacked-dimension configuration, allowing the module to access fields such as `samples.at([1, 2]).data` directly. Net-typed leaves are also accepted by typed inout ports. ## 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/src/interfaces/interface.dart b/lib/src/interfaces/interface.dart index 4e23d5512d..43de17ebd1 100644 --- a/lib/src/interfaces/interface.dart +++ b/lib/src/interfaces/interface.dart @@ -93,9 +93,9 @@ class Interface { elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ), - BaseLogicArray() => module.addTypedInput( + LogicArrayOf() => module.addTypedInput( uniquify(port.name), - srcInterface.port(port.name) as BaseLogicArray, + srcInterface.port(port.name) as LogicArrayOf, ), _ => module.addInput( uniquify(port.name), @@ -115,7 +115,7 @@ class Interface { elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ), - BaseLogicArray() => module.addTypedOutput( + LogicArrayOf() => module.addTypedOutput( uniquify(port.name), port.clone, ), @@ -131,7 +131,7 @@ class Interface { if (inOutTags != null) { for (final port in getPorts(inOutTags).values) { - if (port is BaseLogicArray) { + if (port is LogicArrayOf) { if (!port.isNet) { throw PortTypeException( port, 'LogicArray nets must be used for inOut array ports.'); @@ -150,9 +150,9 @@ class Interface { elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ), - BaseLogicArray() => module.addTypedInOut( + LogicArrayOf() => module.addTypedInOut( uniquify(port.name), - srcInterface.port(port.name) as BaseLogicArray, + srcInterface.port(port.name) as LogicArrayOf, ), _ => module.addInOut( uniquify(port.name), diff --git a/lib/src/module.dart b/lib/src/module.dart index d830b0c9e6..64235cc5b8 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -769,7 +769,7 @@ abstract class Module { _inOutDrivers.add(source); // we need to properly detect all inout sources, even for arrays - if (source.isArrayMember || source is BaseLogicArray) { + if (source.isArrayMember || source is LogicArrayOf) { final sourceElems = TraverseableCollection()..add(source); for (var i = 0; i < sourceElems.length; i++) { final sei = sourceElems[i]; @@ -779,7 +779,7 @@ abstract class Module { sourceElems.add(sei.parentStructure!); } - if (sei is BaseLogicArray) { + if (sei is LogicArrayOf) { sourceElems.addAll(sei.elements); } } @@ -1054,7 +1054,7 @@ abstract class Module { sourceElems.add(sei.parentStructure!); } - if (sei is BaseLogicArray) { + if (sei is LogicArrayOf) { sourceElems.addAll(sei.elements); } } diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index 1c8e6c2e6c..8347f6da73 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -433,7 +433,7 @@ ${inputLines.join('\n')} var current = logic; while (current?.parentStructure != null) { final parentStructure = current!.parentStructure!; - if (parentStructure is BaseLogicArray && + if (parentStructure is LogicArrayOf && parentStructure.numUnpackedDimensions > 0) { return true; } diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index 6a1e82f7f3..119caff3d3 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -133,8 +133,8 @@ class Logic { LogicStructure? get parentStructure => _parentStructure; LogicStructure? _parentStructure; - /// True if this is a member of a [BaseLogicArray]. - bool get isArrayMember => parentStructure is BaseLogicArray; + /// True if this is a direct member of a [LogicArrayOf]. + bool get isArrayMember => parentStructure is LogicArrayOf; /// Returns the name relative to the [parentStructure]-defined hierarchy, if /// one exists. Otherwise, this is the same as [name]. @@ -143,7 +143,7 @@ class Logic { /// [LogicArray] or [LogicStructure]. String get structureName { if (parentStructure != null) { - if (parentStructure is BaseLogicArray) { + if (parentStructure is LogicArrayOf) { return '${parentStructure!.structureName}[${arrayIndex!}]'; } else { return '${parentStructure!.structureName}.$name'; diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index adfa3cf3e0..79be2b14ff 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -9,11 +9,8 @@ part of 'signals.dart'; -/// Shared implementation for multidimensional arrays of [Logic] values. -/// -/// Most callers should construct [LogicArray] for ordinary [Logic] leaves or -/// [LogicArrayOf] for a specialized leaf type. -class BaseLogicArray extends LogicStructure { +/// Shared private implementation for multidimensional logic arrays. +class _BaseLogicArray extends LogicStructure { /// The number of elements at each level of the array, starting from the most /// significant outermost level. /// @@ -32,9 +29,12 @@ class BaseLogicArray extends LogicStructure { /// Unlike [LogicStructure.leafElements], traversal stops at the configured /// array leaf. This distinction matters when an array leaf is itself a /// [LogicStructure], such as a typed floating-point value. - late final List arrayElements = + late final List _arrayElements = UnmodifiableListView(_calculateArrayElements()); + /// Elements reached after traversing exactly [dimensions] array levels. + List get arrayElements => _arrayElements; + @override final Naming naming; @@ -62,15 +62,16 @@ class BaseLogicArray extends LogicStructure { /// 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. - factory BaseLogicArray(List dimensions, int elementWidth, + factory _BaseLogicArray(List dimensions, int elementWidth, {String? name, int numUnpackedDimensions = 0, Naming? naming}) => - BaseLogicArray._factory(dimensions, elementWidth, + _BaseLogicArray._factory(dimensions, elementWidth, name: name, numUnpackedDimensions: numUnpackedDimensions, naming: naming, logicBuilder: Logic.new, - logicArrayBuilder: BaseLogicArray.new, - arrayBuilder: BaseLogicArray._); + logicArrayBuilder: _BaseLogicArray.new, + arrayBuilder: _BaseLogicArray._, + isNet: false); @override final bool isNet; @@ -85,25 +86,26 @@ class BaseLogicArray extends LogicStructure { /// 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. - factory BaseLogicArray.net(List dimensions, int elementWidth, + factory _BaseLogicArray.net(List dimensions, int elementWidth, {String? name, int numUnpackedDimensions = 0, Naming? naming}) => - BaseLogicArray._factory(dimensions, elementWidth, + _BaseLogicArray._factory(dimensions, elementWidth, name: name, numUnpackedDimensions: numUnpackedDimensions, naming: naming, logicBuilder: LogicNet.new, - logicArrayBuilder: BaseLogicArray.net, - arrayBuilder: BaseLogicArray._); + logicArrayBuilder: _BaseLogicArray.net, + arrayBuilder: _BaseLogicArray._, + isNet: true); /// 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 [BaseLogicArray]s matching the remaining + /// 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, + _BaseLogicArray.structured(List elements, {required List dimensions, required this.elementWidth, String? name, @@ -140,7 +142,7 @@ class BaseLogicArray extends LogicStructure { } else { final childDimensions = dimensions.sublist(1); if (elements.any((element) => - element is! BaseLogicArray || + element is! _BaseLogicArray || !_sameDimensions(element.dimensions, childDimensions) || element.elementWidth != elementWidth)) { throw LogicConstructionException( @@ -168,16 +170,17 @@ class BaseLogicArray extends LogicStructure { /// /// The [logicBuilder] and [logicArrayBuilder] functions should generate /// proper types of [Logic]s as elements for the array. - factory BaseLogicArray._factory(List dimensions, int elementWidth, + 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, + required _BaseLogicArray Function(List nextDimensions, int width, {int numUnpackedDimensions, String name}) logicArrayBuilder, - required BaseLogicArray Function(List elements, + required _BaseLogicArray Function(List elements, {required List dimensions, required int elementWidth, required int numUnpackedDimensions, @@ -230,12 +233,12 @@ class BaseLogicArray extends LogicStructure { numUnpackedDimensions: numUnpackedDimensions, name: name, naming: naming, - isNet: elements.every((element) => element.isNet)); + isNet: isNet); } @override - BaseLogicArray _clone({String? name, Naming? naming}) => - BaseLogicArray._factory(dimensions, elementWidth, + _BaseLogicArray _clone({String? name, Naming? naming}) => + _BaseLogicArray._factory(dimensions, elementWidth, name: name ?? this.name, numUnpackedDimensions: numUnpackedDimensions, naming: Naming.chooseCloneNaming( @@ -244,10 +247,11 @@ class BaseLogicArray extends LogicStructure { originalNaming: this.naming, newNaming: naming), logicBuilder: isNet ? LogicNet.new : Logic.new, - logicArrayBuilder: isNet ? BaseLogicArray.net : BaseLogicArray.new, - arrayBuilder: BaseLogicArray._); + logicArrayBuilder: isNet ? _BaseLogicArray.net : _BaseLogicArray.new, + arrayBuilder: _BaseLogicArray._, + isNet: isNet); - /// Creates a new [BaseLogicArray] which has the same [dimensions], + /// Creates a new array 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. @@ -256,7 +260,7 @@ class BaseLogicArray extends LogicStructure { /// returns the same type as itself. @override @mustBeOverridden - BaseLogicArray clone({String? name}) => _clone(name: name); + _BaseLogicArray clone({String? name}) => _clone(name: name); /// Makes a [clone] with the provided [name] and optionally [naming], then /// assigns it to be driven by `this`. @@ -265,13 +269,13 @@ class BaseLogicArray extends LogicStructure { /// construction without separately declaring a new named signal and then /// assigning. @override - BaseLogicArray named(String name, {Naming? naming}) => + _BaseLogicArray named(String name, {Naming? naming}) => _clone(name: name, naming: naming)..gets(this); - /// Private constructor for the factory [BaseLogicArray] constructor. + /// Private constructor for the factory [_BaseLogicArray] constructor. /// /// The [name] and [naming] should have been identified before calling this. - BaseLogicArray._(super.elements, + _BaseLogicArray._(super.elements, {required this.dimensions, required this.elementWidth, required this.numUnpackedDimensions, @@ -282,57 +286,16 @@ class BaseLogicArray extends LogicStructure { List _calculateArrayElements() => dimensions.length == 1 ? elements : elements - .cast() + .cast<_BaseLogicArray>() .expand((element) => element.arrayElements) .toList(growable: false); - - /// Constructs a new [BaseLogicArray] with a 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]. - factory BaseLogicArray.port(String name, - [List dimensions = const [1], - int elementWidth = 1, - int numUnpackedDimensions = 0]) { - if (!Sanitizer.isSanitary(name)) { - throw InvalidPortNameException(name); - } - - return BaseLogicArray(dimensions, elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - name: name, - - // make port names mergeable so we don't duplicate the ports - // when calling connectIO - naming: Naming.mergeable); - } - - /// Constructs a new [BaseLogicArray.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]. - factory BaseLogicArray.netPort(String name, - [List dimensions = const [1], - int elementWidth = 1, - int numUnpackedDimensions = 0]) { - if (!Sanitizer.isSanitary(name)) { - throw InvalidPortNameException(name); - } - - return BaseLogicArray.net(dimensions, elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - name: name, - - // make port names mergeable so we don't duplicate the ports - // when calling connectIO - naming: Naming.mergeable); - } } /// A multi-dimensional array structure of independent [Logic]s. /// /// This is the ordinary [Logic]-leaf specialization of [LogicArrayOf]. It has /// the same construction, port, clone, and naming API as the historical -/// `LogicArray` type. [typedLeafElements] exposes its leaves as `List`. +/// `LogicArray` type. class LogicArray extends LogicArrayOf { /// Creates an array with specified [dimensions] and [elementWidth] named /// [name]. @@ -347,7 +310,8 @@ class LogicArray extends LogicArrayOf { numUnpackedDimensions: numUnpackedDimensions, naming: naming, logicBuilder: Logic.new, - logicArrayBuilder: LogicArray.new); + logicArrayBuilder: LogicArray.new, + isNet: false); /// Creates an array of [LogicNet]s with [dimensions] and [elementWidth] /// named [name]. @@ -361,7 +325,8 @@ class LogicArray extends LogicArrayOf { numUnpackedDimensions: numUnpackedDimensions, naming: naming, logicBuilder: LogicNet.new, - logicArrayBuilder: LogicArray.net); + logicArrayBuilder: LogicArray.net, + isNet: true); LogicArray._(List elements, {required super.dimensions, @@ -369,25 +334,33 @@ class LogicArray extends LogicArrayOf { required super.numUnpackedDimensions, required super.name, required super.naming, - required super.isNet}) - : super.structured(elements, Logic.new); + required bool isNet}) + : super.structured( + elements, + ({name}) => isNet + ? LogicNet(name: name, width: elementWidth) + : Logic(name: name, width: elementWidth), + 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, + _BaseLogicArray._factory(dimensions, elementWidth, name: name, numUnpackedDimensions: numUnpackedDimensions, naming: naming, logicBuilder: logicBuilder, logicArrayBuilder: logicArrayBuilder, - arrayBuilder: LogicArray._) as LogicArray; + arrayBuilder: LogicArray._, + isNet: isNet) as LogicArray; @override @@ -403,7 +376,8 @@ class LogicArray extends LogicArrayOf { originalNaming: naming, newNaming: null), logicBuilder: isNet ? LogicNet.new : Logic.new, - logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new); + logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new, + isNet: isNet); @override @@ -418,9 +392,55 @@ class LogicArray extends LogicArrayOf { originalNaming: this.naming, newNaming: naming), logicBuilder: isNet ? LogicNet.new : Logic.new, - logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new) + logicArrayBuilder: isNet ? LogicArray.net : LogicArray.new, + isNet: isNet) ..gets(this); + @override + LogicArray getsEach(Iterable sources) { + super.getsEach(sources); + return this; + } + + @override + LogicArray getsGenerated(Logic Function(List indices) generator) { + super.getsGenerated(generator); + return this; + } + + @override + LogicArray reshape(List newDimensions, {String? name}) { + if (_arrayLength(newDimensions) != arrayElements.length) { + throw ArgumentError.value(newDimensions, 'newDimensions', + 'Must contain ${arrayElements.length} array elements.'); + } + return (isNet ? LogicArray.net : LogicArray.new)( + newDimensions, + elementWidth, + name: name, + numUnpackedDimensions: min(numUnpackedDimensions, newDimensions.length), + )..getsEach(arrayElements); + } + + @override + LogicArray transpose2D({String? name}) { + _checkArrayIsTwoDimensional(dimensions); + return (isNet ? LogicArray.net : LogicArray.new)( + [dimensions[1], dimensions[0]], + elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + )..getsGenerated((indices) => at([indices[1], indices[0]])); + } + + @override + Iterable get majorSlices { + if (dimensions.length < 2) { + throw StateError('majorSlices requires at least two dimensions.'); + } + return elements.cast(); + } + /// Creates an array port with a convenient constructor signature. /// /// The port uses mergeable naming so repeated interface connections retain a diff --git a/lib/src/signals/logic_array_of.dart b/lib/src/signals/logic_array_of.dart index f240003d47..d23b072061 100644 --- a/lib/src/signals/logic_array_of.dart +++ b/lib/src/signals/logic_array_of.dart @@ -14,18 +14,29 @@ typedef LogicArrayElementBuilder = T Function({String? name}); /// A multidimensional logic array with leaves of type [T]. /// -/// Intermediate dimensions are [BaseLogicArray]s, while the configured array -/// leaf can be any [Logic], including a [LogicStructure] such as a -/// floating-point signal. Packed conversion happens only at that leaf boundary. -class LogicArrayOf extends BaseLogicArray { +/// [elements] contains the immediate children of the outermost dimension. +/// [arrayElements] traverses the number of array levels declared by +/// [dimensions] and then stops, returning one [T] for each declared array +/// position. It does not recurse into a [LogicStructure] or nested array stored +/// as a [T]. +/// +/// 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. +class LogicArrayOf extends _BaseLogicArray { /// Labels used to name elements at each dimension. final List dimensionNames; + /// Recreates one [T] for cloning and shape transformations. final LogicArrayElementBuilder _elementBuilder; - late final List _typedLeafElements = List.unmodifiable( - arrayElements.cast(), - ); + /// Unmodifiable typed view of the configured array-element boundary. + late final List _typedArrayElements = + List.unmodifiable(super.arrayElements.cast()); + + @override + List get arrayElements => _typedArrayElements; /// Creates an array with [dimensions] and typed leaves from [elementBuilder]. /// @@ -59,7 +70,8 @@ class LogicArrayOf extends BaseLogicArray { dimensions: build.dimensions, elementWidth: build.elementWidth, numUnpackedDimensions: numUnpackedDimensions, - naming: naming); + naming: naming, + isNet: build.isNet); /// Creates a typed array from structurally compatible [elements]. @protected @@ -72,22 +84,88 @@ class LogicArrayOf extends BaseLogicArray { required String name, required Naming naming, required super.isNet, - }) : dimensionNames = List.unmodifiable( - List.generate(dimensions.length, (index) => 'd${index}_')), + List? dimensionNames, + }) : dimensionNames = + _normalizeLogicArrayDimensionNames(dimensions, dimensionNames), super.structured( name: name, naming: naming, ); - /// Typed leaves in row-major order. - List get typedLeafElements => UnmodifiableListView(_typedLeafElements); + /// 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; + } + + /// Connects row-major [arrayElements] to [sources]. + LogicArrayOf getsEach(Iterable sources) { + for (final (target, source) in _zipExact(arrayElements, sources)) { + target <= source; + } + // ignore: avoid_returning_this - preserves the fluent array receiver type. + return this; + } + + /// Connects each array element to a value generated from its indices. + LogicArrayOf getsGenerated(Logic Function(List indices) generator) { + for (final (indices, target) in indexedElements) { + target <= generator(indices); + } + // ignore: avoid_returning_this - preserves the fluent array receiver type. + return this; + } + + /// Returns a typed row-major view with [newDimensions]. + LogicArrayOf reshape(List newDimensions, {String? name}) { + if (_arrayLength(newDimensions) != arrayElements.length) { + throw ArgumentError.value(newDimensions, 'newDimensions', + 'Must contain ${arrayElements.length} array elements.'); + } + return LogicArrayOf(newDimensions, _elementBuilder, + dimensionNames: newDimensions.length == dimensions.length + ? dimensionNames + : _defaultDimensionNames(newDimensions.length), + name: name, + numUnpackedDimensions: min(numUnpackedDimensions, newDimensions.length)) + ..getsEach(arrayElements); + } - /// Typed leaves paired with their multidimensional indices. - Iterable<(List, T)> get indexedElements => - indexedLeaves.map((entry) => (entry.$1, entry.$2 as T)); + /// Transposes this two-dimensional array while preserving [T]. + LogicArrayOf transpose2D({String? name}) { + _checkArrayIsTwoDimensional(dimensions); + return LogicArrayOf([dimensions[1], dimensions[0]], _elementBuilder, + dimensionNames: [dimensionNames[1], dimensionNames[0]], + name: name, + numUnpackedDimensions: numUnpackedDimensions) + ..getsGenerated((indices) => at([indices[1], indices[0]])); + } - /// Returns the typed leaf at multidimensional [indices]. - T elementAt(List indices) => at(indices) 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>(); + } /// Flattens all nested array dimensions into one typed array of [U] leaves. /// @@ -95,78 +173,123 @@ class LogicArrayOf extends BaseLogicArray { /// same dimensions and element width. The returned dimensions concatenate /// every nested layer, preserving row-major ordering and index addresses. LogicArrayOf flattenNestedDimensions({String? name}) { - var leaves = typedLeafElements.cast().toList(growable: false); + var leaves = arrayElements.cast().toList(growable: false); final flattenedDimensions = [...dimensions]; var flattenedUnpackedDimensions = numUnpackedDimensions; - - while (leaves.any((leaf) => leaf is BaseLogicArray)) { - if (leaves.any((leaf) => leaf is! BaseLogicArray)) { + LogicArrayElementBuilder flattenedElementBuilder = _elementBuilder; + var prototype = leaves.isEmpty + ? flattenedElementBuilder(name: 'flatten_prototype') + : leaves.first; + + while (prototype is LogicArrayOf) { + if (leaves.isNotEmpty && + leaves.any((leaf) => leaf is! LogicArrayOf)) { throw LogicConstructionException( 'Nested array leaves must have a uniform depth.'); } - final arrays = leaves.cast(); - final reference = arrays.first; + final arrays = leaves.cast>(); + final reference = arrays.isEmpty ? prototype : arrays.first; if (arrays.any((array) => !_sameDimensions(array.dimensions, reference.dimensions) || - array.elementWidth != reference.elementWidth)) { + array.elementWidth != reference.elementWidth || + array.numUnpackedDimensions != reference.numUnpackedDimensions)) { + throw LogicConstructionException( + 'Nested array leaves must have matching dimensions, widths, and ' + 'unpacked dimensions.'); + } + if (reference.numUnpackedDimensions > 0 && + flattenedUnpackedDimensions != flattenedDimensions.length) { throw LogicConstructionException( - 'Nested array leaves must have matching dimensions and widths.'); + 'Cannot flatten unpacked inner dimensions after packed outer ' + 'dimensions.'); } flattenedDimensions.addAll(reference.dimensions); flattenedUnpackedDimensions += reference.numUnpackedDimensions; leaves = arrays.expand((array) => array.arrayElements).toList(growable: false); + flattenedElementBuilder = reference._elementBuilder; + prototype = leaves.isEmpty + ? flattenedElementBuilder(name: 'flatten_prototype') + : leaves.first; } - if (leaves.any((leaf) => leaf is! U)) { + if (prototype is! U || leaves.any((leaf) => leaf is! U)) { throw LogicConstructionException( 'Nested array leaves must have type $U.'); } - final prototype = leaves.first as U; if (leaves.any((leaf) => leaf.width != prototype.width)) { throw LogicConstructionException( 'Nested array leaves must have matching widths.'); } + var returnPrototype = leaves.isEmpty; + U buildFlattenedElement({String? name}) { + final element = + returnPrototype ? prototype : flattenedElementBuilder(name: name); + returnPrototype = false; + if (element is! U) { + throw LogicConstructionException( + 'Nested array element builder must produce type $U.'); + } + return element; + } + return LogicArrayOf( flattenedDimensions, - ({name}) => prototype.clone(name: name) as U, + buildFlattenedElement, name: name, numUnpackedDimensions: flattenedUnpackedDimensions, )..getsEach(leaves); } - /// Current packed leaves in the value domain. - LogicValueArray get logicValues => LogicValueArray.fromLogicArray(this); - - /// Decodes current packed leaves into semantic values using [codec]. - LogicValueArrayOf valueArrayOf(LogicValueCodec codec) => - LogicValueArrayOf.fromLogicValues(logicValues, codec: codec); + @override + LogicValueArray get value => LogicValueArray.fromLogicArray(this); - /// Drives typed leaves from [values]. - void putLogicValues(LogicValueArray values) => values.putInto(this); + @override + LogicValueArray? get previousValue => + arrayElements.any((element) => element.previousValue == null) + ? null + : LogicValueArray.fromFlat( + dimensions, + elementWidth, + arrayElements.map((element) => element.previousValue!), + ); - /// Drives typed logic leaves from semantic [values]. - void putValueArrayOf(LogicValueArrayOf values) => - putLogicValues(values.logicValues); + @override + void put(dynamic val, {bool fill = false}) { + if (val is LogicValueArrayOf) { + if (fill) { + throw ArgumentError.value( + fill, 'fill', 'Cannot fill an array from a shaped value.'); + } + val.putInto(this); + return; + } + super.put(val, fill: fill); + } /// Packs typed leaves into a conventional [LogicArray]. LogicArray toLogicArray({String? name}) => - LogicArray(dimensions, elementWidth, name: name) - ..getsEach(_typedLeafElements.map((element) => element.packed)); + (isNet ? LogicArray.net : LogicArray.new)( + dimensions, + elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + )..getsEach(arrayElements.map((element) => element.packed)); /// Drives typed leaves from a packed [LogicArray]. void getsPackedValues(LogicArray packedValues) { _validateShape(packedValues.dimensions, packedValues.elementWidth); for (final (target, source) - in _typedLeafElements.zipExact(packedValues.arrayElements)) { + in _zipExact(arrayElements, packedValues.arrayElements)) { target <= source; } } + /// Verifies that another array shape can be applied element by element. void _validateShape(List dimensions, int elementWidth) { if (!_sameDimensions(this.dimensions, dimensions) || this.elementWidth != elementWidth) { @@ -193,7 +316,12 @@ class LogicArrayOf extends BaseLogicArray { @override LogicArrayOf _clone({String? name, Naming? naming}) => createClone( name: name, - naming: naming, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: this.naming, + newNaming: naming, + ), numUnpackedDimensions: numUnpackedDimensions, ); @@ -212,23 +340,38 @@ class LogicArrayOf extends BaseLogicArray { )..gets(this); } +/// Validated construction data prepared before initializing [LogicArrayOf]. class _LogicArrayOfBuild { + /// 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; - _LogicArrayOfBuild._( - this.dimensions, this.dimensionNames, this.elements, this.elementWidth); + /// Whether every typed array element is a net. + final bool isNet; + + /// Stores construction data after all validation is complete. + _LogicArrayOfBuild._(this.dimensions, this.dimensionNames, this.elements, + this.elementWidth, this.isNet); + /// Builds and validates the complete typed-array hierarchy. factory _LogicArrayOfBuild.build( List dimensions, LogicArrayElementBuilder elementBuilder, - {List? dimensionNames, int numUnpackedDimensions = 0}) { + {List? dimensionNames, + int numUnpackedDimensions = 0, + T? emptyPrototype}) { final normalizedDimensions = List.unmodifiable(dimensions); if (normalizedDimensions.isEmpty || - normalizedDimensions.any((dimension) => dimension <= 0)) { + normalizedDimensions.any((dimension) => dimension < 0)) { throw LogicConstructionException( - 'LogicArrayOf dimensions must all be positive.'); + 'LogicArrayOf dimensions must be non-negative.'); } if (numUnpackedDimensions < 0 || numUnpackedDimensions > normalizedDimensions.length) { @@ -237,68 +380,148 @@ class _LogicArrayOfBuild { 'dimensions.'); } - final normalizedNames = List.unmodifiable( - dimensionNames ?? - Iterable.generate( - normalizedDimensions.length, (dimension) => 'd${dimension}_'), - ); - if (normalizedNames.length != normalizedDimensions.length) { - throw LogicConstructionException( - 'dimensionNames must match the number of dimensions.'); - } - if (normalizedNames.any((name) => !Sanitizer.isSanitary(name)) || - normalizedNames.toSet().length != normalizedNames.length) { - throw LogicConstructionException( - 'dimensionNames must be sanitary and unique.'); - } + 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) - : LogicArrayOf(normalizedDimensions.sublist(1), elementBuilder, - dimensionNames: normalizedNames.sublist(1), + : LogicArrayOf._( + _LogicArrayOfBuild.build( + normalizedDimensions.sublist(1), + elementBuilder, + dimensionNames: normalizedNames.sublist(1), + numUnpackedDimensions: max(0, numUnpackedDimensions - 1), + emptyPrototype: emptyPrototype, + ), + elementBuilder, name: elementName, - numUnpackedDimensions: (numUnpackedDimensions - 1) - .clamp(0, normalizedDimensions.length)); + numUnpackedDimensions: max(0, numUnpackedDimensions - 1), + ); }, growable: false); final typedLeaves = normalizedDimensions.length == 1 ? elements.cast().toList(growable: false) : elements .cast>() - .expand((element) => element.typedLeafElements) + .expand((element) => element.arrayElements) .toList(growable: false); if (typedLeaves.any((leaf) => leaf is LogicStructure && - leaf is! BaseLogicArray && + leaf is! LogicArrayOf && _containsNestedArray(leaf))) { throw LogicConstructionException( - 'LogicArrayOf leaves cannot contain nested BaseLogicArray fields.'); + 'LogicArrayOf leaves cannot contain nested LogicArrayOf fields.'); } if (typedLeaves.any(_containsUnassignableLeaf)) { throw LogicConstructionException( 'LogicArrayOf leaves must be driveable and cannot contain Consts.'); } - return _LogicArrayOfBuild._(normalizedDimensions, normalizedNames, elements, - _validateElementWidths(typedLeaves)); + final prototype = typedLeaves.isEmpty ? emptyPrototype! : typedLeaves.first; + if (_containsUnassignableLeaf(prototype)) { + throw LogicConstructionException( + 'LogicArrayOf leaves must be driveable and cannot contain Consts.'); + } + if (prototype is LogicStructure && + prototype is! LogicArrayOf && + _containsNestedArray(prototype)) { + throw LogicConstructionException( + 'LogicArrayOf leaves cannot contain nested LogicArrayOf fields.'); + } + return _LogicArrayOfBuild._( + normalizedDimensions, + normalizedNames, + elements, + _validateElementWidths(typedLeaves, prototype.width), + typedLeaves.isEmpty + ? prototype.isNet + : typedLeaves.every((element) => element.isNet)); } + /// Whether [structure] recursively contains an array field. static bool _containsNestedArray(LogicStructure structure) => structure.elements.any((element) => - element is BaseLogicArray || + element is LogicArrayOf || (element is LogicStructure && _containsNestedArray(element))); + /// Whether [leaf] is or recursively contains an unassignable constant. static bool _containsUnassignableLeaf(Logic leaf) => leaf is Const || (leaf is LogicStructure && leaf.leafElements.any((element) => element is Const)); - static int _validateElementWidths(List elements) { - final width = elements.first.width; - if (elements.any((element) => element.width != width)) { + /// Validates that [elements] all have [expectedWidth]. + static int _validateElementWidths( + List elements, int expectedWidth) { + if (elements.any((element) => element.width != expectedWidth)) { throw LogicConstructionException( 'All LogicArrayOf leaves must have the same width.'); } - return width; + return expectedWidth; + } +} + +/// Pairs [leftValues] and [rightValues], rejecting unequal lengths. +Iterable<(T, U)> _zipExact( + Iterable leftValues, Iterable rightValues) sync* { + final left = leftValues.iterator; + final right = rightValues.iterator; + while (true) { + final hasLeft = left.moveNext(); + final hasRight = right.moveNext(); + if (hasLeft != hasRight) { + throw StateError('Cannot zip iterables of different lengths.'); + } + if (!hasLeft) { + return; + } + yield (left.current, right.current); + } +} + +/// 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; +} + +/// Rejects shapes that cannot be transposed by [LogicArrayOf.transpose2D]. +void _checkArrayIsTwoDimensional(List dimensions) { + if (dimensions.length != 2) { + throw StateError('Expected exactly two dimensions, got $dimensions.'); } } diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index 86d18e41e7..a453cfa147 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 BaseLogicArray; + bool get isArrayMember => parentStructure is LogicArrayOf; @override void put(dynamic val, {bool fill = false}) { @@ -184,7 +184,7 @@ class LogicStructure implements Logic { LogicStructure flattenOuter({String? name, bool prefixFieldNames = true}) { final sources = <(Logic, String)>[]; for (final element in elements) { - if (element is LogicStructure && element is! BaseLogicArray) { + if (element is LogicStructure && element is! LogicArrayOf) { for (final field in element.elements) { final prefix = element.name.endsWith('_') ? element.name : '${element.name}_'; diff --git a/lib/src/signals/logic_value_array.dart b/lib/src/signals/logic_value_array.dart deleted file mode 100644 index 9aaf36b1f4..0000000000 --- a/lib/src/signals/logic_value_array.dart +++ /dev/null @@ -1,327 +0,0 @@ -// 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 'signals.dart'; - -/// Value-domain counterpart to [LogicArray]. -/// -/// Stores fixed-width [LogicValue] leaves with [dimensions] matching the shape -/// used by a [LogicArray]. Values are kept in the same row-major leaf order -/// used by [LogicArray.arrayElements]. -class LogicValueArray { - /// The number of elements at each array level. - final List dimensions; - - /// Width of each leaf value. - final int elementWidth; - - final List _values; - - /// Creates a value array from row-major [values]. - LogicValueArray( - List dimensions, - this.elementWidth, - Iterable values, - ) : dimensions = List.unmodifiable(dimensions), - _values = List.unmodifiable(values) { - if (dimensions.isEmpty) { - throw ArgumentError.value(dimensions, 'dimensions', 'Must not be empty.'); - } - if (dimensions.any((dimension) => dimension < 0)) { - throw ArgumentError.value( - dimensions, 'dimensions', 'Dimensions must be non-negative.'); - } - if (elementWidth < 0) { - throw ArgumentError.value( - elementWidth, 'elementWidth', 'Must be non-negative.'); - } - if (_values.length != length) { - throw ArgumentError.value( - _values.length, 'values', 'Must contain exactly $length values.'); - } - for (final value in _values) { - if (value.width != elementWidth) { - throw ArgumentError.value( - value, 'values', 'All values must have width $elementWidth.'); - } - } - } - - /// Creates an empty zero-width value array. - LogicValueArray.empty() - : dimensions = const [0], - elementWidth = 0, - _values = const []; - - /// Generates values from row-major multidimensional indices. - factory LogicValueArray.generate(List dimensions, int elementWidth, - LogicValue Function(List indices) generator) { - final length = _lengthFor(dimensions); - return LogicValueArray(dimensions, elementWidth, [ - for (var index = 0; index < length; index++) - generator(_indicesFor(dimensions, index)) - ]); - } - - /// Creates a value array from integer values. - factory LogicValueArray.fromInts( - List dimensions, int elementWidth, Iterable values) => - LogicValueArray(dimensions, elementWidth, - values.map((value) => LogicValue.ofInt(value, elementWidth))); - - /// Captures the current values of a [BaseLogicArray]. - factory LogicValueArray.fromLogicArray(BaseLogicArray values) => - LogicValueArray(values.dimensions, values.elementWidth, - values.arrayElements.map((element) => element.value)); - - /// 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; - for (final slice in slices.skip(1)) { - first._checkCompatible(slice.dimensions, slice.elementWidth); - } - return LogicValueArray([slices.length, ...first.dimensions], - first.elementWidth, slices.expand((slice) => slice.flatValues)); - } - - /// Number of leaf values. - int get length => _lengthFor(dimensions); - - /// Row-major leaf values. - List get flatValues => UnmodifiableListView(_values); - - /// Row-major values paired with their multidimensional indices. - Iterable<(List, LogicValue)> get indexedValues => Iterable.generate( - length, (index) => (_indicesFor(dimensions, index), _values[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 sliceLength = _lengthFor(sliceDimensions); - for (var start = 0; start < length; start += sliceLength) { - yield LogicValueArray(sliceDimensions, elementWidth, - _values.getRange(start, start + sliceLength)); - } - } - - /// Returns the value at multidimensional [indices]. - LogicValue at(List indices) => _values[_flatIndex(indices)]; - - /// Returns the row-major flat index for multidimensional [indices]. - int flatIndexOf(List indices) => _flatIndex(indices); - - /// Maps this array while preserving dimensions and element width. - LogicValueArray map(LogicValue Function(LogicValue value) transform) => - LogicValueArray(dimensions, elementWidth, _values.map(transform)); - - /// Maps this array with row-major multidimensional indices. - LogicValueArray indexedMap( - LogicValue Function(List indices, LogicValue value) transform, - ) => - LogicValueArray(dimensions, elementWidth, - indexedValues.map((entry) => transform(entry.$1, entry.$2))); - - /// Maps slices along the first dimension and stacks the results. - LogicValueArray mapMajorSlices( - LogicValueArray Function(LogicValueArray slice) transform, - ) => - LogicValueArray.stack(majorSlices.map(transform)); - - /// Returns a row-major view with new [dimensions]. - LogicValueArray reshape(List dimensions) { - if (_lengthFor(dimensions) != length) { - throw ArgumentError.value( - dimensions, 'dimensions', 'Must contain $length values.'); - } - return LogicValueArray(dimensions, elementWidth, _values); - } - - /// Transposes a two-dimensional value array. - LogicValueArray transpose2D() { - _checkTwoDimensional(dimensions); - return LogicValueArray.generate([dimensions[1], dimensions[0]], - elementWidth, (indices) => at([indices[1], indices[0]])); - } - - /// Creates a [LogicArray] with the same shape and drives it with this value. - LogicArray toLogicArray({String? name}) => - putInto(LogicArray(dimensions, elementWidth, name: name)); - - /// Drives [target] with this value array. - T putInto(T target) { - _checkCompatible(target.dimensions, target.elementWidth); - for (var index = 0; index < _values.length; index++) { - target.arrayElements[index].put(_values[index]); - } - return target; - } - - void _checkCompatible(List otherDimensions, int otherElementWidth) { - if (!_sameDimensions(dimensions, otherDimensions) || - elementWidth != otherElementWidth) { - throw ArgumentError.value(otherDimensions, 'target', - 'Must have dimensions $dimensions and elementWidth $elementWidth.'); - } - } - - int _flatIndex(List indices) { - if (indices.length != dimensions.length) { - throw RangeError.range(indices.length, dimensions.length, - dimensions.length, 'indices.length'); - } - - var index = 0; - for (var dimension = 0; dimension < dimensions.length; dimension++) { - final indexAtDimension = indices[dimension]; - if (indexAtDimension < 0 || indexAtDimension >= dimensions[dimension]) { - throw RangeError.range(indexAtDimension, 0, dimensions[dimension] - 1, - 'indices[$dimension]'); - } - index = index * dimensions[dimension] + indexAtDimension; - } - return index; - } - - static int _lengthFor(List dimensions) => - dimensions.fold(1, (length, dimension) => length * dimension); - - static List _indicesFor(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; - } - - static bool _sameDimensions(List left, List right) => - left.length == right.length && - left.indexed.every((entry) => entry.$2 == right[entry.$1]); -} - -/// Functional traversal helpers for [BaseLogicArray]. -extension LogicArrayTraversal on BaseLogicArray { - /// Row-major leaves paired with their multidimensional indices. - Iterable<(List, Logic)> get indexedLeaves => Iterable.generate( - arrayElements.length, - (index) => ( - LogicValueArray._indicesFor(dimensions, index), - arrayElements[index] - ), - ); - - /// Returns the leaf at multidimensional [indices]. - Logic 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; - } - - /// Connects row-major leaves to [sources], requiring equal lengths. - BaseLogicArray getsEach(Iterable sources) { - for (final (target, source) in arrayElements.zipExact(sources)) { - target <= source; - } - return this; - } - - /// Connects each leaf to a value generated from its array indices. - BaseLogicArray getsGenerated(Logic Function(List indices) generator) { - for (final (indices, target) in indexedLeaves) { - target <= generator(indices); - } - return this; - } - - /// Returns a row-major view with new [dimensions]. - BaseLogicArray reshape(List dimensions, {String? name}) { - if (LogicValueArray._lengthFor(dimensions) != arrayElements.length) { - throw ArgumentError.value(dimensions, 'dimensions', - 'Must contain ${arrayElements.length} leaves.'); - } - return LogicArray(dimensions, elementWidth, name: name) - ..getsEach(arrayElements); - } - - /// Transposes a two-dimensional logic array. - BaseLogicArray transpose2D({String? name}) { - _checkTwoDimensional(dimensions); - return LogicArray([dimensions[1], dimensions[0]], elementWidth, name: name) - ..getsGenerated((indices) => at([indices[1], indices[0]])); - } - - /// Immediate child arrays along the first dimension. - Iterable get majorSlices { - if (dimensions.length < 2) { - throw StateError('majorSlices requires at least two dimensions.'); - } - return elements.cast(); - } -} - -/// Exact pairwise iteration for functional array wiring. -extension ExactZip on Iterable { - /// Zips this iterable with [other], throwing when lengths differ. - Iterable<(T, U)> zipExact(Iterable other) sync* { - final left = iterator; - final right = other.iterator; - while (true) { - final hasLeft = left.moveNext(); - final hasRight = right.moveNext(); - if (hasLeft != hasRight) { - throw StateError('Cannot zip iterables of different lengths.'); - } - if (!hasLeft) { - return; - } - yield (left.current, right.current); - } - } -} - -/// Splits an iterable of pairs into two lists. -extension Unzip on Iterable<(T, U)> { - /// Unzips pairs while preserving iteration order. - (List, List) unzip() { - final left = []; - final right = []; - for (final (first, second) in this) { - left.add(first); - right.add(second); - } - return (left, right); - } -} - -void _checkTwoDimensional(List dimensions) { - if (dimensions.length != 2) { - throw StateError('Expected exactly two dimensions, got $dimensions.'); - } -} diff --git a/lib/src/signals/logic_value_array_of.dart b/lib/src/signals/logic_value_array_of.dart deleted file mode 100644 index 1fe85060f2..0000000000 --- a/lib/src/signals/logic_value_array_of.dart +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (C) 2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// logic_value_array_of.dart -// Definition of typed multi-dimensional logic value arrays. -// -// 2026 July 21 -// Author: Desmond A. Kirkpatrick - -part of 'signals.dart'; - -/// 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; - - final Function _encode; - - /// Converts a semantic value into its packed representation. - LogicValue encode(T value) => - (_encode as LogicValue Function(T value))(value); - - /// Creates a bidirectional value [decode]/[encode] codec. - const LogicValueCodec( - {required this.decode, required LogicValue Function(T value) encode}) - : _encode = encode; -} - -/// A multidimensional array of semantic values backed by [LogicValue]s. -class LogicValueArrayOf { - final LogicValueArray _logicValues; - final List _values; - - /// Codec used at the packed logic boundary. - final LogicValueCodec codec; - - /// Creates a typed value array from row-major [values]. - factory LogicValueArrayOf( - List dimensions, - int elementWidth, - Iterable values, { - required LogicValueCodec codec, - }) { - final typedValues = List.unmodifiable(values); - return LogicValueArrayOf._( - LogicValueArray( - dimensions, elementWidth, typedValues.map(codec.encode)), - typedValues, - codec); - } - - /// Decodes a packed [LogicValueArray]. - factory LogicValueArrayOf.fromLogicValues(LogicValueArray values, - {required LogicValueCodec codec}) => - LogicValueArrayOf._(values, - List.unmodifiable(values.flatValues.map(codec.decode)), codec); - - /// Stacks equally shaped typed arrays along a new outer dimension. - factory LogicValueArrayOf.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; - return LogicValueArrayOf( - [slices.length, ...first.dimensions], first.elementWidth, - slices.expand((slice) { - first._checkCompatible(slice); - return slice.flatValues; - }), codec: first.codec); - } - - LogicValueArrayOf._(this._logicValues, this._values, this.codec); - - /// Number of elements at each array level. - List get dimensions => _logicValues.dimensions; - - /// Width of each packed leaf. - int get elementWidth => _logicValues.elementWidth; - - /// Number of typed leaves. - int get length => _logicValues.length; - - /// Typed leaves in row-major order. - List get flatValues => UnmodifiableListView(_values); - - /// Packed value-domain representation. - LogicValueArray get logicValues => _logicValues; - - /// Typed values paired with their multidimensional indices. - Iterable<(List, T)> get indexedValues => _logicValues.indexedValues - .zipExact(_values) - .map((entry) => (entry.$1.$1, entry.$2)); - - /// Slices along the first dimension. - Iterable> get majorSlices => - _logicValues.majorSlices.map(_fromLogicValues); - - /// Returns the typed value at multidimensional [indices]. - T at(List indices) => _values[_logicValues.flatIndexOf(indices)]; - - /// Maps typed values while preserving shape and codec. - LogicValueArrayOf map(T Function(T value) transform) => - LogicValueArrayOf(dimensions, elementWidth, _values.map(transform), - codec: codec); - - /// Maps typed values with their multidimensional indices. - LogicValueArrayOf indexedMap( - T Function(List indices, T value) transform, - ) => - LogicValueArrayOf(dimensions, elementWidth, - indexedValues.map((entry) => transform(entry.$1, entry.$2)), - codec: codec); - - /// Maps slices along the first dimension and stacks the results. - LogicValueArrayOf mapMajorSlices( - LogicValueArrayOf Function(LogicValueArrayOf slice) transform, - ) => - LogicValueArrayOf.stack(majorSlices.map(transform)); - - /// Returns a row-major view with new [dimensions]. - LogicValueArrayOf reshape(List dimensions) => - LogicValueArrayOf.fromLogicValues(_logicValues.reshape(dimensions), - codec: codec); - - /// Transposes a two-dimensional typed value array. - LogicValueArrayOf transpose2D() => - LogicValueArrayOf.fromLogicValues(_logicValues.transpose2D(), - codec: codec); - - /// Creates a [LogicArray] driven by the packed values. - LogicArray toLogicArray({String? name}) => - _logicValues.toLogicArray(name: name); - - /// Drives [target] with the packed values. - U putInto(U target) => _logicValues.putInto(target); - - LogicValueArrayOf _fromLogicValues(LogicValueArray values) => - LogicValueArrayOf.fromLogicValues(values, codec: codec); - - void _checkCompatible(LogicValueArrayOf other) { - if (elementWidth != other.elementWidth || - !_sameDimensions(dimensions, other.dimensions)) { - throw ArgumentError.value( - other.dimensions, - 'arrays', - 'All arrays must have dimensions $dimensions and ' - 'elementWidth $elementWidth.'); - } - } -} diff --git a/lib/src/signals/signals.dart b/lib/src/signals/signals.dart index 93e1d5e75b..184fcdc9a3 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -22,8 +22,6 @@ part 'wire_net.dart'; part 'logic_structure.dart'; part 'logic_array.dart'; part 'logic_array_of.dart'; -part 'logic_value_array.dart'; -part 'logic_value_array_of.dart'; part 'logic_net.dart'; part 'logic_enum.dart'; part 'logic_def.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index ee5469f137..ae40fb87f5 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -131,7 +131,7 @@ class NetlistModuleTranslation { final portLogic = modulePorts[portName]; final emitOutputArrayConcat = direction == NetlistPortDirection.output && - portLogic is BaseLogicArray && + portLogic is LogicArrayOf && !_hasExistingOutputArrayConcat(synthLogic) && !_hasDirectSubmoduleOutputDriver(synthLogic); final originalIds = getIds(synthLogic); @@ -168,7 +168,7 @@ class NetlistModuleTranslation { /// Emits a concat cell that assembles a LogicArray output port. void _emitOutputArrayConcat( String portName, - BaseLogicArray array, + LogicArrayOf array, List outputIds, ) { _emitOutputArrayConcatForArray(portName, array, outputIds); @@ -177,7 +177,7 @@ class NetlistModuleTranslation { /// Recursively emits concat cells for nested LogicArray output elements. bool _emitOutputArrayConcatForArray( String concatName, - BaseLogicArray array, + LogicArrayOf array, List outputIds, ) { final definition = synthDef; @@ -195,7 +195,7 @@ class NetlistModuleTranslation { return false; } var elementIds = getIds(synthLogic); - if (element is BaseLogicArray && + if (element is LogicArrayOf && !_hasExistingOutputArrayConcat(synthLogic) && !_hasDirectSubmoduleOutputDriver(synthLogic)) { final aggregateIds = List.generate( @@ -210,6 +210,12 @@ class NetlistModuleTranslation { return false; } elementIds = aggregateIds; + } else if (element is LogicStructure) { + _emitOutputArrayStructurePack( + '${concatName}_$index', + element, + elementIds, + ); } final upperIndex = lowerIndex + elementIds.length - 1; concatConnections['[$upperIndex:$lowerIndex]'] = @@ -244,6 +250,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 +436,7 @@ class NetlistModuleTranslation { for (final portEntry in submodule.inputs.entries) { final portName = portEntry.key; final port = portEntry.value; - if (port is! BaseLogicArray || + if (port is! LogicArrayOf || 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 0b7e0afa0e..a5bcd25e8b 100644 --- a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -19,14 +19,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 +39,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(BaseLogicArray array) { + void addNestedArrays(LogicArrayOf array) { for (final element in array.elements) { - if (element is BaseLogicArray) { + if (element is LogicArrayOf) { excludedArrays.add(element); addNestedArrays(element); } } } - { + >{ ...portArrays, ...subModuleOutputArrays, }.forEach(addNestedArrays); @@ -68,7 +68,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 +81,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { } /// Adds slice cells that decompose a LogicArray port into element signals. - void _subsetReceiveArrayPort(BaseLogicArray port) { + void _subsetReceiveArrayPort(LogicArrayOf port) { final portSynth = getSynthLogic(port)!; var index = 0; @@ -101,12 +101,16 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { ..setInputMapping(subsetModule.original.name, portSynth) ..pickName(module); + if (element is LogicArrayOf) { + _subsetReceiveArrayPort(element); + } + index += element.width; } } /// Adds a concat cell that assembles independent LogicArray element signals. - void _concatAssembleArray(BaseLogicArray array) { + void _concatAssembleArray(LogicArrayOf 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 49490abdcd..732030325f 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -164,7 +164,8 @@ class NetlistSynthesizer extends Synthesizer { void aliasArrayChildren(SynthLogic src, SynthLogic dst) { final srcLogic = src.logics.firstOrNull; final dstLogic = dst.logics.firstOrNull; - if (srcLogic is! BaseLogicArray || dstLogic is! BaseLogicArray) { + if (srcLogic is! LogicArrayOf || + dstLogic is! LogicArrayOf) { return; } if (srcLogic.elements.length != dstLogic.elements.length) { @@ -181,8 +182,8 @@ class NetlistSynthesizer extends Synthesizer { final srcElementLogic = srcElementSynth.logics.firstOrNull; final dstElementLogic = dstElementSynth.logics.firstOrNull; - if (srcElementLogic is BaseLogicArray && - dstElementLogic is BaseLogicArray) { + if (srcElementLogic is LogicArrayOf && + dstElementLogic is LogicArrayOf) { aliasArrayChildren(srcElementSynth, dstElementSynth); } @@ -232,12 +233,26 @@ 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! BaseLogicArray) { + if (elem is LogicStructure && elem is! LogicArrayOf) { addStructAndDescendants(elem, set); } } } + void addArrayStructureElements(LogicArrayOf array) { + for (final element in array.elements) { + if (element is LogicArrayOf) { + 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); @@ -252,7 +267,7 @@ class NetlistSynthesizer extends Synthesizer { // current module.) final isSubModuleInputStructPort = !isCurrentModuleOutputPort && pa.dst.logics - .any((l) => l is LogicStructure && l is! BaseLogicArray); + .any((l) => l is LogicStructure && l is! LogicArrayOf); if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { // Record as pending compose cell instead of aliasing. @@ -267,7 +282,7 @@ 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! BaseLogicArray) { + if (l is LogicStructure && l is! LogicArrayOf) { addStructAndDescendants(l, outputStructPortLogics); } } @@ -341,7 +356,7 @@ class NetlistSynthesizer extends Synthesizer { ) .map((e) => e.key) .firstOrNull; - if (logic != null && logic is BaseLogicArray) { + if (logic != null && logic is LogicArrayOf) { arraysWithExplicitCells.add(logic); } // Also check the resolved replacement chain. @@ -350,7 +365,7 @@ class NetlistSynthesizer extends Synthesizer { .where((e) => e.value == resolved) .map((e) => e.key) .firstOrNull; - if (logic2 != null && logic2 is BaseLogicArray) { + if (logic2 != null && logic2 is LogicArrayOf) { arraysWithExplicitCells.add(logic2); } } @@ -364,7 +379,7 @@ class NetlistSynthesizer extends Synthesizer { ) .map((e) => e.key) .firstOrNull; - if (logic != null && logic is BaseLogicArray) { + if (logic != null && logic is LogicArrayOf) { arraysWithExplicitCells.add(logic); } } @@ -379,7 +394,7 @@ class NetlistSynthesizer extends Synthesizer { final parentSL = entry.value; final parentIds = getIds(parentSL); - if (logic is BaseLogicArray) { + if (logic is LogicArrayOf) { // Skip aliasing for arrays that have explicit $slice/$concat cells. if (arraysWithExplicitCells.contains(logic)) { continue; @@ -428,7 +443,7 @@ class NetlistSynthesizer extends Synthesizer { fullParentIds: parentIds, )); } - } else if (elem is LogicStructure && elem is! BaseLogicArray) { + } else if (elem is LogicStructure && elem is! LogicArrayOf) { // Nested InterfaceStructure: the intermediate struct // itself has no SynthLogic, but its leaf elements do // (created by _subsetReceiveStructPort). Walk leaf @@ -488,6 +503,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) { @@ -526,12 +542,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) @@ -560,18 +581,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') { @@ -580,7 +594,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; @@ -590,6 +607,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 @@ -646,7 +671,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 diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 51512dd692..99ff22b637 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -424,7 +424,7 @@ abstract class NetlistUtils { Logic logic, [ List? bits, ]) { - if (logic is BaseLogicArray) { + if (logic is LogicArrayOf) { final result = { 'width': logic.width, 'arrayDims': logic.dimensions, @@ -434,9 +434,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! BaseLogicArray) { + if (first is LogicStructure && first is! LogicArrayOf) { result['elementType'] = buildLogicType(first); - } else if (first is BaseLogicArray) { + } else if (first is LogicArrayOf) { // Nested array — encode inner dimensions via recursive call. result['elementType'] = buildLogicType(first); } @@ -447,13 +447,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! BaseLogicArray) { + if (e is LogicStructure && e is! LogicArrayOf) { return { 'name': e.name, if (fieldBits != null) 'bits': fieldBits, 'type': buildLogicType(e, fieldBits), }; - } else if (e is BaseLogicArray) { + } else if (e is LogicArrayOf) { return { 'name': e.name, 'width': e.width, @@ -483,7 +483,7 @@ abstract class NetlistUtils { final logics = sl.logics; return logics .whereType() - .where((l) => l is! BaseLogicArray) + .where((l) => l is! LogicArrayOf) .firstOrNull ?? logics.firstOrNull; } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index c9e2ff6468..25b288a929 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -519,7 +519,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 +527,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { .toList(); final aggregateLogic = agg.logics.singleOrNull; - final isPackedBitArray = aggregateLogic is BaseLogicArray && + final isPackedBitArray = aggregateLogic is LogicArrayOf && aggregateLogic.dimensions.length == 1 && aggregateLogic.elementWidth == 1 && aggregateLogic.numUnpackedDimensions == 0; @@ -1322,7 +1322,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } final allElements = parentArray.logics - .whereType() + .whereType>() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1487,8 +1487,9 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } bool elementsAllAbsent(SynthLogic parentArray) => - parentArray.logics.every((logic) => - !(logic as BaseLogicArray).elements.any(logicHasPresentSynthLogic)); + parentArray.logics.every((logic) => !(logic as LogicArrayOf) + .elements + .any(logicHasPresentSynthLogic)); var droppedAny = true; while (droppedAny) { diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index dfba7ecf49..f0f7383126 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -11,16 +11,16 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; /// A [Swizzle] used by synthesis backends to explicitly assemble a -/// [BaseLogicArray] from its elements. +/// [LogicArrayOf] from its elements. @internal class SynthArrayConcat extends Swizzle { /// The canonical base name for synthesized array concat operations. static const String operationName = 'array_concat'; - final BaseLogicArray _destination; + final LogicArrayOf _destination; /// Creates a synthesis array concatenation from [signals]. - SynthArrayConcat(super.signals, {required BaseLogicArray destination}) + SynthArrayConcat(super.signals, {required LogicArrayOf 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 f03761c013..1101612476 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -11,7 +11,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; /// A [BusSubset] used by synthesis backends to explicitly extract a -/// [BaseLogicArray] element from its packed parent representation. +/// [LogicArrayOf] 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 227a1b4169..7ede6a0642 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -173,7 +173,8 @@ 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 BaseLogicArray).isNet); + logics.first.isNet || + (isArray && (logics.first as LogicArrayOf).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 [BaseLogicArray]. + /// True only if this represents a [LogicArrayOf]. final bool isArray; /// The chosen name of this. @@ -623,7 +624,7 @@ class SynthLogic { final logic = logics.first; if (isArray) { - final logicArr = logic as BaseLogicArray; + final logicArr = logic as LogicArrayOf; final packedDimsBuf = StringBuffer(); final unpackedDimsBuf = StringBuffer(); @@ -831,7 +832,7 @@ class SynthLogicPackedRangeReference extends SynthLogic { } } -/// Represents an element of a [BaseLogicArray]. +/// Represents an element of a [LogicArrayOf]. /// /// Does not fully override or properly implement all characteristics of /// [SynthLogic], so this should be used cautiously. @@ -915,13 +916,13 @@ class SynthLogicArrayElement extends SynthLogic { /// The element of the [parentArray]. final Logic logic; - /// Creates an instance of an element of a [BaseLogicArray]. + /// Creates an instance of an element of a [LogicArrayOf]. SynthLogicArrayElement( this.logic, { required super.parentSynthModuleDefinition, }) : assert( logic.isArrayMember, - 'Should only be used for elements in a BaseLogicArray', + 'Should only be used for elements in a LogicArrayOf', ), super(logic) { // make sure we have created the synthLogic for the parent array @@ -934,7 +935,7 @@ class SynthLogicArrayElement extends SynthLogic { ' logics contained: ${logics.map((e) => e.name).toList()}'; } -/// Represents a scalar field within a structured [BaseLogicArray] element. +/// Represents a scalar field within a structured [LogicArrayOf] element. /// /// The field has no standalone declaration: it is selected from the packed /// array element that contains it. @@ -947,7 +948,7 @@ class SynthLogicArrayStructureElement extends SynthLogic { var current = logic; var parent = current.parentStructure; while (parent != null) { - if (parent is BaseLogicArray) { + if (parent is LogicArrayOf) { current = parent; parent = current.parentStructure; } else { @@ -963,7 +964,7 @@ class SynthLogicArrayStructureElement extends SynthLogic { var current = logic; var parent = current.parentStructure; while (parent != null) { - if (parent is BaseLogicArray) { + if (parent is LogicArrayOf) { yield current; } current = parent; @@ -1030,7 +1031,7 @@ class SynthLogicArrayStructureElement extends SynthLogic { var current = logic; var parent = current.parentStructure; while (parent != null) { - if (parent is BaseLogicArray) { + if (parent is LogicArrayOf) { return current; } current = parent; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 18c9e25ae2..0a3518c523 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -383,7 +383,7 @@ class SynthModuleDefinition { logicToSynthMap[logic] = newSynth; - if (logic is BaseLogicArray) { + if (logic is LogicArrayOf) { // if we are an array, make sure we go down the stack of elements too logic.elements.forEach(getSynthLogic); } @@ -396,9 +396,9 @@ class SynthModuleDefinition { static bool _hasTopLevelArrayAncestor(Logic logic) { var current = logic; var parent = current.parentStructure; - BaseLogicArray? rootArray; + LogicArrayOf? rootArray; while (parent != null) { - if (parent is BaseLogicArray) { + if (parent is LogicArrayOf) { rootArray = parent; } current = parent; @@ -440,7 +440,8 @@ class SynthModuleDefinition { /// the module, or for driving the input of a sub-module. @protected void _partialAssignStructPort(LogicStructure port) { - assert(port is! BaseLogicArray, 'Should only be used on non-array structs'); + assert(port is! LogicArrayOf, + 'Should only be used on non-array structs'); final portSynth = getSynthLogic(port)!; @@ -517,7 +518,7 @@ class SynthModuleDefinition { final outputSynth = getSynthLogic(output)!; outputs.add(outputSynth); - if (output is LogicStructure && output is! BaseLogicArray) { + if (output is LogicStructure && output is! LogicArrayOf) { _partialAssignStructPort(output); } } @@ -527,7 +528,7 @@ class SynthModuleDefinition { final inputSynth = getSynthLogic(input)!; inputs.add(inputSynth); - if (input is LogicStructure && input is! BaseLogicArray) { + if (input is LogicStructure && input is! LogicArrayOf) { _subsetReceiveStructPort(input); } } @@ -536,7 +537,7 @@ class SynthModuleDefinition { for (final inOut in module.inOuts.values) { inOuts.add(getSynthLogic(inOut)!); - if (inOut is LogicStructure && inOut is! BaseLogicArray) { + if (inOut is LogicStructure && inOut is! LogicArrayOf) { // for nets, we can just use the normal bus subset here in either // direction! _subsetReceiveStructPort(inOut); @@ -561,17 +562,17 @@ class SynthModuleDefinition { subModule.inputs.values .whereType() - .where((e) => e is! BaseLogicArray) + .where((e) => e is! LogicArrayOf) .forEach(_partialAssignStructPort); subModule.outputs.values .whereType() - .where((e) => e is! BaseLogicArray) + .where((e) => e is! LogicArrayOf) .forEach(_subsetReceiveStructPort); subModule.inOuts.values .whereType() - .where((e) => e is! BaseLogicArray) + .where((e) => e is! LogicArrayOf) .forEach(_subsetReceiveStructPort); } @@ -1155,7 +1156,7 @@ class SynthModuleDefinition { } candidatesByArray.forEach((parentArray, arrayCandidates) { final allElementSynthLogics = parentArray.logics - .whereType() + .whereType>() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1829,7 +1830,7 @@ class SynthModuleDefinition { return false; } final parentLogic = signal.parentArray.resolved.logics.singleOrNull; - return parentLogic is BaseLogicArray && + return parentLogic is LogicArrayOf && parentLogic.dimensions.length == 1 && parentLogic.elementWidth == 1 && parentLogic.numUnpackedDimensions == 0; @@ -2087,7 +2088,7 @@ class SynthModuleDefinition { } final srcLogic = srcArray.logics.first; final dstLogic = dstArray.logics.first; - if (srcLogic is! BaseLogicArray || dstLogic is! BaseLogicArray) { + if (srcLogic is! LogicArrayOf || dstLogic is! LogicArrayOf) { return false; } @@ -2479,7 +2480,7 @@ class SynthModuleDefinition { final dst = assignment.dst.resolved; if (dst.width <= 1 || - dst.logics.any((logic) => logic is BaseLogicArray)) { + dst.logics.any((logic) => logic is LogicArrayOf)) { updatedAssignments.add(assignment); continue; } @@ -2744,7 +2745,7 @@ class SynthModuleDefinition { return false; } final logic = base.logics.first; - return logic is BaseLogicArray && + return logic is LogicArrayOf && logic.dimensions.length == 1 && logic.elementWidth == 1 && logic.numUnpackedDimensions == 0; @@ -3853,7 +3854,7 @@ class SynthModuleDefinition { } final arrayLogic = parentArray.logics.first; - if (arrayLogic is! BaseLogicArray || + if (arrayLogic is! LogicArrayOf || indexedInputs.length != arrayLogic.elements.length) { continue; } @@ -3914,7 +3915,7 @@ class SynthModuleDefinition { } final logic = intermediate.logics.firstOrNull; - return logic is! BaseLogicArray || logic.numUnpackedDimensions == 0; + return logic is! LogicArrayOf || logic.numUnpackedDimensions == 0; } /// Whether [intermediate] has the unnamed packed-array shape generated by @@ -3929,7 +3930,8 @@ class SynthModuleDefinition { } final arrayLogic = intermediate.logics.singleOrNull; - return arrayLogic is BaseLogicArray && arrayLogic.naming == Naming.unnamed; + return arrayLogic is LogicArrayOf && + arrayLogic.naming == Naming.unnamed; } /// Whether [swizzleOutput] feeds a full-width disposable internal signal. @@ -3955,7 +3957,7 @@ class SynthModuleDefinition { return false; } - return dst.logics.singleOrNull is! BaseLogicArray; + return dst.logics.singleOrNull is! LogicArrayOf; }); } @@ -4149,12 +4151,12 @@ class SynthModuleDefinition { if (mergedAway.isArray) { for (final (keptElementIndex, keptElementLogic) - in (kept.logics.first as BaseLogicArray).elements.indexed) { + in (kept.logics.first as LogicArrayOf).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 BaseLogicArray) + (mergedAway.logics.first as LogicArrayOf) .elements[keptElementIndex], )!; diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart index ce89308c9b..22898baa0c 100644 --- a/lib/src/synthesizers/utilities/synth_structure_layout.dart +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -53,7 +53,7 @@ class SynthStructureLayout { fieldPath: fieldPath, indexInParent: index, )); - if (element is LogicStructure && element is! BaseLogicArray) { + if (element is LogicStructure && element is! LogicArrayOf) { _addStructure(element, offset, path, fieldPath); } offset = end; diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index ed5c70f1a2..66c9080b69 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -91,7 +91,7 @@ class Vector { final value = LogicValue.of(inputValues[signalName], width: signal.width); return '${packedInputDrivers[signalName]} = $value;'; - } else if (signal is BaseLogicArray) { + } else if (signal is LogicArrayOf) { final arrAssigns = StringBuffer(); var index = 0; final fullVal = @@ -121,7 +121,7 @@ class Vector { ); final inputStimulus = inputValues.toString(); - if (outputPort is BaseLogicArray) { + if (outputPort is LogicArrayOf) { var index = 0; for (final element in outputPort.arrayElements) { final subVal = expectedValue.getRange(index, index + element.width); @@ -709,7 +709,8 @@ abstract class SimCompare { final signal = module.signals.firstWhere((e) => e.name == signalName); final signalType = signalTypeOverride ?? - ((signal is LogicNet || (signal is BaseLogicArray && signal.isNet)) + ((signal is LogicNet || + (signal is LogicArrayOf && signal.isNet)) ? 'wire' : 'logic'); @@ -717,7 +718,7 @@ abstract class SimCompare { signalName = adjust(signalName); } - if (signal is BaseLogicArray) { + if (signal is LogicArrayOf) { final unpackedDims = signal.dimensions.getRange(0, signal.numUnpackedDimensions); final packedDims = signal.dimensions 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..123440a7d3 --- /dev/null +++ b/lib/src/values/logic_value_array.dart @@ -0,0 +1,196 @@ +// 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'; + +LogicValue _decodeIdentityLogicValue(LogicValue value) => value; +LogicValue _encodeIdentityLogicValue(LogicValue value) => value; + +const _identityLogicValueCodec = LogicValueCodec( + decode: _decodeIdentityLogicValue, + encode: _encodeIdentityLogicValue, +); + +/// 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 LogicValueArrayOf { + /// 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] and explicit metadata. + factory LogicValueArray.fromFlat( + List dimensions, + int elementWidth, + Iterable values, + ) { + final normalizedDimensions = _validateValueArrayDimensions(dimensions); + _validateValueArrayElementWidth(elementWidth); + final packedValues = values.toList(growable: false); + _validateValueCount(normalizedDimensions, packedValues.length, 'values'); + _validateElementWidths(packedValues, elementWidth, 'values'); + return LogicValueArray._stored( + normalizedDimensions, elementWidth, packedValues); + } + + /// Creates an empty, zero-width value array. + factory LogicValueArray.empty() => + LogicValueArray.fromFlat(const [0], 0, const []); + + /// Generates values from row-major multidimensional indices. + factory LogicValueArray.generate( + List dimensions, + int elementWidth, + LogicValue Function(List indices) generator, + ) { + final normalizedDimensions = _validateValueArrayDimensions(dimensions); + return LogicValueArray.fromFlat( + normalizedDimensions, + elementWidth, + Iterable.generate( + _valueArrayLength(normalizedDimensions), + (index) => generator( + _valueArrayIndices(normalizedDimensions, index), + ), + ), + ); + } + + /// 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, + elementWidth, + nested.values.map((value) => LogicValue.ofInt(value, elementWidth)), + ); + } + + /// Creates a value array from flat row-major integer [values]. + factory LogicValueArray.fromFlatInts( + List dimensions, + int elementWidth, + Iterable values, + ) => + LogicValueArray.fromFlat( + dimensions, + elementWidth, + values.map((value) => LogicValue.ofInt(value, elementWidth)), + ); + + /// Captures the current values of a hardware [LogicArrayOf]. + factory LogicValueArray.fromLogicArray(LogicArrayOf values) => + LogicValueArray.fromFlat( + values.dimensions, + values.elementWidth, + values.arrayElements.map((element) => element.value), + ); + + /// 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), + ); + } + + LogicValueArray._stored( + List dimensions, + int elementWidth, + List values, + ) : super._normalized( + dimensions, + elementWidth, + values, + values, + _identityLogicValueCodec, + ); + + @override + Iterable get majorSlices => + super.majorSlices.cast(); + + @override + LogicValueArray map(LogicValue Function(LogicValue value) transform) => + LogicValueArray.fromFlat( + dimensions, elementWidth, arrayValues.map(transform)); + + @override + LogicValueArray indexedMap( + LogicValue Function(List indices, LogicValue value) transform, + ) => + LogicValueArray.fromFlat( + dimensions, + elementWidth, + indexedValues.map((entry) => transform(entry.$1, entry.$2)), + ); + + @override + LogicValueArray mapMajorSlices( + LogicValueArrayOf Function(LogicValueArrayOf slice) + transform, + ) { + final transformed = majorSlices.map(transform).toList(growable: false); + if (transformed.isEmpty) { + throw StateError('Cannot infer a mapped shape from zero major slices.'); + } + final first = transformed.first; + transformed.skip(1).forEach(first._checkStackCompatible); + return LogicValueArray._stored( + [transformed.length, ...first.dimensions], + first.elementWidth, + transformed + .expand((slice) => slice._packedElements) + .toList(growable: false), + ); + } + + @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/logic_value_array_of.dart b/lib/src/values/logic_value_array_of.dart new file mode 100644 index 0000000000..618b47ef64 --- /dev/null +++ b/lib/src/values/logic_value_array_of.dart @@ -0,0 +1,505 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_value_array_of.dart +// Definition of typed multi-dimensional logic value arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +part of 'values.dart'; + +/// 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; + + final Function _encode; + + /// Creates a bidirectional value [decode]/[encode] codec. + const LogicValueCodec({ + required this.decode, + required LogicValue Function(T value) encode, + }) : _encode = encode; + + /// 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 [LogicValueArrayOf.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. +class LogicValueArrayOf 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; + + final List _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 + /// [LogicValueArrayOf.fromFlat] to construct an empty array because its + /// element width cannot be inferred. + factory LogicValueArrayOf( + 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 LogicValueArrayOf._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 LogicValueArrayOf.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 LogicValueArrayOf._normalized( + normalizedDimensions, + elementWidth, + encoded.map(codec.decode).toList(growable: false), + encoded, + codec, + ); + } + + /// Decodes the packed elements of [values] with [codec]. + factory LogicValueArrayOf.fromLogicValueArray( + LogicValueArray values, { + required LogicValueCodec codec, + }) => + LogicValueArrayOf._normalized( + values.dimensions, + values.elementWidth, + values._packedElements.map(codec.decode).toList(growable: false), + values._packedElements, + 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 LogicValueArrayOf.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 LogicValueArrayOf._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, + ); + } + + LogicValueArrayOf._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.'); + } + + /// Number of semantic array elements. + int get elementCount => _arrayValues.length; + + /// 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( + elementCount, + (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[flatIndexOf(indices)]; + + /// Returns the row-major flat index for multidimensional [indices]. + int flatIndexOf(List indices) => + _valueArrayFlatIndex(dimensions, indices); + + /// Maps semantic values while preserving shape and codec. + LogicValueArrayOf map(T Function(T value) transform) => + LogicValueArrayOf.fromFlat( + dimensions, + elementWidth, + _arrayValues.map(transform), + codec: codec, + ); + + /// Maps semantic values with their multidimensional indices. + LogicValueArrayOf indexedMap( + T Function(List indices, T value) transform, + ) => + LogicValueArrayOf.fromFlat( + dimensions, + elementWidth, + indexedValues.map((entry) => transform(entry.$1, entry.$2)), + codec: codec, + ); + + /// Maps slices along the first dimension and stacks the results. + LogicValueArrayOf mapMajorSlices( + LogicValueArrayOf Function(LogicValueArrayOf slice) transform, + ) => + LogicValueArrayOf.stack(majorSlices.map(transform)); + + /// Returns a row-major view with [newDimensions]. + LogicValueArrayOf reshape(List newDimensions) { + final normalizedDimensions = _validateValueArrayDimensions(newDimensions); + if (_valueArrayLength(normalizedDimensions) != elementCount) { + throw ArgumentError.value(newDimensions, 'newDimensions', + 'Must contain $elementCount array elements.'); + } + return _createStored(normalizedDimensions, _arrayValues, _packedElements); + } + + /// Transposes this two-dimensional value array. + LogicValueArrayOf transpose2D() { + _checkValueArrayIsTwoDimensional(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 = flatIndexOf([column, row]); + semanticValues.add(_arrayValues[source]); + packedElements.add(_packedElements[source]); + } + } + return _createStored(newDimensions, semanticValues, packedElements); + } + + /// Creates a [LogicArray] with the same shape and drives it with this value. + LogicArray toLogicArray({String? name}) => + putInto(LogicArray(dimensions, elementWidth, name: name)); + + /// Drives a compatible hardware [target] with this shaped value. + U putInto>(U target) { + _checkHardwareCompatible(target); + for (var index = 0; index < elementCount; index++) { + target.arrayElements[index].put(_packedElements[index]); + } + return target; + } + + LogicValueArrayOf _createStored( + List dimensions, + List arrayValues, + List packedElements, + ) => + LogicValueArrayOf._normalized( + dimensions, elementWidth, arrayValues, packedElements, codec); + + void _checkStackCompatible(LogicValueArrayOf 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.'); + } + } + + void _checkHardwareCompatible(LogicArrayOf target) { + if (elementWidth != target.elementWidth || + !_sameValueArrayDimensions(dimensions, target.dimensions)) { + throw ArgumentError.value( + target, + 'target', + '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; +} + +class _NestedValueArray { + final List dimensions; + final List values; + + _NestedValueArray(this.dimensions, this.values); +} + +_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 []); +} + +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; +} + +void _validateValueArrayElementWidth(int elementWidth) { + if (elementWidth < 0) { + throw ArgumentError.value( + elementWidth, 'elementWidth', 'Must be non-negative.'); + } +} + +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.'); + } +} + +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.'); + } + } +} + +int _valueArrayLength(List dimensions) => + dimensions.fold(1, (length, dimension) => length * 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; +} + +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; +} + +bool _sameValueArrayDimensions(List left, List right) => + left.length == right.length && + left.indexed.every((entry) => entry.$2 == right[entry.$1]); + +void _checkValueArrayIsTwoDimensional(List dimensions) { + if (dimensions.length != 2) { + throw StateError('Expected exactly two dimensions, got $dimensions.'); + } +} diff --git a/lib/src/values/values.dart b/lib/src/values/values.dart index b58ce04032..16294ac168 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 'logic_value_array_of.dart'; part 'small_logic_value.dart'; part 'big_logic_value.dart'; part 'filled_logic_value.dart'; diff --git a/test/logic_array_of_test.dart b/test/logic_array_of_test.dart index bcf00395b5..a621b4f1ca 100644 --- a/test/logic_array_of_test.dart +++ b/test/logic_array_of_test.dart @@ -1,5 +1,7 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +// ignore_for_file: prefer_const_constructors +// ignore_for_file: prefer_const_literals_to_create_immutables // // logic_array_of_test.dart // Tests for typed logic and logic value arrays. @@ -13,33 +15,40 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; -class _TwoBitStructure extends LogicStructure { +class _SampleStructure extends LogicStructure { final Logic low; final Logic high; - factory _TwoBitStructure({String? name}) => _TwoBitStructure._( + factory _SampleStructure({String? name}) => _SampleStructure._( Logic(name: 'low'), - Logic(name: 'high'), - name: name ?? 'twoBit', + Logic(name: 'high', width: 2), + name: name ?? 'sample', ); - _TwoBitStructure._(this.low, this.high, {required String name}) + _SampleStructure._(this.low, this.high, {required String name}) : super([low, high], name: name); @override - _TwoBitStructure clone({String? name}) => - _TwoBitStructure(name: name ?? this.name); + _SampleStructure clone({String? name}) => + _SampleStructure(name: name ?? this.name); +} + +class _SpecialLogic extends Logic { + _SpecialLogic({super.name}) : super(width: 2); + + @override + Logic clone({String? name}) => Logic(name: name ?? this.name, width: width); } class _TypedArrayPortModule extends Module { - LogicArrayOf<_TwoBitStructure> get valuesOut => - output('valuesOut') as LogicArrayOf<_TwoBitStructure>; + LogicArrayOf<_SampleStructure> get valuesOut => + output('valuesOut') as LogicArrayOf<_SampleStructure>; - _TypedArrayPortModule(LogicArrayOf<_TwoBitStructure> valuesIn) { + _TypedArrayPortModule(LogicArrayOf<_SampleStructure> valuesIn) { valuesIn = addTypedInput('valuesIn', valuesIn); final valuesOut = addTypedOutput('valuesOut', valuesIn.clone); - for (final (index, source) in valuesIn.typedLeafElements.indexed) { - final destination = valuesOut.typedLeafElements[index]; + for (final (index, source) in valuesIn.arrayElements.indexed) { + final destination = valuesOut.arrayElements[index]; destination.low <= source.low; destination.high <= ~source.high; } @@ -49,9 +58,9 @@ class _TypedArrayPortModule extends Module { class _TypedArrayInterface extends Interface<_TypedArrayDirection> { _TypedArrayInterface() { setPorts([ - LogicArrayOf<_TwoBitStructure>( + LogicArrayOf<_SampleStructure>( [2], - _TwoBitStructure.new, + _SampleStructure.new, name: 'values', ), ], [ @@ -75,28 +84,64 @@ class _TypedArrayInterfaceModule extends Module { } } +class _TypedArrayHierarchy extends Module { + LogicArrayOf<_SampleStructure> get valuesOut => + output('valuesOut') as LogicArrayOf<_SampleStructure>; + + _TypedArrayHierarchy(LogicArrayOf<_SampleStructure> source) { + final valuesIn = addTypedInput('valuesIn', source); + final child = _TypedArrayPortModule(valuesIn); + addTypedOutput('valuesOut', valuesIn.clone).gets(child.valuesOut); + } +} + +class _TypedInputModule extends Module { + late final T values; + + _TypedInputModule(T source) { + values = addTypedInput('values', source); + } +} + int _decodeLogicValue(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('LogicArrayOf', () { test('keeps structured leaves at the array boundary', () { - final values = LogicArrayOf<_TwoBitStructure>( + final values = LogicArrayOf<_SampleStructure>( [2, 3], - _TwoBitStructure.new, + _SampleStructure.new, dimensionNames: const ['row_', 'column_'], ); - expect(values, isA()); + expect(values, isA>()); expect(values.dimensions, equals([2, 3])); - expect(values.elementWidth, 2); + expect(values.elementWidth, 3); expect(values.arrayElements, hasLength(6)); - expect(values.typedLeafElements, hasLength(6)); - expect(values.arrayElements, everyElement(isA<_TwoBitStructure>())); + expect(values.arrayElements, everyElement(isA<_SampleStructure>())); expect(values.leafElements, hasLength(12)); - expect(values.elementAt([1, 2]), same(values.arrayElements[5])); + expect(values.at([1, 2]), same(values.arrayElements[5])); expect(values.indexedElements.last.$1, equals([1, 2])); + expect( + values.reshape([3, 2]).dimensionNames, + equals(['row_', 'column_']), + ); + expect( + values.transpose2D().dimensionNames, + equals(['column_', 'row_']), + ); }); test('provides typed indexing, cloning, and packed conversions', () { @@ -107,12 +152,12 @@ void main() { final packed = values.toLogicArray(name: 'packed'); final clone = values.clone(name: 'clone'); - expect(values.elementAt([1, 0]), same(values.typedLeafElements[2])); + expect(values.at([1, 0]), same(values.arrayElements[2])); expect(packed.dimensions, equals([2, 2])); expect(packed.elementWidth, 8); expect(clone, isA>()); expect(clone.name, 'clone'); - expect(clone.typedLeafElements, hasLength(4)); + expect(clone.arrayElements, hasLength(4)); expect( () => values.getsPackedValues(LogicArray([4], 8)), throwsA(isA()), @@ -123,6 +168,36 @@ void main() { ); }); + test('derives the net kind for empty multidimensional typed arrays', () { + var logicBuilds = 0; + var netBuilds = 0; + final logicValues = LogicArrayOf( + [2, 0], + ({name}) { + logicBuilds++; + return Logic(name: name, width: 8); + }, + ); + final netValues = LogicArrayOf( + [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('preserves unpacked dimensions through cloning', () { final values = LogicArrayOf( [2, 3], @@ -143,12 +218,12 @@ void main() { ({name}) => LogicArray([3, 2], 4, name: name), ); final flattened = nested.flattenNestedDimensions(name: 'flat'); - final source = nested.elementAt([1, 0]).elementAt([2, 1]); - final target = flattened.elementAt([1, 0, 2, 1]); + final source = nested.at([1, 0]).at([2, 1]); + final target = flattened.at([1, 0, 2, 1]); expect(flattened.dimensions, equals([2, 2, 3, 2])); expect(flattened.elementWidth, 4); - expect(flattened.typedLeafElements, hasLength(24)); + expect(flattened.arrayElements, hasLength(24)); expect(target, isA()); expect(target.srcConnections, contains(source)); }); @@ -163,14 +238,46 @@ void main() { ), ); final flattened = nested.flattenNestedDimensions(); - final source = nested.elementAt([1]).elementAt([2]).elementAt([3]); - final target = flattened.elementAt([1, 2, 3]); + final source = nested.at([1]).at([2]).at([3]); + final target = flattened.at([1, 2, 3]); expect(flattened.dimensions, equals([2, 3, 4])); - expect(flattened.typedLeafElements, hasLength(24)); + expect(flattened.arrayElements, hasLength(24)); expect(target.srcConnections, contains(source)); }); + test('flattens empty arrays and leaves without specialized clones', () { + final empty = LogicArrayOf( + [2, 0], + ({name}) => Logic(name: name, width: 4), + ); + final emptyNested = LogicArrayOf>( + [0], + ({name}) => LogicArrayOf( + [3], + ({name}) => Logic(name: name, width: 2), + name: name, + ), + ); + final specialized = LogicArrayOf<_SpecialLogic>([2], _SpecialLogic.new); + + final flattenedEmpty = empty.flattenNestedDimensions(); + final flattenedNested = emptyNested.flattenNestedDimensions(); + final flattenedSpecialized = + specialized.flattenNestedDimensions<_SpecialLogic>(); + + expect(flattenedEmpty.dimensions, [2, 0]); + expect(flattenedEmpty.elementWidth, 4); + expect(flattenedEmpty.arrayElements, isEmpty); + expect(flattenedNested.dimensions, [0, 3]); + expect(flattenedNested.elementWidth, 2); + expect(flattenedNested.arrayElements, isEmpty); + expect( + flattenedSpecialized.arrayElements, + everyElement(isA<_SpecialLogic>()), + ); + }); + test('validates dimensions, names, and leaf widths', () { expect( () => LogicArrayOf(const [], Logic.new), @@ -232,7 +339,29 @@ void main() { expect(renamed.naming, Naming.reserved); }); - test('drives and captures compatible packed and typed values', () { + test('matches LogicArray clone naming policy', () { + final typed = LogicArrayOf( + [2], + Logic.new, + name: 'typedValues', + naming: Naming.reserved, + ); + final ordinary = LogicArray( + [2], + 1, + name: 'ordinaryValues', + naming: Naming.reserved, + ); + + expect(typed.clone().naming, ordinary.clone().naming); + expect( + typed.clone(name: 'typedClone').naming, + ordinary.clone(name: 'ordinaryClone').naming, + ); + }); + + test('drives, injects, and captures packed and typed values', () async { + addTearDown(Simulator.reset); const codec = LogicValueCodec( decode: _decodeLogicValue, encode: _encodeLogicValue, @@ -241,81 +370,273 @@ void main() { [2], ({name}) => Logic(name: name, width: 8), ); - final packedValues = LogicValueArray.fromInts([2], 8, [12, 34]); + final packedValues = LogicValueArray.fromInts([12, 34], elementWidth: 8); final typedValues = LogicValueArrayOf( - [2], - 8, - [56, 78], + [90, 123], codec: codec, ); expect(packedValues.putInto(values), same(values)); expect( - values.logicValues.flatValues.map((value) => value.toInt()), + values.value.arrayValues.map((value) => value.toInt()), [12, 34], ); - values.putValueArrayOf(typedValues); - expect(values.valueArrayOf(codec).flatValues, [56, 78]); + values.put(LogicValue.ofInt(0x4e38, 16)); + expect( + values.value.arrayValues.map((value) => value.toInt()), + [56, 78], + ); + expect( + () => values.put(typedValues, fill: true), + throwsArgumentError, + ); + + values.inject(typedValues); + await Simulator.run(); + expect( + LogicValueArrayOf.fromLogicValueArray( + values.value, + codec: codec, + ).arrayValues, + [90, 123], + ); }); test('preserves specialized leaves in typed input and output ports', () { - final source = LogicArrayOf<_TwoBitStructure>( + final source = LogicArrayOf<_SampleStructure>( [2, 3], - _TwoBitStructure.new, + _SampleStructure.new, ); final module = _TypedArrayPortModule(source); final valuesIn = - module.input('valuesIn') as LogicArrayOf<_TwoBitStructure>; + module.input('valuesIn') as LogicArrayOf<_SampleStructure>; - expect(valuesIn, isA>()); + expect(valuesIn, isA>()); expect(valuesIn.dimensions, [2, 3]); - expect(valuesIn.elementAt([1, 2]), isA<_TwoBitStructure>()); - expect(module.valuesOut, isA>()); - expect(module.valuesOut.elementAt([1, 2]).low.width, 1); + expect(valuesIn.at([1, 2]), isA<_SampleStructure>()); + expect(module.valuesOut, isA>()); + expect(module.valuesOut.at([1, 2]).low.width, 1); + expect(module.valuesOut.at([1, 2]).high.width, 2); }); test('preserves typed arrays through interfaces and synthesis', () async { - final source = LogicArrayOf<_TwoBitStructure>( - [2], - _TwoBitStructure.new, + final source = LogicArrayOf<_SampleStructure>( + [2, 2], + _SampleStructure.new, name: 'valuesIn', ); - final module = _TypedArrayPortModule(source); + final module = _TypedArrayHierarchy(source); await module.build(); final vectors = [ - Vector({'valuesIn': 0x9}, {'valuesOut': 0x3}), - Vector({'valuesIn': 0x6}, {'valuesOut': 0xc}), + 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; - expect(netlist['modules'], isNotEmpty); + 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>(), + isA>(), ); }); }); group('LogicValueArray', () { - test('indexes, slices, reshapes, and stacks row-major values', () { - final values = LogicValueArray.fromInts([2, 3], 8, [1, 2, 3, 4, 5, 6]); + 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([2, 2], 8, leaves); + final nestedInts = LogicValueArray.fromInts([ + [1, 2], + [3, 4], + ], elementWidth: 8); + final flatInts = LogicValueArray.fromFlatInts([2, 2], 8, [1, 2, 3, 4]); + + expect(nested.dimensions, [2, 2]); + expect(nested.elementWidth, 8); + expect(nested.elementCount, 4); + expect(nested.width, 32); + // The deprecated LogicValue length retains its packed-bit meaning. + // ignore: deprecated_member_use_from_same_package + expect(nested.length, 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([ + [1, 2, 3], + [4, 5, 6], + ], elementWidth: 8); - expect(values.length, 6); expect(values.at([1, 1]).toInt(), 5); expect(values.flatIndexOf([1, 2]), 5); expect(values.indexedValues.last.$1, equals([1, 2])); expect( - values.majorSlices.map( - (slice) => slice.flatValues.map((value) => value.toInt()).toList()), + values.majorSlices.map((slice) => + slice.arrayValues.map((value) => value.toInt()).toList()), equals([ [1, 2, 3], [4, 5, 6], @@ -323,96 +644,248 @@ void main() { ); expect(values.reshape([3, 2]).at([2, 1]).toInt(), 6); expect( - LogicValueArray.stack(values.majorSlices).dimensions, - equals([2, 3]), + values.transpose2D().arrayValues.map((value) => value.toInt()), + [1, 4, 2, 5, 3, 6], ); - }); - - test('maps values and validates incompatible operations', () { - final values = LogicValueArray.fromInts([2, 2], 8, [1, 2, 3, 4]); - expect( values .indexedMap((indices, value) => LogicValue.ofInt(value.toInt() + indices[0], 8)) - .flatValues + .arrayValues .map((value) => value.toInt()), - equals([1, 2, 4, 5]), + equals([1, 2, 3, 5, 6, 7]), + ); + expect( + LogicValueArray.stack(values.majorSlices).dimensions, + equals([2, 3]), ); + }); + + test('round-trips slices with an empty inner dimension', () { + final values = LogicValueArray.fromFlat([2, 0], 8, const []); + 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]); + expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); + }); + + test('uses packed LogicValue semantics independent of shape', () { + final shaped = LogicValueArray.fromFlatInts([2], 4, [1, 2]); + final reshaped = LogicValueArray.fromFlatInts([1, 2], 4, [1, 2]); + final packed = LogicValue.ofInt(0x21, 8); + final mask = LogicValue.ofInt(0xf0, 8); + final zeros = LogicValueArray.fromFlatInts([2], 4, [0, 0]); + + expect(shaped, packed); + expect(packed, shaped); + expect(shaped, reshaped); + expect(shaped.hashCode, packed.hashCode); + expect(shaped[0], LogicValue.one); + expect(shaped & mask, packed & mask); + expect(mask & shaped, mask & packed); + expect(shaped | mask, packed | mask); + expect(mask | shaped, mask | packed); + expect(shaped ^ mask, packed ^ mask); + expect(mask ^ shaped, mask ^ packed); + expect(shaped + LogicValue.one.zeroExtend(8), + packed + LogicValue.one.zeroExtend(8)); + expect(LogicValue.one.zeroExtend(8) + shaped, + LogicValue.one.zeroExtend(8) + packed); + expect(LogicValue.ofIterable([zeros]), zeros.packed); expect( - () => values.at([2, 0]), - throwsA(isA()), + LogicValue.ofIterable([zeros, shaped]), + LogicValue.ofIterable([zeros.packed, shaped.packed]), ); + expect({shaped, packed}, hasLength(1)); + }); + + test('rejects ragged, inconsistent, empty, and mismatched input', () { + final one = LogicValue.ofInt(1, 8); + expect(() => LogicValueArray(const []), throwsArgumentError); expect( - () => values.reshape([3, 2]), - throwsA(isA()), + () => LogicValueArray([ + [one], + [one, one], + ]), + throwsArgumentError, ); expect( - () => LogicValueArray.stack([ - values, - LogicValueArray.fromInts([4], 8, [1, 2, 3, 4]), + () => LogicValueArray([ + one, + [one], ]), - throwsA(isA()), + throwsArgumentError, ); + expect( + () => LogicValueArray([one, LogicValue.ofInt(2, 4)]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat([2, 2], 8, [one]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromInts(const [], elementWidth: 8), + throwsArgumentError, + ); + expect(() => LogicValueArray.fromFlat(const [], 8, const []), + throwsArgumentError); }); }); group('LogicValueArrayOf', () { - test('maps, reshapes, and transposes packed value arrays', () { - final values = LogicValueArray.fromInts([2, 3], 8, [1, 2, 3, 4, 5, 6]); - final transposed = values.transpose2D(); - - expect(transposed.dimensions, equals([3, 2])); - expect( - transposed.flatValues.map((value) => value.toInt()), - equals([1, 4, 2, 5, 3, 6]), + test('constructs equivalent nested and flat list-valued data', () { + const codec = LogicValueCodec>( + decode: _decodeListLogicValue, + encode: _encodeListLogicValue, + ); + final leaves = [ + [1], + [2], + [3], + [4], + ]; + final nested = LogicValueArrayOf>([ + [leaves[0], leaves[1]], + [leaves[2], leaves[3]], + ], codec: codec); + final flat = LogicValueArrayOf>.fromFlat( + [2, 2], + 8, + leaves, + codec: codec, ); - expect(values.reshape([3, 2]).at([2, 1]).toInt(), 6); + 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, transposes, and converts typed values', () { const codec = LogicValueCodec( decode: _decodeLogicValue, encode: _encodeLogicValue, ); - final typed = LogicValueArrayOf.fromLogicValues( - values, + final packed = LogicValueArray.fromInts([ + [1, 2, 3], + [4, 5, 6], + ], elementWidth: 8); + final values = LogicValueArrayOf.fromLogicValueArray( + packed, codec: codec, ); - expect(typed.at([1, 1]), 5); - expect(typed.map((value) => value + 1).at([1, 1]), 6); - expect(typed.transpose2D().at([2, 1]), 6); + + 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]); + expect(values.toLogicArray().dimensions, [2, 3]); }); - test('supports slices, indexed mapping, stacking, and conversion', () { + test('supports slices, indexed mapping, and compatible stacking', () { const codec = LogicValueCodec( decode: _decodeLogicValue, encode: _encodeLogicValue, ); - final values = LogicValueArrayOf( - [2, 2], - 8, - [1, 2, 3, 4], - codec: codec, - ); + final values = LogicValueArrayOf([ + [1, 2], + [3, 4], + ], codec: codec); - expect(values.majorSlices.map((slice) => slice.flatValues), [ + expect(values.majorSlices.map((slice) => slice.arrayValues), [ [1, 2], [3, 4], ]); expect( - values.indexedMap((indices, value) => value + indices[1]).flatValues, + values.indexedMap((indices, value) => value + indices[1]).arrayValues, [1, 3, 3, 5], ); expect( - LogicValueArrayOf.stack(values.majorSlices).flatValues, - values.flatValues, + LogicValueArrayOf.stack(values.majorSlices).arrayValues, + values.arrayValues, ); - expect(values.logicValues.flatValues.map((value) => value.toInt()), [ - 1, - 2, - 3, - 4, + 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 = LogicValueArrayOf([ + [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( + LogicValueArrayOf.stack(values.majorSlices).arrayValues, + values.arrayValues, + ); + expect(values.transpose2D().arrayValues, [1.0, 4.0, 2.0, 5.0]); + }); + + test('requires identical codecs when stacking', () { + final firstCodec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final secondCodec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final first = LogicValueArrayOf.fromFlat( + [1], + 8, + [1], + codec: firstCodec, + ); + final second = LogicValueArrayOf.fromFlat( + [1], + 8, + [2], + codec: secondCodec, + ); + + expect( + () => LogicValueArrayOf.stack([first, second]), + throwsArgumentError, + ); + }); + + test('round-trips typed slices with an empty inner dimension', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final values = LogicValueArrayOf.fromFlat( + [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(values.toLogicArray().dimensions, [2, 2]); + expect( + LogicValueArrayOf.stack(slices).dimensions, + [2, 0], + ); + expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); }); }); } diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 088ec820e0..47abd9061e 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,35 @@ 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('single-dim array', () { final dim = [5]; const w = 16; From 0463e8e0f96d532e98d01d00d6ff465178c6e102 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 9 Sep 2026 14:11:41 -0700 Subject: [PATCH 05/43] handle mixed net kinds and fix PairInterface cloning --- doc/user_guide/_docs/A20-logic-arrays.md | 2 +- lib/src/interfaces/pair_interface.dart | 2 + lib/src/signals/logic_array_of.dart | 37 +++++++- test/logic_array_of_test.dart | 106 +++++++++++++++++++++++ test/pair_interface_test.dart | 60 +++++++++++++ 5 files changed, 203 insertions(+), 4 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index c01d8c3590..7dce89f920 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -53,7 +53,7 @@ final samples = LogicArrayOf( final bottomRightData = samples.at([1, 2]).data; ``` -The element builder must always produce the configured type, width, and net kind. A zero-sized array calls it once as a prototype so that metadata remains available even though the array has no positions. +The element builder must always produce the configured type, width, and recursively ordered net composition. Every element must be uniformly variable or uniformly net: a structure cannot mix `Logic` and `LogicNet` leaves. A zero-sized array calls the builder once as a prototype so that the same metadata and validation remain available even though the array has no positions. ### Traversal boundaries diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index 3edfacc235..b27083739e 100644 --- a/lib/src/interfaces/pair_interface.dart +++ b/lib/src/interfaces/pair_interface.dart @@ -108,6 +108,8 @@ class PairInterface extends Interface { p.numUnpackedDimensions) : LogicArray.port(name, p.dimensions, p.elementWidth, p.numUnpackedDimensions); + case LogicArrayOf(): + return p.clone(); case LogicNet(): return LogicNet.port(name, p.width); default: diff --git a/lib/src/signals/logic_array_of.dart b/lib/src/signals/logic_array_of.dart index d23b072061..b111ee3779 100644 --- a/lib/src/signals/logic_array_of.dart +++ b/lib/src/signals/logic_array_of.dart @@ -431,14 +431,27 @@ class _LogicArrayOfBuild { throw LogicConstructionException( 'LogicArrayOf leaves cannot contain nested LogicArrayOf fields.'); } + 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( + 'LogicArrayOf elements cannot mix net and non-net leaves.'); + } + if (elementsForNetValidation.any((element) => !_sameNetComposition( + _netComposition(element), prototypeNetComposition))) { + throw LogicConstructionException( + 'All LogicArrayOf elements must have matching net composition.'); + } return _LogicArrayOfBuild._( normalizedDimensions, normalizedNames, elements, _validateElementWidths(typedLeaves, prototype.width), - typedLeaves.isEmpty - ? prototype.isNet - : typedLeaves.every((element) => element.isNet)); + prototypeNetComposition.every((isNet) => isNet)); } /// Whether [structure] recursively contains an array field. @@ -453,6 +466,24 @@ class _LogicArrayOfBuild { (leaf is LogicStructure && leaf.leafElements.any((element) => element is Const)); + /// 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 LogicArrayOf && 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) { diff --git a/test/logic_array_of_test.dart b/test/logic_array_of_test.dart index a621b4f1ca..785c022c8f 100644 --- a/test/logic_array_of_test.dart +++ b/test/logic_array_of_test.dart @@ -33,6 +33,52 @@ class _SampleStructure extends LogicStructure { _SampleStructure(name: name ?? this.name); } +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 _SpecialLogic extends Logic { _SpecialLogic({super.name}) : super(width: 2); @@ -198,6 +244,66 @@ void main() { ); }); + test('requires a uniform recursive net composition', () { + var buildIndex = 0; + expect( + () => LogicArrayOf( + [2], + ({name}) => buildIndex++ == 0 + ? Logic(name: name, width: 3) + : LogicNet(name: name, width: 3), + ), + throwsA(isA()), + ); + expect( + () => LogicArrayOf<_PartiallyNetSampleStructure>( + [2], + _PartiallyNetSampleStructure.new, + ), + throwsA(isA()), + ); + expect( + () => LogicArrayOf<_PartiallyNetSampleStructure>( + [0], + _PartiallyNetSampleStructure.new, + ), + throwsA(isA()), + ); + var structureBuildIndex = 0; + expect( + () => LogicArrayOf( + [2], + ({name}) => structureBuildIndex++ == 0 + ? _SampleStructure(name: name) + : _NetSampleStructure(name: name), + ), + throwsA(isA()), + ); + + final logicValues = LogicArrayOf<_SampleStructure>( + [2], + _SampleStructure.new, + ); + final emptyStructureValues = LogicArrayOf<_EmptyStructure>( + [0], + _EmptyStructure.new, + ); + final netValues = LogicArrayOf<_NetSampleStructure>( + [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('preserves unpacked dimensions through cloning', () { final values = LogicArrayOf( [2, 3], diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 7b72005870..307d7d9f3b 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -190,6 +190,33 @@ 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 _TypedPairConsumer extends Module { + late final LogicArrayOf<_PairSample> samples; + + _TypedPairConsumer(PairInterface source) { + final internal = addPairInterfacePorts(source, PairRole.consumer); + samples = internal.port('samples') as LogicArrayOf<_PairSample>; + addOutput('selected', width: samples.elementWidth) <= samples.at([1, 1]); + } +} + void main() { tearDown(() async { await Simulator.reset(); @@ -204,6 +231,39 @@ void main() { expect(sv, contains('input logic simple_clk')); }); + test('clone preserves typed arrays through pair interface ports', () async { + final samples = LogicArrayOf<_PairSample>( + [2, 2], + _PairSample.new, + dimensionNames: const ['row_', 'column_'], + name: 'samples', + ); + final source = PairInterface(portsFromProvider: [samples]); + final clone = source.clone(); + final clonedSamples = clone.port('samples') as LogicArrayOf<_PairSample>; + + expect(clonedSamples, isNot(same(samples))); + expect(clonedSamples.dimensions, [2, 2]); + expect(clonedSamples.dimensionNames, ['row_', 'column_']); + expect(clonedSamples.arrayElements, everyElement(isA<_PairSample>())); + expect(clonedSamples.at([1, 1]).high.width, 2); + + final module = _TypedPairConsumer(source); + await module.build(); + + expect(module.samples.dimensions, [2, 2]); + expect(module.samples.dimensionNames, ['row_', 'column_']); + expect(module.samples.at([1, 1]), isA<_PairSample>()); + expect(module.samples.at([1, 1]).high.width, 2); + + 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 { From a631d42792d395a37fc07b672a192b56300ec6bb Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 10 Sep 2026 07:39:32 -0700 Subject: [PATCH 06/43] rearchitected around TypedLogicArray/TypedLogicValue dichotomy --- CHANGELOG.md | 2 +- doc/user_guide/_docs/A20-logic-arrays.md | 48 +- lib/src/interfaces/interface.dart | 12 +- lib/src/interfaces/pair_interface.dart | 2 +- lib/src/module.dart | 18 +- lib/src/modules/bus.dart | 2 +- lib/src/signals/logic.dart | 6 +- lib/src/signals/logic_array.dart | 169 +- lib/src/signals/logic_structure.dart | 4 +- lib/src/signals/signals.dart | 2 +- ...c_array_of.dart => typed_logic_array.dart} | 277 ++- .../netlist/netlist_module_translation.dart | 12 +- .../netlist_synth_module_definition.dart | 26 +- .../netlist/netlist_synthesizer.dart | 29 +- .../synthesizers/netlist/netlist_utils.dart | 12 +- ...systemverilog_synth_module_definition.dart | 11 +- .../utilities/synth_array_concat.dart | 6 +- .../utilities/synth_array_slice.dart | 2 +- .../synthesizers/utilities/synth_logic.dart | 21 +- .../utilities/synth_module_definition.dart | 44 +- .../utilities/synth_structure_layout.dart | 2 +- lib/src/utilities/simcompare.dart | 9 +- lib/src/values/logic_value_array.dart | 19 +- ...e_array_of.dart => typed_value_array.dart} | 134 +- lib/src/values/values.dart | 2 +- test/logic_array_of_test.dart | 997 --------- test/logic_array_test.dart | 33 + test/netlist_synthesizer_test.dart | 62 + test/pair_interface_test.dart | 30 +- test/typed_logic_array_test.dart | 1926 +++++++++++++++++ 30 files changed, 2531 insertions(+), 1388 deletions(-) rename lib/src/signals/{logic_array_of.dart => typed_logic_array.dart} (66%) rename lib/src/values/{logic_value_array_of.dart => typed_value_array.dart} (79%) delete mode 100644 test/logic_array_of_test.dart create mode 100644 test/typed_logic_array_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index cca2511de8..612d661e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ ## 0.6.10 -- Added `LogicArrayOf` for multidimensional arrays with fixed typed elements, including typed ports, traversal, cloning, shape operations, net support, and synthesis. Added shaped `LogicValueArray` and codec-backed `LogicValueArrayOf` values with nested construction, packed `LogicValue` interoperability, and hardware-array assignment (). +- Added `TypedLogicArray` for multidimensional arrays with fixed typed hardware elements and associated semantic values, including typed snapshots and ports, traversal, cloning, shape operations, net support, and synthesis. Added shaped `LogicValueArray` and codec-backed `TypedValueArray` values with nested construction, packed `LogicValue` interoperability, and hardware-array assignment. Existing `LogicArray`, `LogicValueArray`, and concrete array port APIs remain available as specializations (). - Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (). - Improved simulation performance and generated outputs by avoiding module creation for four-state-safe operations involving `Const`s, zero shifts, and muxes with constant controls (). - Enforced that `Const` values cannot be changed through `put` or `inject`, including through another `Logic` driven by a `Const` (). diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 7dce89f920..3df00bdeee 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -1,7 +1,7 @@ --- title: "Logic Arrays" permalink: /docs/logic-arrays/ -last_modified_at: 2026-9-9 +last_modified_at: 2026-09-10 toc: true --- @@ -24,7 +24,9 @@ As long as the total width of a `LogicArray` and another type of `Logic` (includ ## Typed arrays -Use `LogicArrayOf` when every position at the declared array boundary has the same specialized `Logic` type. `LogicArray` is the ordinary `LogicArrayOf` specialization, so existing `LogicArray` construction, port, clone, naming, and array behavior is retained. For example, this creates a two-dimensional array of samples with separate data and valid fields: +Use `TypedLogicArray` when every position at the declared array boundary has the same specialized hardware type and associated semantic value type. `LogicArray` remains the ordinary `TypedLogicArray` specialization, preserving its existing constructors, ports, cloning, naming, transformations, and packed assignment behavior. Both extend the non-generic `BaseLogicArray`, which synthesis and hierarchy utilities use when the element and value types are irrelevant. + +For example, these sample hardware and value types preserve named fields in hardware while exposing typed snapshots: ```dart class Sample extends LogicStructure { @@ -44,34 +46,58 @@ class Sample extends LogicStructure { Sample clone({String? name}) => Sample(name: name ?? this.name); } -final samples = LogicArrayOf( +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, dimensionNames: ['row_', 'column_'], ); final bottomRightData = samples.at([1, 2]).data; +final TypedValueArray currentSamples = samples.value; ``` -The element builder must always produce the configured type, width, and recursively ordered net composition. Every element must be uniformly variable or uniformly net: a structure cannot mix `Logic` and `LogicNet` leaves. A zero-sized array calls the builder once as a prototype so that the same metadata and validation remain available even though the array has no positions. +The element builder must always produce the configured type, width, and recursively ordered net composition. Every element must be uniformly variable or uniformly net: a structure cannot mix `Logic` and `LogicNet` leaves. The optional `elementCompatibility` callback can additionally reject elements whose semantic representations differ from the prototype, such as floating-point elements with different exponent and mantissa layouts despite having the same total width. + +A zero-sized array calls the builder once as a prototype so that the same metadata and validation remain available even though the array has no positions. Generic zero-sized arrays retain the prototype's element width; `LogicArray` retains its historical behavior of reporting an element width of zero for an empty shape. + +`valueCodec` may be omitted when `TValue` is exactly `LogicValue`; the canonical identity codec is selected automatically. Other semantic types require a codec. Since hardware may contain `X` and `Z`, a codec used by `TypedLogicArray` should decode every four-state value that can appear in that hardware. + +### Hardware shape transformations + +`reshape` and `transpose2D` construct new typed hardware and connect it to the source array; they do not create aliases or views. `reshape` preserves row-major element order and clamps `numUnpackedDimensions` to the new rank when necessary. It retains dimension names when the rank is unchanged and otherwise generates default names. `transpose2D` swaps the two dimensions and their names while retaining `numUnpackedDimensions`. ### Traversal boundaries These APIs intentionally stop at different boundaries: - `elements` contains the immediate children of the outermost structure or array level. -- `LogicArrayOf.arrayElements` traverses exactly the dimensions declared by that array and then stops at `T`. It is an unmodifiable `List`. +- `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. -When typed array elements are themselves arrays, use `flattenNestedDimensions()` to create one rectangular `LogicArrayOf` with all nested dimensions concatenated. The full address is preserved: `nested.at(outerIndices).at(innerIndices)` maps to `flattened.at([...outerIndices, ...innerIndices])`. Every sibling nested array must have matching dimensions, element width, depth, and unpacked-dimension configuration. Empty shapes are supported because the configured element builders supply the missing metadata. +When typed array elements are themselves arrays, use `flattenNestedDimensions(valueCodec: ...)` to create one rectangular `TypedLogicArray` with all nested dimensions concatenated. The supplied codec must be the identical codec stored by the innermost array elements. The full address is preserved: `nested.at(outerIndices).at(innerIndices)` maps to `flattened.at([...outerIndices, ...innerIndices])`. Every sibling nested array must have matching dimensions, element width, depth, and unpacked-dimension configuration. Empty shapes are supported because the configured element builders supply the missing metadata. -A `LogicArrayOf` element can be a `LogicStructure`, but it must be driveable. Direct `Const` elements, structures containing a `Const`, and element structures containing nested arrays are rejected. An array can instead directly contain another array and then be flattened as described above. +A `TypedLogicArray` element can be a `LogicStructure`, but it must be driveable. Direct `Const` elements, structures containing a `Const`, and element structures containing nested arrays are rejected. An array can instead directly contain another array and then be flattened as described above. -`LogicArrayOf` is also the supported base for custom typed arrays. A subclass using the protected `LogicArrayOf.structured` constructor must supply a correctly configured element builder and should override `createClone` to preserve its runtime type and metadata. +`TypedLogicArray` is also the supported base for custom typed arrays. A subclass using the protected `TypedLogicArray.structured` constructor must supply a correctly configured element builder and value codec, and should override `createClone` to preserve its runtime type and metadata. ## Value-domain arrays @@ -98,13 +124,13 @@ final signals = values.toLogicArray(name: 'values'); Nested constructors reject ragged rows, inconsistent nesting depth, and mismatched element widths. Empty nested input cannot reveal the element width or trailing dimensions, so it must use `fromFlat`. `majorSlices` iterates the outer dimension rather than the total element count, so a `[2, 0]` value contains two empty `[0]` slices and can round-trip through `stack`. -`LogicValueArrayOf` adds a `LogicValueCodec` for application-level values. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. +`TypedValueArray` adds a `LogicValueCodec` for application-level values. `LogicValueArray` remains its `TypedValueArray` specialization with the existing convenience constructors and concrete transform return types. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. The root list of a nested constructor always represents an array dimension. Below the root, an object matching `T` is treated as one semantic value before it is considered as another list dimension. This permits list-valued semantic elements; use `fromFlat` when the intended interpretation would otherwise be ambiguous. Both value-array classes are `LogicValue`s. Their `width` and deprecated `length` count packed bits, while `elementCount` counts array positions. Bit indexing, equality, hashing, arithmetic, and bitwise operations use the packed value and do not consider shape. The `packed` getter exposes the ordinary `LogicValue` representation. -`LogicArrayOf.value` captures a `LogicValueArray` without adding hardware to the graph. A compatible shaped value can drive a hardware array through `put`, `inject`, or `putInto`; ordinary same-width packed `LogicValue` assignment remains supported. +`TypedLogicArray.value` and `previousValue` return `TypedValueArray` snapshots without adding hardware to the graph. `LogicArray` overrides these with the concrete `LogicValueArray` return type. The standard `changed`, `glitch`, and edge APIs remain packed `LogicValueChanged` events, so typed arrays retain the normal `Logic` event contract. Since all value arrays are `LogicValue`s, use the target-side `put` API for immediate assignment or `inject` for scheduled assignment. Both follow the ordinary packed-value contract, so same-width values remain assignable regardless of their shape metadata. ## Unpacked arrays @@ -127,7 +153,7 @@ 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 `addTypedInput` and `addTypedOutput` for `LogicArrayOf` ports. These methods preserve the array's specialized leaf type, net kind, dimensions, and unpacked-dimension configuration, allowing the module to access fields such as `samples.at([1, 2]).data` directly. Net-typed leaves are also accepted by typed inout ports. +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 diff --git a/lib/src/interfaces/interface.dart b/lib/src/interfaces/interface.dart index 43de17ebd1..4e23d5512d 100644 --- a/lib/src/interfaces/interface.dart +++ b/lib/src/interfaces/interface.dart @@ -93,9 +93,9 @@ class Interface { elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ), - LogicArrayOf() => module.addTypedInput( + BaseLogicArray() => module.addTypedInput( uniquify(port.name), - srcInterface.port(port.name) as LogicArrayOf, + srcInterface.port(port.name) as BaseLogicArray, ), _ => module.addInput( uniquify(port.name), @@ -115,7 +115,7 @@ class Interface { elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ), - LogicArrayOf() => module.addTypedOutput( + BaseLogicArray() => module.addTypedOutput( uniquify(port.name), port.clone, ), @@ -131,7 +131,7 @@ class Interface { if (inOutTags != null) { for (final port in getPorts(inOutTags).values) { - if (port is LogicArrayOf) { + if (port is BaseLogicArray) { if (!port.isNet) { throw PortTypeException( port, 'LogicArray nets must be used for inOut array ports.'); @@ -150,9 +150,9 @@ class Interface { elementWidth: port.elementWidth, numUnpackedDimensions: port.numUnpackedDimensions, ), - LogicArrayOf() => module.addTypedInOut( + BaseLogicArray() => module.addTypedInOut( uniquify(port.name), - srcInterface.port(port.name) as LogicArrayOf, + srcInterface.port(port.name) as BaseLogicArray, ), _ => module.addInOut( uniquify(port.name), diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index b27083739e..0091419aaa 100644 --- a/lib/src/interfaces/pair_interface.dart +++ b/lib/src/interfaces/pair_interface.dart @@ -108,7 +108,7 @@ class PairInterface extends Interface { p.numUnpackedDimensions) : LogicArray.port(name, p.dimensions, p.elementWidth, p.numUnpackedDimensions); - case LogicArrayOf(): + case BaseLogicArray(): return p.clone(); case LogicNet(): return LogicNet.port(name, p.width); diff --git a/lib/src/module.dart b/lib/src/module.dart index 64235cc5b8..ac1ad40a50 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -694,7 +694,9 @@ 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], the [LogicType] must be set to [Logic], since [Const]s cannot be @@ -769,7 +771,7 @@ abstract class Module { _inOutDrivers.add(source); // we need to properly detect all inout sources, even for arrays - if (source.isArrayMember || source is LogicArrayOf) { + if (source.isArrayMember || source is BaseLogicArray) { final sourceElems = TraverseableCollection()..add(source); for (var i = 0; i < sourceElems.length; i++) { final sei = sourceElems[i]; @@ -779,7 +781,7 @@ abstract class Module { sourceElems.add(sei.parentStructure!); } - if (sei is LogicArrayOf) { + if (sei is BaseLogicArray) { sourceElems.addAll(sei.elements); } } @@ -812,7 +814,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 @@ -961,7 +965,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 @@ -1054,7 +1060,7 @@ abstract class Module { sourceElems.add(sei.parentStructure!); } - if (sei is LogicArrayOf) { + if (sei is BaseLogicArray) { sourceElems.addAll(sei.elements); } } diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index 8347f6da73..1c8e6c2e6c 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -433,7 +433,7 @@ ${inputLines.join('\n')} var current = logic; while (current?.parentStructure != null) { final parentStructure = current!.parentStructure!; - if (parentStructure is LogicArrayOf && + if (parentStructure is BaseLogicArray && parentStructure.numUnpackedDimensions > 0) { return true; } diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index 119caff3d3..7815451f5c 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -133,8 +133,8 @@ class Logic { LogicStructure? get parentStructure => _parentStructure; LogicStructure? _parentStructure; - /// True if this is a direct member of a [LogicArrayOf]. - bool get isArrayMember => parentStructure is LogicArrayOf; + /// True if this is a direct member of a [BaseLogicArray]. + 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 +143,7 @@ class Logic { /// [LogicArray] or [LogicStructure]. String get structureName { if (parentStructure != null) { - if (parentStructure is LogicArrayOf) { + if (parentStructure is BaseLogicArray) { return '${parentStructure!.structureName}[${arrayIndex!}]'; } else { return '${parentStructure!.structureName}.$name'; diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 79be2b14ff..bf3432aaa5 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -9,8 +9,11 @@ part of 'signals.dart'; -/// Shared private implementation for multidimensional logic arrays. -class _BaseLogicArray extends LogicStructure { +/// Shared structural base for multidimensional logic arrays. +/// +/// Construct [LogicArray] for ordinary logic values or [TypedLogicArray] for +/// hardware elements with an associated semantic value type. +abstract class BaseLogicArray extends LogicStructure { /// The number of elements at each level of the array, starting from the most /// significant outermost level. /// @@ -20,8 +23,8 @@ class _BaseLogicArray extends LogicStructure { /// 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. + /// An empty [LogicArray] retains its historical zero element width. + /// An empty [TypedLogicArray] retains the width of its constructed prototype. final int elementWidth; /// Elements at the leaf dimension of this array. @@ -40,7 +43,7 @@ class _BaseLogicArray extends LogicStructure { @override String toString() => [ - 'LogicArray($dimensions, $elementWidth): $name', + '$runtimeType($dimensions, $elementWidth): $name', if (isArrayMember) 'index $arrayIndex of ($parentStructure)', if (isNet) '[Net]' ].join(', '); @@ -52,51 +55,9 @@ class _BaseLogicArray extends LogicStructure { /// as a hint for [Synthesizer]s. final int numUnpackedDimensions; - /// 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. - factory _BaseLogicArray(List dimensions, int elementWidth, - {String? name, int numUnpackedDimensions = 0, Naming? naming}) => - _BaseLogicArray._factory(dimensions, elementWidth, - name: name, - numUnpackedDimensions: numUnpackedDimensions, - naming: naming, - logicBuilder: Logic.new, - logicArrayBuilder: _BaseLogicArray.new, - arrayBuilder: _BaseLogicArray._, - isNet: false); - @override final bool isNet; - /// Creates an array of [LogicNet]s 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. - factory _BaseLogicArray.net(List dimensions, int elementWidth, - {String? name, int numUnpackedDimensions = 0, Naming? naming}) => - _BaseLogicArray._factory(dimensions, elementWidth, - name: name, - numUnpackedDimensions: numUnpackedDimensions, - naming: naming, - logicBuilder: LogicNet.new, - logicArrayBuilder: _BaseLogicArray.net, - arrayBuilder: _BaseLogicArray._, - isNet: true); - /// Creates an array from pre-built [elements]. /// /// This constructor supports subclasses whose leaf dimension contains a @@ -105,7 +66,7 @@ class _BaseLogicArray extends LogicStructure { /// dimensions. For a one-dimensional array, each element must have /// [elementWidth] bits. @protected - _BaseLogicArray.structured(List elements, + BaseLogicArray.structured(List elements, {required List dimensions, required this.elementWidth, String? name, @@ -125,9 +86,15 @@ class _BaseLogicArray extends LogicStructure { throw LogicConstructionException( 'Array dimensions must be non-negative.'); } - if (numUnpackedDimensions > dimensions.length) { + if (elementWidth < 0) { + throw LogicConstructionException( + 'Array element width must be non-negative.'); + } + if (numUnpackedDimensions < 0 || + numUnpackedDimensions > dimensions.length) { throw LogicConstructionException( - 'Cannot unpack more than all of the dimensions.'); + 'The number of unpacked dimensions must be between 0 and the array ' + 'rank.'); } if (elements.length != dimensions.first) { throw LogicConstructionException( @@ -142,7 +109,7 @@ class _BaseLogicArray extends LogicStructure { } else { final childDimensions = dimensions.sublist(1); if (elements.any((element) => - element is! _BaseLogicArray || + element is! BaseLogicArray || !_sameDimensions(element.dimensions, childDimensions) || element.elementWidth != elementWidth)) { throw LogicConstructionException( @@ -155,6 +122,14 @@ class _BaseLogicArray extends LogicStructure { } } + /// 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 @@ -170,17 +145,17 @@ class _BaseLogicArray extends LogicStructure { /// /// The [logicBuilder] and [logicArrayBuilder] functions should generate /// proper types of [Logic]s as elements for the array. - factory _BaseLogicArray._factory(List dimensions, int elementWidth, + 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, + required BaseLogicArray Function(List nextDimensions, int width, {int numUnpackedDimensions, String name}) logicArrayBuilder, - required _BaseLogicArray Function(List elements, + required BaseLogicArray Function(List elements, {required List dimensions, required int elementWidth, required int numUnpackedDimensions, @@ -192,10 +167,19 @@ class _BaseLogicArray extends LogicStructure { throw LogicConstructionException( 'Arrays must have at least 1 dimension.'); } - - if (numUnpackedDimensions > dimensions.length) { + 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( - 'Cannot unpack more than all of the dimensions.'); + 'The number of unpacked dimensions must be between 0 and the array ' + 'rank.'); } // calculate the next layer's dimensions @@ -236,67 +220,20 @@ class _BaseLogicArray extends LogicStructure { isNet: isNet); } - @override - _BaseLogicArray _clone({String? name, Naming? naming}) => - _BaseLogicArray._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 ? _BaseLogicArray.net : _BaseLogicArray.new, - arrayBuilder: _BaseLogicArray._, - isNet: isNet); - - /// Creates a new array 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 - _BaseLogicArray clone({String? name}) => _clone(name: name); - - /// 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 - _BaseLogicArray named(String name, {Naming? naming}) => - _clone(name: name, naming: naming)..gets(this); - - /// Private constructor for the factory [_BaseLogicArray] constructor. - /// - /// The [name] and [naming] should have been identified before calling this. - _BaseLogicArray._(super.elements, - {required this.dimensions, - required this.elementWidth, - required this.numUnpackedDimensions, - required String super.name, - required this.naming, - required this.isNet}); - List _calculateArrayElements() => dimensions.length == 1 ? elements : elements - .cast<_BaseLogicArray>() + .cast() .expand((element) => element.arrayElements) .toList(growable: false); } /// A multi-dimensional array structure of independent [Logic]s. /// -/// This is the ordinary [Logic]-leaf specialization of [LogicArrayOf]. It has -/// the same construction, port, clone, and naming API as the historical +/// 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 LogicArrayOf { +class LogicArray extends TypedLogicArray { /// Creates an array with specified [dimensions] and [elementWidth] named /// [name]. /// @@ -340,6 +277,7 @@ class LogicArray extends LogicArrayOf { ({name}) => isNet ? LogicNet(name: name, width: elementWidth) : Logic(name: name, width: elementWidth), + valueCodec: LogicValueCodec.logicValue, isNet: isNet, ); @@ -353,7 +291,7 @@ class LogicArray extends LogicArrayOf { required LogicArray Function(List nextDimensions, int width, {int numUnpackedDimensions, String name}) logicArrayBuilder}) => - _BaseLogicArray._factory(dimensions, elementWidth, + BaseLogicArray._factory(dimensions, elementWidth, name: name, numUnpackedDimensions: numUnpackedDimensions, naming: naming, @@ -362,6 +300,19 @@ class LogicArray extends LogicArrayOf { arrayBuilder: LogicArray._, isNet: isNet) as LogicArray; + @override + LogicValueArray get value => LogicValueArray.fromLogicArray(this); + + @override + LogicValueArray? get previousValue => + arrayElements.any((element) => element.previousValue == null) + ? null + : LogicValueArray.fromFlat( + dimensions, + elementWidth, + arrayElements.map((element) => element.previousValue!), + ); + @override /// Creates a [LogicArray] with the same dimensions, element width, unpacked diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index a453cfa147..86d18e41e7 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 LogicArrayOf; + bool get isArrayMember => parentStructure is BaseLogicArray; @override void put(dynamic val, {bool fill = false}) { @@ -184,7 +184,7 @@ class LogicStructure implements Logic { LogicStructure flattenOuter({String? name, bool prefixFieldNames = true}) { final sources = <(Logic, String)>[]; for (final element in elements) { - if (element is LogicStructure && element is! LogicArrayOf) { + if (element is LogicStructure && element is! BaseLogicArray) { for (final field in element.elements) { final prefix = element.name.endsWith('_') ? element.name : '${element.name}_'; diff --git a/lib/src/signals/signals.dart b/lib/src/signals/signals.dart index 184fcdc9a3..3be7461b1b 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -21,7 +21,7 @@ part 'wire.dart'; part 'wire_net.dart'; part 'logic_structure.dart'; part 'logic_array.dart'; -part 'logic_array_of.dart'; +part 'typed_logic_array.dart'; part 'logic_net.dart'; part 'logic_enum.dart'; part 'logic_def.dart'; diff --git a/lib/src/signals/logic_array_of.dart b/lib/src/signals/typed_logic_array.dart similarity index 66% rename from lib/src/signals/logic_array_of.dart rename to lib/src/signals/typed_logic_array.dart index b111ee3779..dc5722dca1 100644 --- a/lib/src/signals/logic_array_of.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -1,7 +1,7 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// logic_array_of.dart +// typed_logic_array.dart // Definition of typed logic arrays. // // 2026 July 21 @@ -9,10 +9,28 @@ part of 'signals.dart'; -/// Builds one leaf element of a [LogicArrayOf]. -typedef LogicArrayElementBuilder = T Function({String? name}); +/// Builds one leaf element of a [TypedLogicArray]. +typedef TypedLogicArrayElementBuilder = T Function( + {String? name}); -/// A multidimensional logic array with leaves of type [T]. +/// 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]. +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]. /// /// [elements] contains the immediate children of the outermost dimension. /// [arrayElements] traverses the number of array levels declared by @@ -24,12 +42,24 @@ typedef LogicArrayElementBuilder = T Function({String? name}); /// [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. -class LogicArrayOf extends _BaseLogicArray { +/// +/// [value] and [previousValue] preserve [V] through [TypedValueArray] +/// snapshots. Standard [Logic.changed] events remain packed +/// [LogicValueChanged] events. +class TypedLogicArray extends BaseLogicArray { /// Labels used to name elements at each dimension. final List dimensionNames; /// Recreates one [T] for cloning and shape transformations. - final LogicArrayElementBuilder _elementBuilder; + 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; /// Unmodifiable typed view of the configured array-element boundary. late final List _typedArrayElements = @@ -40,28 +70,38 @@ class LogicArrayOf extends _BaseLogicArray { /// 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. + /// /// [numUnpackedDimensions] controls how many outer dimensions are emitted as /// unpacked dimensions by synthesis. - LogicArrayOf( + TypedLogicArray( List dimensions, - LogicArrayElementBuilder elementBuilder, { + TypedLogicArrayElementBuilder elementBuilder, { + LogicValueCodec? valueCodec, + TypedLogicArrayElementCompatibility? elementCompatibility, List? dimensionNames, String? name, Naming? naming, int numUnpackedDimensions = 0, }) : this._( - _LogicArrayOfBuild.build(dimensions, elementBuilder, + _TypedLogicArrayBuild.build(dimensions, elementBuilder, + _resolveTypedLogicValueCodec(valueCodec), elementCompatibility, dimensionNames: dimensionNames, numUnpackedDimensions: numUnpackedDimensions), elementBuilder, + _resolveTypedLogicValueCodec(valueCodec), + elementCompatibility, name: name, naming: naming, numUnpackedDimensions: numUnpackedDimensions); // ignore: use_super_parameters - invokes the protected structured constructor. - LogicArrayOf._( - _LogicArrayOfBuild build, - this._elementBuilder, { + TypedLogicArray._( + _TypedLogicArrayBuild build, + this._elementBuilder, + this.valueCodec, + this._elementCompatibility, { required int numUnpackedDimensions, super.name, Naming? naming, @@ -75,17 +115,20 @@ class LogicArrayOf extends _BaseLogicArray { /// Creates a typed array from structurally compatible [elements]. @protected - LogicArrayOf.structured( + 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, - }) : dimensionNames = + }) : _elementCompatibility = elementCompatibility, + dimensionNames = _normalizeLogicArrayDimensionNames(dimensions, dimensionNames), super.structured( name: name, @@ -117,7 +160,7 @@ class LogicArrayOf extends _BaseLogicArray { } /// Connects row-major [arrayElements] to [sources]. - LogicArrayOf getsEach(Iterable sources) { + TypedLogicArray getsEach(Iterable sources) { for (final (target, source) in _zipExact(arrayElements, sources)) { target <= source; } @@ -126,7 +169,8 @@ class LogicArrayOf extends _BaseLogicArray { } /// Connects each array element to a value generated from its indices. - LogicArrayOf getsGenerated(Logic Function(List indices) generator) { + TypedLogicArray getsGenerated( + Logic Function(List indices) generator) { for (final (indices, target) in indexedElements) { target <= generator(indices); } @@ -134,13 +178,20 @@ class LogicArrayOf extends _BaseLogicArray { return this; } - /// Returns a typed row-major view with [newDimensions]. - LogicArrayOf reshape(List newDimensions, {String? name}) { + /// Constructs connected hardware with [newDimensions] in row-major order. + /// + /// The returned array contains new elements driven by this array; it is not + /// an alias or view. [numUnpackedDimensions] is retained when the rank grows + /// and clamped to the new rank when it shrinks. Dimension names are retained + /// only when the rank is unchanged. + TypedLogicArray reshape(List newDimensions, {String? name}) { if (_arrayLength(newDimensions) != arrayElements.length) { throw ArgumentError.value(newDimensions, 'newDimensions', 'Must contain ${arrayElements.length} array elements.'); } - return LogicArrayOf(newDimensions, _elementBuilder, + return TypedLogicArray(newDimensions, _elementBuilder, + valueCodec: valueCodec, + elementCompatibility: _elementCompatibility, dimensionNames: newDimensions.length == dimensions.length ? dimensionNames : _defaultDimensionNames(newDimensions.length), @@ -149,10 +200,16 @@ class LogicArrayOf extends _BaseLogicArray { ..getsEach(arrayElements); } - /// Transposes this two-dimensional array while preserving [T]. - LogicArrayOf transpose2D({String? name}) { + /// Constructs a connected two-dimensional transpose while preserving [T]. + /// + /// The returned array contains new elements, swaps the dimension names, and + /// retains [numUnpackedDimensions]. + TypedLogicArray transpose2D({String? name}) { _checkArrayIsTwoDimensional(dimensions); - return LogicArrayOf([dimensions[1], dimensions[0]], _elementBuilder, + return TypedLogicArray( + [dimensions[1], dimensions[0]], _elementBuilder, + valueCodec: valueCodec, + elementCompatibility: _elementCompatibility, dimensionNames: [dimensionNames[1], dimensionNames[0]], name: name, numUnpackedDimensions: numUnpackedDimensions) @@ -160,11 +217,11 @@ class LogicArrayOf extends _BaseLogicArray { } /// Immediate typed child arrays along the first dimension. - Iterable> get majorSlices { + Iterable> get majorSlices { if (dimensions.length < 2) { throw StateError('majorSlices requires at least two dimensions.'); } - return elements.cast>(); + return elements.cast>(); } /// Flattens all nested array dimensions into one typed array of [U] leaves. @@ -172,23 +229,29 @@ class LogicArrayOf extends _BaseLogicArray { /// Each nested array layer must be rectangular: sibling arrays must have the /// same dimensions and element width. The returned dimensions concatenate /// every nested layer, preserving row-major ordering and index addresses. - LogicArrayOf flattenNestedDimensions({String? name}) { + TypedLogicArray flattenNestedDimensions({ + required LogicValueCodec valueCodec, + TypedLogicArrayElementCompatibility? elementCompatibility, + String? name, + }) { var leaves = arrayElements.cast().toList(growable: false); final flattenedDimensions = [...dimensions]; var flattenedUnpackedDimensions = numUnpackedDimensions; - LogicArrayElementBuilder flattenedElementBuilder = _elementBuilder; + TypedLogicArrayElementBuilder flattenedElementBuilder = + _elementBuilder; + Object discoveredValueCodec = this.valueCodec; var prototype = leaves.isEmpty ? flattenedElementBuilder(name: 'flatten_prototype') : leaves.first; - while (prototype is LogicArrayOf) { + while (prototype is TypedLogicArray) { if (leaves.isNotEmpty && - leaves.any((leaf) => leaf is! LogicArrayOf)) { + leaves.any((leaf) => leaf is! TypedLogicArray)) { throw LogicConstructionException( 'Nested array leaves must have a uniform depth.'); } - final arrays = leaves.cast>(); + final arrays = leaves.cast>(); final reference = arrays.isEmpty ? prototype : arrays.first; if (arrays.any((array) => !_sameDimensions(array.dimensions, reference.dimensions) || @@ -210,6 +273,7 @@ class LogicArrayOf extends _BaseLogicArray { leaves = arrays.expand((array) => array.arrayElements).toList(growable: false); flattenedElementBuilder = reference._elementBuilder; + discoveredValueCodec = reference.valueCodec; prototype = leaves.isEmpty ? flattenedElementBuilder(name: 'flatten_prototype') : leaves.first; @@ -224,6 +288,10 @@ class LogicArrayOf extends _BaseLogicArray { throw LogicConstructionException( 'Nested array leaves must have matching widths.'); } + if (!identical(discoveredValueCodec, valueCodec)) { + throw LogicConstructionException( + 'valueCodec must be identical to the flattened element codec.'); + } var returnPrototype = leaves.isEmpty; U buildFlattenedElement({String? name}) { @@ -237,40 +305,35 @@ class LogicArrayOf extends _BaseLogicArray { return element; } - return LogicArrayOf( + return TypedLogicArray( flattenedDimensions, buildFlattenedElement, + valueCodec: valueCodec, + elementCompatibility: elementCompatibility, name: name, numUnpackedDimensions: flattenedUnpackedDimensions, )..getsEach(leaves); } @override - LogicValueArray get value => LogicValueArray.fromLogicArray(this); + TypedValueArray get value => TypedValueArray.fromPacked( + dimensions, + elementWidth, + arrayElements.map((element) => element.value), + codec: valueCodec, + ); @override - LogicValueArray? get previousValue => + TypedValueArray? get previousValue => arrayElements.any((element) => element.previousValue == null) ? null - : LogicValueArray.fromFlat( + : TypedValueArray.fromPacked( dimensions, elementWidth, arrayElements.map((element) => element.previousValue!), + codec: valueCodec, ); - @override - void put(dynamic val, {bool fill = false}) { - if (val is LogicValueArrayOf) { - if (fill) { - throw ArgumentError.value( - fill, 'fill', 'Cannot fill an array from a shaped value.'); - } - val.putInto(this); - return; - } - super.put(val, fill: fill); - } - /// Packs typed leaves into a conventional [LogicArray]. LogicArray toLogicArray({String? name}) => (isNet ? LogicArray.net : LogicArray.new)( @@ -300,13 +363,19 @@ class LogicArrayOf extends _BaseLogicArray { } /// Creates a clone while allowing subclasses to preserve their runtime type. + /// + /// Subclasses with additional metadata should override this method and + /// reconstruct that metadata together with [dimensions], [dimensionNames], + /// [elementWidth], [isNet], and [numUnpackedDimensions]. @protected - LogicArrayOf createClone({ + TypedLogicArray createClone({ String? name, Naming? naming, int? numUnpackedDimensions, }) => - LogicArrayOf(dimensions, _elementBuilder, + TypedLogicArray(dimensions, _elementBuilder, + valueCodec: valueCodec, + elementCompatibility: _elementCompatibility, dimensionNames: dimensionNames, name: name ?? this.name, naming: naming, @@ -314,7 +383,7 @@ class LogicArrayOf extends _BaseLogicArray { numUnpackedDimensions ?? this.numUnpackedDimensions); @override - LogicArrayOf _clone({String? name, Naming? naming}) => createClone( + TypedLogicArray _clone({String? name, Naming? naming}) => createClone( name: name, naming: Naming.chooseCloneNaming( originalName: this.name, @@ -326,10 +395,10 @@ class LogicArrayOf extends _BaseLogicArray { ); @override - LogicArrayOf clone({String? name}) => _clone(name: name); + TypedLogicArray clone({String? name}) => _clone(name: name); @override - LogicArrayOf named(String name, {Naming? naming}) => _clone( + TypedLogicArray named(String name, {Naming? naming}) => _clone( name: name, naming: Naming.chooseCloneNaming( originalName: this.name, @@ -340,8 +409,8 @@ class LogicArrayOf extends _BaseLogicArray { )..gets(this); } -/// Validated construction data prepared before initializing [LogicArrayOf]. -class _LogicArrayOfBuild { +/// Validated construction data prepared before initializing [TypedLogicArray]. +class _TypedLogicArrayBuild { /// Validated array dimensions. final List dimensions; @@ -358,20 +427,24 @@ class _LogicArrayOfBuild { final bool isNet; /// Stores construction data after all validation is complete. - _LogicArrayOfBuild._(this.dimensions, this.dimensionNames, this.elements, + _TypedLogicArrayBuild._(this.dimensions, this.dimensionNames, this.elements, this.elementWidth, this.isNet); /// Builds and validates the complete typed-array hierarchy. - factory _LogicArrayOfBuild.build( - List dimensions, LogicArrayElementBuilder elementBuilder, + factory _TypedLogicArrayBuild.build( + List dimensions, + TypedLogicArrayElementBuilder elementBuilder, + LogicValueCodec valueCodec, + TypedLogicArrayElementCompatibility? elementCompatibility, {List? dimensionNames, int numUnpackedDimensions = 0, - T? emptyPrototype}) { + T? emptyPrototype, + bool validateValueCodec = true}) { final normalizedDimensions = List.unmodifiable(dimensions); if (normalizedDimensions.isEmpty || normalizedDimensions.any((dimension) => dimension < 0)) { throw LogicConstructionException( - 'LogicArrayOf dimensions must be non-negative.'); + 'TypedLogicArray dimensions must be non-negative.'); } if (numUnpackedDimensions < 0 || numUnpackedDimensions > normalizedDimensions.length) { @@ -390,15 +463,20 @@ class _LogicArrayOfBuild { final elementName = '${normalizedNames.first}$index'; return normalizedDimensions.length == 1 ? elementBuilder(name: elementName) - : LogicArrayOf._( - _LogicArrayOfBuild.build( + : 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), ); @@ -406,30 +484,30 @@ class _LogicArrayOfBuild { final typedLeaves = normalizedDimensions.length == 1 ? elements.cast().toList(growable: false) : elements - .cast>() + .cast>() .expand((element) => element.arrayElements) .toList(growable: false); if (typedLeaves.any((leaf) => leaf is LogicStructure && - leaf is! LogicArrayOf && + leaf is! BaseLogicArray && _containsNestedArray(leaf))) { throw LogicConstructionException( - 'LogicArrayOf leaves cannot contain nested LogicArrayOf fields.'); + 'TypedLogicArray leaves cannot contain nested typed array fields.'); } if (typedLeaves.any(_containsUnassignableLeaf)) { - throw LogicConstructionException( - 'LogicArrayOf leaves must be driveable and cannot contain Consts.'); + throw LogicConstructionException('TypedLogicArray leaves must be ' + 'driveable and cannot contain Consts.'); } final prototype = typedLeaves.isEmpty ? emptyPrototype! : typedLeaves.first; if (_containsUnassignableLeaf(prototype)) { - throw LogicConstructionException( - 'LogicArrayOf leaves must be driveable and cannot contain Consts.'); + throw LogicConstructionException('TypedLogicArray leaves must be ' + 'driveable and cannot contain Consts.'); } if (prototype is LogicStructure && - prototype is! LogicArrayOf && + prototype is! BaseLogicArray && _containsNestedArray(prototype)) { throw LogicConstructionException( - 'LogicArrayOf leaves cannot contain nested LogicArrayOf fields.'); + 'TypedLogicArray leaves cannot contain nested typed array fields.'); } final prototypeNetComposition = _netComposition(prototype); final elementsForNetValidation = @@ -439,25 +517,36 @@ class _LogicArrayOfBuild { return composition.contains(true) && composition.contains(false); })) { throw LogicConstructionException( - 'LogicArrayOf elements cannot mix net and non-net leaves.'); + 'TypedLogicArray elements cannot mix net and non-net leaves.'); } if (elementsForNetValidation.any((element) => !_sameNetComposition( _netComposition(element), prototypeNetComposition))) { throw LogicConstructionException( - 'All LogicArrayOf elements must have matching net composition.'); + 'All TypedLogicArray elements must have matching net composition.'); + } + if (elementCompatibility != null && + elementsForNetValidation + .cast() + .any((element) => !elementCompatibility(prototype, element))) { + throw LogicConstructionException( + 'All TypedLogicArray elements must have compatible value formats.'); } - return _LogicArrayOfBuild._( + final elementWidth = _validateElementWidths(typedLeaves, prototype.width); + if (validateValueCodec) { + _validateValueCodec(prototype.value, elementWidth, valueCodec); + } + return _TypedLogicArrayBuild._( normalizedDimensions, normalizedNames, elements, - _validateElementWidths(typedLeaves, prototype.width), + elementWidth, prototypeNetComposition.every((isNet) => isNet)); } /// Whether [structure] recursively contains an array field. static bool _containsNestedArray(LogicStructure structure) => structure.elements.any((element) => - element is LogicArrayOf || + element is BaseLogicArray || (element is LogicStructure && _containsNestedArray(element))); /// Whether [leaf] is or recursively contains an unassignable constant. @@ -476,7 +565,7 @@ class _LogicArrayOfBuild { .map((leaf) => leaf.isNet) .toList(growable: false); } - return [element is LogicArrayOf && element.isNet]; + return [element is BaseLogicArray && element.isNet]; } /// Whether two recursive net-kind signatures are identical. @@ -489,28 +578,34 @@ class _LogicArrayOfBuild { List elements, int expectedWidth) { if (elements.any((element) => element.width != expectedWidth)) { throw LogicConstructionException( - 'All LogicArrayOf leaves must have the same width.'); + '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.'); + } + } } /// Pairs [leftValues] and [rightValues], rejecting unequal lengths. -Iterable<(T, U)> _zipExact( - Iterable leftValues, Iterable rightValues) sync* { - final left = leftValues.iterator; - final right = rightValues.iterator; - while (true) { - final hasLeft = left.moveNext(); - final hasRight = right.moveNext(); - if (hasLeft != hasRight) { - throw StateError('Cannot zip iterables of different lengths.'); - } - if (!hasLeft) { - return; - } - yield (left.current, right.current); +List<(T, U)> _zipExact(Iterable leftValues, Iterable rightValues) { + final left = leftValues.toList(growable: false); + final right = rightValues.toList(growable: false); + if (left.length != right.length) { + throw StateError('Cannot zip iterables of different lengths.'); } + return List.generate( + left.length, + (index) => (left[index], right[index]), + growable: false, + ); } /// Returns the number of array positions described by [dimensions]. @@ -550,7 +645,7 @@ List _normalizeLogicArrayDimensionNames( return normalized; } -/// Rejects shapes that cannot be transposed by [LogicArrayOf.transpose2D]. +/// Rejects shapes that cannot be transposed by [TypedLogicArray.transpose2D]. void _checkArrayIsTwoDimensional(List dimensions) { if (dimensions.length != 2) { throw StateError('Expected exactly two dimensions, got $dimensions.'); diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index ae40fb87f5..9208e02334 100644 --- a/lib/src/synthesizers/netlist/netlist_module_translation.dart +++ b/lib/src/synthesizers/netlist/netlist_module_translation.dart @@ -131,7 +131,7 @@ class NetlistModuleTranslation { final portLogic = modulePorts[portName]; final emitOutputArrayConcat = direction == NetlistPortDirection.output && - portLogic is LogicArrayOf && + portLogic is BaseLogicArray && !_hasExistingOutputArrayConcat(synthLogic) && !_hasDirectSubmoduleOutputDriver(synthLogic); final originalIds = getIds(synthLogic); @@ -168,7 +168,7 @@ class NetlistModuleTranslation { /// Emits a concat cell that assembles a LogicArray output port. void _emitOutputArrayConcat( String portName, - LogicArrayOf array, + BaseLogicArray array, List outputIds, ) { _emitOutputArrayConcatForArray(portName, array, outputIds); @@ -177,7 +177,7 @@ class NetlistModuleTranslation { /// Recursively emits concat cells for nested LogicArray output elements. bool _emitOutputArrayConcatForArray( String concatName, - LogicArrayOf array, + BaseLogicArray array, List outputIds, ) { final definition = synthDef; @@ -195,7 +195,7 @@ class NetlistModuleTranslation { return false; } var elementIds = getIds(synthLogic); - if (element is LogicArrayOf && + if (element is BaseLogicArray && !_hasExistingOutputArrayConcat(synthLogic) && !_hasDirectSubmoduleOutputDriver(synthLogic)) { final aggregateIds = List.generate( @@ -210,7 +210,7 @@ class NetlistModuleTranslation { return false; } elementIds = aggregateIds; - } else if (element is LogicStructure) { + } else if (element is LogicStructure && element is! BaseLogicArray) { _emitOutputArrayStructurePack( '${concatName}_$index', element, @@ -436,7 +436,7 @@ class NetlistModuleTranslation { for (final portEntry in submodule.inputs.entries) { final portName = portEntry.key; final port = portEntry.value; - if (port is! LogicArrayOf || + 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 a5bcd25e8b..118ddde304 100644 --- a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -19,14 +19,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 +39,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(LogicArrayOf array) { + void addNestedArrays(BaseLogicArray array) { for (final element in array.elements) { - if (element is LogicArrayOf) { + if (element is BaseLogicArray) { excludedArrays.add(element); addNestedArrays(element); } } } - >{ + { ...portArrays, ...subModuleOutputArrays, }.forEach(addNestedArrays); @@ -68,7 +68,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 +81,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { } /// Adds slice cells that decompose a LogicArray port into element signals. - void _subsetReceiveArrayPort(LogicArrayOf port) { + void _subsetReceiveArrayPort(BaseLogicArray port) { final portSynth = getSynthLogic(port)!; var index = 0; @@ -101,7 +101,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { ..setInputMapping(subsetModule.original.name, portSynth) ..pickName(module); - if (element is LogicArrayOf) { + if (element is BaseLogicArray) { _subsetReceiveArrayPort(element); } @@ -110,7 +110,7 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { } /// Adds a concat cell that assembles independent LogicArray element signals. - void _concatAssembleArray(LogicArrayOf 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 732030325f..00f6b06ce9 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -164,8 +164,7 @@ class NetlistSynthesizer extends Synthesizer { void aliasArrayChildren(SynthLogic src, SynthLogic dst) { final srcLogic = src.logics.firstOrNull; final dstLogic = dst.logics.firstOrNull; - if (srcLogic is! LogicArrayOf || - dstLogic is! LogicArrayOf) { + if (srcLogic is! BaseLogicArray || dstLogic is! BaseLogicArray) { return; } if (srcLogic.elements.length != dstLogic.elements.length) { @@ -182,8 +181,8 @@ class NetlistSynthesizer extends Synthesizer { final srcElementLogic = srcElementSynth.logics.firstOrNull; final dstElementLogic = dstElementSynth.logics.firstOrNull; - if (srcElementLogic is LogicArrayOf && - dstElementLogic is LogicArrayOf) { + if (srcElementLogic is BaseLogicArray && + dstElementLogic is BaseLogicArray) { aliasArrayChildren(srcElementSynth, dstElementSynth); } @@ -233,15 +232,15 @@ 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! LogicArrayOf) { + if (elem is LogicStructure && elem is! BaseLogicArray) { addStructAndDescendants(elem, set); } } } - void addArrayStructureElements(LogicArrayOf array) { + void addArrayStructureElements(BaseLogicArray array) { for (final element in array.elements) { - if (element is LogicArrayOf) { + if (element is BaseLogicArray) { addArrayStructureElements(element); } else if (element is LogicStructure) { addStructAndDescendants(element, outputStructPortLogics); @@ -250,7 +249,7 @@ class NetlistSynthesizer extends Synthesizer { } module.outputs.values - .whereType>() + .whereType() .forEach(addArrayStructureElements); for (final pa @@ -267,7 +266,7 @@ class NetlistSynthesizer extends Synthesizer { // current module.) final isSubModuleInputStructPort = !isCurrentModuleOutputPort && pa.dst.logics - .any((l) => l is LogicStructure && l is! LogicArrayOf); + .any((l) => l is LogicStructure && l is! BaseLogicArray); if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { // Record as pending compose cell instead of aliasing. @@ -282,7 +281,7 @@ 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! LogicArrayOf) { + if (l is LogicStructure && l is! BaseLogicArray) { addStructAndDescendants(l, outputStructPortLogics); } } @@ -356,7 +355,7 @@ class NetlistSynthesizer extends Synthesizer { ) .map((e) => e.key) .firstOrNull; - if (logic != null && logic is LogicArrayOf) { + if (logic != null && logic is BaseLogicArray) { arraysWithExplicitCells.add(logic); } // Also check the resolved replacement chain. @@ -365,7 +364,7 @@ class NetlistSynthesizer extends Synthesizer { .where((e) => e.value == resolved) .map((e) => e.key) .firstOrNull; - if (logic2 != null && logic2 is LogicArrayOf) { + if (logic2 != null && logic2 is BaseLogicArray) { arraysWithExplicitCells.add(logic2); } } @@ -379,7 +378,7 @@ class NetlistSynthesizer extends Synthesizer { ) .map((e) => e.key) .firstOrNull; - if (logic != null && logic is LogicArrayOf) { + if (logic != null && logic is BaseLogicArray) { arraysWithExplicitCells.add(logic); } } @@ -394,7 +393,7 @@ class NetlistSynthesizer extends Synthesizer { final parentSL = entry.value; final parentIds = getIds(parentSL); - if (logic is LogicArrayOf) { + if (logic is BaseLogicArray) { // Skip aliasing for arrays that have explicit $slice/$concat cells. if (arraysWithExplicitCells.contains(logic)) { continue; @@ -443,7 +442,7 @@ class NetlistSynthesizer extends Synthesizer { fullParentIds: parentIds, )); } - } else if (elem is LogicStructure && elem is! LogicArrayOf) { + } 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 diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 99ff22b637..51512dd692 100644 --- a/lib/src/synthesizers/netlist/netlist_utils.dart +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -424,7 +424,7 @@ abstract class NetlistUtils { Logic logic, [ List? bits, ]) { - if (logic is LogicArrayOf) { + if (logic is BaseLogicArray) { final result = { 'width': logic.width, 'arrayDims': logic.dimensions, @@ -434,9 +434,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! LogicArrayOf) { + if (first is LogicStructure && first is! BaseLogicArray) { result['elementType'] = buildLogicType(first); - } else if (first is LogicArrayOf) { + } else if (first is BaseLogicArray) { // Nested array — encode inner dimensions via recursive call. result['elementType'] = buildLogicType(first); } @@ -447,13 +447,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! LogicArrayOf) { + if (e is LogicStructure && e is! BaseLogicArray) { return { 'name': e.name, if (fieldBits != null) 'bits': fieldBits, 'type': buildLogicType(e, fieldBits), }; - } else if (e is LogicArrayOf) { + } else if (e is BaseLogicArray) { return { 'name': e.name, 'width': e.width, @@ -483,7 +483,7 @@ abstract class NetlistUtils { final logics = sl.logics; return logics .whereType() - .where((l) => l is! LogicArrayOf) + .where((l) => l is! BaseLogicArray) .firstOrNull ?? logics.firstOrNull; } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 25b288a929..c9e2ff6468 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -519,7 +519,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 +527,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { .toList(); final aggregateLogic = agg.logics.singleOrNull; - final isPackedBitArray = aggregateLogic is LogicArrayOf && + final isPackedBitArray = aggregateLogic is BaseLogicArray && aggregateLogic.dimensions.length == 1 && aggregateLogic.elementWidth == 1 && aggregateLogic.numUnpackedDimensions == 0; @@ -1322,7 +1322,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } final allElements = parentArray.logics - .whereType>() + .whereType() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1487,9 +1487,8 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } bool elementsAllAbsent(SynthLogic parentArray) => - parentArray.logics.every((logic) => !(logic as LogicArrayOf) - .elements - .any(logicHasPresentSynthLogic)); + parentArray.logics.every((logic) => + !(logic as BaseLogicArray).elements.any(logicHasPresentSynthLogic)); var droppedAny = true; while (droppedAny) { diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index f0f7383126..dfba7ecf49 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -11,16 +11,16 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; /// A [Swizzle] used by synthesis backends to explicitly assemble a -/// [LogicArrayOf] 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 LogicArrayOf _destination; + final BaseLogicArray _destination; /// Creates a synthesis array concatenation from [signals]. - SynthArrayConcat(super.signals, {required LogicArrayOf 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 1101612476..f03761c013 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -11,7 +11,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; /// A [BusSubset] used by synthesis backends to explicitly extract a -/// [LogicArrayOf] 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 7ede6a0642..227a1b4169 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -173,8 +173,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 LogicArrayOf).isNet); + logics.first.isNet || (isArray && (logics.first as BaseLogicArray).isNet); /// Whether this represents an enum. bool get isEnum => characteristicEnum != null; @@ -270,7 +269,7 @@ class SynthLogic { /// without making the resulting signal clearable. bool get mergeable => !hasPreservedName && _constLogic == null; - /// True only if this represents a [LogicArrayOf]. + /// True only if this represents a [BaseLogicArray]. final bool isArray; /// The chosen name of this. @@ -624,7 +623,7 @@ class SynthLogic { final logic = logics.first; if (isArray) { - final logicArr = logic as LogicArrayOf; + final logicArr = logic as BaseLogicArray; final packedDimsBuf = StringBuffer(); final unpackedDimsBuf = StringBuffer(); @@ -832,7 +831,7 @@ class SynthLogicPackedRangeReference extends SynthLogic { } } -/// Represents an element of a [LogicArrayOf]. +/// Represents an element of a [BaseLogicArray]. /// /// Does not fully override or properly implement all characteristics of /// [SynthLogic], so this should be used cautiously. @@ -916,13 +915,13 @@ class SynthLogicArrayElement extends SynthLogic { /// The element of the [parentArray]. final Logic logic; - /// Creates an instance of an element of a [LogicArrayOf]. + /// 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 LogicArrayOf', + 'Should only be used for elements in a BaseLogicArray', ), super(logic) { // make sure we have created the synthLogic for the parent array @@ -935,7 +934,7 @@ class SynthLogicArrayElement extends SynthLogic { ' logics contained: ${logics.map((e) => e.name).toList()}'; } -/// Represents a scalar field within a structured [LogicArrayOf] element. +/// 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. @@ -948,7 +947,7 @@ class SynthLogicArrayStructureElement extends SynthLogic { var current = logic; var parent = current.parentStructure; while (parent != null) { - if (parent is LogicArrayOf) { + if (parent is BaseLogicArray) { current = parent; parent = current.parentStructure; } else { @@ -964,7 +963,7 @@ class SynthLogicArrayStructureElement extends SynthLogic { var current = logic; var parent = current.parentStructure; while (parent != null) { - if (parent is LogicArrayOf) { + if (parent is BaseLogicArray) { yield current; } current = parent; @@ -1031,7 +1030,7 @@ class SynthLogicArrayStructureElement extends SynthLogic { var current = logic; var parent = current.parentStructure; while (parent != null) { - if (parent is LogicArrayOf) { + if (parent is BaseLogicArray) { return current; } current = parent; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 0a3518c523..18c9e25ae2 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -383,7 +383,7 @@ class SynthModuleDefinition { logicToSynthMap[logic] = newSynth; - if (logic is LogicArrayOf) { + if (logic is BaseLogicArray) { // if we are an array, make sure we go down the stack of elements too logic.elements.forEach(getSynthLogic); } @@ -396,9 +396,9 @@ class SynthModuleDefinition { static bool _hasTopLevelArrayAncestor(Logic logic) { var current = logic; var parent = current.parentStructure; - LogicArrayOf? rootArray; + BaseLogicArray? rootArray; while (parent != null) { - if (parent is LogicArrayOf) { + if (parent is BaseLogicArray) { rootArray = parent; } current = parent; @@ -440,8 +440,7 @@ class SynthModuleDefinition { /// the module, or for driving the input of a sub-module. @protected void _partialAssignStructPort(LogicStructure port) { - assert(port is! LogicArrayOf, - 'Should only be used on non-array structs'); + assert(port is! BaseLogicArray, 'Should only be used on non-array structs'); final portSynth = getSynthLogic(port)!; @@ -518,7 +517,7 @@ class SynthModuleDefinition { final outputSynth = getSynthLogic(output)!; outputs.add(outputSynth); - if (output is LogicStructure && output is! LogicArrayOf) { + if (output is LogicStructure && output is! BaseLogicArray) { _partialAssignStructPort(output); } } @@ -528,7 +527,7 @@ class SynthModuleDefinition { final inputSynth = getSynthLogic(input)!; inputs.add(inputSynth); - if (input is LogicStructure && input is! LogicArrayOf) { + if (input is LogicStructure && input is! BaseLogicArray) { _subsetReceiveStructPort(input); } } @@ -537,7 +536,7 @@ class SynthModuleDefinition { for (final inOut in module.inOuts.values) { inOuts.add(getSynthLogic(inOut)!); - if (inOut is LogicStructure && inOut is! LogicArrayOf) { + if (inOut is LogicStructure && inOut is! BaseLogicArray) { // for nets, we can just use the normal bus subset here in either // direction! _subsetReceiveStructPort(inOut); @@ -562,17 +561,17 @@ class SynthModuleDefinition { subModule.inputs.values .whereType() - .where((e) => e is! LogicArrayOf) + .where((e) => e is! BaseLogicArray) .forEach(_partialAssignStructPort); subModule.outputs.values .whereType() - .where((e) => e is! LogicArrayOf) + .where((e) => e is! BaseLogicArray) .forEach(_subsetReceiveStructPort); subModule.inOuts.values .whereType() - .where((e) => e is! LogicArrayOf) + .where((e) => e is! BaseLogicArray) .forEach(_subsetReceiveStructPort); } @@ -1156,7 +1155,7 @@ class SynthModuleDefinition { } candidatesByArray.forEach((parentArray, arrayCandidates) { final allElementSynthLogics = parentArray.logics - .whereType>() + .whereType() .expand((logicArray) => logicArray.elements) .map(getSynthLogic) .nonNulls @@ -1830,7 +1829,7 @@ class SynthModuleDefinition { return false; } final parentLogic = signal.parentArray.resolved.logics.singleOrNull; - return parentLogic is LogicArrayOf && + return parentLogic is BaseLogicArray && parentLogic.dimensions.length == 1 && parentLogic.elementWidth == 1 && parentLogic.numUnpackedDimensions == 0; @@ -2088,7 +2087,7 @@ class SynthModuleDefinition { } final srcLogic = srcArray.logics.first; final dstLogic = dstArray.logics.first; - if (srcLogic is! LogicArrayOf || dstLogic is! LogicArrayOf) { + if (srcLogic is! BaseLogicArray || dstLogic is! BaseLogicArray) { return false; } @@ -2480,7 +2479,7 @@ class SynthModuleDefinition { final dst = assignment.dst.resolved; if (dst.width <= 1 || - dst.logics.any((logic) => logic is LogicArrayOf)) { + dst.logics.any((logic) => logic is BaseLogicArray)) { updatedAssignments.add(assignment); continue; } @@ -2745,7 +2744,7 @@ class SynthModuleDefinition { return false; } final logic = base.logics.first; - return logic is LogicArrayOf && + return logic is BaseLogicArray && logic.dimensions.length == 1 && logic.elementWidth == 1 && logic.numUnpackedDimensions == 0; @@ -3854,7 +3853,7 @@ class SynthModuleDefinition { } final arrayLogic = parentArray.logics.first; - if (arrayLogic is! LogicArrayOf || + if (arrayLogic is! BaseLogicArray || indexedInputs.length != arrayLogic.elements.length) { continue; } @@ -3915,7 +3914,7 @@ class SynthModuleDefinition { } final logic = intermediate.logics.firstOrNull; - return logic is! LogicArrayOf || logic.numUnpackedDimensions == 0; + return logic is! BaseLogicArray || logic.numUnpackedDimensions == 0; } /// Whether [intermediate] has the unnamed packed-array shape generated by @@ -3930,8 +3929,7 @@ class SynthModuleDefinition { } final arrayLogic = intermediate.logics.singleOrNull; - return arrayLogic is LogicArrayOf && - arrayLogic.naming == Naming.unnamed; + return arrayLogic is BaseLogicArray && arrayLogic.naming == Naming.unnamed; } /// Whether [swizzleOutput] feeds a full-width disposable internal signal. @@ -3957,7 +3955,7 @@ class SynthModuleDefinition { return false; } - return dst.logics.singleOrNull is! LogicArrayOf; + return dst.logics.singleOrNull is! BaseLogicArray; }); } @@ -4151,12 +4149,12 @@ class SynthModuleDefinition { if (mergedAway.isArray) { for (final (keptElementIndex, keptElementLogic) - in (kept.logics.first as LogicArrayOf).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 LogicArrayOf) + (mergedAway.logics.first as BaseLogicArray) .elements[keptElementIndex], )!; diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart index 22898baa0c..ce89308c9b 100644 --- a/lib/src/synthesizers/utilities/synth_structure_layout.dart +++ b/lib/src/synthesizers/utilities/synth_structure_layout.dart @@ -53,7 +53,7 @@ class SynthStructureLayout { fieldPath: fieldPath, indexInParent: index, )); - if (element is LogicStructure && element is! LogicArrayOf) { + 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 66c9080b69..ed5c70f1a2 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -91,7 +91,7 @@ class Vector { final value = LogicValue.of(inputValues[signalName], width: signal.width); return '${packedInputDrivers[signalName]} = $value;'; - } else if (signal is LogicArrayOf) { + } else if (signal is BaseLogicArray) { final arrAssigns = StringBuffer(); var index = 0; final fullVal = @@ -121,7 +121,7 @@ class Vector { ); final inputStimulus = inputValues.toString(); - if (outputPort is LogicArrayOf) { + if (outputPort is BaseLogicArray) { var index = 0; for (final element in outputPort.arrayElements) { final subVal = expectedValue.getRange(index, index + element.width); @@ -709,8 +709,7 @@ abstract class SimCompare { final signal = module.signals.firstWhere((e) => e.name == signalName); final signalType = signalTypeOverride ?? - ((signal is LogicNet || - (signal is LogicArrayOf && signal.isNet)) + ((signal is LogicNet || (signal is BaseLogicArray && signal.isNet)) ? 'wire' : 'logic'); @@ -718,7 +717,7 @@ abstract class SimCompare { signalName = adjust(signalName); } - if (signal is LogicArrayOf) { + if (signal is BaseLogicArray) { final unpackedDims = signal.dimensions.getRange(0, signal.numUnpackedDimensions); final packedDims = signal.dimensions diff --git a/lib/src/values/logic_value_array.dart b/lib/src/values/logic_value_array.dart index 123440a7d3..f230809d0d 100644 --- a/lib/src/values/logic_value_array.dart +++ b/lib/src/values/logic_value_array.dart @@ -9,20 +9,12 @@ part of 'values.dart'; -LogicValue _decodeIdentityLogicValue(LogicValue value) => value; -LogicValue _encodeIdentityLogicValue(LogicValue value) => value; - -const _identityLogicValueCodec = LogicValueCodec( - decode: _decodeIdentityLogicValue, - encode: _encodeIdentityLogicValue, -); - /// 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 LogicValueArrayOf { +class LogicValueArray extends TypedValueArray { /// Creates a value array from nested [values]. factory LogicValueArray(List values) { final nested = @@ -104,8 +96,9 @@ class LogicValueArray extends LogicValueArrayOf { values.map((value) => LogicValue.ofInt(value, elementWidth)), ); - /// Captures the current values of a hardware [LogicArrayOf]. - factory LogicValueArray.fromLogicArray(LogicArrayOf values) => + /// Captures the current values of a hardware [LogicArray]. + factory LogicValueArray.fromLogicArray( + TypedLogicArray values) => LogicValueArray.fromFlat( values.dimensions, values.elementWidth, @@ -137,7 +130,7 @@ class LogicValueArray extends LogicValueArrayOf { elementWidth, values, values, - _identityLogicValueCodec, + LogicValueCodec.logicValue, ); @override @@ -161,7 +154,7 @@ class LogicValueArray extends LogicValueArrayOf { @override LogicValueArray mapMajorSlices( - LogicValueArrayOf Function(LogicValueArrayOf slice) + TypedValueArray Function(TypedValueArray slice) transform, ) { final transformed = majorSlices.map(transform).toList(growable: false); diff --git a/lib/src/values/logic_value_array_of.dart b/lib/src/values/typed_value_array.dart similarity index 79% rename from lib/src/values/logic_value_array_of.dart rename to lib/src/values/typed_value_array.dart index 618b47ef64..ad9ee0e19e 100644 --- a/lib/src/values/logic_value_array_of.dart +++ b/lib/src/values/typed_value_array.dart @@ -1,7 +1,7 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// logic_value_array_of.dart +// typed_value_array.dart // Definition of typed multi-dimensional logic value arrays. // // 2026 July 21 @@ -9,6 +9,12 @@ 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. @@ -22,6 +28,12 @@ class LogicValueCodec { 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); @@ -37,13 +49,13 @@ class LogicValueCodec { /// 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 [LogicValueArrayOf.fromFlat] when list-valued semantic data is ambiguous +/// Use [TypedValueArray.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. -class LogicValueArrayOf extends LogicValue { +class TypedValueArray extends LogicValue { /// The number of elements at each array level. final List dimensions; @@ -63,9 +75,9 @@ class LogicValueArrayOf extends LogicValue { /// Creates a typed value array from nested [values]. /// /// Empty or ragged nested input is rejected. Use - /// [LogicValueArrayOf.fromFlat] to construct an empty array because its + /// [TypedValueArray.fromFlat] to construct an empty array because its /// element width cannot be inferred. - factory LogicValueArrayOf( + factory TypedValueArray( List values, { required LogicValueCodec codec, }) { @@ -78,7 +90,7 @@ class LogicValueArrayOf extends LogicValue { final encoded = nested.values.map(codec.encode).toList(growable: false); final elementWidth = encoded.first.width; _validateElementWidths(encoded, elementWidth, 'values'); - return LogicValueArrayOf._normalized( + return TypedValueArray._normalized( nested.dimensions, elementWidth, encoded.map(codec.decode).toList(growable: false), @@ -88,7 +100,7 @@ class LogicValueArrayOf extends LogicValue { } /// Creates a typed value array from row-major [values] and explicit metadata. - factory LogicValueArrayOf.fromFlat( + factory TypedValueArray.fromFlat( List dimensions, int elementWidth, Iterable values, { @@ -100,7 +112,7 @@ class LogicValueArrayOf extends LogicValue { _validateValueCount(normalizedDimensions, semanticValues.length, 'values'); final encoded = semanticValues.map(codec.encode).toList(growable: false); _validateElementWidths(encoded, elementWidth, 'values'); - return LogicValueArrayOf._normalized( + return TypedValueArray._normalized( normalizedDimensions, elementWidth, encoded.map(codec.decode).toList(growable: false), @@ -109,24 +121,44 @@ class LogicValueArrayOf extends LogicValue { ); } + /// Decodes row-major packed [values] with [codec]. + factory TypedValueArray.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 TypedValueArray._normalized( + normalizedDimensions, + elementWidth, + packedValues.map(codec.decode).toList(growable: false), + packedValues, + codec, + ); + } + /// Decodes the packed elements of [values] with [codec]. - factory LogicValueArrayOf.fromLogicValueArray( - LogicValueArray values, { + factory TypedValueArray.fromLogicValueArray( + TypedValueArray values, { required LogicValueCodec codec, }) => - LogicValueArrayOf._normalized( + TypedValueArray.fromPacked( values.dimensions, values.elementWidth, - values._packedElements.map(codec.decode).toList(growable: false), values._packedElements, - codec, + 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 LogicValueArrayOf.stack(Iterable> arrays) { + factory TypedValueArray.stack(Iterable> arrays) { final slices = arrays.toList(growable: false); if (slices.isEmpty) { throw ArgumentError.value(arrays, 'arrays', 'Must not be empty.'); @@ -134,7 +166,7 @@ class LogicValueArrayOf extends LogicValue { final first = slices.first; slices.skip(1).forEach(first._checkStackCompatible); - return LogicValueArrayOf._normalized( + return TypedValueArray._normalized( [slices.length, ...first.dimensions], first.elementWidth, slices.expand((slice) => slice._arrayValues).toList(growable: false), @@ -143,7 +175,7 @@ class LogicValueArrayOf extends LogicValue { ); } - LogicValueArrayOf._normalized( + TypedValueArray._normalized( List dimensions, this.elementWidth, List arrayValues, @@ -180,7 +212,7 @@ class LogicValueArrayOf extends LogicValue { ); /// Slices along the first dimension. - Iterable> get majorSlices sync* { + Iterable> get majorSlices sync* { if (dimensions.length < 2) { throw StateError('majorSlices requires at least two dimensions.'); } @@ -206,8 +238,8 @@ class LogicValueArrayOf extends LogicValue { _valueArrayFlatIndex(dimensions, indices); /// Maps semantic values while preserving shape and codec. - LogicValueArrayOf map(T Function(T value) transform) => - LogicValueArrayOf.fromFlat( + TypedValueArray map(T Function(T value) transform) => + TypedValueArray.fromFlat( dimensions, elementWidth, _arrayValues.map(transform), @@ -215,10 +247,10 @@ class LogicValueArrayOf extends LogicValue { ); /// Maps semantic values with their multidimensional indices. - LogicValueArrayOf indexedMap( + TypedValueArray indexedMap( T Function(List indices, T value) transform, ) => - LogicValueArrayOf.fromFlat( + TypedValueArray.fromFlat( dimensions, elementWidth, indexedValues.map((entry) => transform(entry.$1, entry.$2)), @@ -226,13 +258,13 @@ class LogicValueArrayOf extends LogicValue { ); /// Maps slices along the first dimension and stacks the results. - LogicValueArrayOf mapMajorSlices( - LogicValueArrayOf Function(LogicValueArrayOf slice) transform, + TypedValueArray mapMajorSlices( + TypedValueArray Function(TypedValueArray slice) transform, ) => - LogicValueArrayOf.stack(majorSlices.map(transform)); + TypedValueArray.stack(majorSlices.map(transform)); - /// Returns a row-major view with [newDimensions]. - LogicValueArrayOf reshape(List newDimensions) { + /// Returns the same row-major values with [newDimensions]. + TypedValueArray reshape(List newDimensions) { final normalizedDimensions = _validateValueArrayDimensions(newDimensions); if (_valueArrayLength(normalizedDimensions) != elementCount) { throw ArgumentError.value(newDimensions, 'newDimensions', @@ -242,7 +274,7 @@ class LogicValueArrayOf extends LogicValue { } /// Transposes this two-dimensional value array. - LogicValueArrayOf transpose2D() { + TypedValueArray transpose2D() { _checkValueArrayIsTwoDimensional(dimensions); final newDimensions = [dimensions[1], dimensions[0]]; final semanticValues = []; @@ -259,26 +291,19 @@ class LogicValueArrayOf extends LogicValue { /// Creates a [LogicArray] with the same shape and drives it with this value. LogicArray toLogicArray({String? name}) => - putInto(LogicArray(dimensions, elementWidth, name: name)); + LogicArray(dimensions, elementWidth, name: name)..put(this); - /// Drives a compatible hardware [target] with this shaped value. - U putInto>(U target) { - _checkHardwareCompatible(target); - for (var index = 0; index < elementCount; index++) { - target.arrayElements[index].put(_packedElements[index]); - } - return target; - } - - LogicValueArrayOf _createStored( + /// Rebuilds a shaped value from already-normalized semantic and packed data. + TypedValueArray _createStored( List dimensions, List arrayValues, List packedElements, ) => - LogicValueArrayOf._normalized( + TypedValueArray._normalized( dimensions, elementWidth, arrayValues, packedElements, codec); - void _checkStackCompatible(LogicValueArrayOf other) { + /// Validates shape, width, and codec identity for [TypedValueArray.stack]. + void _checkStackCompatible(TypedValueArray other) { if (!identical(codec, other.codec)) { throw ArgumentError.value( other.codec, 'arrays', 'All arrays must use the identical codec.'); @@ -293,17 +318,6 @@ class LogicValueArrayOf extends LogicValue { } } - void _checkHardwareCompatible(LogicArrayOf target) { - if (elementWidth != target.elementWidth || - !_sameValueArrayDimensions(dimensions, target.dimensions)) { - throw ArgumentError.value( - target, - 'target', - 'Must have dimensions $dimensions and ' - 'elementWidth $elementWidth.'); - } - } - @override bool _equals(Object other) => other is LogicValue && packed == other.packed; @@ -381,13 +395,22 @@ class LogicValueArrayOf extends LogicValue { 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, @@ -425,6 +448,7 @@ _NestedValueArray _parseNestedArray( return parse(root, const []); } +/// Validates dimension sizes and returns an immutable shape. List _validateValueArrayDimensions(List dimensions) { final normalized = List.unmodifiable(dimensions); if (normalized.isEmpty) { @@ -437,6 +461,7 @@ List _validateValueArrayDimensions(List dimensions) { return normalized; } +/// Rejects a negative packed width for each array element. void _validateValueArrayElementWidth(int elementWidth) { if (elementWidth < 0) { throw ArgumentError.value( @@ -444,6 +469,7 @@ void _validateValueArrayElementWidth(int elementWidth) { } } +/// Requires the flat value count implied by [dimensions]. void _validateValueCount( List dimensions, int valueCount, String argumentName) { final expected = _valueArrayLength(dimensions); @@ -453,6 +479,7 @@ void _validateValueCount( } } +/// Requires every packed value to have [elementWidth] bits. void _validateElementWidths( Iterable values, int elementWidth, String argumentName) { for (final value in values) { @@ -463,9 +490,11 @@ void _validateElementWidths( } } +/// 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--) { @@ -476,6 +505,7 @@ List _valueArrayIndices(List dimensions, int flatIndex) { 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( @@ -494,10 +524,12 @@ int _valueArrayFlatIndex(List dimensions, List indices) { 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]); +/// Requires exactly two dimensions for transpose-only operations. void _checkValueArrayIsTwoDimensional(List dimensions) { if (dimensions.length != 2) { throw StateError('Expected exactly two dimensions, got $dimensions.'); diff --git a/lib/src/values/values.dart b/lib/src/values/values.dart index 16294ac168..dd80a45bab 100644 --- a/lib/src/values/values.dart +++ b/lib/src/values/values.dart @@ -14,7 +14,7 @@ import 'package:rohd/src/utilities/web.dart'; part 'logic_value.dart'; part 'logic_value_array.dart'; -part 'logic_value_array_of.dart'; +part 'typed_value_array.dart'; part 'small_logic_value.dart'; part 'big_logic_value.dart'; part 'filled_logic_value.dart'; diff --git a/test/logic_array_of_test.dart b/test/logic_array_of_test.dart deleted file mode 100644 index 785c022c8f..0000000000 --- a/test/logic_array_of_test.dart +++ /dev/null @@ -1,997 +0,0 @@ -// Copyright (C) 2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// ignore_for_file: prefer_const_constructors -// ignore_for_file: prefer_const_literals_to_create_immutables -// -// logic_array_of_test.dart -// Tests for typed logic and logic value arrays. -// -// 2026 July 21 -// 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 _SampleStructure extends LogicStructure { - final Logic low; - final Logic high; - - factory _SampleStructure({String? name}) => _SampleStructure._( - Logic(name: 'low'), - Logic(name: 'high', width: 2), - name: name ?? 'sample', - ); - - _SampleStructure._(this.low, this.high, {required String name}) - : super([low, high], name: name); - - @override - _SampleStructure clone({String? name}) => - _SampleStructure(name: name ?? this.name); -} - -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 _SpecialLogic extends Logic { - _SpecialLogic({super.name}) : super(width: 2); - - @override - Logic clone({String? name}) => Logic(name: name ?? this.name, width: width); -} - -class _TypedArrayPortModule extends Module { - LogicArrayOf<_SampleStructure> get valuesOut => - output('valuesOut') as LogicArrayOf<_SampleStructure>; - - _TypedArrayPortModule(LogicArrayOf<_SampleStructure> 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 _TypedArrayInterface extends Interface<_TypedArrayDirection> { - _TypedArrayInterface() { - setPorts([ - LogicArrayOf<_SampleStructure>( - [2], - _SampleStructure.new, - 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 { - LogicArrayOf<_SampleStructure> get valuesOut => - output('valuesOut') as LogicArrayOf<_SampleStructure>; - - _TypedArrayHierarchy(LogicArrayOf<_SampleStructure> source) { - final valuesIn = addTypedInput('valuesIn', source); - final child = _TypedArrayPortModule(valuesIn); - addTypedOutput('valuesOut', valuesIn.clone).gets(child.valuesOut); - } -} - -class _TypedInputModule extends Module { - late final T values; - - _TypedInputModule(T source) { - values = addTypedInput('values', source); - } -} - -int _decodeLogicValue(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('LogicArrayOf', () { - test('keeps structured leaves at the array boundary', () { - final values = LogicArrayOf<_SampleStructure>( - [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])); - expect( - values.reshape([3, 2]).dimensionNames, - equals(['row_', 'column_']), - ); - expect( - values.transpose2D().dimensionNames, - equals(['column_', 'row_']), - ); - }); - - test('provides typed indexing, cloning, and packed conversions', () { - final values = LogicArrayOf( - [2, 2], - ({name}) => Logic(name: name, width: 8), - ); - final packed = values.toLogicArray(name: 'packed'); - final clone = values.clone(name: 'clone'); - - expect(values.at([1, 0]), same(values.arrayElements[2])); - expect(packed.dimensions, equals([2, 2])); - expect(packed.elementWidth, 8); - expect(clone, isA>()); - expect(clone.name, 'clone'); - expect(clone.arrayElements, hasLength(4)); - expect( - () => values.getsPackedValues(LogicArray([4], 8)), - throwsA(isA()), - ); - expect( - () => values.getsPackedValues(LogicArray([2, 2], 4)), - throwsA(isA()), - ); - }); - - test('derives the net kind for empty multidimensional typed arrays', () { - var logicBuilds = 0; - var netBuilds = 0; - final logicValues = LogicArrayOf( - [2, 0], - ({name}) { - logicBuilds++; - return Logic(name: name, width: 8); - }, - ); - final netValues = LogicArrayOf( - [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( - () => LogicArrayOf( - [2], - ({name}) => buildIndex++ == 0 - ? Logic(name: name, width: 3) - : LogicNet(name: name, width: 3), - ), - throwsA(isA()), - ); - expect( - () => LogicArrayOf<_PartiallyNetSampleStructure>( - [2], - _PartiallyNetSampleStructure.new, - ), - throwsA(isA()), - ); - expect( - () => LogicArrayOf<_PartiallyNetSampleStructure>( - [0], - _PartiallyNetSampleStructure.new, - ), - throwsA(isA()), - ); - var structureBuildIndex = 0; - expect( - () => LogicArrayOf( - [2], - ({name}) => structureBuildIndex++ == 0 - ? _SampleStructure(name: name) - : _NetSampleStructure(name: name), - ), - throwsA(isA()), - ); - - final logicValues = LogicArrayOf<_SampleStructure>( - [2], - _SampleStructure.new, - ); - final emptyStructureValues = LogicArrayOf<_EmptyStructure>( - [0], - _EmptyStructure.new, - ); - final netValues = LogicArrayOf<_NetSampleStructure>( - [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('preserves unpacked dimensions through cloning', () { - final values = LogicArrayOf( - [2, 3], - ({name}) => Logic(name: name, width: 4), - numUnpackedDimensions: 1, - ); - - expect(values.clone().numUnpackedDimensions, 1); - expect( - values.named('renamed').numUnpackedDimensions, - 1, - ); - }); - - test('recursively flattens nested array dimensions with typed leaves', () { - final nested = LogicArrayOf( - [2, 2], - ({name}) => LogicArray([3, 2], 4, name: name), - ); - final flattened = nested.flattenNestedDimensions(name: 'flat'); - final source = nested.at([1, 0]).at([2, 1]); - final target = flattened.at([1, 0, 2, 1]); - - expect(flattened.dimensions, equals([2, 2, 3, 2])); - expect(flattened.elementWidth, 4); - expect(flattened.arrayElements, hasLength(24)); - expect(target, isA()); - expect(target.srcConnections, contains(source)); - }); - - test('flattens every nested array layer', () { - final nested = LogicArrayOf>( - [2], - ({name}) => LogicArrayOf( - [3], - ({name}) => LogicArray([4], 2, name: name), - name: name, - ), - ); - final flattened = nested.flattenNestedDimensions(); - final source = nested.at([1]).at([2]).at([3]); - final target = flattened.at([1, 2, 3]); - - expect(flattened.dimensions, equals([2, 3, 4])); - expect(flattened.arrayElements, hasLength(24)); - expect(target.srcConnections, contains(source)); - }); - - test('flattens empty arrays and leaves without specialized clones', () { - final empty = LogicArrayOf( - [2, 0], - ({name}) => Logic(name: name, width: 4), - ); - final emptyNested = LogicArrayOf>( - [0], - ({name}) => LogicArrayOf( - [3], - ({name}) => Logic(name: name, width: 2), - name: name, - ), - ); - final specialized = LogicArrayOf<_SpecialLogic>([2], _SpecialLogic.new); - - final flattenedEmpty = empty.flattenNestedDimensions(); - final flattenedNested = emptyNested.flattenNestedDimensions(); - final flattenedSpecialized = - specialized.flattenNestedDimensions<_SpecialLogic>(); - - expect(flattenedEmpty.dimensions, [2, 0]); - expect(flattenedEmpty.elementWidth, 4); - expect(flattenedEmpty.arrayElements, isEmpty); - expect(flattenedNested.dimensions, [0, 3]); - expect(flattenedNested.elementWidth, 2); - expect(flattenedNested.arrayElements, isEmpty); - expect( - flattenedSpecialized.arrayElements, - everyElement(isA<_SpecialLogic>()), - ); - }); - - test('validates dimensions, names, and leaf widths', () { - expect( - () => LogicArrayOf(const [], Logic.new), - throwsA(isA()), - ); - expect( - () => LogicArrayOf( - [2], - Logic.new, - dimensionNames: const [], - ), - throwsA(isA()), - ); - expect( - () => LogicArrayOf( - [2, 2], - Logic.new, - dimensionNames: const ['row.', 'column_'], - ), - throwsA(isA()), - ); - expect( - () => LogicArrayOf( - [2, 2], - Logic.new, - dimensionNames: const ['row_', 'row_'], - ), - throwsA(isA()), - ); - expect( - () => LogicArrayOf( - [2], - ({name}) => Const(0, width: 1), - ), - throwsA(isA()), - ); - - var width = 1; - expect( - () => LogicArrayOf( - [2], - ({name}) => Logic(name: name, width: width++), - ), - throwsA(isA()), - ); - }); - - test('preserves naming when named', () { - final values = LogicArrayOf( - [2], - Logic.new, - name: 'values', - naming: Naming.mergeable, - ); - - final renamed = values.named('renamed', naming: Naming.reserved); - - expect(renamed.name, 'renamed'); - expect(renamed.naming, Naming.reserved); - }); - - test('matches LogicArray clone naming policy', () { - final typed = LogicArrayOf( - [2], - Logic.new, - name: 'typedValues', - naming: Naming.reserved, - ); - final ordinary = LogicArray( - [2], - 1, - name: 'ordinaryValues', - naming: Naming.reserved, - ); - - expect(typed.clone().naming, ordinary.clone().naming); - expect( - typed.clone(name: 'typedClone').naming, - ordinary.clone(name: 'ordinaryClone').naming, - ); - }); - - test('drives, injects, and captures packed and typed values', () async { - addTearDown(Simulator.reset); - const codec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final values = LogicArrayOf( - [2], - ({name}) => Logic(name: name, width: 8), - ); - final packedValues = LogicValueArray.fromInts([12, 34], elementWidth: 8); - final typedValues = LogicValueArrayOf( - [90, 123], - codec: codec, - ); - - expect(packedValues.putInto(values), same(values)); - expect( - values.value.arrayValues.map((value) => value.toInt()), - [12, 34], - ); - - values.put(LogicValue.ofInt(0x4e38, 16)); - expect( - values.value.arrayValues.map((value) => value.toInt()), - [56, 78], - ); - expect( - () => values.put(typedValues, fill: true), - throwsArgumentError, - ); - - values.inject(typedValues); - await Simulator.run(); - expect( - LogicValueArrayOf.fromLogicValueArray( - values.value, - codec: codec, - ).arrayValues, - [90, 123], - ); - }); - - test('preserves specialized leaves in typed input and output ports', () { - final source = LogicArrayOf<_SampleStructure>( - [2, 3], - _SampleStructure.new, - ); - final module = _TypedArrayPortModule(source); - final valuesIn = - module.input('valuesIn') as LogicArrayOf<_SampleStructure>; - - expect(valuesIn, isA>()); - expect(valuesIn.dimensions, [2, 3]); - expect(valuesIn.at([1, 2]), isA<_SampleStructure>()); - expect(module.valuesOut, isA>()); - expect(module.valuesOut.at([1, 2]).low.width, 1); - expect(module.valuesOut.at([1, 2]).high.width, 2); - }); - - test('preserves typed arrays through interfaces and synthesis', () async { - final source = LogicArrayOf<_SampleStructure>( - [2, 2], - _SampleStructure.new, - 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>(), - ); - }); - }); - - 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([2, 2], 8, leaves); - final nestedInts = LogicValueArray.fromInts([ - [1, 2], - [3, 4], - ], elementWidth: 8); - final flatInts = LogicValueArray.fromFlatInts([2, 2], 8, [1, 2, 3, 4]); - - expect(nested.dimensions, [2, 2]); - expect(nested.elementWidth, 8); - expect(nested.elementCount, 4); - expect(nested.width, 32); - // The deprecated LogicValue length retains its packed-bit meaning. - // ignore: deprecated_member_use_from_same_package - expect(nested.length, 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([ - [1, 2, 3], - [4, 5, 6], - ], elementWidth: 8); - - expect(values.at([1, 1]).toInt(), 5); - expect(values.flatIndexOf([1, 2]), 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('round-trips slices with an empty inner dimension', () { - final values = LogicValueArray.fromFlat([2, 0], 8, const []); - 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]); - expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); - }); - - test('uses packed LogicValue semantics independent of shape', () { - final shaped = LogicValueArray.fromFlatInts([2], 4, [1, 2]); - final reshaped = LogicValueArray.fromFlatInts([1, 2], 4, [1, 2]); - final packed = LogicValue.ofInt(0x21, 8); - final mask = LogicValue.ofInt(0xf0, 8); - final zeros = LogicValueArray.fromFlatInts([2], 4, [0, 0]); - - expect(shaped, packed); - expect(packed, shaped); - expect(shaped, reshaped); - expect(shaped.hashCode, packed.hashCode); - expect(shaped[0], LogicValue.one); - expect(shaped & mask, packed & mask); - expect(mask & shaped, mask & packed); - expect(shaped | mask, packed | mask); - expect(mask | shaped, mask | packed); - expect(shaped ^ mask, packed ^ mask); - expect(mask ^ shaped, mask ^ packed); - expect(shaped + LogicValue.one.zeroExtend(8), - packed + LogicValue.one.zeroExtend(8)); - expect(LogicValue.one.zeroExtend(8) + shaped, - LogicValue.one.zeroExtend(8) + packed); - expect(LogicValue.ofIterable([zeros]), zeros.packed); - expect( - LogicValue.ofIterable([zeros, shaped]), - LogicValue.ofIterable([zeros.packed, shaped.packed]), - ); - expect({shaped, packed}, hasLength(1)); - }); - - 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([2, 2], 8, [one]), - throwsArgumentError, - ); - expect( - () => LogicValueArray.fromInts(const [], elementWidth: 8), - throwsArgumentError, - ); - expect(() => LogicValueArray.fromFlat(const [], 8, const []), - throwsArgumentError); - }); - }); - - group('LogicValueArrayOf', () { - test('constructs equivalent nested and flat list-valued data', () { - const codec = LogicValueCodec>( - decode: _decodeListLogicValue, - encode: _encodeListLogicValue, - ); - final leaves = [ - [1], - [2], - [3], - [4], - ]; - final nested = LogicValueArrayOf>([ - [leaves[0], leaves[1]], - [leaves[2], leaves[3]], - ], codec: codec); - final flat = LogicValueArrayOf>.fromFlat( - [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, transposes, and converts typed values', () { - const codec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final packed = LogicValueArray.fromInts([ - [1, 2, 3], - [4, 5, 6], - ], elementWidth: 8); - final values = LogicValueArrayOf.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]); - expect(values.toLogicArray().dimensions, [2, 3]); - }); - - test('supports slices, indexed mapping, and compatible stacking', () { - const codec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final values = LogicValueArrayOf([ - [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( - LogicValueArrayOf.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 = LogicValueArrayOf([ - [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( - LogicValueArrayOf.stack(values.majorSlices).arrayValues, - values.arrayValues, - ); - expect(values.transpose2D().arrayValues, [1.0, 4.0, 2.0, 5.0]); - }); - - test('requires identical codecs when stacking', () { - final firstCodec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final secondCodec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final first = LogicValueArrayOf.fromFlat( - [1], - 8, - [1], - codec: firstCodec, - ); - final second = LogicValueArrayOf.fromFlat( - [1], - 8, - [2], - codec: secondCodec, - ); - - expect( - () => LogicValueArrayOf.stack([first, second]), - throwsArgumentError, - ); - }); - - test('round-trips typed slices with an empty inner dimension', () { - const codec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final values = LogicValueArrayOf.fromFlat( - [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( - LogicValueArrayOf.stack(slices).dimensions, - [2, 0], - ); - expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); - }); - }); -} diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 47abd9061e..10d475ffc8 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -662,6 +662,39 @@ void main() { } }); + 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/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 8aed6ccf6a..4a8a32ab3f 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); diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 307d7d9f3b..07b588e276 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -207,12 +207,28 @@ class _PairSample extends LogicStructure { _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 LogicArrayOf<_PairSample> samples; + late final TypedLogicArray<_PairSample, _PairValue> samples; _TypedPairConsumer(PairInterface source) { final internal = addPairInterfacePorts(source, PairRole.consumer); - samples = internal.port('samples') as LogicArrayOf<_PairSample>; + samples = + internal.port('samples') as TypedLogicArray<_PairSample, _PairValue>; addOutput('selected', width: samples.elementWidth) <= samples.at([1, 1]); } } @@ -232,21 +248,25 @@ void main() { }); test('clone preserves typed arrays through pair interface ports', () async { - final samples = LogicArrayOf<_PairSample>( + 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 LogicArrayOf<_PairSample>; + final clonedSamples = + clone.port('samples') as TypedLogicArray<_PairSample, _PairValue>; expect(clonedSamples, isNot(same(samples))); expect(clonedSamples.dimensions, [2, 2]); expect(clonedSamples.dimensionNames, ['row_', 'column_']); 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(); @@ -255,6 +275,8 @@ void main() { expect(module.samples.dimensionNames, ['row_', 'column_']); 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}), diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart new file mode 100644 index 0000000000..cdd141c0e8 --- /dev/null +++ b/test/typed_logic_array_test.dart @@ -0,0 +1,1926 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// ignore_for_file: prefer_const_constructors +// ignore_for_file: prefer_const_literals_to_create_immutables +// +// typed_logic_array_test.dart +// Tests for typed logic and logic value arrays. +// +// 2026 July 21 +// Author: Desmond A. Kirkpatrick + +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; + + // 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, + }) : 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: dimensionNames, + name: name ?? this.name, + naming: naming, + numUnpackedDimensions: + numUnpackedDimensions ?? this.numUnpackedDimensions, + ); + + @override + _SampleArray clone({String? name}) => super.clone(name: name) as _SampleArray; +} + +class _SpecialLogic extends Logic { + _SpecialLogic({super.name}) : super(width: 2); + + @override + Logic clone({String? name}) => Logic(name: name ?? this.name, width: width); +} + +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 _TypedInputModule extends Module { + late final T values; + + _TypedInputModule(T source) { + values = addTypedInput('values', source); + } +} + +int _decodeLogicValue(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('TypedLogicArray', () { + 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 semantic values and codecs through hardware transforms', + () { + final values = _SampleArray( + [2, 2], + schema: 'sample-v2', + dimensionNames: const ['row_', 'column_'], + ); + final assigned = TypedValueArray<_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(), + values.reshape([4]), + values.transpose2D(), + ]; + for (final result in transformed) { + expect(result, isA>()); + expect(identical(result.valueCodec, _sampleValueCodec), isTrue); + expect(result.value, isA>()); + } + expect(transformed[1].value.packed, assigned.packed); + expect( + transformed[2].value.arrayValues.map((value) => value.value), + [ + LogicValue.ofString('1xz'), + LogicValue.ofString('010'), + LogicValue.ofString('z01'), + LogicValue.ofString('111'), + ], + ); + }); + + test('preserves typed values through inherited inject and previousValue', + () async { + addTearDown(Simulator.reset); + final values = _SampleArray([2], schema: 'sample-v2')..put(0); + final injected = TypedValueArray<_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()), + ); + }); + + 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], 4, [1, 2, 3, 4, 5, 6]); + + 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 reshaped = array.reshape([3, 2]); + final transposed = array.transpose2D(); + final clone = array.clone(); + expect(reshaped is LogicArray, array is LogicArray); + expect(transposed is LogicArray, array is LogicArray); + expect(clone is LogicArray, array is LogicArray); + expect(reshaped.dimensions, [3, 2]); + expect( + reshaped.value.arrayValues.map((value) => value.toInt()), + [1, 2, 3, 4, 5, 6], + ); + expect(transposed.dimensions, [3, 2]); + expect( + transposed.value.arrayValues.map((value) => value.toInt()), + [1, 4, 2, 5, 3, 6], + ); + 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.dimensionNames, ['row_', 'column_']); + expect(typedUpdated.elementWidth, 3); + expect(typedUpdated.numUnpackedDimensions, 1); + expect( + typedUpdated.value.packed, + source.value.packed.withSet(2, update.value), + ); + }); + + test('shape transforms create hardware with explicit unpacked policy', () { + final source = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 3], + _SampleStructure.new, + dimensionNames: const ['row_', 'column_'], + numUnpackedDimensions: 2, + ); + final reshapeCases = [ + ( + dimensions: [6], + dimensionNames: ['d0_'], + numUnpackedDimensions: 1, + ), + ( + dimensions: [3, 2], + dimensionNames: ['row_', 'column_'], + numUnpackedDimensions: 2, + ), + ( + dimensions: [1, 2, 3], + dimensionNames: ['d0_', 'd1_', 'd2_'], + numUnpackedDimensions: 2, + ), + ]; + final reshaped = [ + for (final testCase in reshapeCases) + source.reshape(testCase.dimensions), + ]; + final transposed = source.transpose2D(); + + for (var caseIndex = 0; caseIndex < reshapeCases.length; caseIndex++) { + final testCase = reshapeCases[caseIndex]; + final result = reshaped[caseIndex]; + expect(result.dimensions, testCase.dimensions); + expect(result.dimensionNames, testCase.dimensionNames); + expect( + result.numUnpackedDimensions, + testCase.numUnpackedDimensions, + ); + expect(result.arrayElements, everyElement(isA<_SampleStructure>())); + for (var index = 0; index < result.arrayElements.length; index++) { + expect( + result.arrayElements[index], + isNot(same(source.arrayElements[index])), + ); + } + } + expect(transposed.dimensions, [3, 2]); + expect(transposed.dimensionNames, ['column_', 'row_']); + expect(transposed.numUnpackedDimensions, 2); + + source.put( + LogicValueArray.fromFlatInts([2, 3], 3, [0, 1, 2, 3, 4, 5]), + ); + for (final result in reshaped) { + expect( + result.value.arrayValues.map((value) => value.toInt()), + [0, 1, 2, 3, 4, 5], + ); + } + expect( + transposed.value.arrayValues.map((value) => value.toInt()), + [0, 3, 1, 4, 2, 5], + ); + + final empty = TypedLogicArray<_SampleStructure, LogicValue>( + [2, 0], + _SampleStructure.new, + numUnpackedDimensions: 2, + ); + final emptyReshaped = empty.reshape([0]); + final emptyTransposed = empty.transpose2D(); + expect(emptyReshaped.dimensions, [0]); + expect(emptyReshaped.numUnpackedDimensions, 1); + expect(emptyReshaped.arrayElements, isEmpty); + expect(emptyTransposed.dimensions, [0, 2]); + expect(emptyTransposed.numUnpackedDimensions, 2); + expect(emptyTransposed.arrayElements, isEmpty); + }); + + 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.dimensionNames, ['column_', 'lane_']); + 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('provides typed indexing, cloning, and packed conversions', () { + final values = TypedLogicArray( + [2, 2], + ({name}) => Logic(name: name, width: 8), + ); + final packed = values.toLogicArray(name: 'packed'); + final clone = values.clone(name: 'clone'); + + expect(values.at([1, 0]), same(values.arrayElements[2])); + expect(packed.dimensions, equals([2, 2])); + expect(packed.elementWidth, 8); + expect(clone, isA>()); + expect(clone.name, 'clone'); + expect(clone.arrayElements, hasLength(4)); + expect( + () => values.getsPackedValues(LogicArray([4], 8)), + throwsA(isA()), + ); + expect( + () => values.getsPackedValues(LogicArray([2, 2], 4)), + throwsA(isA()), + ); + }); + + test('getsPackedValues 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], 3, [1, 2, 3, 4])); + + values.getsPackedValues(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('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('preserves unpacked dimensions through cloning', () { + final values = TypedLogicArray( + [2, 3], + ({name}) => Logic(name: name, width: 4), + numUnpackedDimensions: 1, + ); + + expect(values.clone().numUnpackedDimensions, 1); + expect( + values.named('renamed').numUnpackedDimensions, + 1, + ); + }); + + test('recursively flattens nested array dimensions with typed leaves', () { + final nested = TypedLogicArray( + [2, 2], + ({name}) => LogicArray([3, 2], 4, name: name), + ); + final flattened = nested.flattenNestedDimensions( + valueCodec: LogicValueCodec.logicValue, + name: 'flat', + ); + final source = nested.at([1, 0]).at([2, 1]); + final target = flattened.at([1, 0, 2, 1]); + + expect(flattened.dimensions, equals([2, 2, 3, 2])); + expect(flattened.elementWidth, 4); + expect(flattened.arrayElements, hasLength(24)); + expect(target, isA()); + expect(target.srcConnections, contains(source)); + }); + + test('flattens every nested array layer', () { + final nested = + TypedLogicArray, LogicValue>( + [2], + ({name}) => TypedLogicArray( + [3], + ({name}) => LogicArray([4], 2, name: name), + name: name, + ), + ); + final flattened = nested.flattenNestedDimensions( + valueCodec: LogicValueCodec.logicValue, + ); + final source = nested.at([1]).at([2]).at([3]); + final target = flattened.at([1, 2, 3]); + + expect(flattened.dimensions, equals([2, 3, 4])); + expect(flattened.arrayElements, hasLength(24)); + expect(target.srcConnections, contains(source)); + }); + + test('flattens empty arrays and leaves without specialized clones', () { + final empty = TypedLogicArray( + [2, 0], + ({name}) => Logic(name: name, width: 4), + ); + final emptyNested = + TypedLogicArray, LogicValue>( + [0], + ({name}) => TypedLogicArray( + [3], + ({name}) => Logic(name: name, width: 2), + name: name, + ), + ); + final specialized = + TypedLogicArray<_SpecialLogic, LogicValue>([2], _SpecialLogic.new); + + final flattenedEmpty = empty.flattenNestedDimensions( + valueCodec: LogicValueCodec.logicValue, + ); + final flattenedNested = + emptyNested.flattenNestedDimensions( + valueCodec: LogicValueCodec.logicValue, + ); + final flattenedSpecialized = + specialized.flattenNestedDimensions<_SpecialLogic, LogicValue>( + valueCodec: LogicValueCodec.logicValue, + ); + + expect(flattenedEmpty.dimensions, [2, 0]); + expect(flattenedEmpty.elementWidth, 4); + expect(flattenedEmpty.arrayElements, isEmpty); + expect(flattenedNested.dimensions, [0, 3]); + expect(flattenedNested.elementWidth, 2); + expect(flattenedNested.arrayElements, isEmpty); + expect( + flattenedSpecialized.arrayElements, + everyElement(isA<_SpecialLogic>()), + ); + }); + + 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()), + ); + + 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())); + } + }); + + test('rejects invalid indexing and transforms without partial wiring', () { + 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())); + } + expect(() => values.reshape([3]), throwsArgumentError); + expect( + () => values.reshape([-2, -2]), + throwsA(isA()), + ); + expect( + () => TypedLogicArray( + [4], + ({name}) => Logic(name: name, width: 2), + ).transpose2D(), + throwsA(isA()), + ); + expect( + () => TypedLogicArray( + [1, 2, 2], + ({name}) => Logic(name: name, width: 2), + ).transpose2D(), + throwsA(isA()), + ); + + for (final sourceCount in [3, 5]) { + final target = TypedLogicArray( + [2, 2], + ({name}) => Logic(name: name, width: 2), + ); + final sources = [ + for (var index = 0; index < sourceCount; index++) + Logic(name: 'source$index', width: 2), + ]; + + expect( + () => target.getsEach(sources), + throwsA(isA()), + ); + expect( + target.arrayElements.expand((element) => element.srcConnections), + isEmpty, + ); + } + }); + + 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.dimensionNames, ['row_', 'column_']); + 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(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], 3, [0, 1, 2, 3]); + final differentShape = sameShape.reshape([4]); + final typedValues = TypedValueArray.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 TypedValueArray', + 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], 3, const []); + 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( + TypedValueArray.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 semantic net arrays through typed inout ports', () { + 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>()); + }); + + 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>(), + ); + }); + }); + + 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([2, 2], 8, leaves); + final nestedInts = LogicValueArray.fromInts([ + [1, 2], + [3, 4], + ], elementWidth: 8); + final flatInts = LogicValueArray.fromFlatInts([2, 2], 8, [1, 2, 3, 4]); + + expect(nested.dimensions, [2, 2]); + expect(nested.elementWidth, 8); + expect(nested.elementCount, 4); + expect(nested.width, 32); + // The deprecated LogicValue length retains its packed-bit meaning. + // ignore: deprecated_member_use_from_same_package + expect(nested.length, 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([ + [1, 2, 3], + [4, 5, 6], + ], elementWidth: 8); + + expect(values.at([1, 1]).toInt(), 5); + expect(values.flatIndexOf([1, 2]), 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( + [2, 3], + 8, + (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( + [2, 0], + 8, + (indices) => throw StateError('Generator must not run for $indices.'), + ); + + expect(generatorCalls, 6); + 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.elementCount, 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([]), + throwsArgumentError, + ); + expect( + () => TypedValueArray.stack(>[]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat([0, 2], 8, const []) + .mapMajorSlices((slice) => slice), + throwsA(isA()), + ); + }); + + test('round-trips slices with an empty inner dimension', () { + final values = LogicValueArray.fromFlat([2, 0], 8, const []); + 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]); + expect(values.mapMajorSlices((slice) => slice).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.elementCount]); + + 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([2, 2], 8, [one]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromInts(const [], elementWidth: 8), + throwsArgumentError, + ); + expect(() => LogicValueArray.fromFlat(const [], 8, const []), + throwsArgumentError); + }); + + test('rejects invalid value indices, shapes, widths, and stacks', () { + final values = LogicValueArray.fromFlatInts( + [2, 2], + 8, + [1, 2, 3, 4], + ); + final oneDimensional = LogicValueArray.fromFlatInts([4], 8, [1, 2, 3, 4]); + final threeDimensional = LogicValueArray.fromFlatInts( + [1, 2, 2], + 8, + [1, 2, 3, 4], + ); + + 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([-1], 8, const []), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat([1], -1, const []), + throwsArgumentError, + ); + expect( + () => LogicValueArray.generate( + [1], + 8, + (indices) => LogicValue.ofInt(indices.single, 4), + ), + throwsArgumentError, + ); + expect( + () => LogicValueArray.stack([ + LogicValueArray.fromFlatInts([2], 8, [1, 2]), + LogicValueArray.fromFlatInts([1, 2], 8, [3, 4]), + ]), + throwsArgumentError, + ); + expect( + () => LogicValueArray.stack([ + LogicValueArray.fromFlatInts([2], 8, [1, 2]), + LogicValueArray.fromFlatInts([2], 4, [3, 4]), + ]), + throwsArgumentError, + ); + }); + }); + + group('TypedValueArray', () { + test('rejects empty and non-semantic nested input', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + + expect( + () => TypedValueArray(const [], codec: codec), + throwsArgumentError, + ); + expect( + () => TypedValueArray( + [ + 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 = TypedValueArray>([ + [leaves[0], leaves[1]], + [leaves[2], leaves[3]], + ], codec: codec); + final flat = TypedValueArray>.fromFlat( + [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, transposes, and converts typed values', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final packed = LogicValueArray.fromInts([ + [1, 2, 3], + [4, 5, 6], + ], elementWidth: 8); + final values = TypedValueArray.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]); + final signals = values.toLogicArray(); + expect(signals.dimensions, [2, 3]); + expect(signals.value, values); + }); + + test('supports slices, indexed mapping, and compatible stacking', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final values = TypedValueArray([ + [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( + TypedValueArray.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 = TypedValueArray([ + [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( + TypedValueArray.stack(values.majorSlices).arrayValues, + values.arrayValues, + ); + expect(values.transpose2D().arrayValues, [1.0, 4.0, 2.0, 5.0]); + }); + + test('requires identical codecs when stacking', () { + final firstCodec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final secondCodec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final first = TypedValueArray.fromFlat( + [1], + 8, + [1], + codec: firstCodec, + ); + final second = TypedValueArray.fromFlat( + [1], + 8, + [2], + codec: secondCodec, + ); + + expect( + () => TypedValueArray.stack([first, second]), + throwsArgumentError, + ); + }); + + test('round-trips typed slices with an empty inner dimension', () { + const codec = LogicValueCodec( + decode: _decodeLogicValue, + encode: _encodeLogicValue, + ); + final values = TypedValueArray.fromFlat( + [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( + TypedValueArray.stack(slices).dimensions, + [2, 0], + ); + expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); + }); + }); +} From 0c0dc1212e2f9208426a271262c4f760d907ed0e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 10 Sep 2026 13:09:44 -0700 Subject: [PATCH 07/43] flattenOuter creates Logic not Const and nested arrays need codecs for values --- lib/src/signals/logic_structure.dart | 23 +++++++++++++++--- lib/src/signals/typed_logic_array.dart | 7 +++--- test/logic_structure_test.dart | 16 +++++++++++++ test/typed_logic_array_test.dart | 32 ++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index 86d18e41e7..cf4b69049b 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -206,14 +206,31 @@ class LogicStructure implements Logic { } final flattenedElements = sources.map((source) { - final flattened = source.$1 is Const - ? Logic(width: source.$1.width, name: source.$2) - : source.$1.clone(name: source.$2); + final flattened = _cloneDriveable(source.$1, name: source.$2); return flattened..gets(source.$1); }).toList(growable: false); return LogicStructure(flattenedElements, name: name ?? this.name); } + /// Clones a promoted field without leaving any constant leaves to drive. + Logic _cloneDriveable(Logic source, {required String name}) { + if (source is Const) { + return Logic(width: source.width, name: name); + } + if (source is LogicStructure && source.hasConsts) { + if (source is BaseLogicArray) { + throw LogicConstructionException( + 'Cannot promote an array containing constant elements.'); + } + return LogicStructure( + source.elements + .map((element) => _cloneDriveable(element, name: element.name)), + name: name, + ); + } + return source.clone(name: name); + } + /// Compute the list of all leaf elements, to be cached in [leafElements]. List _calculateLeafElements() { final leaves = []; diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index dc5722dca1..d066d41000 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -256,10 +256,11 @@ class TypedLogicArray extends BaseLogicArray { if (arrays.any((array) => !_sameDimensions(array.dimensions, reference.dimensions) || array.elementWidth != reference.elementWidth || - array.numUnpackedDimensions != reference.numUnpackedDimensions)) { + array.numUnpackedDimensions != reference.numUnpackedDimensions || + !identical(array.valueCodec, reference.valueCodec))) { throw LogicConstructionException( - 'Nested array leaves must have matching dimensions, widths, and ' - 'unpacked dimensions.'); + 'Nested array leaves must have matching dimensions, widths, ' + 'unpacked dimensions, and value codecs.'); } if (reference.numUnpackedDimensions > 0 && flattenedUnpackedDimensions != flattenedDimensions.length) { diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index 2bce2a829b..e6aab242fa 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -281,6 +281,22 @@ void main() { contains(config.elements.single)); }); + test('flatten outer connects nested constant promoted fields', () { + final grandchild = LogicStructure([ + Const(1, width: 1), + ], name: 'grandchild'); + final config = LogicStructure([grandchild], name: 'config'); + final control = LogicStructure([config], name: 'control'); + + final flattened = control.flattenOuter(name: 'flatControl'); + final promoted = flattened.elements.single; + + expect(promoted, isA()); + expect((promoted as LogicStructure).elements.single, isA()); + expect(promoted.elements.single.srcConnections, + contains(grandchild.elements.single)); + }); + test('flatten outer rejects duplicate promoted field names', () { final control = LogicStructure([ Logic(name: 'mode'), diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index cdd141c0e8..8f21b18221 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -771,6 +771,38 @@ void main() { expect(target.srcConnections, contains(source)); }); + test('rejects nested arrays with different value codecs', () { + var codecIndex = 0; + final codecA = LogicValueCodec( + decode: (value) => value, + encode: (value) => value, + ); + final codecB = LogicValueCodec( + decode: (value) => value, + encode: (value) => value, + ); + final nested = + TypedLogicArray, LogicValue>( + [2], + ({name}) { + final codec = codecIndex++ == 0 ? codecA : codecB; + return TypedLogicArray( + [2], + ({name}) => Logic(width: 8, name: name), + valueCodec: codec, + name: name, + ); + }, + ); + + expect( + () => nested.flattenNestedDimensions( + valueCodec: codecA, + ), + throwsA(isA()), + ); + }); + test('flattens every nested array layer', () { final nested = TypedLogicArray, LogicValue>( From 8a160fe14692a14ad20b978224a90a8798dde046 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 10 Sep 2026 15:36:35 -0700 Subject: [PATCH 08/43] missed an empty dimension test --- lib/src/signals/typed_logic_array.dart | 7 +++++-- test/typed_logic_array_test.dart | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index d066d41000..e368773938 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -442,8 +442,11 @@ class _TypedLogicArrayBuild { T? emptyPrototype, bool validateValueCodec = true}) { final normalizedDimensions = List.unmodifiable(dimensions); - if (normalizedDimensions.isEmpty || - normalizedDimensions.any((dimension) => dimension < 0)) { + 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.'); } diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 8f21b18221..7b9944658e 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -251,6 +251,20 @@ LogicValue _encodeListLogicValue(List value) => 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], From 9c3552e53b0589950da16b20c6420702858b3bfb Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 11 Sep 2026 07:39:58 -0700 Subject: [PATCH 09/43] remove redundant methods and rename the TypedLogicValueArray --- CHANGELOG.md | 2 +- doc/user_guide/_docs/A20-logic-arrays.md | 6 +- lib/rohd.dart | 2 +- lib/src/interfaces/interface.dart | 1 + lib/src/interfaces/pair_interface.dart | 1 + lib/src/module.dart | 3 + lib/src/modules/bus.dart | 1 + lib/src/signals/logic_array.dart | 23 ++--- lib/src/signals/typed_logic_array.dart | 94 +++++-------------- .../netlist/netlist_module_translation.dart | 1 + .../netlist_synth_module_definition.dart | 1 + .../netlist/netlist_synthesizer.dart | 1 + .../synthesizers/netlist/netlist_utils.dart | 1 + ...systemverilog_synth_module_definition.dart | 1 + .../utilities/synth_array_concat.dart | 1 + .../utilities/synth_array_slice.dart | 1 + .../synthesizers/utilities/synth_logic.dart | 1 + .../utilities/synth_module_definition.dart | 1 + .../utilities/synth_structure_layout.dart | 1 + lib/src/utilities/simcompare.dart | 1 + lib/src/values/logic_value_array.dart | 5 +- ...rray.dart => typed_logic_value_array.dart} | 72 ++++++++------ lib/src/values/values.dart | 2 +- test/pair_interface_test.dart | 4 +- test/typed_logic_array_test.dart | 93 +++++++----------- 25 files changed, 138 insertions(+), 182 deletions(-) rename lib/src/values/{typed_value_array.dart => typed_logic_value_array.dart} (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612d661e1e..e548fede86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ ## 0.6.10 -- Added `TypedLogicArray` for multidimensional arrays with fixed typed hardware elements and associated semantic values, including typed snapshots and ports, traversal, cloning, shape operations, net support, and synthesis. Added shaped `LogicValueArray` and codec-backed `TypedValueArray` values with nested construction, packed `LogicValue` interoperability, and hardware-array assignment. Existing `LogicArray`, `LogicValueArray`, and concrete array port APIs remain available as specializations (). +- Added `TypedLogicArray` for multidimensional arrays with fixed typed hardware elements and associated semantic values, including typed snapshots and ports, traversal, cloning, shape operations, 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 (). - Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (). - Improved simulation performance and generated outputs by avoiding module creation for four-state-safe operations involving `Const`s, zero shifts, and muxes with constant controls (). - Enforced that `Const` values cannot be changed through `put` or `inject`, including through another `Logic` driven by a `Const` (). diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 3df00bdeee..532f190bbf 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -68,7 +68,7 @@ final samples = TypedLogicArray( ); final bottomRightData = samples.at([1, 2]).data; -final TypedValueArray currentSamples = samples.value; +final TypedLogicValueArray currentSamples = samples.value; ``` The element builder must always produce the configured type, width, and recursively ordered net composition. Every element must be uniformly variable or uniformly net: a structure cannot mix `Logic` and `LogicNet` leaves. The optional `elementCompatibility` callback can additionally reject elements whose semantic representations differ from the prototype, such as floating-point elements with different exponent and mantissa layouts despite having the same total width. @@ -124,13 +124,13 @@ final signals = values.toLogicArray(name: 'values'); Nested constructors reject ragged rows, inconsistent nesting depth, and mismatched element widths. Empty nested input cannot reveal the element width or trailing dimensions, so it must use `fromFlat`. `majorSlices` iterates the outer dimension rather than the total element count, so a `[2, 0]` value contains two empty `[0]` slices and can round-trip through `stack`. -`TypedValueArray` adds a `LogicValueCodec` for application-level values. `LogicValueArray` remains its `TypedValueArray` specialization with the existing convenience constructors and concrete transform return types. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. +`TypedLogicValueArray` adds a `LogicValueCodec` for application-level values. `LogicValueArray` remains its `TypedLogicValueArray` specialization with the existing convenience constructors and concrete transform return types. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. The root list of a nested constructor always represents an array dimension. Below the root, an object matching `T` is treated as one semantic value before it is considered as another list dimension. This permits list-valued semantic elements; use `fromFlat` when the intended interpretation would otherwise be ambiguous. Both value-array classes are `LogicValue`s. Their `width` and deprecated `length` count packed bits, while `elementCount` counts array positions. Bit indexing, equality, hashing, arithmetic, and bitwise operations use the packed value and do not consider shape. The `packed` getter exposes the ordinary `LogicValue` representation. -`TypedLogicArray.value` and `previousValue` return `TypedValueArray` snapshots without adding hardware to the graph. `LogicArray` overrides these with the concrete `LogicValueArray` return type. The standard `changed`, `glitch`, and edge APIs remain packed `LogicValueChanged` events, so typed arrays retain the normal `Logic` event contract. Since all value arrays are `LogicValue`s, use the target-side `put` API for immediate assignment or `inject` for scheduled assignment. Both follow the ordinary packed-value contract, so same-width values remain assignable regardless of their shape metadata. +`TypedLogicArray.value` and `previousValue` return `TypedLogicValueArray` snapshots without adding hardware to the graph. `LogicArray` overrides these with the concrete `LogicValueArray` return type. The standard `changed`, `glitch`, and edge APIs remain packed `LogicValueChanged` events, so typed arrays retain the normal `Logic` event contract. Since all value arrays are `LogicValue`s, use the target-side `put` API for immediate assignment or `inject` for scheduled assignment. Both follow the ordinary packed-value contract, so same-width values remain assignable regardless of their shape metadata. ## Unpacked arrays 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 4e23d5512d..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]. /// diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index 0091419aaa..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. diff --git a/lib/src/module.dart b/lib/src/module.dart index ac1ad40a50..a790a6e64c 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,6 +13,9 @@ import 'dart:collection'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; +import 'package:rohd/src/diagnostics/inspector_service.dart'; +import 'package:rohd/src/signals/signals.dart'; +import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index 1c8e6c2e6c..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. /// diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index bf3432aaa5..8d955789b6 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -13,6 +13,7 @@ part of 'signals.dart'; /// /// 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. @@ -347,18 +348,6 @@ class LogicArray extends TypedLogicArray { isNet: isNet) ..gets(this); - @override - LogicArray getsEach(Iterable sources) { - super.getsEach(sources); - return this; - } - - @override - LogicArray getsGenerated(Logic Function(List indices) generator) { - super.getsGenerated(generator); - return this; - } - @override LogicArray reshape(List newDimensions, {String? name}) { if (_arrayLength(newDimensions) != arrayElements.length) { @@ -370,18 +359,22 @@ class LogicArray extends TypedLogicArray { elementWidth, name: name, numUnpackedDimensions: min(numUnpackedDimensions, newDimensions.length), - )..getsEach(arrayElements); + )..gets(this); } @override LogicArray transpose2D({String? name}) { _checkArrayIsTwoDimensional(dimensions); - return (isNet ? LogicArray.net : LogicArray.new)( + final transposed = (isNet ? LogicArray.net : LogicArray.new)( [dimensions[1], dimensions[0]], elementWidth, name: name, numUnpackedDimensions: numUnpackedDimensions, - )..getsGenerated((indices) => at([indices[1], indices[0]])); + ); + for (final (indices, target) in transposed.indexedElements) { + target <= at([indices[1], indices[0]]); + } + return transposed; } @override diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index e368773938..7583066fc5 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -18,6 +18,8 @@ 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) { @@ -43,7 +45,7 @@ LogicValueCodec _resolveTypedLogicValueCodec( /// 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. /// -/// [value] and [previousValue] preserve [V] through [TypedValueArray] +/// [value] and [previousValue] preserve [V] through [TypedLogicValueArray] /// snapshots. Standard [Logic.changed] events remain packed /// [LogicValueChanged] events. class TypedLogicArray extends BaseLogicArray { @@ -96,21 +98,22 @@ class TypedLogicArray extends BaseLogicArray { naming: naming, numUnpackedDimensions: numUnpackedDimensions); - // ignore: use_super_parameters - invokes the protected structured constructor. + /// 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 int numUnpackedDimensions, + required super.numUnpackedDimensions, super.name, - Naming? naming, + super.naming, }) : dimensionNames = build.dimensionNames, super.structured(build.elements, dimensions: build.dimensions, elementWidth: build.elementWidth, - numUnpackedDimensions: numUnpackedDimensions, - naming: naming, isNet: build.isNet); /// Creates a typed array from structurally compatible [elements]. @@ -159,25 +162,6 @@ class TypedLogicArray extends BaseLogicArray { return current as T; } - /// Connects row-major [arrayElements] to [sources]. - TypedLogicArray getsEach(Iterable sources) { - for (final (target, source) in _zipExact(arrayElements, sources)) { - target <= source; - } - // ignore: avoid_returning_this - preserves the fluent array receiver type. - return this; - } - - /// Connects each array element to a value generated from its indices. - TypedLogicArray getsGenerated( - Logic Function(List indices) generator) { - for (final (indices, target) in indexedElements) { - target <= generator(indices); - } - // ignore: avoid_returning_this - preserves the fluent array receiver type. - return this; - } - /// Constructs connected hardware with [newDimensions] in row-major order. /// /// The returned array contains new elements driven by this array; it is not @@ -197,7 +181,7 @@ class TypedLogicArray extends BaseLogicArray { : _defaultDimensionNames(newDimensions.length), name: name, numUnpackedDimensions: min(numUnpackedDimensions, newDimensions.length)) - ..getsEach(arrayElements); + ..gets(this); } /// Constructs a connected two-dimensional transpose while preserving [T]. @@ -206,14 +190,17 @@ class TypedLogicArray extends BaseLogicArray { /// retains [numUnpackedDimensions]. TypedLogicArray transpose2D({String? name}) { _checkArrayIsTwoDimensional(dimensions); - return TypedLogicArray( + final transposed = TypedLogicArray( [dimensions[1], dimensions[0]], _elementBuilder, valueCodec: valueCodec, elementCompatibility: _elementCompatibility, dimensionNames: [dimensionNames[1], dimensionNames[0]], name: name, - numUnpackedDimensions: numUnpackedDimensions) - ..getsGenerated((indices) => at([indices[1], indices[0]])); + numUnpackedDimensions: numUnpackedDimensions); + for (final (indices, target) in transposed.indexedElements) { + target <= at([indices[1], indices[0]]); + } + return transposed; } /// Immediate typed child arrays along the first dimension. @@ -306,18 +293,22 @@ class TypedLogicArray extends BaseLogicArray { return element; } - return TypedLogicArray( + final flattened = TypedLogicArray( flattenedDimensions, buildFlattenedElement, valueCodec: valueCodec, elementCompatibility: elementCompatibility, name: name, numUnpackedDimensions: flattenedUnpackedDimensions, - )..getsEach(leaves); + ); + for (final (index, target) in flattened.arrayElements.indexed) { + target <= leaves[index]; + } + return flattened; } @override - TypedValueArray get value => TypedValueArray.fromPacked( + TypedLogicValueArray get value => TypedLogicValueArray.fromPacked( dimensions, elementWidth, arrayElements.map((element) => element.value), @@ -325,10 +316,10 @@ class TypedLogicArray extends BaseLogicArray { ); @override - TypedValueArray? get previousValue => + TypedLogicValueArray? get previousValue => arrayElements.any((element) => element.previousValue == null) ? null - : TypedValueArray.fromPacked( + : TypedLogicValueArray.fromPacked( dimensions, elementWidth, arrayElements.map((element) => element.previousValue!), @@ -342,26 +333,7 @@ class TypedLogicArray extends BaseLogicArray { elementWidth, name: name, numUnpackedDimensions: numUnpackedDimensions, - )..getsEach(arrayElements.map((element) => element.packed)); - - /// Drives typed leaves from a packed [LogicArray]. - void getsPackedValues(LogicArray packedValues) { - _validateShape(packedValues.dimensions, packedValues.elementWidth); - for (final (target, source) - in _zipExact(arrayElements, packedValues.arrayElements)) { - target <= source; - } - } - - /// Verifies that another array shape can be applied element by element. - void _validateShape(List dimensions, int elementWidth) { - if (!_sameDimensions(this.dimensions, dimensions) || - this.elementWidth != elementWidth) { - throw LogicConstructionException( - 'Values must have dimensions ${this.dimensions} and ' - 'elementWidth ${this.elementWidth}.'); - } - } + )..gets(this); /// Creates a clone while allowing subclasses to preserve their runtime type. /// @@ -598,20 +570,6 @@ class _TypedLogicArrayBuild { } } -/// Pairs [leftValues] and [rightValues], rejecting unequal lengths. -List<(T, U)> _zipExact(Iterable leftValues, Iterable rightValues) { - final left = leftValues.toList(growable: false); - final right = rightValues.toList(growable: false); - if (left.length != right.length) { - throw StateError('Cannot zip iterables of different lengths.'); - } - return List.generate( - left.length, - (index) => (left[index], right[index]), - growable: false, - ); -} - /// Returns the number of array positions described by [dimensions]. int _arrayLength(List dimensions) => dimensions.fold(1, (length, dimension) => length * dimension); diff --git a/lib/src/synthesizers/netlist/netlist_module_translation.dart b/lib/src/synthesizers/netlist/netlist_module_translation.dart index 9208e02334..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'; diff --git a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart index 118ddde304..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. diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 00f6b06ce9..4fcad37754 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'; diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart index 51512dd692..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'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index c9e2ff6468..28cf8ed5bd 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'; diff --git a/lib/src/synthesizers/utilities/synth_array_concat.dart b/lib/src/synthesizers/utilities/synth_array_concat.dart index dfba7ecf49..3046d371a0 100644 --- a/lib/src/synthesizers/utilities/synth_array_concat.dart +++ b/lib/src/synthesizers/utilities/synth_array_concat.dart @@ -9,6 +9,7 @@ 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 /// [BaseLogicArray] from its elements. diff --git a/lib/src/synthesizers/utilities/synth_array_slice.dart b/lib/src/synthesizers/utilities/synth_array_slice.dart index f03761c013..6bd5092658 100644 --- a/lib/src/synthesizers/utilities/synth_array_slice.dart +++ b/lib/src/synthesizers/utilities/synth_array_slice.dart @@ -9,6 +9,7 @@ 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 /// [BaseLogicArray] element from its packed parent representation. diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 227a1b4169..e7265b1302 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'; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 18c9e25ae2..2840b62469 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'; diff --git a/lib/src/synthesizers/utilities/synth_structure_layout.dart b/lib/src/synthesizers/utilities/synth_structure_layout.dart index ce89308c9b..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}); diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index ed5c70f1a2..d7597e1e14 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'; diff --git a/lib/src/values/logic_value_array.dart b/lib/src/values/logic_value_array.dart index f230809d0d..c57de49829 100644 --- a/lib/src/values/logic_value_array.dart +++ b/lib/src/values/logic_value_array.dart @@ -14,7 +14,7 @@ part of 'values.dart'; /// 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 TypedValueArray { +class LogicValueArray extends TypedLogicValueArray { /// Creates a value array from nested [values]. factory LogicValueArray(List values) { final nested = @@ -154,7 +154,8 @@ class LogicValueArray extends TypedValueArray { @override LogicValueArray mapMajorSlices( - TypedValueArray Function(TypedValueArray slice) + TypedLogicValueArray Function( + TypedLogicValueArray slice) transform, ) { final transformed = majorSlices.map(transform).toList(growable: false); diff --git a/lib/src/values/typed_value_array.dart b/lib/src/values/typed_logic_value_array.dart similarity index 89% rename from lib/src/values/typed_value_array.dart rename to lib/src/values/typed_logic_value_array.dart index ad9ee0e19e..7f8621c347 100644 --- a/lib/src/values/typed_value_array.dart +++ b/lib/src/values/typed_logic_value_array.dart @@ -1,7 +1,7 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// typed_value_array.dart +// typed_logic_value_array.dart // Definition of typed multi-dimensional logic value arrays. // // 2026 July 21 @@ -20,6 +20,7 @@ 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. @@ -49,13 +50,13 @@ class LogicValueCodec { /// 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 [TypedValueArray.fromFlat] when list-valued semantic data is ambiguous -/// or when empty data requires explicit shape and width metadata. +/// 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. -class TypedValueArray extends LogicValue { +class TypedLogicValueArray extends LogicValue { /// The number of elements at each array level. final List dimensions; @@ -65,7 +66,10 @@ class TypedValueArray extends LogicValue { /// 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. @@ -75,9 +79,9 @@ class TypedValueArray extends LogicValue { /// Creates a typed value array from nested [values]. /// /// Empty or ragged nested input is rejected. Use - /// [TypedValueArray.fromFlat] to construct an empty array because its + /// [TypedLogicValueArray.fromFlat] to construct an empty array because its /// element width cannot be inferred. - factory TypedValueArray( + factory TypedLogicValueArray( List values, { required LogicValueCodec codec, }) { @@ -90,7 +94,7 @@ class TypedValueArray extends LogicValue { final encoded = nested.values.map(codec.encode).toList(growable: false); final elementWidth = encoded.first.width; _validateElementWidths(encoded, elementWidth, 'values'); - return TypedValueArray._normalized( + return TypedLogicValueArray._normalized( nested.dimensions, elementWidth, encoded.map(codec.decode).toList(growable: false), @@ -100,7 +104,7 @@ class TypedValueArray extends LogicValue { } /// Creates a typed value array from row-major [values] and explicit metadata. - factory TypedValueArray.fromFlat( + factory TypedLogicValueArray.fromFlat( List dimensions, int elementWidth, Iterable values, { @@ -112,7 +116,7 @@ class TypedValueArray extends LogicValue { _validateValueCount(normalizedDimensions, semanticValues.length, 'values'); final encoded = semanticValues.map(codec.encode).toList(growable: false); _validateElementWidths(encoded, elementWidth, 'values'); - return TypedValueArray._normalized( + return TypedLogicValueArray._normalized( normalizedDimensions, elementWidth, encoded.map(codec.decode).toList(growable: false), @@ -122,7 +126,7 @@ class TypedValueArray extends LogicValue { } /// Decodes row-major packed [values] with [codec]. - factory TypedValueArray.fromPacked( + factory TypedLogicValueArray.fromPacked( List dimensions, int elementWidth, Iterable values, { @@ -133,7 +137,7 @@ class TypedValueArray extends LogicValue { final packedValues = values.toList(growable: false); _validateValueCount(normalizedDimensions, packedValues.length, 'values'); _validateElementWidths(packedValues, elementWidth, 'values'); - return TypedValueArray._normalized( + return TypedLogicValueArray._normalized( normalizedDimensions, elementWidth, packedValues.map(codec.decode).toList(growable: false), @@ -143,11 +147,11 @@ class TypedValueArray extends LogicValue { } /// Decodes the packed elements of [values] with [codec]. - factory TypedValueArray.fromLogicValueArray( - TypedValueArray values, { + factory TypedLogicValueArray.fromLogicValueArray( + TypedLogicValueArray values, { required LogicValueCodec codec, }) => - TypedValueArray.fromPacked( + TypedLogicValueArray.fromPacked( values.dimensions, values.elementWidth, values._packedElements, @@ -158,7 +162,7 @@ class TypedValueArray extends LogicValue { /// /// All arrays must use the identical [LogicValueCodec] instance. Codec /// functions cannot be compared for semantic equivalence. - factory TypedValueArray.stack(Iterable> arrays) { + factory TypedLogicValueArray.stack(Iterable> arrays) { final slices = arrays.toList(growable: false); if (slices.isEmpty) { throw ArgumentError.value(arrays, 'arrays', 'Must not be empty.'); @@ -166,7 +170,7 @@ class TypedValueArray extends LogicValue { final first = slices.first; slices.skip(1).forEach(first._checkStackCompatible); - return TypedValueArray._normalized( + return TypedLogicValueArray._normalized( [slices.length, ...first.dimensions], first.elementWidth, slices.expand((slice) => slice._arrayValues).toList(growable: false), @@ -175,7 +179,12 @@ class TypedValueArray extends LogicValue { ); } - TypedValueArray._normalized( + /// 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, @@ -212,7 +221,7 @@ class TypedValueArray extends LogicValue { ); /// Slices along the first dimension. - Iterable> get majorSlices sync* { + Iterable> get majorSlices sync* { if (dimensions.length < 2) { throw StateError('majorSlices requires at least two dimensions.'); } @@ -238,8 +247,8 @@ class TypedValueArray extends LogicValue { _valueArrayFlatIndex(dimensions, indices); /// Maps semantic values while preserving shape and codec. - TypedValueArray map(T Function(T value) transform) => - TypedValueArray.fromFlat( + TypedLogicValueArray map(T Function(T value) transform) => + TypedLogicValueArray.fromFlat( dimensions, elementWidth, _arrayValues.map(transform), @@ -247,10 +256,10 @@ class TypedValueArray extends LogicValue { ); /// Maps semantic values with their multidimensional indices. - TypedValueArray indexedMap( + TypedLogicValueArray indexedMap( T Function(List indices, T value) transform, ) => - TypedValueArray.fromFlat( + TypedLogicValueArray.fromFlat( dimensions, elementWidth, indexedValues.map((entry) => transform(entry.$1, entry.$2)), @@ -258,13 +267,13 @@ class TypedValueArray extends LogicValue { ); /// Maps slices along the first dimension and stacks the results. - TypedValueArray mapMajorSlices( - TypedValueArray Function(TypedValueArray slice) transform, + TypedLogicValueArray mapMajorSlices( + TypedLogicValueArray Function(TypedLogicValueArray slice) transform, ) => - TypedValueArray.stack(majorSlices.map(transform)); + TypedLogicValueArray.stack(majorSlices.map(transform)); /// Returns the same row-major values with [newDimensions]. - TypedValueArray reshape(List newDimensions) { + TypedLogicValueArray reshape(List newDimensions) { final normalizedDimensions = _validateValueArrayDimensions(newDimensions); if (_valueArrayLength(normalizedDimensions) != elementCount) { throw ArgumentError.value(newDimensions, 'newDimensions', @@ -274,7 +283,7 @@ class TypedValueArray extends LogicValue { } /// Transposes this two-dimensional value array. - TypedValueArray transpose2D() { + TypedLogicValueArray transpose2D() { _checkValueArrayIsTwoDimensional(dimensions); final newDimensions = [dimensions[1], dimensions[0]]; final semanticValues = []; @@ -294,16 +303,17 @@ class TypedValueArray extends LogicValue { LogicArray(dimensions, elementWidth, name: name)..put(this); /// Rebuilds a shaped value from already-normalized semantic and packed data. - TypedValueArray _createStored( + TypedLogicValueArray _createStored( List dimensions, List arrayValues, List packedElements, ) => - TypedValueArray._normalized( + TypedLogicValueArray._normalized( dimensions, elementWidth, arrayValues, packedElements, codec); - /// Validates shape, width, and codec identity for [TypedValueArray.stack]. - void _checkStackCompatible(TypedValueArray other) { + /// 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.'); diff --git a/lib/src/values/values.dart b/lib/src/values/values.dart index dd80a45bab..066dd4f19a 100644 --- a/lib/src/values/values.dart +++ b/lib/src/values/values.dart @@ -14,7 +14,7 @@ import 'package:rohd/src/utilities/web.dart'; part 'logic_value.dart'; part 'logic_value_array.dart'; -part 'typed_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/test/pair_interface_test.dart b/test/pair_interface_test.dart index 07b588e276..5eff4f3ef6 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -266,7 +266,7 @@ void main() { expect(clonedSamples.arrayElements, everyElement(isA<_PairSample>())); expect(clonedSamples.at([1, 1]).high.width, 2); expect(identical(clonedSamples.valueCodec, _pairValueCodec), isTrue); - expect(clonedSamples.value, isA>()); + expect(clonedSamples.value, isA>()); final module = _TypedPairConsumer(source); await module.build(); @@ -276,7 +276,7 @@ void main() { 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>()); + expect(module.samples.value, isA>()); final vectors = [ Vector({'samples': 0xabc}, {'selected': 5}), diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 7b9944658e..43d4cca80b 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -289,7 +289,7 @@ void main() { schema: 'sample-v2', dimensionNames: const ['row_', 'column_'], ); - final assigned = TypedValueArray<_SampleValue>.fromFlat( + final assigned = TypedLogicValueArray<_SampleValue>.fromFlat( [2, 2], 3, [ @@ -303,7 +303,7 @@ void main() { values.put(assigned); - expect(values.value, isA>()); + 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')); @@ -318,7 +318,7 @@ void main() { for (final result in transformed) { expect(result, isA>()); expect(identical(result.valueCodec, _sampleValueCodec), isTrue); - expect(result.value, isA>()); + expect(result.value, isA>()); } expect(transformed[1].value.packed, assigned.packed); expect( @@ -336,7 +336,7 @@ void main() { () async { addTearDown(Simulator.reset); final values = _SampleArray([2], schema: 'sample-v2')..put(0); - final injected = TypedValueArray<_SampleValue>.fromFlat( + final injected = TypedLogicValueArray<_SampleValue>.fromFlat( [2], 3, [ @@ -354,9 +354,9 @@ void main() { await Simulator.run(); - expect(values.value, isA>()); + expect(values.value, isA>()); expect(values.value.arrayValues, injected.arrayValues); - expect(values.previousValue, isA>()); + expect(values.previousValue, isA>()); expect(values.previousValue!.packed, LogicValue.ofInt(0, 6)); expect(identical(values.previousValue!.codec, _sampleValueCodec), isTrue); expect(changes, hasLength(1)); @@ -631,17 +631,14 @@ void main() { expect(clone, isA>()); expect(clone.name, 'clone'); expect(clone.arrayElements, hasLength(4)); + values <= LogicArray([4], 8); expect( - () => values.getsPackedValues(LogicArray([4], 8)), - throwsA(isA()), - ); - expect( - () => values.getsPackedValues(LogicArray([2, 2], 4)), - throwsA(isA()), + () => values <= LogicArray([2, 2], 4), + throwsA(isA()), ); }); - test('getsPackedValues drives structured leaves in row-major order', () { + test('ordinary assignment drives structured leaves in row-major order', () { final values = TypedLogicArray<_SampleStructure, LogicValue>( [2, 2], _SampleStructure.new, @@ -649,7 +646,7 @@ void main() { final packed = LogicArray([2, 2], 3) ..put(LogicValueArray.fromFlatInts([4], 3, [1, 2, 3, 4])); - values.getsPackedValues(packed); + values <= packed; expect(values.value.packed, packed.value.packed); expect( @@ -981,26 +978,6 @@ void main() { ).transpose2D(), throwsA(isA()), ); - - for (final sourceCount in [3, 5]) { - final target = TypedLogicArray( - [2, 2], - ({name}) => Logic(name: name, width: 2), - ); - final sources = [ - for (var index = 0; index < sourceCount; index++) - Logic(name: 'source$index', width: 2), - ]; - - expect( - () => target.getsEach(sources), - throwsA(isA()), - ); - expect( - target.arrayElements.expand((element) => element.srcConnections), - isEmpty, - ); - } }); test('named applies every requested naming mode', () { @@ -1070,7 +1047,7 @@ void main() { expect(specialized.numUnpackedDimensions, 1); expect(specialized.isNet, isFalse); expect(identical(specialized.valueCodec, _sampleValueCodec), isTrue); - expect(specialized.value, isA>()); + expect(specialized.value, isA>()); expect( specialized.arrayElements, everyElement(isA<_SampleStructure>()), @@ -1094,7 +1071,7 @@ void main() { ); final sameShape = LogicValueArray.fromFlatInts([2, 2], 3, [0, 1, 2, 3]); final differentShape = sameShape.reshape([4]); - final typedValues = TypedValueArray.fromFlat( + final typedValues = TypedLogicValueArray.fromFlat( [1, 4], 3, [7, 6, 5, 4], @@ -1127,7 +1104,7 @@ void main() { expected: differentShape.packed, ), ( - name: 'generic TypedValueArray', + name: 'generic TypedLogicValueArray', value: typedValues, expected: typedValues.packed, ), @@ -1169,7 +1146,7 @@ void main() { expect(values.value.packed, LogicValue.ofInt(0, values.width)); await Simulator.run(); expect( - TypedValueArray.fromLogicValueArray( + TypedLogicValueArray.fromLogicValueArray( values.value, codec: codec, ).arrayValues, @@ -1234,7 +1211,7 @@ void main() { expect(changes.single.previousValue, initial); expect(changes.single.newValue, updated); expect(values.value.packed, updated); - expect(values.previousValue, isA>()); + expect(values.previousValue, isA>()); expect(values.previousValue!.dimensions, [2]); expect(values.previousValue!.elementWidth, 3); expect(values.previousValue!.packed, initial); @@ -1250,11 +1227,11 @@ void main() { expect(valuesIn.dimensions, [2, 3]); expect(valuesIn.at([1, 2]), isA<_SampleStructure>()); expect(identical(valuesIn.valueCodec, _sampleValueCodec), isTrue); - expect(valuesIn.value, isA>()); + expect(valuesIn.value, isA>()); expect(module.valuesOut, isA>()); expect(identical(module.valuesOut.valueCodec, _sampleValueCodec), isTrue); - expect(module.valuesOut.value, isA>()); + expect(module.valuesOut.value, isA>()); expect(module.valuesOut.at([1, 2]).low.width, 1); expect(module.valuesOut.at([1, 2]).high.width, 2); }); @@ -1271,7 +1248,7 @@ void main() { isA>()); expect(module.values.isNet, isTrue); expect(identical(module.values.valueCodec, _sampleValueCodec), isTrue); - expect(module.values.value, isA>()); + expect(module.values.value, isA>()); }); test('preserves typed arrays through interfaces and synthesis', () async { @@ -1567,7 +1544,7 @@ void main() { throwsArgumentError, ); expect( - () => TypedValueArray.stack(>[]), + () => TypedLogicValueArray.stack(>[]), throwsArgumentError, ); expect( @@ -1796,7 +1773,7 @@ void main() { }); }); - group('TypedValueArray', () { + group('TypedLogicValueArray', () { test('rejects empty and non-semantic nested input', () { const codec = LogicValueCodec( decode: _decodeLogicValue, @@ -1804,11 +1781,11 @@ void main() { ); expect( - () => TypedValueArray(const [], codec: codec), + () => TypedLogicValueArray(const [], codec: codec), throwsArgumentError, ); expect( - () => TypedValueArray( + () => TypedLogicValueArray( [ 1, 'not an integer', @@ -1830,11 +1807,11 @@ void main() { [3], [4], ]; - final nested = TypedValueArray>([ + final nested = TypedLogicValueArray>([ [leaves[0], leaves[1]], [leaves[2], leaves[3]], ], codec: codec); - final flat = TypedValueArray>.fromFlat( + final flat = TypedLogicValueArray>.fromFlat( [2, 2], 8, leaves, @@ -1858,7 +1835,7 @@ void main() { [1, 2, 3], [4, 5, 6], ], elementWidth: 8); - final values = TypedValueArray.fromLogicValueArray( + final values = TypedLogicValueArray.fromLogicValueArray( packed, codec: codec, ); @@ -1877,7 +1854,7 @@ void main() { decode: _decodeLogicValue, encode: _encodeLogicValue, ); - final values = TypedValueArray([ + final values = TypedLogicValueArray([ [1, 2], [3, 4], ], codec: codec); @@ -1891,7 +1868,7 @@ void main() { [1, 3, 3, 5], ); expect( - TypedValueArray.stack(values.majorSlices).arrayValues, + TypedLogicValueArray.stack(values.majorSlices).arrayValues, values.arrayValues, ); expect(values.packed, LogicValue.ofInt(0x04030201, 32)); @@ -1902,7 +1879,7 @@ void main() { decode: _decodeRoundedLogicValue, encode: _encodeRoundedLogicValue, ); - final values = TypedValueArray([ + final values = TypedLogicValueArray([ [1.2, 2.4], [3.6, 4.8], ], codec: codec); @@ -1910,7 +1887,7 @@ void main() { expect(values.arrayValues, [1.0, 2.0, 4.0, 5.0]); expect(values.reshape([4]).arrayValues, values.arrayValues); expect( - TypedValueArray.stack(values.majorSlices).arrayValues, + TypedLogicValueArray.stack(values.majorSlices).arrayValues, values.arrayValues, ); expect(values.transpose2D().arrayValues, [1.0, 4.0, 2.0, 5.0]); @@ -1925,13 +1902,13 @@ void main() { decode: _decodeLogicValue, encode: _encodeLogicValue, ); - final first = TypedValueArray.fromFlat( + final first = TypedLogicValueArray.fromFlat( [1], 8, [1], codec: firstCodec, ); - final second = TypedValueArray.fromFlat( + final second = TypedLogicValueArray.fromFlat( [1], 8, [2], @@ -1939,7 +1916,7 @@ void main() { ); expect( - () => TypedValueArray.stack([first, second]), + () => TypedLogicValueArray.stack([first, second]), throwsArgumentError, ); }); @@ -1949,7 +1926,7 @@ void main() { decode: _decodeLogicValue, encode: _encodeLogicValue, ); - final values = TypedValueArray.fromFlat( + final values = TypedLogicValueArray.fromFlat( [2, 0], 8, const [], @@ -1963,7 +1940,7 @@ void main() { [0], ]); expect( - TypedValueArray.stack(slices).dimensions, + TypedLogicValueArray.stack(slices).dimensions, [2, 0], ); expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); From 692f6952e87e90901c701765bf705efe3cf47dc9 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 11 Sep 2026 07:46:03 -0700 Subject: [PATCH 10/43] remove flattenNestedDimensions and document retained method justification --- doc/user_guide/_docs/A20-logic-arrays.md | 8 +- lib/src/signals/typed_logic_array.dart | 114 +++-------------------- test/typed_logic_array_test.dart | 113 ---------------------- 3 files changed, 16 insertions(+), 219 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 532f190bbf..b864bcf84d 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -79,7 +79,7 @@ A zero-sized array calls the builder once as a prototype so that the same metada ### Hardware shape transformations -`reshape` and `transpose2D` construct new typed hardware and connect it to the source array; they do not create aliases or views. `reshape` preserves row-major element order and clamps `numUnpackedDimensions` to the new rank when necessary. It retains dimension names when the rank is unchanged and otherwise generates default names. `transpose2D` swaps the two dimensions and their names while retaining `numUnpackedDimensions`. +`reshape` and `transpose2D` construct new typed hardware and connect it to the source array; they do not create aliases or views. The receiver drives the returned array. `reshape` preserves row-major element order and clamps `numUnpackedDimensions` to the new rank when necessary. It retains dimension names when the rank is unchanged and otherwise generates default names. `transpose2D` swaps the two dimensions and their names while retaining `numUnpackedDimensions`. These operations do not infer reverse or permutation-aware connections from a later whole-array assignment; use `indexedElements` and `at` when the transformed array must drive the original or when constructing an explicitly disconnected mapping. ### Traversal boundaries @@ -93,9 +93,7 @@ These APIs intentionally stop at different boundaries: 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. -When typed array elements are themselves arrays, use `flattenNestedDimensions(valueCodec: ...)` to create one rectangular `TypedLogicArray` with all nested dimensions concatenated. The supplied codec must be the identical codec stored by the innermost array elements. The full address is preserved: `nested.at(outerIndices).at(innerIndices)` maps to `flattened.at([...outerIndices, ...innerIndices])`. Every sibling nested array must have matching dimensions, element width, depth, and unpacked-dimension configuration. Empty shapes are supported because the configured element builders supply the missing metadata. - -A `TypedLogicArray` element can be a `LogicStructure`, but it must be driveable. Direct `Const` elements, structures containing a `Const`, and element structures containing nested arrays are rejected. An array can instead directly contain another array and then be flattened as described above. +A `TypedLogicArray` element can be a `LogicStructure`, but it must be driveable. Direct `Const` elements, structures containing a `Const`, and element structures containing nested arrays are rejected. Nested arrays remain available through their existing `arrayElements`, `at`, and `indexedElements` traversal APIs; no public recursive layout-conversion helper is provided. `TypedLogicArray` is also the supported base for custom typed arrays. A subclass using the protected `TypedLogicArray.structured` constructor must supply a correctly configured element builder and value codec, and should override `createClone` to preserve its runtime type and metadata. @@ -119,7 +117,7 @@ final sameValues = LogicValueArray.fromFlatInts( final emptyRows = LogicValueArray.fromFlat([2, 0], 8, const []); final transposed = values.transpose2D(); // Dimensions: [3, 2] -final signals = values.toLogicArray(name: 'values'); +final signals = values.toLogicArray(name: 'values'); // signals are driven by values ``` Nested constructors reject ragged rows, inconsistent nesting depth, and mismatched element widths. Empty nested input cannot reveal the element width or trailing dimensions, so it must use `fromFlat`. `majorSlices` iterates the outer dimension rather than the total element count, so a `[2, 0]` value contains two empty `[0]` slices and can round-trip through `stack`. diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index 7583066fc5..47ceb6034f 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -165,9 +165,11 @@ class TypedLogicArray extends BaseLogicArray { /// Constructs connected hardware with [newDimensions] in row-major order. /// /// The returned array contains new elements driven by this array; it is not - /// an alias or view. [numUnpackedDimensions] is retained when the rank grows - /// and clamped to the new rank when it shrinks. Dimension names are retained - /// only when the rank is unchanged. + /// an alias or view. This operation does not create a disconnected result. + /// [numUnpackedDimensions] is retained when the rank grows and clamped to + /// the new rank when it shrinks. Dimension names are retained only when the + /// rank is unchanged. Use [indexedElements] and [at] for an explicit + /// connection in the opposite direction. TypedLogicArray reshape(List newDimensions, {String? name}) { if (_arrayLength(newDimensions) != arrayElements.length) { throw ArgumentError.value(newDimensions, 'newDimensions', @@ -186,8 +188,10 @@ class TypedLogicArray extends BaseLogicArray { /// Constructs a connected two-dimensional transpose while preserving [T]. /// - /// The returned array contains new elements, swaps the dimension names, and - /// retains [numUnpackedDimensions]. + /// The returned array contains new elements driven by this array, swaps the + /// dimension names, and retains [numUnpackedDimensions]. A whole-array + /// assignment does not infer this permutation; use [indexedElements] and + /// [at] for an explicit reverse-direction connection. TypedLogicArray transpose2D({String? name}) { _checkArrayIsTwoDimensional(dimensions); final transposed = TypedLogicArray( @@ -211,102 +215,6 @@ class TypedLogicArray extends BaseLogicArray { return elements.cast>(); } - /// Flattens all nested array dimensions into one typed array of [U] leaves. - /// - /// Each nested array layer must be rectangular: sibling arrays must have the - /// same dimensions and element width. The returned dimensions concatenate - /// every nested layer, preserving row-major ordering and index addresses. - TypedLogicArray flattenNestedDimensions({ - required LogicValueCodec valueCodec, - TypedLogicArrayElementCompatibility? elementCompatibility, - String? name, - }) { - var leaves = arrayElements.cast().toList(growable: false); - final flattenedDimensions = [...dimensions]; - var flattenedUnpackedDimensions = numUnpackedDimensions; - TypedLogicArrayElementBuilder flattenedElementBuilder = - _elementBuilder; - Object discoveredValueCodec = this.valueCodec; - var prototype = leaves.isEmpty - ? flattenedElementBuilder(name: 'flatten_prototype') - : leaves.first; - - while (prototype is TypedLogicArray) { - if (leaves.isNotEmpty && - leaves.any((leaf) => leaf is! TypedLogicArray)) { - throw LogicConstructionException( - 'Nested array leaves must have a uniform depth.'); - } - - final arrays = leaves.cast>(); - final reference = arrays.isEmpty ? prototype : arrays.first; - if (arrays.any((array) => - !_sameDimensions(array.dimensions, reference.dimensions) || - array.elementWidth != reference.elementWidth || - array.numUnpackedDimensions != reference.numUnpackedDimensions || - !identical(array.valueCodec, reference.valueCodec))) { - throw LogicConstructionException( - 'Nested array leaves must have matching dimensions, widths, ' - 'unpacked dimensions, and value codecs.'); - } - if (reference.numUnpackedDimensions > 0 && - flattenedUnpackedDimensions != flattenedDimensions.length) { - throw LogicConstructionException( - 'Cannot flatten unpacked inner dimensions after packed outer ' - 'dimensions.'); - } - - flattenedDimensions.addAll(reference.dimensions); - flattenedUnpackedDimensions += reference.numUnpackedDimensions; - leaves = - arrays.expand((array) => array.arrayElements).toList(growable: false); - flattenedElementBuilder = reference._elementBuilder; - discoveredValueCodec = reference.valueCodec; - prototype = leaves.isEmpty - ? flattenedElementBuilder(name: 'flatten_prototype') - : leaves.first; - } - - if (prototype is! U || leaves.any((leaf) => leaf is! U)) { - throw LogicConstructionException( - 'Nested array leaves must have type $U.'); - } - - if (leaves.any((leaf) => leaf.width != prototype.width)) { - throw LogicConstructionException( - 'Nested array leaves must have matching widths.'); - } - if (!identical(discoveredValueCodec, valueCodec)) { - throw LogicConstructionException( - 'valueCodec must be identical to the flattened element codec.'); - } - - var returnPrototype = leaves.isEmpty; - U buildFlattenedElement({String? name}) { - final element = - returnPrototype ? prototype : flattenedElementBuilder(name: name); - returnPrototype = false; - if (element is! U) { - throw LogicConstructionException( - 'Nested array element builder must produce type $U.'); - } - return element; - } - - final flattened = TypedLogicArray( - flattenedDimensions, - buildFlattenedElement, - valueCodec: valueCodec, - elementCompatibility: elementCompatibility, - name: name, - numUnpackedDimensions: flattenedUnpackedDimensions, - ); - for (final (index, target) in flattened.arrayElements.indexed) { - target <= leaves[index]; - } - return flattened; - } - @override TypedLogicValueArray get value => TypedLogicValueArray.fromPacked( dimensions, @@ -327,6 +235,10 @@ class TypedLogicArray extends BaseLogicArray { ); /// Packs typed leaves into a conventional [LogicArray]. + /// + /// The returned array is newly allocated and driven by this array. This is + /// an interoperability adapter for APIs that require a conventional packed + /// [LogicArray]; it is not an alias or a disconnected clone. LogicArray toLogicArray({String? name}) => (isNet ? LogicArray.net : LogicArray.new)( dimensions, diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 43d4cca80b..7548bb79b3 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -763,119 +763,6 @@ void main() { ); }); - test('recursively flattens nested array dimensions with typed leaves', () { - final nested = TypedLogicArray( - [2, 2], - ({name}) => LogicArray([3, 2], 4, name: name), - ); - final flattened = nested.flattenNestedDimensions( - valueCodec: LogicValueCodec.logicValue, - name: 'flat', - ); - final source = nested.at([1, 0]).at([2, 1]); - final target = flattened.at([1, 0, 2, 1]); - - expect(flattened.dimensions, equals([2, 2, 3, 2])); - expect(flattened.elementWidth, 4); - expect(flattened.arrayElements, hasLength(24)); - expect(target, isA()); - expect(target.srcConnections, contains(source)); - }); - - test('rejects nested arrays with different value codecs', () { - var codecIndex = 0; - final codecA = LogicValueCodec( - decode: (value) => value, - encode: (value) => value, - ); - final codecB = LogicValueCodec( - decode: (value) => value, - encode: (value) => value, - ); - final nested = - TypedLogicArray, LogicValue>( - [2], - ({name}) { - final codec = codecIndex++ == 0 ? codecA : codecB; - return TypedLogicArray( - [2], - ({name}) => Logic(width: 8, name: name), - valueCodec: codec, - name: name, - ); - }, - ); - - expect( - () => nested.flattenNestedDimensions( - valueCodec: codecA, - ), - throwsA(isA()), - ); - }); - - test('flattens every nested array layer', () { - final nested = - TypedLogicArray, LogicValue>( - [2], - ({name}) => TypedLogicArray( - [3], - ({name}) => LogicArray([4], 2, name: name), - name: name, - ), - ); - final flattened = nested.flattenNestedDimensions( - valueCodec: LogicValueCodec.logicValue, - ); - final source = nested.at([1]).at([2]).at([3]); - final target = flattened.at([1, 2, 3]); - - expect(flattened.dimensions, equals([2, 3, 4])); - expect(flattened.arrayElements, hasLength(24)); - expect(target.srcConnections, contains(source)); - }); - - test('flattens empty arrays and leaves without specialized clones', () { - final empty = TypedLogicArray( - [2, 0], - ({name}) => Logic(name: name, width: 4), - ); - final emptyNested = - TypedLogicArray, LogicValue>( - [0], - ({name}) => TypedLogicArray( - [3], - ({name}) => Logic(name: name, width: 2), - name: name, - ), - ); - final specialized = - TypedLogicArray<_SpecialLogic, LogicValue>([2], _SpecialLogic.new); - - final flattenedEmpty = empty.flattenNestedDimensions( - valueCodec: LogicValueCodec.logicValue, - ); - final flattenedNested = - emptyNested.flattenNestedDimensions( - valueCodec: LogicValueCodec.logicValue, - ); - final flattenedSpecialized = - specialized.flattenNestedDimensions<_SpecialLogic, LogicValue>( - valueCodec: LogicValueCodec.logicValue, - ); - - expect(flattenedEmpty.dimensions, [2, 0]); - expect(flattenedEmpty.elementWidth, 4); - expect(flattenedEmpty.arrayElements, isEmpty); - expect(flattenedNested.dimensions, [0, 3]); - expect(flattenedNested.elementWidth, 2); - expect(flattenedNested.arrayElements, isEmpty); - expect( - flattenedSpecialized.arrayElements, - everyElement(isA<_SpecialLogic>()), - ); - }); - test('validates dimensions, names, and leaf widths', () { expect( () => TypedLogicArray(const [], Logic.new), From a121f6e13c72072aff1dc064238b5ce388603b61 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 11 Sep 2026 07:47:10 -0700 Subject: [PATCH 11/43] leftover code --- test/typed_logic_array_test.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 7548bb79b3..118543b03a 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -155,13 +155,6 @@ class _SampleArray extends TypedLogicArray<_SampleStructure, _SampleValue> { _SampleArray clone({String? name}) => super.clone(name: name) as _SampleArray; } -class _SpecialLogic extends Logic { - _SpecialLogic({super.name}) : super(width: 2); - - @override - Logic clone({String? name}) => Logic(name: name ?? this.name, width: width); -} - class _TypedArrayPortModule extends Module { TypedLogicArray<_SampleStructure, _SampleValue> get valuesOut => output('valuesOut') as TypedLogicArray<_SampleStructure, _SampleValue>; From e050dd179a7a7d33e323e9c6ef10932871edbb8a Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 11 Sep 2026 07:49:11 -0700 Subject: [PATCH 12/43] updated doc on typed value-array api --- doc/user_guide/_docs/A20-logic-arrays.md | 2 +- lib/src/values/typed_logic_value_array.dart | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index b864bcf84d..fe1c9c21da 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -122,7 +122,7 @@ final signals = values.toLogicArray(name: 'values'); // signals are driven by va Nested constructors reject ragged rows, inconsistent nesting depth, and mismatched element widths. Empty nested input cannot reveal the element width or trailing dimensions, so it must use `fromFlat`. `majorSlices` iterates the outer dimension rather than the total element count, so a `[2, 0]` value contains two empty `[0]` slices and can round-trip through `stack`. -`TypedLogicValueArray` adds a `LogicValueCodec` for application-level values. `LogicValueArray` remains its `TypedLogicValueArray` specialization with the existing convenience constructors and concrete transform return types. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. +`TypedLogicValueArray` adds a `LogicValueCodec` for application-level values. `LogicValueArray` remains its `TypedLogicValueArray` specialization with the existing convenience constructors and concrete transform return types. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Decoded semantic elements are exposed by reference, so mutating a mutable element does not update the stored packed bits; callers using mutable semantic values are responsible for treating them consistently with snapshot semantics. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. The root list of a nested constructor always represents an array dimension. Below the root, an object matching `T` is treated as one semantic value before it is considered as another list dimension. This permits list-valued semantic elements; use `fromFlat` when the intended interpretation would otherwise be ambiguous. diff --git a/lib/src/values/typed_logic_value_array.dart b/lib/src/values/typed_logic_value_array.dart index 7f8621c347..42e765c185 100644 --- a/lib/src/values/typed_logic_value_array.dart +++ b/lib/src/values/typed_logic_value_array.dart @@ -56,6 +56,10 @@ class LogicValueCodec { /// 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; From 415abd3340d3bf236474ba0b7e516150ea12b7ae Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 11 Sep 2026 17:57:17 -0700 Subject: [PATCH 13/43] reduced reshape/transform operations to not involve connectivity, just the array arrangement --- doc/user_guide/_docs/A20-logic-arrays.md | 8 +- lib/src/signals/logic_array.dart | 39 ++----- lib/src/signals/typed_logic_array.dart | 74 ++----------- test/typed_logic_array_test.dart | 135 +---------------------- 4 files changed, 23 insertions(+), 233 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index fe1c9c21da..0949dfd213 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -5,7 +5,7 @@ last_modified_at: 2026-09-10 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. +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. `TypedLogicArray` uses the same structural model. An array is not a scalar `Logic` with an internal wire sliced into elements: it owns a hierarchy of child signals. `LogicStructure` supplies the common `Logic` behavior by packing those children when a scalar-like operation is needed, while the array layer adds dimensions, array-boundary traversal, and indexing. `TypedLogicArray` is indirectly a `Logic` through `LogicStructure`. It does not extend `LogicArray` because `LogicArray` is already the concrete `TypedLogicArray` specialization; making it the superclass would require redesigning the public hierarchy around a separate generic or abstract array base. `LogicArray`s can be constructed easily using the constructor: @@ -24,7 +24,7 @@ As long as the total width of a `LogicArray` and another type of `Logic` (includ ## Typed arrays -Use `TypedLogicArray` when every position at the declared array boundary has the same specialized hardware type and associated semantic value type. `LogicArray` remains the ordinary `TypedLogicArray` specialization, preserving its existing constructors, ports, cloning, naming, transformations, and packed assignment behavior. Both extend the non-generic `BaseLogicArray`, which synthesis and hierarchy utilities use when the element and value types are irrelevant. +Use `TypedLogicArray` when every position at the declared array boundary has the same specialized hardware type and associated semantic value type. `LogicArray` remains the ordinary `TypedLogicArray` specialization, preserving its existing constructors, ports, cloning, naming, and packed assignment behavior. Both extend the non-generic `BaseLogicArray`, which contains array-specific metadata and traversal shared by the two public types; it is an internal implementation base rather than another public construction API. For example, these sample hardware and value types preserve named fields in hardware while exposing typed snapshots: @@ -77,9 +77,7 @@ A zero-sized array calls the builder once as a prototype so that the same metada `valueCodec` may be omitted when `TValue` is exactly `LogicValue`; the canonical identity codec is selected automatically. Other semantic types require a codec. Since hardware may contain `X` and `Z`, a codec used by `TypedLogicArray` should decode every four-state value that can appear in that hardware. -### Hardware shape transformations - -`reshape` and `transpose2D` construct new typed hardware and connect it to the source array; they do not create aliases or views. The receiver drives the returned array. `reshape` preserves row-major element order and clamps `numUnpackedDimensions` to the new rank when necessary. It retains dimension names when the rank is unchanged and otherwise generates default names. `transpose2D` swaps the two dimensions and their names while retaining `numUnpackedDimensions`. These operations do not infer reverse or permutation-aware connections from a later whole-array assignment; use `indexedElements` and `at` when the transformed array must drive the original or when constructing an explicitly disconnected mapping. +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 diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 8d955789b6..311fd4ec31 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -11,8 +11,14 @@ part of 'signals.dart'; /// Shared structural base for multidimensional logic arrays. /// -/// Construct [LogicArray] for ordinary logic values or [TypedLogicArray] for -/// hardware elements with an associated semantic value type. +/// 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 @@ -348,35 +354,6 @@ class LogicArray extends TypedLogicArray { isNet: isNet) ..gets(this); - @override - LogicArray reshape(List newDimensions, {String? name}) { - if (_arrayLength(newDimensions) != arrayElements.length) { - throw ArgumentError.value(newDimensions, 'newDimensions', - 'Must contain ${arrayElements.length} array elements.'); - } - return (isNet ? LogicArray.net : LogicArray.new)( - newDimensions, - elementWidth, - name: name, - numUnpackedDimensions: min(numUnpackedDimensions, newDimensions.length), - )..gets(this); - } - - @override - LogicArray transpose2D({String? name}) { - _checkArrayIsTwoDimensional(dimensions); - final transposed = (isNet ? LogicArray.net : LogicArray.new)( - [dimensions[1], dimensions[0]], - elementWidth, - name: name, - numUnpackedDimensions: numUnpackedDimensions, - ); - for (final (indices, target) in transposed.indexedElements) { - target <= at([indices[1], indices[0]]); - } - return transposed; - } - @override Iterable get majorSlices { if (dimensions.length < 2) { diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index 47ceb6034f..22b3eb043b 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -34,6 +34,15 @@ LogicValueCodec _resolveTypedLogicValueCodec( /// 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] is the +/// concrete [TypedLogicArray]<[Logic], [LogicValue]> specialization, so the +/// two public array types are siblings rather than one extending the other. +/// The array-specific metadata and traversal live in [BaseLogicArray], while +/// [T] preserves the type at the declared array boundary. +/// /// [elements] contains the immediate children of the outermost dimension. /// [arrayElements] traverses the number of array levels declared by /// [dimensions] and then stops, returning one [T] for each declared array @@ -162,51 +171,6 @@ class TypedLogicArray extends BaseLogicArray { return current as T; } - /// Constructs connected hardware with [newDimensions] in row-major order. - /// - /// The returned array contains new elements driven by this array; it is not - /// an alias or view. This operation does not create a disconnected result. - /// [numUnpackedDimensions] is retained when the rank grows and clamped to - /// the new rank when it shrinks. Dimension names are retained only when the - /// rank is unchanged. Use [indexedElements] and [at] for an explicit - /// connection in the opposite direction. - TypedLogicArray reshape(List newDimensions, {String? name}) { - if (_arrayLength(newDimensions) != arrayElements.length) { - throw ArgumentError.value(newDimensions, 'newDimensions', - 'Must contain ${arrayElements.length} array elements.'); - } - return TypedLogicArray(newDimensions, _elementBuilder, - valueCodec: valueCodec, - elementCompatibility: _elementCompatibility, - dimensionNames: newDimensions.length == dimensions.length - ? dimensionNames - : _defaultDimensionNames(newDimensions.length), - name: name, - numUnpackedDimensions: min(numUnpackedDimensions, newDimensions.length)) - ..gets(this); - } - - /// Constructs a connected two-dimensional transpose while preserving [T]. - /// - /// The returned array contains new elements driven by this array, swaps the - /// dimension names, and retains [numUnpackedDimensions]. A whole-array - /// assignment does not infer this permutation; use [indexedElements] and - /// [at] for an explicit reverse-direction connection. - TypedLogicArray transpose2D({String? name}) { - _checkArrayIsTwoDimensional(dimensions); - final transposed = TypedLogicArray( - [dimensions[1], dimensions[0]], _elementBuilder, - valueCodec: valueCodec, - elementCompatibility: _elementCompatibility, - dimensionNames: [dimensionNames[1], dimensionNames[0]], - name: name, - numUnpackedDimensions: numUnpackedDimensions); - for (final (indices, target) in transposed.indexedElements) { - target <= at([indices[1], indices[0]]); - } - return transposed; - } - /// Immediate typed child arrays along the first dimension. Iterable> get majorSlices { if (dimensions.length < 2) { @@ -234,19 +198,6 @@ class TypedLogicArray extends BaseLogicArray { codec: valueCodec, ); - /// Packs typed leaves into a conventional [LogicArray]. - /// - /// The returned array is newly allocated and driven by this array. This is - /// an interoperability adapter for APIs that require a conventional packed - /// [LogicArray]; it is not an alias or a disconnected clone. - LogicArray toLogicArray({String? name}) => - (isNet ? LogicArray.net : LogicArray.new)( - dimensions, - elementWidth, - name: name, - numUnpackedDimensions: numUnpackedDimensions, - )..gets(this); - /// Creates a clone while allowing subclasses to preserve their runtime type. /// /// Subclasses with additional metadata should override this method and @@ -518,10 +469,3 @@ List _normalizeLogicArrayDimensionNames( } return normalized; } - -/// Rejects shapes that cannot be transposed by [TypedLogicArray.transpose2D]. -void _checkArrayIsTwoDimensional(List dimensions) { - if (dimensions.length != 2) { - throw StateError('Expected exactly two dimensions, got $dimensions.'); - } -} diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 118543b03a..286f7354c2 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -275,8 +275,7 @@ void main() { expect(values.indexedElements.last.$1, equals([1, 2])); }); - test('preserves semantic values and codecs through hardware transforms', - () { + test('preserves typed metadata through hardware cloning', () { final values = _SampleArray( [2, 2], schema: 'sample-v2', @@ -305,24 +304,12 @@ void main() { final transformed = [ values.clone(), - values.reshape([4]), - values.transpose2D(), ]; for (final result in transformed) { expect(result, isA>()); expect(identical(result.valueCodec, _sampleValueCodec), isTrue); expect(result.value, isA>()); } - expect(transformed[1].value.packed, assigned.packed); - expect( - transformed[2].value.arrayValues.map((value) => value.value), - [ - LogicValue.ofString('1xz'), - LogicValue.ofString('010'), - LogicValue.ofString('z01'), - LogicValue.ofString('111'), - ], - ); }); test('preserves typed values through inherited inject and previousValue', @@ -444,22 +431,8 @@ void main() { expect(slices.last.numUnpackedDimensions, 0); expect(slices.last.at([2]), same(array.at([1, 2]))); - final reshaped = array.reshape([3, 2]); - final transposed = array.transpose2D(); final clone = array.clone(); - expect(reshaped is LogicArray, array is LogicArray); - expect(transposed is LogicArray, array is LogicArray); expect(clone is LogicArray, array is LogicArray); - expect(reshaped.dimensions, [3, 2]); - expect( - reshaped.value.arrayValues.map((value) => value.toInt()), - [1, 2, 3, 4, 5, 6], - ); - expect(transposed.dimensions, [3, 2]); - expect( - transposed.value.arrayValues.map((value) => value.toInt()), - [1, 4, 2, 5, 3, 6], - ); expect(clone.dimensions, array.dimensions); expect(clone.elementWidth, array.elementWidth); expect(clone.numUnpackedDimensions, array.numUnpackedDimensions); @@ -497,86 +470,6 @@ void main() { ); }); - test('shape transforms create hardware with explicit unpacked policy', () { - final source = TypedLogicArray<_SampleStructure, LogicValue>( - [2, 3], - _SampleStructure.new, - dimensionNames: const ['row_', 'column_'], - numUnpackedDimensions: 2, - ); - final reshapeCases = [ - ( - dimensions: [6], - dimensionNames: ['d0_'], - numUnpackedDimensions: 1, - ), - ( - dimensions: [3, 2], - dimensionNames: ['row_', 'column_'], - numUnpackedDimensions: 2, - ), - ( - dimensions: [1, 2, 3], - dimensionNames: ['d0_', 'd1_', 'd2_'], - numUnpackedDimensions: 2, - ), - ]; - final reshaped = [ - for (final testCase in reshapeCases) - source.reshape(testCase.dimensions), - ]; - final transposed = source.transpose2D(); - - for (var caseIndex = 0; caseIndex < reshapeCases.length; caseIndex++) { - final testCase = reshapeCases[caseIndex]; - final result = reshaped[caseIndex]; - expect(result.dimensions, testCase.dimensions); - expect(result.dimensionNames, testCase.dimensionNames); - expect( - result.numUnpackedDimensions, - testCase.numUnpackedDimensions, - ); - expect(result.arrayElements, everyElement(isA<_SampleStructure>())); - for (var index = 0; index < result.arrayElements.length; index++) { - expect( - result.arrayElements[index], - isNot(same(source.arrayElements[index])), - ); - } - } - expect(transposed.dimensions, [3, 2]); - expect(transposed.dimensionNames, ['column_', 'row_']); - expect(transposed.numUnpackedDimensions, 2); - - source.put( - LogicValueArray.fromFlatInts([2, 3], 3, [0, 1, 2, 3, 4, 5]), - ); - for (final result in reshaped) { - expect( - result.value.arrayValues.map((value) => value.toInt()), - [0, 1, 2, 3, 4, 5], - ); - } - expect( - transposed.value.arrayValues.map((value) => value.toInt()), - [0, 3, 1, 4, 2, 5], - ); - - final empty = TypedLogicArray<_SampleStructure, LogicValue>( - [2, 0], - _SampleStructure.new, - numUnpackedDimensions: 2, - ); - final emptyReshaped = empty.reshape([0]); - final emptyTransposed = empty.transpose2D(); - expect(emptyReshaped.dimensions, [0]); - expect(emptyReshaped.numUnpackedDimensions, 1); - expect(emptyReshaped.arrayElements, isEmpty); - expect(emptyTransposed.dimensions, [0, 2]); - expect(emptyTransposed.numUnpackedDimensions, 2); - expect(emptyTransposed.arrayElements, isEmpty); - }); - test('majorSlices returns existing typed child arrays', () { final values = TypedLogicArray<_SampleStructure, LogicValue>( [2, 3, 2], @@ -610,17 +503,14 @@ void main() { ); }); - test('provides typed indexing, cloning, and packed conversions', () { + test('provides typed indexing and cloning', () { final values = TypedLogicArray( [2, 2], ({name}) => Logic(name: name, width: 8), ); - final packed = values.toLogicArray(name: 'packed'); final clone = values.clone(name: 'clone'); expect(values.at([1, 0]), same(values.arrayElements[2])); - expect(packed.dimensions, equals([2, 2])); - expect(packed.elementWidth, 8); expect(clone, isA>()); expect(clone.name, 'clone'); expect(clone.arrayElements, hasLength(4)); @@ -825,7 +715,7 @@ void main() { } }); - test('rejects invalid indexing and transforms without partial wiring', () { + test('rejects invalid indexing', () { final values = TypedLogicArray( [2, 2], ({name}) => Logic(name: name, width: 2), @@ -839,25 +729,6 @@ void main() { ]) { expect(() => values.at(indices), throwsA(isA())); } - expect(() => values.reshape([3]), throwsArgumentError); - expect( - () => values.reshape([-2, -2]), - throwsA(isA()), - ); - expect( - () => TypedLogicArray( - [4], - ({name}) => Logic(name: name, width: 2), - ).transpose2D(), - throwsA(isA()), - ); - expect( - () => TypedLogicArray( - [1, 2, 2], - ({name}) => Logic(name: name, width: 2), - ).transpose2D(), - throwsA(isA()), - ); }); test('named applies every requested naming mode', () { From 77f622c5f8a279c3cc37365ec97a249cba417ad0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 13 Sep 2026 15:25:58 -0700 Subject: [PATCH 14/43] further API simplifications --- doc/user_guide/_docs/A20-logic-arrays.md | 12 +- lib/src/signals/logic_array.dart | 10 +- lib/src/signals/typed_logic_array.dart | 45 ++-- .../synthesizers/utilities/synth_logic.dart | 1 + lib/src/values/logic_value_array.dart | 97 +++---- lib/src/values/typed_logic_value_array.dart | 24 +- test/pair_interface_test.dart | 2 - test/typed_logic_array_test.dart | 244 ++++++++++++++---- 8 files changed, 272 insertions(+), 163 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 0949dfd213..d4c9d31ea7 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -26,6 +26,8 @@ As long as the total width of a `LogicArray` and another type of `Logic` (includ Use `TypedLogicArray` when every position at the declared array boundary has the same specialized hardware type and associated semantic value type. `LogicArray` remains the ordinary `TypedLogicArray` specialization, preserving its existing constructors, ports, cloning, naming, and packed assignment behavior. Both extend the non-generic `BaseLogicArray`, which contains array-specific metadata and traversal shared by the two public types; it is an internal implementation base rather than another public construction API. +`dimensionNames`, when supplied to `TypedLogicArray`, is construction metadata used to derive names within typed-array hierarchies and preserve them during internal cloning. It is not a public axis-metadata property, and it does not change the established generated naming convention of ordinary `LogicArray`. + For example, these sample hardware and value types preserve named fields in hardware while exposing typed snapshots: ```dart @@ -64,7 +66,6 @@ final samples = TypedLogicArray( [2, 3], Sample.new, valueCodec: sampleCodec, - dimensionNames: ['row_', 'column_'], ); final bottomRightData = samples.at([1, 2]).data; @@ -93,7 +94,7 @@ For example, a `[2, 3]` array of two-field `Sample`s has six `arrayElements` and A `TypedLogicArray` element can be a `LogicStructure`, but it must be driveable. Direct `Const` elements, structures containing a `Const`, and element structures containing nested arrays are rejected. Nested arrays remain available through their existing `arrayElements`, `at`, and `indexedElements` traversal APIs; no public recursive layout-conversion helper is provided. -`TypedLogicArray` is also the supported base for custom typed arrays. A subclass using the protected `TypedLogicArray.structured` constructor must supply a correctly configured element builder and value codec, and should override `createClone` to preserve its runtime type and metadata. +`TypedLogicArray` is also the supported base for custom typed arrays. Subclasses should use the normal constructor and override `createClone` to preserve their runtime type and metadata. The lower-level prebuilt-element constructor is library-private and is reserved for trusted in-library construction. ## Value-domain arrays @@ -109,10 +110,11 @@ final values = LogicValueArray.fromInts( ); final sameValues = LogicValueArray.fromFlatInts( [2, 3], - 8, [1, 2, 3, 4, 5, 6], + elementWidth: 8, ); -final emptyRows = LogicValueArray.fromFlat([2, 0], 8, const []); +final emptyRows = + LogicValueArray.fromFlat([2, 0], const [], elementWidth: 8); final transposed = values.transpose2D(); // Dimensions: [3, 2] final signals = values.toLogicArray(name: 'values'); // signals are driven by values @@ -124,7 +126,7 @@ Nested constructors reject ragged rows, inconsistent nesting depth, and mismatch The root list of a nested constructor always represents an array dimension. Below the root, an object matching `T` is treated as one semantic value before it is considered as another list dimension. This permits list-valued semantic elements; use `fromFlat` when the intended interpretation would otherwise be ambiguous. -Both value-array classes are `LogicValue`s. Their `width` and deprecated `length` count packed bits, while `elementCount` counts array positions. Bit indexing, equality, hashing, arithmetic, and bitwise operations use the packed value and do not consider shape. The `packed` getter exposes the ordinary `LogicValue` representation. +Both value-array classes are `LogicValue`s. Their `width` and deprecated `length` count packed bits, while `arrayValues.length` counts array positions. `arrayValues` has one entry for each configured array position in row-major order. Bit indexing, equality, hashing, arithmetic, and bitwise operations use the packed value and do not consider shape. The `packed` getter exposes the ordinary `LogicValue` representation. `TypedLogicArray.value` and `previousValue` return `TypedLogicValueArray` snapshots without adding hardware to the graph. `LogicArray` overrides these with the concrete `LogicValueArray` return type. The standard `changed`, `glitch`, and edge APIs remain packed `LogicValueChanged` events, so typed arrays retain the normal `Logic` event contract. Since all value arrays are `LogicValue`s, use the target-side `put` API for immediate assignment or `inject` for scheduled assignment. Both follow the ordinary packed-value contract, so same-width values remain assignable regardless of their shape metadata. diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 311fd4ec31..b13324fe3e 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -279,7 +279,7 @@ class LogicArray extends TypedLogicArray { required super.name, required super.naming, required bool isNet}) - : super.structured( + : super._structured( elements, ({name}) => isNet ? LogicNet(name: name, width: elementWidth) @@ -308,7 +308,11 @@ class LogicArray extends TypedLogicArray { isNet: isNet) as LogicArray; @override - LogicValueArray get value => LogicValueArray.fromLogicArray(this); + LogicValueArray get value => LogicValueArray.fromFlat( + dimensions, + arrayElements.map((element) => element.value), + elementWidth: elementWidth, + ); @override LogicValueArray? get previousValue => @@ -316,8 +320,8 @@ class LogicArray extends TypedLogicArray { ? null : LogicValueArray.fromFlat( dimensions, - elementWidth, arrayElements.map((element) => element.previousValue!), + elementWidth: elementWidth, ); @override diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index 22b3eb043b..f88e748aba 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -57,9 +57,14 @@ LogicValueCodec _resolveTypedLogicValueCodec( /// [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 { - /// Labels used to name elements at each dimension. - final List dimensionNames; + /// Construction prefixes retained for typed-array cloning. + final List _dimensionNames; /// Recreates one [T] for cloning and shape transformations. final TypedLogicArrayElementBuilder _elementBuilder; @@ -119,15 +124,18 @@ class TypedLogicArray extends BaseLogicArray { required super.numUnpackedDimensions, super.name, super.naming, - }) : dimensionNames = build.dimensionNames, + }) : _dimensionNames = build.dimensionNames, super.structured(build.elements, dimensions: build.dimensions, elementWidth: build.elementWidth, isNet: build.isNet); - /// Creates a typed array from structurally compatible [elements]. - @protected - TypedLogicArray.structured( + /// 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, @@ -140,7 +148,7 @@ class TypedLogicArray extends BaseLogicArray { TypedLogicArrayElementCompatibility? elementCompatibility, List? dimensionNames, }) : _elementCompatibility = elementCompatibility, - dimensionNames = + _dimensionNames = _normalizeLogicArrayDimensionNames(dimensions, dimensionNames), super.structured( name: name, @@ -201,7 +209,7 @@ class TypedLogicArray extends BaseLogicArray { /// Creates a clone while allowing subclasses to preserve their runtime type. /// /// Subclasses with additional metadata should override this method and - /// reconstruct that metadata together with [dimensions], [dimensionNames], + /// reconstruct that metadata together with [dimensions], [_dimensionNames], /// [elementWidth], [isNet], and [numUnpackedDimensions]. @protected TypedLogicArray createClone({ @@ -212,7 +220,7 @@ class TypedLogicArray extends BaseLogicArray { TypedLogicArray(dimensions, _elementBuilder, valueCodec: valueCodec, elementCompatibility: _elementCompatibility, - dimensionNames: dimensionNames, + dimensionNames: _dimensionNames, name: name ?? this.name, naming: naming, numUnpackedDimensions: @@ -326,13 +334,6 @@ class _TypedLogicArrayBuild { .cast>() .expand((element) => element.arrayElements) .toList(growable: false); - if (typedLeaves.any((leaf) => - leaf is LogicStructure && - leaf is! BaseLogicArray && - _containsNestedArray(leaf))) { - throw LogicConstructionException( - 'TypedLogicArray leaves cannot contain nested typed array fields.'); - } if (typedLeaves.any(_containsUnassignableLeaf)) { throw LogicConstructionException('TypedLogicArray leaves must be ' 'driveable and cannot contain Consts.'); @@ -342,12 +343,6 @@ class _TypedLogicArrayBuild { throw LogicConstructionException('TypedLogicArray leaves must be ' 'driveable and cannot contain Consts.'); } - if (prototype is LogicStructure && - prototype is! BaseLogicArray && - _containsNestedArray(prototype)) { - throw LogicConstructionException( - 'TypedLogicArray leaves cannot contain nested typed array fields.'); - } final prototypeNetComposition = _netComposition(prototype); final elementsForNetValidation = typedLeaves.isEmpty ? [prototype] : typedLeaves; @@ -382,12 +377,6 @@ class _TypedLogicArrayBuild { prototypeNetComposition.every((isNet) => isNet)); } - /// Whether [structure] recursively contains an array field. - static bool _containsNestedArray(LogicStructure structure) => - structure.elements.any((element) => - element is BaseLogicArray || - (element is LogicStructure && _containsNestedArray(element))); - /// Whether [leaf] is or recursively contains an unassignable constant. static bool _containsUnassignableLeaf(Logic leaf) => leaf is Const || diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index e7265b1302..8e42a47379 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -939,6 +939,7 @@ class SynthLogicArrayElement extends SynthLogic { /// /// 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; diff --git a/lib/src/values/logic_value_array.dart b/lib/src/values/logic_value_array.dart index c57de49829..2f633a3cdc 100644 --- a/lib/src/values/logic_value_array.dart +++ b/lib/src/values/logic_value_array.dart @@ -29,41 +29,47 @@ class LogicValueArray extends TypedLogicValueArray { nested.dimensions, elementWidth, nested.values); } - /// Creates a value array from row-major [values] and explicit metadata. + /// 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, - int elementWidth, - Iterable values, - ) { + List dimensions, Iterable values, + {int? elementWidth}) { final normalizedDimensions = _validateValueArrayDimensions(dimensions); - _validateValueArrayElementWidth(elementWidth); final packedValues = values.toList(growable: false); _validateValueCount(normalizedDimensions, packedValues.length, 'values'); - _validateElementWidths(packedValues, elementWidth, '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, elementWidth, packedValues); + normalizedDimensions, resolvedElementWidth, packedValues); } /// Creates an empty, zero-width value array. factory LogicValueArray.empty() => - LogicValueArray.fromFlat(const [0], 0, const []); + LogicValueArray.fromFlat(const [0], const [], elementWidth: 0); /// Generates values from row-major multidimensional indices. factory LogicValueArray.generate( - List dimensions, - int elementWidth, - LogicValue Function(List indices) generator, - ) { + List dimensions, LogicValue Function(List indices) generator, + {int? elementWidth}) { final normalizedDimensions = _validateValueArrayDimensions(dimensions); + final values = [ + for (var index = 0; + index < _valueArrayLength(normalizedDimensions); + index++) + generator(_valueArrayIndices(normalizedDimensions, index)), + ]; return LogicValueArray.fromFlat( normalizedDimensions, - elementWidth, - Iterable.generate( - _valueArrayLength(normalizedDimensions), - (index) => generator( - _valueArrayIndices(normalizedDimensions, index), - ), - ), + values, + elementWidth: elementWidth, ); } @@ -79,30 +85,19 @@ class LogicValueArray extends TypedLogicValueArray { } return LogicValueArray.fromFlat( nested.dimensions, - elementWidth, 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, - int elementWidth, - Iterable values, - ) => + List dimensions, Iterable values, + {required int elementWidth}) => LogicValueArray.fromFlat( dimensions, - elementWidth, values.map((value) => LogicValue.ofInt(value, elementWidth)), - ); - - /// Captures the current values of a hardware [LogicArray]. - factory LogicValueArray.fromLogicArray( - TypedLogicArray values) => - LogicValueArray.fromFlat( - values.dimensions, - values.elementWidth, - values.arrayElements.map((element) => element.value), + elementWidth: elementWidth, ); /// Stacks equally shaped arrays along a new outer dimension. @@ -121,6 +116,10 @@ class LogicValueArray extends TypedLogicValueArray { ); } + /// Stores already-normalized packed [values] 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, @@ -140,7 +139,10 @@ class LogicValueArray extends TypedLogicValueArray { @override LogicValueArray map(LogicValue Function(LogicValue value) transform) => LogicValueArray.fromFlat( - dimensions, elementWidth, arrayValues.map(transform)); + dimensions, + arrayValues.map(transform), + elementWidth: elementWidth, + ); @override LogicValueArray indexedMap( @@ -148,31 +150,10 @@ class LogicValueArray extends TypedLogicValueArray { ) => LogicValueArray.fromFlat( dimensions, - elementWidth, indexedValues.map((entry) => transform(entry.$1, entry.$2)), + elementWidth: elementWidth, ); - @override - LogicValueArray mapMajorSlices( - TypedLogicValueArray Function( - TypedLogicValueArray slice) - transform, - ) { - final transformed = majorSlices.map(transform).toList(growable: false); - if (transformed.isEmpty) { - throw StateError('Cannot infer a mapped shape from zero major slices.'); - } - final first = transformed.first; - transformed.skip(1).forEach(first._checkStackCompatible); - return LogicValueArray._stored( - [transformed.length, ...first.dimensions], - first.elementWidth, - transformed - .expand((slice) => slice._packedElements) - .toList(growable: false), - ); - } - @override LogicValueArray reshape(List newDimensions) => super.reshape(newDimensions) as LogicValueArray; diff --git a/lib/src/values/typed_logic_value_array.dart b/lib/src/values/typed_logic_value_array.dart index 42e765c185..c34359072d 100644 --- a/lib/src/values/typed_logic_value_array.dart +++ b/lib/src/values/typed_logic_value_array.dart @@ -209,15 +209,12 @@ class TypedLogicValueArray extends LogicValue { packed.width == width, 'Packed width must match the LogicValue width.'); } - /// Number of semantic array elements. - int get elementCount => _arrayValues.length; - /// 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( - elementCount, + arrayValues.length, (index) => ( _valueArrayIndices(dimensions, index), _arrayValues[index], @@ -244,11 +241,8 @@ class TypedLogicValueArray extends LogicValue { } /// Returns the semantic value at multidimensional [indices]. - T at(List indices) => _arrayValues[flatIndexOf(indices)]; - - /// Returns the row-major flat index for multidimensional [indices]. - int flatIndexOf(List indices) => - _valueArrayFlatIndex(dimensions, indices); + T at(List indices) => + _arrayValues[_valueArrayFlatIndex(dimensions, indices)]; /// Maps semantic values while preserving shape and codec. TypedLogicValueArray map(T Function(T value) transform) => @@ -270,18 +264,12 @@ class TypedLogicValueArray extends LogicValue { codec: codec, ); - /// Maps slices along the first dimension and stacks the results. - TypedLogicValueArray mapMajorSlices( - TypedLogicValueArray Function(TypedLogicValueArray slice) transform, - ) => - TypedLogicValueArray.stack(majorSlices.map(transform)); - /// Returns the same row-major values with [newDimensions]. TypedLogicValueArray reshape(List newDimensions) { final normalizedDimensions = _validateValueArrayDimensions(newDimensions); - if (_valueArrayLength(normalizedDimensions) != elementCount) { + if (_valueArrayLength(normalizedDimensions) != arrayValues.length) { throw ArgumentError.value(newDimensions, 'newDimensions', - 'Must contain $elementCount array elements.'); + 'Must contain ${arrayValues.length} array elements.'); } return _createStored(normalizedDimensions, _arrayValues, _packedElements); } @@ -294,7 +282,7 @@ class TypedLogicValueArray extends LogicValue { final packedElements = []; for (var row = 0; row < newDimensions[0]; row++) { for (var column = 0; column < newDimensions[1]; column++) { - final source = flatIndexOf([column, row]); + final source = _valueArrayFlatIndex(dimensions, [column, row]); semanticValues.add(_arrayValues[source]); packedElements.add(_packedElements[source]); } diff --git a/test/pair_interface_test.dart b/test/pair_interface_test.dart index 5eff4f3ef6..57bf3874ba 100644 --- a/test/pair_interface_test.dart +++ b/test/pair_interface_test.dart @@ -262,7 +262,6 @@ void main() { expect(clonedSamples, isNot(same(samples))); expect(clonedSamples.dimensions, [2, 2]); - expect(clonedSamples.dimensionNames, ['row_', 'column_']); expect(clonedSamples.arrayElements, everyElement(isA<_PairSample>())); expect(clonedSamples.at([1, 1]).high.width, 2); expect(identical(clonedSamples.valueCodec, _pairValueCodec), isTrue); @@ -272,7 +271,6 @@ void main() { await module.build(); expect(module.samples.dimensions, [2, 2]); - expect(module.samples.dimensionNames, ['row_', 'column_']); expect(module.samples.at([1, 1]), isA<_PairSample>()); expect(module.samples.at([1, 1]).high.width, 2); expect(identical(module.samples.valueCodec, _pairValueCodec), isTrue); diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 286f7354c2..843888d7fd 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -144,7 +144,6 @@ class _SampleArray extends TypedLogicArray<_SampleStructure, _SampleValue> { _SampleArray( dimensions, schema: schema, - dimensionNames: dimensionNames, name: name ?? this.name, naming: naming, numUnpackedDimensions: @@ -220,6 +219,62 @@ class _TypedArrayHierarchy extends Module { } } +class _NestedArrayStructure extends LogicStructure { + final Logic before; + final LogicArray lanes; + final Logic after; + + factory _NestedArrayStructure({String? name}) => _NestedArrayStructure._( + Logic(name: 'before', width: 2), + LogicArray([2], 3, name: 'lanes'), + Logic(name: 'after'), + name: name ?? 'nested', + ); + + _NestedArrayStructure._( + this.before, + this.lanes, + this.after, { + required String name, + }) : super([before, lanes, after], name: name); + + @override + _NestedArrayStructure clone({String? name}) => + _NestedArrayStructure(name: name ?? this.name); +} + +class _NestedArrayBoundaryModule extends Module { + _NestedArrayBoundaryModule( + TypedLogicArray, + LogicValue> + source) { + final input = addTypedInput('valuesIn', source); + final output = 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 = output.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]); + } + } + } + } +} + +LogicValue _nestedPacked( + int before, + int lane0, + int lane1, + int after, +) => + LogicValue.ofInt( + before | (lane0 << 2) | (lane1 << 5) | (after << 8), + 9, + ); + class _TypedInputModule extends Module { late final T values; @@ -280,6 +335,7 @@ void main() { [2, 2], schema: 'sample-v2', dimensionNames: const ['row_', 'column_'], + numUnpackedDimensions: 1, ); final assigned = TypedLogicValueArray<_SampleValue>.fromFlat( [2, 2], @@ -308,6 +364,7 @@ void main() { for (final result in transformed) { expect(result, isA>()); expect(identical(result.valueCodec, _sampleValueCodec), isTrue); + expect(result.numUnpackedDimensions, 1); expect(result.value, isA>()); } }); @@ -397,7 +454,11 @@ void main() { numUnpackedDimensions: 1, ), ]; - final packed = LogicValueArray.fromFlatInts([6], 4, [1, 2, 3, 4, 5, 6]); + final packed = LogicValueArray.fromFlatInts( + [6], + [1, 2, 3, 4, 5, 6], + elementWidth: 4, + ); for (final array in arrays) { array.put(packed); @@ -461,7 +522,6 @@ void main() { final typedUpdated = updated as TypedLogicArray<_SampleStructure, LogicValue>; expect(typedUpdated.dimensions, [2, 2]); - expect(typedUpdated.dimensionNames, ['row_', 'column_']); expect(typedUpdated.elementWidth, 3); expect(typedUpdated.numUnpackedDimensions, 1); expect( @@ -485,7 +545,6 @@ void main() { for (final slice in slices) { expect(slice, isA>()); expect(slice.dimensions, [3, 2]); - expect(slice.dimensionNames, ['column_', 'lane_']); expect(slice.elementWidth, 3); expect(slice.numUnpackedDimensions, 1); expect( @@ -527,7 +586,11 @@ void main() { _SampleStructure.new, ); final packed = LogicArray([2, 2], 3) - ..put(LogicValueArray.fromFlatInts([4], 3, [1, 2, 3, 4])); + ..put(LogicValueArray.fromFlatInts( + [4], + [1, 2, 3, 4], + elementWidth: 3, + )); values <= packed; @@ -632,20 +695,6 @@ void main() { ); }); - test('preserves unpacked dimensions through cloning', () { - final values = TypedLogicArray( - [2, 3], - ({name}) => Logic(name: name, width: 4), - numUnpackedDimensions: 1, - ); - - expect(values.clone().numUnpackedDimensions, 1); - expect( - values.named('renamed').numUnpackedDimensions, - 1, - ); - }); - test('validates dimensions, names, and leaf widths', () { expect( () => TypedLogicArray(const [], Logic.new), @@ -793,7 +842,6 @@ void main() { final specialized = result as _SampleArray; expect(specialized.schema, 'sample-v2'); expect(specialized.dimensions, [2, 3]); - expect(specialized.dimensionNames, ['row_', 'column_']); expect(specialized.elementWidth, 3); expect(specialized.numUnpackedDimensions, 1); expect(specialized.isNet, isFalse); @@ -820,7 +868,11 @@ void main() { [2, 2], _SampleStructure.new, ); - final sameShape = LogicValueArray.fromFlatInts([2, 2], 3, [0, 1, 2, 3]); + final sameShape = LogicValueArray.fromFlatInts( + [2, 2], + [0, 1, 2, 3], + elementWidth: 3, + ); final differentShape = sameShape.reshape([4]); final typedValues = TypedLogicValueArray.fromFlat( [1, 4], @@ -872,7 +924,8 @@ void main() { expect(values.value.elementWidth, 3); } - final emptyValues = LogicValueArray.fromFlat([0, 3], 3, const []); + final emptyValues = + LogicValueArray.fromFlat([0, 3], const [], elementWidth: 3); final emptyTarget = TypedLogicArray<_SampleStructure, LogicValue>( [2, 0], _SampleStructure.new, @@ -987,7 +1040,8 @@ void main() { expect(module.valuesOut.at([1, 2]).high.width, 2); }); - test('preserves semantic net arrays through typed inout ports', () { + test('preserves and builds semantic net arrays through typed inout ports', + () async { final source = TypedLogicArray<_NetSampleStructure, _SampleValue>( [2], _NetSampleStructure.new, @@ -1000,6 +1054,8 @@ void main() { 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 { @@ -1175,6 +1231,48 @@ void main() { isA>(), ); }); + + test('simulates nested typed 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 input = [ + _nestedPacked(1, 2, 3, 0), + _nestedPacked(2, 4, 5, 1), + _nestedPacked(3, 6, 7, 0), + _nestedPacked(0, 1, 2, 1), + _nestedPacked(3, 5, 1, 0), + _nestedPacked(2, 7, 4, 1), + ]; + final output = [242, 423, 40, 479, 150, 333]; + final inputValue = LogicValue.ofIterable(input); + final outputValue = LogicValue.ofInt( + output.indexed.fold( + 0, + (packed, entry) => packed | (entry.$2 << (entry.$1 * 9)), + ), + 54, + ); + final vectors = [ + Vector({'valuesIn': inputValue}, {'valuesOut': outputValue}), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + // Icarus rejects the generated nested packed/unpacked indexing here; + // the ROHD functional result is still checked above. + expect(module.generateSynth(), contains('module')); + }); }); group('LogicValueArray', () { @@ -1186,16 +1284,20 @@ void main() { [leaves[0], leaves[1]], [leaves[2], leaves[3]], ]); - final flat = LogicValueArray.fromFlat([2, 2], 8, leaves); + final flat = LogicValueArray.fromFlat([2, 2], leaves); final nestedInts = LogicValueArray.fromInts([ [1, 2], [3, 4], ], elementWidth: 8); - final flatInts = LogicValueArray.fromFlatInts([2, 2], 8, [1, 2, 3, 4]); + final flatInts = LogicValueArray.fromFlatInts( + [2, 2], + [1, 2, 3, 4], + elementWidth: 8, + ); expect(nested.dimensions, [2, 2]); expect(nested.elementWidth, 8); - expect(nested.elementCount, 4); + expect(nested.arrayValues.length, 4); expect(nested.width, 32); // The deprecated LogicValue length retains its packed-bit meaning. // ignore: deprecated_member_use_from_same_package @@ -1214,7 +1316,6 @@ void main() { ], elementWidth: 8); expect(values.at([1, 1]).toInt(), 5); - expect(values.flatIndexOf([1, 2]), 5); expect(values.indexedValues.last.$1, equals([1, 2])); expect( values.majorSlices.map((slice) => @@ -1247,7 +1348,6 @@ void main() { var generatorCalls = 0; final generated = LogicValueArray.generate( [2, 3], - 8, (indices) { generatorCalls++; return LogicValue.ofInt(indices[0] * 3 + indices[1] + 1, 8); @@ -1264,11 +1364,12 @@ void main() { }); final generatedEmpty = LogicValueArray.generate( [2, 0], - 8, (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()), @@ -1281,7 +1382,7 @@ void main() { ); expect(empty.dimensions, [0]); expect(empty.elementWidth, 0); - expect(empty.elementCount, 0); + expect(empty.arrayValues.length, 0); expect(empty.width, 0); expect(empty.arrayValues, isEmpty); expect(empty.packed, LogicValue.empty); @@ -1299,14 +1400,17 @@ void main() { throwsArgumentError, ); expect( - () => LogicValueArray.fromFlat([0, 2], 8, const []) - .mapMajorSlices((slice) => slice), - throwsA(isA()), + () => LogicValueArray.stack( + LogicValueArray.fromFlat([0, 2], const [], elementWidth: 8) + .majorSlices, + ), + throwsArgumentError, ); }); test('round-trips slices with an empty inner dimension', () { - final values = LogicValueArray.fromFlat([2, 0], 8, const []); + final values = + LogicValueArray.fromFlat([2, 0], const [], elementWidth: 8); final slices = values.majorSlices.toList(growable: false); expect(slices, hasLength(2)); @@ -1316,7 +1420,6 @@ void main() { ]); expect(slices.map((slice) => slice.elementWidth), [8, 8]); expect(LogicValueArray.stack(slices).dimensions, [2, 0]); - expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); }); test('matches packed semantics across storage and validity domains', () { @@ -1375,7 +1478,7 @@ void main() { final shaped = testCase.shaped; final packed = shaped.packed; final other = testCase.other; - final reshaped = shaped.reshape([1, shaped.elementCount]); + final reshaped = shaped.reshape([1, shaped.arrayValues.length]); expect(shaped, packed, reason: testCase.name); expect(packed, shaped, reason: testCase.name); @@ -1445,28 +1548,72 @@ void main() { throwsArgumentError, ); expect( - () => LogicValueArray.fromFlat([2, 2], 8, [one]), + () => LogicValueArray.fromFlat([2, 2], [one]), + throwsArgumentError, + ); + expect( + LogicValueArray.fromFlat( + [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], + ).elementWidth, + 4, + ); + expect( + LogicValueArray.fromFlat( + [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], + elementWidth: 4, + ).elementWidth, + 4, + ); + expect( + () => LogicValueArray.fromFlat( + [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 3)], + ), + throwsArgumentError, + ); + expect( + () => LogicValueArray.fromFlat( + [2], + [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], + elementWidth: 3, + ), throwsArgumentError, ); expect( () => LogicValueArray.fromInts(const [], elementWidth: 8), throwsArgumentError, ); - expect(() => LogicValueArray.fromFlat(const [], 8, const []), + expect( + () => LogicValueArray.fromFlat(const [], const [], elementWidth: 8), throwsArgumentError); + expect( + LogicValueArray.fromFlat([2, 0], const [], elementWidth: 8) + .elementWidth, + 8, + ); + expect( + () => LogicValueArray.fromFlat([2, 0], const []), + throwsArgumentError, + ); }); test('rejects invalid value indices, shapes, widths, and stacks', () { final values = LogicValueArray.fromFlatInts( [2, 2], - 8, [1, 2, 3, 4], + elementWidth: 8, + ); + final oneDimensional = LogicValueArray.fromFlatInts( + [4], + [1, 2, 3, 4], + elementWidth: 8, ); - final oneDimensional = LogicValueArray.fromFlatInts([4], 8, [1, 2, 3, 4]); final threeDimensional = LogicValueArray.fromFlatInts( [1, 2, 2], - 8, [1, 2, 3, 4], + elementWidth: 8, ); for (final indices in >[ @@ -1492,32 +1639,32 @@ void main() { throwsA(isA()), ); expect( - () => LogicValueArray.fromFlat([-1], 8, const []), + () => LogicValueArray.fromFlat([-1], const [], elementWidth: 8), throwsArgumentError, ); expect( - () => LogicValueArray.fromFlat([1], -1, const []), + () => LogicValueArray.fromFlat([1], const [], elementWidth: -1), throwsArgumentError, ); expect( () => LogicValueArray.generate( [1], - 8, (indices) => LogicValue.ofInt(indices.single, 4), + elementWidth: 8, ), throwsArgumentError, ); expect( () => LogicValueArray.stack([ - LogicValueArray.fromFlatInts([2], 8, [1, 2]), - LogicValueArray.fromFlatInts([1, 2], 8, [3, 4]), + LogicValueArray.fromFlatInts([2], [1, 2], elementWidth: 8), + LogicValueArray.fromFlatInts([1, 2], [3, 4], elementWidth: 8), ]), throwsArgumentError, ); expect( () => LogicValueArray.stack([ - LogicValueArray.fromFlatInts([2], 8, [1, 2]), - LogicValueArray.fromFlatInts([2], 4, [3, 4]), + LogicValueArray.fromFlatInts([2], [1, 2], elementWidth: 8), + LogicValueArray.fromFlatInts([2], [3, 4], elementWidth: 4), ]), throwsArgumentError, ); @@ -1694,7 +1841,6 @@ void main() { TypedLogicValueArray.stack(slices).dimensions, [2, 0], ); - expect(values.mapMajorSlices((slice) => slice).dimensions, [2, 0]); }); }); } From 358907ceeac7d0f7b78b20fcd05baf81342fd00c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 13 Sep 2026 17:25:50 -0700 Subject: [PATCH 15/43] removed more redundant API elements --- doc/user_guide/_docs/A19-logic-structures.md | 20 ------ lib/src/signals/logic_structure.dart | 57 ----------------- lib/src/values/logic_value_array.dart | 3 +- test/logic_structure_test.dart | 65 -------------------- 4 files changed, 2 insertions(+), 143 deletions(-) diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index f6bfab5479..aa2d53971a 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -41,26 +41,6 @@ rvStruct.elements[0] <= ready; rvStruct.elements[1] <= valid; ``` -## Promoting nested fields - -Use `flattenOuter` to promote fields from direct child structures into a new generic `LogicStructure`. The new fields are clones connected to their original sources, so the original structure remains unchanged. By default, promoted names are prefixed with the direct child structure name to avoid collisions. - -```dart -final config = LogicStructure([ - Logic(name: 'mode', width: 2), - Logic(name: 'valid'), -], name: 'config'); -final control = LogicStructure([ - Logic(name: 'enable'), - config, -], name: 'control'); - -final flattened = control.flattenOuter(); -// Field names: enable, config_mode, config_valid. -``` - -Only direct non-array child structures are promoted. Nested grandchildren remain structures, and a duplicate resulting field name throws `LogicConstructionException`. - ## Making your own structure Referencing elements by index is often not ideal for named signals. We can do better by building our own structure that inherits from `LogicStructure`. diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index cf4b69049b..f18f1a9423 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -174,63 +174,6 @@ class LogicStructure implements Logic { _calculateLeafElements(), ); - /// Promotes direct non-array child structures into a new generic structure. - /// - /// Each promoted element is cloned and connected to its source. When - /// [prefixFieldNames] is `true`, promoted fields are named - /// `${child.name}_${field.name}`. This preserves the immediate structure - /// path and avoids common field-name collisions. The method throws a - /// [LogicConstructionException] when the resulting field names collide. - LogicStructure flattenOuter({String? name, bool prefixFieldNames = true}) { - final sources = <(Logic, String)>[]; - for (final element in elements) { - if (element is LogicStructure && element is! BaseLogicArray) { - for (final field in element.elements) { - final prefix = - element.name.endsWith('_') ? element.name : '${element.name}_'; - final fieldName = - prefixFieldNames ? '$prefix${field.name}' : field.name; - sources.add((field, fieldName)); - } - } else { - sources.add((element, element.name)); - } - } - - final names = {}; - for (final source in sources) { - if (!names.add(source.$2)) { - throw LogicConstructionException( - 'Flattened structure contains duplicate field name ${source.$2}.'); - } - } - - final flattenedElements = sources.map((source) { - final flattened = _cloneDriveable(source.$1, name: source.$2); - return flattened..gets(source.$1); - }).toList(growable: false); - return LogicStructure(flattenedElements, name: name ?? this.name); - } - - /// Clones a promoted field without leaving any constant leaves to drive. - Logic _cloneDriveable(Logic source, {required String name}) { - if (source is Const) { - return Logic(width: source.width, name: name); - } - if (source is LogicStructure && source.hasConsts) { - if (source is BaseLogicArray) { - throw LogicConstructionException( - 'Cannot promote an array containing constant elements.'); - } - return LogicStructure( - source.elements - .map((element) => _cloneDriveable(element, name: element.name)), - name: name, - ); - } - return source.clone(name: name); - } - /// Compute the list of all leaf elements, to be cached in [leafElements]. List _calculateLeafElements() { final leaves = []; diff --git a/lib/src/values/logic_value_array.dart b/lib/src/values/logic_value_array.dart index 2f633a3cdc..4cf29ff4fa 100644 --- a/lib/src/values/logic_value_array.dart +++ b/lib/src/values/logic_value_array.dart @@ -116,7 +116,8 @@ class LogicValueArray extends TypedLogicValueArray { ); } - /// Stores already-normalized packed [values] using the identity codec. + /// 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. diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index e6aab242fa..7fea35e0bf 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -244,71 +244,6 @@ void main() { expect(orig.clone(name: 'newName').name, 'newName'); }); - test('flatten outer clones and connects promoted fields', () { - final config = LogicStructure([ - Logic(name: 'mode', width: 2), - Logic(name: 'valid'), - ], name: 'config'); - final control = LogicStructure([ - Logic(name: 'enable'), - config, - ], name: 'control'); - - final flattened = control.flattenOuter(name: 'flatControl'); - - expect(flattened.name, 'flatControl'); - expect(flattened.elements.map((element) => element.name), - ['enable', 'config_mode', 'config_valid']); - expect(flattened.elements[1].width, 2); - expect( - flattened.elements[0].srcConnections, contains(control.elements[0])); - expect( - flattened.elements[1].srcConnections, contains(config.elements[0])); - expect( - flattened.elements[2].srcConnections, contains(config.elements[1])); - }); - - test('flatten outer connects constant promoted fields', () { - final config = LogicStructure([ - Const(1, width: 1), - ], name: 'config'); - final control = LogicStructure([config], name: 'control'); - - final flattened = control.flattenOuter(name: 'flatControl'); - - expect(flattened.elements.single, isA()); - expect(flattened.elements.single.srcConnections, - contains(config.elements.single)); - }); - - test('flatten outer connects nested constant promoted fields', () { - final grandchild = LogicStructure([ - Const(1, width: 1), - ], name: 'grandchild'); - final config = LogicStructure([grandchild], name: 'config'); - final control = LogicStructure([config], name: 'control'); - - final flattened = control.flattenOuter(name: 'flatControl'); - final promoted = flattened.elements.single; - - expect(promoted, isA()); - expect((promoted as LogicStructure).elements.single, isA()); - expect(promoted.elements.single.srcConnections, - contains(grandchild.elements.single)); - }); - - test('flatten outer rejects duplicate promoted field names', () { - final control = LogicStructure([ - Logic(name: 'mode'), - LogicStructure([ - Logic(name: 'mode'), - ], name: 'config'), - ], name: 'control'); - - expect(() => control.flattenOuter(prefixFieldNames: false), - throwsA(isA())); - }); - test('tricky withSet', () { // first field has width of 72 so this is the starting point // second field has a width of 12 From becc0d1eaae8c3157b182b2786cefa4dc3534fea Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 13 Sep 2026 23:21:51 -0700 Subject: [PATCH 16/43] updated expected value for packed value --- test/typed_logic_array_test.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 843888d7fd..c5683640d8 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -1257,13 +1257,8 @@ void main() { ]; final output = [242, 423, 40, 479, 150, 333]; final inputValue = LogicValue.ofIterable(input); - final outputValue = LogicValue.ofInt( - output.indexed.fold( - 0, - (packed, entry) => packed | (entry.$2 << (entry.$1 * 9)), - ), - 54, - ); + final outputValue = LogicValue.ofIterable( + output.map((value) => LogicValue.ofInt(value, 9))); final vectors = [ Vector({'valuesIn': inputValue}, {'valuesOut': outputValue}), ]; From e31ef81e7213a38e7d29a9e8065b4b621e3edc21 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 09:22:04 -0700 Subject: [PATCH 17/43] major push into synthesis for nested structures --- doc/user_guide/_docs/A20-logic-arrays.md | 28 +- .../netlist/netlist_synthesizer.dart | 2 +- .../netlist/netlist_validation.dart | 35 +- ...systemverilog_synth_module_definition.dart | 67 ++- .../systemverilog_synthesis_result.dart | 96 ++-- .../synthesizers/utilities/synth_logic.dart | 305 +++++++++--- .../utilities/synth_module_definition.dart | 118 ++++- lib/src/utilities/simcompare.dart | 15 +- test/netlist_synthesizer_test.dart | 37 ++ test/synth_name_parity_test.dart | 88 ++++ test/typed_logic_array_test.dart | 442 +++++++++++++++++- 11 files changed, 1115 insertions(+), 118 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index d4c9d31ea7..433ffbed1b 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -92,7 +92,33 @@ These APIs intentionally stop at different boundaries: 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, structures containing a `Const`, and element structures containing nested arrays are rejected. Nested arrays remain available through their existing `arrayElements`, `at`, and `indexedElements` traversal APIs; no public recursive layout-conversion helper is provided. +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. + +SystemVerilog declarations retain the packed/unpacked split of the root array. +When a configured element contains another array, that nested value occupies +packed bits within the root element at the port boundary. Nested coordinates +and structure fields are therefore lowered to one packed bit or range selection +instead of adding dimensions that are absent from the declaration. An +array-valued structure field may use a separate internal declaration with its +own packed/unpacked split; synthesis inserts the packing or unpacking needed at +the enclosing port. + +This lowering preserves the existing inline declaration and naming style; it +does not introduce SystemVerilog `typedef`s or generated structural type names. +Whole assignments involving nested unpacked arrays are emitted as element or +packed-range assignments for simulator portability. The netlist output retains +the recursive array and structure metadata in `logic_type`. + +ROHD, Icarus, and Verilator simulation cover fully packed nested arrays, +mixed packed/unpacked outer and inner arrays, and structures containing +unpacked ordinary and typed array fields across module boundaries. Four-state +structured inout drive and release, including an unpacked nested net-array +field across a child boundary, is covered in ROHD and Icarus. Verilator does +not currently simulate that last case because it does not support the +bidirectional `tran` primitive used for net aliasing. `TypedLogicArray` is also the supported base for custom typed arrays. Subclasses should use the normal constructor and override `createClone` to preserve their runtime type and metadata. The lower-level prebuilt-element constructor is library-private and is reserved for trusted in-library construction. diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 4fcad37754..7884d41f4d 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -1051,7 +1051,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_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index c8399f2ae2..ae8b556a5d 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -53,8 +53,9 @@ class NetlistValidation { static void validate( Map> ports, Map> cells, - String moduleName, - ) { + String moduleName, { + Map? netnames, + }) { final issues = []; final driversByBit = _driversByBit(ports, cells); @@ -72,11 +73,41 @@ class NetlistValidation { )); } + if (netnames != null) { + 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; + } + final bits = (netname['bits'] as List?)?.whereType() ?? const []; + final aggregateDrivers = { + for (final bit in bits) ...driversByBit[bit] ?? const [], + }; + if (aggregateDrivers.length <= 1 || + aggregateDrivers.every(_isTriStateDriver)) { + continue; + } + issues.add(NetlistValidationIssue( + 'aggregate net "${entry.key}" is reached from multiple drivers: ' + '${aggregateDrivers.join(', ')}', + netname: entry.key, + drivers: aggregateDrivers.toList(), + )); + } + } + if (issues.isNotEmpty) { throw NetlistValidationException(moduleName, issues); } } + static bool _isTriStateDriver(String driver) => driver.endsWith(r'($tribuf)'); + /// Collects the port and cell output drivers for each integer bit ID. static Map> _driversByBit( Map> ports, diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 28cf8ed5bd..51788ef580 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -34,6 +34,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { @override void process() { + _expandNestedUnpackedArrayAssignments(); _inlinePackedRangesIntoSubmoduleInputs(); _collapseAggregateConnections(); _collapseWholeNetBuses(); @@ -136,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 @@ -1625,8 +1684,12 @@ class _NetConnect extends Module with SystemVerilog { @override String? definitionVerilog(String definitionType) => ''' // A special module for connecting two nets bidirectionally -module $definitionType #(parameter int WIDTH=1) (w, w); -inout wire[WIDTH-1:0] w; +module $definitionType #(parameter int WIDTH=1) ($n0Name, $n1Name); +inout wire[WIDTH-1:0] $n0Name; +inout wire[WIDTH-1:0] $n1Name; +for (genvar i = 0; i < WIDTH; i++) begin + tran ($n0Name[i], $n1Name[i]); +end endmodule'''; } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 2ca7afa988..edd02d5032 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -11,6 +11,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/systemverilog/systemverilog_synth_module_definition.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart'; import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; @@ -170,7 +171,13 @@ 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', + _requiresNetDeclaration(sig) ? 'wire' : 'var', + configuration.outputPortType, + sig, + forceObjectType: _requiresNetDeclaration(sig), + ); }); /// Representation of all inout port declarations in generated SV. @@ -193,15 +200,56 @@ class SystemVerilogSynthesisResult extends SynthesisResult { String direction, String objectType, SystemVerilogPortTypeConfiguration portType, - SynthLogic sig, - ) => + SynthLogic sig, { + bool forceObjectType = false, + }) => [ direction, - if (portType.objectType == SystemVerilogPortType.explicit) objectType, + if (forceObjectType || + portType.objectType == SystemVerilogPortType.explicit) + objectType, if (portType.dataType == SystemVerilogPortType.explicit) 'logic', sig.definitionName(useEnumType: false), ].join(' '); + /// Whether [sig] is a nested array aggregate that must be a net. + /// + /// Nested array payloads are lowered into multiple continuous assignments to + /// disjoint packed ranges. Declaring the aggregate as a variable causes some + /// simulators to retain only one of those drivers. Submodule outputs also + /// require a net actual when the aggregate crosses a port boundary. + bool _requiresNetDeclaration(SynthLogic sig) { + final logic = sig.logics.firstOrNull; + if (logic is! BaseLogicArray || !logic.arrayElements.any(_containsArray)) { + return false; + } + + final resolved = sig.resolved; + return module.tryOutput(sig.name) != null || + _synthModuleDefinition.assignments.any( + (assignment) => identical(_referenceRoot(assignment.dst), resolved), + ) || + _synthModuleDefinition.subModuleInstantiations.any( + (instantiation) => instantiation.outputMapping.values + .any((mapped) => identical(mapped.resolved, resolved)), + ); + } + + SynthLogic _referenceRoot(SynthLogic signal) { + final resolved = signal.resolved; + return switch (resolved) { + SynthLogicArrayElement() => _referenceRoot(resolved.parentArray), + SynthLogicArrayStructureElement() => _referenceRoot(resolved.rootArray), + SynthLogicPackedBitReference() => _referenceRoot(resolved.packedBase), + SynthLogicPackedRangeReference() => _referenceRoot(resolved.packedBase), + _ => resolved, + }; + } + + static bool _containsArray(Logic logic) => + logic is BaseLogicArray || + (logic is LogicStructure && logic.elements.any(_containsArray)); + /// Representation of all internal net declarations in generated SV. String _verilogInternalSignals(_SvTraceHelper traceHelper) { final declarations = []; @@ -215,8 +263,11 @@ class SystemVerilogSynthesisResult extends SynthesisResult { final comment = _assignedSignalNames.contains(sig.name) ? '' : traceHelper.signalComment(sig); + final type = _requiresNetDeclaration(sig) + ? 'wire' + : sig.definitionType(useEnumType: configuration.generateEnums); final decl = - '${sig.definitionType(useEnumType: configuration.generateEnums)} ' + '$type ' '${sig.definitionName(useEnumType: configuration.generateEnums)};'; declarations.add(_SvTraceHelper._pad(decl, comment)); } @@ -230,11 +281,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 +288,34 @@ 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.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); @@ -293,8 +334,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/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 8e42a47379..dd791bc5d3 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -290,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 { @@ -714,6 +726,9 @@ class SynthLogicPackedBitReference extends SynthLogic { ), super(Logic()); + @override + bool get isNet => packedBase.resolved.isNet; + @override bool get needsDeclaration => false; @@ -738,9 +753,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; @@ -752,6 +767,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]. @@ -776,7 +800,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.', @@ -792,6 +815,9 @@ class SynthLogicPackedRangeReference extends SynthLogic { ), super(Logic(width: upperIndex - lowerIndex + 1)); + @override + bool get isNet => packedBase.resolved.isNet; + @override bool get needsDeclaration => false; @@ -816,9 +842,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; @@ -830,6 +856,17 @@ 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 [BaseLogicArray]. @@ -892,17 +929,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 { @@ -944,6 +983,18 @@ 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 root array containing [logic]. SynthLogic get rootArray { var current = logic; @@ -960,32 +1011,6 @@ class SynthLogicArrayStructureElement extends SynthLogic { return parentSynthModuleDefinition.getSynthLogic(current)!; } - /// The array elements from the configured array leaf to the root array. - Iterable get _arrayMembers sync* { - var current = logic; - var parent = current.parentStructure; - while (parent != null) { - if (parent is BaseLogicArray) { - yield current; - } - current = parent; - parent = current.parentStructure; - } - } - - /// The packed offset of [logic] within its configured array leaf. - int get _leafOffset { - final leaf = _arrayLeafAncestor(logic)! as LogicStructure; - var offset = 0; - for (final leafElement in leaf.leafElements) { - if (identical(leafElement, logic)) { - return offset; - } - offset += leafElement.width; - } - throw StateError('Array leaf does not contain ${logic.name}.'); - } - @override bool get needsDeclaration => false; @@ -1004,18 +1029,16 @@ class SynthLogicArrayStructureElement extends SynthLogic { super.hasDstConnectionsPresent() || rootArray.hasDstConnectionsPresent(); @override - String get name { - final arrayName = rootArray.replacement?.name ?? rootArray.name; - final indices = _arrayMembers - .toList() - .reversed - .map((member) => '[${member.arrayIndex}]') - .join(); - final lower = _leafOffset; - final upper = lower + width - 1; - return '$arrayName$indices' - '${upper == lower ? '[$lower]' : '[$upper:$lower]'}'; - } + 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( @@ -1041,3 +1064,177 @@ class SynthLogicArrayStructureElement extends SynthLogic { 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) { + final nestedSynth = parentSynthModuleDefinition.getSynthLogic(nestedArray)!; + final nestedIndices = []; + current = target; + while (!identical(current, nestedArray)) { + if (current.isArrayMember) { + nestedIndices.add(current.arrayIndex!); + } + current = current.parentStructure!; + } + final orderedNestedIndices = nestedIndices.reversed.toList(growable: false); + final nestedReference = '${nestedSynth.name}' + '${orderedNestedIndices.map((index) => '[$index]').join()}'; + final nestedElement = _arrayElementAt( + nestedArray, + orderedNestedIndices, + nestedArray.dimensions.length, + ); + if (identical(target, nestedElement) && lowerIndex == null) { + return nestedReference; + } + + final offset = _packedOffset(nestedElement, target); + final lower = offset + (lowerIndex ?? 0); + final upper = offset + (upperIndex ?? target.width - 1); + return '$nestedReference${_packedRangeSuffix(lower, upper)}'; + } + + final indices = []; + current = target; + while (current.parentStructure != null) { + if (current.isArrayMember) { + indices.add(current.arrayIndex!); + } + + current = current.parentStructure!; + } + final orderedIndices = indices.reversed.toList(growable: false); + final rootRank = rootArray.dimensions.length; + if (orderedIndices.length < rootRank) { + if (target is! BaseLogicArray || lowerIndex != null) { + throw StateError('Array descendant is missing root array indices.'); + } + final rootSynth = parentSynthModuleDefinition.getSynthLogic(rootArray)!; + final rootName = rootSynth.replacement?.name ?? rootSynth.name; + return '$rootName' + '${orderedIndices.map((index) => '[$index]').join()}'; + } + + final rootSynth = parentSynthModuleDefinition.getSynthLogic(rootArray)!; + final rootName = rootSynth.replacement?.name ?? rootSynth.name; + final rootIndices = + orderedIndices.take(rootRank).map((index) => '[$index]').join(); + + final rootElement = _arrayElementAt(rootArray, orderedIndices, rootRank); + if (identical(target, rootElement) && lowerIndex == null) { + return '$rootName$rootIndices'; + } + + final offset = _packedOffset(rootElement, target); + final lower = offset + (lowerIndex ?? 0); + final upper = offset + (upperIndex ?? target.width - 1); + final range = _packedRangeSuffix(lower, upper); + final rendered = '$rootName$rootIndices$range'; + assert( + Sanitizer.isSanitary(rootName), + '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 2840b62469..97ec8b7738 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -29,6 +29,9 @@ class _BusSubsetForStructSlice extends BusSubset { /// signal and therefore does not drift run-to-run. final Logic _destination; + /// Whether the mapped input already represents the requested range. + final bool _inputIsSelected; + /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. _BusSubsetForStructSlice( @@ -36,7 +39,9 @@ class _BusSubsetForStructSlice extends BusSubset { super.startIndex, super.endIndex, { required Logic destination, + bool inputIsSelected = false, }) : _destination = destination, + _inputIsSelected = inputIsSelected, super(name: 'struct_slice'); // we override this since it's added post-build @@ -45,6 +50,10 @@ class _BusSubsetForStructSlice extends BusSubset { @override Object get instanceNameKey => _destination; + + @override + String inlineVerilog(Map inputs) => + _inputIsSelected ? inputs.values.single : super.inlineVerilog(inputs); } /// A packed range of a base [SynthLogic], inclusive of [lower] and [upper]. @@ -419,7 +428,7 @@ class SynthModuleDefinition { signal is SynthLogicArrayStructureElement) { signal = switch (signal) { SynthLogicArrayElement() => signal.parentArray.resolved, - SynthLogicArrayStructureElement() => signal.rootArray.resolved, + SynthLogicArrayStructureElement() => signal.parentArray.resolved, _ => throw StateError('Unexpected synthesized array signal'), }; signalAndAncestors.add(signal); @@ -520,6 +529,8 @@ class SynthModuleDefinition { if (output is LogicStructure && output is! BaseLogicArray) { _partialAssignStructPort(output); + } else if (output is BaseLogicArray) { + _connectNestedArrayFields(output); } } @@ -530,6 +541,8 @@ class SynthModuleDefinition { if (input is LogicStructure && input is! BaseLogicArray) { _subsetReceiveStructPort(input); + } else if (input is BaseLogicArray) { + _connectNestedArrayFields(input, receive: true); } } @@ -541,6 +554,8 @@ class SynthModuleDefinition { // 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); } } @@ -564,16 +579,24 @@ class SynthModuleDefinition { .whereType() .where((e) => e is! BaseLogicArray) .forEach(_partialAssignStructPort); - + subModule.inputs.values + .whereType() + .forEach(_connectNestedArrayFields); subModule.outputs.values .whereType() .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! BaseLogicArray) .forEach(_subsetReceiveStructPort); + subModule.inOuts.values + .whereType() + .forEach((array) => _connectNestedArrayFields(array, receive: true)); } // search for other modules contained within this module @@ -1525,6 +1548,97 @@ 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) { + final packedReference = leafElement.width == 1 + ? SynthLogicPackedBitReference( + packedParent, + index, + parentSynthModuleDefinition: this, + ) + : SynthLogicPackedRangeReference( + packedParent, + index, + index + leafElement.width - 1, + parentSynthModuleDefinition: this, + ); + if (port.isNet) { + assignments.add(SynthAssignment(leafSynth, packedReference)); + } else { + final subsetMod = _BusSubsetForStructSlice( + Logic( + width: leafElement.width, + name: 'DUMMY', + ), + 0, + leafElement.width - 1, + destination: leafElement, + inputIsSelected: true, + ); + getSynthSubModuleInstantiation(subsetMod) + ..setOutputMapping(subsetMod.subset.name, leafSynth) + ..setInputMapping( + subsetMod.original.name, + packedReference, + ); + } + } 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() { diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7597e1e14..9fdec1130d 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -708,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 BaseLogicArray && signal.isNet)) + ((isOutput || + signal is LogicNet || + (signal is BaseLogicArray && signal.isNet)) ? 'wire' : 'logic'); @@ -760,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 = @@ -768,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/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 4a8a32ab3f..707684d229 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2451,6 +2451,43 @@ void main() { ); }); + 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('optimized netlist removes concat aliases of named vectors', () async { final json = await _synthToMap( NestedInternalArrayToChildModule(), diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index 09ebfcebc3..7b5986eba7 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -11,6 +11,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; import 'package:test/test.dart'; import '../example/filter_bank.dart'; @@ -134,6 +135,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 +559,46 @@ 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 netlistHierarchy = + NetlistHierarchyAdapter.fromJson(netlistModule.generateNetlist()); + final netlistNames = names(netlistModule); + await Simulator.reset(); + + final svModule = _NestedNameModule(); + await svModule.build(); + final sv = svModule.generateSynth(); + 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'); + final loadedNames = netlistHierarchy.root + .depthFirstSignals() + .map((signal) => signal.name) + .toSet(); + for (final name in svNames.values) { + expect(sv, contains(name)); + expect(loadedNames, contains(name), + reason: 'SV and loaded netlist should preserve signal name $name.'); + } + }); }); } diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index c5683640d8..0b3a220986 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -14,6 +14,7 @@ import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; import 'package:test/test.dart'; class _SampleStructure extends LogicStructure { @@ -222,16 +223,68 @@ class _TypedArrayHierarchy extends Module { class _NestedArrayStructure extends LogicStructure { final Logic before; final LogicArray lanes; + final TypedLogicArray<_SampleStructure, LogicValue> samples; final Logic after; - factory _NestedArrayStructure({String? name}) => _NestedArrayStructure._( + factory _NestedArrayStructure({ + String? name, + int numUnpackedDimensions = 0, + }) => + _NestedArrayStructure._( Logic(name: 'before', width: 2), - LogicArray([2], 3, name: 'lanes'), + 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 _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, { @@ -239,8 +292,10 @@ class _NestedArrayStructure extends LogicStructure { }) : super([before, lanes, after], name: name); @override - _NestedArrayStructure clone({String? name}) => - _NestedArrayStructure(name: name ?? this.name); + _NestedNetArrayStructure clone({String? name}) => _NestedNetArrayStructure( + name: name ?? this.name, + numUnpackedDimensions: lanes.numUnpackedDimensions, + ); } class _NestedArrayBoundaryModule extends Module { @@ -249,30 +304,123 @@ class _NestedArrayBoundaryModule extends Module { LogicValue> source) { final input = addTypedInput('valuesIn', source); - final output = addTypedOutput('valuesOut', input.clone); + 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 = output.at([outer]).at([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 _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 _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]); + } +} + LogicValue _nestedPacked( int before, int lane0, int lane1, + int sample0, + int sample1, int after, ) => LogicValue.ofInt( - before | (lane0 << 2) | (lane1 << 5) | (after << 8), - 9, + before | + (lane0 << 2) | + (lane1 << 5) | + (sample0 << 8) | + (sample1 << 11) | + (after << 14), + 15, ); class _TypedInputModule extends Module { @@ -1232,8 +1380,9 @@ void main() { ); }); - test('simulates nested typed arrays with array-valued structure fields', - () async { + test( + 'simulates packed and mixed nested arrays with array-valued ' + 'structure fields', () async { await Simulator.reset(); final source = TypedLogicArray< TypedLogicArray<_NestedArrayStructure, LogicValue>, LogicValue>( @@ -1247,26 +1396,275 @@ void main() { 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 = [ - _nestedPacked(1, 2, 3, 0), - _nestedPacked(2, 4, 5, 1), - _nestedPacked(3, 6, 7, 0), - _nestedPacked(0, 1, 2, 1), - _nestedPacked(3, 5, 1, 0), - _nestedPacked(2, 7, 4, 1), + for (final fields in inputFields) + _nestedPacked( + fields.before, + fields.lane0, + fields.lane1, + fields.sample0, + fields.sample1, + fields.after, + ), ]; - final output = [242, 423, 40, 479, 150, 333]; + 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.map((value) => LogicValue.ofInt(value, 9))); + final outputValue = LogicValue.ofIterable(output); final vectors = [ Vector({'valuesIn': inputValue}, {'valuesOut': outputValue}), ]; await SimCompare.checkFunctionalVector(module, vectors); - // Icarus rejects the generated nested packed/unpacked indexing here; - // the ROHD functional result is still checked above. - expect(module.generateSynth(), contains('module')); + final sv = module.generateSynth(); + expect(sv, contains('valuesIn[1][31:30]')); + expect(sv, contains('valuesOut[0][31:30]')); + final netlistJson = NetlistSynthesizer().synthesizeToJson(module); + final netlist = jsonDecode(netlistJson) as Map; + final hierarchy = NetlistHierarchyAdapter.fromJson(netlistJson); + 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 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 childCell = parentCells.values + .cast>() + .singleWhere((cell) => cell['type'] == '_NestedArrayBoundaryChild'); + 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(hierarchy.root.definition, '_NestedArrayBoundaryModule'); + expect( + hierarchy.root.children + .singleWhere( + (occurrence) => + occurrence.definition == '_NestedArrayBoundaryChild', + ) + .name, + 'unnamed_module', + ); + final loadedNames = hierarchy.root + .depthFirstSignals() + .map((signal) => signal.name) + .toSet(); + for (final name in [ + 'valuesIn', + 'valuesOut', + 'valuesIn_0__0__lanes', + 'valuesIn_0__0__samples', + 'valuesOut_1__2__lanes', + ]) { + expect(sv, contains(name)); + expect(loadedNames, contains(name), + reason: 'SV and loaded 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.generateSynth(); + expect( + mixedSv, + contains('input logic [44:0] valuesIn [1:0]'), + ); + expect( + mixedSv, + contains('output wire 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];'), + ); + expect( + () => NetlistHierarchyAdapter.fromJson( + NetlistSynthesizer().synthesizeToJson(mixedModule), + ), + returnsNormally, + ); + SimCompare.checkIverilogVector(mixedModule, vectors); + SimCompare.checkVerilatorVector(mixedModule, vectors); + }); + + 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.generateSynth(); + expect( + sv, + contains('input logic [2:0][159:0] valuesIn [1:0]'), + ); + expect( + sv, + contains('output wire 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)); + } + SimCompare.checkIverilogVector(module, vectors); + 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); + }); + + test('drives and releases a nested structured typed inout array', () async { + await Simulator.reset(); + 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}, + ), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.generateSynth(); + expect(sv, contains('inout wire [1:0][8:0] bus')); + expect(source.numUnpackedDimensions, 0); + expect(source.at([0]).lanes.numUnpackedDimensions, 1); + SimCompare.checkIverilogVector(module, vectors); + final netlistJson = NetlistSynthesizer().synthesizeToJson(module); + final hierarchy = NetlistHierarchyAdapter.fromJson(netlistJson); + expect(hierarchy.root.definition, '_NestedStructuredInOutDriveModule'); + final loadedSignals = { + for (final signal in hierarchy.root.depthFirstSignals()) + signal.name: signal, + }; + expect(loadedSignals.keys, containsAll(['bus', 'observed'])); + expect(loadedSignals['bus']!.direction, 'inout'); + expect(loadedSignals['bus']!.width, source.width); + // Verilator does not support the bidirectional `tran` primitive needed + // to model four-state drive and release. }); }); From 90766813da5d21de28d0320209050555179eb558 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 10:40:37 -0700 Subject: [PATCH 18/43] remove rohd_hierarchy dependency (used during testing) --- test/synth_name_parity_test.dart | 23 ++++++----- test/typed_logic_array_test.dart | 67 +++++++++++++++----------------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index 7b5986eba7..f030492cd0 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -8,10 +8,11 @@ // 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'; -import 'package:rohd_hierarchy/rohd_hierarchy.dart'; import 'package:test/test.dart'; import '../example/filter_bank.dart'; @@ -575,8 +576,16 @@ void main() { final netlistModule = _NestedNameModule(); await netlistModule.build(); - final netlistHierarchy = - NetlistHierarchyAdapter.fromJson(netlistModule.generateNetlist()); + 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(); @@ -590,14 +599,10 @@ void main() { expect(svNames['output'], 'valuesOut'); expect(svNames['inputLanes'], 'valuesIn_0__lanes'); expect(svNames['outputLanes'], 'valuesOut_1__lanes'); - final loadedNames = netlistHierarchy.root - .depthFirstSignals() - .map((signal) => signal.name) - .toSet(); for (final name in svNames.values) { expect(sv, contains(name)); - expect(loadedNames, contains(name), - reason: 'SV and loaded netlist should preserve signal name $name.'); + expect(emittedNetlistNames, contains(name), + reason: 'SV and netlist should preserve signal name $name.'); } }); }); diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 0b3a220986..8527d89adc 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -14,7 +14,6 @@ import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; -import 'package:rohd_hierarchy/rohd_hierarchy.dart'; import 'package:test/test.dart'; class _SampleStructure extends LogicStructure { @@ -1443,7 +1442,6 @@ void main() { expect(sv, contains('valuesOut[0][31:30]')); final netlistJson = NetlistSynthesizer().synthesizeToJson(module); final netlist = jsonDecode(netlistJson) as Map; - final hierarchy = NetlistHierarchyAdapter.fromJson(netlistJson); final modules = netlist['modules'] as Map; final child = modules['_NestedArrayBoundaryChild'] as Map; @@ -1457,9 +1455,12 @@ void main() { final fields = (elementType['fields'] as List).cast>(); final parentCells = parent['cells'] as Map; - final childCell = parentCells.values - .cast>() - .singleWhere((cell) => cell['type'] == '_NestedArrayBoundaryChild'); + 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]); @@ -1478,20 +1479,14 @@ void main() { ); expect(childConnections['valuesIn'], hasLength(90)); expect(childConnections['valuesOut'], hasLength(90)); - expect(hierarchy.root.definition, '_NestedArrayBoundaryModule'); - expect( - hierarchy.root.children - .singleWhere( - (occurrence) => - occurrence.definition == '_NestedArrayBoundaryChild', - ) - .name, - 'unnamed_module', - ); - final loadedNames = hierarchy.root - .depthFirstSignals() - .map((signal) => signal.name) - .toSet(); + expect(childCellEntry.key, 'unnamed_module'); + 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', @@ -1500,8 +1495,8 @@ void main() { 'valuesOut_1__2__lanes', ]) { expect(sv, contains(name)); - expect(loadedNames, contains(name), - reason: 'SV and loaded netlist should preserve signal name $name.'); + expect(netlistNames, contains(name), + reason: 'SV and netlist should preserve signal name $name.'); } SimCompare.checkIverilogVector(module, vectors); SimCompare.checkVerilatorVector(module, vectors); @@ -1542,11 +1537,12 @@ void main() { mixedSv, contains('logic [2:0] valuesIn_0__0__samples [1:0];'), ); + final mixedNetlist = jsonDecode( + NetlistSynthesizer().synthesizeToJson(mixedModule), + ) as Map; expect( - () => NetlistHierarchyAdapter.fromJson( - NetlistSynthesizer().synthesizeToJson(mixedModule), - ), - returnsNormally, + mixedNetlist['modules'], + containsPair('_NestedArrayBoundaryModule', isA>()), ); SimCompare.checkIverilogVector(mixedModule, vectors); SimCompare.checkVerilatorVector(mixedModule, vectors); @@ -1653,16 +1649,17 @@ void main() { expect(source.numUnpackedDimensions, 0); expect(source.at([0]).lanes.numUnpackedDimensions, 1); SimCompare.checkIverilogVector(module, vectors); - final netlistJson = NetlistSynthesizer().synthesizeToJson(module); - final hierarchy = NetlistHierarchyAdapter.fromJson(netlistJson); - expect(hierarchy.root.definition, '_NestedStructuredInOutDriveModule'); - final loadedSignals = { - for (final signal in hierarchy.root.depthFirstSignals()) - signal.name: signal, - }; - expect(loadedSignals.keys, containsAll(['bus', 'observed'])); - expect(loadedSignals['bus']!.direction, 'inout'); - expect(loadedSignals['bus']!.width, source.width); + 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)); // Verilator does not support the bidirectional `tran` primitive needed // to model four-state drive and release. }); From 5e8d9d5778aedb6be3271d2422e9c1d03d62df96 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 10:46:32 -0700 Subject: [PATCH 19/43] updated documentation on the LogicArray class hierarchy --- doc/user_guide/_docs/A20-logic-arrays.md | 4 ++-- lib/src/signals/typed_logic_array.dart | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 433ffbed1b..885eed94f2 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -5,7 +5,7 @@ last_modified_at: 2026-09-10 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. `TypedLogicArray` uses the same structural model. An array is not a scalar `Logic` with an internal wire sliced into elements: it owns a hierarchy of child signals. `LogicStructure` supplies the common `Logic` behavior by packing those children when a scalar-like operation is needed, while the array layer adds dimensions, array-boundary traversal, and indexing. `TypedLogicArray` is indirectly a `Logic` through `LogicStructure`. It does not extend `LogicArray` because `LogicArray` is already the concrete `TypedLogicArray` specialization; making it the superclass would require redesigning the public hierarchy around a separate generic or abstract array base. +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. `TypedLogicArray` uses the same structural model. An array is not a scalar `Logic` with an internal wire sliced into elements: it owns a hierarchy of child signals. `LogicStructure` supplies the common `Logic` behavior by packing those children when a scalar-like operation is needed, while the array layer adds dimensions, array-boundary traversal, and indexing. `TypedLogicArray` is indirectly a `Logic` through `LogicStructure`. `LogicArray` directly extends `TypedLogicArray` as its ordinary-`Logic` specialization and inherits the typed-array API. `LogicArray`s can be constructed easily using the constructor: @@ -24,7 +24,7 @@ As long as the total width of a `LogicArray` and another type of `Logic` (includ ## Typed arrays -Use `TypedLogicArray` when every position at the declared array boundary has the same specialized hardware type and associated semantic value type. `LogicArray` remains the ordinary `TypedLogicArray` specialization, preserving its existing constructors, ports, cloning, naming, and packed assignment behavior. Both extend the non-generic `BaseLogicArray`, which contains array-specific metadata and traversal shared by the two public types; it is an internal implementation base rather than another public construction API. +Use `TypedLogicArray` when every position at the declared array boundary has the same specialized hardware type and associated semantic value type. `LogicArray` directly extends `TypedLogicArray` as the ordinary specialization, inheriting its array metadata, traversal, indexing, and assignment APIs while preserving its existing constructors and concrete clone and naming behavior. `TypedLogicArray` extends the non-generic `BaseLogicArray`, which is an internal implementation base rather than another public construction API. `dimensionNames`, when supplied to `TypedLogicArray`, is construction metadata used to derive names within typed-array hierarchies and preserve them during internal cloning. It is not a public axis-metadata property, and it does not change the established generated naming convention of ordinary `LogicArray`. diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index f88e748aba..ebdf9f4f59 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -37,11 +37,13 @@ LogicValueCodec _resolveTypedLogicValueCodec( /// 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] is the -/// concrete [TypedLogicArray]<[Logic], [LogicValue]> specialization, so the -/// two public array types are siblings rather than one extending the other. -/// The array-specific metadata and traversal live in [BaseLogicArray], while -/// [T] preserves the type at the declared array boundary. +/// [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. /// /// [elements] contains the immediate children of the outermost dimension. /// [arrayElements] traverses the number of array levels declared by From 19c7d9c14754c2fabbfc3f8a0eb24fe295b3f5b5 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 11:03:27 -0700 Subject: [PATCH 20/43] net bases issue, and partial array ranges fix --- doc/user_guide/_docs/A20-logic-arrays.md | 2 +- lib/src/signals/typed_logic_array.dart | 12 ++++++--- .../synthesizers/utilities/synth_logic.dart | 18 +++++++++++-- .../utilities/synth_module_definition.dart | 2 ++ test/typed_logic_array_test.dart | 26 ++++++++++++++++--- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 885eed94f2..79f556a0ce 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -120,7 +120,7 @@ field across a child boundary, is covered in ROHD and Icarus. Verilator does not currently simulate that last case because it does not support the bidirectional `tran` primitive used for net aliasing. -`TypedLogicArray` is also the supported base for custom typed arrays. Subclasses should use the normal constructor and override `createClone` to preserve their runtime type and metadata. The lower-level prebuilt-element constructor is library-private and is reserved for trusted in-library construction. +`TypedLogicArray` is also the supported base for custom typed arrays. Subclasses should use the normal constructor and override `createClone` to preserve their runtime type and metadata. Constructor-only configuration needed during reconstruction, including custom `dimensionNames`, should be retained in private subclass fields and passed back through the subclass constructor. The lower-level prebuilt-element constructor is library-private and is reserved for trusted in-library construction. ## Value-domain arrays diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index ebdf9f4f59..d46589aca0 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -210,9 +210,15 @@ class TypedLogicArray extends BaseLogicArray { /// Creates a clone while allowing subclasses to preserve their runtime type. /// - /// Subclasses with additional metadata should override this method and - /// reconstruct that metadata together with [dimensions], [_dimensionNames], - /// [elementWidth], [isNet], and [numUnpackedDimensions]. + /// 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, diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index dd791bc5d3..53581c8047 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -710,11 +710,16 @@ class SynthLogicPackedBitReference extends SynthLogic { this.packedBase, this.bitIndex, { required super.parentSynthModuleDefinition, + @internal bool allowNet = false, }) : assert( !packedBase.isArray, 'Packed reference base must not be an array.', ), assert(!packedBase.isNet, 'Packed reference base must not be a net.'), + assert( + allowNet || !packedBase.isNet, + 'Packed reference base must not be a net.', + ), assert( !packedBase.isConstant, 'Packed reference base must not be a constant.', @@ -798,8 +803,13 @@ class SynthLogicPackedRangeReference extends SynthLogic { this.lowerIndex, this.upperIndex, { required super.parentSynthModuleDefinition, + @internal bool allowNet = false, }) : assert( !packedBase.isArray, 'Packed reference base must not be an array.'), + assert( + allowNet || !packedBase.isNet, + 'Packed reference base must not be a net.', + ), assert( !packedBase.isConstant, 'Packed reference base must not be a constant.', @@ -1144,13 +1154,17 @@ String _synthArrayReferenceName( final orderedIndices = indices.reversed.toList(growable: false); final rootRank = rootArray.dimensions.length; if (orderedIndices.length < rootRank) { - if (target is! BaseLogicArray || lowerIndex != null) { + if (target is! BaseLogicArray) { throw StateError('Array descendant is missing root array indices.'); } final rootSynth = parentSynthModuleDefinition.getSynthLogic(rootArray)!; final rootName = rootSynth.replacement?.name ?? rootSynth.name; - return '$rootName' + final partialReference = '$rootName' '${orderedIndices.map((index) => '[$index]').join()}'; + return lowerIndex == null + ? partialReference + : '$partialReference' + '${_packedRangeSuffix(lowerIndex, upperIndex!)}'; } final rootSynth = parentSynthModuleDefinition.getSynthLogic(rootArray)!; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 97ec8b7738..27c0d29612 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -1593,12 +1593,14 @@ class SynthModuleDefinition { ? SynthLogicPackedBitReference( packedParent, index, + allowNet: port.isNet, parentSynthModuleDefinition: this, ) : SynthLogicPackedRangeReference( packedParent, index, index + leafElement.width - 1, + allowNet: port.isNet, parentSynthModuleDefinition: this, ); if (port.isNet) { diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 8527d89adc..cadb0969fb 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -116,6 +116,7 @@ class _EmptyStructure extends LogicStructure { class _SampleArray extends TypedLogicArray<_SampleStructure, _SampleValue> { final String schema; + final List? _constructionDimensionNames; // ignore: use_super_parameters - fixes the element builder for this subtype. _SampleArray( @@ -125,7 +126,10 @@ class _SampleArray extends TypedLogicArray<_SampleStructure, _SampleValue> { String? name, Naming? naming, int numUnpackedDimensions = 0, - }) : super( + }) : _constructionDimensionNames = dimensionNames == null + ? null + : List.unmodifiable(dimensionNames), + super( dimensions, _SampleStructure.new, valueCodec: _sampleValueCodec, @@ -144,6 +148,7 @@ class _SampleArray extends TypedLogicArray<_SampleStructure, _SampleValue> { _SampleArray( dimensions, schema: schema, + dimensionNames: _constructionDimensionNames, name: name ?? this.name, naming: naming, numUnpackedDimensions: @@ -998,6 +1003,21 @@ void main() { 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); @@ -1546,7 +1566,7 @@ void main() { ); SimCompare.checkIverilogVector(mixedModule, vectors); SimCompare.checkVerilatorVector(mixedModule, vectors); - }); + }, tags: ['verilator']); test('simulates mixed nested arrays across a child boundary', () async { await Simulator.reset(); @@ -1613,7 +1633,7 @@ void main() { expect(source.numUnpackedDimensions, 1); expect(source.at([1, 2]).numUnpackedDimensions, 1); expect(source.at([1, 2]).at([3, 4]).width, 8); - }); + }, tags: ['verilator']); test('drives and releases a nested structured typed inout array', () async { await Simulator.reset(); From af04f9f51901641e5b0fa38ce770e5a6d0e5632a Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 12:43:28 -0700 Subject: [PATCH 21/43] fix CI failure --- test/typed_logic_array_test.dart | 80 ++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index cadb0969fb..49501c03ea 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -267,6 +267,30 @@ class _NestedArrayStructure extends LogicStructure { ); } +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 _NestedNetArrayStructure extends LogicStructure { final LogicNet before; final LogicArray lanes; @@ -302,6 +326,16 @@ class _NestedNetArrayStructure extends LogicStructure { ); } +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, @@ -427,6 +461,13 @@ LogicValue _nestedPacked( 15, ); +LogicValue _nineBitNestedPacked( + int prefix, int payload0, int payload1, int suffix) => + LogicValue.ofInt( + prefix | (payload0 << 2) | (payload1 << 5) | (suffix << 8), + 9, + ); + class _TypedInputModule extends Module { late final T values; @@ -1399,6 +1440,45 @@ void main() { ); }); + 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.generateSynth(); + 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( 'simulates packed and mixed nested arrays with array-valued ' 'structure fields', () async { From 90abadaba393cf2720d4b7966953b5f36479c141 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 13:48:55 -0700 Subject: [PATCH 22/43] updated flutter version for material-ui but checked that dart will not reformat --- .github/workflows/coverage.yml | 2 ++ .github/workflows/general.yml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ae81ea8411..b3149a58cf 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,6 +24,8 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@v1 + with: + sdk: 3.6.2 - name: Install dependencies run: tool/gh_actions/install_dependencies.sh diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 10e20747c5..c2739bcf18 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -50,6 +50,8 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@v1 + with: + sdk: 3.6.2 - name: Setup Pana source analysis run: tool/gh_actions/install_pana.sh @@ -265,6 +267,8 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@v1 + with: + sdk: 3.6.2 - name: Install project dependencies run: tool/gh_actions/install_dependencies.sh From c2d7e8fdfccd8305c6461cd218c884eba8e82e64 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 15:38:36 -0700 Subject: [PATCH 23/43] added change descritptions to round out our PR Signed-off-by: Desmond A. Kirkpatrick --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e548fede86..1a869adfc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ ### Improvements - 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 (). From 7b597f7ae503053ec5ee79f0e2c88b83a7d362f0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 16:07:34 -0700 Subject: [PATCH 24/43] removed accidental checkin of conflict markers Signed-off-by: Desmond A. Kirkpatrick --- rohd_devtools_extension/pubspec.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 From 06b946530139b95ceae01cb8b70d7a36076f2224 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 15 Sep 2026 21:11:34 -0700 Subject: [PATCH 25/43] flutter 3.44 aligned --- .github/workflows/coverage.yml | 2 -- .github/workflows/general.yml | 4 ---- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b3149a58cf..ae81ea8411 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,8 +24,6 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@v1 - with: - sdk: 3.6.2 - name: Install dependencies run: tool/gh_actions/install_dependencies.sh diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index c2739bcf18..10e20747c5 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -50,8 +50,6 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@v1 - with: - sdk: 3.6.2 - name: Setup Pana source analysis run: tool/gh_actions/install_pana.sh @@ -267,8 +265,6 @@ jobs: - name: Setup Dart uses: dart-lang/setup-dart@v1 - with: - sdk: 3.6.2 - name: Install project dependencies run: tool/gh_actions/install_dependencies.sh From 12829d1105b0b7cf455bee002c5574e3750102b8 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 16 Sep 2026 17:47:13 -0700 Subject: [PATCH 26/43] better handling of inout, complex use of typed arrays in typed arrays, split testing for inout versus value versus array --- CHANGELOG.md | 2 +- lib/src/signals/base_logic_array.dart | 237 +++++ lib/src/signals/logic.dart | 15 +- lib/src/signals/logic_array.dart | 230 ----- lib/src/signals/signals.dart | 1 + lib/src/signals/typed_logic_array.dart | 27 +- .../netlist/netlist_validation.dart | 12 +- .../synthesizers/utilities/synth_logic.dart | 114 +-- .../utilities/synth_module_definition.dart | 2 - lib/src/values/logic_value_array.dart | 5 +- lib/src/values/typed_logic_value_array.dart | 11 +- test/logic_value_array_test.dart | 414 ++++++++ test/netlist_synthesizer_test.dart | 81 ++ test/subset_range_mapping_test.dart | 29 +- test/typed_logic_array_inout_test.dart | 365 +++++++ test/typed_logic_array_test.dart | 932 +++++------------- test/typed_logic_value_array_test.dart | 202 ++++ 17 files changed, 1649 insertions(+), 1030 deletions(-) create mode 100644 lib/src/signals/base_logic_array.dart create mode 100644 test/logic_value_array_test.dart create mode 100644 test/typed_logic_array_inout_test.dart create mode 100644 test/typed_logic_value_array_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a869adfc1..6c4f8dd44f 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 (). @@ -56,7 +57,6 @@ ## 0.6.10 -- Added `TypedLogicArray` for multidimensional arrays with fixed typed hardware elements and associated semantic values, including typed snapshots and ports, traversal, cloning, shape operations, 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 (). - Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (). - Improved simulation performance and generated outputs by avoiding module creation for four-state-safe operations involving `Const`s, zero shifts, and muxes with constant controls (). - Enforced that `Const` values cannot be changed through `put` or `inject`, including through another `Logic` driven by a `Const` (). 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 7815451f5c..9daf72d947 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -133,7 +133,12 @@ class Logic { LogicStructure? get parentStructure => _parentStructure; LogicStructure? _parentStructure; - /// True if this is a direct member of a [BaseLogicArray]. + /// 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 @@ -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 b13324fe3e..000556aad1 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -9,232 +9,6 @@ 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; - - /// Elements at the leaf dimension of this array. - /// - /// Unlike [LogicStructure.leafElements], traversal stops at the configured - /// array leaf. This distinction matters when an array leaf is itself a - /// [LogicStructure], such as a typed floating-point value. - late final List _arrayElements = - UnmodifiableListView(_calculateArrayElements()); - - /// Elements reached after traversing exactly [dimensions] array levels. - 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.'); - } - - // 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; - } - - // 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; - - 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); -} - /// A multi-dimensional array structure of independent [Logic]s. /// /// This is the ordinary [Logic]-leaf specialization of [TypedLogicArray]. It @@ -402,7 +176,3 @@ class LogicArray extends TypedLogicArray { naming: Naming.mergeable); } } - -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/signals.dart b/lib/src/signals/signals.dart index 3be7461b1b..03b760ed06 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -20,6 +20,7 @@ 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'; diff --git a/lib/src/signals/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index d46589aca0..8c050b6e9f 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -45,17 +45,6 @@ LogicValueCodec _resolveTypedLogicValueCodec( /// array metadata and traversal live, while [T] preserves the type at the /// declared array boundary. /// -/// [elements] contains the immediate children of the outermost dimension. -/// [arrayElements] traverses the number of array levels declared by -/// [dimensions] and then stops, returning one [T] for each declared array -/// position. It does not recurse into a [LogicStructure] or nested array stored -/// as a [T]. -/// -/// 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. -/// /// [value] and [previousValue] preserve [V] through [TypedLogicValueArray] /// snapshots. Standard [Logic.changed] events remain packed /// [LogicValueChanged] events. @@ -68,7 +57,7 @@ class TypedLogicArray extends BaseLogicArray { /// Construction prefixes retained for typed-array cloning. final List _dimensionNames; - /// Recreates one [T] for cloning and shape transformations. + /// Recreates [T] elements when cloning this array. final TypedLogicArrayElementBuilder _elementBuilder; /// Converts each element's packed bits to and from its semantic value type. @@ -79,10 +68,22 @@ class TypedLogicArray extends BaseLogicArray { // ignore: unsafe_variance final TypedLogicArrayElementCompatibility? _elementCompatibility; - /// Unmodifiable typed view of the configured array-element boundary. + /// 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; diff --git a/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index ae8b556a5d..828a674412 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -85,18 +85,18 @@ class NetlistValidation { continue; } final bits = (netname['bits'] as List?)?.whereType() ?? const []; - final aggregateDrivers = { - for (final bit in bits) ...driversByBit[bit] ?? const [], + final aggregateDrivers = <({String description, bool isTriState})>{ + for (final bit in bits) ...driversByBit[bit] ?? const [], }; if (aggregateDrivers.length <= 1 || - aggregateDrivers.every(_isTriStateDriver)) { + aggregateDrivers.every((driver) => driver.isTriState)) { continue; } issues.add(NetlistValidationIssue( 'aggregate net "${entry.key}" is reached from multiple drivers: ' - '${aggregateDrivers.join(', ')}', + '${aggregateDrivers.map((driver) => driver.description).join(', ')}', netname: entry.key, - drivers: aggregateDrivers.toList(), + drivers: aggregateDrivers.map((driver) => driver.description), )); } } @@ -106,8 +106,6 @@ class NetlistValidation { } } - static bool _isTriStateDriver(String driver) => driver.endsWith(r'($tribuf)'); - /// Collects the port and cell output drivers for each integer bit ID. static Map> _driversByBit( Map> ports, diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 53581c8047..8ee60276c6 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -710,16 +710,11 @@ class SynthLogicPackedBitReference extends SynthLogic { this.packedBase, this.bitIndex, { required super.parentSynthModuleDefinition, - @internal bool allowNet = false, }) : assert( !packedBase.isArray, 'Packed reference base must not be an array.', ), assert(!packedBase.isNet, 'Packed reference base must not be a net.'), - assert( - allowNet || !packedBase.isNet, - 'Packed reference base must not be a net.', - ), assert( !packedBase.isConstant, 'Packed reference base must not be a constant.', @@ -803,13 +798,8 @@ class SynthLogicPackedRangeReference extends SynthLogic { this.lowerIndex, this.upperIndex, { required super.parentSynthModuleDefinition, - @internal bool allowNet = false, }) : assert( !packedBase.isArray, 'Packed reference base must not be an array.'), - assert( - allowNet || !packedBase.isNet, - 'Packed reference base must not be a net.', - ), assert( !packedBase.isConstant, 'Packed reference base must not be a constant.', @@ -1005,20 +995,20 @@ class SynthLogicArrayStructureElement extends SynthLogic { return parentSynthModuleDefinition.getSynthLogic(parent)!; } - /// The synthesized root array containing [logic]. + /// The synthesized outermost array ancestor containing [logic]. SynthLogic get rootArray { - var current = logic; - var parent = current.parentStructure; + BaseLogicArray? rootArray; + var parent = logic.parentStructure; while (parent != null) { if (parent is BaseLogicArray) { - current = parent; - parent = current.parentStructure; - } else { - current = parent; - parent = current.parentStructure; + rootArray = parent; } + parent = parent.parentStructure; + } + if (rootArray == null) { + throw StateError('Structured array field has no array ancestor.'); } - return parentSynthModuleDefinition.getSynthLogic(current)!; + return parentSynthModuleDefinition.getSynthLogic(rootArray)!; } @override @@ -1115,75 +1105,69 @@ String _synthArrayReferenceName( final nestedArray = rootArray.isNet ? null : _nearestNestedArray(target, rootArray); if (nestedArray != null) { - final nestedSynth = parentSynthModuleDefinition.getSynthLogic(nestedArray)!; - final nestedIndices = []; - current = target; - while (!identical(current, nestedArray)) { - if (current.isArrayMember) { - nestedIndices.add(current.arrayIndex!); - } - current = current.parentStructure!; - } - final orderedNestedIndices = nestedIndices.reversed.toList(growable: false); - final nestedReference = '${nestedSynth.name}' - '${orderedNestedIndices.map((index) => '[$index]').join()}'; - final nestedElement = _arrayElementAt( + return _synthRepresentedArrayReference( + target, nestedArray, - orderedNestedIndices, - nestedArray.dimensions.length, + parentSynthModuleDefinition, + lowerIndex: lowerIndex, + upperIndex: upperIndex, ); - if (identical(target, nestedElement) && lowerIndex == null) { - return nestedReference; - } - - final offset = _packedOffset(nestedElement, target); - final lower = offset + (lowerIndex ?? 0); - final upper = offset + (upperIndex ?? target.width - 1); - return '$nestedReference${_packedRangeSuffix(lower, upper)}'; } + 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 = []; - current = target; - while (current.parentStructure != null) { + 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 rootRank = rootArray.dimensions.length; - if (orderedIndices.length < rootRank) { + 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 root array indices.'); + throw StateError('Array descendant is missing declared array indices.'); } - final rootSynth = parentSynthModuleDefinition.getSynthLogic(rootArray)!; - final rootName = rootSynth.replacement?.name ?? rootSynth.name; - final partialReference = '$rootName' - '${orderedIndices.map((index) => '[$index]').join()}'; + final partialReference = + '$arrayName${orderedIndices.map((index) => '[$index]').join()}'; return lowerIndex == null ? partialReference - : '$partialReference' - '${_packedRangeSuffix(lowerIndex, upperIndex!)}'; + : '$partialReference${_packedRangeSuffix(lowerIndex, upperIndex!)}'; } - final rootSynth = parentSynthModuleDefinition.getSynthLogic(rootArray)!; - final rootName = rootSynth.replacement?.name ?? rootSynth.name; - final rootIndices = - orderedIndices.take(rootRank).map((index) => '[$index]').join(); - - final rootElement = _arrayElementAt(rootArray, orderedIndices, rootRank); - if (identical(target, rootElement) && lowerIndex == null) { - return '$rootName$rootIndices'; + 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(rootElement, target); + final offset = _packedOffset(arrayElement, target); final lower = offset + (lowerIndex ?? 0); final upper = offset + (upperIndex ?? target.width - 1); final range = _packedRangeSuffix(lower, upper); - final rendered = '$rootName$rootIndices$range'; + final rendered = '$arrayName$declarationIndices$range'; assert( - Sanitizer.isSanitary(rootName), + Sanitizer.isSanitary(arrayName), 'Array name should be sanitary, but found $rendered', ); return rendered; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 27c0d29612..97ec8b7738 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -1593,14 +1593,12 @@ class SynthModuleDefinition { ? SynthLogicPackedBitReference( packedParent, index, - allowNet: port.isNet, parentSynthModuleDefinition: this, ) : SynthLogicPackedRangeReference( packedParent, index, index + leafElement.width - 1, - allowNet: port.isNet, parentSynthModuleDefinition: this, ); if (port.isNet) { diff --git a/lib/src/values/logic_value_array.dart b/lib/src/values/logic_value_array.dart index 4cf29ff4fa..866dfe39b6 100644 --- a/lib/src/values/logic_value_array.dart +++ b/lib/src/values/logic_value_array.dart @@ -60,10 +60,9 @@ class LogicValueArray extends TypedLogicValueArray { List dimensions, LogicValue Function(List indices) generator, {int? elementWidth}) { final normalizedDimensions = _validateValueArrayDimensions(dimensions); + final elementCount = _valueArrayLength(normalizedDimensions); final values = [ - for (var index = 0; - index < _valueArrayLength(normalizedDimensions); - index++) + for (var index = 0; index < elementCount; index++) generator(_valueArrayIndices(normalizedDimensions, index)), ]; return LogicValueArray.fromFlat( diff --git a/lib/src/values/typed_logic_value_array.dart b/lib/src/values/typed_logic_value_array.dart index c34359072d..ba6abfd50c 100644 --- a/lib/src/values/typed_logic_value_array.dart +++ b/lib/src/values/typed_logic_value_array.dart @@ -276,7 +276,9 @@ class TypedLogicValueArray extends LogicValue { /// Transposes this two-dimensional value array. TypedLogicValueArray transpose2D() { - _checkValueArrayIsTwoDimensional(dimensions); + if (dimensions.length != 2) { + throw StateError('Expected exactly two dimensions, got $dimensions.'); + } final newDimensions = [dimensions[1], dimensions[0]]; final semanticValues = []; final packedElements = []; @@ -530,10 +532,3 @@ int _valueArrayFlatIndex(List dimensions, List indices) { bool _sameValueArrayDimensions(List left, List right) => left.length == right.length && left.indexed.every((entry) => entry.$2 == right[entry.$1]); - -/// Requires exactly two dimensions for transpose-only operations. -void _checkValueArrayIsTwoDimensional(List dimensions) { - if (dimensions.length != 2) { - throw StateError('Expected exactly two dimensions, got $dimensions.'); - } -} diff --git a/test/logic_value_array_test.dart b/test/logic_value_array_test.dart new file mode 100644 index 0000000000..5af6668b47 --- /dev/null +++ b/test/logic_value_array_test.dart @@ -0,0 +1,414 @@ +// 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); + // The deprecated LogicValue length retains its packed-bit meaning. + // ignore: deprecated_member_use_from_same_package + expect(nested.length, 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/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 707684d229..a19637ed1f 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2488,6 +2488,87 @@ void main() { ); }); + test('struct net aggregates reject 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, + ), + throwsA(isA()), + ); + }); + + test('struct net aggregates reject normal 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()), + ); + }); + test('optimized netlist removes concat aliases of named vectors', () async { final json = await _synthToMap( NestedInternalArrayToChildModule(), 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/typed_logic_array_inout_test.dart b/test/typed_logic_array_inout_test.dart new file mode 100644 index 0000000000..7cd6607e54 --- /dev/null +++ b/test/typed_logic_array_inout_test.dart @@ -0,0 +1,365 @@ +// 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 that support the required four-state `tran` behavior. +/// +/// Verilator is intentionally excluded because it does not support `tran`. +Future _checkRohdAndIverilogVectors( + Module module, + List vectors, +) async { + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); +} + +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.generateSynth(); + expect(sv, contains('inout wire [1:0][8:0] bus')); + expect(sv, contains('tran (')); + 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.generateSynth(); + expect(sv, contains('inout wire [1:0][20:0] bus')); + expect(sv, contains('tran (')); + + 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 index 49501c03ea..a8259ea48f 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -1,7 +1,5 @@ // Copyright (C) 2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause -// ignore_for_file: prefer_const_constructors -// ignore_for_file: prefer_const_literals_to_create_immutables // // typed_logic_array_test.dart // Tests for typed logic and logic value arrays. @@ -9,6 +7,9 @@ // 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'; @@ -291,38 +292,69 @@ class _NineBitNestedStructure extends LogicStructure { _NineBitNestedStructure(name: name ?? this.name); } -class _NestedNetArrayStructure extends LogicStructure { - final LogicNet before; - final LogicArray lanes; - final LogicNet after; +class _MatrixCell extends LogicStructure { + final Logic low; + final Logic high; - factory _NestedNetArrayStructure({ + 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, - int numUnpackedDimensions = 0, }) => - _NestedNetArrayStructure._( - LogicNet(name: 'before', width: 2), - LogicArray.net( - [2], - 3, - name: 'lanes', - numUnpackedDimensions: numUnpackedDimensions, + _ArrayValuedElementStructure._( + Logic(name: 'prefix', width: 2), + TypedLogicArray, LogicValue>( + matrixDimensions, + ({name}) => TypedLogicArray<_MatrixCell, LogicValue>( + [3], + _MatrixCell.new, + name: name, + ), + name: 'matrix', + numUnpackedDimensions: matrixUnpackedDimensions, ), - LogicNet(name: 'after'), - name: name ?? 'nestedNet', + Logic(name: 'suffix'), + matrixDimensions: List.unmodifiable(matrixDimensions), + matrixUnpackedDimensions: matrixUnpackedDimensions, + name: name ?? 'arrayValuedElement', ); - _NestedNetArrayStructure._( - this.before, - this.lanes, - this.after, { + _ArrayValuedElementStructure._( + this.prefix, + this.matrix, + this.suffix, { + required this.matrixDimensions, + required this.matrixUnpackedDimensions, required String name, - }) : super([before, lanes, after], name: name); + }) : super([prefix, matrix, suffix], name: name); @override - _NestedNetArrayStructure clone({String? name}) => _NestedNetArrayStructure( + _ArrayValuedElementStructure clone({String? name}) => + _ArrayValuedElementStructure( + matrixDimensions: matrixDimensions, + matrixUnpackedDimensions: matrixUnpackedDimensions, name: name ?? this.name, - numUnpackedDimensions: lanes.numUnpackedDimensions, ); } @@ -375,6 +407,38 @@ class _NestedArrayBoundaryChild extends Module { } } +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) { @@ -404,45 +468,6 @@ class _MixedNestedArrayChild extends Module { } } -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]); - } -} - LogicValue _nestedPacked( int before, int lane0, @@ -468,6 +493,22 @@ LogicValue _nineBitNestedPacked( 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.ofInt( + 2 << (2 + matrixElementIndex * 9 + 7), + 2 + matrixWidth * 9 + 1, + ); +} + class _TypedInputModule extends Module { late final T values; @@ -476,20 +517,6 @@ class _TypedInputModule extends Module { } } -int _decodeLogicValue(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('TypedLogicArray', () { test('requires at least one dimension', () { @@ -1479,6 +1506,139 @@ void main() { 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 vectors = [ + Vector( + {'values': LogicValue.ofInt(2 << 39, source.width)}, + {'selected': 2}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.generateSynth(); + 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.generateSynth(); + 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.generateSynth(); + 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 vectors = [ + Vector( + {'values': LogicValue.ofInt(payload << 32, source.width)}, + {'selected': payload}, + ), + ]; + + await SimCompare.checkFunctionalVector(module, vectors); + final sv = module.generateSynth(); + 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 { @@ -1714,623 +1874,5 @@ void main() { expect(source.at([1, 2]).numUnpackedDimensions, 1); expect(source.at([1, 2]).at([3, 4]).width, 8); }, tags: ['verilator']); - - test('drives and releases a nested structured typed inout array', () async { - await Simulator.reset(); - 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}, - ), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); - expect(sv, contains('inout wire [1:0][8:0] bus')); - expect(source.numUnpackedDimensions, 0); - expect(source.at([0]).lanes.numUnpackedDimensions, 1); - SimCompare.checkIverilogVector(module, vectors); - 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)); - // Verilator does not support the bidirectional `tran` primitive needed - // to model four-state drive and release. - }); - }); - - 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([2, 2], leaves); - final nestedInts = LogicValueArray.fromInts([ - [1, 2], - [3, 4], - ], elementWidth: 8); - final flatInts = LogicValueArray.fromFlatInts( - [2, 2], - [1, 2, 3, 4], - elementWidth: 8, - ); - - expect(nested.dimensions, [2, 2]); - expect(nested.elementWidth, 8); - expect(nested.arrayValues.length, 4); - expect(nested.width, 32); - // The deprecated LogicValue length retains its packed-bit meaning. - // ignore: deprecated_member_use_from_same_package - expect(nested.length, 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([ - [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( - [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( - [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([]), - throwsArgumentError, - ); - expect( - () => TypedLogicValueArray.stack(>[]), - throwsArgumentError, - ); - expect( - () => LogicValueArray.stack( - LogicValueArray.fromFlat([0, 2], const [], elementWidth: 8) - .majorSlices, - ), - throwsArgumentError, - ); - }); - - test('round-trips slices with an empty inner dimension', () { - final values = - LogicValueArray.fromFlat([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([2, 2], [one]), - throwsArgumentError, - ); - expect( - LogicValueArray.fromFlat( - [2], - [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], - ).elementWidth, - 4, - ); - expect( - LogicValueArray.fromFlat( - [2], - [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 4)], - elementWidth: 4, - ).elementWidth, - 4, - ); - expect( - () => LogicValueArray.fromFlat( - [2], - [LogicValue.ofInt(1, 4), LogicValue.ofInt(2, 3)], - ), - throwsArgumentError, - ); - expect( - () => LogicValueArray.fromFlat( - [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([2, 0], const [], elementWidth: 8) - .elementWidth, - 8, - ); - expect( - () => LogicValueArray.fromFlat([2, 0], const []), - throwsArgumentError, - ); - }); - - test('rejects invalid value indices, shapes, widths, and stacks', () { - final values = LogicValueArray.fromFlatInts( - [2, 2], - [1, 2, 3, 4], - elementWidth: 8, - ); - final oneDimensional = LogicValueArray.fromFlatInts( - [4], - [1, 2, 3, 4], - elementWidth: 8, - ); - final threeDimensional = LogicValueArray.fromFlatInts( - [1, 2, 2], - [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([-1], const [], elementWidth: 8), - throwsArgumentError, - ); - expect( - () => LogicValueArray.fromFlat([1], const [], elementWidth: -1), - throwsArgumentError, - ); - expect( - () => LogicValueArray.generate( - [1], - (indices) => LogicValue.ofInt(indices.single, 4), - elementWidth: 8, - ), - throwsArgumentError, - ); - expect( - () => LogicValueArray.stack([ - LogicValueArray.fromFlatInts([2], [1, 2], elementWidth: 8), - LogicValueArray.fromFlatInts([1, 2], [3, 4], elementWidth: 8), - ]), - throwsArgumentError, - ); - expect( - () => LogicValueArray.stack([ - LogicValueArray.fromFlatInts([2], [1, 2], elementWidth: 8), - LogicValueArray.fromFlatInts([2], [3, 4], elementWidth: 4), - ]), - throwsArgumentError, - ); - }); - }); - - 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( - [ - 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( - [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, transposes, and converts typed values', () { - const codec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final packed = LogicValueArray.fromInts([ - [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]); - final signals = values.toLogicArray(); - expect(signals.dimensions, [2, 3]); - expect(signals.value, values); - }); - - test('supports slices, indexed mapping, and compatible stacking', () { - const codec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final values = TypedLogicValueArray([ - [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([ - [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', () { - final firstCodec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final secondCodec = LogicValueCodec( - decode: _decodeLogicValue, - encode: _encodeLogicValue, - ); - final first = TypedLogicValueArray.fromFlat( - [1], - 8, - [1], - codec: firstCodec, - ); - final second = TypedLogicValueArray.fromFlat( - [1], - 8, - [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( - [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/typed_logic_value_array_test.dart b/test/typed_logic_value_array_test.dart new file mode 100644 index 0000000000..02ecc2d688 --- /dev/null +++ b/test/typed_logic_value_array_test.dart @@ -0,0 +1,202 @@ +// 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, transposes, and converts 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]); + final signals = values.toLogicArray(); + expect(signals.dimensions, [2, 3]); + expect(signals.value, values); + }); + + 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], + ); + }); + }); +} From 515b696373af3f4da7a2667c997b73bff14b8eac Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 16 Sep 2026 22:13:20 -0700 Subject: [PATCH 27/43] several simulator-related issues with workarounds --- CHANGELOG.md | 1 + doc/user_guide/_docs/A20-logic-arrays.md | 70 ++++++----- .../netlist/netlist_synthesizer.dart | 11 +- ...systemverilog_synth_module_definition.dart | 8 +- .../systemverilog_synthesis_result.dart | 52 +++++--- ...stemverilog_synthesizer_configuration.dart | 15 +++ .../utilities/synth_module_definition.dart | 55 +++----- test/systemverilog_port_types_test.dart | 118 ++++++++++++++++++ test/typed_logic_array_inout_test.dart | 18 ++- test/typed_logic_array_test.dart | 78 +++++++++++- 10 files changed, 326 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4f8dd44f..23af18f47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - 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 (). +- Preserved configured SystemVerilog output object types for nested arrays and added the opt-in `iverilogWorkaroundForUnpackedArrayVariables` setting for child-driven unpacked array variables that remain unknown in Icarus Verilog 12.0 (). - 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 (). diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 79f556a0ce..09c1b31b48 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: 2026-09-10 +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. `TypedLogicArray` uses the same structural model. An array is not a scalar `Logic` with an internal wire sliced into elements: it owns a hierarchy of child signals. `LogicStructure` supplies the common `Logic` behavior by packing those children when a scalar-like operation is needed, while the array layer adds dimensions, array-boundary traversal, and indexing. `TypedLogicArray` is indirectly a `Logic` through `LogicStructure`. `LogicArray` directly extends `TypedLogicArray` as its ordinary-`Logic` specialization and inherits the typed-array API. +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: @@ -24,9 +29,9 @@ As long as the total width of a `LogicArray` and another type of `Logic` (includ ## Typed arrays -Use `TypedLogicArray` when every position at the declared array boundary has the same specialized hardware type and associated semantic value type. `LogicArray` directly extends `TypedLogicArray` as the ordinary specialization, inheriting its array metadata, traversal, indexing, and assignment APIs while preserving its existing constructors and concrete clone and naming behavior. `TypedLogicArray` extends the non-generic `BaseLogicArray`, which is an internal implementation base rather than another public construction API. - -`dimensionNames`, when supplied to `TypedLogicArray`, is construction metadata used to derive names within typed-array hierarchies and preserve them during internal cloning. It is not a public axis-metadata property, and it does not change the established generated naming convention of ordinary `LogicArray`. +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: @@ -72,11 +77,18 @@ final bottomRightData = samples.at([1, 2]).data; final TypedLogicValueArray currentSamples = samples.value; ``` -The element builder must always produce the configured type, width, and recursively ordered net composition. Every element must be uniformly variable or uniformly net: a structure cannot mix `Logic` and `LogicNet` leaves. The optional `elementCompatibility` callback can additionally reject elements whose semantic representations differ from the prototype, such as floating-point elements with different exponent and mantissa layouts despite having the same total width. - -A zero-sized array calls the builder once as a prototype so that the same metadata and validation remain available even though the array has no positions. Generic zero-sized arrays retain the prototype's element width; `LogicArray` retains its historical behavior of reporting an element width of zero for an empty shape. +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` may be omitted when `TValue` is exactly `LogicValue`; the canonical identity codec is selected automatically. Other semantic types require a codec. Since hardware may contain `X` and `Z`, a codec used by `TypedLogicArray` should decode every four-state value that can appear in that hardware. +`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. @@ -97,30 +109,22 @@ 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. -SystemVerilog declarations retain the packed/unpacked split of the root array. -When a configured element contains another array, that nested value occupies -packed bits within the root element at the port boundary. Nested coordinates -and structure fields are therefore lowered to one packed bit or range selection -instead of adding dimensions that are absent from the declaration. An -array-valued structure field may use a separate internal declaration with its -own packed/unpacked split; synthesis inserts the packing or unpacking needed at -the enclosing port. - -This lowering preserves the existing inline declaration and naming style; it -does not introduce SystemVerilog `typedef`s or generated structural type names. -Whole assignments involving nested unpacked arrays are emitted as element or -packed-range assignments for simulator portability. The netlist output retains -the recursive array and structure metadata in `logic_type`. - -ROHD, Icarus, and Verilator simulation cover fully packed nested arrays, -mixed packed/unpacked outer and inner arrays, and structures containing -unpacked ordinary and typed array fields across module boundaries. Four-state -structured inout drive and release, including an unpacked nested net-array -field across a child boundary, is covered in ROHD and Icarus. Verilator does -not currently simulate that last case because it does not support the -bidirectional `tran` primitive used for net aliasing. - -`TypedLogicArray` is also the supported base for custom typed arrays. Subclasses should use the normal constructor and override `createClone` to preserve their runtime type and metadata. Constructor-only configuration needed during reconstruction, including custom `dimensionNames`, should be retained in private subclass fields and passed back through the subclass constructor. The lower-level prebuilt-element constructor is library-private and is reserved for trusted in-library construction. +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. + +Icarus Verilog 12.0 can leave child-driven unpacked array variables unknown +during simulation. When targeting that tool, enable +`SystemVerilogSynthesizerConfiguration.iverilogWorkaroundForUnpackedArrayVariables`. + +Custom typed-array subclasses should override `createClone` to preserve their +runtime type and constructor configuration. See the API documentation for the +full subclassing contract. ## Value-domain arrays diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart index 7884d41f4d..ea6b15d2be 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -257,6 +257,9 @@ class NetlistSynthesizer extends Synthesizer { 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 = @@ -272,7 +275,7 @@ class NetlistSynthesizer extends Synthesizer { 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, @@ -288,10 +291,10 @@ class NetlistSynthesizer extends Synthesizer { } } 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]; } } } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 51788ef580..5e084c43b8 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -1684,12 +1684,8 @@ class _NetConnect extends Module with SystemVerilog { @override String? definitionVerilog(String definitionType) => ''' // A special module for connecting two nets bidirectionally -module $definitionType #(parameter int WIDTH=1) ($n0Name, $n1Name); -inout wire[WIDTH-1:0] $n0Name; -inout wire[WIDTH-1:0] $n1Name; -for (genvar i = 0; i < WIDTH; i++) begin - tran ($n0Name[i], $n1Name[i]); -end +module $definitionType #(parameter int WIDTH=1) (w, w); +inout wire[WIDTH-1:0] w; endmodule'''; } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index edd02d5032..b46d7d59d6 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -171,12 +171,22 @@ class SystemVerilogSynthesisResult extends SynthesisResult { module.tryOutput(sig.name) != null, 'Named output ${sig.name} not found in module ${module.name}.', ); + final useIverilogWorkaround = + configuration.iverilogWorkaroundForUnpackedArrayVariables && + _isUnpackedArray(sig); + final portType = useIverilogWorkaround + ? SystemVerilogPortTypeConfiguration( + dataType: configuration.outputPortType.dataType, + ) + : configuration.outputPortType; return _verilogPort( 'output', - _requiresNetDeclaration(sig) ? 'wire' : 'var', - configuration.outputPortType, + useIverilogWorkaround || _requiresNetDeclaration(sig) + ? 'wire' + : 'var', + portType, sig, - forceObjectType: _requiresNetDeclaration(sig), + forceObjectType: useIverilogWorkaround || _requiresNetDeclaration(sig), ); }); @@ -212,12 +222,12 @@ class SystemVerilogSynthesisResult extends SynthesisResult { sig.definitionName(useEnumType: false), ].join(' '); - /// Whether [sig] is a nested array aggregate that must be a net. - /// - /// Nested array payloads are lowered into multiple continuous assignments to - /// disjoint packed ranges. Declaring the aggregate as a variable causes some - /// simulators to retain only one of those drivers. Submodule outputs also - /// require a net actual when the aggregate crosses a port boundary. + /// Whether [sig] represents an array with an unpacked dimension. + bool _isUnpackedArray(SynthLogic sig) { + final array = sig.logics.whereType().firstOrNull; + return array != null && array.numUnpackedDimensions > 0; + } + bool _requiresNetDeclaration(SynthLogic sig) { final logic = sig.logics.firstOrNull; if (logic is! BaseLogicArray || !logic.arrayElements.any(_containsArray)) { @@ -235,6 +245,20 @@ class SystemVerilogSynthesisResult extends SynthesisResult { ); } + static bool _containsArray(Logic logic) => + logic is BaseLogicArray || + (logic is LogicStructure && logic.elements.any(_containsArray)); + + /// Whether [sig] is driven by a submodule output. + bool _isDrivenBySubmoduleOutput(SynthLogic sig) { + final target = sig.resolved; + return _synthModuleDefinition.subModuleInstantiations.any( + (instantiation) => instantiation.outputMapping.values.any( + (mapped) => identical(_referenceRoot(mapped), target), + ), + ); + } + SynthLogic _referenceRoot(SynthLogic signal) { final resolved = signal.resolved; return switch (resolved) { @@ -246,10 +270,6 @@ class SystemVerilogSynthesisResult extends SynthesisResult { }; } - static bool _containsArray(Logic logic) => - logic is BaseLogicArray || - (logic is LogicStructure && logic.elements.any(_containsArray)); - /// Representation of all internal net declarations in generated SV. String _verilogInternalSignals(_SvTraceHelper traceHelper) { final declarations = []; @@ -263,7 +283,11 @@ class SystemVerilogSynthesisResult extends SynthesisResult { final comment = _assignedSignalNames.contains(sig.name) ? '' : traceHelper.signalComment(sig); - final type = _requiresNetDeclaration(sig) + final useIverilogWorkaround = + configuration.iverilogWorkaroundForUnpackedArrayVariables && + _isUnpackedArray(sig) && + _isDrivenBySubmoduleOutput(sig); + final type = useIverilogWorkaround || _requiresNetDeclaration(sig) ? 'wire' : sig.definitionType(useEnumType: configuration.generateEnums); final decl = diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart index 717f9f30e0..16bf02d848 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. @@ -46,6 +50,16 @@ class SystemVerilogSynthesizerConfiguration { /// Type configuration for inout ports. final SystemVerilogPortTypeConfiguration inOutPortType; + /// Whether to work around Icarus Verilog's unpacked array variable issue. + /// + /// Icarus Verilog 12.0 accepts child-driven unpacked array variables, + /// including output ports and internal intermediates, but their values remain + /// unknown during simulation. Enabling this option emits an explicit `wire` + /// object type for unpacked array output ports and child-driven internal + /// unpacked arrays. It overrides `outputPortType.objectType` for those ports + /// and is disabled by default. + final bool iverilogWorkaroundForUnpackedArrayVariables; + /// Creates a new configuration for SystemVerilog synthesis. const SystemVerilogSynthesizerConfiguration({ this.generateEnums = true, @@ -58,5 +72,6 @@ class SystemVerilogSynthesizerConfiguration { this.inOutPortType = const SystemVerilogPortTypeConfiguration( dataType: SystemVerilogPortType.implicit, ), + this.iverilogWorkaroundForUnpackedArrayVariables = false, }); } diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 97ec8b7738..d53b696486 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -29,9 +29,6 @@ class _BusSubsetForStructSlice extends BusSubset { /// signal and therefore does not drift run-to-run. final Logic _destination; - /// Whether the mapped input already represents the requested range. - final bool _inputIsSelected; - /// Creates a [BusSubset] for use in [SynthModuleDefinition]s during /// [LogicStructure] port slicing. _BusSubsetForStructSlice( @@ -39,9 +36,7 @@ class _BusSubsetForStructSlice extends BusSubset { super.startIndex, super.endIndex, { required Logic destination, - bool inputIsSelected = false, }) : _destination = destination, - _inputIsSelected = inputIsSelected, super(name: 'struct_slice'); // we override this since it's added post-build @@ -50,10 +45,6 @@ class _BusSubsetForStructSlice extends BusSubset { @override Object get instanceNameKey => _destination; - - @override - String inlineVerilog(Map inputs) => - _inputIsSelected ? inputs.values.single : super.inlineVerilog(inputs); } /// A packed range of a base [SynthLogic], inclusive of [lower] and [upper]. @@ -1589,37 +1580,31 @@ class SynthModuleDefinition { _retainInternalSignal(leafSynth); } if (receive) { - final packedReference = leafElement.width == 1 - ? SynthLogicPackedBitReference( - packedParent, - index, - parentSynthModuleDefinition: this, - ) - : SynthLogicPackedRangeReference( - packedParent, - index, - index + leafElement.width - 1, - parentSynthModuleDefinition: this, - ); 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 { - final subsetMod = _BusSubsetForStructSlice( - Logic( - width: leafElement.width, - name: 'DUMMY', + assignments.add( + RangeSynthAssignment( + packedParent, + leafSynth, + srcUpperIndex: index + leafElement.width - 1, + srcLowerIndex: index, + dstUpperIndex: leafElement.width - 1, + dstLowerIndex: 0, ), - 0, - leafElement.width - 1, - destination: leafElement, - inputIsSelected: true, ); - getSynthSubModuleInstantiation(subsetMod) - ..setOutputMapping(subsetMod.subset.name, leafSynth) - ..setInputMapping( - subsetMod.original.name, - packedReference, - ); } } else { assignments.add( diff --git a/test/systemverilog_port_types_test.dart b/test/systemverilog_port_types_test.dart index 58765cd266..39cfcdde4c 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,87 @@ 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, + ), + ( + name: 'Icarus unpacked variable workaround', + configuration: const SystemVerilogSynthesizerConfiguration( + outputPortType: SystemVerilogPortTypeConfiguration(), + iverilogWorkaroundForUnpackedArrayVariables: true, + ), + declaration: 'output wire logic [8:0] valuesOut [1:0]', + expectedCount: 2, + checkIverilog: true, + ), + ]; + + for (final configuration in configurations) { + final sv = module.generateSynth( + 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 index 7cd6607e54..df90195db4 100644 --- a/test/typed_logic_array_inout_test.dart +++ b/test/typed_logic_array_inout_test.dart @@ -180,9 +180,10 @@ class _ArrayValuedStructuredInOutDriveChild extends Module { } } -/// Checks the backends that support the required four-state `tran` behavior. +/// Checks the backends used for four-state inout resolution. /// -/// Verilator is intentionally excluded because it does not support `tran`. +/// 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, @@ -191,6 +192,15 @@ Future _checkRohdAndIverilogVectors( 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) => [ @@ -264,7 +274,7 @@ void main() { await _checkRohdAndIverilogVectors(module, vectors); final sv = module.generateSynth(); expect(sv, contains('inout wire [1:0][8:0] bus')); - expect(sv, contains('tran (')); + _expectCompatibleNetConnectDefinition(sv); expect(source.numUnpackedDimensions, 0); expect(source.at([0]).lanes.numUnpackedDimensions, 1); @@ -332,7 +342,7 @@ void main() { await _checkRohdAndIverilogVectors(module, vectors); final sv = module.generateSynth(); expect(sv, contains('inout wire [1:0][20:0] bus')); - expect(sv, contains('tran (')); + _expectCompatibleNetConnectDefinition(sv); final netlist = jsonDecode( NetlistSynthesizer().synthesizeToJson(module), diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index a8259ea48f..89c335bdc8 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -17,6 +17,10 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; +const _iverilogUnpackedArrayWorkaround = SystemVerilogSynthesizerConfiguration( + iverilogWorkaroundForUnpackedArrayVariables: true, +); + class _SampleStructure extends LogicStructure { final Logic low; final Logic high; @@ -1700,6 +1704,13 @@ void main() { final sv = module.generateSynth(); 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; @@ -1710,6 +1721,11 @@ void main() { 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 = @@ -1740,6 +1756,32 @@ void main() { 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>()) ...[ @@ -1787,7 +1829,7 @@ void main() { ); expect( mixedSv, - contains('output wire logic [44:0] valuesOut [1:0]'), + contains('output logic [44:0] valuesOut [1:0]'), ); expect( mixedSv, @@ -1797,6 +1839,19 @@ void main() { mixedSv, contains('logic [2:0] valuesIn_0__0__samples [1:0];'), ); + final workaroundSv = mixedModule.generateSynth( + configuration: _iverilogUnpackedArrayWorkaround, + ); + expect( + RegExp( + RegExp.escape('output wire logic [44:0] valuesOut [1:0]'), + ).allMatches(workaroundSv), + hasLength(2), + ); + expect( + workaroundSv, + contains('wire [44:0] valuesOut_0 [1:0];'), + ); final mixedNetlist = jsonDecode( NetlistSynthesizer().synthesizeToJson(mixedModule), ) as Map; @@ -1804,7 +1859,11 @@ void main() { mixedNetlist['modules'], containsPair('_NestedArrayBoundaryModule', isA>()), ); - SimCompare.checkIverilogVector(mixedModule, vectors); + SimCompare.checkIverilogVector( + mixedModule, + vectors, + synthesizerConfiguration: _iverilogUnpackedArrayWorkaround, + ); SimCompare.checkVerilatorVector(mixedModule, vectors); }, tags: ['verilator']); @@ -1843,13 +1902,20 @@ void main() { ); expect( sv, - contains('output wire logic [2:0][159:0] valuesOut [1:0]'), + contains('output logic [2:0][159:0] valuesOut [1:0]'), ); expect( sv, contains('valuesOut[1][2][159:152] = ' 'valuesIn[1][2][7:0]'), ); + final workaroundSv = module.generateSynth( + configuration: _iverilogUnpackedArrayWorkaround, + ); + expect( + workaroundSv, + contains('output wire logic [2:0][159:0] valuesOut [1:0]'), + ); final netlist = jsonDecode(NetlistSynthesizer().synthesizeToJson(module)) as Map; final modules = netlist['modules'] as Map; @@ -1868,7 +1934,11 @@ void main() { expect(innerType['elementWidth'], 8); expect(port['bits'], hasLength(960)); } - SimCompare.checkIverilogVector(module, vectors); + SimCompare.checkIverilogVector( + module, + vectors, + synthesizerConfiguration: _iverilogUnpackedArrayWorkaround, + ); SimCompare.checkVerilatorVector(module, vectors); expect(source.numUnpackedDimensions, 1); expect(source.at([1, 2]).numUnpackedDimensions, 1); From 3a00af840fd74032f7b685374c7f24994ec8eaaa Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 06:03:56 -0700 Subject: [PATCH 28/43] comment out unpublished link for now --- doc/user_guide/_docs/A20-logic-arrays.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 09c1b31b48..e1ff740911 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -7,7 +7,8 @@ toc: true 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) +`TypedLogicArray` + 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. @@ -87,7 +88,8 @@ representations. 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) +`TypedLogicArray` API documentation + 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. From b696ca55e470b7f5a058515b2ea7b244280479d3 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 17 Sep 2026 06:53:04 -0700 Subject: [PATCH 29/43] fix tests to use BigInt due to Node failure, add TypedLogicArray naming to the naming test matrix Signed-off-by: Desmond A. Kirkpatrick --- test/naming_cases_test.dart | 79 ++++++++++++++++++++++++++++++++ test/typed_logic_array_test.dart | 23 ++++++++-- 2 files changed, 98 insertions(+), 4 deletions(-) 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/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 89c335bdc8..c746a0ffa7 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -507,8 +507,8 @@ LogicValue _arrayValuedElementPacked( matrixElementIndex * matrixDimensions[index] + matrixIndices[index]; } final matrixWidth = matrixDimensions.fold(1, (width, size) => width * size); - return LogicValue.ofInt( - 2 << (2 + matrixElementIndex * 9 + 7), + return LogicValue.ofBigInt( + BigInt.from(2) << (2 + matrixElementIndex * 9 + 7), 2 + matrixWidth * 9 + 1, ); } @@ -1520,9 +1520,15 @@ void main() { ); final module = _ArrayValuedElementFieldModule(source); await module.build(); + final selectedElement = _arrayValuedElementPacked(const [2], const [1]); final vectors = [ Vector( - {'values': LogicValue.ofInt(2 << 39, source.width)}, + { + 'values': LogicValue.ofIterable([ + LogicValue.ofBigInt(BigInt.zero, selectedElement.width), + selectedElement, + ]), + }, {'selected': 2}, ), ]; @@ -1627,9 +1633,18 @@ void main() { 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.ofInt(payload << 32, source.width)}, + { + 'values': LogicValue.ofIterable([ + LogicValue.ofBigInt(BigInt.zero, source.elementWidth), + arrayElement, + ]), + }, {'selected': payload}, ), ]; From d882b754fb76d0f8744158112dbcf69c06861c1f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Mon, 21 Sep 2026 09:32:18 -0700 Subject: [PATCH 30/43] several regressions found in merge compatibility, assignability, field name matching, withSubset issue, empty arrays (SV issue) Signed-off-by: Desmond A. Kirkpatrick --- doc/user_guide/_docs/A20-logic-arrays.md | 4 + lib/src/signals/logic_array.dart | 3 + lib/src/signals/logic_structure.dart | 4 +- lib/src/signals/typed_logic_array.dart | 62 ++++- ...systemverilog_synth_module_definition.dart | 2 +- .../synthesizers/utilities/synth_logic.dart | 73 +++++- test/logic_array_test.dart | 20 ++ test/logic_name_config_test.dart | 226 +++++++++++++++++ test/logic_structure_test.dart | 15 ++ test/typed_logic_array_test.dart | 238 ++++++++++++++++++ 10 files changed, 625 insertions(+), 22 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index e1ff740911..8e2a9571fc 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -120,6 +120,10 @@ 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. + Icarus Verilog 12.0 can leave child-driven unpacked array variables unknown during simulation. When targeting that tool, enable `SystemVerilogSynthesizerConfiguration.iverilogWorkaroundForUnpackedArrayVariables`. diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 000556aad1..367f0dfa55 100644 --- a/lib/src/signals/logic_array.dart +++ b/lib/src/signals/logic_array.dart @@ -21,6 +21,9 @@ class LogicArray extends TypedLogicArray { /// [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, diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index f18f1a9423..1196799c7c 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -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/typed_logic_array.dart b/lib/src/signals/typed_logic_array.dart index 8c050b6e9f..3f4a821a3f 100644 --- a/lib/src/signals/typed_logic_array.dart +++ b/lib/src/signals/typed_logic_array.dart @@ -92,6 +92,9 @@ class TypedLogicArray extends BaseLogicArray { /// [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( @@ -344,13 +347,13 @@ class _TypedLogicArrayBuild { .expand((element) => element.arrayElements) .toList(growable: false); if (typedLeaves.any(_containsUnassignableLeaf)) { - throw LogicConstructionException('TypedLogicArray leaves must be ' - 'driveable and cannot contain Consts.'); + throw LogicConstructionException( + 'TypedLogicArray leaves must be driveable.'); } final prototype = typedLeaves.isEmpty ? emptyPrototype! : typedLeaves.first; if (_containsUnassignableLeaf(prototype)) { - throw LogicConstructionException('TypedLogicArray leaves must be ' - 'driveable and cannot contain Consts.'); + throw LogicConstructionException( + 'TypedLogicArray leaves must be driveable.'); } final prototypeNetComposition = _netComposition(prototype); final elementsForNetValidation = @@ -367,6 +370,11 @@ class _TypedLogicArrayBuild { 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() @@ -386,11 +394,47 @@ class _TypedLogicArrayBuild { prototypeNetComposition.every((isNet) => isNet)); } - /// Whether [leaf] is or recursively contains an unassignable constant. - static bool _containsUnassignableLeaf(Logic leaf) => - leaf is Const || - (leaf is LogicStructure && - leaf.leafElements.any((element) => element is Const)); + /// 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) { diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 5e084c43b8..1124580576 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -1446,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) diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 8ee60276c6..3da733d244 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -401,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; } @@ -499,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 = [ @@ -520,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. /// @@ -637,6 +684,14 @@ class SynthLogic { if (isArray) { 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(); diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 10d475ffc8..e1c0f5b3d1 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -662,6 +662,26 @@ void main() { } }); + 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.generateSynth, + 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); diff --git a/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 236766e29c..74cf74b732 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.generateSynth(); + 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/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index c746a0ffa7..13c7adb188 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -472,6 +472,28 @@ class _MixedNestedArrayChild extends Module { } } +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, @@ -661,6 +683,38 @@ void main() { ), 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', () { @@ -754,6 +808,25 @@ void main() { ); }); + 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], @@ -786,6 +859,33 @@ void main() { ); }); + 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], @@ -804,6 +904,83 @@ void main() { ); }); + 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.generateSynth, + 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], @@ -829,6 +1006,35 @@ void main() { ); }); + 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; @@ -956,6 +1162,29 @@ void main() { 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( @@ -986,6 +1215,15 @@ void main() { ]) { expect(create, throwsA(isA())); } + + final reused = Logic(width: 3); + expect( + () => TypedLogicArray( + [2], + ({name}) => reused, + ), + throwsA(isA()), + ); }); test('rejects invalid indexing', () { From 67b6455fc755954fb7ee4334723e25e3fac518db Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 7 Oct 2026 21:59:10 -0700 Subject: [PATCH 31/43] doc fixes --- .github/configs/mlc_config.json | 3 ++ doc/user_guide/_docs/A20-logic-arrays.md | 39 +++++++++++++----------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index 98d888fe97..8fa659d7ac 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -32,6 +32,9 @@ }, { "pattern":"^https://.*\\.fandom\\.com" + }, + { + "pattern": "^https://intel\\.github\\.io/rohd/rohd/" } ] } diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 8e2a9571fc..8369ee8f16 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -7,8 +7,7 @@ toc: true Use [`LogicArray`](https://intel.github.io/rohd/rohd/LogicArray-class.html) for multidimensional arrays of ordinary `Logic`. Use -`TypedLogicArray` - +[`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. @@ -88,8 +87,7 @@ representations. 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 - +[`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. @@ -128,13 +126,15 @@ Icarus Verilog 12.0 can leave child-driven unpacked array variables unknown during simulation. When targeting that tool, enable `SystemVerilogSynthesizerConfiguration.iverilogWorkaroundForUnpackedArrayVariables`. -Custom typed-array subclasses should override `createClone` to preserve their -runtime type and constructor configuration. See the API documentation for the -full subclassing contract. +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: +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( @@ -156,15 +156,20 @@ final transposed = values.transpose2D(); // Dimensions: [3, 2] final signals = values.toLogicArray(name: 'values'); // signals are driven by values ``` -Nested constructors reject ragged rows, inconsistent nesting depth, and mismatched element widths. Empty nested input cannot reveal the element width or trailing dimensions, so it must use `fromFlat`. `majorSlices` iterates the outer dimension rather than the total element count, so a `[2, 0]` value contains two empty `[0]` slices and can round-trip through `stack`. - -`TypedLogicValueArray` adds a `LogicValueCodec` for application-level values. `LogicValueArray` remains its `TypedLogicValueArray` specialization with the existing convenience constructors and concrete transform return types. Construction immediately encodes and decodes every value: the packed representation is authoritative, and a lossy codec therefore exposes normalized semantic values from the start. Decoded semantic elements are exposed by reference, so mutating a mutable element does not update the stored packed bits; callers using mutable semantic values are responsible for treating them consistently with snapshot semantics. Shape-only operations preserve those normalized values without re-encoding them. `stack` requires every typed value array to use the identical codec object because codec functions cannot be compared for semantic equivalence. - -The root list of a nested constructor always represents an array dimension. Below the root, an object matching `T` is treated as one semantic value before it is considered as another list dimension. This permits list-valued semantic elements; use `fromFlat` when the intended interpretation would otherwise be ambiguous. - -Both value-array classes are `LogicValue`s. Their `width` and deprecated `length` count packed bits, while `arrayValues.length` counts array positions. `arrayValues` has one entry for each configured array position in row-major order. Bit indexing, equality, hashing, arithmetic, and bitwise operations use the packed value and do not consider shape. The `packed` getter exposes the ordinary `LogicValue` representation. - -`TypedLogicArray.value` and `previousValue` return `TypedLogicValueArray` snapshots without adding hardware to the graph. `LogicArray` overrides these with the concrete `LogicValueArray` return type. The standard `changed`, `glitch`, and edge APIs remain packed `LogicValueChanged` events, so typed arrays retain the normal `Logic` event contract. Since all value arrays are `LogicValue`s, use the target-side `put` API for immediate assignment or `inject` for scheduled assignment. Both follow the ordinary packed-value contract, so same-width values remain assignable regardless of their shape metadata. +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 From 2215bab547152325d66e09520a8a97037713fe4b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 7 Oct 2026 23:56:56 -0700 Subject: [PATCH 32/43] sv bug in source rendering --- .../systemverilog/systemverilog_synthesis_result.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index b46d7d59d6..4a87a0fbf7 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -326,10 +326,13 @@ class SystemVerilogSynthesisResult extends SynthesisResult { assignment.dstUpperIndex, ); } - source = assignment.src.rangeName( - assignment.srcLowerIndex, - assignment.srcUpperIndex, - ); + 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) { From 54243d9cd3ae82b1cb124a82ab08058cac78b567 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 17:28:12 -0700 Subject: [PATCH 33/43] Clean up rebase imports and test API usage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/module.dart | 2 -- test/logic_array_test.dart | 2 +- test/synth_name_parity_test.dart | 2 +- test/systemverilog_port_types_test.dart | 2 +- test/typed_logic_array_inout_test.dart | 4 ++-- test/typed_logic_array_test.dart | 22 +++++++++++----------- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/lib/src/module.dart b/lib/src/module.dart index a790a6e64c..bc2825e9c5 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -13,9 +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/diagnostics/inspector_service.dart'; import 'package:rohd/src/signals/signals.dart'; -import 'package:rohd/src/utilities/config.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index e1c0f5b3d1..ddd6de3332 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -670,7 +670,7 @@ void main() { final module = _TypedInputModule(array); await module.build(); expect( - module.generateSynth, + module.dumpSystemVerilog, throwsA( isA().having( (exception) => exception.message, diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart index f030492cd0..250a87a29c 100644 --- a/test/synth_name_parity_test.dart +++ b/test/synth_name_parity_test.dart @@ -591,7 +591,7 @@ void main() { final svModule = _NestedNameModule(); await svModule.build(); - final sv = svModule.generateSynth(); + final sv = svModule.dumpSystemVerilog(); final svNames = names(svModule); expect(svNames, equals(netlistNames)); diff --git a/test/systemverilog_port_types_test.dart b/test/systemverilog_port_types_test.dart index 39cfcdde4c..8bad5789b1 100644 --- a/test/systemverilog_port_types_test.dart +++ b/test/systemverilog_port_types_test.dart @@ -256,7 +256,7 @@ void main() { ]; for (final configuration in configurations) { - final sv = module.generateSynth( + final sv = module.dumpSystemVerilog( configuration: configuration.configuration, ); expect( diff --git a/test/typed_logic_array_inout_test.dart b/test/typed_logic_array_inout_test.dart index df90195db4..241f614790 100644 --- a/test/typed_logic_array_inout_test.dart +++ b/test/typed_logic_array_inout_test.dart @@ -272,7 +272,7 @@ void main() { ]; await _checkRohdAndIverilogVectors(module, vectors); - final sv = module.generateSynth(); + final sv = module.dumpSystemVerilog(); expect(sv, contains('inout wire [1:0][8:0] bus')); _expectCompatibleNetConnectDefinition(sv); expect(source.numUnpackedDimensions, 0); @@ -340,7 +340,7 @@ void main() { ]; await _checkRohdAndIverilogVectors(module, vectors); - final sv = module.generateSynth(); + final sv = module.dumpSystemVerilog(); expect(sv, contains('inout wire [1:0][20:0] bus')); _expectCompatibleNetConnectDefinition(sv); diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 13c7adb188..4d105c400c 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -969,7 +969,7 @@ void main() { final module = _TypedInputModule(values); await module.build(); expect( - module.generateSynth, + module.dumpSystemVerilog, throwsA( isA().having( (exception) => exception.message, @@ -1740,7 +1740,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); + 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]'))); @@ -1772,7 +1772,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); + 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]'))); @@ -1813,7 +1813,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); + 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]'))); @@ -1854,7 +1854,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); + 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]'))); @@ -1888,7 +1888,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); + 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]'))); @@ -1954,7 +1954,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); + final sv = module.dumpSystemVerilog(); expect(sv, contains('valuesIn[1][31:30]')); expect(sv, contains('valuesOut[0][31:30]')); expect( @@ -2075,7 +2075,7 @@ void main() { await mixedModule.build(); await SimCompare.checkFunctionalVector(mixedModule, vectors); - final mixedSv = mixedModule.generateSynth(); + final mixedSv = mixedModule.dumpSystemVerilog(); expect( mixedSv, contains('input logic [44:0] valuesIn [1:0]'), @@ -2092,7 +2092,7 @@ void main() { mixedSv, contains('logic [2:0] valuesIn_0__0__samples [1:0];'), ); - final workaroundSv = mixedModule.generateSynth( + final workaroundSv = mixedModule.dumpSystemVerilog( configuration: _iverilogUnpackedArrayWorkaround, ); expect( @@ -2148,7 +2148,7 @@ void main() { Vector({'valuesIn': input}, {'valuesOut': expected}) ]; await SimCompare.checkFunctionalVector(module, vectors); - final sv = module.generateSynth(); + final sv = module.dumpSystemVerilog(); expect( sv, contains('input logic [2:0][159:0] valuesIn [1:0]'), @@ -2162,7 +2162,7 @@ void main() { contains('valuesOut[1][2][159:152] = ' 'valuesIn[1][2][7:0]'), ); - final workaroundSv = module.generateSynth( + final workaroundSv = module.dumpSystemVerilog( configuration: _iverilogUnpackedArrayWorkaround, ); expect( From 970c5d3f60d46b5a4f3b2c103473fcfffd09908a Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 17:41:12 -0700 Subject: [PATCH 34/43] ignore microsoft site problem with visualstudiocode slowness --- .github/configs/mlc_config.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index 8fa659d7ac..488f763593 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -27,6 +27,9 @@ { "pattern": "^https://code\\.visualstudio\\.com/" }, + { + "pattern": "^https://marketplace\\.visualstudio\\.com/items\\?itemName=Dart-Code\\.dart-code$" + }, { "pattern": "^https://www.nandland.com" }, From 59bcd29ab5b2a09ef84e4e819772105f89c22be0 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 15 Oct 2026 12:23:38 -0700 Subject: [PATCH 35/43] LogicEnum with TypedLogicArray coverage --- CHANGELOG.md | 4 + .../netlist/netlist_validation.dart | 66 ++--- .../systemverilog_synthesis_result.dart | 2 + test/logic_enum_test.dart | 235 ++++++++++++++++++ test/netlist_synthesizer_test.dart | 161 ++++++++---- 5 files changed, 391 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23af18f47f..58d92971d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ ### 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 (). @@ -39,6 +42,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/lib/src/synthesizers/netlist/netlist_validation.dart b/lib/src/synthesizers/netlist/netlist_validation.dart index 828a674412..0a6a570dcf 100644 --- a/lib/src/synthesizers/netlist/netlist_validation.dart +++ b/lib/src/synthesizers/netlist/netlist_validation.dart @@ -59,53 +59,61 @@ class NetlistValidation { 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, )); } - if (netnames != null) { - 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; - } - final bits = (netname['bits'] as List?)?.whereType() ?? const []; - final aggregateDrivers = <({String description, bool isTriState})>{ - for (final bit in bits) ...driversByBit[bit] ?? const [], - }; - if (aggregateDrivers.length <= 1 || - aggregateDrivers.every((driver) => driver.isTriState)) { - continue; - } - issues.add(NetlistValidationIssue( - 'aggregate net "${entry.key}" is reached from multiple drivers: ' - '${aggregateDrivers.map((driver) => driver.description).join(', ')}', - netname: entry.key, - drivers: aggregateDrivers.map((driver) => driver.description), - )); - } - } - if (issues.isNotEmpty) { throw NetlistValidationException(moduleName, issues); } } + 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/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 4a87a0fbf7..e85f6c9ff2 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -351,6 +351,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( 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/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index a19637ed1f..dd4da394c5 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -2412,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, ); }); @@ -2438,15 +2466,38 @@ 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', + ), ), ); }); @@ -2488,51 +2539,54 @@ void main() { ); }); - test('struct net aggregates reject 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] + 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] + '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}, - ], + }; + 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, - ), - throwsA(isA()), - ); - }); + expect( + () => NetlistValidation.validate( + const {}, + cells, + 'structured_net_module', + netnames: netnames, + ), + returnsNormally, + ); + }, + ); - test('struct net aggregates reject normal partial overlaps', () { + test('struct net aggregates reject mixed partial overlaps', () { final cells = >{ 'tri_state': { 'type': r'$tribuf', @@ -2565,7 +2619,18 @@ void main() { 'structured_net_module', netnames: netnames, ), - throwsA(isA()), + 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)', + ]))), ); }); From a5652ae8882c8621e34eb44127263c739cd8b5e3 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 15 Oct 2026 16:08:33 -0700 Subject: [PATCH 36/43] Allow widget Pana score deductions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tool/gh_actions/pana_source.sh | 4 ++-- tool/test/pana_source_test.sh | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) 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..45aaf7346c 100644 --- a/tool/test/pana_source_test.sh +++ b/tool/test/pana_source_test.sh @@ -150,6 +150,7 @@ run_case 0 "$NAVIGATOR" dart export EXPECTED_SDK=flutter EXPECTED_THRESHOLD=10 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_STATUS=73 run_case 73 "$WIDGETS" flutter export EXPECTED_SDK=dart EXPECTED_THRESHOLD=10 run_case 0 "$WAVEFORM" dart From fc43ce039f57bbd01e519df7147af11767287e64 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 15 Oct 2026 16:54:22 -0700 Subject: [PATCH 37/43] lower pana requirement for devtools_widgets until we can update dart --- doc/releases.md | 11 +++++++---- tool/test/pana_source_test.sh | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) 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/tool/test/pana_source_test.sh b/tool/test/pana_source_test.sh index 45aaf7346c..c9ec9bf297 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,8 +151,9 @@ 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 From 928cd1891aa40dbcd27d1725c8984f716d7477b7 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 15 Oct 2026 17:38:42 -0700 Subject: [PATCH 38/43] Allow waveform Pana score deductions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tool/test/pana_source_test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tool/test/pana_source_test.sh b/tool/test/pana_source_test.sh index c9ec9bf297..1ca3f50a7c 100644 --- a/tool/test/pana_source_test.sh +++ b/tool/test/pana_source_test.sh @@ -159,6 +159,7 @@ 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" From 754ba862136d6201bea1c62281c84eaac8ee97af Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 17 Oct 2026 23:02:46 -0700 Subject: [PATCH 39/43] Restore typed-array synthesis behavior after rebase Keep nested array output declarations configurable and allow packed references to net-backed signals while retaining the opt-in Icarus workaround. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../systemverilog_synthesis_result.dart | 40 +++---------------- .../synthesizers/utilities/synth_logic.dart | 1 - 2 files changed, 6 insertions(+), 35 deletions(-) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index e85f6c9ff2..416b28aaf6 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -181,12 +181,9 @@ class SystemVerilogSynthesisResult extends SynthesisResult { : configuration.outputPortType; return _verilogPort( 'output', - useIverilogWorkaround || _requiresNetDeclaration(sig) - ? 'wire' - : 'var', + useIverilogWorkaround ? 'wire' : 'var', portType, sig, - forceObjectType: useIverilogWorkaround || _requiresNetDeclaration(sig), ); }); @@ -210,14 +207,11 @@ class SystemVerilogSynthesisResult extends SynthesisResult { String direction, String objectType, SystemVerilogPortTypeConfiguration portType, - SynthLogic sig, { - bool forceObjectType = false, - }) => + SynthLogic sig, + ) => [ direction, - if (forceObjectType || - portType.objectType == SystemVerilogPortType.explicit) - objectType, + if (portType.objectType == SystemVerilogPortType.explicit) objectType, if (portType.dataType == SystemVerilogPortType.explicit) 'logic', sig.definitionName(useEnumType: false), ].join(' '); @@ -228,27 +222,6 @@ class SystemVerilogSynthesisResult extends SynthesisResult { return array != null && array.numUnpackedDimensions > 0; } - bool _requiresNetDeclaration(SynthLogic sig) { - final logic = sig.logics.firstOrNull; - if (logic is! BaseLogicArray || !logic.arrayElements.any(_containsArray)) { - return false; - } - - final resolved = sig.resolved; - return module.tryOutput(sig.name) != null || - _synthModuleDefinition.assignments.any( - (assignment) => identical(_referenceRoot(assignment.dst), resolved), - ) || - _synthModuleDefinition.subModuleInstantiations.any( - (instantiation) => instantiation.outputMapping.values - .any((mapped) => identical(mapped.resolved, resolved)), - ); - } - - static bool _containsArray(Logic logic) => - logic is BaseLogicArray || - (logic is LogicStructure && logic.elements.any(_containsArray)); - /// Whether [sig] is driven by a submodule output. bool _isDrivenBySubmoduleOutput(SynthLogic sig) { final target = sig.resolved; @@ -287,11 +260,10 @@ class SystemVerilogSynthesisResult extends SynthesisResult { configuration.iverilogWorkaroundForUnpackedArrayVariables && _isUnpackedArray(sig) && _isDrivenBySubmoduleOutput(sig); - final type = useIverilogWorkaround || _requiresNetDeclaration(sig) + final type = useIverilogWorkaround ? 'wire' : sig.definitionType(useEnumType: configuration.generateEnums); - final decl = - '$type ' + final decl = '$type ' '${sig.definitionName(useEnumType: configuration.generateEnums)};'; declarations.add(_SvTraceHelper._pad(decl, comment)); } diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index 3da733d244..b5eafb405a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -769,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.', From 3e4a5969dce0c127592d670700e746f26d2d1161 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 07:05:49 -0700 Subject: [PATCH 40/43] improve testing time, fixed nested array output and packed references --- .github/workflows/general.yml | 4 ---- README.md | 22 +++++++++++++--------- test/logic_name_config_test.dart | 2 +- test/logic_value_array_test.dart | 3 --- test/unpacked_array_synthesis_test.dart | 11 +++-------- 5 files changed, 17 insertions(+), 25 deletions(-) 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/README.md b/README.md index aeee83e1ba..00aee8ea6d 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. - 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 +- Simulate with **various abstraction levels of models** from architectural, to functional, to cycle-accurate, to RTL levels in the same language and environment - **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 +- Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. +- 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/test/logic_name_config_test.dart b/test/logic_name_config_test.dart index 74cf74b732..5723051f72 100644 --- a/test/logic_name_config_test.dart +++ b/test/logic_name_config_test.dart @@ -446,7 +446,7 @@ void main() { } } - final sv = dut.generateSynth(); + final sv = dut.dumpSystemVerilog(); expect(sv, isNot(contains('shared_'))); final vectors = [ for (final value in [0, 0xa5, 0x5a, '10xz01zx']) diff --git a/test/logic_value_array_test.dart b/test/logic_value_array_test.dart index 5af6668b47..10ac6ab427 100644 --- a/test/logic_value_array_test.dart +++ b/test/logic_value_array_test.dart @@ -35,9 +35,6 @@ void main() { expect(nested.elementWidth, 8); expect(nested.arrayValues.length, 4); expect(nested.width, 32); - // The deprecated LogicValue length retains its packed-bit meaning. - // ignore: deprecated_member_use_from_same_package - expect(nested.length, 32); expect(nested.arrayValues, leaves); expect(nested.packed, LogicValue.ofInt(0x04030201, 32)); expect(flat, nested); 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']); } } From 88c63ca0b70836c198f309f62fba181ebcc4f85d Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 11:15:46 -0700 Subject: [PATCH 41/43] coverage badges --- .github/configs/mlc_config.json | 3 +++ .github/workflows/coverage.yml | 23 ++++++++++++++++++++--- README.md | 2 +- rohd_devtools_extension/README.md | 2 ++ tool/generate_coverage.sh | 2 +- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index 488f763593..0b6ff56de4 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -30,6 +30,9 @@ { "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" }, 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/README.md b/README.md index 00aee8ea6d..15ae6fc5e0 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ You can also open this repository in a GitHub Codespace to run the example in yo - Fewer bugs and lines of code means **shorter development schedule** - Use **modern IDEs** like Visual Studio Code, with excellent static analysis, fast autocomplete, built-in debugger, linting, git integration, extensions, and much more -- The ROHD Framework is a complete Design and Verification 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 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/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. From 82f6710add711997746e4f661ddd02642409dba3 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 13:18:59 -0700 Subject: [PATCH 42/43] systemc fix for async, README updates --- README.md | 14 ++--- .../systemc/systemc_synthesis_result.dart | 13 +++- test/systemc_async_reset_test.dart | 63 +++++++++++++++++++ test/systemc_service_test.dart | 3 + test/systemc_split_packed_output_test.dart | 1 + 5 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 test/systemc_async_reset_test.dart diff --git a/README.md b/README.md index 15ae6fc5e0..97543d0419 100644 --- a/README.md +++ b/README.md @@ -38,20 +38,20 @@ 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 +- 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 -- Simulate with **various abstraction levels of models** from architectural, to functional, to cycle-accurate, to RTL levels in the same language and environment +- **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 +- 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 -- 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** -- Use **modern IDEs** like Visual Studio Code, with excellent static analysis, fast autocomplete, built-in debugger, linting, git integration, extensions, and much more - 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 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/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'; From 442e10ce1378948db5e6907c8c4217bfe64b5c06 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 14:58:06 -0700 Subject: [PATCH 43/43] Remove unreleased value-array conversion and Icarus workaround Retain configurable standard SystemVerilog and verify child-driven unpacked arrays using Verilator where Icarus cannot simulate them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 - doc/user_guide/_docs/A20-logic-arrays.md | 5 -- .../systemverilog_synthesis_result.dart | 48 ++----------------- ...stemverilog_synthesizer_configuration.dart | 11 ----- lib/src/values/typed_logic_value_array.dart | 4 -- test/systemverilog_port_types_test.dart | 10 ---- test/typed_logic_array_test.dart | 36 +------------- test/typed_logic_value_array_test.dart | 5 +- 8 files changed, 6 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d92971d6..8c1a5e3fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,6 @@ - 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 (). -- Preserved configured SystemVerilog output object types for nested arrays and added the opt-in `iverilogWorkaroundForUnpackedArrayVariables` setting for child-driven unpacked array variables that remain unknown in Icarus Verilog 12.0 (). - 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 (). diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 8369ee8f16..889db3b827 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -122,10 +122,6 @@ Zero-sized arrays are supported for simulation and value operations. SystemVerilog generation rejects them because the language has no portable zero-width array declaration. -Icarus Verilog 12.0 can leave child-driven unpacked array variables unknown -during simulation. When targeting that tool, enable -`SystemVerilogSynthesizerConfiguration.iverilogWorkaroundForUnpackedArrayVariables`. - See the [`TypedLogicArray` API documentation](https://intel.github.io/rohd/rohd/TypedLogicArray-class.html) for subclassing and cloning details. @@ -153,7 +149,6 @@ final emptyRows = LogicValueArray.fromFlat([2, 0], const [], elementWidth: 8); final transposed = values.transpose2D(); // Dimensions: [3, 2] -final signals = values.toLogicArray(name: 'values'); // signals are driven by values ``` Nested lists infer shape and reject ragged or inconsistent input. Use `fromFlat` diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 416b28aaf6..4c8cb310aa 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -11,7 +11,6 @@ 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/systemverilog/systemverilog_synth_module_definition.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart'; import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; @@ -171,18 +170,10 @@ class SystemVerilogSynthesisResult extends SynthesisResult { module.tryOutput(sig.name) != null, 'Named output ${sig.name} not found in module ${module.name}.', ); - final useIverilogWorkaround = - configuration.iverilogWorkaroundForUnpackedArrayVariables && - _isUnpackedArray(sig); - final portType = useIverilogWorkaround - ? SystemVerilogPortTypeConfiguration( - dataType: configuration.outputPortType.dataType, - ) - : configuration.outputPortType; return _verilogPort( 'output', - useIverilogWorkaround ? 'wire' : 'var', - portType, + 'var', + configuration.outputPortType, sig, ); }); @@ -216,33 +207,6 @@ class SystemVerilogSynthesisResult extends SynthesisResult { sig.definitionName(useEnumType: false), ].join(' '); - /// Whether [sig] represents an array with an unpacked dimension. - bool _isUnpackedArray(SynthLogic sig) { - final array = sig.logics.whereType().firstOrNull; - return array != null && array.numUnpackedDimensions > 0; - } - - /// Whether [sig] is driven by a submodule output. - bool _isDrivenBySubmoduleOutput(SynthLogic sig) { - final target = sig.resolved; - return _synthModuleDefinition.subModuleInstantiations.any( - (instantiation) => instantiation.outputMapping.values.any( - (mapped) => identical(_referenceRoot(mapped), target), - ), - ); - } - - SynthLogic _referenceRoot(SynthLogic signal) { - final resolved = signal.resolved; - return switch (resolved) { - SynthLogicArrayElement() => _referenceRoot(resolved.parentArray), - SynthLogicArrayStructureElement() => _referenceRoot(resolved.rootArray), - SynthLogicPackedBitReference() => _referenceRoot(resolved.packedBase), - SynthLogicPackedRangeReference() => _referenceRoot(resolved.packedBase), - _ => resolved, - }; - } - /// Representation of all internal net declarations in generated SV. String _verilogInternalSignals(_SvTraceHelper traceHelper) { final declarations = []; @@ -256,13 +220,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { final comment = _assignedSignalNames.contains(sig.name) ? '' : traceHelper.signalComment(sig); - final useIverilogWorkaround = - configuration.iverilogWorkaroundForUnpackedArrayVariables && - _isUnpackedArray(sig) && - _isDrivenBySubmoduleOutput(sig); - final type = useIverilogWorkaround - ? 'wire' - : 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)); diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart index 16bf02d848..2cf8c281a8 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart @@ -50,16 +50,6 @@ class SystemVerilogSynthesizerConfiguration { /// Type configuration for inout ports. final SystemVerilogPortTypeConfiguration inOutPortType; - /// Whether to work around Icarus Verilog's unpacked array variable issue. - /// - /// Icarus Verilog 12.0 accepts child-driven unpacked array variables, - /// including output ports and internal intermediates, but their values remain - /// unknown during simulation. Enabling this option emits an explicit `wire` - /// object type for unpacked array output ports and child-driven internal - /// unpacked arrays. It overrides `outputPortType.objectType` for those ports - /// and is disabled by default. - final bool iverilogWorkaroundForUnpackedArrayVariables; - /// Creates a new configuration for SystemVerilog synthesis. const SystemVerilogSynthesizerConfiguration({ this.generateEnums = true, @@ -72,6 +62,5 @@ class SystemVerilogSynthesizerConfiguration { this.inOutPortType = const SystemVerilogPortTypeConfiguration( dataType: SystemVerilogPortType.implicit, ), - this.iverilogWorkaroundForUnpackedArrayVariables = false, }); } diff --git a/lib/src/values/typed_logic_value_array.dart b/lib/src/values/typed_logic_value_array.dart index ba6abfd50c..8ce8bbcda6 100644 --- a/lib/src/values/typed_logic_value_array.dart +++ b/lib/src/values/typed_logic_value_array.dart @@ -292,10 +292,6 @@ class TypedLogicValueArray extends LogicValue { return _createStored(newDimensions, semanticValues, packedElements); } - /// Creates a [LogicArray] with the same shape and drives it with this value. - LogicArray toLogicArray({String? name}) => - LogicArray(dimensions, elementWidth, name: name)..put(this); - /// Rebuilds a shaped value from already-normalized semantic and packed data. TypedLogicValueArray _createStored( List dimensions, diff --git a/test/systemverilog_port_types_test.dart b/test/systemverilog_port_types_test.dart index 8bad5789b1..3a8473a629 100644 --- a/test/systemverilog_port_types_test.dart +++ b/test/systemverilog_port_types_test.dart @@ -243,16 +243,6 @@ void main() { expectedCount: 2, checkIverilog: true, ), - ( - name: 'Icarus unpacked variable workaround', - configuration: const SystemVerilogSynthesizerConfiguration( - outputPortType: SystemVerilogPortTypeConfiguration(), - iverilogWorkaroundForUnpackedArrayVariables: true, - ), - declaration: 'output wire logic [8:0] valuesOut [1:0]', - expectedCount: 2, - checkIverilog: true, - ), ]; for (final configuration in configurations) { diff --git a/test/typed_logic_array_test.dart b/test/typed_logic_array_test.dart index 4d105c400c..9ab1728a41 100644 --- a/test/typed_logic_array_test.dart +++ b/test/typed_logic_array_test.dart @@ -17,10 +17,6 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; -const _iverilogUnpackedArrayWorkaround = SystemVerilogSynthesizerConfiguration( - iverilogWorkaroundForUnpackedArrayVariables: true, -); - class _SampleStructure extends LogicStructure { final Logic low; final Logic high; @@ -2092,19 +2088,6 @@ void main() { mixedSv, contains('logic [2:0] valuesIn_0__0__samples [1:0];'), ); - final workaroundSv = mixedModule.dumpSystemVerilog( - configuration: _iverilogUnpackedArrayWorkaround, - ); - expect( - RegExp( - RegExp.escape('output wire logic [44:0] valuesOut [1:0]'), - ).allMatches(workaroundSv), - hasLength(2), - ); - expect( - workaroundSv, - contains('wire [44:0] valuesOut_0 [1:0];'), - ); final mixedNetlist = jsonDecode( NetlistSynthesizer().synthesizeToJson(mixedModule), ) as Map; @@ -2112,11 +2095,7 @@ void main() { mixedNetlist['modules'], containsPair('_NestedArrayBoundaryModule', isA>()), ); - SimCompare.checkIverilogVector( - mixedModule, - vectors, - synthesizerConfiguration: _iverilogUnpackedArrayWorkaround, - ); + // Icarus 12.0 leaves child-driven unpacked array variables unknown. SimCompare.checkVerilatorVector(mixedModule, vectors); }, tags: ['verilator']); @@ -2162,13 +2141,6 @@ void main() { contains('valuesOut[1][2][159:152] = ' 'valuesIn[1][2][7:0]'), ); - final workaroundSv = module.dumpSystemVerilog( - configuration: _iverilogUnpackedArrayWorkaround, - ); - expect( - workaroundSv, - contains('output wire logic [2:0][159:0] valuesOut [1:0]'), - ); final netlist = jsonDecode(NetlistSynthesizer().synthesizeToJson(module)) as Map; final modules = netlist['modules'] as Map; @@ -2187,11 +2159,7 @@ void main() { expect(innerType['elementWidth'], 8); expect(port['bits'], hasLength(960)); } - SimCompare.checkIverilogVector( - module, - vectors, - synthesizerConfiguration: _iverilogUnpackedArrayWorkaround, - ); + // 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); diff --git a/test/typed_logic_value_array_test.dart b/test/typed_logic_value_array_test.dart index 02ecc2d688..6f4bfce6bb 100644 --- a/test/typed_logic_value_array_test.dart +++ b/test/typed_logic_value_array_test.dart @@ -80,7 +80,7 @@ void main() { expect(flat.packed, nested.packed); }); - test('maps, reshapes, transposes, and converts typed values', () { + test('maps, reshapes, and transposes typed values', () { const codec = LogicValueCodec( decode: _decodeLogicValue, encode: _encodeLogicValue, @@ -98,9 +98,6 @@ void main() { 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]); - final signals = values.toLogicArray(); - expect(signals.dimensions, [2, 3]); - expect(signals.value, values); }); test('supports slices, indexed mapping, and compatible stacking', () {