Repository: intel/rohd (dart)

Issue:
Implement a shorthand for case statements
### Motivation

Sometimes it's desirable to assign/create some signal based on some condition, similar to a `mux`, but there is more than one case to consider.  The current easy options could be either a chain of `mux`s, which implies priority, or building a `Case` in a `Conditional`, which is a little heavy-weight for a simple scenario.

It would be nice to have some automation that creates a `Conditional` and `Case`, optionally with the usual set of settings like `unique`.

### Desired solution

A function which builds a `Conditional` and `Case` statement with some simple set of inputs.  Probably it should only be capable of applying to a single signal (i.e. one output, like `mux`).  For more complex scenarios, people can just create the full thing themselves.

### Alternatives considered

_No response_

### Additional details

_No response_

Pull request description:
<!-- Please make sure you check out the contribution guidelines before submitting a pull request! -->

## Description & Motivation

Implemented a shorthand for creating a Conditional and Case. This is for the scenario where it is needed to assign/create a signal based on a condition, similar to a mux. The output is a single `Logic` signal. The width of the output signal is optional and if not specified, is inferred from the width of the conditions.

## Related Issue(s)

Fix #304

## Testing

Added 5 new testcases which test the expression and condition match as well as width mismatch exceptions.

## Backwards-compatibility

> Is this a breaking change that will not be backwards-compatible? If yes, how so?

No

## Documentation

> Does the change require any updates to documentation? If so, where? Are they included?

Yes, API docs

Patch:
diff --git a/lib/src/exceptions/logic/signal_width_mismatch_exception.dart b/lib/src/exceptions/logic/signal_width_mismatch_exception.dart
index 806ec7f5c..304fb398a 100644
--- a/lib/src/exceptions/logic/signal_width_mismatch_exception.dart
+++ b/lib/src/exceptions/logic/signal_width_mismatch_exception.dart
@@ -18,4 +18,18 @@ class SignalWidthMismatchException extends RohdException {
       : super('Signal ${signal.name} has the wrong width.'
             ' Expected $expectedWidth but found ${signal.width}.'
             ' $additionalMessage');
+
+  /// Constructs a new [Exception] for when a dynamic has a wrong width.
+  SignalWidthMismatchException.forDynamic(
+      dynamic val, int expectedWidth, int actualWidth,
+      {String additionalMessage = ''})
+      : super('Value $val has the wrong width.'
+            ' Expected $expectedWidth but found $actualWidth.'
+            ' $additionalMessage');
+
+  /// Constructs a new [Exception] for when a dynamic has no width or it could
+  /// not be inferred.
+  SignalWidthMismatchException.forNull(dynamic val)
+      : super('Could not infer width of input $val.'
+            ' Please provide a valid width.');
 }
diff --git a/lib/src/modules/conditional.dart b/lib/src/modules/conditional.dart
index 306121f81..b39a97123 100644
--- a/lib/src/modules/conditional.dart
+++ b/lib/src/modules/conditional.dart
@@ -10,12 +10,12 @@
 import 'dart:async';
 import 'dart:collection';
 
+import 'package:collection/collection.dart';
 import 'package:meta/meta.dart';
 import 'package:rohd/rohd.dart';
 import 'package:rohd/src/collections/duplicate_detection_set.dart';
 import 'package:rohd/src/collections/traverseable_collection.dart';
-import 'package:rohd/src/exceptions/conditionals/conditional_exceptions.dart';
-import 'package:rohd/src/exceptions/module/port_width_mismatch_exception.dart';
+import 'package:rohd/src/exceptions/exceptions.dart';
 import 'package:rohd/src/utilities/sanitizer.dart';
 import 'package:rohd/src/utilities/synchronous_propagator.dart';
 import 'package:rohd/src/utilities/uniquifier.dart';
@@ -890,6 +890,79 @@ enum ConditionalType {
   priority
 }
 
