From 360926ee68c7cf3d64df4257cc87d93aea71aa74 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 2 Sep 2026 11:30:36 -0700 Subject: [PATCH 01/33] Add Typed operations to retain LogicStructures --- doc/user_guide/_docs/A19-logic-structures.md | 26 +- doc/user_guide/_docs/A20-logic-arrays.md | 14 + lib/src/modules/conditionals/case.dart | 55 ++ lib/src/modules/conditionals/flop.dart | 213 ++++++- lib/src/modules/gates.dart | 107 +++- lib/src/modules/modules.dart | 4 +- lib/src/modules/passthrough.dart | 85 ++- lib/src/modules/pipeline.dart | 101 +++ lib/src/modules/typed_op.dart | 24 + lib/src/selection.dart | 37 +- lib/src/signals/logic_structure.dart | 102 +++ .../netlist/netlist_cell_mapper.dart | 7 + .../netlist_synthesizer_configuration.dart | 9 +- .../utilities/synth_module_stop_policy.dart | 5 +- test/long_chain_test.dart | 1 - test/multimodule3_test.dart | 1 - test/multimodule4_test.dart | 1 - test/multimodule5_test.dart | 1 - test/netlist_synthesizer_test.dart | 63 +- test/shuffle_test.dart | 1 - test/typed_operations_test.dart | 599 ++++++++++++++++++ 21 files changed, 1424 insertions(+), 32 deletions(-) create mode 100644 lib/src/modules/typed_op.dart create mode 100644 test/typed_operations_test.dart diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index aa2d53971a..cab0bd28d8 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -1,7 +1,7 @@ --- title: "Logic Structures" permalink: /docs/logic-structures/ -last_modified_at: 2025-7-23 +last_modified_at: 2026-9-2 toc: true --- @@ -17,6 +17,30 @@ Ports with matching types to the original `LogicStructure` can be created using `LogicArray`s are a type of `LogicStructure` and thus inherit these behavioral traits. +## Type-preserving operations + +`Mux`, `FlipFlop`, and `Passthrough` preserve a concrete `LogicStructure` type when their structure operands match. Dart infers the type from an ordinary structure input, so named fields remain available on the result: + +```dart +final selected = Mux(select, packet1, packet0).out; +final registered = FlipFlop(clk, selected, reset: reset).q; +final forwarded = Passthrough(registered).out; + +// All three values have type Packet. +forwarded.valid <= selected.valid; +``` + +The same behavior applies to `LogicArray` and `LogicArrayOf`, including nested typed arrays. Both mux operands must have the same concrete type and recursive shape: field widths, array dimensions, packing hints, and net/constant layout must match. + +For ordinary scalar operations, infer or explicitly request `Logic`. When a source is a `Const` or `LogicNet`, request `Logic` explicitly because the operation output is normalized to driveable logic: + +```dart +final selected = Mux(select, Const(0, width: 8), data).out; +final registered = FlipFlop(clk, selected).q; +``` + +Use `typedCases`, `selectIndexTyped`, and `selectFromTyped` for structure-preserving multi-way selection. `StructurePipeline` preserves the same concrete type at every registered pipeline boundary. Specify `T` when creating a pipeline with inline stage transforms so Dart can type the transform parameter. + ## Using `LogicStructure` to group signals The simplest way to use a `LogicStructure` is to just use its constructor, which requires a collection of `Logic`s. diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 889db3b827..8a4478a27a 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -189,6 +189,20 @@ Array ports in generated SystemVerilog will match dimensions (including unpacked Use the existing `addTypedInput`, `addTypedOutput`, and `addTypedInOut` methods for `TypedLogicArray` ports. Their generic `LogicType` preserves the complete array subtype, including its hardware element type, semantic value type, codec, net kind, dimensions, and unpacked-dimension configuration. This allows the module to access fields such as `samples.at([1, 2]).data` directly. The established `addInputArray`, `addOutputArray`, and `addInOutArray` APIs remain the concrete `LogicArray` helpers. +## Type-preserving operations + +`LogicArray` and `LogicArrayOf` can be used with `Mux`, `FlipFlop`, and `Passthrough`. The output retains the array's concrete type, dimensions, and specialized leaf type: + +```dart +final selected = Mux(select, samplesA, samplesB).out; +final delayed = FlipFlop(clk, selected, reset: reset).q; +final forwarded = Passthrough(delayed).out; + +final bottomRightData = forwarded.elementAt([1, 2]).data; +``` + +The mux inputs must have matching concrete array types and geometry, including dimensions, leaf widths, packed/unpacked configuration, and leaf structure. Use `typedCases`, `selectIndexTyped`, or `selectFromTyped` when selecting one complete typed array from multiple choices. Specify the array type parameter on `StructurePipeline` when its stages use inline transforms. + ## Elements of arrays Use `elements` to inspect immediate children, `arrayElements` or `indexedElements` to traverse declared array positions, and `leafElements` only when fully recursive traversal is intended. The normal `[n]` operator selects the `n`th packed bit for both `LogicArray` and `Logic`; use `at` for multidimensional typed element indexing. diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index b50ef2f62c..80f096e5d9 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -126,6 +126,61 @@ Logic cases(Logic expression, Map conditions, return result; } +/// Selects a matching [LogicStructure] value using a hardware case expression. +/// +/// Every condition value must have the same concrete type and recursive shape. +/// [defaultValue] may be another matching structure or any packed value +/// accepted by [LogicStructure.operator <]. If omitted, unmatched expression +/// values follow the same incomplete-assignment and latch semantics as [cases]. +/// [ConditionalType.unique] and [ConditionalType.priority] do not make an +/// otherwise incomplete case exhaustive. +LogicType typedCases( + Logic expression, Map conditions, + {ConditionalType conditionalType = ConditionalType.none, + dynamic defaultValue, + String name = 'result'}) { + if (conditions.isEmpty) { + throw LogicConstructionException( + 'typedCases requires at least one typed condition value.'); + } + + final prototype = conditions.values.first; + for (final value in conditions.values.skip(1)) { + validateMatchingLogicStructure(value, prototype, operation: 'typedCases'); + } + if (defaultValue is LogicStructure) { + validateMatchingLogicStructure(defaultValue, prototype, + operation: 'typedCases default'); + } else if (defaultValue is Logic && defaultValue.width != prototype.width) { + throw PortWidthMismatchException.equalWidth(defaultValue, prototype); + } + + final result = typedClone(prototype, name: name); + if (result.hasConsts || result.hasNets) { + throw LogicConstructionException( + 'typedCases result clones cannot contain constants or nets.'); + } + + Combinational([ + Case( + expression, + [ + for (final condition in conditions.entries) + CaseItem( + condition.key is Logic + ? condition.key as Logic + : Const(condition.key, width: expression.width), + [result < condition.value], + ), + ], + conditionalType: conditionalType, + defaultItem: defaultValue != null ? [result < defaultValue] : null, + ), + ]); + + return result; +} + /// A block of [CaseItem]s where only the one with a matching [CaseItem.value] /// is executed. /// diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index 1c2a310320..6e6d4b89e9 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -32,7 +32,7 @@ Logic flop( dynamic resetValue, bool asyncReset = false, }) => - FlipFlop( + FlipFlop( clk, d, en: en, @@ -41,8 +41,79 @@ Logic flop( asyncReset: asyncReset, ).q; -/// Represents a single flip-flop with no reset. -class FlipFlop extends Module with SystemVerilog { +/// A flip-flop with an output represented by [LogicType]. +/// +/// For [FlipFlop]<[Logic]>, inputs can be any [Logic] and the output is +/// normalized to ordinary driveable logic. A concrete [LogicStructure] input +/// infers the matching output type. +abstract class FlipFlop extends TypedOp { + /// Registered output. + LogicType get q; + + /// Indicates whether the reset is asynchronous. + bool get asyncReset; + + /// The constant reset value, or `null` when reset is absent or data-driven. + LogicValue? get constantResetValue; + + /// The registered output. + @override + LogicType get out => q; + + /// Creates a flip-flop with an output represented by [LogicType]. + /// + /// Concrete [LogicStructure] inputs infer their output type. Constants and + /// nets cannot be [LogicType] because they cannot represent a driveable + /// output; request `FlipFlop` for those inputs. + factory FlipFlop( + Logic clk, + LogicType d, { + Logic? en, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, + String name = 'flipflop', + }) { + if (LogicType == Logic) { + return ScalarFlipFlop( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: name, + ) as FlipFlop; + } + if (d is! LogicStructure) { + throw LogicConstructionException( + 'FlipFlop<$LogicType> requires LogicType to be Logic or a ' + 'concrete LogicStructure.'); + } + return _StructureFlipFlop( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: name, + ); + } + + FlipFlop._({ + super.name, + super.reserveName, + super.definitionName, + super.reserveDefinitionName, + }); +} + +/// Scalar implementation of [FlipFlop]. +/// +/// Extend this class when a custom scalar flip-flop implementation must retain +/// the standard flip-flop behavior. +class ScalarFlipFlop extends FlipFlop with SystemVerilog { /// Name for the enable input of this flop final String _enName = Naming.unpreferredName('en'); @@ -78,6 +149,7 @@ class FlipFlop extends Module with SystemVerilog { late final Logic _d = input(_dName); /// The output of the flop. + @override late final Logic q = output(_qName); /// The reset value for this flop, if it was a port. @@ -90,9 +162,11 @@ class FlipFlop extends Module with SystemVerilog { /// Indicates whether provided `reset` signals should be treated as an async /// reset. If no `reset` is provided, this will have no effect. + @override final bool asyncReset; /// The constant reset value, or `null` when reset is absent or data-driven. + @override LogicValue? get constantResetValue => _reset == null || _resetValuePort != null ? null : _resetValueConst; @@ -111,7 +185,7 @@ class FlipFlop extends Module with SystemVerilog { /// If [asyncReset] is true, the [reset] signal (if provided) will be treated /// as an async reset. If [asyncReset] is false, the reset signal will be /// treated as synchronous. - FlipFlop( + ScalarFlipFlop( Logic clk, Logic d, { Logic? en, @@ -119,7 +193,7 @@ class FlipFlop extends Module with SystemVerilog { dynamic resetValue, this.asyncReset = false, super.name = 'flipflop', - }) { + }) : super._() { if (clk.width != 1) { throw Exception('clk must be 1 bit'); } @@ -210,3 +284,132 @@ class FlipFlop extends Module with SystemVerilog { return svBuffer.toString(); } } + +String _structureFlipFlopResetIdentity(LogicStructure structure, bool hasReset, + bool hasResetPort, dynamic resetValue) { + if (!hasReset) { + return 'N'; + } + if (hasResetPort) { + return 'RP'; + } + final value = LogicValue.of(resetValue ?? 0, width: structure.width); + return 'RC${value.toRadixString(includeWidth: false, sepChar: '')}'; +} + +String _structureFlipFlopDefinitionName(LogicStructure structure, + {required bool hasEnable, + required bool hasReset, + required bool hasResetPort, + required dynamic resetValue, + required bool asyncReset}) => + 'StructureFlipFlop_${logicStructureShapeSignature(structure)}_' + '${hasEnable ? 'E' : 'N'}_' + '${_structureFlipFlopResetIdentity( + structure, + hasReset, + hasResetPort, + resetValue, + )}_' + '${asyncReset ? 'A' : 'S'}'; + +/// Structure-preserving implementation of [FlipFlop]. +/// +/// Each structure leaf is registered independently with common clock, enable, +/// and reset controls. A [Logic] reset value is interpreted in the same packed +/// leaf order as [LogicStructure.packed]. A structured reset value must match +/// the input structure's recursive shape. +class _StructureFlipFlop extends FlipFlop { + /// Indicates whether the reset is asynchronous. + @override + final bool asyncReset; + + /// Constant packed reset value, or `null` when reset is absent or driven by + /// a [Logic] input. + @override + final LogicValue? constantResetValue; + + @override + late final LogicType q; + + /// Creates a structure-preserving flip-flop. + _StructureFlipFlop( + Logic clk, + LogicType d, { + Logic? en, + Logic? reset, + dynamic resetValue, + this.asyncReset = false, + super.name = 'structure_flip_flop', + }) : constantResetValue = reset != null && resetValue is! Logic + ? LogicValue.of(resetValue ?? 0, width: d.width) + : null, + super._( + definitionName: _structureFlipFlopDefinitionName( + d as LogicStructure, + hasEnable: en != null, + hasReset: reset != null, + hasResetPort: reset != null && resetValue is Logic, + resetValue: resetValue, + asyncReset: asyncReset, + ), + ) { + if (clk.width != 1) { + throw PortWidthMismatchException(clk, 1); + } + if (en != null && en.width != 1) { + throw PortWidthMismatchException(en, 1); + } + if (reset != null && reset.width != 1) { + throw PortWidthMismatchException(reset, 1); + } + final structuredD = d as LogicStructure; + if (resetValue is Logic) { + if (resetValue.width != d.width) { + throw PortWidthMismatchException.equalWidth(resetValue, d); + } + if (resetValue is LogicStructure) { + validateMatchingLogicStructure(resetValue, structuredD, + operation: 'StructureFlipFlop reset'); + } + } + + LogicType cloneOutput({String name = 'q'}) { + final cloned = structuredD.clone(name: name); + if (cloned is! LogicType) { + throw LogicConstructionException( + 'FlipFlop output clone did not preserve its concrete type.'); + } + return cloned as LogicType; + } + + final localClk = addInput('clk', clk); + final localD = addTypedInput('d', d); + final localEnable = en == null ? null : addInput('en', en); + final localReset = reset == null ? null : addInput('reset', reset); + final localResetValue = reset != null && resetValue is Logic + ? addInput('resetValue', resetValue, width: d.width) + : null; + q = addTypedOutput('q', cloneOutput); + + var offset = 0; + final structuredQ = q as LogicStructure; + final structuredLocalD = localD as LogicStructure; + for (var index = 0; index < structuredQ.leafElements.length; index++) { + final width = structuredQ.leafElements[index].width; + final dynamic leafResetValue = localResetValue != null + ? localResetValue.getRange(offset, offset + width) + : constantResetValue?.getRange(offset, offset + width); + structuredQ.leafElements[index] <= + flop( + localClk, + structuredLocalD.leafElements[index], + en: localEnable, + reset: localReset, + resetValue: leafResetValue, + asyncReset: asyncReset, + ); + offset += width; + } + } +} diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index 6501554e4f..f6c44542dc 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -793,14 +793,52 @@ Logic mux(Logic control, Logic d1, Logic d0) { return control.value == LogicValue.one ? d1 : d0; } - return Mux(control, d1, d0).out; + return Mux(control, d1, d0).out; } -/// A mux (multiplexer) module. +/// A multiplexer with an output represented by [LogicType]. +/// +/// For [Mux]<[Logic]>, inputs can be any width-compatible [Logic], including +/// constants and nets, and the output is normalized to an ordinary [Logic]. +/// For a concrete [LogicStructure] type, both inputs must have that type and +/// matching recursive geometry. +abstract class Mux extends TypedOp { + /// Output selected by the control input. + @override + LogicType get out; + + /// Creates a multiplexer with an output represented by [LogicType]. + /// + /// A scalar output is selected with `Mux`. Concrete + /// [LogicStructure] outputs are inferred from ordinary structure operands. + /// Constants and nets cannot be used as [LogicType] because they cannot + /// represent a driveable mux output; request `Mux` for them. + factory Mux(Logic control, LogicType d1, LogicType d0, + {String name = 'mux'}) { + if (LogicType == Logic) { + return _LogicMux(control, d1, d0, name: name) as Mux; + } + if (d1 is! LogicStructure || d0 is! LogicStructure) { + throw LogicConstructionException( + 'Mux<$LogicType> requires LogicType to be Logic or a ' + 'concrete LogicStructure.'); + } + return _StructureMux(control, d1, d0, name: name); + } + + Mux._({ + super.name, + super.reserveName, + super.definitionName, + super.reserveDefinitionName, + }); +} + +/// Scalar implementation of [Mux]. /// /// If [_control] has value `1`, then [out] gets [_d1]. /// If [_control] has value `0`, then [out] gets [_d0]. -class Mux extends Module with InlineSystemVerilog { +class _LogicMux extends Mux with InlineSystemVerilog { /// Name for the control signal of this mux. late final String _controlName; @@ -813,21 +851,23 @@ class Mux extends Module with InlineSystemVerilog { /// Name for the output port of this mux. late final String _outName; - /// The control signal for this [Mux]. + /// The control signal for this mux. late final Logic _control = input(_controlName); - /// [Mux] input propagated when [out] is `0`. + /// Input propagated when [out] is `0`. late final Logic _d0 = input(_d0Name); - /// [Mux] input propagated when [out] is `1`. + /// Input propagated when [out] is `1`. late final Logic _d1 = input(_d1Name); /// Output port of the [Mux]. + @override late final Logic out = output(_outName); /// Constructs a multiplexer which passes [d0] or [d1] to [out] depending /// on if [control] is 0 or 1, respectively. - Mux(Logic control, Logic d1, Logic d0, {super.name = 'mux'}) { + _LogicMux(Logic control, Logic d1, Logic d0, {super.name = 'mux'}) + : super._() { if (control.width != 1) { throw PortWidthMismatchException(control, 1); } @@ -886,6 +926,59 @@ class Mux extends Module with InlineSystemVerilog { } } +String _structureMuxDefinitionName(LogicStructure structure) => + 'StructureMux_${logicStructureShapeSignature(structure)}'; + +/// Structure-preserving implementation of [Mux]. +/// +/// Both operands must have identical recursive structure and legal typed-port +/// clones. Each corresponding leaf is selected independently, preserving +/// nested [LogicArray] and [LogicArrayOf] boundaries at the output. +class _StructureMux extends Mux { + late final Logic _control; + late final LogicType _d0; + late final LogicType _d1; + + @override + late final LogicType out; + + /// Creates a structure-preserving multiplexer. + _StructureMux(Logic control, LogicType d1, LogicType d0, + {super.name = 'structure_mux'}) + : super._( + definitionName: _structureMuxDefinitionName(d0 as LogicStructure)) { + if (control.width != 1) { + throw PortWidthMismatchException(control, 1); + } + final structuredD0 = d0 as LogicStructure; + final structuredD1 = d1 as LogicStructure; + validateMatchingLogicStructure(structuredD1, structuredD0, + operation: 'Mux'); + + LogicType cloneOutput({String name = 'out'}) { + final cloned = structuredD0.clone(name: name); + if (cloned is! LogicType) { + throw LogicConstructionException( + 'Mux output clone did not preserve its concrete type.'); + } + return cloned as LogicType; + } + + _control = addInput('control', control); + _d0 = addTypedInput('d0', d0); + _d1 = addTypedInput('d1', d1); + out = addTypedOutput('out', cloneOutput); + final structuredOut = out as LogicStructure; + final structuredInput0 = _d0 as LogicStructure; + final structuredInput1 = _d1 as LogicStructure; + for (var index = 0; index < structuredOut.leafElements.length; index++) { + structuredOut.leafElements[index] <= + mux(_control, structuredInput1.leafElements[index], + structuredInput0.leafElements[index]); + } + } +} + /// A two-input bit index gate [Module]. /// /// It always takes two inputs and has one output of width 1. diff --git a/lib/src/modules/modules.dart b/lib/src/modules/modules.dart index 7a4afaca5a..69ec86a178 100644 --- a/lib/src/modules/modules.dart +++ b/lib/src/modules/modules.dart @@ -1,9 +1,11 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'bus.dart'; export 'clkgen.dart'; export 'conditionals/conditionals.dart'; export 'gates.dart'; +export 'passthrough.dart'; export 'pipeline.dart'; export 'tristate.dart'; +export 'typed_op.dart'; diff --git a/lib/src/modules/passthrough.dart b/lib/src/modules/passthrough.dart index 4a93f15941..91dc9161ac 100644 --- a/lib/src/modules/passthrough.dart +++ b/lib/src/modules/passthrough.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // passthrough.dart @@ -9,17 +9,53 @@ import 'package:rohd/rohd.dart'; -/// A very simple noop module that just passes a signal through. -class Passthrough extends Module { +/// A pass-through module with an input and output represented by [LogicType]. +abstract class Passthrough extends TypedOp { + /// Input port. + LogicType get in_; + + /// Output port. + @override + LogicType get out; + + /// Creates a pass-through with an output represented by [LogicType]. + /// + /// Concrete [LogicStructure] inputs infer their output type. Constants and + /// nets require `Passthrough` because the result is driveable logic. + factory Passthrough(LogicType input, [String name = 'passthrough']) { + if (LogicType == Logic) { + return _LogicPassthrough(input, name) as Passthrough; + } + if (input is! LogicStructure) { + throw LogicConstructionException( + 'Passthrough<$LogicType> requires LogicType to be Logic or a ' + 'concrete LogicStructure.'); + } + return _StructurePassthrough(input, name: name); + } + + Passthrough._({ + super.name, + super.reserveName, + super.definitionName, + super.reserveDefinitionName, + }); +} + +/// Scalar implementation of [Passthrough]. +class _LogicPassthrough extends Passthrough { /// The input port. + @override Logic get in_ => input('in'); /// The output port. + @override Logic get out => output('out'); /// Constructs a simple pass-through module that performs no operations /// between [a] and [out]. - Passthrough(Logic a, [String name = 'passthrough']) : super(name: name) { + _LogicPassthrough(Logic a, [String name = 'passthrough']) + : super._(name: name) { addInput('in', a, width: a.width); addOutput('out', width: a.width); _setup(); @@ -31,3 +67,44 @@ class Passthrough extends Module { out <= inner; } } + +String _structurePassthroughDefinitionName(LogicStructure structure) => + 'StructurePassthrough_${logicStructureShapeSignature(structure)}'; + +/// Structure-preserving implementation of [Passthrough]. +/// +/// The input and output retain all nested [LogicArray] and [LogicArrayOf] +/// boundaries. Typed ports reject structures containing constants or nets. +class _StructurePassthrough + extends Passthrough { + @override + late final LogicType in_; + + @override + late final LogicType out; + + /// Creates a structure-preserving pass-through module. + _StructurePassthrough(LogicType input, {super.name = 'structure_passthrough'}) + : super._( + definitionName: + _structurePassthroughDefinitionName(input as LogicStructure), + ) { + final structuredInput = input as LogicStructure; + LogicType cloneOutput({String name = 'out'}) { + final cloned = structuredInput.clone(name: name); + if (cloned is! LogicType) { + throw LogicConstructionException( + 'Passthrough output clone did not preserve its concrete type.'); + } + return cloned as LogicType; + } + + in_ = addTypedInput('in', input); + out = addTypedOutput('out', cloneOutput); + final structuredOut = out as LogicStructure; + final structuredIn = in_ as LogicStructure; + for (var index = 0; index < structuredOut.leafElements.length; index++) { + structuredOut.leafElements[index] <= structuredIn.leafElements[index]; + } + } +} diff --git a/lib/src/modules/pipeline.dart b/lib/src/modules/pipeline.dart index 97cda2dbfe..099f7ccdcf 100644 --- a/lib/src/modules/pipeline.dart +++ b/lib/src/modules/pipeline.dart @@ -10,6 +10,107 @@ import 'package:rohd/rohd.dart'; +/// Builds the combinational value registered by one [StructurePipeline] stage. +typedef StructurePipelineTransform = LogicType + Function(StructurePipelineStageInfo stage); + +/// Typed values and history visible while building one structure pipeline +/// stage. +class StructurePipelineStageInfo { + /// Zero-based transform stage being constructed. + final int stage; + + final List _values; + + StructurePipelineStageInfo._(this.stage, this._values); + + /// Typed value entering this transform stage. + LogicType get value => _values[stage]; + + /// Returns a typed value relative to this transform stage's input. + /// + /// Zero selects [value], while negative values select earlier registered + /// stages. Future stages cannot be referenced during construction. + LogicType get([int stageAdjustment = 0]) => getAbs(stage + stageAdjustment); + + /// Returns a typed value at an absolute pipeline boundary. + /// + /// Boundary zero is the original input. Boundary `n + 1` is the registered + /// output of transform stage `n`. + LogicType getAbs(int boundary) { + if (boundary < 0 || boundary >= _values.length) { + throw RangeError.range(boundary, 0, _values.length - 1, 'boundary'); + } + return _values[boundary]; + } +} + +/// A single-payload pipeline that preserves a concrete structure type. +/// +/// Every transform receives a [StructurePipelineStageInfo] whose `value` is the +/// typed value entering that stage. The returned matching structure is +/// registered through [FlipFlop]. [values] contains the original +/// input followed by every registered stage output, so `stageCount` matches +/// [Pipeline]'s boundary-count convention and `latency` is one less. +class StructurePipeline { + /// Original input followed by each registered transform result. + late final List values; + + /// Number of combinational boundaries, including the input boundary. + int get stageCount => values.length; + + /// Number of registered transform stages. + int get latency => values.length - 1; + + /// Final typed pipeline value. + LogicType get output => values.last; + + /// Creates a typed structure pipeline. + /// + /// Each optional [stalls] entry is active-high. A high value prevents the + /// corresponding registered stage boundary from advancing. + StructurePipeline( + Logic clk, + LogicType input, { + required List> stages, + List? stalls, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, + String name = 'structure_pipeline', + }) { + if (stalls != null && stalls.length != stages.length) { + throw ArgumentError.value(stalls, 'stalls', + 'Must contain one optional stall signal per transform stage.'); + } + + final generatedValues = [input]; + for (var stage = 0; stage < stages.length; stage++) { + final transformed = stages[stage]( + StructurePipelineStageInfo._(stage, generatedValues)); + validateMatchingLogicStructure(transformed, input, + operation: 'StructurePipeline stage $stage'); + final stall = stalls?[stage]; + if (stall != null && stall.width != 1) { + throw PortWidthMismatchException(stall, 1); + } + generatedValues.add(FlipFlop( + clk, + transformed, + en: stall == null ? null : ~stall, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: '${name}_stage_$stage', + ).q); + } + values = List.unmodifiable(generatedValues); + } + + /// Returns a typed value at [boundary], defaulting to [output]. + LogicType get([int? boundary]) => values[boundary ?? values.length - 1]; +} + /// Information and accessors associated with a [Pipeline] stage. class PipelineStageInfo { /// The index of the current stage in the associated [Pipeline]. diff --git a/lib/src/modules/typed_op.dart b/lib/src/modules/typed_op.dart new file mode 100644 index 0000000000..92fd1a8552 --- /dev/null +++ b/lib/src/modules/typed_op.dart @@ -0,0 +1,24 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_op.dart +// Common contract for hardware operations with typed outputs. +// +// 2026 September 2 +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// A hardware operation whose output preserves the requested [Logic] type. +abstract class TypedOp extends Module { + /// The output produced by this operation. + LogicType get out; + + /// Creates a typed operation. + TypedOp({ + super.name = 'typed_op', + super.reserveName, + super.definitionName, + super.reserveDefinitionName, + }); +} diff --git a/lib/src/selection.dart b/lib/src/selection.dart index 2d46499682..46806f20a3 100644 --- a/lib/src/selection.dart +++ b/lib/src/selection.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // selection.dart @@ -30,3 +30,38 @@ extension IndexedLogic on List { Logic selectIndex(Logic index, {Logic? defaultValue}) => index.selectFrom(this, defaultValue: defaultValue); } + +/// Allows lists of matching structures to retain their type when selected. +extension TypedIndexedLogic + on List { + /// Selects one structure using [index], preserving its concrete type. + /// + /// An out-of-range index produces zero unless [defaultValue] is supplied. + LogicType selectIndexTyped(Logic index, + {LogicType? defaultValue, String name = 'selectFrom'}) { + if (isEmpty) { + throw LogicConstructionException( + 'selectIndexTyped requires at least one value.'); + } + return typedCases( + index, + { + for (var valueIndex = 0; valueIndex < length; valueIndex++) + valueIndex: this[valueIndex] + }, + conditionalType: ConditionalType.unique, + defaultValue: defaultValue ?? 0, + name: name, + ); + } +} + +/// Allows a hardware index to select from a typed structure list. +extension TypedSelectionLogic on Logic { + /// Selects one value from [values], preserving its concrete structure type. + LogicType selectFromTyped( + List values, + {LogicType? defaultValue, + String name = 'selectFrom'}) => + values.selectIndexTyped(this, defaultValue: defaultValue, name: name); +} diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index 1196799c7c..110ae6dc94 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -9,6 +9,108 @@ part of 'signals.dart'; +String _logicShapeSignature(Logic logic) { + if (logic is BaseLogicArray) { + final signatures = + logic.arrayElements.map(_logicShapeSignature).toList(growable: false); + final elementSignature = signatures.isEmpty + ? 'E' + : signatures.every((signature) => signature == signatures.first) + ? 'R${signatures.length}_${signatures.first}' + : 'H${signatures.join('_')}'; + return 'A${logic.dimensions.join('x')}_W${logic.elementWidth}_' + 'U${logic.numUnpackedDimensions}_${logic.isNet ? 'N' : 'L'}_' + '$elementSignature'; + } + if (logic is LogicStructure) { + return 'S${logic.elements.length}_' + '${logic.elements.map(_logicShapeSignature).join('_')}'; + } + return '${logic is Const ? 'C' : logic.isNet ? 'N' : 'L'}${logic.width}'; +} + +/// Returns a deterministic signature of [structure]'s recursive geometry. +/// +/// The signature includes field counts and widths plus array dimensions, +/// packing hints, leaf geometry, and net or constant distinctions. Signal +/// instance names are intentionally excluded. It can be embedded in a module +/// definition name when generated hardware depends on structure shape. +String logicStructureShapeSignature(LogicStructure structure) => + _logicShapeSignature(structure); + +void _validateMatchingStructure( + Logic first, Logic second, String operation, String path) { + if (first.width != second.width) { + throw LogicConstructionException( + '$operation operands differ in width at $path.'); + } + + if (first is LogicStructure || second is LogicStructure) { + if (first is! LogicStructure || + second is! LogicStructure || + first.runtimeType != second.runtimeType || + first.elements.length != second.elements.length) { + throw LogicConstructionException( + '$operation operands differ in structure at $path.'); + } + + if (first is BaseLogicArray && second is BaseLogicArray) { + if (!_sameDimensions(first.dimensions, second.dimensions) || + first.elementWidth != second.elementWidth || + first.numUnpackedDimensions != second.numUnpackedDimensions || + first.isNet != second.isNet) { + throw LogicConstructionException( + '$operation array operands differ in shape at $path.'); + } + } + + for (var index = 0; index < first.elements.length; index++) { + _validateMatchingStructure(first.elements[index], second.elements[index], + operation, '$path[$index]'); + } + } +} + +/// Verifies that [first] and [second] have identical recursive structure. +/// +/// Compatibility requires matching concrete structure types, element counts, +/// leaf widths, and array geometry. Field names may differ because separately +/// constructed instances of one structure type can use different root names. +/// +/// Throws a [LogicConstructionException] when the structures do not match. +void validateMatchingLogicStructure(LogicStructure first, LogicStructure second, + {String operation = 'Typed operation'}) => + _validateMatchingStructure(first, second, operation, 'root'); + +/// Clones [source] while preserving its concrete [LogicStructure] type. +/// +/// Throws a [LogicConstructionException] when [LogicStructure.clone] is not +/// implemented covariantly by the concrete type. +LogicType typedClone(LogicType source, + {String? name}) { + final cloned = source.clone(name: name); + if (cloned is! LogicType) { + throw LogicConstructionException( + 'Structure clone did not preserve its concrete type.'); + } + return cloned; +} + +/// Creates a named, connected alias while preserving [source]'s concrete type. +LogicType typedNamed( + LogicType source, String name) => + typedClone(source, name: name)..gets(source); + +/// Type-preserving conveniences for concrete [LogicStructure] values. +extension TypedLogicStructureUtilities + on LogicType { + /// Equivalent to [LogicStructure.clone], with a concrete return type. + LogicType cloneTyped({String? name}) => typedClone(this, name: name); + + /// Equivalent to [LogicStructure.named], with a concrete return type. + LogicType namedTyped(String name) => typedNamed(this, name); +} + /// Collects a group of [Logic] signals into one entity which can be manipulated /// in a similar way as an individual [Logic]. class LogicStructure implements Logic { diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart index eb3a16742a..1ad56a7f9d 100644 --- a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -391,6 +391,13 @@ class NetlistCellMapper { parameters: {'WIDTH': ctx.width(d0)}, ); }) + // ── Passthrough ──────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Passthrough) { + return null; + } + return unaryAY(ctx, r'$buf'); + }) // ── Add ─────────────────────────────────────────────────────────── ..register((ctx) { if (ctx.module is! Add) { diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart index 506df78655..a338524ef2 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -58,8 +58,8 @@ class NetlistSynthesizerConfiguration { /// Determines which modules should stop netlist hierarchy traversal and be /// emitted as cells in their parent. /// - /// Defaults to matching [FlipFlop] and its subclasses, which contain internal - /// sequential submodules but should be emitted as `$dff` netlist cells. + /// Defaults to matching [Mux], [FlipFlop], and [Passthrough] and their + /// subclasses, which should be emitted as aggregate primitive netlist cells. final SynthModuleLeafPredicate leafModulePredicate; /// The netlist-internal mapper used to convert selected leaf modules to @@ -113,7 +113,7 @@ class NetlistSynthesizerConfiguration { /// Creates a configuration for netlist synthesis. const NetlistSynthesizerConfiguration({ this.moduleStopPolicy, - this.leafModulePredicate = _isFlipFlop, + this.leafModulePredicate = _isNetlistPrimitive, this.netlistCellMapper, @visibleForTesting this.collapseTransparentClusters = false, @visibleForTesting this.enableDeadCellElimination = true, @@ -129,4 +129,5 @@ class NetlistSynthesizerConfiguration { trace ? (packageRoot ?? Directory.current.path) : null; } -bool _isFlipFlop(Module module) => module is FlipFlop; +bool _isNetlistPrimitive(Module module) => + module is Mux || module is FlipFlop || module is Passthrough; diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart index 9447f27642..e62aa5f099 100644 --- a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -45,7 +45,7 @@ class SynthModuleStopPolicy { /// Creates the default netlist stopping policy. factory SynthModuleStopPolicy.netlist({ - SynthModuleLeafPredicate leafModulePredicate = _isFlipFlop, + SynthModuleLeafPredicate leafModulePredicate = _isNetlistPrimitive, }) => SynthModuleStopPolicy( generatesDefinitionByDefault: (module) => module.subModules.isNotEmpty, @@ -62,4 +62,5 @@ class SynthModuleStopPolicy { bool generatesDefinition(Module module) => !isLeaf(module); } -bool _isFlipFlop(Module module) => module is FlipFlop; +bool _isNetlistPrimitive(Module module) => + module is Mux || module is FlipFlop || module is Passthrough; diff --git a/test/long_chain_test.dart b/test/long_chain_test.dart index d22352b0c6..8a78ad7bb0 100644 --- a/test/long_chain_test.dart +++ b/test/long_chain_test.dart @@ -8,7 +8,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; diff --git a/test/multimodule3_test.dart b/test/multimodule3_test.dart index 76fde0b1dc..20a2703899 100644 --- a/test/multimodule3_test.dart +++ b/test/multimodule3_test.dart @@ -9,7 +9,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 5db4ec513d..12b30d1a3c 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -9,7 +9,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:test/test.dart'; // mostly all inputs diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index a8813276f1..f9db9ebf96 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -9,7 +9,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:test/test.dart'; class TopModule extends Module { diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index dd4da394c5..bbd5061e08 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -102,8 +102,8 @@ class FlopModule extends Module { } } -/// A custom [FlipFlop] used to verify inheritance-aware leaf matching. -class CustomFlipFlop extends FlipFlop { +/// A custom [ScalarFlipFlop] used to verify inheritance-aware leaf matching. +class CustomFlipFlop extends ScalarFlipFlop { CustomFlipFlop(super.clk, super.d); } @@ -452,6 +452,26 @@ class StructOutputProducerModule extends Module { } } +/// Applies structure-preserving primitive operations to an aggregate port. +class TypedStructOperationModule extends Module { + TypedStructOperationModule( + Logic clk, + Logic control, + NetlistPairStruct first, + NetlistPairStruct second, + ) : super(name: 'typedstructoperation') { + clk = addInput('clk', clk); + control = addInput('control', control); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux(control, second, first).out; + final forwarded = Passthrough(selected).out; + final registered = FlipFlop(clk, forwarded); + addTypedOutput('out', first.clone) <= registered.q; + } +} + /// Instantiates identical structure-packing children at distinct parent paths. class StructOutputProducerDedupTop extends Module { StructOutputProducerDedupTop() : super(name: 'structoutputproducerdeduptop') { @@ -971,6 +991,45 @@ void main() { expect(_hasCellType(json, r'$dff'), isTrue); }); + test('typed structure operations map to aggregate primitive cells', + () async { + final module = TypedStructOperationModule( + SimpleClockGenerator(10).clk, + Logic(), + NetlistPairStruct(name: 'first'), + NetlistPairStruct(name: 'second'), + ); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef).values.cast>(); + + for (final cellType in [r'$mux', r'$buf', r'$dff']) { + final matchingCells = + cells.where((candidate) => candidate['type'] == cellType).toList(); + expect( + matchingCells, + hasLength(1), + reason: cells.map((candidate) => candidate['type']).join(', '), + ); + final cell = matchingCells.single; + final parameters = cell['parameters'] as Map; + final connections = cell['connections'] as Map; + if (cellType == r'$buf') { + expect(parameters['A_WIDTH'], 8); + expect(parameters['Y_WIDTH'], 8); + expect(connections['A'] as List, hasLength(8)); + expect(connections['Y'] as List, hasLength(8)); + } else { + expect(parameters['WIDTH'], 8); + final dataPorts = cellType == r'$mux' ? ['A', 'B', 'Y'] : ['D', 'Q']; + for (final port in dataPorts) { + expect(connections[port] as List, hasLength(8)); + } + } + } + }); + test('FlipFlop controls map to standard Yosys register cells', () async { final clk = SimpleClockGenerator(10).clk; final d = Logic(width: 4); diff --git a/test/shuffle_test.dart b/test/shuffle_test.dart index 8ab355decb..3b77e6a18f 100644 --- a/test/shuffle_test.dart +++ b/test/shuffle_test.dart @@ -8,7 +8,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart new file mode 100644 index 0000000000..16c17e01aa --- /dev/null +++ b/test/typed_operations_test.dart @@ -0,0 +1,599 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_operations_test.dart +// Tests operations that preserve concrete LogicStructure types. +// +// 2026 September 2 +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +class _TypedLane extends LogicStructure { + Logic get data => elements[0]; + Logic get enable => elements[1]; + + _TypedLane({String? name}) + : super([ + Logic(name: 'data', width: 8), + Logic(name: 'enable'), + ], name: name ?? 'typed_lane'); + + @override + _TypedLane clone({String? name}) => _TypedLane(name: name ?? this.name); +} + +class _TypedPacket extends LogicStructure { + Logic get opcode => elements[0]; + LogicArrayOf<_TypedLane> get lanes => elements[1] as LogicArrayOf<_TypedLane>; + + final List laneDimensions; + + _TypedPacket({this.laneDimensions = const [2], String? name}) + : super([ + Logic(name: 'opcode', width: 3), + LogicArrayOf<_TypedLane>(laneDimensions, _TypedLane.new, + name: 'lanes'), + ], name: name ?? 'typed_packet'); + + @override + _TypedPacket clone({String? name}) => + _TypedPacket(laneDimensions: laneDimensions, name: name ?? this.name); +} + +class _BadClonePacket extends LogicStructure { + _BadClonePacket({String? name}) + : super([Logic(name: 'data')], name: name ?? 'bad_clone_packet'); + + @override + LogicStructure clone({String? name}) => + LogicStructure([Logic(name: 'data')], name: name ?? this.name); +} + +class _ConstPacket extends LogicStructure { + _ConstPacket({String? name}) + : super([Const(1)], name: name ?? 'const_packet'); + + @override + _ConstPacket clone({String? name}) => _ConstPacket(name: name ?? this.name); +} + +class _NetPacket extends LogicStructure { + _NetPacket({String? name}) : super([LogicNet()], name: name ?? 'net_packet'); + + @override + _NetPacket clone({String? name}) => _NetPacket(name: name ?? this.name); +} + +class _TypedOperationsSynthesisHarness extends Module { + _TypedOperationsSynthesisHarness(Logic clk, Logic reset, Logic control, + Logic selector, _TypedPacket first, _TypedPacket second) { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + control = addInput('control', control); + selector = addInput('selector', selector, width: selector.width); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux(control, second, first).out; + final selectedByCase = typedCases( + selector, + {0: first, 1: second}, + defaultValue: selected, + name: 'selected_by_case', + ); + final selectedByIndex = [first, second, selectedByCase].selectIndexTyped( + selector, + defaultValue: first, + name: 'selected_by_index', + ); + final passed = Passthrough(selectedByIndex).out; + final registered = FlipFlop( + clk, + passed, + reset: reset, + resetValue: 0, + ).q; + final piped = StructurePipeline<_TypedPacket>( + clk, + registered, + reset: reset, + resetValue: 0, + stages: [ + (stage) => stage.value.namedTyped('pipeline_stage_value'), + ], + ).output; + addTypedOutput('out', piped.clone).gets(piped); + } +} + +void _expectLogicArray(LogicArray array) { + expect(array, isA()); +} + +void _expectTypedLaneArray(LogicArrayOf<_TypedLane> array) { + expect(array, isA>()); +} + +void main() { + tearDown(Simulator.reset); + + test('Mux implements the scalar TypedOp contract', () { + final muxModule = Mux(Logic(), Logic(width: 4), Logic(width: 4)); + expect(muxModule, isA>()); + expect(muxModule.out.runtimeType, Logic); + }); + + test('scalar typed operations normalize constants, ports, and nets', + () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final ordinary = Logic(width: 4); + final constant = Const(0xa, width: 4); + final portPrototype = Logic.port('port_data', 4); + final netDriver = Logic(width: 4); + final net = LogicNet(width: 4)..gets(netDriver); + final constantMux = Mux(control, constant, ordinary); + final portMux = Mux(control, portPrototype, ordinary); + final netMux = Mux(control, net, ordinary); + final constantFlop = FlipFlop(clk, constant); + final portFlop = FlipFlop(clk, portPrototype); + final netFlop = FlipFlop(clk, net); + await Future.wait([ + constantMux.build(), + portMux.build(), + netMux.build(), + constantFlop.build(), + portFlop.build(), + netFlop.build(), + ]); + unawaited(Simulator.run()); + + for (final output in [ + constantMux.out, + portMux.out, + netMux.out, + constantFlop.q, + portFlop.q, + netFlop.q, + ]) { + expect(output.runtimeType, Logic); + expect(output.isNet, isFalse); + } + + ordinary.inject(3); + portPrototype.inject(5); + netDriver.inject(7); + control.inject(1); + await Simulator.tick(); + expect(constantMux.out.value.toInt(), 0xa); + expect(portMux.out.value.toInt(), 5); + expect(netMux.out.value.toInt(), 7); + await clk.nextPosedge; + expect(constantFlop.q.value.toInt(), 0xa); + expect(portFlop.q.value.toInt(), 5); + expect(netFlop.q.value.toInt(), 7); + await Simulator.endSimulation(); + }); + + test('typed operations infer top-level array types', () { + final clk = Logic(); + final selector = Logic(width: 2); + final array0 = LogicArray([2], 4, name: 'array0'); + final array1 = LogicArray([2], 4, name: 'array1'); + final typedArray0 = + LogicArrayOf<_TypedLane>([2], _TypedLane.new, name: 'typed_array0'); + final typedArray1 = + LogicArrayOf<_TypedLane>([2], _TypedLane.new, name: 'typed_array1'); + + final muxedArray = Mux(selector[0], array1, array0).out; + final floppedArray = FlipFlop(clk, array0).q; + final passedArray = Passthrough(array0).out; + final casedArray = typedCases(selector, {0: array0, 1: array1}); + final selectedArray = [array0, array1].selectIndexTyped(selector); + final selectedFromArray = selector.selectFromTyped([array0, array1]); + final clonedArray = array0.cloneTyped(); + final namedArray = array0.namedTyped('named_array'); + final pipelinedArray = StructurePipeline( + clk, + array0, + stages: [(stage) => stage.value], + ).output; + + final muxedTypedArray = Mux(selector[0], typedArray1, typedArray0).out; + final floppedTypedArray = FlipFlop(clk, typedArray0).q; + final passedTypedArray = Passthrough(typedArray0).out; + final casedTypedArray = + typedCases(selector, {0: typedArray0, 1: typedArray1}); + final selectedTypedArray = + [typedArray0, typedArray1].selectIndexTyped(selector); + final selectedFromTypedArray = + selector.selectFromTyped([typedArray0, typedArray1]); + final clonedTypedArray = typedArray0.cloneTyped(); + final namedTypedArray = typedArray0.namedTyped('named_typed_array'); + final pipelinedTypedArray = StructurePipeline>( + clk, + typedArray0, + stages: [(stage) => stage.value], + ).output; + + [ + muxedArray, + floppedArray, + passedArray, + casedArray, + selectedArray, + selectedFromArray, + clonedArray, + namedArray, + pipelinedArray, + ].forEach(_expectLogicArray); + [ + muxedTypedArray, + floppedTypedArray, + passedTypedArray, + casedTypedArray, + selectedTypedArray, + selectedFromTypedArray, + clonedTypedArray, + namedTypedArray, + pipelinedTypedArray, + ].forEach(_expectTypedLaneArray); + }); + + test('Mux infers nested LogicArrayOf types', () async { + final control = Logic(); + final d1 = _TypedPacket(name: 'd1'); + final d0 = _TypedPacket(name: 'd0'); + final muxModule = Mux(control, d1, d0); + await muxModule.build(); + + expect(muxModule, isA>()); + expect(muxModule.out, isA<_TypedPacket>()); + expect(muxModule.out.lanes, isA>()); + expect( + muxModule.out.lanes.typedLeafElements, everyElement(isA<_TypedLane>())); + + d0.opcode.put(1); + d0.lanes.typedLeafElements[0].data.put(0x10); + d0.lanes.typedLeafElements[1].data.put(0x20); + d1.opcode.put(6); + d1.lanes.typedLeafElements[0].data.put(0xa0); + d1.lanes.typedLeafElements[1].data.put(0xb0); + + control.put(0); + expect(muxModule.out.opcode.value.toInt(), 1); + expect(muxModule.out.lanes.typedLeafElements[0].data.value.toInt(), 0x10); + expect(muxModule.out.lanes.typedLeafElements[1].data.value.toInt(), 0x20); + + control.put(1); + expect(muxModule.out.opcode.value.toInt(), 6); + expect(muxModule.out.lanes.typedLeafElements[0].data.value.toInt(), 0xa0); + expect(muxModule.out.lanes.typedLeafElements[1].data.value.toInt(), 0xb0); + }); + + test('Mux preserves structure type with a constant control', () { + final d1 = _TypedPacket(name: 'd1'); + final d0 = _TypedPacket(name: 'd0'); + expect(Mux(Const(0), d1, d0).out, isA<_TypedPacket>()); + expect(Mux(Const(1), d1, d0).out, isA<_TypedPacket>()); + }); + + test('Mux rejects differently shaped typed arrays', () { + expect( + () => Mux(Logic(), _TypedPacket(laneDimensions: const [2, 2]), + _TypedPacket(laneDimensions: const [4])), + throwsA(isA())); + }); + + test('typed operations reject clones, constants, nets, and reset mismatches', + () { + expect(() => typedClone(_BadClonePacket()), + throwsA(isA())); + expect(() => Mux<_ConstPacket>(Logic(), _ConstPacket(), _ConstPacket()), + throwsA(isA())); + expect(() => Passthrough<_NetPacket>(_NetPacket()), + throwsA(isA())); + expect( + () => FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + reset: Logic(), + resetValue: _TypedPacket(laneDimensions: const [1, 2])), + throwsA(isA())); + expect( + () => typedCases(Logic(), { + 0: _TypedPacket(), + 1: _TypedPacket(laneDimensions: const [1, 2]) + }), + throwsA(isA())); + expect(() => <_TypedPacket>[].selectIndexTyped(Logic()), + throwsA(isA())); + }); + + test('typed module definitions encode structure and constant reset identity', + () { + final sameShapeFirst = Mux<_TypedPacket>(Logic(), + _TypedPacket(name: 'first_d1'), _TypedPacket(name: 'first_d0')); + final sameShapeSecond = Mux<_TypedPacket>(Logic(), + _TypedPacket(name: 'second_d1'), _TypedPacket(name: 'second_d0')); + final differentShape = Mux<_TypedPacket>( + Logic(), + _TypedPacket(laneDimensions: const [1, 2], name: 'different_d1'), + _TypedPacket(laneDimensions: const [1, 2], name: 'different_d0')); + final resetZero = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + reset: Logic(), resetValue: 0); + final resetOne = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + reset: Logic(), resetValue: 1); + + expect(sameShapeFirst.definitionName, sameShapeSecond.definitionName); + expect(sameShapeFirst.definitionName, isNot(differentShape.definitionName)); + expect(resetZero.definitionName, isNot(resetOne.definitionName)); + }); + + test('FlipFlop implements the scalar TypedOp contract', () { + final flipFlop = FlipFlop(Logic(), Logic(width: 4)); + expect(flipFlop, isA>()); + expect(flipFlop.q.runtimeType, Logic); + }); + + test('FlipFlop infers type, reset, and enable behavior', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final enable = Logic(); + final d = _TypedPacket(name: 'd'); + final flipFlop = FlipFlop( + clk, + d, + en: enable, + reset: reset, + resetValue: 0, + ); + await flipFlop.build(); + unawaited(Simulator.run()); + + expect(flipFlop, isA>()); + expect(flipFlop.q, isA<_TypedPacket>()); + expect(flipFlop.q.lanes, isA>()); + + reset.inject(1); + enable.inject(0); + await clk.nextPosedge; + reset.inject(0); + expect(flipFlop.q.value, LogicValue.ofInt(0, flipFlop.q.width)); + + d.opcode.inject(5); + d.lanes.typedLeafElements[0].data.inject(0x12); + d.lanes.typedLeafElements[0].enable.inject(1); + d.lanes.typedLeafElements[1].data.inject(0x34); + d.lanes.typedLeafElements[1].enable.inject(0); + enable.inject(1); + await clk.nextPosedge; + expect(flipFlop.q.opcode.value.toInt(), 5); + expect(flipFlop.q.lanes.typedLeafElements[0].data.value.toInt(), 0x12); + expect(flipFlop.q.lanes.typedLeafElements[1].data.value.toInt(), 0x34); + + d.opcode.inject(2); + d.lanes.typedLeafElements[0].data.inject(0xaa); + enable.inject(0); + await clk.nextPosedge; + expect(flipFlop.q.opcode.value.toInt(), 5); + expect(flipFlop.q.lanes.typedLeafElements[0].data.value.toInt(), 0x12); + await Simulator.endSimulation(); + }); + + test('FlipFlop maps packed reset bits to nested fields', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final d = _TypedPacket(name: 'reset_order_d'); + final resetValue = LogicValue.ofString('000111100110100101101'); + final flipFlop = FlipFlop( + clk, + d, + reset: reset, + resetValue: resetValue, + ); + await flipFlop.build(); + unawaited(Simulator.run()); + + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + expect(flipFlop.q.opcode.value.toInt(), 0x5); + expect(flipFlop.q.lanes.typedLeafElements[0].data.value.toInt(), 0xa5); + expect(flipFlop.q.lanes.typedLeafElements[0].enable.value, LogicValue.one); + expect(flipFlop.q.lanes.typedLeafElements[1].data.value.toInt(), 0x3c); + expect(flipFlop.q.lanes.typedLeafElements[1].enable.value, LogicValue.zero); + await Simulator.endSimulation(); + }); + + test('typed clone and naming helpers retain concrete field access', () { + final source = _TypedPacket(name: 'source'); + final clone = source.cloneTyped(name: 'clone'); + final named = source.namedTyped('named'); + + expect(clone, isA<_TypedPacket>()); + expect(clone.name, 'clone'); + expect(named, isA<_TypedPacket>()); + expect(named.name, 'named'); + for (var index = 0; index < named.leafElements.length; index++) { + expect(named.leafElements[index].srcConnections, + contains(source.leafElements[index])); + } + }); + + test('Passthrough infers typed contracts', () async { + final scalar = Passthrough(Logic(width: 4)); + final input = _TypedPacket(name: 'passthrough_input'); + final structured = Passthrough(input); + await Future.wait([scalar.build(), structured.build()]); + + expect(scalar, isA>()); + expect(scalar.out.runtimeType, Logic); + expect(structured, isA>()); + expect(structured.in_, isA<_TypedPacket>()); + expect(structured.out, isA<_TypedPacket>()); + expect(structured.out.lanes, isA>()); + + input.opcode.put(5); + input.lanes.typedLeafElements[0].data.put(0x12); + input.lanes.typedLeafElements[1].data.put(0x34); + expect(structured.out.opcode.value.toInt(), 5); + expect(structured.out.lanes.typedLeafElements[0].data.value.toInt(), 0x12); + expect(structured.out.lanes.typedLeafElements[1].data.value.toInt(), 0x34); + }); + + test('typedCases preserves structure and selects a default', () { + final selector = Logic(width: 2); + final first = _TypedPacket(name: 'first'); + final second = _TypedPacket(name: 'second'); + final fallback = _TypedPacket(name: 'fallback'); + final selected = typedCases(selector, {0: first, 2: second}, + defaultValue: fallback, name: 'selected'); + + expect(selected, isA<_TypedPacket>()); + first.opcode.put(1); + second.opcode.put(2); + fallback.opcode.put(7); + selector.put(0); + expect(selected.opcode.value.toInt(), 1); + selector.put(2); + expect(selected.opcode.value.toInt(), 2); + selector.put(3); + expect(selected.opcode.value.toInt(), 7); + }); + + test('typed indexed selection works in both invocation directions', () { + final index = Logic(width: 2); + final values = [ + _TypedPacket(name: 'value0'), + _TypedPacket(name: 'value1'), + _TypedPacket(name: 'value2'), + ]; + final selectedByList = + values.selectIndexTyped(index, name: 'selected_by_list'); + final selectedByIndex = + index.selectFromTyped(values, name: 'selected_by_index'); + + for (var valueIndex = 0; valueIndex < values.length; valueIndex++) { + values[valueIndex].opcode.put(valueIndex + 3); + } + index.put(1); + expect(selectedByList.opcode.value.toInt(), 4); + expect(selectedByIndex.opcode.value.toInt(), 4); + index.put(3); + expect(selectedByList.value, LogicValue.ofInt(0, selectedByList.width)); + expect(selectedByIndex.value, LogicValue.ofInt(0, selectedByIndex.width)); + }); + + test('StructurePipeline preserves typed stages and history', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final stall = Logic(); + final input = _TypedPacket(name: 'pipeline_input'); + final pipeline = StructurePipeline<_TypedPacket>( + clk, + input, + reset: reset, + resetValue: 0, + stalls: [null, stall], + stages: [ + (stage) { + final next = stage.value.cloneTyped(name: 'stage_0_next'); + next.opcode <= stage.value.opcode + 1; + for (var lane = 0; + lane < next.lanes.typedLeafElements.length; + lane++) { + next.lanes.typedLeafElements[lane].data <= + stage.value.lanes.typedLeafElements[lane].data; + next.lanes.typedLeafElements[lane].enable <= + stage.value.lanes.typedLeafElements[lane].enable; + } + return next; + }, + (stage) { + final next = stage.value.cloneTyped(name: 'stage_1_next'); + next.opcode <= stage.value.opcode + stage.get(-1).opcode; + for (var lane = 0; + lane < next.lanes.typedLeafElements.length; + lane++) { + next.lanes.typedLeafElements[lane].data <= + stage.value.lanes.typedLeafElements[lane].data; + next.lanes.typedLeafElements[lane].enable <= + stage.value.lanes.typedLeafElements[lane].enable; + } + return next; + }, + ], + ); + unawaited(Simulator.run()); + + expect(pipeline.stageCount, 3); + expect(pipeline.latency, 2); + expect(pipeline.output, isA<_TypedPacket>()); + expect(pipeline.get(0), same(input)); + expect(pipeline.output.lanes, isA>()); + expect(() => pipeline.values.add(input), throwsUnsupportedError); + + reset.inject(1); + stall.inject(0); + await clk.nextPosedge; + reset.inject(0); + + input.opcode.inject(2); + input.lanes.typedLeafElements[0].data.inject(0x45); + await clk.nextPosedge; + input.opcode.inject(4); + input.lanes.typedLeafElements[0].data.inject(0x67); + await clk.nextPosedge; + expect(pipeline.output.opcode.value.toInt(), 7); + expect(pipeline.output.lanes.typedLeafElements[0].data.value.toInt(), 0x45); + + stall.inject(1); + input.opcode.inject(1); + await clk.nextPosedge; + expect(pipeline.output.opcode.value.toInt(), 7); + await Simulator.endSimulation(); + }); + + test('StructurePipeline treats constant stalls as active-high', () async { + final clk = SimpleClockGenerator(10).clk; + final input = _TypedPacket(name: 'constant_stall_input'); + final running = StructurePipeline<_TypedPacket>( + clk, + input, + stalls: [Const(0)], + stages: [(stage) => stage.value.namedTyped('running_stage')], + ); + final stalled = StructurePipeline<_TypedPacket>( + clk, + input, + stalls: [Const(1)], + stages: [(stage) => stage.value.namedTyped('stalled_stage')], + ); + unawaited(Simulator.run()); + + input.opcode.inject(6); + await clk.nextPosedge; + expect(running.output.opcode.value.toInt(), 6); + expect(stalled.output.opcode.value.isValid, isFalse); + await Simulator.endSimulation(); + }); + + test('all structure-preserving operations compile to SystemVerilog', + () async { + final harness = _TypedOperationsSynthesisHarness( + Logic(), + Logic(), + Logic(), + Logic(width: 2), + _TypedPacket(name: 'first'), + _TypedPacket(name: 'second'), + ); + await harness.build(); + + SimCompare.checkIverilogVector(harness, const [], buildOnly: true); + }); +} From 421ad2ddc0fc17204d080d6c94f7cf32f56c427e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 2 Sep 2026 16:18:37 -0700 Subject: [PATCH 02/33] add support for Const LogicStructure and fix tests --- doc/user_guide/_docs/A19-logic-structures.md | 4 +- test/async_reset_test.dart | 4 +- test/typed_operations_test.dart | 59 +++++++++++++++++--- test/typed_port_test.dart | 22 ++++++++ 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index cab0bd28d8..cff0a3b401 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -30,7 +30,9 @@ final forwarded = Passthrough(registered).out; forwarded.valid <= selected.valid; ``` -The same behavior applies to `LogicArray` and `LogicArrayOf`, including nested typed arrays. Both mux operands must have the same concrete type and recursive shape: field widths, array dimensions, packing hints, and net/constant layout must match. +The same behavior applies to `LogicArray` and `LogicArrayOf`, including nested typed arrays. Both mux operands must have the same concrete type and recursive shape: field widths, array dimensions, and packing hints must match. + +Typed operations can consume a structure containing `Const` leaves when its `clone()` implementation returns the same concrete structure type with driveable `Logic` leaves. The operation preserves the structure type while normalizing its input port and output to driveable logic. This supports domain-specific constant structures, such as a floating-point structure assembled from constant sign, exponent, and mantissa fields. For ordinary scalar operations, infer or explicitly request `Logic`. When a source is a `Const` or `LogicNet`, request `Logic` explicitly because the operation output is normalized to driveable logic: diff --git a/test/async_reset_test.dart b/test/async_reset_test.dart index 82e1dbcdfc..4e176a1682 100644 --- a/test/async_reset_test.dart +++ b/test/async_reset_test.dart @@ -114,7 +114,7 @@ void main() { ), 'FlipFlop with no asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), @@ -198,7 +198,7 @@ void main() { ), 'FlipFlop with asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index 16c17e01aa..d21eb44a73 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -55,11 +55,19 @@ class _BadClonePacket extends LogicStructure { } class _ConstPacket extends LogicStructure { - _ConstPacket({String? name}) - : super([Const(1)], name: name ?? 'const_packet'); + Logic get data => elements.single; + + _ConstPacket({int? value = 1, String? name}) + : super([ + if (value == null) + Logic(name: 'data', width: 4) + else + Const(value, width: 4) + ], name: name ?? 'const_packet'); @override - _ConstPacket clone({String? name}) => _ConstPacket(name: name ?? this.name); + _ConstPacket clone({String? name}) => + _ConstPacket(value: null, name: name ?? this.name); } class _NetPacket extends LogicStructure { @@ -290,12 +298,49 @@ void main() { throwsA(isA())); }); - test('typed operations reject clones, constants, nets, and reset mismatches', - () { + test('typed operations consume structures with constant leaves', () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final constant = _ConstPacket(value: 0xa, name: 'constant'); + final variable = _ConstPacket(value: null, name: 'variable'); + final muxModule = Mux(control, constant, variable); + final passthrough = Passthrough(constant); + final flipFlop = FlipFlop(clk, constant); + await Future.wait([ + muxModule.build(), + passthrough.build(), + flipFlop.build(), + ]); + for (final module in [muxModule, passthrough, flipFlop]) { + SimCompare.checkIverilogVector(module, const [], buildOnly: true); + } + + expect(muxModule.out, isA<_ConstPacket>()); + expect(muxModule.out.hasConsts, isFalse); + expect(passthrough.out, isA<_ConstPacket>()); + expect(passthrough.out.hasConsts, isFalse); + expect(flipFlop.q, isA<_ConstPacket>()); + expect(flipFlop.q.hasConsts, isFalse); + + variable.data.inject(3); + control.inject(0); + await Simulator.tick(); + expect(muxModule.out.data.value.toInt(), 3); + expect(passthrough.out.data.value.toInt(), 0xa); + + control.inject(1); + await Simulator.tick(); + expect(muxModule.out.data.value.toInt(), 0xa); + + unawaited(Simulator.run()); + await clk.nextPosedge; + expect(flipFlop.q.data.value.toInt(), 0xa); + await Simulator.endSimulation(); + }); + + test('typed operations reject clones, nets, and reset mismatches', () { expect(() => typedClone(_BadClonePacket()), throwsA(isA())); - expect(() => Mux<_ConstPacket>(Logic(), _ConstPacket(), _ConstPacket()), - throwsA(isA())); expect(() => Passthrough<_NetPacket>(_NetPacket()), throwsA(isA())); expect( diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index 2f05069f2f..d834b43970 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -162,6 +162,19 @@ class StructWithConst extends LogicStructure { StructWithConst clone({String? name}) => StructWithConst(name: name); } +class StructWithConstAndDriveableClone extends LogicStructure { + Logic get data => elements.single; + + StructWithConstAndDriveableClone.constant({String? name}) + : this._(Const(1), name: name); + + StructWithConstAndDriveableClone._(Logic data, {super.name}) : super([data]); + + @override + StructWithConstAndDriveableClone clone({String? name}) => + StructWithConstAndDriveableClone._(Logic(), name: name); +} + class ModuleWithPartialAssignInlineAndOutReuseModule extends Module { ModuleWithPartialAssignInlineAndOutReuseModule(MyStruct inp) { inp = addTypedInput('inp', inp); @@ -347,6 +360,15 @@ void main() { expect(failed, portType == 'inOut'); }); } + + test('input with const structure and driveable clone', () { + final source = StructWithConstAndDriveableClone.constant(); + final input = DummyModule().addTypedInput('p', source); + + expect(input, isA()); + expect(input.hasConsts, isFalse); + expect(input.data.value, LogicValue.one); + }); }); group('simple struct module with nets', () { From ee8c5f82774296d38639cafd4ce5075da86d5f88 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 4 Sep 2026 15:16:43 -0700 Subject: [PATCH 03/33] update doc and tests --- CHANGELOG.md | 2 + doc/user_guide/_docs/A19-logic-structures.md | 67 +++++- lib/src/modules/conditionals/case.dart | 183 +++++++++----- lib/src/modules/conditionals/flop.dart | 168 ++++++------- lib/src/modules/gates.dart | 237 +++++++++++-------- lib/src/modules/passthrough.dart | 61 ++--- lib/src/modules/pipeline.dart | 142 ++++++----- lib/src/selection.dart | 19 +- lib/src/signals/logic_structure.dart | 43 +++- test/async_reset_test.dart | 4 +- test/long_chain_test.dart | 2 +- test/multimodule3_test.dart | 2 +- test/multimodule4_test.dart | 19 +- test/netlist_synthesizer_test.dart | 4 +- test/shuffle_test.dart | 2 +- test/typed_operations_test.dart | 65 ++--- 16 files changed, 574 insertions(+), 446 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1a5e3fe3..b26efa4d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ - Netlist JSON format is now `0.1.0`, preserving enum types and encodings on retained netnames using Yosys-style attributes, including constant-driven enum uses (). - Refined the typed-array hierarchy so `LogicArray` directly specializes `TypedLogicArray`, and strengthened custom `TypedLogicArray` cloning to preserve runtime types, codecs, unpacked dimensions, and constructor-only naming prefixes without exposing them as public axis metadata (). - Expanded `TypedLogicArray` support for recursively nested typed and ordinary arrays, structures containing array fields, mixed packed/unpacked dimensions, structured net and inout ports, and recursive netlist type metadata. Fixed generated SystemVerilog for array-valued elements to use packed offsets consistent with declarations and module-port connections, avoiding invalid chained selections in Icarus and Verilator (). +- Preserved configured SystemVerilog output object types for nested arrays and added the opt-in `iverilogWorkaroundForUnpackedArrayVariables` setting for child-driven unpacked array variables that remain unknown in Icarus Verilog 12.0 (). +- Added structure-preserving typed operations for multiplexing, flip-flops, case and indexed selection, pipelines, naming, cloning, and pass-through modules. Nested `LogicArray` and `TypedLogicArray` boundaries remain available through the concrete output type while existing scalar operations retain their `Logic` APIs. - Added configurable waveform timescales, recording windows, and output overwrite policies (). - Added opt-in `DebugSession` setup for source tracing, netlist and FLC services, and live waveform recording (). - Expanded reusable `rohd_devtools_widgets` components for hierarchy browsing, signal formatting, bit expansion, overlays, help, capture, and cross-probing (). diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index cff0a3b401..be10a32d8d 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -1,7 +1,7 @@ --- title: "Logic Structures" permalink: /docs/logic-structures/ -last_modified_at: 2026-9-2 +last_modified_at: 2026-9-4 toc: true --- @@ -19,12 +19,14 @@ Ports with matching types to the original `LogicStructure` can be created using ## Type-preserving operations -`Mux`, `FlipFlop`, and `Passthrough` preserve a concrete `LogicStructure` type when their structure operands match. Dart infers the type from an ordinary structure input, so named fields remain available on the result: +`StructureMux`, `StructureFlipFlop`, and `StructurePassthrough` preserve a +concrete `LogicStructure` type when their structure operands match: ```dart -final selected = Mux(select, packet1, packet0).out; -final registered = FlipFlop(clk, selected, reset: reset).q; -final forwarded = Passthrough(registered).out; +final selected = StructureMux(select, packet1, packet0).out; +final registered = + StructureFlipFlop(clk, selected, reset: reset).q; +final forwarded = StructurePassthrough(registered).out; // All three values have type Packet. forwarded.valid <= selected.valid; @@ -32,16 +34,61 @@ forwarded.valid <= selected.valid; The same behavior applies to `LogicArray` and `LogicArrayOf`, including nested typed arrays. Both mux operands must have the same concrete type and recursive shape: field widths, array dimensions, and packing hints must match. -Typed operations can consume a structure containing `Const` leaves when its `clone()` implementation returns the same concrete structure type with driveable `Logic` leaves. The operation preserves the structure type while normalizing its input port and output to driveable logic. This supports domain-specific constant structures, such as a floating-point structure assembled from constant sign, exponent, and mantissa fields. +Typed operations can consume a structure containing `Const` leaves when its +`clone()` implementation returns the same concrete structure type with +driveable `Logic` leaves. The operation preserves the structure type while +normalizing its input port and output to driveable logic. This supports +domain-specific constant structures, such as a floating-point structure +assembled from constant sign, exponent, and mantissa fields. -For ordinary scalar operations, infer or explicitly request `Logic`. When a source is a `Const` or `LogicNet`, request `Logic` explicitly because the operation output is normalized to driveable logic: +The existing `Mux`, `FlipFlop`, and `Passthrough` APIs always produce ordinary +`Logic`. They accept structures as packed inputs when widths match, but do not +preserve named fields: ```dart -final selected = Mux(select, Const(0, width: 8), data).out; -final registered = FlipFlop(clk, selected).q; +final Logic selected = Mux(select, packet1, packet0).out; +final Logic registered = FlipFlop(clk, selected).q; ``` -Use `typedCases`, `selectIndexTyped`, and `selectFromTyped` for structure-preserving multi-way selection. `StructurePipeline` preserves the same concrete type at every registered pipeline boundary. Specify `T` when creating a pipeline with inline stage transforms so Dart can type the transform parameter. +### Case selection + +`Case` can assign structures directly because its branches contain ordinary +conditional assignments. The destination determines the result type: + +```dart +final selected = packet0.cloneTyped(name: 'selected'); + +Combinational([ + Case(selector, [ + CaseItem(Const(0, width: selector.width), [selected < packet0]), + CaseItem(Const(1, width: selector.width), [selected < packet1]), + ]), +]); +``` + +Direct structure assignments require matching total widths and map bits in +packed leaf order. They do not require the source and destination to have the +same concrete structure type. + +Use `typedCases` when the operation should construct and return a value while +preserving its concrete type: + +```dart +final Packet selected = typedCases( + selector, + {0: packet0, 1: packet1}, + defaultValue: fallback, +); +``` + +All structured values passed to `typedCases` must have the same concrete type +and recursive shape. The legacy `cases` helper accepts structures as packed +values but always returns an ordinary `Logic`. + +Use `selectIndexTyped` and `selectFromTyped` for structure-preserving indexed +selection. `StructurePipeline` preserves the same concrete type at every +registered pipeline boundary. Specify `T` when creating a pipeline with inline +stage transforms so Dart can type the transform parameter. ## Using `LogicStructure` to group signals diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index 80f096e5d9..7b1b4671de 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -66,7 +66,10 @@ Logic cases(Logic expression, Map conditions, if (width != inferredWidth && width != null && inferredWidth != null) { throw SignalWidthMismatchException.forDynamic( - conditionValue, width, inferredWidth); + conditionValue, + width, + inferredWidth, + ); } width ??= inferredWidth; @@ -98,14 +101,20 @@ Logic cases(Logic expression, Map conditions, if (condition.key is Logic) { if (expression.width != (condition.key as Logic).width) { throw SignalWidthMismatchException.forDynamic( - condition.key, expression.width, (condition.key as Logic).width); + condition.key, + expression.width, + (condition.key as Logic).width, + ); } } if (condition.key is LogicValue) { if (expression.width != (condition.key as LogicValue).width) { - throw SignalWidthMismatchException.forDynamic(condition.key, - expression.width, (condition.key as LogicValue).width); + throw SignalWidthMismatchException.forDynamic( + condition.key, + expression.width, + (condition.key as LogicValue).width, + ); } } } @@ -114,13 +123,17 @@ Logic cases(Logic expression, Map conditions, Combinational([ Case( - expression, - [ - for (final condition in conditions.entries) - CaseItem(conditionLogic(condition.key), [result < condition.value]) - ], - conditionalType: conditionalType, - defaultItem: defaultValue != null ? [result < defaultValue] : null) + expression, + [ + for (final condition in conditions.entries) + CaseItem( + conditionLogic(condition.key), + [result < condition.value], + ), + ], + conditionalType: conditionalType, + defaultItem: defaultValue != null ? [result < defaultValue] : null, + ), ]); return result; @@ -135,13 +148,16 @@ Logic cases(Logic expression, Map conditions, /// [ConditionalType.unique] and [ConditionalType.priority] do not make an /// otherwise incomplete case exhaustive. LogicType typedCases( - Logic expression, Map conditions, - {ConditionalType conditionalType = ConditionalType.none, - dynamic defaultValue, - String name = 'result'}) { + Logic expression, + Map conditions, { + ConditionalType conditionalType = ConditionalType.none, + dynamic defaultValue, + String name = 'result', +}) { if (conditions.isEmpty) { throw LogicConstructionException( - 'typedCases requires at least one typed condition value.'); + 'typedCases requires at least one typed condition value.', + ); } final prototype = conditions.values.first; @@ -149,8 +165,11 @@ LogicType typedCases( validateMatchingLogicStructure(value, prototype, operation: 'typedCases'); } if (defaultValue is LogicStructure) { - validateMatchingLogicStructure(defaultValue, prototype, - operation: 'typedCases default'); + validateMatchingLogicStructure( + defaultValue, + prototype, + operation: 'typedCases default', + ); } else if (defaultValue is Logic && defaultValue.width != prototype.width) { throw PortWidthMismatchException.equalWidth(defaultValue, prototype); } @@ -158,7 +177,8 @@ LogicType typedCases( final result = typedClone(prototype, name: name); if (result.hasConsts || result.hasNets) { throw LogicConstructionException( - 'typedCases result clones cannot contain constants or nets.'); + 'typedCases result clones cannot contain constants or nets.', + ); } Combinational([ @@ -229,10 +249,12 @@ class Case extends Conditional { /// Whenever an item in [items] matches [expression], it will be executed. /// /// If none of [items] match, then [defaultItem] is executed. - Case(this.expression, this.items, - {List? defaultItem, - this.conditionalType = ConditionalType.none}) - : _defaultItem = defaultItem { + Case( + this.expression, + this.items, { + List? defaultItem, + this.conditionalType = ConditionalType.none, + }) : _defaultItem = defaultItem { for (final item in items) { if (item.value.width != expression.width) { throw PortWidthMismatchException.equalWidth(expression, item.value); @@ -302,13 +324,14 @@ class Case extends Conditional { } @override - late final List conditionals = - UnmodifiableListView(_getConditionals()); + late final List conditionals = UnmodifiableListView( + _getConditionals(), + ); /// Calculates the set of conditionals directly within this. List _getConditionals() => [ ...items.map((item) => item.then).expand((conditional) => conditional), - if (defaultItem != null) ...defaultItem! + if (defaultItem != null) ...defaultItem!, ]; @override @@ -320,16 +343,20 @@ class Case extends Conditional { for (final item in items) { drivers ..add(item.value) - ..addAll(item.then - .map((conditional) => conditional.drivers) - .expand((driver) => driver) - .toList(growable: false)); + ..addAll( + item.then + .map((conditional) => conditional.drivers) + .expand((driver) => driver) + .toList(growable: false), + ); } if (defaultItem != null) { - drivers.addAll(defaultItem! - .map((conditional) => conditional.drivers) - .expand((driver) => driver) - .toList(growable: false)); + drivers.addAll( + defaultItem! + .map((conditional) => conditional.drivers) + .expand((driver) => driver) + .toList(growable: false), + ); } return drivers; } @@ -341,23 +368,31 @@ class Case extends Conditional { List calculateReceivers() { final receivers = []; for (final item in items) { - receivers.addAll(item.then - .map((conditional) => conditional.receivers) - .flattened - .toList(growable: false)); + receivers.addAll( + item.then + .map((conditional) => conditional.receivers) + .flattened + .toList(growable: false), + ); } if (defaultItem != null) { - receivers.addAll(defaultItem! - .map((conditional) => conditional.receivers) - .flattened - .toList(growable: false)); + receivers.addAll( + defaultItem! + .map((conditional) => conditional.receivers) + .flattened + .toList(growable: false), + ); } return receivers; } @override - String verilogContents(int indent, Map inputsNameMap, - Map outputsNameMap, String assignOperator) { + String verilogContents( + int indent, + Map inputsNameMap, + Map outputsNameMap, + String assignOperator, + ) { final padding = Conditional.calcPadding(indent); final expressionName = inputsNameMap[driverInput(expression).name]; var caseHeader = caseType; @@ -371,8 +406,14 @@ class Case extends Conditional { for (final item in items) { final conditionName = inputsNameMap[driverInput(item.value).name]; final caseContents = item.then - .map((conditional) => conditional.verilogContents( - indent + 4, inputsNameMap, outputsNameMap, assignOperator)) + .map( + (conditional) => conditional.verilogContents( + indent + 4, + inputsNameMap, + outputsNameMap, + assignOperator, + ), + ) .join('\n'); verilog.write(''' $subPadding$conditionName : begin @@ -382,8 +423,14 @@ ${subPadding}end } if (defaultItem != null) { final defaultCaseContents = defaultItem! - .map((conditional) => conditional.verilogContents( - indent + 4, inputsNameMap, outputsNameMap, assignOperator)) + .map( + (conditional) => conditional.verilogContents( + indent + 4, + inputsNameMap, + outputsNameMap, + assignOperator, + ), + ) .join('\n'); verilog.write(''' ${subPadding}default : begin @@ -397,25 +444,30 @@ ${subPadding}end } @override - Map processSsa(Map currentMappings, - {required int context}) { + Map processSsa( + Map currentMappings, { + required int context, + }) { // add an empty default if there isn't already one, since we need it for phi _defaultItem ??= []; // first connect direct drivers into the case statement - Conditional.connectSsaDriverFromMappings(expression, currentMappings, - context: context); + Conditional.connectSsaDriverFromMappings( + expression, + currentMappings, + context: context, + ); for (final itemDriver in items.map((e) => e.value)) { - Conditional.connectSsaDriverFromMappings(itemDriver, currentMappings, - context: context); + Conditional.connectSsaDriverFromMappings( + itemDriver, + currentMappings, + context: context, + ); } // calculate mappings locally within each item final phiMappings = {}; - for (final conditionals in [ - ...items.map((e) => e.then), - defaultItem!, - ]) { + for (final conditionals in [...items.map((e) => e.then), defaultItem!]) { var localMappings = {...currentMappings}; for (final conditional in conditionals) { @@ -442,16 +494,13 @@ ${subPadding}end // an inferred latch final signalsNeedingInit = [ ...items.map((e) => e.then).flattened, - ...defaultItem!, + ...defaultItem! ] .map((e) => e.calculateReceivers()) .flattened .whereType() .toSet(); - for (final conditionals in [ - ...items.map((e) => e.then), - defaultItem!, - ]) { + for (final conditionals in [...items.map((e) => e.then), defaultItem!]) { final alreadyDrivenSsaSignals = conditionals .map((e) => e.calculateReceivers()) .flattened @@ -502,8 +551,12 @@ class CaseZ extends Case { /// the definition of matches allows for `z` to be a wildcard. /// /// If none of [items] match, then [defaultItem] is executed. - CaseZ(super.expression, super.items, - {super.defaultItem, super.conditionalType}); + CaseZ( + super.expression, + super.items, { + super.defaultItem, + super.conditionalType, + }); @override String get caseType => 'casez'; diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index 6e6d4b89e9..bc4519074c 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -32,7 +32,7 @@ Logic flop( dynamic resetValue, bool asyncReset = false, }) => - FlipFlop( + FlipFlop( clk, d, en: en, @@ -43,77 +43,28 @@ Logic flop( /// A flip-flop with an output represented by [LogicType]. /// -/// For [FlipFlop]<[Logic]>, inputs can be any [Logic] and the output is -/// normalized to ordinary driveable logic. A concrete [LogicStructure] input -/// infers the matching output type. -abstract class FlipFlop extends TypedOp { +/// Implementations decide how an input is normalized into the promised output +/// type. [FlipFlop] produces an ordinary [Logic], while +/// [StructureFlipFlop] preserves matching structure and array boundaries. +abstract class TypedFlipFlop + extends TypedOp { /// Registered output. LogicType get q; - /// Indicates whether the reset is asynchronous. - bool get asyncReset; - - /// The constant reset value, or `null` when reset is absent or data-driven. - LogicValue? get constantResetValue; - - /// The registered output. @override LogicType get out => q; - /// Creates a flip-flop with an output represented by [LogicType]. - /// - /// Concrete [LogicStructure] inputs infer their output type. Constants and - /// nets cannot be [LogicType] because they cannot represent a driveable - /// output; request `FlipFlop` for those inputs. - factory FlipFlop( - Logic clk, - LogicType d, { - Logic? en, - Logic? reset, - dynamic resetValue, - bool asyncReset = false, - String name = 'flipflop', - }) { - if (LogicType == Logic) { - return ScalarFlipFlop( - clk, - d, - en: en, - reset: reset, - resetValue: resetValue, - asyncReset: asyncReset, - name: name, - ) as FlipFlop; - } - if (d is! LogicStructure) { - throw LogicConstructionException( - 'FlipFlop<$LogicType> requires LogicType to be Logic or a ' - 'concrete LogicStructure.'); - } - return _StructureFlipFlop( - clk, - d, - en: en, - reset: reset, - resetValue: resetValue, - asyncReset: asyncReset, - name: name, - ); - } - - FlipFlop._({ - super.name, + /// Creates a typed flip-flop implementation. + TypedFlipFlop({ + super.name = 'typed_flip_flop', super.reserveName, super.definitionName, super.reserveDefinitionName, }); } -/// Scalar implementation of [FlipFlop]. -/// -/// Extend this class when a custom scalar flip-flop implementation must retain -/// the standard flip-flop behavior. -class ScalarFlipFlop extends FlipFlop with SystemVerilog { +/// Represents a single flip-flop with no reset. +class FlipFlop extends TypedFlipFlop with SystemVerilog { /// Name for the enable input of this flop final String _enName = Naming.unpreferredName('en'); @@ -162,11 +113,9 @@ class ScalarFlipFlop extends FlipFlop with SystemVerilog { /// Indicates whether provided `reset` signals should be treated as an async /// reset. If no `reset` is provided, this will have no effect. - @override final bool asyncReset; /// The constant reset value, or `null` when reset is absent or data-driven. - @override LogicValue? get constantResetValue => _reset == null || _resetValuePort != null ? null : _resetValueConst; @@ -185,7 +134,7 @@ class ScalarFlipFlop extends FlipFlop with SystemVerilog { /// If [asyncReset] is true, the [reset] signal (if provided) will be treated /// as an async reset. If [asyncReset] is false, the reset signal will be /// treated as synchronous. - ScalarFlipFlop( + FlipFlop( Logic clk, Logic d, { Logic? en, @@ -193,7 +142,7 @@ class ScalarFlipFlop extends FlipFlop with SystemVerilog { dynamic resetValue, this.asyncReset = false, super.name = 'flipflop', - }) : super._() { + }) { if (clk.width != 1) { throw Exception('clk must be 1 bit'); } @@ -240,7 +189,10 @@ class ScalarFlipFlop extends FlipFlop with SystemVerilog { @override String instantiationVerilog( - String instanceType, String instanceName, Map ports) { + String instanceType, + String instanceName, + Map ports, + ) { var expectedInputs = 2; if (_en != null) { expectedInputs++; @@ -252,8 +204,10 @@ class ScalarFlipFlop extends FlipFlop with SystemVerilog { expectedInputs++; } - assert(ports.length == expectedInputs + 1, - 'FlipFlop has exactly $expectedInputs inputs and one output.'); + assert( + ports.length == expectedInputs + 1, + 'FlipFlop has exactly $expectedInputs inputs and one output.', + ); final clk = ports[_clkName]!; final d = ports[_dName]!; @@ -285,8 +239,12 @@ class ScalarFlipFlop extends FlipFlop with SystemVerilog { } } -String _structureFlipFlopResetIdentity(LogicStructure structure, bool hasReset, - bool hasResetPort, dynamic resetValue) { +String _structureFlipFlopResetIdentity( + LogicStructure structure, + bool hasReset, + bool hasResetPort, + dynamic resetValue, +) { if (!hasReset) { return 'N'; } @@ -297,12 +255,14 @@ String _structureFlipFlopResetIdentity(LogicStructure structure, bool hasReset, return 'RC${value.toRadixString(includeWidth: false, sepChar: '')}'; } -String _structureFlipFlopDefinitionName(LogicStructure structure, - {required bool hasEnable, - required bool hasReset, - required bool hasResetPort, - required dynamic resetValue, - required bool asyncReset}) => +String _structureFlipFlopDefinitionName( + LogicStructure structure, { + required bool hasEnable, + required bool hasReset, + required bool hasResetPort, + required dynamic resetValue, + required bool asyncReset, +}) => 'StructureFlipFlop_${logicStructureShapeSignature(structure)}_' '${hasEnable ? 'E' : 'N'}_' '${_structureFlipFlopResetIdentity( @@ -313,27 +273,26 @@ String _structureFlipFlopDefinitionName(LogicStructure structure, )}_' '${asyncReset ? 'A' : 'S'}'; -/// Structure-preserving implementation of [FlipFlop]. +/// A flip-flop that preserves a concrete [LogicStructure] type. /// /// Each structure leaf is registered independently with common clock, enable, /// and reset controls. A [Logic] reset value is interpreted in the same packed /// leaf order as [LogicStructure.packed]. A structured reset value must match /// the input structure's recursive shape. -class _StructureFlipFlop extends FlipFlop { +class StructureFlipFlop + extends TypedFlipFlop { /// Indicates whether the reset is asynchronous. - @override final bool asyncReset; /// Constant packed reset value, or `null` when reset is absent or driven by /// a [Logic] input. - @override final LogicValue? constantResetValue; @override late final LogicType q; /// Creates a structure-preserving flip-flop. - _StructureFlipFlop( + StructureFlipFlop( Logic clk, LogicType d, { Logic? en, @@ -344,9 +303,9 @@ class _StructureFlipFlop extends FlipFlop { }) : constantResetValue = reset != null && resetValue is! Logic ? LogicValue.of(resetValue ?? 0, width: d.width) : null, - super._( + super( definitionName: _structureFlipFlopDefinitionName( - d as LogicStructure, + d, hasEnable: en != null, hasReset: reset != null, hasResetPort: reset != null && resetValue is Logic, @@ -363,25 +322,20 @@ class _StructureFlipFlop extends FlipFlop { if (reset != null && reset.width != 1) { throw PortWidthMismatchException(reset, 1); } - final structuredD = d as LogicStructure; if (resetValue is Logic) { if (resetValue.width != d.width) { throw PortWidthMismatchException.equalWidth(resetValue, d); } if (resetValue is LogicStructure) { - validateMatchingLogicStructure(resetValue, structuredD, - operation: 'StructureFlipFlop reset'); + validateMatchingLogicStructure( + resetValue, + d, + operation: 'StructureFlipFlop reset', + ); } } - LogicType cloneOutput({String name = 'q'}) { - final cloned = structuredD.clone(name: name); - if (cloned is! LogicType) { - throw LogicConstructionException( - 'FlipFlop output clone did not preserve its concrete type.'); - } - return cloned as LogicType; - } + LogicType cloneOutput({String name = 'q'}) => typedClone(d, name: name); final localClk = addInput('clk', clk); final localD = addTypedInput('d', d); @@ -393,17 +347,15 @@ class _StructureFlipFlop extends FlipFlop { q = addTypedOutput('q', cloneOutput); var offset = 0; - final structuredQ = q as LogicStructure; - final structuredLocalD = localD as LogicStructure; - for (var index = 0; index < structuredQ.leafElements.length; index++) { - final width = structuredQ.leafElements[index].width; + for (var index = 0; index < q.leafElements.length; index++) { + final width = q.leafElements[index].width; final dynamic leafResetValue = localResetValue != null ? localResetValue.getRange(offset, offset + width) : constantResetValue?.getRange(offset, offset + width); - structuredQ.leafElements[index] <= + q.leafElements[index] <= flop( localClk, - structuredLocalD.leafElements[index], + localD.leafElements[index], en: localEnable, reset: localReset, resetValue: leafResetValue, @@ -413,3 +365,21 @@ class _StructureFlipFlop extends FlipFlop { } } } + +/// Constructs a positive-edge-triggered structure-preserving flip-flop. +LogicType typedFlop( + Logic clk, + LogicType d, { + Logic? en, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, +}) => + StructureFlipFlop( + clk, + d, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + ).q; diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index f6c44542dc..93783c186a 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -31,8 +31,10 @@ class NotGate extends Module with InlineSystemVerilog { _inName = Naming.unpreferredName(in_.name); _outName = Naming.unpreferredName('${in_.name}_b'); addInput(_inName, in_, width: in_.width); - addOutput(_outName, width: in_.width) - .makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); + addOutput( + _outName, + width: in_.width, + ).makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -166,11 +168,15 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a /// String between the two input signal names (e.g. if [_opStr] was "&", /// generated SystemVerilog may look like "a & b"). - _TwoInputBitwiseGate(this._op, this._opStr, Logic in0, dynamic in1, - {String name = 'gate2', - int outputSvWidthExpansion = 0, - bool makeSelfDetermined = false}) - : width = in0.width, + _TwoInputBitwiseGate( + this._op, + this._opStr, + Logic in0, + dynamic in1, { + String name = 'gate2', + int outputSvWidthExpansion = 0, + bool makeSelfDetermined = false, + }) : width = in0.width, assert(!outputSvWidthExpansion.isNegative, 'Should not be negative.'), _outputSvWidthExpansion = outputSvWidthExpansion, _makeSelfDetermined = makeSelfDetermined, @@ -187,8 +193,10 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { addInput(_in0Name, in0, width: width); addInput(_in1Name, in1Logic, width: width); - addOutput(_outName, width: width + _outputSvWidthExpansion) - .makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); + addOutput( + _outName, + width: width + _outputSvWidthExpansion, + ).makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -258,9 +266,13 @@ abstract class _TwoInputComparisonGate extends Module with InlineSystemVerilog { /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a /// String between the two input signal names (e.g. if [_opStr] was ">", /// generated SystemVerilog may look like "a > b"). - _TwoInputComparisonGate(this._op, this._opStr, Logic in0, dynamic in1, - {String name = 'cmp2'}) - : super(name: name) { + _TwoInputComparisonGate( + this._op, + this._opStr, + Logic in0, + dynamic in1, { + String name = 'cmp2', + }) : super(name: name) { if (in1 is Logic && in0.width != in1.width) { throw PortWidthMismatchException.equalWidth(in0, in1); } @@ -365,11 +377,15 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a /// String between the two input signal names (e.g. if [_opStr] was ">>", /// generated SystemVerilog may look like "a >> b"). - _ShiftGate(this._op, this._opStr, Logic in_, dynamic shiftAmount, - {String name = 'gate2', - this.signed = false, - bool outputSvWidthExpansion = false}) - : width = in_.width, + _ShiftGate( + this._op, + this._opStr, + Logic in_, + dynamic shiftAmount, { + String name = 'gate2', + this.signed = false, + bool outputSvWidthExpansion = false, + }) : width = in_.width, _outputSvWidthExpansion = outputSvWidthExpansion, _isNet = in_.isNet && // if it's a Logic, then we can't treat this like a net @@ -398,17 +414,22 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { _inName = Naming.unpreferredName('in_${in_.name}'); - _shiftAmountName = - Naming.unpreferredName('shiftAmount_${shiftAmountLogic.name}'); + _shiftAmountName = Naming.unpreferredName( + 'shiftAmount_${shiftAmountLogic.name}', + ); - _outName = - Naming.unpreferredName('${in_.name}_${name}_${shiftAmountLogic.name}'); + _outName = Naming.unpreferredName( + '${in_.name}_${name}_${shiftAmountLogic.name}', + ); final inputCreator = _isNet ? addInOut : addInput; _in = inputCreator(_inName, in_, width: in_.width); - _shiftAmount = inputCreator(_shiftAmountName, shiftAmountLogic, - width: shiftAmountLogic.width); + _shiftAmount = inputCreator( + _shiftAmountName, + shiftAmountLogic, + width: shiftAmountLogic.width, + ); if (_isNet) { out = LogicNet(name: _outName, width: width, naming: Naming.unnamed); @@ -416,9 +437,10 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { _netSetup(internalOut); } else { - out = addOutput(_outName, width: width) - ..makeUnassignable( - reason: 'Output of a gate $this cannot be assigned.'); + out = addOutput( + _outName, + width: width, + )..makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -576,7 +598,10 @@ class Add extends Module with SystemVerilog { @override String instantiationVerilog( - String instanceType, String instanceName, Map ports) { + String instanceType, + String instanceName, + Map ports, + ) { assert(inputs.length == 2, 'Gate has exactly two inputs'); assert(outputs.length == 2, 'Gate has exactly two outputs'); @@ -685,9 +710,11 @@ class GreaterThanOrEqual extends _TwoInputComparisonGate { /// /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. - GreaterThanOrEqual(Logic in0, dynamic in1, - {super.name = 'greaterThanOrEqual'}) - : super((a, b) => a >= b, '>=', in0, in1); + GreaterThanOrEqual( + Logic in0, + dynamic in1, { + super.name = 'greaterThanOrEqual', + }) : super((a, b) => a >= b, '>=', in0, in1); } /// A unary AND gate. @@ -763,8 +790,13 @@ class LShift extends _ShiftGate { /// [shiftAmount] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. LShift(Logic in_, dynamic shiftAmount, {super.name = 'lshift'}) - : super((a, shamt) => a << shamt, '<<', in_, shiftAmount, - outputSvWidthExpansion: true); + : super( + (a, shamt) => a << shamt, + '<<', + in_, + shiftAmount, + outputSvWidthExpansion: true, + ); @override void _netSetup(LogicNet internalOut) { @@ -793,52 +825,34 @@ Logic mux(Logic control, Logic d1, Logic d0) { return control.value == LogicValue.one ? d1 : d0; } - return Mux(control, d1, d0).out; + return Mux(control, d1, d0).out; } /// A multiplexer with an output represented by [LogicType]. /// -/// For [Mux]<[Logic]>, inputs can be any width-compatible [Logic], including -/// constants and nets, and the output is normalized to an ordinary [Logic]. -/// For a concrete [LogicStructure] type, both inputs must have that type and -/// matching recursive geometry. -abstract class Mux extends TypedOp { +/// Implementations decide how inputs are normalized into an output of the +/// promised type. For example, [Mux] accepts constants and nets as sources but +/// produces an ordinary single-driver [Logic], while [StructureMux] preserves +/// the concrete structure type and recursively selects corresponding leaves. +abstract class TypedMux extends TypedOp { /// Output selected by the control input. @override LogicType get out; - /// Creates a multiplexer with an output represented by [LogicType]. - /// - /// A scalar output is selected with `Mux`. Concrete - /// [LogicStructure] outputs are inferred from ordinary structure operands. - /// Constants and nets cannot be used as [LogicType] because they cannot - /// represent a driveable mux output; request `Mux` for them. - factory Mux(Logic control, LogicType d1, LogicType d0, - {String name = 'mux'}) { - if (LogicType == Logic) { - return _LogicMux(control, d1, d0, name: name) as Mux; - } - if (d1 is! LogicStructure || d0 is! LogicStructure) { - throw LogicConstructionException( - 'Mux<$LogicType> requires LogicType to be Logic or a ' - 'concrete LogicStructure.'); - } - return _StructureMux(control, d1, d0, name: name); - } - - Mux._({ - super.name, + /// Creates a typed multiplexer implementation. + TypedMux({ + super.name = 'typed_mux', super.reserveName, super.definitionName, super.reserveDefinitionName, }); } -/// Scalar implementation of [Mux]. +/// A mux (multiplexer) module. /// /// If [_control] has value `1`, then [out] gets [_d1]. /// If [_control] has value `0`, then [out] gets [_d0]. -class _LogicMux extends Mux with InlineSystemVerilog { +class Mux extends TypedMux with InlineSystemVerilog { /// Name for the control signal of this mux. late final String _controlName; @@ -851,13 +865,13 @@ class _LogicMux extends Mux with InlineSystemVerilog { /// Name for the output port of this mux. late final String _outName; - /// The control signal for this mux. + /// The control signal for this [Mux]. late final Logic _control = input(_controlName); - /// Input propagated when [out] is `0`. + /// [Mux] input propagated when [out] is `0`. late final Logic _d0 = input(_d0Name); - /// Input propagated when [out] is `1`. + /// [Mux] input propagated when [out] is `1`. late final Logic _d1 = input(_d1Name); /// Output port of the [Mux]. @@ -866,8 +880,7 @@ class _LogicMux extends Mux with InlineSystemVerilog { /// Constructs a multiplexer which passes [d0] or [d1] to [out] depending /// on if [control] is 0 or 1, respectively. - _LogicMux(Logic control, Logic d1, Logic d0, {super.name = 'mux'}) - : super._() { + Mux(Logic control, Logic d1, Logic d0, {super.name = 'mux'}) { if (control.width != 1) { throw PortWidthMismatchException(control, 1); } @@ -883,8 +896,10 @@ class _LogicMux extends Mux with InlineSystemVerilog { addInput(_controlName, control); addInput(_d0Name, d0, width: d0.width); addInput(_d1Name, d1, width: d1.width); - addOutput(_outName, width: d0.width) - .makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); + addOutput( + _outName, + width: d0.width, + ).makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -929,12 +944,13 @@ class _LogicMux extends Mux with InlineSystemVerilog { String _structureMuxDefinitionName(LogicStructure structure) => 'StructureMux_${logicStructureShapeSignature(structure)}'; -/// Structure-preserving implementation of [Mux]. +/// A multiplexer that preserves a concrete [LogicStructure] type. /// /// Both operands must have identical recursive structure and legal typed-port /// clones. Each corresponding leaf is selected independently, preserving /// nested [LogicArray] and [LogicArrayOf] boundaries at the output. -class _StructureMux extends Mux { +class StructureMux + extends TypedMux { late final Logic _control; late final LogicType _d0; late final LogicType _d1; @@ -943,42 +959,50 @@ class _StructureMux extends Mux { late final LogicType out; /// Creates a structure-preserving multiplexer. - _StructureMux(Logic control, LogicType d1, LogicType d0, - {super.name = 'structure_mux'}) - : super._( - definitionName: _structureMuxDefinitionName(d0 as LogicStructure)) { + StructureMux( + Logic control, + LogicType d1, + LogicType d0, { + super.name = 'structure_mux', + }) : super(definitionName: _structureMuxDefinitionName(d0)) { if (control.width != 1) { throw PortWidthMismatchException(control, 1); } - final structuredD0 = d0 as LogicStructure; - final structuredD1 = d1 as LogicStructure; - validateMatchingLogicStructure(structuredD1, structuredD0, - operation: 'Mux'); - - LogicType cloneOutput({String name = 'out'}) { - final cloned = structuredD0.clone(name: name); - if (cloned is! LogicType) { - throw LogicConstructionException( - 'Mux output clone did not preserve its concrete type.'); - } - return cloned as LogicType; - } + validateMatchingLogicStructure(d1, d0, operation: 'StructureMux'); + + LogicType cloneOutput({String name = 'out'}) => typedClone(d0, name: name); _control = addInput('control', control); _d0 = addTypedInput('d0', d0); _d1 = addTypedInput('d1', d1); out = addTypedOutput('out', cloneOutput); - final structuredOut = out as LogicStructure; - final structuredInput0 = _d0 as LogicStructure; - final structuredInput1 = _d1 as LogicStructure; - for (var index = 0; index < structuredOut.leafElements.length; index++) { - structuredOut.leafElements[index] <= - mux(_control, structuredInput1.leafElements[index], - structuredInput0.leafElements[index]); + + for (var index = 0; index < out.leafElements.length; index++) { + out.leafElements[index] <= + mux(_control, _d1.leafElements[index], _d0.leafElements[index]); } } } +/// Selects between matching structures while preserving their concrete type. +/// +/// If [control] is a valid [Const], returns the selected operand directly. +/// Otherwise, returns [StructureMux.out]. +LogicType typedMux( + Logic control, + LogicType d1, + LogicType d0, +) { + if (control.width != 1) { + throw PortWidthMismatchException(control, 1); + } + validateMatchingLogicStructure(d1, d0, operation: 'typedMux'); + if (control is Const && control.value.isValid) { + return control.value == LogicValue.one ? d1 : d0; + } + return StructureMux(control, d1, d0).out; +} + /// A two-input bit index gate [Module]. /// /// It always takes two inputs and has one output of width 1. @@ -1007,8 +1031,9 @@ class IndexGate extends Module with InlineSystemVerilog { IndexGate(Logic original, Logic index) : super() { _originalName = 'original_${original.name}'; _indexName = Naming.unpreferredName('index_${index.name}'); - _selectionName = - Naming.unpreferredName('${original.name}_indexby_${index.name}'); + _selectionName = Naming.unpreferredName( + '${original.name}_indexby_${index.name}', + ); addInput(_originalName, original, width: original.width); addInput(_indexName, index, width: index.width); @@ -1105,19 +1130,25 @@ class ReplicationOp extends Module with InlineSystemVerilog { if (_isNet) { original = addInOut(_inputName, original, width: original.width); - replicated = - LogicNet(name: _outputName, width: newWidth, naming: Naming.unnamed); + replicated = LogicNet( + name: _outputName, + width: newWidth, + naming: Naming.unnamed, + ); final internalOut = addInOut(_outputName, replicated, width: newWidth); for (var i = 0; i < _multiplier; i++) { - internalOut.quietlyMergeSubsetTo(original as LogicNet, - start: i * original.width); + internalOut.quietlyMergeSubsetTo( + original as LogicNet, + start: i * original.width, + ); } } else { addInput(_inputName, original, width: original.width); - replicated = addOutput(_outputName, width: original.width * _multiplier) - ..makeUnassignable( - reason: 'Output of a gate $this cannot be assigned.'); + replicated = addOutput( + _outputName, + width: original.width * _multiplier, + )..makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } } diff --git a/lib/src/modules/passthrough.dart b/lib/src/modules/passthrough.dart index 91dc9161ac..49a8b7e822 100644 --- a/lib/src/modules/passthrough.dart +++ b/lib/src/modules/passthrough.dart @@ -10,7 +10,8 @@ import 'package:rohd/rohd.dart'; /// A pass-through module with an input and output represented by [LogicType]. -abstract class Passthrough extends TypedOp { +abstract class TypedPassthrough + extends TypedOp { /// Input port. LogicType get in_; @@ -18,32 +19,17 @@ abstract class Passthrough extends TypedOp { @override LogicType get out; - /// Creates a pass-through with an output represented by [LogicType]. - /// - /// Concrete [LogicStructure] inputs infer their output type. Constants and - /// nets require `Passthrough` because the result is driveable logic. - factory Passthrough(LogicType input, [String name = 'passthrough']) { - if (LogicType == Logic) { - return _LogicPassthrough(input, name) as Passthrough; - } - if (input is! LogicStructure) { - throw LogicConstructionException( - 'Passthrough<$LogicType> requires LogicType to be Logic or a ' - 'concrete LogicStructure.'); - } - return _StructurePassthrough(input, name: name); - } - - Passthrough._({ - super.name, + /// Creates a typed pass-through implementation. + TypedPassthrough({ + super.name = 'typed_passthrough', super.reserveName, super.definitionName, super.reserveDefinitionName, }); } -/// Scalar implementation of [Passthrough]. -class _LogicPassthrough extends Passthrough { +/// A very simple noop module that just passes a signal through. +class Passthrough extends TypedPassthrough { /// The input port. @override Logic get in_ => input('in'); @@ -54,8 +40,7 @@ class _LogicPassthrough extends Passthrough { /// Constructs a simple pass-through module that performs no operations /// between [a] and [out]. - _LogicPassthrough(Logic a, [String name = 'passthrough']) - : super._(name: name) { + Passthrough(Logic a, [String name = 'passthrough']) : super(name: name) { addInput('in', a, width: a.width); addOutput('out', width: a.width); _setup(); @@ -71,12 +56,12 @@ class _LogicPassthrough extends Passthrough { String _structurePassthroughDefinitionName(LogicStructure structure) => 'StructurePassthrough_${logicStructureShapeSignature(structure)}'; -/// Structure-preserving implementation of [Passthrough]. +/// A no-op module that preserves a concrete [LogicStructure] type. /// /// The input and output retain all nested [LogicArray] and [LogicArrayOf] /// boundaries. Typed ports reject structures containing constants or nets. -class _StructurePassthrough - extends Passthrough { +class StructurePassthrough + extends TypedPassthrough { @override late final LogicType in_; @@ -84,27 +69,15 @@ class _StructurePassthrough late final LogicType out; /// Creates a structure-preserving pass-through module. - _StructurePassthrough(LogicType input, {super.name = 'structure_passthrough'}) - : super._( - definitionName: - _structurePassthroughDefinitionName(input as LogicStructure), - ) { - final structuredInput = input as LogicStructure; - LogicType cloneOutput({String name = 'out'}) { - final cloned = structuredInput.clone(name: name); - if (cloned is! LogicType) { - throw LogicConstructionException( - 'Passthrough output clone did not preserve its concrete type.'); - } - return cloned as LogicType; - } + StructurePassthrough(LogicType input, {super.name = 'structure_passthrough'}) + : super(definitionName: _structurePassthroughDefinitionName(input)) { + LogicType cloneOutput({String name = 'out'}) => + typedClone(input, name: name); in_ = addTypedInput('in', input); out = addTypedOutput('out', cloneOutput); - final structuredOut = out as LogicStructure; - final structuredIn = in_ as LogicStructure; - for (var index = 0; index < structuredOut.leafElements.length; index++) { - structuredOut.leafElements[index] <= structuredIn.leafElements[index]; + for (var index = 0; index < out.leafElements.length; index++) { + out.leafElements[index] <= in_.leafElements[index]; } } } diff --git a/lib/src/modules/pipeline.dart b/lib/src/modules/pipeline.dart index 099f7ccdcf..ccde4e8fed 100644 --- a/lib/src/modules/pipeline.dart +++ b/lib/src/modules/pipeline.dart @@ -49,7 +49,7 @@ class StructurePipelineStageInfo { /// /// Every transform receives a [StructurePipelineStageInfo] whose `value` is the /// typed value entering that stage. The returned matching structure is -/// registered through [FlipFlop]. [values] contains the original +/// registered through [StructureFlipFlop]. [values] contains the original /// input followed by every registered stage output, so `stageCount` matches /// [Pipeline]'s boundary-count convention and `latency` is one less. class StructurePipeline { @@ -80,29 +80,38 @@ class StructurePipeline { String name = 'structure_pipeline', }) { if (stalls != null && stalls.length != stages.length) { - throw ArgumentError.value(stalls, 'stalls', - 'Must contain one optional stall signal per transform stage.'); + throw ArgumentError.value( + stalls, + 'stalls', + 'Must contain one optional stall signal per transform stage.', + ); } final generatedValues = [input]; for (var stage = 0; stage < stages.length; stage++) { final transformed = stages[stage]( - StructurePipelineStageInfo._(stage, generatedValues)); - validateMatchingLogicStructure(transformed, input, - operation: 'StructurePipeline stage $stage'); + StructurePipelineStageInfo._(stage, generatedValues), + ); + validateMatchingLogicStructure( + transformed, + input, + operation: 'StructurePipeline stage $stage', + ); final stall = stalls?[stage]; if (stall != null && stall.width != 1) { throw PortWidthMismatchException(stall, 1); } - generatedValues.add(FlipFlop( - clk, - transformed, - en: stall == null ? null : ~stall, - reset: reset, - resetValue: resetValue, - asyncReset: asyncReset, - name: '${name}_stage_$stage', - ).q); + generatedValues.add( + StructureFlipFlop( + clk, + transformed, + en: stall == null ? null : ~stall, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: '${name}_stage_$stage', + ).q, + ); } values = List.unmodifiable(generatedValues); } @@ -245,30 +254,34 @@ class Pipeline { /// Each stage can be stalled independently using [stalls], where every index /// of [stalls] corresponds to the index of the stage to be stalled. When a /// stage's stall is asserted, the output of that stage will not change. - Pipeline(Logic clk, - {List Function(PipelineStageInfo p)> stages = const [], - List? stalls, - List signals = const [], - Map resetValues = const {}, - Logic? reset, - bool asyncReset = false}) - : this.multi([clk], - stages: stages, - stalls: stalls, - signals: signals, - resetValues: resetValues, - reset: reset, - asyncReset: asyncReset); + Pipeline( + Logic clk, { + List Function(PipelineStageInfo p)> stages = const [], + List? stalls, + List signals = const [], + Map resetValues = const {}, + Logic? reset, + bool asyncReset = false, + }) : this.multi( + [clk], + stages: stages, + stalls: stalls, + signals: signals, + resetValues: resetValues, + reset: reset, + asyncReset: asyncReset, + ); /// Constructs a [Pipeline] with multiple triggers on any of [_clks]. - Pipeline.multi(this._clks, - {List Function(PipelineStageInfo p)> stages = const [], - List? stalls, - List signals = const [], - Map? resetValues, - this.reset, - this.asyncReset = false}) - : _resetValues = resetValues == null ? null : Map.from(resetValues) { + Pipeline.multi( + this._clks, { + List Function(PipelineStageInfo p)> stages = const [], + List? stalls, + List signals = const [], + Map? resetValues, + this.reset, + this.asyncReset = false, + }) : _resetValues = resetValues == null ? null : Map.from(resetValues) { _stages = stages.map(_PipeStage.new).toList(); _stages.add(_PipeStage((p) => [])); // output stage @@ -318,8 +331,10 @@ class Pipeline { void _setStalls(List? stalls) { if (stalls != null) { if (stalls.length != stageCount - 1) { - throw Exception('Stall list length (${stalls.length}) must match ' - 'number of stages (${stageCount - 1}).'); + throw Exception( + 'Stall list length (${stalls.length}) must match ' + 'number of stages (${stageCount - 1}).', + ); } for (var i = 0; i < stageCount - 1; i++) { final stall = stalls[i]; @@ -346,15 +361,17 @@ class Pipeline { ffAssigns.add(_i(newLogic, i) < _o(newLogic, i - 1) as ConditionalAssign); } - final ffAssignsWithStall = - List.generate(stageCount - 1, (index) { - final stall = _stages[index].stall; - final ffAssign = ffAssigns[index]; - final driver = stall != null - ? mux(stall, ffAssign.receiver, ffAssign.driver) - : ffAssign.driver; - return ffAssign.receiver < driver as ConditionalAssign; - }); + final ffAssignsWithStall = List.generate( + stageCount - 1, + (index) { + final stall = _stages[index].stall; + final ffAssign = ffAssigns[index]; + final driver = stall != null + ? mux(stall, ffAssign.receiver, ffAssign.driver) + : ffAssign.driver; + return ffAssign.receiver < driver as ConditionalAssign; + }, + ); final stageResetVal = _resetValues?[newLogic]; final resetValuesForNewLogic = {}; @@ -365,12 +382,13 @@ class Pipeline { } Sequential.multi( - _clks, - reset: reset, - resetValues: resetValuesForNewLogic, - asyncReset: asyncReset, - ffAssignsWithStall, - name: 'ff_${newLogic.name}'); + _clks, + reset: reset, + resetValues: resetValuesForNewLogic, + asyncReset: asyncReset, + ffAssignsWithStall, + name: 'ff_${newLogic.name}', + ); } /// The stage input for a signal associated with [logic] to [stageIndex]. @@ -410,7 +428,8 @@ class Pipeline { if (!_isRegistered(logic)) { if (_constructionComplete) { throw PortDoesNotExistException( - 'Signal $logic was not piped through this Pipeline.'); + 'Signal $logic was not piped through this Pipeline.', + ); } else { _add(logic); } @@ -493,17 +512,20 @@ class ReadyValidPipeline extends Pipeline { }) : super.multi( stages: stages, signals: [validPipeIn, ...signals], - stalls: List.generate(stages.length, - (index) => Logic(name: 'stall_$index', naming: Naming.mergeable)), + stalls: List.generate( + stages.length, + (index) => Logic(name: 'stall_$index', naming: Naming.mergeable), + ), ) { final valid = validPipeIn; final stalls = _stages.map((stage) => stage.stall).toList() ..removeLast(); // garbage value at the end - final readys = List.generate(stages.length, - (index) => Logic(name: 'ready_$index', naming: Naming.mergeable)) - ..add(readyPipeOut); + final readys = List.generate( + stages.length, + (index) => Logic(name: 'ready_$index', naming: Naming.mergeable), + )..add(readyPipeOut); for (var i = 0; i < stalls.length; i++) { readys[i] <= ~get(valid, i + 1) | readys[i + 1]; diff --git a/lib/src/selection.dart b/lib/src/selection.dart index 46806f20a3..c2a89fae10 100644 --- a/lib/src/selection.dart +++ b/lib/src/selection.dart @@ -37,17 +37,21 @@ extension TypedIndexedLogic /// Selects one structure using [index], preserving its concrete type. /// /// An out-of-range index produces zero unless [defaultValue] is supplied. - LogicType selectIndexTyped(Logic index, - {LogicType? defaultValue, String name = 'selectFrom'}) { + LogicType selectIndexTyped( + Logic index, { + LogicType? defaultValue, + String name = 'selectFrom', + }) { if (isEmpty) { throw LogicConstructionException( - 'selectIndexTyped requires at least one value.'); + 'selectIndexTyped requires at least one value.', + ); } return typedCases( index, { for (var valueIndex = 0; valueIndex < length; valueIndex++) - valueIndex: this[valueIndex] + valueIndex: this[valueIndex], }, conditionalType: ConditionalType.unique, defaultValue: defaultValue ?? 0, @@ -60,8 +64,9 @@ extension TypedIndexedLogic extension TypedSelectionLogic on Logic { /// Selects one value from [values], preserving its concrete structure type. LogicType selectFromTyped( - List values, - {LogicType? defaultValue, - String name = 'selectFrom'}) => + List values, { + LogicType? defaultValue, + String name = 'selectFrom', + }) => values.selectIndexTyped(this, defaultValue: defaultValue, name: name); } diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index 110ae6dc94..e7b4c43b30 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -39,10 +39,15 @@ String logicStructureShapeSignature(LogicStructure structure) => _logicShapeSignature(structure); void _validateMatchingStructure( - Logic first, Logic second, String operation, String path) { + Logic first, + Logic second, + String operation, + String path, +) { if (first.width != second.width) { throw LogicConstructionException( - '$operation operands differ in width at $path.'); + '$operation operands differ in width at $path.', + ); } if (first is LogicStructure || second is LogicStructure) { @@ -51,7 +56,8 @@ void _validateMatchingStructure( first.runtimeType != second.runtimeType || first.elements.length != second.elements.length) { throw LogicConstructionException( - '$operation operands differ in structure at $path.'); + '$operation operands differ in structure at $path.', + ); } if (first is BaseLogicArray && second is BaseLogicArray) { @@ -60,13 +66,18 @@ void _validateMatchingStructure( first.numUnpackedDimensions != second.numUnpackedDimensions || first.isNet != second.isNet) { throw LogicConstructionException( - '$operation array operands differ in shape at $path.'); + '$operation array operands differ in shape at $path.', + ); } } for (var index = 0; index < first.elements.length; index++) { - _validateMatchingStructure(first.elements[index], second.elements[index], - operation, '$path[$index]'); + _validateMatchingStructure( + first.elements[index], + second.elements[index], + operation, + '$path[$index]', + ); } } } @@ -78,27 +89,35 @@ void _validateMatchingStructure( /// constructed instances of one structure type can use different root names. /// /// Throws a [LogicConstructionException] when the structures do not match. -void validateMatchingLogicStructure(LogicStructure first, LogicStructure second, - {String operation = 'Typed operation'}) => +void validateMatchingLogicStructure( + LogicStructure first, + LogicStructure second, { + String operation = 'Typed operation', +}) => _validateMatchingStructure(first, second, operation, 'root'); /// Clones [source] while preserving its concrete [LogicStructure] type. /// /// Throws a [LogicConstructionException] when [LogicStructure.clone] is not /// implemented covariantly by the concrete type. -LogicType typedClone(LogicType source, - {String? name}) { +LogicType typedClone( + LogicType source, { + String? name, +}) { final cloned = source.clone(name: name); if (cloned is! LogicType) { throw LogicConstructionException( - 'Structure clone did not preserve its concrete type.'); + 'Structure clone did not preserve its concrete type.', + ); } return cloned; } /// Creates a named, connected alias while preserving [source]'s concrete type. LogicType typedNamed( - LogicType source, String name) => + LogicType source, + String name, +) => typedClone(source, name: name)..gets(source); /// Type-preserving conveniences for concrete [LogicStructure] values. diff --git a/test/async_reset_test.dart b/test/async_reset_test.dart index 4e176a1682..82e1dbcdfc 100644 --- a/test/async_reset_test.dart +++ b/test/async_reset_test.dart @@ -114,7 +114,7 @@ void main() { ), 'FlipFlop with no asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), @@ -198,7 +198,7 @@ void main() { ), 'FlipFlop with asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), diff --git a/test/long_chain_test.dart b/test/long_chain_test.dart index 8a78ad7bb0..309c6d0e8e 100644 --- a/test/long_chain_test.dart +++ b/test/long_chain_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // long_chain_test.dart diff --git a/test/multimodule3_test.dart b/test/multimodule3_test.dart index 20a2703899..e1275682fe 100644 --- a/test/multimodule3_test.dart +++ b/test/multimodule3_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multimodule3_test.dart diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 12b30d1a3c..94922aeb37 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -46,17 +46,22 @@ void main() { // find a module with 'z' output 2 levels deep assert( - ftm.subModules - .where((pIn1) => pIn1.subModules + ftm.subModules + .where( + (pIn1) => pIn1.subModules .where((pIn2) => pIn2.outputs.containsKey('z')) - .isNotEmpty) - .isNotEmpty, - 'Should find a z two levels deep'); + .isNotEmpty, + ) + .isNotEmpty, + 'Should find a z two levels deep', + ); final synth = ftm.dumpSystemVerilog(); // "z = 1" means it correctly traversed down from inputs - assert(synth.contains('z = 1'), - 'Should correctly traverse from inputs to z=1'); + assert( + synth.contains('z = 1'), + 'Should correctly traverse from inputs to z=1', + ); }); } diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index bbd5061e08..d5641d305e 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -102,8 +102,8 @@ class FlopModule extends Module { } } -/// A custom [ScalarFlipFlop] used to verify inheritance-aware leaf matching. -class CustomFlipFlop extends ScalarFlipFlop { +/// A custom [FlipFlop] used to verify inheritance-aware leaf matching. +class CustomFlipFlop extends FlipFlop { CustomFlipFlop(super.clk, super.d); } diff --git a/test/shuffle_test.dart b/test/shuffle_test.dart index 3b77e6a18f..85e46bab63 100644 --- a/test/shuffle_test.dart +++ b/test/shuffle_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // shuffle_test.dart diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index d21eb44a73..3f736ea70c 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -87,7 +87,7 @@ class _TypedOperationsSynthesisHarness extends Module { first = addTypedInput('first', first); second = addTypedInput('second', second); - final selected = Mux(control, second, first).out; + final selected = StructureMux(control, second, first).out; final selectedByCase = typedCases( selector, {0: first, 1: second}, @@ -99,8 +99,8 @@ class _TypedOperationsSynthesisHarness extends Module { defaultValue: first, name: 'selected_by_index', ); - final passed = Passthrough(selectedByIndex).out; - final registered = FlipFlop( + final passed = StructurePassthrough(selectedByIndex).out; + final registered = StructureFlipFlop( clk, passed, reset: reset, @@ -145,12 +145,12 @@ void main() { final portPrototype = Logic.port('port_data', 4); final netDriver = Logic(width: 4); final net = LogicNet(width: 4)..gets(netDriver); - final constantMux = Mux(control, constant, ordinary); - final portMux = Mux(control, portPrototype, ordinary); - final netMux = Mux(control, net, ordinary); - final constantFlop = FlipFlop(clk, constant); - final portFlop = FlipFlop(clk, portPrototype); - final netFlop = FlipFlop(clk, net); + final constantMux = Mux(control, constant, ordinary); + final portMux = Mux(control, portPrototype, ordinary); + final netMux = Mux(control, net, ordinary); + final constantFlop = FlipFlop(clk, constant); + final portFlop = FlipFlop(clk, portPrototype); + final netFlop = FlipFlop(clk, net); await Future.wait([ constantMux.build(), portMux.build(), @@ -198,9 +198,9 @@ void main() { final typedArray1 = LogicArrayOf<_TypedLane>([2], _TypedLane.new, name: 'typed_array1'); - final muxedArray = Mux(selector[0], array1, array0).out; - final floppedArray = FlipFlop(clk, array0).q; - final passedArray = Passthrough(array0).out; + final muxedArray = StructureMux(selector[0], array1, array0).out; + final floppedArray = StructureFlipFlop(clk, array0).q; + final passedArray = StructurePassthrough(array0).out; final casedArray = typedCases(selector, {0: array0, 1: array1}); final selectedArray = [array0, array1].selectIndexTyped(selector); final selectedFromArray = selector.selectFromTyped([array0, array1]); @@ -212,9 +212,10 @@ void main() { stages: [(stage) => stage.value], ).output; - final muxedTypedArray = Mux(selector[0], typedArray1, typedArray0).out; - final floppedTypedArray = FlipFlop(clk, typedArray0).q; - final passedTypedArray = Passthrough(typedArray0).out; + final muxedTypedArray = + StructureMux(selector[0], typedArray1, typedArray0).out; + final floppedTypedArray = StructureFlipFlop(clk, typedArray0).q; + final passedTypedArray = StructurePassthrough(typedArray0).out; final casedTypedArray = typedCases(selector, {0: typedArray0, 1: typedArray1}); final selectedTypedArray = @@ -257,7 +258,7 @@ void main() { final control = Logic(); final d1 = _TypedPacket(name: 'd1'); final d0 = _TypedPacket(name: 'd0'); - final muxModule = Mux(control, d1, d0); + final muxModule = StructureMux(control, d1, d0); await muxModule.build(); expect(muxModule, isA>()); @@ -287,13 +288,13 @@ void main() { test('Mux preserves structure type with a constant control', () { final d1 = _TypedPacket(name: 'd1'); final d0 = _TypedPacket(name: 'd0'); - expect(Mux(Const(0), d1, d0).out, isA<_TypedPacket>()); - expect(Mux(Const(1), d1, d0).out, isA<_TypedPacket>()); + expect(typedMux(Const(0), d1, d0), isA<_TypedPacket>()); + expect(typedMux(Const(1), d1, d0), isA<_TypedPacket>()); }); test('Mux rejects differently shaped typed arrays', () { expect( - () => Mux(Logic(), _TypedPacket(laneDimensions: const [2, 2]), + () => StructureMux(Logic(), _TypedPacket(laneDimensions: const [2, 2]), _TypedPacket(laneDimensions: const [4])), throwsA(isA())); }); @@ -303,9 +304,9 @@ void main() { final control = Logic(); final constant = _ConstPacket(value: 0xa, name: 'constant'); final variable = _ConstPacket(value: null, name: 'variable'); - final muxModule = Mux(control, constant, variable); - final passthrough = Passthrough(constant); - final flipFlop = FlipFlop(clk, constant); + final muxModule = StructureMux(control, constant, variable); + final passthrough = StructurePassthrough(constant); + final flipFlop = StructureFlipFlop(clk, constant); await Future.wait([ muxModule.build(), passthrough.build(), @@ -341,10 +342,10 @@ void main() { test('typed operations reject clones, nets, and reset mismatches', () { expect(() => typedClone(_BadClonePacket()), throwsA(isA())); - expect(() => Passthrough<_NetPacket>(_NetPacket()), + expect(() => StructurePassthrough<_NetPacket>(_NetPacket()), throwsA(isA())); expect( - () => FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + () => StructureFlipFlop<_TypedPacket>(Logic(), _TypedPacket(), reset: Logic(), resetValue: _TypedPacket(laneDimensions: const [1, 2])), throwsA(isA())); @@ -360,17 +361,17 @@ void main() { test('typed module definitions encode structure and constant reset identity', () { - final sameShapeFirst = Mux<_TypedPacket>(Logic(), + final sameShapeFirst = StructureMux<_TypedPacket>(Logic(), _TypedPacket(name: 'first_d1'), _TypedPacket(name: 'first_d0')); - final sameShapeSecond = Mux<_TypedPacket>(Logic(), + final sameShapeSecond = StructureMux<_TypedPacket>(Logic(), _TypedPacket(name: 'second_d1'), _TypedPacket(name: 'second_d0')); - final differentShape = Mux<_TypedPacket>( + final differentShape = StructureMux<_TypedPacket>( Logic(), _TypedPacket(laneDimensions: const [1, 2], name: 'different_d1'), _TypedPacket(laneDimensions: const [1, 2], name: 'different_d0')); - final resetZero = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + final resetZero = StructureFlipFlop<_TypedPacket>(Logic(), _TypedPacket(), reset: Logic(), resetValue: 0); - final resetOne = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + final resetOne = StructureFlipFlop<_TypedPacket>(Logic(), _TypedPacket(), reset: Logic(), resetValue: 1); expect(sameShapeFirst.definitionName, sameShapeSecond.definitionName); @@ -389,7 +390,7 @@ void main() { final reset = Logic(); final enable = Logic(); final d = _TypedPacket(name: 'd'); - final flipFlop = FlipFlop( + final flipFlop = StructureFlipFlop( clk, d, en: enable, @@ -434,7 +435,7 @@ void main() { final reset = Logic(); final d = _TypedPacket(name: 'reset_order_d'); final resetValue = LogicValue.ofString('000111100110100101101'); - final flipFlop = FlipFlop( + final flipFlop = StructureFlipFlop( clk, d, reset: reset, @@ -472,7 +473,7 @@ void main() { test('Passthrough infers typed contracts', () async { final scalar = Passthrough(Logic(width: 4)); final input = _TypedPacket(name: 'passthrough_input'); - final structured = Passthrough(input); + final structured = StructurePassthrough(input); await Future.wait([scalar.build(), structured.build()]); expect(scalar, isA>()); From 62ca033c104fd4da4a1e1d7a4baa138c396d5642 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 4 Sep 2026 15:38:05 -0700 Subject: [PATCH 04/33] go back to generateSynth() for now in testing verilog --- test/multimodule4_test.dart | 2 +- test/multimodule5_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 94922aeb37..599654a325 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -56,7 +56,7 @@ void main() { 'Should find a z two levels deep', ); - final synth = ftm.dumpSystemVerilog(); + final synth = ftm.generateSynth(); // "z = 1" means it correctly traversed down from inputs assert( diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index f9db9ebf96..84a91a6e88 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -34,7 +34,7 @@ void main() { final mod = TopModule(Logic()); await mod.build(); - final sv = mod.dumpSystemVerilog(); + final sv = mod.generateSynth(); expect(sv, contains('Passthrough')); }); From 33f0d29e68224bfa087e785f0943d5eeb8e5ca86 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 4 Sep 2026 23:27:49 -0700 Subject: [PATCH 05/33] ambiguity on typedCases, missing propagation of LogicArrayOf through array methods --- lib/src/modules/conditionals/case.dart | 23 +++++++++++++++++++++++ test/typed_operations_test.dart | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index 7b1b4671de..7843998bb5 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -164,6 +164,22 @@ LogicType typedCases( for (final value in conditions.values.skip(1)) { validateMatchingLogicStructure(value, prototype, operation: 'typedCases'); } + for (final condition in conditions.keys) { + if (condition is Logic && condition.width != expression.width) { + throw SignalWidthMismatchException.forDynamic( + condition, + expression.width, + condition.width, + ); + } + if (condition is LogicValue && condition.width != expression.width) { + throw SignalWidthMismatchException.forDynamic( + condition, + expression.width, + condition.width, + ); + } + } if (defaultValue is LogicStructure) { validateMatchingLogicStructure( defaultValue, @@ -172,6 +188,13 @@ LogicType typedCases( ); } else if (defaultValue is Logic && defaultValue.width != prototype.width) { throw PortWidthMismatchException.equalWidth(defaultValue, prototype); + } else if (defaultValue is LogicValue && + defaultValue.width != prototype.width) { + throw SignalWidthMismatchException.forDynamic( + defaultValue, + prototype.width, + defaultValue.width, + ); } final result = typedClone(prototype, name: name); diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index 3f736ea70c..08e862584f 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -359,6 +359,28 @@ void main() { throwsA(isA())); }); + test('typedCases rejects mismatched LogicValue widths', () { + final selector = Logic(width: 2); + final first = _TypedPacket(name: 'first'); + final second = _TypedPacket(name: 'second'); + + expect( + () => typedCases(selector, { + LogicValue.ofInt(4, 3): first, + 1: second, + }), + throwsA(isA()), + ); + expect( + () => typedCases( + selector, + {0: first, 1: second}, + defaultValue: LogicValue.ofInt(0, first.width + 1), + ), + throwsA(isA()), + ); + }); + test('typed module definitions encode structure and constant reset identity', () { final sameShapeFirst = StructureMux<_TypedPacket>(Logic(), From 4832b3c827ab66fbca560f4571d4773f3c7cfa5c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 8 Sep 2026 12:05:08 -0700 Subject: [PATCH 06/33] added issues resolved, fixed SystemVerilog assignment issue --- doc/user_guide/_docs/A20-logic-arrays.md | 49 ++++++++++-- lib/src/modules/bus.dart | 54 ++++++++++++++ ...systemverilog_synth_module_definition.dart | 27 +++++++ test/array_collapsing_test.dart | 4 +- test/sv_gen_test.dart | 74 +++++++++++++++++++ test/swizzle_test.dart | 6 +- 6 files changed, 202 insertions(+), 12 deletions(-) diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 8a4478a27a..2d623f6ec5 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -27,7 +27,44 @@ LogicArray([5, 5, 5], 128); As long as the total width of a `LogicArray` and another type of `Logic` (including `Logic`, `LogicStructure`, and another `LogicArray`) are the same, assignments and bitwise operations will work in per-element order. This means you can assign two `LogicArray`s of different dimensions to each other as long as the total width matches. -## Typed arrays +## Assigning arrays + +Use `<=` for continuous assignments outside conditional blocks. You can assign +an entire array or an individual element: + +```dart +final source = LogicArray([4], 8, name: 'source'); +final copied = LogicArray([4], 8, name: 'copied'); +final updated = LogicArray([4], 8, name: 'updated'); +final replacement = Logic(name: 'replacement', width: 8); + +copied <= source; +updated.elements[0] <= replacement; +``` + +Use `<` for assignments inside `Combinational`, just as with ordinary +`Logic` signals. Whole-array conditional assignments preserve the array's +element ordering: + +```dart +final select = Logic(name: 'select'); +final sourceA = LogicArray([4], 8, name: 'sourceA'); +final sourceB = LogicArray([4], 8, name: 'sourceB'); +final selected = LogicArray([4], 8, name: 'selected'); + +Combinational([ + If( + select, + then: [selected < sourceA], + orElse: [selected < sourceB], + ), +]); +``` + +The `elements` list follows array index order, so `array.elements[0]` +corresponds to `array[0]` in generated SystemVerilog. + +## Typed and value-domain arrays Use `TypedLogicArray` when every array position has the same specialized hardware type and associated semantic value type. `LogicArray` is @@ -191,12 +228,14 @@ Use the existing `addTypedInput`, `addTypedOutput`, and `addTypedInOut` methods ## Type-preserving operations -`LogicArray` and `LogicArrayOf` can be used with `Mux`, `FlipFlop`, and `Passthrough`. The output retains the array's concrete type, dimensions, and specialized leaf type: +Use `StructureMux`, `StructureFlipFlop`, and `StructurePassthrough` when the +output must retain the array's concrete type, dimensions, and specialized leaf +type: ```dart -final selected = Mux(select, samplesA, samplesB).out; -final delayed = FlipFlop(clk, selected, reset: reset).q; -final forwarded = Passthrough(delayed).out; +final selected = StructureMux(select, samplesA, samplesB).out; +final delayed = StructureFlipFlop(clk, selected, reset: reset).q; +final forwarded = StructurePassthrough(delayed).out; final bottomRightData = forwarded.elementAt([1, 2]).data; ``` diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index 7fbaee7bd9..533ce5f4e3 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -272,6 +272,20 @@ class Swizzle extends Module with InlineSystemVerilog { @override String get resultSignalName => _out; + @override + String instantiationVerilog( + String instanceType, + String instanceName, + Map ports, + ) { + final result = ports[resultSignalName]; + final inputPorts = Map.of(ports)..remove(resultSignalName); + final packedArrayAlias = _packedArrayAlias(inputPorts); + return packedArrayAlias == null + ? super.instantiationVerilog(instanceType, instanceName, ports) + : 'assign $result = $packedArrayAlias;'; + } + @override String inlineVerilog(Map inputs) { assert( @@ -281,6 +295,11 @@ class Swizzle extends Module with InlineSystemVerilog { ' but saw $inputs with ${inputs.length} values.', ); + final packedArrayAlias = _packedArrayAlias(inputs); + if (packedArrayAlias != null) { + return packedArrayAlias; + } + // Calculate all width descriptions upfront to determine alignment final validInputs = _swizzleInputs.reversed.where((e) => e.width > 0).toList(); @@ -351,6 +370,41 @@ ${inputLines.join('\n')} }'''; } + /// Returns the source array for a natural-order packing of a packed 1D array. + String? _packedArrayAlias(Map inputs) { + if (isNet || _swizzleInputs.isEmpty) { + return null; + } + + final firstSource = _swizzleInputs.first.srcConnection; + final array = firstSource?.parentStructure; + if (array is! BaseLogicArray || + array.dimensions.length != 1 || + array.numUnpackedDimensions != 0 || + array.elements.length != _swizzleInputs.length) { + return null; + } + + String? arrayExpression; + for (var index = 0; index < _swizzleInputs.length; index++) { + final input = _swizzleInputs[index]; + final source = input.srcConnection; + if (source?.parentStructure != array || source?.arrayIndex != index) { + return null; + } + + final match = _singleBitSelectRegex.firstMatch(inputs[input.name]!); + if (match == null || + int.parse(match.group(2)!) != index || + (arrayExpression != null && match.group(1) != arrayExpression)) { + return null; + } + arrayExpression ??= match.group(1); + } + + return arrayExpression; + } + /// Rewrites runs of adjacent descending single-bit selects from the same /// packed signal into wider SystemVerilog slices. /// diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 1124580576..4fbff4fc49 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -37,6 +37,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { _expandNestedUnpackedArrayAssignments(); _inlinePackedRangesIntoSubmoduleInputs(); _collapseAggregateConnections(); + _collapsePackedArrayLogicAssignments(); _collapseWholeNetBuses(); _forwardPassthroughElementsIntoInlineables(); _replaceNetConnections(); @@ -195,6 +196,32 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { logic is BaseLogicArray || (logic is LogicStructure && logic.elements.any(_containsArray)); + /// Collapses a full packed [Logic] split across 1D array elements. + void _collapsePackedArrayLogicAssignments() { + final subsetsByOutput = _logicSubsetLookups(); + for (final arraySynth in outputs.where((signal) => signal.isArray)) { + final array = arraySynth.logics.whereType().firstOrNull; + if (array == null || + array.dimensions.length != 1 || + array.numUnpackedDimensions != 0) { + continue; + } + + final packedSource = _packedLogicSubsetSource( + array.elements.map(getSynthLogic).toList(growable: false), + subsetsByOutput, + expectedWidth: array.width, + ); + if (packedSource == null) { + continue; + } + + assignments.add(SynthAssignment(packedSource.source, arraySynth)); + for (final subset in packedSource.subsets) { + subset.clearInstantiation(); + } + } + } /// Inlines a fully covered packed bus into its sole submodule input. /// /// Each driver must cover the next contiguous destination range and supply diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 18c18d3f71..be7431f72b 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -2894,7 +2894,7 @@ void main() { expect(topBody, isNot(contains('_subset'))); expect(topBody, isNot(contains('assign dst[0]'))); - expect(topBody, contains('assign dst = src[7:0];')); + expect(topBody, contains('assign dst = src;')); final vectors = [ for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) @@ -3423,7 +3423,7 @@ void main() { expect(topBody, contains('manual_subset')); expect(topBody, contains('assign manual_subset[4:2] = src[4:2];')); - expect(topBody, contains('assign y = manual_subset[5:0];')); + expect(topBody, contains('assign y = manual_subset;')); final vectors = [ for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 7f95bb305f..820c232916 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -467,6 +467,38 @@ class ModWithPartialArrayAssignment extends Module { } } +class PackedArrayToLogic extends Module { + PackedArrayToLogic(LogicArray array) { + array = addInputArray( + 'array', + array, + dimensions: array.dimensions, + elementWidth: array.elementWidth, + numUnpackedDimensions: array.numUnpackedDimensions, + ); + addOutput('out', width: array.width) <= array; + } +} + +class LogicToPackedArray extends Module { + LogicToPackedArray(Logic data) { + data = addInput('data', data, width: 8); + addOutputArray('out', dimensions: [4], elementWidth: 2) <= data; + } +} + +class ReversedPackedArrayToLogic extends Module { + ReversedPackedArrayToLogic(LogicArray array) { + array = addInputArray( + 'array', + array, + dimensions: array.dimensions, + elementWidth: array.elementWidth, + ); + addOutput('out', width: array.width) <= array.elements.swizzle(); + } +} + class ModWithConstInlineUnaryOp extends Module { ModWithConstInlineUnaryOp() { addOutput('b', width: 8) <= NotGate(Const(0, width: 8)).out; @@ -888,6 +920,48 @@ void main() { SimCompare.checkIverilogVector(mod, vectors); }); + test('packed one-dimensional array assigns directly to Logic', () async { + final mod = PackedArrayToLogic(LogicArray([4], 2, name: 'array')); + await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, contains('assign out = array;'), reason: sv); + expect(sv, isNot(contains('// swizzle')), reason: sv); + + final vectors = [ + Vector({'array': 0x00}, {'out': 0x00}), + Vector({'array': 0xa5}, {'out': 0xa5}), + Vector({'array': 0xff}, {'out': 0xff}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('Logic assigns directly to packed one-dimensional array', () async { + final mod = LogicToPackedArray(Logic(width: 8)); + await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, contains('assign out = data;'), reason: sv); + + final vectors = [ + Vector({'data': 0x00}, {'out': 0x00}), + Vector({'data': 0xa5}, {'out': 0xa5}), + Vector({'data': 0xff}, {'out': 0xff}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('reordered packed array assignment retains its swizzle', () async { + final mod = ReversedPackedArrayToLogic(LogicArray([4], 2, name: 'array')); + await mod.build(); + final sv = mod.generateSynth(); + + expect(sv, isNot(contains('assign out = array;')), reason: sv); + expect(sv, contains('// swizzle'), reason: sv); + }); + group('connected ports and pruning', () { for (final naming in [Naming.renameable, Naming.mergeable]) { test('with naming $naming', () async { diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index c155927189..6269b01afd 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -542,11 +542,7 @@ assign b = { 1'h0, /* 34 */ a, /* 33:26 */ x, /* 25:22 */ -({ -y[2], /* 5:4 */ -y[1], /* 3:2 */ -y[0] /* 1:0 */ -}), /* 21:16 */ +(y), /* 21:16 */ 5'h3, /* 15:11 */ ({ 3'h2, /* 10:8 */ From ff8243efe46debb5a8a95981a88c7aed120142ab Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 8 Sep 2026 12:19:26 -0700 Subject: [PATCH 07/33] fixed SV golden expected output --- test/net_bus_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index e09c1c8293..7a6cae924b 100644 --- a/test/net_bus_test.dart +++ b/test/net_bus_test.dart @@ -788,8 +788,7 @@ void main() { expect( sv, contains('assign _swizzled = ' - '{({({in0[1][1],in0[1][0]}),({in0[0][1],in0[0][0]})}),' - '(in1[3:0])};')); + '{({(in0[1]),(in0[0])}),(in1)};')); }); test('net array 2', () async { From 85671c8fc17baf318c2a9f616dbd0356ca60b0a2 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 7 Oct 2026 21:38:55 -0700 Subject: [PATCH 08/33] typedlogicarray checks in external sims --- CHANGELOG.md | 13 +- doc/user_guide/_docs/A19-logic-structures.md | 65 ++- doc/user_guide/_docs/A20-logic-arrays.md | 17 +- lib/src/modules/conditionals/case.dart | 136 ++--- lib/src/modules/conditionals/flop.dart | 459 ++++++++++------- lib/src/modules/gates.dart | 210 +++++--- lib/src/modules/modules.dart | 1 - lib/src/modules/operation_utils.dart | 109 ++++ lib/src/modules/passthrough.dart | 150 ++++-- lib/src/modules/pipeline.dart | 4 +- lib/src/modules/typed_op.dart | 24 - lib/src/selection.dart | 56 +-- lib/src/signals/logic.dart | 47 +- lib/src/signals/logic_structure.dart | 85 +++- test/async_reset_test.dart | 8 +- test/module_test.dart | 2 +- test/multimodule5_test.dart | 2 +- test/netlist_synthesizer_test.dart | 4 +- test/typed_operations_test.dart | 493 ++++++++++++++++--- 19 files changed, 1262 insertions(+), 623 deletions(-) create mode 100644 lib/src/modules/operation_utils.dart delete mode 100644 lib/src/modules/typed_op.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b26efa4d76..bcf408a702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,18 @@ - Refined the typed-array hierarchy so `LogicArray` directly specializes `TypedLogicArray`, and strengthened custom `TypedLogicArray` cloning to preserve runtime types, codecs, unpacked dimensions, and constructor-only naming prefixes without exposing them as public axis metadata (). - Expanded `TypedLogicArray` support for recursively nested typed and ordinary arrays, structures containing array fields, mixed packed/unpacked dimensions, structured net and inout ports, and recursive netlist type metadata. Fixed generated SystemVerilog for array-valued elements to use packed offsets consistent with declarations and module-port connections, avoiding invalid chained selections in Icarus and Verilator (). - Preserved configured SystemVerilog output object types for nested arrays and added the opt-in `iverilogWorkaroundForUnpackedArrayVariables` setting for child-driven unpacked array variables that remain unknown in Icarus Verilog 12.0 (). -- Added structure-preserving typed operations for multiplexing, flip-flops, case and indexed selection, pipelines, naming, cloning, and pass-through modules. Nested `LogicArray` and `TypedLogicArray` boundaries remain available through the concrete output type while existing scalar operations retain their `Logic` APIs. +- Upgraded `Mux`, `FlipFlop`, `Passthrough`, `mux`, `flop`, `cases`, + `selectIndex`, and `selectFrom` to preserve compatible concrete `Logic` + subtypes, including nested `LogicArray` and `TypedLogicArray` boundaries. + Removed the parallel structure-only operation APIs. +- **Migration:** plain `Const` and `LogicNet` sources no longer infer + driveable outputs for operations that construct hardware. Request + ``, for example `flop(clk, Const(1))`, to normalize them. + Use an operation `outputGenerator` when an explicit output representation is + required, including mixed packed and structured `cases` branches. +- Added public `cloneTyped()` and `namedTyped()` conveniences for retaining a + receiver's static type. `Const.cloneTyped()` remains a literal clone; + `Const.namedTyped()` is rejected because a named alias must be driveable. - Added configurable waveform timescales, recording windows, and output overwrite policies (). - Added opt-in `DebugSession` setup for source tracing, netlist and FLC services, and live waveform recording (). - Expanded reusable `rohd_devtools_widgets` components for hierarchy browsing, signal formatting, bit expansion, overlays, help, capture, and cross-probing (). diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index be10a32d8d..547d136006 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -19,37 +19,44 @@ Ports with matching types to the original `LogicStructure` can be created using ## Type-preserving operations -`StructureMux`, `StructureFlipFlop`, and `StructurePassthrough` preserve a -concrete `LogicStructure` type when their structure operands match: +`Mux`, `FlipFlop`, and `Passthrough` preserve a concrete `Logic` subtype when +their operands establish a compatible output representation: ```dart -final selected = StructureMux(select, packet1, packet0).out; -final registered = - StructureFlipFlop(clk, selected, reset: reset).q; -final forwarded = StructurePassthrough(registered).out; +final selected = Mux(select, packet1, packet0).out; +final registered = FlipFlop(clk, selected, reset: reset).q; +final forwarded = Passthrough(registered).out; // All three values have type Packet. forwarded.valid <= selected.valid; ``` -The same behavior applies to `LogicArray` and `LogicArrayOf`, including nested typed arrays. Both mux operands must have the same concrete type and recursive shape: field widths, array dimensions, and packing hints must match. +The same behavior applies to `LogicArray` and `LogicArrayOf`, including +nested typed arrays. A mux uses its `d0` operand as the default output +prototype. Both operands must have matching concrete types and recursive +geometry: field widths, array dimensions, packing hints, and leaf structure. +Pass an `outputGenerator` to `Mux`, `FlipFlop`, `mux`, `flop`, or `cases` to +select a different result representation. Use `Passthrough.withOutput` for +the corresponding pass-through override while retaining the historical +positional `name` argument of `Passthrough`. -Typed operations can consume a structure containing `Const` leaves when its +These operations can consume a structure containing `Const` leaves when its `clone()` implementation returns the same concrete structure type with driveable `Logic` leaves. The operation preserves the structure type while normalizing its input port and output to driveable logic. This supports domain-specific constant structures, such as a floating-point structure -assembled from constant sign, exponent, and mantissa fields. - -The existing `Mux`, `FlipFlop`, and `Passthrough` APIs always produce ordinary -`Logic`. They accept structures as packed inputs when widths match, but do not -preserve named fields: +assembled from constant sign, exponent, and mantissa fields. Plain `Const` and +`LogicNet` sources cannot infer a driveable result type, so normalize them +explicitly: ```dart -final Logic selected = Mux(select, packet1, packet0).out; -final Logic registered = FlipFlop(clk, selected).q; +final Logic registered = flop(clk, Const(0x5a, width: 8)); ``` +For a valid constant mux control without an `outputGenerator`, `mux` returns +the selected source directly. Supplying an `outputGenerator` always constructs +a fresh driveable output, including for a constant control. + ### Case selection `Case` can assign structures directly because its branches contain ordinary @@ -70,26 +77,38 @@ Direct structure assignments require matching total widths and map bits in packed leaf order. They do not require the source and destination to have the same concrete structure type. -Use `typedCases` when the operation should construct and return a value while -preserving its concrete type: +Use generic `cases` when the operation should construct and return a value +while preserving its concrete type: ```dart -final Packet selected = typedCases( +final selected = cases( selector, {0: packet0, 1: packet1}, defaultValue: fallback, + outputGenerator: packet0.clone, ); ``` -All structured values passed to `typedCases` must have the same concrete type -and recursive shape. The legacy `cases` helper accepts structures as packed -values but always returns an ordinary `Logic`. +The output generator is also how a structured result can accept deliberately +packed branch values. A packed source is interpreted using the generated +output's representation. Different structured domain types should be converted +to `packed` explicitly before that bit reinterpretation. -Use `selectIndexTyped` and `selectFromTyped` for structure-preserving indexed -selection. `StructurePipeline` preserves the same concrete type at every +Use `selectIndex` and `selectFrom` for structure-preserving indexed selection. +`StructurePipeline` preserves the same concrete type at every registered pipeline boundary. Specify `T` when creating a pipeline with inline stage transforms so Dart can type the transform parameter. +### Migrating type-preserving operations + +The former `StructureMux`, `StructureFlipFlop`, `StructurePassthrough`, +`typedCases`, `typedMux`, `typedFlop`, `selectIndexTyped`, and +`selectFromTyped` entry points are replaced by the corresponding established +generic APIs. Use `cloneTyped()` and `namedTyped()` when the receiver's static +type should be retained without a cast. `Const.cloneTyped()` remains a literal +clone, while `Const.namedTyped()` is rejected because a named alias is +driveable; use `Const.named()` when an ordinary `Logic` alias is intended. + ## Using `LogicStructure` to group signals The simplest way to use a `LogicStructure` is to just use its constructor, which requires a collection of `Logic`s. diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 2d623f6ec5..d9b9432eb4 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -228,19 +228,22 @@ Use the existing `addTypedInput`, `addTypedOutput`, and `addTypedInOut` methods ## Type-preserving operations -Use `StructureMux`, `StructureFlipFlop`, and `StructurePassthrough` when the -output must retain the array's concrete type, dimensions, and specialized leaf -type: +Use generic `Mux`, `FlipFlop`, and `Passthrough` when the output must retain +the array's concrete type, dimensions, and specialized leaf type: ```dart -final selected = StructureMux(select, samplesA, samplesB).out; -final delayed = StructureFlipFlop(clk, selected, reset: reset).q; -final forwarded = StructurePassthrough(delayed).out; +final selected = Mux(select, samplesA, samplesB).out; +final delayed = FlipFlop(clk, selected, reset: reset).q; +final forwarded = Passthrough(delayed).out; final bottomRightData = forwarded.elementAt([1, 2]).data; ``` -The mux inputs must have matching concrete array types and geometry, including dimensions, leaf widths, packed/unpacked configuration, and leaf structure. Use `typedCases`, `selectIndexTyped`, or `selectFromTyped` when selecting one complete typed array from multiple choices. Specify the array type parameter on `StructurePipeline` when its stages use inline transforms. +The mux inputs must have matching concrete array types and geometry, including +dimensions, leaf widths, packed/unpacked configuration, and leaf structure. +Use generic `cases`, `selectIndex`, or `selectFrom` when selecting one complete +typed array from multiple choices. Specify the array type parameter on +`StructurePipeline` when its stages use inline transforms. ## Elements of arrays diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index 7843998bb5..b15b293c8e 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -11,6 +11,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/conditionals/ssa.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; /// Represents a single case within a [Case] block. class CaseItem { @@ -32,20 +33,28 @@ class CaseItem { /// It is used to assign a signal based on a condition with multiple cases to /// consider. For e.g., this can be used instead of a nested [mux]. /// -/// The result is of type [Logic] and it is determined by conditionaly matching -/// the expression with the values of each item in conditions. If width of the -/// input is not provided, then the width of the result is inferred from the -/// width of the entries. Bare [Enum] values are not supported as results -/// because an untyped result has no hardware encoding for them; use explicitly -/// mapped [LogicEnum] signals instead. -Logic cases(Logic expression, Map conditions, - {int? width, - ConditionalType conditionalType = ConditionalType.none, - dynamic defaultValue}) { +/// The result is of type [LogicType] and is determined by conditionally +/// matching [expression] with values in [conditions]. +/// +/// When [outputGenerator] is omitted, a requested [Logic] result is +/// normalized to ordinary [Logic]. For a more specific [LogicType], the first +/// matching typed branch supplies the output prototype. Supply +/// [outputGenerator] to establish an explicit result representation when +/// sources are mixed or no typed branch is available. +LogicType cases( + Logic expression, + Map conditions, { + int? width, + ConditionalType conditionalType = ConditionalType.none, + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, + String name = 'result', +}) { final resultValues = [ ...conditions.values, - if (defaultValue != null) defaultValue + if (defaultValue != null) defaultValue, ]; + if (resultValues.any((value) => value is Enum)) { throw ArgumentError.value( resultValues, @@ -75,7 +84,8 @@ Logic cases(Logic expression, Map conditions, width ??= inferredWidth; } - if (width == null) { + final prototype = resultValues.whereType().firstOrNull; + if (width == null && prototype == null && outputGenerator == null) { throw SignalWidthMismatchException.forNull(conditions); } @@ -97,6 +107,14 @@ Logic cases(Logic expression, Map conditions, } } + final result = createOperationOutput( + width: width ?? prototype?.width, + name: name, + operation: 'cases<$LogicType>', + prototype: prototype, + outputGenerator: outputGenerator, + ); + for (final condition in conditions.entries) { if (condition.key is Logic) { if (expression.width != (condition.key as Logic).width) { @@ -119,90 +137,22 @@ Logic cases(Logic expression, Map conditions, } } - final result = Logic(name: 'result', width: width, naming: Naming.mergeable); - - Combinational([ - Case( - expression, - [ - for (final condition in conditions.entries) - CaseItem( - conditionLogic(condition.key), - [result < condition.value], - ), - ], - conditionalType: conditionalType, - defaultItem: defaultValue != null ? [result < defaultValue] : null, - ), - ]); - - return result; -} - -/// Selects a matching [LogicStructure] value using a hardware case expression. -/// -/// Every condition value must have the same concrete type and recursive shape. -/// [defaultValue] may be another matching structure or any packed value -/// accepted by [LogicStructure.operator <]. If omitted, unmatched expression -/// values follow the same incomplete-assignment and latch semantics as [cases]. -/// [ConditionalType.unique] and [ConditionalType.priority] do not make an -/// otherwise incomplete case exhaustive. -LogicType typedCases( - Logic expression, - Map conditions, { - ConditionalType conditionalType = ConditionalType.none, - dynamic defaultValue, - String name = 'result', -}) { - if (conditions.isEmpty) { - throw LogicConstructionException( - 'typedCases requires at least one typed condition value.', - ); - } - - final prototype = conditions.values.first; - for (final value in conditions.values.skip(1)) { - validateMatchingLogicStructure(value, prototype, operation: 'typedCases'); - } - for (final condition in conditions.keys) { - if (condition is Logic && condition.width != expression.width) { - throw SignalWidthMismatchException.forDynamic( - condition, - expression.width, - condition.width, + for (final conditionValue in resultValues) { + if (conditionValue is Logic) { + validateOperationSource( + conditionValue, + result, + operation: 'cases branch', ); - } - if (condition is LogicValue && condition.width != expression.width) { + } else if (conditionValue is LogicValue && + conditionValue.width != result.width) { throw SignalWidthMismatchException.forDynamic( - condition, - expression.width, - condition.width, + conditionValue, + result.width, + conditionValue.width, ); } } - if (defaultValue is LogicStructure) { - validateMatchingLogicStructure( - defaultValue, - prototype, - operation: 'typedCases default', - ); - } else if (defaultValue is Logic && defaultValue.width != prototype.width) { - throw PortWidthMismatchException.equalWidth(defaultValue, prototype); - } else if (defaultValue is LogicValue && - defaultValue.width != prototype.width) { - throw SignalWidthMismatchException.forDynamic( - defaultValue, - prototype.width, - defaultValue.width, - ); - } - - final result = typedClone(prototype, name: name); - if (result.hasConsts || result.hasNets) { - throw LogicConstructionException( - 'typedCases result clones cannot contain constants or nets.', - ); - } Combinational([ Case( @@ -210,9 +160,7 @@ LogicType typedCases( [ for (final condition in conditions.entries) CaseItem( - condition.key is Logic - ? condition.key as Logic - : Const(condition.key, width: expression.width), + conditionLogic(condition.key), [result < condition.value], ), ], diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index bc4519074c..f9d50569f1 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -8,148 +8,246 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; -/// Constructs a positive edge triggered flip flop on [clk]. +/// Constructs a positive-edge-triggered flip-flop preserving [LogicType]. /// -/// It returns [FlipFlop.q]. -/// -/// When the optional [en] is provided, an additional input will be created for -/// flop. If optional [en] is high or not provided, output will vary as per -/// input[d]. For low [en], output remains frozen irrespective of input [d]. -/// -/// When the optional [reset] is provided, the flop will be reset (active-high). -/// If no [resetValue] is provided, the reset value is always `0`. Otherwise, -/// it will reset to the provided [resetValue]. -/// -/// If [asyncReset] is true, the [reset] signal (if provided) will be treated -/// as an async reset. If [asyncReset] is false, the reset signal will be -/// treated as synchronous. -Logic flop( +/// The input [d] supplies the output representation when [outputGenerator] is +/// omitted. Request `` explicitly to normalize a plain [Const] or +/// [LogicNet] source into a driveable scalar result. +LogicType flop( Logic clk, - Logic d, { + LogicType d, { Logic? en, Logic? reset, dynamic resetValue, bool asyncReset = false, + LogicType Function({String? name})? outputGenerator, }) => - FlipFlop( + FlipFlop( clk, d, en: en, reset: reset, resetValue: resetValue, asyncReset: asyncReset, + outputGenerator: outputGenerator, ).q; -/// A flip-flop with an output represented by [LogicType]. +/// A positive-edge-triggered flip-flop preserving `LogicType`. /// -/// Implementations decide how an input is normalized into the promised output -/// type. [FlipFlop] produces an ordinary [Logic], while -/// [StructureFlipFlop] preserves matching structure and array boundaries. -abstract class TypedFlipFlop - extends TypedOp { +/// The returned `q` is created from `d`'s clone by default, or from +/// `outputGenerator` when supplied. Dynamic operation outputs must be +/// driveable, so a concrete [Const] or [LogicNet] result type is rejected. +class FlipFlop extends Module { /// Registered output. - LogicType get q; + LogicType get q => _scalarQ; - @override + late final LogicType _scalarQ; + + /// Alias for [q] shared with other single-output operations. LogicType get out => q; - /// Creates a typed flip-flop implementation. - TypedFlipFlop({ - super.name = 'typed_flip_flop', - super.reserveName, - super.definitionName, - super.reserveDefinitionName, - }); + /// Indicates whether reset is asynchronous. + bool get asyncReset => _scalarAsyncReset; + + late final bool _scalarAsyncReset; + + /// Constant packed reset value, when one is used. + LogicValue? get constantResetValue => _scalarConstantResetValue; + + late final LogicValue? _scalarConstantResetValue; + + /// Constructs a flip-flop preserving [LogicType]. + factory FlipFlop( + Logic clk, + LogicType d, { + Logic? en, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, + LogicType Function({String? name})? outputGenerator, + String name = 'flipflop', + }) { + if (clk.width != 1) { + throw PortWidthMismatchException(clk, 1); + } + if (en != null && en.width != 1) { + throw PortWidthMismatchException(en, 1); + } + if (reset != null && reset.width != 1) { + throw PortWidthMismatchException(reset, 1); + } + + final outputSchema = createOperationOutput( + width: d.width, + name: 'q', + operation: 'FlipFlop<$LogicType>', + prototype: d, + outputGenerator: outputGenerator, + ); + validateOperationSource(d, outputSchema, operation: 'FlipFlop d'); + + if (resetValue is Logic && resetValue.width != outputSchema.width) { + throw PortWidthMismatchException.equalWidth(resetValue, outputSchema); + } + if (resetValue is LogicStructure && outputSchema is LogicStructure) { + validateMatchingLogicStructure( + resetValue, + outputSchema, + operation: 'FlipFlop reset', + ); + } + + if (LogicType == Logic) { + return _ScalarFlipFlop( + clk, + d, + outputSchema as Logic, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + usesOutputGenerator: outputGenerator != null, + name: name, + ) as FlipFlop; + } + if (d is LogicStructure) { + return _StructuredFlipFlop( + clk, + d, + outputSchema, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: name, + ); + } + return _TypedScalarFlipFlop( + clk, + d, + outputSchema, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: name, + ); + } + + FlipFlop._({super.name, super.definitionName}); + + /// Constructs a scalar sequential flip-flop for subclass implementations. + /// + /// The public unnamed constructor should be preferred for normal use. This + /// constructor preserves the historical subclassing seam for a custom + /// scalar flip-flop; it must be instantiated as [FlipFlop]<[Logic]>. + FlipFlop.scalar( + Logic clk, + Logic d, { + Logic? en, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, + super.name = 'flipflop', + }) : super() { + if (LogicType != Logic) { + throw LogicConstructionException( + 'FlipFlop.scalar is only valid for FlipFlop.', + ); + } + if (clk.width != 1) { + throw PortWidthMismatchException(clk, 1); + } + if (en != null && en.width != 1) { + throw PortWidthMismatchException(en, 1); + } + if (reset != null && reset.width != 1) { + throw PortWidthMismatchException(reset, 1); + } + if (resetValue is Logic && resetValue.width != d.width) { + throw PortWidthMismatchException.equalWidth(resetValue, d); + } + + _scalarAsyncReset = asyncReset; + final localClock = addInput('clk', clk); + final localD = addInput('d', d, width: d.width); + final localEnable = en == null ? null : addInput('en', en); + final localReset = reset == null ? null : addInput('reset', reset); + final localResetValue = reset != null && resetValue is Logic + ? addInput('resetValue', resetValue, width: d.width) + : null; + _scalarConstantResetValue = reset == null || resetValue is Logic + ? null + : LogicValue.of(resetValue ?? 0, width: d.width); + _scalarQ = addOutput('q', width: d.width) as LogicType; + + var contents = [_scalarQ < localD]; + if (localEnable case final enable?) { + contents = [If(enable, then: contents)]; + } + Sequential( + localClock, + contents, + reset: localReset, + asyncReset: asyncReset, + resetValues: localReset == null + ? null + : {_scalarQ: localResetValue ?? _scalarConstantResetValue!}, + ); + } } -/// Represents a single flip-flop with no reset. -class FlipFlop extends TypedFlipFlop with SystemVerilog { - /// Name for the enable input of this flop +/// Scalar SystemVerilog implementation of [FlipFlop]. +class _ScalarFlipFlop extends FlipFlop with SystemVerilog { final String _enName = Naming.unpreferredName('en'); - - /// Name for the clk of this flop. final String _clkName = Naming.unpreferredName('clk'); - - /// Name for the input of this flop. final String _dName = Naming.unpreferredName('d'); - - /// Name for the output of this flop. final String _qName = Naming.unpreferredName('q'); - - /// Name for the reset of this flop. final String _resetName = Naming.unpreferredName('reset'); - - /// Name for the reset value of this flop. final String _resetValueName = Naming.unpreferredName('resetValue'); - /// The clock, posedge triggered. late final Logic _clk = input(_clkName); - - /// Optional enable input to the flop. - /// - /// If enable is high or enable is not provided then flop output will vary - /// on the basis of clock [_clk] and input [_d]. If enable is low, then - /// output of the flop remains frozen irrespective of the input [_d]. late final Logic? _en = tryInput(_enName); - - /// Optional reset input to the flop. late final Logic? _reset = tryInput(_resetName); - - /// The input to the flop. late final Logic _d = input(_dName); - /// The output of the flop. @override - late final Logic q = output(_qName); + late final Logic q; - /// The reset value for this flop, if it was a port. Logic? _resetValuePort; - - /// The reset value for this flop, if it was a constant. - /// - /// Only initialized if a constant value is provided. late LogicValue _resetValueConst; - /// Indicates whether provided `reset` signals should be treated as an async - /// reset. If no `reset` is provided, this will have no effect. + @override final bool asyncReset; - /// The constant reset value, or `null` when reset is absent or data-driven. + @override LogicValue? get constantResetValue => _reset == null || _resetValuePort != null ? null : _resetValueConst; - /// Constructs a flip flop which is positive edge triggered on [clk]. - /// - /// When optional [en] is provided, an additional input will be created for - /// flop. If optional [en] is high or not provided, output will vary as per - /// input[d]. For low [en], output remains frozen irrespective of input [d] - /// - /// When the optional [reset] is provided, the flop will be reset active-high. - /// If no [resetValue] is provided, the reset value is always `0`. Otherwise, - /// it will reset to the provided [resetValue]. The type of [resetValue] must - /// be a valid driver of a [ConditionalAssign] (e.g. [Logic], [LogicValue], - /// [int], etc.). - /// - /// If [asyncReset] is true, the [reset] signal (if provided) will be treated - /// as an async reset. If [asyncReset] is false, the reset signal will be - /// treated as synchronous. - FlipFlop( + _ScalarFlipFlop( Logic clk, - Logic d, { + Logic d, + Logic outputSchema, { + required this.asyncReset, Logic? en, Logic? reset, dynamic resetValue, - this.asyncReset = false, super.name = 'flipflop', - }) { - if (clk.width != 1) { - throw Exception('clk must be 1 bit'); - } - + required bool usesOutputGenerator, + }) : super._() { addInput(_clkName, clk); addInput(_dName, d, width: d.width); - addOutput(_qName, width: d.width); + q = usesOutputGenerator + ? addTypedOutput( + _qName, + operationOutputClone(outputSchema), + ) + : addOutput( + _qName, + width: d.width, + ); if (en != null) { addInput(_enName, en); @@ -157,7 +255,6 @@ class FlipFlop extends TypedFlipFlop with SystemVerilog { if (reset != null) { addInput(_resetName, reset); - if (resetValue != null && resetValue is Logic) { _resetValuePort = addInput(_resetValueName, resetValue, width: d.width); } else { @@ -168,12 +265,9 @@ class FlipFlop extends TypedFlipFlop with SystemVerilog { _setup(); } - /// Performs setup for custom functional behavior. void _setup() { var contents = [q < _d]; - - final en = _en; - if (en != null) { + if (_en case final en?) { contents = [If(en, then: contents)]; } @@ -214,32 +308,27 @@ class FlipFlop extends TypedFlipFlop with SystemVerilog { final q = ports[_qName]!; final en = _en != null ? ports[_enName]! : null; final reset = _reset != null ? ports[_resetName]! : null; - final triggerString = [ clk, if (reset != null && asyncReset) reset, - ].map((e) => 'posedge $e').join(' or '); + ].map((value) => 'posedge $value').join(' or '); final svBuffer = StringBuffer('always_ff @($triggerString) '); - - if (_reset != null) { + if (reset != null) { final resetValueString = _resetValuePort != null ? ports[_resetValueName]! : _resetValueConst.toString(); - svBuffer.write('if(${reset!}) $q <= $resetValueString; else '); + svBuffer.write('if($reset) $q <= $resetValueString; else '); } - - if (_en != null) { - svBuffer.write('if(${en!}) '); + if (en != null) { + svBuffer.write('if($en) '); } - svBuffer.write('$q <= $d; // $instanceName'); - return svBuffer.toString(); } } -String _structureFlipFlopResetIdentity( +String _structuredFlipFlopResetIdentity( LogicStructure structure, bool hasReset, bool hasResetPort, @@ -255,7 +344,7 @@ String _structureFlipFlopResetIdentity( return 'RC${value.toRadixString(includeWidth: false, sepChar: '')}'; } -String _structureFlipFlopDefinitionName( +String _structuredFlipFlopDefinitionName( LogicStructure structure, { required bool hasEnable, required bool hasReset, @@ -263,9 +352,9 @@ String _structureFlipFlopDefinitionName( required dynamic resetValue, required bool asyncReset, }) => - 'StructureFlipFlop_${logicStructureShapeSignature(structure)}_' + 'FlipFlop_${logicStructureShapeSignature(structure)}_' '${hasEnable ? 'E' : 'N'}_' - '${_structureFlipFlopResetIdentity( + '${_structuredFlipFlopResetIdentity( structure, hasReset, hasResetPort, @@ -273,39 +362,32 @@ String _structureFlipFlopDefinitionName( )}_' '${asyncReset ? 'A' : 'S'}'; -/// A flip-flop that preserves a concrete [LogicStructure] type. -/// -/// Each structure leaf is registered independently with common clock, enable, -/// and reset controls. A [Logic] reset value is interpreted in the same packed -/// leaf order as [LogicStructure.packed]. A structured reset value must match -/// the input structure's recursive shape. -class StructureFlipFlop - extends TypedFlipFlop { - /// Indicates whether the reset is asynchronous. - final bool asyncReset; +/// Structure-preserving implementation of [FlipFlop]. +class _StructuredFlipFlop extends FlipFlop { + @override + late final LogicType q; - /// Constant packed reset value, or `null` when reset is absent or driven by - /// a [Logic] input. - final LogicValue? constantResetValue; + @override + final bool asyncReset; @override - late final LogicType q; + final LogicValue? constantResetValue; - /// Creates a structure-preserving flip-flop. - StructureFlipFlop( + _StructuredFlipFlop( Logic clk, - LogicType d, { + LogicType d, + LogicType outputSchema, { + required this.asyncReset, Logic? en, Logic? reset, dynamic resetValue, - this.asyncReset = false, - super.name = 'structure_flip_flop', + super.name = 'flipflop', }) : constantResetValue = reset != null && resetValue is! Logic ? LogicValue.of(resetValue ?? 0, width: d.width) : null, - super( - definitionName: _structureFlipFlopDefinitionName( - d, + super._( + definitionName: _structuredFlipFlopDefinitionName( + outputSchema as LogicStructure, hasEnable: en != null, hasReset: reset != null, hasResetPort: reset != null && resetValue is Logic, @@ -313,49 +395,31 @@ class StructureFlipFlop asyncReset: asyncReset, ), ) { - if (clk.width != 1) { - throw PortWidthMismatchException(clk, 1); - } - if (en != null && en.width != 1) { - throw PortWidthMismatchException(en, 1); - } - if (reset != null && reset.width != 1) { - throw PortWidthMismatchException(reset, 1); - } - if (resetValue is Logic) { - if (resetValue.width != d.width) { - throw PortWidthMismatchException.equalWidth(resetValue, d); - } - if (resetValue is LogicStructure) { - validateMatchingLogicStructure( - resetValue, - d, - operation: 'StructureFlipFlop reset', - ); - } - } - - LogicType cloneOutput({String name = 'q'}) => typedClone(d, name: name); - - final localClk = addInput('clk', clk); + final localClock = addInput('clk', clk); final localD = addTypedInput('d', d); final localEnable = en == null ? null : addInput('en', en); final localReset = reset == null ? null : addInput('reset', reset); final localResetValue = reset != null && resetValue is Logic ? addInput('resetValue', resetValue, width: d.width) : null; - q = addTypedOutput('q', cloneOutput); + q = addTypedOutput( + 'q', + operationOutputClone(outputSchema), + ); + final structuredQ = q as LogicStructure; + final structuredD = localD as LogicStructure; var offset = 0; - for (var index = 0; index < q.leafElements.length; index++) { - final width = q.leafElements[index].width; - final dynamic leafResetValue = localResetValue != null + for (var index = 0; index < structuredQ.leafElements.length; index++) { + final leaf = structuredQ.leafElements[index]; + final width = leaf.width; + final leafResetValue = localResetValue != null ? localResetValue.getRange(offset, offset + width) : constantResetValue?.getRange(offset, offset + width); - q.leafElements[index] <= - flop( - localClk, - localD.leafElements[index], + leaf <= + flop( + localClock, + structuredD.leafElements[index], en: localEnable, reset: localReset, resetValue: leafResetValue, @@ -366,20 +430,55 @@ class StructureFlipFlop } } -/// Constructs a positive-edge-triggered structure-preserving flip-flop. -LogicType typedFlop( - Logic clk, - LogicType d, { - Logic? en, - Logic? reset, - dynamic resetValue, - bool asyncReset = false, -}) => - StructureFlipFlop( - clk, - d, - en: en, - reset: reset, - resetValue: resetValue, +/// Generic scalar-subclass implementation of [FlipFlop]. +/// +/// This path supports scalar domain types such as future `LogicEnum` signals. +class _TypedScalarFlipFlop + extends FlipFlop { + @override + late final LogicType q; + + @override + final bool asyncReset; + + @override + LogicValue? get constantResetValue => null; + + _TypedScalarFlipFlop( + Logic clk, + LogicType d, + LogicType outputSchema, { + required this.asyncReset, + Logic? en, + Logic? reset, + dynamic resetValue, + super.name = 'flipflop', + }) : super._( + definitionName: + 'FlipFlop_${outputSchema.runtimeType}_W${outputSchema.width}', + ) { + final localClock = addInput('clk', clk); + final localD = addTypedInput('d', d); + final localEnable = en == null ? null : addInput('en', en); + final localReset = reset == null ? null : addInput('reset', reset); + final localResetValue = reset != null && resetValue is Logic + ? addInput('resetValue', resetValue, width: d.width) + : resetValue; + q = addTypedOutput( + 'q', + operationOutputClone(outputSchema), + ); + + var contents = [q < localD]; + if (localEnable case final enable?) { + contents = [If(enable, then: contents)]; + } + Sequential( + localClock, + contents, + reset: localReset, asyncReset: asyncReset, - ).q; + resetValues: localReset == null ? null : {q: localResetValue ?? 0}, + ); + } +} diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index 93783c186a..c99074f85d 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -9,6 +9,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; /// A gate [Module] that performs bit-wise inversion. class NotGate extends Module with InlineSystemVerilog { @@ -813,46 +814,103 @@ class LShift extends _ShiftGate { /// ``` /// /// If [control] is a valid [Const], returns the selected input directly. -Logic mux(Logic control, Logic d1, Logic d0) { +LogicType mux( + Logic control, + LogicType d1, + LogicType d0, { + LogicType Function({String? name})? outputGenerator, +}) { if (control.width != 1) { throw PortWidthMismatchException(control, 1); } if (d0.width != d1.width) { throw PortWidthMismatchException.equalWidth(d0, d1); } + if (d0 is LogicStructure && d1 is LogicStructure) { + validateMatchingLogicStructure(d1, d0, operation: 'mux'); + } - if (control is Const && control.value.isValid) { + if (outputGenerator == null && control is Const && control.value.isValid) { return control.value == LogicValue.one ? d1 : d0; } - return Mux(control, d1, d0).out; + return Mux( + control, + d1, + d0, + outputGenerator: outputGenerator, + ).out; } +String _muxSchemaSignature(Logic logic) => logic is LogicStructure + ? logicStructureShapeSignature(logic) + : '${logic.runtimeType}_W${logic.width}'; + /// A multiplexer with an output represented by [LogicType]. /// -/// Implementations decide how inputs are normalized into an output of the -/// promised type. For example, [Mux] accepts constants and nets as sources but -/// produces an ordinary single-driver [Logic], while [StructureMux] preserves -/// the concrete structure type and recursively selects corresponding leaves. -abstract class TypedMux extends TypedOp { +/// The false/default data operand `d0` supplies the output representation when +/// `outputGenerator` is omitted. A supplied `outputGenerator` overrides that +/// default and is invoked exactly once to create the output schema. +/// +/// Plain [Logic] results normalize scalar constants and nets into driveable +/// outputs. A concrete [Const] or [LogicNet] result type is rejected because a +/// dynamic mux output must be driveable. +abstract class Mux extends Module { /// Output selected by the control input. - @override LogicType get out; - /// Creates a typed multiplexer implementation. - TypedMux({ - super.name = 'typed_mux', - super.reserveName, - super.definitionName, - super.reserveDefinitionName, - }); + /// Constructs a mux preserving [LogicType]. + factory Mux( + Logic control, + LogicType d1, + LogicType d0, { + LogicType Function({String? name})? outputGenerator, + String name = 'mux', + }) { + if (control.width != 1) { + throw PortWidthMismatchException(control, 1); + } + if (d0.width != d1.width) { + throw PortWidthMismatchException.equalWidth(d0, d1); + } + if (d0 is LogicStructure && d1 is LogicStructure) { + validateMatchingLogicStructure(d1, d0, operation: 'Mux'); + } + + final outputSchema = createOperationOutput( + width: d0.width, + name: 'out', + operation: 'Mux<$LogicType>', + prototype: d0, + outputGenerator: outputGenerator, + ); + validateOperationSource(d0, outputSchema, operation: 'Mux d0'); + validateOperationSource(d1, outputSchema, operation: 'Mux d1'); + + if (LogicType == Logic) { + return _ScalarMux( + control, + d1, + d0, + outputSchema as Logic, + usesOutputGenerator: outputGenerator != null, + name: name, + ) as Mux; + } + return _DomainMux( + control, + d1, + d0, + outputSchema, + name: name, + ); + } + + Mux._({super.name, super.definitionName}); } -/// A mux (multiplexer) module. -/// -/// If [_control] has value `1`, then [out] gets [_d1]. -/// If [_control] has value `0`, then [out] gets [_d0]. -class Mux extends TypedMux with InlineSystemVerilog { +/// A scalar inline implementation of [Mux]. +class _ScalarMux extends Mux with InlineSystemVerilog { /// Name for the control signal of this mux. late final String _controlName; @@ -874,20 +932,18 @@ class Mux extends TypedMux with InlineSystemVerilog { /// [Mux] input propagated when [out] is `1`. late final Logic _d1 = input(_d1Name); - /// Output port of the [Mux]. + /// Output port of the mux. @override - late final Logic out = output(_outName); - - /// Constructs a multiplexer which passes [d0] or [d1] to [out] depending - /// on if [control] is 0 or 1, respectively. - Mux(Logic control, Logic d1, Logic d0, {super.name = 'mux'}) { - if (control.width != 1) { - throw PortWidthMismatchException(control, 1); - } - if (d0.width != d1.width) { - throw PortWidthMismatchException.equalWidth(d0, d1); - } + late final Logic out; + _ScalarMux( + Logic control, + Logic d1, + Logic d0, + Logic outputSchema, { + required bool usesOutputGenerator, + super.name = 'mux', + }) : super._() { _controlName = Naming.unpreferredName('control_${control.name}'); _d0Name = Naming.unpreferredName('d0_${d0.name}'); _d1Name = Naming.unpreferredName('d1_${d1.name}'); @@ -896,10 +952,16 @@ class Mux extends TypedMux with InlineSystemVerilog { addInput(_controlName, control); addInput(_d0Name, d0, width: d0.width); addInput(_d1Name, d1, width: d1.width); - addOutput( - _outName, - width: d0.width, - ).makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); + out = (usesOutputGenerator + ? addTypedOutput( + _outName, + operationOutputClone(outputSchema), + ) + : addOutput( + _outName, + width: d0.width, + )) + ..makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -941,16 +1003,8 @@ class Mux extends TypedMux with InlineSystemVerilog { } } -String _structureMuxDefinitionName(LogicStructure structure) => - 'StructureMux_${logicStructureShapeSignature(structure)}'; - -/// A multiplexer that preserves a concrete [LogicStructure] type. -/// -/// Both operands must have identical recursive structure and legal typed-port -/// clones. Each corresponding leaf is selected independently, preserving -/// nested [LogicArray] and [LogicArrayOf] boundaries at the output. -class StructureMux - extends TypedMux { +/// A non-inline implementation preserving a non-scalar [LogicType]. +class _DomainMux extends Mux { late final Logic _control; late final LogicType _d0; late final LogicType _d1; @@ -958,51 +1012,43 @@ class StructureMux @override late final LogicType out; - /// Creates a structure-preserving multiplexer. - StructureMux( + _DomainMux( Logic control, LogicType d1, - LogicType d0, { - super.name = 'structure_mux', - }) : super(definitionName: _structureMuxDefinitionName(d0)) { - if (control.width != 1) { - throw PortWidthMismatchException(control, 1); - } - validateMatchingLogicStructure(d1, d0, operation: 'StructureMux'); - - LogicType cloneOutput({String name = 'out'}) => typedClone(d0, name: name); - + LogicType d0, + LogicType outputSchema, { + super.name = 'mux', + }) : super._( + definitionName: 'Mux_${_muxSchemaSignature(outputSchema)}', + ) { _control = addInput('control', control); _d0 = addTypedInput('d0', d0); _d1 = addTypedInput('d1', d1); - out = addTypedOutput('out', cloneOutput); + out = addTypedOutput( + 'out', + operationOutputClone(outputSchema), + ); - for (var index = 0; index < out.leafElements.length; index++) { - out.leafElements[index] <= - mux(_control, _d1.leafElements[index], _d0.leafElements[index]); + if (out is LogicStructure && + _d0 is LogicStructure && + _d1 is LogicStructure) { + final structuredOut = out as LogicStructure; + final structuredD0 = _d0 as LogicStructure; + final structuredD1 = _d1 as LogicStructure; + for (var index = 0; index < structuredOut.leafElements.length; index++) { + structuredOut.leafElements[index] <= + mux( + _control, + structuredD1.leafElements[index], + structuredD0.leafElements[index], + ); + } + } else { + out <= mux(_control, _d1, _d0); } } } -/// Selects between matching structures while preserving their concrete type. -/// -/// If [control] is a valid [Const], returns the selected operand directly. -/// Otherwise, returns [StructureMux.out]. -LogicType typedMux( - Logic control, - LogicType d1, - LogicType d0, -) { - if (control.width != 1) { - throw PortWidthMismatchException(control, 1); - } - validateMatchingLogicStructure(d1, d0, operation: 'typedMux'); - if (control is Const && control.value.isValid) { - return control.value == LogicValue.one ? d1 : d0; - } - return StructureMux(control, d1, d0).out; -} - /// A two-input bit index gate [Module]. /// /// It always takes two inputs and has one output of width 1. diff --git a/lib/src/modules/modules.dart b/lib/src/modules/modules.dart index 69ec86a178..bfd5244c3a 100644 --- a/lib/src/modules/modules.dart +++ b/lib/src/modules/modules.dart @@ -8,4 +8,3 @@ export 'gates.dart'; export 'passthrough.dart'; export 'pipeline.dart'; export 'tristate.dart'; -export 'typed_op.dart'; diff --git a/lib/src/modules/operation_utils.dart b/lib/src/modules/operation_utils.dart new file mode 100644 index 0000000000..bc0e494b89 --- /dev/null +++ b/lib/src/modules/operation_utils.dart @@ -0,0 +1,109 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// operation_utils.dart +// Shared construction and validation for type-preserving operations. +// +// Author: Desmond A. Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +/// Creates the output representation for a type-preserving operation. +/// +/// If [outputGenerator] is supplied, it defines the output representation. +/// Otherwise, [prototype] supplies it. A requested [Logic] result is always +/// normalized to a fresh ordinary [Logic], allowing constants and nets to be +/// used as sources without making them the output type. +@internal +LogicType createOperationOutput({ + required String name, + required String operation, + int? width, + LogicType? prototype, + LogicType Function({String? name})? outputGenerator, +}) { + if (width != null && prototype != null && prototype.width != width) { + throw PortWidthMismatchException(prototype, width); + } + + final LogicType output; + if (outputGenerator != null) { + output = outputGenerator(name: name); + } else if (LogicType == Logic) { + if (width == null) { + throw LogicConstructionException( + '$operation requires a width for a normalized Logic output.', + ); + } + output = Logic(name: name, width: width) as LogicType; + } else if (prototype != null) { + output = prototype.cloneTyped(name: name); + } else { + throw LogicConstructionException( + '$operation cannot infer a $LogicType output representation. ' + 'Provide an outputGenerator.', + ); + } + + if (width != null && output.width != width) { + throw PortWidthMismatchException(output, width); + } + if (output is Const || + output.isNet || + (output is LogicStructure && (output.hasConsts || output.hasNets))) { + throw LogicConstructionException( + '$operation requires a driveable output, but $output is not driveable. ' + 'Request to normalize a constant or net source.', + ); + } + if (output.name != name) { + throw LogicConstructionException( + '$operation output generator did not apply the requested name "$name".', + ); + } + + return output; +} + +/// Validates that [source] can drive [output] without losing a domain type. +/// +/// Structured sources must match the output structure exactly. Callers that +/// intentionally reinterpret a different structure as packed bits must pass +/// its [Logic.packed] representation instead. +@internal +void validateOperationSource( + Logic source, + Logic output, { + required String operation, +}) { + if (source.width != output.width) { + throw PortWidthMismatchException.equalWidth(source, output); + } + + if (source is LogicStructure && output is LogicStructure) { + validateMatchingLogicStructure(source, output, operation: operation); + } else if (source is LogicStructure && output is! LogicStructure) { + // An explicit result intentionally normalizes packed structures. + return; + } else if (source is! LogicStructure && output is LogicStructure) { + // A scalar Logic source is an explicit packed representation. + return; + } +} + +/// Returns a typed output generator that clones [schema]. +@internal +LogicType Function({String? name}) + operationOutputClone(LogicType schema) { + final cloneFactory = _OperationOutputClone(schema); + return cloneFactory.call; +} + +class _OperationOutputClone { + final LogicType schema; + + _OperationOutputClone(this.schema); + + LogicType call({String? name}) => schema.cloneTyped(name: name); +} diff --git a/lib/src/modules/passthrough.dart b/lib/src/modules/passthrough.dart index 49a8b7e822..acbab840d3 100644 --- a/lib/src/modules/passthrough.dart +++ b/lib/src/modules/passthrough.dart @@ -3,81 +3,137 @@ // // passthrough.dart // A module that does nothing but pass a signal through. -// -// 2021 May 7 -// Author: Max Korbel import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; -/// A pass-through module with an input and output represented by [LogicType]. -abstract class TypedPassthrough - extends TypedOp { +/// A no-op module that preserves [LogicType]. +/// +/// The input supplies the output representation when `outputGenerator` is +/// omitted. Request `` explicitly to normalize a constant or net input +/// into a driveable scalar output. +abstract class Passthrough extends Module { /// Input port. LogicType get in_; /// Output port. - @override LogicType get out; - /// Creates a typed pass-through implementation. - TypedPassthrough({ - super.name = 'typed_passthrough', - super.reserveName, - super.definitionName, - super.reserveDefinitionName, - }); + /// Constructs a pass-through preserving [LogicType]. + factory Passthrough( + LogicType input, [ + String name = 'passthrough', + ]) => + Passthrough._create(input, name, null); + + /// Constructs a pass-through with an explicit output representation. + factory Passthrough.withOutput( + LogicType input, { + required LogicType Function({String? name}) outputGenerator, + String name = 'passthrough', + }) => + Passthrough._create(input, name, outputGenerator); + + static Passthrough _create( + LogicType input, + String name, + LogicType Function({String? name})? outputGenerator, + ) { + final outputSchema = createOperationOutput( + width: input.width, + name: 'out', + operation: 'Passthrough<$LogicType>', + prototype: input, + outputGenerator: outputGenerator, + ); + validateOperationSource( + input, + outputSchema, + operation: 'Passthrough input', + ); + + if (LogicType == Logic) { + return _ScalarPassthrough( + input, + outputSchema as Logic, + usesOutputGenerator: outputGenerator != null, + name: name, + ) as Passthrough; + } + return _DomainPassthrough( + input, + outputSchema, + name: name, + ); + } + + Passthrough._({super.name, super.definitionName}); } -/// A very simple noop module that just passes a signal through. -class Passthrough extends TypedPassthrough { - /// The input port. +/// Scalar implementation of [Passthrough]. +class _ScalarPassthrough extends Passthrough { @override - Logic get in_ => input('in'); + late final Logic in_; - /// The output port. @override - Logic get out => output('out'); - - /// Constructs a simple pass-through module that performs no operations - /// between [a] and [out]. - Passthrough(Logic a, [String name = 'passthrough']) : super(name: name) { - addInput('in', a, width: a.width); - addOutput('out', width: a.width); - _setup(); - } + late final Logic out; - void _setup() { + _ScalarPassthrough( + Logic input, + Logic outputSchema, { + required bool usesOutputGenerator, + super.name = 'passthrough', + }) : super._() { + in_ = addInput('in', input, width: input.width); + out = usesOutputGenerator + ? addTypedOutput( + 'out', + operationOutputClone(outputSchema), + ) + : addOutput( + 'out', + width: input.width, + ); final inner = Logic(name: 'inner', width: in_.width); inner <= in_; out <= inner; } } -String _structurePassthroughDefinitionName(LogicStructure structure) => - 'StructurePassthrough_${logicStructureShapeSignature(structure)}'; - -/// A no-op module that preserves a concrete [LogicStructure] type. -/// -/// The input and output retain all nested [LogicArray] and [LogicArrayOf] -/// boundaries. Typed ports reject structures containing constants or nets. -class StructurePassthrough - extends TypedPassthrough { +/// Type-preserving implementation of [Passthrough]. +class _DomainPassthrough + extends Passthrough { @override late final LogicType in_; @override late final LogicType out; - /// Creates a structure-preserving pass-through module. - StructurePassthrough(LogicType input, {super.name = 'structure_passthrough'}) - : super(definitionName: _structurePassthroughDefinitionName(input)) { - LogicType cloneOutput({String name = 'out'}) => - typedClone(input, name: name); - + _DomainPassthrough( + LogicType input, + LogicType outputSchema, { + super.name = 'passthrough', + }) : super._( + definitionName: 'Passthrough_${_schemaSignature(outputSchema)}', + ) { in_ = addTypedInput('in', input); - out = addTypedOutput('out', cloneOutput); - for (var index = 0; index < out.leafElements.length; index++) { - out.leafElements[index] <= in_.leafElements[index]; + out = addTypedOutput( + 'out', + operationOutputClone(outputSchema), + ); + + if (out is LogicStructure && in_ is LogicStructure) { + final structuredOut = out as LogicStructure; + final structuredIn = in_ as LogicStructure; + for (var index = 0; index < structuredOut.leafElements.length; index++) { + structuredOut.leafElements[index] <= structuredIn.leafElements[index]; + } + } else { + out <= in_; } } } + +String _schemaSignature(Logic logic) => logic is LogicStructure + ? logicStructureShapeSignature(logic) + : '${logic.runtimeType}_W${logic.width}'; diff --git a/lib/src/modules/pipeline.dart b/lib/src/modules/pipeline.dart index ccde4e8fed..1b8294f915 100644 --- a/lib/src/modules/pipeline.dart +++ b/lib/src/modules/pipeline.dart @@ -49,7 +49,7 @@ class StructurePipelineStageInfo { /// /// Every transform receives a [StructurePipelineStageInfo] whose `value` is the /// typed value entering that stage. The returned matching structure is -/// registered through [StructureFlipFlop]. [values] contains the original +/// registered through [FlipFlop]. [values] contains the original /// input followed by every registered stage output, so `stageCount` matches /// [Pipeline]'s boundary-count convention and `latency` is one less. class StructurePipeline { @@ -102,7 +102,7 @@ class StructurePipeline { throw PortWidthMismatchException(stall, 1); } generatedValues.add( - StructureFlipFlop( + FlipFlop( clk, transformed, en: stall == null ? null : ~stall, diff --git a/lib/src/modules/typed_op.dart b/lib/src/modules/typed_op.dart deleted file mode 100644 index 92fd1a8552..0000000000 --- a/lib/src/modules/typed_op.dart +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// typed_op.dart -// Common contract for hardware operations with typed outputs. -// -// 2026 September 2 -// Author: Desmond A. Kirkpatrick - -import 'package:rohd/rohd.dart'; - -/// A hardware operation whose output preserves the requested [Logic] type. -abstract class TypedOp extends Module { - /// The output produced by this operation. - LogicType get out; - - /// Creates a typed operation. - TypedOp({ - super.name = 'typed_op', - super.reserveName, - super.definitionName, - super.reserveDefinitionName, - }); -} diff --git a/lib/src/selection.dart b/lib/src/selection.dart index c2a89fae10..cf519eeec6 100644 --- a/lib/src/selection.dart +++ b/lib/src/selection.dart @@ -9,10 +9,10 @@ import 'package:rohd/rohd.dart'; -/// Allows a lists of [Logic]s to have its elemets picked -/// by a [Logic] index value. -extension IndexedLogic on List { - /// Performs a [index] based selection on an [List] of [Logic]. +/// Allows a list of [LogicType]s to have an element selected by a [Logic] +/// index. +extension IndexedLogic on List { + /// Performs an [index]-based selection on this list. /// /// Given a [List] of [Logic] say `logicList` on which we apply [selectIndex] /// and an element [index] as argument , we can select any valid element @@ -27,46 +27,16 @@ extension IndexedLogic on List { /// selected <= logicList.selectIndex(index); /// ``` /// - Logic selectIndex(Logic index, {Logic? defaultValue}) => - index.selectFrom(this, defaultValue: defaultValue); -} - -/// Allows lists of matching structures to retain their type when selected. -extension TypedIndexedLogic - on List { - /// Selects one structure using [index], preserving its concrete type. - /// - /// An out-of-range index produces zero unless [defaultValue] is supplied. - LogicType selectIndexTyped( + LogicType selectIndex( Logic index, { - LogicType? defaultValue, - String name = 'selectFrom', - }) { - if (isEmpty) { - throw LogicConstructionException( - 'selectIndexTyped requires at least one value.', - ); - } - return typedCases( - index, - { - for (var valueIndex = 0; valueIndex < length; valueIndex++) - valueIndex: this[valueIndex], - }, - conditionalType: ConditionalType.unique, - defaultValue: defaultValue ?? 0, - name: name, - ); - } -} - -/// Allows a hardware index to select from a typed structure list. -extension TypedSelectionLogic on Logic { - /// Selects one value from [values], preserving its concrete structure type. - LogicType selectFromTyped( - List values, { - LogicType? defaultValue, + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, String name = 'selectFrom', }) => - values.selectIndexTyped(this, defaultValue: defaultValue, name: name); + index.selectFrom( + this, + defaultValue: defaultValue, + outputGenerator: outputGenerator, + name: name, + ); } diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index 9daf72d947..e1a332c7df 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -1001,13 +1001,12 @@ class Logic { return isLogicIn; } - /// Performs a [Logic] `index` based selection on an [List] of [Logic] - /// named [busList]. + /// Performs a [Logic] `index` based selection on [busList]. /// /// Using the [Logic] `index` on which [selectFrom] is performed on and /// a [List] of [Logic] named [busList] for `index` based selection, we can - /// select any valid element of type [Logic] within the `logicList` using - /// the `index` of [Logic] type. + /// select any valid element of type [LogicType] within the `logicList` + /// using the `index` of [Logic] type. /// /// Alternatively we can approach this with `busList.selectIndex(index)` /// @@ -1016,26 +1015,28 @@ class Logic { /// // ordering matches closer to array indexing with `0` index-based. /// selected <= index.selectFrom(busList); /// ``` - Logic selectFrom(List busList, {Logic? defaultValue}) { - final selected = Logic( - name: 'selectFrom', - width: busList.first.width, - naming: Naming.mergeable, + LogicType selectFrom( + List busList, { + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, + String name = 'selectFrom', + }) { + if (busList.isEmpty) { + throw LogicConstructionException( + 'selectFrom requires at least one value.', + ); + } + return cases( + this, + { + for (var index = 0; index < busList.length; index++) + index: busList[index], + }, + conditionalType: ConditionalType.unique, + defaultValue: defaultValue ?? 0, + outputGenerator: outputGenerator, + name: name, ); - - Combinational([ - Case( - this, - [ - for (var i = 0; i < busList.length; i++) - CaseItem(Const(i, width: width), [selected < busList[i]]), - ], - conditionalType: ConditionalType.unique, - defaultItem: [selected < (defaultValue ?? 0)], - ), - ]); - - return selected; } /// If [assignSubset] has been used on this signal, a reference to the diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index e7b4c43b30..ea8bf32a55 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -96,38 +96,67 @@ void validateMatchingLogicStructure( }) => _validateMatchingStructure(first, second, operation, 'root'); -/// Clones [source] while preserving its concrete [LogicStructure] type. -/// -/// Throws a [LogicConstructionException] when [LogicStructure.clone] is not -/// implemented covariantly by the concrete type. -LogicType typedClone( +LogicType _checkedTypedClone( LogicType source, { String? name, }) { final cloned = source.clone(name: name); if (cloned is! LogicType) { throw LogicConstructionException( - 'Structure clone did not preserve its concrete type.', + 'Clone did not preserve the requested $LogicType type.', ); } return cloned; } -/// Creates a named, connected alias while preserving [source]'s concrete type. -LogicType typedNamed( - LogicType source, - String name, -) => - typedClone(source, name: name)..gets(source); - -/// Type-preserving conveniences for concrete [LogicStructure] values. -extension TypedLogicStructureUtilities - on LogicType { - /// Equivalent to [LogicStructure.clone], with a concrete return type. - LogicType cloneTyped({String? name}) => typedClone(this, name: name); - - /// Equivalent to [LogicStructure.named], with a concrete return type. - LogicType namedTyped(String name) => typedNamed(this, name); +void _validateTypedNamedClone(Logic source, String name) { + if (source is! LogicStructure) { + return; + } + + final clone = source.clone(name: name); + if (clone is Const || clone.hasConsts) { + throw LogicConstructionException( + 'A named typed alias requires driveable cloned fields.', + ); + } +} + +/// Type-preserving conveniences for concrete [Logic] values. +/// +/// The static receiver type determines the static result type. If a value has +/// already been widened to [Logic], this extension cannot recover its original +/// subtype. +extension TypedLogicUtilities on LogicType { + /// Equivalent to [Logic.clone], preserving the receiver's static type. + /// + /// Literal cloning is legal: a concrete [Const] receiver returns another + /// [Const] rather than a driveable alias. + LogicType cloneTyped({String? name}) => _checkedTypedClone(this, name: name); + + /// Creates a named driveable alias while preserving the static result type. + /// + /// A concrete [Const] receiver cannot satisfy this contract because + /// [Const.clone] is immutable while [Const.named] creates an ordinary + /// [Logic] alias. Use [Logic.named] when an ordinary [Logic] alias is + /// intended. + LogicType namedTyped(String name, {Naming? naming}) { + if (this is Const && LogicType == Const) { + throw LogicConstructionException( + 'Const.namedTyped cannot preserve Const because a named alias must be ' + 'driveable. Use named() to create a Logic alias.', + ); + } + + _validateTypedNamedClone(this, name); + final named = this.named(name, naming: naming); + if (named is! LogicType) { + throw LogicConstructionException( + 'Named alias did not preserve the requested $LogicType type.', + ); + } + return named; + } } /// Collects a group of [Logic] signals into one entity which can be manipulated @@ -748,8 +777,18 @@ class LogicStructure implements Logic { } @override - Logic selectFrom(List busList, {Logic? defaultValue}) => - packed.selectFrom(busList, defaultValue: defaultValue); + LogicType selectFrom( + List busList, { + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, + String name = 'selectFrom', + }) => + packed.selectFrom( + busList, + defaultValue: defaultValue, + outputGenerator: outputGenerator, + name: name, + ); @override bool get isNet => _isNet; diff --git a/test/async_reset_test.dart b/test/async_reset_test.dart index 82e1dbcdfc..5e573643bb 100644 --- a/test/async_reset_test.dart +++ b/test/async_reset_test.dart @@ -114,7 +114,7 @@ void main() { ), 'FlipFlop with no asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), @@ -122,7 +122,7 @@ void main() { }, 'flop with no asyncReset': (Logic clk, Logic reset, Logic val) { val <= - flop( + flop( clk, reset: reset, Const(1), @@ -198,7 +198,7 @@ void main() { ), 'FlipFlop with asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), @@ -207,7 +207,7 @@ void main() { }, 'flop with asyncReset': (Logic clk, Logic reset, Logic val) { val <= - flop( + flop( clk, reset: reset, Const(1), diff --git a/test/module_test.dart b/test/module_test.dart index 07963890cf..313831dfe7 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -126,7 +126,7 @@ class StructWithOutputAsElementMod extends Module { b = addInput('b', b); final s = SimpleLogicStructure( - a: mux(a, Const(0), Const(1)), + a: mux(a, Const(0), Const(1)), b: Const(1), ); diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index 84a91a6e88..6cd89bcf62 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -21,7 +21,7 @@ class TopModule extends Module { Combinational([internalNet < inPort]); Combinational([outPort < internalNet]); - Passthrough(internalNet); + Passthrough(internalNet); } } diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index d5641d305e..20c7d386cf 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -103,8 +103,8 @@ class FlopModule extends Module { } /// A custom [FlipFlop] used to verify inheritance-aware leaf matching. -class CustomFlipFlop extends FlipFlop { - CustomFlipFlop(super.clk, super.d); +class CustomFlipFlop extends FlipFlop { + CustomFlipFlop(super.clk, super.d) : super.scalar(); } /// Exercises flip-flops with optional control signals. diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index 08e862584f..f602ad38dd 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -27,6 +27,16 @@ class _TypedLane extends LogicStructure { _TypedLane clone({String? name}) => _TypedLane(name: name ?? this.name); } +class _ScalarDomainLogic extends Logic { + final String schema; + + _ScalarDomainLogic({required this.schema, super.name}) : super(width: 4); + + @override + _ScalarDomainLogic clone({String? name}) => + _ScalarDomainLogic(schema: schema, name: name ?? this.name); +} + class _TypedPacket extends LogicStructure { Logic get opcode => elements[0]; LogicArrayOf<_TypedLane> get lanes => elements[1] as LogicArrayOf<_TypedLane>; @@ -77,6 +87,33 @@ class _NetPacket extends LogicStructure { _NetPacket clone({String? name}) => _NetPacket(name: name ?? this.name); } +class _ConfiguredPacket extends LogicStructure { + final String schema; + + _ConfiguredPacket({required this.schema, String? name}) + : super([ + Logic(name: 'data', width: 4), + ], name: name ?? 'configured_packet'); + + Logic get data => elements.single; + + @override + _ConfiguredPacket clone({String? name}) => + _ConfiguredPacket(schema: schema, name: name ?? this.name); +} + +class _ConfiguredPacketGenerator { + final String schema; + int calls = 0; + + _ConfiguredPacketGenerator(this.schema); + + _ConfiguredPacket call({String? name}) { + calls++; + return _ConfiguredPacket(schema: schema, name: name); + } +} + class _TypedOperationsSynthesisHarness extends Module { _TypedOperationsSynthesisHarness(Logic clk, Logic reset, Logic control, Logic selector, _TypedPacket first, _TypedPacket second) { @@ -87,24 +124,34 @@ class _TypedOperationsSynthesisHarness extends Module { first = addTypedInput('first', first); second = addTypedInput('second', second); - final selected = StructureMux(control, second, first).out; - final selectedByCase = typedCases( + final selected = Mux( + control, + second, + first, + outputGenerator: first.clone, + ).out; + final _TypedPacket selectedByCase = cases( selector, {0: first, 1: second}, defaultValue: selected, + outputGenerator: first.clone, name: 'selected_by_case', ); - final selectedByIndex = [first, second, selectedByCase].selectIndexTyped( + final selectedByIndex = [first, second, selectedByCase].selectIndex( selector, defaultValue: first, name: 'selected_by_index', ); - final passed = StructurePassthrough(selectedByIndex).out; - final registered = StructureFlipFlop( + final passed = Passthrough.withOutput( + selectedByIndex, + outputGenerator: selectedByIndex.clone, + ).out; + final registered = FlipFlop( clk, passed, reset: reset, resetValue: 0, + outputGenerator: passed.clone, ).q; final piped = StructurePipeline<_TypedPacket>( clk, @@ -119,6 +166,49 @@ class _TypedOperationsSynthesisHarness extends Module { } } +class _GeneratorVectorHarness extends Module { + _GeneratorVectorHarness( + Logic control, + Logic selector, + _ConfiguredPacket first, + _ConfiguredPacket second, + Logic rawBits, + ) { + control = addInput('control', control); + selector = addInput('selector', selector); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + rawBits = addInput('rawBits', rawBits, width: rawBits.width); + + final muxGenerator = _ConfiguredPacketGenerator('mux'); + final muxed = Mux( + control, + second, + first, + outputGenerator: muxGenerator.call, + ).out; + final caseGenerator = _ConfiguredPacketGenerator('case'); + final selected = cases( + selector, + { + 0: muxed, + 1: rawBits, + }, + outputGenerator: caseGenerator.call, + ); + final constantGenerator = _ConfiguredPacketGenerator('constant'); + final constantSelected = mux( + Const(0), + second, + first, + outputGenerator: constantGenerator.call, + ); + addTypedOutput('out', selected.clone).gets(selected); + addTypedOutput('constantOut', constantSelected.clone) + .gets(constantSelected); + } +} + void _expectLogicArray(LogicArray array) { expect(array, isA()); } @@ -127,17 +217,23 @@ void _expectTypedLaneArray(LogicArrayOf<_TypedLane> array) { expect(array, isA>()); } +void _expectConst(Const value) { + expect(value, isA()); +} + +void _expectLogic(Logic value) { + expect(value, isA()); +} + void main() { tearDown(Simulator.reset); - test('Mux implements the scalar TypedOp contract', () { + test('Mux exposes a scalar Logic output', () { final muxModule = Mux(Logic(), Logic(width: 4), Logic(width: 4)); - expect(muxModule, isA>()); expect(muxModule.out.runtimeType, Logic); }); - test('scalar typed operations normalize constants, ports, and nets', - () async { + test('scalar operations explicitly normalize constants and nets', () async { final clk = SimpleClockGenerator(10).clk; final control = Logic(); final ordinary = Logic(width: 4); @@ -146,11 +242,13 @@ void main() { final netDriver = Logic(width: 4); final net = LogicNet(width: 4)..gets(netDriver); final constantMux = Mux(control, constant, ordinary); + final constantD0Mux = Mux(control, ordinary, constant); final portMux = Mux(control, portPrototype, ordinary); final netMux = Mux(control, net, ordinary); - final constantFlop = FlipFlop(clk, constant); + final netD0Mux = Mux(control, ordinary, net); + final constantFlop = FlipFlop(clk, constant); final portFlop = FlipFlop(clk, portPrototype); - final netFlop = FlipFlop(clk, net); + final netFlop = FlipFlop(clk, net); await Future.wait([ constantMux.build(), portMux.build(), @@ -163,8 +261,10 @@ void main() { for (final output in [ constantMux.out, + constantD0Mux.out, portMux.out, netMux.out, + netD0Mux.out, constantFlop.q, portFlop.q, netFlop.q, @@ -179,8 +279,16 @@ void main() { control.inject(1); await Simulator.tick(); expect(constantMux.out.value.toInt(), 0xa); + expect(constantD0Mux.out.value.toInt(), 3); expect(portMux.out.value.toInt(), 5); expect(netMux.out.value.toInt(), 7); + expect(netD0Mux.out.value.toInt(), 3); + control.inject(0); + await Simulator.tick(); + expect(constantMux.out.value.toInt(), 3); + expect(constantD0Mux.out.value.toInt(), 0xa); + expect(netMux.out.value.toInt(), 3); + expect(netD0Mux.out.value.toInt(), 7); await clk.nextPosedge; expect(constantFlop.q.value.toInt(), 0xa); expect(portFlop.q.value.toInt(), 5); @@ -188,6 +296,139 @@ void main() { await Simulator.endSimulation(); }); + test('scalar domain subclasses retain their concrete output type', () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final d0 = _ScalarDomainLogic(schema: 'd0', name: 'd0'); + final d1 = _ScalarDomainLogic(schema: 'd1', name: 'd1'); + final muxed = Mux(control, d1, d0).out; + final forwarded = Passthrough(d0).out; + final registered = FlipFlop(clk, d0).q; + + expect(muxed, isA<_ScalarDomainLogic>()); + expect(muxed.schema, 'd0'); + expect(forwarded, isA<_ScalarDomainLogic>()); + expect(registered, isA<_ScalarDomainLogic>()); + + unawaited(Simulator.run()); + d0.inject(0x2); + d1.inject(0xd); + control.inject(1); + await Simulator.tick(); + expect(muxed.value, LogicValue.ofInt(0xd, 4)); + expect(forwarded.value, LogicValue.ofInt(0x2, 4)); + await clk.nextPosedge; + expect(registered.value, LogicValue.ofInt(0x2, 4)); + await Simulator.endSimulation(); + }); + + test('inferred constants and nets reject driveable operation results', () { + final clk = Logic(); + final control = Logic(); + final constant = Const(3, width: 4); + final net = LogicNet(width: 4); + + expect( + () => FlipFlop(clk, constant), + throwsA(isA()), + ); + expect( + () => FlipFlop(clk, net), + throwsA(isA()), + ); + expect( + () => Passthrough(constant), + throwsA(isA()), + ); + expect( + () => Mux(control, Const(1, width: 4), Const(0, width: 4)), + throwsA(isA()), + ); + }); + + test('mux constant shortcuts and generators have explicit identities', + () async { + final d0 = _ConfiguredPacket(schema: 'd0', name: 'd0'); + final d1 = _ConfiguredPacket(schema: 'd1', name: 'd1'); + + expect(mux(Const(0), d1, d0), same(d0)); + expect(mux(Const(1), d1, d0), same(d1)); + + final generator = _ConfiguredPacketGenerator('generated'); + final generated = mux( + Const(0), + d1, + d0, + outputGenerator: generator.call, + ); + expect(generator.calls, 1); + expect(generated, isNot(same(d0))); + expect(generated.schema, 'generated'); + + d0.data.put(0x3); + d1.data.put(0xa); + await Simulator.tick(); + expect(generated.data.value.toInt(), 0x3); + }); + + test('mux defaults to d0 configuration and generators override it', () async { + final control = Logic(); + final d0 = _ConfiguredPacket(schema: 'd0', name: 'd0'); + final d1 = _ConfiguredPacket(schema: 'd1', name: 'd1'); + final defaultSchema = Mux(control, d1, d0).out; + final generator = _ConfiguredPacketGenerator('generated'); + final generatedSchema = Mux( + control, + d1, + d0, + outputGenerator: generator.call, + ).out; + + expect(defaultSchema.schema, 'd0'); + expect(generatedSchema.schema, 'generated'); + expect(generator.calls, 1); + + d0.data.put(0x1); + d1.data.put(0xe); + control.put(0); + await Simulator.tick(); + expect(defaultSchema.data.value.toInt(), 0x1); + expect(generatedSchema.data.value.toInt(), 0x1); + control.put(1); + await Simulator.tick(); + expect(defaultSchema.data.value.toInt(), 0xe); + expect(generatedSchema.data.value.toInt(), 0xe); + }); + + test('flop and passthrough generators override output schemas', () async { + final clk = SimpleClockGenerator(10).clk; + final source = _ConfiguredPacket(schema: 'source', name: 'source'); + final passthroughGenerator = _ConfiguredPacketGenerator('passthrough'); + final flopGenerator = _ConfiguredPacketGenerator('flop'); + final forwarded = Passthrough.withOutput( + source, + outputGenerator: passthroughGenerator.call, + ).out; + final registered = FlipFlop( + clk, + source, + outputGenerator: flopGenerator.call, + ).q; + + expect(forwarded.schema, 'passthrough'); + expect(registered.schema, 'flop'); + expect(passthroughGenerator.calls, 1); + expect(flopGenerator.calls, 1); + + unawaited(Simulator.run()); + source.data.inject(0x9); + await Simulator.tick(); + expect(forwarded.data.value.toInt(), 0x9); + await clk.nextPosedge; + expect(registered.data.value.toInt(), 0x9); + await Simulator.endSimulation(); + }); + test('typed operations infer top-level array types', () { final clk = Logic(); final selector = Logic(width: 2); @@ -198,12 +439,12 @@ void main() { final typedArray1 = LogicArrayOf<_TypedLane>([2], _TypedLane.new, name: 'typed_array1'); - final muxedArray = StructureMux(selector[0], array1, array0).out; - final floppedArray = StructureFlipFlop(clk, array0).q; - final passedArray = StructurePassthrough(array0).out; - final casedArray = typedCases(selector, {0: array0, 1: array1}); - final selectedArray = [array0, array1].selectIndexTyped(selector); - final selectedFromArray = selector.selectFromTyped([array0, array1]); + final muxedArray = Mux(selector[0], array1, array0).out; + final floppedArray = FlipFlop(clk, array0).q; + final passedArray = Passthrough(array0).out; + final LogicArray casedArray = cases(selector, {0: array0, 1: array1}); + final selectedArray = [array0, array1].selectIndex(selector); + final selectedFromArray = selector.selectFrom([array0, array1]); final clonedArray = array0.cloneTyped(); final namedArray = array0.namedTyped('named_array'); final pipelinedArray = StructurePipeline( @@ -212,16 +453,14 @@ void main() { stages: [(stage) => stage.value], ).output; - final muxedTypedArray = - StructureMux(selector[0], typedArray1, typedArray0).out; - final floppedTypedArray = StructureFlipFlop(clk, typedArray0).q; - final passedTypedArray = StructurePassthrough(typedArray0).out; - final casedTypedArray = - typedCases(selector, {0: typedArray0, 1: typedArray1}); - final selectedTypedArray = - [typedArray0, typedArray1].selectIndexTyped(selector); + final muxedTypedArray = Mux(selector[0], typedArray1, typedArray0).out; + final floppedTypedArray = FlipFlop(clk, typedArray0).q; + final passedTypedArray = Passthrough(typedArray0).out; + final LogicArrayOf<_TypedLane> casedTypedArray = + cases(selector, {0: typedArray0, 1: typedArray1}); + final selectedTypedArray = [typedArray0, typedArray1].selectIndex(selector); final selectedFromTypedArray = - selector.selectFromTyped([typedArray0, typedArray1]); + selector.selectFrom([typedArray0, typedArray1]); final clonedTypedArray = typedArray0.cloneTyped(); final namedTypedArray = typedArray0.namedTyped('named_typed_array'); final pipelinedTypedArray = StructurePipeline>( @@ -258,10 +497,8 @@ void main() { final control = Logic(); final d1 = _TypedPacket(name: 'd1'); final d0 = _TypedPacket(name: 'd0'); - final muxModule = StructureMux(control, d1, d0); + final muxModule = Mux(control, d1, d0); await muxModule.build(); - - expect(muxModule, isA>()); expect(muxModule.out, isA<_TypedPacket>()); expect(muxModule.out.lanes, isA>()); expect( @@ -288,13 +525,13 @@ void main() { test('Mux preserves structure type with a constant control', () { final d1 = _TypedPacket(name: 'd1'); final d0 = _TypedPacket(name: 'd0'); - expect(typedMux(Const(0), d1, d0), isA<_TypedPacket>()); - expect(typedMux(Const(1), d1, d0), isA<_TypedPacket>()); + expect(mux(Const(0), d1, d0), isA<_TypedPacket>()); + expect(mux(Const(1), d1, d0), isA<_TypedPacket>()); }); test('Mux rejects differently shaped typed arrays', () { expect( - () => StructureMux(Logic(), _TypedPacket(laneDimensions: const [2, 2]), + () => Mux(Logic(), _TypedPacket(laneDimensions: const [2, 2]), _TypedPacket(laneDimensions: const [4])), throwsA(isA())); }); @@ -304,9 +541,9 @@ void main() { final control = Logic(); final constant = _ConstPacket(value: 0xa, name: 'constant'); final variable = _ConstPacket(value: null, name: 'variable'); - final muxModule = StructureMux(control, constant, variable); - final passthrough = StructurePassthrough(constant); - final flipFlop = StructureFlipFlop(clk, constant); + final muxModule = Mux(control, constant, variable); + final passthrough = Passthrough(constant); + final flipFlop = FlipFlop(clk, constant); await Future.wait([ muxModule.build(), passthrough.build(), @@ -339,40 +576,67 @@ void main() { await Simulator.endSimulation(); }); + test('explicit Logic flops constants through reset without mutating source', + () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final constant = Const(0xa, width: 4); + final q = flop( + clk, + constant, + reset: reset, + resetValue: 0, + ); + + expect(q, isA()); + expect(q, isNot(isA())); + expect(constant.value, LogicValue.ofInt(0xa, 4)); + + unawaited(Simulator.run()); + reset.inject(1); + await clk.nextPosedge; + expect(q.value, LogicValue.ofInt(0, 4)); + reset.inject(0); + await clk.nextPosedge; + expect(q.value, LogicValue.ofInt(0xa, 4)); + expect(constant.value, LogicValue.ofInt(0xa, 4)); + await Simulator.endSimulation(); + }); + test('typed operations reject clones, nets, and reset mismatches', () { - expect(() => typedClone(_BadClonePacket()), + expect(() => _BadClonePacket().cloneTyped(), + throwsA(isA())); + expect(() => Passthrough<_NetPacket>(_NetPacket()), throwsA(isA())); - expect(() => StructurePassthrough<_NetPacket>(_NetPacket()), - throwsA(isA())); expect( - () => StructureFlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + () => FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), reset: Logic(), resetValue: _TypedPacket(laneDimensions: const [1, 2])), throwsA(isA())); expect( - () => typedCases(Logic(), { + () => cases<_TypedPacket>(Logic(), { 0: _TypedPacket(), 1: _TypedPacket(laneDimensions: const [1, 2]) }), throwsA(isA())); - expect(() => <_TypedPacket>[].selectIndexTyped(Logic()), + expect(() => <_TypedPacket>[].selectIndex(Logic()), throwsA(isA())); }); - test('typedCases rejects mismatched LogicValue widths', () { + test('cases rejects mismatched LogicValue widths', () { final selector = Logic(width: 2); final first = _TypedPacket(name: 'first'); final second = _TypedPacket(name: 'second'); expect( - () => typedCases(selector, { + () => cases(selector, { LogicValue.ofInt(4, 3): first, 1: second, }), throwsA(isA()), ); expect( - () => typedCases( + () => cases( selector, {0: first, 1: second}, defaultValue: LogicValue.ofInt(0, first.width + 1), @@ -383,17 +647,17 @@ void main() { test('typed module definitions encode structure and constant reset identity', () { - final sameShapeFirst = StructureMux<_TypedPacket>(Logic(), + final sameShapeFirst = Mux<_TypedPacket>(Logic(), _TypedPacket(name: 'first_d1'), _TypedPacket(name: 'first_d0')); - final sameShapeSecond = StructureMux<_TypedPacket>(Logic(), + final sameShapeSecond = Mux<_TypedPacket>(Logic(), _TypedPacket(name: 'second_d1'), _TypedPacket(name: 'second_d0')); - final differentShape = StructureMux<_TypedPacket>( + final differentShape = Mux<_TypedPacket>( Logic(), _TypedPacket(laneDimensions: const [1, 2], name: 'different_d1'), _TypedPacket(laneDimensions: const [1, 2], name: 'different_d0')); - final resetZero = StructureFlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + final resetZero = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), reset: Logic(), resetValue: 0); - final resetOne = StructureFlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + final resetOne = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), reset: Logic(), resetValue: 1); expect(sameShapeFirst.definitionName, sameShapeSecond.definitionName); @@ -401,9 +665,8 @@ void main() { expect(resetZero.definitionName, isNot(resetOne.definitionName)); }); - test('FlipFlop implements the scalar TypedOp contract', () { + test('FlipFlop exposes a scalar Logic output', () { final flipFlop = FlipFlop(Logic(), Logic(width: 4)); - expect(flipFlop, isA>()); expect(flipFlop.q.runtimeType, Logic); }); @@ -412,7 +675,7 @@ void main() { final reset = Logic(); final enable = Logic(); final d = _TypedPacket(name: 'd'); - final flipFlop = StructureFlipFlop( + final flipFlop = FlipFlop( clk, d, en: enable, @@ -421,8 +684,6 @@ void main() { ); await flipFlop.build(); unawaited(Simulator.run()); - - expect(flipFlop, isA>()); expect(flipFlop.q, isA<_TypedPacket>()); expect(flipFlop.q.lanes, isA>()); @@ -457,7 +718,7 @@ void main() { final reset = Logic(); final d = _TypedPacket(name: 'reset_order_d'); final resetValue = LogicValue.ofString('000111100110100101101'); - final flipFlop = StructureFlipFlop( + final flipFlop = FlipFlop( clk, d, reset: reset, @@ -492,15 +753,47 @@ void main() { } }); + test('typed clone and naming distinguish constants from aliases', () { + final constant = Const(0x5, width: 4); + final constantClone = constant.cloneTyped(); + _expectConst(constantClone); + expect(constantClone, isA()); + expect(constantClone.value, constant.value); + expect( + () => constant.namedTyped('constant_alias'), + throwsA(isA()), + ); + + final widenedConstant = constant as Logic; + final alias = widenedConstant.namedTyped('constant_alias'); + _expectLogic(alias); + expect(alias, isNot(isA())); + expect(alias.value, constant.value); + + final net = LogicNet(width: 4); + final netClone = net.cloneTyped(); + final netAlias = net.namedTyped('net_alias'); + expect(netClone, isA()); + expect(netAlias, isA()); + + final packet = _TypedPacket(name: 'packet'); + final widenedPacket = packet as Logic; + final widenedClone = widenedPacket.cloneTyped(name: 'packet_clone'); + _expectLogic(widenedClone); + expect(widenedClone, isA<_TypedPacket>()); + + final constantPacket = _ConstPacket(name: 'constant_packet'); + final namedPacket = constantPacket.namedTyped('named_packet'); + expect(namedPacket, isA<_ConstPacket>()); + expect(namedPacket.hasConsts, isFalse); + }); + test('Passthrough infers typed contracts', () async { final scalar = Passthrough(Logic(width: 4)); final input = _TypedPacket(name: 'passthrough_input'); - final structured = StructurePassthrough(input); + final structured = Passthrough(input); await Future.wait([scalar.build(), structured.build()]); - - expect(scalar, isA>()); expect(scalar.out.runtimeType, Logic); - expect(structured, isA>()); expect(structured.in_, isA<_TypedPacket>()); expect(structured.out, isA<_TypedPacket>()); expect(structured.out.lanes, isA>()); @@ -513,12 +806,12 @@ void main() { expect(structured.out.lanes.typedLeafElements[1].data.value.toInt(), 0x34); }); - test('typedCases preserves structure and selects a default', () { + test('cases preserves structure and selects a default', () { final selector = Logic(width: 2); final first = _TypedPacket(name: 'first'); final second = _TypedPacket(name: 'second'); final fallback = _TypedPacket(name: 'fallback'); - final selected = typedCases(selector, {0: first, 2: second}, + final _TypedPacket selected = cases(selector, {0: first, 2: second}, defaultValue: fallback, name: 'selected'); expect(selected, isA<_TypedPacket>()); @@ -533,6 +826,78 @@ void main() { expect(selected.opcode.value.toInt(), 7); }); + test('cases uses an explicit generator for mixed packed sources', () { + final selector = Logic(); + final packet = _ConfiguredPacket(schema: 'packet', name: 'packet'); + final packed = Logic(width: packet.width); + final generator = _ConfiguredPacketGenerator('generated'); + final selected = cases( + selector, + { + 0: packet, + 1: packed, + }, + outputGenerator: generator.call, + ); + + expect(selected, isA<_ConfiguredPacket>()); + expect(selected.schema, 'generated'); + expect(generator.calls, 1); + packet.data.put(0x4); + packed.put(0xb); + selector.put(0); + expect(selected.data.value.toInt(), 0x4); + selector.put(1); + expect(selected.data.value.toInt(), 0xb); + }); + + test('generator-backed mux and cases match functional and SV vectors', + () async { + final harness = _GeneratorVectorHarness( + Logic(), + Logic(), + _ConfiguredPacket(schema: 'first', name: 'first'), + _ConfiguredPacket(schema: 'second', name: 'second'), + Logic(width: 4), + ); + await harness.build(); + + final vectors = [ + Vector( + { + 'control': 0, + 'selector': 0, + 'first': 0x3, + 'second': 0xc, + 'rawBits': 0xe, + }, + {'out': 0x3, 'constantOut': 0x3}, + ), + Vector( + { + 'control': 1, + 'selector': 0, + 'first': 0x3, + 'second': 0xc, + 'rawBits': 0xe, + }, + {'out': 0xc, 'constantOut': 0x3}, + ), + Vector( + { + 'control': 1, + 'selector': 1, + 'first': 0x3, + 'second': 0xc, + 'rawBits': 0xe, + }, + {'out': 0xe, 'constantOut': 0x3}, + ), + ]; + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkIverilogVector(harness, vectors); + }); + test('typed indexed selection works in both invocation directions', () { final index = Logic(width: 2); final values = [ @@ -540,10 +905,8 @@ void main() { _TypedPacket(name: 'value1'), _TypedPacket(name: 'value2'), ]; - final selectedByList = - values.selectIndexTyped(index, name: 'selected_by_list'); - final selectedByIndex = - index.selectFromTyped(values, name: 'selected_by_index'); + final selectedByList = values.selectIndex(index, name: 'selected_by_list'); + final selectedByIndex = index.selectFrom(values, name: 'selected_by_index'); for (var valueIndex = 0; valueIndex < values.length; valueIndex++) { values[valueIndex].opcode.put(valueIndex + 3); From a0ba8d210c764f3ee6d1726d60e77b016e7e47a6 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 7 Oct 2026 22:08:36 -0700 Subject: [PATCH 09/33] Preserve scalar operation compatibility Retain scalar output naming and mergeable cases results during typed operation migration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 +++-- lib/src/modules/conditionals/case.dart | 1 + lib/src/modules/conditionals/flop.dart | 2 +- lib/src/modules/operation_utils.dart | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf408a702..af11d3a8c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,8 +42,9 @@ - **Migration:** plain `Const` and `LogicNet` sources no longer infer driveable outputs for operations that construct hardware. Request ``, for example `flop(clk, Const(1))`, to normalize them. - Use an operation `outputGenerator` when an explicit output representation is - required, including mixed packed and structured `cases` branches. + Use an operation `outputGenerator` (or `Passthrough.withOutput`) when an + explicit output representation is required, including mixed packed and + structured `cases` branches. - Added public `cloneTyped()` and `namedTyped()` conveniences for retaining a receiver's static type. `Const.cloneTyped()` remains a literal clone; `Const.namedTyped()` is rejected because a named alias must be driveable. diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index b15b293c8e..798d5be430 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -111,6 +111,7 @@ LogicType cases( width: width ?? prototype?.width, name: name, operation: 'cases<$LogicType>', + naming: Naming.mergeable, prototype: prototype, outputGenerator: outputGenerator, ); diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index f9d50569f1..7a4700099c 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -231,11 +231,11 @@ class _ScalarFlipFlop extends FlipFlop with SystemVerilog { Logic d, Logic outputSchema, { required this.asyncReset, + required bool usesOutputGenerator, Logic? en, Logic? reset, dynamic resetValue, super.name = 'flipflop', - required bool usesOutputGenerator, }) : super._() { addInput(_clkName, clk); addInput(_dName, d, width: d.width); diff --git a/lib/src/modules/operation_utils.dart b/lib/src/modules/operation_utils.dart index bc0e494b89..fa63615bb1 100644 --- a/lib/src/modules/operation_utils.dart +++ b/lib/src/modules/operation_utils.dart @@ -20,6 +20,7 @@ LogicType createOperationOutput({ required String name, required String operation, int? width, + Naming? naming, LogicType? prototype, LogicType Function({String? name})? outputGenerator, }) { @@ -36,7 +37,7 @@ LogicType createOperationOutput({ '$operation requires a width for a normalized Logic output.', ); } - output = Logic(name: name, width: width) as LogicType; + output = Logic(name: name, width: width, naming: naming) as LogicType; } else if (prototype != null) { output = prototype.cloneTyped(name: name); } else { From 0115418be5c34f7f6b90f643daad3f5352525470 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 7 Oct 2026 23:56:55 -0700 Subject: [PATCH 10/33] Restrict packed array output collapse Preserve internally computed sources for packed array outputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...systemverilog_synth_module_definition.dart | 4 ++++ test/sv_gen_test.dart | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 4fbff4fc49..eda1cc1122 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -215,6 +215,10 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { if (packedSource == null) { continue; } + if (!packedSource.source.isPort(module) || + !inputs.contains(packedSource.source.resolved)) { + continue; + } assignments.add(SynthAssignment(packedSource.source, arraySynth)); for (final subset in packedSource.subsets) { diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 820c232916..486bd25b34 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -487,6 +487,14 @@ class LogicToPackedArray extends Module { } } +class ComputedLogicToPackedArray extends Module { + ComputedLogicToPackedArray(Logic data) { + data = addInput('data', data, width: 8); + final computed = (data + Const(1, width: 8)).named('computed'); + addOutputArray('out', dimensions: [4], elementWidth: 2) <= computed; + } +} + class ReversedPackedArrayToLogic extends Module { ReversedPackedArrayToLogic(LogicArray array) { array = addInputArray( @@ -953,6 +961,19 @@ void main() { SimCompare.checkIverilogVector(mod, vectors); }); + test('computed Logic remains declared for packed array assignment', () async { + final mod = ComputedLogicToPackedArray(Logic(width: 8)); + await mod.build(); + + final vectors = [ + Vector({'data': 0x00}, {'out': 0x01}), + Vector({'data': 0xa5}, {'out': 0xa6}), + Vector({'data': 0xff}, {'out': 0x00}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + test('reordered packed array assignment retains its swizzle', () async { final mod = ReversedPackedArrayToLogic(LogicArray([4], 2, name: 'array')); await mod.build(); From 5c060139171c0f98d05dd753734733b4fe99a1b3 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 05:57:05 -0700 Subject: [PATCH 11/33] Internalize structure signatures for 0.7.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/modules/conditionals/flop.dart | 1 + lib/src/modules/gates.dart | 1 + lib/src/modules/passthrough.dart | 1 + lib/src/signals/logic_structure.dart | 29 --------------- .../signals/logic_structure_signature.dart | 37 +++++++++++++++++++ lib/src/utilities/config.dart | 2 +- pubspec.yaml | 2 +- 7 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 lib/src/signals/logic_structure_signature.dart diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index 7a4700099c..ea3595f4b2 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -9,6 +9,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/operation_utils.dart'; +import 'package:rohd/src/signals/logic_structure_signature.dart'; /// Constructs a positive-edge-triggered flip-flop preserving [LogicType]. /// diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index c99074f85d..57e34465e5 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/operation_utils.dart'; +import 'package:rohd/src/signals/logic_structure_signature.dart'; /// A gate [Module] that performs bit-wise inversion. class NotGate extends Module with InlineSystemVerilog { diff --git a/lib/src/modules/passthrough.dart b/lib/src/modules/passthrough.dart index acbab840d3..e8d56437cb 100644 --- a/lib/src/modules/passthrough.dart +++ b/lib/src/modules/passthrough.dart @@ -6,6 +6,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/operation_utils.dart'; +import 'package:rohd/src/signals/logic_structure_signature.dart'; /// A no-op module that preserves [LogicType]. /// diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index ea8bf32a55..6860daaca7 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -9,35 +9,6 @@ part of 'signals.dart'; -String _logicShapeSignature(Logic logic) { - if (logic is BaseLogicArray) { - final signatures = - logic.arrayElements.map(_logicShapeSignature).toList(growable: false); - final elementSignature = signatures.isEmpty - ? 'E' - : signatures.every((signature) => signature == signatures.first) - ? 'R${signatures.length}_${signatures.first}' - : 'H${signatures.join('_')}'; - return 'A${logic.dimensions.join('x')}_W${logic.elementWidth}_' - 'U${logic.numUnpackedDimensions}_${logic.isNet ? 'N' : 'L'}_' - '$elementSignature'; - } - if (logic is LogicStructure) { - return 'S${logic.elements.length}_' - '${logic.elements.map(_logicShapeSignature).join('_')}'; - } - return '${logic is Const ? 'C' : logic.isNet ? 'N' : 'L'}${logic.width}'; -} - -/// Returns a deterministic signature of [structure]'s recursive geometry. -/// -/// The signature includes field counts and widths plus array dimensions, -/// packing hints, leaf geometry, and net or constant distinctions. Signal -/// instance names are intentionally excluded. It can be embedded in a module -/// definition name when generated hardware depends on structure shape. -String logicStructureShapeSignature(LogicStructure structure) => - _logicShapeSignature(structure); - void _validateMatchingStructure( Logic first, Logic second, diff --git a/lib/src/signals/logic_structure_signature.dart b/lib/src/signals/logic_structure_signature.dart new file mode 100644 index 0000000000..e9364bd0c0 --- /dev/null +++ b/lib/src/signals/logic_structure_signature.dart @@ -0,0 +1,37 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_structure_signature.dart +// Internal structural signatures for operation module definitions. + +import 'package:meta/meta.dart'; +import 'package:rohd/src/signals/signals.dart'; + +String _logicShapeSignature(Logic logic) { + if (logic is BaseLogicArray) { + final signatures = + logic.arrayElements.map(_logicShapeSignature).toList(growable: false); + final elementSignature = signatures.isEmpty + ? 'E' + : signatures.every((signature) => signature == signatures.first) + ? 'R${signatures.length}_${signatures.first}' + : 'H${signatures.join('_')}'; + return 'A${logic.dimensions.join('x')}_W${logic.elementWidth}_' + 'U${logic.numUnpackedDimensions}_${logic.isNet ? 'N' : 'L'}_' + '$elementSignature'; + } + if (logic is LogicStructure) { + return 'S${logic.elements.length}_' + '${logic.elements.map(_logicShapeSignature).join('_')}'; + } + return '${logic is Const ? 'C' : logic.isNet ? 'N' : 'L'}${logic.width}'; +} + +/// Returns an internal signature for a structure's recursive geometry. +/// +/// This is used only when an operation's generated module definition depends +/// on the represented structure shape. It is intentionally not exported from +/// the public signal API. +@internal +String logicStructureShapeSignature(LogicStructure structure) => + _logicShapeSignature(structure); diff --git a/lib/src/utilities/config.dart b/lib/src/utilities/config.dart index a3b930b389..5bef3bff83 100644 --- a/lib/src/utilities/config.dart +++ b/lib/src/utilities/config.dart @@ -10,5 +10,5 @@ /// A utility for ROHD configuration file. class Config { /// The version of the ROHD framework. - static const String version = '0.6.11'; + static const String version = '0.7.0'; } diff --git a/pubspec.yaml b/pubspec.yaml index 3894b28c72..b16d7813f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: rohd -version: 0.6.11 +version: 0.7.0 description: The Rapid Open Hardware Development (ROHD) framework, a framework for describing and verifying hardware. homepage: https://intel.github.io/rohd-website repository: https://github.com/intel/rohd From 6055759d694f7efc3fdc819f7eb027c914f9379e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 06:59:14 -0700 Subject: [PATCH 12/33] remove deprecations --- lib/src/interfaces/pair_interface.dart | 23 ++++------------------- lib/src/modules/conditionals/if.dart | 4 ++-- lib/src/values/logic_value.dart | 4 ++-- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index 2bbf8cada6..9fc9160192 100644 --- a/lib/src/interfaces/pair_interface.dart +++ b/lib/src/interfaces/pair_interface.dart @@ -45,23 +45,12 @@ enum PairRole { /// It can be either directly used for simple scenarios, or extended for more /// complex situations. class PairInterface extends Interface { - /// A function that can be used to modify all port names in a certain way. - @Deprecated( - 'Use `uniquify` when connecting or adding sub interfaces instead.') - String Function(String original)? modify; - /// Constructs an instance of a [PairInterface] with the specified ports. - /// - /// The [modify] function will allow modification of all port names, in - /// addition to the usual uniquification that can occur during [connectIO]. PairInterface({ List? portsFromConsumer, List? portsFromProvider, List? sharedInputPorts, List? commonInOutPorts, - @Deprecated( - 'Use `uniquify` when connecting or adding sub interfaces instead.') - this.modify, }) { if (portsFromConsumer != null) { setPorts(portsFromConsumer, [PairDirection.fromConsumer]); @@ -169,16 +158,12 @@ class PairInterface extends Interface { Iterable? inOutTags, String Function(String original)? uniquify}) { final nonNullUniquify = uniquify ?? (original) => original; - // ignore: deprecated_member_use_from_same_package - deprecated backwards compat - final nonNullModify = modify ?? (original) => original; - String newUniquify(String original) => - nonNullUniquify(nonNullModify(original)); super.connectIO(module, srcInterface, inputTags: inputTags, outputTags: outputTags, inOutTags: inOutTags, - uniquify: newUniquify); + uniquify: nonNullUniquify); if (subInterfaces.isNotEmpty) { if (srcInterface is! PairInterface) { @@ -196,7 +181,7 @@ class PairInterface extends Interface { subInterfaceEntry.value.uniquify ?? (original) => original; String newSubIntfUniquify(String original) => - nonNullUniquify(nonNullSubIntfUniquify(nonNullModify(original))); + nonNullUniquify(nonNullSubIntfUniquify(original)); if (!srcInterface._subInterfaces.containsKey(subInterfaceName)) { throw InterfaceTypeException( @@ -421,8 +406,8 @@ class PairInterface extends Interface { portsFromProvider: _getMatchPorts(this, PairDirection.fromProvider), sharedInputPorts: _getMatchPorts(this, PairDirection.sharedInputs), commonInOutPorts: _getMatchPorts(this, PairDirection.commonInOuts), - // ignore: deprecated_member_use_from_same_package - deprecated backwards compat - modify: modify, + // ignore: deprecated_member_use_from_same_package - deprecated backwards compat + modify: modify, ); } diff --git a/lib/src/modules/conditionals/if.dart b/lib/src/modules/conditionals/if.dart index 8f46e4090e..12b7a86560 100644 --- a/lib/src/modules/conditionals/if.dart +++ b/lib/src/modules/conditionals/if.dart @@ -90,12 +90,12 @@ class If extends Conditional { if (iff is Else) { if (iff != iffs.last) { throw InvalidConditionalException( - 'Else must come last in an IfBlock.'); + 'Else must come last in an If.block.'); } if (iff == iffs.first) { throw InvalidConditionalException( - 'Else cannot be the first in an IfBlock.'); + 'Else cannot be the first in an If.block.'); } } } diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart index c3826dc162..bce00a5b92 100644 --- a/lib/src/values/logic_value.dart +++ b/lib/src/values/logic_value.dart @@ -425,8 +425,8 @@ abstract class LogicValue implements Comparable { /// The [stringRepresentation] should only contain bit values (e.g. no `0b` /// at the start). The order of the created [LogicValue] will be such that /// the `i`th character in [stringRepresentation] corresponds to the - /// `length - i - 1`th bit. That is, the last character of - /// [stringRepresentation] will be the 0th bit of the returned [LogicValue]. + /// `width - i - 1`th bit. That is, the last character in + /// [stringRepresentation] is the 0th bit of the returned [LogicValue]. /// /// ```dart /// var stringRepresentation = '1x0'; From 4fe9b6ad24391cad43c9ed692f1ab95476276c90 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 07:05:00 -0700 Subject: [PATCH 13/33] doc update --- CHANGELOG.md | 6 ++++++ doc/user_guide/_docs/A21-generation.md | 7 ++++++- ...fy_test.dart => pair_interface_hier_uniquify_test.dart} | 2 +- ...dify_test.dart => provider_consumer_uniquify_test.dart} | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) rename test/{pair_interface_hier_w_modify_test.dart => pair_interface_hier_uniquify_test.dart} (98%) rename test/{provider_consumer_w_modify_test.dart => provider_consumer_uniquify_test.dart} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index af11d3a8c2..a79de239dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,12 @@ Use an operation `outputGenerator` (or `Passthrough.withOutput`) when an explicit output representation is required, including mixed packed and structured `cases` branches. +- **Breaking:** Removed deprecated compatibility APIs. Use the canonical + `ExternalSystemVerilogModule`, `FiniteStateMachine`, `Sequential`, + `If.block`, `Logic.port`, `LogicValue` `of...` constructors, `SystemVerilog`, + `SynthFileContents`, and instance `clone()` APIs. Apply interface port names + at connection time with `uniquify`; use canonical operation outputs such as + `out` and `sum`; and access signal values through `value`. - Added public `cloneTyped()` and `namedTyped()` conveniences for retaining a receiver's static type. `Const.cloneTyped()` remains a literal clone; `Const.namedTyped()` is rejected because a named alias must be driveable. diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 92d1568d06..ba279ec838 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -285,7 +285,12 @@ With `multiFile: true`, `SystemVerilogService` writes one `.sv` file per generated module definition. For custom synthesis flows, [`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) accepts a `Module` and a `Synthesizer` (usually a -`SystemVerilogSynthesizer`). +`SystemVerilogSynthesizer`). It provides `SynthFileContents` objects through +`getSynthFileContents`; each includes its contents, file name, and context +about the `module` it refers to and its `instanceTypeName`. With these APIs, +you can generate named files, add file headers, ignore generation of some +modules, and generate file lists for other tools. `SynthBuilder.multi` makes +it convenient to generate outputs for multiple independent hierarchies. ## Capturing waveforms diff --git a/test/pair_interface_hier_w_modify_test.dart b/test/pair_interface_hier_uniquify_test.dart similarity index 98% rename from test/pair_interface_hier_w_modify_test.dart rename to test/pair_interface_hier_uniquify_test.dart index 57756dc76a..8cd354aa28 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_uniquify_test.dart @@ -1,7 +1,7 @@ // Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// pair_interface_hier_w_modify_test.dart +// pair_interface_hier_uniquify_test.dart // Tests for PairInterface with hierarchy and explicit uniquification. // // 2023 March 9 diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_uniquify_test.dart similarity index 99% rename from test/provider_consumer_w_modify_test.dart rename to test/provider_consumer_uniquify_test.dart index f4d087cc02..fd28d7e5d0 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_uniquify_test.dart @@ -1,7 +1,7 @@ // Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// provider_consumer_w_modify_test.dart +// provider_consumer_uniquify_test.dart // Tests PairInterface provider and consumer naming with explicit uniquifiers. // // 2023 March 9 From d5c6021f5772b4f872f84f6719ad0c57dad3b052 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 08:57:40 -0700 Subject: [PATCH 14/33] Ignore flaky external markdown link hosts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/configs/mlc_config.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index 0b6ff56de4..3cbaed9f60 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -18,6 +18,12 @@ { "pattern":"^https://intel\\.github\\.io(?:/|$)" }, + { + "pattern":"^https://marketplace\\.visualstudio\\.com" + }, + { + "pattern":"^https://pub\\.dev" + }, { "pattern":"^https://pymtl3.readthedocs.io/en/latest/$" }, From 0cc2a7fe902fccf50aca447ce6a7951de5584af4 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 11:46:52 -0700 Subject: [PATCH 15/33] Document LogicEnum integration plans Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/logic_enum_netlist_metadata.md | 179 +++++++++++++++++++++++++++++ doc/logic_enum_typed_operations.md | 155 +++++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 doc/logic_enum_netlist_metadata.md create mode 100644 doc/logic_enum_typed_operations.md diff --git a/doc/logic_enum_netlist_metadata.md b/doc/logic_enum_netlist_metadata.md new file mode 100644 index 0000000000..b197c5b4a6 --- /dev/null +++ b/doc/logic_enum_netlist_metadata.md @@ -0,0 +1,179 @@ +# LogicEnum JSON Netlist Metadata + +## Purpose + +This note defines the proposed JSON-netlist work needed after `LogicEnum` from #599 is merged. + +The JSON netlist must always preserve hardware behavior as packed bits. The +proposal adds optional metadata so ROHD-aware tools can also preserve the +source-level enum domain, member names, and explicit encodings. + +## Current behavior and gap + +An enum member can lower to a mapped packed constant, so a generic netlist can +represent its hardware value correctly. For example, a member mapped to +`2'b10` remains two constant bits with that value. + +That is not enough for source-aware consumers. Without additional metadata, +the netlist cannot distinguish: + +```text +2'b10 +``` + +from: + +```text +Mode.execute, where Mode.execute is encoded as 2'b10 +``` + +The SystemVerilog backend can emit enum typedefs and symbolic values, but the +JSON netlist backend currently has no equivalent enum-definition or +enum-reference serialization. + +## Design principles + +1. `LogicEnum` remains a driveable signal, not a `Const` subtype. +2. Enum member literals continue to lower to ordinary packed constant bits. +3. Metadata supplements bit-accurate netlist output; it must not change + ordinary netlist semantics or require generic netlist consumers to + understand enums. +4. Metadata is emitted only for typed ports and retained named nets, not every + anonymous temporary signal. +5. Explicit conversion to packed `Logic` erases enum-domain metadata. +6. Incompatible enum mappings must never acquire equivalence solely from + matching width. + +## Proposed representation + +Declare enum definitions once per generated module in module attributes: + +```json +{ + "attributes": { + "rohd_enum_definitions": { + "Mode": { + "width": 2, + "members": { + "idle": "00", + "execute": "10", + "halt": "11" + } + } + } + } +} +``` + +Reference the definition from typed ports and retained named nets: + +```json +{ + "ports": { + "state": { + "direction": "input", + "bits": [1, 2], + "attributes": { + "rohd_enum": "Mode" + } + } + }, + "netnames": { + "next_state": { + "bits": [3, 4], + "attributes": { + "rohd_enum": "Mode" + } + } + } +} +``` + +The exact attribute spelling may change, but the schema must be stable, +versioned, and documented. Member encodings should use an unambiguous +most-significant-bit-first binary representation with no implied ordinal +meaning. + +## Synthesis integration + +The shared synthesis model should carry an optional enum-domain descriptor +containing: + +- enum definition identity; +- width; +- member-to-encoding mapping; +- preferred/reserved definition-name policy; and +- compatibility identity based on Dart enum type and exact mapping. + +The netlist translator should consume that descriptor when emitting: + +- module-level enum definitions; +- input, output, and inout port attributes; +- retained named internal net attributes; and +- hierarchy boundary metadata where the child and parent domains remain + compatible. + +Constant folding must continue to emit raw bit constants. A named enum signal +driven by a legal enum constant may retain its enum attribute; a standalone +constant has no independent enum domain. + +## Configuration and versioning + +Add a configuration option similar to: + +```dart +const NetlistSynthesizerConfiguration( + preserveEnumMetadata: true, +); +``` + +When disabled, the generated netlist must omit all enum metadata while +preserving exactly the same ports, cells, wire IDs, and packed constant bits. + +Adding this schema requires a `NetlistSynthesizer.formatVersion` update and +documentation describing compatibility expectations for consumers. + +## Validation + +Extend netlist validation to check: + +- each enum reference names a definition in its module; +- referenced width matches the associated port or net width; +- member names are unique and valid identifiers for the metadata schema; +- member encodings have the declared width and are unique; +- disabled metadata emits no enum references; and +- aliases and hierarchy transformations do not leave dangling references. + +## Required tests + +| Scenario | Required assertion | +| --- | --- | +| Enum input and output | Raw bit IDs remain correct and both port attributes reference the expected definition. | +| Named internal enum net | Retained net has the expected enum attribute. | +| Sparse mapping | Metadata preserves non-ordinal member encodings exactly. | +| Enum constant assignment | Netlist retains raw constant bits; the typed destination retains its enum metadata. | +| FSM | State ports/nets retain their mapping, including reset-state encoding. | +| Hierarchy | Compatible enum domains remain annotated across a parent/child boundary. | +| Type mismatch | Incompatible mapping/type connections fail before netlist emission or are explicitly lowered through a packed boundary. | +| Packed conversion | An explicit `Logic` conversion drops enum metadata. | +| Disabled metadata | No enum metadata is emitted and packed netlist behavior is unchanged. | +| Validation | Malformed or dangling enum attributes are rejected. | + +Tests should compare both parsed JSON structure and behaviorally relevant bit +connections. They should not rely only on pretty-printed JSON strings. + +## Non-goals + +- Emitting SystemVerilog typedef syntax in JSON netlists. +- Making a `LogicEnum` signal a constant object. +- Attaching domain metadata to anonymous temporary expressions. +- Treating a whole `LogicStructure`, such as a floating-point value, as an + enum. Structured literals and codecs are a separate abstraction. + +## Acceptance criteria + +The work is complete when a ROHD-aware netlist consumer can recover the enum +domain and exact member encodings for retained enum signals, while a generic +consumer can ignore the metadata and still consume a correct packed-bit +netlist. diff --git a/doc/logic_enum_typed_operations.md b/doc/logic_enum_typed_operations.md new file mode 100644 index 0000000000..c46f9871b1 --- /dev/null +++ b/doc/logic_enum_typed_operations.md @@ -0,0 +1,155 @@ +# LogicEnum Integration with Generic Typed Operations + +## Purpose + +This note records the work required in #704 after `LogicEnum` +from #599 is merged and rebased onto the current codebase. + +The goal is to make `LogicEnum` a first-class scalar domain type for the +existing generic operation APIs. This work must extend `mux`, `flop`, `cases`, +selection, and passthrough rather than introducing parallel typed-operation +APIs. + +## Preconditions + +Before implementing this work: + +1. Rebase #599 onto current `main`. +2. Preserve the existing `LogicEnum` mapping, clone, assignment, case, FSM, + and SystemVerilog contracts. +3. Keep the generic operation APIs introduced by #704 as the only public + operation spellings. + +## Domain and literal contract + +`LogicEnum` is a driveable domain signal, not a `Const` subtype. + +- A `LogicEnum` clone must retain its Dart enum type, mapping, width, + `definitionName`, and definition-name reservation policy while creating a + driveable signal. +- A Dart enum member is a domain literal only in the context of a + `LogicEnum` mapping. It must never silently mean the member ordinal. +- `getsEnum`, `put`, `inject`, and conditional assignment map a member through + the receiving enum's explicit mapping. +- A packed `Const` may drive an enum only when its exact value is represented + in the enum mapping. +- Raw packed `Logic` may cross into an enum domain under the enum's existing + runtime validation policy. It is an explicit bit-level boundary. +- Requesting `` is the explicit domain-erasure path for an operation + result. + +These rules match the existing distinction between a literal source and a +driveable operation output. + +## Operation behavior + +### Mux + +`mux` and `Mux` must infer `LogicEnum` from enum operands. + +- The default result schema is `d0`, including its mapping and configuration. +- An output generator can intentionally select another compatible enum schema. +- Inputs with different Dart enum types or incompatible mappings must be + rejected rather than reinterpreted by width. +- A constant control without an output generator returns the selected source by + identity. +- A supplied output generator is invoked once and materializes a fresh, + driveable result even for a constant control. +- `mux` deliberately drops enum-domain information and operates on + packed bits. + +### Flop + +`flop` and `FlipFlop` must preserve `LogicEnum`. + +- The default result schema is `d`. +- A reset by enum member must use the output mapping. +- A packed reset constant is legal only when its encoding belongs to the + output mapping. +- An unmapped reset value must fail with a mapping-specific diagnostic. +- A reset signal with a compatible enum domain must preserve that domain across + the register boundary. +- `flop` is the explicit packed, driveable normalization path. + +### Cases + +`cases` must distinguish enum selector literals from enum result literals. + +- An enum expression may use members of its mapping as case keys. +- Keys not present in the expression mapping must be rejected. +- A bare enum member cannot be a result value because it has no independent + output mapping. +- Result values must be enum signals, or callers must supply an enum output + generator. +- The default result prototype remains the first typed branch unless an output + generator is supplied. +- Mixed packed and enum result branches require an explicit output schema. + +### Selection and passthrough + +`selectFrom`, `selectIndex`, and `Passthrough` must preserve concrete +`LogicEnum` types and their mappings for homogeneous inputs. Explicit +`Logic` output requests intentionally erase the domain. + +## Required test matrix + +Add real `LogicEnum` coverage to `test/typed_operations_test.dart`; the +existing scalar-domain test double is not sufficient proof of enum behavior. + +| Surface | Required coverage | +| --- | --- | +| Clone and naming | Static type, mapping, width, definition name, reserved definition name, and driveability survive `cloneTyped` and `namedTyped`. | +| Mux | Inferred type, `d0` schema, output generator, constant-control identity, generator materialization, sparse mappings, mapping mismatch rejection, and `` erasure. | +| Flop | Inferred type, enable, synchronous/asynchronous reset, enum-member reset, legal packed reset, rejected unmapped reset, and `` normalization. | +| Cases | Enum expression/member keys, typed result signals, rejected bare result members, default behavior, mixed packed/domain branches, and output generators. | +| Selection | `selectFrom` and `selectIndex` retain enum type and reject incompatible mappings. | +| Passthrough | Default schema preservation and explicit output generator behavior. | +| Arrays | `LogicArrayOf>` works through the relevant operations and preserves its element builder and dimensions. | +| Structures | Structures with enum leaves work through mux, flop, cases, selection, and module hierarchy. | +| Backends | ROHD functional simulation, SystemVerilog with enums enabled and disabled, Icarus, and Verilator agree. | + +Every synthesized scenario should have a representative nested-composition +test, not only a root-module test. + +## FSM coverage + +`FiniteStateMachine` is a primary user of `LogicEnum`. + +Add or retain tests for: + +- reset to an enum state; +- legal transitions and default transitions; +- sparse and non-ordinal mappings; +- direct enum-member case labels; +- typed current and next state access; +- generated SystemVerilog enum typedefs and symbolic state members; and +- functional and external-simulator agreement. + +Constraining FSM state identifiers to `Enum` is a breaking API change and +needs a release migration note. + +## Structures and structured literals + +`LogicEnum` should support enum leaves in a `LogicStructure`, but it should +not become a general structured-value abstraction. + +A type such as `FloatingPoint` should remain a `LogicStructure` with a +separate literal/codec API, for example `FloatingPoint.one()` or +`FloatingPoint.fromDouble(1.0)`. Such a literal may use constant leaves, but +its `clone()` must construct driveable leaves so generic operations can create +a driveable output while retaining the `FloatingPoint` static type. + +This follows the existing structured-constant operation contract and keeps +finite scalar enum domains distinct from arbitrary structured encodings. + +## Acceptance criteria + +The work is complete when: + +1. No parallel typed operation API is introduced. +2. Enum type and mapping information is retained by every applicable generic + operation unless the caller explicitly requests `Logic`. +3. Invalid mappings and unmapped literals fail explicitly. +4. The test matrix above passes in ROHD simulation and emitted-SystemVerilog + simulation. +5. The migration and domain-erasure rules are documented for users. From 8742b56a06817797ddb3e6485498cf097afe338f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 8 Oct 2026 17:11:32 -0700 Subject: [PATCH 16/33] Retain 0.7 service compatibility APIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- lib/src/module.dart | 1 + .../systemverilog/system_verilog_service.dart | 10 +++++ lib/src/wave_dumper.dart | 40 +++++++++++++++++-- test/multimodule4_test.dart | 2 +- test/multimodule5_test.dart | 2 +- test/sv_gen_test.dart | 6 +-- 7 files changed, 54 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a79de239dc..e19913beb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ Use an operation `outputGenerator` (or `Passthrough.withOutput`) when an explicit output representation is required, including mixed packed and structured `cases` branches. -- **Breaking:** Removed deprecated compatibility APIs. Use the canonical +- **Breaking:** Removed compatibility APIs deprecated before `0.7.0`. Use the canonical `ExternalSystemVerilogModule`, `FiniteStateMachine`, `Sequential`, `If.block`, `Logic.port`, `LogicValue` `of...` constructors, `SystemVerilog`, `SynthFileContents`, and instance `clone()` APIs. Apply interface port names diff --git a/lib/src/module.dart b/lib/src/module.dart index bc2825e9c5..d6a329ee2b 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1178,6 +1178,7 @@ abstract class Module { /// file writing, see [SystemVerilogService] (and /// [SystemVerilogService.output] for the equivalent one-shot string). /// The [configuration] controls options specific to SystemVerilog output. + // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.7.0. @Deprecated('Use Module.dumpSystemVerilog(configuration: ...) for in-memory ' 'output or SystemVerilogService for advanced options.') String generateSynth({ diff --git a/lib/src/synthesizers/systemverilog/system_verilog_service.dart b/lib/src/synthesizers/systemverilog/system_verilog_service.dart index 2f064c5816..1a3ea307b0 100644 --- a/lib/src/synthesizers/systemverilog/system_verilog_service.dart +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -352,6 +352,16 @@ class SystemVerilogService extends CodeGenService { return null; } + /// Returns a map from generated module instance type name to its SV contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + @Deprecated('Use instanceTypeOutput(instanceTypeName) for lookup or ' + 'fileContents for iteration instead.') + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + /// The artifacts generated by this service. /// /// Single-file output is named `.sv`. Split output retains diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 0b46e88666..a71b7b753c 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -11,8 +11,35 @@ import 'package:rohd/rohd.dart'; /// Records VCD waveforms through [Module.dumpWaves] for legacy callers. /// -/// Use [Module.dumpWaves] for simple VCD output or [WaveformService] for -/// filtering, timescales, and recording windows. +/// [WaveDumper] is a simple wrapper around [WaveformService] for backward +/// compatibility. It provides the legacy API for recording all signal changes +/// in a simulation to a VCD file without retaining the complete VCD in memory. +/// +/// **Migration guide:** +/// +/// Replace: +/// ```dart +/// var dumper = WaveDumper(module, outputPath: 'output.vcd'); +/// ``` +/// +/// With: +/// ```dart +/// var service = module.dumpWaves(outputPath: 'output.vcd'); +/// ``` +/// +/// For more control over filtering, timescale, and recording windows, create a +/// [WaveformService] directly: +/// ```dart +/// final service = WaveformService( +/// module, +/// outputDirectory: 'build', +/// outputBaseName: 'debug', +/// writeToFile: true, +/// timescale: '1ns', +/// startTime: 100, +/// signalFilter: (signal) => signal.name.startsWith('debug_'), +/// ); +/// ``` @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') class WaveDumper { @@ -24,7 +51,14 @@ class WaveDumper { /// The output filepath of the generated waveforms. String get outputPath => _service.outputPath; - /// Records all signal changes in the built [module] at [outputPath]. + /// Attaches a [WaveDumper] to record all signal changes in a simulation of + /// [module] in a VCD file at [outputPath]. + /// + /// [module] must be built prior to construction. + /// + /// **Deprecated:** Use [Module.dumpWaves] for simple VCD output, or + /// [WaveformService] for signal filtering, custom timescale, recording + /// windows, and extensibility hooks for streaming applications. @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') WaveDumper(Module module, {String outputPath = 'waves.vcd'}) diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 599654a325..94922aeb37 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -56,7 +56,7 @@ void main() { 'Should find a z two levels deep', ); - final synth = ftm.generateSynth(); + final synth = ftm.dumpSystemVerilog(); // "z = 1" means it correctly traversed down from inputs assert( diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index 6cd89bcf62..8b0c8dc83a 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -34,7 +34,7 @@ void main() { final mod = TopModule(Logic()); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('Passthrough')); }); diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 486bd25b34..400d760766 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -931,7 +931,7 @@ void main() { test('packed one-dimensional array assigns directly to Logic', () async { final mod = PackedArrayToLogic(LogicArray([4], 2, name: 'array')); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign out = array;'), reason: sv); expect(sv, isNot(contains('// swizzle')), reason: sv); @@ -948,7 +948,7 @@ void main() { test('Logic assigns directly to packed one-dimensional array', () async { final mod = LogicToPackedArray(Logic(width: 8)); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, contains('assign out = data;'), reason: sv); @@ -977,7 +977,7 @@ void main() { test('reordered packed array assignment retains its swizzle', () async { final mod = ReversedPackedArrayToLogic(LogicArray([4], 2, name: 'array')); await mod.build(); - final sv = mod.generateSynth(); + final sv = mod.dumpSystemVerilog(); expect(sv, isNot(contains('assign out = array;')), reason: sv); expect(sv, contains('// swizzle'), reason: sv); From c983c57169224efec5d645f8cf0eff2cff40a263 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 9 Oct 2026 07:45:49 -0700 Subject: [PATCH 17/33] Integrate typed operations with TypedLogicArray Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/logic_enum_typed_operations.md | 2 +- doc/user_guide/_docs/A19-logic-structures.md | 2 +- test/typed_operations_test.dart | 118 ++++++++++--------- 3 files changed, 62 insertions(+), 60 deletions(-) diff --git a/doc/logic_enum_typed_operations.md b/doc/logic_enum_typed_operations.md index c46f9871b1..2d2cc4836e 100644 --- a/doc/logic_enum_typed_operations.md +++ b/doc/logic_enum_typed_operations.md @@ -104,7 +104,7 @@ existing scalar-domain test double is not sufficient proof of enum behavior. | Cases | Enum expression/member keys, typed result signals, rejected bare result members, default behavior, mixed packed/domain branches, and output generators. | | Selection | `selectFrom` and `selectIndex` retain enum type and reject incompatible mappings. | | Passthrough | Default schema preservation and explicit output generator behavior. | -| Arrays | `LogicArrayOf>` works through the relevant operations and preserves its element builder and dimensions. | +| Arrays | `TypedLogicArray, LogicValue>` works through the relevant operations and preserves its element builder and dimensions. | | Structures | Structures with enum leaves work through mux, flop, cases, selection, and module hierarchy. | | Backends | ROHD functional simulation, SystemVerilog with enums enabled and disabled, Icarus, and Verilator agree. | diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index 547d136006..8d547b2489 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -31,7 +31,7 @@ final forwarded = Passthrough(registered).out; forwarded.valid <= selected.valid; ``` -The same behavior applies to `LogicArray` and `LogicArrayOf`, including +The same behavior applies to `LogicArray` and `TypedLogicArray`, including nested typed arrays. A mux uses its `d0` operand as the default output prototype. Both operands must have matching concrete types and recursive geometry: field widths, array dimensions, packing hints, and leaf structure. diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index f602ad38dd..72a682ce6d 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -39,14 +39,16 @@ class _ScalarDomainLogic extends Logic { class _TypedPacket extends LogicStructure { Logic get opcode => elements[0]; - LogicArrayOf<_TypedLane> get lanes => elements[1] as LogicArrayOf<_TypedLane>; + TypedLogicArray<_TypedLane, LogicValue> get lanes => + elements[1] as TypedLogicArray<_TypedLane, LogicValue>; final List laneDimensions; _TypedPacket({this.laneDimensions = const [2], String? name}) : super([ Logic(name: 'opcode', width: 3), - LogicArrayOf<_TypedLane>(laneDimensions, _TypedLane.new, + TypedLogicArray<_TypedLane, LogicValue>( + laneDimensions, _TypedLane.new, name: 'lanes'), ], name: name ?? 'typed_packet'); @@ -213,8 +215,8 @@ void _expectLogicArray(LogicArray array) { expect(array, isA()); } -void _expectTypedLaneArray(LogicArrayOf<_TypedLane> array) { - expect(array, isA>()); +void _expectTypedLaneArray(TypedLogicArray<_TypedLane, LogicValue> array) { + expect(array, isA>()); } void _expectConst(Const value) { @@ -434,10 +436,12 @@ void main() { final selector = Logic(width: 2); final array0 = LogicArray([2], 4, name: 'array0'); final array1 = LogicArray([2], 4, name: 'array1'); - final typedArray0 = - LogicArrayOf<_TypedLane>([2], _TypedLane.new, name: 'typed_array0'); - final typedArray1 = - LogicArrayOf<_TypedLane>([2], _TypedLane.new, name: 'typed_array1'); + final typedArray0 = TypedLogicArray<_TypedLane, LogicValue>( + [2], _TypedLane.new, + name: 'typed_array0'); + final typedArray1 = TypedLogicArray<_TypedLane, LogicValue>( + [2], _TypedLane.new, + name: 'typed_array1'); final muxedArray = Mux(selector[0], array1, array0).out; final floppedArray = FlipFlop(clk, array0).q; @@ -456,14 +460,15 @@ void main() { final muxedTypedArray = Mux(selector[0], typedArray1, typedArray0).out; final floppedTypedArray = FlipFlop(clk, typedArray0).q; final passedTypedArray = Passthrough(typedArray0).out; - final LogicArrayOf<_TypedLane> casedTypedArray = + final TypedLogicArray<_TypedLane, LogicValue> casedTypedArray = cases(selector, {0: typedArray0, 1: typedArray1}); final selectedTypedArray = [typedArray0, typedArray1].selectIndex(selector); final selectedFromTypedArray = selector.selectFrom([typedArray0, typedArray1]); final clonedTypedArray = typedArray0.cloneTyped(); final namedTypedArray = typedArray0.namedTyped('named_typed_array'); - final pipelinedTypedArray = StructurePipeline>( + final pipelinedTypedArray = + StructurePipeline>( clk, typedArray0, stages: [(stage) => stage.value], @@ -493,33 +498,32 @@ void main() { ].forEach(_expectTypedLaneArray); }); - test('Mux infers nested LogicArrayOf types', () async { + test('Mux infers nested TypedLogicArray types', () async { final control = Logic(); final d1 = _TypedPacket(name: 'd1'); final d0 = _TypedPacket(name: 'd0'); final muxModule = Mux(control, d1, d0); await muxModule.build(); expect(muxModule.out, isA<_TypedPacket>()); - expect(muxModule.out.lanes, isA>()); - expect( - muxModule.out.lanes.typedLeafElements, everyElement(isA<_TypedLane>())); + expect(muxModule.out.lanes, isA>()); + expect(muxModule.out.lanes.arrayElements, everyElement(isA<_TypedLane>())); d0.opcode.put(1); - d0.lanes.typedLeafElements[0].data.put(0x10); - d0.lanes.typedLeafElements[1].data.put(0x20); + d0.lanes.arrayElements[0].data.put(0x10); + d0.lanes.arrayElements[1].data.put(0x20); d1.opcode.put(6); - d1.lanes.typedLeafElements[0].data.put(0xa0); - d1.lanes.typedLeafElements[1].data.put(0xb0); + d1.lanes.arrayElements[0].data.put(0xa0); + d1.lanes.arrayElements[1].data.put(0xb0); control.put(0); expect(muxModule.out.opcode.value.toInt(), 1); - expect(muxModule.out.lanes.typedLeafElements[0].data.value.toInt(), 0x10); - expect(muxModule.out.lanes.typedLeafElements[1].data.value.toInt(), 0x20); + expect(muxModule.out.lanes.arrayElements[0].data.value.toInt(), 0x10); + expect(muxModule.out.lanes.arrayElements[1].data.value.toInt(), 0x20); control.put(1); expect(muxModule.out.opcode.value.toInt(), 6); - expect(muxModule.out.lanes.typedLeafElements[0].data.value.toInt(), 0xa0); - expect(muxModule.out.lanes.typedLeafElements[1].data.value.toInt(), 0xb0); + expect(muxModule.out.lanes.arrayElements[0].data.value.toInt(), 0xa0); + expect(muxModule.out.lanes.arrayElements[1].data.value.toInt(), 0xb0); }); test('Mux preserves structure type with a constant control', () { @@ -685,7 +689,7 @@ void main() { await flipFlop.build(); unawaited(Simulator.run()); expect(flipFlop.q, isA<_TypedPacket>()); - expect(flipFlop.q.lanes, isA>()); + expect(flipFlop.q.lanes, isA>()); reset.inject(1); enable.inject(0); @@ -694,22 +698,22 @@ void main() { expect(flipFlop.q.value, LogicValue.ofInt(0, flipFlop.q.width)); d.opcode.inject(5); - d.lanes.typedLeafElements[0].data.inject(0x12); - d.lanes.typedLeafElements[0].enable.inject(1); - d.lanes.typedLeafElements[1].data.inject(0x34); - d.lanes.typedLeafElements[1].enable.inject(0); + d.lanes.arrayElements[0].data.inject(0x12); + d.lanes.arrayElements[0].enable.inject(1); + d.lanes.arrayElements[1].data.inject(0x34); + d.lanes.arrayElements[1].enable.inject(0); enable.inject(1); await clk.nextPosedge; expect(flipFlop.q.opcode.value.toInt(), 5); - expect(flipFlop.q.lanes.typedLeafElements[0].data.value.toInt(), 0x12); - expect(flipFlop.q.lanes.typedLeafElements[1].data.value.toInt(), 0x34); + expect(flipFlop.q.lanes.arrayElements[0].data.value.toInt(), 0x12); + expect(flipFlop.q.lanes.arrayElements[1].data.value.toInt(), 0x34); d.opcode.inject(2); - d.lanes.typedLeafElements[0].data.inject(0xaa); + d.lanes.arrayElements[0].data.inject(0xaa); enable.inject(0); await clk.nextPosedge; expect(flipFlop.q.opcode.value.toInt(), 5); - expect(flipFlop.q.lanes.typedLeafElements[0].data.value.toInt(), 0x12); + expect(flipFlop.q.lanes.arrayElements[0].data.value.toInt(), 0x12); await Simulator.endSimulation(); }); @@ -731,10 +735,10 @@ void main() { await clk.nextPosedge; reset.inject(0); expect(flipFlop.q.opcode.value.toInt(), 0x5); - expect(flipFlop.q.lanes.typedLeafElements[0].data.value.toInt(), 0xa5); - expect(flipFlop.q.lanes.typedLeafElements[0].enable.value, LogicValue.one); - expect(flipFlop.q.lanes.typedLeafElements[1].data.value.toInt(), 0x3c); - expect(flipFlop.q.lanes.typedLeafElements[1].enable.value, LogicValue.zero); + expect(flipFlop.q.lanes.arrayElements[0].data.value.toInt(), 0xa5); + expect(flipFlop.q.lanes.arrayElements[0].enable.value, LogicValue.one); + expect(flipFlop.q.lanes.arrayElements[1].data.value.toInt(), 0x3c); + expect(flipFlop.q.lanes.arrayElements[1].enable.value, LogicValue.zero); await Simulator.endSimulation(); }); @@ -796,14 +800,15 @@ void main() { expect(scalar.out.runtimeType, Logic); expect(structured.in_, isA<_TypedPacket>()); expect(structured.out, isA<_TypedPacket>()); - expect(structured.out.lanes, isA>()); + expect( + structured.out.lanes, isA>()); input.opcode.put(5); - input.lanes.typedLeafElements[0].data.put(0x12); - input.lanes.typedLeafElements[1].data.put(0x34); + input.lanes.arrayElements[0].data.put(0x12); + input.lanes.arrayElements[1].data.put(0x34); expect(structured.out.opcode.value.toInt(), 5); - expect(structured.out.lanes.typedLeafElements[0].data.value.toInt(), 0x12); - expect(structured.out.lanes.typedLeafElements[1].data.value.toInt(), 0x34); + expect(structured.out.lanes.arrayElements[0].data.value.toInt(), 0x12); + expect(structured.out.lanes.arrayElements[1].data.value.toInt(), 0x34); }); test('cases preserves structure and selects a default', () { @@ -934,26 +939,22 @@ void main() { (stage) { final next = stage.value.cloneTyped(name: 'stage_0_next'); next.opcode <= stage.value.opcode + 1; - for (var lane = 0; - lane < next.lanes.typedLeafElements.length; - lane++) { - next.lanes.typedLeafElements[lane].data <= - stage.value.lanes.typedLeafElements[lane].data; - next.lanes.typedLeafElements[lane].enable <= - stage.value.lanes.typedLeafElements[lane].enable; + for (var lane = 0; lane < next.lanes.arrayElements.length; lane++) { + next.lanes.arrayElements[lane].data <= + stage.value.lanes.arrayElements[lane].data; + next.lanes.arrayElements[lane].enable <= + stage.value.lanes.arrayElements[lane].enable; } return next; }, (stage) { final next = stage.value.cloneTyped(name: 'stage_1_next'); next.opcode <= stage.value.opcode + stage.get(-1).opcode; - for (var lane = 0; - lane < next.lanes.typedLeafElements.length; - lane++) { - next.lanes.typedLeafElements[lane].data <= - stage.value.lanes.typedLeafElements[lane].data; - next.lanes.typedLeafElements[lane].enable <= - stage.value.lanes.typedLeafElements[lane].enable; + for (var lane = 0; lane < next.lanes.arrayElements.length; lane++) { + next.lanes.arrayElements[lane].data <= + stage.value.lanes.arrayElements[lane].data; + next.lanes.arrayElements[lane].enable <= + stage.value.lanes.arrayElements[lane].enable; } return next; }, @@ -965,7 +966,8 @@ void main() { expect(pipeline.latency, 2); expect(pipeline.output, isA<_TypedPacket>()); expect(pipeline.get(0), same(input)); - expect(pipeline.output.lanes, isA>()); + expect( + pipeline.output.lanes, isA>()); expect(() => pipeline.values.add(input), throwsUnsupportedError); reset.inject(1); @@ -974,13 +976,13 @@ void main() { reset.inject(0); input.opcode.inject(2); - input.lanes.typedLeafElements[0].data.inject(0x45); + input.lanes.arrayElements[0].data.inject(0x45); await clk.nextPosedge; input.opcode.inject(4); - input.lanes.typedLeafElements[0].data.inject(0x67); + input.lanes.arrayElements[0].data.inject(0x67); await clk.nextPosedge; expect(pipeline.output.opcode.value.toInt(), 7); - expect(pipeline.output.lanes.typedLeafElements[0].data.value.toInt(), 0x45); + expect(pipeline.output.lanes.arrayElements[0].data.value.toInt(), 0x45); stall.inject(1); input.opcode.inject(1); From 1a5f1df6840cdcacac6fd98addd338d310ee1657 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 9 Oct 2026 17:03:52 -0700 Subject: [PATCH 18/33] a format was missed Signed-off-by: Desmond A. Kirkpatrick --- .../systemverilog/systemverilog_synth_module_definition.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index eda1cc1122..9fe398934a 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -226,6 +226,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } } + /// Inlines a fully covered packed bus into its sole submodule input. /// /// Each driver must cover the next contiguous destination range and supply From fdb418fec0fbbea68251269189ca19c792edbe15 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 10 Oct 2026 18:00:07 -0700 Subject: [PATCH 19/33] Add typed ready-valid structure pipeline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/modules/pipeline.dart | 127 ++++++++++++++++++++++++++++++++ test/typed_operations_test.dart | 74 +++++++++++++++++++ 2 files changed, 201 insertions(+) diff --git a/lib/src/modules/pipeline.dart b/lib/src/modules/pipeline.dart index 1b8294f915..6975eecc3a 100644 --- a/lib/src/modules/pipeline.dart +++ b/lib/src/modules/pipeline.dart @@ -120,6 +120,133 @@ class StructurePipeline { LogicType get([int? boundary]) => values[boundary ?? values.length - 1]; } +/// A ready/valid pipeline that preserves a concrete structure type. +/// +/// Each transform produces the payload registered at its corresponding stage. +/// The pipeline accepts input when [readyPipeIn] is high and advances each +/// occupied stage when the following stage can accept it. +class ReadyValidStructurePipeline { + /// Original input followed by each registered stage output. + late final List values; + + /// Indicates whether the final payload is valid. + late final Logic validPipeOut; + + /// Indicates whether the first stage can accept an input payload. + late final Logic readyPipeIn; + + /// Indicates whether the input payload is valid. + final Logic validPipeIn; + + /// Indicates whether the receiver can accept the output payload. + final Logic readyPipeOut; + + /// Number of combinational boundaries, including the input boundary. + int get stageCount => values.length; + + /// Number of registered transform stages. + int get latency => values.length - 1; + + /// Final typed pipeline value. + LogicType get output => values.last; + + /// Creates a typed ready/valid pipeline. + /// + /// Every entry in [stages] creates one registered elastic stage. A pipeline + /// without stages passes [input] through combinationally. + ReadyValidStructurePipeline( + Logic clk, + LogicType input, { + required this.validPipeIn, + required this.readyPipeOut, + required List> stages, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, + String name = 'ready_valid_structure_pipeline', + }) { + if (clk.width != 1) { + throw PortWidthMismatchException(clk, 1); + } + if (validPipeIn.width != 1) { + throw PortWidthMismatchException(validPipeIn, 1); + } + if (readyPipeOut.width != 1) { + throw PortWidthMismatchException(readyPipeOut, 1); + } + if (reset != null && reset.width != 1) { + throw PortWidthMismatchException(reset, 1); + } + + if (stages.isEmpty) { + values = List.unmodifiable([input]); + validPipeOut = validPipeIn; + readyPipeIn = readyPipeOut; + return; + } + + final readyValues = List.generate( + stages.length + 1, + (index) => index == stages.length + ? readyPipeOut + : Logic( + name: '${name}_ready_$index', + naming: Naming.mergeable, + ), + ); + final generatedValues = [input]; + final generatedValids = [validPipeIn]; + + for (var stage = 0; stage < stages.length; stage++) { + final transformed = stages[stage]( + StructurePipelineStageInfo._(stage, generatedValues), + ); + validateMatchingLogicStructure( + transformed, + input, + operation: 'ReadyValidStructurePipeline stage $stage', + ); + + generatedValues.add( + FlipFlop( + clk, + transformed, + en: readyValues[stage], + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: '${name}_stage_$stage', + ).q, + ); + generatedValids.add( + flop( + clk, + generatedValids.last, + en: readyValues[stage], + reset: reset, + resetValue: 0, + asyncReset: asyncReset, + ).named( + '${name}_valid_${stage + 1}', + naming: Naming.mergeable, + ), + ); + } + + for (var stage = stages.length - 1; stage >= 0; stage--) { + readyValues[stage] <= + ~generatedValids[stage + 1] | readyValues[stage + 1]; + } + + values = List.unmodifiable(generatedValues); + validPipeOut = generatedValids.last; + readyPipeIn = readyValues.first; + } + + /// Returns a typed value at [boundary], defaulting to [output]. + LogicType get([int? boundary]) => values[boundary ?? values.length - 1]; +} + /// Information and accessors associated with a [Pipeline] stage. class PipelineStageInfo { /// The index of the current stage in the associated [Pipeline]. diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index 72a682ce6d..b0c38919d1 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -165,6 +165,19 @@ class _TypedOperationsSynthesisHarness extends Module { ], ).output; addTypedOutput('out', piped.clone).gets(piped); + + final readyValidPiped = ReadyValidStructurePipeline<_TypedPacket>( + clk, + registered, + validPipeIn: control, + readyPipeOut: reset, + reset: reset, + stages: [(stage) => stage.value], + ); + addTypedOutput('ready_valid_out', readyValidPiped.output.clone) + .gets(readyValidPiped.output); + addOutput('ready_valid').gets(readyValidPiped.validPipeOut); + addOutput('ready_ready').gets(readyValidPiped.readyPipeIn); } } @@ -1015,6 +1028,67 @@ void main() { await Simulator.endSimulation(); }); + test('ReadyValidStructurePipeline preserves payloads under backpressure', + () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final valid = Logic(); + final ready = Logic(); + final input = _TypedPacket(name: 'ready_valid_pipeline_input'); + final pipeline = ReadyValidStructurePipeline<_TypedPacket>( + clk, + input, + validPipeIn: valid, + readyPipeOut: ready, + reset: reset, + stages: [ + (stage) { + final next = stage.value.cloneTyped(name: 'ready_valid_stage_0'); + next.opcode <= stage.value.opcode + 1; + next.lanes <= stage.value.lanes; + return next; + }, + (stage) { + final next = stage.value.cloneTyped(name: 'ready_valid_stage_1'); + next.opcode <= stage.value.opcode + 1; + next.lanes <= stage.value.lanes; + return next; + }, + ], + ); + unawaited(Simulator.run()); + + reset.inject(1); + valid.inject(0); + ready.inject(1); + await clk.nextPosedge; + reset.inject(0); + + input.opcode.inject(2); + input.lanes.arrayElements[0].data.inject(0x45); + valid.inject(1); + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 0); + + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 1); + expect(pipeline.output.opcode.value.toInt(), 4); + expect(pipeline.output.lanes.arrayElements[0].data.value.toInt(), 0x45); + + ready.inject(0); + input.opcode.inject(5); + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 1); + expect(pipeline.output.opcode.value.toInt(), 4); + + ready.inject(1); + await clk.nextPosedge; + expect(pipeline.output.opcode.value.toInt(), 4); + await clk.nextPosedge; + expect(pipeline.output.opcode.value.toInt(), 7); + await Simulator.endSimulation(); + }); + test('all structure-preserving operations compile to SystemVerilog', () async { final harness = _TypedOperationsSynthesisHarness( From 375c7e62f31a75cce2a4d8cfd4fb6c7eecfeb45b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 10 Oct 2026 21:32:23 -0700 Subject: [PATCH 20/33] Preserve aggregate structure assignment metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../conditionals/conditional_group.dart | 21 ++++++++ lib/src/signals/logic_structure.dart | 42 +++++++++++++++- test/logic_structure_test.dart | 49 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/lib/src/modules/conditionals/conditional_group.dart b/lib/src/modules/conditionals/conditional_group.dart index a8745eac61..f8573af4d8 100644 --- a/lib/src/modules/conditionals/conditional_group.dart +++ b/lib/src/modules/conditionals/conditional_group.dart @@ -61,3 +61,24 @@ class ConditionalGroup extends Conditional { )) .join('\n'); } + +/// A [ConditionalGroup] originating from one complete [LogicStructure] +/// assignment. +/// +/// The contained leaf assignments preserve the normal ROHD connection graph, +/// while [receiver] and [driver] retain the aggregate operation for backends +/// that can emit it directly. +class StructureConditionalGroup extends ConditionalGroup { + /// The complete structure assigned by this group. + final LogicStructure receiver; + + /// The source structure or packed value assigned to [receiver]. + final Logic driver; + + /// Creates leaf assignments for one aggregate structure assignment. + StructureConditionalGroup( + this.receiver, + this.driver, + super.conditionals, + ); +} diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index 6860daaca7..67d1f4aea3 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -286,7 +286,9 @@ class LogicStructure implements Logic { index += element.width; } - return ConditionalGroup(conditionalAssigns); + return this is BaseLogicArray + ? ConditionalGroup(conditionalAssigns) + : StructureConditionalGroup(this, otherLogic, conditionalAssigns); } /// A list of all leaf-level elements at the deepest hierarchy of this @@ -295,6 +297,44 @@ class LogicStructure implements Logic { _calculateLeafElements(), ); + /// Promotes direct non-array child structures into a new generic structure. + /// + /// Each promoted element is cloned and connected to its source. When + /// [prefixFieldNames] is `true`, promoted fields are named + /// `${child.name}_${field.name}`. This preserves the immediate structure + /// path and avoids common field-name collisions. The method throws a + /// [LogicConstructionException] when the resulting field names collide. + LogicStructure flattenOuter({String? name, bool prefixFieldNames = true}) { + final sources = <(Logic, String)>[]; + for (final element in elements) { + if (element is LogicStructure && element is! BaseLogicArray) { + for (final field in element.elements) { + final prefix = + element.name.endsWith('_') ? element.name : '${element.name}_'; + final fieldName = + prefixFieldNames ? '$prefix${field.name}' : field.name; + sources.add((field, fieldName)); + } + } else { + sources.add((element, element.name)); + } + } + + final names = {}; + for (final source in sources) { + if (!names.add(source.$2)) { + throw LogicConstructionException( + 'Flattened structure contains duplicate field name ${source.$2}.', + ); + } + } + + final flattenedElements = sources + .map((source) => source.$1.clone(name: source.$2)..gets(source.$1)) + .toList(growable: false); + return LogicStructure(flattenedElements, name: name ?? this.name); + } + /// Compute the list of all leaf elements, to be cached in [leafElements]. List _calculateLeafElements() { final leaves = []; diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index 362706f8e7..d2be8e329f 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -244,6 +244,55 @@ void main() { expect(orig.clone(name: 'newName').name, 'newName'); }); + test('whole structure conditional assignment retains metadata', () { + final receiver = MyStruct(name: 'receiver'); + final driver = MyStruct(name: 'driver'); + + final assignment = receiver < driver; + + expect(assignment, isA()); + final group = assignment as StructureConditionalGroup; + expect(group.receiver, same(receiver)); + expect(group.driver, same(driver)); + expect(group.conditionals, hasLength(receiver.leafElements.length)); + }); + + test('flatten outer clones and connects promoted fields', () { + final config = LogicStructure([ + Logic(name: 'mode', width: 2), + Logic(name: 'valid'), + ], name: 'config'); + final control = LogicStructure([ + Logic(name: 'enable'), + config, + ], name: 'control'); + + final flattened = control.flattenOuter(name: 'flatControl'); + + expect(flattened.name, 'flatControl'); + expect(flattened.elements.map((element) => element.name), + ['enable', 'config_mode', 'config_valid']); + expect(flattened.elements[1].width, 2); + expect( + flattened.elements[0].srcConnections, contains(control.elements[0])); + expect( + flattened.elements[1].srcConnections, contains(config.elements[0])); + expect( + flattened.elements[2].srcConnections, contains(config.elements[1])); + }); + + test('flatten outer rejects duplicate promoted field names', () { + final control = LogicStructure([ + Logic(name: 'mode'), + LogicStructure([ + Logic(name: 'mode'), + ], name: 'config'), + ], name: 'control'); + + expect(() => control.flattenOuter(prefixFieldNames: false), + throwsA(isA())); + }); + test('tricky withSet', () { // first field has width of 72 so this is the starting point // second field has a width of 12 From 42cddf1b03ba21ab3d6a24337dce19f7cb46cca2 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sat, 10 Oct 2026 21:32:41 -0700 Subject: [PATCH 21/33] Generalize ready-valid pipeline payloads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- benchmark/ssa_driver_search_benchmark.dart | 2 +- lib/src/modules/pipeline.dart | 67 +++++++++++++++++----- test/pipeline_test.dart | 2 +- test/typed_operations_test.dart | 36 +++++++++++- 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/benchmark/ssa_driver_search_benchmark.dart b/benchmark/ssa_driver_search_benchmark.dart index 86ac8e91c7..1528e88a2c 100644 --- a/benchmark/ssa_driver_search_benchmark.dart +++ b/benchmark/ssa_driver_search_benchmark.dart @@ -42,7 +42,7 @@ class LotsOfSsa extends Module { final signals = List.generate(signalsCount, (index) => Logic(name: 's$index')..gets(LotsOfLogic(a, b, counter).c)); - ReadyValidPipeline(a, a, b, + ReadyValidSignalPipeline(a, a, b, reset: b, signals: signals, stages: List.generate( diff --git a/lib/src/modules/pipeline.dart b/lib/src/modules/pipeline.dart index 6975eecc3a..fd6d253a63 100644 --- a/lib/src/modules/pipeline.dart +++ b/lib/src/modules/pipeline.dart @@ -9,6 +9,7 @@ // import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; /// Builds the combinational value registered by one [StructurePipeline] stage. typedef StructurePipelineTransform = LogicType @@ -120,12 +121,48 @@ class StructurePipeline { LogicType get([int? boundary]) => values[boundary ?? values.length - 1]; } -/// A ready/valid pipeline that preserves a concrete structure type. +/// Builds the combinational value registered by one ready/valid pipeline +/// stage. +typedef ReadyValidPipelineTransform = LogicType + Function(ReadyValidPipelineStageInfo stage); + +/// Typed values and history visible while building one ready/valid pipeline +/// stage. +class ReadyValidPipelineStageInfo { + /// Zero-based transform stage being constructed. + final int stage; + + final List _values; + + ReadyValidPipelineStageInfo._(this.stage, this._values); + + /// Typed value entering this transform stage. + LogicType get value => _values[stage]; + + /// Returns a typed value relative to this transform stage's input. + /// + /// Zero selects [value], while negative values select earlier registered + /// stages. Future stages cannot be referenced during construction. + LogicType get([int stageAdjustment = 0]) => getAbs(stage + stageAdjustment); + + /// Returns a typed value at an absolute pipeline boundary. + /// + /// Boundary zero is the original input. Boundary `n + 1` is the registered + /// output of transform stage `n`. + LogicType getAbs(int boundary) { + if (boundary < 0 || boundary >= _values.length) { + throw RangeError.range(boundary, 0, _values.length - 1, 'boundary'); + } + return _values[boundary]; + } +} + +/// A ready/valid pipeline that preserves its payload's concrete type. /// /// Each transform produces the payload registered at its corresponding stage. /// The pipeline accepts input when [readyPipeIn] is high and advances each /// occupied stage when the following stage can accept it. -class ReadyValidStructurePipeline { +class ReadyValidPipeline { /// Original input followed by each registered stage output. late final List values; @@ -154,12 +191,12 @@ class ReadyValidStructurePipeline { /// /// Every entry in [stages] creates one registered elastic stage. A pipeline /// without stages passes [input] through combinationally. - ReadyValidStructurePipeline( + ReadyValidPipeline( Logic clk, LogicType input, { required this.validPipeIn, required this.readyPipeOut, - required List> stages, + required List> stages, Logic? reset, dynamic resetValue, bool asyncReset = false, @@ -199,12 +236,12 @@ class ReadyValidStructurePipeline { for (var stage = 0; stage < stages.length; stage++) { final transformed = stages[stage]( - StructurePipelineStageInfo._(stage, generatedValues), + ReadyValidPipelineStageInfo._(stage, generatedValues), ); - validateMatchingLogicStructure( + validateOperationSource( transformed, input, - operation: 'ReadyValidStructurePipeline stage $stage', + operation: 'ReadyValidPipeline stage $stage', ); generatedValues.add( @@ -569,8 +606,12 @@ class Pipeline { } } -/// A pipeline that implements Ready/Valid protocol at each stage. -class ReadyValidPipeline extends Pipeline { +/// Legacy multi-signal ready/valid pipeline. +/// +/// Use [ReadyValidPipeline] for a single typed payload. This class remains +/// available for pipelines that need the legacy arbitrary-signal [Pipeline] +/// stage API. +class ReadyValidSignalPipeline extends Pipeline { /// Indicates that valid contents are ready to be received /// at the output of the pipeline. late final Logic validPipeOut; @@ -585,7 +626,7 @@ class ReadyValidPipeline extends Pipeline { /// is ready to pull out of the pipeline. final Logic readyPipeOut; - /// Constructs a pipeline with Ready/Valid protocol at each stage. + /// Constructs a legacy multi-signal ready/valid pipeline. /// /// The [validPipeIn] signal indicates that the input to the pipeline is /// valid. The [readyPipeOut] signal indicates that the receiver of the @@ -606,7 +647,7 @@ class ReadyValidPipeline extends Pipeline { /// Note that the [resetValues] will take effect the same way as a normal /// [Pipeline], but the valid indication on the output will remain at 0 until /// a valid input has made its way from the input to the output. - ReadyValidPipeline( + ReadyValidSignalPipeline( Logic clk, Logic validPipeIn, Logic readyPipeOut, { @@ -626,8 +667,8 @@ class ReadyValidPipeline extends Pipeline { asyncReset: asyncReset, ); - /// Creates a [ReadyValidPipeline] with multiple triggers. - ReadyValidPipeline.multi( + /// Creates a [ReadyValidSignalPipeline] with multiple triggers. + ReadyValidSignalPipeline.multi( super._clks, this.validPipeIn, this.readyPipeOut, { diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart index 31bf38bd9c..8a593c7b58 100644 --- a/test/pipeline_test.dart +++ b/test/pipeline_test.dart @@ -189,7 +189,7 @@ class RVPipelineModule extends Module { reset = addInput('reset', reset); final b = addOutput('b', width: a.width); - final pipeline = ReadyValidPipeline(clk, validIn, readyForOut, + final pipeline = ReadyValidSignalPipeline(clk, validIn, readyForOut, reset: reset, stages: [ (p) => [p.get(a) < p.get(a) + 1], diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index b0c38919d1..8c2bc30886 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -166,7 +166,7 @@ class _TypedOperationsSynthesisHarness extends Module { ).output; addTypedOutput('out', piped.clone).gets(piped); - final readyValidPiped = ReadyValidStructurePipeline<_TypedPacket>( + final readyValidPiped = ReadyValidPipeline<_TypedPacket>( clk, registered, validPipeIn: control, @@ -1028,14 +1028,14 @@ void main() { await Simulator.endSimulation(); }); - test('ReadyValidStructurePipeline preserves payloads under backpressure', + test('ReadyValidPipeline preserves structured payloads under backpressure', () async { final clk = SimpleClockGenerator(10).clk; final reset = Logic(); final valid = Logic(); final ready = Logic(); final input = _TypedPacket(name: 'ready_valid_pipeline_input'); - final pipeline = ReadyValidStructurePipeline<_TypedPacket>( + final pipeline = ReadyValidPipeline<_TypedPacket>( clk, input, validPipeIn: valid, @@ -1089,6 +1089,36 @@ void main() { await Simulator.endSimulation(); }); + test('ReadyValidPipeline preserves scalar payloads', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final valid = Logic(); + final ready = Logic(); + final input = Logic(width: 4); + final pipeline = ReadyValidPipeline( + clk, + input, + validPipeIn: valid, + readyPipeOut: ready, + reset: reset, + stages: [(stage) => stage.value], + ); + unawaited(Simulator.run()); + + reset.inject(1); + valid.inject(0); + ready.inject(1); + await clk.nextPosedge; + reset.inject(0); + + input.inject(0xa); + valid.inject(1); + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 1); + expect(pipeline.output.value.toInt(), 0xa); + await Simulator.endSimulation(); + }); + test('all structure-preserving operations compile to SystemVerilog', () async { final harness = _TypedOperationsSynthesisHarness( From c75bf5f458ba1d2b8120070b12d7ff6e54d72c17 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 13 Oct 2026 17:39:10 -0700 Subject: [PATCH 22/33] Integrate LogicEnum with typed operations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/modules/operation_utils.dart | 8 +++ test/typed_operations_test.dart | 92 ++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/lib/src/modules/operation_utils.dart b/lib/src/modules/operation_utils.dart index fa63615bb1..cbd55e4bda 100644 --- a/lib/src/modules/operation_utils.dart +++ b/lib/src/modules/operation_utils.dart @@ -82,6 +82,14 @@ void validateOperationSource( throw PortWidthMismatchException.equalWidth(source, output); } + if (source is LogicEnum && + output is LogicEnum && + !source.isEquivalentTypeTo(output)) { + throw LogicConstructionException( + '$operation source and output use incompatible LogicEnum mappings.', + ); + } + if (source is LogicStructure && output is LogicStructure) { validateMatchingLogicStructure(source, output, operation: operation); } else if (source is LogicStructure && output is! LogicStructure) { diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index 8c2bc30886..ddc8c07589 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -37,6 +37,19 @@ class _ScalarDomainLogic extends Logic { _ScalarDomainLogic(schema: schema, name: name ?? this.name); } +enum _OperationEnum { idle, active, done } + +LogicEnum<_OperationEnum> _operationEnum({String? name}) => + LogicEnum<_OperationEnum>.withMapping( + { + _OperationEnum.idle: 0, + _OperationEnum.active: 2, + _OperationEnum.done: 3, + }, + width: 2, + name: name, + ); + class _TypedPacket extends LogicStructure { Logic get opcode => elements[0]; TypedLogicArray<_TypedLane, LogicValue> get lanes => @@ -337,6 +350,85 @@ void main() { await Simulator.endSimulation(); }); + test('LogicEnum domains compose with generic operations', () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final reset = Logic(); + final selector = _operationEnum(name: 'selector'); + final d0 = _operationEnum(name: 'd0'); + final d1 = _operationEnum(name: 'd1'); + final muxed = Mux(control, d1, d0).out; + final cased = cases>( + selector, + { + _OperationEnum.idle: d0, + _OperationEnum.active: d1, + }, + defaultValue: d0, + ); + final indexed = [d0, d1].selectIndex(control, defaultValue: d0); + final forwarded = Passthrough(d0).out; + final flipFlop = FlipFlop( + clk, + cased, + reset: reset, + resetValue: _OperationEnum.idle, + ); + final registered = flipFlop.q; + + expect(muxed, isA>()); + expect(cased, isA>()); + expect(indexed, isA>()); + expect(forwarded, isA>()); + expect(registered, isA>()); + for (final result in [muxed, cased, indexed, forwarded, registered]) { + expect(result.mapping, d0.mapping); + } + expect( + () => Mux( + control, + LogicEnum<_OperationEnum>.withMapping( + { + _OperationEnum.idle: 0, + _OperationEnum.active: 1, + _OperationEnum.done: 3, + }, + width: 2, + ), + d0, + ), + throwsA(isA()), + ); + expect( + () => cases>( + selector, + {_OperationEnum.idle: _OperationEnum.idle}, + ), + throwsA(isA()), + ); + + await flipFlop.build(); + unawaited(Simulator.run()); + d0.inject(_OperationEnum.idle); + d1.inject(_OperationEnum.done); + control.inject(1); + selector.inject(_OperationEnum.active); + await Simulator.tick(); + expect(muxed.valueEnum, _OperationEnum.done); + expect(cased.valueEnum, _OperationEnum.done); + expect(indexed.valueEnum, _OperationEnum.done); + expect(forwarded.valueEnum, _OperationEnum.idle); + + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + expect(registered.valueEnum, _OperationEnum.idle); + selector.inject(_OperationEnum.active); + await clk.nextPosedge; + expect(registered.valueEnum, _OperationEnum.done); + await Simulator.endSimulation(); + }); + test('inferred constants and nets reject driveable operation results', () { final clk = Logic(); final control = Logic(); From a70797f0fe11451370f08a9e587cb396d3f8afdd Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 13 Oct 2026 19:43:39 -0700 Subject: [PATCH 23/33] Validate typed-array operation codecs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 ++ doc/user_guide/_docs/A19-logic-structures.md | 3 +- doc/user_guide/_docs/A20-logic-arrays.md | 5 ++- lib/src/signals/logic_structure.dart | 13 ++++++-- test/typed_operations_test.dart | 35 ++++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e19913beb4..e82478d8f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ Use an operation `outputGenerator` (or `Passthrough.withOutput`) when an explicit output representation is required, including mixed packed and structured `cases` branches. +- **Migration:** type-preserving operations on `TypedLogicArray` operands now + require the identical `valueCodec` instance. Reuse a shared codec or cross + an explicit `packed` boundary to reinterpret compatible bits. - **Breaking:** Removed compatibility APIs deprecated before `0.7.0`. Use the canonical `ExternalSystemVerilogModule`, `FiniteStateMachine`, `Sequential`, `If.block`, `Logic.port`, `LogicValue` `of...` constructors, `SystemVerilog`, diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index 8d547b2489..cbf9fb0f5f 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -26,9 +26,10 @@ their operands establish a compatible output representation: final selected = Mux(select, packet1, packet0).out; final registered = FlipFlop(clk, selected, reset: reset).q; final forwarded = Passthrough(registered).out; +final observedValid = forwarded.valid; // All three values have type Packet. -forwarded.valid <= selected.valid; +assert(observedValid.width == 1); ``` The same behavior applies to `LogicArray` and `TypedLogicArray`, including diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index d9b9432eb4..8bfb50e519 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -122,6 +122,9 @@ representations. `valueCodec` is optional only when `TValue` is `LogicValue`. A custom codec should decode every four-state value that can appear in its hardware. +Type-preserving operations require compatible arrays to use the identical +`valueCodec` instance; reuse a shared codec instance or pass `packed` +explicitly when intentionally reinterpreting the bits with a different codec. `dimensionNames` controls child naming during construction and cloning; it is not public axis metadata. See the [`TypedLogicArray` API documentation](https://intel.github.io/rohd/rohd/TypedLogicArray-class.html) @@ -236,7 +239,7 @@ final selected = Mux(select, samplesA, samplesB).out; final delayed = FlipFlop(clk, selected, reset: reset).q; final forwarded = Passthrough(delayed).out; -final bottomRightData = forwarded.elementAt([1, 2]).data; +final bottomRightData = forwarded.at([1, 2]).data; ``` The mux inputs must have matching concrete array types and geometry, including diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index 67d1f4aea3..fbc9c57dc1 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -40,6 +40,14 @@ void _validateMatchingStructure( '$operation array operands differ in shape at $path.', ); } + if (first is TypedLogicArray && + second is TypedLogicArray && + !identical(first.valueCodec, second.valueCodec)) { + throw LogicConstructionException( + '$operation typed-array operands use different value codecs at ' + '$path. Convert through packed to reinterpret their bits explicitly.', + ); + } } for (var index = 0; index < first.elements.length; index++) { @@ -56,8 +64,9 @@ void _validateMatchingStructure( /// Verifies that [first] and [second] have identical recursive structure. /// /// Compatibility requires matching concrete structure types, element counts, -/// leaf widths, and array geometry. Field names may differ because separately -/// constructed instances of one structure type can use different root names. +/// leaf widths, array geometry, and typed-array codec identity. Field names +/// may differ because separately constructed instances of one structure type +/// can use different root names. /// /// Throws a [LogicConstructionException] when the structures do not match. void validateMatchingLogicStructure( diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index ddc8c07589..11604ff765 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -50,6 +50,11 @@ LogicEnum<_OperationEnum> _operationEnum({String? name}) => name: name, ); +LogicValueCodec _typedLaneCodec() => LogicValueCodec( + decode: (value) => value.isValid ? value.toInt() : 0, + encode: (value) => LogicValue.ofInt(value, 9), + ); + class _TypedPacket extends LogicStructure { Logic get opcode => elements[0]; TypedLogicArray<_TypedLane, LogicValue> get lanes => @@ -645,6 +650,36 @@ void main() { throwsA(isA())); }); + test('typed operations require identical typed-array codecs', () { + final first = TypedLogicArray<_TypedLane, int>( + [2], + _TypedLane.new, + valueCodec: _typedLaneCodec(), + ); + final second = TypedLogicArray<_TypedLane, int>( + [2], + _TypedLane.new, + valueCodec: _typedLaneCodec(), + ); + + expect(identical(first.valueCodec, second.valueCodec), isFalse); + expect( + () => Mux(Logic(), second, first), + throwsA(isA()), + ); + expect( + () => cases>( + Logic(), + {0: first, 1: second}, + ), + throwsA(isA()), + ); + expect( + Mux(Logic(), second.packed, first.packed).out, + isA(), + ); + }); + test('typed operations consume structures with constant leaves', () async { final clk = SimpleClockGenerator(10).clk; final control = Logic(); From 4e8b954406037b006100a146c943fe6bc9046304 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 13 Oct 2026 19:49:10 -0700 Subject: [PATCH 24/33] Clarify 0.7 deprecation policy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 8 +++++--- doc/user_guide/_docs/A21-generation.md | 8 ++++++++ lib/src/diagnostics/inspector_service.dart | 3 ++- lib/src/module.dart | 2 +- .../systemverilog/system_verilog_service.dart | 1 + lib/src/wave_dumper.dart | 2 ++ 6 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e82478d8f1..73ac4fd339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,12 +48,14 @@ - **Migration:** type-preserving operations on `TypedLogicArray` operands now require the identical `valueCodec` instance. Reuse a shared codec or cross an explicit `packed` boundary to reinterpret compatible bits. -- **Breaking:** Removed compatibility APIs deprecated before `0.7.0`. Use the canonical - `ExternalSystemVerilogModule`, `FiniteStateMachine`, `Sequential`, +- **Breaking:** Removed compatibility APIs deprecated before `0.6.11`. Use the + canonical `ExternalSystemVerilogModule`, `FiniteStateMachine`, `Sequential`, `If.block`, `Logic.port`, `LogicValue` `of...` constructors, `SystemVerilog`, `SynthFileContents`, and instance `clone()` APIs. Apply interface port names at connection time with `uniquify`; use canonical operation outputs such as - `out` and `sum`; and access signal values through `value`. + `out` and `sum`; and access signal values through `value`. Compatibility APIs + first deprecated in `0.6.11` remain available, including `generateSynth`, + `WaveDumper`, `hierarchyJSON`, and `contentsByName`. - Added public `cloneTyped()` and `namedTyped()` conveniences for retaining a receiver's static type. `Const.cloneTyped()` remains a literal clone; `Const.namedTyped()` is rejected because a named alias must be driveable. diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index ba279ec838..e54ebb2b37 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -205,6 +205,14 @@ The `Naming.unpreferredName` function will modify a signal name to indicate to d ## Services API +### Migrating legacy generation APIs + +`generateSynth` and `WaveDumper` remain available as deprecated compatibility +APIs because their replacements were introduced in `0.6.11`. Prefer +`dumpSystemVerilog`, `SystemVerilogService`, `dumpWaves`, and +`WaveformService` for new code. Their deprecation does not imply removal in +this `0.7.0` release. + `ModuleService` is the shared API for module-scoped generation, capture, and inspection. Services can register with `ModuleServices` for lookup by DevTools and other consumers. `ArtifactProducingService` implementations expose named diff --git a/lib/src/diagnostics/inspector_service.dart b/lib/src/diagnostics/inspector_service.dart index 53ea49d553..74cc905746 100644 --- a/lib/src/diagnostics/inspector_service.dart +++ b/lib/src/diagnostics/inspector_service.dart @@ -118,7 +118,8 @@ class ModuleTree { json.encode({'status': 'fail', 'reason': 'module not yet build'}); } - /// Returns the module hierarchy as a JSON string. + /// Returns the built module hierarchy as JSON. + // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use hierarchyJson instead.') String get hierarchyJSON => hierarchyJson; } diff --git a/lib/src/module.dart b/lib/src/module.dart index d6a329ee2b..fda501fac6 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1178,7 +1178,7 @@ abstract class Module { /// file writing, see [SystemVerilogService] (and /// [SystemVerilogService.output] for the equivalent one-shot string). /// The [configuration] controls options specific to SystemVerilog output. - // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.7.0. + // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use Module.dumpSystemVerilog(configuration: ...) for in-memory ' 'output or SystemVerilogService for advanced options.') String generateSynth({ diff --git a/lib/src/synthesizers/systemverilog/system_verilog_service.dart b/lib/src/synthesizers/systemverilog/system_verilog_service.dart index 1a3ea307b0..0ffa9b8f7e 100644 --- a/lib/src/synthesizers/systemverilog/system_verilog_service.dart +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -356,6 +356,7 @@ class SystemVerilogService extends CodeGenService { /// /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition /// name used in the generated SV). + // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use instanceTypeOutput(instanceTypeName) for lookup or ' 'fileContents for iteration instead.') Map get contentsByName => { diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index a71b7b753c..62475aba34 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -40,6 +40,7 @@ import 'package:rohd/rohd.dart'; /// signalFilter: (signal) => signal.name.startsWith('debug_'), /// ); /// ``` +// ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') class WaveDumper { @@ -59,6 +60,7 @@ class WaveDumper { /// **Deprecated:** Use [Module.dumpWaves] for simple VCD output, or /// [WaveformService] for signal filtering, custom timescale, recording /// windows, and extensibility hooks for streaming applications. + // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') WaveDumper(Module module, {String outputPath = 'waves.vcd'}) From 9d78c89e896b81c7bec027eb6cbdde814621347f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 13 Oct 2026 20:18:21 -0700 Subject: [PATCH 25/33] Keep release changes under Next Release Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ lib/src/diagnostics/inspector_service.dart | 1 - lib/src/module.dart | 1 - .../synthesizers/systemverilog/system_verilog_service.dart | 1 - lib/src/utilities/config.dart | 2 +- lib/src/wave_dumper.dart | 2 -- pubspec.yaml | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ac4fd339..92005cdf74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ - Refined the typed-array hierarchy so `LogicArray` directly specializes `TypedLogicArray`, and strengthened custom `TypedLogicArray` cloning to preserve runtime types, codecs, unpacked dimensions, and constructor-only naming prefixes without exposing them as public axis metadata (). - Expanded `TypedLogicArray` support for recursively nested typed and ordinary arrays, structures containing array fields, mixed packed/unpacked dimensions, structured net and inout ports, and recursive netlist type metadata. Fixed generated SystemVerilog for array-valued elements to use packed offsets consistent with declarations and module-port connections, avoiding invalid chained selections in Icarus and Verilator (). - Preserved configured SystemVerilog output object types for nested arrays and added the opt-in `iverilogWorkaroundForUnpackedArrayVariables` setting for child-driven unpacked array variables that remain unknown in Icarus Verilog 12.0 (). +- `LogicArray` remains source-compatible for construction and operations. Its `value` and `previousValue` now return shape-carrying `LogicValueArray` instances, which remain `LogicValue` subtypes; code relying on their exact concrete runtime types may observe this behavior change (). +- Added support for structures with constant leaves when their `clone()` implementation produces driveable fields, preserving domain-specific constant structures through generic operations. +- Integrated `LogicEnum` with generic operations. Enum mappings are preserved through typed operation results, enum members can be used as `cases` keys, and incompatible enum mappings are rejected. +- Added typed `ReadyValidPipeline` payload support, preserving structured, typed-array, and aggregate assignments through pipeline stages. +- Preserved aggregate structure assignment metadata through synthesis and netlist generation. +- Fixed SystemVerilog synthesis of internally computed packed logic assigned to packed-array outputs, retaining required intermediate declarations. - Upgraded `Mux`, `FlipFlop`, `Passthrough`, `mux`, `flop`, `cases`, `selectIndex`, and `selectFrom` to preserve compatible concrete `Logic` subtypes, including nested `LogicArray` and `TypedLogicArray` boundaries. diff --git a/lib/src/diagnostics/inspector_service.dart b/lib/src/diagnostics/inspector_service.dart index 74cc905746..f34926be93 100644 --- a/lib/src/diagnostics/inspector_service.dart +++ b/lib/src/diagnostics/inspector_service.dart @@ -119,7 +119,6 @@ class ModuleTree { } /// Returns the built module hierarchy as JSON. - // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use hierarchyJson instead.') String get hierarchyJSON => hierarchyJson; } diff --git a/lib/src/module.dart b/lib/src/module.dart index fda501fac6..bc2825e9c5 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1178,7 +1178,6 @@ abstract class Module { /// file writing, see [SystemVerilogService] (and /// [SystemVerilogService.output] for the equivalent one-shot string). /// The [configuration] controls options specific to SystemVerilog output. - // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use Module.dumpSystemVerilog(configuration: ...) for in-memory ' 'output or SystemVerilogService for advanced options.') String generateSynth({ diff --git a/lib/src/synthesizers/systemverilog/system_verilog_service.dart b/lib/src/synthesizers/systemverilog/system_verilog_service.dart index 0ffa9b8f7e..1a3ea307b0 100644 --- a/lib/src/synthesizers/systemverilog/system_verilog_service.dart +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -356,7 +356,6 @@ class SystemVerilogService extends CodeGenService { /// /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition /// name used in the generated SV). - // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use instanceTypeOutput(instanceTypeName) for lookup or ' 'fileContents for iteration instead.') Map get contentsByName => { diff --git a/lib/src/utilities/config.dart b/lib/src/utilities/config.dart index 5bef3bff83..a3b930b389 100644 --- a/lib/src/utilities/config.dart +++ b/lib/src/utilities/config.dart @@ -10,5 +10,5 @@ /// A utility for ROHD configuration file. class Config { /// The version of the ROHD framework. - static const String version = '0.7.0'; + static const String version = '0.6.11'; } diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 62475aba34..a71b7b753c 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -40,7 +40,6 @@ import 'package:rohd/rohd.dart'; /// signalFilter: (signal) => signal.name.startsWith('debug_'), /// ); /// ``` -// ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') class WaveDumper { @@ -60,7 +59,6 @@ class WaveDumper { /// **Deprecated:** Use [Module.dumpWaves] for simple VCD output, or /// [WaveformService] for signal filtering, custom timescale, recording /// windows, and extensibility hooks for streaming applications. - // ignore: remove_deprecations_in_breaking_versions - Introduced in 0.6.11. @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') WaveDumper(Module module, {String outputPath = 'waves.vcd'}) diff --git a/pubspec.yaml b/pubspec.yaml index b16d7813f2..3894b28c72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: rohd -version: 0.7.0 +version: 0.6.11 description: The Rapid Open Hardware Development (ROHD) framework, a framework for describing and verifying hardware. homepage: https://intel.github.io/rohd-website repository: https://github.com/intel/rohd From 5e15527def0fb3ab26393a54a3b00cfc1bf70c72 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Thu, 15 Oct 2026 13:04:24 -0700 Subject: [PATCH 26/33] Share packed array rewrite across synthesizers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../netlist_synth_module_definition.dart | 2 +- ...systemverilog_synth_module_definition.dart | 111 +-------------- .../utilities/synth_module_definition.dart | 130 +++++++++++++++++- test/netlist_synthesizer_test.dart | 42 ++++++ 4 files changed, 174 insertions(+), 111 deletions(-) diff --git a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart index 4227226e40..aa508dc4f6 100644 --- a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -139,6 +139,6 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { @override void process() { - // Netlist synthesis preserves every submodule as a cell. + super.process(); } } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 9fe398934a..33b2bda794 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -37,7 +37,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { _expandNestedUnpackedArrayAssignments(); _inlinePackedRangesIntoSubmoduleInputs(); _collapseAggregateConnections(); - _collapsePackedArrayLogicAssignments(); + super.process(); _collapseWholeNetBuses(); _forwardPassthroughElementsIntoInlineables(); _replaceNetConnections(); @@ -196,37 +196,6 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { logic is BaseLogicArray || (logic is LogicStructure && logic.elements.any(_containsArray)); - /// Collapses a full packed [Logic] split across 1D array elements. - void _collapsePackedArrayLogicAssignments() { - final subsetsByOutput = _logicSubsetLookups(); - for (final arraySynth in outputs.where((signal) => signal.isArray)) { - final array = arraySynth.logics.whereType().firstOrNull; - if (array == null || - array.dimensions.length != 1 || - array.numUnpackedDimensions != 0) { - continue; - } - - final packedSource = _packedLogicSubsetSource( - array.elements.map(getSynthLogic).toList(growable: false), - subsetsByOutput, - expectedWidth: array.width, - ); - if (packedSource == null) { - continue; - } - if (!packedSource.source.isPort(module) || - !inputs.contains(packedSource.source.resolved)) { - continue; - } - - assignments.add(SynthAssignment(packedSource.source, arraySynth)); - for (final subset in packedSource.subsets) { - subset.clearInstantiation(); - } - } - } - /// Inlines a fully covered packed bus into its sole submodule input. /// /// Each driver must cover the next contiguous destination range and supply @@ -497,7 +466,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // sources that are tied through a pass-through net bus rather than via a // direct assignment (see [_traceNetSubsetSource]). final netSubsets = _netSubsetLookups(); - final logicSubsets = _logicSubsetLookups(); + final logicSubsets = logicSubsetLookups(); // Count how many times each array [SynthLogic] is used "as a whole", // along with where (a submodule port mapping is the only use we can @@ -624,7 +593,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { aggregateLogic.elementWidth == 1 && aggregateLogic.numUnpackedDimensions == 0; final packedSubsetSource = isPackedBitArray - ? _packedLogicSubsetSource( + ? packedLogicSubsetSource( elementLogics, logicSubsets, expectedWidth: agg.width, @@ -768,70 +737,6 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } - /// Indexes non-net [BusSubset] views by their resolved subset output. - Map> _logicSubsetLookups() { - final bySubset = >{}; - for (final instantiation in subModuleInstantiations) { - instantiation as SystemVerilogSynthSubModuleInstantiation; - final subsetModule = instantiation.module; - if (subsetModule is! BusSubset || subsetModule.original.isNet) { - continue; - } - final original = - instantiation.inputMapping[subsetModule.original.name]?.resolved; - final subset = - instantiation.outputMapping[subsetModule.subset.name]?.resolved; - if (original == null || subset == null) { - continue; - } - final view = ( - inst: instantiation, - original: original, - subset: subset, - start: subsetModule.startIndex, - end: subsetModule.endIndex, - ); - bySubset.putIfAbsent(subset, () => []).add(view); - } - return bySubset; - } - - /// Reconstructs one fully covered packed source from ordered element subset - /// mappings, returning the source and helper instantiations to clear. - ({ - SynthLogic source, - Set subsets, - })? _packedLogicSubsetSource( - List elements, - Map> subsetsByOutput, { - required int expectedWidth, - }) { - SynthLogic? source; - var nextBit = 0; - final subsets = {}; - for (final element in elements) { - if (element == null) { - return null; - } - final view = subsetsByOutput[element]?.singleOrNull; - if (view == null || - view.start != nextBit || - view.end != nextBit + element.width - 1 || - (source != null && source != view.original)) { - return null; - } - source ??= view.original; - subsets.add(view.inst); - nextBit += element.width; - } - if (source == null || - source.width != expectedWidth || - nextBit != expectedWidth) { - return null; - } - return (source: source, subsets: subsets); - } - /// Builds resolved lookups over every net [BusSubset] instantiation: a list /// of all views, plus maps keyed by the resolved `original` bus and the /// resolved `subset`. Reversed slices (`start > end`) are skipped because @@ -1656,16 +1561,6 @@ typedef _NetSubsetView = ({ int end, }); -/// A resolved view of a non-net [BusSubset] used to reconstruct a packed bus -/// from an array input's element mappings. -typedef _LogicSubsetView = ({ - SystemVerilogSynthSubModuleInstantiation inst, - SynthLogic original, - SynthLogic subset, - int start, - int end, -}); - /// A special [Module] for connecting or assigning two SystemVerilog nets /// together bidirectionally. /// diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index d53b696486..848800efae 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -1279,11 +1279,137 @@ class SynthModuleDefinition { } /// Performs additional processing on the current definition to simplify, - /// reduce, etc. + /// reduce, etc. Overrides must invoke `super.process()` at the appropriate + /// graph-processing point. @protected @visibleForOverriding void process() { - // by default, nothing! + _collapsePackedArrayLogicAssignments(); + } + + /// Collapses a full packed module input split across a packed output array. + /// + /// The rewrite is backend-independent: it replaces complete [BusSubset] + /// coverage with a direct [SynthAssignment] and removes only the consumed + /// helper instantiations. Backends remain responsible for declaration + /// legality and rendering of the resulting assignment. + void _collapsePackedArrayLogicAssignments() { + final subsetsByOutput = logicSubsetLookups(); + for (final arraySynth in outputs.where((signal) => signal.isArray)) { + final array = arraySynth.logics.whereType().firstOrNull; + if (array == null || + array.dimensions.length != 1 || + array.numUnpackedDimensions != 0) { + continue; + } + + final packedSource = packedLogicSubsetSource( + array.elements.map(getSynthLogic).toList(growable: false), + subsetsByOutput, + expectedWidth: array.width, + ); + if (packedSource == null || + !packedSource.source.isPort(module) || + !inputs.contains(packedSource.source.resolved)) { + continue; + } + + assignments.add(SynthAssignment(packedSource.source, arraySynth)); + for (final subset in packedSource.subsets) { + subset.clearInstantiation(); + } + } + } + + /// Indexes non-net [BusSubset] views by their resolved subset output. + @protected + Map< + SynthLogic, + List< + ({ + SynthSubModuleInstantiation inst, + SynthLogic original, + SynthLogic subset, + int start, + int end, + })>> logicSubsetLookups() { + final bySubset = >{}; + for (final instantiation in subModuleInstantiations) { + final subsetModule = instantiation.module; + if (subsetModule is! BusSubset || subsetModule.original.isNet) { + continue; + } + final original = + instantiation.inputMapping[subsetModule.original.name]?.resolved; + final subset = + instantiation.outputMapping[subsetModule.subset.name]?.resolved; + if (original == null || subset == null) { + continue; + } + final view = ( + inst: instantiation, + original: original, + subset: subset, + start: subsetModule.startIndex, + end: subsetModule.endIndex, + ); + bySubset.putIfAbsent(subset, () => []).add(view); + } + return bySubset; + } + + /// Reconstructs one fully covered packed source from ordered element subsets. + @protected + ({ + SynthLogic source, + Set subsets, + })? packedLogicSubsetSource( + List elements, + Map< + SynthLogic, + List< + ({ + SynthSubModuleInstantiation inst, + SynthLogic original, + SynthLogic subset, + int start, + int end, + })>> + subsetsByOutput, { + required int expectedWidth, + }) { + SynthLogic? source; + var nextBit = 0; + final subsets = {}; + for (final element in elements) { + if (element == null) { + return null; + } + final view = subsetsByOutput[element]?.singleOrNull; + if (view == null || + view.start != nextBit || + view.end != nextBit + element.width - 1 || + (source != null && source != view.original)) { + return null; + } + source ??= view.original; + subsets.add(view.inst); + nextBit += element.width; + } + if (source == null || + source.width != expectedWidth || + nextBit != expectedWidth) { + return null; + } + return (source: source, subsets: subsets); } /// Prunes any signals that are not used in this definition, including any diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 20c7d386cf..499a8d918e 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -14,8 +14,10 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synth_module_definition.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/synth_logic.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_module_definition.dart'; import 'package:rohd/src/synthesizers/utilities/synth_structure_concat.dart'; import 'package:test/test.dart'; @@ -70,6 +72,14 @@ class NotModule extends Module { } } +/// Splits a packed input across a packed array output. +class PackedInputArrayOutputModule extends Module { + PackedInputArrayOutputModule(Logic data) { + data = addInput('data', data, width: 8); + addOutputArray('out', dimensions: [4], elementWidth: 2) <= data; + } +} + /// Exercises preservation of user-named internal signals. class UserNamedInternalSignalsModule extends Module { UserNamedInternalSignalsModule(Logic a, Logic b) @@ -873,6 +883,38 @@ void main() { expect(replacementIds, isNot(equals(storageIds))); }); + test('shared packed-array rewrite applies before backend rendering', + () async { + final module = PackedInputArrayOutputModule(Logic(width: 8)); + await module.build(); + + void expectSharedRewrite(SynthModuleDefinition definition) { + final input = + definition.inputs.singleWhere((signal) => signal.name == 'data'); + final output = + definition.outputs.singleWhere((signal) => signal.name == 'out'); + expect( + definition.assignments.any( + (assignment) => + identical(assignment.src.resolved, input.resolved) && + identical(assignment.dst.resolved, output.resolved), + ), + isTrue, + ); + + final subsets = definition.subModuleInstantiations + .where((instantiation) => instantiation.module is BusSubset) + .toList(); + expect(subsets, isNotEmpty); + expect( + subsets.every((instantiation) => !instantiation.needsInstantiation), + isTrue); + } + + expectSharedRewrite(SynthModuleDefinition(module)); + expectSharedRewrite(NetlistSynthModuleDefinition(module)); + }); + for (final unpackedDimensions in [0, 2]) { test('split packed child drives permuted array bits $unpackedDimensions', () async { From 3b5153f2f986dc8d96392a628e2ff7e930f6138f Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 16 Oct 2026 00:35:40 -0700 Subject: [PATCH 27/33] Preserve signal types through pipelines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- benchmark/ssa_driver_search_benchmark.dart | 2 +- doc/user_guide/_docs/A19-logic-structures.md | 7 +- doc/user_guide/_docs/A20-logic-arrays.md | 2 +- lib/src/modules/conditionals/case.dart | 14 +- .../modules/conditionals/combinational.dart | 43 +- lib/src/modules/conditionals/conditional.dart | 24 +- .../conditionals/conditional_assign.dart | 6 +- lib/src/modules/conditionals/if.dart | 14 +- lib/src/modules/conditionals/ssa.dart | 129 +++++ lib/src/modules/pipeline.dart | 452 +++++------------- test/pipeline_test.dart | 2 +- test/typed_operations_test.dart | 315 ++++++++---- 13 files changed, 550 insertions(+), 462 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92005cdf74..765147096a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ - `LogicArray` remains source-compatible for construction and operations. Its `value` and `previousValue` now return shape-carrying `LogicValueArray` instances, which remain `LogicValue` subtypes; code relying on their exact concrete runtime types may observe this behavior change (). - Added support for structures with constant leaves when their `clone()` implementation produces driveable fields, preserving domain-specific constant structures through generic operations. - Integrated `LogicEnum` with generic operations. Enum mappings are preserved through typed operation results, enum members can be used as `cases` keys, and incompatible enum mappings are rejected. -- Added typed `ReadyValidPipeline` payload support, preserving structured, typed-array, and aggregate assignments through pipeline stages. +- `Pipeline.get`, `PipelineStageInfo.get`, and `PipelineStageInfo.getAbs` now preserve each identifier's concrete type through stage reconstruction and SSA, allowing structures, typed arrays, enums, and scalar signals in one pipeline. `ReadyValidPipeline` retains its established multi-signal API and gains the same per-identifier typed access without separate payload pipeline classes. - Preserved aggregate structure assignment metadata through synthesis and netlist generation. - Fixed SystemVerilog synthesis of internally computed packed logic assigned to packed-array outputs, retaining required intermediate declarations. - Upgraded `Mux`, `FlipFlop`, `Passthrough`, `mux`, `flop`, `cases`, diff --git a/benchmark/ssa_driver_search_benchmark.dart b/benchmark/ssa_driver_search_benchmark.dart index 1528e88a2c..86ac8e91c7 100644 --- a/benchmark/ssa_driver_search_benchmark.dart +++ b/benchmark/ssa_driver_search_benchmark.dart @@ -42,7 +42,7 @@ class LotsOfSsa extends Module { final signals = List.generate(signalsCount, (index) => Logic(name: 's$index')..gets(LotsOfLogic(a, b, counter).c)); - ReadyValidSignalPipeline(a, a, b, + ReadyValidPipeline(a, a, b, reset: b, signals: signals, stages: List.generate( diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index cbf9fb0f5f..645d41cd53 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -96,9 +96,10 @@ output's representation. Different structured domain types should be converted to `packed` explicitly before that bit reinterpretation. Use `selectIndex` and `selectFrom` for structure-preserving indexed selection. -`StructurePipeline` preserves the same concrete type at every -registered pipeline boundary. Specify `T` when creating a pipeline with inline -stage transforms so Dart can type the transform parameter. +`Pipeline.get`, `PipelineStageInfo.get`, and `PipelineStageInfo.getAbs` +preserve the concrete type of each supplied identifier. A single `Pipeline` +can therefore carry structures alongside unrelated arrays, enums, and scalar +signals. ### Migrating type-preserving operations diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 8bfb50e519..b1636a8ce2 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -246,7 +246,7 @@ The mux inputs must have matching concrete array types and geometry, including dimensions, leaf widths, packed/unpacked configuration, and leaf structure. Use generic `cases`, `selectIndex`, or `selectFrom` when selecting one complete typed array from multiple choices. Specify the array type parameter on -`StructurePipeline` when its stages use inline transforms. +`Pipeline` when its stages access the array with `p.get(array)`. ## Elements of arrays diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index 798d5be430..3d7df8d335 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -448,10 +448,10 @@ ${subPadding}end for (final localMapping in localMappings.entries) { if (!phiMappings.containsKey(localMapping.key)) { - phiMappings[localMapping.key] = Logic( + phiMappings[localMapping.key] = SsaLogic.cloneLike( + localMapping.key, + localMapping.value, name: '${localMapping.key.name}_phi', - width: localMapping.key.width, - naming: Naming.mergeable, ); } @@ -470,18 +470,20 @@ ${subPadding}end ] .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); for (final conditionals in [...items.map((e) => e.then), defaultItem!]) { final alreadyDrivenSsaSignals = conditionals .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); for (final signalNeedingInit in signalsNeedingInit) { if (!alreadyDrivenSsaSignals.contains(signalNeedingInit)) { - conditionals.add(signalNeedingInit < 0); + conditionals.add( + signalNeedingInit < SsaLogic.branchDefaultValue(signalNeedingInit), + ); } } } diff --git a/lib/src/modules/conditionals/combinational.dart b/lib/src/modules/conditionals/combinational.dart index bc3abff54b..4b6f71b2a3 100644 --- a/lib/src/modules/conditionals/combinational.dart +++ b/lib/src/modules/conditionals/combinational.dart @@ -87,16 +87,39 @@ class Combinational extends Always { /// mitigated by constructing the [Combinational.ssa] before connecting /// inputs to the rest of the design, but usually the impact is so small /// that it will not be noticeable. + /// factory Combinational.ssa( - List Function(Logic Function(Logic signal) s) construct, - {String name = 'combinational_ssa'}) { + List Function(Logic Function(Logic signal) s) construct, + {String name = 'combinational_ssa'}) => + Combinational._constructSsa(construct, name: name); + + /// Creates SSA combinational logic with type-preserving proxies. + /// + /// This supports internal users, such as [Pipeline], that need an SSA + /// callback to retain concrete signal types and configuration. + @internal + factory Combinational.ssaWithTypes( + List Function(Logic Function(Logic signal) s) construct, { + String name = 'combinational_ssa', + }) => + Combinational._constructSsa(construct, name: name, preserveTypes: true); + + factory Combinational._constructSsa( + List Function(Logic Function(Logic signal) s) construct, { + required String name, + bool preserveTypes = false, + }) { final context = _ssaContextCounter++; - final ssas = []; + final ssas = {}; Logic getSsa(Logic ref) { - final newSsa = SsaLogic(ref, context); - ssas.add(newSsa); + final newSsa = SsaLogic.create( + ref, + context, + preserveTypes: preserveTypes, + ); + ssas.addAll(SsaLogic.proxyLeaves(newSsa, context: context)); return newSsa; } @@ -112,22 +135,22 @@ class Combinational extends Always { return Combinational(conditionals, name: name); } - /// A map from [SsaLogic]s to signals that they drive. + /// A map from SSA proxies to signals that they drive. /// /// This only stores information temporarily during construction of a /// [Combinational.ssa] and clears afterwards. @internal - static final Map> signalToSsaDrivers = {}; + static final Map> signalToSsaDrivers = {}; /// Tags each downstream [Logic] from [ssaDriver] as such in /// [signalToSsaDrivers]. - static void _updateSsaDriverMap(SsaLogic ssaDriver) { + static void _updateSsaDriverMap(Logic ssaDriver) { final toParse = TraverseableCollection() ..addAll(ssaDriver.dstConnections); for (var i = 0; i < toParse.length; i++) { final tpi = toParse[i]; - signalToSsaDrivers.putIfAbsent(tpi, () => {}).add(ssaDriver); + signalToSsaDrivers.putIfAbsent(tpi, () => {}).add(ssaDriver); if (tpi.isInput && tpi.parentModule! is SystemVerilog) { toParse.addAll(tpi.parentModule!.outputs.values); @@ -145,7 +168,7 @@ class Combinational extends Always { // reference an element of a structure without separately SSA'ing it. // However, leaving this in here just in case (probably negligible perf // impact). - if (tpi is LogicStructure) { + if (tpi is LogicStructure && !SsaLogic.containsTypedProxy(tpi)) { toParse.addAll(tpi.elements); } } diff --git a/lib/src/modules/conditionals/conditional.dart b/lib/src/modules/conditionals/conditional.dart index 091c368442..c478a475ca 100644 --- a/lib/src/modules/conditionals/conditional.dart +++ b/lib/src/modules/conditionals/conditional.dart @@ -216,39 +216,45 @@ abstract class Conditional { // take all the "current" names for these signals for (final ssaDriver in ssaDrivers) { - if (!mappings.containsKey(ssaDriver.ref)) { - throw UninitializedSignalException(ssaDriver.ref.name); + final reference = SsaLogic.referenceOf(ssaDriver, context: context); + if (!mappings.containsKey(reference)) { + throw UninitializedSignalException(reference.name); } // if these are already connected, just skip it, we're fine already if (ssaDriver.srcConnection != null && - ssaDriver.srcConnection == mappings[ssaDriver.ref]!) { + ssaDriver.srcConnection == mappings[reference]!) { continue; } // if these are the same signal, also just skip it - if (ssaDriver == mappings[ssaDriver.ref]!) { + if (ssaDriver == mappings[reference]!) { continue; } // if at this point there's still a srcConnection, then something is wrong // and someone probably reused a signal when they shouldn't have if (ssaDriver.srcConnection != null) { - throw MappedSignalAlreadyAssignedException(ssaDriver.ref.name); + throw MappedSignalAlreadyAssignedException(reference.name); } - ssaDriver <= mappings[ssaDriver.ref]!; + ssaDriver <= mappings[reference]!; } } /// Searches for SSA nodes from a source [driver] which match the [context]. - static List _findSsaDriversFrom(Logic driver, int context) { - if (driver is SsaLogic && driver.context == context) { + static List _findSsaDriversFrom(Logic driver, int context) { + if (SsaLogic.isSsaProxy(driver, context: context)) { return [driver]; } // no need to check for context on this map since it clears each time - return Combinational.signalToSsaDrivers[driver]?.toList() ?? const []; + return Combinational.signalToSsaDrivers[driver] + ?.where( + (signal) => SsaLogic.isSsaProxy(signal, context: context), + ) + .toList() ?? + const []; } /// Given existing [currentMappings], connects [drivers] and [receivers] diff --git a/lib/src/modules/conditionals/conditional_assign.dart b/lib/src/modules/conditionals/conditional_assign.dart index 6e968fd367..424f0ef226 100644 --- a/lib/src/modules/conditionals/conditional_assign.dart +++ b/lib/src/modules/conditionals/conditional_assign.dart @@ -113,9 +113,9 @@ class ConditionalAssign extends Conditional { context: context); final newMappings = {...currentMappings}; - // if the receiver is an ssa node, then update the mapping - if (receiver is SsaLogic) { - newMappings[(receiver as SsaLogic).ref] = receiver; + // If the receiver is an SSA node, update the mapping. + if (SsaLogic.isSsaProxy(receiver, context: context)) { + newMappings[SsaLogic.referenceOf(receiver, context: context)] = receiver; } return newMappings; diff --git a/lib/src/modules/conditionals/if.dart b/lib/src/modules/conditionals/if.dart index 12b7a86560..627b253465 100644 --- a/lib/src/modules/conditionals/if.dart +++ b/lib/src/modules/conditionals/if.dart @@ -220,10 +220,10 @@ ${padding}end '''); for (final localMapping in localMappings.entries) { if (!phiMappings.containsKey(localMapping.key)) { - phiMappings[localMapping.key] = Logic( + phiMappings[localMapping.key] = SsaLogic.cloneLike( + localMapping.key, + localMapping.value, name: '${localMapping.key.name}_phi', - width: localMapping.key.width, - naming: Naming.mergeable, ); } @@ -241,18 +241,20 @@ ${padding}end '''); .flattened .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); for (final iff in iffs) { final alreadyDrivenSsaSignals = iff.then .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); for (final signalNeedingInit in signalsNeedingInit) { if (!alreadyDrivenSsaSignals.contains(signalNeedingInit)) { - iff.then.add(signalNeedingInit < 0); + iff.then.add( + signalNeedingInit < SsaLogic.branchDefaultValue(signalNeedingInit), + ); } } } diff --git a/lib/src/modules/conditionals/ssa.dart b/lib/src/modules/conditionals/ssa.dart index b76884ff3b..16a2bdc140 100644 --- a/lib/src/modules/conditionals/ssa.dart +++ b/lib/src/modules/conditionals/ssa.dart @@ -26,4 +26,133 @@ class SsaLogic extends Logic { @override SsaLogic clone({String? name}) => throw UnimplementedError('Should not clone an SsaLogic'); + + /// Creates an SSA proxy for [ref], preserving its concrete type when that + /// type can be reconstructed as a driveable signal. + static Logic create( + Logic ref, + int context, { + required bool preserveTypes, + }) { + if (preserveTypes && _canPreserveType(ref)) { + final proxy = ref.clone(name: ref.name); + if (proxy.runtimeType != ref.runtimeType) { + throw LogicConstructionException( + 'SSA proxy clone did not preserve ${ref.runtimeType}.', + ); + } + _markProxy(ref, proxy, context); + return proxy; + } + + return SsaLogic(ref, context); + } + + /// Returns whether [signal] is an SSA proxy in [context]. + static bool isSsaProxy(Logic signal, {required int context}) { + if (signal is SsaLogic) { + return signal.context == context; + } + return _typedReferences[signal]?.context == context; + } + + /// Returns the signal represented by an SSA [signal] in [context]. + static Logic referenceOf(Logic signal, {required int context}) { + if (signal is SsaLogic && signal.context == context) { + return signal.ref; + } + + final reference = _typedReferences[signal]; + if (reference != null && reference.context == context) { + return reference.ref; + } + + throw ArgumentError.value( + signal, + 'signal', + 'Not an SSA proxy in context $context.', + ); + } + + /// Returns all leaf-level SSA proxies contained by [signal]. + static Iterable proxyLeaves(Logic signal, {required int context}) { + if (signal is LogicStructure) { + return signal.leafElements.where( + (element) => isSsaProxy(element, context: context), + ); + } + + return isSsaProxy(signal, context: context) ? [signal] : const []; + } + + /// Returns whether [signal] contains a type-preserving SSA proxy. + static bool containsTypedProxy(Logic signal) => signal is LogicStructure + ? signal.leafElements.any( + (element) => _typedReferences[element] != null, + ) + : _typedReferences[signal] != null; + + /// Creates a driveable signal with [reference]'s concrete type. + static Logic cloneLike( + Logic reference, + Logic currentValue, { + required String name, + }) => + _typedReferences[currentValue] != null + ? reference.clone(name: name) + : Logic(name: name, width: reference.width, naming: Naming.mergeable); + + /// Returns the zero-value driver used to initialize an SSA branch. + /// + /// A [LogicEnum] must receive a variable driver so its mapping can apply the + /// same invalid-value handling as a width-only SSA node. + static dynamic branchDefaultValue(Logic signal) => signal is LogicEnum + ? (Logic( + name: '${signal.name}_ssa_default', + width: signal.width, + naming: Naming.mergeable, + )..gets(Const(0, width: signal.width))) + : 0; + + static bool _canPreserveType(Logic ref) { + if (ref is Const || ref is LogicNet || ref is SsaLogic) { + return false; + } + if (ref is! LogicStructure) { + return ref.runtimeType != Logic; + } + + return ref.leafElements.every( + (element) => element is! Const && element is! LogicNet, + ); + } + + static void _markProxy(Logic ref, Logic proxy, int context) { + if (ref is LogicStructure || proxy is LogicStructure) { + if (ref is! LogicStructure || + proxy is! LogicStructure || + ref.elements.length != proxy.elements.length) { + throw LogicConstructionException( + 'SSA proxy clone did not preserve the structure of ${ref.name}.', + ); + } + + for (var index = 0; index < ref.elements.length; index++) { + _markProxy(ref.elements[index], proxy.elements[index], context); + } + return; + } + + _typedReferences[proxy] = _SsaReference(ref, context); + } } + +class _SsaReference { + final Logic ref; + final int context; + + const _SsaReference(this.ref, this.context); +} + +final Expando<_SsaReference> _typedReferences = + Expando<_SsaReference>('typedSsaReferences'); diff --git a/lib/src/modules/pipeline.dart b/lib/src/modules/pipeline.dart index fd6d253a63..df7b9168f2 100644 --- a/lib/src/modules/pipeline.dart +++ b/lib/src/modules/pipeline.dart @@ -9,280 +9,6 @@ // import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/operation_utils.dart'; - -/// Builds the combinational value registered by one [StructurePipeline] stage. -typedef StructurePipelineTransform = LogicType - Function(StructurePipelineStageInfo stage); - -/// Typed values and history visible while building one structure pipeline -/// stage. -class StructurePipelineStageInfo { - /// Zero-based transform stage being constructed. - final int stage; - - final List _values; - - StructurePipelineStageInfo._(this.stage, this._values); - - /// Typed value entering this transform stage. - LogicType get value => _values[stage]; - - /// Returns a typed value relative to this transform stage's input. - /// - /// Zero selects [value], while negative values select earlier registered - /// stages. Future stages cannot be referenced during construction. - LogicType get([int stageAdjustment = 0]) => getAbs(stage + stageAdjustment); - - /// Returns a typed value at an absolute pipeline boundary. - /// - /// Boundary zero is the original input. Boundary `n + 1` is the registered - /// output of transform stage `n`. - LogicType getAbs(int boundary) { - if (boundary < 0 || boundary >= _values.length) { - throw RangeError.range(boundary, 0, _values.length - 1, 'boundary'); - } - return _values[boundary]; - } -} - -/// A single-payload pipeline that preserves a concrete structure type. -/// -/// Every transform receives a [StructurePipelineStageInfo] whose `value` is the -/// typed value entering that stage. The returned matching structure is -/// registered through [FlipFlop]. [values] contains the original -/// input followed by every registered stage output, so `stageCount` matches -/// [Pipeline]'s boundary-count convention and `latency` is one less. -class StructurePipeline { - /// Original input followed by each registered transform result. - late final List values; - - /// Number of combinational boundaries, including the input boundary. - int get stageCount => values.length; - - /// Number of registered transform stages. - int get latency => values.length - 1; - - /// Final typed pipeline value. - LogicType get output => values.last; - - /// Creates a typed structure pipeline. - /// - /// Each optional [stalls] entry is active-high. A high value prevents the - /// corresponding registered stage boundary from advancing. - StructurePipeline( - Logic clk, - LogicType input, { - required List> stages, - List? stalls, - Logic? reset, - dynamic resetValue, - bool asyncReset = false, - String name = 'structure_pipeline', - }) { - if (stalls != null && stalls.length != stages.length) { - throw ArgumentError.value( - stalls, - 'stalls', - 'Must contain one optional stall signal per transform stage.', - ); - } - - final generatedValues = [input]; - for (var stage = 0; stage < stages.length; stage++) { - final transformed = stages[stage]( - StructurePipelineStageInfo._(stage, generatedValues), - ); - validateMatchingLogicStructure( - transformed, - input, - operation: 'StructurePipeline stage $stage', - ); - final stall = stalls?[stage]; - if (stall != null && stall.width != 1) { - throw PortWidthMismatchException(stall, 1); - } - generatedValues.add( - FlipFlop( - clk, - transformed, - en: stall == null ? null : ~stall, - reset: reset, - resetValue: resetValue, - asyncReset: asyncReset, - name: '${name}_stage_$stage', - ).q, - ); - } - values = List.unmodifiable(generatedValues); - } - - /// Returns a typed value at [boundary], defaulting to [output]. - LogicType get([int? boundary]) => values[boundary ?? values.length - 1]; -} - -/// Builds the combinational value registered by one ready/valid pipeline -/// stage. -typedef ReadyValidPipelineTransform = LogicType - Function(ReadyValidPipelineStageInfo stage); - -/// Typed values and history visible while building one ready/valid pipeline -/// stage. -class ReadyValidPipelineStageInfo { - /// Zero-based transform stage being constructed. - final int stage; - - final List _values; - - ReadyValidPipelineStageInfo._(this.stage, this._values); - - /// Typed value entering this transform stage. - LogicType get value => _values[stage]; - - /// Returns a typed value relative to this transform stage's input. - /// - /// Zero selects [value], while negative values select earlier registered - /// stages. Future stages cannot be referenced during construction. - LogicType get([int stageAdjustment = 0]) => getAbs(stage + stageAdjustment); - - /// Returns a typed value at an absolute pipeline boundary. - /// - /// Boundary zero is the original input. Boundary `n + 1` is the registered - /// output of transform stage `n`. - LogicType getAbs(int boundary) { - if (boundary < 0 || boundary >= _values.length) { - throw RangeError.range(boundary, 0, _values.length - 1, 'boundary'); - } - return _values[boundary]; - } -} - -/// A ready/valid pipeline that preserves its payload's concrete type. -/// -/// Each transform produces the payload registered at its corresponding stage. -/// The pipeline accepts input when [readyPipeIn] is high and advances each -/// occupied stage when the following stage can accept it. -class ReadyValidPipeline { - /// Original input followed by each registered stage output. - late final List values; - - /// Indicates whether the final payload is valid. - late final Logic validPipeOut; - - /// Indicates whether the first stage can accept an input payload. - late final Logic readyPipeIn; - - /// Indicates whether the input payload is valid. - final Logic validPipeIn; - - /// Indicates whether the receiver can accept the output payload. - final Logic readyPipeOut; - - /// Number of combinational boundaries, including the input boundary. - int get stageCount => values.length; - - /// Number of registered transform stages. - int get latency => values.length - 1; - - /// Final typed pipeline value. - LogicType get output => values.last; - - /// Creates a typed ready/valid pipeline. - /// - /// Every entry in [stages] creates one registered elastic stage. A pipeline - /// without stages passes [input] through combinationally. - ReadyValidPipeline( - Logic clk, - LogicType input, { - required this.validPipeIn, - required this.readyPipeOut, - required List> stages, - Logic? reset, - dynamic resetValue, - bool asyncReset = false, - String name = 'ready_valid_structure_pipeline', - }) { - if (clk.width != 1) { - throw PortWidthMismatchException(clk, 1); - } - if (validPipeIn.width != 1) { - throw PortWidthMismatchException(validPipeIn, 1); - } - if (readyPipeOut.width != 1) { - throw PortWidthMismatchException(readyPipeOut, 1); - } - if (reset != null && reset.width != 1) { - throw PortWidthMismatchException(reset, 1); - } - - if (stages.isEmpty) { - values = List.unmodifiable([input]); - validPipeOut = validPipeIn; - readyPipeIn = readyPipeOut; - return; - } - - final readyValues = List.generate( - stages.length + 1, - (index) => index == stages.length - ? readyPipeOut - : Logic( - name: '${name}_ready_$index', - naming: Naming.mergeable, - ), - ); - final generatedValues = [input]; - final generatedValids = [validPipeIn]; - - for (var stage = 0; stage < stages.length; stage++) { - final transformed = stages[stage]( - ReadyValidPipelineStageInfo._(stage, generatedValues), - ); - validateOperationSource( - transformed, - input, - operation: 'ReadyValidPipeline stage $stage', - ); - - generatedValues.add( - FlipFlop( - clk, - transformed, - en: readyValues[stage], - reset: reset, - resetValue: resetValue, - asyncReset: asyncReset, - name: '${name}_stage_$stage', - ).q, - ); - generatedValids.add( - flop( - clk, - generatedValids.last, - en: readyValues[stage], - reset: reset, - resetValue: 0, - asyncReset: asyncReset, - ).named( - '${name}_valid_${stage + 1}', - naming: Naming.mergeable, - ), - ); - } - - for (var stage = stages.length - 1; stage >= 0; stage--) { - readyValues[stage] <= - ~generatedValids[stage + 1] | readyValues[stage + 1]; - } - - values = List.unmodifiable(generatedValues); - validPipeOut = generatedValids.last; - readyPipeIn = readyValues.first; - } - - /// Returns a typed value at [boundary], defaulting to [output]. - LogicType get([int? boundary]) => values[boundary ?? values.length - 1]; -} /// Information and accessors associated with a [Pipeline] stage. class PipelineStageInfo { @@ -306,15 +32,25 @@ class PipelineStageInfo { /// stage, you can access it relatively using [stageAdjustment]. For example, /// `p.get(x, -1)` will access the value of `x` at the output of one stage /// prior. - Logic get(Logic identifier, [int stageAdjustment = 0]) => + T get(T identifier, [int stageAdjustment = 0]) => getAbs(identifier, stage + stageAdjustment); /// Returns the output of [identifier] at the specified absolute [stageIndex] /// stage, if other than the current [stage]. Otherwise, returns the same /// thing as [get], for the current [stage]. - Logic getAbs(Logic identifier, int stageIndex) { + T getAbs(T identifier, int stageIndex) { final l = _pipeline.get(identifier, stageIndex); - return stageIndex == stage ? _ssa(l) : l; + if (stageIndex != stage) { + return l; + } + + final remapped = _ssa(l); + if (remapped is! T) { + throw LogicConstructionException( + 'SSA remapping did not preserve the requested $T type.', + ); + } + return remapped; } } @@ -341,24 +77,86 @@ class _PipeStage { /// Registers [newLogic] with this stage and creates appropriate inputs, /// outputs, and internal signals for the stage. void _addLogic(Logic newLogic, int index) { - input[newLogic] = Logic( + input[newLogic] = _clonePipelineLogic( + newLogic, name: '${newLogic.name}_stage${index}_i', - width: newLogic.width, - naming: Naming.mergeable, ); - output[newLogic] = Logic( + output[newLogic] = _clonePipelineLogic( + newLogic, name: '${newLogic.name}_stage${index}_o', - width: newLogic.width, - naming: Naming.mergeable, ); - main[newLogic] = Logic( + main[newLogic] = _clonePipelineLogic( + newLogic, name: '${newLogic.name}_stage$index', - width: newLogic.width, - naming: Naming.mergeable, ); } } +Logic _clonePipelineLogic(Logic source, {required String name}) { + if (_containsNonDriveablePipelineSignal(source)) { + throw LogicConstructionException( + 'Pipeline signals must contain only driveable, non-net Logic values. ' + 'Create an ordinary Logic alias before adding $source to a Pipeline.', + ); + } + + final clone = source.clone(name: name); + if (clone.runtimeType != source.runtimeType) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve ${source.runtimeType}.', + ); + } + return clone; +} + +bool _containsNonDriveablePipelineSignal(Logic logic) { + if (logic is Const || logic is LogicNet) { + return true; + } + return logic is LogicStructure && + logic.leafElements.any(_containsNonDriveablePipelineSignal); +} + +Conditional _pipelineAssignment(Logic receiver, Logic driver) { + if (receiver is! LogicStructure || driver is! LogicStructure) { + return receiver < driver; + } + + final receiverLeaves = receiver.leafElements; + final driverLeaves = driver.leafElements; + if (receiverLeaves.length != driverLeaves.length) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve the structure of ' + '${receiver.name}.', + ); + } + + return ConditionalGroup([ + for (var index = 0; index < receiverLeaves.length; index++) + receiverLeaves[index] < driverLeaves[index], + ]); +} + +void _connectPipelineLogic(Logic receiver, Logic driver) { + if (receiver is! LogicStructure || driver is! LogicStructure) { + receiver <= driver; + return; + } + + final receiverLeaves = receiver.leafElements; + final driverLeaves = driver.leafElements; + if (receiverLeaves.length != driverLeaves.length) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve the structure of ' + '${receiver.name}.', + ); + } + + for (var index = 0; index < receiverLeaves.length; index++) { + receiverLeaves[index] <= driverLeaves[index]; + } +} + /// A simple pipeline, separating arbitrary combinational logic by flop stages. class Pipeline { /// The clocks whose positive edges trigger the flops in this pipeline. @@ -454,7 +252,7 @@ class Pipeline { signals.forEach(_add); for (var stageIndex = 0; stageIndex < stageCount; stageIndex++) { - Combinational.ssa((ssa) { + Combinational.ssaWithTypes((ssa) { // keep track of the previously registered logics: final prevRegisteredLogics = _registeredLogics.toSet(); @@ -469,22 +267,22 @@ class Pipeline { if (!prevRegisteredLogics.contains(l)) { for (var i = 0; i < stageIndex; i++) { // make sure to hook up in-to-out through main for .get's - get(l, i) <= _i(l, i); - _o(l, i) <= get(l, i); + _connectPipelineLogic(get(l, i), _i(l, i)); + _connectPipelineLogic(_o(l, i), get(l, i)); } } } return [ for (final l in _registeredLogics) - ssa(get(l, stageIndex)) < _i(l, stageIndex), + _pipelineAssignment(ssa(get(l, stageIndex)), _i(l, stageIndex)), ...stageConditionals, ]; }, name: 'comb_stage$stageIndex'); // do output connections as assignments so they can be collapsed for (final l in _registeredLogics) { - _o(l, stageIndex) <= get(l, stageIndex); + _connectPipelineLogic(_o(l, stageIndex), get(l, stageIndex)); } } @@ -519,40 +317,35 @@ class Pipeline { _stages[i]._addLogic(newLogic, i); } - _stages[0].input[newLogic]! <= newLogic; - final ffAssigns = []; + _connectPipelineLogic(_stages[0].input[newLogic]!, newLogic); + final ffAssigns = []; for (var i = 1; i < _stages.length; i++) { - ffAssigns.add(_i(newLogic, i) < _o(newLogic, i - 1) as ConditionalAssign); + final assignment = + _pipelineAssignment(_i(newLogic, i), _o(newLogic, i - 1)); + final stall = _stages[i - 1].stall; + ffAssigns.add( + stall == null ? assignment : If(~stall, then: [assignment]), + ); } - final ffAssignsWithStall = List.generate( - stageCount - 1, - (index) { - final stall = _stages[index].stall; - final ffAssign = ffAssigns[index]; - final driver = stall != null - ? mux(stall, ffAssign.receiver, ffAssign.driver) - : ffAssign.driver; - return ffAssign.receiver < driver as ConditionalAssign; - }, - ); - - final stageResetVal = _resetValues?[newLogic]; final resetValuesForNewLogic = {}; - if (stageResetVal != null) { - for (final ffAssign in ffAssignsWithStall) { - resetValuesForNewLogic[ffAssign.receiver] = stageResetVal; + if (_resetValues?.containsKey(newLogic) ?? false) { + final stageResetVal = _resetValues![newLogic]; + for (var i = 1; i < _stages.length; i++) { + resetValuesForNewLogic[_i(newLogic, i)] = stageResetVal; } } - Sequential.multi( - _clks, - reset: reset, - resetValues: resetValuesForNewLogic, - asyncReset: asyncReset, - ffAssignsWithStall, - name: 'ff_${newLogic.name}', - ); + if (ffAssigns.isNotEmpty) { + Sequential.multi( + _clks, + ffAssigns, + reset: reset, + resetValues: resetValuesForNewLogic, + asyncReset: asyncReset, + name: 'ff_${newLogic.name}', + ); + } } /// The stage input for a signal associated with [logic] to [stageIndex]. @@ -588,7 +381,7 @@ class Pipeline { /// /// Use [stageIndex] to select the value of [logic] at a specific stage of the /// pipeline. The [stageIndex] must be less than [stageCount]. - Logic get(Logic logic, [int? stageIndex]) { + T get(T logic, [int? stageIndex]) { if (!_isRegistered(logic)) { if (_constructionComplete) { throw PortDoesNotExistException( @@ -602,16 +395,17 @@ class Pipeline { stageIndex ??= _stages.length - 1; final stageLogic = _stages[stageIndex].main[logic]!; + if (stageLogic is! T) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve the requested $T type.', + ); + } return stageLogic; } } -/// Legacy multi-signal ready/valid pipeline. -/// -/// Use [ReadyValidPipeline] for a single typed payload. This class remains -/// available for pipelines that need the legacy arbitrary-signal [Pipeline] -/// stage API. -class ReadyValidSignalPipeline extends Pipeline { +/// A multi-signal ready/valid [Pipeline]. +class ReadyValidPipeline extends Pipeline { /// Indicates that valid contents are ready to be received /// at the output of the pipeline. late final Logic validPipeOut; @@ -626,7 +420,7 @@ class ReadyValidSignalPipeline extends Pipeline { /// is ready to pull out of the pipeline. final Logic readyPipeOut; - /// Constructs a legacy multi-signal ready/valid pipeline. + /// Constructs a multi-signal ready/valid pipeline. /// /// The [validPipeIn] signal indicates that the input to the pipeline is /// valid. The [readyPipeOut] signal indicates that the receiver of the @@ -647,7 +441,7 @@ class ReadyValidSignalPipeline extends Pipeline { /// Note that the [resetValues] will take effect the same way as a normal /// [Pipeline], but the valid indication on the output will remain at 0 until /// a valid input has made its way from the input to the output. - ReadyValidSignalPipeline( + ReadyValidPipeline( Logic clk, Logic validPipeIn, Logic readyPipeOut, { @@ -667,8 +461,8 @@ class ReadyValidSignalPipeline extends Pipeline { asyncReset: asyncReset, ); - /// Creates a [ReadyValidSignalPipeline] with multiple triggers. - ReadyValidSignalPipeline.multi( + /// Creates a [ReadyValidPipeline] with multiple triggers. + ReadyValidPipeline.multi( super._clks, this.validPipeIn, this.readyPipeOut, { diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart index 8a593c7b58..31bf38bd9c 100644 --- a/test/pipeline_test.dart +++ b/test/pipeline_test.dart @@ -189,7 +189,7 @@ class RVPipelineModule extends Module { reset = addInput('reset', reset); final b = addOutput('b', width: a.width); - final pipeline = ReadyValidSignalPipeline(clk, validIn, readyForOut, + final pipeline = ReadyValidPipeline(clk, validIn, readyForOut, reset: reset, stages: [ (p) => [p.get(a) < p.get(a) + 1], diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index 11604ff765..c239c1dba7 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -173,29 +173,32 @@ class _TypedOperationsSynthesisHarness extends Module { resetValue: 0, outputGenerator: passed.clone, ).q; - final piped = StructurePipeline<_TypedPacket>( + final pipeline = Pipeline( clk, - registered, reset: reset, - resetValue: 0, + resetValues: {registered: 0}, stages: [ - (stage) => stage.value.namedTyped('pipeline_stage_value'), + (p) => [p.get(registered) < p.get(registered)], ], - ).output; + ); + final piped = pipeline.get(registered); addTypedOutput('out', piped.clone).gets(piped); - final readyValidPiped = ReadyValidPipeline<_TypedPacket>( + final readyValidPipeline = ReadyValidPipeline( clk, - registered, - validPipeIn: control, - readyPipeOut: reset, + control, + reset, reset: reset, - stages: [(stage) => stage.value], + resetValues: {registered: 0}, + stages: [ + (p) => [p.get(registered) < p.get(registered)], + ], ); - addTypedOutput('ready_valid_out', readyValidPiped.output.clone) - .gets(readyValidPiped.output); - addOutput('ready_valid').gets(readyValidPiped.validPipeOut); - addOutput('ready_ready').gets(readyValidPiped.readyPipeIn); + final readyValidPiped = readyValidPipeline.get(registered); + addTypedOutput('ready_valid_out', readyValidPiped.clone) + .gets(readyValidPiped); + addOutput('ready_valid').gets(readyValidPipeline.validPipeOut); + addOutput('ready_ready').gets(readyValidPipeline.readyPipeIn); } } @@ -561,11 +564,13 @@ void main() { final selectedFromArray = selector.selectFrom([array0, array1]); final clonedArray = array0.cloneTyped(); final namedArray = array0.namedTyped('named_array'); - final pipelinedArray = StructurePipeline( + final arrayPipeline = Pipeline( clk, - array0, - stages: [(stage) => stage.value], - ).output; + stages: [ + (p) => [p.get(array0) < p.get(array0)] + ], + ); + final LogicArray pipelinedArray = arrayPipeline.get(array0); final muxedTypedArray = Mux(selector[0], typedArray1, typedArray0).out; final floppedTypedArray = FlipFlop(clk, typedArray0).q; @@ -577,12 +582,14 @@ void main() { selector.selectFrom([typedArray0, typedArray1]); final clonedTypedArray = typedArray0.cloneTyped(); final namedTypedArray = typedArray0.namedTyped('named_typed_array'); - final pipelinedTypedArray = - StructurePipeline>( + final typedArrayPipeline = Pipeline( clk, - typedArray0, - stages: [(stage) => stage.value], - ).output; + stages: [ + (p) => [p.get(typedArray0) < p.get(typedArray0)] + ], + ); + final TypedLogicArray<_TypedLane, LogicValue> pipelinedTypedArray = + typedArrayPipeline.get(typedArray0); [ muxedArray, @@ -1064,51 +1071,55 @@ void main() { expect(selectedByIndex.value, LogicValue.ofInt(0, selectedByIndex.width)); }); - test('StructurePipeline preserves typed stages and history', () async { + test('Pipeline preserves typed structures, arrays, and history', () async { final clk = SimpleClockGenerator(10).clk; final reset = Logic(); final stall = Logic(); final input = _TypedPacket(name: 'pipeline_input'); - final pipeline = StructurePipeline<_TypedPacket>( + final pipeline = Pipeline( clk, - input, reset: reset, - resetValue: 0, + resetValues: {input: 0}, stalls: [null, stall], stages: [ - (stage) { - final next = stage.value.cloneTyped(name: 'stage_0_next'); - next.opcode <= stage.value.opcode + 1; - for (var lane = 0; lane < next.lanes.arrayElements.length; lane++) { - next.lanes.arrayElements[lane].data <= - stage.value.lanes.arrayElements[lane].data; - next.lanes.arrayElements[lane].enable <= - stage.value.lanes.arrayElements[lane].enable; - } - return next; + (p) { + final source = p.get(input); + final target = p.get(input); + return [ + target.opcode < source.opcode + 1, + for (var lane = 0; + lane < target.lanes.arrayElements.length; + lane++) ...[ + target.lanes.arrayElements[lane].data < + source.lanes.arrayElements[lane].data, + target.lanes.arrayElements[lane].enable < + source.lanes.arrayElements[lane].enable, + ], + ]; }, - (stage) { - final next = stage.value.cloneTyped(name: 'stage_1_next'); - next.opcode <= stage.value.opcode + stage.get(-1).opcode; - for (var lane = 0; lane < next.lanes.arrayElements.length; lane++) { - next.lanes.arrayElements[lane].data <= - stage.value.lanes.arrayElements[lane].data; - next.lanes.arrayElements[lane].enable <= - stage.value.lanes.arrayElements[lane].enable; - } - return next; + (p) { + final target = p.get(input); + final previous = p.get(input, -1); + return [ + target.opcode < previous.opcode, + for (var lane = 0; + lane < target.lanes.arrayElements.length; + lane++) ...[ + target.lanes.arrayElements[lane].data < + previous.lanes.arrayElements[lane].data, + target.lanes.arrayElements[lane].enable < + previous.lanes.arrayElements[lane].enable, + ], + ]; }, ], ); + final output = pipeline.get(input); unawaited(Simulator.run()); expect(pipeline.stageCount, 3); - expect(pipeline.latency, 2); - expect(pipeline.output, isA<_TypedPacket>()); - expect(pipeline.get(0), same(input)); - expect( - pipeline.output.lanes, isA>()); - expect(() => pipeline.values.add(input), throwsUnsupportedError); + expect(output, isA<_TypedPacket>()); + expect(output.lanes, isA>()); reset.inject(1); stall.inject(0); @@ -1121,68 +1132,186 @@ void main() { input.opcode.inject(4); input.lanes.arrayElements[0].data.inject(0x67); await clk.nextPosedge; - expect(pipeline.output.opcode.value.toInt(), 7); - expect(pipeline.output.lanes.arrayElements[0].data.value.toInt(), 0x45); + expect(output.opcode.value.toInt(), 5); + expect(output.lanes.arrayElements[0].data.value.toInt(), 0x67); stall.inject(1); input.opcode.inject(1); await clk.nextPosedge; - expect(pipeline.output.opcode.value.toInt(), 7); + expect(output.opcode.value.toInt(), 5); await Simulator.endSimulation(); }); - test('StructurePipeline treats constant stalls as active-high', () async { + test( + 'Pipeline preserves heterogeneous identifiers through relative and ' + 'absolute access', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final stall = Logic(); + final packet = _TypedPacket(name: 'heterogeneous_packet'); + final array = TypedLogicArray<_TypedLane, LogicValue>( + [1], + _TypedLane.new, + name: 'heterogeneous_array', + ); + final state = _operationEnum(name: 'heterogeneous_state'); + final enable = Logic(name: 'heterogeneous_enable'); + + final pipeline = Pipeline( + clk, + reset: reset, + resetValues: { + packet: 0, + array: 0, + state: _OperationEnum.idle, + enable: 0, + }, + stalls: [null, stall], + stages: [ + (p) { + final sourcePacket = p.get(packet); + final targetPacket = p.get(packet); + final sourceArray = p.get(array); + final targetArray = p.get(array); + final targetState = p.get(state); + final sourceEnable = p.get(enable); + final targetEnable = p.get(enable); + final stateCondition = p.get(enable); + return [ + targetPacket.opcode < sourcePacket.opcode + 1, + targetPacket.lanes < sourcePacket.lanes, + targetArray < sourceArray, + If( + stateCondition, + then: [targetState < _OperationEnum.active], + orElse: [targetState < _OperationEnum.idle], + ), + targetEnable < ~sourceEnable, + ]; + }, + (p) { + final targetPacket = p.get(packet); + final priorPacket = p.get(packet, -1); + final firstPacket = p.getAbs(packet, 0); + final targetArray = p.get(array); + final firstArray = p.getAbs(array, 0); + final targetState = p.get(state); + final firstState = p.getAbs(state, 0); + final targetEnable = p.get(enable); + final firstEnable = p.getAbs(enable, 0); + return [ + targetPacket.opcode < priorPacket.opcode, + targetPacket.lanes < firstPacket.lanes, + targetArray < firstArray, + targetState < firstState, + targetEnable < firstEnable, + ]; + }, + ], + ); + + final pipedPacket = pipeline.get(packet); + final pipedArray = pipeline.get(array); + final pipedState = pipeline.get(state); + final pipedEnable = pipeline.get(enable); + unawaited(Simulator.run()); + + reset.inject(1); + stall.inject(0); + await clk.nextPosedge; + expect(pipedState.valueEnum, _OperationEnum.idle); + expect(pipedEnable.value, LogicValue.zero); + reset.inject(0); + + packet.opcode.inject(1); + packet.lanes.arrayElements[0].data.inject(0x10); + array.arrayElements[0].data.inject(0x20); + state.inject(_OperationEnum.active); + enable.inject(0); + await clk.nextPosedge; + + packet.opcode.inject(3); + packet.lanes.arrayElements[0].data.inject(0x30); + array.arrayElements[0].data.inject(0x40); + state.inject(_OperationEnum.active); + enable.inject(1); + await clk.nextPosedge; + expect(pipedPacket.opcode.value.toInt(), 4); + expect(pipedPacket.lanes.arrayElements[0].data.value.toInt(), 0x30); + expect(pipedArray.arrayElements[0].data.value.toInt(), 0x40); + expect(pipedState.valueEnum, _OperationEnum.active); + expect(pipedEnable.value, LogicValue.zero); + + stall.inject(1); + packet.opcode.inject(6); + array.arrayElements[0].data.inject(0x60); + await clk.nextPosedge; + expect(pipedPacket.opcode.value.toInt(), 4); + expect(pipedArray.arrayElements[0].data.value.toInt(), 0x40); + await Simulator.endSimulation(); + }); + + test('Pipeline treats constant stalls as active-high', () async { final clk = SimpleClockGenerator(10).clk; final input = _TypedPacket(name: 'constant_stall_input'); - final running = StructurePipeline<_TypedPacket>( + final running = Pipeline( clk, - input, stalls: [Const(0)], - stages: [(stage) => stage.value.namedTyped('running_stage')], + stages: [ + (p) => [p.get(input) < p.get(input)], + ], ); - final stalled = StructurePipeline<_TypedPacket>( + final stalled = Pipeline( clk, - input, stalls: [Const(1)], - stages: [(stage) => stage.value.namedTyped('stalled_stage')], + stages: [ + (p) => [p.get(input) < p.get(input)], + ], ); + final runningOutput = running.get(input); + final stalledOutput = stalled.get(input); unawaited(Simulator.run()); input.opcode.inject(6); await clk.nextPosedge; - expect(running.output.opcode.value.toInt(), 6); - expect(stalled.output.opcode.value.isValid, isFalse); + expect(runningOutput.opcode.value.toInt(), 6); + expect(stalledOutput.opcode.value.isValid, isFalse); await Simulator.endSimulation(); }); - test('ReadyValidPipeline preserves structured payloads under backpressure', + test('ReadyValidPipeline preserves structured signals under backpressure', () async { final clk = SimpleClockGenerator(10).clk; final reset = Logic(); final valid = Logic(); final ready = Logic(); final input = _TypedPacket(name: 'ready_valid_pipeline_input'); - final pipeline = ReadyValidPipeline<_TypedPacket>( + final pipeline = ReadyValidPipeline( clk, - input, - validPipeIn: valid, - readyPipeOut: ready, + valid, + ready, reset: reset, + resetValues: {input: 0}, stages: [ - (stage) { - final next = stage.value.cloneTyped(name: 'ready_valid_stage_0'); - next.opcode <= stage.value.opcode + 1; - next.lanes <= stage.value.lanes; - return next; + (p) { + final source = p.get(input); + final target = p.get(input); + return [ + target.opcode < source.opcode + 1, + target.lanes < source.lanes, + ]; }, - (stage) { - final next = stage.value.cloneTyped(name: 'ready_valid_stage_1'); - next.opcode <= stage.value.opcode + 1; - next.lanes <= stage.value.lanes; - return next; + (p) { + final source = p.get(input); + final target = p.get(input); + return [ + target.opcode < source.opcode + 1, + target.lanes < source.lanes, + ]; }, ], ); + final output = pipeline.get(input); unawaited(Simulator.run()); reset.inject(1); @@ -1199,37 +1328,39 @@ void main() { await clk.nextPosedge; expect(pipeline.validPipeOut.value.toInt(), 1); - expect(pipeline.output.opcode.value.toInt(), 4); - expect(pipeline.output.lanes.arrayElements[0].data.value.toInt(), 0x45); + expect(output.opcode.value.toInt(), 4); + expect(output.lanes.arrayElements[0].data.value.toInt(), 0x45); ready.inject(0); input.opcode.inject(5); await clk.nextPosedge; expect(pipeline.validPipeOut.value.toInt(), 1); - expect(pipeline.output.opcode.value.toInt(), 4); + expect(output.opcode.value.toInt(), 4); ready.inject(1); await clk.nextPosedge; - expect(pipeline.output.opcode.value.toInt(), 4); + expect(output.opcode.value.toInt(), 4); await clk.nextPosedge; - expect(pipeline.output.opcode.value.toInt(), 7); + expect(output.opcode.value.toInt(), 7); await Simulator.endSimulation(); }); - test('ReadyValidPipeline preserves scalar payloads', () async { + test('ReadyValidPipeline preserves scalar signals', () async { final clk = SimpleClockGenerator(10).clk; final reset = Logic(); final valid = Logic(); final ready = Logic(); final input = Logic(width: 4); - final pipeline = ReadyValidPipeline( + final pipeline = ReadyValidPipeline( clk, - input, - validPipeIn: valid, - readyPipeOut: ready, + valid, + ready, reset: reset, - stages: [(stage) => stage.value], + stages: [ + (p) => [p.get(input) < p.get(input)], + ], ); + final output = pipeline.get(input); unawaited(Simulator.run()); reset.inject(1); @@ -1242,7 +1373,7 @@ void main() { valid.inject(1); await clk.nextPosedge; expect(pipeline.validPipeOut.value.toInt(), 1); - expect(pipeline.output.value.toInt(), 0xa); + expect(output.value.toInt(), 0xa); await Simulator.endSimulation(); }); From 860c0f43e229e4752d729536bc905bbdf1e610e1 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Fri, 16 Oct 2026 05:55:54 -0700 Subject: [PATCH 28/33] types not needed --- test/typed_operations_test.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index c239c1dba7..42d84b24f2 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -570,7 +570,7 @@ void main() { (p) => [p.get(array0) < p.get(array0)] ], ); - final LogicArray pipelinedArray = arrayPipeline.get(array0); + final pipelinedArray = arrayPipeline.get(array0); final muxedTypedArray = Mux(selector[0], typedArray1, typedArray0).out; final floppedTypedArray = FlipFlop(clk, typedArray0).q; @@ -588,8 +588,7 @@ void main() { (p) => [p.get(typedArray0) < p.get(typedArray0)] ], ); - final TypedLogicArray<_TypedLane, LogicValue> pipelinedTypedArray = - typedArrayPipeline.get(typedArray0); + final pipelinedTypedArray = typedArrayPipeline.get(typedArray0); [ muxedArray, From 0c372a5523db69ede8560e7c42290ad29eb969d1 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 18:23:15 -0700 Subject: [PATCH 29/33] Restore rebased compatibility contracts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/src/interfaces/pair_interface.dart | 21 ++++++++++++++++--- .../systemc/systemc_synthesis_result.dart | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index 9fc9160192..08569737f6 100644 --- a/lib/src/interfaces/pair_interface.dart +++ b/lib/src/interfaces/pair_interface.dart @@ -45,12 +45,23 @@ enum PairRole { /// It can be either directly used for simple scenarios, or extended for more /// complex situations. class PairInterface extends Interface { + /// A function that can be used to modify all port names in a certain way. + @Deprecated( + 'Use `uniquify` when connecting or adding sub interfaces instead.') + String Function(String original)? modify; + /// Constructs an instance of a [PairInterface] with the specified ports. + /// + /// The [modify] function will allow modification of all port names, in + /// addition to the usual uniquification that can occur during [connectIO]. PairInterface({ List? portsFromConsumer, List? portsFromProvider, List? sharedInputPorts, List? commonInOutPorts, + @Deprecated( + 'Use `uniquify` when connecting or adding sub interfaces instead.') + this.modify, }) { if (portsFromConsumer != null) { setPorts(portsFromConsumer, [PairDirection.fromConsumer]); @@ -158,12 +169,16 @@ class PairInterface extends Interface { Iterable? inOutTags, String Function(String original)? uniquify}) { final nonNullUniquify = uniquify ?? (original) => original; + // ignore: deprecated_member_use_from_same_package - deprecated backwards compat + final nonNullModify = modify ?? (original) => original; + String newUniquify(String original) => + nonNullUniquify(nonNullModify(original)); super.connectIO(module, srcInterface, inputTags: inputTags, outputTags: outputTags, inOutTags: inOutTags, - uniquify: nonNullUniquify); + uniquify: newUniquify); if (subInterfaces.isNotEmpty) { if (srcInterface is! PairInterface) { @@ -406,8 +421,8 @@ class PairInterface extends Interface { portsFromProvider: _getMatchPorts(this, PairDirection.fromProvider), sharedInputPorts: _getMatchPorts(this, PairDirection.sharedInputs), commonInOutPorts: _getMatchPorts(this, PairDirection.commonInOuts), - // ignore: deprecated_member_use_from_same_package - deprecated backwards compat - modify: modify, + // ignore: deprecated_member_use_from_same_package - deprecated backwards compat + modify: modify, ); } diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart index 5e759fd1f8..af55ba244e 100644 --- a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -794,7 +794,7 @@ class SystemCSynthesisResult extends SynthesisResult { // ── Mux (ternary) ── if (m is Mux) { final vals = inputs.values.toList(); - final w = m.out.width; + final w = m.outputs[m.resultSignalName]!.width; final utype = systemCType(w); // Cast both branches to avoid C++ ternary type mismatch // (e.g., when one branch is bool and the other is sc_uint<1>) From 1e62b454b38939cb723e8dcda4c3bc259ed4754c Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 18:23:27 -0700 Subject: [PATCH 30/33] Narrow output mapping test matrix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/submodule_output_mapping_test.dart | 1490 ++++++++++++++--------- 1 file changed, 923 insertions(+), 567 deletions(-) diff --git a/test/submodule_output_mapping_test.dart b/test/submodule_output_mapping_test.dart index c037675277..15fee8cd75 100644 --- a/test/submodule_output_mapping_test.dart +++ b/test/submodule_output_mapping_test.dart @@ -506,424 +506,665 @@ void main() { }); group('independent outputs to packed parent bits', () { - for (final producer in ProducerKind.values) { - for (final order in AssignmentOrder.values) { - // Three bits distinguish both boundaries and an interior position. - for (final width in [2, 3]) { - for (final aliasDepth in [0, 3]) { - test( - '${producer.name} ${order.name} ' - 'width=$width aliases=$aliasDepth', () async { - final module = OutputMappingTop( - width: width, - producer: producer, - order: order, - aliasDepth: aliasDepth, - ); - await module.build(); - final body = topBody(module); - for (var index = 0; index < width; index++) { - expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), - reason: body); - } - expect(body, isNot(matches(r'assign\s+observed\['))); - - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); + // Cover every producer and assignment mechanism while retaining the + // two-bit edge case, three-bit interior bit, and deep alias path. + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.batch, + width: 2, + aliasDepth: 0, + ), + ( + producer: ProducerKind.multiport, + order: AssignmentOrder.forward, + width: 3, + aliasDepth: 3, + ), + ( + producer: ProducerKind.hierarchical, + order: AssignmentOrder.reverse, + width: 3, + aliasDepth: 0, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + width: 2, + aliasDepth: 3, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + width: 3, + aliasDepth: 3, + ), + ]; - final vectors = [ - for (final input in twoStateInputPatterns(width)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 0; index < width; index++) - invertedBit(input, index), - ].join()), - }), - ]; - await SimCompare.checkVectors( - module, - vectors, - excludedBackends: producer == ProducerKind.custom - ? const { - SimCompareBackend.systemc: - 'CustomStage provides only a custom SystemVerilog ' - 'definition.', - } - : const {}, - ); - expect(body, isNot(matches(r'logic\s+result\w*;')), - reason: 'Directly mapped outputs should not leave unused ' - 'intermediate declarations.\n$body'); - }); - } + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final width = configuration.width; + final aliasDepth = configuration.aliasDepth; + test( + '${producer.name} ${order.name} ' + 'width=$width aliases=$aliasDepth', () async { + final module = OutputMappingTop( + width: width, + producer: producer, + order: order, + aliasDepth: aliasDepth, + ); + await module.build(); + final body = topBody(module); + for (var index = 0; index < width; index++) { + expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), + reason: body); } - } + expect(body, isNot(matches(r'assign\s+observed\['))); + + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + + final vectors = [ + for (final input in twoStateInputPatterns(width)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 0; index < width; index++) + invertedBit(input, index), + ].join()), + }), + ]; + await SimCompare.checkVectors( + module, + vectors, + excludedBackends: producer == ProducerKind.custom + ? const { + SimCompareBackend.systemc: + 'CustomStage provides only a custom SystemVerilog ' + 'definition.', + } + : const {}, + ); + expect(body, isNot(matches(r'logic\s+result\w*;')), + reason: 'Directly mapped outputs should not leave unused ' + 'intermediate declarations.\n$body'); + }); } }); group('concatenated output alias naming', () { - for (final producer in [ProducerKind.independent, ProducerKind.custom]) { - for (final order in [AssignmentOrder.swizzle, AssignmentOrder.rswizzle]) { - for (final naming in Naming.values) { - test('${producer.name} ${order.name} ${naming.name}', () async { - final module = OutputMappingTop( - width: 3, - producer: producer, - order: order, - aliasDepth: 3, - bitNaming: naming, - ); - await module.build(); - final body = topBody(module); - final preserve = - naming == Naming.reserved || naming == Naming.renameable; - for (var index = 0; index < 3; index++) { - if (preserve) { - expect(body, contains('logic bit_alias$index;')); - expect(body, contains('.result(bit_alias$index)')); - } else { - expect(body, contains('.result(observed[$index])'), - reason: body); - expect(body, isNot(contains('logic bit_alias$index;'))); - } - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in inputPatterns(3)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 0; index < 3; index++) - invertedBit(input, index), - ].join()), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + naming: Naming.unnamed, + ), + ]; + + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final naming = configuration.naming; + test('${producer.name} ${order.name} ${naming.name}', () async { + final module = OutputMappingTop( + width: 3, + producer: producer, + order: order, + aliasDepth: 3, + bitNaming: naming, + ); + await module.build(); + final body = topBody(module); + final preserve = + naming == Naming.reserved || naming == Naming.renameable; + for (var index = 0; index < 3; index++) { + if (preserve) { + expect(body, contains('logic bit_alias$index;')); + expect(body, contains('.result(bit_alias$index)')); + } else { + expect(body, contains('.result(observed[$index])'), reason: body); + expect(body, isNot(contains('logic bit_alias$index;'))); + } } - } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in inputPatterns(3)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 0; index < 3; index++) + invertedBit(input, index), + ].join()), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('output mapping safety cross-products', () { - for (final guard in MappingGuard.values) { - for (final producer in [ - ProducerKind.independent, - ProducerKind.multiport, - ProducerKind.custom, - ]) { - for (final order in [ - AssignmentOrder.batch, - AssignmentOrder.reverse, - if (guard != MappingGuard.partial) ...[ - AssignmentOrder.swizzle, - AssignmentOrder.rswizzle, - ], - ]) { - test('${guard.name} ${producer.name} ${order.name}', () async { - final module = GuardedOutputTop( - guard: guard, - producer: producer, - order: order, - ); - await module.build(); - final body = topBody(module); - expect(body, isNot(matches(r'\.result\d*\(\)')), reason: body); - if (guard == MappingGuard.renameableBus || - guard == MappingGuard.reservedBus || - guard == MappingGuard.partial) { - expect(body, isNot(matches(r'\.result\d*\(observed\['))); - } else { - for (final index in [0, 2]) { - expect(body, - matches('\\.result\\d*\\((observed|mirror)\\[$index\\]\\)'), - reason: body); - } - } - if (guard == MappingGuard.renameableBus || - guard == MappingGuard.reservedBus) { - expect(body, contains('logic [3:0] retained;')); - } - if (guard == MappingGuard.namedBit) { - expect(body, contains('logic retained;')); - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); + // Each guard represents a distinct condition that prevents direct output + // mapping. Rotate the producer and assignment forms across the guards. + final configurations = [ + ( + guard: MappingGuard.outputFanout, + producer: ProducerKind.independent, + order: AssignmentOrder.batch + ), + ( + guard: MappingGuard.siblingFanout, + producer: ProducerKind.multiport, + order: AssignmentOrder.reverse + ), + ( + guard: MappingGuard.busFanout, + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle + ), + ( + guard: MappingGuard.repeatedBit, + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle + ), + ( + guard: MappingGuard.wholeBusFanout, + producer: ProducerKind.multiport, + order: AssignmentOrder.batch + ), + ( + guard: MappingGuard.renameableBus, + producer: ProducerKind.custom, + order: AssignmentOrder.reverse + ), + ( + guard: MappingGuard.reservedBus, + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle + ), + ( + guard: MappingGuard.namedBit, + producer: ProducerKind.multiport, + order: AssignmentOrder.rswizzle + ), + ( + guard: MappingGuard.partial, + producer: ProducerKind.custom, + order: AssignmentOrder.reverse + ), + ]; - final vectors = [ - for (final input in inputPatterns(4)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - if (guard == MappingGuard.partial) 'z', - for (var index = 3; index >= 0; index--) - invertedBit( - input, - guard == MappingGuard.repeatedBit && index == 3 - ? 1 - : index), - ].join()), - if (guard == MappingGuard.outputFanout) - 'tap': LogicValue.ofString(invertedBit(input, 1)), - if (guard == MappingGuard.siblingFanout) - 'tap': input[1].isValid ? input[1] : LogicValue.x, - if (guard == MappingGuard.busFanout) - 'secondary': - LogicValue.ofString('10${invertedBit(input, 1)}'), - if (guard == MappingGuard.wholeBusFanout) 'mirror': ~input, - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + for (final configuration in configurations) { + final guard = configuration.guard; + final producer = configuration.producer; + final order = configuration.order; + test('${guard.name} ${producer.name} ${order.name}', () async { + final module = GuardedOutputTop( + guard: guard, + producer: producer, + order: order, + ); + await module.build(); + final body = topBody(module); + expect(body, isNot(matches(r'\.result\d*\(\)')), reason: body); + if (guard == MappingGuard.renameableBus || + guard == MappingGuard.reservedBus || + guard == MappingGuard.partial) { + expect(body, isNot(matches(r'\.result\d*\(observed\['))); + } else { + for (final index in [0, 2]) { + expect(body, + matches('\\.result\\d*\\((observed|mirror)\\[$index\\]\\)'), + reason: body); + } } - } + if (guard == MappingGuard.renameableBus || + guard == MappingGuard.reservedBus) { + expect(body, contains('logic [3:0] retained;')); + } + if (guard == MappingGuard.namedBit) { + expect(body, contains('logic retained;')); + } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + + final vectors = [ + for (final input in inputPatterns(4)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + if (guard == MappingGuard.partial) 'z', + for (var index = 3; index >= 0; index--) + invertedBit( + input, + guard == MappingGuard.repeatedBit && index == 3 + ? 1 + : index), + ].join()), + if (guard == MappingGuard.outputFanout) + 'tap': LogicValue.ofString(invertedBit(input, 1)), + if (guard == MappingGuard.siblingFanout) + 'tap': input[1].isValid ? input[1] : LogicValue.x, + if (guard == MappingGuard.busFanout) + 'secondary': LogicValue.ofString('10${invertedBit(input, 1)}'), + if (guard == MappingGuard.wholeBusFanout) 'mirror': ~input, + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('custom output declaration retention', () { - for (final acceptsEmptyPortConnections in [false, true]) { - for (final fanout in [false, true]) { - for (final order in AssignmentOrder.values) { - test( - 'empty=$acceptsEmptyPortConnections ' - 'fanout=$fanout ${order.name}', () async { - final module = CustomRetentionTop( - acceptsEmptyPortConnections: acceptsEmptyPortConnections, - fanout: fanout, - order: order, - ); - await module.build(); - final body = topBody(module); - String connection(int index, String target) => - '.result$index($target)'; + final configurations = [ + ( + acceptsEmptyPortConnections: false, + fanout: false, + order: AssignmentOrder.batch, + ), + ( + acceptsEmptyPortConnections: true, + fanout: false, + order: AssignmentOrder.forward, + ), + ( + acceptsEmptyPortConnections: false, + fanout: true, + order: AssignmentOrder.reverse, + ), + ( + acceptsEmptyPortConnections: true, + fanout: true, + order: AssignmentOrder.swizzle, + ), + ( + acceptsEmptyPortConnections: false, + fanout: true, + order: AssignmentOrder.rswizzle, + ), + ]; - expect(body, contains(connection(0, 'observed[0]'))); - expect(body, isNot(contains('logic result0;'))); - if (fanout) { - expect(body, contains('logic result1;')); - expect(body, contains(connection(1, 'result1'))); - } else { - expect(body, isNot(contains('logic result1;'))); - expect(body, contains(connection(1, 'observed[1]'))); - } - if (acceptsEmptyPortConnections) { - expect(body, contains('.result2()')); - expect(body, isNot(contains('logic result2;'))); - } else { - expect(body, contains(connection(2, 'result2'))); - expect(body, contains('logic result2;')); - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); + for (final configuration in configurations) { + final acceptsEmptyPortConnections = + configuration.acceptsEmptyPortConnections; + final fanout = configuration.fanout; + final order = configuration.order; + test( + 'empty=$acceptsEmptyPortConnections ' + 'fanout=$fanout ${order.name}', () async { + final module = CustomRetentionTop( + acceptsEmptyPortConnections: acceptsEmptyPortConnections, + fanout: fanout, + order: order, + ); + await module.build(); + final body = topBody(module); + String connection(int index, String target) => '.result$index($target)'; - final vectors = [ - for (final input in inputPatterns(3)) - Vector({ - 'data': input - }, { - 'observed': ~input.getRange(0, 2), - if (fanout) 'tap': input[1].isValid ? input[1] : LogicValue.x, - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + expect(body, contains(connection(0, 'observed[0]'))); + expect(body, isNot(contains('logic result0;'))); + if (fanout) { + expect(body, contains('logic result1;')); + expect(body, contains(connection(1, 'result1'))); + } else { + expect(body, isNot(contains('logic result1;'))); + expect(body, contains(connection(1, 'observed[1]'))); } - } + if (acceptsEmptyPortConnections) { + expect(body, contains('.result2()')); + expect(body, isNot(contains('logic result2;'))); + } else { + expect(body, contains(connection(2, 'result2'))); + expect(body, contains('logic result2;')); + } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + + final vectors = [ + for (final input in inputPatterns(3)) + Vector({ + 'data': input + }, { + 'observed': ~input.getRange(0, 2), + if (fanout) 'tap': input[1].isValid ? input[1] : LogicValue.x, + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('constant position and four-state cross-products', () { - for (final constant in ['0', '1', 'x', 'z']) { - for (final constantIndex in [0, 1, 2]) { - for (final namedConstant in [false, true]) { - for (final order in [ - AssignmentOrder.batch, - AssignmentOrder.reverse, - AssignmentOrder.swizzle, - AssignmentOrder.rswizzle, - ]) { - test( - '$constant at $constantIndex ' - 'named=$namedConstant ${order.name}', () async { - final module = ConstantOutputTop( - constant: constant, - constantIndex: constantIndex, - namedConstant: namedConstant, - order: order, - ); - await module.build(); - final body = topBody(module); - expect(body, isNot(contains('.result()')), reason: body); - if (constant != 'z') { - for (var index = 0; - index < module.output('observed').width; - index++) { - if (index != constantIndex) { - expect(body, contains('.result(observed[$index])'), - reason: body); - } - } - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in inputPatterns(module.input('data').width)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = module.output('observed').width - 1; - index >= 0; - index--) - if (index == constantIndex) - constant - else - invertedBit( - input, index > constantIndex ? index - 1 : index), - ].join()), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + // Retain each constant state, destination position, naming behavior, and + // assignment mechanism. Z remains covered at every position because it + // follows the distinct floating-output pruning path. + final configurations = [ + ( + constant: '0', + constantIndex: 0, + namedConstant: false, + order: AssignmentOrder.batch, + ), + ( + constant: '1', + constantIndex: 1, + namedConstant: true, + order: AssignmentOrder.reverse, + ), + ( + constant: 'x', + constantIndex: 2, + namedConstant: false, + order: AssignmentOrder.swizzle, + ), + ( + constant: 'z', + constantIndex: 0, + namedConstant: true, + order: AssignmentOrder.rswizzle, + ), + ( + constant: 'z', + constantIndex: 1, + namedConstant: false, + order: AssignmentOrder.batch, + ), + ( + constant: 'z', + constantIndex: 2, + namedConstant: true, + order: AssignmentOrder.reverse, + ), + ]; + + for (final configuration in configurations) { + final constant = configuration.constant; + final constantIndex = configuration.constantIndex; + final namedConstant = configuration.namedConstant; + final order = configuration.order; + test( + '$constant at $constantIndex ' + 'named=$namedConstant ${order.name}', () async { + final module = ConstantOutputTop( + constant: constant, + constantIndex: constantIndex, + namedConstant: namedConstant, + order: order, + ); + await module.build(); + final body = topBody(module); + expect(body, isNot(contains('.result()')), reason: body); + if (constant != 'z') { + for (var index = 0; + index < module.output('observed').width; + index++) { + if (index != constantIndex) { + expect(body, contains('.result(observed[$index])'), reason: body); + } } } - } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in inputPatterns(module.input('data').width)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = module.output('observed').width - 1; + index >= 0; + index--) + if (index == constantIndex) + constant + else + invertedBit( + input, index > constantIndex ? index - 1 : index), + ].join()), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('scalar producers beside packed ranges', () { - for (final wideProducer in [false, true]) { - for (final fanout in [false, true]) { - for (final order in AssignmentOrder.values) { - test('wide=$wideProducer fanout=$fanout ${order.name}', () async { - final module = MixedRangeTop( - wideProducer: wideProducer, - fanout: fanout, - order: order, - ); - await module.build(); - final body = topBody(module); - for (final index in [0, 4]) { - expect(body, contains('.result(observed[$index])'), reason: body); - } - expect(body, isNot(contains('.wide_result()'))); - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in inputPatterns(5)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 4; index >= 0; index--) - if (wideProducer || index == 0 || index == 4) - invertedBit(input, index) - else - input[index].toString(includeWidth: false), - ].join()), - if (fanout) - 'tap': wideProducer - ? ~input.getRange(1, 4) - : input.getRange(1, 4), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + final configurations = [ + ( + wideProducer: false, + fanout: false, + order: AssignmentOrder.batch, + ), + ( + wideProducer: true, + fanout: false, + order: AssignmentOrder.forward, + ), + ( + wideProducer: false, + fanout: true, + order: AssignmentOrder.reverse, + ), + ( + wideProducer: true, + fanout: true, + order: AssignmentOrder.swizzle, + ), + ( + wideProducer: true, + fanout: false, + order: AssignmentOrder.rswizzle, + ), + ]; + + for (final configuration in configurations) { + final wideProducer = configuration.wideProducer; + final fanout = configuration.fanout; + final order = configuration.order; + test('wide=$wideProducer fanout=$fanout ${order.name}', () async { + final module = MixedRangeTop( + wideProducer: wideProducer, + fanout: fanout, + order: order, + ); + await module.build(); + final body = topBody(module); + for (final index in [0, 4]) { + expect(body, contains('.result(observed[$index])'), reason: body); } - } + expect(body, isNot(contains('.wide_result()'))); + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in inputPatterns(5)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 4; index >= 0; index--) + if (wideProducer || index == 0 || index == 4) + invertedBit(input, index) + else + input[index].toString(includeWidth: false), + ].join()), + if (fanout) + 'tap': + wideProducer ? ~input.getRange(1, 4) : input.getRange(1, 4), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('sliced swizzle equivalence cross-products', () { - for (final producer in ProducerKind.values) { - for (final order in [AssignmentOrder.swizzle, AssignmentOrder.rswizzle]) { - for (final naming in Naming.values) { - for (final use in SwizzleUse.values) { - for (final fanout in [false, true]) { - for (final (lower, upper) in [(0, 1), (1, 3), (3, 4)]) { - // Cover all names on independent producers; exercise other - // producer kinds with mergeable names to bound the matrix. - if (producer != ProducerKind.independent && - naming != Naming.mergeable) { - continue; - } - // The middle slice covers naming and producer variations; - // boundary slices still cover every use, order, and fanout. - if (lower != 1 && - (producer != ProducerKind.independent || - naming != Naming.mergeable)) { - continue; - } - test( - '${producer.name} ${order.name} ${naming.name} ' - '${use.name} fanout=$fanout [$lower:$upper]', () async { - final module = SlicedSwizzleTop( - producer: producer, - order: order, - naming: naming, - use: use, - fanout: fanout, - lower: lower, - upper: upper, - ); - await module.build(); - final body = topBody(module); - printOnFailure(body); - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in fourStateInputPatterns(4, - exhaustive: producer == ProducerKind.independent && - order == AssignmentOrder.rswizzle && - naming == Naming.mergeable && - use == SwizzleUse.wholeAndSlice && - fanout)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - if (use == SwizzleUse.partialDestination) 'z', - if (use == SwizzleUse.filledDestination) '1', - for (var bit = upper - 1; bit >= lower; bit--) - invertedBit( - input, - order == AssignmentOrder.swizzle - ? 3 - bit - : bit), - if (use == SwizzleUse.partialDestination) 'z', - if (use == SwizzleUse.filledDestination) '0', - ].join()), - if (use == SwizzleUse.wholeAndSlice) - 'whole': LogicValue.ofString([ - for (var bit = 3; bit >= 0; bit--) - invertedBit( - input, - order == AssignmentOrder.swizzle - ? 3 - bit - : bit), - ].join()), - if (fanout) - 'tap': input[1].isValid ? input[1] : LogicValue.x, - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); - } - } - } - } - } + // Retain each naming policy, producer topology, destination form, slice + // boundary, and fanout path. The mergeable whole-and-slice case remains + // exhaustive over all four-state inputs. + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + use: SwizzleUse.slice, + fanout: false, + lower: 0, + upper: 1, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + use: SwizzleUse.wholeAndSlice, + fanout: true, + lower: 1, + upper: 3, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + use: SwizzleUse.wholeAndSlice, + fanout: true, + lower: 1, + upper: 3, + exhaustive: true, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + use: SwizzleUse.partialDestination, + fanout: false, + lower: 3, + upper: 4, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.unnamed, + use: SwizzleUse.filledDestination, + fanout: true, + lower: 0, + upper: 1, + exhaustive: false, + ), + ( + producer: ProducerKind.multiport, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + use: SwizzleUse.slice, + fanout: true, + lower: 1, + upper: 3, + exhaustive: false, + ), + ( + producer: ProducerKind.hierarchical, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + use: SwizzleUse.wholeAndSlice, + fanout: false, + lower: 1, + upper: 3, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + use: SwizzleUse.partialDestination, + fanout: true, + lower: 1, + upper: 3, + exhaustive: false, + ), + ]; + + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final naming = configuration.naming; + final use = configuration.use; + final fanout = configuration.fanout; + final lower = configuration.lower; + final upper = configuration.upper; + final exhaustive = configuration.exhaustive; + test( + '${producer.name} ${order.name} ${naming.name} ' + '${use.name} fanout=$fanout [$lower:$upper]', () async { + final module = SlicedSwizzleTop( + producer: producer, + order: order, + naming: naming, + use: use, + fanout: fanout, + lower: lower, + upper: upper, + ); + await module.build(); + final body = topBody(module); + printOnFailure(body); + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in fourStateInputPatterns(4, exhaustive: exhaustive)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + if (use == SwizzleUse.partialDestination) 'z', + if (use == SwizzleUse.filledDestination) '1', + for (var bit = upper - 1; bit >= lower; bit--) + invertedBit( + input, order == AssignmentOrder.swizzle ? 3 - bit : bit), + if (use == SwizzleUse.partialDestination) 'z', + if (use == SwizzleUse.filledDestination) '0', + ].join()), + if (use == SwizzleUse.wholeAndSlice) + 'whole': LogicValue.ofString([ + for (var bit = 3; bit >= 0; bit--) + invertedBit(input, + order == AssignmentOrder.swizzle ? 3 - bit : bit), + ].join()), + if (fanout) 'tap': input[1].isValid ? input[1] : LogicValue.x, + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); @@ -971,196 +1212,311 @@ void main() { }); group('range operand equivalence cross-products', () { - for (final producer in [ProducerKind.independent, ProducerKind.custom]) { - for (final order in [AssignmentOrder.swizzle, AssignmentOrder.rswizzle]) { - for (final naming in Naming.values) { - for (final ranges in OperandRanges.values) { - for (final fanout in [false, true]) { - for (final constant in [ - '0', - '1', - 'x', - 'z', - ]) { - for (final constantPosition in [0, 1, -1]) { - // Cover every topology/name/fanout combination with middle Z. - final topologyCase = constant == 'z' && constantPosition == 1; - // Sweep all constant states and positions on one topology. - final constantCase = producer == ProducerKind.independent && - naming == Naming.mergeable && - !fanout; - // Keep all preserved-Z positions for the pruning regression. - final preservedFloatingCase = constant == 'z' && - (naming == Naming.reserved || - naming == Naming.renameable) && - !fanout; - // Bound runtime by omitting the remaining cross-products. - if (!topologyCase && - !constantCase && - !preservedFloatingCase) { - continue; - } - test( - '${producer.name} ${order.name} ${naming.name} ' - '${ranges.name} fanout=$fanout ' - '$constant at $constantPosition', () async { - final module = RangeOperandSwizzleTop( - producer: producer, - order: order, - naming: naming, - ranges: ranges, - constant: constant, - constantPosition: constantPosition, - fanout: fanout, - ); - await module.build(); - final body = topBody(module); - printOnFailure(body); - if (naming == Naming.reserved || - naming == Naming.renameable) { - expect(body, contains('wide_alias')); - expect(body, contains('range0')); - } else if (constantPosition != -1) { - expect(body, matches(r'\.result\(observed\[\d+\]\)')); - } - if (topologyCase) { - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - } - final sourceIndices = switch (ranges) { - OperandRanges.slice => [ - [1, 2] - ], - OperandRanges.reordered => [ - [2, 3], - [0, 1] - ], - OperandRanges.overlapping => [ - [0, 1], - [1, 2] - ], - OperandRanges.nested => [ - [2] - ], - }; - final vectors = []; - for (final input in fourStateInputPatterns(4, - exhaustive: producer == ProducerKind.custom && - order == AssignmentOrder.rswizzle && - naming == Naming.mergeable && - !fanout && - constant == 'z' && - constantPosition == 1)) { - final expectedOperands = [ - [invertedBit(input, 0)], - for (final indices in sourceIndices) - [ - for (final index in indices) - invertedBit(input, index) - ], - [invertedBit(input, 3)], - ]; - expectedOperands.insert( - constantPosition < 0 - ? expectedOperands.length - : constantPosition, - [constant]); - final expectedBits = (order == AssignmentOrder.swizzle - ? expectedOperands.reversed - : expectedOperands) - .expand((operand) => operand) - .toList(); - vectors.add(Vector({ - 'data': input - }, { - 'observed': - LogicValue.ofString(expectedBits.reversed.join()), - if (fanout) ...{ - 'wide_tap': ~input, - 'bit_tap': input[0].isValid ? input[0] : LogicValue.x, - 'window': LogicValue.ofString(expectedBits - .sublist(1, expectedBits.length - 1) - .reversed - .join()), - }, - })); - } - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); - } - } - } - } + // Preserve every range topology, naming mode, constant state and + // placement. Reserved and renameable names keep Z at all positions because + // those are the pruning-sensitive cases. + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + ranges: OperandRanges.slice, + constant: 'z', + constantPosition: 0, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.reserved, + ranges: OperandRanges.reordered, + constant: 'z', + constantPosition: 1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + ranges: OperandRanges.overlapping, + constant: 'z', + constantPosition: -1, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + ranges: OperandRanges.nested, + constant: 'z', + constantPosition: 0, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.renameable, + ranges: OperandRanges.slice, + constant: 'z', + constantPosition: 1, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + ranges: OperandRanges.reordered, + constant: 'z', + constantPosition: -1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + ranges: OperandRanges.overlapping, + constant: '0', + constantPosition: 0, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + ranges: OperandRanges.nested, + constant: '1', + constantPosition: 1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + ranges: OperandRanges.reordered, + constant: 'x', + constantPosition: -1, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.unnamed, + ranges: OperandRanges.slice, + constant: 'z', + constantPosition: 1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + ranges: OperandRanges.overlapping, + constant: 'z', + constantPosition: 1, + fanout: false, + exhaustive: true, + ), + ]; + + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final naming = configuration.naming; + final ranges = configuration.ranges; + final constant = configuration.constant; + final constantPosition = configuration.constantPosition; + final fanout = configuration.fanout; + final exhaustive = configuration.exhaustive; + test( + '${producer.name} ${order.name} ${naming.name} ' + '${ranges.name} fanout=$fanout ' + '$constant at $constantPosition', () async { + final module = RangeOperandSwizzleTop( + producer: producer, + order: order, + naming: naming, + ranges: ranges, + constant: constant, + constantPosition: constantPosition, + fanout: fanout, + ); + await module.build(); + final body = topBody(module); + printOnFailure(body); + if (naming == Naming.reserved || naming == Naming.renameable) { + expect(body, contains('wide_alias')); + expect(body, contains('range0')); + } else if (constantPosition != -1) { + expect(body, matches(r'\.result\(observed\[\d+\]\)')); } - } + if (constant == 'z' && constantPosition == 1) { + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + } + final sourceIndices = switch (ranges) { + OperandRanges.slice => [ + [1, 2] + ], + OperandRanges.reordered => [ + [2, 3], + [0, 1] + ], + OperandRanges.overlapping => [ + [0, 1], + [1, 2] + ], + OperandRanges.nested => [ + [2] + ], + }; + final vectors = []; + for (final input in fourStateInputPatterns(4, exhaustive: exhaustive)) { + final expectedOperands = [ + [invertedBit(input, 0)], + for (final indices in sourceIndices) + [ + for (final index in indices) invertedBit(input, index), + ], + [invertedBit(input, 3)], + ]; + expectedOperands.insert( + constantPosition < 0 ? expectedOperands.length : constantPosition, + [constant]); + final expectedBits = (order == AssignmentOrder.swizzle + ? expectedOperands.reversed + : expectedOperands) + .expand((operand) => operand) + .toList(); + vectors.add(Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString(expectedBits.reversed.join()), + if (fanout) ...{ + 'wide_tap': ~input, + 'bit_tap': input[0].isValid ? input[0] : LogicValue.x, + 'window': LogicValue.ofString(expectedBits + .sublist(1, expectedBits.length - 1) + .reversed + .join()), + }, + })); + } + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('packed word boundaries and deep aliases', () { - for (final (width, order) in [ - for (final width in [33, 65]) - for (final order in [ - AssignmentOrder.reverse, - AssignmentOrder.swizzle, - AssignmentOrder.rswizzle, - ]) - (width, order), - ]) { - for (final producer in [ - ProducerKind.independent, - ProducerKind.multiport - ]) { - for (final aliasDepth in [0, 16]) { - test( - 'width=$width ${producer.name} ${order.name} aliases=$aliasDepth', - () async { - final module = OutputMappingTop( - width: width, - producer: producer, - order: order, - aliasDepth: aliasDepth, - ); - await module.build(); - final body = topBody(module); - for (var index = 0; index < width; index++) { - expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), - reason: body); - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final inputs = [ - LogicValue.ofBigInt(BigInt.zero, width), - LogicValue.ofBigInt((BigInt.one << width) - BigInt.one, width), - for (var index = 0; index < width; index++) - LogicValue.ofBigInt(BigInt.one << index, width), - for (final index in [ - 31, - 32, - if (width > 64) ...[63, 64] - ]) - for (final state in ['x', 'z']) - LogicValue.ofString([ - for (var bit = width - 1; bit >= 0; bit--) - if (bit == index) state else (bit % 2).toString(), - ].join()), - ]; - final vectors = [ - for (final input in inputs) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 0; index < width; index++) - invertedBit(input, index), - ].join()), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + // Cover every pair of word boundary, assignment order, producer form, and + // alias depth without compiling their full Cartesian product. + final configurations = [ + ( + width: 33, + order: AssignmentOrder.reverse, + producer: ProducerKind.independent, + aliasDepth: 0, + ), + ( + width: 33, + order: AssignmentOrder.swizzle, + producer: ProducerKind.multiport, + aliasDepth: 16, + ), + ( + width: 33, + order: AssignmentOrder.rswizzle, + producer: ProducerKind.independent, + aliasDepth: 16, + ), + ( + width: 65, + order: AssignmentOrder.reverse, + producer: ProducerKind.multiport, + aliasDepth: 16, + ), + ( + width: 65, + order: AssignmentOrder.swizzle, + producer: ProducerKind.independent, + aliasDepth: 0, + ), + ( + width: 65, + order: AssignmentOrder.rswizzle, + producer: ProducerKind.multiport, + aliasDepth: 0, + ), + ]; + + for (final configuration in configurations) { + final width = configuration.width; + final order = configuration.order; + final producer = configuration.producer; + final aliasDepth = configuration.aliasDepth; + test('width=$width ${producer.name} ${order.name} aliases=$aliasDepth', + () async { + final module = OutputMappingTop( + width: width, + producer: producer, + order: order, + aliasDepth: aliasDepth, + ); + await module.build(); + final body = topBody(module); + for (var index = 0; index < width; index++) { + expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), + reason: body); } - } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final boundaryBits = { + 0, + width - 1, + 31, + 32, + if (width > 64) ...[63, 64], + }; + final inputs = [ + LogicValue.ofBigInt(BigInt.zero, width), + LogicValue.ofBigInt((BigInt.one << width) - BigInt.one, width), + for (final index in boundaryBits) + LogicValue.ofBigInt(BigInt.one << index, width), + for (final index in [ + 31, + 32, + if (width > 64) ...[63, 64] + ]) + for (final state in ['x', 'z']) + LogicValue.ofString([ + for (var bit = width - 1; bit >= 0; bit--) + if (bit == index) state else (bit % 2).toString(), + ].join()), + ]; + final vectors = [ + for (final input in inputs) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 0; index < width; index++) + invertedBit(input, index), + ].join()), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); From 9ff4b5e17ee6ce453782bc48478bff0b4d36f303 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 18:30:26 -0700 Subject: [PATCH 31/33] minor readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97543d0419..6c7c57b239 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ You can also open this repository in a GitHub Codespace to run the example in yo - Makes **validation collateral** simpler to develop and debug. The [ROHD Verification Framework](https://github.com/intel/rohd-vf) helps build well-structured testbenches - Powerful capabilities like the [ROHD Schematic Viewer](https://github.com/rohd-schematic-viewer) for high-fidelity rendering of ROHD designs as well as [ROHD Wave Viewer](https://github.com/rohd-wave-viewer) for viewing simulation waveforms even for complex ROHD structures - Debug your design with [ROHD Live Debugger](https://github.com/intel/rohd/rohd_devtools_extension), an integrated debugger with cross-probing into ROHD source and synthesized RTL output from schematics rendering and waveform display all connected to live RTL simulation - - Support for **cosimulation with verilog modules** (via [ROHD Cosim](https://github.com/intel/rohd-cosim)) and **instantiation of verilog modules** in generated SystemVerilog code + - Support for **cosimulation with Verilog modules** (via [ROHD Cosim](https://github.com/intel/rohd-cosim)) and **instantiation of Verilog modules** in generated SystemVerilog code ROHD is *not* a new language, it is *not* a hardware description language (HDL), and it is *not* a version of High-Level Synthesis (HLS). ROHD can be classified as a generator framework, extending a powerful software language into the hardware domain and providing a full suite of tools and capabilities to accelerate hardware design and validation. From 47d55f89b7fa6d42f36c41b2f9bd4a5333e93b21 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 21:39:20 -0700 Subject: [PATCH 32/33] Address typed operation review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 6 +- doc/user_guide/_docs/A19-logic-structures.md | 6 +- .../conditionals/conditional_group.dart | 1 + .../modules/conditionals/conditionals.dart | 2 +- lib/src/modules/conditionals/flop.dart | 123 ++-- lib/src/modules/gates.dart | 36 +- lib/src/modules/operation_utils.dart | 78 ++- lib/src/modules/passthrough.dart | 36 +- lib/src/signals/logic_structure.dart | 105 ---- lib/src/signals/signals.dart | 2 + test/logic_structure_test.dart | 44 +- test/netlist_synthesizer_test.dart | 68 +++ test/typed_operations_test.dart | 547 +++++++++++++++++- test/unassignable_test.dart | 2 +- 14 files changed, 758 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 765147096a..ed2890553f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - `LogicArray` remains source-compatible for construction and operations. Its `value` and `previousValue` now return shape-carrying `LogicValueArray` instances, which remain `LogicValue` subtypes; code relying on their exact concrete runtime types may observe this behavior change (). - Added support for structures with constant leaves when their `clone()` implementation produces driveable fields, preserving domain-specific constant structures through generic operations. - Integrated `LogicEnum` with generic operations. Enum mappings are preserved through typed operation results, enum members can be used as `cases` keys, and incompatible enum mappings are rejected. +- Fixed generic operations to reject incompatible `LogicEnum` mappings inside structures and typed arrays, and to preserve mapped enum constant reset values in netlist register cells. - `Pipeline.get`, `PipelineStageInfo.get`, and `PipelineStageInfo.getAbs` now preserve each identifier's concrete type through stage reconstruction and SSA, allowing structures, typed arrays, enums, and scalar signals in one pipeline. `ReadyValidPipeline` retains its established multi-signal API and gains the same per-identifier typed access without separate payload pipeline classes. - Preserved aggregate structure assignment metadata through synthesis and netlist generation. - Fixed SystemVerilog synthesis of internally computed packed logic assigned to packed-array outputs, retaining required intermediate declarations. @@ -48,9 +49,8 @@ - **Migration:** plain `Const` and `LogicNet` sources no longer infer driveable outputs for operations that construct hardware. Request ``, for example `flop(clk, Const(1))`, to normalize them. - Use an operation `outputGenerator` (or `Passthrough.withOutput`) when an - explicit output representation is required, including mixed packed and - structured `cases` branches. + Use an operation `outputGenerator` when an explicit output representation is + required, including mixed packed and structured `cases` branches. - **Migration:** type-preserving operations on `TypedLogicArray` operands now require the identical `valueCodec` instance. Reuse a shared codec or cross an explicit `packed` boundary to reinterpret compatible bits. diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index 645d41cd53..3f87a047a9 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -37,9 +37,9 @@ nested typed arrays. A mux uses its `d0` operand as the default output prototype. Both operands must have matching concrete types and recursive geometry: field widths, array dimensions, packing hints, and leaf structure. Pass an `outputGenerator` to `Mux`, `FlipFlop`, `mux`, `flop`, or `cases` to -select a different result representation. Use `Passthrough.withOutput` for -the corresponding pass-through override while retaining the historical -positional `name` argument of `Passthrough`. +select a different result representation. `Passthrough` always preserves its +input representation; create and connect an explicit destination when another +representation is needed. These operations can consume a structure containing `Const` leaves when its `clone()` implementation returns the same concrete structure type with diff --git a/lib/src/modules/conditionals/conditional_group.dart b/lib/src/modules/conditionals/conditional_group.dart index f8573af4d8..15a1aa3b37 100644 --- a/lib/src/modules/conditionals/conditional_group.dart +++ b/lib/src/modules/conditionals/conditional_group.dart @@ -68,6 +68,7 @@ class ConditionalGroup extends Conditional { /// The contained leaf assignments preserve the normal ROHD connection graph, /// while [receiver] and [driver] retain the aggregate operation for backends /// that can emit it directly. +@internal class StructureConditionalGroup extends ConditionalGroup { /// The complete structure assigned by this group. final LogicStructure receiver; diff --git a/lib/src/modules/conditionals/conditionals.dart b/lib/src/modules/conditionals/conditionals.dart index 69e4c2665b..2e9f74a3f1 100644 --- a/lib/src/modules/conditionals/conditionals.dart +++ b/lib/src/modules/conditionals/conditionals.dart @@ -5,7 +5,7 @@ export 'case.dart'; export 'combinational.dart'; export 'conditional.dart'; export 'conditional_assign.dart'; -export 'conditional_group.dart'; +export 'conditional_group.dart' hide StructureConditionalGroup; export 'flop.dart'; export 'if.dart'; export 'sequential.dart'; diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index ea3595f4b2..76639b1e31 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -93,28 +93,15 @@ class FlipFlop extends Module { throw PortWidthMismatchException.equalWidth(resetValue, outputSchema); } if (resetValue is LogicStructure && outputSchema is LogicStructure) { - validateMatchingLogicStructure( + validateMatchingOperationStructure( resetValue, outputSchema, operation: 'FlipFlop reset', ); } - if (LogicType == Logic) { - return _ScalarFlipFlop( - clk, - d, - outputSchema as Logic, - en: en, - reset: reset, - resetValue: resetValue, - asyncReset: asyncReset, - usesOutputGenerator: outputGenerator != null, - name: name, - ) as FlipFlop; - } - if (d is LogicStructure) { - return _StructuredFlipFlop( + if (LogicType == Logic || d is! LogicStructure) { + return _ScalarFlipFlop( clk, d, outputSchema, @@ -122,10 +109,11 @@ class FlipFlop extends Module { reset: reset, resetValue: resetValue, asyncReset: asyncReset, + usesOutputGenerator: outputGenerator != null, name: name, ); } - return _TypedScalarFlipFlop( + return _StructuredFlipFlop( clk, d, outputSchema, @@ -200,8 +188,31 @@ class FlipFlop extends Module { } } +LogicValue? _mappedConstantResetValue( + Logic outputSchema, + Logic? reset, + dynamic resetValue, +) { + if (reset == null || resetValue is Logic) { + return null; + } + if (outputSchema is LogicEnum && resetValue is Enum) { + final encodedValue = outputSchema.mapping[resetValue]; + if (encodedValue == null) { + throw ArgumentError.value( + resetValue, + 'resetValue', + 'Not present in the mapping for ${outputSchema.runtimeType}.', + ); + } + return encodedValue; + } + return LogicValue.of(resetValue ?? 0, width: outputSchema.width); +} + /// Scalar SystemVerilog implementation of [FlipFlop]. -class _ScalarFlipFlop extends FlipFlop with SystemVerilog { +class _ScalarFlipFlop extends FlipFlop + with SystemVerilog { final String _enName = Naming.unpreferredName('en'); final String _clkName = Naming.unpreferredName('clk'); final String _dName = Naming.unpreferredName('d'); @@ -215,10 +226,10 @@ class _ScalarFlipFlop extends FlipFlop with SystemVerilog { late final Logic _d = input(_dName); @override - late final Logic q; + late final LogicType q; Logic? _resetValuePort; - late LogicValue _resetValueConst; + LogicValue? _resetValueConst; @override final bool asyncReset; @@ -229,8 +240,8 @@ class _ScalarFlipFlop extends FlipFlop with SystemVerilog { _ScalarFlipFlop( Logic clk, - Logic d, - Logic outputSchema, { + LogicType d, + LogicType outputSchema, { required this.asyncReset, required bool usesOutputGenerator, Logic? en, @@ -239,8 +250,12 @@ class _ScalarFlipFlop extends FlipFlop with SystemVerilog { super.name = 'flipflop', }) : super._() { addInput(_clkName, clk); - addInput(_dName, d, width: d.width); - q = usesOutputGenerator + if (LogicType == Logic) { + addInput(_dName, d, width: d.width); + } else { + addTypedInput(_dName, d); + } + q = (LogicType != Logic || usesOutputGenerator ? addTypedOutput( _qName, operationOutputClone(outputSchema), @@ -248,7 +263,7 @@ class _ScalarFlipFlop extends FlipFlop with SystemVerilog { : addOutput( _qName, width: d.width, - ); + ) as LogicType); if (en != null) { addInput(_enName, en); @@ -259,7 +274,8 @@ class _ScalarFlipFlop extends FlipFlop with SystemVerilog { if (resetValue != null && resetValue is Logic) { _resetValuePort = addInput(_resetValueName, resetValue, width: d.width); } else { - _resetValueConst = LogicValue.of(resetValue ?? 0, width: d.width); + _resetValueConst = + _mappedConstantResetValue(outputSchema, reset, resetValue); } } @@ -278,7 +294,7 @@ class _ScalarFlipFlop extends FlipFlop with SystemVerilog { reset: _reset, asyncReset: asyncReset, resetValues: - _reset != null ? {q: _resetValuePort ?? _resetValueConst} : null, + _reset != null ? {q: _resetValuePort ?? _resetValueConst!} : null, ); } @@ -430,56 +446,3 @@ class _StructuredFlipFlop extends FlipFlop { } } } - -/// Generic scalar-subclass implementation of [FlipFlop]. -/// -/// This path supports scalar domain types such as future `LogicEnum` signals. -class _TypedScalarFlipFlop - extends FlipFlop { - @override - late final LogicType q; - - @override - final bool asyncReset; - - @override - LogicValue? get constantResetValue => null; - - _TypedScalarFlipFlop( - Logic clk, - LogicType d, - LogicType outputSchema, { - required this.asyncReset, - Logic? en, - Logic? reset, - dynamic resetValue, - super.name = 'flipflop', - }) : super._( - definitionName: - 'FlipFlop_${outputSchema.runtimeType}_W${outputSchema.width}', - ) { - final localClock = addInput('clk', clk); - final localD = addTypedInput('d', d); - final localEnable = en == null ? null : addInput('en', en); - final localReset = reset == null ? null : addInput('reset', reset); - final localResetValue = reset != null && resetValue is Logic - ? addInput('resetValue', resetValue, width: d.width) - : resetValue; - q = addTypedOutput( - 'q', - operationOutputClone(outputSchema), - ); - - var contents = [q < localD]; - if (localEnable case final enable?) { - contents = [If(enable, then: contents)]; - } - Sequential( - localClock, - contents, - reset: localReset, - asyncReset: asyncReset, - resetValues: localReset == null ? null : {q: localResetValue ?? 0}, - ); - } -} diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index 57e34465e5..6e4f7a4d06 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -828,7 +828,7 @@ LogicType mux( throw PortWidthMismatchException.equalWidth(d0, d1); } if (d0 is LogicStructure && d1 is LogicStructure) { - validateMatchingLogicStructure(d1, d0, operation: 'mux'); + validateMatchingOperationStructure(d1, d0, operation: 'mux'); } if (outputGenerator == null && control is Const && control.value.isValid) { @@ -875,7 +875,7 @@ abstract class Mux extends Module { throw PortWidthMismatchException.equalWidth(d0, d1); } if (d0 is LogicStructure && d1 is LogicStructure) { - validateMatchingLogicStructure(d1, d0, operation: 'Mux'); + validateMatchingOperationStructure(d1, d0, operation: 'Mux'); } final outputSchema = createOperationOutput( @@ -888,15 +888,15 @@ abstract class Mux extends Module { validateOperationSource(d0, outputSchema, operation: 'Mux d0'); validateOperationSource(d1, outputSchema, operation: 'Mux d1'); - if (LogicType == Logic) { - return _ScalarMux( + if (LogicType == Logic || d0 is! LogicStructure) { + return _ScalarMux( control, d1, d0, - outputSchema as Logic, + outputSchema, usesOutputGenerator: outputGenerator != null, name: name, - ) as Mux; + ); } return _DomainMux( control, @@ -911,7 +911,8 @@ abstract class Mux extends Module { } /// A scalar inline implementation of [Mux]. -class _ScalarMux extends Mux with InlineSystemVerilog { +class _ScalarMux extends Mux + with InlineSystemVerilog { /// Name for the control signal of this mux. late final String _controlName; @@ -935,13 +936,13 @@ class _ScalarMux extends Mux with InlineSystemVerilog { /// Output port of the mux. @override - late final Logic out; + late final LogicType out; _ScalarMux( Logic control, - Logic d1, - Logic d0, - Logic outputSchema, { + LogicType d1, + LogicType d0, + LogicType outputSchema, { required bool usesOutputGenerator, super.name = 'mux', }) : super._() { @@ -951,9 +952,14 @@ class _ScalarMux extends Mux with InlineSystemVerilog { _outName = Naming.unpreferredName('out'); addInput(_controlName, control); - addInput(_d0Name, d0, width: d0.width); - addInput(_d1Name, d1, width: d1.width); - out = (usesOutputGenerator + if (LogicType == Logic) { + addInput(_d0Name, d0, width: d0.width); + addInput(_d1Name, d1, width: d1.width); + } else { + addTypedInput(_d0Name, d0); + addTypedInput(_d1Name, d1); + } + out = (LogicType != Logic || usesOutputGenerator ? addTypedOutput( _outName, operationOutputClone(outputSchema), @@ -961,7 +967,7 @@ class _ScalarMux extends Mux with InlineSystemVerilog { : addOutput( _outName, width: d0.width, - )) + ) as LogicType) ..makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); diff --git a/lib/src/modules/operation_utils.dart b/lib/src/modules/operation_utils.dart index cbd55e4bda..da0172f14b 100644 --- a/lib/src/modules/operation_utils.dart +++ b/lib/src/modules/operation_utils.dart @@ -8,6 +8,82 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart' show BaseLogicArray; + +bool _sameDimensions(List first, List second) => + first.length == second.length && + first.indexed.every((entry) => entry.$2 == second[entry.$1]); + +void _validateMatchingOperationStructure( + Logic first, + Logic second, + String operation, + String path, +) { + if (first.width != second.width) { + throw LogicConstructionException( + '$operation operands differ in width at $path.', + ); + } + + if (first is LogicEnum || second is LogicEnum) { + if (first is! LogicEnum || !first.isEquivalentTypeTo(second)) { + throw LogicConstructionException( + '$operation operands use incompatible LogicEnum mappings at $path.', + ); + } + } + + if (first is LogicStructure || second is LogicStructure) { + if (first is! LogicStructure || + second is! LogicStructure || + first.runtimeType != second.runtimeType || + first.elements.length != second.elements.length) { + throw LogicConstructionException( + '$operation operands differ in structure at $path.', + ); + } + + if (first is BaseLogicArray && second is BaseLogicArray) { + if (!_sameDimensions(first.dimensions, second.dimensions) || + first.elementWidth != second.elementWidth || + first.numUnpackedDimensions != second.numUnpackedDimensions || + first.isNet != second.isNet) { + throw LogicConstructionException( + '$operation array operands differ in shape at $path.', + ); + } + if (first is TypedLogicArray && + second is TypedLogicArray && + !identical(first.valueCodec, second.valueCodec)) { + throw LogicConstructionException( + '$operation typed-array operands use different value codecs at ' + '$path. Convert through packed to reinterpret their bits explicitly.', + ); + } + } + + for (var index = 0; index < first.elements.length; index++) { + _validateMatchingOperationStructure( + first.elements[index], + second.elements[index], + operation, + '$path[$index]', + ); + } + } +} + +/// Verifies structural compatibility for type-preserving operations. +/// +/// This is an internal operation-building contract, not a signal API. +@internal +void validateMatchingOperationStructure( + LogicStructure first, + LogicStructure second, { + required String operation, +}) => + _validateMatchingOperationStructure(first, second, operation, 'root'); /// Creates the output representation for a type-preserving operation. /// @@ -91,7 +167,7 @@ void validateOperationSource( } if (source is LogicStructure && output is LogicStructure) { - validateMatchingLogicStructure(source, output, operation: operation); + validateMatchingOperationStructure(source, output, operation: operation); } else if (source is LogicStructure && output is! LogicStructure) { // An explicit result intentionally normalizes packed structures. return; diff --git a/lib/src/modules/passthrough.dart b/lib/src/modules/passthrough.dart index e8d56437cb..86e5a3cbe8 100644 --- a/lib/src/modules/passthrough.dart +++ b/lib/src/modules/passthrough.dart @@ -10,9 +10,8 @@ import 'package:rohd/src/signals/logic_structure_signature.dart'; /// A no-op module that preserves [LogicType]. /// -/// The input supplies the output representation when `outputGenerator` is -/// omitted. Request `` explicitly to normalize a constant or net input -/// into a driveable scalar output. +/// The input supplies the output representation. Request `` explicitly +/// to normalize a constant or net input into a driveable scalar output. abstract class Passthrough extends Module { /// Input port. LogicType get in_; @@ -25,27 +24,17 @@ abstract class Passthrough extends Module { LogicType input, [ String name = 'passthrough', ]) => - Passthrough._create(input, name, null); - - /// Constructs a pass-through with an explicit output representation. - factory Passthrough.withOutput( - LogicType input, { - required LogicType Function({String? name}) outputGenerator, - String name = 'passthrough', - }) => - Passthrough._create(input, name, outputGenerator); + Passthrough._create(input, name); static Passthrough _create( LogicType input, String name, - LogicType Function({String? name})? outputGenerator, ) { final outputSchema = createOperationOutput( width: input.width, name: 'out', operation: 'Passthrough<$LogicType>', prototype: input, - outputGenerator: outputGenerator, ); validateOperationSource( input, @@ -56,8 +45,6 @@ abstract class Passthrough extends Module { if (LogicType == Logic) { return _ScalarPassthrough( input, - outputSchema as Logic, - usesOutputGenerator: outputGenerator != null, name: name, ) as Passthrough; } @@ -79,22 +66,9 @@ class _ScalarPassthrough extends Passthrough { @override late final Logic out; - _ScalarPassthrough( - Logic input, - Logic outputSchema, { - required bool usesOutputGenerator, - super.name = 'passthrough', - }) : super._() { + _ScalarPassthrough(Logic input, {super.name = 'passthrough'}) : super._() { in_ = addInput('in', input, width: input.width); - out = usesOutputGenerator - ? addTypedOutput( - 'out', - operationOutputClone(outputSchema), - ) - : addOutput( - 'out', - width: input.width, - ); + out = addOutput('out', width: input.width); final inner = Logic(name: 'inner', width: in_.width); inner <= in_; out <= inner; diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index fbc9c57dc1..6b98ff2924 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -9,73 +9,6 @@ part of 'signals.dart'; -void _validateMatchingStructure( - Logic first, - Logic second, - String operation, - String path, -) { - if (first.width != second.width) { - throw LogicConstructionException( - '$operation operands differ in width at $path.', - ); - } - - if (first is LogicStructure || second is LogicStructure) { - if (first is! LogicStructure || - second is! LogicStructure || - first.runtimeType != second.runtimeType || - first.elements.length != second.elements.length) { - throw LogicConstructionException( - '$operation operands differ in structure at $path.', - ); - } - - if (first is BaseLogicArray && second is BaseLogicArray) { - if (!_sameDimensions(first.dimensions, second.dimensions) || - first.elementWidth != second.elementWidth || - first.numUnpackedDimensions != second.numUnpackedDimensions || - first.isNet != second.isNet) { - throw LogicConstructionException( - '$operation array operands differ in shape at $path.', - ); - } - if (first is TypedLogicArray && - second is TypedLogicArray && - !identical(first.valueCodec, second.valueCodec)) { - throw LogicConstructionException( - '$operation typed-array operands use different value codecs at ' - '$path. Convert through packed to reinterpret their bits explicitly.', - ); - } - } - - for (var index = 0; index < first.elements.length; index++) { - _validateMatchingStructure( - first.elements[index], - second.elements[index], - operation, - '$path[$index]', - ); - } - } -} - -/// Verifies that [first] and [second] have identical recursive structure. -/// -/// Compatibility requires matching concrete structure types, element counts, -/// leaf widths, array geometry, and typed-array codec identity. Field names -/// may differ because separately constructed instances of one structure type -/// can use different root names. -/// -/// Throws a [LogicConstructionException] when the structures do not match. -void validateMatchingLogicStructure( - LogicStructure first, - LogicStructure second, { - String operation = 'Typed operation', -}) => - _validateMatchingStructure(first, second, operation, 'root'); - LogicType _checkedTypedClone( LogicType source, { String? name, @@ -306,44 +239,6 @@ class LogicStructure implements Logic { _calculateLeafElements(), ); - /// Promotes direct non-array child structures into a new generic structure. - /// - /// Each promoted element is cloned and connected to its source. When - /// [prefixFieldNames] is `true`, promoted fields are named - /// `${child.name}_${field.name}`. This preserves the immediate structure - /// path and avoids common field-name collisions. The method throws a - /// [LogicConstructionException] when the resulting field names collide. - LogicStructure flattenOuter({String? name, bool prefixFieldNames = true}) { - final sources = <(Logic, String)>[]; - for (final element in elements) { - if (element is LogicStructure && element is! BaseLogicArray) { - for (final field in element.elements) { - final prefix = - element.name.endsWith('_') ? element.name : '${element.name}_'; - final fieldName = - prefixFieldNames ? '$prefix${field.name}' : field.name; - sources.add((field, fieldName)); - } - } else { - sources.add((element, element.name)); - } - } - - final names = {}; - for (final source in sources) { - if (!names.add(source.$2)) { - throw LogicConstructionException( - 'Flattened structure contains duplicate field name ${source.$2}.', - ); - } - } - - final flattenedElements = sources - .map((source) => 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/signals.dart b/lib/src/signals/signals.dart index 03b760ed06..fadac258a8 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -9,6 +9,8 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/conditional_group.dart' + show StructureConditionalGroup; import 'package:rohd/src/utilities/index_utilities.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/synchronous_propagator.dart'; diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index d2be8e329f..9232175247 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -244,55 +244,17 @@ void main() { expect(orig.clone(name: 'newName').name, 'newName'); }); - test('whole structure conditional assignment retains metadata', () { + test('whole structure conditional assignment groups leaf assignments', () { final receiver = MyStruct(name: 'receiver'); final driver = MyStruct(name: 'driver'); final assignment = receiver < driver; - expect(assignment, isA()); - final group = assignment as StructureConditionalGroup; - expect(group.receiver, same(receiver)); - expect(group.driver, same(driver)); + expect(assignment, isA()); + final group = assignment as ConditionalGroup; expect(group.conditionals, hasLength(receiver.leafElements.length)); }); - test('flatten outer clones and connects promoted fields', () { - final config = LogicStructure([ - Logic(name: 'mode', width: 2), - Logic(name: 'valid'), - ], name: 'config'); - final control = LogicStructure([ - Logic(name: 'enable'), - config, - ], name: 'control'); - - final flattened = control.flattenOuter(name: 'flatControl'); - - expect(flattened.name, 'flatControl'); - expect(flattened.elements.map((element) => element.name), - ['enable', 'config_mode', 'config_valid']); - expect(flattened.elements[1].width, 2); - expect( - flattened.elements[0].srcConnections, contains(control.elements[0])); - expect( - flattened.elements[1].srcConnections, contains(config.elements[0])); - expect( - flattened.elements[2].srcConnections, contains(config.elements[1])); - }); - - test('flatten outer rejects duplicate promoted field names', () { - final control = LogicStructure([ - Logic(name: 'mode'), - LogicStructure([ - Logic(name: 'mode'), - ], name: 'config'), - ], name: 'control'); - - expect(() => control.flattenOuter(prefixFieldNames: false), - throwsA(isA())); - }); - test('tricky withSet', () { // first field has width of 72 so this is the starting point // second field has a width of 12 diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index 499a8d918e..b44ed130ad 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -151,6 +151,39 @@ class ControlledFlopModule extends Module { } } +enum _NetlistResetState { idle, active, done } + +LogicEnum<_NetlistResetState> _netlistResetState({String? name}) => + LogicEnum<_NetlistResetState>.withMapping( + { + _NetlistResetState.idle: 0, + _NetlistResetState.active: 2, + _NetlistResetState.done: 3, + }, + width: 2, + name: name, + ); + +class EnumResetFlopModule extends Module { + EnumResetFlopModule( + Logic clk, + Logic reset, { + required bool asyncReset, + }) : super(name: 'enumresetflop') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + final d = addTypedInput('d', _netlistResetState(name: 'd')); + final q = flop( + clk, + d, + reset: reset, + resetValue: _NetlistResetState.active, + asyncReset: asyncReset, + ); + addTypedOutput('q', q.clone).gets(q); + } +} + /// Exercises Add. class AddModule extends Module { Logic get sum => output('sum'); @@ -1232,6 +1265,41 @@ void main() { ); }); + test('LogicEnum constant resets preserve mapped netlist parameters', + () async { + final cases = [ + ( + asyncReset: false, + type: r'$sdff', + parameter: 'SRST_VALUE', + ), + ( + asyncReset: true, + type: r'$adff', + parameter: 'ARST_VALUE', + ), + ]; + + for (final testCase in cases) { + final module = EnumResetFlopModule( + SimpleClockGenerator(10).clk, + Logic(), + asyncReset: testCase.asyncReset, + ); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (candidate) => candidate['type'] == testCase.type, + ); + final parameters = cell['parameters'] as Map; + + expect(parameters['WIDTH'], 2); + expect(parameters[testCase.parameter], '10'); + } + }); + test(r'Add maps to $add cell', () async { final json = await _synthToMap( AddModule(Logic(width: 8), Logic(width: 8)), diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart index 42d84b24f2..87b98207a4 100644 --- a/test/typed_operations_test.dart +++ b/test/typed_operations_test.dart @@ -50,6 +50,20 @@ LogicEnum<_OperationEnum> _operationEnum({String? name}) => name: name, ); +LogicEnum<_OperationEnum> _operationEnumWithActiveEncoding( + int activeEncoding, { + String? name, +}) => + LogicEnum<_OperationEnum>.withMapping( + { + _OperationEnum.idle: 0, + _OperationEnum.active: activeEncoding, + _OperationEnum.done: 3, + }, + width: 2, + name: name, + ); + LogicValueCodec _typedLaneCodec() => LogicValueCodec( decode: (value) => value.isValid ? value.toInt() : 0, encode: (value) => LogicValue.ofInt(value, 9), @@ -62,17 +76,41 @@ class _TypedPacket extends LogicStructure { final List laneDimensions; - _TypedPacket({this.laneDimensions = const [2], String? name}) - : super([ + final int laneNumUnpackedDimensions; + + _TypedPacket({ + this.laneDimensions = const [2], + this.laneNumUnpackedDimensions = 0, + String? name, + }) : super([ Logic(name: 'opcode', width: 3), TypedLogicArray<_TypedLane, LogicValue>( laneDimensions, _TypedLane.new, - name: 'lanes'), + name: 'lanes', numUnpackedDimensions: laneNumUnpackedDimensions), ], name: name ?? 'typed_packet'); @override - _TypedPacket clone({String? name}) => - _TypedPacket(laneDimensions: laneDimensions, name: name ?? this.name); + _TypedPacket clone({String? name}) => _TypedPacket( + laneDimensions: laneDimensions, + laneNumUnpackedDimensions: laneNumUnpackedDimensions, + name: name ?? this.name, + ); +} + +class _EnumPacket extends LogicStructure { + LogicEnum<_OperationEnum> get state => + elements.single as LogicEnum<_OperationEnum>; + + final int activeEncoding; + + _EnumPacket({required this.activeEncoding, String? name}) + : super([ + _operationEnumWithActiveEncoding(activeEncoding, name: 'state'), + ], name: name ?? 'enum_packet'); + + @override + _EnumPacket clone({String? name}) => + _EnumPacket(activeEncoding: activeEncoding, name: name ?? this.name); } class _BadClonePacket extends LogicStructure { @@ -134,6 +172,87 @@ class _ConfiguredPacketGenerator { } } +class _ScalarDomainVectorHarness extends Module { + _ScalarDomainVectorHarness( + Logic control, + Logic reset, + _ScalarDomainLogic d0, + _ScalarDomainLogic d1, + ) { + final clk = SimpleClockGenerator(10).clk; + control = addInput('control', control); + reset = addInput('reset', reset); + d0 = addTypedInput('d0', d0); + d1 = addTypedInput('d1', d1); + + final selected = Mux(control, d1, d0).out; + final registered = FlipFlop( + clk, + selected, + reset: reset, + resetValue: 0, + ).q; + addTypedOutput('selected', selected.clone).gets(selected); + addTypedOutput('registered', registered.clone).gets(registered); + } +} + +class _ArrayOperationVectorHarness extends Module { + _ArrayOperationVectorHarness( + Logic control, + Logic enable, + Logic reset, + LogicArray first, + LogicArray second, + ) { + final clk = SimpleClockGenerator(10).clk; + control = addInput('control', control); + enable = addInput('enable', enable); + reset = addInput('reset', reset); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux(control, second, first).out; + final registered = FlipFlop( + clk, + selected, + en: enable, + reset: reset, + resetValue: 0, + ).q; + addTypedOutput('selected', selected.clone).gets(selected); + addTypedOutput('out', registered.clone).gets(registered); + } +} + +class _PacketArrayOperationVectorHarness extends Module { + _PacketArrayOperationVectorHarness( + Logic control, + Logic enable, + Logic reset, + _TypedPacket first, + _TypedPacket second, + ) { + final clk = SimpleClockGenerator(10).clk; + control = addInput('control', control); + enable = addInput('enable', enable); + reset = addInput('reset', reset); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux(control, second, first).out; + final registered = FlipFlop( + clk, + selected, + en: enable, + reset: reset, + resetValue: 0, + ).q; + addTypedOutput('selected', selected.clone).gets(selected); + addTypedOutput('out', registered.clone).gets(registered); + } +} + class _TypedOperationsSynthesisHarness extends Module { _TypedOperationsSynthesisHarness(Logic clk, Logic reset, Logic control, Logic selector, _TypedPacket first, _TypedPacket second) { @@ -162,10 +281,7 @@ class _TypedOperationsSynthesisHarness extends Module { defaultValue: first, name: 'selected_by_index', ); - final passed = Passthrough.withOutput( - selectedByIndex, - outputGenerator: selectedByIndex.clone, - ).out; + final passed = Passthrough(selectedByIndex).out; final registered = FlipFlop( clk, passed, @@ -358,6 +474,371 @@ void main() { await Simulator.endSimulation(); }); + test('scalar domain muxes and flops emit directly and match vectors', + () async { + final harness = _ScalarDomainVectorHarness( + Logic(), + Logic(), + _ScalarDomainLogic(schema: 'd0', name: 'd0'), + _ScalarDomainLogic(schema: 'd1', name: 'd1'), + ); + await harness.build(); + + final systemVerilog = harness.dumpSystemVerilog(); + expect(systemVerilog, contains('always_ff')); + expect(systemVerilog, contains('?')); + expect( + systemVerilog, + isNot(matches(RegExp(r'module\s+\w*(?:Mux|FlipFlop)'))), + ); + + final vectors = [ + Vector( + { + 'control': 0, + 'reset': 1, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0x3, + }, + ), + Vector( + { + 'control': 0, + 'reset': 1, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0x3, + 'registered': 0, + }, + ), + Vector( + { + 'control': 1, + 'reset': 0, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0xc, + 'registered': 0, + }, + ), + Vector( + { + 'control': 1, + 'reset': 0, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0xc, + 'registered': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'reset': 0, + 'd0': 0x5, + 'd1': 0xa, + }, + { + 'selected': 0x5, + 'registered': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'reset': 0, + 'd0': 0x5, + 'd1': 0xa, + }, + { + 'selected': 0x5, + 'registered': 0x5, + }, + ), + ]; + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkIverilogVector(harness, vectors); + }); + + test('array muxes and flops match packed and unpacked backends', () async { + final configurations = [ + ( + label: 'packed', + numUnpackedDimensions: 0, + ), + ( + label: 'mixed', + numUnpackedDimensions: 1, + ), + ( + label: 'unpacked', + numUnpackedDimensions: 2, + ), + ]; + final vectors = [ + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0x3, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0x3, + 'out': 0, + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0xc, + 'out': 0, + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0xc, + 'out': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'enable': 0, + 'reset': 0, + 'first': 0x5, + 'second': 0xa, + }, + { + 'selected': 0x5, + 'out': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': 0x5, + 'second': 0xa, + }, + { + 'selected': 0x5, + 'out': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': 0x5, + 'second': 0xa, + }, + { + 'selected': 0x5, + 'out': 0x5, + }, + ), + ]; + + for (final configuration in configurations) { + final first = LogicArray( + [2, 2], + 1, + name: 'first_${configuration.label}', + numUnpackedDimensions: configuration.numUnpackedDimensions, + ); + final second = LogicArray( + [2, 2], + 1, + name: 'second_${configuration.label}', + numUnpackedDimensions: configuration.numUnpackedDimensions, + ); + final harness = _ArrayOperationVectorHarness( + Logic(), + Logic(), + Logic(), + first, + second, + ); + await harness.build(); + + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkVerilatorVector(harness, vectors); + if (configuration.numUnpackedDimensions == 0) { + SimCompare.checkIverilogVector(harness, vectors); + } + await Simulator.reset(); + } + }, timeout: const Timeout(Duration(minutes: 2))); + + test('nested typed-array packet operations match an emitted backend', + () async { + final configurations = [ + ( + label: 'mixed', + laneDimensions: const [2, 2], + laneNumUnpackedDimensions: 1, + ), + ]; + + for (final configuration in configurations) { + final first = _TypedPacket( + laneDimensions: configuration.laneDimensions, + laneNumUnpackedDimensions: configuration.laneNumUnpackedDimensions, + name: 'first_${configuration.label}', + ); + final second = _TypedPacket( + laneDimensions: configuration.laneDimensions, + laneNumUnpackedDimensions: configuration.laneNumUnpackedDimensions, + name: 'second_${configuration.label}', + ); + final firstValue = LogicValue.ofInt(0x15, first.width); + final secondValue = LogicValue.ofBigInt( + (BigInt.one << second.width) - BigInt.one, second.width); + final harness = _PacketArrayOperationVectorHarness( + Logic(), + Logic(), + Logic(), + first, + second, + ); + final vectors = [ + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': LogicValue.ofInt(0, first.width), + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': secondValue, + 'out': LogicValue.ofInt(0, first.width), + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': secondValue, + 'out': secondValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 0, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': secondValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': secondValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': firstValue, + }, + ), + ]; + + await harness.build(); + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkVerilatorVector(harness, vectors); + await Simulator.reset(); + } + }); + test('LogicEnum domains compose with generic operations', () async { final clk = SimpleClockGenerator(10).clk; final control = Logic(); @@ -437,6 +918,43 @@ void main() { await Simulator.endSimulation(); }); + test('operations reject incompatible nested enum mappings', () { + final firstPacket = _EnumPacket(activeEncoding: 2, name: 'first_packet'); + final secondPacket = _EnumPacket(activeEncoding: 1, name: 'second_packet'); + final firstArray = TypedLogicArray, LogicValue>( + [2], + ({name}) => _operationEnumWithActiveEncoding(2, name: name), + name: 'first_array', + ); + final secondArray = TypedLogicArray, LogicValue>( + [2], + ({name}) => _operationEnumWithActiveEncoding(1, name: name), + name: 'second_array', + ); + + expect( + () => Mux(Logic(), secondPacket, firstPacket), + throwsA(isA()), + ); + expect( + () => Mux( + Logic(), + firstPacket, + firstPacket, + outputGenerator: ({name}) => _EnumPacket(activeEncoding: 1, name: name), + ), + throwsA(isA()), + ); + expect( + () => Mux(Logic(), secondArray, firstArray), + throwsA(isA()), + ); + expect( + Mux(Logic(), secondArray.packed, firstArray.packed).out, + isA(), + ); + }); + test('inferred constants and nets reject driveable operation results', () { final clk = Logic(); final control = Logic(); @@ -515,24 +1033,19 @@ void main() { expect(generatedSchema.data.value.toInt(), 0xe); }); - test('flop and passthrough generators override output schemas', () async { + test('flop generators override output schemas', () async { final clk = SimpleClockGenerator(10).clk; final source = _ConfiguredPacket(schema: 'source', name: 'source'); - final passthroughGenerator = _ConfiguredPacketGenerator('passthrough'); final flopGenerator = _ConfiguredPacketGenerator('flop'); - final forwarded = Passthrough.withOutput( - source, - outputGenerator: passthroughGenerator.call, - ).out; + final forwarded = Passthrough(source).out; final registered = FlipFlop( clk, source, outputGenerator: flopGenerator.call, ).q; - expect(forwarded.schema, 'passthrough'); + expect(forwarded.schema, 'source'); expect(registered.schema, 'flop'); - expect(passthroughGenerator.calls, 1); expect(flopGenerator.calls, 1); unawaited(Simulator.run()); diff --git a/test/unassignable_test.dart b/test/unassignable_test.dart index dcbe039619..26eb1f36f1 100644 --- a/test/unassignable_test.dart +++ b/test/unassignable_test.dart @@ -63,7 +63,7 @@ void main() { out <= Logic(); fail('Should have thrown an exception'); } on UnassignableException catch (e) { - expect(e.toString(), contains('${out.parentModule.runtimeType}')); + expect(e.toString(), contains(out.parentModule!.definitionName)); } }); } From 1918cdb521b73d795b1fc03c2554b2491d5c9025 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 18 Oct 2026 21:39:34 -0700 Subject: [PATCH 33/33] Narrow array synthesis test coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/array_collapsing_test.dart | 6 +-- test/logic_array_test.dart | 75 +++++++++++++--------------- test/logic_enum_test.dart | 7 ++- test/verilator_compilation_test.dart | 20 ++++---- 4 files changed, 55 insertions(+), 53 deletions(-) diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index be7431f72b..18beec7b92 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -2565,7 +2565,7 @@ void main() { expect(_topModuleBody(mod.dumpSystemVerilog()), topBody); final vectors = [ for (final text in [ - for (final pattern in [0, 0xff, 0xe4, 0x1b, 0x55, 0xaa]) + for (final pattern in [0, 0xff, 0xe4]) pattern.toRadixString(2).padLeft(8, '0'), 'xz10zx01', 'z01x10xz', @@ -2582,7 +2582,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); if (configuration.unpacked > 0) { - SimCompare.checkVerilatorVector(mod, vectors.take(6).toList()); + SimCompare.checkVerilatorVector(mod, vectors.take(3).toList()); } else { SimCompare.checkIverilogVector(mod, vectors); } @@ -2599,7 +2599,7 @@ void main() { expect(topBody, isNot(contains('splitStage'))); expect(topBody, isNot(contains('assign '))); final vectors = [ - for (final pattern in [0, 0xff, 0x1b, 0xe4, 0x55, 0xaa]) + for (final pattern in [0, 0xff, 0x1b]) Vector({ 'source': pattern }, { diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index ddd6de3332..e0a11a1f77 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -7,13 +7,10 @@ // 2023 May 2 // Author: Max Korbel -import 'dart:math'; - import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:rohd/src/utilities/sv_cleaner.dart'; -import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; class _TypedInputModule extends Module { @@ -580,14 +577,14 @@ class PartialArrayAssignTop extends Module { void main() { List passthroughVectors(int width) { - const randWidth = 23; - final rand = Random(1234); - final values = List.generate( - 10, - (index) => - LogicValue.ofInt(rand.nextInt(oneSllBy(randWidth)), randWidth) - .replicate(width ~/ randWidth + 1) - .getRange(0, width)); + final values = [ + LogicValue.ofInt(0, width), + LogicValue.filled(width, LogicValue.one), + LogicValue.ofString([ + for (var bit = width - 1; bit >= 0; bit--) + if (bit.isEven) '1' else '0', + ].join()), + ]; return [ for (final value in values) Vector({'laIn': value}, {'laOut': value}) ]; @@ -881,7 +878,7 @@ void main() { group('simple', () { test('single dimension', () async { - final mod = SimpleLAPassthrough(LogicArray([3], 8)); + final mod = SimpleLAPassthrough(LogicArray([3], 3)); await testArrayPassthrough(mod); }); @@ -891,12 +888,12 @@ void main() { }); test('2 dimensions', () async { - final mod = SimpleLAPassthrough(LogicArray([3, 2], 8)); + final mod = SimpleLAPassthrough(LogicArray([3, 2], 3)); await testArrayPassthrough(mod); }); test('3 dimensions', () async { - final mod = SimpleLAPassthrough(LogicArray([3, 2, 3], 8)); + final mod = SimpleLAPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod); }); @@ -941,7 +938,7 @@ void main() { }); test('sub-array', () async { - final superArray = LogicArray([4, 3, 2], 8); + final superArray = LogicArray([3, 2, 2], 3); final subArray = superArray.elements[0] as LogicArray; final mod = SimpleLAPassthrough(subArray); await testArrayPassthrough(mod); @@ -949,8 +946,8 @@ void main() { test('3 dimensions with interface', () async { final mod = LAPassthroughWithIntf(LAPassthroughIntf( - dimensions: [3, 2, 3], - elementWidth: 3, + dimensions: [3, 2, 2], + elementWidth: 2, numUnpackedDimensions: 0, )); @@ -958,29 +955,29 @@ void main() { // ensure ports with interface are still an array final sv = mod.dumpSystemVerilog(); - expect(sv, contains('input logic [2:0][1:0][2:0][2:0] laIn')); - expect(sv, contains('output logic [2:0][1:0][2:0][2:0] laOut')); + expect(sv, contains('input logic [2:0][1:0][1:0][1:0] laIn')); + expect(sv, contains('output logic [2:0][1:0][1:0][1:0] laOut')); }); testWithVerilator( '3 dimensions with interface and unpacked', () => LAPassthroughWithIntf(LAPassthroughIntf( - dimensions: [3, 2, 3], - elementWidth: 3, + dimensions: [3, 2, 2], + elementWidth: 2, numUnpackedDimensions: 1, )), (mod) async { await testArrayPassthrough(mod, noSvSim: true); // ensure ports with interface are still an array final sv = mod.dumpSystemVerilog(); - expect(sv, contains('input logic [1:0][2:0][2:0] laIn [2:0]')); - expect(sv, contains('output logic [1:0][2:0][2:0] laOut [2:0]')); + expect(sv, contains('input logic [1:0][1:0][1:0] laIn [2:0]')); + expect(sv, contains('output logic [1:0][1:0][1:0] laOut [2:0]')); }); }); group('pack and unpack', () { test('3d', () async { - final mod = PackAndUnpackPassthrough(LogicArray([5, 3, 2], 8)); + final mod = PackAndUnpackPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); @@ -995,13 +992,13 @@ void main() { group('pack and unpack with arrays', () { test('2d', () async { - final mod = PackAndUnpackWithArraysPassthrough(LogicArray([3, 2], 8)); + final mod = PackAndUnpackWithArraysPassthrough(LogicArray([3, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); test('3d', () async { final mod = - PackAndUnpackWithArraysPassthrough(LogicArray([4, 3, 2], 8)); + PackAndUnpackWithArraysPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); @@ -1017,31 +1014,31 @@ void main() { group('change array dimensions around and back', () { test('3d', () async { - final mod = RearrangeArraysPassthrough(LogicArray([4, 3, 2], 8)); + final mod = RearrangeArraysPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod); }); testWithVerilator( '3d unpacked', () => RearrangeArraysPassthrough( - LogicArray([4, 3, 2], 3, numUnpackedDimensions: 2), + LogicArray([3, 2, 2], 3, numUnpackedDimensions: 2), intermediateUnpacked: 1), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); final sv = mod.dumpSystemVerilog(); - expect(sv.contains('logic [2:0][3:0][2:0] intermediate [1:0]'), true); + expect(sv.contains('logic [1:0][2:0][2:0] intermediate [1:0]'), true); }); }); group('different port and input widths', () { test('array param mismatch', () async { - final i = LogicArray([3, 2], 8, numUnpackedDimensions: 1); - final o = LogicArray([3, 2], 8, numUnpackedDimensions: 1); + final i = LogicArray([3, 2], 3, numUnpackedDimensions: 1); + final o = LogicArray([3, 2], 3, numUnpackedDimensions: 1); final mod = SimpleLAPassthrough( i, dimOverride: [1, 3], - elemWidthOverride: 16, + elemWidthOverride: 6, numUnpackedOverride: 0, ); o <= mod.laOut; @@ -1049,12 +1046,12 @@ void main() { }); test('logic into array', () async { - final i = Logic(width: 3 * 2 * 8); - final o = Logic(width: 3 * 2 * 8); + final i = Logic(width: 3 * 2 * 3); + final o = Logic(width: 3 * 2 * 3); final mod = SimpleLAPassthroughLogic( i, dimensions: [1, 3], - elementWidth: 16, + elementWidth: 6, numUnpackedDimensions: 0, ); o <= mod.laOut; @@ -1064,7 +1061,7 @@ void main() { group('name collisions', () { test('3d', () async { - final mod = ArrayNameConflicts(LogicArray([4, 3, 2], 8)); + final mod = ArrayNameConflicts(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); @@ -1080,7 +1077,7 @@ void main() { group('simple hierarchy', () { test('3d', () async { - final mod = SimpleArraysAndHierarchy(LogicArray([2], 8)); + final mod = SimpleArraysAndHierarchy(LogicArray([2], 3)); await testArrayPassthrough(mod); final sv = mod.dumpSystemVerilog(); expect(sv, contains('SimpleLAPassthrough simple_la_passthrough')); @@ -1099,7 +1096,7 @@ void main() { group('fancy hierarchy', () { test('3d', () async { - final mod = FancyArraysAndHierarchy(LogicArray([4, 3, 2], 8)); + final mod = FancyArraysAndHierarchy(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); final sv = mod.dumpSystemVerilog(); @@ -1120,7 +1117,7 @@ void main() { group('conditionals', () { test('3 dimensions conditional assignment', () async { - final mod = CondAssignArray(LogicArray([3, 2, 3], 8)); + final mod = CondAssignArray(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod); }); diff --git a/test/logic_enum_test.dart b/test/logic_enum_test.dart index 202b6b8815..32020534db 100644 --- a/test/logic_enum_test.dart +++ b/test/logic_enum_test.dart @@ -3132,7 +3132,12 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - for (final generateEnums in [true, false]) { + // Array-layout coverage needs every packing mode. The packed case also + // covers disabling enum declarations, which is representation-only. + for (final generateEnums in [ + true, + if (unpacked == 0) false, + ]) { final configuration = SystemVerilogSynthesizerConfiguration( generateEnums: generateEnums); final sv = module.dumpSystemVerilog(configuration: configuration); diff --git a/test/verilator_compilation_test.dart b/test/verilator_compilation_test.dart index db5fcaea33..758c7694d9 100644 --- a/test/verilator_compilation_test.dart +++ b/test/verilator_compilation_test.dart @@ -37,10 +37,10 @@ endmodule class VectorArrayFixture extends Module { VectorArrayFixture() { final array = addInputArray( - 'array', LogicArray([3], 4, numUnpackedDimensions: 1), - dimensions: [3], elementWidth: 4, numUnpackedDimensions: 1); + 'array', LogicArray([2], 2, numUnpackedDimensions: 1), + dimensions: [2], elementWidth: 2, numUnpackedDimensions: 1); addOutputArray('observed', - dimensions: [3], elementWidth: 4, numUnpackedDimensions: 1) <= + dimensions: [2], elementWidth: 2, numUnpackedDimensions: 1) <= array; } } @@ -100,8 +100,8 @@ void main() { final module = VectorArrayFixture(); await module.build(); SimCompare.checkVerilatorVector(module, [ - Vector({'array': 0x123}, {'observed': 0x123}), - Vector({'array': 0xabc}, {'observed': 0xabc}), + Vector({'array': 0x3}, {'observed': 0x3}), + Vector({'array': 0xc}, {'observed': 0xc}), Vector({'array': 0}, {'observed': 0}), ]); }); @@ -112,7 +112,7 @@ void main() { SimCompare.checkVerilatorVector( module, [ - Vector({'array': 0x123}, {'observed': 0x321}), + Vector({'array': 0x3}, {'observed': 0x2}), ], buildOnly: true); }); @@ -126,8 +126,8 @@ void main() { TestFailure? failure; try { SimCompare.checkVerilatorVector(module, [ - Vector({'array': 0xabc}, {'observed': 0xabc}), - Vector({'array': 0x123}, {'observed': 0x321}), + Vector({'array': 0xc}, {'observed': 0xc}), + Vector({'array': 0x3}, {'observed': 0x2}), ]); } on TestFailure catch (error) { failure = error; @@ -333,8 +333,8 @@ esac if (!capture.run(() => SimCompare.checkVerilatorVector( module, [ - Vector({'array': 0x123}, {'observed': 0x123}), - Vector({'array': 0xabc}, {'observed': 0xabc}), + Vector({'array': 0x3}, {'observed': 0x3}), + Vector({'array': 0xc}, {'observed': 0xc}), ], buildOnly: buildOnly, dumpWaves: true,