From 83bfd54f31e06b9620d65f63175e96842f1dc969 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 21 Jul 2026 08:59:38 -0700 Subject: [PATCH 1/3] New LogicArrayOf datatype --- doc/user_guide/_docs/A20-logic-arrays.md | 44 ++- lib/src/signals/logic_array.dart | 92 +++++- lib/src/signals/logic_array_of.dart | 163 +++++++++++ lib/src/signals/logic_value_array.dart | 328 ++++++++++++++++++++++ lib/src/signals/logic_value_array_of.dart | 147 ++++++++++ lib/src/signals/signals.dart | 5 +- test/logic_array_of_test.dart | 241 ++++++++++++++++ 7 files changed, 1016 insertions(+), 4 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/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 5650f48c4..7d1127559 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,48 @@ 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 + +[`LogicArrayOf`](https://intel.github.io/rohd/rohd/LogicArrayOf-class.html) extends `LogicArray` when every leaf should have the same specialized `Logic` type. It preserves the normal array dimensions while exposing typed leaves with `typedLeafElements` and `elementAt`. 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; +``` + +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`, 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. diff --git a/lib/src/signals/logic_array.dart b/lib/src/signals/logic_array.dart index 6e7c9bd34..964d15b7f 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 @@ -24,6 +24,15 @@ 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; @@ -90,6 +99,73 @@ class LogicArray extends LogicStructure { 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 [LogicArray]s matching the remaining + /// dimensions. For a one-dimensional array, each element must have + /// [elementWidth] bits. + @protected + LogicArray.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! LogicArray || + !_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. /// /// Creates an array with specified [dimensions] and [elementWidth] named @@ -226,6 +302,13 @@ class LogicArray extends LogicStructure { required this.isNet, }); + List _calculateArrayElements() => dimensions.length == 1 + ? elements + : elements + .cast() + .expand((element) => element.arrayElements) + .toList(growable: false); + /// Constructs a new [LogicArray] with a more convenient constructor signature /// for when many ports in an interface are declared together. Also performs /// some basic checks on the legality of the array as a port of a [Module]. @@ -239,7 +322,8 @@ class LogicArray extends LogicStructure { return LogicArray( dimensions, elementWidth, - numUnpackedDimensions: numUnpackedDimensions, name: name, + numUnpackedDimensions: numUnpackedDimensions, + name: name, // make port names mergeable so we don't duplicate the ports // when calling connectIO @@ -270,3 +354,7 @@ class LogicArray extends LogicStructure { ); } } + +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..e30124255 --- /dev/null +++ b/lib/src/signals/logic_array_of.dart @@ -0,0 +1,163 @@ +// 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 [LogicArray]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 LogicArray { + /// 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); + + /// 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; + + /// 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_value_array.dart b/lib/src/signals/logic_value_array.dart new file mode 100644 index 000000000..d8c26d097 --- /dev/null +++ b/lib/src/signals/logic_value_array.dart @@ -0,0 +1,328 @@ +// 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 [LogicArray]. + factory LogicValueArray.fromLogicArray(LogicArray 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 [LogicArray]. +extension LogicArrayTraversal on LogicArray { + /// 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. + LogicArray 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. + LogicArray 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]. + LogicArray 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. + LogicArray 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..4bd922a41 --- /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..164aec5f2 --- /dev/null +++ b/test/logic_array_of_test.dart @@ -0,0 +1,241 @@ +// 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); +} + +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('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]); + }); + }); + + 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]); + }); + }); +} From 16bd1fba67932351a7c14a60c5826768d4f88397 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 21 Jul 2026 09:12:09 -0700 Subject: [PATCH 2/3] temporarily unlink the LogicArrayOf class pointer until it is installed --- doc/user_guide/_docs/A20-logic-arrays.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 7d1127559..b75472b69 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -24,7 +24,7 @@ As long as the total width of a `LogicArray` and another type of `Logic` (includ ## Typed and value-domain arrays -[`LogicArrayOf`](https://intel.github.io/rohd/rohd/LogicArrayOf-class.html) extends `LogicArray` when every leaf should have the same specialized `Logic` type. It preserves the normal array dimensions while exposing typed leaves with `typedLeafElements` and `elementAt`. For example, this creates a two-dimensional array of samples with separate data and valid fields: +`LogicArrayOf` extends `LogicArray` when every leaf should have the same specialized `Logic` type. It preserves the normal array dimensions while exposing typed leaves with `typedLeafElements` and `elementAt`. For example, this creates a two-dimensional array of samples with separate data and valid fields: ```dart class Sample extends LogicStructure { From 4eedf5f748c8f3f81487d35bba0f9f3f1544b82c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 21 Jul 2026 09:14:26 -0700 Subject: [PATCH 3/3] confirm that addTypedInput works with the new LogicArrayOf datatype --- doc/user_guide/_docs/A20-logic-arrays.md | 2 ++ test/logic_array_of_test.dart | 26 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index b75472b69..3572e2fd5 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -85,6 +85,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/test/logic_array_of_test.dart b/test/logic_array_of_test.dart index 164aec5f2..2cd977839 100644 --- a/test/logic_array_of_test.dart +++ b/test/logic_array_of_test.dart @@ -28,6 +28,16 @@ class _TwoBitStructure extends LogicStructure { _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); @@ -126,6 +136,22 @@ void main() { 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', () {