From b731bd85cc2721b55e7068807221331afdd103b7 Mon Sep 17 00:00:00 2001 From: Max Korbel Date: Mon, 16 Sep 2024 14:12:43 -0700 Subject: [PATCH] `Sum` and `Counter` (#89) --- .github/workflows/general.yml | 4 +- .../lib/hcl/view/screen/content_widget.dart | 15 +- confapp/lib/hcl/view/screen/schematic.dart | 2 +- confapp/web/flutter_bootstrap.js | 15 + confapp/web/index.html | 20 +- doc/README.md | 3 +- doc/components/summation.md | 40 ++ lib/rohd_hcl.dart | 1 + .../components/component_registry.dart | 2 + .../components/components.dart | 1 + .../components/config_summation.dart | 144 ++++++ .../config_knobs/config_knobs.dart | 2 + .../config_knobs/int_config_knob.dart | 12 +- .../int_optional_config_knob.dart | 59 +++ .../config_knobs/string_config_knob.dart | 5 +- .../config_knobs/text_config_knob.dart | 26 + lib/src/summation/counter.dart | 157 ++++++ lib/src/summation/sum.dart | 196 ++++++++ lib/src/summation/sum_interface.dart | 79 +++ lib/src/summation/summation.dart | 6 + lib/src/summation/summation_base.dart | 165 +++++++ test/arithmetic/divider_test.dart | 2 +- test/configurator_test.dart | 20 + test/summation/counter_test.dart | 271 ++++++++++ test/summation/sum_test.dart | 467 ++++++++++++++++++ 25 files changed, 1676 insertions(+), 38 deletions(-) create mode 100644 confapp/web/flutter_bootstrap.js create mode 100644 doc/components/summation.md create mode 100644 lib/src/component_config/components/config_summation.dart create mode 100644 lib/src/component_config/config_knobs/int_optional_config_knob.dart create mode 100644 lib/src/component_config/config_knobs/text_config_knob.dart create mode 100644 lib/src/summation/counter.dart create mode 100644 lib/src/summation/sum.dart create mode 100644 lib/src/summation/sum_interface.dart create mode 100644 lib/src/summation/summation.dart create mode 100644 lib/src/summation/summation_base.dart create mode 100644 test/summation/counter_test.dart create mode 100644 test/summation/sum_test.dart diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 50ca44b8..d1aea52f 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -84,7 +84,7 @@ jobs: uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.16.3 + version: 3.24.2 - name: Analyze flutter source run: tool/gh_actions/analyze_flutter_source.sh @@ -151,7 +151,7 @@ jobs: uses: flutter-actions/setup-flutter@v2 with: channel: stable - version: 3.16.3 + version: 3.24.2 - name: Build static site run: tool/gh_actions/hcl_site_generation_build.sh diff --git a/confapp/lib/hcl/view/screen/content_widget.dart b/confapp/lib/hcl/view/screen/content_widget.dart index 4e807935..93dac807 100644 --- a/confapp/lib/hcl/view/screen/content_widget.dart +++ b/confapp/lib/hcl/view/screen/content_widget.dart @@ -69,15 +69,13 @@ class _SVGeneratorState extends State { ); final key = Key(label); - if (knob is IntConfigKnob || knob is StringConfigKnob) { + if (knob is TextConfigKnob) { selector = TextFormField( key: key, - initialValue: (knob is IntConfigKnob && knob.value > 255) - ? '0x${knob.value.toRadixString(16)}' - : knob.value.toString(), + initialValue: knob.valueString, decoration: decoration, validator: (value) { - if (value!.isEmpty) { + if ((value == null || value.isEmpty) && !knob.allowEmpty) { return 'Please enter value'; } return null; @@ -91,12 +89,7 @@ class _SVGeneratorState extends State { return; } - if (knob is IntConfigKnob) { - final newValue = int.tryParse(value.toString()); - knob.value = newValue ?? knob.value; - } else { - knob.value = value; - } + knob.setValueFromString(value); }); }, ); diff --git a/confapp/lib/hcl/view/screen/schematic.dart b/confapp/lib/hcl/view/screen/schematic.dart index b756e756..32c16037 100644 --- a/confapp/lib/hcl/view/screen/schematic.dart +++ b/confapp/lib/hcl/view/screen/schematic.dart @@ -75,7 +75,7 @@ const _suffix = r""" svg.call(zoom) .on("dblclick.zoom", null) - graph = JSON.parse(exmpl); + graph = JSON.parse(exmpl.replaceAll("\\", "__")); if ("creator" in graph) { graph = d3.HwSchematic.fromYosys(graph); } diff --git a/confapp/web/flutter_bootstrap.js b/confapp/web/flutter_bootstrap.js new file mode 100644 index 00000000..ca3d3864 --- /dev/null +++ b/confapp/web/flutter_bootstrap.js @@ -0,0 +1,15 @@ +{{flutter_js}} +{{flutter_build_config}} +_flutter.loader.load({ + serviceWorkerSettings: { + serviceWorkerVersion: {{flutter_service_worker_version}}, + }, + onEntrypointLoaded: function(engineInitializer) { + engineInitializer.initializeEngine().then(function(appRunner) { + appRunner.runApp(); + }); + }, + config: { + canvasKitBaseUrl: 'canvaskit/', + } +}); \ No newline at end of file diff --git a/confapp/web/index.html b/confapp/web/index.html index 164aee14..79ef299f 100644 --- a/confapp/web/index.html +++ b/confapp/web/index.html @@ -32,28 +32,10 @@ confapp - - + diff --git a/doc/README.md b/doc/README.md index bca3f30c..f76b7f5c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -57,7 +57,8 @@ Some in-development items will have opened issues, as well. Feel free to create - Binary-Coded Decimal (BCD) - [Rotate](./components/rotate.md) - Counters - - Binary counter + - [Summation](./components/summation.md#sum) + - [Binary counter](./components/summation.md#counter) - Gray counter - Pseudorandom - LFSR diff --git a/doc/components/summation.md b/doc/components/summation.md new file mode 100644 index 00000000..bf13556d --- /dev/null +++ b/doc/components/summation.md @@ -0,0 +1,40 @@ +# Summation + +ROHD-HCL comes with combinational and sequential components for summing any number of input values, including support for increment/decrement and saturation/roll-over behavior. + +## SumInterface + +The `SumInterface` is shared between [`Sum`](#sum) and [`Counter`](#counter) components and represents a single element to be summed. Each instance of a `SumInterface` has an associated `amount`, which could either be a fixed constant value (`fixedAmount`) or a dynamic `Logic`. Fixed amounts will do some automatic width inference, unless a `width` is specified. The interface can also optionally include an enable signal. It is implemented as a `PairInterface` where all ports are `fromProvider`. Each interface may be either incrementing or decrementing. + +```dart +// An interface with a dynamic 4-bit amount to increment by +SumInterface(width: 4); +``` + +## Sum + +The `Sum` component takes a list of `SumInterface`s and adds them all up. The `saturates` configuration enables saturation behavior, otherwise there will be roll-over at overflow/underflow of the counter at `minValue` and `maxValue`. The sum can also be given an `initialValue` to start at. + +Internally, the `Sum` component builds a wider bus which is large enough to hold the biggest possible intermediate value during summation before consideration of overflow and saturation. + +Note that the implementation's size and complexity when synthesized depend significantly on the configuration. For example, if everything is a nice power-of-2 number, then the logic is much simpler than otherwise where hardware modulos may be required to properly handle roll-over/under scenarios. + +A simpler `Sum.ofLogics` constructor has less configurability, but can be passed a simple `List` without needing to construct per-element interfaces. + +```dart +// An 8-bit sum of a list of `SumInterface`s +Sum(intfs, width: 8); +``` + +## Counter + +The `Counter` component is a similarly configurable version of the `Sum` which maintains a sequential element to store the previous value. + +One additional nice feature of the `Counter` is that it supports a `restart` in addition to the normal `reset`. While `reset` will reset the internal flops, `restart` will re-initialize the internal `Sum` back to the reset value, but still perform the computation on inputs in the current cycle. This is especially useful in case you want to restart a counter while events worth counting may still be occuring. + +The `Counter` also has a `Counter.simple` constructor which is intended for very basic scenarios like "count up by 1 each cycle". + +```dart +// A counter which increments by 1 each cycle up to 5, then rolls over. +Counter.simple(clk: clk, reset: reset, maxValue: 5); +``` diff --git a/lib/rohd_hcl.dart b/lib/rohd_hcl.dart index 9b147023..de909ffa 100644 --- a/lib/rohd_hcl.dart +++ b/lib/rohd_hcl.dart @@ -18,4 +18,5 @@ export 'src/models/models.dart'; export 'src/rotate.dart'; export 'src/shift_register.dart'; export 'src/sort.dart'; +export 'src/summation/summation.dart'; export 'src/utils.dart'; diff --git a/lib/src/component_config/components/component_registry.dart b/lib/src/component_config/components/component_registry.dart index 6d3665e1..5d452b11 100644 --- a/lib/src/component_config/components/component_registry.dart +++ b/lib/src/component_config/components/component_registry.dart @@ -15,6 +15,8 @@ List get componentRegistry => [ FifoConfigurator(), EccConfigurator(), RoundRobinArbiterConfigurator(), + CounterConfigurator(), + SumConfigurator(), PriorityArbiterConfigurator(), RippleCarryAdderConfigurator(), CarrySaveMultiplierConfigurator(), diff --git a/lib/src/component_config/components/components.dart b/lib/src/component_config/components/components.dart index 32c33fda..0d7e59a6 100644 --- a/lib/src/component_config/components/components.dart +++ b/lib/src/component_config/components/components.dart @@ -15,3 +15,4 @@ export 'config_ripple_carry_adder.dart'; export 'config_rotate.dart'; export 'config_round_robin_arbiter.dart'; export 'config_sort.dart'; +export 'config_summation.dart'; diff --git a/lib/src/component_config/components/config_summation.dart b/lib/src/component_config/components/config_summation.dart new file mode 100644 index 00000000..0e2824e0 --- /dev/null +++ b/lib/src/component_config/components/config_summation.dart @@ -0,0 +1,144 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// config_summation.dart +// Configurators for summation. +// +// 2024 September 6 +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:rohd_hcl/src/summation/summation_base.dart'; + +/// A knob for a single sum interface. +class SumInterfaceKnob extends GroupOfKnobs { + /// Whether the sum interface has an enable signal. + ToggleConfigKnob hasEnableKnob = ToggleConfigKnob(value: false); + + /// Whether the sum interface has a fixed value. + ToggleConfigKnob isFixedValueKnob = ToggleConfigKnob(value: false); + + /// The fixed value of the sum interface, only present when [isFixedValueKnob] + /// is true. + IntConfigKnob fixedValueKnob = IntConfigKnob(value: 1); + + /// The width of the sum interface. + IntOptionalConfigKnob widthKnob = IntOptionalConfigKnob(value: 8); + + /// Whether the sum interface increments (vs. decrements). + ToggleConfigKnob incrementsKnob = ToggleConfigKnob(value: true); + + /// Creates a new sum interface knob. + SumInterfaceKnob() : super({}, name: 'Sum Interface'); + + @override + Map> get subKnobs => { + 'Has Enable': hasEnableKnob, + 'Is Fixed Value': isFixedValueKnob, + if (isFixedValueKnob.value) 'Fixed Value': fixedValueKnob, + 'Width': widthKnob, + 'Increments': incrementsKnob, + }; +} + +/// A configurator for a module like [SummationBase]. +abstract class SummationConfigurator extends Configurator { + /// The interface knobs. + final ListOfKnobsKnob sumInterfaceKnobs = ListOfKnobsKnob( + count: 1, + generateKnob: (i) => SumInterfaceKnob(), + name: 'Sum Interfaces', + ); + + /// The width. + final IntOptionalConfigKnob widthKnob = IntOptionalConfigKnob(value: null); + + /// The minimum value. + final IntOptionalConfigKnob minValueKnob = IntOptionalConfigKnob(value: 0); + + /// The maximum value. + final IntOptionalConfigKnob maxValueKnob = IntOptionalConfigKnob(value: null); + + /// Whether the output saturates (vs. rolling over/under). + final ToggleConfigKnob saturatesKnob = ToggleConfigKnob(value: false); + + @override + Map> get knobs => { + 'Sum Interfaces': sumInterfaceKnobs, + 'Width': widthKnob, + 'Minimum Value': minValueKnob, + 'Maximum Value': maxValueKnob, + 'Saturates': saturatesKnob, + }; +} + +/// A configurator for [Sum]. +class SumConfigurator extends SummationConfigurator { + /// The initial value. + final IntConfigKnob initialValueKnob = IntConfigKnob(value: 0); + + @override + Map> get knobs => { + ...super.knobs, + 'Initial Value': initialValueKnob, + }; + + @override + Module createModule() => Sum( + sumInterfaceKnobs.knobs + .map((e) => e as SumInterfaceKnob) + .map((e) => SumInterface( + hasEnable: e.hasEnableKnob.value, + fixedAmount: + e.isFixedValueKnob.value ? e.fixedValueKnob.value : null, + width: e.widthKnob.value, + increments: e.incrementsKnob.value, + )) + .toList(), + initialValue: initialValueKnob.value, + width: widthKnob.value, + minValue: minValueKnob.value, + maxValue: maxValueKnob.value, + saturates: saturatesKnob.value, + ); + + @override + String get name => 'Sum'; +} + +/// A configurator for [Counter]. +class CounterConfigurator extends SummationConfigurator { + /// The reset value. + final IntConfigKnob resetValueKnob = IntConfigKnob(value: 0); + + @override + Map> get knobs => { + ...super.knobs, + 'Reset Value': resetValueKnob, + }; + + @override + Module createModule() => Counter( + sumInterfaceKnobs.knobs + .map((e) => e as SumInterfaceKnob) + .map((e) => SumInterface( + hasEnable: e.hasEnableKnob.value, + fixedAmount: + e.isFixedValueKnob.value ? e.fixedValueKnob.value : null, + width: e.widthKnob.value, + increments: e.incrementsKnob.value, + )) + .toList(), + resetValue: resetValueKnob.value, + width: widthKnob.value, + minValue: minValueKnob.value, + maxValue: maxValueKnob.value, + saturates: saturatesKnob.value, + clk: Logic(), + reset: Logic(), + ); + + @override + String get name => 'Counter'; +} diff --git a/lib/src/component_config/config_knobs/config_knobs.dart b/lib/src/component_config/config_knobs/config_knobs.dart index 6f33c5ac..bf271a36 100644 --- a/lib/src/component_config/config_knobs/config_knobs.dart +++ b/lib/src/component_config/config_knobs/config_knobs.dart @@ -5,6 +5,8 @@ export 'choice_config_knob.dart'; export 'config_knob.dart'; export 'group_of_knobs_knob.dart'; export 'int_config_knob.dart'; +export 'int_optional_config_knob.dart'; export 'list_of_knobs_knob.dart'; export 'string_config_knob.dart'; +export 'text_config_knob.dart'; export 'toggle_config_knob.dart'; diff --git a/lib/src/component_config/config_knobs/int_config_knob.dart b/lib/src/component_config/config_knobs/int_config_knob.dart index e5a70794..e85ab237 100644 --- a/lib/src/component_config/config_knobs/int_config_knob.dart +++ b/lib/src/component_config/config_knobs/int_config_knob.dart @@ -9,7 +9,7 @@ import 'package:rohd_hcl/rohd_hcl.dart'; /// A knob to store an [int]. -class IntConfigKnob extends ConfigKnob { +class IntConfigKnob extends TextConfigKnob { /// Creates a new config knob with the specified initial [value]. IntConfigKnob({required super.value}); @@ -17,13 +17,21 @@ class IntConfigKnob extends ConfigKnob { Map toJson() => {'value': value > 255 ? '0x${value.toRadixString(16)}' : value}; + @override + String get valueString => + value > 255 ? '0x${value.toRadixString(16)}' : value.toString(); + @override void loadJson(Map decodedJson) { final val = decodedJson['value']; if (val is String) { - value = int.parse(val); + setValueFromString(val); } else { value = val as int; } } + + @override + void setValueFromString(String valueString) => + value = int.tryParse(valueString) ?? value; } diff --git a/lib/src/component_config/config_knobs/int_optional_config_knob.dart b/lib/src/component_config/config_knobs/int_optional_config_knob.dart new file mode 100644 index 00000000..99e6b123 --- /dev/null +++ b/lib/src/component_config/config_knobs/int_optional_config_knob.dart @@ -0,0 +1,59 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// int_optional_config_knob.dart +// A knob for holding a number or null. +// +// 2024 September 9 + +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// A knob to store an [int]. +class IntOptionalConfigKnob extends TextConfigKnob { + /// Creates a new config knob with the specified initial [value]. + IntOptionalConfigKnob({required super.value}); + + @override + Map toJson() => { + 'value': value == null + ? null + : value! > 255 + ? '0x${value!.toRadixString(16)}' + : value + }; + + @override + String get valueString => value == null + ? '' + : value! > 255 + ? '0x${value!.toRadixString(16)}' + : value.toString(); + + @override + bool get allowEmpty => true; + + @override + void loadJson(Map decodedJson) { + final val = decodedJson['value']; + if (val == null) { + value = null; + } else if (val is String) { + if (val.isEmpty) { + value = null; + } else { + value = int.parse(val); + } + } else { + value = val as int; + } + } + + @override + void setValueFromString(String valueString) { + if (valueString.isEmpty) { + value = null; + } else { + value = int.parse(valueString); + } + } +} diff --git a/lib/src/component_config/config_knobs/string_config_knob.dart b/lib/src/component_config/config_knobs/string_config_knob.dart index fc90d668..a8a590a9 100644 --- a/lib/src/component_config/config_knobs/string_config_knob.dart +++ b/lib/src/component_config/config_knobs/string_config_knob.dart @@ -9,7 +9,10 @@ import 'package:rohd_hcl/rohd_hcl.dart'; /// A knob for holding a [String]. -class StringConfigKnob extends ConfigKnob { +class StringConfigKnob extends TextConfigKnob { /// Creates a new knob with the specified initial [value]. StringConfigKnob({required super.value}); + + @override + void setValueFromString(String valueString) => value = valueString; } diff --git a/lib/src/component_config/config_knobs/text_config_knob.dart b/lib/src/component_config/config_knobs/text_config_knob.dart new file mode 100644 index 00000000..503a0cf1 --- /dev/null +++ b/lib/src/component_config/config_knobs/text_config_knob.dart @@ -0,0 +1,26 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// text_config_knob.dart +// Definition of a configuration knob which can be configured by parsing text. +// +// 2023 December 5 +// Author: Max Korbel + +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// A configuration knob for use in [Configurator]s which can be configured by +/// text. +abstract class TextConfigKnob extends ConfigKnob { + /// Creates a new knob with an initial [value]. + TextConfigKnob({required super.value}); + + /// A [String] representation of the [value]. + String get valueString => value.toString(); + + /// Sets the [value] from a [String]. + void setValueFromString(String valueString); + + /// Whether the knob allows an empty string as input. + bool get allowEmpty => false; +} diff --git a/lib/src/summation/counter.dart b/lib/src/summation/counter.dart new file mode 100644 index 00000000..6094f2fc --- /dev/null +++ b/lib/src/summation/counter.dart @@ -0,0 +1,157 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// counter.dart +// A flexible counter implementation. +// +// 2024 August 26 +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:rohd_hcl/src/summation/summation_base.dart'; + +/// Keeps a count of the running sum of any number of sources with optional +/// configuration for widths, saturation behavior, and restarting. +class Counter extends SummationBase { + /// The output value of the counter. + Logic get count => output('count'); + + /// Creates a counter that increments according to the provided [interfaces]. + /// + /// The [width] can be either explicitly provided or inferred from other + /// values such as a [maxValue], [minValue], or [resetValue] that contain + /// width information (e.g. a [LogicValue]), or by making it large enough to + /// fit [maxValue], or by inspecting widths of [interfaces]. There must be + /// enough information provided to determine the [width]. + /// + /// If no [maxValue] is provided, one will be inferred by the maximum that can + /// fit inside of the [width]. + /// + /// The [restart] input can be used to restart the counter to a new value, but + /// also continue to increment in that same cycle. This is distinct from + /// [reset] which will reset the counter, holding the [count] at [resetValue]. + /// + /// If [saturates] is `true`, then it will saturate at the [maxValue] and + /// [minValue]. If `false`, will wrap around (overflow/underflow) at the + /// [maxValue] and [minValue]. The [equalsMax], [equalsMin], [overflowed], + /// and [underflowed] outputs can be used to determine if the sum is at the + /// maximum, minimum, (would have) overflowed, or (would have) underflowed, + /// respectively. + Counter( + super.interfaces, { + required Logic clk, + required Logic reset, + Logic? restart, + dynamic resetValue = 0, + super.maxValue, + super.minValue = 0, + super.width, + super.saturates, + super.name = 'counter', + }) : super(initialValue: resetValue) { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + if (restart != null) { + restart = addInput('restart', restart); + } + + addOutput('count', width: width); + + final sum = Sum( + interfaces, + initialValue: + restart != null ? mux(restart, initialValueLogic, count) : count, + maxValue: maxValueLogic, + minValue: minValueLogic, + width: width, + saturates: saturates, + ); + + count <= + flop( + clk, + sum.sum, + reset: reset, + resetValue: initialValueLogic, + ); + + // need to flop these since value is flopped + overflowed <= flop(clk, sum.overflowed, reset: reset); + underflowed <= flop(clk, sum.underflowed, reset: reset); + + equalsMax <= count.eq(maxValueLogic); + equalsMin <= count.eq(minValueLogic); + } + + /// A simplified constructor for [Counter] that accepts a single fixed amount + /// to count [by] (up or down based on [increments]) along with much of the + /// other available configuration in the default [Counter] constructor. + Counter.simple({ + required Logic clk, + required Logic reset, + int by = 1, + Logic? enable, + int minValue = 0, + int? maxValue, + int? width, + Logic? restart, + bool saturates = false, + bool increments = true, + int resetValue = 0, + String name = 'counter', + }) : this([ + SumInterface( + width: width, + fixedAmount: by, + hasEnable: enable != null, + increments: increments) + ..enable?.gets(enable!), + ], + clk: clk, + reset: reset, + resetValue: resetValue, + restart: restart, + maxValue: maxValue, + minValue: minValue, + width: width, + saturates: saturates, + name: name); + + /// Creates a [Counter] that counts up by all of the provided [logics], + /// including much of the other available configuration in the default + /// constructor. + /// + /// All [logics] are always incrementing and controlled optionally by a single + /// [enable]. + factory Counter.ofLogics( + List logics, { + required Logic clk, + required Logic reset, + Logic? restart, + dynamic resetValue = 0, + dynamic maxValue, + dynamic minValue = 0, + Logic? enable, + int? width, + bool saturates = false, + String name = 'counter', + }) => + Counter( + logics + .map((e) => SumInterface(width: e.width, hasEnable: enable != null) + ..amount.gets(e) + ..enable?.gets(enable!)) + .toList(), + clk: clk, + reset: reset, + resetValue: resetValue, + maxValue: maxValue, + minValue: minValue, + width: width, + saturates: saturates, + restart: restart, + name: name, + ); +} diff --git a/lib/src/summation/sum.dart b/lib/src/summation/sum.dart new file mode 100644 index 00000000..87fd42f4 --- /dev/null +++ b/lib/src/summation/sum.dart @@ -0,0 +1,196 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sum.dart +// A flexible sum implementation. +// +// 2024 August 26 +// Author: Max Korbel + +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:rohd_hcl/src/summation/summation_base.dart'; + +/// An extension on [SumInterface] to provide additional functionality for +/// computing in [Sum]. +extension on SumInterface { + /// Adjusts the [nextVal] by the amount specified in this interface, to be + /// used within a [Combinational.ssa] block. + List _combAdjustments(Logic Function(Logic) s, Logic nextVal) { + final conds = [ + if (increments) + nextVal.incr(s: s, val: amount.zeroExtend(nextVal.width)) + else + nextVal.decr(s: s, val: amount.zeroExtend(nextVal.width)), + ]; + + if (hasEnable) { + return [If(enable!, then: conds)]; + } else { + return conds; + } + } +} + +/// Computes a sum of any number of sources with optional configuration for +/// widths and saturation behavior. +class Sum extends SummationBase { + /// The resulting [sum]. + Logic get sum => output('sum'); + + /// Computes a [sum] across the provided [interfaces]. + /// + /// The [width] can be either explicitly provided or inferred from other + /// values such as a [maxValue], [minValue], or [initialValue] that contain + /// width information (e.g. a [LogicValue]), or by making it large enough to + /// fit [maxValue], or by inspecting widths of [interfaces]. There must be + /// enough information provided to determine the [width]. + /// + /// If no [maxValue] is provided, one will be inferred by the maximum that can + /// fit inside of the [width]. + /// + /// It is expected that [maxValue] is at least [minValue], or else results may + /// be unpredictable. + /// + /// If [saturates] is `true`, then it will saturate at the [maxValue] and + /// [minValue]. If `false`, will wrap around (overflow/underflow) at the + /// [maxValue] and [minValue]. The [equalsMax], [equalsMin], [overflowed], + /// and [underflowed] outputs can be used to determine if the sum is at the + /// maximum, minimum, (would have) overflowed, or (would have) underflowed, + /// respectively. + Sum( + super.interfaces, { + dynamic initialValue = 0, + super.maxValue, + super.minValue, + super.width, + super.saturates, + super.name = 'sum', + }) : super(initialValue: initialValue) { + addOutput('sum', width: width); + + var maxPosMagnitude = SummationBase.biggestVal(width); + var maxNegMagnitude = BigInt.zero; + for (final intf in interfaces) { + final maxMagnitude = intf.fixedAmount != null + ? intf.amount.value.toBigInt() + : SummationBase.biggestVal(intf.width); + + if (intf.increments) { + maxPosMagnitude += maxMagnitude; + } else { + maxNegMagnitude += maxMagnitude; + } + } + + // also consider that initialValue may be less than min or more than max + final maxInitialValueMagnitude = initialValue is Logic + ? SummationBase.biggestVal(initialValue.width) + : LogicValue.ofInferWidth(initialValue).toBigInt(); + maxPosMagnitude += maxInitialValueMagnitude; + maxNegMagnitude += maxInitialValueMagnitude; + + // calculate the largest number that we could have in intermediate + final internalWidth = max( + (maxPosMagnitude + maxNegMagnitude + BigInt.one).bitLength, width + 1); + + final initialValueLogicExt = initialValueLogic.zeroExtend(internalWidth); + final minValueLogicExt = minValueLogic.zeroExtend(internalWidth); + final maxValueLogicExt = maxValueLogic.zeroExtend(internalWidth); + + // lazy range so that it's not generated if not necessary + late final range = Logic(name: 'range', width: internalWidth) + ..gets(maxValueLogicExt - minValueLogicExt + 1); + + final zeroPoint = Logic(name: 'zeroPoint', width: internalWidth) + ..gets(Const(maxNegMagnitude, width: internalWidth)); + + final upperSaturation = Logic(name: 'upperSaturation', width: internalWidth) + ..gets(maxValueLogicExt + zeroPoint); + final lowerSaturation = Logic(name: 'lowerSaturation', width: internalWidth) + ..gets(minValueLogicExt + zeroPoint); + + final internalValue = Logic(name: 'internalValue', width: internalWidth); + sum <= (internalValue - zeroPoint).getRange(0, width); + + final preAdjustmentValue = + Logic(name: 'preAdjustmentValue', width: internalWidth); + + // here we use an `ssa` block to iteratively update the value of + // `internalValue` based on the adjustments from the interfaces and + // saturation/roll-over behavior + // + // For more details, see: + // https://intel.github.io/rohd-website/blog/combinational-ssa/ + Combinational.ssa((s) => [ + // initialize + s(internalValue) < initialValueLogicExt + zeroPoint, + + // perform increments and decrements per-interface + ...interfaces + .map((e) => e._combAdjustments(s, internalValue)) + .flattened, + + // identify if we're at a max/min case + overflowed < s(internalValue).gt(upperSaturation), + underflowed < s(internalValue).lt(lowerSaturation), + + // useful as an internal node for debug/visibility + preAdjustmentValue < s(internalValue), + + // handle saturation or over/underflow + If.block([ + Iff.s( + overflowed, + s(internalValue) < + (saturates + ? upperSaturation + : ((s(internalValue) - upperSaturation - 1) % range + + lowerSaturation)), + ), + ElseIf.s( + underflowed, + s(internalValue) < + (saturates + ? lowerSaturation + : (upperSaturation - + ((lowerSaturation - s(internalValue) - 1) % range))), + ) + ]), + ]); + + equalsMax <= internalValue.eq(upperSaturation); + equalsMin <= internalValue.eq(lowerSaturation); + } + + /// Computes a [Sum] across the provided [logics]. + /// + /// All [logics] are always incrementing and controlled optionally by a single + /// [enable]. + factory Sum.ofLogics( + List logics, { + dynamic initialValue = 0, + dynamic maxValue, + dynamic minValue = 0, + Logic? enable, + int? width, + bool saturates = false, + String name = 'sum', + }) => + Sum( + logics + .map( + (e) => SumInterface(width: e.width, hasEnable: enable != null) + ..amount.gets(e) + ..enable?.gets(enable!)) + .toList(), + initialValue: initialValue, + maxValue: maxValue, + minValue: minValue, + width: width, + saturates: saturates, + name: name); +} diff --git a/lib/src/summation/sum_interface.dart b/lib/src/summation/sum_interface.dart new file mode 100644 index 00000000..5e5ff7b0 --- /dev/null +++ b/lib/src/summation/sum_interface.dart @@ -0,0 +1,79 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sum_interface.dart +// Interface for summation and counting. +// +// 2024 August 26 +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// A [PairInterface] representing an amount and behavior for inclusion in a +/// sum or count. +class SumInterface extends PairInterface { + /// Whether an [enable] signal is present on this interface. + final bool hasEnable; + + /// The [amount] to increment/decrement by, depending on [increments]. + late final Logic amount = + fixedAmount != null ? Const(fixedAmount, width: width) : port('amount'); + + /// Controls whether it should increment or decrement (based on [increments]) + /// (active high). + /// + /// Present if [hasEnable] is `true`. + Logic? get enable => tryPort('enable'); + + /// The [width] of the [amount]. + final int width; + + /// If `true`, will increment. If `false`, will decrement. + final bool increments; + + /// If non-`null`, the constant value of [amount]. + final dynamic fixedAmount; + + /// Creates a new [SumInterface] with a fixed or variable [amount], optionally + /// with an [enable], in either positive or negative direction based on + /// [increments]. + /// + /// If [width] is `null`, it can be inferred from [fixedAmount] if provided + /// with a type that contains width information (e.g. a [LogicValue]). There + /// must be enough information provided to determine the [width]. + /// + /// If a [fixedAmount] is provided, then [amount] will be tied to a [Const]. A + /// provided [fixedAmount] must be parseable by [LogicValue.of]. Note that the + /// [fixedAmount] will always be interpreted as a positive value truncated to + /// [width]. If no [fixedAmount] is provided, then [amount] will be a normal + /// [port] with [width] bits. + /// + /// If [hasEnable] is `true`, then an [enable] port will be added to the + /// interface. + SumInterface( + {this.fixedAmount, + this.increments = true, + int? width, + this.hasEnable = false}) + : width = (width == null && fixedAmount == null) + ? throw RohdHclException( + 'Must provide either a fixedAmount or width.') + : width ?? LogicValue.ofInferWidth(fixedAmount).width { + setPorts([ + if (fixedAmount == null) Port('amount', this.width), + if (hasEnable) Port('enable'), + ], [ + PairDirection.fromProvider + ]); + } + + /// Creates a clone of this [SumInterface] for things like [pairConnectIO]. + SumInterface.clone(SumInterface other) + : this( + fixedAmount: other.fixedAmount, + increments: other.increments, + width: other.width, + hasEnable: other.hasEnable, + ); +} diff --git a/lib/src/summation/summation.dart b/lib/src/summation/summation.dart new file mode 100644 index 00000000..d1c994b6 --- /dev/null +++ b/lib/src/summation/summation.dart @@ -0,0 +1,6 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +export 'counter.dart'; +export 'sum.dart'; +export 'sum_interface.dart'; diff --git a/lib/src/summation/summation_base.dart b/lib/src/summation/summation_base.dart new file mode 100644 index 00000000..53e7e11b --- /dev/null +++ b/lib/src/summation/summation_base.dart @@ -0,0 +1,165 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// summation_base.dart +// A flexible sum implementation. +// +// 2024 August 26 +// Author: Max Korbel + +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// A base class for modules doing summation operation such as [Counter] and +/// [Sum]. +abstract class SummationBase extends Module { + /// The width of the resulting sum. + final int width; + + /// An internal [Logic] version of the provided initial value. + @protected + late final Logic initialValueLogic; + + /// An internal [Logic] version of the provided minimum value. + @protected + late final Logic minValueLogic; + + /// An internal [Logic] version of the provided maximum value. + @protected + late final Logic maxValueLogic; + + /// The "internal" versions of the [SumInterface]s for this computation. + @protected + late final List interfaces; + + /// If `true`, will saturate at the `maxValue` and `minValue`. If `false`, + /// will wrap around (overflow/underflow) at the `maxValue` and `minValue`. + final bool saturates; + + /// Indicates whether the sum is greater than the maximum value. The actual + /// resulting value depends on the provided [saturates] behavior (staturation + /// or overflow). + Logic get overflowed => output('overflowed'); + + /// Indicates whether the sum is less than the minimum value. The actual + /// resulting value depends on the provided [saturates] behavior (saturation + /// or underflow). + Logic get underflowed => output('underflowed'); + + /// Indicates whether the sum (including potential saturation) is currently + /// equal to the maximum. + Logic get equalsMax => output('equalsMax'); + + /// Indicates whether the sum (including potential saturation) is currently + /// equal to the minimum. + Logic get equalsMin => output('equalsMin'); + + /// Sums the values across the provided [interfaces] within the bounds of the + /// [saturates] behavior, [initialValue], [maxValue], and [minValue], with the + /// specified [width], if provided. + SummationBase( + List interfaces, { + dynamic initialValue = 0, + dynamic maxValue, + dynamic minValue = 0, + this.saturates = false, + int? width, + super.name, + }) : width = + _inferWidth([initialValue, maxValue, minValue], width, interfaces) { + if (interfaces.isEmpty) { + throw RohdHclException('At least one interface must be provided.'); + } + + this.interfaces = interfaces + .mapIndexed((i, e) => SumInterface.clone(e) + ..pairConnectIO(this, e, PairRole.consumer, + uniquify: (original) => '${original}_$i')) + .toList(); + + initialValueLogic = _dynamicInputToLogic('initialValue', initialValue); + minValueLogic = _dynamicInputToLogic('minValue', minValue); + maxValueLogic = + _dynamicInputToLogic('maxValue', maxValue ?? biggestVal(this.width)); + + addOutput('overflowed'); + addOutput('underflowed'); + addOutput('equalsMax'); + addOutput('equalsMin'); + } + + /// Takes a given `dynamic` [value] and converts it into a [Logic], + /// potentially as an input port, if necessary. + Logic _dynamicInputToLogic(String name, dynamic value) { + if (value is Logic) { + return addInput(name, value.zeroExtend(width), width: width); + } else { + // if it's a LogicValue, then don't assume the width is necessary + if (value is LogicValue) { + // ignore: parameter_assignments + value = value.toBigInt(); + } + + if (LogicValue.ofInferWidth(value).width > width) { + throw RohdHclException( + 'Value $value for $name is too large for width $width'); + } + + return Logic(name: name, width: width)..gets(Const(value, width: width)); + } + } + + /// Returns the largest value that can fit within [width]. + @protected + static BigInt biggestVal(int width) => BigInt.two.pow(width) - BigInt.one; + + /// Infers the width of the sum based on the provided values, interfaces, and + /// optionally the provided [width]. + static int _inferWidth( + List values, int? width, List interfaces) { + if (width != null) { + if (width <= 0) { + throw RohdHclException('Width must be greater than 0.'); + } + + if (values.any((v) => v is Logic && v.width > width)) { + throw RohdHclException( + 'Width must be at least as large as the largest value.'); + } + + return width; + } + + int? maxWidthFound; + + for (final value in values) { + int? inferredValWidth; + if (value is Logic) { + inferredValWidth = value.width; + } else if (value != null) { + inferredValWidth = LogicValue.ofInferWidth(value).width; + } + + if (inferredValWidth != null && + (maxWidthFound == null || inferredValWidth > maxWidthFound)) { + maxWidthFound = inferredValWidth; + } + } + + for (final interface in interfaces) { + if (interface.width > maxWidthFound!) { + maxWidthFound = interface.width; + } + } + + if (maxWidthFound == null) { + throw RohdHclException('Unabled to infer width.'); + } + + return max(1, maxWidthFound); + } +} diff --git a/test/arithmetic/divider_test.dart b/test/arithmetic/divider_test.dart index 80134f4d..6a015461 100644 --- a/test/arithmetic/divider_test.dart +++ b/test/arithmetic/divider_test.dart @@ -471,7 +471,7 @@ void main() { group('divider tests', () { test('VF tests', () async { // Set the logger level - Logger.root.level = Level.ALL; + Logger.root.level = Level.OFF; // Create the testbench final intf = MultiCycleDividerInterface(); diff --git a/test/configurator_test.dart b/test/configurator_test.dart index 01316bf4..3e256195 100644 --- a/test/configurator_test.dart +++ b/test/configurator_test.dart @@ -285,6 +285,26 @@ void main() { expect(sv, contains('swizzle')); }); + test('sum configurator', () async { + final cfg = SumConfigurator(); + cfg.initialValueKnob.value = 6; + cfg.widthKnob.value = 10; + cfg.minValueKnob.value = 5; + cfg.maxValueKnob.value = 25; + cfg.saturatesKnob.value = true; + + final mod = cfg.createModule() as Sum; + + // ignore: invalid_use_of_protected_member + expect(mod.initialValueLogic.value.toInt(), 6); + expect(mod.width, 10); + // ignore: invalid_use_of_protected_member + expect(mod.minValueLogic.value.toInt(), 5); + // ignore: invalid_use_of_protected_member + expect(mod.maxValueLogic.value.toInt(), 25); + expect(mod.saturates, true); + }); + group('configurator builds', () { for (final componentConfigurator in componentRegistry) { test(componentConfigurator.name, () async { diff --git a/test/summation/counter_test.dart b/test/summation/counter_test.dart new file mode 100644 index 00000000..5a8565ef --- /dev/null +++ b/test/summation/counter_test.dart @@ -0,0 +1,271 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// counter_test.dart +// Tests for the counter. +// +// 2024 August 26 +// Author: Max Korbel + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + test('basic 1-bit rolling counter', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final enable = Logic()..inject(1); + + final counter = + Counter.ofLogics([Const(1)], clk: clk, reset: reset, enable: enable); + + await counter.build(); + + Simulator.setMaxSimTime(1000); + unawaited(Simulator.run()); + + // little reset routine + reset.inject(0); + await clk.nextNegedge; + reset.inject(1); + await clk.nextNegedge; + await clk.nextNegedge; + reset.inject(0); + + // check initial value + expect(counter.count.value.toInt(), 0); + + // wait a cycle, see 1 + await clk.nextNegedge; + expect(counter.count.value.toInt(), 1); + + // wait a cycle, should overflow (1-bit counter), back to 0 + await clk.nextNegedge; + expect(counter.count.value.toInt(), 0); + + // wait a cycle, see 1 + await clk.nextNegedge; + expect(counter.count.value.toInt(), 1); + + enable.inject(0); + // wait a cycle, no change + await clk.nextNegedge; + expect(counter.count.value.toInt(), 1); + await clk.nextNegedge; + expect(counter.count.value.toInt(), 1); + + enable.inject(1); + + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + + await Simulator.endSimulation(); + }); + + test('simple up counter', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final counter = Counter.simple(clk: clk, reset: reset, maxValue: 5); + + await counter.build(); + + Simulator.setMaxSimTime(1000); + unawaited(Simulator.run()); + + // little reset routine + reset.inject(0); + await clk.nextNegedge; + reset.inject(1); + await clk.nextNegedge; + await clk.nextNegedge; + reset.inject(0); + + expect(counter.overflowed.value.toBool(), false); + expect(counter.underflowed.value.toBool(), false); + expect(counter.equalsMax.value.toBool(), false); + expect(counter.equalsMin.value.toBool(), true); + + for (var i = 0; i < 20; i++) { + expect(counter.count.value.toInt(), i % 6); + + if (i % 6 == 5) { + expect(counter.overflowed.value.toBool(), false); + expect(counter.equalsMax.value.toBool(), true); + expect(counter.equalsMin.value.toBool(), false); + } else if (i % 6 == 0 && i > 0) { + expect(counter.overflowed.value.toBool(), true); + expect(counter.equalsMax.value.toBool(), false); + expect(counter.equalsMin.value.toBool(), true); + } else { + expect(counter.overflowed.value.toBool(), false); + expect(counter.equalsMax.value.toBool(), false); + if (i % 6 != 0) { + expect(counter.equalsMin.value.toBool(), false); + } else { + expect(counter.equalsMin.value.toBool(), true); + } + } + + expect(counter.underflowed.value.toBool(), false); + + await clk.nextNegedge; + } + + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + + await Simulator.endSimulation(); + }); + + test('simple down counter', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final counter = Counter.simple( + clk: clk, reset: reset, maxValue: 5, resetValue: 5, increments: false); + + await counter.build(); + + Simulator.setMaxSimTime(1000); + unawaited(Simulator.run()); + + // little reset routine + reset.inject(0); + await clk.nextNegedge; + reset.inject(1); + await clk.nextNegedge; + await clk.nextNegedge; + reset.inject(0); + + expect(counter.overflowed.value.toBool(), false); + expect(counter.underflowed.value.toBool(), false); + expect(counter.equalsMax.value.toBool(), true); + expect(counter.equalsMin.value.toBool(), false); + + for (var i = 0; i < 20; i++) { + expect(counter.count.value.toInt(), 5 - (i % 6)); + + if (i % 6 == 5) { + expect(counter.underflowed.value.toBool(), false); + expect(counter.equalsMax.value.toBool(), false); + expect(counter.equalsMin.value.toBool(), true); + } else if (i % 6 == 0 && i > 0) { + expect(counter.underflowed.value.toBool(), true); + expect(counter.equalsMax.value.toBool(), true); + expect(counter.equalsMin.value.toBool(), false); + } else { + expect(counter.underflowed.value.toBool(), false); + expect(counter.equalsMin.value.toBool(), false); + if (i % 6 != 0) { + expect(counter.equalsMax.value.toBool(), false); + } else { + expect(counter.equalsMax.value.toBool(), true); + } + } + + expect(counter.overflowed.value.toBool(), false); + + await clk.nextNegedge; + } + + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + + await Simulator.endSimulation(); + }); + + test('reset and restart counter', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final restart = Logic(); + + final counter = Counter( + [ + SumInterface(fixedAmount: 4), + SumInterface(fixedAmount: 2, increments: false), + ], + clk: clk, + reset: reset, + restart: restart, + resetValue: 10, + maxValue: 15, + saturates: true, + width: 8, + ); + + await counter.build(); + WaveDumper(counter); + + Simulator.setMaxSimTime(1000); + unawaited(Simulator.run()); + + // little reset routine + reset.inject(0); + restart.inject(0); + await clk.nextNegedge; + reset.inject(1); + await clk.nextNegedge; + await clk.nextNegedge; + reset.inject(0); + + // check initial value after reset drops + expect(counter.count.value.toInt(), 10); + + // increment each cycle + await clk.nextNegedge; + expect(counter.count.value.toInt(), 12); + await clk.nextNegedge; + expect(counter.count.value.toInt(), 14); + expect(counter.overflowed.value.toBool(), false); + + // saturate + await clk.nextNegedge; + expect(counter.count.value.toInt(), 15); + expect(counter.overflowed.value.toBool(), true); + await clk.nextNegedge; + expect(counter.count.value.toInt(), 15); + expect(counter.overflowed.value.toBool(), true); + + // restart (not reset!) + restart.inject(1); + + // now we should catch the next +2 still, not miss it + await clk.nextNegedge; + expect(counter.count.value.toInt(), 12); + + // and hold there + await clk.nextNegedge; + expect(counter.count.value.toInt(), 12); + + // drop it and should continue + restart.inject(0); + await clk.nextNegedge; + expect(counter.count.value.toInt(), 14); + + // now back to reset + reset.inject(1); + await clk.nextNegedge; + expect(counter.count.value.toInt(), 10); + await clk.nextNegedge; + expect(counter.count.value.toInt(), 10); + + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + await clk.nextNegedge; + + await Simulator.endSimulation(); + }); +} diff --git a/test/summation/sum_test.dart b/test/summation/sum_test.dart new file mode 100644 index 00000000..536c0d47 --- /dev/null +++ b/test/summation/sum_test.dart @@ -0,0 +1,467 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sum_test.dart +// Tests for sum. +// +// 2024 August 27 +// Author: Max Korbel + +import 'dart:math'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:test/test.dart'; + +int goldenSumOfLogics( + List logics, { + required int width, + bool saturates = false, + int? maxVal, + int minVal = 0, +}) => + goldenSum( + logics.map((e) => SumInterface(width: e.width)..amount.gets(e)).toList(), + width: width, + saturates: saturates, + minVal: minVal, + maxVal: maxVal, + ); + +int goldenSum( + List interfaces, { + required int width, + bool saturates = false, + int? maxVal, + int? minVal, + int initialValue = 0, + bool debug = false, +}) { + void log(String message) { + if (debug) { + // ignore: avoid_print + print(message); + } + } + + log('width: $width'); + + var sum = initialValue; + + log('min $minVal -> max $maxVal'); + + maxVal ??= (1 << width) - 1; + if (maxVal > (1 << width) - 1) { + // ignore: parameter_assignments + maxVal = (1 << width) - 1; + } + minVal ??= 0; + + log('min $minVal -> max $maxVal [adjusted]'); + + if (minVal > maxVal) { + throw Exception('minVal must be less than or equal to maxVal'); + } + + log('init: $initialValue'); + + for (final intf in interfaces) { + final amount = intf.amount.value.toInt(); + final enabled = !intf.hasEnable || intf.enable!.value.toBool(); + + log('${intf.increments ? '+' : '-'}' + '$amount${enabled ? '' : ' [disabled]'}'); + + if (enabled) { + if (intf.increments) { + sum += amount; + } else { + sum -= amount; + } + } + } + + log('=$sum'); + + if (saturates) { + if (sum > maxVal) { + sum = maxVal; + } else if (sum < minVal) { + sum = minVal; + } + log('saturates to $sum'); + } else { + final range = maxVal - minVal + 1; + if (sum > maxVal) { + sum = (sum - maxVal - 1) % range + minVal; + } else if (sum < minVal) { + sum = maxVal - (minVal - sum - 1) % range; + } + log('rolls-over to $sum'); + } + + return sum; +} + +void main() { + test('simple sum of 1 ofLogics', () async { + final logics = [Const(1)]; + final dut = Sum.ofLogics(logics); + await dut.build(); + expect(dut.sum.value.toInt(), 1); + expect(dut.width, 1); + expect(goldenSumOfLogics(logics, width: dut.width), 1); + }); + + group('sum indications', () { + test('equalsMax', () { + expect( + Sum.ofLogics([Const(5, width: 8)], maxValue: 5) + .equalsMax + .value + .toBool(), + isTrue); + }); + + test('equalsMin', () { + final dut = + Sum.ofLogics([Const(0, width: 8)], minValue: 3, saturates: true); + expect(dut.equalsMin.value.toBool(), isTrue); + }); + + test('underflowed', () { + final dut = Sum.ofLogics([Const(5, width: 8)], minValue: 6); + expect(dut.underflowed.value.toBool(), isTrue); + }); + + test('overflowed', () { + final dut = + Sum.ofLogics([Const(5, width: 8)], maxValue: 4, saturates: true); + expect(dut.overflowed.value.toBool(), isTrue); + }); + }); + + group('simple 2 numbers', () { + final pairs = [ + // fits + (3, 5), + (7, 1), + + // barely fits + (10, 5), + + // barely overflows + (7, 9), + + // overflows + (8, 10), + ]; + + for (final increments in [true, false]) { + final initialValue = increments ? 0 : 15; + for (final saturates in [true, false]) { + test('increments=$increments, saturate=$saturates', () async { + final a = Logic(width: 4); + final b = Logic(width: 4); + final intfs = [a, b] + .map((e) => SumInterface( + width: e.width, + increments: increments, + )..amount.gets(e)) + .toList(); + final dut = + Sum(intfs, saturates: saturates, initialValue: initialValue); + + await dut.build(); + + expect(dut.width, 4); + + for (final pair in pairs) { + a.put(pair.$1); + b.put(pair.$2); + final expected = goldenSum( + intfs, + width: dut.width, + saturates: saturates, + initialValue: initialValue, + ); + expect(dut.sum.value.toInt(), expected); + } + }); + } + } + }); + + test('small width, big increment', () async { + final a = Logic(width: 4); + final b = Logic(width: 4); + final intfs = [a, b] + .map((e) => SumInterface( + width: e.width, + )..amount.gets(e)) + .toList(); + final dut = Sum( + intfs, + width: 2, + ); + await dut.build(); + + expect(dut.width, 2); + + a.put(3); + b.put(2); + expect(dut.sum.value.toInt(), 1); + expect(goldenSum(intfs, width: dut.width, maxVal: 5), 1); + }); + + test('large increment on small width needs modulo', () async { + final a = Logic(width: 8); + final b = Logic(width: 8); + final dut = Sum.ofLogics( + [a, b], + width: 2, + ); + await dut.build(); + + expect(dut.width, 2); + + a.put(10); + b.put(11); + expect(dut.sum.value.toInt(), 1); + }); + + test('one up, one down', () { + final intfs = [ + SumInterface(fixedAmount: 3), + SumInterface(fixedAmount: 2, increments: false), + ]; + final dut = Sum(intfs, saturates: true, initialValue: 5, width: 7); + + expect(dut.width, 7); + + expect(dut.sum.value.toInt(), 6); + expect(dut.sum.value.toInt(), + goldenSum(intfs, width: dut.width, saturates: true, initialValue: 5)); + }); + + test('init less than min', () { + final intfs = [ + SumInterface(fixedAmount: 2), + ]; + final dut = Sum(intfs, initialValue: 13, minValue: 16, maxValue: 31); + + final actual = dut.sum.value.toInt(); + final expected = goldenSum( + intfs, + width: dut.width, + minVal: 16, + maxVal: 31, + initialValue: 13, + ); + expect(actual, 31); + expect(actual, expected); + }); + + test('init more than max', () { + final intfs = [ + SumInterface(fixedAmount: 2, increments: false), + ]; + final dut = Sum(intfs, initialValue: 34, minValue: 16, maxValue: 31); + + final actual = dut.sum.value.toInt(); + final expected = goldenSum( + intfs, + width: dut.width, + minVal: 16, + maxVal: 31, + initialValue: 34, + ); + expect(actual, 16); + expect(actual, expected); + }); + + test('min == max', () { + final intfs = [ + SumInterface(fixedAmount: 2, increments: false), + ]; + final dut = Sum(intfs, initialValue: 4, minValue: 12, maxValue: 12); + + final actual = dut.sum.value.toInt(); + final expected = goldenSum( + intfs, + width: dut.width, + minVal: 12, + maxVal: 12, + initialValue: 4, + ); + expect(actual, 12); + expect(actual, expected); + }); + + group('reached', () { + test('has overflowed', () { + final dut = Sum.ofLogics([Const(10, width: 8)], + width: 8, maxValue: 5, saturates: true); + expect(dut.overflowed.value.toBool(), true); + expect(dut.sum.value.toInt(), 5); + }); + + test('not overflowed', () { + final dut = Sum.ofLogics([Const(3, width: 8)], + width: 8, maxValue: 5, saturates: true); + expect(dut.overflowed.value.toBool(), false); + expect(dut.sum.value.toInt(), 3); + }); + + test('has underflowed', () { + final dut = Sum([ + SumInterface(fixedAmount: 10, increments: false), + ], width: 8, minValue: 15, initialValue: 20, saturates: true); + expect(dut.underflowed.value.toBool(), true); + expect(dut.sum.value.toInt(), 15); + }); + + test('not underflowed', () { + final dut = Sum([ + SumInterface(fixedAmount: 3, increments: false), + ], width: 8, minValue: 15, initialValue: 20, saturates: true); + expect(dut.underflowed.value.toBool(), false); + expect(dut.sum.value.toInt(), 17); + }); + }); + + test('sum ofLogics enable', () async { + final a = Logic(width: 3); + final b = Logic(width: 3); + final en = Logic(); + final dut = Sum.ofLogics( + [a, b], + width: 2, + enable: en, + ); + await dut.build(); + + expect(dut.width, 2); + + a.put(2); + b.put(3); + en.put(false); + expect(dut.sum.value.toInt(), 0); + en.put(true); + expect(dut.sum.value.toInt(), 1); + }); + + test('very large widths', () async { + final initVal = BigInt.two.pow(66) + BigInt.one; + final a = BigInt.two.pow(80) + BigInt.one; + final b = BigInt.from(1234); + final dut = Sum( + [ + SumInterface(fixedAmount: a), + SumInterface(width: 75)..amount.gets(Const(b, width: 75)), + ], + initialValue: initVal, + maxValue: BigInt.two.pow(82), + ); + await dut.build(); + + expect(dut.sum.value.toBigInt(), initVal + a + b); + expect(dut.width, greaterThan(80)); + }); + + test('random', () { + final rand = Random(123); + + SumInterface genRandomInterface() { + final isFixed = rand.nextBool(); + return SumInterface( + fixedAmount: isFixed ? rand.nextInt(100) : null, + width: isFixed ? null : rand.nextInt(8), + increments: rand.nextBool(), + hasEnable: rand.nextBool(), + ); + } + + List genRandomInterfaces() { + final numInterfaces = rand.nextInt(8) + 1; + return List.generate(numInterfaces, (_) => genRandomInterface()); + } + + for (var i = 0; i < 1000; i++) { + final interfaces = genRandomInterfaces(); + + final width = rand.nextBool() ? null : rand.nextInt(10) + 1; + + final saturates = rand.nextBool(); + var minVal = rand.nextBool() ? rand.nextInt(30) : 0; + var maxVal = rand.nextBool() + ? rand.nextInt(width == null ? 70 : ((1 << width) - 1)) + minVal + 1 + : null; + var initialValue = rand.nextBool() ? rand.nextInt(maxVal ?? 100) : 0; + + if (maxVal != null && width != null) { + // truncate to width + maxVal = max(1, LogicValue.ofInt(maxVal, width).toInt()); + } + + if (width != null) { + // truncate to width + initialValue = LogicValue.ofInt(initialValue, width).toInt(); + } + + if (maxVal == null || minVal >= maxVal) { + if (maxVal == null && width == null) { + minVal = 0; + } else { + minVal = + rand.nextInt(maxVal ?? (width == null ? 0 : (1 << width) - 1)); + } + } + + for (final intf in interfaces) { + if (intf.hasEnable) { + intf.enable!.put(rand.nextBool()); + } + + if (intf.fixedAmount == null) { + intf.amount.put(rand.nextInt(1 << intf.width)); + } + } + + int safeWidthFor(int val) { + final lv = LogicValue.ofInferWidth(val); + final inferredWidth = lv.width; + + return min(max(inferredWidth, 1), width ?? inferredWidth); + } + + final dut = Sum(interfaces, + saturates: saturates, + maxValue: maxVal != null && rand.nextBool() + ? Const(LogicValue.ofInferWidth(maxVal), + width: safeWidthFor(maxVal)) + : maxVal, + minValue: rand.nextBool() + ? Const(LogicValue.ofInferWidth(minVal), + width: safeWidthFor(minVal)) + : minVal, + width: width, + initialValue: rand.nextBool() + ? Const(LogicValue.ofInferWidth(initialValue), + width: safeWidthFor(initialValue)) + : initialValue); + + final actual = dut.sum.value.toInt(); + final expected = goldenSum( + interfaces, + width: dut.width, + saturates: saturates, + maxVal: maxVal, + minVal: minVal, + initialValue: initialValue, + ); + + expect(actual, expected); + } + }); +}