diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json
index 0c4421967..8a610098c 100644
--- a/.github/configs/mlc_config.json
+++ b/.github/configs/mlc_config.json
@@ -12,6 +12,9 @@
{
"pattern":"^https://github.com"
},
+ {
+ "pattern":"^https://intel\\.github\\.io(?:/|$)"
+ },
{
"pattern":"^https://pymtl3.readthedocs.io/en/latest/$"
},
diff --git a/doc/user_guide/_docs/A02-logical_signals.md b/doc/user_guide/_docs/A02-logical_signals.md
index 1530c9e04..b3f4b70c6 100644
--- a/doc/user_guide/_docs/A02-logical_signals.md
+++ b/doc/user_guide/_docs/A02-logical_signals.md
@@ -2,7 +2,7 @@
title: "Logical Signals"
permalink: /docs/logical-signals/
excerpt: "Logic signals"
-last_modified_at: 2025-7-24
+last_modified_at: 2026-08-10
toc: true
---
@@ -18,7 +18,7 @@ var x = Logic();
var bus = Logic(name: 'b', width: 8)
```
-There are other types like [`LogicArray`](https://intel.github.io/rohd/rohd/LogicArray-class.html)s and [`LogicStructure`](https://intel.github.io/rohd/rohd/LogicStructure-class.html)s which extend from `Logic`, as well.
+There are other types which extend `Logic`, including [`LogicArray`](https://intel.github.io/rohd/rohd/LogicArray-class.html), [`LogicStructure`](https://intel.github.io/rohd/rohd/LogicStructure-class.html), and [`LogicEnum`](https://intel.github.io/rohd-website/docs/logic-enums/) for representing Dart enum values as typed hardware signals.
#### The value of a signal
diff --git a/doc/user_guide/_docs/A08-modules.md b/doc/user_guide/_docs/A08-modules.md
index 374a71703..2252fbb83 100644
--- a/doc/user_guide/_docs/A08-modules.md
+++ b/doc/user_guide/_docs/A08-modules.md
@@ -2,7 +2,7 @@
title: "Modules"
permalink: /docs/modules/
excerpt: "Modules"
-last_modified_at: 2025-7-24
+last_modified_at: 2026-08-10
toc: true
---
@@ -54,7 +54,7 @@ All gates or functionality apart from assign statements in ROHD are implemented
The default width of a port is 1. You can control the width of ports using the `width` argument of `addInput()` and `addOutput()`. You may choose to set them to a static number, based on some other variable, or even dynamically based on the width of input parameters. These functions also return the input/output signal.
-There are also similar functions called `addTypedInput` and `addTypedOutput` which will create a port with matching widths and types. This is especially useful for creating `LogicStructure` ports.
+There are also similar functions called `addTypedInput` and `addTypedOutput` which will create a port with matching widths and types. This is especially useful for creating `LogicStructure` and [`LogicEnum`](https://intel.github.io/rohd-website/docs/logic-enums/) ports.
Available mechanisms for creating ports on a `Module` are listed below:
@@ -70,6 +70,8 @@ Available mechanisms for creating ports on a `Module` are listed below:
| `addInOut` | Adds an in/out (bidirectional) `Logic` port to the module with explicit width. Requires an external source. |
| `addInOutArray` | Adds an in/out (bidirectional) `LogicArray` port to the module with explicit dimensions and element width. Requires an external source. |
+`LogicEnum` supports typed input and output ports. Typed enum in/out ports are not currently supported because there is no net-backed `LogicEnum` type.
+
You can also use [`Interface`s](https://intel.github.io/rohd-website/docs/interfaces/) to create groups of ports.
It can be convenient to use dart getters for signal names so that accessing inputs and outputs of a module doesn't require calling `input()` and `output()` every time. It also makes it easier to consume your module.
diff --git a/doc/user_guide/_docs/A10-conditionals.md b/doc/user_guide/_docs/A10-conditionals.md
index 94f72cef3..d28c5e2e5 100644
--- a/doc/user_guide/_docs/A10-conditionals.md
+++ b/doc/user_guide/_docs/A10-conditionals.md
@@ -2,7 +2,7 @@
title: "Conditionals"
permalink: /docs/conditionals/
excerpt: "Conditionals"
-last_modified_at: 2022-12-06
+last_modified_at: 2026-08-10
toc: true
---
@@ -92,6 +92,8 @@ Combinational([
]);
```
+The `cases` helper accepts enum members as keys when its selector is a compatible [`LogicEnum`](https://intel.github.io/rohd-website/docs/logic-enums/). It returns an ordinary `Logic`, so enum results must be supplied as explicitly mapped signals rather than bare Dart enum members.
+
Note that ROHD supports the 'z' syntax, not the '?' syntax (these are equivalent in SystemVerilog).
There is no support for an equivalent of `casex` from SystemVerilog, since it can easily cause unsynthesizeable code to be generated (see: ).
diff --git a/doc/user_guide/_docs/A15-fsm.md b/doc/user_guide/_docs/A15-fsm.md
index 256ba9abe..cc296a559 100644
--- a/doc/user_guide/_docs/A15-fsm.md
+++ b/doc/user_guide/_docs/A15-fsm.md
@@ -2,7 +2,7 @@
title: "Finite State Machines"
permalink: /docs/fsm/
excerpt: "Finite State Machines"
-last_modified_at: 2023-09-19
+last_modified_at: 2026-08-10
toc: true
---
@@ -47,14 +47,17 @@ Now, let's define the encoding of outputs (the color of the light).
```dart
enum LightColor {
- green(0),
- yellow(1),
- red(2);
-
- final int value;
-
- const LightColor(this.value);
+ green,
+ yellow,
+ red,
}
+
+final lightType = LogicEnum(
+ LightColor.values,
+ definitionName: 'LightColor',
+);
+final northLight = addTypedOutput('northLight', lightType.clone);
+final eastLight = addTypedOutput('eastLight', lightType.clone);
```
Now we can go ahead and describe the set of states for our state machine. Note that for each state, we describe a few things:
@@ -68,16 +71,16 @@ final states = >[
State(LightStates.northFlowing, events: {
TrafficPresence.isEastActive(traffic): LightStates.northSlowing,
}, actions: [
- northLight < LightColor.green.value,
- eastLight < LightColor.red.value,
+ northLight < LightColor.green,
+ eastLight < LightColor.red,
]),
State(
LightStates.northSlowing,
events: {},
defaultNextState: LightStates.eastFlowing,
actions: [
- northLight < LightColor.yellow.value,
- eastLight < LightColor.red.value,
+ northLight < LightColor.yellow,
+ eastLight < LightColor.red,
],
),
State(
@@ -86,8 +89,8 @@ final states = >[
TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing,
},
actions: [
- northLight < LightColor.red.value,
- eastLight < LightColor.green.value,
+ northLight < LightColor.red,
+ eastLight < LightColor.green,
],
),
State(
@@ -95,8 +98,8 @@ final states = >[
events: {},
defaultNextState: LightStates.northFlowing,
actions: [
- northLight < LightColor.red.value,
- eastLight < LightColor.yellow.value,
+ northLight < LightColor.red,
+ eastLight < LightColor.yellow,
],
),
];
@@ -120,14 +123,14 @@ final states = >[
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(
@@ -136,7 +139,7 @@ final states = >[
TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing,
},
actions: [
- eastLight < LightColor.green.value,
+ eastLight < LightColor.green,
],
),
State(
@@ -144,7 +147,7 @@ final states = >[
events: {},
defaultNextState: LightStates.northFlowing,
actions: [
- eastLight < LightColor.yellow.value,
+ eastLight < LightColor.yellow,
],
),
];
@@ -156,13 +159,13 @@ FiniteStateMachine(
states,
setupActions: [
// by default, lights should be red
- northLight < LightColor.red.value,
- eastLight < LightColor.red.value,
+ northLight < LightColor.red,
+ eastLight < LightColor.red,
],
);
```
-This state machine is now functional and synthesizable into SystemVerilog!
+This state machine is now functional and synthesizable into SystemVerilog. Its `currentState` and `nextState` signals are `LogicEnum`, and generated SystemVerilog uses the `LightColor` enum for internal output backing signals and symbolic assignments. See [Logic Enums](https://intel.github.io/rohd-website/docs/logic-enums/) for details about enum mappings and generation.
You can even generate a mermaid diagram for the state machine using the [`generateDiagram`](https://intel.github.io/rohd/rohd/FiniteStateMachine/generateDiagram.html) API.
diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md
index e4e625952..261f84fbe 100644
--- a/doc/user_guide/_docs/A21-generation.md
+++ b/doc/user_guide/_docs/A21-generation.md
@@ -1,7 +1,7 @@
---
title: "Generating Outputs"
permalink: /docs/generation/
-last_modified_at: 2023-11-13
+last_modified_at: 2026-08-10
toc: true
---
@@ -53,6 +53,20 @@ final generatedSv = myModule.generateSynth(
The same configuration can be passed directly to `SystemVerilogSynthesizer` when using `SynthBuilder`.
+## Controlling enum generation
+
+[`LogicEnum`](https://intel.github.io/rohd-website/docs/logic-enums/) signals generate SystemVerilog enum typedefs and symbolic values by default. Enum generation can be disabled for compatibility with tools or flows that require packed logic:
+
+```dart
+final generatedSv = myModule.generateSynth(
+ configuration: const SystemVerilogSynthesizerConfiguration(
+ generateEnums: false,
+ ),
+);
+```
+
+With enum generation disabled, the same design is emitted using ordinary packed logic and numeric values.
+
## Controlling naming
### Modules
diff --git a/doc/user_guide/_docs/A23-logic-enums.md b/doc/user_guide/_docs/A23-logic-enums.md
new file mode 100644
index 000000000..f95723318
--- /dev/null
+++ b/doc/user_guide/_docs/A23-logic-enums.md
@@ -0,0 +1,108 @@
+---
+title: "Logic Enums"
+permalink: /docs/logic-enums/
+excerpt: "Representing enumerable values as hardware signals"
+last_modified_at: 2026-08-10
+toc: true
+---
+
+## Logic Enums
+
+[`LogicEnum`](https://intel.github.io/rohd/rohd/LogicEnum-class.html) is a `Logic` signal constrained to values from a Dart enum. It associates each enum member with a bit-vector encoding and preserves that type information through simulation and SystemVerilog generation.
+
+```dart
+enum Operation { idle, read, write }
+
+final operation = LogicEnum(
+ Operation.values,
+ name: 'operation',
+ definitionName: 'Operation',
+);
+```
+
+The default constructor assigns sequential encodings in the order provided. The width is inferred from the number of values, or it can be set explicitly with `width`.
+
+For sparse or protocol-defined encodings, use `LogicEnum.withMapping`:
+
+```dart
+final operation = LogicEnum.withMapping(
+ {
+ Operation.idle: 0,
+ Operation.read: 1,
+ Operation.write: 3,
+ },
+ width: 2,
+ definitionName: 'Operation',
+);
+```
+
+Mappings must be non-empty and contain unique, valid, non-negative encodings that fit within the signal width. Reading a valid but unmapped bit pattern in simulation produces `x`.
+
+## Using Enum Values
+
+Enum members can be used directly in conditional assignments to a compatible `LogicEnum`:
+
+```dart
+Combinational([
+ If(enable, then: [
+ operation < Operation.read,
+ ], orElse: [
+ operation < Operation.idle,
+ ]),
+]);
+```
+
+Use `getsEnum` for a continuous connection to a constant enum value. In testbench code, `put` and `inject` also accept enum members. The current enum member is available through `valueEnum` when the signal contains a mapped value.
+
+```dart
+final constantOperation = operation.clone()
+ ..getsEnum(Operation.write);
+
+operation.inject(Operation.read);
+expect(operation.valueEnum, Operation.read);
+```
+
+Assignments between `LogicEnum`s require the same Dart enum type, width, and compatible encodings. A destination may have additional mapped values, but every value mapped by the source must have the same encoding in the destination.
+
+## Cases
+
+Enum members can be used as keys when a `LogicEnum` is the selector:
+
+```dart
+final selected = cases(operation, {
+ Operation.idle: idleData,
+ Operation.read: readData,
+ Operation.write: writeData,
+});
+```
+
+The `cases` helper returns an ordinary `Logic`, so bare enum members are not accepted as result or default values. When an enum result is required, provide explicitly mapped `LogicEnum` branch signals and connect the result to a compatible `LogicEnum` destination.
+
+## Module Ports
+
+Use `addTypedInput` and `addTypedOutput` to preserve an enum's mapping across module boundaries:
+
+```dart
+final operationIn = addTypedInput('operationIn', operationSource);
+final operationOut = addTypedOutput('operationOut', operationIn.clone);
+
+operationOut <= operationIn;
+```
+
+Generated SystemVerilog keeps input and output ports packed and uses internal enum-typed backing signals. Typed `LogicEnum` in/out ports are not supported because `LogicEnum` does not currently have a net-backed variant.
+
+## Generated SystemVerilog
+
+By default, SystemVerilog generation emits enum typedefs and symbolic values for `LogicEnum` signals. The `definitionName` supplies the preferred typedef name, and generated names are uniquified when necessary.
+
+Enum generation can be disabled for tools or flows that require ordinary packed logic:
+
+```dart
+final generatedSv = module.generateSynth(
+ configuration: const SystemVerilogSynthesizerConfiguration(
+ generateEnums: false,
+ ),
+);
+```
+
+Disabling enum generation changes only the generated representation; simulation behavior and the ROHD model remain typed.
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..612526924 100644
--- a/lib/src/modules/conditionals/case.dart
+++ b/lib/src/modules/conditionals/case.dart
@@ -35,15 +35,27 @@ class CaseItem {
/// The result is of type [Logic] and it is determined by conditionaly matching
/// the expression with the values of each item in conditions. If width of the
/// input is not provided, then the width of the result is inferred from the
-/// width of the entries.
+/// width of the entries. Bare [Enum] values are not supported as results
+/// because an untyped result has no hardware encoding for them; use explicitly
+/// mapped [LogicEnum] signals instead.
Logic cases(Logic expression, Map conditions,
{int? width,
ConditionalType conditionalType = ConditionalType.none,
dynamic defaultValue}) {
- for (final conditionValue in [
+ final resultValues = [
...conditions.values,
if (defaultValue != null) defaultValue
- ]) {
+ ];
+ if (resultValues.any((value) => value is Enum)) {
+ throw ArgumentError.value(
+ resultValues,
+ 'conditions',
+ 'Bare enum result values have no hardware encoding. Convert each result '
+ 'to a LogicEnum signal with an explicit mapping.',
+ );
+ }
+
+ for (final conditionValue in resultValues) {
int? inferredWidth;
if (conditionValue is Logic) {
@@ -64,6 +76,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) {
@@ -87,11 +117,7 @@ Logic cases(Logic expression, Map conditions,
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 +151,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 +171,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 f964d320a..63fa981cc 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