diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 12e172c74..88f9ff31d 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 aa2d53971..f6bfab547 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 5650f48c4..c68555124 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 6e7c9bd34..b42c8b866 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,147 @@ 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 List dimensions, + required int elementWidth, + required int numUnpackedDimensions, + required String name, + required Naming naming, + required bool isNet}) + : super.structured(elements, Logic.new, + dimensions: dimensions, + elementWidth: elementWidth, + numUnpackedDimensions: numUnpackedDimensions, + name: name, + naming: naming, + isNet: isNet); + + factory LogicArray._factory(List dimensions, int elementWidth, + {required String? name, + required int numUnpackedDimensions, + required Naming? naming, + required bool isNet, + required Logic Function({int width, Naming naming, String name}) + logicBuilder, + required LogicArray Function(List nextDimensions, int width, + {int numUnpackedDimensions, String name}) + logicArrayBuilder}) => + BaseLogicArray._factory(dimensions, elementWidth, + name: name, + numUnpackedDimensions: numUnpackedDimensions, + naming: naming, + logicBuilder: logicBuilder, + logicArrayBuilder: logicArrayBuilder, + arrayBuilder: LogicArray._, + isNet: isNet) as LogicArray; + + @override + + /// 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 +446,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 000000000..96035d9fa --- /dev/null +++ b/lib/src/signals/logic_array_of.dart @@ -0,0 +1,228 @@ +// 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, { + String? name, + }) : dimensionNames = build.dimensionNames, + super.structured(build.elements, + dimensions: build.dimensions, + elementWidth: build.elementWidth, + name: name); + + /// 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 ef463b9bb..e7df8647c 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -168,6 +168,43 @@ class LogicStructure implements Logic { late final List leafElements = UnmodifiableListView(_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 000000000..9d8ed6de0 --- /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 000000000..6c0c41c79 --- /dev/null +++ b/lib/src/signals/logic_value_array_of.dart @@ -0,0 +1,147 @@ +// 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; + + /// Converts a semantic value into its packed representation. + final LogicValue Function(T value) encode; + + /// Creates a bidirectional value [decode]/[encode] codec. + const LogicValueCodec({required this.decode, required this.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 348487a72..176fabe33 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; @@ -22,4 +22,7 @@ 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'; diff --git a/test/logic_array_of_test.dart b/test/logic_array_of_test.dart new file mode 100644 index 000000000..4d0cd9162 --- /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 fdc522e96..34a390a0c 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -235,6 +235,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', () async { // first field has width of 72 so this is the starting point // second field has a width of 12