Repository: intel/rohd
Pull request #199 (2022-11-15 22:15:45)

`_Wire`s underneath `Logic`s
<!-- Please make sure you check out the contribution guidelines before submitting a pull request! -->

## Description & Motivation

Refactored `Logic` to hold its value on a `_Wire` which can be shared across many `Logic`s that are connected to each other.  This has two big benefits:
- Improved simulation performance by reducing the number of `glitch`es that need to propagate during combinational logic
- Decreases the stack requirements to help mitigate this issue: #194

## Related Issue(s)

Helps mitigate #194

## Testing

- Added new tests where corner cases could have been introduced around combinational logic propagation and edge listening.
- Ran the (limited) existing benchmark suite and observed slight performance improvement.  The existing pipeline benchmark is not sufficient probably to show the true benefit for larger designs, though.

## Backwards-compatibility

> Is this a breaking change that will not be backwards-compatible?  If yes, how so?

Sort of, `posedge` and `negedge` will now throw an `Exception` if the width is not 1.

## Documentation

> Does the change require any updates to documentation?  If so, where?  Are they included?

No


eric-norige: This refactor seems good to me.  Is there any measurement on sim performance speed improvement?  Do we need a benchmark suite of designs and stimulus to simulate to identify progress/regressions on simulation performance?
mkorbel1: > Is there any measurement on sim performance speed improvement?

The one benchmark that's currently in the repo showed some modest relative improvement in simulation performance, but it's not a very meaningful measurement or dramatic difference.

> Do we need a benchmark suite of designs and stimulus to simulate to identify progress/regressions on simulation performance?

Yes!  I don't have them available and haven't had time to develop them, though.  If you (or anyone else) is interested in developing some or contributing existing things that could be reused as benchmarks it would be greatly valuable!

Patch:
diff --git a/lib/src/logic.dart b/lib/src/logic.dart
index 12ecc0df..f5b00436 100644
--- a/lib/src/logic.dart
+++ b/lib/src/logic.dart
@@ -26,7 +26,7 @@ class LogicValueChanged {
 
   /// Represents the event of a [Logic] changing value from [previousValue]
   /// to [newValue].
-  LogicValueChanged(this.newValue, this.previousValue);
+  const LogicValueChanged(this.newValue, this.previousValue);
 
   @override
   String toString() => '$previousValue  -->  $newValue';
@@ -49,12 +49,245 @@ class Const extends Logic {
   }
 }
 
