Repository: intel/rohd (dart)

Issue:
radixString should have an option to produce fixed-length strings
### Motivation

In some formatting cases, a fixed length string makes sense for alignment or for future parsing.

So instead of 16h'1,  we may need to have preceding zeros to pad the string like 16h'0001.
Furthermore, this may allow for printing just the raw data in a string like "0001" when you know it will be parsed with a radix argument.  This creates a situation where output can be aligned independent of value.
For binary, this is often the case where numeric values (mantissas) are examined in aligned form.

### Desired solution

Modify LogicValue.toRadixString to have an option that allows for preceding zero padding.
LogicValue.ofRadixString can already process padded strings.

### 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

Enable printing of leading zeros to get fixed-width output to help with alignment during printing and for parsing
from strings without having to use the size prefix, just passing the radix along with a set of strings.

## Related Issue(s)

#582 

## Testing

Added tests that enabled the leadingZero option and checked that they match as well as not breaking existing tests.

## Backwards-compatibility

> Is this a breaking change that will not be backwards-compatible? If yes, how so?

Yes.  This option is off by default.

## Documentation

> Does the change require any updates to documentation? If so, where? Are they included?

Yes.  An example was added to one of the user guide sections and the API for the method was updated.

Patch:
diff --git a/doc/user_guide/_docs/A02-logical_signals.md b/doc/user_guide/_docs/A02-logical_signals.md
index 10f59d348..4b038efd4 100644
--- a/doc/user_guide/_docs/A02-logical_signals.md
+++ b/doc/user_guide/_docs/A02-logical_signals.md
@@ -37,7 +37,7 @@ x.value.toInt()
 x.value.toBigInt()
 
 // constructing a LogicValue a handful of different ways
-LogicValue.ofRadixString("31'h5761 F87A");            // 0x5761F87A
+LogicValue.ofRadixString("31'h5761_F87A");            // 0x5761F87A
 LogicValue.ofString('0101xz01');                      // 0b0101xz01
 LogicValue.of([LogicValue.one, LogicValue.zero]);     // 0b10
 [LogicValue.z, LogicValue.x].swizzle();               // 0bzx
diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart
index cc9ce8c90..1f2aa0bd4 100644
--- a/lib/src/values/logic_value.dart
+++ b/lib/src/values/logic_value.dart
@@ -632,8 +632,11 @@ abstract class LogicValue implements Comparable<LogicValue> {
   ///  - [chunkSize] = default: `61'h2_9ebc_5f06_5bf7`
   ///  - [chunkSize] = 10: `61'h29e_bc5f065bf7`
   ///
-  /// Leading 0s are omitted in the output string:
+  /// [leadingZeros] defaults to false, so leading 0s are omitted in
+  /// the output string:
   /// - `25'h1`
+  /// otherwise if [leadingZeros] is set to true then the output string is:
+  /// - `25'h000_0001`
   ///
   /// When a [LogicValue] has 'x' or 'z' bits, then the radix characters those
   /// bits overlap will be expanded into binary form with '<' '>' bracketing
@@ -648,7 +651,10 @@ abstract class LogicValue implements Comparable<LogicValue> {
   /// - `9'bz_zzzz_zzzz = 9'hZZZ`
   ///
   String toRadixString(
-      {int radix = 2, int chunkSize = 4, String sepChar = '_'}) {
+      {int radix = 2,
+      int chunkSize = 4,
+      bool leadingZeros = false,
+      String sepChar = '_'}) {
     if (radixStringChars.contains(sepChar)) {
       throw LogicValueConversionException('separation character invalid');
     }
@@ -661,22 +667,33 @@ abstract class LogicValue implements Comparable<LogicValue> {
       _ => throw LogicValueConversionException('Unsupported radix: $radix')
     };
     final String reversedStr;
-    if (isValid) {
-      final radixString =
-          toBigInt().toUnsigned(width).toRadixString(radix).toUpperCase();
-      reversedStr = _reverse(radixString);
-    } else if (radix == 10) {
-      final span = (width * math.log(2) / math.log(radix)).floor();
-      if (toRadixString().contains(RegExp('[xX]'))) {
-        reversedStr = 'X' * span;
+    if (radix == 10) {
+      if (isValid) {
+        var radixString =
+            toBigInt().toUnsigned(width).toRadixString(radix).toUpperCase();
+        if (leadingZeros) {
+          final span =
+              math.max(1, (width * math.log(2) / math.log(radix)).floor());
+          for (var i = radixString.length; i < (width / span).ceil(); i++) {
+            radixString = '0$radixString';
+          }
+        }
+        reversedStr = _reverse(radixString);
       } else {
-        reversedStr = 'Z' * span;
+        final span =
+            math.max(1, (width * math.log(2) / math.log(radix)).floor());
+        if (toRadixString().contains(RegExp('[xX]'))) {
+          reversedStr = 'X' * span;
+        } else {
+          reversedStr = 'Z' * span;
+        }
       }
     } else {
       final span = (math.log(radix) / math.log(2)).ceil();
       final extendedStr =
           LogicValue.of(this, width: span * (width / span).ceil());
       final buf = StringBuffer();
+      var haveLeadingZeros = true;
       for (var i = (extendedStr.width ~/ span) - 1; i >= 0; i--) {
         final binaryChunk = extendedStr.slice((i + 1) * span - 1, i * span);
         var chunkString = binaryChunk.toString(includeWidth: false);
@@ -695,10 +712,17 @@ abstract class LogicValue implements Comparable<LogicValue> {
           else
             binaryChunk.toBigInt().toUnsigned(span).toRadixString(radix)
         ].first;
+        if (s != '0') {
+          haveLeadingZeros = false;
+        }
+        if ((s == '0') & !leadingZeros & haveLeadingZeros) {
+          continue;
+        }
         buf.write(_reverse(s));
       }
       reversedStr = _reverse(buf.toString());
     }
+
     final spaceString = _reverse(reversedStr
         .replaceAllMapped(
             RegExp('((>(.){$chunkSize}<)|([a-zA-Z0-9])){$chunkSize}'),
@@ -739,10 +763,10 @@ abstract class LogicValue implements Comparable<LogicValue> {
   /// If the LogicValue width is not encoded as round number of radix
   /// characters, the leading character must be small enough to be encoded
   /// in the remaining width:
-  ///  - 9'h1AA
-  ///  - 10'h2AA
-  ///  - 11'h4AA
-  ///  - 12'hAAA
+  ///  - 9'h1aa
+  ///  - 10'h2aa
+  ///  - 11'h4aa
+  ///  - 12'haa
   static LogicValue ofRadixString(String valueString, {String sepChar = '_'}) {
     if (radixStringChars.contains(sepChar)) {
       throw LogicValueConstructionException('separation character invalid');


Test patch:
diff --git a/test/logic_value_test.dart b/test/logic_value_test.dart
index 9f19111c3..7dbaf074a 100644
--- a/test/logic_value_test.dart
+++ b/test/logic_value_test.dart
@@ -2057,6 +2057,15 @@ void main() {
             LogicValue.ofRadixString(lv.toRadixString(radix: i)), equals(lv));
       }
     });
+    test('radixString roundTrip with leading zeros', () {
+      final lv = LogicValue.ofBigInt(BigInt.from(737481838713847), 61);
+      for (final i in [2, 4, 8, 10, 16]) {
+        expect(
+            LogicValue.ofRadixString(
+                lv.toRadixString(radix: i, leadingZeros: true)),
+            equals(lv));
+      }
+    });
     test('radixString binary expansion', () {
       final lv = LogicValue.ofRadixString("12'b10z111011z00");
       expect(lv.toRadixString(radix: 16), equals("12'h<10z1>d<1z00>"));
@@ -2069,10 +2078,15 @@ void main() {
     test('radixString leading zero', () {
       final lv = LogicValue.ofRadixString("10'b00_0010_0111");
       expect(lv.toRadixString(), equals("10'b10_0111"));
+      expect(lv.toRadixString(leadingZeros: true), equals("10'b00_0010_0111"));
       expect(lv.toRadixString(radix: 4), equals("10'q213"));
       expect(lv.toRadixString(radix: 8), equals("10'o47"));
       expect(lv.toRadixString(radix: 10), equals("10'd39"));
+      expect(
+          lv.toRadixString(radix: 10, leadingZeros: true), equals("10'd0039"));
       expect(lv.toRadixString(radix: 16), equals("10'h27"));
+      expect(
+          lv.toRadixString(radix: 16, leadingZeros: true), equals("10'h027"));
       for (final i in [2, 4, 8, 10, 16]) {
         expect(
             LogicValue.ofRadixString(lv.toRadixString(radix: i)), equals(lv));
@@ -2164,7 +2178,7 @@ void main() {
       expect(lv.toRadixString(radix: 4), equals("10'q2_2213"));
       expect(lv.toRadixString(radix: 8), equals("10'o1247"));
       expect(lv.toRadixString(radix: 10), equals("10'd679"));
-      expect(lv.toRadixString(radix: 16), equals("10'h2A7"));
+      expect(lv.toRadixString(radix: 16), equals("10'h2a7"));
       for (final i in [2, 4, 8, 10, 16]) {
         expect(
             LogicValue.ofRadixString(lv.toRadixString(radix: i)), equals(lv));
@@ -2178,7 +2192,6 @@ void main() {
         for (var setWidth = 1; setWidth < 12; setWidth++) {
           for (var iterations = 0; iterations < 10; iterations++) {
             final ii = random.nextInt((1 << (setWidth + 1)) - 1);
-
             for (var pos = 0; pos < inL.width - setWidth; pos++) {
               final l = Logic(width: width);
               l <= inL.withSet(pos, Const(ii, width: setWidth));