diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 3d485094f..780efba84 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -61,9 +61,18 @@ jobs: - name: Install software - Icarus Verilog run: tool/gh_actions/install_iverilog.sh + - name: Install software - Accellera SystemC + run: tool/gh_actions/install_systemc.sh + + - name: Pre-build SystemC PCH and Makefile + run: tool/gh_actions/setup_systemc_pch.sh + - name: Run project tests run: tool/gh_actions/run_tests.sh + - name: Run SystemC tests + run: dart test test/systemc_vector_test.dart + - name: Check temporary test files run: tool/gh_actions/check_tmp_test.sh @@ -71,7 +80,10 @@ jobs: - name: Build dev container and run tests in it uses: devcontainers/ci@v0.3 with: - runCmd: tool/gh_actions/run_tests.sh + runCmd: | + tool/gh_actions/run_tests.sh + dart test test/systemc_vector_test.dart + tool/gh_actions/check_tmp_test.sh deploy-documentation: name: Deploy Documentation diff --git a/README.md b/README.md index b812acc2f..1863edac9 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ You can also open this repository in a GitHub Codespace to run the example in yo - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/doc/architecture.md b/doc/architecture.md index cc1e775ae..aacf29c35 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -24,7 +24,7 @@ The `Simulator` acts as a statically accessible driver of the overall simulation ### Synthesizer -A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog. +A separate type of object responsible for taking a `Module` and converting it to some output, such as SystemVerilog or SystemC. ## Organization @@ -44,7 +44,7 @@ Contains a collection of `Module` implementations that can be used as primitive ### Synthesizers -Contains logic for synthesizing `Module`s into some output. It is structured to maximize reusability across different output types (including those not yet supported). +Contains logic for synthesizing `Module`s into some output (e.g. SystemVerilog, SystemC). It is structured to maximize reusability across different output types. ### Utilities diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 00d3d25bb..27135a53f 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -5,7 +5,7 @@ last_modified_at: 2023-11-13 toc: true --- -Hardware in ROHD is convertible to an output format via `Synthesizer`s, the most popular of which is SystemVerilog. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog with structure, hierarchy, ports, and names maintained. +Hardware in ROHD is convertible to an output format via `Synthesizer`s. The most popular output format is SystemVerilog, with SystemC also available. Hardware in ROHD can be converted to logically equivalent, human-readable SystemVerilog or SystemC with structure, hierarchy, ports, and names maintained. The simplest way to generate SystemVerilog is with the helper method `generateSynth` in `Module`: @@ -28,6 +28,26 @@ void main() async { The `generateSynth` function will return a `String` with the SystemVerilog `module` definitions for the top-level it is called on, as well as any sub-modules (recursively). You can dump the entire contents to a file and use it anywhere you would any other SystemVerilog. +## SystemC generation + +ROHD can also generate SystemC (C++ with the SystemC library) from the same hardware description. Use the `generateSystemC` helper method: + +```dart +void main() async { + final myModule = MyModule(); + await myModule.build(); + + final generatedSc = myModule.generateSystemC(); + + // write it to a file + File('myHardware.h').writeAsStringSync(generatedSc); +} +``` + +The generated SystemC uses `SC_MODULE`, `SC_METHOD`, and `SC_CTHREAD` constructs. Combinational logic becomes `SC_METHOD` processes, sequential logic (flip-flops and `Sequential` blocks) sharing the same clock and reset are consolidated into a single `SC_CTHREAD`, and sub-modules are instantiated with port bindings. All signal types map to SystemC equivalents (`bool`, `sc_uint`, `sc_biguint`). + +For more control over SystemC generation, use `SynthBuilder` with `SystemCSynthesizer()` directly. + ## Controlling naming ### Modules diff --git a/doc/user_guide/_get-started/01-overview.md b/doc/user_guide/_get-started/01-overview.md index c1a98cdc1..c30c9f87f 100644 --- a/doc/user_guide/_get-started/01-overview.md +++ b/doc/user_guide/_get-started/01-overview.md @@ -19,7 +19,7 @@ Features of ROHD include: - **Simple and fast build**, free of complex build systems and EDA vendor tools - Can use the excellent pub.dev **package manager** and all the packages it has to offer - Built-in event-based **fast simulator** with **4-value** (0, 1, X, and Z) support and a **waveform dumper** to .vcd file format -- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** for integration or downstream tool consumption +- Conversion of modules to equivalent, human-readable, structurally similar **SystemVerilog** and **SystemC** for integration or downstream tool consumption - **Run-time dynamic** module port definitions (numbers, names, widths, etc.) and internal module logic, including recursive module contents - Leverage the [ROHD Hardware Component Library (ROHD-HCL)](https://github.com/intel/rohd-hcl) with reusable and configurable design and verification components. - Simple, free, **open source tool stack** without any headaches from library dependencies, file ordering, elaboration/analysis options, +defines, etc. diff --git a/example/example.dart b/example/example.dart index 2ddbfc738..7715ffb34 100644 --- a/example/example.dart +++ b/example/example.dart @@ -11,35 +11,16 @@ // allow `print` messages (disable lint): // ignore_for_file: avoid_print -// Import necessary dart packages for this file. +// Import necessary dart pacakges for this file. import 'dart:async'; // Import the ROHD package. import 'package:rohd/rohd.dart'; -// Define a class Counter that extends ROHD's abstract Module class. -class Counter extends Module { - // For convenience, map interesting outputs to short variable names for - // consumers of this module. - Logic get val => output('val'); - - // This counter supports any width, determined at run-time. - final int width; - - Counter(Logic en, Logic reset, Logic clk, - {this.width = 8, super.name = 'counter'}) { - // Register inputs and outputs of the module in the constructor. - // Module logic must consume registered inputs and output to registered - // outputs. - en = addInput('en', en); - reset = addInput('reset', reset); - clk = addInput('clk', clk); - addOutput('val', width: width); - - // We can use the `flop` function to automate creation of a `Sequential`. - val <= flop(clk, reset: reset, en: en, val + 1); - } -} +// Re-export the Counter module from the library examples so that +// existing tests that `import 'example/example.dart'` still see it. +import 'package:rohd/src/examples/oven_fsm_modules.dart' show Counter; +export 'package:rohd/src/examples/oven_fsm_modules.dart' show Counter; // Let's simulate with this counter a little, generate a waveform, and take a // look at generated SystemVerilog. @@ -76,8 +57,9 @@ Future main({bool noPrint = false}) async { // Let's also print a message every time the value on the counter changes, // just for this example to make it easier to see before we look at waves. if (!noPrint) { - counter.val.changed - .listen((e) => print('@${Simulator.time}: Value changed: $e')); + counter.val.changed.listen( + (e) => print('@${Simulator.time}: Value changed: $e'), + ); } // Start off with a disabled counter and asserting reset at the start. @@ -115,7 +97,9 @@ Future main({bool noPrint = false}) async { // We can take a look at the waves now. if (!noPrint) { - print('To view waves, check out waves.vcd with a waveform viewer' - ' (e.g. `gtkwave waves.vcd`).'); + print( + 'To view waves, check out waves.vcd with a waveform viewer' + ' (e.g. `gtkwave waves.vcd`).', + ); } } diff --git a/example/filter_bank.dart b/example/filter_bank.dart new file mode 100644 index 000000000..b1d6f6eb3 --- /dev/null +++ b/example/filter_bank.dart @@ -0,0 +1,117 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank.dart +// A polyphase FIR filter bank design example exercising: +// - Deep hierarchy with shared sub-module definitions +// - Interface (FilterDataInterface) +// - LogicStructure (FilterSample) +// - LogicArray (coefficient storage) +// - Pipeline (pipelined MAC accumulation) +// - FiniteStateMachine (FilterController) +// +// The filter bank has two channels that share an identical MacUnit definition. +// A controller FSM sequences: idle → loading → running → draining → done. +// +// 2026 March 26 +// Author: Desmond Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; + +// Import module definitions. +import 'package:rohd/src/examples/filter_bank_modules.dart'; + +// Re-export so downstream consumers (e.g. devtools loopback) can use. +export 'package:rohd/src/examples/filter_bank_modules.dart'; + +// ────────────────────────────────────────────────────────────────── +// Standalone simulation entry point +// ────────────────────────────────────────────────────────────────── + +Future main({bool noPrint = false}) async { + const dataWidth = 16; + const numTaps = 3; + + // Low-pass-ish coefficients (scaled integers) + const coeffs0 = [1, 2, 1]; // channel 0: symmetric LPF kernel + const coeffs1 = [1, -2, 1]; // channel 1: high-pass kernel + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = LogicArray([2], dataWidth, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [coeffs0, coeffs1], + ); + + // Before we can simulate or generate code, we need to build it. + await dut.build(); + + // Set a maximum time for the simulation so it doesn't keep running forever. + Simulator.setMaxSimTime(500); + + // Attach a waveform dumper so we can see what happens. + if (!noPrint) { + WaveDumper(dut, outputPath: 'filter_bank.vcd'); + } + + // Kick off the simulation. + unawaited(Simulator.run()); + + // ── Reset ── + reset.inject(1); + start.inject(0); + samplesIn.elements[0].inject(0); + samplesIn.elements[1].inject(0); + validIn.inject(0); + inputDone.inject(0); + + await clk.nextPosedge; + await clk.nextPosedge; + reset.inject(0); + + // ── Start filtering ── + await clk.nextPosedge; + start.inject(1); + await clk.nextPosedge; + start.inject(0); + validIn.inject(1); + + // ── Feed sample stream: impulse response test ── + // Send a single '1' followed by zeros to get the impulse response + samplesIn.elements[0].inject(1); + samplesIn.elements[1].inject(1); + await clk.nextPosedge; + + for (var i = 0; i < 8; i++) { + samplesIn.elements[0].inject(0); + samplesIn.elements[1].inject(0); + await clk.nextPosedge; + } + + // ── Signal end of input ── + validIn.inject(0); + inputDone.inject(1); + await clk.nextPosedge; + inputDone.inject(0); + + // ── Wait for drain ── + for (var i = 0; i < 15; i++) { + await clk.nextPosedge; + } + + await Simulator.endSimulation(); +} diff --git a/example/oven_fsm.dart b/example/oven_fsm.dart index 2788baa55..8f69f697b 100644 --- a/example/oven_fsm.dart +++ b/example/oven_fsm.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // oven_fsm.dart @@ -14,175 +14,12 @@ import 'dart:async'; import 'package:rohd/rohd.dart'; -// Import the counter module implement in example.dart. -import './example.dart'; +// Import module definitions (Counter, OvenModule, enums). +import 'package:rohd/src/examples/oven_fsm_modules.dart'; -// Enumerated type named `OvenState` with four possible states: -// `standby`, `cooking`,`paused`, and `completed`. -enum OvenState { standby, cooking, paused, completed } - -// One-hot encoded `Button` using dart enhanced enums. -// Represent start, pause, and resume as integer value 0, 1, -// and 2 respectively. -enum Button { - start(value: 0), - pause(value: 1), - resume(value: 2); - - const Button({required this.value}); - - final int value; -} - -// One-hot encoded `LEDLight` using dart enhanced enums. -// Represent yellow, blue, red, and green as integer value 0, 1, -// 2, and 3 respectively. -enum LEDLight { - yellow(value: 0), - blue(value: 1), - red(value: 2), - green(value: 3); - - const LEDLight({required this.value}); - - final int value; -} - -// Define a class OvenModule that extends ROHD's abstract Module class. -class OvenModule extends Module { - // A private variable with type FiniteStateMachine `_oven`. - // - // Use `late` to indicate that the value will not be null - // and will be assign in the later section. - late FiniteStateMachine _oven; - - // We can expose an LED light output as a getter to retrieve it value. - Logic get led => output('led'); - - // This oven module receives a `button` and a `reset` input from runtime. - OvenModule(Logic button, Logic reset, Logic clk) : super(name: 'OvenModule') { - // Register inputs and outputs of the module in the constructor. - // Module logic must consume registered inputs and output to registered - // outputs. `led` output also added as the output port. - button = addInput('button', button, width: button.width); - reset = addInput('reset', reset); - clk = addInput('clk', clk); - final led = addOutput('led', width: button.width); - - // Register local signals, `counterReset` and `en` - // for Counter module. - final counterReset = Logic(name: 'counter_reset'); - final en = Logic(name: 'counter_en'); - - // An internal counter module that will be used to time the cooking state. - // Receive `en`, `counterReset` and `clk` as input. - final counter = Counter(en, counterReset, clk, name: 'counter_module'); - - // A list of `OvenState` that describe the FSM. Note that - // `OvenState` consists of identifier, events and actions. We - // can think of `identifier` as the state name, `events` is a map of event - // that trigger next state. `actions` is the behaviour of current state, - // like what is the actions need to be shown separate current state with - // other state. Represented as List of conditionals to be executed. - final states = [ - // identifier: standby state, represent by `OvenState.standby`. - State(OvenState.standby, - // events: - // When the button `start` is pressed during standby state, - // OvenState will changed to `OvenState.cooking` state. - events: { - Logic(name: 'button_start') - ..gets(button - .eq(Const(Button.start.value, width: button.width))): - OvenState.cooking, - }, - // actions: - // During the standby state, `led` is change to blue; timer's - // `counterReset` is set to 1 (Reset the timer); - // timer's `en` is set to 0 (Disable value update). - actions: [ - led < LEDLight.blue.value, - counterReset < 1, - en < 0, - ]), - - // identifier: cooking state, represent by `OvenState.cooking`. - State(OvenState.cooking, - // events: - // When the button `paused` is pressed during cooking state, - // OvenState will changed to `OvenState.paused` state. - // - // When the button `counter` time is elapsed during cooking state, - // OvenState will changed to `OvenState.completed` state. - events: { - Logic(name: 'button_pause') - ..gets(button - .eq(Const(Button.pause.value, width: button.width))): - OvenState.paused, - Logic(name: 'counter_time_complete')..gets(counter.val.eq(4)): - OvenState.completed - }, - // actions: - // During the cooking state, `led` is change to yellow; timer's - // `counterReset` is set to 0 (Do not reset); - // timer's `en` is set to 1 (Enable value update). - actions: [ - led < LEDLight.yellow.value, - counterReset < 0, - en < 1, - ]), - - // identifier: paused state, represent by `OvenState.paused`. - State(OvenState.paused, - // events: - // When the button `resume` is pressed during paused state, - // OvenState will changed to `OvenState.cooking` state. - events: { - Logic(name: 'button_resume') - ..gets(button - .eq(Const(Button.resume.value, width: button.width))): - OvenState.cooking - }, - // actions: - // During the paused state, `led` is change to red; timer's - // `counterReset` is set to 0 (Do not reset); - // timer's `en` is set to 0 (Disable value update). - actions: [ - led < LEDLight.red.value, - counterReset < 0, - en < 0, - ]), - - // identifier: completed state, represent by `OvenState.completed`. - State(OvenState.completed, - // events: - // When the button `start` is pressed during completed state, - // OvenState will changed to `OvenState.cooking` state. - events: { - Logic(name: 'button_start') - ..gets(button - .eq(Const(Button.start.value, width: button.width))): - OvenState.cooking - }, - // actions: - // During the start state, `led` is change to green; timer's - // `counterReset` is set to 1 (Reset value); - // timer's `en` is set to 0 (Disable value update). - actions: [ - led < LEDLight.green.value, - counterReset < 1, - en < 0, - ]) - ]; - - // Assign the _oven FiniteStateMachine object to private variable declared. - _oven = - FiniteStateMachine(clk, reset, OvenState.standby, states); - } - - // An oven FiniteStateMachine that represent in getter. - FiniteStateMachine get ovenStateMachine => _oven; -} +// Re-export module definitions so test files that import this file +// get access to OvenModule, OvenState, Button, LEDLight, etc. +export 'package:rohd/src/examples/oven_fsm_modules.dart' hide Counter; /// A helper function to wait for a number of cycles. Future waitCycles(Logic clk, int numCycles) async { diff --git a/example/tree.dart b/example/tree.dart index f5c30a979..8f5b2f96a 100644 --- a/example/tree.dart +++ b/example/tree.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // tree.dart @@ -13,6 +13,13 @@ import 'package:rohd/rohd.dart'; +// Import module definition. +import 'package:rohd/src/examples/tree_modules.dart'; + +// Re-export module definition so test files that import this file +// get access to TreeOfTwoInputModules. +export 'package:rohd/src/examples/tree_modules.dart'; + /// The below example demonstrates some aspects of the power of ROHD where /// writing equivalent design code in SystemVerilog can be challenging or /// impossible. The example is a port from an example used by Chisel. @@ -35,36 +42,6 @@ import 'package:rohd/rohd.dart'; /// number of inputs and different logic without any explicit /// parameterization. -class TreeOfTwoInputModules extends Module { - final Logic Function(Logic a, Logic b) _op; - final List _seq = []; - Logic get out => output('out'); - - TreeOfTwoInputModules(List seq, this._op) - : super(name: 'tree_of_two_input_modules') { - if (seq.isEmpty) { - throw Exception("Don't use TreeOfTwoInputModules with an empty sequence"); - } - - for (var i = 0; i < seq.length; i++) { - _seq.add(addInput('seq$i', seq[i], width: seq[i].width)); - } - addOutput('out', width: seq[0].width); - - if (_seq.length == 1) { - out <= _seq[0]; - } else { - final a = TreeOfTwoInputModules( - _seq.getRange(0, _seq.length ~/ 2).toList(), _op) - .out; - final b = TreeOfTwoInputModules( - _seq.getRange(_seq.length ~/ 2, _seq.length).toList(), _op) - .out; - out <= _op(a, b); - } - } -} - Future main({bool noPrint = false}) async { // You could instantiate this module with some code such as: final tree = TreeOfTwoInputModules( diff --git a/lib/rohd.dart b/lib/rohd.dart index 841505590..075bda975 100644 --- a/lib/rohd.dart +++ b/lib/rohd.dart @@ -1,6 +1,8 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'src/diagnostics/inspector_service.dart'; +export 'src/diagnostics/module_services.dart'; export 'src/exceptions/exceptions.dart'; export 'src/external.dart'; export 'src/finite_state_machine.dart'; diff --git a/lib/src/diagnostics/module_services.dart b/lib/src/diagnostics/module_services.dart new file mode 100644 index 000000000..30a51a410 --- /dev/null +++ b/lib/src/diagnostics/module_services.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services.dart +// Singleton service registry for DevTools integration. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; + +/// Singleton service registry that provides a unified query surface for +/// DevTools and other inspection tools. +/// +/// Services register themselves here on construction; DevTools evaluates +/// getters on [instance] via `EvalOnDartLibrary` to pull data. +/// +/// **Auto-registered:** +/// - [rootModule] / [hierarchyJSON] — set by [Module.build]. +/// +/// **Opt-in (registered by service constructors):** +/// - [svService] — SystemVerilog synthesis results. +/// - [netlistService] — Yosys-format netlist JSON. +/// +/// Additional services (trace, waveform) can be added by setting the +/// corresponding field after construction. +class ModuleServices { + ModuleServices._(); + + /// The singleton instance. + static final ModuleServices instance = ModuleServices._(); + + // ─── Hierarchy (auto-registered by Module.build) ────────────── + + /// The most recently built top-level [Module]. + /// + /// Set automatically at the end of [Module.build]. + Module? rootModule; + + /// Returns the module hierarchy as a JSON string. + /// + /// DevTools evaluates this via `EvalOnDartLibrary` to display + /// the module hierarchy. + String get hierarchyJSON { + ModuleTree.rootModuleInstance = rootModule; + return ModuleTree.instance.hierarchyJSON; + } + + /// Returns the unified inspector JSON — the primary entry point for + /// DevTools to load the design. + /// + /// When a [NetlistService] is registered, this returns the slim netlist + /// (hierarchy + ports + cells without connectivity). + /// + /// Falls back to the hierarchy JSON when no netlist service is available. + String get inspectorJSON { + if (netlistService != null) { + return netlistService!.slimJson; + } + return hierarchyJSON; + } + + /// Returns the full netlist JSON for a single module definition. + /// + /// When a [NetlistService] is registered, returns the per-module netlist + /// (with full connectivity). + String inspectorModuleJSON(String definitionName) { + if (netlistService != null) { + return netlistService!.moduleJson(definitionName); + } + return _unavailable('netlist'); + } + + // ─── SystemVerilog service (opt-in) ─────────────────────────── + + /// The active [SvService], if one has been registered. + SvService? svService; + + /// Returns SV synthesis metadata as JSON, or an unavailable status. + String get svJSON => + svService != null ? jsonEncode(svService!.toJson()) : _unavailable('sv'); + + // ─── Netlist service (opt-in) ───────────────────────────────── + + /// The active [NetlistService], if one has been registered. + NetlistService? netlistService; + + /// Returns the full netlist hierarchy as JSON, or an unavailable status. + String get netlistJSON => netlistService != null + ? netlistService!.toJson() + : _unavailable('netlist'); + + /// Returns the netlist for a single module definition, or unavailable. + String netlistModuleJSON(String definitionName) => netlistService != null + ? netlistService!.moduleJson(definitionName) + : _unavailable('netlist'); + + // ─── Helpers ────────────────────────────────────────────────── + + static String _unavailable(String service) => jsonEncode({ + 'status': 'unavailable', + 'reason': '$service service not registered', + }); + + /// Resets all services. Intended for test teardown. + void reset() { + rootModule = null; + svService = null; + netlistService = null; + } +} diff --git a/lib/src/examples/filter_bank_modules.dart b/lib/src/examples/filter_bank_modules.dart new file mode 100644 index 000000000..21a0f1ccd --- /dev/null +++ b/lib/src/examples/filter_bank_modules.dart @@ -0,0 +1,954 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_modules.dart +// Module class definitions for the polyphase FIR filter bank example. +// +// 2025 March 26 +// Author: Desmond Kirkpatrick +// +// Architecture: each FilterChannel uses a single MacUnit that is +// time-multiplexed across taps. A tap counter sequences CoeffBank +// and a delay-line mux so the MAC accumulates one tap per clock cycle. +// After numTaps cycles the accumulated result is latched as the output +// sample and the accumulator resets for the next input sample. +// +// ROHD features exercised: +// - LogicStructure (FilterSample) +// - Interface (FilterDataInterface) +// - LogicArray (CoeffBank coefficient ROM, delay line) +// - Pipeline (MacUnit multiply-accumulate) +// - FiniteStateMachine (FilterController) +// - Multiple instantiation (two FilterChannels share one definition) +// +// Separated from filter_bank.dart so these classes can be imported +// in web-targeted code (no dart:io dependency). +// +// 2026 March 26 +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +// ────────────────────────────────────────────────────────────────── +// LogicStructure: a typed sample word carrying data + valid + channel +// ────────────────────────────────────────────────────────────────── + +/// A structured signal bundling a data sample with metadata. +/// +/// Packs three fields — [data], [valid], and [channel] — into a single +/// bus that can be driven and sampled as a unit. Used throughout the +/// [FilterBank] to carry tagged samples between modules. +class FilterSample extends LogicStructure { + /// The sample data word. + late final Logic data; + + /// Whether this sample is valid. + late final Logic valid; + + /// The channel index this sample belongs to. + late final Logic channel; + + /// Creates a [FilterSample] with the given [dataWidth] (default 16) + /// and optional [name]. + FilterSample({int dataWidth = 16, String? name}) + : super( + [ + Logic(name: 'data', width: dataWidth), + Logic(name: 'valid'), + Logic(name: 'channel'), + ], + name: name ?? 'filter_sample', + ) { + data = elements[0]; + valid = elements[1]; + channel = elements[2]; + } + + // Private constructor for clone to share element structure. + FilterSample._clone(super.elements, {required super.name}) { + data = elements[0]; + valid = elements[1]; + channel = elements[2]; + } + + @override + + /// Returns a structural clone of this sample, preserving element names. + FilterSample clone({String? name}) => FilterSample._clone( + elements.map((e) => e.clone(name: e.name)), + name: name ?? this.name, + ); +} + +// ────────────────────────────────────────────────────────────────── +// Interface: tagged port bundle for filter data I/O +// ────────────────────────────────────────────────────────────────── + +/// Tags for grouping port directions in [FilterDataInterface]. +enum FilterPortTag { + /// Ports carrying data into the filter (`sampleIn`, `validIn`). + inputPorts, + + /// Ports carrying data out of the filter (`dataOut`, `validOut`). + outputPorts, +} + +/// An interface carrying sample data and control into/out of filter modules. +/// +/// Groups ports by [FilterPortTag] so that [connectIO] can wire +/// inputs and outputs in a single call. +class FilterDataInterface extends Interface { + /// Input sample data bus. + Logic get sampleIn => port('sampleIn'); + + /// Input valid strobe. + Logic get validIn => port('validIn'); + + /// Output filtered data bus. + Logic get dataOut => port('dataOut'); + + /// Output valid strobe. + Logic get validOut => port('validOut'); + + /// The data width used by this interface. + final int _dataWidth; + + /// Creates a [FilterDataInterface] with the given [dataWidth] + /// (default 16 bits). + FilterDataInterface({int dataWidth = 16}) : _dataWidth = dataWidth { + setPorts([ + Logic.port('sampleIn', dataWidth), + Logic.port('validIn'), + ], [ + FilterPortTag.inputPorts + ]); + + setPorts([ + Logic.port('dataOut', dataWidth), + Logic.port('validOut'), + ], [ + FilterPortTag.outputPorts + ]); + } + + @override + + /// Returns a new interface with the same data width. + FilterDataInterface clone() => FilterDataInterface(dataWidth: _dataWidth); +} + +// ────────────────────────────────────────────────────────────────── +// CoeffBank: stores FIR tap coefficients in a LogicArray +// ────────────────────────────────────────────────────────────────── + +/// A coefficient storage module backed by a [LogicArray] input port. +/// +/// Accepts a [LogicArray] of per-tap coefficients via [addInputArray] +/// and a tap index, then mux-selects the corresponding coefficient. +class CoeffBank extends Module { + /// The coefficient value at the selected index. + Logic get coeffOut => output('coeffOut'); + + /// The per-tap coefficient array (registered input port). + @protected + LogicArray get coeffArray => input('coeffArray') as LogicArray; + + /// The tap index input. + @protected + Logic get tapIndex => input('tapIndex'); + + /// Number of taps. + final int numTaps; + + /// Data width. + final int dataWidth; + + /// Creates a [CoeffBank] with [numTaps] taps at [dataWidth] bits. + /// + /// [coefficients] is a [LogicArray] with one element per tap — + /// registered as an input port via [addInputArray]. + /// [tapIndex] selects the active coefficient. + CoeffBank(Logic tapIndex, LogicArray coefficients, + {required this.numTaps, + required this.dataWidth, + super.name = 'CoeffBank'}) + : super(definitionName: 'CoeffBank_T${numTaps}_W$dataWidth') { + // Register ports + tapIndex = addInput('tapIndex', tapIndex, width: tapIndex.width); + final coeffArray = addInputArray('coeffArray', coefficients, + dimensions: [numTaps], elementWidth: dataWidth); + final coeffOut = addOutput('coeffOut', width: dataWidth); + + // Mux-chain ROM: priority-select coefficient by tap index. + Logic selected = Const(0, width: dataWidth); + for (var i = numTaps - 1; i >= 0; i--) { + selected = mux( + tapIndex.eq(Const(i, width: tapIndex.width)).named('tapMatch$i'), + coeffArray.elements[i], + selected, + ); + } + coeffOut <= selected; + } +} + +// ────────────────────────────────────────────────────────────────── +// MacUnit: a single multiply-accumulate pipeline stage +// ────────────────────────────────────────────────────────────────── + +/// A pipelined multiply-accumulate unit. +/// +/// Pipeline stage 0: multiply sample × coefficient +/// Pipeline stage 1: add product to running accumulator +class MacUnit extends Module { + /// Accumulated result. + Logic get result => output('result'); + + /// Sample data input. + @protected + Logic get sampleInPin => input('sampleIn'); + + /// Coefficient input. + @protected + Logic get coeffInPin => input('coeffIn'); + + /// Accumulator input. + @protected + Logic get accumInPin => input('accumIn'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Data width. + final int dataWidth; + + /// Creates a [MacUnit] that multiplies [sampleIn] by [coeffIn] in + /// stage 0 and adds the product to [accumIn] in stage 1. + /// + /// [clk], [reset], and [enable] control the pipeline registers. + MacUnit(Logic sampleIn, Logic coeffIn, Logic accumIn, Logic clk, Logic reset, + Logic enable, + {required this.dataWidth, super.name = 'MacUnit'}) + : super(definitionName: 'MacUnit_W$dataWidth') { + sampleIn = addInput('sampleIn', sampleIn, width: dataWidth); + coeffIn = addInput('coeffIn', coeffIn, width: dataWidth); + accumIn = addInput('accumIn', accumIn, width: dataWidth); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + final result = addOutput('result', width: dataWidth); + + // A 2-stage pipeline: multiply, then accumulate + final pipe = Pipeline( + clk, + reset: reset, + stages: [ + // Stage 0: multiply + (p) => [ + // Product = sample * coefficient (truncated to dataWidth) + p.get(sampleIn) < + (p.get(sampleIn) * p.get(coeffIn)).named('product'), + ], + // Stage 1: accumulate + (p) => [ + p.get(sampleIn) < + (p.get(sampleIn) + p.get(accumIn)).named('macSum'), + ], + ], + signals: [sampleIn, coeffIn, accumIn], + ); + + result <= pipe.get(sampleIn); + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterChannel: one polyphase FIR channel with time-multiplexed MAC +// ────────────────────────────────────────────────────────────────── + +/// A single polyphase FIR filter channel with [numTaps] taps. +/// +/// Uses a [FilterDataInterface] for its sample I/O ports. +/// +/// Architecture: +/// - A delay line (shift register) captures incoming samples. +/// - A tap counter cycles 0 … numTaps-1 each sample period. +/// - [CoeffBank] provides the coefficient for the current tap. +/// - A mux selects the delay-line sample for the current tap. +/// - A single [MacUnit] multiplies the selected sample by the +/// coefficient and adds it to a running accumulator. +/// - After all taps are processed the accumulator is latched as +/// the output and the accumulator resets for the next sample. +class FilterChannel extends Module { + /// The data interface for this channel (internal use only). + @protected + late final FilterDataInterface intf; + + /// Filtered output. + Logic get dataOut => intf.dataOut; + + /// Output valid. + Logic get validOut => intf.validOut; + + /// Number of FIR taps in this channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Enable input. + @protected + Logic get enablePin => input('enable'); + + /// Creates a [FilterChannel] with [numTaps] taps at [dataWidth] bits. + /// + /// [srcIntf] provides the sample/valid input ports. [coefficients] + /// supplies per-tap constant coefficients. + FilterChannel( + FilterDataInterface srcIntf, + Logic clk, + Logic reset, + Logic enable, { + required this.numTaps, + required this.dataWidth, + required List coefficients, + super.name = 'FilterChannel', + }) : super(definitionName: 'FilterChannel_T${numTaps}_W$dataWidth') { + // Connect the Interface — creates module input/output ports + intf = FilterDataInterface(dataWidth: dataWidth) + ..connectIO(this, srcIntf, + inputTags: [FilterPortTag.inputPorts], + outputTags: [FilterPortTag.outputPorts]); + + final sampleIn = intf.sampleIn; + final validIn = intf.validIn; + clk = addInput('clk', clk); + reset = addInput('reset', reset); + enable = addInput('enable', enable); + + final tapIdxWidth = _bitsFor(numTaps); + + // ── Delay line (shift register via explicit flop bank + gates) ── + // AND gate: shift enable = enable & validIn & tapCounter==0 + // Samples shift in only when starting a new accumulation cycle. + final tapCounter = Logic(width: tapIdxWidth, name: 'tapCounter'); + final atFirstTap = + tapCounter.eq(Const(0, width: tapIdxWidth)).named('atFirstTap'); + final shiftEn = Logic(name: 'shiftEn'); + shiftEn <= (enable & validIn).named('enableAndValid') & atFirstTap; + + // LogicArray-backed delay line: one element per tap register. + final delayLine = LogicArray([numTaps], dataWidth, name: 'delayLine'); + for (var i = 0; i < numTaps; i++) { + final tapInput = (i == 0) ? sampleIn : delayLine.elements[i - 1]; + // Mux: hold current value or shift in new sample + final tapNext = Logic(width: dataWidth, name: 'nextTap$i'); + tapNext <= mux(shiftEn, tapInput, delayLine.elements[i]); + // Flop: register the next-state value + delayLine.elements[i] <= flop(clk, reset: reset, tapNext); + } + + // ── Coefficient bank — driven by tapCounter ── + // Build a LogicArray of constants from the coefficient list and + // pass it as an input port to CoeffBank (demonstrates addInputArray + // on a sub-module). + final coeffArray = LogicArray([numTaps], dataWidth, name: 'coeffArray'); + for (var i = 0; i < numTaps; i++) { + coeffArray.elements[i] <= Const(coefficients[i], width: dataWidth); + } + + final coeffBank = CoeffBank( + tapCounter, + coeffArray, + numTaps: numTaps, + dataWidth: dataWidth, + name: 'coeffBank', + ); + + // ── Delay-line mux — select sample for current tap ── + var selectedSample = delayLine.elements[0]; + for (var i = 1; i < numTaps; i++) { + final tapSelect = + tapCounter.eq(Const(i, width: tapIdxWidth)).named('tapSelect$i'); + selectedSample = mux(tapSelect, delayLine.elements[i], selectedSample) + .named('tapMux$i'); + } + + // ── Running accumulator (feedback register) ── + final accumReg = Logic(width: dataWidth, name: 'accumReg'); + // Reset accumulator at the start of each new sample (tap 0). + // Combinational block: equivalent to `always_comb` in SystemVerilog. + final accumFeedback = Logic(width: dataWidth, name: 'accumFeedback'); + Combinational([ + If(atFirstTap, then: [ + accumFeedback < Const(0, width: dataWidth), + ], orElse: [ + accumFeedback < accumReg, + ]), + ]); + + // ── Single MAC unit — time-multiplexed across taps ── + final mac = MacUnit( + selectedSample, + coeffBank.coeffOut, + accumFeedback, + clk, + reset, + enable, + dataWidth: dataWidth, + name: 'mac', + ); + + // Register the MAC result for accumulator feedback. + accumReg <= flop(clk, reset: reset, mac.result); + + // ── Tap counter: cycles 0 … numTaps-1 while enabled ── + // Sequential block: equivalent to `always_ff @(posedge clk)` in SV. + // When enabled, the counter increments and wraps at numTaps-1. + // When disabled, it resets to 0. + final lastTap = + tapCounter.eq(Const(numTaps - 1, width: tapIdxWidth)).named('lastTap'); + Sequential(clk, reset: reset, [ + If(enable, then: [ + If(lastTap, then: [ + tapCounter < Const(0, width: tapIdxWidth), + ], orElse: [ + tapCounter < tapCounter + Const(1, width: tapIdxWidth), + ]), + ], orElse: [ + tapCounter < Const(0, width: tapIdxWidth), + ]), + ]); + + // ── Output latch: capture accumulator when all taps processed ── + // The MAC pipeline has 2 stages, so the result is ready 2 cycles + // after the last tap enters. A 2-stage shift register of lastTap + // creates the latch strobe. + final lastTapD1 = Logic(name: 'lastTapD1'); + final lastTapD2 = Logic(name: 'lastTapD2'); + final outputReg = Logic(width: dataWidth, name: 'outputReg'); + + // Sequential block with If: latch strobe delay and output register. + Sequential(clk, reset: reset, [ + lastTapD1 < lastTap, + lastTapD2 < lastTapD1, + If(lastTapD2, then: [ + outputReg < accumReg, + ]), + ]); + + // ── Valid pipeline: track whether we have a valid output ── + // validIn is high during data injection. After the MAC pipeline + // latency (numTaps + 2 cycles), outputs become valid. + final validPipe = Logic(name: 'validPipe'); + final outputReady = (lastTapD2 & enable).named('outputReady'); + + // Sequential block: register the valid strobe and hold it. + Sequential(clk, reset: reset, [ + If(enable, then: [ + validPipe < outputReady, + ]), + ]); + + // Combinational block: gate the output to zero when not valid. + final dataOut = intf.dataOut; + final validOut = intf.validOut; + Combinational([ + If(validPipe, then: [ + dataOut < outputReg, + ], orElse: [ + dataOut < Const(0, width: dataWidth), + ]), + validOut < validPipe, + ]); + } + + /// Minimum bits needed to represent [n] values. + static int _bitsFor(int n) { + if (n <= 1) { + return 1; + } + var bits = 0; + var v = n - 1; + while (v > 0) { + bits++; + v >>= 1; + } + return bits; + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterController: FSM sequencing the filter bank +// ────────────────────────────────────────────────────────────────── + +/// States for the [FilterController] finite state machine. +enum FilterState { + /// Waiting for the start signal. + idle, + + /// Accepting initial samples into the delay line. + loading, + + /// Normal filtering operation. + running, + + /// Flushing the pipeline after the input stream ends. + draining, + + /// Processing complete. + done, +} + +/// Controls the filter bank operation via a [FiniteStateMachine]. +/// +/// - idle: waiting for start signal +/// - loading: accepting initial samples into delay line +/// - running: normal filtering +/// - draining: flushing pipeline after input stream ends +/// - done: processing complete +class FilterController extends Module { + /// Encoded FSM state (3 bits). + Logic get state => output('state'); + + /// High while the filter channels should be processing. + Logic get filterEnable => output('filterEnable'); + + /// High during the initial sample-loading phase. + Logic get loadingPhase => output('loadingPhase'); + + /// Asserted when the filter bank has finished processing. + Logic get doneFlag => output('doneFlag'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Input valid. + @protected + Logic get inputValidPin => input('inputValid'); + + /// Input done. + @protected + Logic get inputDonePin => input('inputDone'); + + late final FiniteStateMachine _fsm; + + /// Returns the FSM's current state index for a given [FilterState]. + int? getStateIndex(FilterState s) => _fsm.getStateIndex(s); + + /// Creates a [FilterController] that sequences the filter bank. + /// + /// After [start] is asserted the FSM moves through loading → running + /// → draining (for [drainCycles] cycles) → done. + FilterController( + Logic clk, Logic reset, Logic start, Logic inputValid, Logic inputDone, + {required int drainCycles, super.name = 'FilterController'}) + : super(definitionName: 'FilterController') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + inputValid = addInput('inputValid', inputValid); + inputDone = addInput('inputDone', inputDone); + + final filterEnable = addOutput('filterEnable'); + final loadingPhase = addOutput('loadingPhase'); + final doneFlag = addOutput('doneFlag'); + final state = addOutput('state', width: 3); + + // Drain counter + final drainCount = Logic(width: 8, name: 'drainCount'); + final drainDone = + drainCount.eq(Const(drainCycles, width: 8)).named('drainDone'); + + _fsm = FiniteStateMachine( + clk, + reset, + FilterState.idle, + [ + State( + FilterState.idle, + events: { + start: FilterState.loading, + }, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.loading, + events: { + inputValid: FilterState.running, + }, + actions: [ + filterEnable < 1, + loadingPhase < 1, + doneFlag < 0, + ], + ), + State( + FilterState.running, + events: { + inputDone: FilterState.draining, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.draining, + events: { + drainDone: FilterState.done, + }, + actions: [ + filterEnable < 1, + loadingPhase < 0, + doneFlag < 0, + ], + ), + State( + FilterState.done, + events: {}, + actions: [ + filterEnable < 0, + loadingPhase < 0, + doneFlag < 1, + ], + ), + ], + ); + + state <= _fsm.currentState.zeroExtend(state.width); + + // Drain counter: Sequential block increments while draining, + // resets to zero otherwise. + final drainIdx = _fsm.getStateIndex(FilterState.draining)!; + final isDraining = Logic(name: 'isDraining'); + isDraining <= _fsm.currentState.eq(Const(drainIdx, width: _fsm.stateWidth)); + + Sequential(clk, reset: reset, [ + If(isDraining, then: [ + drainCount < drainCount + Const(1, width: 8), + ], orElse: [ + drainCount < Const(0, width: 8), + ]), + ]); + } +} + +// ────────────────────────────────────────────────────────────────── +// FilterBank: top-level 2-channel polyphase FIR filter +// ────────────────────────────────────────────────────────────────── + +/// A 2-channel polyphase FIR filter bank. +/// +/// Hierarchy: +/// ```text +/// FilterBank (top) +/// ├── FilterController (FSM) +/// ├── FilterChannel 'ch0' +/// │ ├── CoeffBank (coefficient ROM via LogicArray + mux chain) +/// │ └── MacUnit 'mac' (pipelined multiply-accumulate) +/// └── FilterChannel 'ch1' +/// ├── CoeffBank +/// └── MacUnit 'mac' +/// ``` +/// +/// Each channel time-multiplexes a single MacUnit across all taps, +/// sequenced by a tap counter that drives the CoeffBank tap index +/// and a delay-line sample mux. +/// +/// Uses: +/// - [FilterDataInterface] for I/O port bundles +/// - [FilterSample] LogicStructure for structured sample signals +/// - [LogicArray] in CoeffBank for coefficient storage +/// - [Pipeline] in MacUnit for pipelined MAC +/// - [FiniteStateMachine] in FilterController for sequencing +/// - Multiple instantiation: two [FilterChannel]s share one definition +/// - [LogicNet] / [addInOut] for bidirectional shared data bus + +// ────────────────────────────────────────────────────────────────── +// SharedDataBus: bidirectional port for coefficient/status I/O +// ────────────────────────────────────────────────────────────────── + +/// A module with a bidirectional data bus for loading/reading data. +/// +/// In real hardware, a shared data bus is common for: +/// - Loading filter coefficients from external memory +/// - Reading diagnostic status or filter output snapshots +/// +/// Direction is controlled by `writeEnable`: when high, the module's +/// internal [TriStateBuffer] drives `storedValue` onto `dataBus`; +/// when low, the external driver owns the bus and the module latches +/// the incoming value into a register. +/// +/// Exercises `addInOut` / `LogicNet` / [TriStateBuffer] / inout port +/// direction through the full ROHD stack: synthesis, hierarchy, +/// waveform capture, and DevTools rendering. +class SharedDataBus extends Module { + /// The bidirectional data bus port. + Logic get dataBus => inOut('dataBus'); + + /// The stored value (latched when the bus is driven externally). + Logic get storedValue => output('storedValue'); + + /// Write-enable input. + @protected + Logic get writeEnablePin => input('writeEnable'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Data width in bits. + final int dataWidth; + + /// Creates a [SharedDataBus] with a [dataWidth]-bit bidirectional port. + /// + /// [dataBusNet] is the external [LogicNet] to connect. + /// [writeEnable] controls bus direction: 1 = module drives bus, + /// 0 = external drives bus (module reads). + /// [clk] and [reset] provide synchronous storage. + SharedDataBus( + LogicNet dataBusNet, + Logic writeEnable, + Logic clk, + Logic reset, { + required this.dataWidth, + super.name = 'SharedDataBus', + }) : super(definitionName: 'SharedDataBus') { + final bus = addInOut('dataBus', dataBusNet, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + final storedValue = addOutput('storedValue', width: dataWidth); + + // Latch the bus value on clock edge when the external side is driving. + storedValue <= + flop( + clk, + bus, + reset: reset, + en: ~writeEnable, + resetValue: Const(0, width: dataWidth), + ); + + // Drive the latched value back onto the bus when writeEnable is high. + // TriStateBuffer drives its out (a LogicNet) with storedValue when + // enabled; otherwise it outputs high-Z. Joining out↔bus makes the + // two nets share the same wire. + TriStateBuffer(storedValue, enable: writeEnable, name: 'busDriver') + .out + .gets(bus); + } +} + +/// The top-level polyphase FIR filter bank. +class FilterBank extends Module { + /// Per-channel filtered outputs as a [LogicArray]. + /// + /// `channelOut.elements[i]` is the filtered output of channel `i`. + LogicArray get channelOut => output('channelOut') as LogicArray; + + /// Channel 0 filtered output (convenience getter). + Logic get out0 => channelOut.elements[0]; + + /// Channel 1 filtered output (convenience getter). + Logic get out1 => channelOut.elements[1]; + + /// Output valid (aligned with filtered outputs). + Logic get validOut => output('validOut'); + + /// Done signal from the controller FSM. + Logic get done => output('done'); + + /// Controller state (for debug visibility). + Logic get state => output('state'); + + /// Clock input. + @protected + Logic get clkPin => input('clk'); + + /// Reset input. + @protected + Logic get resetPin => input('reset'); + + /// Start input. + @protected + Logic get startPin => input('start'); + + /// Per-channel sample input array. + @protected + LogicArray get samplesInPin => input('samplesIn') as LogicArray; + + /// Input valid strobe. + @protected + Logic get validInPin => input('validIn'); + + /// Input-done strobe. + @protected + Logic get inputDonePin => input('inputDone'); + + /// Number of FIR taps per channel. + final int numTaps; + + /// Bit width of each data sample. + final int dataWidth; + + /// Number of filter channels. + final int numChannels; + + /// Creates a [FilterBank] with [numChannels] channels (default 2). + /// + /// Each channel has [numTaps] FIR taps at [dataWidth] bits. + /// [coefficients] is a list of per-channel coefficient lists — + /// `coefficients[i]` supplies the tap weights for channel `i`. + /// [samplesIn] is a [LogicArray] with one element per channel. + /// [validIn] qualifies the sample data. Assert [start] to begin + /// and [inputDone] when the input stream is complete. + /// + /// Optionally pass [dataBus] (a `LogicNet`) and [writeEnable] to + /// attach a bidirectional shared data bus via [SharedDataBus]. + /// The bus latches external data when [writeEnable] is low and + /// drives `storedValue` output. + FilterBank( + Logic clk, + Logic reset, + Logic start, + LogicArray samplesIn, + Logic validIn, + Logic inputDone, { + required this.numTaps, + required this.dataWidth, + required List> coefficients, + this.numChannels = 2, + LogicNet? dataBus, + Logic? writeEnable, + super.name = 'FilterBank', + String? definitionName, + }) : super(definitionName: definitionName ?? 'FilterBank') { + if (coefficients.length != numChannels) { + throw Exception( + 'coefficients must have $numChannels entries (one per channel).'); + } + + // ── Register ports ── + clk = addInput('clk', clk); + reset = addInput('reset', reset); + start = addInput('start', start); + samplesIn = addInputArray('samplesIn', samplesIn, + dimensions: [numChannels], elementWidth: dataWidth); + validIn = addInput('validIn', validIn); + inputDone = addInput('inputDone', inputDone); + + final channelOut = addOutputArray('channelOut', + dimensions: [numChannels], elementWidth: dataWidth); + final validOut = addOutput('validOut'); + final done = addOutput('done'); + final state = addOutput('state', width: 3); + + // ── FilterSample LogicStructure for input bundling ── + final samples = []; + for (var ch = 0; ch < numChannels; ch++) { + final sample = FilterSample(dataWidth: dataWidth, name: 'sample$ch'); + sample.data <= samplesIn.elements[ch]; + sample.valid <= validIn; + sample.channel <= Const(ch); + samples.add(sample); + } + + // ── Controller FSM ── + // Drain cycles: numTaps cycles per accumulation + pipeline depth (2) + 1 + final controller = FilterController( + clk, + reset, + start, + validIn, + inputDone, + drainCycles: numTaps + 3, + name: 'controller', + ); + + final filterEnable = controller.filterEnable; + + // ── Per-channel filter instantiation ── + final srcIntfs = []; + for (var ch = 0; ch < numChannels; ch++) { + final srcIntf = FilterDataInterface(dataWidth: dataWidth); + srcIntf.sampleIn <= samples[ch].data; + srcIntf.validIn <= samples[ch].valid; + + FilterChannel( + srcIntf, + clk, + reset, + filterEnable, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients[ch], + name: 'ch$ch', + ); + + srcIntfs.add(srcIntf); + } + + // ── Connect outputs ── + for (var ch = 0; ch < numChannels; ch++) { + channelOut.elements[ch] <= srcIntfs[ch].dataOut; + } + validOut <= srcIntfs[0].validOut; + done <= controller.doneFlag; + state <= controller.state; + + // ── Optional shared data bus (inOut port) ── + if (dataBus != null && writeEnable != null) { + final busPort = addInOut('dataBus', dataBus, width: dataWidth); + writeEnable = addInput('writeEnable', writeEnable); + final storedValue = addOutput('storedValue', width: dataWidth); + + final sharedBus = SharedDataBus( + LogicNet(name: 'busNet', width: dataWidth)..gets(busPort), + writeEnable, + clk, + reset, + dataWidth: dataWidth, + ); + storedValue <= sharedBus.storedValue; + } + } +} diff --git a/lib/src/examples/oven_fsm_modules.dart b/lib/src/examples/oven_fsm_modules.dart new file mode 100644 index 000000000..b1f18a3f5 --- /dev/null +++ b/lib/src/examples/oven_fsm_modules.dart @@ -0,0 +1,211 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// oven_fsm_modules.dart +// Web-safe module class definitions for the Oven FSM example. +// +// Extracted from example/oven_fsm.dart and example/example.dart so these +// classes can be imported in web-targeted code (no dart:io dependency). +// +// 2026 April +// Original authors: Yao Jing Quek, Max Korbel + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +// ────────────────────────────────────────────────────────────────── +// Counter (from example/example.dart) +// ────────────────────────────────────────────────────────────────── + +/// A simple 8-bit counter with enable and synchronous reset. +class Counter extends Module { + /// The current counter value. + Logic get val => output('val'); + + /// The enable input. + @protected + Logic get en => input('en'); + + /// The reset input. + @protected + Logic get resetPin => input('reset'); + + /// The clock input. + @protected + Logic get clkPin => input('clk'); + + /// Bit width of the counter (default 8). + final int width; + + /// Creates a [Counter] of [width] bits driven by [clk]. + /// + /// Increments on each rising edge when [en] is high. + /// [reset] synchronously clears the count to zero. + Counter( + Logic en, + Logic reset, + Logic clk, { + this.width = 8, + super.name = 'counter', + }) : super(definitionName: 'Counter_W$width') { + en = addInput('en', en); + reset = addInput('reset', reset); + clk = addInput('clk', clk); + addOutput('val', width: width); + + val <= flop(clk, reset: reset, en: en, val + 1); + } +} + +// ────────────────────────────────────────────────────────────────── +// Oven FSM enums +// ────────────────────────────────────────────────────────────────── + +/// Oven states: standby → cooking → paused → completed. +enum OvenState { + /// Waiting for the start button. + standby, + + /// Actively cooking (timer running). + cooking, + + /// Cooking paused (timer held). + paused, + + /// Cooking finished (timer expired). + completed, +} + +/// One-hot encoded button inputs. +enum Button { + /// Start or restart cooking. + start(value: 0), + + /// Pause cooking. + pause(value: 1), + + /// Resume from pause. + resume(value: 2); + + /// Creates a button with the given encoded [value]. + const Button({required this.value}); + + /// The encoded value for this button. + final int value; +} + +/// One-hot encoded LED output colors. +enum LEDLight { + /// Yellow — cooking in progress. + yellow(value: 0), + + /// Blue — standby. + blue(value: 1), + + /// Red — paused. + red(value: 2), + + /// Green — cooking complete. + green(value: 3); + + /// Creates an LED color with the given encoded [value]. + const LEDLight({required this.value}); + + /// The encoded value for this LED color. + final int value; +} + +// ────────────────────────────────────────────────────────────────── +// OvenModule +// ────────────────────────────────────────────────────────────────── + +/// A microwave oven FSM with 4 states and an internal timer counter. +/// +/// Inputs: +/// - `button` (2-bit): start / pause / resume +/// - `reset`: active-high synchronous reset +/// - `clk`: clock +/// +/// Outputs: +/// - `led` (2-bit): blue (standby), yellow (cooking), +/// red (paused), green (completed) +class OvenModule extends Module { + late final FiniteStateMachine _oven; + + /// The LED output encoding the current state. + Logic get led => output('led'); + + /// The button input. + @protected + Logic get button => input('button'); + + /// The reset input. + @protected + Logic get resetPin => input('reset'); + + /// The clock input. + @protected + Logic get clkPin => input('clk'); + + /// Creates an [OvenModule] controlled by [button] with [clk] and [reset]. + OvenModule(Logic button, Logic reset, Logic clk) + : super(name: 'oven', definitionName: 'OvenModule') { + button = addInput('button', button, width: button.width); + reset = addInput('reset', reset); + clk = addInput('clk', clk); + final led = addOutput('led', width: button.width); + + final counterReset = Logic(name: 'counter_reset'); + final en = Logic(name: 'counter_en'); + + final counter = Counter(en, counterReset, clk, name: 'counter_module'); + + final states = [ + State(OvenState.standby, events: { + Logic(name: 'button_start') + ..gets(button.eq(Const(Button.start.value, width: button.width))): + OvenState.cooking, + }, actions: [ + led < LEDLight.blue.value, + counterReset < 1, + en < 0, + ]), + State(OvenState.cooking, events: { + Logic(name: 'button_pause') + ..gets(button.eq(Const(Button.pause.value, width: button.width))): + OvenState.paused, + Logic(name: 'counter_time_complete')..gets(counter.val.eq(4)): + OvenState.completed, + }, actions: [ + led < LEDLight.yellow.value, + counterReset < 0, + en < 1, + ]), + State(OvenState.paused, events: { + Logic(name: 'button_resume') + ..gets( + button.eq(Const(Button.resume.value, width: button.width))): + OvenState.cooking, + }, actions: [ + led < LEDLight.red.value, + counterReset < 0, + en < 0, + ]), + State(OvenState.completed, events: { + Logic(name: 'button_start') + ..gets(button.eq(Const(Button.start.value, width: button.width))): + OvenState.cooking, + }, actions: [ + led < LEDLight.green.value, + counterReset < 1, + en < 0, + ]), + ]; + + _oven = + FiniteStateMachine(clk, reset, OvenState.standby, states); + } + + /// The internal [FiniteStateMachine] driving the oven states. + FiniteStateMachine get ovenStateMachine => _oven; +} diff --git a/lib/src/examples/tree_modules.dart b/lib/src/examples/tree_modules.dart new file mode 100644 index 000000000..96fc7d283 --- /dev/null +++ b/lib/src/examples/tree_modules.dart @@ -0,0 +1,63 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// tree_modules.dart +// Web-safe module class definition for the Tree of Two-Input Modules example. +// +// Extracted from example/tree.dart so it can be imported in web-targeted code. +// +// 2026 April +// Original author: Max Korbel + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; + +// ────────────────────────────────────────────────────────────────── +// TreeOfTwoInputModules +// ────────────────────────────────────────────────────────────────── + +/// A logarithmic-height tree of arbitrary two-input/one-output modules. +/// +/// Recursively instantiates itself, splitting the input list in half at each +/// level. The operation [op] is applied to combine pairs of results. +class TreeOfTwoInputModules extends Module { + /// The combining operation (internal use only). + @protected + final Logic Function(Logic a, Logic b) op; + + final List _seq = []; + + /// The combined output of the tree. + Logic get out => output('out'); + + /// Creates a tree that reduces [seq] using [op]. + /// + /// Recursively splits [seq] in half until single elements remain, + /// then combines them pair-wise with the supplied operation. + TreeOfTwoInputModules(List seq, this.op) + : super( + name: 'tree_of_two_input_modules', + definitionName: 'TreeMax_N${seq.length}', + ) { + if (seq.isEmpty) { + throw Exception("Don't use TreeOfTwoInputModules with an empty sequence"); + } + + for (var i = 0; i < seq.length; i++) { + _seq.add(addInput('seq$i', seq[i], width: seq[i].width)); + } + addOutput('out', width: seq[0].width); + + if (_seq.length == 1) { + out <= _seq[0]; + } else { + final a = + TreeOfTwoInputModules(_seq.getRange(0, _seq.length ~/ 2).toList(), op) + .out; + final b = TreeOfTwoInputModules( + _seq.getRange(_seq.length ~/ 2, _seq.length).toList(), op) + .out; + out <= op(a, b); + } + } +} diff --git a/lib/src/module.dart b/lib/src/module.dart index 92fc410e0..c9e9a7610 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // module.dart @@ -11,11 +11,10 @@ import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; - import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; -import 'package:rohd/src/diagnostics/inspector_service.dart'; import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/timestamper.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; @@ -52,6 +51,22 @@ abstract class Module { /// An internal mapping of input names to their sources to this [Module]. late final Map _inputSources = {}; + // ─── Central naming (Namer) ───────────────────────────────────── + + /// Central namer that owns both the signal and instance namespaces. + /// Initialized lazily on first access (after build). + @internal + late final Namer namer = _createNamer(); + + Namer _createNamer() { + assert(hasBuilt, 'Module must be built before canonical names are bound.'); + return Namer.forModule( + inputs: _inputs, + outputs: _outputs, + inOuts: _inOuts, + ); + } + /// An internal mapping of inOut names to their sources to this [Module]. late final Map _inOutSources = {}; @@ -288,8 +303,11 @@ abstract class Module { /// /// The hierarchy is built "bottom-up", so leaf-level [Module]s are built /// before the [Module]s which contain them. + /// + /// If [netlistOptions] is provided, a [NetlistService] is automatically + /// created and registered after the module hierarchy is constructed. @mustCallSuper - Future build() async { + Future build({NetlistOptions? netlistOptions}) async { if (hasBuilt) { throw Exception( 'This Module has already been built, and can only be built once.'); @@ -317,7 +335,12 @@ abstract class Module { _hasBuilt = true; - ModuleTree.rootModuleInstance = this; + ModuleServices.instance.rootModule = this; + + // Optionally synthesize a netlist and register the service. + if (netlistOptions != null) { + await NetlistService.create(this, options: netlistOptions); + } } /// Confirms that the post-[build] hierarchy is valid. @@ -1118,18 +1141,28 @@ abstract class Module { throw ModuleNotBuiltException(this); } - final synthHeader = ''' -/** - * Generated by ROHD - www.github.com/intel/rohd - * Generation time: ${Timestamper.stamp()} - * ROHD Version: ${Config.version} - */ - -'''; - return synthHeader + - SynthBuilder(this, SystemVerilogSynthesizer()) - .getSynthFileContents() - .join('\n\n////////////////////\n\n'); + return SvService(this, register: false).synthOutput; + } + + /// Returns a synthesized SystemC version of this [Module]. + /// + /// Generates SystemC code that is equivalent to the hardware described by + /// this module, using the same naming strategy as [generateSynth]. + String generateSystemC() { + if (!_hasBuilt) { + throw ModuleNotBuiltException(this); + } + + final synthBuilder = SynthBuilder(this, SystemCSynthesizer()); + final moduleContents = + synthBuilder.getSynthFileContents().map((e) => e.contents).join('\n'); + return '// Generated by ROHD - www.github.com/intel/rohd\n' + '// Generation time: ${Timestamper.stamp()}\n' + '// ROHD Version: ${Config.version}\n' + '\n' + '#include \n' + '\n' + '$moduleContents'; } } diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index cd9aa8750..3e3f4acdf 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -88,6 +88,11 @@ class FlipFlop extends Module with SystemVerilog { /// Only initialized if a constant value is provided. late LogicValue _resetValueConst; + /// Returns the constant reset value if one was provided, or null if the + /// reset value is a port or no reset exists. + LogicValue? get constantResetValue => + _reset != null && _resetValuePort == null ? _resetValueConst : null; + /// Indicates whether provided `reset` signals should be treated as an async /// reset. If no `reset` is provided, this will have no effect. final bool asyncReset; diff --git a/lib/src/modules/conditionals/sequential.dart b/lib/src/modules/conditionals/sequential.dart index 62a7c1129..8871202fd 100644 --- a/lib/src/modules/conditionals/sequential.dart +++ b/lib/src/modules/conditionals/sequential.dart @@ -135,6 +135,14 @@ class Sequential extends Always { /// The input edge triggers used in this block. final List<_SequentialTrigger> _triggers = []; + /// Returns the edge polarity for each trigger input port. + /// + /// Each entry pairs the trigger input port name with whether the trigger + /// fires on a positive edge (`true`) or negative edge (`false`). + List<({String portName, bool isPosedge})> get triggerEdges => _triggers + .map((t) => (portName: t.signal.name, isPosedge: t.isPosedge)) + .toList(); + /// When `false`, an [SignalRedrivenException] will be thrown during /// simulation if the same signal is driven multiple times within this /// [Sequential]. diff --git a/lib/src/synthesizers/netlist/leaf_cell_mapper.dart b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart new file mode 100644 index 000000000..606747dee --- /dev/null +++ b/lib/src/synthesizers/netlist/leaf_cell_mapper.dart @@ -0,0 +1,486 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// leaf_cell_mapper.dart +// Maps ROHD leaf modules to Yosys-primitive cell representations. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// The result of mapping a leaf ROHD module to a Yosys-style cell. +typedef LeafCellMapping = ({ + String cellType, + Map portDirs, + Map> connections, + Map parameters, +}); + +/// Context provided to each leaf-cell mapping handler. +/// +/// Contains the module instance plus the raw ROHD port directions and +/// connections built by the synthesizer, so handlers can remap them to +/// Yosys-primitive port names. +class LeafCellContext { + /// The ROHD [Module] being mapped. + final Module module; + + /// Raw ROHD port-direction map (`{'portName': 'input'|'output'|'inout'}`). + final Map rawPortDirs; + + /// Raw ROHD connection map (`{'portName': [wireId, ...]}`). + final Map> rawConns; + + /// Creates a [LeafCellContext]. + const LeafCellContext(this.module, this.rawPortDirs, this.rawConns); + + // ── Shared helper methods ─────────────────────────────────────────── + + /// Find the first input port name matching [prefix]. + String? findInput(String prefix) { + for (final k in module.inputs.keys) { + if (k.startsWith(prefix)) { + return k; + } + } + return null; + } + + /// The first output port name, or `null` if there are none. + String? get firstOutput => + module.outputs.keys.isEmpty ? null : module.outputs.keys.first; + + /// The first input port name, or `null` if there are none. + String? get firstInput => + module.inputs.keys.isEmpty ? null : module.inputs.keys.first; + + /// Width (number of wire IDs) for a given ROHD port name. + int width(String portName) => rawConns[portName]?.length ?? 0; + + /// Build new port-direction and connection maps from a + /// `{rohdPortName: yosysPortName}` mapping. + ({ + Map portDirs, + Map> connections, + }) remap(Map nameMap) { + final pd = {}; + final cn = >{}; + for (final e in nameMap.entries) { + final rohdName = e.key; + final netlistPortName = e.value; + pd[netlistPortName] = rawPortDirs[rohdName] ?? 'output'; + cn[netlistPortName] = rawConns[rohdName] ?? []; + } + return (portDirs: pd, connections: cn); + } +} + +/// Signature for a leaf-cell mapping handler. +/// +/// Returns a [LeafCellMapping] if the handler recognises the module, +/// or `null` to let the next handler try. +typedef LeafCellHandler = LeafCellMapping? Function(LeafCellContext ctx); + +/// Maps ROHD leaf [Module]s to Yosys-primitive cell representations. +/// +/// Handlers are registered via [register] and tried in registration order. +/// A singleton instance with all built-in ROHD types pre-registered is +/// available via [LeafCellMapper.defaultMapper]. +/// +/// ```dart +/// final mapper = LeafCellMapper.defaultMapper; +/// final result = mapper.map(sub, rawPortDirs, rawConns); +/// ``` +class LeafCellMapper { + /// Ordered list of registered handlers. + final _handlers = []; + + /// Creates an empty [LeafCellMapper] with no registered handlers. + LeafCellMapper(); + + /// The default mapper with all built-in ROHD leaf types registered. + static final defaultMapper = LeafCellMapper._withDefaults(); + + /// Register a mapping [handler]. + /// + /// Handlers are tried in registration order; the first non-null result + /// wins. Register more-specific handlers before less-specific ones. + void register(LeafCellHandler handler) { + _handlers.add(handler); + } + + /// Try to map [module] to a Yosys-primitive cell. + /// + /// Returns `null` if no registered handler matches. + LeafCellMapping? map( + Module module, + Map rawPortDirs, + Map> rawConns, + ) { + final ctx = LeafCellContext(module, rawPortDirs, rawConns); + for (final handler in _handlers) { + final result = handler(ctx); + if (result != null) { + return result; + } + } + return null; + } + + // ══════════════════════════════════════════════════════════════════════ + // Reusable mapping patterns + // ══════════════════════════════════════════════════════════════════════ + + /// Map a single-input, single-output gate (e.g. `$not`, `$reduce_and`). + static LeafCellMapping? unaryAY( + LeafCellContext ctx, + String cellType, + ) { + final inN = ctx.firstInput; + final out = ctx.firstOutput; + if (inN == null || out == null) { + return null; + } + final r = ctx.remap({inN: 'A', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(inN), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + /// Map a two-input gate with ports A, B, Y (e.g. `$and`, `$eq`, `$shl`). + static LeafCellMapping? binaryABY( + LeafCellContext ctx, + String cellType, { + required String inAPrefix, + required String inBPrefix, + }) { + final a = ctx.findInput(inAPrefix); + final b = ctx.findInput(inBPrefix); + final out = ctx.firstOutput; + if (a == null || b == null || out == null) { + return null; + } + final r = ctx.remap({a: 'A', b: 'B', out: 'Y'}); + return ( + cellType: cellType, + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(a), + 'B_WIDTH': ctx.width(b), + 'Y_WIDTH': ctx.width(out), + }, + ); + } + + // ══════════════════════════════════════════════════════════════════════ + // Built-in handler registration + // ══════════════════════════════════════════════════════════════════════ + + /// Creates a [LeafCellMapper] with built-in handlers for common ROHD leaf + /// types. + factory LeafCellMapper._withDefaults() { + final m = LeafCellMapper(); + + // Helper to reduce boilerplate for type-map-based handlers. + void registerByTypeMap( + Map typeMap, + LeafCellMapping? Function(LeafCellContext ctx, String cellType) handler, + ) { + m.register((ctx) { + final cellType = typeMap[ctx.module.runtimeType]; + return cellType == null ? null : handler(ctx, cellType); + }); + } + + m + // ── BusSubset → $slice ──────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! BusSubset) { + return null; + } + final sub = ctx.module as BusSubset; + final inName = sub.inputs.keys.first; + final outName = sub.outputs.keys.first; + final r = ctx.remap({inName: 'A', outName: 'Y'}); + return ( + cellType: r'$slice', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'OFFSET': sub.startIndex, + 'A_WIDTH': ctx.width(inName), + 'Y_WIDTH': ctx.width(outName), + }, + ); + }) + + // ── Swizzle → $concat ───────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Swizzle) { + return null; + } + final outName = ctx.firstOutput; + final inputKeys = ctx.module.inputs.keys.toList(); + + // Filter out zero-width inputs (degenerate concat operands). + final nonZeroKeys = inputKeys.where((k) => ctx.width(k) > 0).toList(); + + if (nonZeroKeys.length == 2 && outName != null) { + final r = ctx + .remap({nonZeroKeys[0]: 'A', nonZeroKeys[1]: 'B', outName: 'Y'}); + return ( + cellType: r'$concat', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'A_WIDTH': ctx.width(nonZeroKeys[0]), + 'B_WIDTH': ctx.width(nonZeroKeys[1]), + }, + ); + } + + // Single non-zero input ⇒ emit as $buf. + if (nonZeroKeys.length == 1 && outName != null) { + final r = ctx.remap({nonZeroKeys[0]: 'A', outName: 'Y'}); + return ( + cellType: r'$buf', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'WIDTH': ctx.width(nonZeroKeys[0]), + }, + ); + } + + if (nonZeroKeys.isEmpty) { + return null; + } + + // N-input concat: per-input range labels, output is Y. + final pd = {}; + final cn = >{}; + final params = {}; + var bitOffset = 0; + for (var i = 0; i < nonZeroKeys.length; i++) { + final ik = nonZeroKeys[i]; + final w = ctx.width(ik); + final label = + w == 1 ? '[$bitOffset]' : '[${bitOffset + w - 1}:$bitOffset]'; + pd[label] = 'input'; + cn[label] = ctx.rawConns[ik] ?? []; + params['IN${i}_WIDTH'] = w; + bitOffset += w; + } + if (outName != null) { + pd['Y'] = 'output'; + cn['Y'] = ctx.rawConns[outName] ?? []; + } + return ( + cellType: r'$concat', + portDirs: pd, + connections: cn, + parameters: params, + ); + }) + + // ── NOT gate ────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! NotGate) { + return null; + } + return unaryAY(ctx, r'$not'); + }) + + // ── Mux ─────────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Mux) { + return null; + } + final ctrl = ctx.findInput('_control') ?? ctx.findInput('control'); + final d0 = ctx.findInput('_d0') ?? ctx.findInput('d0'); + final d1 = ctx.findInput('_d1') ?? ctx.findInput('d1'); + final out = ctx.firstOutput; + if (ctrl == null || d0 == null || d1 == null || out == null) { + return null; + } + // Yosys: S=select, A=d0 (when S=0), B=d1 (when S=1). + final r = ctx.remap({ctrl: 'S', d0: 'A', d1: 'B', out: 'Y'}); + return ( + cellType: r'$mux', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'WIDTH': ctx.width(d0), + }, + ); + }) + + // ── Add ─────────────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Add) { + return null; + } + final in0 = ctx.findInput('_in0') ?? ctx.findInput('in0'); + final in1 = ctx.findInput('_in1') ?? ctx.findInput('in1'); + final sumName = ctx.module.outputs.keys + .firstWhere((k) => !k.contains('carry'), orElse: () => ''); + final carryName = ctx.module.outputs.keys + .firstWhere((k) => k.contains('carry'), orElse: () => ''); + if (in0 == null || in1 == null || sumName.isEmpty) { + return null; + } + final pd = { + 'A': 'input', + 'B': 'input', + 'Y': 'output', + }; + final cn = >{ + 'A': ctx.rawConns[in0] ?? [], + 'B': ctx.rawConns[in1] ?? [], + 'Y': ctx.rawConns[sumName] ?? [], + }; + if (carryName.isNotEmpty) { + pd['CO'] = 'output'; + cn['CO'] = ctx.rawConns[carryName] ?? []; + } + return ( + cellType: r'$add', + portDirs: pd, + connections: cn, + parameters: { + 'A_WIDTH': ctx.width(in0), + 'B_WIDTH': ctx.width(in1), + 'Y_WIDTH': ctx.width(sumName), + }, + ); + }) + + // ── FlipFlop → $dff ─────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! FlipFlop) { + return null; + } + final clk = ctx.findInput('_clk') ?? ctx.findInput('clk'); + final d = ctx.findInput('_d') ?? ctx.findInput('d'); + final en = ctx.findInput('_en') ?? ctx.findInput('en'); + final rst = ctx.findInput('_reset') ?? ctx.findInput('reset'); + final q = ctx.firstOutput; + if (clk == null || d == null || q == null) { + return null; + } + final pd = { + '_clk': 'input', + '_d': 'input', + '_q': 'output', + }; + final cn = >{ + '_clk': ctx.rawConns[clk] ?? [], + '_d': ctx.rawConns[d] ?? [], + '_q': ctx.rawConns[q] ?? [], + }; + if (en != null && ctx.rawConns.containsKey(en)) { + pd['_en'] = 'input'; + cn['_en'] = ctx.rawConns[en] ?? []; + } + if (rst != null && ctx.rawConns.containsKey(rst)) { + pd['_reset'] = 'input'; + cn['_reset'] = ctx.rawConns[rst] ?? []; + } + final rstVal = + ctx.findInput('_resetValue') ?? ctx.findInput('resetValue'); + if (rstVal != null && ctx.rawConns.containsKey(rstVal)) { + pd['_resetValue'] = 'input'; + cn['_resetValue'] = ctx.rawConns[rstVal] ?? []; + } + return ( + cellType: r'$dff', + portDirs: pd, + connections: cn, + parameters: { + 'WIDTH': ctx.width(d), + 'CLK_POLARITY': 1, + }, + ); + }); + + // ── Type-map-based gates ─────────────────────────────────────────── + final gateRegistrations = <( + Map, + LeafCellMapping? Function(LeafCellContext, String), + )>[ + ( + const { + And2Gate: r'$and', + Or2Gate: r'$or', + Xor2Gate: r'$xor', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + AndUnary: r'$reduce_and', + OrUnary: r'$reduce_or', + XorUnary: r'$reduce_xor', + }, + unaryAY, + ), + ( + const { + Multiply: r'$mul', + Subtract: r'$sub', + Equals: r'$eq', + NotEquals: r'$ne', + LessThan: r'$lt', + GreaterThan: r'$gt', + LessThanOrEqual: r'$le', + GreaterThanOrEqual: r'$ge', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in0', inBPrefix: '_in1'), + ), + ( + const { + LShift: r'$shl', + RShift: r'$shr', + ARShift: r'$shiftx', + }, + (ctx, type) => + binaryABY(ctx, type, inAPrefix: '_in', inBPrefix: '_shiftAmount'), + ), + ]; + for (final (typeMap, handler) in gateRegistrations) { + registerByTypeMap(typeMap, handler); + } + + // ── TriStateBuffer → $tribuf ────────────────────────────────────── + m.register((ctx) { + if (ctx.module is! TriStateBuffer) { + return null; + } + final tsb = ctx.module as TriStateBuffer; + final inName = tsb.inputs.keys.first; // data input + final enName = tsb.inputs.keys.last; // enable + final outName = tsb.inOuts.keys.first; // inout output + final r = ctx.remap({inName: 'A', enName: 'EN', outName: 'Y'}); + return ( + cellType: r'$tribuf', + portDirs: r.portDirs, + connections: r.connections, + parameters: { + 'WIDTH': ctx.width(inName), + }, + ); + }); + + return m; + } +} diff --git a/lib/src/synthesizers/netlist/netlist.dart b/lib/src/synthesizers/netlist/netlist.dart new file mode 100644 index 000000000..8d335e812 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist.dart @@ -0,0 +1,16 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist.dart +// Barrel file for netlist synthesis library. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +export 'leaf_cell_mapper.dart'; +export 'netlist_options.dart'; +export 'netlist_passes.dart'; +export 'netlist_service.dart'; +export 'netlist_synthesis_result.dart'; +export 'netlist_synthesizer.dart'; +export 'netlist_utils.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_options.dart b/lib/src/synthesizers/netlist/netlist_options.dart new file mode 100644 index 000000000..95d0856c1 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_options.dart @@ -0,0 +1,138 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_options.dart +// Configuration for netlist synthesis. +// +// 2026 March 12 +// Author: Desmond Kirkpatrick + +import 'package:rohd/src/synthesizers/netlist/leaf_cell_mapper.dart'; + +/// Configuration options for netlist synthesis. +/// +/// The netlist synthesizer serves two main consumer flows, both configured +/// through these options: +/// +/// **Flow 1 — Slim JSON** (`NetlistService.slimJson`): +/// Batch synthesis of the entire design, producing a lightweight +/// representation with ports, signals, and cell stubs but **no cell +/// connections**. Used for the initial DevTools hierarchy load. +/// +/// **Flow 2 — Full JSON, incremental** (`NetlistService.moduleJson`): +/// Returns the complete netlist (with cell connections) for a single +/// module definition on demand. Results are cached; the first call +/// may trigger a lazy `SynthBuilder` run on the requested subtree. +/// +/// Both flows run the identical pipeline: `SynthBuilder` → +/// `collectModuleEntries` → `applyPostProcessingPasses`. Flow 1 +/// then strips cell connections from the cached data; Flow 2 returns +/// it verbatim. This guarantees cell keys and wire IDs are stable +/// across both flows. +/// +/// Bundles all parameters that control netlist generation into a single +/// object, making it easier to pass through call chains and to store +/// for incremental synthesis. +/// +/// Example usage: +/// ```dart +/// const options = NetlistOptions( +/// groupStructConversions: true, +/// collapseStructGroups: true, +/// ); +/// final synth = NetlistSynthesizer(options: options); +/// ``` +class NetlistOptions { + /// The leaf-cell mapper used to convert ROHD leaf modules to Yosys + /// primitive cell types. When `null`, [LeafCellMapper.defaultMapper] + /// is used. + final LeafCellMapper? leafCellMapper; + + /// When `true`, groups of `$slice` + `$concat` cells that represent + /// structure-to-structure signal conversions are collapsed into + /// synthetic child modules, reducing visual clutter in the netlist. + final bool groupStructConversions; + + /// When `true` (requires [groupStructConversions] to also be `true`), + /// the synthetic child modules created for struct conversions will have + /// all their internal `$slice`/`$concat` cells and intermediate nets + /// removed, leaving only a single `$buf` cell that directly connects + /// each input port to the corresponding output port. + final bool collapseStructGroups; + + /// When `true` (requires [groupStructConversions] to also be `true`), + /// enables an additional grouping pass that finds `$concat` cells whose + /// input bits all trace back through `$buf`/`$slice` chains to a + /// contiguous sub-range of a single source bus. + final bool groupMaximalSubsets; + + /// When `true` (requires [groupStructConversions] to also be `true`), + /// enables an additional pass that finds `$concat` cells where a + /// contiguous run of input ports trace back through `$buf`/`$slice` + /// chains to a contiguous sub-range of a single source bus. + final bool collapseConcats; + + /// When `true`, `$slice` cells whose outputs feed directly into a + /// `$struct_pack` input port are absorbed into the pack cell, which + /// already describes the field decomposition. The redundant slices + /// are removed. + final bool collapseSelectsIntoPack; + + /// When `true`, `$struct_unpack` output ports that feed directly into + /// `$concat` input ports are collapsed: the concat is replaced by a + /// `$buf` or `$slice` from the unpack's source bus when all inputs + /// trace back to it contiguously. + final bool collapseUnpackToConcat; + + /// When `true`, `$struct_unpack` output ports that feed (possibly + /// through `$buf`/`$slice` chains) into `$struct_pack` input ports + /// are collapsed: the intermediate cells are removed and the pack + /// input wires are rewired to the unpack output wires directly. + final bool collapseUnpackToPack; + + /// When `true`, dead-cell elimination is performed after aliasing to + /// remove cells whose inputs are entirely undriven or whose outputs + /// are entirely unconsumed. + final bool enableDCE; + + /// When `true`, the synthesizer produces "slim" output: the full + /// synthesis pipeline runs (including all post-processing passes), + /// but cell connection maps are stripped from the result. + /// Netnames and ports are still emitted with full wire-ID fidelity, + /// so a subsequent full-mode synthesis of the same module will + /// produce compatible wire IDs. + final bool slimMode; + + /// When `true`, contiguous ascending runs of ≥3 integer bit IDs in + /// `bits` arrays and cell `connections` arrays are replaced with + /// `"start:end"` range strings (e.g. `[52, 53, 54, 55]` → `["52:55"]`). + /// + /// This is backward-compatible: Yosys-format arrays already mix + /// integers with constant strings `"0"` and `"1"`. Parsers can + /// detect range strings by the presence of `:`. + final bool compressBitRanges; + + /// When `true`, the JSON output uses no indentation (compact form). + /// When `false` (default), the JSON is pretty-printed with two-space + /// indentation. + final bool compactJson; + + /// Creates a [NetlistOptions] with the given configuration. + /// + /// All parameters have sensible defaults matching the current + /// netlist synthesizer behaviour. + const NetlistOptions({ + this.leafCellMapper, + this.groupStructConversions = false, + this.collapseStructGroups = false, + this.groupMaximalSubsets = false, + this.collapseConcats = false, + this.collapseSelectsIntoPack = false, + this.collapseUnpackToConcat = false, + this.collapseUnpackToPack = false, + this.enableDCE = true, + this.slimMode = false, + this.compressBitRanges = false, + this.compactJson = false, + }); +} diff --git a/lib/src/synthesizers/netlist/netlist_passes.dart b/lib/src/synthesizers/netlist/netlist_passes.dart new file mode 100644 index 000000000..7fdcf57fb --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_passes.dart @@ -0,0 +1,2541 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_passes.dart +// Post-processing optimization passes for netlist synthesis. +// +// These passes operate on the modules map (definition name → module data) +// produced by [NetlistSynthesizer.synthesize]. They simplify the netlist +// by grouping struct conversions, collapsing redundant cells, and inserting +// buffer cells for cleaner schematic rendering. +// +// 2025 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; + +/// Post-processing optimization passes for netlist synthesis. +/// +/// All methods are static — no instances are created. +class NetlistPasses { + NetlistPasses._(); + + /// Collects a combined modules map from [SynthesisResult]s suitable for + /// JSON emission. + static Map> collectModuleEntries( + Iterable results, { + Module? topModule, + }) { + final allModules = >{}; + for (final result in results) { + if (result is NetlistSynthesisResult) { + final typeName = result.instanceTypeName; + final attrs = Map.from(result.attributes); + if (topModule != null && result.module == topModule) { + attrs['top'] = 1; + } + allModules[typeName] = { + 'attributes': attrs, + 'ports': result.ports, + 'cells': result.cells, + 'netnames': result.netnames, + }; + } + } + return allModules; + } + + // -- Maximal-subset grouping ------------------------------------------- + + /// Finds `$concat` cells whose input bits all trace back through + /// `$buf`/`$slice` chains to a contiguous sub-range of a single source + /// bus. Replaces the entire concat-tree (the concat itself plus the + /// intermediate `$buf` and `$slice` cells that exclusively serve it) + /// with a single `$slice` (or `$buf` when the sub-range covers the + /// full source width). + /// + /// This pass runs *before* the connected-component grouping so that + /// the simplified cells can be picked up by the standard struct-assign + /// grouping and collapse passes. + static void applyMaximalSubsetGrouping( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + // Build wire-driver, wire-consumer, and bit-to-net maps. + final (:wireDriverCell, :wireConsumerCells, :bitToNetInfo) = + NetlistUtils.buildWireMaps(cells, moduleDef); + + final cellsToRemove = {}; + final cellsToAdd = >{}; + var replIdx = 0; + + // Process each $concat cell. + for (final concatEntry in cells.entries.toList()) { + final concatName = concatEntry.key; + final concatCell = concatEntry.value; + if ((concatCell['type'] as String?) != r'$concat') { + continue; + } + if (cellsToRemove.contains(concatName)) { + continue; + } + + final conns = concatCell['connections'] as Map? ?? {}; + + // Gather the concat's input bits in LSB-first order. + final inputBits = []; + if (conns.containsKey('A')) { + // Standard 2-input concat: A (LSB), B (MSB). + for (final b in conns['A'] as List) { + if (b is int) { + inputBits.add(b); + } + } + for (final b in conns['B'] as List) { + if (b is int) { + inputBits.add(b); + } + } + } else { + // Multi-input concat: range-named ports [lo:hi]. + final rangePorts = >{}; + for (final portName in conns.keys) { + if (portName == 'Y') { + continue; + } + final m = NetlistUtils.rangePortRe.firstMatch(portName); + if (m != null) { + final hi = int.parse(m.group(1)!); + final lo = m.group(2) != null ? int.parse(m.group(2)!) : hi; + rangePorts[lo] = [ + for (final b in conns[portName] as List) + if (b is int) b, + ]; + } + } + for (final k in rangePorts.keys.toList()..sort()) { + inputBits.addAll(rangePorts[k]!); + } + } + + if (inputBits.isEmpty) { + continue; + } + + final outputBits = [ + for (final b in conns['Y'] as List) + if (b is int) b, + ]; + + // Trace each input bit backward through $buf and $slice cells + // to find its ultimate source bit. Record the chain of + // intermediate cells visited. + final sourceBits = []; + final intermediateCells = {}; + var allFromOneBus = true; + String? sourceBusNet; + List? sourceBusBits; + + for (final inputBit in inputBits) { + final (traced, chain) = + NetlistUtils.traceBackward(inputBit, wireDriverCell, cells); + sourceBits.add(traced); + intermediateCells.addAll(chain); + + // Identify which named bus this bit belongs to. + final info = bitToNetInfo[traced]; + if (info == null) { + allFromOneBus = false; + break; + } + if (sourceBusNet == null) { + sourceBusNet = info.$1; + sourceBusBits = info.$2; + } else if (sourceBusNet != info.$1) { + allFromOneBus = false; + break; + } + } + + if (!allFromOneBus || sourceBusNet == null || sourceBusBits == null) { + continue; + } + + // Verify the traced source bits form a contiguous sub-range + // of the source bus. + if (sourceBits.length != inputBits.length) { + continue; + } + + // Find each source bit's index within the source bus. + final indices = []; + var contiguous = true; + for (final sb in sourceBits) { + final idx = sourceBusBits.indexOf(sb); + if (idx < 0) { + contiguous = false; + break; + } + indices.add(idx); + } + if (!contiguous || indices.isEmpty) { + continue; + } + + // Check that indices are sequential (contiguous ascending). + for (var i = 1; i < indices.length; i++) { + if (indices[i] != indices[i - 1] + 1) { + contiguous = false; + break; + } + } + if (!contiguous) { + continue; + } + + // Verify that every intermediate cell is used exclusively + // by this concat chain (no fanout to other consumers). + if (!NetlistUtils.isExclusiveChain( + intermediates: intermediateCells, + ownerCell: concatName, + cells: cells, + wireConsumerCells: wireConsumerCells, + allowPortConsumers: true, + )) { + continue; + } + + // Build the source bus bits list (the full bus from the module). + // We need the A connection to be the full source bus. + final sourceBusParentBits = sourceBusBits.cast().toList(); + + final offset = indices.first; + final yWidth = outputBits.length; + final aWidth = sourceBusBits.length; + + // Mark intermediate cells and the concat for removal. + cellsToRemove + ..addAll(intermediateCells) + ..add(concatName); + + if (yWidth == aWidth) { + cellsToAdd['maxsub_buf_$replIdx'] = NetlistUtils.makeBufCell( + aWidth, sourceBusParentBits, outputBits.cast()); + } else { + cellsToAdd['maxsub_slice_$replIdx'] = NetlistUtils.makeSliceCell( + offset, + aWidth, + yWidth, + sourceBusParentBits, + outputBits.cast()); + } + replIdx++; + } + + // Apply removals and additions. + cellsToRemove.forEach(cells.remove); + cells.addAll(cellsToAdd); + } + } + + // -- Partial concat collapsing ----------------------------------------- + + /// Scans every module in [allModules] for `$concat` cells where a + /// contiguous run of input ports (≥ 2) all trace back through + /// `$buf`/`$slice` chains to a contiguous sub-range of a single source + /// bus with exclusive fan-out. Each such run is replaced by a single + /// `$slice` and the concat is rebuilt with fewer input ports. + /// + /// If *all* ports of a concat qualify as a single run, the concat is + /// eliminated entirely and replaced with a `$slice` (or `$buf` for + /// full-width). + static void applyCollapseConcats( + Map> allModules, + ) { + for (final entry in allModules.entries) { + final moduleDef = entry.value; + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final concatCount = cells.values + .where((c) => (c['type'] as String?) == r'$concat') + .length; + if (concatCount == 0) { + continue; + } + + // Iterate until no more collapses are possible. Nested concat chains + // (e.g. swizzle_4 feeding swizzle_3) require multiple passes because + // traceBackward only traces through $slice/$buf, not $concat. + var changed = true; + var replIdx = 0; + var iteration = 0; + while (changed && iteration < 20) { + changed = false; + iteration++; + + // --- Build wire-driver, wire-consumer, and bit-to-net maps ------- + final (:wireDriverCell, :wireConsumerCells, :bitToNetInfo) = + NetlistUtils.buildWireMaps(cells, moduleDef); + + final cellsToRemove = {}; + final cellsToAdd = >{}; + + // --- Process each $concat cell ------------------------------------ + for (final concatEntry in cells.entries.toList()) { + final concatName = concatEntry.key; + final concatCell = concatEntry.value; + if ((concatCell['type'] as String?) != r'$concat') { + continue; + } + if (cellsToRemove.contains(concatName)) { + continue; + } + + final conns = + concatCell['connections'] as Map? ?? {}; + + // Parse input ports into an ordered list. + // Supports both range-named ports [hi:lo] and A/B form. + final inputPorts = <(int lo, String portName, List bits)>[]; + var hasRangePorts = false; + for (final portName in conns.keys) { + if (portName == 'Y') { + continue; + } + final m = NetlistUtils.rangePortRe.firstMatch(portName); + if (m != null) { + hasRangePorts = true; + final hi = int.parse(m.group(1)!); + final lo = m.group(2) != null ? int.parse(m.group(2)!) : hi; + inputPorts.add(( + lo, + portName, + [ + for (final b in conns[portName] as List) + if (b is int) b, + ], + )); + } + } + if (!hasRangePorts) { + // A/B form: convert to ordered list. + if (conns.containsKey('A') && conns.containsKey('B')) { + final aBits = [ + for (final b in conns['A'] as List) + if (b is int) b, + ]; + final bBits = [ + for (final b in conns['B'] as List) + if (b is int) b, + ]; + inputPorts + ..add((0, 'A', aBits)) + ..add((aBits.length, 'B', bBits)); + } + } + inputPorts.sort((a, b) => a.$1.compareTo(b.$1)); + + if (inputPorts.length < 2) { + continue; + } + + // --- Trace each port's bits back to a source bus ---------------- + final portTraces = <({ + String? busName, + List? busBits, + List sourceIndices, + Set intermediates, + bool valid, + })>[]; + + for (final (_, _, bits) in inputPorts) { + final sourceIndices = []; + final intermediates = {}; + String? busName; + List? busBits; + var valid = true; + + for (final bit in bits) { + final (traced, chain) = + NetlistUtils.traceBackward(bit, wireDriverCell, cells); + intermediates.addAll(chain); + + // Identify source net. + final info = bitToNetInfo[traced]; + if (info == null) { + valid = false; + break; + } + if (busName == null) { + busName = info.$1; + busBits = info.$2; + } else if (busName != info.$1) { + valid = false; + break; + } + final idx = busBits!.indexOf(traced); + if (idx < 0) { + valid = false; + break; + } + sourceIndices.add(idx); + } + + // Check contiguous within this port. + if (valid && sourceIndices.length == bits.length) { + for (var i = 1; i < sourceIndices.length; i++) { + if (sourceIndices[i] != sourceIndices[i - 1] + 1) { + valid = false; + break; + } + } + } else { + valid = false; + } + + portTraces.add(( + busName: busName, + busBits: busBits, + sourceIndices: sourceIndices, + intermediates: intermediates, + valid: valid, + )); + } + + // --- Find maximal runs of consecutive traceable ports ----------- + final runs = <(int startIdx, int endIdx)>[]; + var runStart = 0; + while (runStart < inputPorts.length) { + final t = portTraces[runStart]; + if (!t.valid || t.busName == null) { + runStart++; + continue; + } + var runEnd = runStart; + while (runEnd + 1 < inputPorts.length) { + final nextT = portTraces[runEnd + 1]; + if (!nextT.valid) { + break; + } + if (nextT.busName != t.busName) { + break; + } + // Check contiguity across port boundary. + final curLast = portTraces[runEnd].sourceIndices.last; + final nextFirst = nextT.sourceIndices.first; + if (nextFirst != curLast + 1) { + break; + } + runEnd++; + } + if (runEnd > runStart) { + runs.add((runStart, runEnd)); + } + runStart = runEnd + 1; + } + + if (runs.isEmpty) { + continue; + } + + // --- Verify exclusivity of intermediate cells for each run ------ + final validRuns = + <(int startIdx, int endIdx, Set intermediates)>[]; + for (final (startIdx, endIdx) in runs) { + final allIntermediates = {}; + for (var i = startIdx; i <= endIdx; i++) { + allIntermediates.addAll(portTraces[i].intermediates); + } + if (NetlistUtils.isExclusiveChain( + intermediates: allIntermediates, + ownerCell: concatName, + cells: cells, + wireConsumerCells: wireConsumerCells, + )) { + validRuns.add((startIdx, endIdx, allIntermediates)); + } + } + + if (validRuns.isEmpty) { + continue; + } + + // --- Check whether ALL ports form a single valid run ------------ + final allCollapsed = validRuns.length == 1 && + validRuns.first.$1 == 0 && + validRuns.first.$2 == inputPorts.length - 1; + + // Remove exclusive intermediate cells for all valid runs. + for (final (_, _, intermediates) in validRuns) { + cellsToRemove.addAll(intermediates); + } + + if (allCollapsed) { + // Full collapse — replace concat with a single $slice or $buf. + final t0 = portTraces.first; + final srcOffset = t0.sourceIndices.first; + final yWidth = (conns['Y'] as List).whereType().length; + final aWidth = t0.busBits!.length; + final sourceBusParentBits = t0.busBits!.cast().toList(); + final outputBits = [ + for (final b in conns['Y'] as List) + if (b is int) b, + ]; + + cellsToRemove.add(concatName); + if (yWidth == aWidth) { + cellsToAdd['collapse_buf_$replIdx'] = NetlistUtils.makeBufCell( + aWidth, sourceBusParentBits, outputBits); + } else { + cellsToAdd['collapse_slice_$replIdx'] = + NetlistUtils.makeSliceCell(srcOffset, aWidth, yWidth, + sourceBusParentBits, outputBits); + } + replIdx++; + continue; + } + + // --- Partial collapse — rebuild concat with fewer ports --------- + cellsToRemove.add(concatName); + + final newConns = >{}; + final newDirs = {}; + var outBitOffset = 0; + + var portIdx = 0; + while (portIdx < inputPorts.length) { + // Check if this port starts a valid run. + (int, int, Set)? activeRun; + for (final run in validRuns) { + if (run.$1 == portIdx) { + activeRun = run; + break; + } + } + + if (activeRun != null) { + final (startIdx, endIdx, _) = activeRun; + // Compute combined width and collect original input wire bits. + final originalBits = []; + for (var i = startIdx; i <= endIdx; i++) { + originalBits.addAll(inputPorts[i].$3.cast()); + } + final width = originalBits.length; + final t0 = portTraces[startIdx]; + final srcOffset = t0.sourceIndices.first; + final sourceBusBits = t0.busBits!.cast().toList(); + + // Reuse the original concat-input wire bits as the $slice + // output so that existing netname associations are preserved. + cellsToAdd['collapse_slice_$replIdx'] = + NetlistUtils.makeSliceCell(srcOffset, t0.busBits!.length, + width, sourceBusBits, originalBits); + replIdx++; + + // Add the combined port to the rebuilt concat. + final hi = outBitOffset + width - 1; + final portName = + hi == outBitOffset ? '[$hi]' : '[$hi:$outBitOffset]'; + newConns[portName] = originalBits; + newDirs[portName] = 'input'; + outBitOffset += width; + + portIdx = endIdx + 1; + } else { + // Keep this port as-is. + final port = inputPorts[portIdx]; + final width = port.$3.length; + final hi = outBitOffset + width - 1; + final portName = + hi == outBitOffset ? '[$hi]' : '[$hi:$outBitOffset]'; + newConns[portName] = port.$3.cast(); + newDirs[portName] = 'input'; + outBitOffset += width; + portIdx++; + } + } + + // Preserve Y. + newConns['Y'] = [for (final b in conns['Y'] as List) b as Object]; + newDirs['Y'] = 'output'; + + // Remove original concat, add collapsed replacement. + cellsToRemove.add(concatName); + cellsToAdd['${concatName}_collapsed'] = { + 'hide_name': concatCell['hide_name'], + 'type': r'$concat', + 'parameters': {}, + 'attributes': concatCell['attributes'] ?? {}, + 'port_directions': newDirs, + 'connections': newConns, + }; + } + + // Apply removals and additions. + cellsToRemove.forEach(cells.remove); + cells.addAll(cellsToAdd); + if (cellsToRemove.isNotEmpty || cellsToAdd.isNotEmpty) { + changed = true; + } + } // end while (changed) + } + } + + // -- Struct-conversion grouping ---------------------------------------- + + /// Scans every module in [allModules] for connected components of `$slice` + /// and `$concat` cells that form reconvergent struct-conversion trees. + /// Such trees arise from `LogicStructure.gets()` when a flat bus is + /// assigned to a struct (or vice-versa): leaf fields are sliced out and + /// re-packed through potentially multiple levels of concats. + /// + /// Each connected component is extracted into a new synthetic module + /// definition (added to [allModules]) and replaced in the parent with a + /// single hierarchical cell. This collapses the visual noise in the + /// netlist into a tidy "struct_assign_*" box. + static void applyStructConversionGrouping( + Map> allModules, + ) { + // Collect new module definitions to add (avoid modifying map during + // iteration). + final newModuleDefs = >{}; + + // Process each existing module definition. + for (final moduleName in allModules.keys.toList()) { + final moduleDef = allModules[moduleName]!; + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + // Identify all $slice and $concat cells. + final sliceConcat = {}; + for (final entry in cells.entries) { + final type = entry.value['type'] as String?; + if (type == r'$slice' || type == r'$concat') { + sliceConcat.add(entry.key); + } + } + if (sliceConcat.length < 2) { + continue; + } + + // Build wire-ID → driver cell and wire-ID → consumer cells maps. + final ( + :wireDriverCell, + wireConsumerCells: wireConsumerSets, + :bitToNetInfo, + ) = NetlistUtils.buildWireMaps(cells, moduleDef); + // Convert Set consumers to List for iteration. + final wireConsumerCells = >{ + for (final e in wireConsumerSets.entries) e.key: e.value.toList(), + }; + final modPorts = moduleDef['ports'] as Map>?; + + // Build adjacency among sliceConcat cells: two are adjacent if one's + // output feeds the other's input. + final adj = >{ + for (final cn in sliceConcat) cn: {}, + }; + for (final cn in sliceConcat) { + final cell = cells[cn]!; + final conns = cell['connections'] as Map? ?? {}; + final pdirs = cell['port_directions'] as Map? ?? {}; + for (final pe in conns.entries) { + final d = pdirs[pe.key] as String? ?? ''; + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + if (d == 'output') { + // Find consumers in sliceConcat. + for (final consumer in wireConsumerCells[b] ?? []) { + if (consumer != cn && sliceConcat.contains(consumer)) { + adj[cn]!.add(consumer); + adj[consumer]!.add(cn); + } + } + } else if (d == 'input') { + // Find driver in sliceConcat. + final drv = wireDriverCell[b]; + if (drv != null && drv != cn && sliceConcat.contains(drv)) { + adj[cn]!.add(drv); + adj[drv]!.add(cn); + } + } + } + } + } + + // Find connected components via BFS. + final visited = {}; + final components = >[]; + for (final start in sliceConcat) { + if (visited.contains(start)) { + continue; + } + final comp = {}; + final queue = [start]; + while (queue.isNotEmpty) { + final node = queue.removeLast(); + if (!comp.add(node)) { + continue; + } + visited.add(node); + for (final nb in adj[node]!) { + if (!comp.contains(nb)) { + queue.add(nb); + } + } + } + if (comp.length >= 2) { + components.add(comp); + } + } + + // For each connected component, extract it into a synthetic module. + var groupIdx = 0; + final groupQueue = [...components]; + var gqi = 0; + final claimedCells = {}; + while (gqi < groupQueue.length) { + final comp = groupQueue[gqi++]..removeAll(claimedCells); + if (comp.length < 2) { + continue; + } + + // Collect all wire IDs used inside the component and classify them + // as internal-only (driven AND consumed within comp) or external + // (boundary ports of the synthetic module). + // + // External inputs = wire IDs consumed by comp cells but driven + // outside the component. + // External outputs = wire IDs produced by comp cells but consumed + // outside the component (or by module ports). + final compOutputIds = {}; // driven by comp + final compInputIds = {}; // consumed by comp + + for (final cn in comp) { + final cell = cells[cn]!; + final conns = cell['connections'] as Map? ?? {}; + final pdirs = cell['port_directions'] as Map? ?? {}; + for (final pe in conns.entries) { + final d = pdirs[pe.key] as String? ?? ''; + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + if (d == 'output') { + compOutputIds.add(b); + } else if (d == 'input') { + compInputIds.add(b); + } + } + } + } + + // External input bits: consumed by comp but NOT driven by comp. + final extInputBits = compInputIds.difference(compOutputIds); + // External output bits: driven by comp but consumed outside comp + // (by non-comp cells or by module output ports). + final extOutputBits = {}; + for (final b in compOutputIds) { + // Check non-comp cell consumers. + for (final consumer in wireConsumerCells[b] ?? []) { + if (!comp.contains(consumer)) { + extOutputBits.add(b); + break; + } + } + // Check module output ports. + if (!extOutputBits.contains(b) && modPorts != null) { + for (final portEntry in modPorts.values) { + final dir = portEntry['direction'] as String?; + if (dir != 'output') { + continue; + } + final bits = portEntry['bits'] as List?; + if (bits != null && bits.contains(b)) { + extOutputBits.add(b); + break; + } + } + } + } + + if (extInputBits.isEmpty || extOutputBits.isEmpty) { + continue; // degenerate component, skip + } + + // Group external bits by netname to form named ports. + // Build a net-name → sorted bit IDs mapping for inputs and outputs. + final netnames = moduleDef['netnames'] as Map? ?? {}; + + // Wire → netname map (for bits in this component). + final wireToNet = {}; + for (final nnEntry in netnames.entries) { + final nd = nnEntry.value! as Map; + final bits = nd['bits'] as List? ?? []; + for (final b in bits) { + if (b is int) { + wireToNet[b] = nnEntry.key; + } + } + } + + // Group external input bits by their netname, preserving order. + final inputGroups = >{}; + for (final b in extInputBits) { + final nn = wireToNet[b] ?? 'in_$b'; + (inputGroups[nn] ??= []).add(b); + } + for (final v in inputGroups.values) { + v.sort(); + } + + // Group external output bits by their netname, preserving order. + final outputGroups = >{}; + for (final b in extOutputBits) { + final nn = wireToNet[b] ?? 'out_$b'; + (outputGroups[nn] ??= []).add(b); + } + for (final v in outputGroups.values) { + v.sort(); + } + + // Guard: only group when the component is a true struct + // assignment — one signal split into selections then re-assembled + // into one signal. The input may be wider than the output when + // fields are dropped (e.g. a nonCacheable bit unused in the + // destination struct). Multi-source concats (e.g. swizzles + // combining independent signals) and simple bit-range selections + // must remain as standalone cells. + if (inputGroups.length != 1 || + outputGroups.length != 1 || + extInputBits.length < extOutputBits.length) { + // Try sub-component extraction: for each $concat cell in the + // component, backward-BFS to find the subset of cells that + // transitively feed it. If that subset is strictly smaller + // than the full component it may pass the guard on its own. + for (final cn in comp.toList()) { + final cell = cells[cn]; + if (cell == null) { + continue; + } + if ((cell['type'] as String?) != r'$concat') { + continue; + } + + final subComp = {cn}; + final bfsQueue = [cn]; + while (bfsQueue.isNotEmpty) { + final cur = bfsQueue.removeLast(); + final curCell = cells[cur]; + if (curCell == null) { + continue; + } + final cConns = + curCell['connections'] as Map? ?? {}; + final cDirs = + curCell['port_directions'] as Map? ?? {}; + for (final pe in cConns.entries) { + if ((cDirs[pe.key] as String?) != 'input') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + final drv = wireDriverCell[b]; + if (drv != null && + comp.contains(drv) && + !subComp.contains(drv)) { + subComp.add(drv); + bfsQueue.add(drv); + } + } + } + } + + if (subComp.length >= 2 && subComp.length < comp.length) { + groupQueue.add(subComp); + } + } + continue; + } + + // Build the synthetic module's internal wire-ID space. + final usedIds = {}; + for (final cn in comp) { + final cell = cells[cn]; + if (cell == null) { + continue; + } + final conns = cell['connections'] as Map? ?? {}; + for (final bits in conns.values) { + for (final b in bits as List) { + if (b is int) { + usedIds.add(b); + } + } + } + } + + var nextLocalId = 2; + final idRemap = {}; + for (final id in usedIds) { + idRemap[id] = nextLocalId++; + } + + List remapBits(List bits) => + bits.map((b) => b is int ? (idRemap[b] ?? b) : b).toList(); + + // Build ports: one input port per input group, one output port per + // output group. + final childPorts = >{}; + final instanceConns = >{}; + final instancePortDirs = {}; + + for (final entry in inputGroups.entries) { + final portName = 'in_${entry.key}'; + final parentBits = entry.value.cast(); + childPorts[portName] = { + 'direction': 'input', + 'bits': remapBits(parentBits), + }; + instanceConns[portName] = parentBits; + instancePortDirs[portName] = 'input'; + } + + for (final entry in outputGroups.entries) { + final portName = 'out_${entry.key}'; + final parentBits = entry.value.cast(); + childPorts[portName] = { + 'direction': 'output', + 'bits': remapBits(parentBits), + }; + instanceConns[portName] = parentBits; + instancePortDirs[portName] = 'output'; + } + + // Re-map cells into the child's local ID space. + final childCells = >{}; + for (final cn in comp) { + final cell = Map.from(cells[cn]!); + final conns = Map.from( + cell['connections']! as Map); + for (final key in conns.keys.toList()) { + conns[key] = remapBits((conns[key] as List).cast()); + } + cell['connections'] = conns; + childCells[cn] = cell; + } + + // Build netnames for the child module. + final childNetnames = {}; + for (final pe in childPorts.entries) { + childNetnames[pe.key] = { + 'bits': pe.value['bits'], + 'attributes': {}, + }; + } + + final coveredIds = {}; + for (final nn in childNetnames.values) { + final bits = (nn! as Map)['bits']! as List; + for (final b in bits) { + if (b is int) { + coveredIds.add(b); + } + } + } + for (final cellEntry in childCells.entries) { + final cellName = cellEntry.key; + final conns = + cellEntry.value['connections'] as Map? ?? {}; + for (final connEntry in conns.entries) { + final portName = connEntry.key; + final bits = connEntry.value as List; + final missingBits = []; + for (final b in bits) { + if (b is int && !coveredIds.contains(b)) { + missingBits.add(b); + coveredIds.add(b); + } + } + if (missingBits.isNotEmpty) { + childNetnames['${cellName}_$portName'] = { + 'bits': missingBits, + 'hide_name': 1, + 'attributes': {}, + }; + } + } + } + + // Choose a name for the synthetic module type. + final syntheticTypeName = 'struct_assign_${moduleName}_$groupIdx'; + final syntheticInstanceName = 'struct_assign_$groupIdx'; + groupIdx++; + + // Register the synthetic module definition. + newModuleDefs[syntheticTypeName] = { + 'attributes': {'src': 'generated'}, + 'ports': childPorts, + 'cells': childCells, + 'netnames': childNetnames, + }; + + // Remove the grouped cells from the parent. + claimedCells.addAll(comp); + comp.forEach(cells.remove); + + // Add a hierarchical cell referencing the synthetic module. + cells[syntheticInstanceName] = { + 'hide_name': 0, + 'type': syntheticTypeName, + 'parameters': {}, + 'attributes': {}, + 'port_directions': instancePortDirs, + 'connections': instanceConns, + }; + } + } + + // Add all new synthetic module definitions. + allModules.addAll(newModuleDefs); + } + + /// Replace groups of `$slice` cells that share the same input bus and + /// whose outputs all feed into the same destination cell+port with a + /// single `$buf` cell. + /// + /// This eliminates visual noise from struct-to-flat-bus decomposition + /// when the destination consumes the full struct value unchanged. + /// Both signal names (source struct and destination port) are preserved + /// as separate netnames connected through the buffer. + static void applyStructBufferInsertion( + Map> allModules, + ) { + for (final moduleName in allModules.keys.toList()) { + final moduleDef = allModules[moduleName]!; + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + // Group $slice cells by their input bus (A bits). + final slicesByInput = >{}; + for (final entry in cells.entries) { + final cell = entry.value; + if (cell['type'] != r'$slice') { + continue; + } + final conns = cell['connections'] as Map?; + if (conns == null) { + continue; + } + final aBits = conns['A'] as List?; + if (aBits == null) { + continue; + } + final key = aBits.join(','); + (slicesByInput[key] ??= []).add(entry.key); + } + + var bufIdx = 0; + for (final sliceGroup in slicesByInput.values) { + if (sliceGroup.length < 2) { + continue; + } + + // Collect all Y output bit IDs from the group. + final allYBitIds = {}; + for (final sliceName in sliceGroup) { + final cell = cells[sliceName]!; + final conns = cell['connections']! as Map; + for (final b in conns['Y']! as List) { + if (b is int) { + allYBitIds.add(b); + } + } + } + + // Check: do all Y bits go to the same destination cell+port + // (or a single module output port)? + String? destId; // unique identifier for the destination + var allSameDest = true; + + // Check cell port destinations. + for (final otherEntry in cells.entries) { + if (sliceGroup.contains(otherEntry.key)) { + continue; + } + final otherConns = + otherEntry.value['connections'] as Map? ?? {}; + for (final portEntry in otherConns.entries) { + final bits = portEntry.value as List; + if (bits.any((b) => b is int && allYBitIds.contains(b))) { + final id = '${otherEntry.key}.${portEntry.key}'; + if (destId == null) { + destId = id; + } else if (destId != id) { + allSameDest = false; + break; + } + } + } + if (!allSameDest) { + break; + } + } + + // Also check module output ports as potential destinations. + final modPorts = + moduleDef['ports'] as Map>?; + if (allSameDest && modPorts != null) { + for (final portEntry in modPorts.entries) { + final port = portEntry.value; + final dir = port['direction'] as String?; + if (dir != 'output') { + continue; + } + final bits = port['bits'] as List?; + if (bits != null && + bits.any((b) => b is int && allYBitIds.contains(b))) { + final id = '__port_${portEntry.key}'; + if (destId == null) { + destId = id; + } else if (destId != id) { + allSameDest = false; + break; + } + } + } + } + + if (!allSameDest || destId == null) { + continue; + } + + // Verify slices contiguously cover the full A bus. + final firstSlice = cells[sliceGroup.first]!; + final params0 = firstSlice['parameters'] as Map?; + final aWidth = params0?['A_WIDTH'] as int?; + if (aWidth == null) { + continue; + } + + // Map offset → Y bits list, and validate. + final coverageYBits = >{}; + var totalYBits = 0; + var valid = true; + for (final sliceName in sliceGroup) { + final cell = cells[sliceName]!; + final params = cell['parameters'] as Map?; + final offset = params?['OFFSET'] as int?; + final yWidth = params?['Y_WIDTH'] as int?; + if (offset == null || yWidth == null) { + valid = false; + break; + } + final conns = cell['connections']! as Map; + final yBits = (conns['Y']! as List).cast(); + if (yBits.length != yWidth) { + valid = false; + break; + } + coverageYBits[offset] = yBits; + totalYBits += yWidth; + } + if (!valid || totalYBits != aWidth) { + continue; + } + + // Verify contiguous coverage (no gaps or overlaps). + final sortedOffsets = coverageYBits.keys.toList()..sort(); + var expectedOffset = 0; + for (final off in sortedOffsets) { + if (off != expectedOffset) { + valid = false; + break; + } + expectedOffset += coverageYBits[off]!.length; + } + if (!valid || expectedOffset != aWidth) { + continue; + } + + // Build the buffer cell. + final firstConns = firstSlice['connections']! as Map; + final aBus = (firstConns['A']! as List).cast(); + + // Construct Y by concatenating slice outputs in offset order. + final yBus = []; + for (final off in sortedOffsets) { + yBus.addAll(coverageYBits[off]!); + } + + // Remove slice cells. + sliceGroup.forEach(cells.remove); + + // Insert $buf cell. + cells['struct_buf_$bufIdx'] = + NetlistUtils.makeBufCell(aWidth, aBus, yBus); + bufIdx++; + } + } + } + + /// Replaces each `struct_assign_*` hierarchical instance in parent modules + /// with one `$buf` cell per output port and removes the synthetic module + /// definition. + /// + /// For each output port the internal `$slice`/`$concat` routing is traced + /// back to the corresponding input-port bits so that each `$buf` connects + /// only the bits belonging to that specific net. This keeps distinct + /// signal paths (e.g. `sum_0 → sumRpath` vs `sumP1 → sumPlusOneRpath`) + /// as separate cells so the schematic viewer can route them independently. + static void collapseStructGroupModules( + Map> allModules, + ) { + // Collect the names of all struct_assign module definitions to remove. + final structAssignTypes = { + for (final name in allModules.keys) + if (name.startsWith('struct_assign_')) name, + }; + + if (structAssignTypes.isEmpty) { + return; + } + + // Track which struct_assign types were fully collapsed (all instances + // replaced). Only those will have their definitions removed. + final collapsedTypes = {}; + final keptTypes = {}; + + // In each module, replace cells that instantiate a struct_assign type + // with a $buf cell. + for (final moduleDef in allModules.values) { + final cells = + moduleDef['cells'] as Map>? ?? {}; + + final replacements = >{}; + final removals = []; + + for (final entry in cells.entries) { + final cellName = entry.key; + final cell = entry.value; + final type = cell['type'] as String?; + if (type == null || !structAssignTypes.contains(type)) { + continue; + } + + final conns = cell['connections'] as Map? ?? {}; + + // Look up the synthetic module definition so we can trace the + // actual per-bit routing through its internal $slice/$concat cells. + final synthDef = allModules[type]; + if (synthDef == null) { + continue; + } + + final synthPorts = + synthDef['ports'] as Map>? ?? {}; + final synthCells = + synthDef['cells'] as Map>? ?? {}; + + // Map local (module-internal) input port bits → parent bit IDs, + // and also record which input port name each local bit belongs to + // plus its index within that port. + final localToParent = {}; + final localBitToInputPort = {}; + final localBitToIndex = {}; + final inputPortWidths = {}; + for (final pEntry in synthPorts.entries) { + final dir = pEntry.value['direction'] as String?; + if (dir != 'input' && dir != 'inout') { + continue; + } + final localBits = + (pEntry.value['bits'] as List?)?.cast() ?? []; + final parentBits = (conns[pEntry.key] as List?)?.cast() ?? []; + inputPortWidths[pEntry.key] = localBits.length; + for (var i = 0; i < localBits.length && i < parentBits.length; i++) { + if (localBits[i] is int) { + localToParent[localBits[i] as int] = parentBits[i]; + localBitToInputPort[localBits[i] as int] = pEntry.key; + localBitToIndex[localBits[i] as int] = i; + } + } + } + + final inputPortBits = localToParent.keys.toSet(); + + // Build a net-driver map inside the synthetic module by + // processing its $slice, $concat, and $buf cells. + final driver = {}; + + for (final sc in synthCells.values) { + final ct = sc['type'] as String?; + final cc = sc['connections'] as Map? ?? {}; + final cp = sc['parameters'] as Map? ?? {}; + + if (ct == r'$slice') { + final aBits = (cc['A'] as List?)?.cast() ?? []; + final yBits = (cc['Y'] as List?)?.cast() ?? []; + final offset = cp['OFFSET'] as int? ?? 0; + final yWidth = yBits.length; + final aWidth = aBits.length; + final reversed = offset + yWidth > aWidth; + for (var i = 0; i < yBits.length; i++) { + if (yBits[i] is int) { + final srcIdx = reversed ? (offset - i) : (offset + i); + if (srcIdx >= 0 && srcIdx < aBits.length) { + driver[yBits[i] as int] = aBits[srcIdx]; + } + } + } + } else if (ct == r'$concat') { + final yBits = (cc['Y'] as List?)?.cast() ?? []; + + // Gather input bits in LSB-first order. Two formats: + // 1. Standard 2-input: ports A (LSB) and B (MSB). + // 2. Multi-input: range-named ports [lo:hi] with + // INx_WIDTH parameters — ordered by range start. + final inputBits = []; + if (cc.containsKey('A')) { + inputBits + ..addAll((cc['A'] as List?)?.cast() ?? []) + ..addAll((cc['B'] as List?)?.cast() ?? []); + } else { + // Multi-input concat: collect range-named ports ordered + // by their starting bit position (LSB first). + final rangePorts = >{}; + for (final portName in cc.keys) { + if (portName == 'Y') { + continue; + } + final m = NetlistUtils.rangePortRe.firstMatch(portName); + if (m != null) { + final hi = int.parse(m.group(1)!); + final lo = m.group(2) != null ? int.parse(m.group(2)!) : hi; + rangePorts[lo] = + (cc[portName] as List?)?.cast() ?? []; + } + } + final sortedKeys = rangePorts.keys.toList()..sort(); + for (final k in sortedKeys) { + inputBits.addAll(rangePorts[k]!); + } + } + + for (var i = 0; i < yBits.length; i++) { + if (yBits[i] is int && i < inputBits.length) { + driver[yBits[i] as int] = inputBits[i]; + } + } + } else if (ct == r'$buf') { + final aBits = (cc['A'] as List?)?.cast() ?? []; + final yBits = (cc['Y'] as List?)?.cast() ?? []; + for (var i = 0; i < yBits.length && i < aBits.length; i++) { + if (yBits[i] is int) { + driver[yBits[i] as int] = aBits[i]; + } + } + } + } + + // Trace a local bit backwards through the driver map until we + // reach an input port bit or a string constant. + Object traceToSource(Object bit) { + final visited = {}; + var current = bit; + while (current is int && !inputPortBits.contains(current)) { + if (visited.contains(current)) { + break; + } + visited.add(current); + final next = driver[current]; + if (next == null) { + break; + } + current = next; + } + return current; + } + + // For each output port, trace its bits to their source and build + // the appropriate cell type: + // $buf – output has same width as its single source input port + // $slice – output is a contiguous sub-range of one input port + // $concat – output combines bits from multiple input ports + final perPortCells = >{}; + var anyUnresolved = false; + + for (final pEntry in synthPorts.entries) { + final dir = pEntry.value['direction'] as String?; + if (dir != 'output') { + continue; + } + final localBits = + (pEntry.value['bits'] as List?)?.cast() ?? []; + final parentBits = (conns[pEntry.key] as List?)?.cast() ?? []; + + final portOutputBits = []; + final portInputBits = []; + // Track per-bit source: local input-port bit ID (int) or null. + final sourceBitIds = []; + + for (var i = 0; i < parentBits.length; i++) { + portOutputBits.add(parentBits[i]); + if (i < localBits.length) { + final source = traceToSource(localBits[i]); + if (source is int && localToParent.containsKey(source)) { + portInputBits.add(localToParent[source]!); + sourceBitIds.add(source); + } else if (source is String) { + portInputBits.add(source); + sourceBitIds.add(null); + } else { + portInputBits.add('x'); + sourceBitIds.add(null); + } + } else { + portInputBits.add('x'); + sourceBitIds.add(null); + } + } + + if (portInputBits.contains('x')) { + anyUnresolved = true; + break; + } + + if (portOutputBits.isEmpty) { + continue; + } + + // Determine which input port(s) source this output port. + final sourcePortNames = {}; + for (final sid in sourceBitIds) { + if (sid != null && localBitToInputPort.containsKey(sid)) { + sourcePortNames.add(localBitToInputPort[sid]!); + } + } + + final cellKey = '${cellName}_${pEntry.key}'; + + if (sourcePortNames.length == 1) { + final srcPort = sourcePortNames.first; + final srcWidth = inputPortWidths[srcPort] ?? 0; + if (portOutputBits.length == srcWidth) { + // Same width → $buf + perPortCells['${cellKey}_buf'] = NetlistUtils.makeBufCell( + portOutputBits.length, portInputBits, portOutputBits); + } else { + // Subset of one input port → $slice. Determine the offset + // from the first traced bit's index within its input port. + final firstIdx = sourceBitIds.first; + final offset = + firstIdx != null ? (localBitToIndex[firstIdx] ?? 0) : 0; + perPortCells['${cellKey}_slice'] = NetlistUtils.makeSliceCell( + offset, + srcWidth, + portOutputBits.length, + (conns[srcPort] as List?)?.cast() ?? [], + portOutputBits); + } + } else { + // Multiple source ports – should be rare after the grouping + // guard excludes multi-source concats. Fall back to $buf. + perPortCells['${cellKey}_buf'] = NetlistUtils.makeBufCell( + portOutputBits.length, portInputBits, portOutputBits); + } + } + + if (perPortCells.isEmpty) { + continue; + } + + // Only collapse pure passthroughs: every output bit must trace + // back to an input-port bit or a string constant. If any bit + // fell through as 'x' the module is doing real computation + // (e.g. addition, muxing) and should be kept as a hierarchy. + if (anyUnresolved) { + keptTypes.add(type); + continue; + } + + collapsedTypes.add(type); + removals.add(cellName); + replacements.addAll(perPortCells); + } + + removals.forEach(cells.remove); + cells.addAll(replacements); + } + + // Remove only the synthetic module definitions whose instances were all + // successfully collapsed. Types that had at least one non-passthrough + // instance must keep their definition so the hierarchy is preserved. + collapsedTypes.difference(keptTypes).forEach(allModules.remove); + } + + /// Replace standalone `$concat` cells whose input bits all originate + /// from a single module input (or inout) port and cover its full width + /// with a simple `$buf` cell. + /// + /// This eliminates the visual noise of struct-to-bitvector reassembly + /// when an input [LogicStructure] port is decomposed into fields and + /// immediately re-packed via a [Swizzle]. + static void applyConcatToBufferReplacement( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final modPorts = moduleDef['ports'] as Map>?; + if (modPorts == null) { + continue; + } + + // Build bit → port-name map for input / inout ports. + final bitToPort = {}; + for (final portEntry in modPorts.entries) { + final dir = portEntry.value['direction'] as String?; + if (dir != 'input' && dir != 'inout') { + continue; + } + final bits = portEntry.value['bits'] as List? ?? []; + for (final b in bits) { + if (b is int) { + bitToPort[b] = portEntry.key; + } + } + } + + final removals = []; + final additions = >{}; + var bufIdx = 0; + + // Avoid name collisions with existing concat_buf_* cells. + for (final name in cells.keys) { + if (name.startsWith('concat_buf_')) { + final idx = int.tryParse(name.substring('concat_buf_'.length)); + if (idx != null && idx >= bufIdx) { + bufIdx = idx + 1; + } + } + } + + for (final entry in cells.entries) { + final cell = entry.value; + if ((cell['type'] as String?) != r'$concat') { + continue; + } + + final conns = cell['connections'] as Map? ?? {}; + final pdirs = cell['port_directions'] as Map? ?? {}; + + // Collect input ranges and the Y output. + // Port names follow the pattern "[upper:lower]" or "[bit]". + final rangedInputs = >{}; // lower → bits + List? yBits; + + for (final pe in conns.entries) { + final dir = pdirs[pe.key] as String? ?? ''; + final bits = (pe.value as List).cast(); + if (dir == 'output' && pe.key == 'Y') { + yBits = bits; + continue; + } + if (dir != 'input') { + continue; + } + // Parse "[upper:lower]" or "[bit]". + final match = NetlistUtils.rangePortRe.firstMatch(pe.key); + if (match == null) { + // Also accept the 2-input A/B form. + if (pe.key == 'A') { + rangedInputs[0] = bits; + } else if (pe.key == 'B') { + // Determine A width to set the offset. + final aBits = conns['A'] as List?; + if (aBits != null) { + rangedInputs[aBits.length] = bits; + } + } + continue; + } + final upper = int.parse(match.group(1)!); + final lower = + match.group(2) != null ? int.parse(match.group(2)!) : upper; + rangedInputs[lower] = bits; + } + + if (yBits == null || rangedInputs.isEmpty) { + continue; + } + + // Assemble input bits in LSB-to-MSB order. + final sortedLowers = rangedInputs.keys.toList()..sort(); + final allInputBits = []; + for (final lower in sortedLowers) { + allInputBits.addAll(rangedInputs[lower]!); + } + + // Check that every input bit belongs to the same module port. + String? sourcePort; + var allFromSamePort = true; + for (final b in allInputBits) { + if (b is! int) { + allFromSamePort = false; + break; + } + final port = bitToPort[b]; + if (port == null) { + allFromSamePort = false; + break; + } + sourcePort ??= port; + if (port != sourcePort) { + allFromSamePort = false; + break; + } + } + + if (!allFromSamePort || sourcePort == null) { + continue; + } + + // Verify full-width coverage of the source port. + final portBits = modPorts[sourcePort]!['bits']! as List; + if (allInputBits.length != portBits.length) { + continue; + } + + // Replace $concat with $buf. + removals.add(entry.key); + additions['concat_buf_$bufIdx'] = + NetlistUtils.makeBufCell(allInputBits.length, allInputBits, yBits); + bufIdx++; + } + + removals.forEach(cells.remove); + cells.addAll(additions); + } + } + + // -- Collapse selects into struct_pack --------------------------------- + + /// Finds `$slice` cells whose outputs feed exclusively into a + /// `$struct_pack` input port. The slice is absorbed: the pack input + /// port is rewired to the slice's source bits directly and the + /// now-redundant slice is removed. + /// + /// This is the "selects into a pack" optimization: when a flat bus is + /// decomposed through individual slices and then repacked into a struct, + /// the intermediate slice cells add visual noise beyond what the + /// struct_pack field metadata already provides. + static void applyCollapseSelectsIntoPack( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final (:wireDriverCell, :wireConsumerCells, :bitToNetInfo) = + NetlistUtils.buildWireMaps(cells, moduleDef); + + final cellsToRemove = {}; + + for (final packEntry in cells.entries.toList()) { + final packName = packEntry.key; + final packCell = packEntry.value; + if ((packCell['type'] as String?) != r'$struct_pack') { + continue; + } + + final conns = packCell['connections'] as Map? ?? {}; + final dirs = packCell['port_directions'] as Map? ?? {}; + + for (final portName in conns.keys.toList()) { + if (dirs[portName] != 'input') { + continue; + } + final bits = [ + for (final b in conns[portName] as List) + if (b is int) b, + ]; + if (bits.isEmpty) { + continue; + } + + // All bits must be driven by the same $slice cell. + final firstDriver = wireDriverCell[bits.first]; + if (firstDriver == null) { + continue; + } + final driverCell = cells[firstDriver]; + if (driverCell == null) { + continue; + } + if ((driverCell['type'] as String?) != r'$slice') { + continue; + } + if (cellsToRemove.contains(firstDriver)) { + continue; + } + + final allFromSameSlice = bits.every( + (b) => wireDriverCell[b] == firstDriver, + ); + if (!allFromSameSlice) { + continue; + } + + // The slice must exclusively feed this pack. + final sliceConns = + driverCell['connections'] as Map? ?? {}; + final sliceYBits = [ + for (final b in sliceConns['Y'] as List) + if (b is int) b, + ]; + final exclusive = sliceYBits.every((b) { + final consumers = wireConsumerCells[b]; + if (consumers == null) { + return true; + } + return consumers.every((c) => c == packName || c == '__port__'); + }); + if (!exclusive) { + continue; + } + + // Rewire: replace the pack's input bits with the slice's + // source bits (A port) at the correct offset. + final sliceABits = sliceConns['A'] as List; + final params = + driverCell['parameters'] as Map? ?? {}; + final offset = params['OFFSET'] as int? ?? 0; + final yWidth = sliceYBits.length; + + final newBits = [ + for (var i = 0; i < yWidth; i++) sliceABits[offset + i] as Object, + ]; + conns[portName] = newBits; + + cellsToRemove.add(firstDriver); + } + } + + cellsToRemove.forEach(cells.remove); + + // Second pass: collapse struct_pack → $buf when all field inputs + // form a contiguous ascending sequence (identity pack). + for (final packEntry in cells.entries.toList()) { + final packName = packEntry.key; + final packCell = packEntry.value; + if ((packCell['type'] as String?) != r'$struct_pack') { + continue; + } + + final conns = packCell['connections'] as Map? ?? {}; + final dirs = packCell['port_directions'] as Map? ?? {}; + + // Collect all input bits in field declaration order. + final allInputBits = []; + for (final portName in conns.keys) { + if (dirs[portName] != 'input') { + continue; + } + for (final b in conns[portName] as List) { + if (b is int) { + allInputBits.add(b); + } + } + } + if (allInputBits.length < 2) { + continue; + } + + // Check: input bits must form a contiguous ascending sequence. + var contiguous = true; + for (var i = 1; i < allInputBits.length; i++) { + if (allInputBits[i] != allInputBits[i - 1] + 1) { + contiguous = false; + break; + } + } + if (!contiguous) { + continue; + } + + final yBits = [ + for (final b in conns['Y'] as List) + if (b is int) b, + ]; + if (yBits.length != allInputBits.length) { + continue; + } + + // Replace struct_pack with $buf. + cells[packName] = { + 'type': r'$buf', + 'parameters': {'WIDTH': allInputBits.length}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{ + 'A': allInputBits.cast(), + 'Y': yBits, + }, + }; + } + } + } + + // -- Collapse struct_unpack to concat ---------------------------------- + + /// Finds `$concat` cells whose input ports are driven (directly or + /// through exclusive `$buf`/`$slice` chains) by output ports of + /// `$struct_unpack` cells. When all inputs trace back through a single + /// unpack to its source bus, the concat and intermediate cells are + /// replaced by a `$buf` or `$slice` from the unpack's A bus. + /// + /// Partial collapse is also supported: contiguous runs of concat ports + /// that trace to the same unpack are collapsed individually. + static void applyCollapseUnpackToConcat( + Map> allModules, + ) { + for (final moduleEntry in allModules.entries) { + final moduleDef = moduleEntry.value; + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + // Iterate until convergence: each pass may create bufs that enable + // the next outer concat/unpack to collapse. + var globalReplIdx = 0; + var anyChanged = true; + var iteration = 0; + while (anyChanged && iteration < 20) { + anyChanged = false; + iteration++; + + final (:wireDriverCell, :wireConsumerCells, :bitToNetInfo) = + NetlistUtils.buildWireMaps(cells, moduleDef); + + final cellsToRemove = {}; + final cellsToAdd = >{}; + var replIdx = globalReplIdx; + + for (final concatEntry in cells.entries.toList()) { + final concatName = concatEntry.key; + final concatCell = concatEntry.value; + if ((concatCell['type'] as String?) != r'$concat') { + continue; + } + if (cellsToRemove.contains(concatName)) { + continue; + } + + final conns = + concatCell['connections'] as Map? ?? {}; + + // Parse input ports into ordered list. + final inputPorts = <(int lo, String portName, List bits)>[]; + var hasRangePorts = false; + for (final portName in conns.keys) { + if (portName == 'Y') { + continue; + } + final m = NetlistUtils.rangePortRe.firstMatch(portName); + if (m != null) { + hasRangePorts = true; + final hi = int.parse(m.group(1)!); + final lo = m.group(2) != null ? int.parse(m.group(2)!) : hi; + inputPorts.add(( + lo, + portName, + [ + for (final b in conns[portName] as List) + if (b is int) b + ], + )); + } + } + if (!hasRangePorts) { + if (conns.containsKey('A') && conns.containsKey('B')) { + final aBits = [ + for (final b in conns['A'] as List) + if (b is int) b, + ]; + final bBits = [ + for (final b in conns['B'] as List) + if (b is int) b, + ]; + inputPorts + ..add((0, 'A', aBits)) + ..add((aBits.length, 'B', bBits)); + } + } + inputPorts.sort((a, b) => a.$1.compareTo(b.$1)); + if (inputPorts.length < 2) { + continue; + } + + // --- Extended trace: through $buf/$slice AND $struct_unpack ------ + final portTraces = <({ + String? unpackName, + List? unpackABits, + List sourceIndices, + Set intermediates, + bool valid, + })>[]; + + for (final (_, _, bits) in inputPorts) { + final sourceIndices = []; + final intermediates = {}; + String? unpackName; + List? unpackABits; + var valid = true; + + for (final bit in bits) { + final (traced, chain) = + NetlistUtils.traceBackward(bit, wireDriverCell, cells); + intermediates.addAll(chain); + + // Check if traced bit is driven by a $struct_unpack output. + final driverName = wireDriverCell[traced]; + if (driverName == null) { + valid = false; + break; + } + final driverCell = cells[driverName]; + if (driverCell == null || + (driverCell['type'] as String?) != r'$struct_unpack') { + valid = false; + break; + } + + final uConns = + driverCell['connections'] as Map? ?? {}; + final uDirs = + driverCell['port_directions'] as Map? ?? {}; + final aBits = [ + for (final b in uConns['A'] as List) + if (b is int) b, + ]; + + // Find which output port contains this bit and its index + // within that port. + String? outPort; + int? bitIdx; + for (final pe in uConns.entries) { + if (pe.key == 'A') { + continue; + } + if (uDirs[pe.key] != 'output') { + continue; + } + final pBits = [ + for (final b in pe.value as List) + if (b is int) b, + ]; + final idx = pBits.indexOf(traced); + if (idx >= 0) { + outPort = pe.key; + bitIdx = idx; + break; + } + } + + if (outPort == null || bitIdx == null) { + valid = false; + break; + } + + // Find the field offset for this output port. + final params = + driverCell['parameters'] as Map? ?? {}; + final fc = params['FIELD_COUNT'] as int? ?? 0; + int? fieldOffset; + for (var fi = 0; fi < fc; fi++) { + final fname = params['FIELD_${fi}_NAME'] as String? ?? ''; + if (fname == outPort || outPort == '${fname}_$fi') { + fieldOffset = params['FIELD_${fi}_OFFSET'] as int? ?? 0; + break; + } + } + + if (fieldOffset == null) { + valid = false; + break; + } + + final aIdx = fieldOffset + bitIdx; + if (aIdx >= aBits.length) { + valid = false; + break; + } + + intermediates.add(driverName); + + if (unpackName == null) { + unpackName = driverName; + unpackABits = aBits; + } else if (unpackName != driverName) { + valid = false; + break; + } + sourceIndices.add(aIdx); + } + + portTraces.add(( + unpackName: unpackName, + unpackABits: unpackABits, + sourceIndices: sourceIndices, + intermediates: intermediates, + valid: valid, + )); + } + + // --- Find runs of consecutive ports tracing to the same unpack --- + final runs = <(int startIdx, int endIdx)>[]; + var runStart = 0; + while (runStart < inputPorts.length) { + final t = portTraces[runStart]; + if (!t.valid || t.unpackName == null) { + runStart++; + continue; + } + var runEnd = runStart; + while (runEnd + 1 < inputPorts.length) { + final nextT = portTraces[runEnd + 1]; + if (!nextT.valid || nextT.unpackName != t.unpackName) { + break; + } + final curLast = portTraces[runEnd].sourceIndices.last; + final nextFirst = nextT.sourceIndices.first; + if (nextFirst != curLast + 1) { + break; + } + runEnd++; + } + if (runEnd > runStart) { + runs.add((runStart, runEnd)); + } + runStart = runEnd + 1; + } + + if (runs.isEmpty) { + // No contiguous ascending runs, but check if ALL ports trace + // to the same unpack (general reorder / swizzle case). + final allValid = portTraces.every((t) => t.valid); + if (!allValid) { + continue; + } + final unpackNames = portTraces.map((t) => t.unpackName).toSet(); + if (unpackNames.length != 1 || unpackNames.first == null) { + continue; + } + final uName = unpackNames.first!; + final uABits = portTraces.first.unpackABits!; + + // Gather all intermediates and verify exclusivity. + final allIntermediates = {}; + for (final t in portTraces) { + allIntermediates.addAll(t.intermediates); + } + final removable = allIntermediates.where((c) { + final ct = cells[c]?['type'] as String?; + return ct == r'$buf' || ct == r'$slice'; + }).toSet(); + if (removable.isNotEmpty && + !NetlistUtils.isExclusiveChain( + intermediates: removable, + ownerCell: concatName, + cells: cells, + wireConsumerCells: wireConsumerCells, + )) { + continue; + } + + // Build reordered A bits: for each concat input port (in + // order), map the source indices back to the unpack's A bus. + final reorderedA = []; + for (final t in portTraces) { + for (final aIdx in t.sourceIndices) { + reorderedA.add(uABits[aIdx] as Object); + } + } + final outputBits = [ + for (final b in conns['Y'] as List) + if (b is int) b, + ]; + if (reorderedA.length != outputBits.length) { + continue; + } + + cellsToRemove + ..addAll(removable) + ..add(uName) + ..add(concatName); + cellsToAdd['unpack_concat_buf_$replIdx'] = NetlistUtils.makeBufCell( + reorderedA.length, reorderedA, outputBits); + replIdx++; + continue; + } + + // --- Verify exclusivity of non-unpack intermediates ------ + final validRuns = + <(int startIdx, int endIdx, Set intermediates)>[]; + for (final (startIdx, endIdx) in runs) { + final allIntermediates = {}; + for (var i = startIdx; i <= endIdx; i++) { + allIntermediates.addAll(portTraces[i].intermediates); + } + // Only remove $buf/$slice intermediates, not the unpack itself. + final removable = allIntermediates.where((c) { + final ct = cells[c]?['type'] as String?; + return ct == r'$buf' || ct == r'$slice'; + }).toSet(); + if (removable.isEmpty || + NetlistUtils.isExclusiveChain( + intermediates: removable, + ownerCell: concatName, + cells: cells, + wireConsumerCells: wireConsumerCells, + )) { + validRuns.add((startIdx, endIdx, removable)); + } + } + + if (validRuns.isEmpty) { + continue; + } + + final allCollapsed = validRuns.length == 1 && + validRuns.first.$1 == 0 && + validRuns.first.$2 == inputPorts.length - 1; + + for (final (_, _, intermediates) in validRuns) { + cellsToRemove.addAll(intermediates); + } + + if (allCollapsed) { + // Full collapse — replace concat with $buf or $slice. + // Since we remove intermediates (buf/slice chains between the + // unpack outputs and the concat inputs), we must source the + // replacement buf from the unpack's A bus, not the concat's + // input bits which may reference wires driven by the removed + // intermediates. + final t0 = portTraces.first; + final srcOffset = t0.sourceIndices.first; + final yWidth = (conns['Y'] as List).whereType().length; + final aWidth = t0.unpackABits!.length; + final sourceBits = t0.unpackABits!.cast().toList(); + final outputBits = [ + for (final b in conns['Y'] as List) + if (b is int) b, + ]; + + cellsToRemove + ..add(concatName) + // Also remove the unpack itself — all its outputs are consumed + // exclusively through intermediates into this concat. + ..add(t0.unpackName!); + if (yWidth == aWidth) { + cellsToAdd['unpack_concat_buf_$replIdx'] = + NetlistUtils.makeBufCell(aWidth, sourceBits, outputBits); + } else { + cellsToAdd['unpack_concat_buf_$replIdx'] = + NetlistUtils.makeSliceCell( + srcOffset, aWidth, yWidth, sourceBits, outputBits); + } + replIdx++; + continue; + } + + // --- Partial collapse — rebuild concat with fewer ports --------- + cellsToRemove.add(concatName); + + final newConns = >{}; + final newDirs = {}; + var outBitOffset = 0; + + var portIdx = 0; + while (portIdx < inputPorts.length) { + (int, int, Set)? activeRun; + for (final run in validRuns) { + if (run.$1 == portIdx) { + activeRun = run; + break; + } + } + + if (activeRun != null) { + final (startIdx, endIdx, _) = activeRun; + // Collect the traced source bits — the unpack output bits + // that traceBackward found. We cannot use the concat's raw + // input bits because intermediates (buf/slice chains) between + // the unpack outputs and the concat are being removed. + final tracedBits = []; + final t0 = portTraces[startIdx]; + final uConns = cells[t0.unpackName!]!['connections'] + as Map? ?? + {}; + final uDirs = cells[t0.unpackName!]!['port_directions'] + as Map? ?? + {}; + // Rebuild the unpack's output bits in field declaration order + // to create a mapping from A-index to wire ID. + final unpackOutBitList = []; + for (final pe in uConns.entries) { + if (pe.key == 'A') { + continue; + } + if (uDirs[pe.key] != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is int) { + unpackOutBitList.add(b); + } + } + } + // Build A-index -> output wire ID map. + final aToOutBit = {}; + final uParams = cells[t0.unpackName!]!['parameters'] + as Map? ?? + {}; + final fc = uParams['FIELD_COUNT'] as int? ?? 0; + var outIdx = 0; + for (var fi = 0; fi < fc; fi++) { + final fw = uParams['FIELD_${fi}_WIDTH'] as int? ?? 0; + final fo = uParams['FIELD_${fi}_OFFSET'] as int? ?? 0; + for (var bi = 0; bi < fw; bi++) { + if (outIdx < unpackOutBitList.length) { + aToOutBit[fo + bi] = unpackOutBitList[outIdx]; + } + outIdx++; + } + } + for (var i = startIdx; i <= endIdx; i++) { + for (final aIdx in portTraces[i].sourceIndices) { + final outBit = aToOutBit[aIdx]; + if (outBit != null) { + tracedBits.add(outBit); + } + } + } + final width = tracedBits.length; + + final hi = outBitOffset + width - 1; + final portName = + hi == outBitOffset ? '[$hi]' : '[$hi:$outBitOffset]'; + newConns[portName] = tracedBits; + newDirs[portName] = 'input'; + outBitOffset += width; + + portIdx = endIdx + 1; + } else { + final port = inputPorts[portIdx]; + final width = port.$3.length; + final hi = outBitOffset + width - 1; + final portName = + hi == outBitOffset ? '[$hi]' : '[$hi:$outBitOffset]'; + newConns[portName] = port.$3.cast(); + newDirs[portName] = 'input'; + outBitOffset += width; + portIdx++; + } + } + + newConns['Y'] = [for (final b in conns['Y'] as List) b as Object]; + newDirs['Y'] = 'output'; + + cellsToAdd['${concatName}_collapsed'] = { + 'hide_name': concatCell['hide_name'], + 'type': r'$concat', + 'parameters': {}, + 'attributes': concatCell['attributes'] ?? {}, + 'port_directions': newDirs, + 'connections': newConns, + }; + } + + cellsToRemove.forEach(cells.remove); + cells.addAll(cellsToAdd); + if (cellsToRemove.isNotEmpty || cellsToAdd.isNotEmpty) { + anyChanged = true; + } + globalReplIdx = replIdx; + + // Second pass: collapse identity struct_unpack → $buf chains. + // If ALL outputs of a struct_unpack go exclusively to one $buf whose + // A bits are exactly those outputs in order, replace both with a + // single $buf from the unpack's A to the buf's Y. + final unpacksToRemove = {}; + final bufsToRemove = {}; + final bufsToAdd = >{}; + var identBufIdx = 0; + + final wireMaps2 = NetlistUtils.buildWireMaps(cells, moduleDef); + final wireConsumerCells2 = wireMaps2.wireConsumerCells; + + for (final entry in cells.entries.toList()) { + final unpackName = entry.key; + final unpackCell = entry.value; + if ((unpackCell['type'] as String?) != r'$struct_unpack') { + continue; + } + if (unpacksToRemove.contains(unpackName)) { + continue; + } + + final uConns = + unpackCell['connections'] as Map? ?? {}; + final uDirs = + unpackCell['port_directions'] as Map? ?? {}; + + // Collect all output bits in field declaration order. + final allOutputBits = []; + for (final pname in uConns.keys) { + if (uDirs[pname] != 'output') { + continue; + } + for (final b in uConns[pname] as List) { + if (b is int) { + allOutputBits.add(b); + } + } + } + if (allOutputBits.isEmpty) { + continue; + } + + // Every output bit must be consumed by exactly one $buf cell + // (the same one). + String? targetBufName; + var allToOneBuf = true; + for (final bit in allOutputBits) { + final consumers = wireConsumerCells2[bit]; + if (consumers == null || consumers.length != 1) { + allToOneBuf = false; + break; + } + final consumer = consumers.first; + if (consumer == '__port__') { + allToOneBuf = false; + break; + } + final consumerCell = cells[consumer]; + if (consumerCell == null || + (consumerCell['type'] as String?) != r'$buf') { + allToOneBuf = false; + break; + } + if (targetBufName == null) { + targetBufName = consumer; + } else if (consumer != targetBufName) { + allToOneBuf = false; + break; + } + } + if (!allToOneBuf || targetBufName == null) { + continue; + } + if (bufsToRemove.contains(targetBufName)) { + continue; + } + + final bufCell = cells[targetBufName]!; + final bufConns = + bufCell['connections'] as Map? ?? {}; + final bufABits = [ + for (final b in bufConns['A'] as List) + if (b is int) b, + ]; + + // The buf's A bits must be exactly the unpack's output bits. + if (bufABits.length != allOutputBits.length) { + continue; + } + var bitsMatch = true; + for (var i = 0; i < bufABits.length; i++) { + if (bufABits[i] != allOutputBits[i]) { + bitsMatch = false; + break; + } + } + if (!bitsMatch) { + continue; + } + + // Collapse: single buf from unpack.A → buf.Y + final unpackABits = [ + for (final b in uConns['A'] as List) + if (b is int) b, + ]; + final bufYBits = [ + for (final b in bufConns['Y'] as List) + if (b is int) b, + ]; + + if (unpackABits.length != bufYBits.length) { + continue; + } + + bufsToAdd['${unpackName}_buf_$identBufIdx'] = { + 'type': r'$buf', + 'parameters': {'WIDTH': unpackABits.length}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{ + 'A': unpackABits, + 'Y': bufYBits, + }, + }; + identBufIdx++; + unpacksToRemove.add(unpackName); + bufsToRemove.add(targetBufName); + } + + unpacksToRemove.forEach(cells.remove); + bufsToRemove.forEach(cells.remove); + cells.addAll(bufsToAdd); + if (unpacksToRemove.isNotEmpty || bufsToRemove.isNotEmpty) { + anyChanged = true; + } + } // end while (anyChanged) + } + } + + // -- Collapse struct_unpack to struct_pack ----------------------------- + + /// Finds `$struct_pack` cells whose input ports are driven (directly + /// or through exclusive `$buf`/`$slice` chains) by output ports of + /// `$struct_unpack` cells. The exclusive intermediate `$buf`/`$slice` + /// cells are removed, and the pack input ports are rewired to the + /// unpack output bits directly. + /// + /// The unpack cell itself is preserved (it may have other consumers). + /// Only the intermediate routing cells are removed. + static void applyCollapseUnpackToPack( + Map> allModules, + ) { + for (final moduleDef in allModules.values) { + final cells = moduleDef['cells'] as Map>?; + if (cells == null || cells.isEmpty) { + continue; + } + + final (:wireDriverCell, :wireConsumerCells, :bitToNetInfo) = + NetlistUtils.buildWireMaps(cells, moduleDef); + + final cellsToRemove = {}; + + for (final packEntry in cells.entries.toList()) { + final packName = packEntry.key; + final packCell = packEntry.value; + if ((packCell['type'] as String?) != r'$struct_pack') { + continue; + } + + final conns = packCell['connections'] as Map? ?? {}; + final dirs = packCell['port_directions'] as Map? ?? {}; + + for (final portName in conns.keys.toList()) { + if (dirs[portName] != 'input') { + continue; + } + final bits = [ + for (final b in conns[portName] as List) + if (b is int) b, + ]; + if (bits.isEmpty) { + continue; + } + + // Trace each bit backward through $buf/$slice chains. + final tracedBits = []; + final intermediates = {}; + var allTraceToUnpack = true; + String? unpackName; + + for (final bit in bits) { + final (traced, chain) = + NetlistUtils.traceBackward(bit, wireDriverCell, cells); + intermediates.addAll(chain); + + // Check if traced bit is driven by a $struct_unpack. + final driverName = wireDriverCell[traced]; + if (driverName == null) { + allTraceToUnpack = false; + break; + } + final driverCell = cells[driverName]; + if (driverCell == null || + (driverCell['type'] as String?) != r'$struct_unpack') { + allTraceToUnpack = false; + break; + } + + if (unpackName == null) { + unpackName = driverName; + } else if (unpackName != driverName) { + allTraceToUnpack = false; + break; + } + + tracedBits.add(traced); + } + + if (!allTraceToUnpack || intermediates.isEmpty) { + continue; + } + + // Only remove $buf/$slice intermediates (not the unpack itself). + final removable = intermediates.where((c) { + final ct = cells[c]?['type'] as String?; + return ct == r'$buf' || ct == r'$slice'; + }).toSet(); + + if (removable.isEmpty) { + continue; + } + + // Verify exclusivity. + if (!NetlistUtils.isExclusiveChain( + intermediates: removable, + ownerCell: packName, + cells: cells, + wireConsumerCells: wireConsumerCells, + )) { + continue; + } + + // Rewire: replace the pack's input port with the traced bits. + conns[portName] = tracedBits.cast().toList(); + cellsToRemove.addAll(removable); + } + } + + cellsToRemove.forEach(cells.remove); + } + } +} diff --git a/lib/src/synthesizers/netlist/netlist_service.dart b/lib/src/synthesizers/netlist/netlist_service.dart new file mode 100644 index 000000000..32b164d94 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_service.dart @@ -0,0 +1,223 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_service.dart +// Service wrapper for netlist synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; + +/// A service that wraps netlist (Yosys JSON) synthesis of a [Module] +/// hierarchy. +/// +/// Provides access to the full hierarchy JSON and per-module JSON with +/// lazy caching, and optionally registers with [ModuleServices] for +/// DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final netlist = await NetlistService.create(dut); +/// +/// // Full hierarchy JSON: +/// print(netlist.toJson()); +/// +/// // Single module (lazy, cached): +/// print(netlist.moduleJson('FilterChannel')); +/// ``` +class NetlistService { + /// The top-level [Module] being synthesized. + final Module module; + + /// The [NetlistSynthesizer] used for synthesis. + final NetlistSynthesizer synthesizer; + + /// The underlying [SynthBuilder]. + late final SynthBuilder synthBuilder; + + /// The combined JSON string for the full hierarchy. + late final String _fullJson; + + /// Cached per-module JSON, keyed by definition name. + final Map _moduleJsonCache = {}; + + /// The parsed modules map from the combined JSON. + late final Map _modulesMap; + + NetlistService._(this.module, this.synthesizer, this._fullJson) { + final decoded = jsonDecode(_fullJson) as Map; + _modulesMap = + (decoded['modules'] as Map?) ?? {}; + } + + /// Creates a [NetlistService] for [module]. + /// + /// [module] must already be built. Set [register] to `true` (the + /// default) to register this service with [ModuleServices] for + /// DevTools access. + /// + /// The [options] parameter controls netlist synthesis behaviour; + /// see [NetlistOptions] for details. + static Future create( + Module module, { + NetlistOptions options = const NetlistOptions(), + bool register = true, + }) async { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating NetlistService. ' + 'Call build() first.'); + } + + final synthesizer = NetlistSynthesizer(options: options); + final json = await synthesizer.synthesizeToJson(module); + + final service = NetlistService._(module, synthesizer, json); + + if (register) { + ModuleServices.instance.netlistService = service; + } + + return service; + } + + /// Returns the full netlist hierarchy as a JSON string. + String toJson() => _fullJson; + + /// Returns the netlist JSON for a single module [definitionName]. + /// + /// If the module is not found, returns a JSON error object. + String moduleJson(String definitionName) => + _moduleJsonCache.putIfAbsent(definitionName, () { + final modData = _modulesMap[definitionName]; + if (modData == null) { + return jsonEncode({ + 'status': 'not_found', + 'reason': 'module "$definitionName" not in netlist', + }); + } + return jsonEncode({ + 'creator': 'ROHD netlist synthesizer', + 'modules': {definitionName: modData}, + }); + }); + + /// Returns the set of module definition names in the netlist. + Set get moduleNames => _modulesMap.keys.toSet(); + + /// Read-only access to the parsed modules map. + /// + /// Each key is a definition name and each value is the Yosys-style + /// module descriptor containing `ports`, `cells`, and `netnames`. + Map get synthesizedModules => + Map.unmodifiable(_modulesMap); + + /// Cached slim JSON (lazy). + String? _slimJsonCache; + + /// Returns a slim netlist JSON string — same structure as [toJson] but + /// with cell `connections` stripped. + /// + /// The slim representation preserves ports, cells (type + port_directions + /// + port_widths), and netnames so the DevTools extension can render the + /// hierarchy and signal tree without the full connectivity payload. + /// Full per-module connectivity is fetched on demand via [moduleJson]. + String get slimJson => _slimJsonCache ??= _buildSlimJson(); + + String _buildSlimJson() { + final slimModules = {}; + for (final entry in _modulesMap.entries) { + final mod = entry.value as Map; + final cells = mod['cells'] as Map? ?? {}; + final slimCells = {}; + for (final cellEntry in cells.entries) { + final cell = cellEntry.value as Map; + // Compute per-port widths from connections (bit-array lengths). + final conns = cell['connections'] as Map?; + final portWidths = {}; + if (conns != null) { + for (final c in conns.entries) { + final bits = c.value; + if (bits is List) { + portWidths[c.key] = bits.length; + } + } + } + slimCells[cellEntry.key] = { + 'hide_name': cell['hide_name'] ?? 0, + 'type': cell['type'], + 'parameters': cell['parameters'] ?? {}, + 'attributes': cell['attributes'] ?? {}, + 'port_directions': cell['port_directions'] ?? {}, + if (portWidths.isNotEmpty) 'port_widths': portWidths, + // connections intentionally omitted → slim + }; + } + + // Determine which module-level ports have internal connectivity. + final ports = mod['ports'] as Map? ?? {}; + final slimPorts = {}; + final cellConnectedBits = {}; + for (final cellEntry in cells.values) { + final cell = cellEntry as Map; + final conns = cell['connections'] as Map?; + if (conns == null) { + continue; + } + for (final bits in conns.values) { + if (bits is List) { + for (final b in bits) { + if (b is int) { + cellConnectedBits.add(b); + } + } + } + } + } + for (final portEntry in ports.entries) { + final portData = portEntry.value as Map; + final bits = portData['bits'] as List?; + var connected = false; + if (bits != null) { + for (final b in bits) { + if (b is int && cellConnectedBits.contains(b)) { + connected = true; + break; + } + } + } + slimPorts[portEntry.key] = { + ...portData, + if (connected) 'connected': true, + }; + } + + final netnames = mod['netnames'] as Map? ?? {}; + + slimModules[entry.key] = { + 'attributes': { + ...(mod['attributes'] as Map? ?? {}), + 'original_signal_count': netnames.length, + 'original_cell_count': slimCells.length, + }, + 'ports': slimPorts, + 'cells': slimCells, + 'netnames': netnames, + }; + } + + final rootName = module.hasBuilt ? module.uniqueInstanceName : module.name; + + return jsonEncode({ + 'netlist': { + 'creator': 'ROHD NetlistService (slim)', + 'rootInstanceName': rootName, + 'modules': slimModules, + }, + }); + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesis_result.dart b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart new file mode 100644 index 000000000..aaa7d8f6e --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesis_result.dart @@ -0,0 +1,84 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesis_result.dart +// A simple SynthesisResult that holds netlist data for one module. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; + +/// A [SynthesisResult] that holds the netlist representation of a single +/// module level: its ports, cells, and netnames. +class NetlistSynthesisResult extends SynthesisResult { + /// The ports map: name → {direction, bits}. + final Map> ports; + + /// The cells map: instance name → cell data. + final Map> cells; + + /// The netnames map: net name → {bits, attributes}. + final Map netnames; + + /// Attributes for this module (e.g., top marker). + final Map attributes; + + /// Cached JSON string for comparison and output. + late final String _cachedJson = _buildJson(); + + /// Creates a [NetlistSynthesisResult] for [module]. + NetlistSynthesisResult( + super.module, + super.getInstanceTypeOfModule, { + required this.ports, + required this.cells, + required this.netnames, + this.attributes = const {}, + }); + + String _buildJson() { + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + return const JsonEncoder().convert(moduleEntry); + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is NetlistSynthesisResult && _cachedJson == other._cachedJson; + + @override + int get matchHashCode => _cachedJson.hashCode; + + @override + @Deprecated('Use `toSynthFileContents()` instead.') + String toFileContents() => toSynthFileContents().first.contents; + + @override + List toSynthFileContents() { + final typeName = instanceTypeName; + final moduleEntry = { + 'attributes': attributes, + 'ports': ports, + 'cells': cells, + 'netnames': netnames, + }; + final contents = const JsonEncoder.withIndent(' ').convert({ + 'creator': 'NetlistSynthesizer (rohd)', + 'modules': {typeName: moduleEntry}, + }); + return [ + SynthFileContents( + name: '$typeName.rohd.json', + description: 'netlist for $typeName', + contents: contents, + ), + ]; + } +} diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer.dart b/lib/src/synthesizers/netlist/netlist_synthesizer.dart new file mode 100644 index 000000000..a21760b15 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_synthesizer.dart @@ -0,0 +1,1788 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer.dart +// A netlist synthesizer built on [SynthModuleDefinition]. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; + +/// +/// Skips SystemVerilog-specific processing (chain collapsing, net connects, +/// inOut inline replacement) since netlist represents all sub-modules as +/// cells rather than inline assignment expressions. +class _NetlistSynthModuleDefinition extends SynthModuleDefinition { + _NetlistSynthModuleDefinition(Module module) : super(module) { + // Create explicit $slice cells for LogicArray input ports so the + // netlist shows select gates for element extraction rather than + // flat bit aliasing. + module.inputs.values + .whereType() + .forEach(_subsetReceiveArrayPort); + + // Same for LogicArray outputs on submodules (received into this scope). + module.subModules + .expand((sub) => sub.outputs.values) + .whereType() + .forEach(_subsetReceiveArrayPort); + + // Create explicit $concat cells for internal LogicArrays whose elements + // are driven independently (e.g. by constants) and then consumed by + // submodule input ports. This parallels what _subsetReceiveArrayPort does + // on the decomposition side. + // + // Skip arrays that were merged with a port array's SynthLogic — those + // are already structurally decomposed by the $slice cells created above + // and reassembling them would create a circular driver on the port bus. + final portArrays = { + ...module.inputs.values.whereType(), + ...module.outputs.values.whereType(), + ...module.inOuts.values.whereType(), + }; + final portArraySynthLogics = {}; + for (final pa in portArrays) { + final sl = logicToSynthMap[pa]; + if (sl != null) { + portArraySynthLogics.add(sl.replacement ?? sl); + } + } + module.internalSignals.whereType().where((sig) { + if (portArrays.contains(sig)) { + return false; + } + final sl = logicToSynthMap[sig]; + if (sl == null) { + return false; + } + final resolved = sl.replacement ?? sl; + return !portArraySynthLogics.contains(resolved); + }).forEach(_concatAssembleArray); + } + + /// Creates explicit `$slice` cells for each element of a [LogicArray] port. + /// + /// Each element gets a [_BusSubsetForArraySlice] that extracts its bit range + /// from the packed parent bus. This produces explicit select gates in the + /// netlist, making array decomposition visible and traceable. + void _subsetReceiveArrayPort(LogicArray port) { + final portSynth = getSynthLogic(port)!; + + var idx = 0; + for (final element in port.elements) { + final elemSynth = getSynthLogic(element)!; + internalSignals.add(elemSynth); + + final subsetMod = _BusSubsetForArraySlice( + Logic(width: port.width, name: 'DUMMY'), + idx, + idx + element.width - 1, + ); + + getSynthSubModuleInstantiation(subsetMod) + ..setOutputMapping(subsetMod.subset.name, elemSynth) + ..setInputMapping(subsetMod.original.name, portSynth) + + // Pick a name now — this may be called after _pickNames() has run. + ..pickName(module); + + idx += element.width; + } + } + + /// Creates an explicit `$concat` cell that assembles a [LogicArray]'s + /// elements into the full packed array bus. + /// + /// This is the assembly counterpart to [_subsetReceiveArrayPort]: when + /// individual array elements are driven independently (e.g. by constants), + /// this makes the concatenation explicit as a visible gate in the netlist. + void _concatAssembleArray(LogicArray array) { + final arraySynth = getSynthLogic(array)!; + + // Build dummy signals matching each element's width. + final dummyElements = []; + for (final element in array.elements) { + dummyElements.add(Logic(width: element.width, name: 'DUMMY')); + } + + // Pass reversed dummies so that Swizzle's internal reversal cancels out, + // leaving in0 aligned with element[0] (LSB) and inN with element[N]. + final concatMod = _SwizzleForArrayConcat(dummyElements.reversed.toList()); + + final ssmi = getSynthSubModuleInstantiation(concatMod) + // Map the concat output to the full array. + ..setOutputMapping(concatMod.out.name, arraySynth); + + // Map each element input. + // Because we reversed dummies above, in0 corresponds to element[0], + // in1 to element[1], etc. + for (var i = 0; i < array.elements.length; i++) { + final elemSynth = getSynthLogic(array.elements[i])!; + internalSignals.add(elemSynth); + final inputName = concatMod.inputs.keys.elementAt(i); + ssmi.setInputMapping(inputName, elemSynth); + } + + // Pick a name now — this may be called after _pickNames() has run. + ssmi.pickName(module); + } + + @override + void process() { + // No SV-specific transformations -- we want every sub-module to remain + // as a cell in the JSON. + } +} + +/// A simple [Synthesizer] that produces netlist-compatible JSON. +/// +/// Leverages [SynthModuleDefinition] for signal tracing, naming, and +/// constant resolution, then maps the resulting [SynthLogic]s to integer +/// wire-bit IDs for netlist JSON output. +/// +/// Leaf modules (those with no sub-modules, or special cases like [FlipFlop]) +/// do *not* get their own module definition -- they appear only as cells +/// inside their parent. +/// +/// Usage: +/// ```dart +/// const options = NetlistOptions(groupStructConversions: true); +/// final synth = NetlistSynthesizer(options: options); +/// final builder = SynthBuilder(topModule, synth); +/// final json = await synth.synthesizeToJson(topModule); +/// ``` +class NetlistSynthesizer extends Synthesizer { + /// The configuration options controlling netlist synthesis. + /// + /// See [NetlistOptions] for documentation on individual fields. + final NetlistOptions options; + + /// Convenience accessor for the leaf-cell mapper. + LeafCellMapper get leafCellMapper => + options.leafCellMapper ?? LeafCellMapper.defaultMapper; + + /// Creates a [NetlistSynthesizer]. + /// + /// All synthesis parameters are bundled in [options]; see + /// [NetlistOptions] for documentation on each field. + NetlistSynthesizer({this.options = const NetlistOptions()}); + + @override + bool generatesDefinition(Module module) => + // Only modules with sub-modules generate their own module definition. + // Leaf modules (no children) become cells inside their parent. + // FlipFlop has internal Sequential sub-modules but should be emitted as + // a flat Yosys $dff primitive, not as a hierarchical module. + module is! FlipFlop && module.subModules.isNotEmpty; + + @override + SynthesisResult synthesize( + Module module, + String Function(Module module) getInstanceTypeOfModule, { + SynthesisResult? Function(Module module)? lookupExistingResult, + Map? existingResults, + }) { + final isTop = module.parent == null; + final attr = {'src': 'generated'}; + if (isTop) { + attr['top'] = 1; + } + + // -- Build SynthModuleDefinition ------------------------------------ + // This does all signal tracing, naming, constant handling, + // assignment collapsing, and unused signal pruning. + final canBuildSynthDef = !(module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none); + final synthDef = + canBuildSynthDef ? _NetlistSynthModuleDefinition(module) : null; + + // -- Wire-ID allocation --------------------------------------------- + // Start wire IDs at 2 to avoid collision with Yosys constant string + // bits "0" and "1". JavaScript viewers coerce object keys to strings, + // so integer wire ID 0 becomes "0", clashing with the constant-bit + // string "0". + var nextId = 2; + + // Map from SynthLogic -> assigned wire-bit IDs. + final synthLogicIds = >{}; + + /// Allocate or retrieve wire IDs for a [SynthLogic]. + /// For constants, do NOT follow the replacement chain to ensure each + /// constant usage gets its own separate driver cell in netlist. + List getIds(SynthLogic sl) { + var resolved = sl; + // For non-constants, follow replacement chain to resolve merged logics. + // For constants, keep them separate to create distinct const drivers. + if (!sl.isConstant) { + resolved = NetlistUtils.resolveReplacement(resolved); + } + final ids = synthLogicIds.putIfAbsent( + resolved, () => List.generate(resolved.width, (_) => nextId++)); + return ids; + } + + // -- Ports ----------------------------------------------------------- + final ports = >{}; + + final portGroups = [ + ('input', synthDef?.inputs, module.inputs), + ('output', synthDef?.outputs, module.outputs), + ('inout', synthDef?.inOuts, module.inOuts), + ]; + for (final (direction, synthLogics, modulePorts) in portGroups) { + if (synthLogics != null) { + for (final sl in synthLogics) { + final ids = getIds(sl); + final portName = NetlistUtils.portNameForSynthLogic(sl, modulePorts); + if (portName != null) { + ports[portName] = {'direction': direction, 'bits': ids}; + } + } + } else { + for (final entry in modulePorts.entries) { + final ids = List.generate(entry.value.width, (_) => nextId++); + ports[entry.key] = {'direction': direction, 'bits': ids}; + } + } + } + + // -- Pre-allocate IDs for internal signals in Module order ----------- + // This ensures that internals get IDs in the same order as + // Module.internalSignals, matching WaveformService._collectSignals. + // Signals already allocated during the port phase are skipped by + // putIfAbsent. Synthesis-generated wires get IDs later (during cell + // emission), so they are naturally appended after internals. + // + // Three-tier ordering guarantee: + // Tier 0 (ports): inputs → outputs → inOuts [above] + // Tier 1 (internals): module.internalSignals [here] + // Tier 2 (synth): cell emission wires [below] + if (synthDef != null) { + module.internalSignals + .map((sig) => synthDef.logicToSynthMap[sig]) + .whereType() + .where((sl) => !sl.isConstant) + .forEach(getIds); + } + + // -- Cell emission --------------------------------------------------- + final cells = >{}; + + // Track constant SynthLogics consumed exclusively by + // Combinational/Sequential so we can suppress their driver cells. + final blockedConstSynthLogics = {}; + + // Track emitted cell keys per instance for purging later. + final emittedCellKeys = {}; + + if (synthDef != null) { + for (final instance in synthDef.subModuleInstantiations) { + if (!instance.needsInstantiation) { + continue; + } + + final sub = instance.module; + + final isLeaf = !generatesDefinition(sub); + final defaultCellType = + isLeaf ? sub.definitionName : getInstanceTypeOfModule(sub); + + // Build port directions and connections from instance mappings. + final rawPortDirs = {}; + final rawConnections = >{}; + + for (final (dir, mapping) in [ + ('input', instance.inputMapping), + ('output', instance.outputMapping), + ('inout', instance.inOutMapping), + ]) { + for (final e in mapping.entries) { + rawPortDirs[e.key] = dir; + final ids = getIds(e.value); + rawConnections[e.key] = ids.cast(); + } + } + + // Map leaf cells to Yosys primitive types where possible. + final mapped = isLeaf + ? leafCellMapper.map(sub, rawPortDirs, rawConnections) + : null; + + final cellPortDirs = mapped?.portDirs ?? rawPortDirs; + final cellConns = mapped?.connections ?? rawConnections; + + // Use the SSMI's uniquified name as cell key to avoid + // collisions between identically-named modules (e.g. multiple + // struct_slice instances that share the same Module.name). + final cellKey = instance.name; + emittedCellKeys[instance] = cellKey; + + // -- Collapse bit-slice ports on Combinational / Sequential ---- + if (sub is Combinational || sub is Sequential) { + NetlistUtils.collapseAlwaysBlockPorts( + synthDef, + instance, + cellPortDirs, + cellConns, + getIds, + ); + } + + // -- Filter constant inputs from Combinational / Sequential ---- + if (sub is Combinational || sub is Sequential) { + final portsToRemove = []; + for (final pe in cellConns.entries) { + final portName = pe.key; + final synthLogic = instance.inputMapping[portName] ?? + instance.inOutMapping[portName]; + if (synthLogic != null && + NetlistUtils.isConstantSynthLogic(synthLogic)) { + portsToRemove.add(portName); + blockedConstSynthLogics.add(synthLogic.replacement ?? synthLogic); + } + } + for (final p in portsToRemove) { + cellConns.remove(p); + cellPortDirs.remove(p); + } + } + + // -- Rename Seq/Comb ports to Namer wire names ----------------- + // The port names from _Always.addInput/addOutput are internal + // (e.g. `_out`, `_enable`). Replace them with the Namer's + // resolved wire name so they match SystemVerilog and WaveDumper. + if (sub is Combinational || sub is Sequential) { + final renames = {}; + for (final portName in cellConns.keys.toList()) { + final sl = instance.inputMapping[portName] ?? + instance.outputMapping[portName] ?? + instance.inOutMapping[portName]; + if (sl == null) { + continue; // aggregated port, already renamed + } + final resolved = NetlistUtils.resolveReplacement(sl); + final namerName = NetlistUtils.tryGetSynthLogicName(resolved); + if (namerName != null && namerName != portName) { + renames[portName] = namerName; + } + } + for (final entry in renames.entries) { + final bits = cellConns.remove(entry.key)!; + final dir = cellPortDirs.remove(entry.key)!; + var newName = entry.value; + // Avoid collision with existing port names. + if (cellConns.containsKey(newName)) { + newName = '${entry.value}_${entry.key}'; + } + cellConns[newName] = bits; + cellPortDirs[newName] = dir; + } + } + + cells[cellKey] = { + 'hide_name': 0, + 'type': mapped?.cellType ?? defaultCellType, + 'parameters': mapped?.parameters ?? {}, + 'attributes': {}, + 'port_directions': cellPortDirs, + 'connections': cellConns, + }; + } + } + + // -- Remove cells that were cleared by collapseAlwaysBlockPorts ------ + // Because the iteration order may process a Swizzle/BusSubset cell + // BEFORE the Combinational/Sequential that clears it, we need to purge + // stale cells after all collapsing has been applied. + if (synthDef != null) { + synthDef.subModuleInstantiations + .where((i) => !i.needsInstantiation) + .map((i) => emittedCellKeys[i]) + .whereType() + .forEach(cells.remove); + } + + // -- Wire-ID aliasing from remaining assignments ------------------- + // SynthModuleDefinition._collapseAssignments may leave assignments + // between non-mergeable SynthLogics (e.g., reserved port + + // renameable internal signal). In SV synthesis these become + // `assign` statements. In netlist we need the two sides to + // share wire IDs so that the netlist is properly connected. + // + // Similarly, PartialSynthAssignments for output struct ports tell + // us which leaf-field IDs should compose the port's bits, and + // input-struct BusSubsets (which may be pruned) tell us which + // leaf-field IDs should be carved from the port's bits. + final idAlias = {}; + + // Pending $struct_field cells collected during Step 3. + // Each entry records a single field extraction from a parent struct. + // The `parentLogic` and `fullParentIds` fields are used to group + // entries from the same LogicStructure into a single multi-port + // `$struct_unpack` cell. + final structFieldCells = <({ + List parentIds, + List elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds, + })>[]; + + // Pending $struct_compose cells: for output struct ports, instead of + // aliasing port bits to leaf bits (which causes "shorting"), we + // collect composition operations and emit explicit cells later. + // Each entry records: field (src) → port sub-range [lower:upper]. + final structComposeCells = <({ + List srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + })>[]; + + // Track struct ports (both output ports of the current module AND + // sub-module input struct ports) so Step 3 can skip $struct_field + // collection for them ($struct_pack handles these instead). + final outputStructPortLogics = {}; + + if (synthDef != null) { + // 1. Non-partial assignments: src drives dst → dst IDs become + // src IDs (the driver's IDs are canonical). + for (final assignment + in synthDef.assignments.where((a) => a is! PartialSynthAssignment)) { + final srcIds = getIds(assignment.src); + final dstIds = getIds(assignment.dst); + final len = + srcIds.length < dstIds.length ? srcIds.length : dstIds.length; + for (var i = 0; i < len; i++) { + if (dstIds[i] != srcIds[i]) { + idAlias[dstIds[i]] = srcIds[i]; + } + } + } + + // 2. Partial assignments (output / sub-module struct ports): + // src → dst[lower:upper]. The port-slice IDs become the + // leaf's IDs so that the port is composed from its fields. + // + // For struct ports (both output ports of the current module + // AND sub-module input struct ports), we keep distinct port + // and field IDs and instead collect pending $struct_pack + // cells. This avoids "shorting" where field wires are + // aliased directly to port bits, which creates multi-driver + // conflicts with $struct_unpack cells emitted in Step 3. + // + // For non-struct sub-module input ports, we alias as before. + + /// Recursively add [struct] and all its nested [LogicStructure] + /// descendants (excluding [LogicArray]) to [set]. + void addStructAndDescendants(LogicStructure struct, Set set) { + set.add(struct); + for (final elem in struct.elements) { + if (elem is LogicStructure && elem is! LogicArray) { + addStructAndDescendants(elem, set); + } + } + } + + for (final pa + in synthDef.assignments.whereType()) { + final srcIds = getIds(pa.src); + final dstIds = getIds(pa.dst); + + // Detect: is pa.dst an output struct port of the current module? + final isCurrentModuleOutputPort = + pa.dst.isPort(module) && pa.dst.logics.any((l) => l.isOutput); + + // Detect: is pa.dst a sub-module input struct port? + // (LogicStructure but not LogicArray, and not an output of the + // current module.) + final isSubModuleInputStructPort = !isCurrentModuleOutputPort && + pa.dst.logics.any((l) => l is LogicStructure && l is! LogicArray); + + if (isCurrentModuleOutputPort || isSubModuleInputStructPort) { + // Record as pending compose cell instead of aliasing. + structComposeCells.add(( + srcIds: srcIds, + dstIds: dstIds, + dstLowerIndex: pa.dstLowerIndex, + dstUpperIndex: pa.dstUpperIndex, + srcSynthLogic: pa.src, + dstSynthLogic: pa.dst, + )); + // Track the Logic (and nested structs) so Step 3 skips + // $struct_unpack for them. + for (final l in pa.dst.logics) { + if (l is LogicStructure && l is! LogicArray) { + addStructAndDescendants(l, outputStructPortLogics); + } + } + } else { + // Non-struct sub-module input port: alias as before. + for (var i = 0; i < srcIds.length; i++) { + final dstIdx = pa.dstLowerIndex + i; + if (dstIdx < dstIds.length && dstIds[dstIdx] != srcIds[i]) { + idAlias[dstIds[dstIdx]] = srcIds[i]; + } + } + } + } + + // 3. LogicStructure and LogicArray: child IDs → parent-slice IDs. + // + // LogicArray elements alias their IDs to matching parent bits + // so array connectivity works. + // + // Non-array LogicStructure elements are NOT aliased. Instead, + // their parent→element mappings are collected in + // [structFieldCells] and emitted as explicit $struct_field + // cells after alias resolution. This preserves element signals + // (e.g. "a_mantissa") as distinct named wires visible in the + // schematic, rather than collapsing them into parent bit ranges. + // + // For arrays with explicit $slice/$concat cells (from + // _BusSubsetForArraySlice / _SwizzleForArrayConcat), aliasing + // is skipped entirely — the cells provide the structural link. + // + // Applied to ALL instances (ports AND internal signals) since + // internal arrays/structs (e.g. constant-driven coefficients) + // also need child→parent aliasing. + // + // - LogicStructure (non-array): walks leafElements (recursive) + // - LogicArray: walks elements (direct children only, since + // each element is already a flat bitvector). + // For input array ports that have _BusSubsetForArraySlice + // cells, we skip aliasing so the $slice cells provide the + // structural connection (see _subsetReceiveArrayPort). + // + // When a child ID was already aliased (e.g. by step 1 to a + // constant driver), we also redirect that prior target to the + // parent ID so the transitive chain resolves correctly: + // constId → childId → parentId. + void aliasChildToParent(int childId, int parentId) { + if (childId == parentId) { + return; + } + // If childId already aliases somewhere (e.g. constId → childId + // was set in step 1 as childId → constId), redirect that old + // target to parentId as well, so constId → parentId. + final existing = idAlias[childId]; + if (existing != null && existing != parentId) { + idAlias[existing] = parentId; + } + idAlias[childId] = parentId; + } + + // Collect LogicArray ports that have explicit array_slice or + // array_concat submodules so we can skip aliasing them (the + // $slice/$concat cells provide the structural link). + final arraysWithExplicitCells = {}; + for (final inst in synthDef.subModuleInstantiations) { + if (inst.module is _BusSubsetForArraySlice) { + // The input of the BusSubset is the array port. + for (final inputSL in inst.inputMapping.values) { + final logic = synthDef.logicToSynthMap.entries + .where( + (e) => e.value == inputSL || e.value.replacement == inputSL) + .map((e) => e.key) + .firstOrNull; + if (logic != null && logic is LogicArray) { + arraysWithExplicitCells.add(logic); + } + // Also check the resolved replacement chain. + final resolved = NetlistUtils.resolveReplacement(inputSL); + final logic2 = synthDef.logicToSynthMap.entries + .where((e) => e.value == resolved) + .map((e) => e.key) + .firstOrNull; + if (logic2 != null && logic2 is LogicArray) { + arraysWithExplicitCells.add(logic2); + } + } + } + if (inst.module is _SwizzleForArrayConcat) { + // The output of the Swizzle is the array signal. + for (final outputSL in inst.outputMapping.values) { + final logic = synthDef.logicToSynthMap.entries + .where((e) => + e.value == outputSL || e.value.replacement == outputSL) + .map((e) => e.key) + .firstOrNull; + if (logic != null && logic is LogicArray) { + arraysWithExplicitCells.add(logic); + } + } + } + } + + for (final entry in synthDef.logicToSynthMap.entries) { + final logic = entry.key; + if (logic is! LogicStructure) { + continue; + } + final parentSL = entry.value; + final parentIds = getIds(parentSL); + + if (logic is LogicArray) { + // Skip aliasing for arrays that have explicit $slice/$concat cells. + if (arraysWithExplicitCells.contains(logic)) { + continue; + } + // Array: alias each element's IDs to matching parent slice. + var idx = 0; + for (final element in logic.elements) { + final elemSL = synthDef.logicToSynthMap[element]; + if (elemSL != null) { + final elemIds = getIds(elemSL); + for (var i = 0; + i < elemIds.length && idx + i < parentIds.length; + i++) { + aliasChildToParent(elemIds[i], parentIds[idx + i]); + } + } + idx += element.width; + } + } else { + // Struct: collect element→parent mappings for $struct_field + // cell emission instead of aliasing. This preserves named + // field signals as distinct wires connected through explicit + // cells, making them visible in the schematic and evaluable + // by the netlist evaluator. + // + // Skip output struct ports of the current module — those are + // handled by $struct_compose cells (from Step 2). + if (outputStructPortLogics.contains(logic)) { + continue; + } + var idx = 0; + for (final elem in logic.elements) { + final elemSL = synthDef.logicToSynthMap[elem]; + if (elemSL != null) { + final elemIds = getIds(elemSL); + final sliceLen = elemIds.length < parentIds.length - idx + ? elemIds.length + : parentIds.length - idx; + if (sliceLen > 0) { + structFieldCells.add(( + parentIds: parentIds.sublist(idx, idx + sliceLen), + elemIds: elemIds.sublist(0, sliceLen), + offset: idx, + width: sliceLen, + elemLogic: elem, + parentLogic: logic, + fullParentIds: parentIds, + )); + } + } + idx += elem.width; + } + } + } + } + + // Transitively resolve an alias chain to its canonical ID. + // Uses a visited set to detect cycles created by conflicting + // child→parent and assignment aliasing directions. + int resolveAlias(int id) { + var resolved = id; + final visited = {}; + while (idAlias.containsKey(resolved)) { + if (!visited.add(resolved)) { + // Cycle detected — break the cycle by removing this entry. + idAlias.remove(resolved); + break; + } + resolved = idAlias[resolved]!; + } + return resolved; + } + + // Apply aliases to a list of bit IDs / string constants. + List applyAlias(List bits) => idAlias.isEmpty + ? bits + : bits.map((b) => b is int ? resolveAlias(b) : b).toList(); + + // Alias port bits. + if (idAlias.isNotEmpty) { + for (final p in ports.values) { + p['bits'] = applyAlias((p['bits']! as List).cast()); + } + // Alias cell connections. + for (final c in cells.values) { + final conns = c['connections']! as Map; + for (final key in conns.keys.toList()) { + conns[key] = applyAlias((conns[key] as List).cast()); + } + } + + // -- Elide trivial $slice cells ---------------------------------- + // Also elide struct_slice cells (`_BusSubsetForStructSlice` + // instances from `_subsetReceiveStructPort`) because the new + // `$struct_unpack` cells emitted below supersede them with + // better-named field-level connections. + cells.removeWhere((cellKey, cell) { + if (cell['type'] != r'$slice') { + return false; + } + // Unconditionally remove struct_slice cells — they are + // duplicated by $struct_unpack cells which carry field names. + if (cellKey.startsWith('struct_slice')) { + return true; + } + final params = cell['parameters'] as Map?; + final offset = params?['OFFSET']; + if (offset is! int) { + return false; + } + final conns = cell['connections']! as Map; + final aBits = conns['A'] as List?; + final yBits = conns['Y'] as List?; + if (aBits == null || yBits == null) { + return false; + } + return yBits.indexed.every((e) => + offset + e.$1 < aBits.length && e.$2 == aBits[offset + e.$1]); + }); + } + + // -- Emit $struct_unpack cells for LogicStructure elements ---------- + // Group per-field entries by their parent LogicStructure and emit a + // single multi-port cell per group. Each group has: + // • input port A: the full parent bus (packed bitvector) + // • one output port per non-trivial field: bits for that field + // This replaces the old per-field $struct_field cells. + if (synthDef != null && structFieldCells.isNotEmpty) { + // Group by parent Logic identity. + final groups = parentIds, + List elemIds, + int offset, + int width, + Logic elemLogic, + Logic parentLogic, + List fullParentIds, + })>>{}; + for (final sf in structFieldCells) { + (groups[sf.parentLogic] ??= []).add(sf); + } + + var suIdx = 0; + for (final entry in groups.entries) { + final parentLogic = entry.key; + final fields = entry.value; + final fullParentIds = fields.first.fullParentIds; + final resolvedParentBits = applyAlias(fullParentIds.cast()); + + // Filter out trivial fields (input slice == output after aliasing). + final nonTrivialFields = fields + .map((sf) { + final resolvedElemBits = applyAlias(sf.elemIds.cast()); + return ( + resolvedElemBits: resolvedElemBits, + offset: sf.offset, + width: sf.width, + elemLogic: sf.elemLogic, + ); + }) + .where((f) => !f.resolvedElemBits.indexed.every((e) { + final (i, bit) = e; + return f.offset + i < resolvedParentBits.length && + bit == resolvedParentBits[f.offset + i]; + })) + .toList(); + + if (nonTrivialFields.isEmpty) { + continue; + } + + // Derive struct name for the cell key. + final structName = Sanitizer.sanitizeSV(parentLogic.name); + + // Build element range table for the parent struct so we can + // derive proper field names even when the leaf Logic objects + // have unpreferred names like `_swizzled`. + // Same strategy as $struct_pack: walk the hierarchy collecting + // (start, end, name, path, indexInParent) and look up the + // narrowest non-unpreferred range for each field offset. + final suElementRanges = <({ + int start, + int end, + String name, + String path, + int indexInParent, + })>[]; + if (parentLogic is LogicStructure) { + void walkStruct( + LogicStructure struct, int baseOffset, String parentPath) { + var offset = baseOffset; + for (var idx = 0; idx < struct.elements.length; idx++) { + final elem = struct.elements[idx]; + final elemEnd = offset + elem.width; + final elemPath = + parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; + suElementRanges.add(( + start: offset, + end: elemEnd, + name: elem.name, + path: elemPath, + indexInParent: idx, + )); + if (elem is LogicStructure && elem is! LogicArray) { + walkStruct(elem, offset, elemPath); + } + offset = elemEnd; + } + } + + walkStruct(parentLogic, 0, ''); + } + + String suFieldNameFor(int fieldOffset, String fallbackName) { + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestNamed; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestAny; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? narrowest; + + for (final r in suElementRanges) { + if (fieldOffset >= r.start && fieldOffset < r.end) { + final span = r.end - r.start; + if (narrowest == null || + span < (narrowest.end - narrowest.start)) { + narrowest = r; + } + if (bestAny == null || span < (bestAny.end - bestAny.start)) { + bestAny = r; + } + if (!Naming.isUnpreferred(r.name)) { + if (bestNamed == null || + span < (bestNamed.end - bestNamed.start)) { + bestNamed = r; + } + } + } + } + + if (bestNamed != null) { + if (narrowest != null && + (narrowest.end - narrowest.start) < + (bestNamed.end - bestNamed.start)) { + final bestNamedPrefix = bestNamed.path; + if (narrowest.path.length > bestNamedPrefix.length && + narrowest.path.startsWith(bestNamedPrefix)) { + final suffix = + narrowest.path.substring(bestNamedPrefix.length + 1); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + // All matching elements have unpreferred names — use the + // narrowest element's positional index as discriminator. + if (narrowest != null && Naming.isUnpreferred(narrowest.name)) { + return 'anonymous_${narrowest.indexInParent}'; + } + return bestAny?.name ?? fallbackName; + } + + // Build port_directions and connections with one output per field. + final portDirs = {'A': 'input'}; + final conns = >{'A': resolvedParentBits}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = suFieldNameFor(f.offset, f.elemLogic.name); + // Disambiguate duplicate field names with index suffix. + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = 'output'; + conns[portName] = f.resolvedElemBits; + } + + // Parameters list field metadata for the schematic viewer. + final params = { + 'STRUCT_NAME': parentLogic.name, + 'FIELD_COUNT': nonTrivialFields.length, + }; + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + params['FIELD_${i}_NAME'] = + suFieldNameFor(f.offset, f.elemLogic.name); + params['FIELD_${i}_OFFSET'] = f.offset; + params['FIELD_${i}_WIDTH'] = f.width; + } + + cells['struct_unpack_${suIdx}_$structName'] = { + 'hide_name': 0, + 'type': r'$struct_unpack', + 'parameters': params, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': conns, + }; + suIdx++; + } + } + + // -- Emit $struct_pack cells for output struct ports ------------------ + // Group compose entries by destination port and emit a single + // multi-port cell per group. Each group has: + // • one input port per non-trivial field + // • output port Y: the full packed output bus + // This replaces the old per-field $struct_compose cells. + if (structComposeCells.isNotEmpty) { + // Group by destination SynthLogic identity. + final composeGroups = srcIds, + List dstIds, + int dstLowerIndex, + int dstUpperIndex, + SynthLogic srcSynthLogic, + SynthLogic dstSynthLogic, + })>>{}; + for (final sc in structComposeCells) { + (composeGroups[sc.dstSynthLogic] ??= []).add(sc); + } + + var spIdx = 0; + for (final entry in composeGroups.entries) { + final dstSynthLogic = entry.key; + final fields = entry.value; + final resolvedDstBits = applyAlias(fields.first.dstIds.cast()); + + // Filter out trivial fields. + final nonTrivialFields = fields + .map((sc) { + final resolvedSrcBits = applyAlias(sc.srcIds.cast()); + final yBits = resolvedDstBits.sublist( + sc.dstLowerIndex, sc.dstUpperIndex + 1); + return ( + resolvedSrcBits: resolvedSrcBits, + yBits: yBits, + dstLowerIndex: sc.dstLowerIndex, + dstUpperIndex: sc.dstUpperIndex, + srcSynthLogic: sc.srcSynthLogic, + ); + }) + .where((f) => !f.resolvedSrcBits + .take(f.yBits.length) + .indexed + .every((e) => e.$2 == f.yBits[e.$1])) + .toList(); + + if (nonTrivialFields.isEmpty) { + continue; + } + + // Derive struct name from the destination Logic. + final dstLogic = dstSynthLogic.logics.firstOrNull; + final structName = dstLogic != null + ? Sanitizer.sanitizeSV(dstLogic.name) + : 'struct_$spIdx'; + + // Build a lookup from bit offset to the best struct element + // name, so that field names come from the struct definition + // (e.g. "data", "last", "poison") rather than the source + // signal name (which may be an internal like "_swizzled"). + // + // Elements pack LSB-first via `rswizzle`, so element[0] + // starts at offset 0, element[1] at element[0].width, etc. + // + // We collect (start, end, name, path, parentElementIndex) + // ranges for every element at every nesting level. The + // `path` carries the chain of parent struct names so we can + // produce qualified names like "mmu_info_mmuSid". When + // leaf names are unpreferred, `parentElementIndex` provides + // a fallback discriminator like "mmu_info_0". + final dstElementRanges = <({ + int start, + int end, + String name, + String path, + int indexInParent, + })>[]; + if (dstLogic is LogicStructure) { + void walkStruct( + LogicStructure struct, int baseOffset, String parentPath) { + var offset = baseOffset; + for (var idx = 0; idx < struct.elements.length; idx++) { + final elem = struct.elements[idx]; + final elemEnd = offset + elem.width; + final elemPath = + parentPath.isEmpty ? elem.name : '${parentPath}_${elem.name}'; + dstElementRanges.add(( + start: offset, + end: elemEnd, + name: elem.name, + path: elemPath, + indexInParent: idx, + )); + if (elem is LogicStructure && elem is! LogicArray) { + walkStruct(elem, offset, elemPath); + } + offset = elemEnd; + } + } + + walkStruct(dstLogic, 0, ''); + } + + /// Look up the field name for a compose entry by finding the + /// best struct element whose range contains [dstLowerIndex]. + /// + /// Strategy (deepest-first): + /// 1. Find the narrowest element with a non-unpreferred name. + /// 2. If a narrower unpreferred leaf exists under a named + /// parent, try to qualify with the leaf's proper name + /// (e.g. `mmu_info_mmuSid`). + /// 3. If the leaf name is also unpreferred, fall back to the + /// parent name qualified by the leaf's positional index + /// (e.g. `mmu_info_0`, `mmu_info_1`). + /// 4. Falls back to the resolved source SynthLogic name. + String fieldNameFor( + int dstLowerIndex, + SynthLogic srcSynthLogic, + ) { + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestNamed; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? bestAny; + ({ + int start, + int end, + String name, + String path, + int indexInParent, + })? narrowest; + + for (final r in dstElementRanges) { + if (dstLowerIndex >= r.start && dstLowerIndex < r.end) { + final span = r.end - r.start; + if (narrowest == null || + span < (narrowest.end - narrowest.start)) { + narrowest = r; + } + if (bestAny == null || span < (bestAny.end - bestAny.start)) { + bestAny = r; + } + if (!Naming.isUnpreferred(r.name)) { + if (bestNamed == null || + span < (bestNamed.end - bestNamed.start)) { + bestNamed = r; + } + } + } + } + + if (bestNamed != null) { + // Check if there's a narrower child element under + // bestNamed that we can use to discriminate. + if (narrowest != null && + (narrowest.end - narrowest.start) < + (bestNamed.end - bestNamed.start)) { + final bestNamedPrefix = bestNamed.path; + // Try using the child's proper name as qualifier. + if (narrowest.path.length > bestNamedPrefix.length && + narrowest.path.startsWith(bestNamedPrefix)) { + final suffix = + narrowest.path.substring(bestNamedPrefix.length + 1); + if (!Naming.isUnpreferred(suffix)) { + return '${bestNamed.name}_$suffix'; + } + } + // Child has unpreferred name — use positional index. + return '${bestNamed.name}_${narrowest.indexInParent}'; + } + return bestNamed.name; + } + return bestAny?.name ?? + NetlistUtils.resolveReplacement(srcSynthLogic).name; + } + + // Build port_directions and connections. + final portDirs = {}; + final conns = >{}; + + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + final fieldName = fieldNameFor(f.dstLowerIndex, f.srcSynthLogic); + var portName = fieldName; + if (portDirs.containsKey(portName)) { + portName = '${fieldName}_$i'; + } + portDirs[portName] = 'input'; + conns[portName] = f.resolvedSrcBits; + } + + // Output port Y: full destination bus. + portDirs['Y'] = 'output'; + conns['Y'] = resolvedDstBits; + + // Parameters list field metadata for the schematic viewer. + final params = { + 'STRUCT_NAME': dstLogic?.name ?? 'struct', + 'FIELD_COUNT': nonTrivialFields.length, + }; + for (var i = 0; i < nonTrivialFields.length; i++) { + final f = nonTrivialFields[i]; + params['FIELD_${i}_NAME'] = + fieldNameFor(f.dstLowerIndex, f.srcSynthLogic); + params['FIELD_${i}_OFFSET'] = f.dstLowerIndex; + params['FIELD_${i}_WIDTH'] = f.dstUpperIndex - f.dstLowerIndex + 1; + } + + cells['struct_pack_${spIdx}_$structName'] = { + 'hide_name': 0, + 'type': r'$struct_pack', + 'parameters': params, + 'attributes': {}, + 'port_directions': portDirs, + 'connections': conns, + }; + spIdx++; + } + } + + // -- Passthrough buffer insertion ------------------------------------ + // When a signal passes directly from an input port to an output port, + // they share the same wire IDs after aliasing. This causes the signal + // to appear routed *around* the module in the netlist rather than + // *through* it. Insert a `$buf` cell to break the wire-ID sharing, + // giving the output port fresh IDs driven by the buffer. + { + final inputBitIds = ports.values + .where((p) => p['direction'] == 'input' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType() + .toSet(); + + // Check each output port for overlap with input bits. + var bufIdx = 0; + for (final p + in ports.entries.where((p) => p.value['direction'] == 'output')) { + final outBits = (p.value['bits']! as List).cast(); + if (!outBits.any((b) => b is int && inputBitIds.contains(b))) { + continue; + } + + // Allocate fresh wire IDs for the output side of the buffer. + final freshBits = + List.generate(outBits.length, (_) => nextId++); + + // Insert a $buf cell: input = original (shared) IDs, + // output = fresh IDs. + cells['passthrough_buf_$bufIdx'] = + NetlistUtils.makeBufCell(outBits.length, outBits, freshBits); + + // Update the output port to use the fresh IDs. + p.value['bits'] = freshBits; + bufIdx++; + } + } + + // -- Dead-cell elimination (DCE) ------------------------------------- + // After aliasing and elision, some cells may have inputs whose wire + // IDs are not driven by any cell output or module input port. This + // typically happens when a LogicStructure's `packed` representation + // creates a Swizzle chain whose inputs reference sub-module-internal + // signals that are not accessible from the synthesised module's + // scope. Iteratively remove such dead cells using both forward + // (all-inputs-undriven) and backward (all-outputs-unconsumed) DCE. + if (options.enableDCE) { + var dceChanged = true; + while (dceChanged) { + dceChanged = false; + + // Build set of driven wire IDs (from input/inout ports and cell + // outputs). + final drivenIds = { + ...ports.values + .where( + (p) => p['direction'] == 'input' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + // Build set of consumed wire IDs (from output/inout ports and + // cell inputs). + final consumedIds = { + ...ports.values + .where((p) => + p['direction'] == 'output' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + // Forward DCE: remove cells whose inputs are ALL undriven. + cells + ..removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + final inputPorts = + conns.entries.where((pe) => pdirs[pe.key] == 'input'); + if (inputPorts.isEmpty) { + return false; + } + final allUndriven = !inputPorts + .expand((pe) => pe.value as List) + .any((b) => (b is int && drivenIds.contains(b)) || b is String); + if (allUndriven) { + dceChanged = true; + return true; + } + return false; + }) + + // Backward DCE: remove cells whose outputs are ALL unconsumed. + // Preserve non-leaf cells (user module instances) — their type + // does not start with '$' (Yosys primitive convention). Users + // expect to see all instantiated modules in the schematic even + // when outputs are unconnected. + ..removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + final cellType = cell['type'] as String? ?? ''; + if (!cellType.startsWith(r'$')) { + return false; + } + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + final outputPorts = + conns.entries.where((pe) => pdirs[pe.key] == 'output'); + if (outputPorts.isEmpty) { + return false; + } + final allUnconsumed = !outputPorts + .expand((pe) => pe.value as List) + .whereType() + .any(consumedIds.contains); + if (allUnconsumed) { + dceChanged = true; + return true; + } + return false; + }); + } + } + + // -- Constant driver cells ------------------------------------------- + // Generated AFTER the aliasing pass so that constants discovered + // during aliasing (via getIds(assignment.src)) are included. + // Constant IDs may have been redirected by step 3 (struct/array + // child→parent aliasing), so apply alias resolution to their + // connection bits. + { + var constIdx = 0; + final emittedConstWires = {}; + for (final entry in synthLogicIds.entries + .where((e) => e.key.isConstant) + .where((e) => !blockedConstSynthLogics.contains(e.key)) + .where((e) => e.value.isNotEmpty)) { + final sl = entry.key; + final constValue = NetlistUtils.constValueFromSynthLogic(sl); + if (constValue == null) { + continue; + } + final ids = entry.value; + + // Resolve aliases and skip if these wires are already driven + // by a previously emitted $const cell (can happen when aliasing + // merges two SynthLogic constants onto the same wire IDs). + final resolvedIds = applyAlias(ids.cast()); + final firstWire = + resolvedIds.firstWhere((b) => b is int, orElse: () => -1); + if (firstWire is int && firstWire >= 0) { + if (emittedConstWires.contains(firstWire)) { + continue; + } + emittedConstWires.addAll(resolvedIds.whereType()); + } + + final valuePart = NetlistUtils.constValuePart(constValue); + final cellName = 'const_${constIdx}_$valuePart'; + final valueLiteral = valuePart.replaceFirst('_', "'"); + + cells[cellName] = { + 'hide_name': 0, + 'type': r'$const', + 'parameters': {}, + 'attributes': {}, + 'port_directions': {valueLiteral: 'output'}, + 'connections': >{ + valueLiteral: resolvedIds, + }, + }; + constIdx++; + } + } + + // -- Remove floating $const cells ------------------------------------ + // The $const cells were emitted after the main DCE pass, so they + // may reference wire IDs that no cell input or output port consumes. + if (options.enableDCE) { + final consumedByInputs = { + ...ports.values + .where( + (p) => p['direction'] == 'output' || p['direction'] == 'inout') + .expand((p) => p['bits']! as List) + .whereType(), + ...cells.values.expand((c) { + final cell = c as Map; + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return conns.entries + .where((pe) => pdirs[pe.key] == 'input') + .expand((pe) => pe.value as List) + .whereType(); + }), + }; + + cells.removeWhere((cellKey, cellVal) { + final cell = cellVal as Map; + if (cell['type'] != r'$const') { + return false; + } + final conns = cell['connections']! as Map; + final pdirs = cell['port_directions']! as Map; + return !conns.entries + .where((pe) => pdirs[pe.key] == 'output') + .expand((pe) => pe.value as List) + .whereType() + .any(consumedByInputs.contains); + }); + } + + // -- Break shared wire IDs for array_concat cells -------------------- + // After aliasing, the concat inputs share the same wire IDs as the + // concat Y output (because LogicArray elements share the parent's + // bit storage). This makes the concat transparent -- constants + // appear to drive the parent array directly. + // + // To fix: allocate fresh wire IDs for each concat input port, + // then redirect all other cells whose outputs used those old IDs + // to drive the fresh IDs instead. The concat Y output keeps the + // original parent-array IDs, so the data flow becomes: + // const → fresh_IDs → concat input → concat Y (= parent IDs) + final arrayConcatOldToNew = {}; + + for (final cellEntry in cells.entries) { + if (!cellEntry.key.startsWith('array_concat')) { + continue; + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'input') { + continue; + } + final oldBits = (portEntry.value as List).cast(); + conns[portEntry.key] = [ + for (final b in oldBits) + b is int ? arrayConcatOldToNew.putIfAbsent(b, () => nextId++) : b, + ]; + } + } + + // Redirect other cells: any output port bit that matches an old ID + // gets replaced with the corresponding fresh ID. + if (arrayConcatOldToNew.isNotEmpty) { + for (final cellEntry in cells.entries) { + if (cellEntry.key.startsWith('array_concat')) { + continue; // skip the concat cells themselves + } + final cell = cellEntry.value as Map; + final conns = cell['connections'] as Map; + final dirs = cell['port_directions'] as Map; + + for (final portEntry in conns.entries.toList()) { + if (dirs[portEntry.key] != 'output') { + continue; + } + final bits = (portEntry.value as List).cast(); + final newBits = [ + for (final b in bits) b is int ? (arrayConcatOldToNew[b] ?? b) : b, + ]; + if (bits.indexed.any((e) => e.$2 != newBits[e.$1])) { + conns[portEntry.key] = newBits; + } + } + } + } + + // -- Netnames -------------------------------------------------------- + final netnames = {}; + final emittedNames = {}; + + // InlineSystemVerilog modules are pure combinational — all their + // signals are derivable from the gate netlist. + final isInlineSV = module is InlineSystemVerilog; + + void addNetname(String name, List bits, + {bool hideName = false, bool computed = false}) { + if (emittedNames.contains(name)) { + return; + } + emittedNames.add(name); + netnames[name] = { + 'bits': bits, + if (hideName) 'hide_name': 1, + 'attributes': { + if (computed || isInlineSV) 'computed': 1, + }, + }; + } + + // Port nets (already aliased above). + for (final p in ports.entries) { + addNetname(Sanitizer.sanitizeSV(p.key), + (p.value['bits']! as List).cast()); + } + + // Named signals from SynthModuleDefinition. + if (synthDef != null) { + for (final entry in synthLogicIds.entries + .where((e) => !e.key.isConstant && !e.key.declarationCleared)) { + final sl = entry.key; + final name = NetlistUtils.tryGetSynthLogicName(sl); + if (name != null) { + var bits = applyAlias(entry.value.cast()); + // For element signals whose IDs were remapped by the + // array_concat fresh-ID pass, apply that mapping so the + // element netname matches the concat input (fresh) IDs. + if (arrayConcatOldToNew.isNotEmpty && sl is SynthLogicArrayElement) { + bits = bits + .map((b) => b is int ? (arrayConcatOldToNew[b] ?? b) : b) + .toList(); + } + addNetname(Sanitizer.sanitizeSV(name), bits); + } + } + } + + // Constant netnames for non-blocked constants (already aliased via + // cell connections above). + for (final cellEntry + in cells.entries.where((e) => e.value['type'] == r'$const')) { + final conns = + cellEntry.value['connections'] as Map>?; + if (conns != null && conns.isNotEmpty) { + addNetname(cellEntry.key, conns.values.first, computed: true); + } + } + + // -- Ensure every bit ID in cell connections has a netname ------------ + { + final coveredIds = netnames.values + .expand( + (nn) => ((nn! as Map)['bits'] as List?) ?? []) + .whereType() + .toSet(); + + for (final cellEntry in cells.entries) { + final cellName = cellEntry.key; + final conns = + cellEntry.value['connections'] as Map? ?? {}; + for (final connEntry in conns.entries) { + final portName = connEntry.key; + final bits = connEntry.value as List; + final missingBits = []; + for (final b in bits) { + if (b is int && !coveredIds.contains(b)) { + missingBits.add(b); + coveredIds.add(b); + } + } + if (missingBits.isNotEmpty) { + addNetname( + Sanitizer.sanitizeSV('${cellName}_$portName'), missingBits, + hideName: true); + } + } + } + } + + // -- Slim: strip cell connections ------------------------------------ + // The full pipeline ran identically, so the cell set (keys, ordering) + // is canonical. Now drop the connection maps to reduce the output + // size. This is the ONLY difference between slim and full output. + if (options.slimMode) { + for (final cell in cells.values) { + cell.remove('connections'); + } + } + + return NetlistSynthesisResult( + module, + getInstanceTypeOfModule, + ports: ports, + cells: cells, + netnames: netnames, + attributes: attr, + ); + } + + /// Apply all post-processing passes to the modules map. + /// + /// This is the canonical pass ordering used by both netlist flows: + /// **Flow 1** (slim batch via `_synthesizeSlimModules`) and + /// **Flow 2** (incremental full via `moduleNetlistJson`). + /// Also used internally by [buildModulesMap] / [synthesizeToJson]. + void applyPostProcessingPasses( + Map> modules, + ) { + if (options.groupStructConversions) { + if (options.groupMaximalSubsets) { + NetlistPasses.applyMaximalSubsetGrouping(modules); + } + if (options.collapseConcats) { + NetlistPasses.applyCollapseConcats(modules); + } + if (options.collapseSelectsIntoPack) { + NetlistPasses.applyCollapseSelectsIntoPack(modules); + } + if (options.collapseUnpackToConcat) { + NetlistPasses.applyCollapseUnpackToConcat(modules); + } + if (options.collapseUnpackToPack) { + NetlistPasses.applyCollapseUnpackToPack(modules); + } + NetlistPasses.applyStructConversionGrouping(modules); + if (options.collapseStructGroups) { + NetlistPasses.collapseStructGroupModules(modules); + } + NetlistPasses.applyStructBufferInsertion(modules); + NetlistPasses.applyConcatToBufferReplacement(modules); + } + } + + /// Build the processed modules map from a [SynthBuilder]'s results. + /// + /// Returns the intermediate module map (definition name → module data) + /// after all post-processing passes have been applied. This allows + /// callers to retain per-module results for incremental serving while + /// avoiding redundant re-synthesis. + Future>> buildModulesMap( + SynthBuilder synth, Module top) async { + final swEntries = Stopwatch()..start(); + final modules = NetlistPasses.collectModuleEntries(synth.synthesisResults, + topModule: top); + swEntries.stop(); + + final swPasses = Stopwatch()..start(); + applyPostProcessingPasses(modules); + swPasses.stop(); + + return modules; + } + + /// Generate the combined netlist JSON from a [SynthBuilder]'s results. + Future generateCombinedJson(SynthBuilder synth, Module top) async { + final swCollect = Stopwatch()..start(); + final modules = await buildModulesMap(synth, top); + swCollect.stop(); + + final swCompress = Stopwatch()..start(); + if (options.compressBitRanges) { + _compressModulesMap(modules); + } + swCompress.stop(); + + final combined = { + 'creator': 'NetlistSynthesizer (rohd)', + 'modules': modules, + }; + + final swEncode = Stopwatch()..start(); + final encoder = options.compactJson + ? const JsonEncoder() + : const JsonEncoder.withIndent(' '); + final result = encoder.convert(combined); + swEncode.stop(); + + return result; + } + + /// Compresses a list of bit IDs by replacing contiguous ascending runs of + /// 3 or more integers with `"start:end"` range strings. + static List _compressBits(List bits) { + final result = []; + final pending = []; + + void flushPending() { + if (pending.isEmpty) { + return; + } + var i = 0; + while (i < pending.length) { + var j = i; + while (j + 1 < pending.length && pending[j + 1] == pending[j] + 1) { + j++; + } + final runLen = j - i + 1; + if (runLen >= 3) { + result.add('${pending[i]}:${pending[j]}'); + } else { + for (var k = i; k <= j; k++) { + result.add(pending[k]); + } + } + i = j + 1; + } + pending.clear(); + } + + for (final element in bits) { + if (element is int) { + pending.add(element); + } else { + flushPending(); + result.add(element); + } + } + flushPending(); + return result; + } + + /// Applies [_compressBits] to all `bits` arrays and cell `connections` + /// arrays in a modules map. + static void _compressModulesMap( + Map> modules, + ) { + for (final moduleDef in modules.values) { + final ports = moduleDef['ports'] as Map>?; + if (ports != null) { + for (final port in ports.values) { + final bits = port['bits']; + if (bits is List) { + port['bits'] = _compressBits(bits.cast()); + } + } + } + + final cells = moduleDef['cells'] as Map>?; + if (cells != null) { + for (final cell in cells.values) { + final conns = cell['connections'] as Map>?; + if (conns != null) { + for (final key in conns.keys.toList()) { + conns[key] = _compressBits(conns[key]!); + } + } + } + } + + final netnames = moduleDef['netnames'] as Map?; + if (netnames != null) { + for (final entry in netnames.values) { + if (entry is Map) { + final bits = entry['bits']; + if (bits is List) { + entry['bits'] = _compressBits(bits.cast()); + } + } + } + } + } + } + + /// Convenience: synthesize [top] into a combined netlist JSON string. + /// + /// Builds a [SynthBuilder] internally and returns the full JSON. + Future synthesizeToJson(Module top) async { + final sb = SynthBuilder(top, this); + return generateCombinedJson(sb, top); + } +} + +/// A version of [BusSubset] that creates explicit `$slice` cells for +/// [LogicArray] element extraction in the netlist. +/// +/// When a [LogicArray] port is decomposed into its elements, each element +/// gets its own [_BusSubsetForArraySlice] so the netlist shows explicit +/// select gates rather than flat bit aliasing. +class _BusSubsetForArraySlice extends BusSubset { + _BusSubsetForArraySlice( + super.bus, + super.startIndex, + super.endIndex, + ) : super(name: 'array_slice'); + + @override + bool get hasBuilt => true; +} + +/// A version of [Swizzle] that creates explicit `$concat` cells for +/// [LogicArray] element assembly in the netlist. +/// +/// When a [LogicArray]'s elements are driven independently (e.g. by +/// constants), this creates a visible concat gate in the netlist that +/// assembles the element signals into the full packed array bus. +class _SwizzleForArrayConcat extends Swizzle { + _SwizzleForArrayConcat(super.signals) : super(name: 'array_concat'); + + @override + bool get hasBuilt => true; +} diff --git a/lib/src/synthesizers/netlist/netlist_utils.dart b/lib/src/synthesizers/netlist/netlist_utils.dart new file mode 100644 index 000000000..258fe56a3 --- /dev/null +++ b/lib/src/synthesizers/netlist/netlist_utils.dart @@ -0,0 +1,531 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_utils.dart +// Shared utility functions for netlist synthesis and post-processing passes. +// +// 2026 February 11 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Shared utility functions for netlist synthesis and post-processing passes. +/// +/// All methods are static — no instances are created. +class NetlistUtils { + NetlistUtils._(); + + /// Find the port name in [portMap] that corresponds to [sl]. + static String? portNameForSynthLogic( + SynthLogic sl, Map portMap) { + for (final e in portMap.entries) { + if (sl.logics.contains(e.value)) { + return e.key; + } + } + return null; + } + + /// Safely retrieve the name from a [SynthLogic], returning null if + /// retrieval fails (e.g. name not yet picked, or the SynthLogic has + /// been replaced). + static String? tryGetSynthLogicName(SynthLogic sl) { + try { + return sl.name; + // ignore: avoid_catches_without_on_clauses + } catch (_) { + return null; + } + } + + /// Resolves [sl] to the end of its replacement chain. + static SynthLogic resolveReplacement(SynthLogic sl) { + var r = sl; + while (r.replacement != null) { + r = r.replacement!; + } + return r; + } + + /// Anchored regex for range-named concat port labels like `[7:0]` or `[3]`. + static final rangePortRe = RegExp(r'^\[(\d+)(?::(\d+))?\]$'); + + /// Create a `$buf` cell map. + static Map makeBufCell( + int width, + List aBits, + List yBits, + ) => + { + 'hide_name': 0, + 'type': r'$buf', + 'parameters': {'WIDTH': width}, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{'A': aBits, 'Y': yBits}, + }; + + /// Create a `$slice` cell map. + static Map makeSliceCell( + int offset, + int aWidth, + int yWidth, + List aBits, + List yBits, + ) => + { + 'hide_name': 0, + 'type': r'$slice', + 'parameters': { + 'OFFSET': offset, + 'A_WIDTH': aWidth, + 'Y_WIDTH': yWidth, + }, + 'attributes': {}, + 'port_directions': {'A': 'input', 'Y': 'output'}, + 'connections': >{'A': aBits, 'Y': yBits}, + }; + + /// Build wire-driver, wire-consumer, and bit-to-net maps for a module. + /// + /// Scans every cell's connections to find which cell drives each wire bit + /// (output direction) and which cells consume it (input direction). + /// Module output-port bits are registered as pseudo-consumers (`__port__`) + /// so that cells feeding module ports are never accidentally removed. + static ({ + Map wireDriverCell, + Map> wireConsumerCells, + Map)> bitToNetInfo, + }) buildWireMaps( + Map> cells, + Map moduleDef, + ) { + final wireDriverCell = {}; + final wireConsumerCells = >{}; + for (final entry in cells.entries) { + final cell = entry.value; + final conns = cell['connections'] as Map? ?? {}; + final pdirs = cell['port_directions'] as Map? ?? {}; + for (final pe in conns.entries) { + final d = pdirs[pe.key] as String? ?? ''; + for (final b in pe.value as List) { + if (b is int) { + if (d == 'output') { + wireDriverCell[b] = entry.key; + } else if (d == 'input') { + (wireConsumerCells[b] ??= {}).add(entry.key); + } + } + } + } + } + + final modPorts = moduleDef['ports'] as Map>?; + if (modPorts != null) { + for (final port in modPorts.values) { + if ((port['direction'] as String?) == 'output') { + for (final b in port['bits'] as List? ?? []) { + if (b is int) { + (wireConsumerCells[b] ??= {}).add('__port__'); + } + } + } + } + } + + final netnames = moduleDef['netnames'] as Map? ?? {}; + final bitToNetInfo = )>{}; + for (final nnEntry in netnames.entries) { + final nd = nnEntry.value! as Map; + final bits = (nd['bits'] as List?)?.cast() ?? []; + for (final b in bits) { + bitToNetInfo[b] = (nnEntry.key, bits); + } + } + + return ( + wireDriverCell: wireDriverCell, + wireConsumerCells: wireConsumerCells, + bitToNetInfo: bitToNetInfo, + ); + } + + /// Trace a single wire bit backward through `$buf`/`$slice` cells, + /// returning the ultimate source bit and the set of intermediate cell + /// names visited along the chain. + static (int sourceBit, Set intermediates) traceBackward( + int startBit, + Map wireDriverCell, + Map> cells, + ) { + var current = startBit; + final chain = {}; + while (true) { + final driverName = wireDriverCell[current]; + if (driverName == null) { + break; + } + final driverCell = cells[driverName]; + if (driverCell == null) { + break; + } + final dt = driverCell['type'] as String?; + if (dt != r'$buf' && dt != r'$slice') { + break; + } + if (chain.contains(driverName)) { + break; // Cycle detected — stop tracing. + } + chain.add(driverName); + final dc = driverCell['connections'] as Map? ?? {}; + if (dt == r'$buf') { + final yBits = dc['Y'] as List; + final aBits = dc['A'] as List; + final idx = yBits.indexOf(current); + if (idx < 0 || idx >= aBits.length || aBits[idx] is! int) { + break; + } + current = aBits[idx] as int; + } else { + final yBits = dc['Y'] as List; + final aBits = dc['A'] as List; + final dp = driverCell['parameters'] as Map? ?? {}; + final offset = dp['OFFSET'] as int? ?? 0; + final idx = yBits.indexOf(current); + if (idx < 0) { + break; + } + final srcIdx = offset + idx; + if (srcIdx < 0 || srcIdx >= aBits.length || aBits[srcIdx] is! int) { + break; + } + current = aBits[srcIdx] as int; + } + } + return (current, chain); + } + + /// Whether every intermediate cell in [intermediates] exclusively feeds + /// [ownerCell] or other cells in [intermediates]. + /// + /// When [allowPortConsumers] is true, `'__port__'` pseudo-consumers are + /// also accepted (used when module-output ports registered as consumers). + static bool isExclusiveChain({ + required Set intermediates, + required String ownerCell, + required Map> cells, + required Map> wireConsumerCells, + bool allowPortConsumers = false, + }) { + for (final ic in intermediates) { + final icCell = cells[ic]; + if (icCell == null) { + return false; + } + final icConns = icCell['connections'] as Map? ?? {}; + final icDirs = icCell['port_directions'] as Map? ?? {}; + for (final pe in icConns.entries) { + if ((icDirs[pe.key] as String?) != 'output') { + continue; + } + for (final b in pe.value as List) { + if (b is! int) { + continue; + } + final consumers = wireConsumerCells[b]; + if (consumers == null) { + continue; + } + for (final cn in consumers) { + if (cn != ownerCell && !intermediates.contains(cn)) { + if (allowPortConsumers && cn == '__port__') { + continue; + } + return false; + } + } + } + } + } + return true; + } + + /// Collapses bit-slice ports of a Combinational/Sequential cell into + /// aggregate ports. + /// + /// **Input side**: When a Combinational references individual struct fields, + /// each field creates a BusSubset in the parent scope, and each slice + /// becomes a separate input port. This method detects groups of input + /// ports whose SynthLogics are outputs of BusSubset submodule + /// instantiations that slice the same root signal. For each group + /// forming a contiguous bit range, the N individual ports are replaced + /// with a single aggregate port connected to the corresponding sub-range + /// of the root signal's wire IDs. + /// + /// **Output side**: Similarly, Combinational output ports that feed into + /// the inputs of the same Swizzle submodule are collapsed into a single + /// aggregate port connected to the Swizzle's output wire IDs. + static void collapseAlwaysBlockPorts( + SynthModuleDefinition synthDef, + SynthSubModuleInstantiation instance, + Map portDirs, + Map> connections, + List Function(SynthLogic) getIds, + ) { + // ── Input-side collapsing (BusSubset → Combinational) ────────────── + + // Build reverse lookup: resolved BusSubset output SynthLogic → + // (BusSubset module, resolved root input SynthLogic, + // SynthSubModuleInstantiation). + final busSubsetLookup = + {}; + for (final bsInst in synthDef.subModuleInstantiations) { + if (bsInst.module is! BusSubset) { + continue; + } + final bsMod = bsInst.module as BusSubset; + + // BusSubset has input 'original' and output 'subset' + final outputSL = bsInst.outputMapping.values.firstOrNull; + final inputSL = bsInst.inputMapping.values.firstOrNull; + if (outputSL == null || inputSL == null) { + continue; + } + + final resolvedOutput = resolveReplacement(outputSL); + final resolvedInput = resolveReplacement(inputSL); + + busSubsetLookup[resolvedOutput] = (bsMod, resolvedInput, bsInst); + } + + // Group input ports by root signal, also tracking the BusSubset + // instantiations that produced each port. + final inputGroups = >{}; + + for (final e in instance.inputMapping.entries) { + final portName = e.key; + if (!connections.containsKey(portName)) { + continue; // already filtered + } + + final resolved = resolveReplacement(e.value); + final info = busSubsetLookup[resolved]; + if (info != null) { + final (bsMod, rootSL, bsInst) = info; + final width = bsMod.endIndex - bsMod.startIndex + 1; + inputGroups + .putIfAbsent(rootSL, () => []) + .add((portName, bsMod.startIndex, width, bsInst)); + } + } + + // Collapse each group with > 1 contiguous member. + for (final entry in inputGroups.entries) { + if (entry.value.length <= 1) { + continue; + } + + final rootSL = entry.key; + final ports = entry.value..sort((a, b) => a.$2.compareTo(b.$2)); + + // Verify contiguous non-overlapping coverage. + var expectedBit = ports.first.$2; + var contiguous = true; + for (final (_, startIdx, width, _) in ports) { + if (startIdx != expectedBit) { + contiguous = false; + break; + } + expectedBit += width; + } + if (!contiguous) { + continue; + } + + final minBit = ports.first.$2; + final maxBit = ports.last.$2 + ports.last.$3 - 1; + + // Get the root signal's full wire IDs and extract the sub-range. + final rootIds = getIds(rootSL); + if (maxBit >= rootIds.length) { + continue; // safety check + } + final aggBits = rootIds.sublist(minBit, maxBit + 1).cast(); + + // Choose a name for the aggregate port. + final rootName = tryGetSynthLogicName(rootSL) ?? 'agg_${minBit}_$maxBit'; + + // Replace individual ports with the aggregate. The bypassed + // BusSubset cells are left in place; the post-synthesis DCE pass + // will remove them if their outputs are no longer consumed. + for (final (portName, _, _, _) in ports) { + connections.remove(portName); + portDirs.remove(portName); + } + connections[rootName] = aggBits; + portDirs[rootName] = 'input'; + } + + // ── Output-side collapsing (Combinational → Swizzle) ─────────────── + + // Build reverse lookup: resolved Swizzle input SynthLogic → + // (Swizzle port name, bit offset within the Swizzle output, + // port width, resolved Swizzle output SynthLogic, + // SynthSubModuleInstantiation). + final swizzleLookup = {}; + for (final szInst in synthDef.subModuleInstantiations) { + if (szInst.module is! Swizzle) { + continue; + } + final outputSL = szInst.outputMapping.values.firstOrNull; + if (outputSL == null) { + continue; + } + final resolvedOutput = resolveReplacement(outputSL); + + // Swizzle inputs are in0, in1, ... with bit-0 first. + var offset = 0; + for (final inEntry in szInst.inputMapping.entries) { + final resolvedInput = resolveReplacement(inEntry.value); + final w = resolvedInput.width; + swizzleLookup[resolvedInput] = + (inEntry.key, offset, w, resolvedOutput, szInst); + offset += w; + } + } + + // Group output ports by Swizzle output signal. + final outputGroups = >{}; + + for (final e in instance.outputMapping.entries) { + final portName = e.key; + if (!connections.containsKey(portName)) { + continue; + } + + final resolved = resolveReplacement(e.value); + final info = swizzleLookup[resolved]; + if (info != null) { + final (_, offset, width, swizzleOutputSL, szInst) = info; + outputGroups + .putIfAbsent(swizzleOutputSL, () => []) + .add((portName, offset, width, szInst)); + } + } + + // Collapse each group with > 1 contiguous member. + for (final entry in outputGroups.entries) { + if (entry.value.length <= 1) { + continue; + } + + final swizOutSL = entry.key; + final ports = entry.value..sort((a, b) => a.$2.compareTo(b.$2)); + + // Verify contiguous. + var expectedBit = ports.first.$2; + var contiguous = true; + for (final (_, offset, width, _) in ports) { + if (offset != expectedBit) { + contiguous = false; + break; + } + expectedBit += width; + } + if (!contiguous) { + continue; + } + + final minBit = ports.first.$2; + final maxBit = ports.last.$2 + ports.last.$3 - 1; + + final outIds = getIds(swizOutSL); + if (maxBit >= outIds.length) { + continue; + } + final aggBits = outIds.sublist(minBit, maxBit + 1).cast(); + + final outName = + tryGetSynthLogicName(swizOutSL) ?? 'agg_out_${minBit}_$maxBit'; + + // Replace individual ports with the aggregate. The bypassed + // Swizzle cells are left in place; the post-synthesis DCE pass + // will remove them if their outputs are no longer consumed. + for (final (portName, _, _, _) in ports) { + connections.remove(portName); + portDirs.remove(portName); + } + connections[outName] = aggBits; + portDirs[outName] = 'output'; + } + } + + /// Check if a SynthLogic is a constant (following replacement chain). + static bool isConstantSynthLogic(SynthLogic sl) => + resolveReplacement(sl).isConstant; + + /// Extract the Const value from a constant SynthLogic. + static Const? constValueFromSynthLogic(SynthLogic sl) { + final resolved = resolveReplacement(sl); + for (final logic in resolved.logics) { + if (logic is Const) { + return logic; + } + } + return null; + } + + /// Value portion of a constant name: `_h` or `_b`. + static String constValuePart(Const c) { + final bitChars = []; + var hasXZ = false; + for (var i = c.width - 1; i >= 0; i--) { + final v = c.value[i]; + switch (v) { + case LogicValue.zero: + bitChars.add('0'); + case LogicValue.one: + bitChars.add('1'); + case LogicValue.x: + bitChars.add('x'); + hasXZ = true; + case LogicValue.z: + bitChars.add('z'); + hasXZ = true; + } + } + if (hasXZ) { + return '${c.width}_b${bitChars.join()}'; + } + var value = BigInt.zero; + for (var i = c.width - 1; i >= 0; i--) { + value = value << 1; + if (c.value[i] == LogicValue.one) { + value = value | BigInt.one; + } + } + return '${c.width}_h${value.toRadixString(16)}'; + } +} diff --git a/lib/src/synthesizers/synth_builder.dart b/lib/src/synthesizers/synth_builder.dart index 54e312ab3..f9d0a0d08 100644 --- a/lib/src/synthesizers/synth_builder.dart +++ b/lib/src/synthesizers/synth_builder.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_builder.dart diff --git a/lib/src/synthesizers/synthesizer.dart b/lib/src/synthesizers/synthesizer.dart index b70c9338e..687bbab03 100644 --- a/lib/src/synthesizers/synthesizer.dart +++ b/lib/src/synthesizers/synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synthesizer.dart @@ -6,7 +6,6 @@ // // 2021 August 26 // Author: Max Korbel -// import 'package:rohd/rohd.dart'; diff --git a/lib/src/synthesizers/synthesizers.dart b/lib/src/synthesizers/synthesizers.dart index b8c8523ec..70f47f21a 100644 --- a/lib/src/synthesizers/synthesizers.dart +++ b/lib/src/synthesizers/synthesizers.dart @@ -1,8 +1,10 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'netlist/netlist.dart'; export 'synth_builder.dart'; export 'synth_file_contents.dart'; export 'synthesis_result.dart'; export 'synthesizer.dart'; +export 'systemc/systemc.dart'; export 'systemverilog/systemverilog.dart'; diff --git a/lib/src/synthesizers/systemc/systemc.dart b/lib/src/synthesizers/systemc/systemc.dart new file mode 100644 index 000000000..7bf0f1211 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc.dart @@ -0,0 +1,29 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesizer.dart +// Definition for SystemC Synthesizer +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; + +/// A [Synthesizer] which generates equivalent SystemC as the given [Module]. +/// +/// Attempts to maintain signal naming and structure as much as possible, +/// using the same naming strategy as the SystemVerilog synthesizer. +class SystemCSynthesizer extends Synthesizer { + @override + bool generatesDefinition(Module module) => + // ignore: deprecated_member_use_from_same_package + !((module is CustomSystemVerilog) || + (module is SystemVerilog && + module.generatedDefinitionType == DefinitionGenerationType.none)); + + @override + SynthesisResult synthesize(Module module, + String Function(Module module) getInstanceTypeOfModule) => + SystemCSynthesisResult(module, getInstanceTypeOfModule); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart new file mode 100644 index 000000000..e670279d4 --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_module_definition.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_module_definition.dart +// Definition for SystemCSynthModuleDefinition +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A special [SynthModuleDefinition] for SystemC modules. +class SystemCSynthModuleDefinition extends SynthModuleDefinition { + /// Creates a new [SystemCSynthModuleDefinition] for the given [module]. + SystemCSynthModuleDefinition(super.module); + + @override + void process() { + // For now, do not collapse inline modules. Each InlineSystemVerilog gate + // remains as a sub-module instantiation and gets emitted as an assign-style + // expression in the generated SystemC (similar to SV `assign x = a & b`). + // + // Future: implement chain-collapsing for compound expressions. + } + + @override + SynthSubModuleInstantiation createSubModuleInstantiation(Module m) => + SystemCSynthSubModuleInstantiation(m); +} diff --git a/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart new file mode 100644 index 000000000..7e692ff8a --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart @@ -0,0 +1,113 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synth_sub_module_instantiation.dart +// Definition for SystemCSynthSubModuleInstantiation +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// Represents a submodule instantiation for SystemC. +class SystemCSynthSubModuleInstantiation extends SynthSubModuleInstantiation { + /// Creates a new [SystemCSynthSubModuleInstantiation] for the given + /// [module]. + SystemCSynthSubModuleInstantiation(super.module); + + /// If [module] is [InlineSystemVerilog], this will be the [SynthLogic] that + /// is the `result` of that module. Otherwise, `null`. + SynthLogic? get inlineResultLogic => module is! InlineSystemVerilog + ? null + : (outputMapping[(module as InlineSystemVerilog).resultSignalName] ?? + inOutMapping[(module as InlineSystemVerilog).resultSignalName]); + + /// Mapping from [SynthLogic]s which are outputs of inlineable modules to + /// those inlineable modules. + Map? + synthLogicToInlineableSynthSubmoduleMap; + + /// Provides a mapping from ports of this module to a string that can be fed + /// into that port, which may include inline expressions. + Map _modulePortsMapWithInline( + Map plainPorts) => + plainPorts.map((name, synthLogic) => MapEntry( + name, + synthLogicToInlineableSynthSubmoduleMap?[synthLogic] + ?.inlineSystemC() ?? + (synthLogic.declarationCleared ? '' : synthLogic.name))); + + /// Provides the inline SystemC expression for this module. + /// + /// Should only be called if [module] is [InlineSystemVerilog]. + String inlineSystemC() { + final portNameToValueMapping = _modulePortsMapWithInline( + {...inputMapping, ...inOutMapping} + ..remove((module as InlineSystemVerilog).resultSignalName), + ); + + final inlineRepresentation = + _inlineSystemCExpression(portNameToValueMapping); + + return '($inlineRepresentation)'; + } + + /// Generates the inline SystemC expression for the gate module. + String _inlineSystemCExpression(Map inputs) { + final m = module; + + if (m is NotGate) { + final inVal = inputs.values.first; + return '~$inVal'; + } else if (m is And2Gate) { + return '${inputs.values.first} & ${inputs.values.last}'; + } else if (m is Or2Gate) { + return '${inputs.values.first} | ${inputs.values.last}'; + } else if (m is Xor2Gate) { + return '${inputs.values.first} ^ ${inputs.values.last}'; + } else if (m is Mux) { + // Mux has inputs: control, d0, d1 → output: y + // In SystemC: control ? d1 : d0 + final entries = inputs.entries.toList(); + final control = entries[0].value; + final d0 = entries[1].value; + final d1 = entries[2].value; + return '$control ? $d1 : $d0'; + } else if (m is InlineSystemVerilog) { + // Fallback: use the verilog inline expression as a reasonable + // approximation (many operators are identical between SV and C++) + return m.inlineVerilog(inputs); + } + + throw SynthException('Unsupported inline module type: ${m.runtimeType}'); + } + + /// Provides the full SystemC instantiation for this module as a member + /// declaration and port binding in the constructor. + /// + /// Returns null if this module does not need instantiation. + String? memberDeclaration(String instanceType) { + if (!needsInstantiation) { + return null; + } + return '$instanceType $name{"$name"};'; + } + + /// Generates port binding statements for the constructor body. + String? portBindings() { + if (!needsInstantiation) { + return null; + } + final bindings = []; + final allPorts = {...inputMapping, ...outputMapping, ...inOutMapping}; + for (final entry in allPorts.entries) { + final portName = entry.key; + final synthLogic = entry.value; + if (!synthLogic.declarationCleared) { + bindings.add('$name.$portName(${synthLogic.name});'); + } + } + return bindings.join('\n'); + } +} diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart new file mode 100644 index 000000000..9e242629b --- /dev/null +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -0,0 +1,1576 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_synthesis_result.dart +// Definition for SystemCSynthesisResult +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/always.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synth_sub_module_instantiation.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; + +/// A [SynthesisResult] representing a conversion of a [Module] to SystemC. +class SystemCSynthesisResult extends SynthesisResult { + /// A cached copy of the generated ports. + late final String _portsString; + + /// A cached copy of the generated module body (used for matching). + late final String _moduleBodyString; + + /// The main [SynthModuleDefinition] for this. + final SynthModuleDefinition _synthModuleDefinition; + + @override + List get supportingModules => + _synthModuleDefinition.supportingModules; + + // Cached sections for final assembly + late final String _internalSigs; + late final String _subMembers; + late final String _ctorBody; + late final String _methodBodies; + + /// Creates a new [SystemCSynthesisResult] for the given [module]. + SystemCSynthesisResult(super.module, super.getInstanceTypeOfModule) + : _synthModuleDefinition = SystemCSynthModuleDefinition(module) { + _findClockResetSignals(); + _portsString = _systemCPorts(); + _buildModuleBody(getInstanceTypeOfModule); + _moduleBodyString = '$_ctorBody|$_methodBodies'; + } + + @override + bool matchesImplementation(SynthesisResult other) => + other is SystemCSynthesisResult && + other._portsString == _portsString && + other._moduleBodyString == _moduleBodyString; + + @override + int get matchHashCode => _portsString.hashCode ^ _moduleBodyString.hashCode; + + @override + String toFileContents() => _toSystemC(); + + @override + List toSynthFileContents() => List.unmodifiable([ + SynthFileContents( + name: instanceTypeName, + description: 'SystemC module definition for $instanceTypeName', + contents: _toSystemC(), + ) + ]); + + // ──────────────────────────────────────────────────────────────────── + // Clock/reset detection + // ──────────────────────────────────────────────────────────────────── + + /// Internal clock signals promoted to ports (from SimpleClockGenerator). + late final Set _promotedClockSignals; + + /// Pre-scans sub-module instantiations to identify clock/reset signals + /// and internal clocks that should be promoted to ports. + void _findClockResetSignals() { + final promotedClocks = {}; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + // Detect SimpleClockGenerator and promote its output to a port + if (m is SimpleClockGenerator) { + for (final entry in ssmi.outputMapping.entries) { + promotedClocks.add(entry.value.name); + } + } + } + _promotedClockSignals = promotedClocks; + } + + // ──────────────────────────────────────────────────────────────────── + // Type mapping + // ──────────────────────────────────────────────────────────────────── + + /// Sanitize a signal/port name to be a valid C++ identifier. + /// Replaces `[N]` with `_N_` (LogicArray element indexing). + static String _scName(String name) => + name.replaceAllMapped(RegExp(r'\[(\d+)\]'), (m) => '_${m[1]}_'); + + /// Maps a signal width to the appropriate SystemC data type. + static String systemCType(int width) { + if (width == 1) { + return 'bool'; + } else if (width <= 64) { + return 'sc_uint<$width>'; + } else { + return 'sc_biguint<$width>'; + } + } + + /// SystemC input port type for a given width. + static String systemCInType(int width) => 'sc_in<${systemCType(width)}>'; + + /// SystemC output port type for a given width. + static String systemCOutType(int width) => 'sc_out<${systemCType(width)}>'; + + /// SystemC signal type for a given width. + static String systemCSignalType(int width) => + 'sc_signal<${systemCType(width)}>'; + + // ──────────────────────────────────────────────────────────────────── + // Port declarations + // ──────────────────────────────────────────────────────────────────── + + String _systemCPorts() { + final lines = []; + for (final sig in _synthModuleDefinition.inputs) { + final n = _scName(sig.name); + lines.add(' ${systemCInType(sig.width)} $n{"$n"};'); + } + // Promote internal clock signals (from SimpleClockGenerator) to ports + for (final clkName in _promotedClockSignals) { + final n = _scName(clkName); + lines.add(' ${systemCInType(1)} $n{"$n"};'); + } + for (final sig in _synthModuleDefinition.outputs) { + final n = _scName(sig.name); + lines.add(' ${systemCOutType(sig.width)} $n{"$n"};'); + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Internal signals + // ──────────────────────────────────────────────────────────────────── + + String _buildInternalSignals() { + final declarations = []; + for (final sig in _synthModuleDefinition.internalSignals + .where((e) => e.needsDeclaration) + .where((e) => !_promotedClockSignals.contains(e.name)) + .sorted((a, b) => a.name.compareTo(b.name))) { + final n = _scName(sig.name); + declarations.add(' ${systemCSignalType(sig.width)} $n{"$n"};'); + } + + // Declare individual signals for array elements that are written to + // (FlipFlop/Sequential outputs targeting array elements) + for (final elemName in _arrayElementsWritten.keys) { + final n = _scName(elemName); + final width = _arrayElementsWritten[elemName]!; + declarations.add(' ${systemCSignalType(width)} $n{"$n"};'); + } + return declarations.join('\n'); + } + + /// Maps array element names (e.g. "delayLine[0]") to their widths. + /// These need separate signal declarations because SystemC can't do + /// partial writes to sc_signal. + late final Map _arrayElementsWritten = + _findArrayElementsWritten(); + + /// Groups array elements by parent: parentName → list of (index, elemWidth). + late final Map> + _arrayElementsByParent = _groupArrayElementsByParent(); + + Map _findArrayElementsWritten() { + final result = {}; + + void addIfArrayElement(SynthLogic sl) { + if (sl is SynthLogicArrayElement) { + result[sl.name] = sl.logic.width; + } + } + + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + final m = ssmi.module; + + // All submodule output mappings + ssmi.outputMapping.values.forEach(addIfArrayElement); + + // Inline gate result logics + if (ssmi is SystemCSynthSubModuleInstantiation) { + final rl = ssmi.inlineResultLogic; + if (rl != null) { + addIfArrayElement(rl); + } + } + + // Scan conditionals for nested array element receivers + if (m is Combinational) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } else if (m is Sequential) { + _collectArrayReceiversFromConditionals(m.conditionals, result); + } + } + + // Wire assignments targeting array elements + for (final assignment in _synthModuleDefinition.assignments) { + addIfArrayElement(assignment.dst); + } + + return result; + } + + /// Recursively walks a conditionals tree to find all receivers that + /// are array elements and adds them to [result]. + void _collectArrayReceiversFromConditionals( + List conditionals, Map result) { + for (final c in conditionals) { + for (final receiver in c.receivers) { + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl is SynthLogicArrayElement && !result.containsKey(sl.name)) { + result[sl.name] = sl.logic.width; + } + } + // Recurse into sub-conditionals + _collectArrayReceiversFromConditionals(c.conditionals, result); + } + } + + /// Groups array elements by their root parent signal, + /// computing flat bit offsets for nested elements. + Map> + _groupArrayElementsByParent() { + final result = >{}; + + void addElement(SynthLogicArrayElement sl) { + // Walk up to root and compute flat bit offset + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex; + if (idx == null) { + return; // pruned element — skip + } + flatOffset += idx * current.logic.width; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = current.name; + + final entry = ( + // Use flat bit offset as "index" for assembly ordering + index: flatOffset, + width: sl.logic.width, + elemName: sl.name, + ); + // Avoid duplicates + final list = result.putIfAbsent(rootName, () => []); + if (!list.any((e) => e.elemName == entry.elemName)) { + list.add(entry); + } + } + + // Use logicToSynthMap to find the SynthLogicArrayElement for each written + // element, rather than re-scanning submodule instantiations. + for (final sl in _synthModuleDefinition.logicToSynthMap.values) { + if (sl is SynthLogicArrayElement && sl.replacement == null) { + // Skip elements whose parent has been pruned or not named + final parent = sl.parentArray.replacement ?? sl.parentArray; + if (parent.declarationCleared) { + continue; + } + if (_arrayElementsWritten.containsKey(sl.name)) { + addElement(sl); + } + } + } + + // Sort each list by flat bit offset + for (final list in result.values) { + list.sort((a, b) => a.index.compareTo(b.index)); + } + return result; + } + + // ──────────────────────────────────────────────────────────────────── + // Inline gate expressions + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if a module is a SystemVerilog gate that generates no + /// definition and should be inlined (like Add). + static bool _isInlinableSystemVerilogGate(Module m) => + m is SystemVerilog && + m is! InlineSystemVerilog && + m is! Always && + m is! FlipFlop && + m.generatedDefinitionType == DefinitionGenerationType.none; + + /// Converts a [SynthLogic] to a SystemC read expression. + /// Constants become typed literals; signals get `.read()`. + /// Array elements become range expressions on their parent. + static String _synthLogicReadExpr(SynthLogic sl) { + if (sl.isConstant) { + final c = sl.logics.whereType().first; + return _typedConstExpr(c.value, c.width); + } + if (sl is SynthLogicArrayElement) { + return _arrayElementReadExpr(sl); + } + return '${_scName(sl.name)}.read()'; + } + + /// Generates a typed constant expression for SystemC. + /// Handles x/z values by treating them as 0. + static String _typedConstExpr(LogicValue val, int width) { + if (val.isValid) { + if (width == 0) { + return '0'; + } + final bigVal = val.toBigInt(); + if (width > 64) { + // Use hex string constructor for sc_biguint + var hex = bigVal.toUnsigned(width).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + return '${systemCType(width)}("0x$hex")'; + } + // For uint64 values above INT64_MAX, add ULL suffix + if (bigVal > (BigInt.one << 63) - BigInt.one) { + return '${systemCType(width)}' + '(${bigVal.toUnsigned(width)}ULL)'; + } + return '${systemCType(width)}(${bigVal.toUnsigned(width)})'; + } + // For values with x/z, use 0 (SystemC doesn't have x/z) + return '${systemCType(width)}(0)'; + } + + /// Generates a range read expression for an array element. e.g. + /// deserialized[0] (8-bit in 32-bit parent) → deserialized.read().range(7, 0) + /// Generates a range read expression for an array element, handling + /// arbitrary nesting depth. e.g. `laIn[2][1]` in a `[3,2]x8` array + /// → `laIn.read().range(47, 40)`. + static String _arrayElementReadExpr(SynthLogicArrayElement sl) { + final elemWidth = sl.logic.width; + + // Walk up the parent chain to find the root signal and accumulate + // the flat bit offset. + var flatOffset = 0; + SynthLogic current = sl; + while (current is SynthLogicArrayElement) { + final idx = current.logic.arrayIndex!; + final w = current.logic.width; + flatOffset += idx * w; + current = current.parentArray.replacement ?? current.parentArray; + } + final rootName = _scName(current.name); + final rootWidth = current.width; + + final lo = flatOffset; + final hi = lo + elemWidth - 1; + + // If the root is 1-bit (bool), subscript/range is not valid + if (rootWidth == 1) { + return '$rootName.read()'; + } + if (elemWidth == 1) { + return 'static_cast($rootName.read()[$lo])'; + } + final rangeType = elemWidth <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$elemWidth>($rootName.read().range($hi, $lo))'; + } + + /// Returns the sensitivity signal name for a SynthLogic. + /// For array elements, walks up to the root (non-array-element) parent. + static String _sensitivityName(SynthLogic sl) { + var current = sl; + while (current is SynthLogicArrayElement) { + current = current.parentArray.replacement ?? current.parentArray; + } + return _scName(current.name); + } + + /// Generates an SC_METHOD for inline gates (like SV `assign` stmts). + _MethodResult? _buildInlineGates() { + final inlineGates = _synthModuleDefinition.subModuleInstantiations + .where((s) => + s.needsInstantiation && + (s.module is InlineSystemVerilog || + _isInlinableSystemVerilogGate(s.module))) + .cast() + .toList(); + + if (inlineGates.isEmpty) { + return null; + } + + final sensitivities = {}; + final bodyLines = []; + + for (final ssmi in inlineGates) { + final m = ssmi.module; + + // Collect inputs — constants become literals, signals get .read() + final inputExprs = {}; + for (final entry in ssmi.inputMapping.entries) { + final sl = entry.value; + if (!sl.isConstant) { + sensitivities.add(_sensitivityName(sl)); + } + inputExprs[entry.key] = _synthLogicReadExpr(sl); + } + + if (m is InlineSystemVerilog) { + final resultSynthLogic = ssmi.inlineResultLogic; + if (resultSynthLogic == null) { + continue; + } + final expr = _gateExpression(m, inputExprs); + final dst = _scName(resultSynthLogic.name); + bodyLines.add(' $dst = $expr;'); + } else if (m is Add) { + // Add has two outputs: sum and carry. + // Emit inline expressions for each used output. + final vals = inputExprs.values.toList(); + final sumPortName = m.sum.name; + for (final entry in ssmi.outputMapping.entries) { + final portName = entry.key; + final dst = _scName(entry.value.name); + if (portName == sumPortName) { + bodyLines.add(' $dst = ${vals[0]} + ${vals[1]};'); + } else { + // carry: high bit of (width+1)-bit addition + final w = m.width; + final w1 = w + 1; + final utype = systemCType(w1); + final carryExpr = 'static_cast' + '($utype($utype(${vals[0]})' + ' + $utype(${vals[1]}))[$w])'; + bodyLines.add(' $dst = $carryExpr;'); + } + } + } + ssmi.clearInstantiation(); + } + + if (bodyLines.isEmpty) { + return null; + } + + final setupBuf = StringBuffer()..writeln(' SC_METHOD(assign_method);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: ' void assign_method() {\n' + '${bodyLines.join('\n')}\n' + ' }', + ); + } + + /// Maps an InlineSystemVerilog gate to a C++ expression. + /// + /// Handles all gate types that have SV-specific syntax which needs + /// translation to valid SystemC/C++. + String _gateExpression(InlineSystemVerilog m, Map inputs) { + // ── Single-output bitwise gates (C++ operators identical to SV) ── + if (m is NotGate) { + // For bool (width-1), use logical not; for wider, bitwise not + if ((m as Module).outputs.values.first.width == 1) { + return '!${inputs.values.first}'; + } + return '~${inputs.values.first}'; + } + + // ── Binary operator gates (C++ operators identical to SV) ── + const binaryOps = { + And2Gate: '&', + Or2Gate: '|', + Xor2Gate: '^', + Subtract: '-', + Multiply: '*', + }; + final binOp = binaryOps[m.runtimeType]; + if (binOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $binOp ${vals[1]}'; + } + if (m is Divide || m is Modulo) { + final vals = inputs.values.toList(); + final op = m is Divide ? '/' : '%'; + // Guard against zero divisor (sc_uint defaults to 0 at time-0) + return '(${vals[1]} != 0 ? ${vals[0]} $op ${vals[1]} : 0)'; + } + if (m is Power) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + return '${systemCType(w)}' + '(static_cast' + '(pow(static_cast(${vals[0]}),' + ' static_cast(${vals[1]}))))'; + } + + // ── Comparison (operators identical) ── + const cmpOps = { + Equals: '==', + NotEquals: '!=', + LessThan: '<', + GreaterThan: '>', + LessThanOrEqual: '<=', + GreaterThanOrEqual: '>=', + }; + final cmpOp = cmpOps[m.runtimeType]; + if (cmpOp != null) { + final vals = inputs.values.toList(); + return '${vals[0]} $cmpOp ${vals[1]}'; + } + + // ── Shifts ── + // Cast shift amount to int to avoid ambiguous overloads. + // Width 1 maps to bool in SystemC (no .to_int()), so use (int) cast. + // Clamp: if shift amount >= operand width, result is 0 (or sign-fill + // for arshift), avoiding .to_int() overflow on huge shift amounts. + if (m is LShift || m is RShift || m is ARShift) { + final vals = inputs.values.toList(); + final w = (m as Module).inputs.values.first.width; + final outType = systemCType(w); + final shiftAmtWidth = (m as Module).inputs.values.toList()[1].width; + final shiftExpr = + shiftAmtWidth == 1 ? '(int)(${vals[1]})' : '(${vals[1]}).to_int()'; + if (m is ARShift) { + final signedType = w <= 64 ? 'sc_int<$w>' : 'sc_bigint<$w>'; + final shiftOp = '$outType(($signedType(${vals[0]})) >> $shiftExpr)'; + if (shiftAmtWidth > 31) { + // Sign-fill: shift by width-1 to replicate MSB when shift >= width + final overflow = '$outType(($signedType(${vals[0]})) >> ${w - 1})'; + return '(${vals[1]} >= $w) ? $overflow : $shiftOp'; + } + return shiftOp; + } + final op = m is LShift ? '<<' : '>>'; + final shiftOp = '$outType(${vals[0]} $op $shiftExpr)'; + if (shiftAmtWidth > 31) { + return '(${vals[1]} >= $w) ? $outType(0) : $shiftOp'; + } + return shiftOp; + } + + // ── Unary reductions ── + if (m is AndUnary || m is OrUnary || m is XorUnary) { + final inputWidth = (m as Module).inputs.values.first.width; + // 1-bit: reduce is identity (and bool has no .xor_reduce() in SystemC) + if (inputWidth == 1) { + return 'static_cast(${inputs.values.first})'; + } + if (m is AndUnary) { + return '${inputs.values.first}.and_reduce()'; + } else if (m is OrUnary) { + return '${inputs.values.first}.or_reduce()'; + } else { + return '${inputs.values.first}.xor_reduce()'; + } + } + + // ── Bus subset (slice / index) ── + if (m is BusSubset) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + // If input is already 1-bit (bool), extracting bit 0 is identity + if (inputWidth == 1 && m.startIndex == 0 && m.endIndex == 0) { + return a; + } + if (m.startIndex == m.endIndex) { + return 'static_cast($a[${m.startIndex}])'; + } + if (m.startIndex > m.endIndex) { + // Reverse order — build bit-by-bit concat + // bits[0]=a[endIndex], ..., bits[N]=a[startIndex] + // SystemC concat is MSB-first: output MSB = input[endIndex] + // Use sc_uint<1> (not bool) so SystemC concat operator is invoked + final bits = List.generate(m.startIndex - m.endIndex + 1, + (i) => 'sc_uint<1>($a[${m.endIndex + i}])'); + return '(${bits.join(', ')})'; + } + final w = m.endIndex - m.startIndex + 1; + final rangeType = w <= 64 ? 'sc_uint' : 'sc_biguint'; + return '$rangeType<$w>($a.range(${m.endIndex}, ${m.startIndex}))'; + } + + // ── Dynamic bit index ── + if (m is IndexGate) { + final vals = inputs.values.toList(); + return 'static_cast(${vals[0]}[${vals[1]}])'; + } + + // ── Mux (ternary) ── + if (m is Mux) { + final vals = inputs.values.toList(); + final w = m.out.width; + final utype = systemCType(w); + // Cast both branches to avoid C++ ternary type mismatch + // (e.g., when one branch is bool and the other is sc_uint<1>) + return '${vals[0]}' + ' ? $utype(${vals[2]})' + ' : $utype(${vals[1]})'; + } + + // ── Replication ── + if (m is ReplicationOp) { + final a = inputs.values.first; + final inputWidth = (m as Module).inputs.values.first.width; + final outputWidth = m.replicated.width; + final numReps = outputWidth ~/ inputWidth; + if (inputWidth == 1) { + // Single-bit replicate: all-1s or all-0s + final utype = systemCType(outputWidth); + return '$utype(' + '$a ' + '? $utype(-1) ' + ': $utype(0))'; + } + // Multi-bit replicate: concat N copies + final copies = List.filled(numReps, a); + return '(${copies.join(', ')})'; + } + + // ── Swizzle (concatenation) ── + if (m is Swizzle) { + // SystemC concatenation: (sig1, sig2, sig3) + // bool operands must be cast to sc_uint<1> to use SystemC concat + // (otherwise C++ comma operator is invoked instead) + final modInputs = (m as Module).inputs.values.toList(); + final exprList = []; + var i = 0; + for (final expr in inputs.values) { + final w = modInputs[i].width; + if (w == 0) { + i++; + continue; // skip zero-width padding + } + // Wrap 1-bit (bool) operands in sc_uint<1>() for concat + if (w == 1) { + exprList.add('sc_uint<1>($expr)'); + } else { + exprList.add(expr); + } + i++; + } + if (exprList.length == 1) { + return exprList.first; + } + // Swizzle stores inputs LSB-first (in0=LSB), but SystemC concat + // is MSB-first: (msb, ..., lsb). So reverse. + return '(${exprList.reversed.join(', ')})'; + } + + // Fallback: use SV inline (may not be valid C++ — flag for review) + return '/* TODO: ${m.runtimeType} */ ${m.inlineVerilog(inputs)}'; + } + + // ──────────────────────────────────────────────────────────────────── + // Clock / trigger edge resolution + // ──────────────────────────────────────────────────────────────────── + + /// Resolves a trigger [SynthLogic] to the effective clock port and edge. + /// + /// If the trigger signal is a module input port, it can be used directly + /// with `SC_CTHREAD`. If it is an internal signal derived from a [NotGate], + /// the method traces through the inversion chain to find the original port + /// and flips the edge accordingly (`negedge(~clk) = posedge(clk)`). + ({String clockName, bool isPort, bool isPosedge}) _resolveClockAndEdge( + SynthLogic triggerSL, bool isPosedge) { + final sl = triggerSL.replacement ?? triggerSL; + + if (sl.isPort(_synthModuleDefinition.module)) { + return (clockName: sl.name, isPort: true, isPosedge: isPosedge); + } + + // Try to trace through a NotGate inversion + for (final logic in sl.logics) { + final src = logic.srcConnection; + if (src != null && src.parentModule is NotGate) { + final notInput = src.parentModule!.inputs.values.first; + final notInputSrc = notInput.srcConnection; + if (notInputSrc != null) { + final srcSL = _synthModuleDefinition.logicToSynthMap[notInputSrc]; + if (srcSL != null) { + // Inversion flips the edge + return _resolveClockAndEdge(srcSL, !isPosedge); + } + } + } + } + + // Fallback — use the signal as-is (SC_THREAD will be needed) + return (clockName: sl.name, isPort: false, isPosedge: isPosedge); + } + + // ──────────────────────────────────────────────────────────────────── + // Combinational / Sequential processes + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildProcesses() { + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var idx = 0; + + // Collect clocked processes for consolidation by (clock, reset) pair. + // Sequentials and FlipFlops sharing the same clock/reset are merged + // into a single SC_CTHREAD, eliminating repeated async_reset_signal_is. + final clockedGroups = {}; + + for (final ssmi + in _synthModuleDefinition.subModuleInstantiations.toList()) { + ssmi as SystemCSynthSubModuleInstantiation; + final m = ssmi.module; + + if (m is Combinational) { + final name = 'comb_$idx'; + idx++; + + final sensitivities = ssmi.inputMapping.values + .where((sl) => !sl.declarationCleared && !sl.isConstant) + .map(_sensitivityName) + .toSet(); + + setupBuf.writeln(' SC_METHOD($name);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + // Build maps keyed by port name (what verilogContents expects) + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + bodyBuf.writeln(' void $name() {'); + for (final c in m.conditionals) { + bodyBuf.write(_conditionalToSC(c, 2, inputsMap, outputsMap)); + } + bodyBuf + ..writeln(' }') + ..writeln(); + ssmi.clearInstantiation(); + } else if (m is Sequential) { + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset')) + .firstOrNull; + + // Detect async reset: either explicitly via asyncReset flag, or + // implicitly when the reset signal is also listed as a trigger + // (e.g. Sequential.multi([clk, reset], reset: reset, ...)). + final isAsync = m.asyncReset || + (resetEntry != null && + ssmi.inputMapping.entries.any((e) => + e.key.contains('trigger') && + e.value.name == resetEntry.value.name)); + + // Resolve ALL trigger entries to (signalName, edge, isPort). + final triggerEdges = m.triggerEdges; + final triggerEntries = ssmi.inputMapping.entries + .where((e) => e.key.contains('trigger')) + .toList(); + + final resolvedTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + + for (final te in triggerEntries) { + final triggerSL = te.value; + // Skip if this trigger is the async reset signal + if (resetEntry != null && triggerSL.name == resetEntry.value.name) { + continue; + } + // Skip constant triggers (e.g. clk <= Const(0) — never toggles) + if (triggerSL.isConstant) { + continue; + } + final isPosedge = triggerEdges + .where((t) => t.portName == te.key) + .firstOrNull + ?.isPosedge ?? + true; + final resolved = _resolveClockAndEdge(triggerSL, isPosedge); + // Skip if the resolved signal is constant + final resolvedSL = _synthModuleDefinition.logicToSynthMap.values + .where((sl) => sl.name == resolved.clockName) + .firstOrNull; + if (resolvedSL != null && resolvedSL.isConstant) { + continue; + } + resolvedTriggers.add(( + signalName: resolved.clockName, + isPosedge: resolved.isPosedge, + isPort: resolved.isPort, + )); + } + + // Deduplicate by (signalName, isPosedge) + final seen = {}; + final uniqueTriggers = + <({String signalName, bool isPosedge, bool isPort})>[]; + for (final t in resolvedTriggers) { + final key = '${t.signalName}|${t.isPosedge}'; + if (seen.add(key)) { + uniqueTriggers.add(t); + } + } + + // Build group key from all trigger signals + reset + final triggerKey = uniqueTriggers + .map((t) => '${t.signalName}:${t.isPosedge}') + .join(','); + final groupKey = '$triggerKey|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: isAsync, + )); + // Add all triggers to the group (dedup handled by emission) + for (final t in uniqueTriggers) { + if (!group.triggers.any((existing) => + existing.signalName == t.signalName && + existing.isPosedge == t.isPosedge)) { + group.triggers.add(t); + } + } + if (isAsync) { + group.isAsyncReset = true; + } + + final inputsMap = ssmi.inputMapping + .map((k, sl) => MapEntry(k, _synthLogicReadExpr(sl))); + final outputsMap = + ssmi.outputMapping.map((k, sl) => MapEntry(k, _scName(sl.name))); + + for (final outName in outputsMap.values) { + group.resetLines.add(' $outName = 0;'); + } + final condBuf = StringBuffer(); + for (final c in m.conditionals) { + condBuf.write(_conditionalToSC(c, 3, inputsMap, outputsMap)); + } + group.whileBodyLines.add(condBuf.toString()); + ssmi.clearInstantiation(); + } else if (m is FlipFlop) { + // Resolve port signals via the input/output mapping + final clkSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('clk')) + .value; + final dSl = ssmi.inputMapping.entries + .firstWhere((e) => e.key.contains('d')) + .value; + final resetEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('reset') && !e.key.contains('Value')) + .firstOrNull; + final enEntry = ssmi.inputMapping.entries + .where((e) => e.key.contains('en')) + .firstOrNull; + final resetValueEntry = ssmi.inputMapping.entries + .where( + (e) => e.key.contains('resetValue') || e.key.contains('Value')) + .firstOrNull; + final qSl = ssmi.outputMapping.values.first; + + final groupKey = + '${clkSl.name}:true|${resetEntry?.value.name ?? '_none_'}'; + final group = clockedGroups.putIfAbsent( + groupKey, + () => _ClockedGroupData( + resetName: resetEntry?.value.name, + isAsyncReset: m.asyncReset, + )); + // FlipFlop always posedge + if (!group.triggers + .any((t) => t.signalName == clkSl.name && t.isPosedge)) { + group.triggers.add(( + signalName: clkSl.name, + isPosedge: true, + isPort: clkSl.isPort(_synthModuleDefinition.module), + )); + } + if (m.asyncReset) { + group.isAsyncReset = true; + } + + // Reset value + String resetValExpr; + if (resetValueEntry != null) { + resetValExpr = _synthLogicReadExpr(resetValueEntry.value); + } else if (m.constantResetValue != null) { + resetValExpr = m.constantResetValue!.toBigInt().toString(); + } else { + resetValExpr = '0'; + } + group.resetLines.add(' ${_scName(qSl.name)} = $resetValExpr;'); + + // Build the data assignment (with optional enable gate) + final assignExpr = + ' ${_scName(qSl.name)} = ${_synthLogicReadExpr(dSl)};\n'; + final bodyLine = enEntry != null + ? ' if (${_synthLogicReadExpr(enEntry.value)}) {\n' + ' $assignExpr' + ' }\n' + : assignExpr; + + // Wrap in sync reset check if needed + if (resetEntry != null && !m.asyncReset) { + group.whileBodyLines + .add(' if (${_scName(resetEntry.value.name)}.read()) {\n' + ' ${_scName(qSl.name)} = $resetValExpr;\n' + ' } else {\n' + ' $bodyLine' + ' }\n'); + } else { + group.whileBodyLines.add(bodyLine); + } + ssmi.clearInstantiation(); + } + } + + // Emit one SC_CTHREAD or SC_THREAD per (clock, reset) group + for (final group in clockedGroups.values) { + final name = 'clocked_$idx'; + idx++; + + final triggers = group.triggers; + + if (triggers.isEmpty) { + // All triggers were constant — skip this group + continue; + } + + // Determine if we can use SC_CTHREAD: + // - exactly one trigger signal + // - that signal is a port (sc_in) + // - only one edge direction + final distinctSignals = triggers.map((t) => t.signalName).toSet(); + final useCthread = distinctSignals.length == 1 && + triggers.first.isPort && + triggers.length == 1; + + if (useCthread) { + final t = triggers.first; + final clockRef = _scName(t.signalName); + final edge = t.isPosedge ? '.pos()' : '.neg()'; + setupBuf.writeln(' SC_CTHREAD($name, $clockRef$edge);'); + if (group.resetName != null && group.isAsyncReset) { + setupBuf.writeln(' async_reset_signal_is(' + '${_scName(group.resetName!)}, true);'); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' wait();') + ..writeln(' while (true) {'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' wait();') + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } else { + // SC_THREAD with explicit wait on events + setupBuf.writeln(' SC_THREAD($name);'); + + // Build wait expression from all trigger events + String waitExpr; + if (distinctSignals.length == 1) { + // Same signal, but both edges + final sig = _scName(triggers.first.signalName); + final edges = triggers.map((t) => t.isPosedge).toSet(); + if (edges.length == 2) { + waitExpr = '$sig.value_changed_event()'; + } else if (edges.first) { + waitExpr = '$sig.posedge_event()'; + } else { + waitExpr = '$sig.negedge_event()'; + } + } else { + // Multiple distinct trigger signals — OR them together + final eventExprs = []; + for (final t in triggers) { + final sig = _scName(t.signalName); + eventExprs + .add('$sig.${t.isPosedge ? 'posedge' : 'negedge'}_event()'); + } + waitExpr = eventExprs.join(' | '); + } + + bodyBuf.writeln(' void $name() {'); + group.resetLines.forEach(bodyBuf.writeln); + bodyBuf + ..writeln(' while (true) {') + ..writeln(' wait($waitExpr);'); + group.whileBodyLines.forEach(bodyBuf.write); + bodyBuf + ..writeln(' }') + ..writeln(' }') + ..writeln(); + } + } + + if (setupBuf.isEmpty && bodyBuf.isEmpty) { + return null; + } + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Regular sub-module instantiations + // ──────────────────────────────────────────────────────────────────── + + /// Returns true if the sub-module is handled inline (not a real child + /// instantiation) — i.e. it is an inline gate, Always, FlipFlop, or clock. + static bool _isHandledInline(SystemCSynthSubModuleInstantiation ssmi) => + !ssmi.needsInstantiation || + ssmi.module is InlineSystemVerilog || + ssmi.module is Always || + ssmi.module is FlipFlop || + ssmi.module is SimpleClockGenerator || + _isInlinableSystemVerilogGate(ssmi.module); + + String _buildSubModuleMembers( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + final instanceType = getInstanceTypeOfModule(ssmi.module); + lines.add(' $instanceType ${ssmi.name}{"${ssmi.name}"};'); + } + return lines.join('\n'); + } + + /// Dummy signal declarations needed for unconnected submodule output ports. + /// Populated by [_buildSubModuleBindings]. + final List _unconnectedOutputSignals = []; + + /// Signal declarations for constants bound to submodule input ports. + /// Populated by [_buildSubModuleBindings]. + final List _constInputSignals = []; + + /// Initialization statements for constant signals (in constructor body). + /// Populated by [_buildSubModuleBindings]. + final List _constInputInits = []; + + String _buildSubModuleBindings( + String Function(Module module) getInstanceTypeOfModule) { + final lines = []; + var unconnIdx = 0; + for (final ssmi in _synthModuleDefinition.subModuleInstantiations) { + ssmi as SystemCSynthSubModuleInstantiation; + if (_isHandledInline(ssmi)) { + continue; + } + + // Bind connected ports (inputs, outputs, inouts) + final allPorts = { + ...ssmi.inputMapping, + ...ssmi.outputMapping, + ...ssmi.inOutMapping, + }; + for (final entry in allPorts.entries) { + if (!entry.value.declarationCleared) { + if (entry.value.isConstant) { + // Constants can't be bound directly to sc_in ports; + // create a signal, initialize it, and bind that. + final constName = _scName('_const_${ssmi.name}' + '_${entry.key}_${_constInputSignals.length}'); + final w = entry.value.width; + final c = entry.value.logics.whereType().first; + final constVal = _typedConstExpr(c.value, c.width); + _constInputSignals + .add(' ${systemCSignalType(w)} $constName{"$constName"};'); + _constInputInits.add(' $constName.write($constVal);'); + lines.add(' ${ssmi.name}.${entry.key}($constName);'); + } else { + lines.add(' ' + '${ssmi.name}.${entry.key}(${_scName(entry.value.name)});'); + } + } + } + + // Bind unconnected ports to dummy signals + // (SystemC requires all sc_in/sc_out ports to be bound) + for (final entry in [ + ...ssmi.outputMapping.entries, + ...ssmi.inputMapping.entries, + ]) { + if (entry.value.declarationCleared) { + final dummyName = '_unused_${ssmi.name}_${entry.key}_$unconnIdx'; + final w = entry.value.width; + _unconnectedOutputSignals + .add(' ${systemCSignalType(w)} $dummyName{"$dummyName"};'); + lines.add(' ${ssmi.name}.${entry.key}($dummyName);'); + unconnIdx++; + } + } + } + return lines.join('\n'); + } + + // ──────────────────────────────────────────────────────────────────── + // Wire assignments + // ──────────────────────────────────────────────────────────────────── + + _MethodResult? _buildWireAssignments() { + if (_synthModuleDefinition.assignments.isEmpty) { + return null; + } + + final bodyLines = []; + final sensitivities = {}; + + // Group partial assignments by destination for concatenated writes + final partialsByDst = >{}; + + for (final assignment in _synthModuleDefinition.assignments) { + if (!assignment.src.isConstant) { + sensitivities.add(_sensitivityName(assignment.src)); + } + if (assignment is PartialSynthAssignment) { + partialsByDst + .putIfAbsent(_scName(assignment.dst.name), () => []) + .add(assignment); + } else { + bodyLines.add(' ${_scName(assignment.dst.name)} = ' + '${_synthLogicReadExpr(assignment.src)};'); + } + } + + // Emit grouped partial assignments as shift-or concatenation + for (final entry in partialsByDst.entries) { + final dstName = entry.key; + final partials = entry.value + ..sort((a, b) => a.dstLowerIndex.compareTo(b.dstLowerIndex)); + + // Find total width from the destination SynthLogic + final dstWidth = partials.last.dstUpperIndex + 1; + final utype = systemCType(dstWidth); + final parts = []; + for (final p in partials) { + final srcExpr = _synthLogicReadExpr(p.src); + if (p.dstLowerIndex == 0) { + parts.add('$utype($srcExpr)'); + } else { + parts.add('($utype($srcExpr) << ${p.dstLowerIndex})'); + } + } + bodyLines.add(' $dstName = ${parts.join(' | ')};'); + } + + final setupBuf = StringBuffer()..writeln(' SC_METHOD(wire_assign);'); + for (final sig in sensitivities) { + setupBuf.writeln(' sensitive << $sig;'); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: ' void wire_assign() {\n' + '${bodyLines.join('\n')}\n' + ' }', + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Conditional → SystemC + // ──────────────────────────────────────────────────────────────────── + + String _conditionalToSC(Conditional conditional, int indent, + Map inputsMap, Map outputsMap) { + final padding = ' ' * indent; + + if (conditional is ConditionalAssign) { + final driverExpr = _resolveDriver(conditional.driver, inputsMap); + final receiver = _resolveReceiver(conditional.receiver, outputsMap); + return '$padding$receiver = $driverExpr;\n'; + } else if (conditional is If) { + return _ifToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is Case) { + return _caseToSC(conditional, indent, inputsMap, outputsMap); + } else if (conditional is ConditionalGroup) { + final buf = StringBuffer(); + for (final c in conditional.conditionals) { + buf.write(_conditionalToSC(c, indent, inputsMap, outputsMap)); + } + return buf.toString(); + } + return ''; + } + + String _ifToSC(If ifBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (final iff in ifBlock.iffs) { + final header = iff == ifBlock.iffs.first + ? 'if' + : iff is Else + ? ' else' + : ' else if'; + final condition = + iff is! Else ? ' (${_resolveDriver(iff.condition, inputsMap)})' : ''; + buf.write('$padding$header$condition {\n'); + for (final c in iff.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + String _caseToSC(Case caseBlock, int indent, Map inputsMap, + Map outputsMap) { + final padding = ' ' * indent; + final buf = StringBuffer(); + final expr = _resolveDriver(caseBlock.expression, inputsMap); + + // Check if all case items have compile-time constant values + final allConst = + caseBlock.items.every((item) => _isConstCaseItem(item.value)); + + // CaseZ requires mask matching — always use if/else + // Non-const case items also require if/else + if (caseBlock is CaseZ || !allConst) { + return _caseToIfElseSC(caseBlock, indent, inputsMap, outputsMap, expr); + } + + buf.writeln('${padding}switch ($expr) {'); + for (final item in caseBlock.items) { + buf.writeln('$padding case ${_constLit(item.value)}:'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + if (caseBlock.defaultItem != null) { + buf.writeln('$padding default:'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 2, inputsMap, outputsMap)); + } + buf.writeln('$padding break;'); + } + buf.writeln('$padding}'); + return buf.toString(); + } + + /// Checks whether a case item value is a compile-time constant. + bool _isConstCaseItem(dynamic value) { + if (value is Const) { + return true; + } + if (value is LogicValue) { + return true; + } + if (value is Logic) { + if (value.srcConnection is Const) { + return true; + } + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + return true; + } + return false; + } + return true; // int, string, etc. + } + + /// Converts a Case/CaseZ block to if/else chain (for non-const items + /// or CaseZ with z-masks). + String _caseToIfElseSC( + Case caseBlock, + int indent, + Map inputsMap, + Map outputsMap, + String expr) { + final padding = ' ' * indent; + final buf = StringBuffer(); + + for (var i = 0; i < caseBlock.items.length; i++) { + final item = caseBlock.items[i]; + final condition = _caseItemCondition(item.value, expr, inputsMap, + isCaseZ: caseBlock is CaseZ); + final header = i == 0 ? 'if' : ' else if'; + buf.write('$padding$header ($condition) {\n'); + for (final c in item.then) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + if (caseBlock.defaultItem != null) { + buf.write(' else {\n'); + for (final c in caseBlock.defaultItem!) { + buf.write(_conditionalToSC(c, indent + 1, inputsMap, outputsMap)); + } + buf.write('$padding}'); + } + buf.writeln(); + return buf.toString(); + } + + /// Generates the condition expression for a case item comparison. + String _caseItemCondition( + dynamic value, String expr, Map inputsMap, + {bool isCaseZ = false}) { + // Extract LogicValue from Const for CaseZ mask matching + LogicValue? lv; + if (value is Const) { + lv = value.value; + } else if (value is LogicValue) { + lv = value; + } + if (isCaseZ && lv != null && !lv.isValid) { + // CaseZ: create mask comparison (expr & mask) == pattern + // z bits become don't-care (mask out those bits) + final width = lv.width; + // z→0 in mask, 0/1→1 in mask + var maskStr = ''; + var patStr = ''; + for (var i = width - 1; i >= 0; i--) { + final bit = lv[i]; + if (bit == LogicValue.z || bit == LogicValue.x) { + maskStr += '0'; + patStr += '0'; + } else { + maskStr += '1'; + patStr += bit == LogicValue.one ? '1' : '0'; + } + } + final maskVal = BigInt.parse(maskStr, radix: 2); + final patVal = BigInt.parse(patStr, radix: 2); + return '($expr & $maskVal) == $patVal'; + } + if (value is Logic && value is! Const) { + final resolved = _resolveDriver(value, inputsMap); + return '$expr == $resolved'; + } + return '$expr == ${_constLit(value)}'; + } + + /// Resolves a driver Logic to a SystemC read expression using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveDriver(Logic driver, Map inputsMap) { + if (driver is Const) { + return _constLit(driver); + } + // Look up via logicToSynthMap — the SynthLogic has the canonical name + final sl = _synthModuleDefinition.logicToSynthMap[driver]; + if (sl != null) { + return _synthLogicReadExpr(sl); + } + // Try to find via source connection chain — handles cases where + // the Logic object isn't directly in the map but its source is + var src = driver.srcConnection; + while (src != null) { + final srcSl = _synthModuleDefinition.logicToSynthMap[src]; + if (srcSl != null) { + return _synthLogicReadExpr(srcSl); + } + src = src.srcConnection; + } + // Fallback: try inputsMap by port name + if (inputsMap.containsKey(driver.name)) { + return inputsMap[driver.name]!; + } + return '${_scName(driver.name)}.read()'; + } + + /// Resolves a receiver Logic to a SystemC signal name using the + /// SynthModuleDefinition's logicToSynthMap to find the canonical name. + String _resolveReceiver(Logic receiver, Map outputsMap) { + // Look up via logicToSynthMap + final sl = _synthModuleDefinition.logicToSynthMap[receiver]; + if (sl != null) { + return _scName(sl.name); + } + // Fallback + if (outputsMap.containsKey(receiver.name)) { + return outputsMap[receiver.name]!; + } + return _scName(receiver.name); + } + + String _constLit(dynamic value) { + if (value is Const) { + if (value.value.isValid) { + return value.value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is LogicValue) { + if (value.isValid) { + return value.toBigInt().toString(); + } + return '0'; // x/z → 0 in SystemC + } else if (value is Logic) { + // If the Logic is driven by a Const, resolve to integer literal + if (value.srcConnection is Const) { + final cv = (value.srcConnection! as Const).value; + return cv.isValid ? cv.toBigInt().toString() : '0'; + } + // Check logicToSynthMap for a constant SynthLogic + final sl = _synthModuleDefinition.logicToSynthMap[value]; + if (sl != null && sl.isConstant) { + final constLogic = sl.logics.whereType().firstOrNull; + if (constLogic != null) { + return constLogic.value.isValid + ? constLogic.value.toBigInt().toString() + : '0'; + } + } + // Fallback: use signal read expression + return '${value.name}.read()'; + } + return value.toString(); + } + + // ──────────────────────────────────────────────────────────────────── + // Build all sections + // ──────────────────────────────────────────────────────────────────── + + void _buildModuleBody( + String Function(Module module) getInstanceTypeOfModule) { + _subMembers = _buildSubModuleMembers(getInstanceTypeOfModule); + + final inlineGates = _buildInlineGates(); + final processes = _buildProcesses(); + final wireAssigns = _buildWireAssignments(); + final arrayAssembly = _buildArrayAssemblyMethod(); + final subBindings = _buildSubModuleBindings(getInstanceTypeOfModule); + + // Build internal signals, appending dummy signals for unconnected + // submodule outputs (populated by _buildSubModuleBindings above). + final baseSigs = _buildInternalSignals(); + _internalSigs = [ + baseSigs, + ..._unconnectedOutputSignals, + ..._constInputSignals, + ].where((s) => s.isNotEmpty).join('\n'); + + final ctorParts = [ + if (_constInputInits.isNotEmpty) _constInputInits.join('\n'), + if (inlineGates != null) inlineGates.setup, + if (processes != null) processes.setup, + if (wireAssigns != null) wireAssigns.setup, + if (arrayAssembly != null) arrayAssembly.setup, + if (subBindings.isNotEmpty) subBindings, + ]; + _ctorBody = ctorParts.join(); + + final bodyParts = [ + if (inlineGates != null) inlineGates.body, + if (processes != null) processes.body, + if (wireAssigns != null) wireAssigns.body, + if (arrayAssembly != null) arrayAssembly.body, + ]; + _methodBodies = bodyParts.where((s) => s.isNotEmpty).join('\n'); + } + + /// Builds an SC_METHOD that assembles individual array element signals + /// back into their parent signal via concatenation. + _MethodResult? _buildArrayAssemblyMethod() { + if (_arrayElementsByParent.isEmpty) { + return null; + } + + final setupBuf = StringBuffer(); + final bodyBuf = StringBuffer(); + var methodIdx = 0; + + for (final entry in _arrayElementsByParent.entries) { + final parentName = _scName(entry.key); + final elements = entry.value; + final methodName = 'array_assemble_$methodIdx'; + methodIdx++; + + setupBuf.writeln(' SC_METHOD($methodName);'); + for (final elem in elements) { + setupBuf.writeln(' sensitive << ${_scName(elem.elemName)};'); + } + + // Build concatenation: (elem[N-1], ..., elem[1], elem[0]) + // SystemC concat is MSB-first, so highest index first + // Wrap 1-bit (bool) elements in sc_uint<1>() for proper concat + final concatParts = elements.reversed.map((e) { + final read = '${_scName(e.elemName)}.read()'; + return e.width == 1 ? 'sc_uint<1>($read)' : read; + }).toList(); + + bodyBuf + ..writeln(' void $methodName() {') + ..writeln(' $parentName = (${concatParts.join(', ')});') + ..writeln(' }') + ..writeln(); + } + + return _MethodResult( + setup: setupBuf.toString(), + body: bodyBuf.toString(), + ); + } + + // ──────────────────────────────────────────────────────────────────── + // Final assembly + // ──────────────────────────────────────────────────────────────────── + + String _toSystemC() { + final moduleName = getInstanceTypeOfModule(module); + final buf = StringBuffer()..writeln('SC_MODULE($moduleName) {'); + + if (_portsString.isNotEmpty) { + buf.writeln(_portsString); + } + if (_internalSigs.isNotEmpty) { + buf + ..writeln() + ..writeln(_internalSigs); + } + if (_subMembers.isNotEmpty) { + buf + ..writeln() + ..writeln(_subMembers); + } + + buf + ..writeln() + ..writeln(' SC_CTOR($moduleName) {'); + if (_ctorBody.isNotEmpty) { + buf.write(_ctorBody); + } + buf.writeln(' }'); + + if (_methodBodies.isNotEmpty) { + buf + ..writeln() + ..write(_methodBodies) + ..writeln(); + } + + buf.writeln('};'); + return buf.toString(); + } +} + +/// Helper to hold a constructor setup string and method body string. +class _MethodResult { + final String setup; + final String body; + const _MethodResult({required this.setup, required this.body}); +} + +/// Collects clocked process data for consolidation by (clock, reset) pair. +class _ClockedGroupData { + final String? resetName; + bool isAsyncReset; + + /// All distinct trigger events (signal name, edge, and whether it's a port). + final List<({String signalName, bool isPosedge, bool isPort})> triggers = []; + + final List resetLines = []; + final List whileBodyLines = []; + _ClockedGroupData({this.resetName, this.isAsyncReset = false}); +} diff --git a/lib/src/synthesizers/systemverilog/sv_service.dart b/lib/src/synthesizers/systemverilog/sv_service.dart new file mode 100644 index 000000000..d2d9f359e --- /dev/null +++ b/lib/src/synthesizers/systemverilog/sv_service.dart @@ -0,0 +1,152 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// sv_service.dart +// Service wrapper for SystemVerilog synthesis. +// +// 2026 April 25 +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/config.dart'; +import 'package:rohd/src/utilities/timestamper.dart'; + +/// A service that wraps SystemVerilog synthesis of a [Module] hierarchy. +/// +/// Provides access to the generated SV file contents and per-module +/// synthesis results, and optionally registers with [ModuleServices] +/// for DevTools inspection. +/// +/// Example: +/// ```dart +/// final dut = MyModule(...); +/// await dut.build(); +/// final sv = SvService(dut); +/// +/// // Write individual .sv files: +/// sv.writeFiles('build/'); +/// +/// // Or get the concatenated output (like generateSynth): +/// print(sv.allContents); +/// ``` +class SvService { + /// The separator inserted between module definitions in the + /// concatenated single-file output from [allContents]. + /// + /// Matches the format historically produced by `Module.generateSynth()`. + static const moduleSeparator = '\n\n////////////////////\n\n'; + + /// The top-level [Module] being synthesized. + final Module module; + + /// The underlying [SynthBuilder] that drove synthesis. + late final SynthBuilder synthBuilder; + + /// The generated file contents (one per unique module definition). + late final List fileContents; + + /// Creates an [SvService] for [module]. + /// + /// [module] must already be built. Set [register] to `true` (the + /// default) to register this service with [ModuleServices] for + /// DevTools access. + /// + /// If [outputPath] is provided, the concatenated SV output (with + /// header) is written to that file. The parent directory is created + /// if needed. + SvService(this.module, {bool register = true, String? outputPath}) { + if (!module.hasBuilt) { + throw Exception('Module must be built before creating SvService. ' + 'Call build() first.'); + } + + synthBuilder = SynthBuilder(module, SystemVerilogSynthesizer()); + fileContents = synthBuilder.getSynthFileContents(); + + if (outputPath != null) { + final file = File(outputPath); + file.parent.createSync(recursive: true); + file.writeAsStringSync(synthOutput); + } + + if (register) { + ModuleServices.instance.svService = this; + } + } + + /// All [SynthesisResult]s produced by synthesis. + Set get synthesisResults => synthBuilder.synthesisResults; + + // ─── Single-file output ─────────────────────────────────────── + + /// Returns the concatenated SystemVerilog module definitions as a + /// single string, without the generation header. + /// + /// For the full output with header (matching `Module.generateSynth()`), + /// use [synthOutput]. + String get allContents => + fileContents.map((fc) => fc.contents).join(moduleSeparator); + + /// The ROHD generation header prepended to single-file output. + String get synthHeader => ''' +/** + * Generated by ROHD - www.github.com/intel/rohd + * Generation time: ${Timestamper.stamp()} + * ROHD Version: ${Config.version} + */ + +'''; + + /// Returns the full single-file SystemVerilog output with header, + /// identical to `Module.generateSynth()`. + String get synthOutput => synthHeader + allContents; + + /// Returns a map from module definition name to its SV file contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + + /// Returns a map from module definition name + /// ([Module.definitionName]) to its SV file contents. + /// + /// This uses the original definition name (not uniquified), matching + /// the keys used by FLC trace data. + Map get contentsByDefinitionName { + final result = {}; + for (final sr in synthesisResults) { + final defName = sr.module.definitionName; + final instanceName = sr.instanceTypeName; + // Find the file content matching this instance type name. + final fc = fileContents.firstWhereOrNull((f) => f.name == instanceName); + if (fc != null) { + result[defName] = fc.contents; + } + } + return result; + } + + /// Writes each module's SV to a separate file in [directory]. + /// + /// Files are named `.sv`. + void writeFiles(String directory) { + final dir = Directory(directory)..createSync(recursive: true); + for (final fc in fileContents) { + File('${dir.path}/${fc.name}.sv').writeAsStringSync(fc.contents); + } + } + + /// Returns a JSON-serialisable summary of the SV synthesis. + /// + /// Contains the list of generated module definition names. + Map toJson() => { + 'modules': [ + for (final fc in fileContents) fc.name, + ], + }; +} diff --git a/lib/src/synthesizers/systemverilog/systemverilog.dart b/lib/src/synthesizers/systemverilog/systemverilog.dart index 281b05df9..e5f772e44 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog.dart @@ -1,5 +1,6 @@ // Copyright (C) 2021-2024 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause +export 'sv_service.dart'; export 'systemverilog_mixins.dart'; export 'systemverilog_synthesizer.dart'; diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart index d8b5bae36..062647ac3 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synthesizer.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // systemverilog_synthesizer.dart diff --git a/lib/src/synthesizers/utilities/synth_logic.dart b/lib/src/synthesizers/utilities/synth_logic.dart index c3026a0d5..8fcbc014a 100644 --- a/lib/src/synthesizers/utilities/synth_logic.dart +++ b/lib/src/synthesizers/utilities/synth_logic.dart @@ -11,8 +11,8 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:rohd/src/utilities/namer.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents a logic signal in the generated code within a module. @internal @@ -212,92 +212,25 @@ class SynthLogic { /// The name of this, if it has been picked. String? _name; - /// Picks a [name]. + /// Picks a [name] using the module's signal namer. /// /// Must be called exactly once. - void pickName(Uniquifier uniquifier) { + void pickName() { assert(_name == null, 'Should only pick a name once.'); - _name = _findName(uniquifier); + _name = _findName(); } /// Finds the best name from the collection of [Logic]s. - String _findName(Uniquifier uniquifier) { - // check for const - if (_constLogic != null) { - if (!_constNameDisallowed) { - return _constLogic!.value.toString(); - } else { - assert( - logics.length > 1, - 'If there is a constant, but the const name is not allowed, ' - 'there needs to be another option', - ); - } - } - - // check for reserved - if (_reservedLogic != null) { - return uniquifier.getUniqueName( - initialName: _reservedLogic!.name, - reserved: true, - ); - } - - // check for renameable - if (_renameableLogic != null) { - return uniquifier.getUniqueName( - initialName: _renameableLogic!.preferredSynthName, - ); - } - - // pick a preferred, available, mergeable name, if one exists - final unpreferredMergeableLogics = []; - final uniquifiableMergeableLogics = []; - for (final mergeableLogic in _mergeableLogics) { - if (Naming.isUnpreferred(mergeableLogic.preferredSynthName)) { - unpreferredMergeableLogics.add(mergeableLogic); - } else if (!uniquifier.isAvailable(mergeableLogic.preferredSynthName)) { - uniquifiableMergeableLogics.add(mergeableLogic); - } else { - return uniquifier.getUniqueName( - initialName: mergeableLogic.preferredSynthName, - ); - } - } - - // uniquify a preferred, mergeable name, if one exists - if (uniquifiableMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: uniquifiableMergeableLogics.first.preferredSynthName, - ); - } - - // pick an available unpreferred mergeable name, if one exists, otherwise - // uniquify an unpreferred mergeable name - if (unpreferredMergeableLogics.isNotEmpty) { - return uniquifier.getUniqueName( - initialName: unpreferredMergeableLogics - .firstWhereOrNull( - (element) => - uniquifier.isAvailable(element.preferredSynthName), - ) - ?.preferredSynthName ?? - unpreferredMergeableLogics.first.preferredSynthName, + /// + /// Delegates to signal namer which handles constant value naming, priority + /// selection, and uniquification via the module's shared namespace. + String _findName() => + parentSynthModuleDefinition.module.namer.signalNameOfBest( + logics, + constValue: _constLogic, + constNameDisallowed: _constNameDisallowed, ); - } - - // pick anything (unnamed) and uniquify as necessary (considering preferred) - // no need to prefer an available one here, since it's all unnamed - return uniquifier.getUniqueName( - initialName: _unnamedLogics - .firstWhereOrNull( - (element) => !Naming.isUnpreferred(element.preferredSynthName), - ) - ?.preferredSynthName ?? - _unnamedLogics.first.preferredSynthName, - ); - } /// Creates an instance to represent [initialLogic] and any that merge /// into it. @@ -404,7 +337,7 @@ class SynthLogic { @override String toString() => '${_name == null ? 'null' : '"$name"'}, ' - 'logics contained: ${logics.map((e) => e.preferredSynthName).toList()}'; + 'logics contained: ${logics.map(Namer.baseName).toList()}'; /// Provides a definition for a range in SV from a width. static String _widthToRangeDef(int width, {bool forceRange = false}) { @@ -551,17 +484,3 @@ class SynthLogicArrayElement extends SynthLogic { ' parentArray=($parentArray), element ${logic.arrayIndex}, logic: $logic' ' logics contained: ${logics.map((e) => e.name).toList()}'; } - -extension on Logic { - /// Returns the preferred name for this [Logic] while generating in the synth - /// stack. - String get preferredSynthName => naming == Naming.reserved - // if reserved, keep the exact name - ? name - : isArrayMember - // arrays nicely name their elements already - ? name - // sanitize to remove any `.` in struct names - // the base `name` will be returned if not a structure. - : Sanitizer.sanitizeSV(structureName); -} diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index 37ebfb323..9ea120646 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -14,7 +14,6 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/collections/traverseable_collection.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// A version of [BusSubset] that can be used for slicing on [LogicStructure] /// ports. @@ -110,10 +109,6 @@ class SynthModuleDefinition { @override String toString() => "module name: '${module.name}'"; - /// Used to uniquify any identifiers, including signal names - /// and module instances. - final Uniquifier _synthInstantiationNameUniquifier; - /// Indicates whether [logic] has a corresponding present [SynthLogic] in /// this definition. @internal @@ -289,14 +284,7 @@ class SynthModuleDefinition { /// Creates a new definition representation for this [module]. SynthModuleDefinition(this.module) - : _synthInstantiationNameUniquifier = Uniquifier( - reservedNames: { - ...module.inputs.keys, - ...module.outputs.keys, - ...module.inOuts.keys, - }, - ), - assert( + : assert( !(module is SystemVerilog && module.generatedDefinitionType == DefinitionGenerationType.none), @@ -465,6 +453,7 @@ class SynthModuleDefinition { final receiverIsSubModuleOutput = receiver.isOutput && (receiver.parentModule?.parent == module); + if (receiverIsSubModuleOutput) { final subModule = receiver.parentModule!; @@ -513,6 +502,7 @@ class SynthModuleDefinition { _collapseArrays(); _collapseAssignments(); _assignSubmodulePortMapping(); + _pruneUnused(); process(); _pickNames(); @@ -767,49 +757,59 @@ class SynthModuleDefinition { } /// Picks names of signals and sub-modules. + /// + /// Signal names are read from `Namer.signalNameOf` (for user-created + /// [Logic] objects) or kept as literal constants and are allocated from + /// `Namer.signalNameOf`. Submodule instance names are allocated + /// from `Namer.allocateRawName`. All names share a single + /// namespace managed by the module's `Namer`. void _pickNames() { - // first ports get priority + // Name allocation order matters — earlier claims get the unsuffixed name + // when there are collisions. This matches production ROHD priority: + // 1. Ports (reserved by _initNamespace, claimed via signalName) + // 2. Reserved submodule instances + // 3. Reserved internal signals + // 4. Non-reserved submodule instances + // 5. Non-reserved internal signals for (final input in inputs) { - input.pickName(_synthInstantiationNameUniquifier); + input.pickName(); } for (final output in outputs) { - output.pickName(_synthInstantiationNameUniquifier); + output.pickName(); } for (final inOut in inOuts) { - inOut.pickName(_synthInstantiationNameUniquifier); + inOut.pickName(); } - // pick names of *reserved* submodule instances - final nonReservedSubmodules = []; + // Reserved submodule instances first (they assert their exact name). for (final submodule in subModuleInstantiations) { if (submodule.module.reserveName) { - submodule.pickName(_synthInstantiationNameUniquifier); + submodule.pickName(module); assert(submodule.module.name == submodule.name, 'Expect reserved names to retain their name.'); - } else { - nonReservedSubmodules.add(submodule); } } - // then *reserved* internal signals get priority + // Reserved internal signals next. final nonReservedSignals = []; for (final signal in internalSignals) { if (signal.isReserved) { - signal.pickName(_synthInstantiationNameUniquifier); + signal.pickName(); } else { nonReservedSignals.add(signal); } } - // then submodule instances - for (final submodule in nonReservedSubmodules - .where((element) => element.needsInstantiation)) { - submodule.pickName(_synthInstantiationNameUniquifier); + // Then non-reserved submodule instances. + for (final submodule in subModuleInstantiations) { + if (!submodule.module.reserveName && submodule.needsInstantiation) { + submodule.pickName(module); + } } - // then the rest of the internal signals + // Then the rest of the internal signals. for (final signal in nonReservedSignals) { - signal.pickName(_synthInstantiationNameUniquifier); + signal.pickName(); } } diff --git a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart index 80a415a09..cf7da28e8 100644 --- a/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart +++ b/lib/src/synthesizers/utilities/synth_sub_module_instantiation.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2025 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // synth_sub_module_instantiation.dart @@ -11,7 +11,6 @@ import 'dart:collection'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/utilities/utilities.dart'; -import 'package:rohd/src/utilities/uniquifier.dart'; /// Represents an instantiation of a module within another module. class SynthSubModuleInstantiation { @@ -25,13 +24,15 @@ class SynthSubModuleInstantiation { String get name => _name!; /// Selects a name for this module instance. Must be called exactly once. - void pickName(Uniquifier uniquifier) { + /// + /// Names are allocated from [parentModule]'s `Namer`'s shared namespace + /// via `Namer.allocateName`. + void pickName(Module parentModule) { assert(_name == null, 'Should only pick a name once.'); - _name = uniquifier.getUniqueName( - initialName: module.uniqueInstanceName, + _name = parentModule.namer.allocateRawName( + module.uniqueInstanceName, reserved: module.reserveName, - nullStarter: 'm', ); } diff --git a/lib/src/utilities/namer.dart b/lib/src/utilities/namer.dart new file mode 100644 index 000000000..efbe8e3e4 --- /dev/null +++ b/lib/src/utilities/namer.dart @@ -0,0 +1,233 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// namer.dart +// Central collision-free naming for signals and instances within a module. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/sanitizer.dart'; +import 'package:rohd/src/utilities/uniquifier.dart'; + +/// Central namer that manages collision-free names for both signals and +/// submodule instances within a single module scope. +/// +/// All identifiers (signals and instances) share a single namespace, +/// ensuring no name collisions in the generated SystemVerilog. +/// +/// Port names are reserved at construction time. Internal signal names +/// are assigned lazily on the first [signalNameOf] call. Instance names +/// are allocated explicitly via [allocateRawName]. +@internal +class Namer { + // ─── Shared namespace ─────────────────────────────────────────── + + final Uniquifier _uniquifier; + + /// Cache of resolved names for internal (non-port) signals only. + /// Port names are returned directly from [_portLogics] and never cached here. + final Map _signalNames = {}; + + /// The set of port [Logic] objects, for O(1) port membership tests. + final Set _portLogics; + + // ─── Construction ─────────────────────────────────────────────── + + Namer._({ + required Uniquifier uniquifier, + required Set portLogics, + }) : _uniquifier = uniquifier, + _portLogics = portLogics; + + /// Creates a [Namer] for the given module ports. + /// + /// Port names are reserved in the shared namespace. Port names are + /// guaranteed sanitary by [Module]'s `_checkForSafePortName`. + factory Namer.forModule({ + required Map inputs, + required Map outputs, + required Map inOuts, + }) { + final portLogics = { + ...inputs.values, + ...outputs.values, + ...inOuts.values, + }; + + final uniquifier = Uniquifier(); + for (final logic in portLogics) { + uniquifier.getUniqueName(initialName: logic.name, reserved: true); + } + + return Namer._( + uniquifier: uniquifier, + portLogics: portLogics, + ); + } + + // ─── Name availability / allocation ───────────────────────────── + + /// Returns `true` if [name] has not yet been claimed in the namespace. + bool isAvailable(String name) => _uniquifier.isAvailable(name); + + /// Allocates a collision-free name in the shared namespace. + /// + /// When [reserved] is `true`, the exact [baseName] (after sanitization) + /// is claimed without modification; an exception is thrown if it collides. + String allocateRawName(String baseName, {bool reserved = false}) => + _uniquifier.getUniqueName( + initialName: Sanitizer.sanitizeSV(baseName), + reserved: reserved, + ); + + // ─── Signal naming (Logic → String) ───────────────────────────── + + /// Returns the canonical name for [logic]. + /// + /// The first call for a given [logic] allocates a collision-free name + /// via the underlying [Uniquifier]. Subsequent calls return the cached + /// result in O(1). + String signalNameOf(Logic logic) { + final cached = _signalNames[logic]; + if (cached != null) { + return cached; + } + + if (_portLogics.contains(logic)) { + return logic.name; + } + + String base; + final isReservedInternal = logic.naming == Naming.reserved && !logic.isPort; + if (logic.naming == Naming.reserved || logic.isArrayMember) { + base = logic.name; + } else { + base = Sanitizer.sanitizeSV(logic.structureName); + } + + final name = _uniquifier.getUniqueName( + initialName: base, + reserved: isReservedInternal, + ); + _signalNames[logic] = name; + return name; + } + + /// The base name that would be used for [logic] before uniquification. + static String baseName(Logic logic) => + (logic.naming == Naming.reserved || logic.isArrayMember) + ? logic.name + : Sanitizer.sanitizeSV(logic.structureName); + + /// Chooses the best name from a pool of merged [Logic] signals. + /// + /// When [constValue] is provided and [constNameDisallowed] is `false`, + /// the constant's value string is used directly as the name (no + /// uniquification). When [constNameDisallowed] is `true`, the constant + /// is excluded from the candidate pool and the normal priority applies. + /// + /// Priority (after constant handling): + /// 1. Port of this module (always wins — its name is already reserved). + /// 2. Reserved internal signal (exact name, throws on collision). + /// 3. Renameable signal. + /// 4. Preferred-available mergeable (base name not yet taken). + /// 5. Preferred-uniquifiable mergeable. + /// 6. Available-unpreferred mergeable. + /// 7. First unpreferred mergeable. + /// 8. Unnamed (prefer non-unpreferred base name). + /// + /// The winning name is allocated once and cached for the chosen [Logic]. + /// All other non-port [Logic]s in [candidates] are also cached to the + /// same name. + String signalNameOfBest( + Iterable candidates, { + Const? constValue, + bool constNameDisallowed = false, + }) { + if (constValue != null && !constNameDisallowed) { + return constValue.value.toString(); + } + + Logic? port; + Logic? reserved; + Logic? renameable; + final preferredMergeable = []; + final unpreferredMergeable = []; + final unnamed = []; + + for (final logic in candidates) { + if (_portLogics.contains(logic)) { + port = logic; + } else if (logic.isPort) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else if (logic.naming == Naming.reserved) { + reserved = logic; + } else if (logic.naming == Naming.renameable) { + renameable = logic; + } else if (logic.naming == Naming.mergeable) { + if (Naming.isUnpreferred(baseName(logic))) { + unpreferredMergeable.add(logic); + } else { + preferredMergeable.add(logic); + } + } else { + unnamed.add(logic); + } + } + + if (port != null) { + return _nameAndCacheAll(port, candidates); + } + + if (reserved != null) { + return _nameAndCacheAll(reserved, candidates); + } + + if (renameable != null) { + return _nameAndCacheAll(renameable, candidates); + } + + if (preferredMergeable.isNotEmpty) { + final best = preferredMergeable + .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? + preferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + if (unpreferredMergeable.isNotEmpty) { + final best = unpreferredMergeable + .firstWhereOrNull((e) => isAvailable(baseName(e))) ?? + unpreferredMergeable.first; + return _nameAndCacheAll(best, candidates); + } + + if (unnamed.isNotEmpty) { + final best = + unnamed.firstWhereOrNull((e) => !Naming.isUnpreferred(baseName(e))) ?? + unnamed.first; + return _nameAndCacheAll(best, candidates); + } + + throw StateError('No Logic candidates to name.'); + } + + /// Names [chosen] via [signalNameOf], then caches the same name for all + /// other non-port [Logic]s in [all]. + String _nameAndCacheAll(Logic chosen, Iterable all) { + final name = signalNameOf(chosen); + for (final logic in all) { + if (!identical(logic, chosen) && !_portLogics.contains(logic)) { + _signalNames[logic] = name; + } + } + return name; + } +} diff --git a/lib/src/utilities/simcompare.dart b/lib/src/utilities/simcompare.dart index d7850df4e..d7e9c3eef 100644 --- a/lib/src/utilities/simcompare.dart +++ b/lib/src/utilities/simcompare.dart @@ -14,6 +14,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemc/systemc_synthesis_result.dart'; import 'package:rohd/src/utilities/uniquifier.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; @@ -436,4 +437,792 @@ abstract class SimCompare { } return true; } + + // ══════════════════════════════════════════════════════════════════════ + // SystemC simulation (Accellera SystemC) + // ══════════════════════════════════════════════════════════════════════ + + /// The default SystemC installation path (Accellera). + static const _systemCDefaultHome = '/opt/systemc/include'; + static const _systemCDefaultLib = '/opt/systemc/lib'; + + /// Cache of compiled SystemC executables keyed by generated code hash. + static final _compilationCache = {}; + + /// Path to the precompiled header, built lazily on first compilation. + static String? _pchPath; + + /// Builds the precompiled header for systemc.h if not already done. + /// Returns the directory containing systemc.h.gch, or null on failure. + /// + /// In CI, the PCH is pre-built by `tool/gh_actions/setup_systemc_pch.sh` + /// before tests run, so this just finds it on disk. Locally it builds + /// on first use (safe because local runs are typically sequential). + static String? _ensurePch(String scHome, String cxxStd) { + if (_pchPath != null) { + return _pchPath; + } + + const dir = 'tmp_test'; + const pchDir = '$dir/pch'; + const gchFile = '$pchDir/systemc.h.gch'; + + // Reuse if already on disk (pre-built by CI or a previous run) + if (File(gchFile).existsSync()) { + return _pchPath = pchDir; + } + + Directory(pchDir).createSync(recursive: true); + + // Copy the original header next to the .gch so g++ matches them + File('$scHome/systemc.h').copySync('$pchDir/systemc.h'); + + final args = [ + '-std=$cxxStd', + '-I$scHome', + '-x', + 'c++-header', + '-o', + gchFile, + '$scHome/systemc.h', + ]; + final result = Process.runSync('g++', args); + if (result.exitCode != 0) { + print('PCH compilation failed (falling back to normal headers):'); + print(result.stderr); + return null; + } + + return _pchPath = pchDir; + } + + /// Cached path to the shared Makefile (one per compiler-flags combination). + static String? _makefilePath; + + /// Creates a shared Makefile once, reused for all compilations. + /// TARGET and SRC are passed as make variables at invocation time. + /// Uses atomic write (write-to-temp + rename) to avoid races when + /// multiple test isolates create the file concurrently. + static String _ensureMakefile({ + required String dir, + required String cxxStd, + required String pchInclude, + required String scHome, + required String scLib, + }) { + final path = '$dir/Makefile_sc'; + + if (_makefilePath != null && File(path).existsSync()) { + return _makefilePath!; + } + + // If already on disk from another isolate, just reuse it + if (File(path).existsSync()) { + return _makefilePath = path; + } + + final contents = ''' +CXX = g++ +CXXFLAGS = -std=$cxxStd -pipe $pchInclude-I$scHome +LDFLAGS = -L$scLib -lsystemc + +all: \$(TARGET) + +\$(TARGET): \$(SRC) +\t\$(CXX) \$(CXXFLAGS) -o \$(TARGET) \$(SRC) \$(LDFLAGS) + +.PHONY: all +'''; + Directory(dir).createSync(recursive: true); + + // Atomic write: write to temp file, then rename so concurrent + // readers never see a truncated Makefile. + File('$path.${pid.hashCode}') + ..writeAsStringSync(contents) + ..renameSync(path); + + return _makefilePath = path; + } + + /// Resolves SystemC home/lib paths. If explicit paths are given, uses them. + /// Otherwise uses the default Accellera install paths. + static (String?, String?) _resolveSystemCPaths(String scHome, String scLib) { + if (scHome.isNotEmpty && scLib.isNotEmpty) { + if (Directory(scHome).existsSync()) { + return (scHome, scLib); + } + return (null, null); + } + if (Directory(_systemCDefaultHome).existsSync()) { + return (_systemCDefaultHome, _systemCDefaultLib); + } + return (null, null); + } + + /// Detects the C++ standard the SystemC library was compiled with + /// by inspecting the `sc_api_version` symbol in libsystemc.so. + static String _detectCxxStandard(String scLib) { + try { + final result = Process.runSync('nm', ['-D', '$scLib/libsystemc.so']); + if (result.exitCode == 0) { + final output = result.stdout as String; + if (output.contains('cxx202002L')) { + return 'c++20'; + } + if (output.contains('cxx201703L')) { + return 'c++17'; + } + } + } on Object { + // Fall through to default + } + return 'c++20'; + } + + /// Cleans up all cached SystemC executables and the precompiled header. + /// Call from `tearDownAll` in tests. + /// + /// If [keepPch] is true (the default), the precompiled header is preserved + /// for faster subsequent runs. Pass `keepPch: false` to remove everything. + static void cleanupSystemCCache({bool keepPch = true}) { + _compilationCache.clear(); + _pchPath = null; + _makefilePath = null; + if (kIsWeb) { + return; + } + try { + final dir = Directory('tmp_test'); + if (dir.existsSync()) { + if (keepPch) { + for (final entity in dir.listSync()) { + if (entity is Directory && entity.path.endsWith('/pch')) { + continue; + } + entity.deleteSync(recursive: true); + } + } else { + dir + ..deleteSync(recursive: true) + ..createSync(); + } + } + } on Exception catch (_) {} + } + + /// Compiles a SystemC module into a reusable stdin-driven executable. + /// + /// Returns a [SystemCExecutable] that can be used to run multiple vector + /// sets without recompilation. Use in `setUpAll` for test groups. + /// Results are cached — calling this with the same module definition + /// returns the previously compiled binary. + static SystemCExecutable? buildSystemCExecutable( + Module module, { + String? moduleName, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + }) { + if (kIsWeb) { + return null; + } + + final scHome = systemcHome ?? ''; + final scLib = systemcLib ?? ''; + final (resolvedHome, resolvedLib) = _resolveSystemCPaths(scHome, scLib); + + if (resolvedHome == null || resolvedLib == null) { + print('SystemC installation not found'); + return null; + } + + final topModule = moduleName ?? module.definitionName; + final generatedSystemC = module.generateSystemC(); + + // Check compilation cache + final cacheKey = generatedSystemC.hashCode; + if (_compilationCache.containsKey(cacheKey)) { + return _compilationCache[cacheKey]!; + } + + // Identify clock signals + final clockSignals = {}; + if (clockName != null) { + clockSignals.add(clockName); + } + for (final input in module.inputs.entries) { + final name = input.key; + if (clockSignals.isEmpty && (name == 'clk' || name.contains('clock'))) { + clockSignals.add(name); + } + } + final promotedClocks = {}; + for (final sub in module.subModules) { + if (sub is SimpleClockGenerator) { + final clkSigName = sub.clk.name; + promotedClocks.add(clkSigName); + clockSignals.add(clkSigName); + } + } + + // Collect ALL module ports for the stdin-driven harness + final inputPorts = {}; + for (final input in module.inputs.entries) { + if (promotedClocks.contains(input.key)) { + continue; + } + inputPorts[input.key] = input.value.width; + } + final outputPorts = {}; + for (final output in module.outputs.entries) { + outputPorts[output.key] = output.value.width; + } + + // Generate stdin-driven testbench + final tb = StringBuffer() + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('#include ') + ..writeln('using namespace std;') + ..writeln() + ..writeln(generatedSystemC) + ..writeln() + ..writeln('int sc_main(int argc, char* argv[]) {'); + + // Clock + for (final clkName in clockSignals) { + tb.writeln( + ' sc_clock $clkName("$clkName", ${Vector._period}, SC_NS);'); + } + + // Signals for all non-clock input ports + for (final entry in inputPorts.entries) { + if (clockSignals.contains(entry.key)) { + continue; + } + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};'); + } + + // Signals for all output ports + for (final entry in outputPorts.entries) { + tb.writeln( + ' sc_signal<${SystemCSynthesisResult.systemCType(entry.value)}>' + ' ${entry.key};'); + } + + tb + ..writeln() + // DUT instantiation and port binding + ..writeln(' $topModule dut("dut");'); + for (final name in inputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + for (final clkName in clockSignals) { + if (!inputPorts.containsKey(clkName)) { + tb.writeln(' dut.$clkName($clkName);'); + } + } + for (final name in outputPorts.keys) { + tb.writeln(' dut.$name($name);'); + } + + tb + ..writeln() + ..writeln(' int _tb_errors = 0;') + ..writeln() + ..writeln(' // Initial offset') + ..writeln(' sc_start(sc_time(1, SC_NS));') + ..writeln() + ..writeln(' // Read number of vectors') + ..writeln(' int _tb_nvec;') + ..writeln(' cin >> _tb_nvec;') + ..writeln() + ..writeln(' for (int _tb_v = 0; _tb_v < _tb_nvec; _tb_v++) {'); + + // Read and drive each non-clock input + final drivableInputs = + inputPorts.keys.where((k) => !clockSignals.contains(k)).toList(); + for (final name in drivableInputs) { + final w = inputPorts[name]!; + if (w > 64) { + // BigInt — read as hex string + tb + ..writeln(' { string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _v(_h.c_str());') + ..writeln(' $name.write(_v); }'); + } else { + tb + ..writeln(' { uint64_t _v; cin >> _v;') + ..writeln(' $name.write(_v); }'); + } + } + + // Advance to check point + tb + ..writeln() + ..writeln(' sc_start(sc_time(${Vector._offset}, SC_NS));') + ..writeln() + ..writeln(' // Read number of outputs to check') + ..writeln(' int _tb_nchk;') + ..writeln(' cin >> _tb_nchk;') + ..writeln() + ..writeln(' for (int _tb_c = 0; _tb_c < _tb_nchk; _tb_c++) {') + ..writeln(' string _tb_pn;') + ..writeln(' cin >> _tb_pn;'); + + // Generate if-else chain for each output port + var first = true; + for (final entry in outputPorts.entries) { + final name = entry.key; + final w = entry.value; + final ifKey = first ? 'if' : '} else if'; + first = false; + tb.writeln(' $ifKey (_tb_pn == "$name") {'); + if (w > 64) { + tb + ..writeln(' string _h; cin >> _h;') + ..writeln(' sc_biguint<$w> _tb_exp(_h.c_str());') + ..writeln(' if ($name.read() != _tb_exp) {'); + } else { + tb + ..writeln(' uint64_t _tb_exp; cin >> _tb_exp;') + ..writeln(' if ($name.read() != _tb_exp) {'); + } + tb + ..writeln(' cout << "ERROR vector " << _tb_v' + ' << ": expected $name=" << _tb_exp' + ' << ", got " << $name.read() << endl;') + ..writeln(' _tb_errors++;') + ..writeln(' }'); + } + if (outputPorts.isNotEmpty) { + tb + ..writeln(' } else {') + ..writeln(' string _d; cin >> _d; // skip unknown') + ..writeln(' }'); + } + + tb + ..writeln(' }') + ..writeln() + ..writeln(' sc_start(sc_time(' + '${Vector._period - Vector._offset}, SC_NS));') + ..writeln(' }') + ..writeln() + ..writeln(' if (_tb_errors == 0) {') + ..writeln(' cout << "PASS" << endl;') + ..writeln(' } else {') + ..writeln(' cout << "FAIL: " << _tb_errors << " errors" << endl;') + ..writeln(' }') + ..writeln(' return _tb_errors > 0 ? 1 : 0;') + ..writeln('}'); + + final testbenchCode = tb.toString(); + + // Write and compile + final uniqueId = generatedSystemC.hashCode; + const dir = 'tmp_test'; + final tmpCppFile = '$dir/tmp_sc_$uniqueId.cpp'; + final tmpOutput = '$dir/tmp_sc_out_$uniqueId'; + + Directory(dir).createSync(recursive: true); + File(tmpCppFile).writeAsStringSync(testbenchCode); + + // Detect C++ standard for this installation + final cxxStd = _detectCxxStandard(resolvedLib); + + // Build precompiled header on first use + final pchDir = _ensurePch(resolvedHome, cxxStd); + final pchInclude = pchDir != null ? '-I$pchDir ' : ''; + + // Create shared Makefile once (keyed by compiler flags) + final makefile = _ensureMakefile( + dir: dir, + cxxStd: cxxStd, + pchInclude: pchInclude, + scHome: resolvedHome, + scLib: resolvedLib, + ); + + final compileResult = Process.runSync( + 'make', ['-f', makefile, 'TARGET=$tmpOutput', 'SRC=$tmpCppFile']); + if (compileResult.exitCode != 0) { + print('SystemC compilation failed:'); + print(compileResult.stdout); + print(compileResult.stderr); + return null; + } + + final exe = SystemCExecutable._( + binaryPath: tmpOutput, + cppFile: tmpCppFile, + scLib: resolvedLib, + clockSignals: clockSignals, + inputPorts: inputPorts, + outputPorts: outputPorts, + ); + _compilationCache[cacheKey] = exe; + return exe; + } + + /// Runs [vectors] against a pre-compiled [SystemCExecutable]. + /// + /// Returns `true` if all vectors pass. + static bool runSystemCVectors( + SystemCExecutable exe, + List vectors, + ) { + // Build stdin data + final sb = StringBuffer()..writeln(vectors.length); + + final drivableInputs = exe.inputPorts.keys + .where((k) => !exe.clockSignals.contains(k)) + .toList(); + + // Track last-driven values (persist across vectors like iverilog) + final lastValues = { + for (final name in drivableInputs) name: '0', + }; + + for (final vector in vectors) { + // Update last-driven values with this vector's inputs + for (final name in drivableInputs) { + final value = vector.inputValues[name]; + if (value != null) { + final w = exe.inputPorts[name]!; + if (w > 64) { + final lv = LogicValue.of(value, width: w); + var hex = lv.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + lastValues[name] = '0x$hex'; + } else { + lastValues[name] = '${_systemcIntValue(value, w)}'; + } + } + } + // Write all input values (using persisted values for unspecified) + for (final name in drivableInputs) { + sb.write('${lastValues[name]} '); + } + sb.writeln(); + + // Write expected outputs: count then name/value pairs + // Skip x/z outputs + final checks = {}; + for (final entry in vector.expectedOutputValues.entries) { + final name = entry.key; + final w = exe.outputPorts[name]!; + final expectedLV = LogicValue.of(entry.value, width: w); + if (expectedLV.toString().contains('x') || + expectedLV.toString().contains('z')) { + continue; + } + if (w > 64) { + var hex = expectedLV.toBigInt().toUnsigned(w).toRadixString(16); + if (hex.length.isOdd) { + hex = '0$hex'; + } + checks[name] = '0x$hex'; + } else { + checks[name] = '${_systemcIntValue(entry.value, w)}'; + } + } + sb.write('${checks.length} '); + for (final entry in checks.entries) { + sb.write('${entry.key} ${entry.value} '); + } + sb.writeln(); + } + + // Write vectors to temp file, redirect as stdin + final stdinFile = '${exe.binaryPath}_input.txt'; + File(stdinFile).writeAsStringSync(sb.toString()); + + final result = Process.runSync( + 'sh', + ['-c', '${exe.binaryPath} < $stdinFile'], + environment: { + 'LD_LIBRARY_PATH': exe.scLib, + 'SC_COPYRIGHT_MESSAGE': 'DISABLE', + }, + ); + + File(stdinFile).deleteSync(); + + final stdout = result.stdout.toString(); + final stderr = result.stderr.toString(); + + if (stdout.isNotEmpty && !stdout.contains('PASS')) { + print(stdout); + } + if (stderr.isNotEmpty && !stderr.contains('Info:')) { + print(stderr); + } + + return stdout.contains('PASS') && !stdout.contains('FAIL'); + } + + /// Convenience: runs [vectors] against a pre-compiled executable and + /// asserts the result. + static void checkSystemCVectors( + SystemCExecutable exe, + List vectors, + ) { + expect(runSystemCVectors(exe, vectors), true); + } + + /// Converts a value to an integer for stdin. + static int _systemcIntValue(dynamic value, int width) { + if (value is int) { + return value; + } + if (value is LogicValue) { + if (!value.isValid) { + return 0; + } + return value.toBigInt().toUnsigned(width).toInt(); + } + if (value is BigInt) { + return value.toUnsigned(width).toInt(); + } + if (value is String) { + final lv = LogicValue.of(value, width: width); + if (!lv.isValid) { + return 0; + } + return lv.toBigInt().toUnsigned(width).toInt(); + } + return 0; + } + + /// Executes [vectors] against a SystemC simulator compiled with g++ and + /// checks that it passes (single-shot, compiles each time). + static void checkSystemCVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (buildOnly) { + // Just verify SystemC code generation succeeds + module.generateSystemC(); + return; + } + final exe = buildSystemCExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + if (kIsWeb) { + return; + } + fail('SystemC compilation failed'); + } + final passed = runSystemCVectors(exe, vectors); + expect(passed, true); + } + + /// Legacy API — returns bool. + static bool systemcVector( + Module module, + List vectors, { + String? moduleName, + bool dontDeleteTmpFiles = false, + String? clockName, + String? resetName, + String? systemcHome, + String? systemcLib, + bool buildOnly = false, + }) { + if (kIsWeb) { + return true; + } + final exe = buildSystemCExecutable( + module, + moduleName: moduleName, + clockName: clockName, + resetName: resetName, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + if (exe == null) { + return false; + } + if (buildOnly) { + return true; + } + return runSystemCVectors(exe, vectors); + } + + // ══════════════════════════════════════════════════════════════════════ + // Trace-based SystemC co-simulation + // ══════════════════════════════════════════════════════════════════════ + + /// Runs the ROHD simulation using [stimulus], records input/output values + /// at every posedge of [clk], then replays the captured vectors through + /// the SystemC-synthesized version of [module] and compares results. + /// + /// [stimulus] is an async function that sets up and drives the simulation + /// (inject signals, register actions, etc.) but does NOT call + /// [Simulator.run] — that is done internally. + /// + /// [inputNames] and [outputNames] specify which ports to record. If null, + /// all module inputs (excluding clock) and all module outputs are used. + /// + /// Example usage with an existing test: + /// ```dart + /// await SimCompare.systemcSimCompare( + /// counter, + /// clk, + /// stimulus: () async { + /// reset.inject(1); + /// en.inject(0); + /// Simulator.registerAction(25, () { reset.put(0); en.put(1); }); + /// Simulator.setMaxSimTime(100); + /// }, + /// ); + /// ``` + static Future systemcSimCompare( + Module module, + Logic clk, { + required Future Function() stimulus, + List? inputNames, + List? outputNames, + String? clockName, + String? resetName, + bool dontDeleteTmpFiles = false, + String? systemcHome, + String? systemcLib, + }) async { + // Determine which signals to record + final clkName = clockName ?? + module.inputs.keys.firstWhere((n) => n == 'clk' || n.contains('clock'), + orElse: () => 'clk'); + + final inputs = + inputNames ?? module.inputs.keys.where((n) => n != clkName).toList(); + final outputs = outputNames ?? module.outputs.keys.toList(); + + // Record snapshots at each posedge. + // Use previousValue for outputs — this gives us the output state from + // BEFORE the clock edge, which matches what the SystemC testbench sees + // when it checks at offset (before the posedge). + // Use current value for inputs — these are the values being presented + // to the DUT when the clock edge fires. + final recordings = []; + + clk.posedge.listen((_) { + // Sample inputs (current value — what's being driven now) + final inputValues = {}; + for (final name in inputs) { + final sig = module.input(name); + final val = sig.value; + inputValues[name] = val.isValid ? val.toBigInt().toInt() : 0; + } + + // Sample outputs using previousValue — the settled output + // from before this tick started, which is what a testbench + // checking before the clock edge would observe. + final outputValues = {}; + for (final name in outputs) { + final sig = module.output(name); + final prev = sig.previousValue; + if (prev != null && prev.isValid) { + outputValues[name] = prev.toBigInt().toInt(); + } + // Skip null/x/z — no check for this output + } + + recordings.add(Vector(inputValues, outputValues)); + }); + + // Run the user's stimulus setup + await stimulus(); + + // Run the ROHD simulation + await Simulator.run(); + + if (recordings.length < 2) { + print('Warning: only ${recordings.length} clock edges recorded,' + ' need at least 2 for comparison'); + return true; + } + + // No shifting needed — previousValue already gives us the output + // state from before the posedge, which matches systemcVector's + // check-before-edge timing. Just pass recordings directly as vectors. + + // Run through SystemC + return systemcVector( + module, + recordings, + clockName: clkName, + resetName: resetName, + dontDeleteTmpFiles: dontDeleteTmpFiles, + systemcHome: systemcHome, + systemcLib: systemcLib, + ); + } +} + +/// Holds the compiled state of a SystemC executable for reuse across tests. +class SystemCExecutable { + /// Path to the compiled binary. + final String binaryPath; + + /// Path to the generated C++ source. + final String cppFile; + + /// Path to the SystemC library (for LD_LIBRARY_PATH). + final String scLib; + + /// Clock signal names. + final Set clockSignals; + + /// Input port names and widths (excluding promoted clocks). + final Map inputPorts; + + /// Output port names and widths. + final Map outputPorts; + + SystemCExecutable._({ + required this.binaryPath, + required this.cppFile, + required this.scLib, + required this.clockSignals, + required this.inputPorts, + required this.outputPorts, + }); + + /// Deletes the compiled binary and source. + void cleanup() { + void tryDelete(String path) { + final f = File(path); + if (f.existsSync()) { + f.deleteSync(); + } + } + + try { + tryDelete(cppFile); + tryDelete(binaryPath); + } on Exception catch (_) {} + } } diff --git a/test/assignment_test.dart b/test/assignment_test.dart index 712ebd9ee..e845086a0 100644 --- a/test/assignment_test.dart +++ b/test/assignment_test.dart @@ -110,6 +110,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple bits', () async { @@ -147,6 +148,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('logic net is multi-assignable', () { diff --git a/test/async_reset_test.dart b/test/async_reset_test.dart index 82e1dbcdf..cb0c0a3b2 100644 --- a/test/async_reset_test.dart +++ b/test/async_reset_test.dart @@ -151,6 +151,9 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // SystemC can't handle manually-driven clocks — buildOnly verifies + // the generated code compiles. + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); }); test('simcompare with clk sync reset', () async { @@ -172,6 +175,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -266,6 +270,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -318,6 +323,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('inverted', () async { @@ -339,6 +345,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('trigger earlier inverted', () async { @@ -362,6 +369,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv); }); test('trigger earlier normal', () async { @@ -385,6 +393,7 @@ void main() { ]; SimCompare.checkIverilogVector(mod, vectorsSv); + SimCompare.checkSystemCVector(mod, vectorsSv, buildOnly: true); }); }); @@ -410,6 +419,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } }); diff --git a/test/bus_test.dart b/test/bus_test.dart index 08ccb4c9b..b0802d20b 100644 --- a/test/bus_test.dart +++ b/test/bus_test.dart @@ -238,6 +238,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('functional', () { @@ -389,6 +390,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('Assignment of a const', () async { @@ -400,6 +402,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); final sv = mod.generateSynth(); expect(sv.contains("assign const_subset = 16'habcd;"), true); @@ -450,6 +453,7 @@ void main() { await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('Bus shrink', () async { @@ -635,6 +639,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('selectFrom and selectIndex', () async { diff --git a/test/collapse_test.dart b/test/collapse_test.dart index 0ef7e00c5..3a4b49427 100644 --- a/test/collapse_test.dart +++ b/test/collapse_test.dart @@ -52,6 +52,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('collapse pretty', () async { diff --git a/test/comb_math_test.dart b/test/comb_math_test.dart index b2a7165ed..c8e3b45dc 100644 --- a/test/comb_math_test.dart +++ b/test/comb_math_test.dart @@ -218,6 +218,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); // thank you to @chykon in issue #158 for providing this example! @@ -236,6 +237,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('simpler example', () { @@ -264,6 +266,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -293,6 +296,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/comb_mod_test.dart b/test/comb_mod_test.dart index 280d19e2e..e8399ad3e 100644 --- a/test/comb_mod_test.dart +++ b/test/comb_mod_test.dart @@ -58,6 +58,29 @@ class ReuseExampleSsa extends Module { } } +class ReuseExampleSsaNoLoop extends Module { + /// Like [ReuseExampleSsa] but the shared [IncrModule] reads from the input + /// [a] rather than `intermediate`, avoiding the combo loop while still + /// exercising the SSA codegen (multiple `intermediate_N` versions). + ReuseExampleSsaNoLoop(Logic a) { + a = addInput('a', a, width: a.width); + final b = addOutput('b', width: a.width); + + final intermediate = Logic(name: 'intermediate', width: a.width); + + // Shared sub-module reads from `a` (no feedback loop) + final inc = IncrModule(a); + + Combinational.ssa((s) => [ + s(intermediate) < a, + s(intermediate) < inc.result, + s(intermediate) < inc.result, + ]); + + b <= intermediate; + } +} + class DuplicateExample extends Module { DuplicateExample(Logic a) { a = addInput('a', a, width: a.width); @@ -238,6 +261,7 @@ void main() { if (useSsa) { await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); } else { try { await SimCompare.checkFunctionalVector(dut, vectors); @@ -281,6 +305,27 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); + }); + + test('should resolve correctly with shared sub-module ssa (no loop)', + () async { + final mod = ReuseExampleSsaNoLoop(Logic(width: 8)); + await mod.build(); + + // inc reads a (=3), result = a+1 = 4 + // SSA: intermediate_0 = a(3), intermediate_1 = result(4), + // intermediate = result(4) + // b = intermediate = 4 + final vectors = [ + Vector({'a': 3}, {'b': 4}), + Vector({'a': 0}, {'b': 1}), + Vector({'a': 254}, {'b': 255}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -308,6 +353,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } diff --git a/test/conditionals_test.dart b/test/conditionals_test.dart index d35a8e5bb..c27ebb2ae 100644 --- a/test/conditionals_test.dart +++ b/test/conditionals_test.dart @@ -490,6 +490,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -780,6 +781,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test( @@ -878,6 +880,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); } test('normal logic', () async { diff --git a/test/flop_test.dart b/test/flop_test.dart index 4e3def505..85d41958c 100644 --- a/test/flop_test.dart +++ b/test/flop_test.dart @@ -53,6 +53,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bit with enable', () async { @@ -74,6 +75,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus', () async { @@ -88,6 +90,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus with enable', () async { @@ -111,6 +114,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, no reset value', () async { @@ -124,6 +128,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, const reset value', () async { @@ -141,6 +146,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus reset, logic reset value', () async { @@ -158,6 +164,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus no reset, const reset value', () async { @@ -174,6 +181,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); test('flop bus, enable, reset, const reset value', () async { @@ -194,6 +202,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/fsm_test.dart b/test/fsm_test.dart index b5f010a56..54dd1e661 100644 --- a/test/fsm_test.dart +++ b/test/fsm_test.dart @@ -270,6 +270,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_simpleFSMPath); }); @@ -286,6 +287,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_simpleFSMPath); }); @@ -304,6 +306,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); if (!kIsWeb) { const fsmPath = '$_tmpDir/default_next_state_fsm.md'; @@ -344,6 +347,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); verifyMermaidStateDiagram(_trafficFSMPath); }); diff --git a/test/gate_test.dart b/test/gate_test.dart index 905c912d9..b92839774 100644 --- a/test/gate_test.dart +++ b/test/gate_test.dart @@ -362,6 +362,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('unary and', () async { @@ -470,6 +471,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('rshift logic', () async { @@ -483,6 +485,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('arshift logic', () async { @@ -498,6 +501,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('lshift int', () async { @@ -509,6 +513,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('rshift int', () async { @@ -520,6 +525,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('arshift int', () async { @@ -531,6 +537,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('shift by const zero', () async { @@ -552,6 +559,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by small bus', () async { @@ -573,6 +581,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by large bus', () async { @@ -594,6 +603,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('small logic shifted by large bus', () async { @@ -615,6 +625,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('large logic shifted by huge value on large bus', () async { @@ -636,6 +647,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('small logic shifted by huge value on large bus', () async { @@ -657,6 +669,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); test('very small logic shifted by huge value on large bus', () async { @@ -678,6 +691,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); }); }); diff --git a/test/incremental_expansion_test.dart b/test/incremental_expansion_test.dart new file mode 100644 index 000000000..d5bdeb121 --- /dev/null +++ b/test/incremental_expansion_test.dart @@ -0,0 +1,116 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// incremental_expansion_test.dart +// Tests for the incremental expansion protocol: +// - original_signal_count / original_cell_count attributes in slim JSON +// - HierarchyNode.extendSignals / extendChildren + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart' as ex; +import '../example/filter_bank.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + // ─────── original_signal_count / original_cell_count ──────────────── + + group('original_signal_count / original_cell_count', () { + test('Counter slim JSON has counts in attributes', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = ex.Counter(en, reset, clk); + await counter.build(); + final netSvc = await NetlistService.create(counter); + + final slimStr = netSvc.slimJson; + final unified = jsonDecode(slimStr) as Map; + final netlist = unified['netlist'] as Map; + final modules = netlist['modules'] as Map; + + for (final entry in modules.entries) { + final mod = entry.value as Map; + final attrs = mod['attributes'] as Map; + expect( + attrs.containsKey('original_signal_count'), + isTrue, + reason: '${entry.key} missing original_signal_count', + ); + expect( + attrs.containsKey('original_cell_count'), + isTrue, + reason: '${entry.key} missing original_cell_count', + ); + + final sigCount = attrs['original_signal_count'] as int; + final cellCount = attrs['original_cell_count'] as int; + final netnames = mod['netnames'] as Map? ?? {}; + final cells = mod['cells'] as Map? ?? {}; + + // Counts must match the actual number of entries in slim JSON. + expect( + sigCount, + equals(netnames.length), + reason: '${entry.key}: original_signal_count mismatch', + ); + expect( + cellCount, + equals(cells.length), + reason: '${entry.key}: original_cell_count mismatch', + ); + } + }); + + test('FilterBank slim JSON has counts in attributes', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = LogicArray([2], 16, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + final filterBank = FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: 4, + dataWidth: 16, + coefficients: [List.filled(4, 1), List.filled(4, 1)], + ); + await filterBank.build(); + final netSvc = await NetlistService.create(filterBank); + + final slimStr = netSvc.slimJson; + final unified = jsonDecode(slimStr) as Map; + final netlist = unified['netlist'] as Map; + final modules = netlist['modules'] as Map; + + // At least the root module should have counts. + expect(modules.isNotEmpty, isTrue); + for (final entry in modules.entries) { + final mod = entry.value as Map; + final attrs = mod['attributes'] as Map; + expect( + attrs['original_signal_count'], + isA(), + reason: '${entry.key}: original_signal_count not int', + ); + expect( + attrs['original_cell_count'], + isA(), + reason: '${entry.key}: original_cell_count not int', + ); + } + }); + }); +} diff --git a/test/instance_signal_name_collision_test.dart b/test/instance_signal_name_collision_test.dart new file mode 100644 index 000000000..65747204a --- /dev/null +++ b/test/instance_signal_name_collision_test.dart @@ -0,0 +1,87 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// instance_signal_name_collision_test.dart +// Tests that submodule instance names and signal names share a single +// namespace, so a collision between them results in uniquification. +// +// 2026 April 18 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Minimal repro modules ──────────────────────────────────────────────────── + +/// Leaf module whose default instance name is "inner". +class _Inner extends Module { + _Inner(Logic a) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + addOutput('y', width: a.width) <= a; + } +} + +/// Parent module that: +/// • instantiates [_Inner] (default instance name: "inner") +/// • names an internal wire "inner" as well +/// +/// Because both identifiers live in a single shared namespace, one of them +/// will be suffixed to avoid collision. +class _CollidingParent extends Module { + _CollidingParent(Logic a) : super(name: 'colliding_parent') { + a = addInput('a', a, width: a.width); + + // Internal wire explicitly named "inner". + final inner = Logic(name: 'inner', width: a.width, naming: Naming.reserved) + ..gets(a); + + // Submodule whose uniqueInstanceName will also be "inner". + final sub = _Inner(inner); + + addOutput('y', width: a.width) <= sub.output('y'); + } +} + +// ── Test ───────────────────────────────────────────────────────────────────── + +void main() { + group('instance / signal name collision (shared namespace)', () { + late _CollidingParent mod; + late SynthModuleDefinition def; + + setUpAll(() async { + mod = _CollidingParent(Logic(width: 8)); + await mod.build(); + def = SynthModuleDefinition(mod); + }); + + test('internal signal named "inner" retains its exact name', () { + // The reserved signal should keep its exact name. + final sl = def.internalSignals.cast().firstWhere( + (s) => s!.logics.any((l) => l.name == 'inner'), + orElse: () => null, + ); + expect(sl, isNotNull, reason: 'Expected to find SynthLogic for "inner"'); + expect(sl!.name, 'inner', + reason: 'Reserved signal "inner" must keep its exact name'); + }); + + test( + 'submodule instance is uniquified because signal ' + '"inner" already claimed the name', () { + final inst = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .cast() + .firstWhere( + (s) => s!.module.name == 'inner', + orElse: () => null, + ); + expect(inst, isNotNull, reason: 'Expected submodule instance for inner'); + // The instance should be suffixed since the signal took "inner" first. + expect(inst!.name, isNot('inner'), + reason: 'Instance should be uniquified when signal already ' + 'claims "inner"'); + }); + }); +} diff --git a/test/interface_test.dart b/test/interface_test.dart index eaf4433d2..68e3a60e1 100644 --- a/test/interface_test.dart +++ b/test/interface_test.dart @@ -142,6 +142,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('should return exception when port name is not sanitary.', () async { diff --git a/test/logic_array_sim_test.dart b/test/logic_array_sim_test.dart new file mode 100644 index 000000000..0669dac5d --- /dev/null +++ b/test/logic_array_sim_test.dart @@ -0,0 +1,250 @@ +// Copyright (C) 2023-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_array_sim_test.dart +// Simulation tests for LogicArray with Iverilog and SystemC backends. +// Exercises sequential logic, element-wise operations, and submodule +// hierarchy with array ports — scenarios beyond the combinational +// passthrough tests in logic_array_test.dart. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// Flops each element of a LogicArray independently. +/// Tests sequential (clocked) array element access in generated code. +class ArrayFlopModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayFlopModule(LogicArray dataIn, {required Logic reset}) + : super(name: 'ArrayFlopModule') { + final clk = SimpleClockGenerator(10).clk; + reset = addInput('reset', reset); + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final out = addOutputArray('dataOut', + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + for (var i = 0; i < dataIn.dimensions[0]; i++) { + out.elements[i] <= flop(clk, dataIn.elements[i], reset: reset); + } + } +} + +/// Applies bitwise NOT to each element, then passes through a submodule. +/// Tests combinational element-wise ops + array hierarchy. +class ArrayInvertAndPassModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayInvertAndPassModule(LogicArray dataIn) + : super(name: 'ArrayInvertAndPassModule') { + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final inverted = + LogicArray(dataIn.dimensions, dataIn.elementWidth, name: 'inverted'); + for (var i = 0; i < dataIn.dimensions[0]; i++) { + inverted.elements[i] <= ~dataIn.elements[i]; + } + + // Pass through a sub-module to exercise array port wiring + final sub = _ArrayPassSub(inverted); + + addOutputArray('dataOut', + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth) <= + sub.out; + } +} + +class _ArrayPassSub extends Module { + LogicArray get out => output('out') as LogicArray; + + _ArrayPassSub(LogicArray inp) : super(name: 'ArrayPassSub') { + inp = addInputArray('inp', inp, + dimensions: inp.dimensions, elementWidth: inp.elementWidth); + addOutputArray('out', + dimensions: inp.dimensions, elementWidth: inp.elementWidth) <= + inp; + } +} + +/// Muxes between two LogicArray inputs based on a select signal. +/// Tests conditional array assignment in generated code. +class ArrayMuxModule extends Module { + LogicArray get dataOut => output('dataOut') as LogicArray; + + ArrayMuxModule(LogicArray a, LogicArray b, Logic sel) + : super(name: 'ArrayMuxModule') { + a = addInputArray('a', a, + dimensions: a.dimensions, elementWidth: a.elementWidth); + b = addInputArray('b', b, + dimensions: b.dimensions, elementWidth: b.elementWidth); + sel = addInput('sel', sel); + + final out = addOutputArray('dataOut', + dimensions: a.dimensions, elementWidth: a.elementWidth); + + Combinational([ + If(sel, then: [out < a], orElse: [out < b]), + ]); + } +} + +/// Concatenates two array elements into a wider output and also +/// provides a reduced (OR-reduce) output across array elements. +/// Tests mixed array-element and scalar operations. +class ArrayReduceModule extends Module { + Logic get concat01 => output('concat01'); + Logic get anyNonZero => output('anyNonZero'); + + ArrayReduceModule(LogicArray dataIn) : super(name: 'ArrayReduceModule') { + dataIn = addInputArray('dataIn', dataIn, + dimensions: dataIn.dimensions, elementWidth: dataIn.elementWidth); + + final c = addOutput('concat01', width: dataIn.elementWidth * 2); + final a = addOutput('anyNonZero'); + + // Concatenate elements [1] and [0] + c <= [dataIn.elements[1], dataIn.elements[0]].swizzle(); + + // OR-reduce: is any element non-zero? + a <= dataIn.elements.map((e) => e.or()).toList().swizzle().or(); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('LogicArray simulation', () { + group('sequential flop per element', () { + test('1D array of 4x8-bit', () async { + final reset = Logic(name: 'reset'); + final dataIn = LogicArray([4], 8); + final mod = ArrayFlopModule(dataIn, reset: reset); + await mod.build(); + + // Each element is flopped: output appears one cycle after input. + // Vector check is BEFORE posedge → sees PREVIOUS cycle's result. + final vectors = [ + Vector({'reset': 1, 'dataIn': 0}, {}), + Vector({'reset': 1, 'dataIn': 0}, {}), + Vector({'reset': 1, 'dataIn': 0}, {'dataOut': 0}), + // Deassert reset; still see 0 from reset phase + Vector({'reset': 0, 'dataIn': 0x44332211}, {'dataOut': 0x00000000}), + // Now see 0x44332211 from previous cycle + Vector({'reset': 0, 'dataIn': 0xDDCCBBAA}, {'dataOut': 0x44332211}), + Vector({'reset': 0, 'dataIn': 0x00000000}, {'dataOut': 0xDDCCBBAA}), + Vector({'reset': 0, 'dataIn': 0x00000000}, {'dataOut': 0x00000000}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('element-wise invert with submodule', () { + test('1D array of 3x8-bit', () async { + final dataIn = LogicArray([3], 8); + final mod = ArrayInvertAndPassModule(dataIn); + await mod.build(); + + // 0x00 → 0xFF, 0xAA → 0x55, 0x0F → 0xF0 + // Input: 0x0FAA00 (elem[0]=0x00, elem[1]=0xAA, elem[2]=0x0F) + // Output: 0xF055FF (elem[0]=0xFF, elem[1]=0x55, elem[2]=0xF0) + final vectors = [ + Vector({'dataIn': 0x0FAA00}, {'dataOut': 0xF055FF}), + Vector({'dataIn': 0xFFFFFF}, {'dataOut': 0x000000}), + Vector({'dataIn': 0x000000}, {'dataOut': 0xFFFFFF}), + Vector({ + 'dataIn': 0x123456 + }, { + 'dataOut': LogicValue.ofInt(0x123456, 24) ^ + LogicValue.filled(24, LogicValue.one) + }), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('1D array of 2x4-bit', () async { + final dataIn = LogicArray([2], 4); + final mod = ArrayInvertAndPassModule(dataIn); + await mod.build(); + + final vectors = [ + Vector({'dataIn': 0x00}, {'dataOut': 0xFF}), + Vector({'dataIn': 0xAB}, {'dataOut': 0x54}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('array mux', () { + test('1D array of 3x8-bit', () async { + final a = LogicArray([3], 8); + final b = LogicArray([3], 8); + final sel = Logic(name: 'sel'); + final mod = ArrayMuxModule(a, b, sel); + await mod.build(); + + final vectors = [ + // sel=1 → output = a + Vector( + {'sel': 1, 'a': 0x112233, 'b': 0xAABBCC}, {'dataOut': 0x112233}), + // sel=0 → output = b + Vector( + {'sel': 0, 'a': 0x112233, 'b': 0xAABBCC}, {'dataOut': 0xAABBCC}), + // Toggle + Vector( + {'sel': 1, 'a': 0xFFFFFF, 'b': 0x000000}, {'dataOut': 0xFFFFFF}), + Vector( + {'sel': 0, 'a': 0xFFFFFF, 'b': 0x000000}, {'dataOut': 0x000000}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + + group('array reduce and concat', () { + test('1D array of 4x8-bit', () async { + final dataIn = LogicArray([4], 8); + final mod = ArrayReduceModule(dataIn); + await mod.build(); + + // Elements: [0]=low 8 bits, [1]=next 8, etc. + // concat01 = {elem[1], elem[0]} (16 bits) + // anyNonZero = OR-reduce of all elements + final vectors = [ + // All zero + Vector({'dataIn': 0x00000000}, {'concat01': 0x0000, 'anyNonZero': 0}), + // elem[0]=0x01 + Vector({'dataIn': 0x00000001}, {'concat01': 0x0001, 'anyNonZero': 1}), + // elem[0]=0xAB, elem[1]=0xCD + Vector({'dataIn': 0x0000CDAB}, {'concat01': 0xCDAB, 'anyNonZero': 1}), + // elem[3]=0xFF only (upper byte) + Vector({'dataIn': 0xFF000000}, {'concat01': 0x0000, 'anyNonZero': 1}), + // All 0xFF + Vector({'dataIn': 0xFFFFFFFF}, {'concat01': 0xFFFF, 'anyNonZero': 1}), + ]; + + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); + }); + }); + }); +} diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index 87c6be85a..735eafe12 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -727,6 +727,8 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // buildOnly: array element sub-module binding not yet supported + SimCompare.checkSystemCVector(mod, vectors, buildOnly: true); }); group('logicarray passthrough', () { @@ -760,6 +762,7 @@ void main() { SimCompare.checkIverilogVector(mod, vectors, buildOnly: noSvSim, dontDeleteTmpFiles: dontDeleteTmpFiles); } + SimCompare.checkSystemCVector(mod, vectors, buildOnly: noSvSim); } group('simple', () { @@ -1108,6 +1111,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('assign subset of logic array without mentioning start', () async { @@ -1161,6 +1165,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/logic_name_test.dart b/test/logic_name_test.dart index 8ce9d5f40..2f571baba 100644 --- a/test/logic_name_test.dart +++ b/test/logic_name_test.dart @@ -289,6 +289,7 @@ void main() { // confirm build works SimCompare.checkIverilogVector(mod, []); + SimCompare.checkSystemCVector(mod, []); }); test('array port and simple port with _num name conflict but pruned away', @@ -305,6 +306,7 @@ void main() { // confirm build works SimCompare.checkIverilogVector(mod, []); + SimCompare.checkSystemCVector(mod, []); }); test('badly named intermediate signal sanitization', () async { diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index fdc522e96..59f35bd62 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -256,6 +256,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('simple passthrough struct', () async { @@ -271,6 +272,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('fancy struct inverter', () async { @@ -293,6 +295,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); diff --git a/test/math_test.dart b/test/math_test.dart index d9ada00a0..b4f0ecf5f 100644 --- a/test/math_test.dart +++ b/test/math_test.dart @@ -112,6 +112,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); @@ -136,6 +137,7 @@ void main() { await gtm.build(); await SimCompare.checkFunctionalVector(gtm, vectors); SimCompare.checkIverilogVector(gtm, vectors); + SimCompare.checkSystemCVector(gtm, vectors); } test('power', () async { diff --git a/test/module_services_test.dart b/test/module_services_test.dart new file mode 100644 index 000000000..38350dd14 --- /dev/null +++ b/test/module_services_test.dart @@ -0,0 +1,320 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_services_test.dart +// Tests for ModuleServices, SvService, and NetlistService. + +@TestOn('vm') +library; + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +// --------------------------------------------------------------------------- +// Simple test modules +// --------------------------------------------------------------------------- + +class _InverterModule extends Module { + Logic get out => output('out'); + + _InverterModule(Logic inp) : super(name: 'inverter') { + inp = addInput('inp', inp); + final out = addOutput('out'); + out <= ~inp; + } +} + +class _TopModule extends Module { + Logic get out => output('out'); + + _TopModule(Logic a, Logic b) : super(name: 'top') { + a = addInput('a', a); + b = addInput('b', b); + final out = addOutput('out'); + + final inv = _InverterModule(a); + out <= inv.out & b; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + group('ModuleServices', () { + test('rootModule is set by Module.build', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + expect(ModuleServices.instance.rootModule, equals(mod)); + }); + + test('hierarchyJSON returns valid JSON after build', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final json = ModuleServices.instance.hierarchyJSON; + final decoded = jsonDecode(json) as Map; + expect(decoded['name'], equals('top')); + }); + + test('svJSON returns unavailable when no SvService registered', () { + final json = ModuleServices.instance.svJSON; + final decoded = jsonDecode(json) as Map; + expect(decoded['status'], equals('unavailable')); + }); + + test( + 'netlistJSON returns unavailable when no NetlistService registered', + () { + final json = ModuleServices.instance.netlistJSON; + final decoded = jsonDecode(json) as Map; + expect(decoded['status'], equals('unavailable')); + }, + ); + + test('reset clears all services', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + expect(ModuleServices.instance.rootModule, isNotNull); + ModuleServices.instance.reset(); + expect(ModuleServices.instance.rootModule, isNull); + }); + + test('inspectorJSON returns slim netlist when service registered', + () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + await NetlistService.create(mod); + + final json = ModuleServices.instance.inspectorJSON; + final decoded = jsonDecode(json) as Map; + expect(decoded['netlist'], isA>()); + final netlist = decoded['netlist'] as Map; + expect(netlist['modules'], isA>()); + }); + + test('inspectorJSON falls back to hierarchy when no netlist', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final json = ModuleServices.instance.inspectorJSON; + final decoded = jsonDecode(json) as Map; + // Falls back to hierarchy JSON format. + expect(decoded['name'], equals('top')); + }); + + test('inspectorModuleJSON returns per-module netlist', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + final netSvc = await NetlistService.create(mod); + + for (final name in netSvc.moduleNames) { + final json = ModuleServices.instance.inspectorModuleJSON(name); + final decoded = jsonDecode(json) as Map; + expect(decoded['modules'], isA>()); + } + }); + }); + + group('SvService', () { + test('generates SV for a module hierarchy', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final sv = SvService(mod); + + expect(sv.fileContents, isNotEmpty); + expect(sv.allContents, contains('module')); + expect(sv.allContents, contains('endmodule')); + }); + + test('registers with ModuleServices by default', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + SvService(mod); + + expect(ModuleServices.instance.svService, isNotNull); + final json = ModuleServices.instance.svJSON; + final decoded = jsonDecode(json) as Map; + expect(decoded['modules'], isA>()); + }); + + test('register: false does not register', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + SvService(mod, register: false); + + expect(ModuleServices.instance.svService, isNull); + }); + + test('contentsByName returns per-module SV', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final sv = SvService(mod, register: false); + final byName = sv.contentsByName; + + // Should have at least the top module and the inverter. + expect(byName.length, greaterThanOrEqualTo(2)); + for (final content in byName.values) { + expect(content, contains('module')); + } + }); + + test('synthOutput includes header', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final sv = SvService(mod, register: false); + expect(sv.synthOutput, contains('Generated by ROHD')); + expect(sv.synthOutput, contains(sv.allContents)); + }); + }); + + group('NetlistService', () { + test('generates netlist JSON for a module hierarchy', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final netlist = await NetlistService.create(mod); + final json = netlist.toJson(); + final decoded = jsonDecode(json) as Map; + + expect(decoded['modules'], isA>()); + expect(netlist.moduleNames, isNotEmpty); + }); + + test('registers with ModuleServices by default', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + await NetlistService.create(mod); + + expect(ModuleServices.instance.netlistService, isNotNull); + final json = ModuleServices.instance.netlistJSON; + final decoded = jsonDecode(json) as Map; + expect(decoded['modules'], isA>()); + }); + + test('register: false does not register', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + await NetlistService.create(mod, register: false); + + expect(ModuleServices.instance.netlistService, isNull); + }); + + test('moduleJson returns single module data', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final netlist = await NetlistService.create(mod, register: false); + + // Query for a module that exists. + for (final name in netlist.moduleNames) { + final moduleJson = netlist.moduleJson(name); + final decoded = jsonDecode(moduleJson) as Map; + expect(decoded['modules'], isA>()); + expect((decoded['modules'] as Map).containsKey(name), isTrue); + } + + // Query for a module that doesn't exist. + final missing = netlist.moduleJson('nonexistent'); + final decoded = jsonDecode(missing) as Map; + expect(decoded['status'], equals('not_found')); + }); + + test('slimJson returns netlist envelope without connections', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final netlist = await NetlistService.create(mod, register: false); + final slim = netlist.slimJson; + final decoded = jsonDecode(slim) as Map; + + expect(decoded['netlist'], isA>()); + final netlistSection = decoded['netlist'] as Map; + expect(netlistSection['rootInstanceName'], isNotNull); + expect(netlistSection['modules'], isA>()); + + // Verify cells have no connections + final modules = netlistSection['modules'] as Map; + for (final modEntry in modules.values) { + final cells = + (modEntry as Map)['cells'] as Map; + for (final cellEntry in cells.values) { + final cell = cellEntry as Map; + expect(cell.containsKey('connections'), isFalse, + reason: 'Slim cells should not have connections'); + } + } + }); + + test('synthesizedModules provides read-only access', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(); + + final netlist = await NetlistService.create(mod, register: false); + final modules = netlist.synthesizedModules; + expect(modules, isNotEmpty); + expect(modules.keys, equals(netlist.moduleNames)); + }); + }); + + group('Module.build netlistOptions integration', () { + test('netlistOptions creates and registers NetlistService', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = _TopModule(a, b); + await mod.build(netlistOptions: const NetlistOptions()); + + expect(ModuleServices.instance.netlistService, isNotNull); + final json = ModuleServices.instance.netlistJSON; + final decoded = jsonDecode(json) as Map; + expect(decoded['modules'], isA>()); + }); + }); +} diff --git a/test/name_test.dart b/test/name_test.dart index 2742c0ec8..bde8a9c9f 100644 --- a/test/name_test.dart +++ b/test/name_test.dart @@ -1,7 +1,7 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// definition_name_test.dart +// name_test.dart // Tests for definition names (including reserving them) of Modules. // // 2022 March 7 @@ -136,6 +136,11 @@ void main() { final nameTypes = [nameType1, nameType2]; // skip ones that actually *should* cause a failure + // + // Note: SystemVerilog allows using the same identifier for a signal + // and an instance because they are different namespaces. However, + // Icarus Verilog rejects that pattern, so ROHD treats those as + // conflicts for simulator compatibility. final shouldConflict = [ { NameType.internalModuleDefinition, diff --git a/test/naming_cases_test.dart b/test/naming_cases_test.dart new file mode 100644 index 000000000..fbc1d9536 --- /dev/null +++ b/test/naming_cases_test.dart @@ -0,0 +1,583 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_cases_test.dart +// Systematic test of all signal-naming cases in the synthesis pipeline. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +// ════════════════════════════════════════════════════════ +// NAMING CROSS-PRODUCT TABLE +// ════════════════════════════════════════════════════════ +// +// Axis 1 — Naming enum (set at Logic construction time): +// reserved Exact name required; collision → exception. +// renameable Keeps name, uniquified on collision; never merged. +// mergeable May merge with equivalent signals; any merged name chosen. +// unnamed No user name; system generates one. +// +// Axis 2 — Context role (per SynthModuleDefinition): +// this-port Port of module being synthesized +// (namingOverride → reserved). +// sub-port Port of a child submodule +// (namingOverride → mergeable). +// internal Non-port signal inside the module (no override). +// const Const object (separate path via constValue). +// +// Axis 3 — Name preference: +// preferred baseName does NOT start with '_' +// unpreferred baseName starts with '_' +// +// Axis 4 — Constant context (only for Const): +// allowed Literal value string used as name. +// disallowed Feeding expressionlessInput; +// must use a wire name. +// +// ────────────────────────────────────────────────────── +// Row Naming Context Pref? Test Valid? +// Effective class → Outcome +// ────────────────────────────────────────────────────── +// 1 reserved this-port pref T1 ✓ +// port (in _portLogics) → exact sanitized name +// 2 reserved this-port unpref T2 ✓ unusual +// port → exact _-prefixed port name +// 3 reserved sub-port pref T3 ✓ +// preferred mergeable → merged, uniquified +// 4 reserved sub-port unpref T4 ✓ +// unpreferred mergeable → low-priority merge +// 5 reserved internal pref T5 ✓ +// reserved internal → exact name, throw on clash +// 6 reserved internal unpref T6 ✓ unusual +// reserved internal → exact _-prefixed name +// 7 renameable this-port pref — can't happen* +// port → exact port name +// 8 renameable sub-port pref — can't happen* +// preferred mergeable → merged +// 9 renameable internal pref T9 ✓ +// renameable → base name, uniquified +// 10 renameable internal unpref T10 ✓ unusual +// renameable → uniquified _-prefixed +// 11 mergeable this-port pref T11 ✓ +// port → exact port name (Logic.port()) +// 12 mergeable this-port unpref T12 ✓ unusual +// port → exact _-prefixed port name +// 13 mergeable sub-port pref T3 ✓ (=row 3) +// preferred mergeable → best-available merge +// 14 mergeable sub-port unpref T4 ✓ (=row 4) +// unpreferred mergeable → low-priority merge +// 15 mergeable internal pref T15 ✓ +// preferred mergeable → prefer available name +// 16 mergeable internal unpref T16 ✓ +// unpreferred mergeable → low-priority merge +// 17 unnamed this-port — — ✗ impossible** +// port → exact port name +// 18 unnamed sub-port — — ✗ impossible** +// mergeable → merged +// 19 unnamed internal (unpf) T19 ✓ +// unnamed → generated _s name +// 20 —(Const) — — T20 ✓ +// const allowed → literal value e.g. 8'h42 +// 21 —(Const) — — T21 ✓ +// const disallowed → wire name (not literal) +// ────────────────────────────────────────────────────── +// +// * Rows 7-8: addInput/addOutput always create +// Logic with Naming.reserved, so a port can +// never have intrinsic Naming.renameable. +// The namingOverride makes it moot anyway. +// +// ** Rows 17-18: addInput/addOutput require a +// non-null, non-empty name. chooseName() only +// yields Naming.unnamed for null/empty names, +// so a port can never be unnamed. +// +// ✗ unnamed + reserved: Logic(naming: reserved) +// with null/empty name throws +// NullReservedNameException / +// EmptyReservedNameException at construction +// time. Never reaches synthesizer. +// +// Additional cross-cutting concerns: +// COL Collision between mergeables +// → uniquified suffix (_0) +// MG Merge: directly-connected signals +// share SynthLogic +// INST Submodule instance names: unique, +// don't collide with ports +// ST Structure element: structureName +// = "parent.field" → sanitized ("_") +// AR Array element: isArrayMember +// → uses logic.name (index-based) +// +// ════════════════════════════════════════════════════════ + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Leaf sub-modules ────────────────────────────── + +/// A leaf module whose `in0` is an "expressionless input" — +/// meaning any constant driving it must get a real wire name, not a literal. +class _ExpressionlessSub extends Module with SystemVerilog { + @override + List get expressionlessInputs => const ['in0']; + + _ExpressionlessSub(Logic a, Logic b) : super(name: 'exprsub') { + a = addInput('in0', a, width: a.width); + b = addInput('in1', b, width: b.width); + addOutput('out', width: a.width) <= a & b; + } +} + +/// A simple sub-module with preferred-name ports. +class _SimpleSub extends Module { + _SimpleSub(Logic x) : super(name: 'simplesub') { + x = addInput('x', x, width: x.width); + addOutput('y', width: x.width) <= ~x; + } +} + +/// A sub-module with an unpreferred-name port. +class _UnprefSub extends Module { + _UnprefSub(Logic a) : super(name: 'unprefsub') { + a = addInput('_uport', a, width: a.width); + addOutput('uout', width: a.width) <= ~a; + } +} + +// ── Main test module ────────────────────────────── +// One module that exercises every valid naming case in a minimal design. +// Each signal is tagged with the row number from the table above. + +class _AllNamingCases extends Module { + // Exposed for test inspection. + // Row 1 / Row 2: ports (accessed via mod.input / mod.output). + // Row 5: + late final Logic reservedInternal; + // Row 6: + late final Logic reservedInternalUnpref; + // Row 9: + late final Logic renameableInternal; + // Row 10: + late final Logic renameableInternalUnpref; + // Row 15: + late final Logic mergeablePref; + // Row 15 collision partner: + late final Logic mergeablePrefCollide; + // Row 16: + late final Logic mergeableUnpref; + // Row 19: + late final Logic unnamed; + // Row 20: + late final Logic constAllowed; + // Row 21: + late final Logic constDisallowed; + // MG: + late final Logic mergeTarget; + + // Structure/array elements (ST, AR): + late final LogicStructure structPort; + late final LogicArray arrayPort; + + _AllNamingCases() : super(name: 'allcases') { + // ── Row 1: reserved + this-port + preferred ────────────────── + final inp = addInput('inp', Logic(width: 8), width: 8); + final out = addOutput('out', width: 8); + + // ── Row 2: reserved + this-port + unpreferred ──────────────── + final uInp = addInput('_uinp', Logic(width: 8), width: 8); + + // ── Row 11: mergeable + this-port + preferred ──────────────── + // (This is the Logic.port() → connectIO path. addInput forces + // Naming.reserved regardless of the source's naming, so intrinsic + // mergeable is overridden to reserved. We test the port keeps its + // exact name.) + final mPortInp = addInput('mport', Logic(width: 8), width: 8); + + // ── Row 12: mergeable + this-port + unpreferred ────────────── + final mPortUnpref = addInput('_muprt', Logic(width: 8), width: 8); + + // ── Row 5: reserved + internal + preferred ─────────────────── + reservedInternal = Logic(name: 'resv', width: 8, naming: Naming.reserved) + ..gets(inp ^ Const(0x01, width: 8)); + + // ── Row 6: reserved + internal + unpreferred ───────────────── + reservedInternalUnpref = + Logic(name: '_resvu', width: 8, naming: Naming.reserved) + ..gets(inp ^ Const(0x02, width: 8)); + + // ── Row 9: renameable + internal + preferred ───────────────── + renameableInternal = Logic(name: 'ren', width: 8, naming: Naming.renameable) + ..gets(inp ^ Const(0x03, width: 8)); + + // ── Row 10: renameable + internal + unpreferred ────────────── + renameableInternalUnpref = + Logic(name: '_renu', width: 8, naming: Naming.renameable) + ..gets(inp ^ Const(0x04, width: 8)); + + // ── Row 15: mergeable + internal + preferred ───────────────── + mergeablePref = Logic(name: 'mname', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x05, width: 8)); + + // ── COL: collision partner — same base name 'mname' ────────── + mergeablePrefCollide = + Logic(name: 'mname', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x06, width: 8)); + + // ── Row 16: mergeable + internal + unpreferred ─────────────── + mergeableUnpref = Logic(name: '_hidden', width: 8, naming: Naming.mergeable) + ..gets(inp ^ Const(0x07, width: 8)); + + // ── Row 19: unnamed + internal ─────────────────────────────── + unnamed = Logic(width: 8)..gets(inp ^ Const(0x08, width: 8)); + + // ── Rows 3/13: sub-port preferred (via _SimpleSub.x / .y) ─── + // ── Row 4/14: sub-port unpreferred (via _UnprefSub._uport) ── + final sub = _SimpleSub(renameableInternal); + final subOut = sub.output('y'); + // Use a distinct expression so the submodule port doesn't merge with + // renameableInternal (which is renameable and would win). + final unpSub = _UnprefSub(inp ^ Const(0x0a, width: 8)); + + // ── MG: merge behavior — mergeTarget merges with subOut ────── + mergeTarget = Logic(name: 'mmerge', width: 8, naming: Naming.mergeable) + ..gets(subOut); + + // ── Row 20: constant with name allowed ─────────────────────── + constAllowed = + Const(0x42, width: 8).named('const_ok', naming: Naming.mergeable); + + // ── Row 21: constant with name disallowed (expressionlessInput) + constDisallowed = + Const(0x09, width: 8).named('const_wire', naming: Naming.mergeable); + // ignore: unused_local_variable + final exprSub = _ExpressionlessSub(constDisallowed, inp); + + // ── ST: structure element (structureName = "parent.field") ──── + structPort = _SimpleStruct(); + addInput('stIn', structPort, width: structPort.width); + + // ── AR: array element (isArrayMember, uses logic.name) ─────── + arrayPort = LogicArray([3], 8, name: 'arIn'); + addInputArray('arIn', arrayPort, dimensions: [3], elementWidth: 8); + + // Drive output to use all signals (prevents pruning). + out <= + mergeTarget | + mergeablePrefCollide | + mergeableUnpref | + unnamed | + constAllowed | + uInp | + mPortInp | + mPortUnpref | + reservedInternalUnpref | + renameableInternalUnpref | + unpSub.output('uout'); + } +} + +/// A minimal LogicStructure for testing structureName sanitization. +class _SimpleStruct extends LogicStructure { + final Logic field1; + final Logic field2; + + factory _SimpleStruct({String name = 'st'}) => _SimpleStruct._( + Logic(name: 'a', width: 4), + Logic(name: 'b', width: 4), + name: name, + ); + + _SimpleStruct._(this.field1, this.field2, {required super.name}) + : super([field1, field2]); + + @override + LogicStructure clone({String? name}) => + _SimpleStruct(name: name ?? this.name); +} + +// ── Helpers ─────────────────────────────────────── + +/// Collects a map from Logic → picked name for all SynthLogics. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked (pruned/replaced) + } + } + return names; +} + +/// Finds a SynthLogic that contains [logic]. +SynthLogic? _findSynthLogic(SynthModuleDefinition def, Logic logic) { + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + if (sl.logics.contains(logic)) { + return sl; + } + } + return null; +} + +// ── Tests ──────────────────────────────────────── + +void main() { + late _AllNamingCases mod; + late SynthModuleDefinition def; + late Map names; + + setUp(() async { + mod = _AllNamingCases(); + await mod.build(); + def = SynthModuleDefinition(mod); + names = _collectNames(def); + }); + + group('naming cases', () { + // ── Row 1: reserved + this-port + preferred ──────────────── + + test('T1: reserved preferred port keeps exact name', () { + expect(names[mod.input('inp')], 'inp'); + expect(names[mod.output('out')], 'out'); + }); + + // ── Row 2: reserved + this-port + unpreferred ────────────── + + test('T2: reserved unpreferred port keeps exact _-prefixed name', () { + expect(names[mod.input('_uinp')], '_uinp'); + }); + + // ── Rows 3/13: sub-port + preferred (reserved or mergeable) ─ + + test('T3: submodule preferred port gets a name in parent', () { + final subX = mod.subModules.whereType<_SimpleSub>().first.input('x'); + final n = names[subX]; + expect(n, isNotNull, reason: 'Submodule port must be named'); + // Treated as preferred mergeable — name should not start with _. + expect(n, isNot(startsWith('_')), + reason: 'Preferred submodule port name should not be unpreferred'); + }); + + // ── Row 4/14: sub-port + unpreferred ──────────────────────── + + test('T4: submodule unpreferred port gets an unpreferred name', () { + final subUPort = + mod.subModules.whereType<_UnprefSub>().first.input('_uport'); + final n = names[subUPort]; + expect(n, isNotNull, reason: 'Submodule port must be named'); + expect(n, startsWith('_'), + reason: 'Unpreferred submodule port should keep _-prefix'); + }); + + // ── Row 5: reserved + internal + preferred ────────────────── + + test('T5: reserved preferred internal keeps exact name', () { + expect(names[mod.reservedInternal], 'resv'); + }); + + // ── Row 6: reserved + internal + unpreferred ──────────────── + + test('T6: reserved unpreferred internal keeps exact _-prefixed name', () { + expect(names[mod.reservedInternalUnpref], '_resvu'); + }); + + // ── Row 9: renameable + internal + preferred ──────────────── + + test('T9: renameable preferred internal gets its name', () { + final n = names[mod.renameableInternal]; + expect(n, isNotNull); + expect(n, contains('ren')); + }); + + // ── Row 10: renameable + internal + unpreferred ───────────── + + test('T10: renameable unpreferred internal keeps _-prefix', () { + final n = names[mod.renameableInternalUnpref]; + expect(n, isNotNull); + expect(n, startsWith('_'), + reason: 'Unpreferred renameable should keep _-prefix'); + expect(n, contains('renu')); + }); + + // ── Row 11: mergeable + this-port + preferred ─────────────── + + test('T11: mergeable-origin port (Logic.port) keeps exact port name', () { + // addInput overrides naming to reserved; the port name is exact. + expect(names[mod.input('mport')], 'mport'); + }); + + // ── Row 12: mergeable + this-port + unpreferred ───────────── + + test('T12: mergeable-origin unpreferred port keeps exact name', () { + expect(names[mod.input('_muprt')], '_muprt'); + }); + + // ── Row 15: mergeable + internal + preferred ──────────────── + + test('T15: mergeable preferred internal gets its name', () { + final n = names[mod.mergeablePref]; + expect(n, isNotNull); + expect(n, contains('mname')); + }); + + // ── COL: name collision → uniquified suffix ───────────────── + + test('COL: collision between two mergeables gets uniquified', () { + final n1 = names[mod.mergeablePref]; + final n2 = names[mod.mergeablePrefCollide]; + expect(n1, isNot(n2), reason: 'Colliding names must be uniquified'); + expect({n1, n2}, containsAll(['mname', 'mname_0'])); + }); + + // ── Row 16: mergeable + internal + unpreferred ────────────── + + test('T16: mergeable unpreferred internal keeps _-prefix', () { + final n = names[mod.mergeableUnpref]; + expect(n, isNotNull); + expect(n, startsWith('_'), + reason: 'Unpreferred mergeable should keep _-prefix'); + }); + + // ── Row 19: unnamed + internal ────────────────────────────── + + test('T19: unnamed signal gets a generated name', () { + final n = names[mod.unnamed]; + expect(n, isNotNull, reason: 'Unnamed signal must still get a name'); + // chooseName() gives unnamed signals a name starting with '_s'. + expect(n, startsWith('_'), + reason: 'Unnamed signals get unpreferred generated names'); + }); + + // ── Row 20: constant with name allowed ────────────────────── + + test('T20: constant with name allowed uses literal value', () { + final sl = _findSynthLogic(def, mod.constAllowed); + expect(sl, isNotNull); + if (sl != null && !sl.constNameDisallowed) { + expect(sl.name, contains("8'h42"), + reason: 'Allowed constant should use value literal'); + } + }); + + // ── Row 21: constant with name disallowed ─────────────────── + + test('T21: constant with name disallowed uses wire name', () { + final sl = _findSynthLogic(def, mod.constDisallowed); + expect(sl, isNotNull); + if (sl != null) { + if (sl.constNameDisallowed) { + expect(sl.name, isNot(contains("8'h09")), + reason: 'Disallowed constant should not use value literal'); + expect(sl.name, isNotEmpty); + } + } + }); + + // ── MG: merge behavior ────────────────────────────────────── + + test('MG: merged signals share the same SynthLogic', () { + final sl = _findSynthLogic(def, mod.mergeTarget); + expect(sl, isNotNull); + if (sl != null && sl.logics.length > 1) { + expect(sl.name, isNotEmpty); + } + }); + + // ── INST: submodule instance naming ───────────────────────── + + test('INST: submodule instances get collision-free names', () { + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toList(); + expect(instNames.toSet().length, instNames.length, + reason: 'Instance names must be unique'); + final portNames = {...mod.inputs.keys, ...mod.outputs.keys}; + for (final name in instNames) { + expect(portNames, isNot(contains(name)), + reason: 'Instance "$name" should not collide with a port'); + } + }); + + // ── ST: structure element naming ──────────────────────────── + + test('ST: structure element structureName is sanitized', () { + // structureName for field1 is "st.a" → sanitized to "st_a". + final stIn = mod.input('stIn'); + final n = names[stIn]; + expect(n, isNotNull); + // The port itself should keep its reserved name 'stIn'. + expect(n, 'stIn'); + }); + + // ── AR: array element naming ──────────────────────────────── + + test('AR: array port keeps its name', () { + // Array ports are registered via addInputArray with Naming.reserved. + final arIn = mod.input('arIn'); + final n = names[arIn]; + expect(n, isNotNull); + expect(n, 'arIn'); + }); + + // ── Impossible cases ──────────────────────────────────────── + + test('unnamed + reserved throws at construction time', () { + expect( + () => Logic(naming: Naming.reserved), + throwsA(isA()), + ); + expect( + () => Logic(name: '', naming: Naming.reserved), + throwsA(isA()), + ); + }); + + // ── Golden SV snapshot ────────────────────────────────────── + + test('golden SV output snapshot', () { + final sv = mod.generateSynth(); + + // Port declarations. + expect(sv, contains('input logic [7:0] inp')); + expect(sv, contains('output logic [7:0] out')); + expect(sv, contains('_uinp')); + expect(sv, contains('mport')); + expect(sv, contains('_muprt')); + + // Reserved internals. + expect(sv, contains('resv')); + expect(sv, contains('_resvu')); + + // Renameable internals. + expect(sv, contains('ren')); + expect(sv, contains('_renu')); + + // Constant literal (T20). + expect(sv, contains("8'h42")); + + // Submodule instantiations. + expect(sv, contains('simplesub')); + expect(sv, contains('exprsub')); + expect(sv, contains('unprefsub')); + }); + }); +} diff --git a/test/naming_consistency_test.dart b/test/naming_consistency_test.dart new file mode 100644 index 000000000..f0d7b2d31 --- /dev/null +++ b/test/naming_consistency_test.dart @@ -0,0 +1,247 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_consistency_test.dart +// Validates that both the SystemVerilog synthesizer and a base +// SynthModuleDefinition (used by the netlist synthesizer) produce +// consistent signal names via the shared Module.namer. +// +// 2026 April 10 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart'; +import 'package:rohd/src/synthesizers/utilities/utilities.dart'; +import 'package:test/test.dart'; + +// ── Helper modules ────────────────────────────────────────────────── + +/// A simple module with ports, internal wires, and a sub-module. +class _Inner extends Module { + _Inner(Logic a, Logic b) : super(name: 'inner') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + addOutput('y', width: a.width) <= a & b; + } +} + +class _Outer extends Module { + _Outer(Logic a, Logic b) : super(name: 'outer') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final inner = _Inner(a, b); + addOutput('y', width: a.width) <= inner.output('y'); + } +} + +/// A module with a constant assignment (exercises const naming). +class _ConstModule extends Module { + _ConstModule(Logic a) : super(name: 'constmod') { + a = addInput('a', a, width: 8); + final c = Const(0x42, width: 8).named('myConst', naming: Naming.mergeable); + addOutput('y', width: 8) <= a + c; + } +} + +/// A module with Naming.renameable and Naming.mergeable signals. +class _MixedNaming extends Module { + _MixedNaming(Logic a) : super(name: 'mixednaming') { + a = addInput('a', a, width: 8); + final r = Logic(name: 'renamed', width: 8, naming: Naming.renameable) + ..gets(a); + final m = Logic(name: 'merged', width: 8, naming: Naming.mergeable) + ..gets(r); + addOutput('y', width: 8) <= m; + } +} + +/// A module with a FlipFlop sub-module. +class _FlopOuter extends Module { + _FlopOuter(Logic clk, Logic d) : super(name: 'flopouter') { + clk = addInput('clk', clk); + d = addInput('d', d, width: 8); + addOutput('q', width: 8) <= flop(clk, d); + } +} + +/// Builds [SynthModuleDefinition]s from both bases and collects a +/// Logic→name mapping for all present SynthLogics. +/// +/// Returns maps from Logic to its resolved signal name. +Map _collectNames(SynthModuleDefinition def) { + final names = {}; + for (final sl in [ + ...def.inputs, + ...def.outputs, + ...def.inOuts, + ...def.internalSignals, + ]) { + // Skip SynthLogics whose name was never picked (replaced/pruned). + try { + final n = sl.name; + for (final logic in sl.logics) { + names[logic] = n; + } + // ignore: avoid_catches_without_on_clauses + } catch (_) { + // name not picked — skip + } + } + return names; +} + +void main() { + group('naming consistency', () { + test('SV and base SynthModuleDefinition agree on port names', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // SV synthesizer path + final svDef = SystemVerilogSynthModuleDefinition(mod); + + // Base path (same as netlist synthesizer uses) + // Since namer is late final, the second constructor reuses + // the same naming state — names must be consistent. + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + // Every Logic present in both must have the same name. + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name} ' + '(${logic.runtimeType}, naming=${logic.naming})'); + } + } + + // Port names specifically must match. + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + expect(svNames[port], isNotNull, + reason: 'SV def should have port ${port.name}'); + expect(baseNames[port], isNotNull, + reason: 'Base def should have port ${port.name}'); + expect(svNames[port], baseNames[port], + reason: 'Port name must match for ${port.name}'); + } + }); + + test('constant naming is consistent', () async { + final mod = _ConstModule(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('mixed naming (renameable + mergeable) is consistent', () async { + final mod = _MixedNaming(Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('flop module naming is consistent', () async { + final mod = _FlopOuter(Logic(), Logic(width: 8)); + await mod.build(); + + final svDef = SystemVerilogSynthModuleDefinition(mod); + final baseDef = SynthModuleDefinition(mod); + + final svNames = _collectNames(svDef); + final baseNames = _collectNames(baseDef); + + for (final logic in svNames.keys) { + if (baseNames.containsKey(logic)) { + expect(baseNames[logic], svNames[logic], + reason: 'Name mismatch for ${logic.name}'); + } + } + }); + + test('namer is shared across multiple SynthModuleDefinitions', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + // Build one def, then build another — same namer instance. + final def1 = SynthModuleDefinition(mod); + final def2 = SynthModuleDefinition(mod); + + final names1 = _collectNames(def1); + final names2 = _collectNames(def2); + + for (final logic in names1.keys) { + if (names2.containsKey(logic)) { + expect(names2[logic], names1[logic], + reason: 'Shared namer should produce same name for ' + '${logic.name}'); + } + } + }); + + test('Namer.signalNameOf matches SynthLogic.name for ports', () async { + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + final synthNames = _collectNames(def); + + // Module.namer.signalNameOf uses Namer directly + for (final port in [...mod.inputs.values, ...mod.outputs.values]) { + final moduleName = mod.namer.signalNameOf(port); + final synthName = synthNames[port]; + expect(synthName, moduleName, + reason: 'SynthLogic.name and Module.namer.signalNameOf must agree ' + 'for port ${port.name}'); + } + }); + + test('submodule instance names are allocated from the shared namespace', + () async { + // Instance names come from Module.namer.allocateName, which + // shares the same namespace as signal names. + final mod = _Outer(Logic(width: 8), Logic(width: 8)); + await mod.build(); + + final def = SynthModuleDefinition(mod); + + final instNames = def.subModuleInstantiations + .where((s) => s.needsInstantiation) + .map((s) => s.name) + .toSet(); + + // The inner module instance should have a name + expect(instNames, isNotEmpty, + reason: 'Should have at least one submodule instance'); + + // Instance names are claimed in the shared namespace. + for (final name in instNames) { + expect(mod.namer.isAvailable(name), isFalse, + reason: 'Instance name "$name" should be claimed in the ' + 'namespace'); + } + }); + }); +} diff --git a/test/naming_namespace_test.dart b/test/naming_namespace_test.dart new file mode 100644 index 000000000..a5263a998 --- /dev/null +++ b/test/naming_namespace_test.dart @@ -0,0 +1,129 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// naming_namespace_test.dart +// Tests for constant naming via nameOfBest and shared instance/signal +// namespace uniquification. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +/// A simple submodule whose instance name can collide with a signal name. +class _Inner extends Module { + _Inner(Logic a, {super.name = 'inner'}) { + a = addInput('a', a); + addOutput('b') <= ~a; + } +} + +/// Top module that has a signal named the same as a submodule instance. +class _InstanceSignalCollision extends Module { + _InstanceSignalCollision({String instanceName = 'inner'}) + : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // Create a signal whose name matches the submodule instance name. + final sig = Logic(name: instanceName); + sig <= ~a; + + final sub = _Inner(sig, name: instanceName); + o <= sub.output('b'); + } +} + +/// Top module with two submodule instances that have the same name. +class _DuplicateInstances extends Module { + _DuplicateInstances() : super(name: 'top') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + final sub1 = _Inner(a, name: 'blk'); + final sub2 = _Inner(sub1.output('b'), name: 'blk'); + o <= sub2.output('b'); + } +} + +/// Module that uses a constant in a connection chain, exercising constant +/// naming through nameOfBest. +class _ConstantNamingModule extends Module { + _ConstantNamingModule() : super(name: 'const_mod') { + final a = addInput('a', Logic()); + final o = addOutput('o'); + + // A constant "1" drives one input of the AND gate. + o <= a & Const(1); + } +} + +/// Module with a mux where one input is a constant, exercising the +/// constNameDisallowed path — the mux output cannot use the constant's +/// literal as its name because it also carries non-constant values. +class _ConstNameDisallowedModule extends Module { + _ConstNameDisallowedModule() : super(name: 'const_disallow') { + final a = addInput('a', Logic()); + final sel = addInput('sel', Logic()); + final o = addOutput('o'); + + // mux output can be the constant OR a, so the constant name is disallowed. + o <= mux(sel, Const(1), a); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('constant naming via nameOfBest', () { + test('constant value appears as literal in SV output', () async { + final dut = _ConstantNamingModule(); + await dut.build(); + final sv = dut.generateSynth(); + + // The constant "1" should appear as a literal 1'h1 in the output, + // not as a declared signal. + expect(sv, contains("1'h1")); + }); + + test('constNameDisallowed falls through to signal naming', () async { + final dut = _ConstNameDisallowedModule(); + await dut.build(); + final sv = dut.generateSynth(); + + // The output assignment should NOT use the raw constant literal + // as a wire name; a proper signal name should be used instead. + // The constant still appears as a literal in the mux expression. + expect(sv, contains("1'h1")); + // The output 'o' should be assigned from something. + expect(sv, contains('o')); + }); + }); + + group('shared instance and signal namespace', () { + test( + 'signal and instance with same name get uniquified ' + 'in the shared namespace', () async { + final dut = _InstanceSignalCollision(); + await dut.build(); + final sv = dut.generateSynth(); + + // With a single shared namespace, one of the two "inner" identifiers + // must be suffixed to avoid collision. + expect(sv, contains('inner_0')); + }); + + test('duplicate instance names get uniquified', () async { + final dut = _DuplicateInstances(); + await dut.build(); + final sv = dut.generateSynth(); + + // Two instances named 'blk' — one should be 'blk', the other 'blk_0'. + expect(sv, contains('blk')); + expect(sv, contains(RegExp(r'blk_\d'))); + }); + }); +} diff --git a/test/netlist_example_test.dart b/test/netlist_example_test.dart new file mode 100644 index 000000000..e5d7b7cbe --- /dev/null +++ b/test/netlist_example_test.dart @@ -0,0 +1,285 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_example_test.dart +// Convert examples to netlist JSON and check the produced output. + +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +void main() { + // Detect whether running in JS (dart2js) environment. In JS many + // `dart:io` APIs are unsupported; when running tests with + // `--platform node` we skip filesystem and loader assertions. + const isJS = identical(0, 0.0); + + // Helper used by the tests to synthesize `top` and optionally write the + // produced JSON to `outPath` when running on VM. Returns the decoded + // modules map from the Yosys-format JSON. + Future> convertTestWriteNetlist( + Module top, + String outPath, + ) async { + final synth = SynthBuilder(top, NetlistSynthesizer()); + final jsonStr = + await (synth.synthesizer as NetlistSynthesizer).synthesizeToJson(top); + if (!isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(jsonStr); + } + final decoded = jsonDecode(jsonStr) as Map; + return decoded['modules'] as Map; + } + + test('Netlist dump for example Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + counter.generateSynth(); + + final modules = await convertTestWriteNetlist( + counter, + 'build/Counter.rohd.json', + ); + + expect( + modules, + isNotEmpty, + reason: 'Counter netlist should have module definitions', + ); + // The top module should have cells (sub-module instances or gates) + final topMod = modules[counter.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'Counter should have cells'); + }); + + group('SynthBuilder netlist generation for examples', () { + test('SynthBuilder netlist for Counter', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final counter = Counter(en, reset, clk); + await counter.build(); + + final modules = await convertTestWriteNetlist( + counter, + 'build/Counter.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'Counter synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = + FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await fir.build(); + + final synth = SynthBuilder(fir, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + fir, + 'build/FirFilter.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'FirFilter synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = LogicArrayExample( + arrayA, + id, + selectIndexValue, + selectFromValue, + ); + await la.build(); + + final synth = SynthBuilder(la, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + la, + 'build/LogicArrayExample.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'LogicArrayExample synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + final synth = SynthBuilder(oven, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + final modules = await convertTestWriteNetlist( + oven, + 'build/OvenModule.synth.rohd.json', + ); + expect( + modules, + isNotEmpty, + reason: 'OvenModule synth netlist should have modules', + ); + }); + + test('SynthBuilder netlist for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + final synth = SynthBuilder(tree, NetlistSynthesizer()); + expect(synth.synthesisResults.isNotEmpty, isTrue); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser (pure Dart or JS). + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(tree); + expect( + json, + isNotEmpty, + reason: 'TreeOfTwoInputModules should produce non-empty JSON', + ); + if (!isJS) { + final file = File('build/TreeOfTwoInputModules.synth.rohd.json'); + await file.create(recursive: true); + await file.writeAsString(json); + } + }); + }); + + test('Netlist dump for FIR filter example', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + + final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await fir.build(); + + const outPath = 'build/FirFilter.rohd.json'; + final modules = await convertTestWriteNetlist(fir, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'FirFilter netlist should have module definitions', + ); + }); + + test('Netlist dump for LogicArray example', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + + final la = LogicArrayExample(arrayA, id, selectIndexValue, selectFromValue); + await la.build(); + + const outPath = 'build/LogicArrayExample.rohd.json'; + final modules = await convertTestWriteNetlist(la, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'LogicArrayExample netlist should have module definitions', + ); + }); + + test('Netlist dump for OvenModule example', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final oven = OvenModule(button, reset, clk); + await oven.build(); + + const outPath = 'build/OvenModule.rohd.json'; + final modules = await convertTestWriteNetlist(oven, outPath); + if (!isJS) { + final f = File(outPath); + expect(f.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + final contents = await f.readAsString(); + expect(contents.trim().isNotEmpty, isTrue); + } + expect( + modules, + isNotEmpty, + reason: 'OvenModule netlist should have module definitions', + ); + }); + + test('Netlist dump for TreeOfTwoInputModules example', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser. + const outPath = 'build/TreeOfTwoInputModules.rohd.json'; + final synth = SynthBuilder(tree, NetlistSynthesizer()); + final json = + await (synth.synthesizer as NetlistSynthesizer).synthesizeToJson(tree); + expect( + json, + isNotEmpty, + reason: 'TreeOfTwoInputModules should produce non-empty JSON', + ); + if (!isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(json); + expect(file.existsSync(), isTrue, reason: 'ROHD JSON should be created'); + } + }); +} diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart new file mode 100644 index 000000000..8e08e2945 --- /dev/null +++ b/test/netlist_synthesizer_test.dart @@ -0,0 +1,1436 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_synthesizer_test.dart +// Comprehensive tests for the netlist synthesizer covering leaf cell +// mapping, structural validation, options permutations, and real +// example designs. +// +// 2026 April 13 +// Author: Auto-generated + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +// ──────────────────────────────────────────────────────────────────── +// Tiny helper modules for targeted gate-level tests +// ──────────────────────────────────────────────────────────────────── + +/// Exercises And2Gate. +class AndModule extends Module { + Logic get y => output('y'); + AndModule(Logic a, Logic b) : super(name: 'andmod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a & b; + } +} + +/// Exercises Or2Gate. +class OrModule extends Module { + Logic get y => output('y'); + OrModule(Logic a, Logic b) : super(name: 'ormod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a | b; + } +} + +/// Exercises Xor2Gate. +class XorModule extends Module { + Logic get y => output('y'); + XorModule(Logic a, Logic b) : super(name: 'xormod') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a ^ b; + } +} + +/// Exercises NotGate. +class NotModule extends Module { + Logic get y => output('y'); + NotModule(Logic a) : super(name: 'notmod') { + a = addInput('a', a); + addOutput('y') <= ~a; + } +} + +/// Exercises Mux. +class MuxModule extends Module { + Logic get y => output('y'); + MuxModule(Logic sel, Logic a, Logic b, {int width = 8}) : super(name: 'mux') { + sel = addInput('sel', sel); + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y', width: width) <= mux(sel, a, b); + } +} + +/// Exercises FlipFlop. +class FlopModule extends Module { + Logic get q => output('q'); + FlopModule(Logic clk, Logic d, {int width = 8}) : super(name: 'flopmod') { + clk = addInput('clk', clk); + d = addInput('d', d, width: width); + addOutput('q', width: width) <= flop(clk, d); + } +} + +/// Exercises Add. +class AddModule extends Module { + Logic get sum => output('sum'); + AddModule(Logic a, Logic b, {int width = 8}) : super(name: 'addmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('sum', width: width) <= a + b; + } +} + +/// Exercises Multiply. +class MulModule extends Module { + Logic get prod => output('prod'); + MulModule(Logic a, Logic b, {int width = 8}) : super(name: 'mulmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('prod', width: width) <= a * b; + } +} + +/// Exercises BusSubset ($slice). +class SliceModule extends Module { + Logic get y => output('y'); + SliceModule(Logic a) : super(name: 'slicemod') { + a = addInput('a', a, width: 8); + addOutput('y', width: 4) <= a.getRange(2, 6); + } +} + +/// Exercises comparison operators. +class CompareModule extends Module { + Logic get lt => output('lt'); + Logic get gt => output('gt'); + Logic get eq => output('eq'); + CompareModule(Logic a, Logic b, {int width = 8}) : super(name: 'cmpmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('lt') <= LessThan(a, b).out; + addOutput('gt') <= GreaterThan(a, b).out; + addOutput('eq') <= a.eq(b); + } +} + +/// Exercises shift operations. +class ShiftModule extends Module { + Logic get shl => output('shl'); + Logic get shr => output('shr'); + ShiftModule(Logic a, Logic amt, {int width = 8}) : super(name: 'shiftmod') { + a = addInput('a', a, width: width); + amt = addInput('amt', amt, width: width); + addOutput('shl', width: width) <= a << amt; + addOutput('shr', width: width) <= a >>> amt; + } +} + +/// Exercises Xor2Gate. +class XorGateModule extends Module { + Logic get y => output('y'); + XorGateModule(Logic a, Logic b) : super(name: 'xormod2') { + a = addInput('a', a); + b = addInput('b', b); + addOutput('y') <= a ^ b; + } +} + +/// Exercises Subtract. +class SubModule extends Module { + Logic get diff => output('diff'); + SubModule(Logic a, Logic b, {int width = 8}) : super(name: 'submod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('diff', width: width) <= a - b; + } +} + +/// Exercises Swizzle ($concat). +class SwizzleModule extends Module { + Logic get y => output('y'); + SwizzleModule(Logic a, Logic b, {int width = 4}) : super(name: 'swizmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y', width: width * 2) <= [a, b].swizzle(); + } +} + +/// Exercises arithmetic right shift (ARShift). +class ARShiftModule extends Module { + Logic get y => output('y'); + ARShiftModule(Logic a, Logic amt, {int width = 8}) + : super(name: 'arshiftmod') { + a = addInput('a', a, width: width); + amt = addInput('amt', amt, width: width); + addOutput('y', width: width) <= a >> amt; + } +} + +/// Exercises unary reduction ops. +class ReduceModule extends Module { + Logic get andR => output('andR'); + Logic get orR => output('orR'); + Logic get xorR => output('xorR'); + ReduceModule(Logic a, {int width = 8}) : super(name: 'reducemod') { + a = addInput('a', a, width: width); + addOutput('andR') <= a.and(); + addOutput('orR') <= a.or(); + addOutput('xorR') <= a.xor(); + } +} + +/// Exercises individual comparison ops for cell-type checking. +class LtModule extends Module { + Logic get y => output('y'); + LtModule(Logic a, Logic b, {int width = 8}) : super(name: 'ltmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.lt(b); + } +} + +class GtModule extends Module { + Logic get y => output('y'); + GtModule(Logic a, Logic b, {int width = 8}) : super(name: 'gtmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.gt(b); + } +} + +class EqModule extends Module { + Logic get y => output('y'); + EqModule(Logic a, Logic b, {int width = 8}) : super(name: 'eqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.eq(b); + } +} + +class NeqModule extends Module { + Logic get y => output('y'); + NeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'neqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.neq(b); + } +} + +class LeqModule extends Module { + Logic get y => output('y'); + LeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'leqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.lte(b); + } +} + +class GeqModule extends Module { + Logic get y => output('y'); + GeqModule(Logic a, Logic b, {int width = 8}) : super(name: 'geqmod') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y') <= a.gte(b); + } +} + +/// Exercises TriStateBuffer. +class TriBufModule extends Module { + Logic get bus => inOut('bus'); + TriBufModule(LogicNet busNet, Logic data, Logic en) + : super(name: 'tribufmod') { + final bus = addInOut('bus', busNet, width: data.width); + data = addInput('data', data, width: data.width); + en = addInput('en', en); + TriStateBuffer(data, enable: en, name: 'tsb').out.gets(bus); + } +} + +/// Exercises Combinational with If. +class CombIfModule extends Module { + Logic get y => output('y'); + CombIfModule(Logic sel, Logic a, Logic b, {int width = 8}) + : super(name: 'combif') { + sel = addInput('sel', sel); + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final y = addOutput('y', width: width); + Combinational([ + If(sel, then: [y < a], orElse: [y < b]), + ]); + } +} + +/// Exercises Sequential with If. +class SeqIfModule extends Module { + Logic get q => output('q'); + SeqIfModule(Logic clk, Logic en, Logic d, {int width = 8}) + : super(name: 'seqif') { + clk = addInput('clk', clk); + en = addInput('en', en); + d = addInput('d', d, width: width); + final q = addOutput('q', width: width); + Sequential(clk, [ + If(en, then: [q < d]), + ]); + } +} + +/// Module with multiple instances of the same sub-module (dedup test). +class DedupTop extends Module { + Logic get y0 => output('y0'); + Logic get y1 => output('y1'); + DedupTop(Logic a, Logic b, {int width = 8}) + : super(name: 'deduptop', definitionName: 'DedupTop') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + addOutput('y0', width: width) <= AddModule(a, b, width: width).sum; + addOutput('y1', width: width) <= AddModule(a, b, width: width).sum; + } +} + +/// Module with different-width instances (no dedup). +class NoDedupTop extends Module { + Logic get y0 => output('y0'); + Logic get y1 => output('y1'); + NoDedupTop(Logic a4, Logic b4, Logic a8, Logic b8) + : super(name: 'nodeduptop', definitionName: 'NoDedupTop') { + a4 = addInput('a4', a4, width: 4); + b4 = addInput('b4', b4, width: 4); + a8 = addInput('a8', a8, width: 8); + b8 = addInput('b8', b8, width: 8); + addOutput('y0', width: 4) <= AddModule(a4, b4, width: 4).sum; + addOutput('y1', width: 8) <= AddModule(a8, b8).sum; + } +} + +// ──────────────────────────────────────────────────────────────────── +// Helpers +// ──────────────────────────────────────────────────────────────────── + +/// Build a FilterBank module for testing (not yet built). +FilterBank _buildFilterBank() { + const dataWidth = 16; + const numTaps = 3; + const coeffs0 = [1, 2, 1]; + const coeffs1 = [1, -2, 1]; + + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = LogicArray([2], dataWidth, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [coeffs0, coeffs1], + ); +} + +/// Build a module and synthesize to a parsed JSON map. +Future> _synthToMap( + Module mod, { + NetlistOptions options = const NetlistOptions(), +}) async { + await mod.build(); + final synth = SynthBuilder(mod, NetlistSynthesizer(options: options)); + final json = await (synth.synthesizer as NetlistSynthesizer).synthesizeToJson( + mod, + ); + return jsonDecode(json) as Map; +} + +/// Extract the `modules` map from a synthesized JSON map. +Map _modules(Map json) => + json['modules'] as Map; + +/// Get cells map from a module definition. +Map _cells(Map moduleDef) => + moduleDef['cells'] as Map? ?? {}; + +/// Get ports map from a module definition. +Map _ports(Map moduleDef) => + moduleDef['ports'] as Map? ?? {}; + +/// Get netnames map from a module definition. +Map _netnames(Map moduleDef) => + moduleDef['netnames'] as Map? ?? {}; + +/// Check that a module definition has a port with given name and direction. +void _expectPort( + Map moduleDef, + String portName, + String direction, +) { + final ports = _ports(moduleDef); + expect(ports, contains(portName), reason: 'Expected port "$portName"'); + final port = ports[portName] as Map; + expect( + port['direction'], + equals(direction), + reason: 'Port "$portName" should be "$direction"', + ); +} + +/// Returns true if any cell in any module definition has the given type. +bool _hasCellType(Map json, String cellType) { + final mod = _modules(json); + return mod.values.any((m) { + final def = m as Map; + return _cells(def).values.any((c) { + final cell = c as Map; + return (cell['type'] as String) == cellType; + }); + }); +} + +// ──────────────────────────────────────────────────────────────────── +// Tests +// ──────────────────────────────────────────────────────────────────── + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + // ── Group 1: Leaf cell mapper — individual gate mappings ─────────── + + group('leaf cell mapping', () { + test(r'And2Gate maps to $and cell', () async { + final json = await _synthToMap(AndModule(Logic(), Logic())); + expect(_hasCellType(json, r'$and'), isTrue); + }); + + test(r'Or2Gate maps to $or cell', () async { + final json = await _synthToMap(OrModule(Logic(), Logic())); + expect(_hasCellType(json, r'$or'), isTrue); + }); + + test(r'Xor2Gate maps to $xor cell', () async { + final json = await _synthToMap(XorGateModule(Logic(), Logic())); + expect(_hasCellType(json, r'$xor'), isTrue); + }); + + test(r'NotGate maps to $not cell', () async { + final json = await _synthToMap(NotModule(Logic())); + expect(_hasCellType(json, r'$not'), isTrue); + }); + + test(r'Mux maps to $mux cell', () async { + final json = await _synthToMap( + MuxModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$mux'), isTrue); + }); + + test(r'FlipFlop maps to $dff cell', () async { + final clk = SimpleClockGenerator(10).clk; + final json = await _synthToMap(FlopModule(clk, Logic(width: 8))); + expect(_hasCellType(json, r'$dff'), isTrue); + }); + + test(r'Add maps to $add cell', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$add'), isTrue); + }); + + test(r'Subtract maps to $sub cell', () async { + final json = await _synthToMap( + SubModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$sub'), isTrue); + }); + + test(r'Multiply maps to $mul cell', () async { + final json = await _synthToMap( + MulModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$mul'), isTrue); + }); + + test(r'BusSubset maps to $slice cell', () async { + final json = await _synthToMap(SliceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$slice'), isTrue); + }); + + test(r'Swizzle maps to $concat cell', () async { + final json = await _synthToMap( + SwizzleModule(Logic(width: 4), Logic(width: 4)), + ); + expect(_hasCellType(json, r'$concat'), isTrue); + }); + + test(r'LessThan maps to $lt cell', () async { + final json = await _synthToMap( + LtModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$lt'), isTrue); + }); + + test(r'GreaterThan maps to $gt cell', () async { + final json = await _synthToMap( + GtModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$gt'), isTrue); + }); + + test(r'Equals maps to $eq cell', () async { + final json = await _synthToMap( + EqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$eq'), isTrue); + }); + + test(r'NotEquals maps to $ne cell', () async { + final json = await _synthToMap( + NeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$ne'), isTrue); + }); + + test(r'LessThanOrEqual maps to $le cell', () async { + final json = await _synthToMap( + LeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$le'), isTrue); + }); + + test(r'GreaterThanOrEqual maps to $ge cell', () async { + final json = await _synthToMap( + GeqModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$ge'), isTrue); + }); + + test(r'LShift maps to $shl cell', () async { + final json = await _synthToMap( + ShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shl'), isTrue); + }); + + test(r'RShift maps to $shr cell', () async { + final json = await _synthToMap( + ShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shr'), isTrue); + }); + + test(r'ARShift maps to $shiftx cell', () async { + final json = await _synthToMap( + ARShiftModule(Logic(width: 8), Logic(width: 8)), + ); + expect(_hasCellType(json, r'$shiftx'), isTrue); + }); + + test(r'AndUnary maps to $reduce_and cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_and'), isTrue); + }); + + test(r'OrUnary maps to $reduce_or cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_or'), isTrue); + }); + + test(r'XorUnary maps to $reduce_xor cell', () async { + final json = await _synthToMap(ReduceModule(Logic(width: 8))); + expect(_hasCellType(json, r'$reduce_xor'), isTrue); + }); + + test(r'TriStateBuffer maps to $tribuf cell', () async { + final busNet = LogicNet(width: 8); + final json = await _synthToMap( + TriBufModule(busNet, Logic(width: 8), Logic()), + ); + expect(_hasCellType(json, r'$tribuf'), isTrue); + }); + }); + + // ── Group 2: Structural content validation ───────────────────────── + + group('structural validation', () { + test('ports have correct direction', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + // Find the top-level or AddModule definition + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + final ports = _ports(d); + for (final port in ports.entries) { + final p = port.value as Map; + expect( + ['input', 'output', 'inout'].contains(p['direction']), + isTrue, + reason: 'Port ${port.key} should have valid direction', + ); + // Each port should have bits + expect( + p['bits'], + isNotNull, + reason: 'Port ${port.key} should have bits array', + ); + } + } + }); + + test('cells have type and connections', () async { + final json = await _synthToMap( + MuxModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + expect(c['type'], isNotNull, reason: 'Every cell should have a type'); + expect( + c['connections'], + isNotNull, + reason: 'Every cell should have connections', + ); + } + } + }); + + test('netnames have bits arrays', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final def in mod.values) { + final d = def as Map; + for (final nn in _netnames(d).values) { + final n = nn as Map; + expect( + n['bits'], + isA>(), + reason: 'Each netname should have a bits list', + ); + } + } + }); + + test('inOut ports have direction inout', () async { + final busNet = LogicNet(width: 8); + final json = await _synthToMap( + TriBufModule(busNet, Logic(width: 8), Logic()), + ); + final mod = _modules(json); + // Find the TriBufModule definition + final tribufDef = mod.values.firstWhere((m) { + final d = m as Map; + return _ports(d).values.any((p) { + final port = p as Map; + return port['direction'] == 'inout'; + }); + }, orElse: () => {}) as Map; + expect( + tribufDef, + isNotEmpty, + reason: 'Should have a module with inout ports', + ); + }); + + test('Combinational If produces Combinational cell', () async { + final json = await _synthToMap( + CombIfModule(Logic(), Logic(width: 8), Logic(width: 8)), + ); + // Combinational blocks become Combinational cell type + expect( + _hasCellType(json, 'Combinational'), + isTrue, + reason: 'Combinational If should produce a Combinational cell', + ); + }); + + test('Sequential If produces dff cells', () async { + final clk = SimpleClockGenerator(10).clk; + final json = await _synthToMap( + SeqIfModule(clk, Logic(), Logic(width: 8)), + ); + final mod = _modules(json); + final hasSeq = mod.values.any((m) { + final def = m as Map; + final cells = _cells(def); + return cells.values.any((c) { + final cell = c as Map; + return (cell['type'] as String).contains('Sequential'); + }); + }); + expect( + hasSeq, + isTrue, + reason: 'Sequential If should contain Sequential cells', + ); + }); + }); + + // ── Group 3: Module deduplication ────────────────────────────────── + + group('deduplication', () { + test('identical sub-modules are deduplicated', () async { + final json = await _synthToMap( + DedupTop(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + // AddModule should appear only once as a definition + final addDefs = mod.keys.where((k) => k.contains('Add')).toList(); + expect( + addDefs.length, + equals(1), + reason: 'Two identical AddModules should produce one definition', + ); + // But should be instantiated twice in the top-level cells + final topDef = mod.entries + .firstWhere((e) => e.key.contains('DedupTop')) + .value as Map; + final addCells = _cells(topDef).values.where((c) { + final cell = c as Map; + return (cell['type'] as String).contains('Add'); + }).toList(); + expect( + addCells.length, + equals(2), + reason: 'Top module should instantiate AddModule twice', + ); + }); + + test('different-width sub-modules are not deduplicated', () async { + final json = await _synthToMap( + NoDedupTop( + Logic(width: 4), + Logic(width: 4), + Logic(width: 8), + Logic(width: 8), + ), + ); + final mod = _modules(json); + // Should have two distinct AddModule definitions (different widths) + final addDefs = mod.keys.where((k) => k.contains('Add')).toList(); + expect( + addDefs.length, + greaterThanOrEqualTo(2), + reason: 'Different-width AddModules should NOT be deduplicated', + ); + }); + }); + + // ── Group 4: NetlistOptions permutations ───────────────────────── + + group('NetlistOptions', () { + late Module filterBank; + + setUp(() async { + await Simulator.reset(); + filterBank = _buildFilterBank(); + await filterBank.build(); + }); + + test('default options produce valid netlist', () async { + final synth = SynthBuilder(filterBank, NetlistSynthesizer()); + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('slimMode omits connections', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + ); + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + final mod = _modules(parsed); + expect(mod, isNotEmpty); + // In slim mode, cells should exist but connections should be empty + for (final def in mod.values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + final conns = c['connections'] as Map?; + if (conns != null) { + expect( + conns, + isEmpty, + reason: 'Slim mode cells should have empty connections', + ); + } + } + } + }); + + test('groupStructConversions produces valid netlist', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + options: const NetlistOptions(groupStructConversions: true), + ), + ); + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('collapseStructGroups with groupStructConversions', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer( + options: const NetlistOptions( + groupStructConversions: true, + collapseStructGroups: true, + ), + ), + ); + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('DCE disabled still produces valid netlist', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + ); + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('all optimizations disabled produces valid netlist', () async { + final synth = SynthBuilder( + filterBank, + NetlistSynthesizer(options: const NetlistOptions(enableDCE: false)), + ); + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final parsed = jsonDecode(json) as Map; + expect(_modules(parsed), isNotEmpty); + }); + + test('slim and full produce same module definitions', () async { + final fullSynth = SynthBuilder(filterBank, NetlistSynthesizer()); + final fullJson = await (fullSynth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(filterBank); + final fullParsed = jsonDecode(fullJson) as Map; + + // Rebuild for slim + await Simulator.reset(); + final fb2 = _buildFilterBank(); + await fb2.build(); + final slimSynth = SynthBuilder( + fb2, + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)), + ); + final slimJson = await (slimSynth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(fb2); + final slimParsed = jsonDecode(slimJson) as Map; + + // Same module definition names + expect( + _modules(slimParsed).keys.toSet(), + equals(_modules(fullParsed).keys.toSet()), + reason: 'Slim and full should have identical module definition names', + ); + }); + }); + + // ── Group 5: Example designs — structural checks ─────────────────── + + group('example designs', () { + test('Counter netlist has FlipFlop and FSM-related cells', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = Counter(en, reset, clk); + final json = await _synthToMap(counter); + final mod = _modules(json); + + expect( + mod, + isNotEmpty, + reason: 'Counter should produce module definitions', + ); + // Should have a Counter definition + expect(mod.keys.any((k) => k.contains('Counter')), isTrue); + }); + + test('FirFilter netlist has pipeline and multiplier cells', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + final fir = FirFilter( + en, + resetB, + clk, + inputVal, + [ + 0, + 0, + 0, + 1, + ], + bitWidth: 8); + final json = await _synthToMap(fir); + final mod = _modules(json); + + expect( + mod, + isNotEmpty, + reason: 'FirFilter should produce module definitions', + ); + }); + + test('OvenModule netlist has FSM states', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final oven = OvenModule(button, reset, clk); + final json = await _synthToMap(oven); + final mod = _modules(json); + + expect(mod, isNotEmpty); + // Should have OvenModule definition + expect( + mod.keys.any((k) => k.contains('Oven') || k.contains('oven')), + isTrue, + ); + }); + + test('LogicArrayExample netlist has array-related cells', () async { + final arrayA = LogicArray([4], 8, name: 'arrayA'); + final id = Logic(name: 'id', width: 3); + final selectIndexValue = Logic(name: 'selectIndexValue', width: 8); + final selectFromValue = Logic(name: 'selectFromValue', width: 8); + final la = LogicArrayExample( + arrayA, + id, + selectIndexValue, + selectFromValue, + ); + final json = await _synthToMap(la); + final mod = _modules(json); + + expect(mod, isNotEmpty); + }); + + test('TreeOfTwoInputModules netlist has recursive hierarchy', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + final synth = SynthBuilder(tree, NetlistSynthesizer()); + final json = await (synth.synthesizer as NetlistSynthesizer) + .synthesizeToJson(tree); + expect(json, isNotEmpty); + final parsed = jsonDecode(json) as Map; + final mod = _modules(parsed); + expect(mod, isNotEmpty, reason: 'Tree should have module definitions'); + }); + }); + + // ── Group 6: FilterBank deep structural checks ───────────────────── + + group('FilterBank netlist structure', () { + late Map json; + + setUpAll(() async { + final fb = _buildFilterBank(); + json = await _synthToMap(fb); + }); + + test('contains expected module definitions', () { + final mod = _modules(json); + final defNames = mod.keys.toSet(); + + // FilterBank, FilterChannel, CoeffBank, MacUnit, FilterController + // should all appear (possibly with parameterized suffixes) + expect( + defNames.any((k) => k.contains('FilterBank')), + isTrue, + reason: 'Should have FilterBank definition', + ); + expect( + defNames.any((k) => k.contains('FilterChannel')), + isTrue, + reason: 'Should have FilterChannel definition', + ); + expect( + defNames.any((k) => k.contains('CoeffBank')), + isTrue, + reason: 'Should have CoeffBank definition', + ); + expect( + defNames.any((k) => k.contains('MacUnit')), + isTrue, + reason: 'Should have MacUnit definition', + ); + expect( + defNames.any((k) => k.contains('FilterController')), + isTrue, + reason: 'Should have FilterController definition', + ); + }); + + test('FilterBank has array ports', () { + final mod = _modules(json); + final fbDef = mod.entries + .firstWhere((e) => e.key.contains('FilterBank')) + .value as Map; + final ports = _ports(fbDef); + + // Should have samplesIn and channelOut as array ports + expect( + ports.keys.any( + (k) => k.contains('samplesIn') || k.contains('channelOut'), + ), + isTrue, + reason: 'FilterBank should have array port signals', + ); + }); + + test('FilterBank top instantiates two FilterChannels', () { + final mod = _modules(json); + final fbDef = mod.entries + .firstWhere((e) => e.key.contains('FilterBank')) + .value as Map; + final cells = _cells(fbDef); + + final channelCells = cells.entries.where((e) { + final cell = e.value as Map; + return (cell['type'] as String).contains('FilterChannel'); + }).toList(); + + expect( + channelCells.length, + equals(2), + reason: 'FilterBank should instantiate 2 FilterChannels', + ); + }); + + test( + 'FilterChannels with different coefficients get separate definitions', + () { + final mod = _modules(json); + final channelDefs = + mod.keys.where((k) => k.contains('FilterChannel')).toList(); + + expect( + channelDefs.length, + equals(2), + reason: 'Two FilterChannels with different coefficients ' + 'should produce distinct definitions', + ); + }, + ); + + test('MacUnit definition contains Pipeline-generated cells', () { + final mod = _modules(json); + final macDef = mod.entries + .firstWhere((e) => e.key.contains('MacUnit')) + .value as Map; + final cells = _cells(macDef); + + // Pipeline generates Sequential cells for stage registers + final hasSeq = cells.values.any((c) { + final cell = c as Map; + final type = cell['type'] as String; + return type.contains('Sequential'); + }); + expect( + hasSeq, + isTrue, + reason: 'MacUnit Pipeline should produce Sequential cells', + ); + }); + + test('CoeffBank has coeffArray input port', () { + final mod = _modules(json); + final coeffDef = mod.entries + .firstWhere((e) => e.key.contains('CoeffBank')) + .value as Map; + final ports = _ports(coeffDef); + + // Should have coeffArray-related port names + expect( + ports.keys.any((k) => k.contains('coeffArray')), + isTrue, + reason: 'CoeffBank should have coeffArray port', + ); + + // tapIndex should be input + expect( + ports.keys.any((k) => k.contains('tapIndex')), + isTrue, + reason: 'CoeffBank should have tapIndex port', + ); + }); + + test('FilterController has FSM state output', () { + final mod = _modules(json); + final ctrlDef = mod.entries + .firstWhere((e) => e.key.contains('FilterController')) + .value as Map; + final ports = _ports(ctrlDef); + + _expectPort(ctrlDef, 'state', 'output'); + _expectPort(ctrlDef, 'filterEnable', 'output'); + _expectPort(ctrlDef, 'doneFlag', 'output'); + expect(ports.keys.any((k) => k.contains('clk')), isTrue); + expect(ports.keys.any((k) => k.contains('reset')), isTrue); + }); + + test('all module definitions have valid JSON structure', () { + final mod = _modules(json); + for (final entry in mod.entries) { + final defName = entry.key; + final def = entry.value as Map; + + // Every definition must have ports and cells + expect( + def.containsKey('ports'), + isTrue, + reason: '$defName should have ports', + ); + expect( + def.containsKey('cells'), + isTrue, + reason: '$defName should have cells', + ); + + // All ports must have direction and bits + for (final port in _ports(def).entries) { + final p = port.value as Map; + expect( + p.containsKey('direction'), + isTrue, + reason: '$defName.${port.key} should have direction', + ); + expect( + p.containsKey('bits'), + isTrue, + reason: '$defName.${port.key} should have bits', + ); + } + + // All cells must have type + for (final cell in _cells(def).entries) { + final c = cell.value as Map; + expect( + c.containsKey('type'), + isTrue, + reason: '$defName cell ${cell.key} should have type', + ); + } + } + }); + }); + + // ── Group 7: Design API path ─────────────────────────────────────── + + group('Design API path', () { + test('build with netlistOptions enables NetlistService', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = Counter(en, reset, clk); + + await counter.build(); + final netSvc = await NetlistService.create(counter); + + final fullJson = netSvc.toJson(); + expect(fullJson, isNotNull); + + final parsed = jsonDecode(fullJson) as Map; + expect(parsed.containsKey('modules'), isTrue); + }); + + test('moduleJson returns per-module data', () async { + final fb = _buildFilterBank(); + await fb.build(); + final netSvc = await NetlistService.create(fb); + + // Fetch FilterBank definition specifically + final fbJson = netSvc.moduleJson(fb.definitionName); + final parsed = jsonDecode(fbJson) as Map; + expect(parsed.containsKey('modules'), isTrue); + final modules = parsed['modules'] as Map; + expect(modules.containsKey(fb.definitionName), isTrue); + }); + + test('slimJson produces slim output', () async { + final fb = _buildFilterBank(); + await fb.build(); + final netSvc = await NetlistService.create(fb); + + final slimJson = netSvc.slimJson; + + final parsed = jsonDecode(slimJson) as Map; + expect(parsed.containsKey('netlist'), isTrue); + final netlist = parsed['netlist'] as Map; + final modules = netlist['modules'] as Map; + expect(modules, isNotEmpty); + }); + }); + + // ── Group 8: Wire ID and structural invariants ───────────────────── + + group('wire ID and structural invariants', () { + test('all wire IDs are >= 2 (0 and 1 reserved for constants)', () async { + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + for (final entry in mod.entries) { + final def = entry.value as Map; + // Check ports + for (final port in _ports(def).entries) { + final p = port.value as Map; + final bits = p['bits'] as List; + for (final bit in bits) { + if (bit is int) { + expect( + bit, + greaterThanOrEqualTo(2), + reason: 'Wire ID ${port.key} bit $bit should be >= 2', + ); + } + } + } + } + }); + + test(r'FilterBank contains $const cells for constant drivers', () async { + final json = await _synthToMap(_buildFilterBank()); + expect( + _hasCellType(json, r'$const'), + isTrue, + reason: r'FilterBank should have $const cells for constant values', + ); + }); + + test('passthrough buffers prevent input-output wire sharing', () async { + // A module whose output directly comes from an input should get a + // $buf for wire-ID isolation. + final json = await _synthToMap( + AddModule(Logic(width: 8), Logic(width: 8)), + ); + final mod = _modules(json); + // Verify input and output port bits don't overlap in any definition + for (final entry in mod.entries) { + final def = entry.value as Map; + final ports = _ports(def); + final inputBits = {}; + final outputBits = {}; + for (final port in ports.entries) { + final p = port.value as Map; + final bits = (p['bits'] as List).whereType().toSet(); + final dir = p['direction'] as String; + if (dir == 'input') { + inputBits.addAll(bits); + } else if (dir == 'output') { + outputBits.addAll(bits); + } + } + expect( + inputBits.intersection(outputBits), + isEmpty, + reason: '${entry.key}: input and output ports should not share wire ' + 'IDs (passthrough buffer should break sharing)', + ); + } + }); + }); + + // ── Group 9: DCE (dead-cell elimination) verification ────────────── + + group('dead-cell elimination', () { + test('DCE enabled produces fewer cells than DCE disabled', () async { + final fbDce = _buildFilterBank(); + final jsonDce = await _synthToMap(fbDce); + int countCells(Map j) { + var total = 0; + for (final def in _modules(j).values) { + total += _cells(def as Map).length; + } + return total; + } + + final fbNoDce = _buildFilterBank(); + final jsonNoDce = await _synthToMap( + fbNoDce, + options: const NetlistOptions(enableDCE: false), + ); + + final dceCells = countCells(jsonDce); + final noDceCells = countCells(jsonNoDce); + expect( + dceCells, + lessThanOrEqualTo(noDceCells), + reason: 'DCE should remove at least as many cells as no-DCE', + ); + }); + + test(r'DCE removes floating $const cells', () async { + // With DCE disabled, there may be more $const cells + final fbDce = _buildFilterBank(); + final jsonDce = await _synthToMap(fbDce); + int countConstCells(Map j) { + var total = 0; + for (final def in _modules(j).values) { + final d = def as Map; + for (final cell in _cells(d).values) { + final c = cell as Map; + if ((c['type'] as String) == r'$const') { + total++; + } + } + } + return total; + } + + final fbNoDce = _buildFilterBank(); + final jsonNoDce = await _synthToMap( + fbNoDce, + options: const NetlistOptions(enableDCE: false), + ); + + expect( + countConstCells(jsonDce), + lessThanOrEqualTo(countConstCells(jsonNoDce)), + reason: r'DCE should not produce more $const cells than no-DCE', + ); + }); + }); + + // ── Group 10: Post-processing option combinations ────────────────── + + group('post-processing options', () { + test('groupMaximalSubsets produces valid netlist', () async { + final fb = _buildFilterBank(); + final json = await _synthToMap( + fb, + options: const NetlistOptions( + groupStructConversions: true, + groupMaximalSubsets: true, + ), + ); + expect(_modules(json), isNotEmpty); + }); + + test('collapseConcats produces valid netlist', () async { + final fb = _buildFilterBank(); + final json = await _synthToMap( + fb, + options: const NetlistOptions( + groupStructConversions: true, + collapseConcats: true, + ), + ); + expect(_modules(json), isNotEmpty); + }); + + test( + 'all post-processing options enabled produces valid netlist', + () async { + final fb = _buildFilterBank(); + final json = await _synthToMap( + fb, + options: const NetlistOptions( + groupStructConversions: true, + collapseStructGroups: true, + groupMaximalSubsets: true, + collapseConcats: true, + ), + ); + expect(_modules(json), isNotEmpty); + }, + ); + + test('groupStructConversions adds synthetic module definitions', () async { + final fbPlain = _buildFilterBank(); + final jsonPlain = await _synthToMap(fbPlain); + final plainCount = _modules(jsonPlain).length; + + final fbGrouped = _buildFilterBank(); + final jsonGrouped = await _synthToMap( + fbGrouped, + options: const NetlistOptions(groupStructConversions: true), + ); + final groupedCount = _modules(jsonGrouped).length; + + // groupStructConversions should introduce additional synthetic modules + expect( + groupedCount, + greaterThanOrEqualTo(plainCount), + reason: 'Struct grouping should add module definitions', + ); + }); + + test( + 'collapseStructGroups reduces cells vs groupStructConversions only', + () async { + final fb1 = _buildFilterBank(); + final json1 = await _synthToMap( + fb1, + options: const NetlistOptions(groupStructConversions: true), + ); + int countCells(Map j) { + var total = 0; + for (final def in _modules(j).values) { + total += _cells(def as Map).length; + } + return total; + } + + final fb2 = _buildFilterBank(); + final json2 = await _synthToMap( + fb2, + options: const NetlistOptions( + groupStructConversions: true, + collapseStructGroups: true, + ), + ); + + expect( + countCells(json2), + lessThanOrEqualTo(countCells(json1)), + reason: 'Collapsing struct groups should not increase cell count', + ); + }, + ); + }); +} diff --git a/test/netlist_test.dart b/test/netlist_test.dart new file mode 100644 index 000000000..43eb94cc4 --- /dev/null +++ b/test/netlist_test.dart @@ -0,0 +1,706 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_test.dart +// Tests for the netlist synthesizer: JSON structure, SynthBuilder, +// NetlistSynthesisResult, collectModuleEntries, NetlistOptions, +// and example-based smoke tests. +// +// 2026 March 31 +// Author: Desmond Kirkpatrick + +import 'dart:convert'; +import 'dart:io'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/examples/filter_bank_modules.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart'; +import '../example/fir_filter.dart'; +import '../example/logic_array.dart'; +import '../example/oven_fsm.dart'; +import '../example/tree.dart'; + +// --------------------------------------------------------------------------- +// Simple test modules (self-contained, no example imports needed) +// --------------------------------------------------------------------------- + +/// A trivial module that inverts a single-bit input. +class _InverterModule extends Module { + Logic get out => output('out'); + + _InverterModule(Logic inp) : super(name: 'inverter') { + inp = addInput('inp', inp); + final out = addOutput('out'); + out <= ~inp; + } +} + +/// A module that instantiates two sub-modules: an inverter and an AND gate. +class _CompositeModule extends Module { + Logic get out => output('out'); + + _CompositeModule(Logic a, Logic b) : super(name: 'composite') { + a = addInput('a', a); + b = addInput('b', b); + final out = addOutput('out'); + + final invA = _InverterModule(a); + out <= (_InverterModule(invA.out).out & b); + } +} + +/// A simple adder module with a configurable width. +class _AdderModule extends Module { + Logic get sum => output('sum'); + + _AdderModule(Logic a, Logic b, {int width = 8}) : super(name: 'adder') { + a = addInput('a', a, width: width); + b = addInput('b', b, width: width); + final sum = addOutput('sum', width: width); + sum <= a + b; + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Detect whether running in JS (dart2js) environment. +const _isJS = identical(0, 0.0); + +/// Synthesize [top] and optionally write the produced JSON to [outPath]. +/// Returns the decoded modules map from the Yosys-format JSON. +Future> _synthesizeAndWrite( + Module top, + String outPath, +) async { + final synth = SynthBuilder(top, NetlistSynthesizer()); + final jsonStr = + await (synth.synthesizer as NetlistSynthesizer).synthesizeToJson(top); + if (!_isJS) { + final file = File(outPath); + await file.create(recursive: true); + await file.writeAsString(jsonStr); + } + final decoded = jsonDecode(jsonStr) as Map; + return decoded['modules'] as Map; +} + +/// Build a FilterBank with default test parameters. +FilterBank _buildFilterBank({ + int dataWidth = 16, + int numTaps = 3, + List> coefficients = const [ + [1, 2, 1], + [1, -2, 1], + ], +}) { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = + LogicArray([coefficients.length], dataWidth, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + + return FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: coefficients, + ); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + // ── Example smoke tests ─────────────────────────────────────────────── + // + // Each example is synthesized once, verifying that the netlist is + // non-empty and (on VM) that the JSON file is written successfully. + + group('Example netlist smoke tests', () { + test('Counter', () async { + final counter = Counter(Logic(name: 'en'), Logic(name: 'reset'), + SimpleClockGenerator(10).clk); + await counter.build(); + + final modules = + await _synthesizeAndWrite(counter, 'build/Counter.rohd.json'); + expect(modules, isNotEmpty); + + final topMod = modules[counter.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'Counter should have cells'); + }); + + test('FIR filter', () async { + final fir = FirFilter( + Logic(name: 'en'), + Logic(name: 'resetB'), + SimpleClockGenerator(10).clk, + Logic(name: 'inputVal', width: 8), + [0, 0, 0, 1], + bitWidth: 8, + ); + await fir.build(); + + final modules = + await _synthesizeAndWrite(fir, 'build/FirFilter.rohd.json'); + expect(modules, isNotEmpty); + if (!_isJS) { + expect(File('build/FirFilter.rohd.json').existsSync(), isTrue); + } + }); + + test('LogicArray', () async { + final la = LogicArrayExample( + LogicArray([4], 8, name: 'arrayA'), + Logic(name: 'id', width: 3), + Logic(name: 'selectIndexValue', width: 8), + Logic(name: 'selectFromValue', width: 8), + ); + await la.build(); + + final modules = + await _synthesizeAndWrite(la, 'build/LogicArrayExample.rohd.json'); + expect(modules, isNotEmpty); + }); + + test('OvenModule', () async { + final oven = OvenModule( + Logic(name: 'button', width: 2), + Logic(name: 'reset'), + SimpleClockGenerator(10).clk, + ); + await oven.build(); + + final modules = + await _synthesizeAndWrite(oven, 'build/OvenModule.rohd.json'); + expect(modules, isNotEmpty); + }); + + test('TreeOfTwoInputModules', () async { + final seq = List.generate(4, (_) => Logic(width: 8)); + final tree = TreeOfTwoInputModules(seq, (a, b) => mux(a > b, a, b)); + await tree.build(); + + // Only verify JSON generation succeeds; the deeply nested hierarchy + // causes a stack overflow in any recursive parser. + final json = await NetlistSynthesizer().synthesizeToJson(tree); + expect(json, isNotEmpty); + if (!_isJS) { + final file = File('build/TreeOfTwoInputModules.rohd.json'); + await file.create(recursive: true); + await file.writeAsString(json); + } + }); + + test('FilterBank', () async { + final fb = _buildFilterBank(); + await fb.build(); + + final modules = + await _synthesizeAndWrite(fb, 'build/FilterBank.smoke.rohd.json'); + expect(modules, isNotEmpty); + expect(modules.length, greaterThan(1), + reason: 'FilterBank should have sub-module definitions'); + }); + }); + + // ── JSON structure ──────────────────────────────────────────────────── + + group('JSON structure', () { + test('synthesizeToJson returns valid JSON with modules key', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final json = await NetlistSynthesizer().synthesizeToJson(mod); + expect(json, isNotEmpty); + final decoded = jsonDecode(json) as Map; + expect(decoded, contains('modules')); + }); + + test('top module is present with correct ports and top attribute', + () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final json = await NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + expect(modules, contains(mod.definitionName)); + + final topMod = modules[mod.definitionName] as Map; + + // Port directions + final ports = topMod['ports'] as Map; + expect(ports, contains('inp')); + expect(ports, contains('out')); + expect((ports['inp'] as Map)['direction'], equals('input')); + expect((ports['out'] as Map)['direction'], equals('output')); + + // Top attribute + final attrs = topMod['attributes'] as Map?; + expect(attrs, isNotNull); + expect(attrs!['top'], equals(1)); + }); + + test('port bit widths match module interface', () async { + const width = 16; + final mod = _AdderModule( + Logic(name: 'a', width: width), Logic(name: 'b', width: width), + width: width); + await mod.build(); + + final json = await NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + final topMod = modules[mod.definitionName] as Map; + final ports = topMod['ports'] as Map; + + expect((ports['a'] as Map)['bits'], hasLength(width)); + expect((ports['b'] as Map)['bits'], hasLength(width)); + expect((ports['sum'] as Map)['bits'], hasLength(width)); + }); + + test('cells have connections in default mode', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final json = await NetlistSynthesizer().synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + final topMod = modules[mod.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + + final hasConnections = cells.values.any((cell) { + final c = cell as Map; + final conns = c['connections'] as Map?; + return conns != null && conns.isNotEmpty; + }); + expect(hasConnections, isTrue); + }); + + test('generateCombinedJson and synthesizeToJson produce same module keys', + () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + + final fromCombined = await synthesizer.generateCombinedJson(synth, mod); + final fromConvenience = await NetlistSynthesizer().synthesizeToJson(mod); + + final combinedModules = + (jsonDecode(fromCombined) as Map)['modules'] as Map; + final convenienceModules = + (jsonDecode(fromConvenience) as Map)['modules'] as Map; + expect(combinedModules.keys.toSet(), + equals(convenienceModules.keys.toSet())); + }); + }); + + // ── SynthBuilder ────────────────────────────────────────────────────── + + group('SynthBuilder', () { + test('synthesisResults are NetlistSynthesisResult instances', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + expect(synth.synthesisResults, isNotEmpty); + for (final result in synth.synthesisResults) { + expect(result, isA()); + } + }); + + test('composite module includes sub-module definitions', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + final names = + synth.synthesisResults.map((r) => r.instanceTypeName).toSet(); + expect(names, contains(mod.definitionName)); + expect(synth.synthesisResults.length, greaterThan(1)); + }); + + test('toSynthFileContents produces valid JSON per definition', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final fileContents = + SynthBuilder(mod, NetlistSynthesizer()).getSynthFileContents(); + expect(fileContents, isNotEmpty); + for (final fc in fileContents) { + expect(fc.name, isNotEmpty); + expect(jsonDecode(fc.contents), isA>()); + } + }); + }); + + // ── NetlistSynthesisResult maps ─────────────────────────────────────── + + group('NetlistSynthesisResult maps', () { + test('ports map has direction and bits for each port', () async { + final mod = + _AdderModule(Logic(name: 'a', width: 8), Logic(name: 'b', width: 8)); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults + .whereType() + .firstWhere((r) => r.module == mod); + + for (final portName in ['a', 'b', 'sum']) { + expect(result.ports, contains(portName)); + final port = result.ports[portName]!; + expect(port, contains('direction')); + expect(port, contains('bits')); + } + }); + + test('netnames map is populated', () async { + final mod = _InverterModule(Logic(name: 'inp')); + await mod.build(); + + final result = SynthBuilder(mod, NetlistSynthesizer()) + .synthesisResults + .whereType() + .firstWhere((r) => r.module == mod); + expect(result.netnames, isNotEmpty); + }); + }); + + // ── collectModuleEntries ────────────────────────────────────────────── + + group('collectModuleEntries', () { + test('gathers results with correct structure and top attribute', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synth = SynthBuilder(mod, NetlistSynthesizer()); + final modulesMap = NetlistPasses.collectModuleEntries( + synth.synthesisResults, + topModule: mod); + + expect(modulesMap, contains(mod.definitionName)); + expect(modulesMap.length, greaterThan(1)); + + // Top attribute + final topAttrs = modulesMap[mod.definitionName]!['attributes']! + as Map; + expect(topAttrs['top'], equals(1)); + + // Every entry has the expected sections + for (final entry in modulesMap.values) { + expect(entry, contains('ports')); + expect(entry, contains('cells')); + expect(entry, contains('netnames')); + } + }); + }); + + // ── buildModulesMap ─────────────────────────────────────────────────── + + group('buildModulesMap', () { + test('returns map with all definitions and expected sections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + final modulesMap = await synthesizer.buildModulesMap(synth, mod); + + expect(modulesMap, contains(mod.definitionName)); + expect(modulesMap.length, greaterThan(1)); + for (final modEntry in modulesMap.entries) { + final data = modEntry.value; + expect(data, contains('ports'), reason: modEntry.key); + expect(data, contains('cells'), reason: modEntry.key); + expect(data, contains('netnames'), reason: modEntry.key); + } + }); + }); + + // ── NetlistOptions ─────────────────────────────────────────────────── + + group('NetlistOptions', () { + test('slimMode omits cell connections', () async { + final mod = _CompositeModule(Logic(name: 'a'), Logic(name: 'b')); + await mod.build(); + + final slimSynth = + NetlistSynthesizer(options: const NetlistOptions(slimMode: true)); + final json = await slimSynth.synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + final modules = decoded['modules'] as Map; + + for (final modEntry in modules.values) { + final data = modEntry as Map; + final cells = data['cells'] as Map? ?? {}; + for (final cell in cells.values) { + final c = cell as Map; + final conns = c['connections'] as Map?; + if (conns != null) { + expect(conns, isEmpty, reason: 'slim mode should omit connections'); + } + } + } + }); + }); + + // ── FilterBank (multi-channel, dedup, loopback) ─────────────────────── + + group('FilterBank netlist', () { + test('produces valid netlist with multiple module definitions', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final modules = + await _synthesizeAndWrite(mod, 'build/FilterBank.rohd.json'); + expect(modules, isNotEmpty); + expect(modules.length, greaterThan(1), + reason: 'FilterBank should have sub-module definitions'); + + // Top module should have cells + final topMod = modules[mod.definitionName] as Map; + final cells = topMod['cells'] as Map? ?? {}; + expect(cells, isNotEmpty, reason: 'FilterBank should have cells'); + }); + + test('FilterChannel definitions are deduplicated', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final json = await NetlistSynthesizer().synthesizeToJson(mod); + final parsed = jsonDecode(json) as Map; + final modules = parsed['modules'] as Map; + final channelDefs = + modules.keys.where((k) => k.contains('FilterChannel')).toList(); + // Two channels with different coefficients should produce + // separate definitions (not fully deduplicated). + expect(channelDefs, isNotEmpty, + reason: 'FilterChannel definitions should be present'); + }); + + test('all module entries have ports, cells, and netnames', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + final modulesMap = await synthesizer.buildModulesMap(synth, mod); + + for (final entry in modulesMap.entries) { + final data = entry.value; + expect(data, contains('ports'), reason: '${entry.key} missing ports'); + expect(data, contains('cells'), reason: '${entry.key} missing cells'); + expect(data, contains('netnames'), + reason: '${entry.key} missing netnames'); + } + }); + + test('ports have correct directions on sub-modules', () async { + final mod = _buildFilterBank(); + await mod.build(); + + final synthesizer = NetlistSynthesizer(); + final synth = SynthBuilder(mod, synthesizer); + + for (final result + in synth.synthesisResults.whereType()) { + for (final port in result.ports.entries) { + final dir = port.value['direction']! as String; + expect(['input', 'output', 'inout'], contains(dir), + reason: '${result.instanceTypeName}.${port.key} ' + 'has invalid direction'); + } + } + }); + }); + + // ----------------------------------------------------------------------- + // Bit-range compression & compact JSON + // ----------------------------------------------------------------------- + group('Bit-range compression', () { + test('compressBitRanges option produces range strings in JSON', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthCompressed = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressed = await synthCompressed.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = await synthNormal.synthesizeToJson(mod); + + // Compressed should be shorter. + expect(jsonCompressed.length, lessThan(jsonNormal.length)); + + // Both should parse as valid JSON with the same module keys. + final decodedCompressed = jsonDecode(jsonCompressed) as Map; + final decodedNormal = jsonDecode(jsonNormal) as Map; + expect( + (decodedCompressed['modules'] as Map).keys.toSet(), + equals((decodedNormal['modules'] as Map).keys.toSet()), + ); + + // Compressed JSON should contain range strings like "2:9". + expect(jsonCompressed, contains(RegExp(r'"\d+:\d+"'))); + // Normal JSON should NOT contain range strings. + expect(jsonNormal, isNot(contains(RegExp(r'"\d+:\d+"')))); + }); + + test('compressed ranges preserve constant bit strings', () async { + // Use a module that produces constant "0"/"1" bits in the netlist. + final a = Logic(name: 'a'); + final mod = _InverterModule(a); + await mod.build(); + + final synth = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final json = await synth.synthesizeToJson(mod); + final decoded = jsonDecode(json) as Map; + + // Should still be valid JSON. + expect(decoded['modules'], isNotNull); + }); + + test('compactJson option removes indentation', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthCompact = NetlistSynthesizer( + options: const NetlistOptions(compactJson: true), + ); + final jsonCompact = await synthCompact.synthesizeToJson(mod); + + final synthNormal = NetlistSynthesizer(); + final jsonNormal = await synthNormal.synthesizeToJson(mod); + + // Compact should be shorter. + expect(jsonCompact.length, lessThan(jsonNormal.length)); + // Compact should have no leading whitespace lines. + expect(jsonCompact, isNot(contains('\n '))); + // Both should be valid JSON with the same module keys. + final decodedCompact = jsonDecode(jsonCompact) as Map; + final decodedNormal = jsonDecode(jsonNormal) as Map; + expect( + (decodedCompact['modules'] as Map).keys.toSet(), + equals((decodedNormal['modules'] as Map).keys.toSet()), + ); + }); + + test('both options together produce smallest output', () async { + final a = Logic(name: 'a', width: 8); + final mod = _AdderModule(a, Logic(name: 'b', width: 8)); + await mod.build(); + + final synthBoth = NetlistSynthesizer( + options: const NetlistOptions( + compressBitRanges: true, + compactJson: true, + ), + ); + final jsonBoth = await synthBoth.synthesizeToJson(mod); + + final synthCompressOnly = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressOnly = await synthCompressOnly.synthesizeToJson(mod); + + final synthCompactOnly = NetlistSynthesizer( + options: const NetlistOptions(compactJson: true), + ); + final jsonCompactOnly = await synthCompactOnly.synthesizeToJson(mod); + + expect(jsonBoth.length, lessThan(jsonCompressOnly.length)); + expect(jsonBoth.length, lessThan(jsonCompactOnly.length)); + }); + + test( + 'compressed FilterBank round-trips: range strings expand to ' + 'same bit IDs as uncompressed', () async { + final mod = _buildFilterBank(); + await mod.build(); + + // Generate both compressed and uncompressed. + final synthNormal = NetlistSynthesizer(); + final jsonNormal = await synthNormal.synthesizeToJson(mod); + final normalModules = (jsonDecode(jsonNormal) + as Map)['modules'] as Map; + + final synthCompressed = NetlistSynthesizer( + options: const NetlistOptions(compressBitRanges: true), + ); + final jsonCompressed = await synthCompressed.synthesizeToJson(mod); + final compressedModules = (jsonDecode(jsonCompressed) + as Map)['modules'] as Map; + + // Compressed should be smaller. + expect(jsonCompressed.length, lessThan(jsonNormal.length)); + + // Same module keys. + expect(compressedModules.keys.toSet(), normalModules.keys.toSet()); + + // Verify compressed JSON contains range strings. + expect(jsonCompressed, contains(RegExp(r'"\d+:\d+"'))); + + // For each module, expand compressed port bits and compare to normal. + for (final modName in normalModules.keys) { + final normalPorts = (normalModules[modName] + as Map)['ports'] as Map?; + final compPorts = (compressedModules[modName] + as Map)['ports'] as Map?; + if (normalPorts == null || compPorts == null) { + continue; + } + + for (final portName in normalPorts.keys) { + final normalBits = + (normalPorts[portName] as Map)['bits'] as List; + final compBits = + (compPorts[portName] as Map)['bits'] as List; + + // Expand any range strings in the compressed bits. + final expanded = []; + for (final b in compBits) { + if (b is String && b.contains(':')) { + final parts = b.split(':'); + final start = int.parse(parts[0]); + final end = int.parse(parts[1]); + for (var i = start; i <= end; i++) { + expanded.add(i); + } + } else { + expanded.add(b); + } + } + + expect(expanded, normalBits, + reason: 'round-trip failed for $modName.$portName'); + } + } + }); + }); +} diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart index 31bf38bd9..8a2f2648e 100644 --- a/test/pipeline_test.dart +++ b/test/pipeline_test.dart @@ -264,6 +264,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('simple pipeline with intermediate gets', () async { @@ -280,6 +281,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline with pipelined sub-operation', () async { @@ -297,6 +299,7 @@ void main() { await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline with abs reference', () async { @@ -312,6 +315,7 @@ void main() { await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('getting out of range on pipeline is error', () { @@ -354,6 +358,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('multiuse pipeline', () async { @@ -369,6 +374,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('simple pipeline late add', () async { @@ -389,6 +395,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline initialized via get', () async { @@ -408,6 +415,7 @@ void main() { expect(pipem.b.value.isValid, isTrue); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('pipeline initialized directly instead of via get', () async { @@ -427,6 +435,7 @@ void main() { expect(pipem.b.value.isValid, isTrue); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple', () async { @@ -459,6 +468,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple async reset', () async { @@ -472,6 +482,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline simple reset vals', () async { @@ -504,6 +515,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline notready', () async { @@ -558,6 +570,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); test('rv pipeline multi', () async { @@ -602,6 +615,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(pipem, vectors); SimCompare.checkIverilogVector(pipem, vectors); + SimCompare.checkSystemCVector(pipem, vectors); }); }); } diff --git a/test/provider_consumer_test.dart b/test/provider_consumer_test.dart index 97c15f648..fefe21f58 100644 --- a/test/provider_consumer_test.dart +++ b/test/provider_consumer_test.dart @@ -212,5 +212,6 @@ output logic rd_valid_rsp await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_w_modify_test.dart index d34c8e374..d1b3815f7 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_w_modify_test.dart @@ -182,5 +182,6 @@ output logic rd_valid_rsp await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/test/sequential_test.dart b/test/sequential_test.dart index ade256cf3..bf913328b 100644 --- a/test/sequential_test.dart +++ b/test/sequential_test.dart @@ -182,6 +182,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); }); group('shorthand with sequential', () { @@ -203,6 +204,7 @@ void main() { // await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); } test('normal logic', () async { @@ -235,6 +237,7 @@ void main() { await SimCompare.checkFunctionalVector(dut, vectors); SimCompare.checkIverilogVector(dut, vectors); + SimCompare.checkSystemCVector(dut, vectors); }); test('negedge triggered flop', () async { @@ -252,6 +255,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('multiple triggers, both edges', () async { @@ -269,6 +273,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('negedge trigger actually occurs on negedge', () async { diff --git a/test/signal_registry_test.dart b/test/signal_registry_test.dart new file mode 100644 index 000000000..d51f1f0cd --- /dev/null +++ b/test/signal_registry_test.dart @@ -0,0 +1,183 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_registry_test.dart +// Tests for Module canonical naming (Namer). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; +// ──────────────────────────────────────────────────────────────── +// Simple test modules +// ──────────────────────────────────────────────────────────────── + +class _GateMod extends Module { + _GateMod(Logic a, Logic b) : super(name: 'gatetestmodule') { + a = addInput('a', a); + b = addInput('b', b); + final aBar = addOutput('a_bar'); + final aAndB = addOutput('a_and_b'); + aBar <= ~a; + aAndB <= a & b; + } +} + +class _Counter extends Module { + _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi([ + SimpleClockGenerator(10).clk, + reset, + ], [ + If(reset, then: [ + val < 0, + ], orElse: [ + If(en, then: [val < nextVal]), + ]), + ]); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('signalName basics', () { + test('returns port names after build', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.namer.signalNameOf(mod.input('b')), equals('b')); + expect(mod.namer.signalNameOf(mod.output('a_bar')), equals('a_bar')); + expect(mod.namer.signalNameOf(mod.output('a_and_b')), equals('a_and_b')); + }); + + test('returns internal signal names', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('en')), equals('en')); + expect(mod.namer.signalNameOf(mod.input('reset')), equals('reset')); + expect(mod.namer.signalNameOf(mod.output('val')), equals('val')); + }); + + test('agrees with signalName after synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + for (final entry in mod.inputs.entries) { + expect( + mod.namer.signalNameOf(entry.value), + isNotNull, + reason: 'signalName should work for input ${entry.key}', + ); + } + for (final entry in mod.outputs.entries) { + expect( + mod.namer.signalNameOf(entry.value), + isNotNull, + reason: 'signalName should work for output ${entry.key}', + ); + } + }); + }); + + group('allocateName', () { + test('avoids collision with existing names', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + final allocated = mod.namer.allocateRawName('en'); + expect(allocated, isNot(equals('en')), + reason: 'Should not collide with existing port name'); + expect(allocated, contains('en'), + reason: 'Should be based on the requested name'); + }); + + test('successive allocations are unique', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + final a = mod.namer.allocateRawName('wire'); + final b = mod.namer.allocateRawName('wire'); + expect(a, isNot(equals(b)), reason: 'Each allocation should be unique'); + }); + }); + + group('sparse storage', () { + test('identity names not stored in renames', () async { + final mod = _GateMod(Logic(), Logic()); + await mod.build(); + + expect(mod.namer.signalNameOf(mod.input('a')), equals('a')); + expect(mod.input('a').name, equals('a')); + }); + }); + + group('determinism', () { + test('same module produces identical canonical names', () async { + Future> buildAndGetNames() async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + return { + for (final sig in mod.signals) sig.name: mod.namer.signalNameOf(sig), + }; + } + + final names1 = await buildAndGetNames(); + await Simulator.reset(); + final names2 = await buildAndGetNames(); + + expect(names1, equals(names2)); + }); + }); + + group('filter_bank hierarchy', () { + test('submodule canonical names work independently', () async { + const dataWidth = 16; + const numTaps = 3; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = LogicArray([2], dataWidth, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + + expect(dut.namer.signalNameOf(dut.input('clk')), equals('clk')); + expect(dut.namer.signalNameOf(dut.output('done')), equals('done')); + + for (final sub in dut.subModules) { + for (final entry in sub.inputs.entries) { + final name = sub.namer.signalNameOf(entry.value); + expect(name, isNotEmpty); + } + } + }); + }); +} diff --git a/test/slim_connected_port_test.dart b/test/slim_connected_port_test.dart new file mode 100644 index 000000000..05c0b99d2 --- /dev/null +++ b/test/slim_connected_port_test.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class SimpleModule extends Module { + SimpleModule(Logic a) : super(name: 'SimpleTest') { + a = addInput('a', a, width: 8); + final b = addOutput('b', width: 8); + b <= ~a; + addOutput('unused_port', width: 4); // not connected internally + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + test('slim ports carry connected attribute', () async { + final a = Logic(width: 8, name: 'a'); + final mod = SimpleModule(a); + await mod.build(); + final netSvc = await NetlistService.create(mod); + + final slim = netSvc.slimJson; + + final parsed = json.decode(slim) as Map; + final netlist = parsed['netlist'] as Map; + final modules = netlist['modules'] as Map; + + // Find the SimpleTest module + // The module name may be the type name or uniquified; find it + final simpleTestKey = modules.keys.firstWhere( + (k) => k.contains('SimpleTest'), + orElse: () => modules.keys.first, + ); + final simpleTest = modules[simpleTestKey] as Map; + + final ports = simpleTest['ports'] as Map; + + // Port 'a' is connected internally (feeds ~a) + final portA = ports['a'] as Map; + expect( + portA['connected'], + isTrue, + reason: 'Port a should be marked connected', + ); + + // Port 'b' is connected internally (output of ~a) + final portB = ports['b'] as Map; + expect( + portB['connected'], + isTrue, + reason: 'Port b should be marked connected', + ); + + // Port 'unused_port' is NOT connected internally + final portUnused = ports['unused_port'] as Map; + expect( + portUnused.containsKey('connected'), + isFalse, + reason: 'unused_port should not have connected attribute', + ); + }); +} diff --git a/test/slim_full_canonical_test.dart b/test/slim_full_canonical_test.dart new file mode 100644 index 000000000..b3f230f34 --- /dev/null +++ b/test/slim_full_canonical_test.dart @@ -0,0 +1,175 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// slim_full_canonical_test.dart +// Validates that slim and full synthesis produce identical cell sets. + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + test('slim and full produce identical cell keys for FilterBank', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = LogicArray([2], 16, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: 3, + dataWidth: 16, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + final netSvc = await NetlistService.create(dut); + + // 1. Get slim JSON + final slimJsonStr = netSvc.slimJson; + + final slimUnified = jsonDecode(slimJsonStr) as Map; + final slimNetlist = slimUnified['netlist'] as Map; + final slimModules = slimNetlist['modules'] as Map; + + expect(slimModules, isNotEmpty, reason: 'No slim modules found'); + + // 2. For each slim module, fetch full and compare cell keys + var modulesTested = 0; + final mismatches = []; + + for (final moduleKey in slimModules.keys) { + final slimMod = slimModules[moduleKey] as Map; + final slimCells = slimMod['cells'] as Map? ?? {}; + + // Fetch full data + final fullJsonStr = netSvc.moduleJson(moduleKey); + final fullJson = jsonDecode(fullJsonStr) as Map; + if (fullJson.containsKey('status')) { + mismatches.add('$moduleKey: full fetch returned not_found'); + continue; + } + + final fullModules = fullJson['modules'] as Map?; + final fullMod = fullModules?[moduleKey] as Map?; + if (fullMod == null) { + // The full data might be under a different key (base definition). + // Try the first key. + final firstKey = fullModules?.keys.first; + final altMod = firstKey != null + ? fullModules![firstKey] as Map? + : null; + if (altMod == null) { + mismatches.add('$moduleKey: no module data in full response'); + continue; + } + // Use the alt module + _compareCells(moduleKey, slimCells, altMod, mismatches); + } else { + _compareCells(moduleKey, slimCells, fullMod, mismatches); + } + modulesTested++; + } + + // Report + if (mismatches.isNotEmpty) { + fail( + 'Cell key mismatches found in $modulesTested modules:\n' + '${mismatches.join('\n')}', + ); + } + + // Sanity: we tested a reasonable number of modules + expect(modulesTested, greaterThan(0), reason: 'No modules were tested'); + }); +} + +void _compareCells( + String moduleKey, + Map slimCells, + Map fullMod, + List mismatches, +) { + final fullCells = fullMod['cells'] as Map? ?? {}; + + final slimKeys = slimCells.keys.toList(); + final fullKeys = fullCells.keys.toList(); + + if (slimKeys.length != fullKeys.length) { + mismatches.add( + '$moduleKey: cell count differs — ' + 'slim=${slimKeys.length}, full=${fullKeys.length}', + ); + // Show which keys differ + final slimOnly = slimKeys.toSet().difference(fullKeys.toSet()); + final fullOnly = fullKeys.toSet().difference(slimKeys.toSet()); + if (slimOnly.isNotEmpty) { + mismatches.add(' slim-only: $slimOnly'); + } + if (fullOnly.isNotEmpty) { + mismatches.add(' full-only: $fullOnly'); + } + return; + } + + // Check ordering matches + for (var i = 0; i < slimKeys.length; i++) { + if (slimKeys[i] != fullKeys[i]) { + mismatches.add( + '$moduleKey: cell key ordering differs at index $i — ' + 'slim="${slimKeys[i]}", full="${fullKeys[i]}"', + ); + return; + } + } + + // Check cell types match + for (final key in slimKeys) { + final slimCell = slimCells[key] as Map; + final fullCell = fullCells[key] as Map; + final slimType = slimCell['type'] as String?; + final fullType = fullCell['type'] as String?; + if (slimType != fullType) { + mismatches.add( + '$moduleKey: cell "$key" type differs — ' + 'slim="$slimType", full="$fullType"', + ); + } + } + + // Verify slim cells DON'T have connections + for (final key in slimKeys) { + final slimCell = slimCells[key] as Map; + if (slimCell.containsKey('connections')) { + mismatches.add( + '$moduleKey: slim cell "$key" has connections ' + '(should be stripped)', + ); + } + } + + // Verify full cells DO have connections + for (final key in fullKeys) { + final fullCell = fullCells[key] as Map; + if (!fullCell.containsKey('connections')) { + mismatches.add('$moduleKey: full cell "$key" missing connections'); + } + } +} diff --git a/test/slim_incremental_equivalence_test.dart b/test/slim_incremental_equivalence_test.dart new file mode 100644 index 000000000..697a95377 --- /dev/null +++ b/test/slim_incremental_equivalence_test.dart @@ -0,0 +1,303 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// slim_incremental_equivalence_test.dart +// Validates that assembling full data from slim + per-module fetches +// produces the same result as pulling the full netlist in one shot. + +import 'dart:convert'; + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/example.dart' as ex; +import '../example/filter_bank.dart'; +import '../example/fir_filter.dart'; +import '../example/oven_fsm.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + /// Builds [module] with netlist synthesis, then verifies that + /// reassembling the full netlist from slim + per-module fetches + /// produces a result equivalent to [toFullJson]. + /// + /// For each module definition in the slim netlist: + /// 1. Cell keys, types, and ordering must match the full netlist. + /// 2. Port definitions (direction, bit indices) must match. + /// 3. Fetching full data via [moduleJson] adds connections + /// that exactly match those in the full netlist. + Future validateSlimIncrementalEquivalence(Module module) async { + await module.build(); + final netSvc = await NetlistService.create(module); + + // ── Pull full netlist in one shot ───────────────────────────── + final fullJsonStr = netSvc.toJson(); + final fullNetlist = jsonDecode(fullJsonStr) as Map; + final fullModules = fullNetlist['modules'] as Map; + + // ── Pull slim netlist ───────────────────────────────────────── + final slimJsonStr = netSvc.slimJson; + final slimUnified = jsonDecode(slimJsonStr) as Map; + final slimNetlist = slimUnified['netlist'] as Map; + final slimModules = slimNetlist['modules'] as Map; + + // ── Same set of module definition keys ──────────────────────── + expect( + slimModules.keys.toSet(), + equals(fullModules.keys.toSet()), + reason: 'Slim and full should have identical module keys', + ); + + // ── Per-module comparison ───────────────────────────────────── + final errors = []; + + for (final moduleKey in fullModules.keys) { + final fullMod = fullModules[moduleKey] as Map; + final slimMod = slimModules[moduleKey] as Map; + + final fullCells = fullMod['cells'] as Map? ?? {}; + final slimCells = slimMod['cells'] as Map? ?? {}; + + // ── Cell keys and ordering ────────────────────────────────── + final fullCellKeys = fullCells.keys.toList(); + final slimCellKeys = slimCells.keys.toList(); + if (!_listsEqual(fullCellKeys, slimCellKeys)) { + errors.add( + '$moduleKey: cell keys differ — ' + 'full=$fullCellKeys, slim=$slimCellKeys', + ); + continue; // Skip deeper checks for this module + } + + // ── Cell types match ──────────────────────────────────────── + for (final cellKey in fullCellKeys) { + final fullCell = fullCells[cellKey] as Map; + final slimCell = slimCells[cellKey] as Map; + if (fullCell['type'] != slimCell['type']) { + errors.add( + '$moduleKey.$cellKey: type mismatch — ' + 'full="${fullCell['type']}", slim="${slimCell['type']}"', + ); + } + } + + // ── Port definitions match ────────────────────────────────── + final fullPorts = fullMod['ports'] as Map? ?? {}; + final slimPorts = slimMod['ports'] as Map? ?? {}; + if (!_listsEqual(fullPorts.keys.toList(), slimPorts.keys.toList())) { + errors.add( + '$moduleKey: port keys differ — ' + 'full=${fullPorts.keys.toList()}, ' + 'slim=${slimPorts.keys.toList()}', + ); + } else { + for (final portKey in fullPorts.keys) { + final fullPort = fullPorts[portKey] as Map; + final slimPort = slimPorts[portKey] as Map; + if (fullPort['direction'] != slimPort['direction']) { + errors.add('$moduleKey port $portKey: direction mismatch'); + } + final fullBits = fullPort['bits'] as List?; + final slimBits = slimPort['bits'] as List?; + if (!_listsEqual(fullBits ?? [], slimBits ?? [])) { + errors.add( + '$moduleKey port $portKey: bits mismatch — ' + 'full=$fullBits, slim=$slimBits', + ); + } + } + } + + // ── Slim cells must NOT have connections ──────────────────── + for (final cellKey in slimCellKeys) { + final slimCell = slimCells[cellKey] as Map; + if (slimCell.containsKey('connections')) { + errors.add( + '$moduleKey.$cellKey: slim cell has connections ' + '(should be stripped)', + ); + } + } + + // ── Full cells must have connections ──────────────────────── + for (final cellKey in fullCellKeys) { + final fullCell = fullCells[cellKey] as Map; + if (!fullCell.containsKey('connections')) { + errors.add('$moduleKey.$cellKey: full cell missing connections'); + } + } + + // ── Fetch full data via moduleJson ───────────────────────── + // This is the incremental-loading contract: for EVERY module + // in the slim netlist, fetching full data must recover the + // exact connections present in the one-shot full netlist. + final fetchedStr = netSvc.moduleJson(moduleKey); + final fetchedJson = jsonDecode(fetchedStr) as Map; + if (fetchedJson.containsKey('status')) { + errors.add('$moduleKey: moduleJson returned not_found'); + continue; + } + + // The fetched result is {"creator":..., "modules": {key: data}}. + final fetchedModules = + fetchedJson['modules'] as Map? ?? fetchedJson; + final fetchedMod = (fetchedModules[moduleKey] ?? + fetchedModules.values.first) as Map; + + final fetchedCells = fetchedMod['cells'] as Map? ?? {}; + + // ── Fetched cell keys must match full ─────────────────────── + if (!_listsEqual(fetchedCells.keys.toList(), fullCellKeys)) { + errors.add( + '$moduleKey: fetched cell keys differ from full — ' + 'fetched=${fetchedCells.keys.toList()}, ' + 'full=$fullCellKeys', + ); + continue; + } + + // ── Fetched connections must match full exactly ───────────── + for (final cellKey in fullCellKeys) { + final fullCell = fullCells[cellKey] as Map; + final fetchedCell = fetchedCells[cellKey] as Map; + + final fullConns = + fullCell['connections'] as Map? ?? {}; + final fetchedConns = + fetchedCell['connections'] as Map? ?? {}; + + if (!_connectionsEqual(fullConns, fetchedConns)) { + errors.add( + '$moduleKey.$cellKey: connections mismatch — ' + 'full=$fullConns, fetched=$fetchedConns', + ); + } + } + + // ── Fetched ports must match full ─────────────────────────── + final fetchedPorts = fetchedMod['ports'] as Map? ?? {}; + for (final portKey in fullPorts.keys) { + final fullPort = fullPorts[portKey] as Map; + final fetchedPort = fetchedPorts[portKey] as Map?; + if (fetchedPort == null) { + errors.add('$moduleKey port $portKey: missing in fetched data'); + continue; + } + if (fullPort['direction'] != fetchedPort['direction']) { + errors.add( + '$moduleKey port $portKey: direction mismatch ' + 'in fetched data', + ); + } + final fullBits = fullPort['bits'] as List?; + final fetchedBits = fetchedPort['bits'] as List?; + if (!_listsEqual(fullBits ?? [], fetchedBits ?? [])) { + errors.add( + '$moduleKey port $portKey: bits mismatch — ' + 'full=$fullBits, fetched=$fetchedBits', + ); + } + } + } + + // ── Report ──────────────────────────────────────────────────── + if (errors.isNotEmpty) { + fail('Slim incremental equivalence errors:\n${errors.join('\n')}'); + } + } + + test('Counter: slim + incremental fetch == full', () async { + final en = Logic(name: 'en'); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final counter = ex.Counter(en, reset, clk); + await validateSlimIncrementalEquivalence(counter); + }); + + test('FIR filter: slim + incremental fetch == full', () async { + final en = Logic(name: 'en'); + final resetB = Logic(name: 'resetB'); + final clk = SimpleClockGenerator(10).clk; + final inputVal = Logic(name: 'inputVal', width: 8); + final fir = FirFilter(en, resetB, clk, inputVal, [0, 0, 0, 1], bitWidth: 8); + await validateSlimIncrementalEquivalence(fir); + }); + + test('OvenModule: slim + incremental fetch == full', () async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + final oven = OvenModule(button, reset, clk); + await validateSlimIncrementalEquivalence(oven); + }); + + test('FilterBank: slim + incremental fetch == full', () async { + const dataWidth = 16; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = LogicArray([2], dataWidth, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: 3, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await validateSlimIncrementalEquivalence(dut); + }); +} + +/// Deep-compare two lists element by element. +bool _listsEqual(List a, List b) { + if (a.length != b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +/// Compare two connection maps: {portName: [bit indices]}. +/// +/// All bit indices are numeric IDs; order within each port's list matters +/// because it encodes the wire mapping. +bool _connectionsEqual(Map a, Map b) { + if (a.length != b.length) { + return false; + } + for (final key in a.keys) { + if (!b.containsKey(key)) { + return false; + } + final aBits = a[key] as List?; + final bBits = b[key] as List?; + if (aBits == null && bBits == null) { + continue; + } + if (aBits == null || bBits == null) { + return false; + } + if (!_listsEqual(aBits, bBits)) { + return false; + } + } + return true; +} diff --git a/test/ssa_test.dart b/test/ssa_test.dart index d16f5fb2d..71e35ec2c 100644 --- a/test/ssa_test.dart +++ b/test/ssa_test.dart @@ -495,6 +495,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa multi use model bad reuse', () { @@ -528,6 +529,10 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // Skip SystemC for modules with unsupported patterns. + if (mod is! SsaNested && mod is! SsaModWithStructElements) { + SimCompare.checkSystemCVector(mod, vectors); + } }); } }); @@ -560,6 +565,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa seq of cases', () async { @@ -590,6 +596,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('ssa uninitialized', () async { diff --git a/test/struct_port_pruning_test.dart b/test/struct_port_pruning_test.dart new file mode 100644 index 000000000..b13346ebe --- /dev/null +++ b/test/struct_port_pruning_test.dart @@ -0,0 +1,143 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// struct_port_pruning_test.dart +// Verifies that struct port elements on submodules are not incorrectly +// pruned during SV synthesis. Exercises the `submoduleOutputSynths` / +// `submoduleInputSynths` fix in `_pruneUnused`. +// +// 2026 April 17 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +// ── Struct definition ────────────────────────────────────────── + +class PairStruct extends LogicStructure { + PairStruct({Logic? a, Logic? b, super.name = 'pair'}) + : super([a ?? Logic(name: 'a'), b ?? Logic(name: 'b')]); + + @override + PairStruct clone({String? name}) => PairStruct(name: name); +} + +// ── Leaf submodule with a struct output port ─────────────────── + +class StructProducer extends Module { + Logic get out => PairStruct()..gets(output('out')); + + StructProducer(Logic x, Logic y) : super(name: 'struct_producer') { + x = addInput('x', x); + y = addInput('y', y); + + final s = PairStruct(a: x, b: y); + addOutput('out', width: s.width) <= s; + } +} + +// ── Leaf submodule with a struct input port ──────────────────── + +class StructConsumer extends Module { + Logic get sum => output('sum'); + + StructConsumer(Logic pair) : super(name: 'struct_consumer') { + pair = addInput('pair', pair, width: pair.width); + + final s = PairStruct()..gets(pair); + addOutput('sum') <= s.elements[0] ^ s.elements[1]; + } +} + +// ── Top module: struct output from submodule → struct input ─── + +class StructPipeTop extends Module { + Logic get result => output('result'); + + StructPipeTop(Logic x, Logic y) : super(name: 'struct_pipe_top') { + x = addInput('x', x); + y = addInput('y', y); + + final producer = StructProducer(x, y); + final consumer = StructConsumer(producer.out); + + addOutput('result') <= consumer.sum; + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('struct port pruning', () { + test('SV output retains struct element signals from submodule', () async { + final dut = StructPipeTop(Logic(), Logic()); + await dut.build(); + + final svStr = dut.generateSynth(); + + // The struct_producer submodule should appear in the SV. + expect( + svStr, + contains('struct_producer'), + reason: 'Submodule with struct output should not be pruned', + ); + + // The struct_consumer submodule should appear in the SV. + expect( + svStr, + contains('struct_consumer'), + reason: 'Submodule with struct input should not be pruned', + ); + + // The output port 'out' of struct_producer (width 2) must have a + // connection in the parent — it should not be pruned away. + expect( + svStr, + contains('.out('), + reason: 'Struct output port connection should not be pruned', + ); + + // The input port 'pair' of struct_consumer must be connected. + expect( + svStr, + contains('.pair('), + reason: 'Struct input port connection should not be pruned', + ); + }); + + test('struct element signals survive SV synthesis for producer', () async { + final dut = StructProducer(Logic(), Logic()); + await dut.build(); + + final svStr = dut.generateSynth(); + + // Inside StructProducer, the struct elements (a, b from PairStruct) + // drive the output via struct_slice decomposition. They must not + // be pruned. + expect(svStr, contains('out'), reason: 'Output port should appear in SV'); + expect( + svStr, + contains('input'), + reason: 'Input ports should appear in SV', + ); + }); + + test('struct element signals survive SV synthesis for consumer', () async { + final dut = StructConsumer(Logic(width: 2)); + await dut.build(); + + final svStr = dut.generateSynth(); + + // Inside StructConsumer, the struct elements are extracted from the + // packed input. The XOR of elements drives the output. + expect(svStr, contains('sum'), reason: 'Output port should appear in SV'); + expect( + svStr, + contains('pair'), + reason: 'Input struct port should appear in SV', + ); + }); + }); +} diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 6ad38737a..c64c3192d 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -689,6 +689,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('tieoff ', () { @@ -713,6 +714,8 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + // z-valued outputs are skipped in SystemC checks + SimCompare.checkSystemCVector(mod, vectors); }); test('full port', () async { @@ -734,6 +737,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); }); } @@ -909,6 +913,7 @@ endmodule : ModWithUselessWireMods''')); ]; await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); group('connected ports and pruning', () { diff --git a/test/synth_name_parity_test.dart b/test/synth_name_parity_test.dart new file mode 100644 index 000000000..64b7dcc45 --- /dev/null +++ b/test/synth_name_parity_test.dart @@ -0,0 +1,125 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// synth_name_parity_test.dart +// Tests that verify canonicalNameOf works consistently across +// different synthesis paths (SV and netlist). +// +// 2026 April 14 +// Author: Desmond Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +import '../example/filter_bank.dart'; + +class _Counter extends Module { + _Counter(Logic en, Logic reset, {int width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi( + [SimpleClockGenerator(10).clk, reset], + [ + If( + reset, + then: [val < 0], + orElse: [ + If(en, then: [val < nextVal]), + ], + ), + ], + ); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + ModuleServices.instance.reset(); + }); + + group('canonicalNameOf after netlist synthesis', () { + test('counter — returns names after netlist synthesis', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + await NetlistService.create(mod); + + expect(mod.namer.signalNameOf(mod.input('en')), equals('en')); + expect(mod.namer.signalNameOf(mod.input('reset')), equals('reset')); + expect(mod.namer.signalNameOf(mod.output('val')), equals('val')); + }); + + test('filter_bank — returns names for sub-module signals', () async { + const dataWidth = 16; + const numTaps = 3; + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final start = Logic(name: 'start'); + final samplesIn = LogicArray([2], dataWidth, name: 'samplesIn'); + final validIn = Logic(name: 'validIn'); + final inputDone = Logic(name: 'inputDone'); + + final dut = FilterBank( + clk, + reset, + start, + samplesIn, + validIn, + inputDone, + numTaps: numTaps, + dataWidth: dataWidth, + coefficients: [ + [1, 2, 1], + [1, -2, 1], + ], + ); + await dut.build(); + await NetlistService.create(dut); + + expect(dut.namer.signalNameOf(dut.input('clk')), equals('clk')); + expect(dut.namer.signalNameOf(dut.input('reset')), equals('reset')); + expect(dut.namer.signalNameOf(dut.output('done')), equals('done')); + }); + }); + + group('canonicalNameOf after SV synthesis', () { + test('counter — returns canonical name after SV synth', () async { + final mod = _Counter(Logic(), Logic()); + await mod.build(); + + mod.generateSynth(); + + expect(mod.namer.signalNameOf(mod.input('en')), equals('en')); + expect(mod.namer.signalNameOf(mod.input('reset')), equals('reset')); + }); + }); + + group('cross-synthesizer parity', () { + test( + 'counter — SV and netlist produce identical canonicalNameOf', + () async { + final modNetlist = _Counter(Logic(), Logic()); + await modNetlist.build(); + await NetlistService.create(modNetlist); + await Simulator.reset(); + + final modSv = _Counter(Logic(), Logic()); + await modSv.build(); + modSv.generateSynth(); + + // Both paths use the same Namer, so names must match. + final enNetlist = modNetlist.namer.signalNameOf(modNetlist.input('en')); + final enSv = modSv.namer.signalNameOf(modSv.input('en')); + + expect( + enSv, + equals(enNetlist), + reason: 'SV and netlist should produce identical canonical names', + ); + }, + ); + }); +} diff --git a/test/systemc_simcompare_test.dart b/test/systemc_simcompare_test.dart new file mode 100644 index 000000000..b396c6709 --- /dev/null +++ b/test/systemc_simcompare_test.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2021-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_simcompare_test.dart +// Tests for SystemC synthesis and simulation comparison. +// +// 2026 May +// Author: Desmond A. Kirkpatrick + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +/// A simple module with basic gates for testing SystemC synthesis. +class GateModule extends Module { + GateModule(Logic a, Logic b) : super(name: 'GateModule') { + a = addInput('a', a); + b = addInput('b', b); + final aAndB = addOutput('a_and_b'); + final aOrB = addOutput('a_or_b'); + final notA = addOutput('not_a'); + + aAndB <= a & b; + aOrB <= a | b; + notA <= ~a; + } +} + +/// A simple counter for testing sequential SystemC synthesis. +class SimpleCounter extends Module { + SimpleCounter(Logic clk, Logic reset, Logic en) : super(name: 'Counter') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + final val = addOutput('val', width: 8); + + final nextVal = Logic(name: 'nextVal', width: 8); + + Sequential(clk, reset: reset, [ + If(en, then: [nextVal < nextVal + 1], orElse: [nextVal < nextVal]), + ]); + + val <= nextVal; + } +} + +/// A flip-flop module for testing. +class FlopModule extends Module { + FlopModule(Logic clk, Logic reset, Logic d) : super(name: 'FlopModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset); + } +} + +/// A flip-flop with enable. +class FlopEnModule extends Module { + FlopEnModule(Logic clk, Logic reset, Logic en, Logic d) + : super(name: 'FlopEnModule') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + en = addInput('en', en); + d = addInput('d', d, width: 8); + final q = addOutput('q', width: 8); + q <= flop(clk, d, reset: reset, en: en); + } +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('SimCompare SystemC', () { + test('gate module passes vectors', () async { + final a = Logic(name: 'a'); + final b = Logic(name: 'b'); + final mod = GateModule(a, b); + await mod.build(); + + final vectors = [ + Vector({'a': 0, 'b': 0}, {'a_and_b': 0, 'a_or_b': 0, 'not_a': 1}), + Vector({'a': 1, 'b': 0}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 0}), + Vector({'a': 0, 'b': 1}, {'a_and_b': 0, 'a_or_b': 1, 'not_a': 1}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1, 'a_or_b': 1, 'not_a': 0}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('counter module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Same vectors as counter_test.dart (iverilog-compatible timing) + final vectors = [ + Vector({'en': 0, 'reset': 0}, {}), + Vector({'en': 0, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 1}), + Vector({'en': 1, 'reset': 0}, {'val': 2}), + Vector({'en': 1, 'reset': 0}, {'val': 3}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 1, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 5}), + ]; + + SimCompare.checkSystemCVector(mod, vectors, dontDeleteTmpFiles: true); + }); + + test('flip-flop module passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final d = Logic(name: 'd', width: 8); + final mod = FlopModule(clk, reset, d); + await mod.build(); + + // Flop: output follows input with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0xAA, 'reset': 0}, {'q': 0}), + Vector({'d': 0xBB, 'reset': 0}, {'q': 0xAA}), + Vector({'d': 0xCC, 'reset': 0}, {'q': 0xBB}), + Vector({'d': 0xDD, 'reset': 0}, {'q': 0xCC}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('flip-flop with enable passes vectors', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final d = Logic(name: 'd', width: 8); + final mod = FlopEnModule(clk, reset, en, d); + await mod.build(); + + // When en=0, q holds; when en=1, q follows d with 1-cycle latency + final vectors = [ + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0, 'en': 0, 'reset': 1}, {'q': 0}), + Vector({'d': 0x42, 'en': 1, 'reset': 0}, {'q': 0}), + Vector({'d': 0x55, 'en': 1, 'reset': 0}, {'q': 0x42}), + Vector({'d': 0xFF, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x00, 'en': 0, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0x99, 'en': 1, 'reset': 0}, {'q': 0x55}), + Vector({'d': 0xAA, 'en': 1, 'reset': 0}, {'q': 0x99}), + ]; + + SimCompare.checkSystemCVector(mod, vectors); + }); + + test('counter trace-based comparison', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final en = Logic(name: 'en'); + final mod = SimpleCounter(clk, reset, en); + await mod.build(); + + // Use the trace-based approach: just write normal simulation code, + // no vectors needed. The method records all I/O at every clock edge + // and replays through SystemC. + final result = await SimCompare.systemcSimCompare( + mod, + clk, + stimulus: () async { + reset.inject(1); + en.inject(0); + Simulator.registerAction(25, () { + reset.put(0); + en.put(1); + }); + Simulator.registerAction(65, () { + en.put(0); + }); + Simulator.registerAction(85, () { + en.put(1); + }); + Simulator.setMaxSimTime(120); + }, + dontDeleteTmpFiles: true, + ); + expect(result, isTrue); + }); + }); +} diff --git a/test/systemc_vector_test.dart b/test/systemc_vector_test.dart new file mode 100644 index 000000000..8d516663b --- /dev/null +++ b/test/systemc_vector_test.dart @@ -0,0 +1,1279 @@ +// Copyright (C) 2024-2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// systemc_vector_test.dart +// Parallel SystemC simulation tests for all modules tested with iverilog. +// +// 2026 May 7 +// Author: Desmond A. Kirkpatrick + +import 'dart:math'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +// ===== Modules from flop_test.dart ===== + +class FlopTestModule extends Module { + FlopTestModule(Logic a, {Logic? en, Logic? reset, dynamic resetValue}) + : super(name: 'floptestmodule') { + a = addInput('a', a, width: a.width); + if (en != null) { + en = addInput('en', en); + } + if (reset != null) { + reset = addInput('reset', reset); + } + if (resetValue != null && resetValue is Logic) { + resetValue = addInput('resetValue', resetValue, width: a.width); + } + final y = addOutput('y', width: a.width); + final clk = SimpleClockGenerator(10).clk; + y <= flop(clk, a, en: en, reset: reset, resetValue: resetValue); + } +} + +// ===== Modules from counter_test.dart ===== + +class Counter extends Module { + final int width; + Logic get val => output('val'); + Counter(Logic en, Logic reset, {this.width = 8}) : super(name: 'counter') { + en = addInput('en', en); + reset = addInput('reset', reset); + final val = addOutput('val', width: width); + final nextVal = Logic(name: 'nextVal', width: width); + nextVal <= val + 1; + Sequential.multi([ + SimpleClockGenerator(10).clk, + reset + ], [ + If(reset, then: [ + val < 0 + ], orElse: [ + If(en, then: [val < nextVal]) + ]) + ]); + } +} + +// ===== Modules from comparison_test.dart ===== + +class ComparisonTestModule extends Module { + final int c; + ComparisonTestModule(Logic a, Logic b, {this.c = 5}) + : super(name: 'gatetestmodule') { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + + final aEqB = addOutput('a_eq_b'); + final aNeqB = addOutput('a_neq_b'); + final aLtB = addOutput('a_lt_b'); + final aLteB = addOutput('a_lte_b'); + final aGtB = addOutput('a_gt_b'); + final aGteB = addOutput('a_gte_b'); + final aGtOperatorB = addOutput('a_gt_operator_b'); + final aGteOperatorB = addOutput('a_gte_operator_b'); + + final aEqC = addOutput('a_eq_c'); + final aNeqC = addOutput('a_neq_c'); + final aLtC = addOutput('a_lt_c'); + final aLteC = addOutput('a_lte_c'); + final aGtC = addOutput('a_gt_c'); + final aGteC = addOutput('a_gte_c'); + final aGtOperatorC = addOutput('a_gt_operator_c'); + final aGteOperatorC = addOutput('a_gte_operator_c'); + + aEqB <= a.eq(b); + aNeqB <= a.neq(b); + aLtB <= a.lt(b); + aLteB <= a.lte(b); + aGtB <= a.gt(b); + aGteB <= a.gte(b); + aGtOperatorB <= (a > b); + aGteOperatorB <= (a >= b); + + aEqC <= a.eq(c); + aNeqC <= a.neq(c); + aLtC <= a.lt(c); + aLteC <= a.lte(c); + aGtC <= a.gt(c); + aGteC <= a.gte(c); + aGtOperatorC <= (a > c); + aGteOperatorC <= (a >= c); + } +} + +// ===== Modules from arithmetic_shift_right_test.dart ===== + +class SraUnsignedTestModule extends Module { + Logic get result => output('result'); + SraUnsignedTestModule(Logic toShift, Logic shiftAmount, Logic maskBit) { + toShift = addInput('toShift', toShift, width: toShift.width); + shiftAmount = + addInput('shiftAmount', shiftAmount, width: shiftAmount.width); + maskBit = addInput('maskBit', maskBit); + addOutput('result', width: toShift.width); + result <= (toShift >> shiftAmount) & maskBit.replicate(toShift.width); + } +} + +// ===== Modules from collapse_test.dart ===== + +class CollapseTestModule extends Module { + CollapseTestModule(Logic a, Logic b) : super(name: 'collapsetestmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + final e = addOutput('e'); + final f = addOutput('f'); + + final x = Logic(name: 'x'); + final y = Logic(name: 'y'); + final z = Logic(name: 'z', naming: Naming.mergeable); + c <= a & b; + d <= a & b; + x <= a; + y <= x; + e <= a & b & c & x & y; + z <= b & y; + f <= a & z; + + Logic(name: 'internal') <= ~z; + } +} + +// ===== Modules from extend_test.dart ===== + +class ExtendModule extends Module { + ExtendModule(Logic a, int newWidth, ExtendType extendType) { + a = addInput('a', a, width: a.width); + final b = addOutput('b', width: newWidth); + if (extendType == ExtendType.zero) { + b <= a.zeroExtend(newWidth); + } else { + b <= a.signExtend(newWidth); + } + } +} + +enum ExtendType { zero, sign } + +class WithSetModule extends Module { + WithSetModule(Logic a, int startIndex, Logic b) { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + final c = addOutput('c', width: a.width); + c <= a.withSet(startIndex, b); + } +} + +// ===== Modules from bus_test.dart ===== + +class BusTestModule extends Module { + BusTestModule(Logic a, Logic b) : super(name: 'bustestmodule') { + if (a.width != b.width) { + throw Exception('a and b must be same width.'); + } + if (a.width <= 3) { + throw Exception('a must be more than width 3.'); + } + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + + final aBar = addOutput('a_bar', width: a.width); + final aAndB = addOutput('a_and_b', width: a.width); + final aBJoined = addOutput('a_b_joined', width: a.width + b.width); + final aPlusB = addOutput('a_plus_b', width: a.width); + final a1 = addOutput('a1'); + final expressionBitSelect = addOutput('expression_bit_select', width: 4); + + final aReversed = addOutput('a_reversed', width: a.width); + final aShrunk1 = addOutput('a_shrunk1', width: 3); + final aShrunk2 = addOutput('a_shrunk2', width: 2); + final aShrunk3 = addOutput('a_shrunk3'); + final aNegativeShrunk1 = addOutput('a_neg_shrunk1', width: 3); + final aNegativeShrunk2 = addOutput('a_neg_shrunk2', width: 2); + final aNegativeShrunk3 = addOutput('a_neg_shrunk3'); + final aRSliced1 = addOutput('a_rsliced1', width: 5); + final aRSliced2 = addOutput('a_rsliced2', width: 2); + final aRSliced3 = addOutput('a_rsliced3'); + final aRNegativeSliced1 = addOutput('a_r_neg_sliced1', width: 5); + final aRNegativeSliced2 = addOutput('a_r_neg_sliced2', width: 2); + final aRNegativeSliced3 = addOutput('a_r_neg_sliced3'); + final aRange1 = addOutput('a_range1', width: 3); + final aRange2 = addOutput('a_range2', width: 2); + final aRange3 = addOutput('a_range3'); + final aRange4 = addOutput('a_range4', width: 3); + final aNegativeRange1 = addOutput('a_neg_range1', width: 3); + final aNegativeRange2 = addOutput('a_neg_range2', width: 2); + final aNegativeRange3 = addOutput('a_neg_range3'); + final aNegativeRange4 = addOutput('a_neg_range4', width: 3); + final aOperatorIndexing1 = addOutput('a_operator_indexing1'); + final aOperatorIndexing2 = addOutput('a_operator_indexing2'); + final aOperatorIndexing3 = addOutput('a_operator_indexing3'); + final aOperatorNegIndexing1 = addOutput('a_operator_neg_indexing1'); + final aOperatorNegIndexing2 = addOutput('a_operator_neg_indexing2'); + final aOperatorNegIndexing3 = addOutput('a_operator_neg_indexing3'); + + aBar <= ~a; + aAndB <= a & b; + aBJoined <= [b, a].swizzle(); + a1 <= a[1]; + aPlusB <= a + b; + + aShrunk1 <= a.slice(2, 0); + aShrunk2 <= a.slice(1, 0); + aShrunk3 <= a.slice(0, 0); + aNegativeShrunk1 <= a.slice(-6, 0); + aNegativeShrunk2 <= a.slice(-7, 0); + aNegativeShrunk3 <= a.slice(-8, 0); + + aRSliced1 <= a.slice(3, 7); + aRSliced2 <= a.slice(6, 7); + aRSliced3 <= a.slice(7, 7); + aRNegativeSliced1 <= a.slice(-5, -1); + aRNegativeSliced2 <= a.slice(-2, -1); + aRNegativeSliced3 <= a.slice(-1, -1); + + aRange1 <= a.getRange(5, 8); + aRange2 <= a.getRange(6, 8); + aRange3 <= a.getRange(7, 8); + aRange4 <= a.getRange(5); + aNegativeRange1 <= a.getRange(-3, 8); + aNegativeRange2 <= a.getRange(-2, 8); + aNegativeRange3 <= a.getRange(-1, 8); + aNegativeRange4 <= a.getRange(-3); + + aOperatorIndexing1 <= a.elements[0]; + aOperatorIndexing2 <= a[a.width - 1]; + aOperatorIndexing3 <= a[4]; + aOperatorNegIndexing1 <= a[-a.width]; + aOperatorNegIndexing2 <= a[-1]; + aOperatorNegIndexing3 <= a[-2]; + + aReversed <= a.reversed; + + expressionBitSelect <= + [aBJoined, aShrunk1, aRange1, aRSliced1, aPlusB].swizzle().slice(3, 0); + } +} + +class ConstBusModule extends Module { + ConstBusModule(int c, {required bool subset}) { + final outWidth = subset ? 8 : 16; + addOutput('const_subset', width: outWidth) <= + Const(c, width: 16).getRange(0, outWidth); + } +} + +class SingleBitBusSubsetMod extends Module { + SingleBitBusSubsetMod(Logic oneBit) { + oneBit = addInput('oneBit', oneBit); + addOutput('result') <= BusSubset(oneBit, 0, 0).subset; + } +} + +class SelectTestModule extends Module { + SelectTestModule(Logic a1, Logic a2, Logic a3, Logic b, {Logic? defaultValue}) + : super(name: 'selecttestmodule') { + a1 = addInput('a1', a1, width: a1.width); + a2 = addInput('a2', a2, width: a2.width); + a3 = addInput('a3', a3, width: a3.width); + b = addInput('b', b, width: b.width); + + if (defaultValue != null) { + defaultValue = + addInput('defaultValue', defaultValue, width: defaultValue.width); + _selectWithDefault(a1, a2, a3, b, defaultValue); + } else { + _selectWithout(a1, a2, a3, b); + } + } + + void _selectWithout(Logic a1, Logic a2, Logic a3, Logic b) { + final selectIndexValue = addOutput('selectIndexValue', width: a1.width); + final selectFromValue = addOutput('selectFromValue', width: a1.width); + final logicList = [a1, a2, a3]; + selectIndexValue <= logicList.selectIndex(b); + selectFromValue <= b.selectFrom(logicList); + } + + void _selectWithDefault( + Logic a1, Logic a2, Logic a3, Logic b, Logic defaultValue) { + final selectFromValue = addOutput('selectFromValue', width: a1.width); + final selectIndexValue = addOutput('selectIndexValue', width: a1.width); + final logicList = [a1, a2, a3]; + selectFromValue <= b.selectFrom(logicList, defaultValue: defaultValue); + selectIndexValue <= logicList.selectIndex(b, defaultValue: defaultValue); + } +} + +// ===== Modules from conditionals_test.dart ===== + +class LoopyCombModuleSsa extends Module { + Logic get a => input('a'); + Logic get x => output('x'); + LoopyCombModuleSsa(Logic a) : super(name: 'loopycombmodule') { + a = addInput('a', a); + final x = addOutput('x'); + Combinational.ssa((s) => [ + s(x) < a, + s(x) < ~s(x), + ]); + } +} + +class CaseModule extends Module { + CaseModule(Logic a, Logic b) : super(name: 'casemodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + final e = addOutput('e'); + + Combinational([ + Case( + [b, a].swizzle(), + [ + CaseItem(Const(LogicValue.ofString('01')), [c < 1, d < 0]), + CaseItem(Const(LogicValue.ofString('10')), [c < 1, d < 0]), + ], + defaultItem: [c < 0, d < 1], + conditionalType: ConditionalType.unique), + CaseZ( + [b, a].rswizzle(), + [ + CaseItem(Const(LogicValue.ofString('1z')), [e < 1]) + ], + defaultItem: [e < 0], + conditionalType: ConditionalType.priority) + ]); + } +} + +class IfBlockModule extends Module { + IfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + + Combinational([ + If.block([ + Iff(a & ~b, [c < 1, d < 0]), + ElseIf(b & ~a, [c < 1, d < 0]), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class SingleIfBlockModule extends Module { + SingleIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { + a = addInput('a', a); + final c = addOutput('c'); + Combinational([ + If.block([Iff.s(a, c < 1)]) + ]); + } +} + +class ElseIfBlockModule extends Module { + ElseIfBlockModule(Logic a, Logic b) : super(name: 'ifblockmodule') { + a = addInput('a', a); + b = addInput('b', b); + final c = addOutput('c'); + final d = addOutput('d'); + + Combinational([ + If.block([ + ElseIf(a & ~b, [c < 1, d < 0]), + ElseIf(b & ~a, [c < 1, d < 0]), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class SingleElseIfBlockModule extends Module { + SingleElseIfBlockModule(Logic a) : super(name: 'singleifblockmodule') { + a = addInput('a', a); + final c = addOutput('c'); + final d = addOutput('d'); + Combinational([ + If.block([ + ElseIf.s(a, c < 1), + Else([c < 0, d < 1]) + ]) + ]); + } +} + +class CombModule extends Module { + CombModule(Logic a, Logic b, Logic d) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + final x = addOutput('x'); + d = addInput('d', d, width: d.width); + final q = addOutput('q', width: d.width); + + Combinational([ + If(a, then: [ + y < a, + z < b, + x < a & b, + q < d, + ], orElse: [ + If(b, then: [ + y < b, + z < a, + q < 13, + ], orElse: [ + y < 0, + z < 1, + ]) + ]) + ]); + } +} + +class SequentialModule extends Module { + SequentialModule(Logic a, Logic b, Logic d) : super(name: 'ffmodule') { + a = addInput('a', a); + b = addInput('b', b); + final y = addOutput('y'); + final z = addOutput('z'); + final x = addOutput('x'); + d = addInput('d', d, width: d.width); + final q = addOutput('q', width: d.width); + + Sequential(SimpleClockGenerator(10).clk, [ + If(a, then: [ + q < d, + y < a, + z < b, + x < ~x, + ], orElse: [ + x < a, + If(b, then: [ + y < b, + z < a + ], orElse: [ + y < 0, + z < 1, + ]) + ]) + ]); + } +} + +class SingleIfModule extends Module { + SingleIfModule(Logic a) : super(name: 'combmodule') { + a = addInput('a', a); + final q = addOutput('q'); + Combinational([If.s(a, q < 1)]); + } +} + +class SingleIfOrElseModule extends Module { + SingleIfOrElseModule(Logic a, Logic b) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q'); + final x = addOutput('x'); + Combinational([If.s(a, q < 1, x < 1)]); + } +} + +class SingleElseModule extends Module { + SingleElseModule(Logic a, Logic b) : super(name: 'combmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q'); + final x = addOutput('x'); + Combinational([ + If.block([Iff.s(a, q < 1), Else.s(x < 1)]) + ]); + } +} + +class SignalRedrivenSequentialModule extends Module { + SignalRedrivenSequentialModule(Logic a, Logic b, Logic d, + {required bool allowRedrive}) + : super(name: 'ffmodule') { + a = addInput('a', a); + b = addInput('b', b); + final q = addOutput('q', width: d.width); + d = addInput('d', d, width: d.width); + final k = addOutput('k', width: 8); + Sequential( + SimpleClockGenerator(10).clk, + [ + If(a, then: [k < k, q < k, q < d]) + ], + allowMultipleAssignments: allowRedrive, + ); + } +} + +// ===== Modules from assignment_test.dart ===== + +class ConstAssignModule extends Module { + ConstAssignModule() { + final out = addOutput('out'); + final val = Logic(name: 'val'); + val <= Const(1); + Combinational([out < val]); + } + + Logic get out => output('out'); +} + +// ========================================================================= +// Tests +// ========================================================================= + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + tearDownAll(() => SimCompare.cleanupSystemCCache(keepPch: false)); + + // ===== Flop tests (from flop_test.dart) ===== + group('flop', () { + test('flop bit', () async { + final ftm = FlopTestModule(Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0}, {}), + Vector({'a': 1}, {'y': 0}), + Vector({'a': 1}, {'y': 1}), + Vector({'a': 0}, {'y': 1}), + Vector({'a': 0}, {'y': 0}), + ]); + }); + + test('flop bit with enable', () async { + final ftm = FlopTestModule(Logic(), en: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0, 'en': 1}, {}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 1}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 1, 'en': 0}, {'y': 1}), + Vector({'a': 0, 'en': 0}, {'y': 1}), + Vector({'a': 0, 'en': 1}, {'y': 1}), + Vector({'a': 1, 'en': 1}, {'y': 0}), + Vector({'a': 0, 'en': 0}, {'y': 1}), + Vector({'a': 1, 'en': 0}, {'y': 1}), + ]); + }); + + test('flop bus', () async { + final ftm = FlopTestModule(Logic(width: 8)); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0}, {}), + Vector({'a': 0xff}, {'y': 0}), + Vector({'a': 0xaa}, {'y': 0xff}), + Vector({'a': 0x55}, {'y': 0xaa}), + Vector({'a': 0x1}, {'y': 0x55}), + ]); + }); + + test('flop bus with enable', () async { + final ftm = FlopTestModule(Logic(width: 8), en: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'a': 0, 'en': 1}, {}), + Vector({'a': 0xff, 'en': 1}, {'y': 0}), + Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), + Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), + Vector({'a': 0, 'en': 1}, {'y': 0x1}), + Vector({'a': 0xff, 'en': 1}, {'y': 0}), + Vector({'a': 0xaa, 'en': 1}, {'y': 0xff}), + Vector({'a': 0x55, 'en': 0}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 0}, {'y': 0xaa}), + Vector({'a': 0x55, 'en': 1}, {'y': 0xaa}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x55}), + Vector({'a': 0x55, 'en': 0}, {'y': 0x1}), + Vector({'a': 0x1, 'en': 1}, {'y': 0x1}), + ]); + }); + + test('flop bus reset, no reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), reset: Logic()); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 0}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus reset, const reset value', () async { + final ftm = + FlopTestModule(Logic(width: 8), reset: Logic(), resetValue: 3); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 3}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus reset, logic reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), + reset: Logic(), resetValue: Logic(width: 8)); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1, 'resetValue': 5}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 5}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus no reset, const reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), resetValue: 9); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({}, {}), + Vector({'a': 0xa5}, {}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + + test('flop bus, enable, reset, const reset value', () async { + final ftm = FlopTestModule(Logic(width: 8), + en: Logic(), reset: Logic(), resetValue: 12); + await ftm.build(); + SimCompare.checkSystemCVector(ftm, [ + Vector({'reset': 1, 'en': 0}, {}), + Vector({'reset': 0, 'a': 0xa5}, {'y': 12}), + Vector({}, {'y': 12}), + Vector({'en': 1}, {'y': 12}), + Vector({'a': 0xff}, {'y': 0xa5}), + Vector({}, {'y': 0xff}), + ]); + }); + }); + + // ===== Counter tests (from counter_test.dart) ===== + group('counter', () { + test('counter', () async { + final counter = Counter(Logic(), Logic()); + await counter.build(); + SimCompare.checkSystemCVector(counter, [ + Vector({'en': 0, 'reset': 0}, {}), + Vector({'en': 0, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 1}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 0}), + Vector({'en': 1, 'reset': 0}, {'val': 1}), + Vector({'en': 1, 'reset': 0}, {'val': 2}), + Vector({'en': 1, 'reset': 0}, {'val': 3}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 4}), + Vector({'en': 1, 'reset': 0}, {'val': 4}), + Vector({'en': 0, 'reset': 0}, {'val': 5}), + ]); + }); + }); + + // ===== Comparison tests (from comparison_test.dart) ===== + group('comparison', () { + test('compares', () async { + final gtm = ComparisonTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({ + 'a': 0, + 'b': 0 + }, { + 'a_eq_b': 1, + 'a_neq_b': 0, + 'a_lt_b': 0, + 'a_lte_b': 1, + 'a_gt_b': 0, + 'a_gte_b': 1, + 'a_gt_operator_b': 0, + 'a_gte_operator_b': 1, + 'a_eq_c': 0, + 'a_neq_c': 1, + 'a_lt_c': 1, + 'a_lte_c': 1, + 'a_gt_c': 0, + 'a_gte_c': 0, + 'a_gt_operator_c': 0, + 'a_gte_operator_c': 0, + }), + Vector({ + 'a': 5, + 'b': 6 + }, { + 'a_eq_b': 0, + 'a_neq_b': 1, + 'a_lt_b': 1, + 'a_lte_b': 1, + 'a_gt_b': 0, + 'a_gte_b': 0, + 'a_gt_operator_b': 0, + 'a_gte_operator_b': 0, + 'a_eq_c': 1, + 'a_neq_c': 0, + 'a_lt_c': 0, + 'a_lte_c': 1, + 'a_gt_c': 0, + 'a_gte_c': 1, + 'a_gt_operator_c': 0, + 'a_gte_operator_c': 1, + }), + Vector({ + 'a': 9, + 'b': 7 + }, { + 'a_eq_b': 0, + 'a_neq_b': 1, + 'a_lt_b': 0, + 'a_lte_b': 0, + 'a_gt_b': 1, + 'a_gte_b': 1, + 'a_gt_operator_b': 1, + 'a_gte_operator_b': 1, + 'a_eq_c': 0, + 'a_neq_c': 1, + 'a_lt_c': 0, + 'a_lte_c': 0, + 'a_gt_c': 1, + 'a_gte_c': 1, + 'a_gt_operator_c': 1, + 'a_gte_operator_c': 1, + }), + ]); + }); + }); + + // ===== Arithmetic shift right tests ===== + group('arithmetic shift right', () { + test('shift right and mask', () async { + final mod = + SraUnsignedTestModule(Logic(width: 32), Logic(width: 32), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 1}, + {'result': 0xfe000000}), + Vector({'toShift': 0x10000000, 'shiftAmount': 4, 'maskBit': 1}, + {'result': 0x01000000}), + Vector({'toShift': 0xe0000000, 'shiftAmount': 4, 'maskBit': 0}, + {'result': 0}), + ]); + }); + }); + + // ===== Collapse tests ===== + group('collapse', () { + test('collapse functional', () async { + final mod = CollapseTestModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'b': 1}, {'c': 1, 'd': 1, 'e': 1, 'f': 1}), + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 0, 'e': 0, 'f': 0}), + ]); + }); + }); + + // ===== Extend tests ===== + group('extend', () { + Future extendVectors( + List vectors, int newWidth, ExtendType extendType, + {int originalWidth = 8}) async { + final mod = + ExtendModule(Logic(width: originalWidth), newWidth, extendType); + await mod.build(); + SimCompare.checkSystemCVector(mod, vectors); + } + + test('zero extend same width', () async { + await extendVectors([ + Vector({'a': 0}, {'b': 0}), + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 8, ExtendType.zero); + }); + + test('sign extend same width', () async { + await extendVectors([ + Vector({'a': 0}, {'b': 0}), + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 8, ExtendType.sign); + }); + + test('zero extend pads 0s', () async { + await extendVectors([ + Vector({'a': 0xff}, {'b': 0xff}), + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 12, ExtendType.zero); + }); + + test('sign extend positive pads 0s', () async { + await extendVectors([ + Vector({'a': 0x5a}, {'b': 0x5a}), + ], 12, ExtendType.sign); + }); + + test('sign extend negative pads 1s', () async { + await extendVectors([ + Vector({'a': 0xff}, {'b': 0xfff}), + ], 12, ExtendType.sign); + }); + + test('sign extend single bit(0) pads 0s', () async { + await extendVectors([ + Vector({'a': LogicValue.zero}, {'b': 0x000}), + ], 12, ExtendType.sign, originalWidth: 1); + }); + + test('sign extend single bit(1) pads 1s', () async { + await extendVectors([ + Vector({'a': LogicValue.one}, {'b': 0xfff}), + ], 12, ExtendType.sign, originalWidth: 1); + }); + }); + + group('withSet', () { + Future withSetVectors( + List vectors, int startIndex, int updateWidth) async { + final mod = + WithSetModule(Logic(width: 8), startIndex, Logic(width: updateWidth)); + await mod.build(); + SimCompare.checkSystemCVector(mod, vectors); + } + + test('setting same width', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xff}, {'c': 0xff}), + Vector({'a': 0x45, 'b': 0x5a}, {'c': 0x5a}), + ], 0, 8); + }); + + test('setting at front', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xf}, {'c': 0x2f}), + Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x45}), + ], 0, 4); + }); + + test('setting at end', () async { + await withSetVectors([ + Vector({'a': 0x23, 'b': 0xf}, {'c': 0xf3}), + Vector({'a': 0x4a, 'b': 0x5}, {'c': 0x5a}), + ], 4, 4); + }); + + test('setting in the middle', () async { + await withSetVectors([ + Vector({'a': 0xff, 'b': 0x0}, {'c': bin('11000011')}), + Vector( + {'a': bin('01111110'), 'b': bin('0110')}, {'c': bin('01011010')}), + ], 2, 4); + }); + }); + + // ===== Bus tests ===== + group('bus', () { + test('single-bit bus subset', () async { + final mod = SingleBitBusSubsetMod(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'oneBit': 0}, {'result': 0}), + Vector({'oneBit': 1}, {'result': 1}), + ]); + }); + + test('const subset', () async { + final mod = ConstBusModule(0xabcd, subset: true); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'const_subset': 0xcd}), + ]); + }); + + test('const assignment', () async { + final mod = ConstBusModule(0xabcd, subset: false); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'const_subset': 0xabcd}), + ]); + }); + + // All tests below share the same BusTestModule — compile once + group('BusTestModule', () { + SystemCExecutable? exe; + + setUpAll(() async { + final gtm = BusTestModule(Logic(width: 8), Logic(width: 8)); + await gtm.build(); + exe = SimCompare.buildSystemCExecutable(gtm); + }); + + tearDownAll(() { + exe?.cleanup(); + }); + + test('NotGate bus', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0xff}, {'a_bar': 0}), + Vector({'a': 0}, {'a_bar': 0xff}), + Vector({'a': 0x55}, {'a_bar': 0xaa}), + Vector({'a': 1}, {'a_bar': 0xfe}), + ]); + }); + + test('And2Gate bus', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_and_b': 0}), + Vector({'a': 0, 'b': 1}, {'a_and_b': 0}), + Vector({'a': 1, 'b': 0}, {'a_and_b': 0}), + Vector({'a': 1, 'b': 1}, {'a_and_b': 1}), + Vector({'a': 0xff, 'b': 0xaa}, {'a_and_b': 0xaa}), + ]); + }); + + test('Operator indexing', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': bin('11111110')}, {'a_operator_indexing1': 0}), + Vector({'a': bin('10000000')}, {'a_operator_indexing2': 1}), + Vector({'a': bin('11101111')}, {'a_operator_indexing3': 0}), + Vector({'a': bin('11111110')}, {'a_operator_neg_indexing1': 0}), + Vector({'a': bin('10000000')}, {'a_operator_neg_indexing2': 1}), + Vector({'a': bin('10111111')}, {'a_operator_neg_indexing3': 0}), + ]); + }); + + test('Bus shrink', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_shrunk1': 0}), + Vector({'a': 0xfa}, {'a_shrunk1': bin('010')}), + Vector({'a': 0xab}, {'a_shrunk1': 3}), + Vector({'a': 0}, {'a_shrunk2': 0}), + Vector({'a': 0xec}, {'a_shrunk2': bin('00')}), + Vector({'a': 0xfa}, {'a_shrunk2': 2}), + Vector({'a': 0}, {'a_shrunk3': 0}), + Vector({'a': 0xff}, {'a_shrunk3': bin('1')}), + Vector({'a': 0xba}, {'a_shrunk3': 0}), + Vector({'a': 0}, {'a_neg_shrunk1': 0}), + Vector({'a': 0xfa}, {'a_neg_shrunk1': bin('010')}), + Vector({'a': 0xab}, {'a_neg_shrunk1': 3}), + Vector({'a': 0}, {'a_neg_shrunk2': 0}), + Vector({'a': 0xec}, {'a_neg_shrunk2': bin('00')}), + Vector({'a': 0xfa}, {'a_neg_shrunk2': 2}), + Vector({'a': 0}, {'a_neg_shrunk3': 0}), + Vector({'a': 0xff}, {'a_neg_shrunk3': bin('1')}), + Vector({'a': 0xba}, {'a_neg_shrunk3': 0}), + ]); + }); + + test('Bus reverse slice', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_rsliced1': 0}), + Vector({'a': 0xac}, {'a_rsliced1': bin('10101')}), + Vector({'a': 0xf5}, {'a_rsliced1': 0xf}), + Vector({'a': 0}, {'a_rsliced2': 0}), + Vector({'a': 0xab}, {'a_rsliced2': bin('01')}), + Vector({'a': 0xac}, {'a_rsliced2': 1}), + Vector({'a': 0}, {'a_rsliced3': 0}), + Vector({'a': 0xaf}, {'a_rsliced3': bin('1')}), + Vector({'a': 0xaf}, {'a_rsliced3': 1}), + Vector({'a': 0}, {'a_r_neg_sliced1': 0}), + Vector({'a': 0xac}, {'a_r_neg_sliced1': bin('10101')}), + Vector({'a': 0xf5}, {'a_r_neg_sliced1': 0xf}), + Vector({'a': 0}, {'a_r_neg_sliced2': 0}), + Vector({'a': 0xab}, {'a_r_neg_sliced2': bin('01')}), + Vector({'a': 0xac}, {'a_r_neg_sliced2': 1}), + Vector({'a': 0}, {'a_r_neg_sliced3': 0}), + Vector({'a': 0xaf}, {'a_r_neg_sliced3': bin('1')}), + Vector({'a': 0xaf}, {'a_r_neg_sliced3': 1}), + ]); + }); + + test('Bus reversed', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_reversed': 0}), + Vector({'a': 0xff}, {'a_reversed': 0xff}), + Vector({'a': 0xf5}, {'a_reversed': 0xaf}), + ]); + }); + + test('Bus range', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a_range1': 0}), + Vector({'a': 0xaf}, {'a_range1': 5}), + Vector({'a': bin('11000101')}, {'a_range1': bin('110')}), + Vector({'a': 0}, {'a_range2': 0}), + Vector({'a': 0xaf}, {'a_range2': 2}), + Vector({'a': bin('10111111')}, {'a_range2': bin('10')}), + Vector({'a': 0}, {'a_range3': 0}), + Vector({'a': 0x80}, {'a_range3': 1}), + Vector({'a': bin('10000000')}, {'a_range3': bin('1')}), + Vector({'a': 0}, {'a_range4': 0}), + Vector({'a': 0xaf}, {'a_range4': 5}), + Vector({'a': bin('11000101')}, {'a_range4': bin('110')}), + Vector({'a': 0}, {'a_neg_range1': 0}), + Vector({'a': 0xaf}, {'a_neg_range1': 5}), + Vector({'a': bin('11000101')}, {'a_neg_range1': bin('110')}), + Vector({'a': 0}, {'a_neg_range2': 0}), + Vector({'a': 0xaf}, {'a_neg_range2': 2}), + Vector({'a': bin('10111111')}, {'a_neg_range2': bin('10')}), + Vector({'a': 0}, {'a_neg_range3': 0}), + Vector({'a': 0x80}, {'a_neg_range3': 1}), + Vector({'a': bin('10000000')}, {'a_neg_range3': bin('1')}), + Vector({'a': 0}, {'a_neg_range4': 0}), + Vector({'a': 0xaf}, {'a_neg_range4': 5}), + Vector({'a': bin('11000101')}, {'a_neg_range4': bin('110')}), + ]); + }); + + test('Bus swizzle', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_b_joined': 0}), + Vector({'a': 0xff, 'b': 0xff}, {'a_b_joined': 0xffff}), + Vector({'a': 0xff, 'b': 0}, {'a_b_joined': 0xff}), + Vector({'a': 0, 'b': 0xff}, {'a_b_joined': 0xff00}), + Vector({'a': 0xaa, 'b': 0x55}, {'a_b_joined': 0x55aa}), + ]); + }); + + test('Bus bit', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0}, {'a1': 0}), + Vector({'a': 0xff}, {'a1': 1}), + Vector({'a': 0xf5}, {'a1': 0}), + ]); + }); + + test('add busses', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 0, 'b': 0}, {'a_plus_b': 0}), + Vector({'a': 0, 'b': 1}, {'a_plus_b': 1}), + Vector({'a': 1, 'b': 0}, {'a_plus_b': 1}), + Vector({'a': 1, 'b': 1}, {'a_plus_b': 2}), + Vector({'a': 6, 'b': 7}, {'a_plus_b': 13}), + ]); + }); + + test('expression bit select', () { + if (exe == null) { + return; + } + SimCompare.checkSystemCVectors(exe!, [ + Vector({'a': 1, 'b': 1}, {'expression_bit_select': 2}), + ]); + }); + }); // end BusTestModule group + + test('selectFrom and selectIndex', () async { + final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), + Logic(width: 8), Logic(width: (log(8) / log(2)).ceil())); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 1}, + {'selectIndexValue': 2, 'selectFromValue': 2}), + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 0}, + {'selectIndexValue': 1, 'selectFromValue': 1}), + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 2}, + {'selectIndexValue': 3, 'selectFromValue': 3}), + ]); + }); + + test('selectFrom with default Value', () async { + final gtm = SelectTestModule(Logic(width: 8), Logic(width: 8), + Logic(width: 8), Logic(width: (log(8) / log(2)).ceil()), + defaultValue: Logic(width: 8)); + await gtm.build(); + SimCompare.checkSystemCVector(gtm, [ + Vector({'a1': 1, 'a2': 2, 'a3': 3, 'b': 4, 'defaultValue': 5}, + {'selectFromValue': 5, 'selectIndexValue': 5}), + ]); + }); + }); + + // ===== Conditionals tests ===== + group('conditionals', () { + test('conditional comb', () async { + final mod = CombModule(Logic(), Logic(), Logic(width: 10)); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0, 'd': 5}, + {'y': 0, 'z': 1, 'x': LogicValue.x, 'q': LogicValue.x}), + Vector({'a': 0, 'b': 1, 'd': 6}, + {'y': 1, 'z': 0, 'x': LogicValue.x, 'q': 13}), + Vector({'a': 1, 'b': 0, 'd': 7}, {'y': 1, 'z': 0, 'x': 0, 'q': 7}), + Vector({'a': 1, 'b': 1, 'd': 8}, {'y': 1, 'z': 1, 'x': 1, 'q': 8}), + ]); + }); + + test('iffblock comb', () async { + final mod = IfBlockModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), + ]); + }); + + test('single iffblock comb', () async { + final mod = SingleIfBlockModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'c': 1}), + ]); + }); + + test('elseifblock comb', () async { + final mod = ElseIfBlockModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1}), + ]); + }); + + test('single elseifblock comb', () async { + final mod = SingleElseIfBlockModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'c': 1}), + Vector({'a': 0}, {'c': 0, 'd': 1}), + ]); + }); + + test('case comb', () async { + final mod = CaseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0, 'b': 0}, {'c': 0, 'd': 1, 'e': 0}), + Vector({'a': 0, 'b': 1}, {'c': 1, 'd': 0, 'e': 0}), + Vector({'a': 1, 'b': 0}, {'c': 1, 'd': 0, 'e': 1}), + Vector({'a': 1, 'b': 1}, {'c': 0, 'd': 1, 'e': 1}), + ]); + }); + + test('conditional ff', () async { + final mod = SequentialModule(Logic(), Logic(), Logic(width: 8)); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'd': 1}, {}), + Vector({'a': 0, 'b': 0, 'd': 2}, {'q': 1}), + Vector({'a': 0, 'b': 1, 'd': 3}, {'y': 0, 'z': 1, 'x': 0, 'q': 1}), + Vector({'a': 1, 'b': 0, 'd': 4}, {'y': 1, 'z': 0, 'x': 0, 'q': 1}), + Vector({'a': 1, 'b': 1, 'd': 5}, {'y': 1, 'z': 0, 'x': 1, 'q': 4}), + Vector({}, {'y': 1, 'z': 1, 'x': 0, 'q': 5}), + ]); + }); + + test('loopy comb ssa', () async { + final mod = LoopyCombModuleSsa(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 0}, {'x': 1}), + Vector({'a': 1}, {'x': 0}), + ]); + }); + + test('single if', () async { + final mod = SingleIfModule(Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + ]); + }); + + test('single if or else', () async { + final mod = SingleIfOrElseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + Vector({'a': 0}, {'x': 1}), + ]); + }); + + test('single else', () async { + final mod = SingleElseModule(Logic(), Logic()); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1}, {'q': 1}), + Vector({'a': 0}, {'x': 1}), + ]); + }); + + test('redrive allowed', () async { + final mod = SignalRedrivenSequentialModule( + Logic(), Logic(), Logic(width: 8), + allowRedrive: true); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({'a': 1, 'd': 1}, {}), + Vector({'a': 1, 'b': 0, 'd': 2}, {'q': 1}), + Vector({'a': 1, 'b': 0, 'd': 3}, {'q': 2}), + ]); + }); + }); + + // ===== Assignment tests ===== + group('assignment', () { + test('const comb assignment', () async { + final mod = ConstAssignModule(); + await mod.build(); + SimCompare.checkSystemCVector(mod, [ + Vector({}, {'out': 1}), + ]); + }); + }); +} diff --git a/test/translations_test.dart b/test/translations_test.dart index 6d53d4f74..ed4b574d4 100644 --- a/test/translations_test.dart +++ b/test/translations_test.dart @@ -126,6 +126,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(ftm, vectors); SimCompare.checkIverilogVector(ftm, vectors); + SimCompare.checkSystemCVector(ftm, vectors); }); }); } diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index ff31896d5..a1d748032 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -243,6 +243,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('typed array is an array', () async { @@ -271,6 +272,7 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); test('structure containing ports naming properly', () async { @@ -492,6 +494,9 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + if (!portType.name.contains('net')) { + SimCompare.checkSystemCVector(mod, vectors); + } }); } }); @@ -514,5 +519,6 @@ void main() { await SimCompare.checkFunctionalVector(mod, vectors); SimCompare.checkIverilogVector(mod, vectors); + SimCompare.checkSystemCVector(mod, vectors); }); } diff --git a/tool/gh_actions/install_systemc.sh b/tool/gh_actions/install_systemc.sh new file mode 100755 index 000000000..4ab6a5cb9 --- /dev/null +++ b/tool/gh_actions/install_systemc.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# install_systemc.sh +# GitHub Actions step: Install Accellera SystemC library. +# +# Downloads, builds, and installs SystemC to /opt/systemc. +# Uses a cache-friendly layout so the install directory can be +# cached across CI runs. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SYSTEMC_VERSION="${SYSTEMC_VERSION:-3.0.2}" +INSTALL_PREFIX="${SYSTEMC_INSTALL_PREFIX:-/opt/systemc}" + +# Skip if already installed (e.g. from cache) +if [ -f "$INSTALL_PREFIX/lib/libsystemc.so" ]; then + echo "SystemC already installed at $INSTALL_PREFIX — skipping build." + exit 0 +fi + +echo "Installing Accellera SystemC $SYSTEMC_VERSION to $INSTALL_PREFIX ..." + +# Install build dependencies +sudo apt-get update -qq +sudo apt-get install --yes --no-install-recommends cmake g++ make + +# Download source +TARBALL="systemc-$SYSTEMC_VERSION.tar.gz" +DOWNLOAD_URL="https://github.com/accellera-official/systemc/archive/refs/tags/$SYSTEMC_VERSION.tar.gz" + +cd /tmp +curl -fsSL -o "$TARBALL" "$DOWNLOAD_URL" +tar xzf "$TARBALL" +cd "systemc-$SYSTEMC_VERSION" + +# Build with CMake +mkdir -p build && cd build +cmake .. \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_PREFIX" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DBUILD_SHARED_LIBS=ON \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_REGRESSION=OFF \ + -DDISABLE_COPYRIGHT_MESSAGE=ON + +make -j"$(nproc)" +sudo make install + +echo "SystemC $SYSTEMC_VERSION installed to $INSTALL_PREFIX" diff --git a/tool/gh_actions/setup_systemc_pch.sh b/tool/gh_actions/setup_systemc_pch.sh new file mode 100755 index 000000000..3d36b4005 --- /dev/null +++ b/tool/gh_actions/setup_systemc_pch.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Copyright (C) 2024-2026 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# setup_systemc_pch.sh +# GitHub Actions step: Pre-build SystemC precompiled header and Makefile. +# +# Run this after install_systemc.sh and before tests to avoid race +# conditions when multiple test isolates run in parallel. +# +# 2026 May +# Author: Desmond Kirkpatrick + +set -euo pipefail + +SC_HOME="${SYSTEMC_INCLUDE:-/opt/systemc/include}" +SC_LIB="${SYSTEMC_LIB:-/opt/systemc/lib}" + +if [ ! -d "$SC_HOME" ]; then + echo "SystemC not found at $SC_HOME — skipping PCH setup." + exit 0 +fi + +# Detect C++ standard from the installed library +CXX_STD="c++17" +if command -v nm &>/dev/null && [ -f "$SC_LIB/libsystemc.so" ]; then + if nm -D "$SC_LIB/libsystemc.so" 2>/dev/null | grep -q 'cxx202002L'; then + CXX_STD="c++20" + fi +fi + +echo "Setting up SystemC PCH ($CXX_STD) ..." + +# Build precompiled header +PCH_DIR="tmp_test/pch" +mkdir -p "$PCH_DIR" +cp "$SC_HOME/systemc.h" "$PCH_DIR/systemc.h" +g++ -std="$CXX_STD" -I"$SC_HOME" -x c++-header \ + -o "$PCH_DIR/systemc.h.gch" "$SC_HOME/systemc.h" + +echo "PCH built: $PCH_DIR/systemc.h.gch" + +# Pre-create the shared Makefile +MAKEFILE="tmp_test/Makefile_sc" +cat > "$MAKEFILE" <<'EOF' +CXX = g++ +CXXFLAGS = -std=__CXX_STD__ -pipe -I__PCH_DIR__ -I__SC_HOME__ +LDFLAGS = -L__SC_LIB__ -lsystemc + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CXX) $(CXXFLAGS) -o $(TARGET) $(SRC) $(LDFLAGS) + +.PHONY: all +EOF + +# Substitute paths into the Makefile +sed -i "s|__CXX_STD__|$CXX_STD|g" "$MAKEFILE" +sed -i "s|__PCH_DIR__|$PCH_DIR|g" "$MAKEFILE" +sed -i "s|__SC_HOME__|$SC_HOME|g" "$MAKEFILE" +sed -i "s|__SC_LIB__|$SC_LIB|g" "$MAKEFILE" + +echo "Makefile created: $MAKEFILE" diff --git a/tool/gh_codespaces/run_setup.sh b/tool/gh_codespaces/run_setup.sh index 6523e4147..ba1d14a97 100755 --- a/tool/gh_codespaces/run_setup.sh +++ b/tool/gh_codespaces/run_setup.sh @@ -20,5 +20,11 @@ tool/gh_actions/install_dependencies.sh # Install Icarus Verilog. tool/gh_actions/install_iverilog.sh +# Install Accellera SystemC. +tool/gh_actions/install_systemc.sh + +# Pre-build SystemC precompiled header and Makefile. +tool/gh_actions/setup_systemc_pch.sh + # Install Node tool/gh_actions/install_node.sh \ No newline at end of file