+/// Represents a physical wire which shares a common value with one or
+/// more [Logic]s.
+class _Wire {
+  _Wire({required this.width})
+      : _currentValue = LogicValue.filled(width, LogicValue.z);
+
+  /// The current active value of this signal.
+  LogicValue get value => _currentValue;
+
+  /// The number of bits in this signal.
+  final int width;
+
+  /// The current active value of this signal.
+  LogicValue _currentValue;
+
+  /// The last value of this signal before the [Simulator] tick.
+  ///
+  /// This is useful for detecting when to trigger an edge.
+  LogicValue? _preTickValue;
+
+  /// A stream of [LogicValueChanged] events for every time the signal
+  /// transitions at any time during a [Simulator] tick.
+  ///
+  /// This event can occur more than once per edge, or even if there is no edge.
+  SynchronousEmitter<LogicValueChanged> get glitch => _glitchController.emitter;
+  final SynchronousPropagator<LogicValueChanged> _glitchController =
+      SynchronousPropagator<LogicValueChanged>();
+
+  /// Controller for stable events that can be safely consumed at the
+  /// end of a [Simulator] tick.
+  final StreamController<LogicValueChanged> _changedController =
+      StreamController<LogicValueChanged>.broadcast(sync: true);
+
+  /// Tracks whether is being subscribed to by anything/anyone.
+  bool _changedBeingWatched = false;
+
+  /// A [Stream] of [LogicValueChanged] events which triggers at most once
+  /// per [Simulator] tick, iff the value of the [Logic] has changed.
+  Stream<LogicValueChanged> get changed {
+    if (!_changedBeingWatched) {
+      // only do these simulator subscriptions if someone has asked for
+      // them! saves performance!
+      _changedBeingWatched = true;
+
+      _preTickSubscription = Simulator.preTick.listen((event) {
+        _preTickValue = value;
+      });
+      _postTickSubscription = Simulator.postTick.listen((event) {
+        if (value != _preTickValue && _preTickValue != null) {
+          _changedController.add(LogicValueChanged(value, _preTickValue!));
+        }
+      });
+    }
+    return _changedController.stream;
+  }
+
+  /// The subscription to the [Simulator]'s `preTick`.
+  ///
+  /// Only non-null if [_changedBeingWatched] is true.
+  late final StreamSubscription<void> _preTickSubscription;
+
+  /// The subscription to the [Simulator]'s `postTick`.
+  ///
+  /// Only non-null if [_changedBeingWatched] is true.
+  late final StreamSubscription<void> _postTickSubscription;
+
+  /// Cancels all [Simulator] subscriptions and uses [newChanged] as the
+  /// source to replace all [changed] events for this [_Wire].
+  void _migrateChangedTriggers(Stream<LogicValueChanged> newChanged) {
+    if (_changedBeingWatched) {
+      unawaited(_preTickSubscription.cancel());
+      unawaited(_postTickSubscription.cancel());
+      newChanged.listen(_changedController.add);
+      _changedBeingWatched = false;
+    }
+  }
+
+  /// Tells this [_Wire] to adopt all the behavior of [other] so that
+  /// it can replace [other].
+  void _adopt(_Wire other) {
+    _glitchController.emitter.adopt(other._glitchController.emitter);
+    other._migrateChangedTriggers(changed);
+  }
+
+  /// Store the [negedge] stream to avoid creating multiple copies
+  /// of streams.
+  Stream<LogicValueChanged>? _negedge;
+
+  /// Store the [posedge] stream to avoid creating multiple copies
+  /// of streams.
+  Stream<LogicValueChanged>? _posedge;
+
+  /// A [Stream] of [LogicValueChanged] events which triggers at most once
+  /// per [Simulator] tick, iff the value of the [Logic] has changed
+  /// from `1` to `0`.
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Stream<LogicValueChanged> get negedge {
+    if (width != 1) {
+      throw Exception(
+          'Can only detect negedge when width is 1, but was $width');
+    }
+
+    _negedge ??= changed.where((args) => LogicValue.isNegedge(
+          args.previousValue,
+          args.newValue,
+          ignoreInvalid: true,
+        ));
+
+    return _negedge!;
+  }
+
+  /// A [Stream] of [LogicValueChanged] events which triggers at most once
+  /// per [Simulator] tick, iff the value of the [Logic] has changed
+  /// from `0` to `1`.
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Stream<LogicValueChanged> get posedge {
+    if (width != 1) {
+      throw Exception(
+          'Can only detect posedge when width is 1, but was $width');
+    }
+
+    _posedge ??= changed.where((args) => LogicValue.isPosedge(
+          args.previousValue,
+          args.newValue,
+          ignoreInvalid: true,
+        ));
+
+    return _posedge!;
+  }
+
+  /// Triggers at most once, the next time that this [Logic] changes
+  /// value at the end of a [Simulator] tick.
+  Future<LogicValueChanged> get nextChanged => changed.first;
+
+  /// Triggers at most once, the next time that this [Logic] changes
+  /// value at the end of a [Simulator] tick from `0` to `1`.
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Future<LogicValueChanged> get nextPosedge => posedge.first;
+
+  /// Triggers at most once, the next time that this [Logic] changes
+  /// value at the end of a [Simulator] tick from `1` to `0`.
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Future<LogicValueChanged> get nextNegedge => negedge.first;
+
+  /// Injects a value onto this signal in the current [Simulator] tick.
+  ///
+  /// This function calls [put()] in [Simulator.injectAction()].
+  void inject(dynamic val, {bool fill = false}) {
+    Simulator.injectAction(() => put(val, fill: fill));
+  }
+
+  /// Keeps track of whether there is an active put, to detect reentrance.
+  bool _isPutting = false;
+
+  /// Puts a value [val] onto this signal, which may or may not be picked up
+  /// for [changed] in this [Simulator] tick.
+  ///
+  /// The type of [val] should be an `int`, [BigInt], `bool`, or [LogicValue].
+  ///
+  /// This function is used for propogating glitches through connected signals.
+  /// Use this function for custom definitions of [Module] behavior.
+  ///
+  /// If [fill] is set, all bits of the signal gets set to [val], similar
+  /// to `'` in SystemVerilog.
+  void put(dynamic val, {bool fill = false}) {
+    LogicValue newValue;
+    if (val is int) {
+      if (fill) {
+        newValue = LogicValue.filled(
+            width,
+            val == 0
+                ? LogicValue.zero
+                : val == 1
+                    ? LogicValue.one
+                    : throw Exception('Only can fill 0 or 1, but saw $val.'));
+      } else {
+        newValue = LogicValue.ofInt(val, width);
+      }
+    } else if (val is BigInt) {
+      if (fill) {
+        newValue = LogicValue.filled(
+            width,
+            val == BigInt.zero
+                ? LogicValue.zero
+                : val == BigInt.one
+                    ? LogicValue.one
+                    : throw Exception('Only can fill 0 or 1, but saw $val.'));
+      } else {
+        newValue = LogicValue.ofBigInt(val, width);
+      }
+    } else if (val is bool) {
+      newValue = LogicValue.ofInt(val ? 1 : 0, width);
+    } else if (val is LogicValue) {
+      if (val.width == 1 &&
+          (val == LogicValue.x || val == LogicValue.z || fill)) {
+        newValue = LogicValue.filled(width, val);
+      } else if (fill) {
+        throw Exception(
+            'Failed to fill value with $val.  To fill, it should be 1 bit.');
+      } else {
+        newValue = val;
+      }
+    } else {
+      throw Exception('Unrecognized value "$val" to deposit on this signal. '
+          'Unknown type ${val.runtimeType} cannot be deposited.');
+    }
+
+    if (newValue.width != width) {
+      throw Exception(
+          'Updated value width mismatch.  The width of $val should be $width.');
+    }
+
+    if (_isPutting) {
+      // if this is the result of a cycle, then contention!
+      newValue = LogicValue.filled(width, LogicValue.x);
+    }
+
+    final prevValue = _currentValue;
+    _currentValue = newValue;
+
+    // sends out a glitch if the value deposited has changed
+    if (_currentValue != prevValue) {
+      _isPutting = true;
+      _glitchController.add(LogicValueChanged(_currentValue, prevValue));
+      _isPutting = false;
+    }
+  }
+}
+
 /// Represents a logical signal of any width which can change values.
 class Logic {
   /// An internal counter for encouraging unique naming of unnamed signals.
   static int _signalIdx = 0;
 
-  // special quiet flag to prevent <= and < where inappropriate
+  // A special quiet flag to prevent `<=` and `<` where inappropriate
   bool _unassignable = false;
 
   /// Makes it so that this signal cannot be assigned by any full (`<=`) or
@@ -64,19 +297,16 @@ class Logic {
   /// The name of this signal.
   final String name;
 
-  /// The current active value of this signal.
-  LogicValue _currentValue;
-
-  /// The last value of this signal before the [Simulator] tick.
+  /// The [_Wire] which holds the current value and listeners for this [Logic].
   ///
-  /// This is useful for detecting when to trigger an edge.
-  LogicValue? _preTickValue;
+  /// May be a shared object between multiple [Logic]s.
+  _Wire _wire;
 
   /// The number of bits in this signal.
-  final int width;
+  int get width => _wire.width;
 
   /// The current active value of this signal.
-  LogicValue get value => _currentValue;
+  LogicValue get value => _wire._currentValue;
 
   /// The current active value of this signal if it has width 1, as
   /// a [LogicValue].
@@ -84,7 +314,7 @@ class Logic {
   /// Throws an Exception if width is not 1.
   @Deprecated('Use `value` instead.'
       '  Check `width` separately to confirm single-bit.')
-  LogicValue get bit => _currentValue.bit;
+  LogicValue get bit => value.bit;
 
   /// The current valid active value of this signal as an [int].
   ///
@@ -100,7 +330,7 @@ class Logic {
   BigInt get valueBigInt => value.toBigInt();
 
   /// Returns `true` iff the value of this signal is valid (no `x` or `z`).
-  bool hasValidValue() => _currentValue.isValid;
+  bool hasValidValue() => value.isValid;
 
   /// Returns `true` iff *all* bits of the current value are floating (`z`).
   bool isFloating() => value.isFloating;
@@ -122,65 +352,41 @@ class Logic {
   /// transitions at any time during a [Simulator] tick.
   ///
   /// This event can occur more than once per edge, or even if there is no edge.
-  SynchronousEmitter<LogicValueChanged> get glitch => _glitchController.emitter;
-  final SynchronousPropagator<LogicValueChanged> _glitchController =
-      SynchronousPropagator<LogicValueChanged>();
-
-  /// Controller for stable events that can be safely consumed at the
-  /// end of a [Simulator] tick.
-  final StreamController<LogicValueChanged> _changedController =
-      StreamController<LogicValueChanged>.broadcast(sync: true);
-
-  /// Tracks whether is being subscribed to by anything/anyone.
-  bool _changedBeingWatched = false;
+  SynchronousEmitter<LogicValueChanged> get glitch => _wire.glitch;
 
   /// A [Stream] of [LogicValueChanged] events which triggers at most once
   /// per [Simulator] tick, iff the value of the [Logic] has changed.
-  Stream<LogicValueChanged> get changed {
-    if (!_changedBeingWatched) {
-      // only do these simulator subscriptions if someone has asked for
-      // them! saves performance!
-      _changedBeingWatched = true;
-
-      Simulator.preTick.listen((event) {
-        _preTickValue = value;
-      });
-      Simulator.postTick.listen((event) {
-        if (value != _preTickValue && _preTickValue != null) {
-          _changedController.add(LogicValueChanged(value, _preTickValue!));
-        }
-      });
-    }
-    return _changedController.stream;
-  }
+  Stream<LogicValueChanged> get changed => _wire.changed;
 
   /// A [Stream] of [LogicValueChanged] events which triggers at most once
   /// per [Simulator] tick, iff the value of the [Logic] has changed
   /// from `1` to `0`.
-  Stream<LogicValueChanged> get negedge => changed.where((args) =>
-      width == 1 &&
-      LogicValue.isNegedge(args.previousValue[0], args.newValue[0],
-          ignoreInvalid: true));
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Stream<LogicValueChanged> get negedge => _wire.negedge;
 
   /// A [Stream] of [LogicValueChanged] events which triggers at most once
   /// per [Simulator] tick, iff the value of the [Logic] has changed
   /// from `0` to `1`.
-  Stream<LogicValueChanged> get posedge => changed.where((args) =>
-      width == 1 &&
-      LogicValue.isPosedge(args.previousValue[0], args.newValue[0],
-          ignoreInvalid: true));
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Stream<LogicValueChanged> get posedge => _wire.posedge;
 
   /// Triggers at most once, the next time that this [Logic] changes
   /// value at the end of a [Simulator] tick.
-  Future<LogicValueChanged> get nextChanged => changed.first;
+  Future<LogicValueChanged> get nextChanged => _wire.changed.first;
 
   /// Triggers at most once, the next time that this [Logic] changes
   /// value at the end of a [Simulator] tick from `0` to `1`.
-  Future<LogicValueChanged> get nextPosedge => posedge.first;
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Future<LogicValueChanged> get nextPosedge => _wire.posedge.first;
 
   /// Triggers at most once, the next time that this [Logic] changes
   /// value at the end of a [Simulator] tick from `1` to `0`.
-  Future<LogicValueChanged> get nextNegedge => negedge.first;
+  ///
+  /// Throws an exception if [width] is not `1`.
+  Future<LogicValueChanged> get nextNegedge => _wire.negedge.first;
 
   /// The [Module] that this [Logic] exists within.
   ///
@@ -218,9 +424,9 @@ class Logic {
   ///
   /// The default value for [width] is 1.  The [name] should be synthesizable
   /// to the desired output (e.g. SystemVerilog).
-  Logic({String? name, this.width = 1})
+  Logic({String? name, int width = 1})
       : name = name == null ? 's${_signalIdx++}' : Sanitizer.sanitizeSV(name),
-        _currentValue = LogicValue.filled(width, LogicValue.z) {
+        _wire = _Wire(width: width) {
     if (width < 0) {
       throw Exception('Logic width must be greater than or equal to 0.');
     }
@@ -243,6 +449,24 @@ class Logic {
     }
   }
 
+  /// Injects a value onto this signal in the current [Simulator] tick.
+  ///
+  /// This function calls [put()] in [Simulator.injectAction()].
+  void inject(dynamic val, {bool fill = false}) =>
+      _wire.inject(val, fill: fill);
+
+  /// Puts a value [val] onto this signal, which may or may not be picked up
+  /// for [changed] in this [Simulator] tick.
+  ///
+  /// The type of [val] should be an `int`, [BigInt], `bool`, or [LogicValue].
+  ///
+  /// This function is used for propogating glitches through connected signals.
+  /// Use this function for custom definitions of [Module] behavior.
+  ///
+  /// If [fill] is set, all bits of the signal gets set to [val], similar
+  /// to `'` in SystemVerilog.
+  void put(dynamic val, {bool fill = false}) => _wire.put(val, fill: fill);
+
   /// Connects this [Logic] directly to [other].
   ///
   /// Every time [other] transitions (`glitch`es), this signal will transition
@@ -258,19 +482,19 @@ class Logic {
 
   /// Handles the actual connection of this [Logic] to [other].
   void _connect(Logic other) {
-    if (other.width != width) {
-      throw Exception('Bus widths must match.'
-          ' Cannot connect $this to $other which have different widths.');
-    }
-
     _unassignable = true;
+    _updateWire(other._wire);
+  }
 
-    if (value != other.value) {
-      put(other.value);
+  /// Updates the current active [_Wire] for this [Logic] and also
+  /// notifies all downstream [Logic]s of the new source [_Wire].
+  void _updateWire(_Wire newWire) {
+    newWire._adopt(_wire);
+    _wire = newWire;
+
+    for (final dstConnection in dstConnections) {
+      dstConnection._updateWire(newWire);
     }
-    other.glitch.listen((args) {
-      put(other.value);
-    });
   }
 
   /// Connects this [Logic] directly to another [Logic].
@@ -365,90 +589,6 @@ class Logic {
     }
   }
 
-  /// Injects a value onto this signal in the current [Simulator] tick.
-  ///
-  /// This function calls [put()] in [Simulator.injectAction()].
-  void inject(dynamic val, {bool fill = false}) {
-    Simulator.injectAction(() => put(val, fill: fill));
-  }
-
-  /// Keeps track of whether there is an active put, to detect reentrance.
-  bool _isPutting = false;
-
-  /// Puts a value [val] onto this signal, which may or may not be picked up
-  /// for [changed] in this [Simulator] tick.
-  ///
-  /// The type of [val] should be an `int`, [BigInt], `bool`, or [LogicValue].
-  ///
-  /// This function is used for propogating glitches through connected signals.
-  /// Use this function for custom definitions of [Module] behavior.
-  ///
-  /// If [fill] is set, all bits of the signal gets set to [val], similar
-  /// to `'` in SystemVerilog.
-  void put(dynamic val, {bool fill = false}) {
-    LogicValue newValue;
-    if (val is int) {
-      if (fill) {
-        newValue = LogicValue.filled(
-            width,
-            val == 0
-                ? LogicValue.zero
-                : val == 1
-                    ? LogicValue.one
-                    : throw Exception('Only can fill 0 or 1, but saw $val.'));
-      } else {
-        newValue = LogicValue.ofInt(val, width);
-      }
-    } else if (val is BigInt) {
-      if (fill) {
-        newValue = LogicValue.filled(
-            width,
-            val == BigInt.zero
-                ? LogicValue.zero
-                : val == BigInt.one
-                    ? LogicValue.one
-                    : throw Exception('Only can fill 0 or 1, but saw $val.'));
-      } else {
-        newValue = LogicValue.ofBigInt(val, width);
-      }
-    } else if (val is bool) {
-      newValue = LogicValue.ofInt(val ? 1 : 0, width);
-    } else if (val is LogicValue) {
-      if (val.width == 1 &&
-          (val == LogicValue.x || val == LogicValue.z || fill)) {
-        newValue = LogicValue.filled(width, val);
-      } else if (fill) {
-        throw Exception(
-            'Failed to fill value with $val.  To fill, it should be 1 bit.');
-      } else {
-        newValue = val;
-      }
-    } else {
-      throw Exception('Unrecognized value "$val" to deposit on this signal. '
-          'Unknown type ${val.runtimeType} cannot be deposited.');
-    }
-
-    if (newValue.width != width) {
-      throw Exception(
-          'Updated value width mismatch.  The width of $val should be $width.');
-    }
-
-    if (_isPutting) {
-      // if this is the result of a cycle, then contention!
-      newValue = LogicValue.filled(width, LogicValue.x);
-    }
-
-    final prevValue = _currentValue;
-    _currentValue = newValue;
-
-    // sends out a glitch if the value deposited has changed
-    if (_currentValue != prevValue) {
-      _isPutting = true;
-      _glitchController.add(LogicValueChanged(_currentValue, prevValue));
-      _isPutting = false;
-    }
-  }
-
   /// Accesses the [index]th bit of this signal.
   ///
   /// Accepts both [int] and [Logic] as [index].
diff --git a/lib/src/modules/conditional.dart b/lib/src/modules/conditional.dart
index 589a3d52..6508c6a8 100644
--- a/lib/src/modules/conditional.dart
+++ b/lib/src/modules/conditional.dart
@@ -351,6 +351,7 @@ class Sequential extends _Always {
   void _execute() {
     var anyClkInvalid = false;
     var anyClkPosedge = false;
+
     for (var i = 0; i < _clks.length; i++) {
       // if the pre-tick value is null, then it should have the same value as
       // it currently does
diff --git a/lib/src/utilities/synchronous_propagator.dart b/lib/src/utilities/synchronous_propagator.dart
index b5b2a251..84425854 100644
--- a/lib/src/utilities/synchronous_propagator.dart
+++ b/lib/src/utilities/synchronous_propagator.dart
@@ -8,6 +8,8 @@
 /// Author: Max Korbel <max.korbel@intel.com>
 ///
 
+/***/
+
 /// A controller for a [SynchronousEmitter] that allows for
 /// adding of events of type [T] to be emitted.
 class SynchronousPropagator<T> {
@@ -51,4 +53,14 @@ class SynchronousEmitter<T> {
     }
     _isEmitting = false;
   }
+
+  /// Tells this emitter to adopt all behavior of [other].
+  ///
+  /// Tells this emitter to perform all the actions of [other] each
+  /// time this would propagate.  Also clears all actions from [other]
+  /// so that it will not execute anything in the future.
+  void adopt(SynchronousEmitter<T> other) {
+    _actions.addAll(other._actions);
+    other._actions.clear();
+  }
 }


Test patch:
diff --git a/test/changed_test.dart b/test/changed_test.dart
index 6ada837c..cd24965b 100644
--- a/test/changed_test.dart
+++ b/test/changed_test.dart
@@ -112,4 +112,61 @@ void main() {
 
     expect(qHadPosedge, equals(true));
   });
+
+  test('reconnected signal still hits changed events', () async {
+    final a = Logic(name: 'a');
+    final b = Logic(name: 'b');
+
+    var detectedAChanged = false;
+    a.changed.listen((event) {
+      detectedAChanged = true;
+    });
+
+    a <= b;
+
+    Simulator.registerAction(100, () {
+      b.put(1);
+    });
+
+    await Simulator.run();
+
+    expect(detectedAChanged, isTrue);
+  });
+
+  test('chain of reconnected signals still changes', () async {
+    final a = Logic(name: 'a');
+    final b = Logic(name: 'b');
+    final c = Logic(name: 'c');
+
+    var detectedAChanged = false;
+    a.changed.listen((event) {
+      detectedAChanged = true;
+    });
+
+    a <= b;
+    b <= c;
+
+    Simulator.registerAction(100, () {
+      c.put(1);
+    });
+
+    await Simulator.run();
+
+    expect(detectedAChanged, isTrue);
+  });
+
+  test('chain of reconnected signals still glitches', () async {
+    final a = Logic(name: 'a');
+    final b = Logic(name: 'b');
+    final c = Logic(name: 'c');
+
+    a.put(0);
+
+    a <= b;
+    b <= c;
+
+    c.put(1);
+
+    expect(a.value, equals(LogicValue.one));
+  });
 }
diff --git a/test/counter_test.dart b/test/counter_test.dart
index e9bb9f5d..b48074fa 100644
--- a/test/counter_test.dart
+++ b/test/counter_test.dart
@@ -7,10 +7,8 @@
 /// 2021 May 10
 /// Author: Max Korbel <max.korbel@intel.com>
 ///
-import 'dart:async';
-
-// import 'dart:io';
 
+import 'dart:async';
 import 'package:rohd/rohd.dart';
 import 'package:rohd/src/utilities/simcompare.dart';
 import 'package:test/test.dart';
@@ -50,13 +48,9 @@ void main() {
       final counter = Counter(Logic(), reset);
       await counter.build();
       // WaveDumper(counter);
-      // File('tmp_counter.sv').writeAsStringSync(counter.generateSynth());
 
-      // check that 1 timestep after reset, the value has reset properly
       unawaited(reset.nextPosedge
-          .then((value) => Simulator.registerAction(Simulator.time + 1, () {
-                expect(counter.val.value.toInt(), equals(0));
-              })));
+          .then((value) => expect(counter.val.value.toInt(), equals(0))));
 
       final vectors = [
         Vector({'en': 0, 'reset': 0}, {}),
diff --git a/test/long_chain_test.dart b/test/long_chain_test.dart
new file mode 100644
index 00000000..b1ee90f4
--- /dev/null
+++ b/test/long_chain_test.dart
@@ -0,0 +1,39 @@
+/// Copyright (C) 2022 Intel Corporation
+/// SPDX-License-Identifier: BSD-3-Clause
+///
+/// long_chain_test.dart
+/// Tests with long chains of combinational logic.
+///
+/// 2022 November 8
+/// Author: Max Korbel <max.korbel@intel.com>
+///
+
+import 'package:rohd/rohd.dart';
+import 'package:rohd/src/modules/passthrough.dart';
+import 'package:test/test.dart';
+
+class LongChain extends Module {
+  final int length;
+
+  Logic get chainOut => output('chainOut');
+
+  LongChain(Logic chainIn, {this.length = 1000}) : super(name: 'longChain') {
+    chainIn = addInput('chainIn', chainIn);
+    var intermediate = chainIn;
+    for (var i = 0; i < length; i++) {
+      intermediate = ~Passthrough(intermediate).out;
+    }
+    addOutput('chainOut') <= intermediate;
+  }
+}
+
+void main() {
+  test('long chain of combinational logic and modules', () async {
+    final chainIn = Logic(name: 'chainIn');
+    final chain = LongChain(chainIn);
+    await chain.build();
+
+    chainIn.put(0);
+    expect(chain.chainOut.value.toInt(), equals(chain.length % 2));
+  });
+}