+/// Shorthand for a [Case] inside a [Conditional] block.
+///
+/// It is used to assign a signal based on a condition with multiple cases to
+/// consider. For e.g., this can be used instead of a nested [mux].
+///
+/// The result is of type [Logic] and it is determined by conditionaly matching
+/// the expression with the values of each item in conditions. If width of the
+/// input is not provided, then the width of  the result is inferred from the
+/// width of the entries.
+Logic cases(Logic expression, Map<dynamic, dynamic> conditions,
+    {int? width,
+    ConditionalType conditionalType = ConditionalType.none,
+    dynamic defaultValue}) {
+  for (final conditionValue in [
+    ...conditions.values,
+    if (defaultValue != null) defaultValue
+  ]) {
+    int? inferredWidth;
+
+    if (conditionValue is Logic) {
+      inferredWidth = conditionValue.width;
+    } else if (conditionValue is LogicValue) {
+      inferredWidth = conditionValue.width;
+    }
+
+    if (width != inferredWidth && width != null && inferredWidth != null) {
+      throw SignalWidthMismatchException.forDynamic(
+          conditionValue, width, inferredWidth);
+    }
+
+    width ??= inferredWidth;
+  }
+
+  if (width == null) {
+    throw SignalWidthMismatchException.forNull(conditions);
+  }
+
+  for (final condition in conditions.entries) {
+    if (condition.key is Logic) {
+      if (expression.width != (condition.key as Logic).width) {
+        throw SignalWidthMismatchException.forDynamic(
+            condition.key, expression.width, (condition.key as Logic).width);
+      }
+    }
+
+    if (condition.key is LogicValue) {
+      if (expression.width != (condition.key as LogicValue).width) {
+        throw SignalWidthMismatchException.forDynamic(condition.key,
+            expression.width, (condition.key as LogicValue).width);
+      }
+    }
+  }
+
+  final result = Logic(name: 'result', width: width);
+
+  Combinational([
+    Case(
+        expression,
+        [
+          for (final condition in conditions.entries)
+            CaseItem(
+                condition.key is Logic
+                    ? condition.key as Logic
+                    : Const(condition.key, width: expression.width),
+                [result < condition.value])
+        ],
+        conditionalType: conditionalType,
+        defaultItem: defaultValue != null ? [result < defaultValue] : null)
+  ]);
+
+  return result;
+}
+
 /// A block of [CaseItem]s where only the one with a matching [CaseItem.value]
 /// is executed.
 ///


Test patch:
diff --git a/test/gate_test.dart b/test/gate_test.dart
index 3ea6163e1..9c830b39d 100644
--- a/test/gate_test.dart
+++ b/test/gate_test.dart
@@ -8,6 +8,7 @@
 /// Author: Max Korbel <max.korbel@intel.com>
 ///
 import 'package:rohd/rohd.dart';
+import 'package:rohd/src/exceptions/exceptions.dart';
 import 'package:rohd/src/utilities/simcompare.dart';
 import 'package:test/test.dart';
 
@@ -187,6 +188,161 @@ void main() {
     });
   });
 
