diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4abc553..bdd37dba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Enforced that `Const` values cannot be changed through `put` or `inject`, including through another `Logic` driven by a `Const` (). - Added per-`Const` preferred radix control for generated literals, normalized `Const` names to reflect their displayed radix, and enhanced `LogicValue.toRadixString` to omit its width and radix decorator or digit separators (). - Improved generated SystemVerilog to collapse contiguous partial array and range assignments into packed slice assignments when safe (). -- Added per-direction configuration of explicit or implicit object and data types for generated SystemVerilog ports. Defaults preserve the existing `input logic`, `output logic`, and `inout wire` declarations (). +- Added per-direction configuration of explicit or implicit object and data types for generated SystemVerilog ports. Defaults preserve the existing `input logic`, `output logic`, and `inout wire` declarations (, ). - Improved `Logic.getRange` and `slice` on filled `Const`s to return direct constants instead of constructing `BusSubset` modules (). - Improved generated SystemVerilog for swizzles to compact adjacent bit selections into legal slice expressions (). - Improved generated SystemVerilog to collapse a variety of intermediate `LogicArray`s and net buses (e.g. from bit-blasting, aggregate connections, `assignSubset`) into inline concatenations on their consuming connections, eliminating unnecessary intermediate declarations, `assign`s, and `net_connect`s when it is safe to do so (). diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..4ff9423b1 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'src/exceptions/exceptions.dart'; @@ -8,7 +8,7 @@ export 'src/interfaces/interfaces.dart'; export 'src/module.dart'; export 'src/modules/modules.dart'; export 'src/selection.dart'; -export 'src/signals/signals.dart'; +export 'src/signals/signals.dart' hide LogicDef; export 'src/simulator.dart'; export 'src/swizzle.dart'; export 'src/synthesizers/synthesizers.dart'; diff --git a/lib/src/finite_state_machine.dart b/lib/src/finite_state_machine.dart index 15fa3d870..e80d8fd35 100644 --- a/lib/src/finite_state_machine.dart +++ b/lib/src/finite_state_machine.dart @@ -15,13 +15,13 @@ import 'package:rohd/rohd.dart'; /// Deprecated: use [FiniteStateMachine] instead. @Deprecated('Use FiniteStateMachine instead') -typedef StateMachine = FiniteStateMachine; +typedef StateMachine = FiniteStateMachine; /// Simple class for FSM [FiniteStateMachine]. /// /// Abstraction for representing Finite state machines (FSM). /// Contains the logic for performing the state transitions. -class FiniteStateMachine { +class FiniteStateMachine { /// List of all the [State]s in this machine. List> get states => UnmodifiableListView(_states); final List> _states; @@ -71,7 +71,8 @@ class FiniteStateMachine { /// /// Use [getStateIndex] to map from a [StateIdentifier] to the value on this /// bus. - final Logic currentState; + late final LogicEnum currentState = + stateEnum(name: 'currentState'); /// A [List] of [Conditional] actions to perform at the beginning of the /// evaluation of actions for the [FiniteStateMachine]. This is useful for @@ -82,7 +83,8 @@ class FiniteStateMachine { /// /// Use [getStateIndex] to map from a [StateIdentifier] to the value on this /// bus. - final Logic nextState; + late final LogicEnum nextState = + stateEnum(name: 'nextState'); /// Returns a ceiling on the log of [x] base [base]. static int _logBase(num x, num base) => (log(x) / log(base)).ceil(); @@ -93,6 +95,10 @@ class FiniteStateMachine { /// If `true`, the [reset] signal is asynchronous. final bool asyncReset; + /// Creates a state signal using this machine's state-to-index encoding. + LogicEnum stateEnum({String? name}) => + LogicEnum.withMapping(stateIndexLookup, name: name); + /// Creates an finite state machine for the specified list of [_states], with /// an initial state of [resetState] (when synchronous [reset] is high) and /// transitions on positive [clk] edges. @@ -119,11 +125,7 @@ class FiniteStateMachine { this.asyncReset = false, List setupActions = const [], }) : setupActions = List.unmodifiable(setupActions), - stateWidth = _logBase(_states.length, 2), - currentState = - Logic(name: 'currentState', width: _logBase(_states.length, 2)), - nextState = - Logic(name: 'nextState', width: _logBase(_states.length, 2)) { + stateWidth = max(1, _logBase(_states.length, 2)) { _validate(); var stateCounter = 0; @@ -138,8 +140,9 @@ class FiniteStateMachine { currentState, _states .map((state) => CaseItem( - Const(_stateValueLookup[state], width: stateWidth) - .named(state.identifier.toString()), + stateEnum()..getsEnum(state.identifier), + // Const(_stateValueLookup[state], width: stateWidth) + // .named(state.identifier.toString()), [ ...state.actions, Case( @@ -226,7 +229,7 @@ class FiniteStateMachine { } /// Simple class to initialize each state of the FSM. -class State { +class State { /// Identifier or name of the state. final StateIdentifier identifier; diff --git a/lib/src/module.dart b/lib/src/module.dart index a1cb8ec5c..e64b5d19d 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -67,6 +67,17 @@ abstract class Module { /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; + /// A mapping between [inputs], [outputs], and/or [inOuts] which must have the + /// same type as each other. The keys of the map will be updated to match the + /// type of the values. + /// + /// This is used for type checking for [LogicEnum]s through [Conditional]s. + /// + /// NOTE: This is for internal usage only, and the API will not be guaranteed + /// to be stable. + @internal + final Map portTypePairs = {}; + /// The parent [Module] of this [Module]. /// /// This only gets populated after its parent [Module], if it exists, has diff --git a/lib/src/modules/conditionals/always.dart b/lib/src/modules/conditionals/always.dart index 7c50a9eb6..907b54085 100644 --- a/lib/src/modules/conditionals/always.dart +++ b/lib/src/modules/conditionals/always.dart @@ -141,6 +141,11 @@ abstract class Always extends Module with SystemVerilog { parentConditional: null, parentAlways: this, ); + + portTypePairs.addAll(conditional.portTypePairs.map((k, v) => MapEntry( + conditional.registeredPort(k), + conditional.registeredPort(v), + ))); } } @@ -181,6 +186,9 @@ abstract class Always extends Module with SystemVerilog { final outputs = Map.fromEntries(ports.entries .where((element) => this.outputs.containsKey(element.key))); + assert(ports.length == inputs.length + outputs.length, + 'All ports of an always should be inputs or outputs'); + var verilog = ''; verilog += '// $instanceName\n'; verilog += '${alwaysVerilogStatement(inputs)} begin\n'; diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index 61b90fff3..c02ea179d 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -40,10 +40,30 @@ Logic cases(Logic expression, Map conditions, {int? width, ConditionalType conditionalType = ConditionalType.none, dynamic defaultValue}) { - for (final conditionValue in [ + final resultValues = [ ...conditions.values, if (defaultValue != null) defaultValue - ]) { + ]; + LogicEnum? enumResult; + if (resultValues.any((value) => value is Enum)) { + if (expression is! LogicEnum || + !resultValues.every((value) => + value is Enum && expression.mapping.containsKey(value))) { + throw ArgumentError.value( + resultValues, + 'conditions', + 'Enum results must all belong to the expression enum mapping.', + ); + } + enumResult = expression.clone(); + if (width != null && width != enumResult.width) { + throw SignalWidthMismatchException.forDynamic( + enumResult, width, enumResult.width); + } + width = enumResult.width; + } + + for (final conditionValue in resultValues) { int? inferredWidth; if (conditionValue is Logic) { @@ -64,6 +84,24 @@ Logic cases(Logic expression, Map conditions, throw SignalWidthMismatchException.forNull(conditions); } + Logic conditionLogic(dynamic condition) { + if (expression is LogicEnum && condition is Enum) { + if (!expression.mapping.containsKey(condition)) { + throw ArgumentError.value( + condition, + 'conditions', + 'Not present in the mapping for ${expression.runtimeType}.', + ); + } + + return expression.clone()..gets(Const(expression.mapping[condition])); + } else if (condition is Logic) { + return condition; + } else { + return Const(condition, width: expression.width); + } + } + for (final condition in conditions.entries) { if (condition.key is Logic) { if (expression.width != (condition.key as Logic).width) { @@ -80,18 +118,15 @@ Logic cases(Logic expression, Map conditions, } } - final result = Logic(name: 'result', width: width, naming: Naming.mergeable); + final result = enumResult ?? + Logic(name: 'result', width: width, naming: Naming.mergeable); 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]) + CaseItem(conditionLogic(condition.key), [result < condition.value]) ], conditionalType: conditionalType, defaultItem: defaultValue != null ? [result < defaultValue] : null) @@ -125,6 +160,15 @@ class Case extends Conditional { /// See [ConditionalType] for more details. final ConditionalType conditionalType; + @override + Map get portTypePairs => { + ...super.portTypePairs, + ..._itemTypePortPairs, + }; + + /// Case-item values whose generated ports must match [expression]'s type. + final Map _itemTypePortPairs = {}; + /// Whenever an item in [items] matches [expression], it will be executed. /// /// If none of [items] match, then [defaultItem] is executed. @@ -136,6 +180,8 @@ class Case extends Conditional { if (item.value.width != expression.width) { throw PortWidthMismatchException.equalWidth(expression, item.value); } + + _itemTypePortPairs[item.value] = expression; } } diff --git a/lib/src/modules/conditionals/conditional.dart b/lib/src/modules/conditionals/conditional.dart index 2a52a6b21..7020c729e 100644 --- a/lib/src/modules/conditionals/conditional.dart +++ b/lib/src/modules/conditionals/conditional.dart @@ -119,6 +119,18 @@ abstract class Conditional { Logic receiverOutput(Logic receiver) => _assignedReceiverToOutputMap[receiver]!; + /// Gets the port registered for [driverOrReceiver] by the enclosing block. + @internal + Logic registeredPort(Logic driverOrReceiver) { + final port = _assignedDriverToInputMap[driverOrReceiver] ?? + _assignedReceiverToOutputMap[driverOrReceiver]; + if (port == null) { + throw StateError( + 'Logic $driverOrReceiver is not registered in this Conditional.'); + } + return port; + } + /// Executes the functionality of this [Conditional] and /// populates [drivenSignals] with all [Logic]s that were driven /// during execution. @@ -168,6 +180,15 @@ abstract class Conditional { /// Does *not* recursively call down through sub-[Conditional]s. List get conditionals; + /// A mapping between [receivers] and [drivers] to be fed up to the enclosing + /// [Combinational] or [Sequential]'s [Module.portTypePairs]. + /// + /// NOTE: This is for internal usage only, and the API will not be guaranteed + /// to be stable. + @internal + Map get portTypePairs => + {for (final cond in conditionals) ...cond.portTypePairs}; + /// Returns a [String] of SystemVerilog to be used in generated output. /// /// The [indent] is used for pretty-printing, and should generally be diff --git a/lib/src/modules/conditionals/conditional_assign.dart b/lib/src/modules/conditionals/conditional_assign.dart index 9c77787c8..c1a0afc06 100644 --- a/lib/src/modules/conditionals/conditional_assign.dart +++ b/lib/src/modules/conditionals/conditional_assign.dart @@ -13,15 +13,18 @@ import 'package:rohd/src/modules/conditionals/ssa.dart'; /// An assignment that only happens under certain conditions. /// -/// [Logic] has a short-hand for creating [ConditionalAssign] via the -/// `<` operator. +/// [Logic] has a short-hand for creating [ConditionalAssign] via the `<` +/// operator. class ConditionalAssign extends Conditional { - /// The input to this assignment. + /// The receiver for this assignment. final Logic receiver; - /// The output of this assignment. + /// The driver for this assignment. final Logic driver; + @override + Map get portTypePairs => {driver: receiver}; + /// Conditionally assigns [receiver] to the value of [driver]. ConditionalAssign(this.receiver, this.driver) { if (driver.width != receiver.width) { diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index aca0e5b7c..0c693a309 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -389,7 +389,8 @@ class Logic { /// Handles the actual connection of this [Logic] to be driven by [other]. void _connect(Logic other) { - _unassignable = true; + makeUnassignable(reason: '$this is connected to $other.'); + if (other is LogicNet) { put(other.value); other.glitch.listen((args) { @@ -708,9 +709,15 @@ class Logic { /// [Conditional]. Conditional operator <(dynamic other) { if (_unassignable) { - throw Exception('This signal "$this" has been marked as unassignable. ' - 'It may be a constant expression or otherwise' - ' should not be assigned.'); + throw UnassignableException(this, reason: _unassignableReason); + } + + if (other is Enum) { + throw ArgumentError.value( + other, + 'other', + 'Enum values require a LogicEnum receiver with an explicit mapping.', + ); } if (other is Logic) { diff --git a/lib/src/signals/logic_def.dart b/lib/src/signals/logic_def.dart new file mode 100644 index 000000000..90353c48a --- /dev/null +++ b/lib/src/signals/logic_def.dart @@ -0,0 +1,29 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_def.dart +// Definition for LogicDef. +// +// 2026 July 22 +// Author: Max Korbel + +part of 'signals.dart'; + +@internal +sealed class LogicDef extends Logic { + final bool reserveDefinitionName; + + String get definitionName => _definitionName; + final String _definitionName; + + LogicDef({ + required String definitionName, + super.width, + super.name, + super.naming, + this.reserveDefinitionName = false, + }) : _definitionName = Sanitizer.sanitizeSV(Naming.validatedName( + definitionName, + reserveName: reserveDefinitionName, + )!); +} diff --git a/lib/src/signals/logic_enum.dart b/lib/src/signals/logic_enum.dart new file mode 100644 index 000000000..63f091855 --- /dev/null +++ b/lib/src/signals/logic_enum.dart @@ -0,0 +1,313 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_enum.dart +// Definition for LogicEnum. +// +// 2026 July 22 +// Author: Max Korbel + +part of 'signals.dart'; + +/// A hardware signal constrained to values from a Dart enum [T]. +/// +/// Each enum value has a unique bit-vector encoding in [mapping]. Values not +/// present in that mapping become `x` when observed in simulation. +class LogicEnum extends LogicDef { + /// The hardware encoding for each supported enum value. + late final Map mapping; + + /// The enum value represented by the current signal [value]. + /// + /// Throws a [StateError] when the current value is invalid or unmapped. + T get valueEnum => mapping.entries + .firstWhere((entry) => entry.value == value, + orElse: () => throw StateError( + 'Value $value does not correspond to any enum in $mapping')) + .key; + + static Map _computeMapping( + {required Map mapping, required int width}) { + final computedMapping = mapping + .map((key, value) => MapEntry(key, LogicValue.of(value, width: width))); + + if (computedMapping.values.any((v) => !v.isValid)) { + throw ArgumentError('Mapping values must be valid LogicValues,' + ' but found: $computedMapping'); + } + + // check that any `int` or `BigInt` mappings actually ended up matching + for (final MapEntry(key: key, value: computedValue) + in computedMapping.entries) { + final originalValue = mapping[key]; + if (originalValue is int || originalValue is BigInt) { + final originalBigInt = originalValue is int + ? BigInt.from(originalValue) + : originalValue as BigInt; + if (computedValue.toBigInt() != originalBigInt) { + throw ArgumentError( + 'Mapping value for $key cannot be represented at width $width.' + ' Computed: $computedValue, Original: $originalValue'); + } + } + } + + if (computedMapping.values.toSet().length != + computedMapping.values.length) { + throw ArgumentError('Mapping values must be unique,' + ' but found duplicates: $computedMapping'); + } + + return computedMapping; + } + + static int _computeWidth( + {int? requestedWidth, Map? mapping}) { + var width = 1; + + if (mapping != null) { + if (mapping.isEmpty) { + throw ArgumentError.value(mapping, 'mapping', 'Must not be empty.'); + } + + if (mapping.length > 1) { + width = LogicValue.ofInt(mapping.length, 32).clog2().toInt(); + } + + if (mapping.values.toSet().length != mapping.values.length) { + throw ArgumentError( + 'Mapping values must be unique, but found duplicates: $mapping'); + } + + for (final value in mapping.values.whereType()) { + if (value < 0) { + throw ArgumentError.value( + value, 'mapping', 'Negative encodings are not supported.'); + } + width = max(width, max(1, value.bitLength)); + } + + for (final value in mapping.values.whereType()) { + if (value.isNegative) { + throw ArgumentError.value( + value, 'mapping', 'Negative encodings are not supported.'); + } + width = max(width, max(1, value.bitLength)); + } + + for (final value in [ + ...mapping.values.whereType(), + ...mapping.values.whereType().map(LogicValue.ofString), + ...mapping.values + .whereType>() + .map(LogicValue.ofIterable) + ]) { + if (value.width > width) { + width = value.width; + } + } + } + + if (requestedWidth != null) { + if (requestedWidth < width) { + throw ArgumentError( + 'Requested width $requestedWidth is less than the minimum' + ' required width $width.'); + } + width = requestedWidth; + } + + return width; + } + + /// Creates a signal with sequential encodings matching [values] order. + LogicEnum(List values, + {int? width, + String? name, + Naming? naming, + String? definitionName, + bool reserveDefinitionName = false}) + : this.withMapping( + Map.fromEntries( + values.mapIndexed((index, value) => MapEntry(value, index))), + width: width, + name: name, + naming: naming, + definitionName: definitionName, + reserveDefinitionName: reserveDefinitionName); + + /// Creates a signal using the explicit hardware encodings in [mapping]. + /// + /// The width is inferred from the member count and encoding values unless + /// [width] is provided. If [reserveDefinitionName] is `true`, generated type + /// and member names cannot be uniquified around collisions. + LogicEnum.withMapping( + Map mapping, { + int? width, + super.name, + super.naming, + String? definitionName, + super.reserveDefinitionName, + }) : super( + width: _computeWidth(requestedWidth: width, mapping: mapping), + definitionName: definitionName ?? T.toString()) { + this.mapping = + Map.unmodifiable(_computeMapping(mapping: mapping, width: this.width)); + + _wire._constrainValue((value) { + if (value.isFloating) { + return LogicValue.filled(this.width, LogicValue.z); + } + if (!value.isValid) { + return LogicValue.filled(this.width, LogicValue.x); + } + if (!this.mapping.containsValue(value)) { + return LogicValue.filled(this.width, LogicValue.x); + } + return value; + }); + } + + /// Drives this [LogicEnum] with a constant value matching the enum [value]. + void getsEnum(T value) { + if (!mapping.containsKey(value)) { + throw ArgumentError.value( + value, 'value', 'Not present in the mapping for $T.'); + } + gets(Const(mapping[value])); + } + + /// Connects this signal to a compatible enum, legal constant, or raw logic. + @override + void gets(Logic other) { + if (other is LogicEnum && !_canAcceptValuesFrom(other)) { + throw ArgumentError.value( + other, 'other', 'Enum values must be representable in this mapping.'); + } + + if (other is Const) { + if (!mapping.containsValue(other.value)) { + throw ArgumentError.value( + other.value, 'other', 'Not present in the mapping for $T.'); + } + } + + super.gets(other); + } + + /// Creates a conditional assignment from an enum, legal constant, or signal. + @override + Conditional operator <(dynamic other) { + if (_unassignable) { + throw UnassignableException(this, reason: _unassignableReason); + } + + if (other is T) { + return super < (clone()..getsEnum(other)); + } else if (other is LogicEnum) { + if (!_canAcceptValuesFrom(other)) { + throw ArgumentError.value(other, 'other', + 'Enum values must be representable in this mapping.'); + } + if (!isEquivalentTypeTo(other)) { + // here we build a bridge to convert the other enum to a raw logic + // signal that this enum can accept + final rawBridge = Logic( + name: '${other.name}_raw', + width: width, + naming: Naming.renameable, + )..gets(other); + return super < (clone()..gets(rawBridge)); + } + return super < other; + } else if (other is Logic) { + return super < other; + } else if (other is Enum) { + throw ArgumentError.value(other, 'other', 'Must be a value of $T.'); + } else { + final constant = Const(other, width: width); + if (!mapping.containsValue(constant.value)) { + throw ArgumentError.value( + other, 'other', 'Not present in the mapping for $T.'); + } + return super < constant; + } + } + + /// Injects either a [T] value or a standard logic value into this signal. + @override + void inject(dynamic val, {bool fill = false}) { + if (val is T) { + if (fill) { + throw ArgumentError.value( + fill, 'fill', 'Enum values cannot be used as a fill pattern.'); + } + if (!mapping.containsKey(val)) { + throw ArgumentError.value(val, 'val', 'Not present in the mapping.'); + } + super.inject(mapping[val]); + } else { + super.inject(val, fill: fill); + } + } + + /// Updates the signal value, accepting either [T] or standard logic values. + @override + void put(dynamic val, {bool fill = false}) { + if (val is T) { + if (fill) { + throw ArgumentError.value( + fill, 'fill', 'Enum values cannot be used as a fill pattern.'); + } + + if (!mapping.containsKey(val)) { + throw ArgumentError.value(val, 'val', 'Not present in the mapping.'); + } + + // ignore: unnecessary_null_checks + super.put(mapping[val]!); + } else { + super.put(val, fill: fill); + } + } + + /// Whether [other] has the same enum type and hardware encoding. + bool isEquivalentTypeTo(Logic other) { + if (other is! LogicEnum) { + return false; + } + + final mappingsEqual = const MapEquality().equals( + mapping, + other.mapping, + ); + + if (!mappingsEqual) { + return false; + } + + return true; + } + + /// Whether every enum value from [other] is representable by this signal. + bool _canAcceptValuesFrom(LogicEnum other) => + other is LogicEnum && + width == other.width && + other.mapping.entries.every((entry) => mapping[entry.key] == entry.value); + + /// Creates another enum signal with the same mapping and definition policy. + @override + LogicEnum clone({String? name}) => LogicEnum.withMapping( + mapping, + width: width, + name: name ?? this.name, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: naming, + newNaming: null, + ), + definitionName: definitionName, + reserveDefinitionName: reserveDefinitionName, + ); +} diff --git a/lib/src/signals/signals.dart b/lib/src/signals/signals.dart index 348487a72..8971c04c0 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -23,3 +23,5 @@ part 'wire_net.dart'; part 'logic_structure.dart'; part 'logic_array.dart'; part 'logic_net.dart'; +part 'logic_enum.dart'; +part 'logic_def.dart'; diff --git a/lib/src/signals/wire.dart b/lib/src/signals/wire.dart index 812b09866..8d48cdd39 100644 --- a/lib/src/signals/wire.dart +++ b/lib/src/signals/wire.dart @@ -183,6 +183,7 @@ class _Wire { _glitchController.emitter.adopt(other._glitchController.emitter); other._migrateChangedTriggers(this); + _valueConstraints.addAll(other._valueConstraints); // ignore: avoid_returning_this return this; @@ -286,9 +287,21 @@ class _Wire { newValue = LogicValue.filled(width, LogicValue.x); } + for (final constraint in _valueConstraints) { + newValue = constraint(newValue); + } + _updateValue(newValue, signalName: signalName); } + /// Value transformations applied in registration order before an update. + final List<_LogicValueConstraint> _valueConstraints = []; + + /// Adds a transformation that constrains every value written to this wire. + void _constrainValue(_LogicValueConstraint constraint) { + _valueConstraints.add(constraint); + } + /// Updates the value of this signal to [newValue]. void _updateValue(LogicValue newValue, {required String signalName}) { final prevValue = value; @@ -306,3 +319,6 @@ class _Wire { @override String toString() => 'wire $hashCode'; } + +/// Transforms a proposed wire value into the value that may be stored. +typedef _LogicValueConstraint = LogicValue Function(LogicValue origValue); diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 18ff4caed..bf55232df 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -14,7 +14,13 @@ import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// A special [SynthModuleDefinition] for SystemVerilog modules. class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { /// Creates a new [SystemVerilogSynthModuleDefinition] for the given [module]. - SystemVerilogSynthModuleDefinition(super.module); + SystemVerilogSynthModuleDefinition(super.module, {super.generateEnums}) + : assert( + !(module is SystemVerilog && + module.generatedDefinitionType == + DefinitionGenerationType.none), + 'Do not build a definition for a module' + ' which generates no definition!'); /// A shared mapping from [SynthLogic]s which are the result of an inlineable /// submodule to the instantiation that produces them. @@ -34,6 +40,94 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { _replaceNetConnections(); _collapseMarkedChainableModules(); _replaceInOutConnectionInlineableModules(); + _lowerEnumPorts(); + } + + /// Lowers enum ports to packed boundaries backed by internal enum signals. + void _lowerEnumPorts() { + if (!generateEnums) { + return; + } + + final backingSignals = Map.identity(); + for (final signal in [...inputs, ...outputs]) { + final port = + module.tryInput(signal.name) ?? module.tryOutput(signal.name); + if (port is! LogicEnum || !signal.isEnum) { + continue; + } + + final initialName = '${signal.name}_enum'; + final backingSignal = SynthLogic( + port.clone(name: initialName), + parentSynthModuleDefinition: this, + namingOverride: Naming.renameable, + ) + ..enumDefinition = signal.enumDefinition + ..pickGeneratedName( + ('systemVerilogEnumPortBacking', port), + initialName: initialName, + ); + internalSignals.add(backingSignal); + backingSignals[signal.resolved] = backingSignal; + } + + if (backingSignals.isEmpty) { + return; + } + + final rewrittenAssignments = assignments + .map((assignment) => + _replaceAssignmentSignals(assignment, backingSignals)) + .toList(growable: false); + assignments + ..clear() + ..addAll([ + for (final input in inputs) + if (backingSignals[input.resolved] case final backing?) + SynthAssignment(input, backing), + for (final output in outputs) + if (backingSignals[output.resolved] case final backing?) + SynthAssignment(backing, output), + ...rewrittenAssignments, + ]); + + for (final instantiation in subModuleInstantiations) { + (instantiation as SystemVerilogSynthSubModuleInstantiation) + .replaceMappedSignals(backingSignals); + } + } + + SynthAssignment _replaceAssignmentSignals( + SynthAssignment assignment, + Map replacements, + ) { + final source = replacements[assignment.src.resolved] ?? assignment.src; + final destination = replacements[assignment.dst.resolved] ?? assignment.dst; + if (identical(source, assignment.src) && + identical(destination, assignment.dst)) { + return assignment; + } + + if (assignment is RangeSynthAssignment) { + return RangeSynthAssignment( + source, + destination, + srcUpperIndex: assignment.srcUpperIndex, + srcLowerIndex: assignment.srcLowerIndex, + dstUpperIndex: assignment.dstUpperIndex, + dstLowerIndex: assignment.dstLowerIndex, + ); + } + if (assignment is PartialSynthAssignment) { + return PartialSynthAssignment( + source, + destination, + dstUpperIndex: assignment.dstUpperIndex, + dstLowerIndex: assignment.dstLowerIndex, + ); + } + return SynthAssignment(source, destination); } /// Inlines a fully covered packed bus into its sole submodule input. diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart index eb1a0cd45..7870692f6 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart @@ -41,6 +41,39 @@ class SystemVerilogSynthSubModuleInstantiation // if cleared, then empty port (synthLogic.declarationCleared ? '' : synthLogic.name))); + /// Replaces references to signals that were lowered after normal synthesis. + void replaceMappedSignals(Map replacements) { + SynthLogic replacementFor(SynthLogic signal) => + replacements[signal.resolved] ?? signal; + + for (final entry in inputMapping.entries.toList()) { + final replacement = replacementFor(entry.value); + if (!identical(replacement, entry.value)) { + setInputMapping(entry.key, replacement, replace: true); + } + } + for (final entry in outputMapping.entries.toList()) { + final replacement = replacementFor(entry.value); + if (!identical(replacement, entry.value)) { + setOutputMapping(entry.key, replacement, replace: true); + } + } + for (final entry in inOutMapping.entries.toList()) { + final replacement = replacementFor(entry.value); + if (!identical(replacement, entry.value)) { + setInOutMapping(entry.key, replacement, replace: true); + } + } + + final inlineableMap = synthLogicToInlineableSynthSubmoduleMap; + if (inlineableMap != null) { + synthLogicToInlineableSynthSubmoduleMap = { + for (final entry in inlineableMap.entries) + replacementFor(entry.key): entry.value, + }; + } + } + /// Provides the inline SV representation for this module. /// /// Should only be called if [module] is [InlineSystemVerilog]. @@ -64,18 +97,32 @@ class SystemVerilogSynthSubModuleInstantiation } /// Provides the full SV instantiation for this module. - String? instantiationVerilog(String instanceType) { + String? instantiationVerilog( + String instanceType, { + required bool generateEnums, + }) { if (!needsInstantiation) { return null; } + + final ports = _modulePortsMapWithInline({ + ...inputMapping, + ...outputMapping, + ...inOutMapping, + }); + if (generateEnums && + module is InlineSystemVerilog && + (inlineResultLogic?.isEnum ?? false)) { + final inlineModule = module as InlineSystemVerilog; + final result = ports[inlineModule.resultSignalName]; + final enumType = inlineResultLogic!.enumDefinition!.definitionName; + return "assign $result = $enumType'${inlineVerilog()}; // $name"; + } + return SystemVerilogSynthesizer.instantiationVerilogFor( module: module, instanceType: instanceType, instanceName: name, - ports: _modulePortsMapWithInline({ - ...inputMapping, - ...outputMapping, - ...inOutMapping, - })); + ports: ports); } } diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart index 4ec0c540e..199f1ac71 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesis_result.dart @@ -11,12 +11,28 @@ import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; /// Extra utilities on [SynthLogic] to help with SystemVerilog synthesis. extension on SynthLogic { /// Gets the SystemVerilog type for this signal. - String definitionType() => isNet ? 'wire' : 'logic'; + String definitionType({required bool useEnumType}) => isEnum && useEnumType + ? enumDefinition!.definitionName + : isNet + ? 'wire' + : 'logic'; +} + +extension on SynthEnumDefinition { + String toSystemVerilogTypedef() { + final enumName = definitionName; + final enumType = 'logic [${characteristicEnum.width - 1}:0]'; + final enumValues = enumToNameMapping.entries + .map((e) => '${e.value} = ${characteristicEnum.mapping[e.key]}') + .join(', '); + return 'typedef enum $enumType { $enumValues } $enumName;'; + } } /// A [SynthesisResult] representing a [Module] that provides a custom @@ -82,7 +98,10 @@ class SystemVerilogSynthesisResult extends SynthesisResult { super.module, super.getInstanceTypeOfModule, { this.configuration = const SystemVerilogSynthesizerConfiguration(), - }) : _synthModuleDefinition = SystemVerilogSynthModuleDefinition(module) { + }) : _synthModuleDefinition = SystemVerilogSynthModuleDefinition( + module, + generateEnums: configuration.generateEnums, + ) { _portsString = _verilogPorts(); _moduleContentsString = _verilogModuleContents(getInstanceTypeOfModule); _parameterString = _verilogParameters(module); @@ -142,7 +161,7 @@ class SystemVerilogSynthesisResult extends SynthesisResult { direction, if (portType.objectType == SystemVerilogPortType.explicit) objectType, if (portType.dataType == SystemVerilogPortType.explicit) 'logic', - sig.definitionName(), + sig.definitionName(useEnumType: false), ].join(' '); /// Representation of all internal net declarations in generated SV. @@ -151,7 +170,10 @@ class SystemVerilogSynthesisResult extends SynthesisResult { for (final sig in _synthModuleDefinition.internalSignals .where((e) => e.needsDeclaration) .sorted((a, b) => a.name.compareTo(b.name))) { - declarations.add('${sig.definitionType()} ${sig.definitionName()};'); + declarations.add( + '${sig.definitionType(useEnumType: configuration.generateEnums)} ' + '${sig.definitionName(useEnumType: configuration.generateEnums)};', + ); } return declarations.join('\n'); } @@ -172,24 +194,52 @@ class SystemVerilogSynthesisResult extends SynthesisResult { var dstSliceString = ''; var srcSliceString = ''; + final assignsWholeDestination = assignment is! PartialSynthAssignment || + (assignment.dstLowerIndex == 0 && + assignment.dstUpperIndex == assignment.dst.width - 1); + final normalizesWholeEnumDestination = + assignment.dst.isEnum && assignsWholeDestination; if (assignment is RangeSynthAssignment) { - dstSliceString = rangeString( - assignment.dstUpperIndex, - assignment.dstLowerIndex, - ); + if (!normalizesWholeEnumDestination) { + dstSliceString = rangeString( + assignment.dstUpperIndex, + assignment.dstLowerIndex, + ); + } srcSliceString = rangeString( assignment.srcUpperIndex, assignment.srcLowerIndex, ); - } else if (assignment is PartialSynthAssignment && assignment.width > 1) { + } else if (assignment is PartialSynthAssignment && + assignment.width > 1 && + !normalizesWholeEnumDestination) { dstSliceString = rangeString( assignment.dstUpperIndex, assignment.dstLowerIndex, ); } + var sourceExpression = '${assignment.src.name}$srcSliceString'; + + final sourceIsPackedEnumInput = assignment.src.isEnum && + _synthModuleDefinition.inputs.contains(assignment.src.resolved); + + // Handle enum type casting for assignments where necessary. + if (configuration.generateEnums && + normalizesWholeEnumDestination && + (sourceIsPackedEnumInput || + !assignment.src.isEnum || + assignment is RangeSynthAssignment || + !identical( + assignment.src.enumDefinition, + assignment.dst.enumDefinition, + ))) { + final enumType = assignment.dst.enumDefinition!.definitionName; + sourceExpression = "$enumType'($sourceExpression)"; + } + assignmentLines.add('assign ${assignment.dst.name}$dstSliceString' - ' = ${assignment.src.name}$srcSliceString;'); + ' = $sourceExpression;'); } return assignmentLines.join('\n'); } @@ -205,8 +255,10 @@ class SystemVerilogSynthesisResult extends SynthesisResult { subModuleInstantiation as SystemVerilogSynthSubModuleInstantiation; - final instantiationVerilog = - subModuleInstantiation.instantiationVerilog(instanceType); + final instantiationVerilog = subModuleInstantiation.instantiationVerilog( + instanceType, + generateEnums: configuration.generateEnums, + ); if (instantiationVerilog != null) { subModuleLines.add(instantiationVerilog); } @@ -214,11 +266,20 @@ class SystemVerilogSynthesisResult extends SynthesisResult { return subModuleLines.join('\n'); } + /// Internal `typedef` definitions for this module. + String _verilogTypedefs() => + configuration.generateEnums ? _enumTypeDefs() : ''; + + String _enumTypeDefs() => _synthModuleDefinition.enumDefinitions + .map((e) => e.toSystemVerilogTypedef()) + .join('\n'); + /// The contents of this module converted to SystemVerilog without module /// declaration, ports, etc. String _verilogModuleContents( String Function(Module module) getInstanceTypeOfModule) => [ + _verilogTypedefs(), _verilogInternalSignals(), _verilogAssignments(), // order matters! _verilogSubModuleInstantiations(getInstanceTypeOfModule), diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart index 876f20298..717f9f30e 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer_configuration.dart @@ -34,6 +34,9 @@ class SystemVerilogPortTypeConfiguration { /// Configuration for SystemVerilog synthesis. class SystemVerilogSynthesizerConfiguration { + /// Whether SystemVerilog enum types and symbolic values are generated. + final bool generateEnums; + /// Type configuration for input ports. final SystemVerilogPortTypeConfiguration inputPortType; @@ -45,6 +48,7 @@ class SystemVerilogSynthesizerConfiguration { /// Creates a new configuration for SystemVerilog synthesis. const SystemVerilogSynthesizerConfiguration({ + this.generateEnums = true, this.inputPortType = const SystemVerilogPortTypeConfiguration( objectType: SystemVerilogPortType.implicit, ), diff --git a/lib/src/synthesizers/utilities/synth_enum_definition.dart b/lib/src/synthesizers/utilities/synth_enum_definition.dart new file mode 100644 index 000000000..caeb3b112 --- /dev/null +++ b/lib/src/synthesizers/utilities/synth_enum_definition.dart @@ -0,0 +1,89 @@ +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/namer.dart'; + +/// Canonical synthesis metadata for an enum type in one module scope. +@immutable +@internal +class SynthEnumDefinition { + /// A representative signal carrying this enum's type information. + final LogicEnum characteristicEnum; + + /// The generated enum type name. + final String definitionName; + + /// Generated member names indexed by their Dart enum values. + final Map enumToNameMapping; + + /// Creates or reuses stable generated names through [namer]. + factory SynthEnumDefinition( + LogicEnum characteristicEnum, + Namer namer, + ) { + final definitionKey = SynthEnumDefinitionKey(characteristicEnum); + return SynthEnumDefinition._(characteristicEnum, namer, definitionKey); + } + + SynthEnumDefinition._( + this.characteristicEnum, + Namer namer, + SynthEnumDefinitionKey definitionKey, + ) : definitionName = namer.identifierNameOf( + definitionKey, + initialName: characteristicEnum.definitionName, + reserved: characteristicEnum.reserveDefinitionName, + ), + enumToNameMapping = Map.unmodifiable(characteristicEnum.mapping.map( + (enumValue, value) => MapEntry( + enumValue, + namer.identifierNameOf( + (definitionKey, enumValue), + initialName: enumValue.name, + reserved: characteristicEnum.reserveDefinitionName, + ), + ), + )); +} + +/// Equality key for enum definitions that may share one generated typedef. +/// +/// The enum values in [enumMapping] retain the Dart enum type as part of their +/// identity. An explicitly reserved definition name also participates in +/// equality, while non-reserved preferred names do not prevent type reuse. +@immutable +@internal +class SynthEnumDefinitionKey { + /// The enum members and their exact hardware encodings. + final Map enumMapping; + + /// The required type name, or `null` when the name may be uniquified. + final String? reservedName; + + /// Creates a key describing [characteristicEnum]'s generated type identity. + SynthEnumDefinitionKey(LogicEnum characteristicEnum) + : enumMapping = Map.unmodifiable(characteristicEnum.mapping), + reservedName = characteristicEnum.reserveDefinitionName + ? characteristicEnum.definitionName + : null; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + + return other is SynthEnumDefinitionKey && + const MapEquality() + .equals(other.enumMapping, enumMapping) && + other.reservedName == reservedName; + } + + @override + int get hashCode => + const MapEquality().hash(enumMapping) ^ + reservedName.hashCode; +} diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index d29cc84f3..154637869 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -10,6 +10,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; @@ -88,6 +89,22 @@ class SynthLogic { /// The [Logic] whose name is renameable, if there is one. Logic? _renameableLogic; + /// A [LogicEnum] that is characteristic of any merged [LogicEnum]s into this. + LogicEnum? get characteristicEnum => _characteristicEnum; + + /// The first [LogicEnum] merged into this [SynthLogic], if there is one. + LogicEnum? _characteristicEnum; + + SynthEnumDefinition? get enumDefinition => _enumDefinition; + set enumDefinition(SynthEnumDefinition? definition) { + assert(definition != null, 'Cannot set enum definition to null.'); + assert( + _enumDefinition == null, 'Cannot set enum definition more than once.'); + _enumDefinition = definition; + } + + SynthEnumDefinition? _enumDefinition; + /// [Logic]s that are marked mergeable. final Set _mergeableLogics = {}; @@ -110,6 +127,9 @@ class SynthLogic { // can just look at the first since nets and non-nets cannot be merged logics.first.isNet || (isArray && (logics.first as LogicArray).isNet); + /// Whether this represents an enum. + bool get isEnum => characteristicEnum != null; + /// If set, then this should never pick the constant as the name. bool get constNameDisallowed => _constNameDisallowed; bool _constNameDisallowed; @@ -232,16 +252,37 @@ class SynthLogic { _name = _findName(); } + /// Picks a stable name for a signal fabricated during synthesis. + void pickGeneratedName(Object key, {required String initialName}) { + assert(_name == null, 'Should only pick a name once.'); + + _name = parentSynthModuleDefinition.module.namer.identifierNameOf( + key, + initialName: initialName, + ); + } + /// Finds the best name from the collection of [Logic]s. /// /// Delegates to signal namer which handles constant value naming, priority /// selection, and uniquification via the module's shared namespace. - String _findName() => - parentSynthModuleDefinition.module.namer.signalNameOfBest( - logics, - constValue: _constLogic, - constNameDisallowed: _constNameDisallowed, - ); + String _findName() { + if (isConstant && + !_constNameDisallowed && + isEnum && + parentSynthModuleDefinition.generateEnums) { + return enumDefinition!.enumToNameMapping[characteristicEnum! + .mapping.entries + .firstWhere((entry) => entry.value == _constLogic!.value) + .key]!; + } + + return parentSynthModuleDefinition.module.namer.signalNameOfBest( + logics, + constValue: _constLogic, + constNameDisallowed: _constNameDisallowed, + ); + } /// Creates an instance to represent [initialLogic] and any that merge /// into it. @@ -261,18 +302,32 @@ class SynthLogic { SynthLogic a, SynthLogic b, ) { + assert(a != b, 'Cannot merge a SynthLogic with itself.'); + if (_constantsMergeable(a, b)) { // case to avoid things like a constant assigned to another constant a.adopt(b); return (removed: b, kept: a); } - if (!a.mergeable && !b.mergeable) { + if (a.isNet != b.isNet) { + // do not merge nets with non-nets return null; } - if (a.isNet != b.isNet) { - // do not merge nets with non-nets + if (a.isEnum && b.isEnum && !_enumTypesCompatible(a, b)) { + return null; + } + + if ((a.isEnum && b.isConstant) || (b.isEnum && a.isConstant)) { + if (!_enumAndConstMergeable(a, b)) { + return null; + } + a.adopt(b); + return (removed: b, kept: a); + } + + if (!a.mergeable && !b.mergeable) { return null; } @@ -293,13 +348,37 @@ class SynthLogic { !a._constNameDisallowed && !b._constNameDisallowed; + /// Indicates whether two enum representations have compatible types. + static bool _enumTypesCompatible(SynthLogic a, SynthLogic b) { + assert(a.isEnum && b.isEnum, 'Both signals must represent enums.'); + final aEnum = a.characteristicEnum!; + final bEnum = b.characteristicEnum!; + return aEnum.isEquivalentTypeTo(bEnum) && + !(aEnum.reserveDefinitionName && + bEnum.reserveDefinitionName && + aEnum.definitionName != bEnum.definitionName); + } + + /// Indicates whether [a] and [b] are an enum and a legal enum constant. + static bool _enumAndConstMergeable(SynthLogic a, SynthLogic b) { + final enumLogic = a.isEnum ? a : b; + final constantLogic = a.isConstant ? a : b; + return enumLogic.isEnum && + constantLogic.isConstant && + enumLogic.characteristicEnum!.mapping.values + .contains(constantLogic._constLogic!.value); + } + /// Merges [other] to be represented by `this` instead, and updates the /// [other] that it has been replaced. /// /// If [force] is `true`, then it will adopt even if both are non-mergeable. void adopt(SynthLogic other, {bool force = false}) { assert( - force || other.mergeable || _constantsMergeable(this, other), + force || + other.mergeable || + _constantsMergeable(this, other) || + _enumAndConstMergeable(this, other), 'Cannot merge a non-mergeable into this.', ); assert(other.isArray == isArray, 'Cannot merge arrays and non-arrays'); @@ -318,6 +397,18 @@ class SynthLogic { _constLogic ??= other._constLogic; _reservedLogic ??= other._reservedLogic; _renameableLogic ??= other._renameableLogic; + if (other._characteristicEnum?.reserveDefinitionName ?? false) { + assert( + _characteristicEnum == null || + !_characteristicEnum!.reserveDefinitionName || + _characteristicEnum!.definitionName == + other._characteristicEnum!.definitionName, + 'Cannot merge enums with conflicting reserved definition names.', + ); + _characteristicEnum = other._characteristicEnum; + } else { + _characteristicEnum ??= other._characteristicEnum; + } // the rest, take them all _mergeableLogics.addAll(other._mergeableLogics); @@ -344,6 +435,26 @@ class SynthLogic { _unnamedLogics.add(logic); } } + + if (logic is LogicEnum) { + assert(characteristicEnum?.isEquivalentTypeTo(logic) ?? true, + 'Cannot add a LogicEnum that is not equivalent to the existing one.'); + + if (logic.reserveDefinitionName) { + // if the added `logic` reserves its definition name, then we + // should use it as the characteristic enum + assert( + _characteristicEnum == null || + !_characteristicEnum!.reserveDefinitionName || + logic.definitionName == _characteristicEnum!.definitionName, + 'Cannot add a LogicEnum that reserves its definition name, but has a ' + 'different definition name than the existing characteristic enum.', + ); + _characteristicEnum = logic; + } + + _characteristicEnum ??= logic; + } } @override @@ -361,7 +472,11 @@ class SynthLogic { /// Computes the name of the signal at declaration time with appropriate /// dimensions included. - String definitionName() { + String definitionName({bool useEnumType = true}) { + if (isEnum && useEnumType) { + return name; + } + String packedDims; String unpackedDims; diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 5cd689882..3a9b9dc80 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -13,6 +13,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; @@ -122,6 +123,9 @@ class SynthModuleDefinition { /// The [Module] being defined. final Module module; + /// Whether generated identifiers may use enum types and symbolic values. + final bool generateEnums; + /// All the assignments that are part of this definition. final List assignments = []; @@ -388,7 +392,7 @@ class SynthModuleDefinition { } /// Creates a new definition representation for this [module]. - SynthModuleDefinition(this.module) + SynthModuleDefinition(this.module, {this.generateEnums = true}) : assert( !(module is SystemVerilog && module.generatedDefinitionType == @@ -620,6 +624,7 @@ class SynthModuleDefinition { _collapseConstantBackedRangeIntermediates(); _collapseAssignments(); _assignSubmodulePortMapping(); + _adjustTypePairs(); _pruneUnused(); _collapseConstantBackedRangeIntermediates(); @@ -1167,6 +1172,36 @@ class SynthModuleDefinition { } } + void _adjustTypePairs() { + for (final submoduleInstantiation + in moduleToSubModuleInstantiationMap.values) { + submoduleInstantiation.adjustTypePairs(); + } + } + + final Map _enumDefinitions = + {}; + + List get enumDefinitions => + _enumDefinitions.values.toList(growable: false); + + void _pickDefinitionEnumName(SynthLogic synthEnum) { + assert(synthEnum.isEnum, 'Only call this on SynthLogic that is an enum.'); + final key = SynthEnumDefinitionKey(synthEnum.characteristicEnum!); + if (_enumDefinitions.containsKey(key)) { + // already have a definition for this enum + synthEnum.enumDefinition = _enumDefinitions[key]; + } else { + // create a new definition for this enum + final newDefinition = SynthEnumDefinition( + synthEnum.characteristicEnum!, + module.namer, + ); + _enumDefinitions[key] = newDefinition; + synthEnum.enumDefinition = newDefinition; + } + } + /// Resolves a submodule input mapping through any replacement and, when the /// mapped signal is fully driven by a packed scalar assignment, through that /// driver as well. @@ -1410,6 +1445,13 @@ class SynthModuleDefinition { /// [Namer.instanceNameOf]. All non-constant names share a single namespace /// managed by the module's [Namer]. void _pickNames() { + ({ + ...inputs, + ...outputs, + ...inOuts, + ...internalSignals, + }).where((signal) => signal.isEnum).forEach(_pickDefinitionEnumName); + // Name allocation order matters -- earlier claims receive the unsuffixed // name when there are collisions. Weak-name claimants are intentionally // deferred so emitted objects receive 1st chance at the shortest basenames: diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 1eccf9da9..152ed9326 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -9,7 +9,9 @@ import 'dart:collection'; +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_enum_definition.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; import 'package:rohd/src/utilities/namer.dart'; @@ -110,6 +112,54 @@ class SynthSubModuleInstantiation { _inOutMapping[name] = synthLogic; } + /// Propagates enum type metadata across the module's paired ports. + @internal + void adjustTypePairs() { + SynthLogic mappedPort(Logic port) { + final mapping = inputMapping[port.name] ?? + outputMapping[port.name] ?? + inOutMapping[port.name]; + if (mapping == null) { + throw StateError('No synthesis mapping found for port ${port.name} on ' + '${module.name}.'); + } + return mapping; + } + + for (final MapEntry(key: toUpdate, value: reference) + in module.portTypePairs.entries) { + final toUpdateSynth = mappedPort(toUpdate); + final referenceSynth = mappedPort(reference); + + if (referenceSynth.isEnum) { + if (toUpdateSynth.isEnum && + SynthEnumDefinitionKey(toUpdateSynth.characteristicEnum!) == + SynthEnumDefinitionKey(referenceSynth.characteristicEnum!)) { + // If the types are equivalent, we can just use the original, no need + // to do any additional merging. + continue; + } + + final mergeResult = SynthLogic.tryMerge( + toUpdateSynth, + SynthLogic( + referenceSynth.characteristicEnum!.clone(name: 'reference'), + parentSynthModuleDefinition: + toUpdateSynth.parentSynthModuleDefinition, + ), + ); + if (mergeResult == null) { + throw StateError( + 'Cannot propagate enum type ${referenceSynth.characteristicEnum}' + ' from port ${reference.name} to ${toUpdate.name} on ' + '${module.name}.'); + } + assert(identical(mergeResult.kept, toUpdateSynth), + 'We should not be replacing the original one.'); + } + } + } + /// Indicates whether this module should be declared. bool get needsInstantiation => _needsInstantiation; bool _needsInstantiation = true; diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart index d5f20e783..cc4342ae5 100644 --- a/lib/src/utilities/namer.dart +++ b/lib/src/utilities/namer.dart @@ -38,6 +38,9 @@ class Namer { /// fresh suffixes for the same submodule instances. final Map _instanceNames = {}; + /// Cache of other generated identifiers, such as type and member names. + final Map _identifierNames = {}; + /// The set of port [Logic] objects, for O(1) port membership tests. final Set _portLogics; @@ -72,6 +75,25 @@ class Namer { @visibleForTesting bool isAvailable(String name) => _uniquifier.isAvailable(name); + /// Returns a stable, collision-free name for a generated identifier. + String identifierNameOf( + Object key, { + required String initialName, + bool reserved = false, + }) { + final cached = _identifierNames[key]; + if (cached != null) { + return cached; + } + + final name = _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(initialName), + reserved: reserved, + ); + _identifierNames[key] = name; + return name; + } + // ─── Instance naming (Module → String) ────────────────────────── /// Returns the canonical instance name for [submodule]. diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..32536b327 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -16,6 +16,8 @@ import 'package:test/test.dart'; enum MyStates { state1, state2, state3, state4 } +enum SingleState { idle } + const _tmpDir = 'tmp_test'; const _simpleFSMPath = '$_tmpDir/simple_fsm.md'; const _trafficFSMPath = '$_tmpDir/traffic_light_fsm.md'; @@ -108,12 +110,20 @@ enum LightColor { } class TrafficTestModule extends Module { + late final LogicEnum northLight; + late final LogicEnum eastLight; + TrafficTestModule(Logic traffic, Logic reset) { traffic = addInput('traffic', traffic, width: traffic.width); reset = addInput('reset', reset); - final northLight = addOutput('northLight', width: traffic.width); - final eastLight = addOutput('eastLight', width: traffic.width); + final lightType = LogicEnum( + LightColor.values, + width: traffic.width, + definitionName: 'LightColor', + ); + northLight = addTypedOutput('northLight', lightType.clone); + eastLight = addTypedOutput('eastLight', lightType.clone); final clk = SimpleClockGenerator(10).clk; @@ -121,14 +131,14 @@ class TrafficTestModule extends Module { State(LightStates.northFlowing, events: { TrafficPresence.isEastActive(traffic): LightStates.northSlowing, }, actions: [ - northLight < LightColor.green.value, + northLight < LightColor.green, ]), State( LightStates.northSlowing, events: {}, defaultNextState: LightStates.eastFlowing, actions: [ - northLight < LightColor.yellow.value, + northLight < LightColor.yellow, ], ), State( @@ -137,7 +147,7 @@ class TrafficTestModule extends Module { TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing, }, actions: [ - eastLight < LightColor.green.value, + eastLight < LightColor.green, ], ), State( @@ -145,7 +155,7 @@ class TrafficTestModule extends Module { events: {}, defaultNextState: LightStates.northFlowing, actions: [ - eastLight < LightColor.yellow.value, + eastLight < LightColor.yellow, ], ), ]; @@ -157,8 +167,8 @@ class TrafficTestModule extends Module { states, setupActions: [ // by default, lights should be red - northLight < LightColor.red.value, - eastLight < LightColor.red.value, + northLight < LightColor.red, + eastLight < LightColor.red, ], ); @@ -203,7 +213,7 @@ void main() { final sv = mod.generateSynth(); - expect(sv, contains('MyStates_state1 : begin')); + expect(sv, contains('state1 : begin')); }); test('state value lookup is correct', () async { @@ -256,6 +266,19 @@ void main() { 3); }); + test('single-state FSM uses a one-bit enum', () { + final fsm = FiniteStateMachine( + Logic(), + Logic(), + SingleState.idle, + [State(SingleState.idle, events: {}, actions: [])], + ); + + expect(fsm.stateWidth, 1); + expect(fsm.currentState.width, 1); + expect(fsm.nextState.width, 1); + }); + group('simcompare', () { test('simple fsm', () async { final mod = TestModule(Logic(), Logic(), Logic()); @@ -323,6 +346,11 @@ void main() { final mod = TrafficTestModule(Logic(width: 2), Logic()); await mod.build(); + expect(mod.northLight, isA>()); + expect(mod.eastLight, isA>()); + expect(mod.northLight.mapping.keys, LightColor.values); + expect(mod.eastLight.mapping, mod.northLight.mapping); + final vectors = [ Vector({'reset': 1, 'traffic': 00}, {}), Vector({ @@ -345,6 +373,24 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + final sv = mod.generateSynth(); + expect(sv, contains('} LightColor;')); + expect(sv, contains('LightColor northLight_enum;')); + expect(sv, contains('LightColor eastLight_enum;')); + expect(sv, contains('northLight_enum = green;')); + expect(sv, contains('eastLight_enum = yellow;')); + + const untypedConfiguration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = mod.generateSynth(configuration: untypedConfiguration); + expect(untypedSv, isNot(contains('typedef enum'))); + expect(untypedSv, isNot(contains('northLight_enum'))); + SimCompare.checkIverilogVector( + mod, + vectors, + synthesizerConfiguration: untypedConfiguration, + ); + verifyMermaidStateDiagram(_trafficFSMPath); }); }); diff --git a/test/logic_enum_test.dart b/test/logic_enum_test.dart new file mode 100644 index 000000000..df9df7aaa --- /dev/null +++ b/test/logic_enum_test.dart @@ -0,0 +1,1393 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_enum_test.dart +// Tests for LogicEnum. +// +// 2026 July 22 +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +enum TestEnum { a, b, c } + +enum SingleValueEnum { only } + +enum OtherEnum { a, b, c } + +class MyListLogicEnum extends LogicEnum { + MyListLogicEnum({super.name, super.naming}) : super(TestEnum.values); + + @override + MyListLogicEnum clone({String? name}) => MyListLogicEnum( + name: name ?? this.name, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: naming, + newNaming: null, + ), + ); +} + +class MyMapLogicEnum extends LogicEnum { + MyMapLogicEnum({super.name, super.naming}) + : super.withMapping({ + TestEnum.a: 1, + // TestEnum.b: 5, // `b` is not mapped! + TestEnum.c: 7, + }, width: 3); + + @override + MyMapLogicEnum clone({String? name}) => MyMapLogicEnum( + name: name ?? this.name, + naming: Naming.chooseCloneNaming( + originalName: this.name, + newName: name, + originalNaming: naming, + newNaming: null, + ), + ); +} + +class SimpleModWithEnum extends Module { + SimpleModWithEnum(Logic carrot) { + carrot = addInput('carrot', carrot, width: 3); + final e = MyMapLogicEnum(name: 'elephant'); + addOutput('banana', width: 3) <= carrot & e; + } +} + +class ConflictingEnumMod extends Module { + ConflictingEnumMod(Logic carrot) { + carrot = addInput('carrot', carrot, width: 3); + final e1 = MyListLogicEnum(name: 'elephantList'); + final e2 = MyMapLogicEnum(name: 'elephantMap'); + + addOutput('banana', width: 3) <= carrot & (e1.zeroExtend(3) ^ e2); + } +} + +class ModWithEnumConstAssignment extends Module { + ModWithEnumConstAssignment(Logic carrot) { + carrot = addInput('carrot', carrot, width: 2); + final e = MyListLogicEnum(name: 'elephant')..getsEnum(TestEnum.b); + addOutput('banana', width: 2) <= carrot & e; + } +} + +class ModWithCaseAndEnumCondAssign extends Module { + ModWithCaseAndEnumCondAssign(Logic durian) { + durian = addInput('durian', durian); + + final currState = MyListLogicEnum(name: 'currState'); + final nextState = MyListLogicEnum(name: 'nextState'); + + nextState <= + cases( + currState, + { + 0: 0, + MyListLogicEnum()..getsEnum(TestEnum.b): MyListLogicEnum() + ..getsEnum(TestEnum.b), + TestEnum.c: 2, + }, + width: 2); + + addOutput('pineapple') <= durian & nextState.xor(); + } +} + +class EnumNameCollisionModule extends Module { + EnumNameCollisionModule() { + final enumSignal = MyListLogicEnum(name: 'a'); + addOutput('TestEnum', width: enumSignal.width) <= enumSignal; + addOutput('a'); + } +} + +class EnumCasesModule extends Module { + late final Logic result; + + EnumCasesModule(Logic selector) { + selector = addInput('selector', selector, width: 2); + final enumSelector = MyListLogicEnum(name: 'enumSelector')..gets(selector); + result = cases(enumSelector, { + TestEnum.a: TestEnum.b, + TestEnum.b: TestEnum.c, + TestEnum.c: TestEnum.a, + }); + addOutput('result', width: 2) <= result; + } +} + +class EnumSubsetAssignmentModule extends Module { + EnumSubsetAssignmentModule(Logic selector) { + selector = addInput('selector', selector, width: 2); + final narrow = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.b: 1, + }, + width: 2, + name: 'narrow', + naming: Naming.reserved, + definitionName: 'NarrowEnum', + )..gets(selector); + final broad = LogicEnum( + TestEnum.values, + width: 2, + name: 'broad', + naming: Naming.reserved, + definitionName: 'BroadEnum', + )..gets(narrow); + + addOutput('result', width: 2) <= broad; + } +} + +class EnumSubsetConditionalAssignmentModule extends Module { + EnumSubsetConditionalAssignmentModule(Logic selector) { + selector = addInput('selector', selector, width: 2); + final narrow = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.b: 1, + }, + width: 2, + name: 'narrow', + naming: Naming.reserved, + definitionName: 'NarrowEnum', + )..gets(selector); + final broad = LogicEnum( + TestEnum.values, + width: 2, + name: 'broad', + naming: Naming.reserved, + definitionName: 'BroadEnum', + ); + + Combinational([broad < narrow]); + addOutput('result', width: 2) <= broad; + } +} + +class EnumFromSliceModule extends Module { + EnumFromSliceModule(Logic source) { + source = addInput('source', source, width: 8); + final slicedEnum = MyListLogicEnum( + name: 'slicedEnum', + naming: Naming.reserved, + )..gets(source.getRange(2, 4)); + + addOutput('result', width: slicedEnum.width) <= slicedEnum; + } +} + +class EnumFromAssignedBitsModule extends Module { + EnumFromAssignedBitsModule(Logic source) { + source = addInput('source', source, width: 2); + final state = MyListLogicEnum( + name: 'state', + naming: Naming.reserved, + ); + for (var index = 0; index < state.width; index++) { + state.assignSubset([source[index]], start: index); + } + + addOutput('result', width: state.width) <= state; + } +} + +class PartiallyAssignedEnumModule extends Module { + PartiallyAssignedEnumModule(Logic source) { + source = addInput('source', source); + final state = MyListLogicEnum( + name: 'state', + naming: Naming.reserved, + )..assignSubset([source]); + + addOutput('result', width: state.width) <= state; + } +} + +class SingleValueEnumModule extends Module { + SingleValueEnumModule() { + final state = LogicEnum( + SingleValueEnum.values, + name: 'state', + naming: Naming.reserved, + definitionName: 'SingleState', + )..getsEnum(SingleValueEnum.only); + + addOutput('result', width: state.width) <= state; + } +} + +class WideSparseEnumModule extends Module { + static final wideValue = BigInt.one << 80; + + WideSparseEnumModule() { + final state = LogicEnum.withMapping( + { + TestEnum.a: BigInt.zero, + TestEnum.c: wideValue, + }, + name: 'state', + naming: Naming.reserved, + definitionName: 'WideSparseState', + )..getsEnum(TestEnum.c); + + addOutput('result', width: state.width) <= state; + } +} + +class EnumPacket extends LogicStructure { + final MyListLogicEnum state; + final Logic payload; + + factory EnumPacket({String name = 'packet'}) => EnumPacket._( + MyListLogicEnum(name: 'state'), + Logic(width: 2, name: 'payload'), + name: name, + ); + + EnumPacket._(this.state, this.payload, {required super.name}) + : super([state, payload]); + + @override + EnumPacket clone({String? name}) => EnumPacket(name: name ?? this.name); +} + +class EnumStructureModule extends Module { + EnumStructureModule(Logic source) { + source = addInput('source', source, width: 4); + final packet = EnumPacket()..gets(source); + + addOutput('stateResult', width: packet.state.width) <= packet.state; + addOutput('packedResult', width: packet.width) <= packet.packed; + addOutput('rangeResult', width: packet.state.width) <= + packet.getRange(0, packet.state.width); + } +} + +class EnumArrayBoundaryModule extends Module { + EnumArrayBoundaryModule(Logic source) { + source = addInput('source', source, width: 4); + final lanes = LogicArray( + [2], + 2, + name: 'lanes', + numUnpackedDimensions: 1, + )..gets(source); + final state = MyListLogicEnum( + name: 'state', + naming: Naming.reserved, + )..gets(lanes.elements[1]); + + addOutput('stateResult', width: state.width) <= state; + addOutput('packedResult', width: lanes.width) <= lanes.packed; + } +} + +class EnumHierarchyChild extends Module { + Logic get result => output('result'); + + EnumHierarchyChild(Logic source) : super(name: 'enumHierarchyChild') { + source = addInput('source', source, width: 2); + final narrow = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.b: 1, + }, + width: 2, + name: 'narrow', + naming: Naming.reserved, + definitionName: 'ChildNarrowEnum', + )..gets(source); + + addOutput('result', width: narrow.width) <= narrow; + } +} + +class EnumHierarchyModule extends Module { + EnumHierarchyModule(Logic source) { + source = addInput('source', source, width: 2); + final child = EnumHierarchyChild(source); + final broad = LogicEnum( + TestEnum.values, + width: 2, + name: 'broad', + naming: Naming.reserved, + definitionName: 'ParentBroadEnum', + )..gets(child.result); + + addOutput('result', width: broad.width) <= broad; + } +} + +class TypedEnumPortsModule extends Module { + late final LogicEnum stateIn; + late final LogicEnum stateOut; + + TypedEnumPortsModule(LogicEnum source) + : super(name: 'typedEnumPorts') { + stateIn = addTypedInput('stateIn', source); + stateOut = addTypedOutput('stateOut', stateIn.clone)..gets(stateIn); + } +} + +class TypedEnumPartialSourceModule extends Module { + TypedEnumPartialSourceModule(LogicEnum source) { + final stateIn = addTypedInput('stateIn', source); + final packedValue = Logic( + width: 3, + name: 'packedValue', + naming: Naming.reserved, + ) + ..assignSubset([Const(0)]) + ..assignSubset([stateIn], start: 1) + ..assignSubset([Const(0)], start: 2); + + addOutput('result', width: packedValue.width) <= packedValue; + } +} + +class TypedEnumRangeDestinationModule extends Module { + TypedEnumRangeDestinationModule(Logic source, LogicEnum type) { + source = addInput('source', source, width: type.width); + final stateOut = addTypedOutput('stateOut', type.clone); + for (var index = 0; index < stateOut.width; index++) { + stateOut.assignSubset([source[index]], start: index); + } + } +} + +class TypedEnumHierarchyModule extends Module { + late final TypedEnumPortsModule child; + + TypedEnumHierarchyModule(Logic source) { + source = addInput('source', source, width: 2); + final narrow = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.c: 2, + }, + width: 2, + name: 'narrow', + naming: Naming.reserved, + definitionName: 'TypedChildNarrowEnum', + )..gets(source); + child = TypedEnumPortsModule(narrow); + final broad = LogicEnum( + TestEnum.values, + width: 2, + name: 'broad', + naming: Naming.reserved, + definitionName: 'ParentTypedBroadEnum', + )..gets(child.stateOut); + + addOutput('result', width: broad.width) <= broad; + } +} + +class TypedEnumCasesModule extends Module { + late final LogicEnum stateIn; + late final LogicEnum stateOut; + + TypedEnumCasesModule(LogicEnum source) + : super(name: 'typedEnumCases') { + stateIn = addTypedInput('stateIn', source); + final selected = cases(stateIn, { + TestEnum.a: TestEnum.c, + TestEnum.c: TestEnum.a, + }); + stateOut = addTypedOutput('stateOut', stateIn.clone)..gets(selected); + } +} + +class TypedEnumPortNameCollisionModule extends Module { + TypedEnumPortNameCollisionModule(LogicEnum source) { + final stateIn = addTypedInput('stateIn', source); + final stateInEnum = Logic( + width: stateIn.width, + name: 'stateIn_enum', + naming: Naming.reserved, + )..gets(stateIn); + + addOutput('result', width: stateIn.width) <= stateInEnum; + } +} + +class TypedEnumConsumersModule extends Module { + late final TypedEnumPortsModule child; + + TypedEnumConsumersModule(LogicEnum source) { + final stateIn = addTypedInput('stateIn', source); + child = TypedEnumPortsModule(stateIn); + + addOutput('inlineResult', width: stateIn.width) <= + stateIn ^ Const(1, width: stateIn.width); + addOutput('childResult', width: stateIn.width) <= child.stateOut; + } +} + +class EnumIfElseModule extends Module { + EnumIfElseModule(Logic source, Logic select) { + source = addInput('source', source, width: 2); + select = addInput('select', select); + final narrow = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.b: 1, + }, + width: 2, + name: 'narrow', + definitionName: 'IfNarrowEnum', + )..gets(source); + final broad = LogicEnum( + TestEnum.values, + width: 2, + name: 'broad', + definitionName: 'IfBroadEnum', + ); + + Combinational([ + If(select, then: [broad < narrow], orElse: [broad < TestEnum.c]) + ]); + addOutput('result', width: broad.width) <= broad; + } +} + +class EmptyModule extends Module {} + +Future checkEnumModeParity(Module module, List vectors) async { + await module.build(); + await SimCompare.checkFunctionalVector(module, vectors); + + final typedSv = module.generateSynth(); + expect(typedSv, contains('typedef enum')); + SimCompare.checkIverilogVector(module, vectors); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, isNot(contains('typedef enum'))); + expect(untypedSv, isNot(matches(RegExp(r"[A-Za-z_]\w*'\(")))); + SimCompare.checkIverilogVector( + module, + vectors, + synthesizerConfiguration: configuration, + ); +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('enum populates based on list of values', () { + final e = MyListLogicEnum(); + + expect(e.mapping.length, TestEnum.values.length); + expect(e.width, 2); + + var idx = 0; + for (final val in TestEnum.values) { + expect(e.mapping.containsKey(val), isTrue); + expect(e.mapping[val]!.width, e.width); + expect(e.mapping[val]!.toInt(), idx++); + } + }); + + test('single-value enum has a one-bit representation', () { + final logicEnum = LogicEnum(SingleValueEnum.values); + + expect(logicEnum.width, 1); + expect(logicEnum.mapping[SingleValueEnum.only], LogicValue.zero); + }); + + test('empty enum mapping is rejected', () { + expect( + () => LogicEnum.withMapping({}), + throwsA(isA()), + ); + }); + + test('assignment between incompatible enum mappings is rejected', () { + final destination = LogicEnum(TestEnum.values); + final source = LogicEnum.withMapping({ + TestEnum.a: 0, + TestEnum.b: 2, + TestEnum.c: 3, + }); + + expect(() => destination.gets(source), throwsA(isA())); + }); + + test('assignment from an enum subset to a superset is accepted', () { + final broad = LogicEnum(TestEnum.values, width: 2); + final broadConditional = LogicEnum(TestEnum.values, width: 2); + final narrow = LogicEnum.withMapping({ + TestEnum.a: 0, + TestEnum.b: 1, + }, width: 2); + final conflicting = LogicEnum.withMapping({ + TestEnum.a: 1, + TestEnum.b: 0, + }, width: 2); + + expect(() => broad.gets(narrow), returnsNormally); + expect(broadConditional < narrow, isA()); + expect(() => narrow.gets(broad), throwsA(isA())); + expect(() => broad.gets(conflicting), throwsA(isA())); + }); + + test('mapping width is inferred from integer encodings', () { + final logicEnum = LogicEnum.withMapping({ + TestEnum.a: 1, + TestEnum.c: 7, + }); + final wideValue = BigInt.one << 80; + final wideEnum = LogicEnum.withMapping({ + TestEnum.a: BigInt.zero, + TestEnum.c: wideValue, + }); + + expect(logicEnum.width, 3); + expect(wideEnum.width, 81); + expect(wideEnum.mapping[TestEnum.c]!.toBigInt(), wideValue); + }); + + test('clone follows standard Logic naming policy', () { + final original = LogicEnum( + TestEnum.values, + name: 'state', + naming: Naming.reserved, + ); + + final clone = original.clone(); + final renamedClone = original.clone(name: 'nextState'); + + expect(clone.name, original.name); + expect(clone.naming, Naming.mergeable); + expect(renamedClone.name, 'nextState'); + expect(renamedClone.naming, Naming.renameable); + }); + + test('invalid mapping encodings are rejected', () { + expect( + () => LogicEnum.withMapping({TestEnum.a: -1}), + throwsA(isA()), + ); + expect( + () => LogicEnum.withMapping({TestEnum.a: 4}, width: 2), + throwsA(isA()), + ); + }); + + test('duplicate, invalid, and negative mapping values are rejected', () { + expect( + () => LogicEnum.withMapping({ + TestEnum.a: 0, + TestEnum.b: 0, + }), + throwsA(isA()), + ); + expect( + () => LogicEnum.withMapping({ + TestEnum.a: 0, + TestEnum.b: '0', + }), + throwsA(isA()), + ); + expect( + () => LogicEnum.withMapping({TestEnum.a: 'x'}), + throwsA(isA()), + ); + expect( + () => LogicEnum.withMapping({TestEnum.a: -BigInt.one}), + throwsA(isA()), + ); + }); + + test('mixed mapping representations contribute to inferred width', () { + final logicEnum = LogicEnum.withMapping({ + TestEnum.a: '10101', + TestEnum.b: [ + LogicValue.one, + LogicValue.zero, + LogicValue.one, + LogicValue.zero, + ], + TestEnum.c: 0, + }); + + expect(logicEnum.width, 5); + expect(logicEnum.mapping[TestEnum.a]!.toInt(), 0x15); + expect(logicEnum.mapping[TestEnum.b]!.width, 5); + }); + + test('sparse enum mutation APIs reject missing members and fill', () { + final sparse = LogicEnum.withMapping({TestEnum.a: 0}, width: 2); + + expect(() => sparse.getsEnum(TestEnum.b), throwsA(isA())); + expect(() => sparse.put(TestEnum.b), throwsA(isA())); + expect(() => sparse.inject(TestEnum.b), throwsA(isA())); + expect( + () => sparse.put(TestEnum.a, fill: true), + throwsA(isA()), + ); + expect( + () => sparse.inject(TestEnum.a, fill: true), + throwsA(isA()), + ); + expect( + () => sparse.gets(Const(1, width: sparse.width)), + throwsA(isA()), + ); + }); + + test('raw four-state values and raw conditional sources remain supported', + () { + final floatingEnum = MyListLogicEnum()..put(LogicValue.ofString('zz')); + expect(floatingEnum.value, LogicValue.ofString('zz')); + + final invalidEnum = MyListLogicEnum()..put(LogicValue.ofString('x1')); + expect(invalidEnum.value, LogicValue.ofString('xx')); + + final conditionalEnum = MyListLogicEnum()..inject(1); + expect( + conditionalEnum < Logic(width: conditionalEnum.width), + isA(), + ); + }); + + test('enum type identity requires the same Dart type and exact mapping', () { + final logicEnum = LogicEnum(TestEnum.values); + final remapped = LogicEnum.withMapping({ + TestEnum.a: 0, + TestEnum.b: 2, + TestEnum.c: 3, + }); + final wider = LogicEnum(TestEnum.values, width: 3); + + expect( + logicEnum.isEquivalentTypeTo(Logic(width: logicEnum.width)), + isFalse, + ); + expect(logicEnum.isEquivalentTypeTo(LogicEnum(OtherEnum.values)), isFalse); + expect(logicEnum.isEquivalentTypeTo(remapped), isFalse); + expect(() => logicEnum.gets(wider), throwsA(isA())); + }); + + test('conditional assignment validates known values and enum types', () { + final logicEnum = LogicEnum(TestEnum.values); + final incompatibleType = LogicEnum(OtherEnum.values); + + expect(() => logicEnum < 3, throwsA(isA())); + expect(() => logicEnum < OtherEnum.a, throwsA(isA())); + expect(() => logicEnum < incompatibleType, throwsA(isA())); + expect(logicEnum < TestEnum.b, isA()); + expect(logicEnum < 2, isA()); + }); + + test('untyped Logic rejects enum conditional values', () { + expect( + () => Logic(width: 2) < TestEnum.a, + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('LogicEnum receiver with an explicit mapping'), + ), + ), + ); + }); + + test('cases rejects enum keys outside the expression mapping', () { + final expression = LogicEnum(TestEnum.values); + final sparseExpression = LogicEnum.withMapping({ + TestEnum.a: 0, + TestEnum.c: 1, + }); + + expect( + () => cases(expression, {OtherEnum.a: 0}, width: 1), + throwsA(isA()), + ); + expect( + () => cases(sparseExpression, {TestEnum.b: 0}, width: 1), + throwsA(isA()), + ); + }); + + test('cases rejects mixed enum and raw logic results', () { + final expression = LogicEnum(TestEnum.values); + + expect( + () => cases(expression, { + TestEnum.a: TestEnum.b, + TestEnum.b: Logic(width: expression.width), + }), + throwsA(isA()), + ); + }); + + test('enum only allows legal values', () { + final e = MyListLogicEnum(); + expect(e.value.isFloating, isTrue); + e.put(0); + expect(e.value.toInt(), 0); + expect(e.valueEnum, TestEnum.a); + e.put(1); + expect(e.value.toInt(), 1); + expect(e.valueEnum, TestEnum.b); + e.put(2); + expect(e.value.toInt(), 2); + expect(e.valueEnum, TestEnum.c); + e.put(3); + expect(e.value, LogicValue.filled(e.width, LogicValue.x)); + expect(() => e.valueEnum, throwsStateError); + }); + + test('raw logic drivers are constrained in simulation', () async { + final source = Logic(width: 2); + final logicEnum = MyListLogicEnum()..gets(source); + + source.put(3); + await Simulator.run(); + + expect(logicEnum.value, LogicValue.filled(logicEnum.width, LogicValue.x)); + }); + + test('enum puts with enums', () { + final e = MyListLogicEnum()..put(TestEnum.b); + expect(e.value.toInt(), TestEnum.b.index); + expect(e.valueEnum, TestEnum.b); + }); + + test('enum injects with enums', () async { + final logicEnum = MyListLogicEnum()..inject(TestEnum.c); + + await Simulator.run(); + + expect(logicEnum.valueEnum, TestEnum.c); + }); + + test('synthesis merge policy preserves compatible enum metadata', () async { + final module = EmptyModule(); + await module.build(); + final definition = SynthModuleDefinition(module); + + SynthLogic synth(Logic logic) => SynthLogic( + logic, + parentSynthModuleDefinition: definition, + ); + + final equivalentA = synth(LogicEnum( + TestEnum.values, + name: 'equivalentA', + naming: Naming.mergeable, + )); + final equivalentB = synth(LogicEnum( + TestEnum.values, + name: 'equivalentB', + naming: Naming.renameable, + )); + final equivalentResult = SynthLogic.tryMerge(equivalentA, equivalentB); + expect(equivalentResult, isNotNull); + expect(equivalentA.resolved, same(equivalentB.resolved)); + expect(equivalentResult!.kept.isEnum, isTrue); + + final incompatibleA = synth(LogicEnum(TestEnum.values)); + final incompatibleB = synth(LogicEnum.withMapping({ + TestEnum.a: 0, + TestEnum.b: 2, + TestEnum.c: 3, + })); + expect(SynthLogic.tryMerge(incompatibleA, incompatibleB), isNull); + + final enumLogic = synth(LogicEnum(TestEnum.values)); + final rawLogic = synth(Logic(width: 2)); + final rawResult = SynthLogic.tryMerge(enumLogic, rawLogic); + expect(rawResult, isNotNull); + expect(rawResult!.kept.isEnum, isTrue); + + final legalEnum = synth(LogicEnum(TestEnum.values)); + final legalConstant = synth(Const(2, width: 2)); + expect(SynthLogic.tryMerge(legalEnum, legalConstant), isNotNull); + + final illegalEnum = synth(LogicEnum(TestEnum.values)); + final illegalConstant = synth(Const(3, width: 2)); + expect(SynthLogic.tryMerge(illegalEnum, illegalConstant), isNull); + }); + + group('enum sv gen', () { + test('simple mod with enum gen good sv', () async { + final mod = SimpleModWithEnum(Logic(width: 3)); + await mod.build(); + + final sv = mod.generateSynth(); + + expect( + sv, + contains( + "typedef enum logic [2:0] { a = 3'h1, c = 3'h7 } TestEnum;")); + expect(sv, contains('TestEnum elephant;')); + }); + + test('conflicting enum mod gen good sv', () async { + final mod = ConflictingEnumMod(Logic(width: 3)); + await mod.build(); + + final sv = mod.generateSynth(); + + // Allocation order may choose either enum for the unsuffixed names. + expect( + sv, + anyOf( + contains('typedef enum logic [2:0]' + " { a = 3'h1, c = 3'h7 } TestEnum;"), + contains('typedef enum logic [2:0]' + " { a_0 = 3'h1, c_0 = 3'h7 } TestEnum_0;"), + )); + expect( + sv, + anyOf( + contains('typedef enum logic [1:0]' + " { a = 2'h0, b = 2'h1, c = 2'h2 } TestEnum;"), + contains('typedef enum logic [1:0]' + " { a_0 = 2'h0, b = 2'h1, c_0 = 2'h2 } TestEnum_0;"), + )); + }); + + test('enum constant assignment uses enum name', () async { + final mod = ModWithEnumConstAssignment(Logic(width: 2)); + await mod.build(); + + final sv = mod.generateSynth(); + + expect( + sv, + contains('typedef enum logic [1:0]' + " { a = 2'h0, b = 2'h1, c = 2'h2 } TestEnum;")); + expect(sv, contains('assign banana = carrot & b;')); + }); + + test('generated enum SystemVerilog compiles and matches simulation', + () async { + final module = ModWithEnumConstAssignment(Logic(width: 2)); + await module.build(); + + final vectors = [ + Vector({'carrot': 0}, {'banana': 0}), + Vector({'carrot': 1}, {'banana': 1}), + Vector({'carrot': 2}, {'banana': 0}), + Vector({'carrot': 3}, {'banana': 1}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); + + test('enum identifiers are uniquified and stable', () async { + final module = EnumNameCollisionModule(); + await module.build(); + + final firstSv = module.generateSynth(); + final secondSv = module.generateSynth(); + + const typeDefinition = + "typedef enum logic [1:0] { a_0 = 2'h0, b = 2'h1, c = 2'h2 } " + 'TestEnum_0;'; + expect(firstSv, contains(typeDefinition)); + expect(secondSv, contains(typeDefinition)); + }); + + test('enum generation can be disabled', () async { + final simpleModule = SimpleModWithEnum(Logic(width: 3)); + final constantModule = ModWithEnumConstAssignment(Logic(width: 2)); + final casesModule = EnumCasesModule(Logic(width: 2)); + await simpleModule.build(); + await constantModule.build(); + await casesModule.build(); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final simpleSv = simpleModule.generateSynth(configuration: configuration); + final constantSv = + constantModule.generateSynth(configuration: configuration); + final casesSv = casesModule.generateSynth(configuration: configuration); + + expect(simpleSv, isNot(contains('typedef enum'))); + expect(simpleSv, contains('logic [2:0] elephant;')); + expect(constantSv, isNot(contains('typedef enum'))); + expect(constantSv, contains("assign banana = carrot & 2'h1;")); + expect(casesSv, isNot(contains('typedef enum'))); + expect(casesSv, isNot(contains("TestEnum'("))); + SimCompare.checkIverilogVector( + casesModule, + [ + Vector({'selector': 0}, {'result': 1}), + Vector({'selector': 1}, {'result': 2}), + Vector({'selector': 2}, {'result': 0}), + ], + synthesizerConfiguration: configuration, + ); + }); + + test('cases infers enum-valued results', () async { + final module = EnumCasesModule(Logic(width: 2)); + await module.build(); + + expect(module.result, isA>()); + expect(module.generateSynth(), contains("TestEnum'(selector)")); + final vectors = [ + Vector({'selector': 0}, {'result': 1}), + Vector({'selector': 1}, {'result': 2}), + Vector({'selector': 2}, {'result': 0}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); + + test('subset enum assignment casts between distinct enum types', () async { + final module = EnumSubsetAssignmentModule(Logic(width: 2)); + await module.build(); + + final sv = module.generateSynth(); + expect( + sv, + contains( + "typedef enum logic [1:0] { a = 2'h0, b = 2'h1, c = 2'h2 } " + 'BroadEnum;', + ), + ); + expect( + sv, + contains( + "typedef enum logic [1:0] { a_0 = 2'h0, b_0 = 2'h1 } NarrowEnum;", + ), + ); + expect(sv, contains("assign broad = BroadEnum'(narrow);")); + + final vectors = [ + Vector({'selector': 0}, {'result': 0}), + Vector({'selector': 1}, {'result': 1}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, isNot(contains('typedef enum'))); + expect(untypedSv, contains('assign broad = narrow;')); + SimCompare.checkIverilogVector( + module, + vectors, + synthesizerConfiguration: configuration, + ); + }); + + test('conditional subset enum assignment synthesizes', () async { + final module = EnumSubsetConditionalAssignmentModule(Logic(width: 2)); + await module.build(); + + final sv = module.generateSynth(); + expect(sv, contains("BroadEnum'(narrow)")); + + final vectors = [ + Vector({'selector': 0}, {'result': 0}), + Vector({'selector': 1}, {'result': 1}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, isNot(contains('typedef enum'))); + expect(untypedSv, isNot(contains("BroadEnum'("))); + SimCompare.checkIverilogVector( + module, + vectors, + synthesizerConfiguration: configuration, + ); + }); + + test('enum driven from a packed slice synthesizes in both modes', () async { + final module = EnumFromSliceModule(Logic(width: 8)); + await module.build(); + + final vectors = [ + Vector({'source': 0x00}, {'result': 0}), + Vector({'source': 0x04}, {'result': 1}), + Vector({'source': 0x08}, {'result': 2}), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains("TestEnum'(")); + SimCompare.checkIverilogVector(module, vectors); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, isNot(contains('typedef enum'))); + expect(untypedSv, isNot(contains("TestEnum'("))); + SimCompare.checkIverilogVector( + module, + vectors, + synthesizerConfiguration: configuration, + ); + }); + + test('enum assembled from assigned bits synthesizes in both modes', + () async { + final module = EnumFromAssignedBitsModule(Logic(width: 2)); + final vectors = [ + Vector({'source': 0}, {'result': 0}), + Vector({'source': 1}, {'result': 1}), + Vector({'source': 2}, {'result': 2}), + ]; + await checkEnumModeParity(module, vectors); + + expect( + module.generateSynth(), + contains("assign state = TestEnum'(source[1:0]);"), + ); + }); + + test('partially assigned enum generates compilable SystemVerilog', + () async { + final module = PartiallyAssignedEnumModule(Logic()); + await module.build(); + + SimCompare.checkIverilogVector(module, [], buildOnly: true); + SimCompare.checkIverilogVector( + module, + [], + buildOnly: true, + synthesizerConfiguration: + const SystemVerilogSynthesizerConfiguration(generateEnums: false), + ); + }); + + test('single-value enum synthesizes in both modes', () async { + final module = SingleValueEnumModule(); + await checkEnumModeParity( + module, + [ + Vector({}, {'result': 0}) + ], + ); + + expect(module.generateSynth(), contains('enum logic [0:0]')); + }); + + test('wide sparse enum synthesizes in both modes', () async { + final module = WideSparseEnumModule(); + final vectors = [ + Vector({}, {'result': WideSparseEnumModule.wideValue}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains('enum logic [80:0]')); + expect(sv, contains("c = 81'h100000000000000000000")); + }); + + test('enum leaf in a structure preserves behavior in both modes', () async { + final module = EnumStructureModule(Logic(width: 4)); + final vectors = [ + Vector( + {'source': 0x0}, + {'stateResult': 0, 'packedResult': 0x0, 'rangeResult': 0}, + ), + Vector( + {'source': 0x5}, + {'stateResult': 1, 'packedResult': 0x5, 'rangeResult': 1}, + ), + Vector( + {'source': 0xa}, + {'stateResult': 2, 'packedResult': 0xa, 'rangeResult': 2}, + ), + ]; + await checkEnumModeParity(module, vectors); + + expect(module.generateSynth(), contains("TestEnum'(")); + }); + + test('raw unpacked array lane feeds enum in both modes', () async { + final module = EnumArrayBoundaryModule(Logic(width: 4)); + final vectors = [ + Vector({'source': 0x0}, {'stateResult': 0, 'packedResult': 0x0}), + Vector({'source': 0x4}, {'stateResult': 1, 'packedResult': 0x4}), + Vector({'source': 0xa}, {'stateResult': 2, 'packedResult': 0xa}), + ]; + await checkEnumModeParity(module, vectors); + + expect(module.generateSynth(), contains("TestEnum'(")); + }); + + test('enum metadata crosses a submodule boundary in both modes', () async { + final module = EnumHierarchyModule(Logic(width: 2)); + final vectors = [ + Vector({'source': 0}, {'result': 0}), + Vector({'source': 1}, {'result': 1}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains('ChildNarrowEnum')); + expect(sv, contains('ParentBroadEnum')); + }); + + test('typed enum input and output preserve type in both modes', () async { + final source = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.c: 2, + }, + width: 2, + definitionName: 'TypedPortEnum', + ); + final module = TypedEnumPortsModule(source); + + expect(module.stateIn, isA>()); + expect(module.stateOut, isA>()); + expect(module.stateIn.mapping, source.mapping); + expect(module.stateOut.mapping, source.mapping); + + final vectors = [ + Vector({'stateIn': 0}, {'stateOut': 0}), + Vector({'stateIn': 2}, {'stateOut': 2}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains('input logic [1:0] stateIn')); + expect(sv, contains('output logic [1:0] stateOut')); + expect(sv, contains('} TypedPortEnum;')); + expect(sv, contains('TypedPortEnum stateIn_enum;')); + expect(sv, contains('TypedPortEnum stateOut_enum;')); + expect( + sv, + contains("assign stateIn_enum = TypedPortEnum'(stateIn);"), + ); + expect(sv, contains('assign stateOut = stateOut_enum;')); + expect(sv, contains('assign stateOut_enum = stateIn_enum;')); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, isNot(contains('stateIn_enum'))); + expect(untypedSv, isNot(contains('stateOut_enum'))); + }); + + test('typed enum input survives partial assignment rewriting', () async { + final type = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.b: 1, + }, + width: 1, + definitionName: 'TypedPartialEnum', + ); + final module = TypedEnumPartialSourceModule(type); + final vectors = [ + Vector({'stateIn': 0}, {'result': 0}), + Vector({'stateIn': 1}, {'result': 2}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains('assign packedValue[1] = stateIn_enum;')); + expect( + sv, + contains("assign stateIn_enum = TypedPartialEnum'(stateIn);"), + ); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, contains('assign packedValue[1] = stateIn;')); + expect(untypedSv, isNot(contains('stateIn_enum'))); + }); + + test('typed enum output survives range assignment rewriting', () async { + final type = LogicEnum( + TestEnum.values, + width: 2, + definitionName: 'TypedRangeEnum', + ); + final module = TypedEnumRangeDestinationModule(Logic(width: 2), type); + final vectors = [ + Vector({'source': 0}, {'stateOut': 0}), + Vector({'source': 1}, {'stateOut': 1}), + Vector({'source': 2}, {'stateOut': 2}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect( + sv, + contains("assign stateOut_enum = TypedRangeEnum'(source[1:0]);"), + ); + expect(sv, contains('assign stateOut = stateOut_enum;')); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, contains('assign stateOut = source[1:0];')); + expect(untypedSv, isNot(contains('stateOut_enum'))); + }); + + test('typed enum ports preserve widening across hierarchy in both modes', + () async { + final module = TypedEnumHierarchyModule(Logic(width: 2)); + final vectors = [ + Vector({'source': 0}, {'result': 0}), + Vector({'source': 2}, {'result': 2}), + ]; + await checkEnumModeParity(module, vectors); + + expect(module.child.stateIn.mapping.keys, [TestEnum.a, TestEnum.c]); + expect(module.child.stateOut.mapping, module.child.stateIn.mapping); + + final sv = module.generateSynth(); + expect(sv, contains('TypedChildNarrowEnum')); + expect(sv, contains('ParentTypedBroadEnum')); + expect(sv, contains("ParentTypedBroadEnum'(")); + }); + + test('typed enum backing signals are used by cases in both modes', + () async { + final source = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.c: 2, + }, + width: 2, + definitionName: 'TypedCaseEnum', + ); + final module = TypedEnumCasesModule(source); + final vectors = [ + Vector({'stateIn': 0}, {'stateOut': 2}), + Vector({'stateIn': 2}, {'stateOut': 0}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains('case (stateIn_enum)')); + expect(sv, contains('stateOut_enum = c;')); + expect(sv, contains('stateOut_enum = a;')); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, isNot(contains('stateIn_enum'))); + expect(untypedSv, isNot(contains('stateOut_enum'))); + }); + + test('typed enum backing names avoid signal collisions', () async { + final source = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.c: 2, + }, + width: 2, + definitionName: 'TypedCollisionEnum', + ); + final module = TypedEnumPortNameCollisionModule(source); + final vectors = [ + Vector({'stateIn': 0}, {'result': 0}), + Vector({'stateIn': 2}, {'result': 2}), + ]; + await checkEnumModeParity(module, vectors); + + final firstSv = module.generateSynth(); + final secondSv = module.generateSynth(); + for (final sv in [firstSv, secondSv]) { + expect(sv, contains('logic [1:0] stateIn_enum;')); + expect(sv, contains('TypedCollisionEnum stateIn_enum_0;')); + expect( + sv, + contains("assign stateIn_enum_0 = TypedCollisionEnum'(stateIn);"), + ); + } + }); + + test('typed enum backing signals feed inline and child consumers', + () async { + final source = LogicEnum.withMapping( + { + TestEnum.a: 0, + TestEnum.c: 2, + }, + width: 2, + definitionName: 'TypedConsumerEnum', + ); + final module = TypedEnumConsumersModule(source); + final vectors = [ + Vector({'stateIn': 0}, {'inlineResult': 1, 'childResult': 0}), + Vector({'stateIn': 2}, {'inlineResult': 3, 'childResult': 2}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains("stateIn_enum ^ 2'h1")); + expect(sv, contains('.stateIn(stateIn_enum)')); + + const configuration = + SystemVerilogSynthesizerConfiguration(generateEnums: false); + final untypedSv = module.generateSynth(configuration: configuration); + expect(untypedSv, isNot(contains('stateIn_enum'))); + }); + + test('typed enum inOut requires a net-backed enum type', () { + final module = EmptyModule(); + final logicEnum = LogicEnum(TestEnum.values); + + expect( + () => module.addTypedInOut('state', logicEnum), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('must be nets'), + ), + ), + ); + }); + + test('if-else mixes widened enum and enum constant in both modes', + () async { + final module = EnumIfElseModule(Logic(width: 2), Logic()); + final vectors = [ + Vector({'source': 0, 'select': 1}, {'result': 0}), + Vector({'source': 1, 'select': 1}, {'result': 1}), + Vector({'source': 0, 'select': 0}, {'result': 2}), + ]; + await checkEnumModeParity(module, vectors); + + final sv = module.generateSynth(); + expect(sv, contains('if(select)')); + expect(sv, contains('else begin')); + }); + + test('enum with case and cond assignments', () async { + final mod = ModWithCaseAndEnumCondAssign(Logic()); + await mod.build(); + + final sv = mod.generateSynth(); + + expect(sv, contains(' a : begin')); + expect(sv, contains('nextState = a;')); + }); + }); +}