Repository: intel/rohd (dart)

Issue:
Non-synthesizable APIs on `LogicStructure` rely on `packed`
### Describe the bug

Non-synthesizable APIs such as `posedge` or `value` depend on the result of `packed` to compute their value.  This is generally accurate, but has two major downsides:
- Calling `packed` creates and connects a new signal.  If a testbench calls `packed` (directly or indirectly) on a signal within a `Module`, it will generate some additional hardware (though it won't have any functional impact).
- The `previousValue` will be `null` the first time it is called since `packed` is lazily initialized.  This can cause surprising testbench failures.

### To Reproduce

Call `previousValue` on a `LogicStructure` and observe that it is `null`.

### Expected behavior

The APIs should not have side effects of creating hardware (even without functional impact) and should be functional in the same way as `Logic`.

### Actual behavior

_No response_

### Additional: Dart SDK info

_No response_

### Additional: pubspec.yaml

_No response_

### Additional: Context

_No response_

Pull request description:
<!-- Please make sure you check out the contribution guidelines before submitting a pull request! -->

## Description & Motivation

This PR addresses two categories of issues:
- Non-synthesizable (instrumentation) calls to `LogicStructure`s would call `packed` sometimes, which could result in non-functional generation impact and unexpected `null`s in things like `previousValue`.  https://github.com/intel/rohd/issues/457
- The `changed` mechanism in `Logic` did not work properly across `Simulator.reset` calls, so this PR also fixes these issues.  This was discovered during fixes to the first bullet.

## Related Issue(s)

Fix #457 

## Testing

Added new tests

## 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?

No

Patch:
diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart
index 0a5a2510d..47697c66a 100644
--- a/lib/src/signals/logic_structure.dart
+++ b/lib/src/signals/logic_structure.dart
@@ -281,10 +281,12 @@ class LogicStructure implements Logic {
   Logic? get srcConnection => null;
 
   @override
-  LogicValue get value => packed.value;
+  LogicValue get value =>
+      elements.map((e) => e.value).toList(growable: false).rswizzle();
 
   @override
-  LogicValue? get previousValue => packed.previousValue;
+  LogicValue? get previousValue =>
+      elements.map((e) => e.value).toList(growable: false).rswizzle();
 
   @override
   late final int width = elements.map((e) => e.width).sum;
@@ -396,10 +398,32 @@ class LogicStructure implements Logic {
   @Deprecated('Use `value` instead.'
       '  Check `width` separately to confirm single-bit.')
   @override
+  // Can rely on `packed` here because it must be 1 bit.
   LogicValue get bit => packed.bit;
 
   @override
-  Stream<LogicValueChanged> get changed => packed.changed;
+  late final Stream<LogicValueChanged> changed = _internalPacked.changed;
+
+  /// An internal version of [packed] for instrumentation operations on this
+  /// [LogicStructure].
+  late final _internalPacked = _generateInternalPacked();
+
+  /// Generates and subscribes to be stored lazily into [_internalPacked].
+  Logic _generateInternalPacked() {
+    final internalPacked = Logic(width: width)..put(value);
+
+    void updateInternalPacked() {
+      internalPacked.put(value);
+    }
+
+    for (final element in elements) {
+      element.glitch.listen((args) {
+        updateInternalPacked();
+      });
+    }
+
+    return internalPacked;
+  }
 
   @override
   Logic eq(dynamic other) => packed.eq(other);
@@ -408,7 +432,8 @@ class LogicStructure implements Logic {
   Logic neq(dynamic other) => packed.neq(other);
 
   @override
-  SynchronousEmitter<LogicValueChanged> get glitch => packed.glitch;
+  late final SynchronousEmitter<LogicValueChanged> glitch =
+      _internalPacked.glitch;
 
   @override
   Logic gt(dynamic other) => packed.gt(other);
@@ -424,28 +449,32 @@ class LogicStructure implements Logic {
 
   @Deprecated('Use value.isValid instead.')
   @override
-  bool hasValidValue() => packed.hasValidValue();
+  bool hasValidValue() => value.isValid;
 
   @Deprecated('Use value.isFloating instead.')
   @override
-  bool isFloating() => packed.isFloating();
+  bool isFloating() => value.isFloating;
 
   @override
   Logic isIn(List<dynamic> list) => packed.isIn(list);
 
   @override
+  // Can rely on `packed` here because it must be 1 bit.
   Stream<LogicValueChanged> get negedge => packed.negedge;
 
   @override
+  // Can rely on `packed` here because it must be 1 bit.
   Stream<LogicValueChanged> get posedge => packed.posedge;
 
   @override
-  Future<LogicValueChanged> get nextChanged => packed.nextChanged;
+  Future<LogicValueChanged> get nextChanged => changed.first;
 
   @override
+  // Can rely on `packed` here because it must be 1 bit.
   Future<LogicValueChanged> get nextNegedge => packed.nextNegedge;
 
   @override
+  // Can rely on `packed` here because it must be 1 bit.
   Future<LogicValueChanged> get nextPosedge => packed.nextPosedge;
 
   @override
@@ -460,13 +489,14 @@ class LogicStructure implements Logic {
   @override
   Logic zeroExtend(int newWidth) => packed.zeroExtend(newWidth);
 
+  @Deprecated('Use `value` instead.'
+      '  Check `width` separately to confirm single-bit.')
   @override
-  // ignore: deprecated_member_use_from_same_package
-  BigInt get valueBigInt => packed.valueBigInt;
+  BigInt get valueBigInt => value.toBigInt();
 
+  @Deprecated('Use value.toInt() instead.')
   @override
-  // ignore: deprecated_member_use_from_same_package
-  int get valueInt => packed.valueInt;
+  int get valueInt => value.toInt();
 
   @override
   Logic? get _srcConnection => throw UnsupportedError('Delegated to elements');
diff --git a/lib/src/signals/wire.dart b/lib/src/signals/wire.dart
index 24cd51184..17ab3c5f6 100644
--- a/lib/src/signals/wire.dart
+++ b/lib/src/signals/wire.dart
@@ -55,11 +55,26 @@ class _Wire {
       // them! saves performance!
       _changedBeingWatched = true;
 
-      _postTickSubscription ??= Simulator.postTick.listen((event) {
-        if (value != _preTickValue && _preTickValue != null) {
-          _changedController.add(LogicValueChanged(value, _preTickValue!));
-        }
-      });
+      StreamSubscription<void> subscribe() {
+        unawaited(Simulator.resetRequested.then((_) async {
+          final oldPostTickSubscription = _postTickSubscription;
+
+          _postTickSubscription = subscribe();
+
+          unawaited(oldPostTickSubscription?.cancel());
+        }));
+
+        return Simulator.postTick.listen((event) {
+          if (value != _preTickValue && _preTickValue != null) {
+            _changedController.add(LogicValueChanged(value, _preTickValue!));
+          }
+        });
+      }
+
+      assert(_postTickSubscription == null,
+          'Should not be creating a new subscription if one exists.');
+
+      _postTickSubscription = subscribe();
     }
     return _changedController.stream;
   }
@@ -68,9 +83,22 @@ class _Wire {
   ///
   /// If one already exists, it will not create a new one.
   void _setupPreTickListener() {
-    _preTickSubscription ??= Simulator.preTick.listen((event) {
+    _preTickSubscription = Simulator.preTick.listen((event) {
       _preTickValue = value;
     });
+
+    unawaited(Simulator.resetRequested.then((_) async {
+      assert(_preTickSubscription != null,
+          'Should not be null if we are setting up a new one.');
+
+      _preTickValue = value;
+
+      final oldPreTickSubscription = _preTickSubscription;
+
+      _setupPreTickListener();
+
+      unawaited(oldPreTickSubscription?.cancel());
+    }));
   }
 
   /// The [value] of this signal before the most recent [Simulator.tick] had
diff --git a/lib/src/simulator.dart b/lib/src/simulator.dart
index 0cd8c2532..f115a55f4 100644
--- a/lib/src/simulator.dart
+++ b/lib/src/simulator.dart
@@ -89,25 +89,29 @@ abstract class Simulator {
 
   /// Emits an event before any other actions take place on the tick.
   static Stream<void> get preTick => _preTickController.stream;
+  static StreamController<void> _preTickController =
+      StreamController.broadcast(sync: true);
 
   /// Emits an event at the start of actions within a tick.
   static Stream<void> get startTick => _startTickController.stream;
+  static StreamController<void> _startTickController =
+      StreamController.broadcast(sync: true);
 
   /// Emits an event when most events are complete, and clocks are stable.
   static Stream<void> get clkStable => _clkStableController.stream;
+  static StreamController<void> _clkStableController =
+      StreamController.broadcast(sync: true);
 
   /// Emits an event after all events are completed.
   static Stream<void> get postTick => _postTickController.stream;
-
-  static StreamController<void> _preTickController =
-      StreamController.broadcast(sync: true);
-  static StreamController<void> _startTickController =
-      StreamController.broadcast(sync: true);
-  static StreamController<void> _clkStableController =
-      StreamController.broadcast(sync: true);
   static StreamController<void> _postTickController =
       StreamController.broadcast(sync: true);
 
+  /// Completes when [reset] is called after the [Simulator] has completed any
+  /// actions it needs to perform to prepare for the next simulation.
+  static Future<void> get resetRequested => _resetCompleter.future;
+  static Completer<void> _resetCompleter = Completer<void>();
+
   /// Completes when the simulation has completed.
   static Future<void> get simulationEnded => _simulationEndedCompleter.future;
   static Completer<void> _simulationEndedCompleter = Completer<void>();
@@ -156,6 +160,13 @@ abstract class Simulator {
     _pendingTimestamps.clear();
     _phase = SimulatorPhase.outOfTick;
     _injectedActions.clear();
+
+    // make sure we've already passed the new completer so that listeners can
+    // get the latest
+    final oldResetCompleter = _resetCompleter;
+    _resetCompleter = Completer();
+    oldResetCompleter.complete();
+    await oldResetCompleter.future;
   }
 
   /// Sets a time, after which, the [Simulator] will halt processing of new


Test patch:
diff --git a/test/changed_test.dart b/test/changed_test.dart
index 4313cfbaa..808d59403 100644
--- a/test/changed_test.dart
+++ b/test/changed_test.dart
@@ -39,6 +39,79 @@ void main() {
     expect(numChangesDetected, equals(1));
   });
 
+  group('across simulator reset', () {
+    Future<void> testAcrossSimulatorResets(Logic a) async {
+      var numChangesDetected = 0;
+
+      a.changed.listen((event) {
+        numChangesDetected++;
+        expect(event.newValue, event.previousValue + 1);
+      });
+
+      a.glitch.listen((args) {});
+
+      Simulator.registerAction(10, () => a.put(1));
+
+      await Simulator.run();
+
+      expect(numChangesDetected, 1);
+
+      await Simulator.reset();
+
+      expect(a.value.toInt(), 1);
+
+      // should *not* trigger a change, didn't change!
+      Simulator.registerAction(20, () => a.put(1));
+
+      await Simulator.run();
+
+      expect(numChangesDetected, 1);
+
+      await Simulator.reset();
+
+      expect(a.value.toInt(), 1);
+
+      // *should* trigger a change
+      Simulator.registerAction(30, () => a.put(2));
+      Simulator.registerAction(40, () => a.put(3));
+
+      await Simulator.run();
+
+      expect(a.value.toInt(), 3);
+
+      expect(numChangesDetected, 3);
+    }
+
+    test('normal logic', () async {
+      final a = Logic(width: 8)..put(0);
+
+      await testAcrossSimulatorResets(a);
+    });
+
+    test('logic structure', () async {
+      final a = LogicStructure([Logic(), Logic()])..put(0);
+
+      await testAcrossSimulatorResets(a);
+    });
+  });
+
+  test('changed does not trigger first time if no change', () async {
+    final a = Logic()..put(1);
+
+    var numChangesDetected = 0;
+
+    a.changed.listen((event) {
+      numChangesDetected++;
+    });
+
+    Simulator.registerAction(10, () => a.put(1));
+    Simulator.registerAction(20, () => a.put(1));
+
+    await Simulator.run();
+
+    expect(numChangesDetected, 0);
+  });
+
   test('clk edge counter', () async {
     final clk = SimpleClockGenerator(10).clk;
     final b = Logic();
diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart
index 94705f243..aa8724e27 100644
--- a/test/logic_structure_test.dart
+++ b/test/logic_structure_test.dart
@@ -7,6 +7,8 @@
 // 2023 May 5
 // Author: Max Korbel <max.korbel@intel.com>
 
+import 'dart:async';
+
 import 'package:rohd/rohd.dart';
 import 'package:rohd/src/utilities/simcompare.dart';
 import 'package:test/test.dart';
@@ -70,11 +72,81 @@ class FancyStructInverter extends Module {
   }
 }
 
+class StructModuleWithInstrumentation extends Module {
+  StructModuleWithInstrumentation(Logic a) {
+    a = addInput('a', a, width: 2);
+
+    MyStruct()
+      ..gets(a)
+      ..value
+      ..previousValue
+      ..width
+      ..srcConnection
+      ..dstConnections
+      ..parentModule
+      ..parentStructure
+      ..naming
+      ..arrayIndex
+      ..isArrayMember
+      ..leafElements
+      ..isInput
+      ..isOutput
+      ..changed
+      ..glitch
+      ..nextChanged
+      // ignore: deprecated_member_use_from_same_package
+      ..hasValidValue()
+      // ignore: deprecated_member_use_from_same_package
+      ..isFloating()
+      // ignore: deprecated_member_use_from_same_package
+      ..valueBigInt
+      // ignore: deprecated_member_use_from_same_package
+      ..valueInt;
+  }
+}
+
 void main() {
   tearDown(() async {
     await Simulator.reset();
   });
 
+  test('late previousValue', () async {
+    final s = MyStruct();
+    final clk = SimpleClockGenerator(10).clk;
+    Simulator.setMaxSimTime(200);
+
+    var i = 0;
+    clk.posedge.listen((_) => s.inject(i++));
+
+    unawaited(Simulator.run());
+
+    await clk.nextPosedge;
+    await clk.nextPosedge;
+    await clk.nextPosedge;
+
+    expect(s.previousValue, isNotNull);
+    expect(s.previousValue!.toInt(), 1);
+
+    s.packed;
+    await clk.nextPosedge;
+    await clk.nextPosedge;
+    expect(s.packed.previousValue, s.previousValue);
+    await clk.nextPosedge;
+    expect(s.packed.previousValue, s.previousValue);
+
+    await Simulator.endSimulation();
+  });
+
+  test('instrumentation on struct does not make hardware', () async {
+    final mod = StructModuleWithInstrumentation(Const(0, width: 2));
+    await mod.build();
+
+    final sv = mod.generateSynth();
+
+    expect(sv.contains('swizzle'), isFalse,
+        reason: 'Should not pack from instrumentation!');
+  });
+
   group('LogicStructure construction', () {
     test('simple construction', () {
       final s = LogicStructure([
diff --git a/tool/gh_actions/run_tests.sh b/tool/gh_actions/run_tests.sh
index 4f82a45a3..d426240f3 100755
--- a/tool/gh_actions/run_tests.sh
+++ b/tool/gh_actions/run_tests.sh
@@ -13,5 +13,6 @@ set -euo pipefail
 
 dart test
 
-# run tests in JS
+# run tests in JS (increase heap size also)
+export NODE_OPTIONS="--max-old-space-size=6144"
 dart test --platform node
\ No newline at end of file