+  test('mux shorthand', () {
+    final control = Logic();
+    final d0 = Logic();
+    final d1 = Logic();
+    final result = mux(control, d1, d0);
+
+    d0.put(0);
+    d1.put(1);
+    control.put(0);
+
+    expect(result.value, LogicValue.zero);
+
+    control.put(1);
+
+    expect(result.value, LogicValue.one);
+  });
+
+  group('Cases', () {
+    test('test LogicValue', () {
+      final control = Logic(width: 8);
+      final d0 = Logic(width: 8)..put(LogicValue.ofInt(2, 8));
+      final d1 = Logic(width: 8)..put(LogicValue.ofInt(3, 8));
+      final result = cases(
+          control,
+          {
+            d0: LogicValue.ofInt(2, 8),
+            d1: LogicValue.ofInt(3, 8),
+          },
+          width: 8);
+
+      control.put(2);
+
+      expect(result.value, LogicValue.ofInt(2, 8));
+
+      control.put(3);
+
+      expect(result.value, LogicValue.ofInt(3, 8));
+    });
+
+    test('test Int', () {
+      final control = Logic(width: 4)..put(LogicValue.ofInt(2, 4));
+      const d0 = 2;
+      const d1 = 3;
+
+      final result = cases(
+          control,
+          {
+            d0: 2,
+            d1: 3,
+          },
+          width: 4,
+          defaultValue: 3);
+
+      expect(result.value, LogicValue.ofInt(2, 4));
+    });
+
+    test('test Logic', () {
+      final control = Logic();
+      final d0 = Logic()..put(LogicValue.zero);
+      final d1 = Logic()..put(LogicValue.one);
+      final result = cases(control, {
+        d0: LogicValue.zero,
+        d1: LogicValue.one,
+      });
+
+      control.put(0);
+      expect(result.value, LogicValue.zero);
+
+      control.put(1);
+      expect(result.value, LogicValue.one);
+    });
+
+    test('test Default', () {
+      final control = Logic(width: 4);
+      const d0 = 1;
+      const d1 = 2;
+      final result = cases(
+          control,
+          {
+            d0: 1,
+            d1: 2,
+          },
+          width: 4,
+          defaultValue: 3);
+
+      control.put(LogicValue.zero);
+      expect(result.value, LogicValue.ofInt(3, 4));
+    });
+
+    test('test Exceptions(Int)', () {
+      final control = Logic(width: 4);
+      final d0 = Logic(width: 4);
+      final d1 = Logic(width: 8);
+
+      control.put(LogicValue.ofInt(2, 4));
+      d0.put(LogicValue.ofInt(2, 4));
+      d1.put(LogicValue.ofInt(3, 8));
+
+      expect(() => cases(control, {d0: 2, d1: 3}, width: 4),
+          throwsA(isA<SignalWidthMismatchException>()));
+    });
+
+    test('test Condition width mismatch Exception', () {
+      final control = Logic();
+      final d0 = Logic();
+      final d1 = Logic(width: 8);
+
+      control.put(LogicValue.zero);
+      d0.put(LogicValue.zero);
+      d1.put(LogicValue.ofInt(1, 8));
+
+      expect(
+          () =>
+              cases(control, {d0: LogicValue.zero, d1: LogicValue.ofInt(1, 8)}),
+          throwsA(isA<SignalWidthMismatchException>()));
+    });
+
+    test('test Expression width mismatch Exception for Logic', () {
+      final control = Logic();
+      final d0 = Logic(width: 8);
+      final d1 = Logic(width: 8);
+
+      control.put(LogicValue.one);
+
+      expect(
+          () => cases(control, {d0: Logic(width: 8), d1: Logic(width: 8)},
+              width: 8),
+          throwsA(isA<SignalWidthMismatchException>()));
+    });
+
+    test('test Expression width mismatch Exception for LogicValue', () {
+      final control = Logic();
+      final d0 = LogicValue.ofInt(0, 8);
+      final d1 = LogicValue.ofInt(1, 8);
+
+      control.put(LogicValue.one);
+
+      expect(
+          () => cases(
+              control, {d0: LogicValue.ofInt(0, 8), d1: LogicValue.ofInt(1, 8)},
+              width: 8),
+          throwsA(isA<SignalWidthMismatchException>()));
+    });
+
+    test('test Null width Exception', () {
+      final control = Logic();
+      const d0 = 2;
+      const d1 = 4;
+
+      control.put(LogicValue.zero);
+      expect(() => cases(control, {d0: 2, d1: 4}, defaultValue: 3),
+          throwsA(isA<SignalWidthMismatchException>()));
+    });
+  });
+
   group('simcompare', () {
     test('NotGate single bit', () async {
       final gtm = GateTestModule(Logic(), Logic());