Repository: intel/rohd (dart)

Issue:
Improve lint avoidance for width expansion
### Motivation

The current implementation (https://github.com/intel/rohd/issues/298, https://github.com/intel/rohd/pull/423) for handling width expansion (e.g. from adding, shift left, multiplication, etc.) avoids lint errors successfully, but still hits some lint warnings and is also somewhat ugly.

A nicer way to generate it might look something like this:
```SystemVerilog
assign {cout, out} = a + b + cin;
```

### Desired solution

Upgrade modules to generate prettier SV that avoids both lint errors and warnings

### 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

See #444

## Related Issue(s)

Fix #444

## Testing

<!-- Please describe how you tested your changes. -->

## Backwards-compatibility

> Is this a breaking change that will not be backwards-compatible? If yes, how so?

<!-- Answer here. -->

## Documentation

> Does the change require any updates to documentation? If so, where? Are they included?

<!-- Answer here. -->

Patch:
diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart
index 2e45a44ab..890ab1578 100644
--- a/lib/src/modules/gates.dart
+++ b/lib/src/modules/gates.dart
@@ -1,4 +1,4 @@
-// Copyright (C) 2021-2024 Intel Corporation
+// Copyright (C) 2021-2025 Intel Corporation
 // SPDX-License-Identifier: BSD-3-Clause
 //
 // gates.dart
@@ -52,8 +52,8 @@ class NotGate extends Module with InlineSystemVerilog {
   String inlineVerilog(Map<String, String> inputs) {
     assert(inputs.length == 1, 'Gate has exactly one input.');
 
-    final a = inputs[_inName]!;
-    return '~$a';
+    final in_ = inputs[_inName]!;
+    return '~$in_';
   }
 }
 
@@ -222,9 +222,8 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog {
 
   @override
   String inlineVerilog(Map<String, String> inputs) {
-    if (inputs.length != 2) {
-      throw Exception('Gate has exactly two inputs.');
-    }
+    assert(inputs.length == 2, 'Gate has exactly two inputs.');
+
     final in0 = inputs[_in0Name]!;
     final in1 = inputs[_in1Name]!;
     var sv = '$in0 $_opStr $in1';
@@ -280,8 +279,7 @@ abstract class _TwoInputComparisonGate extends Module with InlineSystemVerilog {
       {String name = 'cmp2'})
       : super(name: name) {
     if (in1 is Logic && in0.width != in1.width) {
-      throw Exception('Input widths must match,'
-          ' but found $in0 and $in1 with different widths.');
+      throw PortWidthMismatchException.equalWidth(in0, in1);
     }
 
     final in1Logic = in1 is Logic ? in1 : Const(in1, width: in0.width);
@@ -316,9 +314,8 @@ abstract class _TwoInputComparisonGate extends Module with InlineSystemVerilog {
 
   @override
   String inlineVerilog(Map<String, String> inputs) {
-    if (inputs.length != 2) {
-      throw Exception('Gate has exactly two inputs.');
-    }
+    assert(inputs.length == 2, 'Gate has exactly two inputs.');
+
     final in0 = inputs[_in0Name]!;
     final in1 = inputs[_in1Name]!;
     return '$in0 $_opStr $in1';
@@ -465,9 +462,8 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog {
 
   @override
   String inlineVerilog(Map<String, String> inputs) {
-    if (inputs.length != 2) {
-      throw Exception('Gate has exactly two inputs.');
-    }
+    assert(inputs.length == 2, 'Gate has exactly two inputs.');
+
     final in_ = inputs[_inName]!;
     final shiftAmount = inputs[_shiftAmountName]!;
 
@@ -508,27 +504,127 @@ class Xor2Gate extends _TwoInputBitwiseGate {
 class Power extends _TwoInputBitwiseGate {
   /// Calculates [in0] raise to power of [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   Power(Logic in0, dynamic in1, {String name = 'power'})
       : super((a, b) => a.pow(b), '**', in0, in1,
             name: name, makeSelfDetermined: true);
 }
 
 /// A two-input addition module.
-class Add extends _TwoInputBitwiseGate {
+class Add extends Module with SystemVerilog {
+  /// Name for a first input port of this module.
+  late final String _in0Name;
+
+  /// Name for a second input port of this module.
+  late final String _in1Name;
+
+  /// Name for the output port of this module.
+  late final String _sumName;
+
+  /// Name for the carry bit.
+  late final String _carryName;
+
+  /// An input to this gate.
+  late final Logic _in0 = input(_in0Name);
+
+  /// An input to this gate.
+  late final Logic _in1 = input(_in1Name);
+
+  /// The calculated sum output of this addition.
+  late final Logic sum = output(_sumName);
+
+  /// The calculated carry bit output of this addition.
+  late final Logic carry = output(_carryName);
+
+  /// The output of this gate.
+  ///
+  /// Deprecated: use [sum] instead.
+  @Deprecated('Use `sum` instead.')
+  Logic get out => sum;
+
+  /// The output of this gate.
+  ///
+  /// Deprecated: use [sum] instead.
+  @Deprecated('Use `sum` instead.')
+  Logic get y => sum;
+
+  /// The functional operation to perform for this gate.
+  LogicValue _addOp(LogicValue in0, LogicValue in1) => in0 + in1;
+
+  /// The `String` representing the operation to perform in generated code.
+  final String _addOpStr = '+';
+
+  /// The width of the inputs and [sum] for this operation.
+  final int width;
+
   /// Calculates the sum of [in0] and [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
-  Add(Logic in0, dynamic in1, {String name = 'add'})
-      : super((a, b) => a + b, '+', in0, in1,
-            name: name, outputSvWidthExpansion: 1);
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
+  Add(Logic in0, dynamic in1, {super.name = 'add'}) : width = in0.width {
+    if (in1 is Logic && in0.width != in1.width) {
+      throw PortWidthMismatchException.equalWidth(in0, in1);
+    }
+
+    final in1Logic = in1 is Logic ? in1 : Const(in1, width: width);
+
+    _in0Name = Naming.unpreferredName('in0_${in0.name}');
+    _in1Name = Naming.unpreferredName('in1_${in1Logic.name}');
+    _sumName = Naming.unpreferredName('${in0.name}_${name}_${in1Logic.name}');
+    _carryName = Naming.unpreferredName('${_sumName}_carry');
+
+    addInput(_in0Name, in0, width: width);
+    addInput(_in1Name, in1Logic, width: width);
+    addOutput(_sumName, width: width);
+    addOutput(_carryName);
+
+    _setup();
+  }
+
+  /// Performs setup steps for custom functional behavior.
+  void _setup() {
+    _execute(); // for initial values
+    _in0.glitch.listen((args) {
+      _execute();
+    });
+    _in1.glitch.listen((args) {
+      _execute();
+    });
+  }
+
+  /// Executes the functional behavior of this gate.
+  void _execute() {
+    final fullSum = _addOp(
+      _in0.value.zeroExtend(width + 1),
+      _in1.value.zeroExtend(width + 1),
+    );
+
+    sum.put(fullSum.getRange(0, width));
+    carry.put(fullSum[width]);
+  }
+
+  @override
+  String instantiationVerilog(
+      String instanceType, String instanceName, Map<String, String> ports) {
+    assert(inputs.length == 2, 'Gate has exactly two inputs');
+    assert(outputs.length == 2, 'Gate has exactly two outputs');
+
+    final in0 = ports[_in0Name]!;
+    final in1 = ports[_in1Name]!;
+    final sum = ports[_sumName]!;
+    final carry = ports[_carryName]!;
+
+    return 'assign {$carry, $sum} = $in0 $_addOpStr $in1;';
+  }
 }
 
 /// A two-input subtraction module.
 class Subtract extends _TwoInputBitwiseGate {
   /// Calculates the difference between [in0] and [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   Subtract(Logic in0, dynamic in1, {String name = 'subtract'})
       : super((a, b) => a - b, '-', in0, in1, name: name);
 }
@@ -537,7 +633,8 @@ class Subtract extends _TwoInputBitwiseGate {
 class Multiply extends _TwoInputBitwiseGate {
   /// Calculates the product of [in0] and [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   Multiply(Logic in0, dynamic in1, {String name = 'multiply'})
       : super((a, b) => a * b, '*', in0, in1,
             name: name, makeSelfDetermined: true);
@@ -547,7 +644,8 @@ class Multiply extends _TwoInputBitwiseGate {
 class Divide extends _TwoInputBitwiseGate {
   /// Calculates [in0] divided by [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   Divide(Logic in0, dynamic in1, {String name = 'divide'})
       : super((a, b) => a / b, '/', in0, in1, name: name);
 }
@@ -556,7 +654,8 @@ class Divide extends _TwoInputBitwiseGate {
 class Modulo extends _TwoInputBitwiseGate {
   /// Calculates the module of [in0] % [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   Modulo(Logic in0, dynamic in1, {String name = 'modulo'})
       : super((a, b) => a % b, '%', in0, in1, name: name);
 }
@@ -565,7 +664,8 @@ class Modulo extends _TwoInputBitwiseGate {
 class Equals extends _TwoInputComparisonGate {
   /// Calculates whether [in0] and [in1] are equal.
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   Equals(Logic in0, dynamic in1, {String name = 'equals'})
       : super((a, b) => a.eq(b), '==', in0, in1, name: name);
 }
@@ -574,7 +674,8 @@ class Equals extends _TwoInputComparisonGate {
 class NotEquals extends _TwoInputComparisonGate {
   /// Calculates whether [in0] and [in1] are not-equal.
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   NotEquals(Logic in0, dynamic in1, {String name = 'notEquals'})
       : super((a, b) => a.neq(b), '!=', in0, in1, name: name);
 }
@@ -583,7 +684,8 @@ class NotEquals extends _TwoInputComparisonGate {
 class LessThan extends _TwoInputComparisonGate {
   /// Calculates whether [in0] is less than [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   LessThan(Logic in0, dynamic in1, {String name = 'lessthan'})
       : super((a, b) => a < b, '<', in0, in1, name: name);
 }
@@ -592,7 +694,8 @@ class LessThan extends _TwoInputComparisonGate {
 class GreaterThan extends _TwoInputComparisonGate {
   /// Calculates whether [in0] is greater than [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   GreaterThan(Logic in0, dynamic in1, {String name = 'greaterThan'})
       : super((a, b) => a > b, '>', in0, in1, name: name);
 }
@@ -601,7 +704,8 @@ class GreaterThan extends _TwoInputComparisonGate {
 class LessThanOrEqual extends _TwoInputComparisonGate {
   /// Calculates whether [in0] is less than or equal to [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   LessThanOrEqual(Logic in0, dynamic in1, {String name = 'lessThanOrEqual'})
       : super((a, b) => a <= b, '<=', in0, in1, name: name);
 }
@@ -610,7 +714,8 @@ class LessThanOrEqual extends _TwoInputComparisonGate {
 class GreaterThanOrEqual extends _TwoInputComparisonGate {
   /// Calculates whether [in0] is greater than or equal to [in1].
   ///
-  /// [in1] can be either a [Logic] or [int].
+  /// [in1] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   GreaterThanOrEqual(Logic in0, dynamic in1,
       {String name = 'greaterThanOrEqual'})
       : super((a, b) => a >= b, '>=', in0, in1, name: name);
@@ -644,6 +749,9 @@ class XorUnary extends _OneInputUnaryGate {
 /// [Logic] will instead use swizzling to accomplish equivalent behavior.
 class RShift extends _ShiftGate {
   /// Calculates the value of [in_] shifted right (logically) by [shiftAmount].
+  ///
+  /// [shiftAmount] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   RShift(Logic in_, dynamic shiftAmount, {String name = 'rshift'})
       : // Note: >>> vs >> is backwards for SystemVerilog and Dart
         super((a, shamt) => a >>> shamt, '>>', in_, shiftAmount, name: name);
@@ -663,6 +771,9 @@ class RShift extends _ShiftGate {
 class ARShift extends _ShiftGate {
   /// Calculates the value of [in_] shifted right (arithmetically) by
   /// [shiftAmount].
+  ///
+  /// [shiftAmount] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   ARShift(Logic in_, dynamic shiftAmount, {String name = 'arshift'})
       : // Note: >>> vs >> is backwards for SystemVerilog and Dart
         super((a, shamt) => a >> shamt, '>>>', in_, shiftAmount,
@@ -680,8 +791,12 @@ class ARShift extends _ShiftGate {
 /// Note that many simulators do not support the SystemVerilog generated by this
 /// module when it operates on [LogicNet]s. The default shift operators on
 /// [Logic] will instead use swizzling to accomplish equivalent behavior.
+
 class LShift extends _ShiftGate {
   /// Calculates the value of [in_] shifted left by [shiftAmount].
+  ///
+  /// [shiftAmount] can be either a [Logic] or a constant be processable by
+  /// [LogicValue.of].
   LShift(Logic in_, dynamic shiftAmount, {String name = 'lshift'})
       : super((a, shamt) => a << shamt, '<<', in_, shiftAmount,
             name: name, outputSvWidthExpansion: true);
@@ -788,9 +903,8 @@ class Mux extends Module with InlineSystemVerilog {
 
   @override
   String inlineVerilog(Map<String, String> inputs) {
-    if (inputs.length != 3) {
-      throw Exception('Mux2 has exactly three inputs.');
-    }
+    assert(inputs.length == 3, 'Mux2 has exactly three inputs.');
+
     final d0 = inputs[_d0Name]!;
     final d1 = inputs[_d1Name]!;
     final control = inputs[_controlName]!;
@@ -861,9 +975,7 @@ class IndexGate extends Module with InlineSystemVerilog {
 
   @override
   String inlineVerilog(Map<String, String> inputs) {
-    if (inputs.length != 2) {
-      throw Exception('Gate has exactly two inputs.');
-    }
+    assert(inputs.length == 2, 'Gate has exactly two inputs.');
 
     final target = inputs[_originalName]!;
 
@@ -957,9 +1069,7 @@ class ReplicationOp extends Module with InlineSystemVerilog {
 
   @override
   String inlineVerilog(Map<String, String> inputs) {
-    if (inputs.length != 1) {
-      throw Exception('Gate has exactly one input.');
-    }
+    assert(inputs.length == 1, 'Gate has exactly one input.');
 
     final target = inputs[_inputName]!;
     final width = _multiplier;
diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart
index 004b5ba22..bb709fadd 100644
--- a/lib/src/signals/logic.dart
+++ b/lib/src/signals/logic.dart
@@ -1,4 +1,4 @@
-// Copyright (C) 2021-2024 Intel Corporation
+// Copyright (C) 2021-2025 Intel Corporation
 // SPDX-License-Identifier: BSD-3-Clause
 //
 // logic.dart
@@ -435,7 +435,7 @@ class Logic {
   Logic pow(dynamic exponent) => Power(this, exponent).out;
 
   /// Addition.
-  Logic operator +(dynamic other) => Add(this, other).out;
+  Logic operator +(dynamic other) => Add(this, other).sum;
 
   /// Subtraction.
   Logic operator -(dynamic other) => Subtract(this, other).out;
diff --git a/lib/src/utilities/naming.dart b/lib/src/utilities/naming.dart
index e2b6e74cc..009af00b2 100644
--- a/lib/src/utilities/naming.dart
+++ b/lib/src/utilities/naming.dart
@@ -1,4 +1,4 @@
-// Copyright (C) 2021-2023 Intel Corporation
+// Copyright (C) 2021-2025 Intel Corporation
 // SPDX-License-Identifier: BSD-3-Clause
 //
 // name_validator.dart
@@ -61,7 +61,8 @@ enum Naming {
   /// choose the other one for the final signal name.  Marking signals as
   /// "unpreferred" can have the effect of making generated output easier to
   /// read.
-  static String unpreferredName(String name) => _unpreferredPrefix + name;
+  static String unpreferredName(String name) =>
+      name.startsWith(_unpreferredPrefix) ? name : _unpreferredPrefix + name;
 
   /// Returns true iff the signal name is "unpreferred".
   ///


Test patch:
diff --git a/test/math_test.dart b/test/math_test.dart
index 89e8a13ab..d9ada00a0 100644
--- a/test/math_test.dart
+++ b/test/math_test.dart
@@ -1,4 +1,4 @@
-// Copyright (C) 2021-2023 Intel Corporation
+// Copyright (C) 2021-2025 Intel Corporation
 // SPDX-License-Identifier: BSD-3-Clause
 //
 // math_test.dart
@@ -68,11 +68,53 @@ class MathTestModule extends Module {
   }
 }
 
+class AddWithCarryMod extends Module {
+  AddWithCarryMod(Logic a, Logic b) {
+    a = addInput('a', a, width: a.width);
+    b = addInput('b', b, width: a.width);
+    final sum = addOutput('sum', width: a.width);
+    final carry = addOutput('carry');
+
+    final adder = Add(a, b);
+    sum <= adder.sum;
+    carry <= adder.carry;
+  }
+}
+
 void main() {
   tearDown(() async {
     await Simulator.reset();
   });
 
+  group('add with carry', () {
+    test('sv gen', () async {
+      final mod = AddWithCarryMod(Logic(width: 8), Logic(width: 8));
+      await mod.build();
+
+      final sv = mod.generateSynth();
+
+      expect(sv, contains('assign {carry, sum} = a + b'));
+    });
+
+    test('simcompare', () async {
+      final mod = AddWithCarryMod(Logic(width: 8), Logic(width: 8));
+      await mod.build();
+
+      final vectors = [
+        Vector({'a': 0, 'b': 0}, {'sum': 0, 'carry': 0}),
+        Vector({'a': 0, 'b': 1}, {'sum': 1, 'carry': 0}),
+        Vector({'a': 1, 'b': 0}, {'sum': 1, 'carry': 0}),
+        Vector({'a': 0xff, 'b': 1}, {'sum': 0, 'carry': 1}),
+        Vector({'a': 0xff, 'b': 0xff}, {'sum': 0xfe, 'carry': 1}),
+        Vector({'a': 0xfe, 'b': 0xff}, {'sum': 0xfd, 'carry': 1}),
+        Vector({'a': 0xff, 'b': 0xfe}, {'sum': 0xfd, 'carry': 1}),
+      ];
+
+      await SimCompare.checkFunctionalVector(mod, vectors);
+      SimCompare.checkIverilogVector(mod, vectors);
+    });
+  });
+
   test('sv expansion handles lint issues', () async {
     final gtm = MathTestModule(Logic(width: 8), Logic(width: 8));
     await gtm.build();
@@ -80,16 +122,6 @@ void main() {
     final sv = gtm.generateSynth();
     final lines = sv.split('\n');
 
-    // should never assign directly off a +
-    expect(lines.where(RegExp(r'plus.*\+').hasMatch), isEmpty);
-
-    // ensure the width of intermediate signals appropriate for add
-    for (final line in lines) {
-      if (RegExp('logic.*a_add').hasMatch(line)) {
-        expect(line, contains('8:0'));
-      }
-    }
-
     // ensure we never lshift by a constant directly
     for (final line in lines) {
       if (RegExp('assign.*a_sl_const.*=').hasMatch(line)) {